docs: complete T-Bank connection change

This commit is contained in:
2026-08-13 19:36:51 +03:00
parent b4515a201a
commit cc05b2b69c
4 changed files with 179 additions and 1 deletions
@@ -0,0 +1,72 @@
## Context
См. мотивацию в `proposal.md` и поведение в `specs/tbank-connection/spec.md`. После `add-authentication` backend имеет user-scoped SQLite-модель, Bearer guard, OpenAPI и Problem Details filter; `/settings` пока содержит только logout. Принятые ADR 003--005 уже устанавливают server-side gRPC boundary, один зашифрованный token на User и per-instance upstream resilience.
## Goals / Non-Goals
**Goals:**
- Хранить один проверенный T-Bank token на User, не раскрывая его за server boundary.
- Добавить минимальный authenticated HTTP contract и UI настроек для lifecycle connection.
- Централизовать TLS gRPC-вызов, лимитирование, retry и безопасную классификацию ошибок до появления read-paths.
**Non-Goals:**
- Получение и отображение счетов, портфеля, инструментов либо их cache; это задачи `add-brokerage-accounts` и `add-portfolio-view`.
- Key rotation, distributed rate limit/cache, persistent очередь, health-check token или serialisation параллельных изменений connection.
- SDK T-Bank, автоматическое обновление protobuf contracts и поддержка нескольких T-Bank connections у одного User.
## Decisions
### Модель connection и HTTP resource
Prisma добавляет `TbankConnection` с уникальным `userId`, `encryptedToken`, `connectedAt` и relation к `User`. Отдельная сущность выражает агрегат User--T-Bank Connection и не смешивает secret с profile. `GET` читает только эту запись. `PUT` сначала trims и проверяет token, затем создаёт или заменяет запись и устанавливает новый `connectedAt`. `DELETE` удаляет запись идемпотентно; сейчас user-scoped T-Bank cache отсутствует, поэтому purge является no-op до его появления.
Без serialisation, optimistic version или conditional writes параллельный `PUT`/`DELETE` разрешается порядком завершения запросов. Это намеренное MVP-ограничение: блокировки на User и public versioning усложняют только редкий конфликт между сессиями.
Альтернативы: поле token в `User` смешивает профиль и внешнюю credential; отдельные create/replace endpoints дублируют lifecycle единственного resource; `404` без connection делает нормальное UI-состояние исключением.
### Шифрование token
`TOKEN_ENCRYPTION_KEY` обязателен при старте, base64-декодируется ровно в 32 bytes. Backend использует встроенный Node `crypto` AES-256-GCM: random 12-byte nonce, ciphertext и 16-byte authentication tag хранятся в одном base64 payload с version byte. Расшифровка выполняется только непосредственно перед формированием `Authorization: Bearer` для gRPC metadata; token и metadata никогда не логируются.
При невалидном ключе backend не запускается. Key rotation не поддерживается: новый ключ требует отдельной миграционной стратегии. Это минимальнее и безопаснее, чем неявно хранить несколько ключей или plaintext fallback.
### T-Bank gRPC boundary
Backend vendoring-ит полный набор `.proto` contracts из `references/moex-vibe` (`common`, `users`, `operations`, `instruments` и их imports); источник последующих ручных обновлений -- официальный T-Bank Invest API. Эта change загружает и использует только `UsersService/GetAccounts` с `ACCOUNT_STATUS_OPEN` для проверки token. Полный набор сохраняет согласованную contract tree для следующих capabilities, не добавляя сейчас operations/instruments client surface.
`@grpc/grpc-js` и `@grpc/proto-loader` создают TLS client к `TBANK_GRPC_ENDPOINT` (default `invest-public-api.tbank.ru:443`). Системное trust store используется по умолчанию; необязательный `TBANK_CA_CERT_PATH` задаёт читаемый PEM root CA для сетей с TLS interception. TLS verification отключать нельзя. Один gRPC attempt имеет deadline 10 seconds.
Альтернативы: готовый SDK расширяет dependency surface и не нужен одному RPC; копирование только `users.proto` затрудняет согласованное продолжение T-Bank integration; custom CA как обязательная настройка ломает обычное публичное TLS-подключение.
### Upstream policy и error mapping
Единый in-memory FIFO limiter на экземпляр запускает не более 5 T-Bank RPC за секунду и содержит не более 50 ожидающих вызовов. Переполненный limiter сразу возвращает `TBANK_UNAVAILABLE`, не вызывая upstream. `UNAVAILABLE`, `DEADLINE_EXCEEDED` и `RESOURCE_EXHAUSTED` повторяются ровно один раз спустя 1 second, вновь проходя limiter. Остальные ошибки не повторяются.
`UNAUTHENTICATED` и `PERMISSION_DENIED` преобразуются в `400 INVALID_TBANK_TOKEN`. После исчерпания retry, overflow очереди и все прочие gRPC ошибки преобразуются в `503 TBANK_UNAVAILABLE`; подробности upstream остаются только в безопасных server logs без token. Проверка не меняет connection, пока gRPC-вызов не завершён успешно.
Фиксированные 5 RPS, queue 50, delay 1 second и deadline 10 seconds соответствуют MVP policy. Отдельные лимиты сервисов, burst, exponential backoff, jitter и distributed coordination не добавляются до появления нагрузки или нескольких экземпляров.
### Frontend settings flow
Новая FSD feature управляет transport calls только через generated OpenAPI client и React Query. Settings page сначала получает connection status и показывает отдельное retryable error state при неуспехе. Loaded state показывает read-only token hint, password field и действие `Подключить` или `Заменить`; token очищается после успешного `PUT`, но сохраняется при error. При подключении отображается локализованный `connectedAt` из RFC 3339 UTC.
Удаление требует Mantine confirmation modal. Пока `PUT` или `DELETE` pending, section field, buttons и modal actions disabled. UI различает `INVALID_TBANK_TOKEN` и `TBANK_UNAVAILABLE` локализованными сообщениями; остальные ошибки получают общий безопасный текст.
## Risks / Trade-offs
- Per-instance limiter не координирует несколько backend instances → MVP рассчитан на один instance; distributed limiter добавляется перед horizontal scaling.
- Параллельные mutation requests могут примениться в порядке завершения → accepted MVP limitation; добавить User-scoped lock или versioning при появлении конфликтов.
- Token может быть отозван после сохранения, а status endpoint этого не определит → будущие accounts/portfolio read-paths классифицируют upstream error; настройки не тратят quota на health check.
- Невозможность расшифровать данные после ручной смены key блокирует token → key rotation требует отдельной миграции до такой операции.
## Migration Plan
1. Добавить gRPC dependencies, vendored protobuf contracts и fail-fast environment validation для encryption key, endpoint, rate limit и optional CA path.
2. Добавить Prisma migration для `TbankConnection`, crypto service, T-Bank client и authenticated connection HTTP endpoints с OpenAPI.
3. Regenerate typed frontend API client из backend OpenAPI и реализовать settings feature с confirmation modal.
4. Добавить минимальные backend unit/e2e checks: crypto round-trip и invalid key, failed `PUT` не сохраняет token, `DELETE` удаляет запись, endpoints требуют auth и не возвращают token. Проверить UI вручную.
5. Создать `docs/features/tbank-connection.md` с API, configuration, RFC 3339 UTC, resilience policy и Mermaid sequence diagram; выполнить lint, tests, builds и OpenSpec validation.
Rollback до release: откатить приложение и Prisma migration. После сохранения реальных token rollback должен выполняться только вместе с безопасным удалением или сохранением зашифрованной таблицы, пока исходный encryption key доступен.
@@ -7,7 +7,7 @@
- Добавить настройки профиля для добавления, замены и удаления одного T-Bank token на пользователя. - Добавить настройки профиля для добавления, замены и удаления одного T-Bank token на пользователя.
- Проверять token вызовом T-Bank `GetAccounts` до сохранения и хранить только шифротекст с ключом из окружения. - Проверять token вызовом T-Bank `GetAccounts` до сохранения и хранить только шифротекст с ключом из окружения.
- Добавить server-side gRPC T-Bank client с глобальным конфигурируемым лимитом 5 запросов в секунду, ограниченными повторами временных ошибок и безопасной классификацией upstream-ошибок. - Добавить server-side gRPC T-Bank client с глобальным конфигурируемым лимитом 5 запросов в секунду, ограниченными повторами временных ошибок и безопасной классификацией upstream-ошибок.
- Добавить краткоживущий in-memory cache для данных счетов/портфеля и долгий cache метаданных инструментов. - Подготовить только проверку token через `GetAccounts`; API счетов, портфеля и их cache остаются следующими changes.
- Документировать поток подключения, правила обработки token и устойчивость upstream-вызовов. - Документировать поток подключения, правила обработки token и устойчивость upstream-вызовов.
## Capabilities ## Capabilities
@@ -0,0 +1,79 @@
## Purpose
Позволяет авторизованному пользователю безопасно подключить T-Bank Invest для будущего получения только собственных инвестиционных данных через backend.
## ADDED Requirements
### Requirement: Статус подключения T-Bank
Система SHALL предоставлять авторизованному User `GET /api/v1/tbank-connection`, возвращающий состояние его единственного подключения в форме `{ connected, connectedAt }`. При отсутствии подключения ответ MUST быть `{ connected: false, connectedAt: null }`; при наличии подключения `connectedAt` MUST быть timestamp в RFC 3339 UTC. Получение статуса MUST NOT обращаться к T-Bank.
#### Scenario: Подключение отсутствует
- **WHEN** авторизованный User запрашивает статус без сохранённого подключения
- **THEN** система отвечает `200` с `{ connected: false, connectedAt: null }`
#### Scenario: Подключение существует
- **WHEN** авторизованный User запрашивает статус после успешного подключения
- **THEN** система отвечает `200` с `{ connected: true, connectedAt: <RFC 3339 UTC timestamp> }` без token
### Requirement: Создание и замена подключения T-Bank
Система SHALL позволять авторизованному User создать или заменить единственное подключение через `PUT /api/v1/tbank-connection` с token. Система MUST удалить внешние пробелы token, отклонить пустое значение как ошибку валидации и проверить token запросом T-Bank открытых счетов до сохранения. Пустой список счетов MUST считаться успешной проверкой. Успех MUST вернуть `200` с текущим статусом подключения; успешная замена MUST установить новый `connectedAt`.
#### Scenario: Успешное создание подключения без счетов
- **WHEN** User отправляет непустой token и T-Bank успешно возвращает пустой список открытых счетов
- **THEN** система сохраняет подключение и отвечает `200` с `{ connected: true, connectedAt: <RFC 3339 UTC timestamp> }`
#### Scenario: Успешная замена подключения
- **WHEN** User с подключением отправляет другой token и T-Bank успешно его проверяет
- **THEN** система заменяет token подключения, обновляет `connectedAt` и отвечает текущим статусом без token
#### Scenario: Невалидный token не заменяет существующее подключение
- **WHEN** T-Bank отклоняет token как неаутентифицированный или не имеющий прав
- **THEN** система отвечает `400` Problem Details с code `INVALID_TBANK_TOKEN` и сохраняет прежнее подключение без изменений
#### Scenario: T-Bank временно недоступен при проверке token
- **WHEN** проверка token не завершается из-за временной или иной upstream-ошибки
- **THEN** система отвечает `503` Problem Details с code `TBANK_UNAVAILABLE` и не создаёт либо не изменяет подключение
### Requirement: Удаление подключения T-Bank
Система SHALL позволять авторизованному User удалить своё подключение через `DELETE /api/v1/tbank-connection`. Удаление MUST быть идемпотентным и отвечать `204`; после успешного удаления backend MUST прекратить использовать удалённый token и удалить относящиеся к User кэшированные данные T-Bank.
#### Scenario: Удаление существующего подключения
- **WHEN** User удаляет существующее подключение
- **THEN** система удаляет token и связанные кэшированные данные User, затем отвечает `204`
#### Scenario: Повторное удаление подключения
- **WHEN** User удаляет уже отсутствующее подключение
- **THEN** система отвечает `204`
### Requirement: Конфиденциальность token T-Bank
Система MUST хранить не более одного token T-Bank для каждого User только в зашифрованном виде. Token MUST расшифровываться только для серверного вызова T-Bank и MUST NOT передаваться в frontend, HTTP-ответах, OpenAPI examples, сообщениях ошибок или application logs.
#### Scenario: API не раскрывает token
- **WHEN** User создаёт, заменяет, удаляет или получает статус подключения
- **THEN** ни один HTTP-ответ не содержит token или его фрагмент
### Requirement: Устойчивость проверки T-Bank
Система SHALL выполнять проверку token только через защищённое TLS-подключение к T-Bank. Все вызовы T-Bank MUST проходить через общий для экземпляра backend лимит, задаваемый конфигурацией и равный 5 запросам в секунду по умолчанию, с FIFO-очередью не более 50 ожидающих вызовов. Для временных ошибок система MUST выполнить не более одного повторного вызова; если проверка не завершается успешно, token MUST NOT сохраняться.
#### Scenario: Лимит ожидания upstream-вызовов исчерпан
- **WHEN** 50 вызовов T-Bank уже ожидают общий лимитер
- **THEN** следующий запрос не обращается к T-Bank и получает `503` Problem Details с code `TBANK_UNAVAILABLE`
#### Scenario: Временная ошибка успешно повторяется
- **WHEN** первая проверка token завершается временной ошибкой, а единственный повтор завершается успешно
- **THEN** система сохраняет подключение и отвечает текущим статусом
### Requirement: Управление подключением в настройках
Клиент SHALL показывать в настройках статус T-Bank connection, текст о необходимости read-only T-Bank Invest token, поле token и действие подключения либо замены. Для существующего подключения клиент MUST показывать действие удаления с modal-подтверждением. Во время создания, замены или удаления все действия секции подключения и действия modal MUST быть отключены. После успешного создания или замены клиент MUST очистить поле token и обновить статус; при ошибке token MUST остаться в поле.
#### Scenario: Пользователь подтверждает удаление
- **WHEN** User подтверждает удаление в modal
- **THEN** клиент блокирует действия секции до результата и после успешного ответа показывает состояние без подключения
#### Scenario: Ошибка загрузки статуса
- **WHEN** клиент не может получить статус подключения
- **THEN** клиент показывает отдельное error-состояние с возможностью повторить запрос и не показывает форму подключения
#### Scenario: Ошибка проверки token в клиенте
- **WHEN** создание или замена получает `INVALID_TBANK_TOKEN` либо `TBANK_UNAVAILABLE`
- **THEN** клиент показывает соответственно локализованное сообщение о token и правах либо о временной недоступности T-Bank, возвращает интерактивность и сохраняет введённый token
@@ -0,0 +1,27 @@
## 1. Backend foundation
- [x] 1.1 Добавить gRPC dependencies, полный vendored набор T-Bank protobuf contracts из `references/moex-vibe`, environment validation для `TOKEN_ENCRYPTION_KEY`, `TBANK_GRPC_ENDPOINT`, `TBANK_RATE_LIMIT_PER_SECOND` и optional `TBANK_CA_CERT_PATH`.
- [x] 1.2 Добавить Prisma-модель `TbankConnection` с уникальным `userId`, зашифрованным token и `connectedAt`, затем создать migration.
- [x] 1.3 Реализовать AES-256-GCM storage для versioned base64 payload и fail-fast обработку невалидного encryption key без логирования secret.
## 2. T-Bank connection API
- [x] 2.1 Реализовать TLS T-Bank gRPC client для `UsersService/GetAccounts`: 10-second deadline, FIFO limit 5 RPS, максимум 50 ожидающих вызовов, один retry временных кодов через 1 second и safe error mapping.
- [x] 2.2 Реализовать authenticated `GET`, `PUT` и idempotent `DELETE /api/v1/tbank-connection`, включая OpenAPI DTO и Problem Details codes `INVALID_TBANK_TOKEN` и `TBANK_UNAVAILABLE`.
- [x] 2.3 Убедиться, что token и его фрагменты не попадают в HTTP responses, OpenAPI examples, exception messages, application logs или gRPC metadata logs.
- [x] 2.4 Добавить минимальные backend checks: crypto round-trip/invalid key, неуспешный `PUT` не сохраняет token, `DELETE` удаляет connection, endpoints требуют auth и не раскрывают token.
## 3. Generated API contract
- [x] 3.1 Запустить backend и перегенерировать коммитируемые frontend OpenAPI-типы из `/api/docs-json`.
## 4. Frontend settings
- [x] 4.1 Добавить FSD feature T-Bank connection на typed API client и React Query: загрузка статуса, create/replace, delete и обновление cache.
- [x] 4.2 Заменить settings placeholder секцией подключения: read-only token hint, password field, локализованный status/ошибки и disabled pending state.
- [x] 4.3 Добавить Mantine confirmation modal для удаления; после успешного `PUT` очищать token field, при error сохранять его. Проверить UI-flow вручную.
## 5. Documentation and verification
- [x] 5.1 Создать `docs/features/tbank-connection.md` с пользовательским потоком, API, configuration, RFC 3339 UTC, правилами secret handling и Mermaid sequence diagram; описать 5 RPS, FIFO queue 50, retry и MVP-ограничения.
- [x] 5.2 Запустить backend lint, tests и build; frontend lint, typecheck, tests, build и no-todo check; провалидировать OpenSpec change.