170 lines
15 KiB
Markdown
170 lines
15 KiB
Markdown
# Приложения, хранилище и взаимодействия
|
||
|
||
Представление 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).
|