Conventional commits / commits (pull_request) Successful in 12s
Docs security / security (pull_request) Successful in 7s
Docs verify / verify (pull_request) Successful in 14s
Docs security / security (push) Successful in 7s
Docs verify / verify (push) Successful in 16s
220 lines
19 KiB
Markdown
220 lines
19 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. Граница системы на
|
||
схеме показывает принадлежность частей, а не их размещение на одном узле.
|
||
|
||
## Контейнерная поставка
|
||
|
||
Deployment view для одного сервера. Caddy принадлежит серверу; Compose
|
||
содержит два постоянных application containers и одноразовую migration.
|
||
|
||
```mermaid
|
||
flowchart LR
|
||
Browser["Браузер · React SPA"] -->|HTTPS| Caddy["Серверный Caddy · TLS"]
|
||
subgraph Host["Один Docker host"]
|
||
Caddy -->|"HTTP · 127.0.0.1:8080"| Frontend["frontend · nginx · SPA + /api/"]
|
||
Frontend -->|"HTTP /api/v1 · internal network"| Backend["backend · NestJS · один process"]
|
||
Migration["migration · Prisma · one-shot"] -->|"До старта backend"| Data[("Host directory · SQLite + WAL")]
|
||
Backend -->|"Чтение и запись /data"| Data
|
||
Secrets["Restricted environment · ключи вне images"] --> Backend
|
||
end
|
||
```
|
||
|
||
Только frontend публикует host port, привязанный к loopback. Backend не
|
||
публикует порт, но имеет исходящий доступ к внешним API. nginx сохраняет
|
||
`/api/` prefix, forwarding headers и request ID; Docker DNS разрешается
|
||
повторно после backend recreation. HTML требует revalidation, hashed assets
|
||
имеют immutable cache. Caddy не содержит отдельного маршрута к API.
|
||
|
||
Development заменяет nginx на Vite с HMR, backend — на Nest watch, а host data
|
||
path — на отдельный named volume. Browser origin остаётся единым. Linux
|
||
node_modules не монтируются с хоста. Production SPA собирается с relative API
|
||
base; production cookies остаются Secure, публичный origin использует HTTPS.
|
||
|
||
Компоненты имеют независимые images и версии. Production фиксирует image
|
||
version tags; migration использует тот же backend image. Успех migration предшествует
|
||
backend healthcheck на существующем auth route; frontend ждёт healthy backend.
|
||
Runtime работает non-root, SQLite сохраняется вне image, ключ шифрования и JWT
|
||
secret поступают из restricted environment и сохраняются между обновлениями.
|
||
|
||
Ограничение SQLite расширено в [ADR 015](../adr/015-single-server-sqlite-deployment.md).
|
||
[Runbook](deployment.md) описывает установку, independent
|
||
updates, backup, rollback и explicit restore. Он также перечисляет внешние
|
||
prerequisites: локальный smoke не подтверждает registry/server rollout.
|
||
|
||
## Ответственность частей
|
||
|
||
| Часть | Ответственность |
|
||
| --- | --- |
|
||
| React SPA | Экраны, навигация, формы, состояние входа и клиентский кэш ответов; форматирование денежных значений |
|
||
| Backend API | Локальная идентичность, сессии, проверка доступа, подключение брокера, нормализация внешних данных, точные расчёты и синхронизация сохранённых источников |
|
||
| SQLite | Долговременное хранение локальных пользователей, сессий, зашифрованных подключений, ручной банковской истории и инвестиционных источников с coverage/work |
|
||
| Внешние системы | Данные брокера, публичные сведения об инструментах и изображения в пределах своей ответственности |
|
||
|
||
Frontend организован слоями `app → pages → features → entities → shared`:
|
||
каркас связывает маршруты, страницы собирают сценарии, а общие средства
|
||
обслуживают транспорт и представление. Backend остаётся модульным NestJS API,
|
||
где локальная аутентификация отделена от подключения брокера и инвестиционных
|
||
сценариев. Это внутренняя организация приложений, не набор микросервисов.
|
||
|
||
Маршруты и AppShell принадлежат `app`. Page controller/VM управляет запросами,
|
||
формой и восстановлением; presentation получает данные и callbacks, поля
|
||
могут использовать контекст React Hook Form. В OperationForm controller
|
||
сохраняет intent операции, черновик и явное принятие актуальной версии после
|
||
конфликта. В account-income чистые преобразования периода принадлежат модели
|
||
страницы и не связывают форму с её запросами.
|
||
|
||
Локальные packages tokens/ui/icons образуют [визуальную основу](../design/overview.md#общая-основа-и-каталог)
|
||
внутри frontend и входят в сборку SPA. Это библиотеки, а не дополнительные
|
||
runtime-контейнеры. Провайдеры темы принадлежат приложению и Storybook;
|
||
предметные компоненты и API-клиент остаются в приложении. Отдельный API-пакет
|
||
и registry-поставка визуальных пакетов не реализованы. Существующие Docker
|
||
images приложения не означают независимого npm-выпуска этих библиотек.
|
||
|
||
Финансовые 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).
|
||
|
||
Сессии и подключение связаны с владельцем. Backend локально проверяет
|
||
membership текущей revision перед каждым финансовым чтением. Unknown membership
|
||
не открывает доступ к произвольному счёту; известный отказ токена блокирует
|
||
личные данные. Ротация refresh token остаётся атомарной.
|
||
|
||
SQLite хранит полный последний portfolio, операции и их подтверждённые
|
||
диапазоны, private прогнозы, общие instrument/market sources и intent/staging.
|
||
Нормализованные финансовые значения сохраняются decimal-строками. Полнота
|
||
отделена от свежести и пагинации. Ответ не ждёт внешних RPC; запрос при
|
||
необходимости запускает работу в том же backend process через существующий
|
||
лимитер. На startup импорт не запускается; восстановление требует следующего
|
||
запроса. Подробности — [модель свежести](cross-cutting/data-freshness.md).
|
||
|
||
PUT подключения сначала проверяет token у T-Bank, затем одной транзакцией
|
||
сохраняет encrypted token, новую revision и проверенный membership.
|
||
Сетевая операция не удерживает SQLite-транзакцию. Общие источники не содержат
|
||
личных позиций, ожидаемых сумм или credentials. Произвольного архива
|
||
отключённых счетов нет.
|
||
|
||
## Сохранение и восстановление данных
|
||
|
||
Поддерживаемая процедура останавливает backend и копирует весь data directory
|
||
с возможными WAL/SHM files до migration. Копирование отдельных файлов
|
||
работающей базы не сохраняет целостный снимок; для live backup потребовался бы
|
||
SQLite backup API. Восстановление проверяется на отдельной копии. Ключ шифрования подключения хранится отдельно от базы;
|
||
его потеря делает сохранённый 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).
|
||
|
||
Поддерживается локальный Compose и ограниченная single-server topology с SQLite.
|
||
Origin браузера, адрес API и cookie policy должны быть согласованы.
|
||
`localhost` и `127.0.0.1` задают разные browser-границы для cookie и CORS.
|
||
Локальный HTTP поддерживается в development-режиме backend; в остальных
|
||
режимах refresh-cookie имеет Secure flag и требует защищённого соединения.
|
||
Single-server deployment не задаёт гарантий HA или zero-downtime.
|
||
|
||
Coordinator и лимитер T-Bank действуют на один экземпляр backend. Сохранённые
|
||
данные переживают перезапуск; processRunId не является distributed lease. Ротация ключа шифрования
|
||
подключений и согласование изменений подключения между распределёнными
|
||
экземплярами не входят в реализованный механизм. Эти ограничения не
|
||
позволяют переносить локальную схему на несколько экземпляров без отдельного
|
||
решения о состоянии, credentials и лимитах.
|
||
|
||
Runtime-сценарии раскрываются у соответствующих владельцев:
|
||
[восстановление сессии](../domains/authentication/overview.md#восстановление-сессии),
|
||
[сохранение подключения](../integrations/tbank.md#проверка-и-сохранение-подключения)
|
||
и [загрузка портфеля](../domains/portfolio/flows/portfolio-loading.md).
|