Files
docs/architecture/containers.md
T
ksv741 9b19a5cf4d
Docs security / security (push) Successful in 7s
Docs verify / verify (push) Successful in 9s
docs: simplify system documentation
2026-09-14 05:51:29 +03:00

170 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Приложения, хранилище и взаимодействия
Представление C4 Container раскрывает внутреннее устройство WealthLedger.
Контейнер здесь означает приложение или хранилище, а не Docker-контейнер.
Внешние системы совпадают с [System Context](context.md).
## Представление Container
```mermaid
flowchart LR
U["Частный инвестор<br/>Человек"]
subgraph W["WealthLedger · программная система"]
F["React SPA<br/>TypeScript, Vite, Mantine, TanStack Query"]
B["Backend API<br/>NestJS, Node.js"]
D[("Локальная база<br/>Embedded SQLite")]
F -->|"HTTP JSON /api/v1; Bearer и auth cookie"| B
B -->|"Prisma + better-sqlite3; доступ к локальному файлу"| D
end
T["T-Bank Invest API<br/>Внешняя система"]
M["MOEX ISS<br/>Внешняя система"]
N["Реестр НКР<br/>Внешняя система"]
C["T-Bank Brands CDN<br/>Внешняя система"]
U -->|"Просматривает данные и управляет подключением"| F
B -->|"gRPC over TLS; token пользователя"| T
B -->|"HTTPS; публичные данные"| M
B -->|"HTTPS; публичный HTML"| N
F -->|"HTTPS; изображения без авторизации"| C
```
SPA выполняется в браузере пользователя. Backend — отдельное локальное
приложение; SQLite используется внутри его процесса через библиотеку и файл
базы. Между API и SQLite нет сетевого database server. Граница системы на
схеме показывает принадлежность частей, а не их размещение на одном узле.
## Ответственность частей
| Часть | Ответственность |
| --- | --- |
| React SPA | Экраны, навигация, формы, состояние входа и клиентский кэш ответов; форматирование денежных значений |
| Backend API | Локальная идентичность, сессии, проверка доступа, подключение брокера, нормализация внешних данных, точные расчёты и серверные кэши |
| SQLite | Долговременное хранение локальных пользователей, сессий, зашифрованных подключений и ручной банковской истории |
| Внешние системы | Данные брокера, публичные сведения об инструментах и изображения в пределах своей ответственности |
Frontend организован слоями `app → pages → features → entities → shared`:
каркас связывает маршруты, страницы собирают сценарии, а общие средства
обслуживают транспорт и представление. Backend остаётся модульным NestJS API,
где локальная аутентификация отделена от подключения брокера и инвестиционных
сценариев. Это внутренняя организация приложений, не набор микросервисов.
Финансовые HTTP-запросы браузера идут через backend. Пользователь вводит token
T-Bank в форме и отправляет его для сохранения, но API не возвращает token
в ответах; SPA не использует его для прямых брокерских запросов. Прямая
загрузка публичного изображения из CDN не проходит через эту финансовую
границу.
Типизированный frontend-клиент производится из предоставленного OpenAPI snapshot
во время разработки. В runtime SPA вызывает API, а не генератор или OpenAPI
endpoint. Форма запросов и ошибок описана в [HTTP API](http-api.md); направление
генерации принято в [ADR 002](../adr/002-nest-generated-openapi-contract.md),
а доверие frontend к предоставленному snapshot — в
[ADR 014](../adr/014-trusted-openapi-input.md).
## Хранимые данные и границы доступа
SQLite содержит пользователя с нормализованным email и hash пароля, его
сессии с hash текущего refresh token, сроком действия и признаком отзыва,
а также одно зашифрованное подключение T-Bank с временем подключения.
Пароли хешируются bcrypt; token брокера шифруется AES-256-GCM. Plaintext
credentials не являются сохраняемым представлением этих данных.
Ручной банковский учёт хранит карты, пользовательские категории и операции
в отдельных таблицах, связанных с User. Начальный остаток и суммы записаны
целыми bigint-копейками, календарные даты — днями без времени. Перевод хранится
одной операцией с двумя картами; возврат связан с исходной покупкой. Баланс
вычисляется из неудалённой истории и отдельно не сохраняется.
Банковские запросы проверяют локального владельца каждой связи и не используют
подключение брокера. Транзакции, ограничения уникальности и версии согласуют
переводы, возвраты и конкурирующие исправления. Fingerprint исходного ввода
и отметка удаления защищают от повторного создания после потерянного ответа
или перезапуска. Архив обратим и сохраняет историю. Подробности —
[банковские карты](../domains/cards/overview.md).
Сессии и подключение связаны с владельцем. Access token подтверждает
локального пользователя, но `accountId` из URL сам по себе не даёт права
читать брокерский счёт. Backend получает текущее подключение пользователя
и его generation перед использованием финансового кэша. Проверка доступности
счёта у брокера зависит от сценария: журнал выполняет её до чтения страницы
кэша; свежий портфель одного счёта может быть возвращён без нового запроса
списка счетов. При обновлении портфеля доступ проверяется у брокера.
Ротация сессии атомарно заменяет hash текущего refresh token: два
конкурентных запроса не могут оба успешно выполнить одну ротацию.
Вызов брокера и запись в локальную базу не образуют общую транзакцию:
при сохранении подключения сначала проверяется token, затем записывается
шифротекст. Подробности находятся в
[аутентификации](../domains/authentication/overview.md) и
[интеграции T-Bank](../integrations/tbank.md).
Брокерские счета, сводки, портфель, страницы журнала и будущие события
не являются долговременной историей в SQLite. Они получаются для текущего
подключения и могут временно кэшироваться. Перезапуск backend не
восстанавливает прежние финансовые снимки и cursor continuations из базы;
[модель свежести](cross-cutting/data-freshness.md) отделяет это от хранения
учётных данных.
## Сохранение и восстановление данных
Резервная копия SQLite должна быть согласованной: для работающей базы
используется SQLite backup API с учётом WAL. Несогласованное копирование
файлов базы и WAL не сохраняет целостный снимок. Восстановление проверяется
на отдельной копии. Ключ шифрования подключения хранится отдельно от базы;
его потеря делает сохранённый T-Bank token нечитаемым.
Откат приложения требует совместимости предыдущей версии с сохранённой
схемой. Расширенная банковскими таблицами схема сохраняет пользователей,
сессии и подключения. Возврат старого backup поверх новой истории удалил бы
введённые банковские операции; откат приложения не равнозначен откату данных.
Совместимость произвольных будущих версий этим не гарантируется.
## Стратегия решения и её основания
Локальная база и Prisma дают типизированный доступ к локальному состоянию
без отдельного сервера базы; исходные причины и пределы выбора сохранены
в [ADR 001](../adr/001-prisma-sqlite-local-mvp.md). Централизованная
[серверная граница T-Bank](../adr/003-tbank-server-side-grpc-boundary.md)
сосредоточивает credentials, лимиты и повторы вне браузера, а
[шифрование подключения](../adr/004-encrypted-per-user-tbank-token.md)
задаёт способ хранения брокерского секрета.
[Политика сессий](../adr/006-auth-session-policy.md) разделяет короткоживущий
access token и вращаемый refresh token. [Политика query-кэша](../adr/007-react-query-fetch-policy.md)
сохраняет доступные данные при обновлении и не запускает refetch при возврате
фокуса. [Ограниченные повторы T-Bank](../adr/005-tbank-upstream-resilience.md)
и [структурированные журналы](../adr/008-pino-backend-logging.md) обслуживают
внешнюю границу. ADR сохраняют контекст принятия решения; текущие детали
уточняются в тематических описаниях.
## Качество и ограничения
Точность представления денег требует сохранять валюту и decimal-значение;
неизвестная сумма не заменяется нулём. Свежесть наблюдения не равна
актуальности рынка в реальном времени. Временные ошибки внешнего поставщика
обрабатываются ограниченными повторами и локальными состояниями интерфейса,
без обещания успешного ответа при недоступном upstream.
Авторизация доступа, недопущение token в публичный ответ и атомарность
ротации сессии — отдельные свойства; наличие типизированного клиента
не доказывает их. Известные расхождения регистрации и деталей валидации
отмечены в [аутентификации](../domains/authentication/overview.md#регистрация-и-вход)
и [HTTP API](http-api.md#error-contract).
Подтверждённая топология — локальная SPA и локальный API с файлом SQLite.
Origin браузера, адрес API и cookie policy должны быть согласованы.
`localhost` и `127.0.0.1` задают разные browser-границы для cookie и CORS.
Локальный HTTP поддерживается в development-режиме backend; в остальных
режимах refresh-cookie имеет Secure flag и требует защищённого соединения.
Наличие локального запуска не задаёт production-топологию или гарантии SLA.
Кэши и лимитер T-Bank действуют на экземпляр backend. Ротация ключа шифрования
подключений и согласование изменений подключения между распределёнными
экземплярами не входят в реализованный механизм. Эти ограничения не
позволяют переносить локальную схему на несколько экземпляров без отдельного
решения о состоянии, credentials и лимитах.
Runtime-сценарии раскрываются у соответствующих владельцев:
[восстановление сессии](../domains/authentication/overview.md#восстановление-сессии),
[сохранение подключения](../integrations/tbank.md#проверка-и-сохранение-подключения)
и [загрузка портфеля](../domains/portfolio/flows/portfolio-loading.md).