diff --git a/openspec/changes/add-tbank-connection/design.md b/openspec/changes/add-tbank-connection/design.md new file mode 100644 index 0000000..593df96 --- /dev/null +++ b/openspec/changes/add-tbank-connection/design.md @@ -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 доступен. diff --git a/openspec/changes/add-tbank-connection/proposal.md b/openspec/changes/add-tbank-connection/proposal.md index 36e90c6..74ae984 100644 --- a/openspec/changes/add-tbank-connection/proposal.md +++ b/openspec/changes/add-tbank-connection/proposal.md @@ -7,7 +7,7 @@ - Добавить настройки профиля для добавления, замены и удаления одного T-Bank token на пользователя. - Проверять token вызовом T-Bank `GetAccounts` до сохранения и хранить только шифротекст с ключом из окружения. - Добавить server-side gRPC T-Bank client с глобальным конфигурируемым лимитом 5 запросов в секунду, ограниченными повторами временных ошибок и безопасной классификацией upstream-ошибок. -- Добавить краткоживущий in-memory cache для данных счетов/портфеля и долгий cache метаданных инструментов. +- Подготовить только проверку token через `GetAccounts`; API счетов, портфеля и их cache остаются следующими changes. - Документировать поток подключения, правила обработки token и устойчивость upstream-вызовов. ## Capabilities diff --git a/openspec/changes/add-tbank-connection/specs/tbank-connection/spec.md b/openspec/changes/add-tbank-connection/specs/tbank-connection/spec.md new file mode 100644 index 0000000..544aa02 --- /dev/null +++ b/openspec/changes/add-tbank-connection/specs/tbank-connection/spec.md @@ -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: }` без 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: }` + +#### 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 diff --git a/openspec/changes/add-tbank-connection/tasks.md b/openspec/changes/add-tbank-connection/tasks.md new file mode 100644 index 0000000..05eec8f --- /dev/null +++ b/openspec/changes/add-tbank-connection/tasks.md @@ -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.