Files
han-app/architectory/arch-01-system-architecture.md
T

618 lines
57 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.
# arch-01. Общая архитектура системы
> Термины — в [`arch-00-glossary.md`](arch-00-glossary.md).
> Приоритет документов — в [`README.md`](README.md).
## Назначение
HAN Chat - приложение для мигрантов, где стартовый экран знакомит клиента с сервисом и предлагает задать вопрос. Авторизация не требуется при первом входе: она запрашивается при попытке отправить первое сообщение, потому что в переписке могут обрабатываться персональные данные.
## Зафиксированные решения MVP
- Авторизация: только OTP по **номеру телефона** (email-канал в MVP не используется).
- Вторая сторона чата: Битрикс24 Open Lines.
- Master source auth-данных: Keycloak; профиль в UI — кэш App DB с двусторонней sync через `bitrix-sync`.
- Диалог приложения соответствует диалогу в Битрикс24 Open Lines.
- Лиды и сделки в MVP не используются.
- Файлы production-хранилища: Selectel S3, бакет **S3-data** (логическое имя; физически два бакета — `han-chat-attachments` для файлов чата и `han-chat-documents` для документов компании).
- Файлы до проверки: Selectel S3, бакет **S3-quarantine**; после `200 allow` — перенос в S3-data (attachments). Имена бакетов — [`arch-00-glossary.md`](arch-00-glossary.md); права доступа — ниже и в «Принципы безопасности».
- Мультиязычность в первом релизе не нужна, но тексты должны храниться по мнемоникам для будущих переводов.
- Среда на первом этапе одна и проектируется как боевая.
- Вложения чата MVP: **только изображения и PDF** — см. [`arch-04-settings-and-content.md`](arch-04-settings-and-content.md), «Разрешённые типы файлов чата».
- SMS OTP вводится поэтапно: до production rollout действует явный mock (`KEYCLOAK_OTP_MOCK_ENABLED=true`); целевой real mode — Keycloak генерирует/локально проверяет OTP и создаёт durable order в `sms-service`, а worker асинхронно вызывает i-Digital Direct. Контракт и gates — [`module-11-idgtl-sms.md`](../modules/module-11-idgtl-sms.md).
- Популярный вопрос при выборе **автоматически отправляется как сообщение**; если пользователь не авторизован — сначала согласия и OTP, затем отправка.
- Перечень таблиц и миграций App DB проектирует модуль `database` (и владельцы схем других сервисов); arch фиксирует только **разделение схем** PostgreSQL и контракты между сервисами.
## Пользовательские сценарии
1. Клиент открывает мобильное или web-приложение и видит главный экран с приветствием, популярными вопросами и полем ввода.
2. Frontend определяет, нужна ли **новая UX-сессия**, но до JWT не вызывает backend write-endpoint: клиент может изучить сервис без авторизации через guest UI и `GET /api/v1/public/*`.
3. Если у клиента сохранён **действующий refresh token**, frontend выполняет silent refresh **без OTP** (см. «Поток возврата пользователя»).
4. Клиент нажимает популярный вопрос — frontend подставляет текст вопроса и **инициирует отправку сообщения** (тот же поток, что ручной ввод). Либо клиент вводит свой текст и отправляет.
5. Если клиент не авторизован, перед отправкой первого сообщения frontend показывает pop-up с согласиями и запускает OTP (см. «Поток авторизации»).
6. После успешной авторизации frontend с JWT вызывает **`POST /api/v1/auth/bootstrap`** (в теле — принятые согласия): api-backend создаёт или находит локального пользователя по `keycloak_sub`, сохраняет согласия на `user_id`, создаёт или обновляет профиль; триггер App DB ставит задачу в `sync_queue` для `bitrix-sync`. Затем, если у frontend нет активной UX-сессии или она истекла, вызывается **`POST /api/v1/analytics/session-start`**.
7. api-backend выполняет find-or-create диалога (см. «Создание диалога») и отправляет сообщение (текст популярного вопроса или введённый клиентом).
8. Сообщение клиента проходит Message Safety и через Bitrix24 Local App направляется в Битрикс24 Open Lines.
9. Ответ оператора из Битрикс24 Open Lines поступает через Bitrix24 Local App в api-backend и отображается в чате приложения.
10. Клиент может открыть историю диалогов.
11. Клиент может открыть профиль, где данные структурированы блоками: «Личные данные» и «Документы». В дальнейшем могут добавляться новые блоки.
12. Редактирование профиля из профиля недоступно. Для изменения данных клиент переходит в чат и пишет запрос оператору.
## Компоненты верхнего уровня
- Expo App: единая frontend-кодовая база для iOS, Android и web.
- Keycloak: identity provider, OTP-only авторизация по номеру телефона.
- SMS Service: internal durable order API, шаблоны и бессрочный журнал SMS; отдельный worker вызывает i-Digital Direct, callback обновляет только журнал.
- api-backend: Python-приложение с REST API, realtime-доставкой сообщений и бизнес-логикой.
- Nginx Reverse Proxy: единая публичная точка входа, HTTPS termination и маршрутизация на Keycloak/API/frontend web/Bitrix24.
- Message Safety Service: отдельный сервис проверки входящих сообщений; вызов из API → `200 allow` | `403 deny` | `203 pending` + `task_id` (при `203` api-backend синхронно поллит task до финального вердикта, без очереди анализа на api-backend).
- Bitrix24 Local App: локальное приложение, custom connector `han_mobile_app` для Bitrix24 Open Lines: чат, OAuth, webhook-события, маппинг `dialog_id``bitrix_chat_id`.
- Bitrix24 sync service: двусторонняя синхронизация App DB ↔ Битрикс24 CRM (Contact на MVP; маппинг ID, очередь через триггеры, webhook от роботов Bitrix24).
- Managed PostgreSQL (приватная сеть, одна база): схемы `han_app`, `bitrix_sync`, `bitrix_local`, `keycloak`, `message_safety`, `sms` — отдельный DB-user на схему.
- Redis: rate limits API и realtime/service coordination (**не** OTP counters — они в Keycloak/SPI);
- S3-data: production-хранилище проверенных файлов чата (`han-chat-attachments`) и документов компании (`han-chat-documents`).
- S3-quarantine: временное хранилище загруженных файлов до вердикта Message Safety Service (`han-chat-quarantine`); read-only для `message-safety`.
- observability: JSON-логи в stdout, `request_id`, `trace_id`, **`ux_session_id`** (если передан), базовая трассировка через OpenTelemetry Collector.
## Инфраструктура развёртывания (зафиксировано)
На первом этапе весь backend-контур работает на **одной VM** в облаке провайдера:
- `nginx`, `api-backend`, `message-safety`, `keycloak`, `sms-service`/worker, `bitrix-sync`, `bitrix-local-app`, `redis`, `otel-collector` — в Docker Compose на VM;
- публичный доступ из интернета только через `nginx` (порты 80/443);
- внутренние сервисы общаются по Docker-сети на localhost VM.
Базы данных — **managed PostgreSQL** того же провайдера в **том же облачном кластере/VPC**, **без публичного доступа** из интернета. VM подключается к БД только по приватной сети.
Схема данных в managed PostgreSQL (перечень таблиц внутри схем — в модульных спецификациях, не в arch-*):
| База / схема | Сервисы | Назначение схемы |
|---|---|---|
| одна база / `han_app` | `api-backend`, `bitrix-sync` (ограниченный GRANT) | прикладные данные приложения, очередь sync, audit |
| одна база / `bitrix_sync` | `bitrix-sync` | worker state, retry/dead letter, sync audit |
| одна база / `message_safety` | `message-safety` | verdict cache, safety_task, rule config |
| одна база / `bitrix_local` | `bitrix-local-app` | OAuth, inbox, `dialog_sessions` |
| одна база / `keycloak` | Keycloak | учётные записи, realm, сессии IdP |
| одна база / `sms` | `sms-service`, `sms-worker` | шаблоны, runtime settings, бессрочный журнал отправки/доставки SMS |
Redis на первом этапе остаётся на VM в Docker (ephemeral/coordination). Selectel S3 — внешнее object storage: три бакета (`han-chat-quarantine`, `han-chat-attachments`, `han-chat-documents`); см. [`arch-00-glossary.md`](arch-00-glossary.md).
## Контекстная схема
```mermaid
flowchart LR
Client[Expo Mobile/Web App]
Nginx[Nginx Reverse Proxy]
Keycloak[Keycloak OTP]
SMS[SMS Service]
SMSWorker[SMS Worker]
Direct[i-Digital Direct]
API[Python api-backend]
Safety[Message Safety Service]
DB[(PostgreSQL)]
Redis[(Redis)]
Sync[Bitrix24 sync service]
LocalApp[Bitrix24 Local App]
Bitrix[Bitrix24 CRM]
S3Data[(S3-data: attachments + documents)]
S3Q[(S3-quarantine)]
Obs[observability]
Client -->|HTTPS REST + Realtime| Nginx
Nginx -->|/auth| Keycloak
Nginx -->|exact POST /callbacks/idgtl/sms| SMS
Nginx -->|"/api REST + WS realtime"| API
Keycloak --> DB
Keycloak -->|durable SMS order| SMS
SMS --> DB
SMSWorker --> DB
SMSWorker -->|HTTPS POST /api/v1/message| Direct
Direct -->|delivery callback| Nginx
API --> DB
API --> Redis
Client -->|presigned PUT| S3Q
API -->|presign / HeadObject / move / delete| S3Q
API -->|promote chat files| S3Data
API -->|internal check message| Safety
Safety --> DB
Safety --> Redis
Safety -->|read scan| S3Q
API -->|send messages| LocalApp
API -->|App DB writes| DB
Sync -->|sync_queue + profile| DB
Sync -->|CRM Contact REST| Bitrix
Bitrix -->|robot webhook| Sync
Bitrix -->|ONIMCONNECTOR*| LocalApp
LocalApp -->|imconnector.send.messages/status| Bitrix
LocalApp -->|normalized inbox events| API
API -->|WebSocket or polling fallback| Client
API --> Obs
Safety --> Obs
Sync --> Obs
LocalApp --> Obs
LocalApp --> DB
```
## Архитектурные границы
### Frontend
Отвечает за:
- стартовый экран с приветствием, популярными вопросами, полем ввода, историей и профилем;
- гостевой режим до первого сообщения;
- показ pop-up с обязательными согласиями на обработку персональных данных и пользовательское соглашение, а также необязательным согласием на рекламные коммуникации;
- сбор данных устройства для передачи в backend;
- **управление аналитической UX-сессией** на клиенте: после получения JWT — `session_start`, хранение `ux_session_id` и `last_activity_at` **только в памяти**, заголовок `X-Ux-Session-Id` в JWT-запросах;
- хранение access token и refresh token в безопасном хранилище после авторизации;
- **жизненный цикл access token**: проактивное обновление по расписанию (до истечения `exp`) и обработка **`401`** от `api-backend` (см. «Обновление access token (frontend)»);
- при открытии приложения: проверку refresh token → silent refresh через Keycloak **или** OTP-flow при истечении refresh token;
- отображение входящих сообщений от оператора;
- загрузку файлов в чат: `init` → presigned PUT в S3 → `complete` (байты не через api-backend);
- работу с текстовыми мнемониками;
- отправку `traceparent`/correlation id в backend.
Frontend не должен:
- хранить бизнес-логику синхронизации с Битрикс24;
- принимать решения о доступе к чужим документам или диалогам;
- обращаться напрямую к Битрикс24, Selectel S3 или базе данных.
### api-backend
Отвечает за:
- публичные настройки приложения для frontend;
- проверку JWT от Keycloak для защищенных операций; при истёкшем или невалидном access token — **`401`** (refresh выполняет frontend, не backend);
- локальную регистрацию пользователя приложения: `find-or-create` `UserIdentity` по `keycloak_sub`, создание минимального `ClientProfile` для нового пользователя, обновление `last_login_at` для существующего (после OTP — см. `POST /api/v1/auth/bootstrap`);
- **приём события `session_start`**: запись `UxSession`, audit/analytics-событие; **не** используется для контроля доступа;
- валидация данных получаемых от frontend (соответствие типов данных, проверка обязательности полей, проверка формата данных, диапазоны значений, размер полей) через Pydantic
- хранение согласий пользователя в App DB (**`user_id`**, **`ux_session_id`**, **`client_ip`**, версии документов) — только после JWT;
- профиль, структурированный блоками;
- API чата, истории, файлов и документов;
- realtime-доставку входящих сообщений клиенту;
- отправку сообщений клиента в Open Lines через Bitrix24 Local App;
- прием нормализованных входящих событий Open Lines от Bitrix24 Local App;
- хранение истории диалогов;
- запись данных профиля в App DB (синхронизация с Bitrix24 — триггеры → `sync_queue``bitrix-sync`, без участия api-backend);
- выдачу **presigned URL** на загрузку в S3-quarantine, проверку объекта при `complete`, promote/delete после вердикта;
- синхронный вызов Message Safety Service (`POST /internal/safety/v1/messages/check`) и интерпретацию ответа: `200 allow`, `403 deny`, `203 pending` + `task_id`;
- при `200`: перенос файлов quarantine → S3-data, сохранение сообщения, отправка в Bitrix24;
- при `403`: удаление файлов из quarantine, безопасный ответ клиенту;
- при `203`: api-backend **синхронно поллит** `GET /internal/safety/v1/messages/tasks/{task_id}` до финального `200`/`403` (timeout budget — arch-04), затем promote/Bitrix или cleanup, и только после этого отвечает клиенту финальным результатом;
- это **ожидание в рамках одного клиентского HTTP-соединения**, а не общая очередь: другие запросы обрабатываются параллельно (workers/async);
- решение «быстрая проверка / долгая» принимает только `message-safety`; на api-backend **нет** очереди анализа сообщений;
- запись checkpoint в `safety_tasks` (App DB) на время poll — для recovery при timeout/crash (I1);
- circuit breaker + timeout budget на вызовы `message-safety` и `bitrix-local-app` (I2);
- auth-aware rate limits для сообщений, пользовательских и сервисных операций;
- аудит пользовательских действий;
- единые ошибки и валидацию входных данных.
### Bitrix24 Local App
Отвечает за Open Lines (чат):
- регистрацию локального приложения Bitrix24;
- OAuth lifecycle Bitrix24 и хранение токенов портала;
- регистрацию и активацию custom connector `han_mobile_app` для открытой линии 8;
- прием публичных событий Bitrix24 `ONIMCONNECTOR*` на `/bitrix/handler`;
- нормализацию событий Open Lines в доменные события HAN;
- хранение `dialog_sessions`: связка `external_chat_id` (=`dialog_id` приложения) ↔ `bitrix_chat_id``session_id`;
- хранение локального inbox до готовности API;
- internal API для api-backend: `POST /internal/openlines/v1/messages`, `GET /internal/openlines/v1/dialogs/{external_chat_id}`;
- forward нормализованных событий оператора в API (`BITRIX_API_FORWARD_URL`);
- вызовы `imconnector.send.messages` и `imconnector.send.status.delivery`.
Не отвечает за:
- CRM Contact mapping и синхронизацию прочих CRM-сущностей;
- сохранение сообщений и истории чата в App DB;
- realtime-доставку в Expo App;
- бизнес-логику профиля и документов.
### Bitrix24 sync service
Отвечает за **двустороннюю** синхронизацию данных между App DB и Битрикс24 CRM:
- **маппинг ID** сущностей приложения ↔ Bitrix24 (`bitrix_contact_id`, `entity_external_mapping`);
- **App DB → Bitrix24:** обработка очереди `sync_queue` (триггеры App DB) — map/create Contact по телефону, push обновлений полей;
- **Bitrix24 → App DB:** приём webhook от роботов Bitrix24, обновление профиля с GUC `han.sync_suppress`;
- реестр синхронизируемых сущностей (MVP: Contact; post-MVP: Lead, Deal, Document);
- повторные попытки, rate limiting Bitrix REST, dead letter;
- прямой доступ к схеме `han_app` и собственной `bitrix_sync`.
Не отвечает за:
- hot path чата Open Lines;
- OAuth lifecycle локального приложения Bitrix24;
- создание `UserIdentity` / `ClientProfile` в auth-flow;
- хранение `dialog_sessions`.
### Keycloak
Отвечает за:
- OTP-only регистрацию и вход;
- OTP по номеру телефона; генерация и локальная проверка кода, challenge lifecycle, limits и verify audit — в Keycloak;
- в real mode — заказ в `sms-service` по закрытому `POST /internal/sms/v1/send`; Keycloak ждёт только `200/202` + `sms_message_id`, не вызывает Direct и не читает provider statuses;
- продуктовые лимиты OTP (`otp.phone.*` из `app_settings`) через authenticator/SPI и settings bridge `api-backend` (см. arch-04); durable counters/challenges/events — в provider-owned таблицах schema `keycloak`, **не** в Redis и не в `api-backend`;
- хранение учетных записей;
- выдачу и обновление токенов (access + refresh);
- настройку realm, clients, roles, policies;
- публикацию OIDC discovery и JWKS для проверки JWT.
Парольная авторизация, magic link и социальные логины не входят в MVP.
**Взаимодействия (MVP):**
| С кем | Направление | Назначение |
|---|---|---|
| Expo frontend | Frontend → Keycloak (`/auth/*` через nginx) | OTP login (Authorization Code + PKCE), Refresh Token Grant, logout |
| `api-backend` | api-backend → Keycloak JWKS/discovery | Валидация access token (issuer, audience, подпись); **не** вызывает Admin API в hot path |
| Managed PostgreSQL | Keycloak → схема `keycloak` | Пользователи IdP, сессии, realm |
| `sms-service` | Keycloak → `sms-service` (real mode) | Durable order; service token, idempotency key и `sms_message_id` |
Confidential **backend client** Keycloak (client credentials) в MVP **не обязателен**: S2S между нашими сервисами идёт по service tokens, не через Keycloak. Client можно завести заранее в realm как optional для будущих admin/ops сценариев.
### Nginx Reverse Proxy
Отвечает за:
- прием внешнего HTTPS-трафика;
- TLS termination;
- редирект HTTP на HTTPS (на веб-домене; для выделенного API-домена HTTP не допускается — см. «Принципы безопасности»);
- маршрутизацию `/api/*` в api-backend (включая `WS /api/v1/realtime`);
- маршрутизацию `/auth/*` или выделенного auth-домена в Keycloak;
- маршрутизацию публичных `/bitrix/*` endpoint в `bitrix-local-app`;
- маршрутизацию `/bitrix/sync/*` webhook endpoint в `bitrix-sync`;
- маршрутизацию только exact `POST /callbacks/idgtl/sms` в `sms-service` по HTTPS, с allowlist актуального IP Direct и без логирования Basic Authorization;
- защиту internal endpoint `bitrix-local-app` через private network или `nginx allowlist`;
- отсутствие публичной маршрутизации к `message-safety` — сервис доступен только из внутренней Docker-сети;
- передачу `X-Forwarded-For`, `X-Forwarded-Proto`, `X-Forwarded-Host`, `X-Request-ID` (если клиент не прислал `X-Request-ID` — nginx **генерирует** UUID и прокидывает upstream);
- базовые лимиты размера запроса и timeout;
- грубые edge rate limits по IP, route и зоне риска;
- TLS 1.2/1.3, HSTS, security headers и скрытие технологических заголовков;
- кэширование публичных endpoint настроек и контента;
- запрет доступа к внутренним сервисам и техническим портам извне.
### Message Safety Service
Отвечает за:
- проверку **входящих сообщений от пользователя** (текст, ссылки, файлы);
- **внутреннюю** orchestration: синхронно текст и ссылки; при необходимости — async-проверка файлов;
- HTTP-контракт для api-backend:
- `200` — синхронная проверка завершена, **allow**;
- `403` — синхронная проверка завершена, **deny**;
- `203` + `task_id` — нужна async-проверка (обычно файлы), сообщение в обработке;
- финальный вердикт async-задачи по `GET /internal/safety/v1/messages/tasks/{task_id}`: `200 allow` | `403 deny` | `203 pending`;
- SHA-256 хеширование и lookup кэша вердиктов;
- отдельный pipeline проверки ссылок;
- запись verdict cache, `safety_task` и audit в схеме `message_safety`;
- internal API: `POST /internal/safety/v1/messages/check`, `GET /internal/safety/v1/messages/tasks/{task_id}`.
Не отвечает за:
- загрузку файлов клиентом, presigned URL, перемещение quarantine → S3-data, удаление из quarantine;
- сохранение сообщений, истории диалогов (CRM sync — зона `bitrix-sync`, не api-backend);
- доставку в Bitrix24 Open Lines и realtime клиенту;
- проверку JWT, согласий, edge rate limits;
- polling `task_id` на стороне клиента запрещён — только api-backend, и только **внутри** обработки `POST .../messages` (sync wait до финального вердикта).
api-backend не решает, sync или async нужна проверка внутри Message Safety: это определяет Message Safety Service. Но для клиента `POST .../messages` всегда завершается финальным allow/deny (или ошибкой timeout/зависимости).
## Гостевой режим (до JWT)
До OTP frontend работает **локально** без записи согласий и UX-сессии в App DB:
- UI главного экрана, популярные вопросы и публичный контент — через `GET /api/v1/public/*` (без JWT);
- pop-up согласий показывается **до** OTP, но факт принятия хранится **только на клиенте** до получения tokens;
- **`POST /api/v1/consents`**, **`POST /api/v1/analytics/session-start`** и остальные write/API чата — **только с JWT**;
- опциональный локальный `guest_session_id` (UUID в secure storage) может использоваться frontend для своей аналитики/идемпотентности UI, но **не** является auth и **не** открывает backend write-endpoint.
## Аналитическая UX-сессия (`ux_session_id`)
**UX-сессия** — период непрерывной активности **авторизованного** пользователя в приложении для аналитики и сквозной трассировки. Это **не** сессия Keycloak, **не** refresh/access token и **не** механизм авторизации.
### Роли компонентов
**Frontend** (источник истины по правилам сессии):
- хранит `ux_session_id` и `last_activity_at` **только в памяти** (не в localStorage/secure storage);
- вызывает `POST /api/v1/analytics/session-start` **только при наличии JWT** (после OTP или silent refresh);
- в гостевом режиме `session-start` **не** вызывается;
- при новой сессии сохраняет полученный `ux_session_id`;
- обновляет `last_activity_at` при пользовательской активности и при возврате из фона;
- при resume проверяет `(now - last_activity_at) > idle_timeout` → при превышении — новая сессия (снова с JWT);
- передаёт **`X-Ux-Session-Id`** во **всех** JWT-запросах к backend, пока сессия активна.
**api-backend**:
1. принимает `session_start` **только с валидным JWT**, создаёт запись **`UxSession`** с `user_id`, возвращает `ux_session_id`;
2. пишет analytics/audit-событие `session_start` (без PII);
3. включает `ux_session_id` из заголовка в JSON-логи (если передан);
4. отсутствие `ux_session_id` **не** блокирует API (кроме endpoint, где id обязателен по контракту) — это не auth, но сам `session-start` без JWT недоступен.
`request_id` — один HTTP-запрос; `ux_session_id` — период UX-активности для аналитики и корреляции логов.
## Поток возврата пользователя (без OTP)
1. Клиент открывает приложение. Пока нет JWT — гостевой UI; `session-start` не вызывается.
2. Frontend проверяет наличие refresh token в secure storage.
3. Если refresh token **действителен** — frontend запрашивает новый access token у Keycloak (Refresh Token Grant), **OTP не показывается**, затем при необходимости начала новой UX-сессии вызывает `POST /api/v1/analytics/session-start`.
4. Frontend работает как авторизованный пользователь (история, профиль, чат).
5. Если refresh token **отсутствует или истёк** — клиент остаётся в гостевом режиме до сценария, требующего auth; при первом сообщении — «Поток авторизации» с OTP.
## Обновление access token (frontend)
Пока refresh token **действителен**, frontend **сам** поддерживает актуальный access token — **не** полагаясь только на открытие приложения и **не** дожидаясь истечения refresh token (использует его для обновления access token заранее).
### Проактивное обновление по расписанию
1. После получения tokens (OTP или refresh) frontend сохраняет access token, refresh token и момент истечения access token (`exp` из JWT или `expires_in` из ответа Keycloak).
2. Запускает таймер/scheduler: обновить access token **до** наступления `exp` (рекомендуемый запас — **60 с** до `exp`; константа модуля frontend).
3. По срабатыванию таймера — **Refresh Token Grant** к Keycloak, сохранение новой пары tokens, перепланирование следующего обновления.
4. **Single-flight:** параллельные refresh-запросы не дублируются (один in-flight refresh, остальные ждут результат).
5. Успешный refresh access token **не** создаёт новую UX-сессию и **не** вызывает `session_start`.
### Обработка `401` от `api-backend`
Если запрос с access token вернул **`401`** (токен уже истёк или отклонён):
1. HTTP-клиент frontend **один раз** инициирует Refresh Token Grant (если refresh ещё не выполняется — через тот же single-flight).
2. При успехе — подставляет новый access token и **повторяет исходный запрос** (без бесконечных retry).
3. При неудаче refresh (`invalid_grant`, истёк refresh token, ошибка Keycloak) — очищает tokens, переводит UI в **гостевой режим**; повторная авторизация — через OTP при следующем защищённом действии.
4. Запросы, пришедшие во время in-flight refresh, **ставятся в очередь** и выполняются после успешного обновления (или отклоняются при провале refresh).
5. Тот же принцип — для **WebSocket** `/api/v1/realtime`: при ошибке auth — refresh и переподключение с новым access token.
### Разделение ответственности
| Компонент | Поведение |
|---|---|
| **Frontend** | scheduler refresh, intercept `401`, retry, single-flight, хранение tokens |
| **Keycloak** | выдача и ротация tokens (Refresh Token Grant) |
| **api-backend** | проверка JWT; при невалидном/expired access token — **`401`**, refresh **не** выполняет |
## Создание диалога (MVP)
- У пользователя **не более одного активного** диалога: статус `open` | `waiting_for_company` | `waiting_for_client`. Закрытые (`closed`) остаются в истории.
- `POST /api/v1/dialogs`: если активный диалог уже есть — возвращает его (`200` / idempotent), новый не создаёт; иначе создаёт (`201`, `status=open`).
- Диалог создаётся **лениво** при первой отправке сообщения авторизованным клиентом.
- Frontend перед `POST .../messages` вызывает `POST /api/v1/dialogs` (заголовок `Idempotency-Key`), получает `dialog_id` и использует его далее.
- Популярный вопрос: после auth тот же порядок — `POST /dialogs``POST .../messages` с текстом вопроса.
- `dialog_id` = `external_chat_id` для Open Lines (см. [`arch-00-glossary.md`](arch-00-glossary.md), «Идентификаторы»).
- При первой доставке в Bitrix24 `bitrix-local-app` создаёт запись `dialog_sessions`.
- Новый активный диалог после `closed` — снова через `POST /dialogs` (когда продукт это разрешит; MVP: один активный в любой момент).
## Поток авторизации (OTP)
Срабатывает, когда клиент **ещё не имеет действующего refresh token** (первый вход) или refresh token **истёк**. Если refresh token валиден — см. «Поток возврата пользователя».
1. Клиент в гостевом режиме (только UI + `GET /api/v1/public/*`).
2. Клиент инициирует отправку сообщения (ручной ввод или популярный вопрос).
3. Frontend показывает pop-up с тремя согласиями; факт принятия хранится **локально** до OTP.
4. Клиент обязан принять согласие на обработку персональных данных и пользовательское соглашение.
5. Клиент может опционально согласиться на рекламные коммуникации.
6. Если обязательные согласия не даны, отправка блокируется.
7. Frontend запрашивает публичные настройки и показывает форму ввода номера телефона (единственный канал MVP).
8. Keycloak запускает OTP-flow: в mock mode challenge сразу активен без SMS; в real mode Keycloak создаёт `ordering`, генерирует OTP, заказывает SMS в `sms-service` и активирует challenge только после durable order.
9. Лимиты OTP на **edge**`nginx`; продуктовые `otp.phone.*` применяет Keycloak. HTTP retry одного durable order использует прежние challenge/idempotency key и не увеличивает send counter.
10. Клиент вводит OTP и отправляет его в Keycloak.
11. **Keycloak проверяет корректность введённого OTP**:
- при **`KEYCLOAK_OTP_MOCK_ENABLED=true`** (MVP и любой режим с включённой заглушкой): введённое значение должно **совпадать** с `KEYCLOAK_OTP_MOCK_CODE` из `.env`;
- при **`KEYCLOAK_OTP_MOCK_ENABLED=false`**: значение сверяется локально с HMAC OTP, сгенерированного Keycloak и переданного в закрытом заказе `sms-service`; статусы Direct и callback на verify не влияют.
- при неверном коде Keycloak возвращает ошибку; frontend не получает tokens, шаг 12 не выполняется.
12. При успешной проверке frontend получает tokens через OIDC Authorization Code Flow with PKCE.
13. Frontend с JWT вызывает **`POST /api/v1/auth/bootstrap`** — в теле передаёт локально принятые согласия и device metadata (см. arch-02). api-backend атомарно: `find-or-create` по JWT `sub` (`keycloak_sub`), телефон из JWT claims (не из body) → сохранение `UserConsent` на `user_id` → минимальный профиль.
14. Frontend вызывает **`POST /api/v1/analytics/session-start`** (если нужна новая UX-сессия) и далее работает с `X-Ux-Session-Id`.
15. Триггер App DB ставит задачу `contact.map_or_create` в `sync_queue`; `bitrix-sync` асинхронно находит или создает Contact в Битрикс24. Авторизация не должна синхронно зависеть от ответа Битрикс24 CRM.
16. Frontend создаёт диалог и отправляет отложенное сообщение (см. «Создание диалога» и поток чата).
Отдельный **`POST /api/v1/consents`** после первого входа нужен, когда пользователь заново принимает обновлённые версии документов (не часть OTP-flow).
## Поток работы с чатом: клиент -> Битрикс24
В MVP сообщение клиента — **`content_kind`** `text` или `file`, не оба (см. [`arch-02-api-contracts.md`](arch-02-api-contracts.md), «Формат исходящего сообщения»).
**Текстовое сообщение:**
1. Frontend вызывает `POST /api/v1/dialogs` (если `dialog_id` ещё нет), затем отправляет `POST /api/v1/dialogs/{dialog_id}/messages` с непустым `text` (без вложения).
2. Nginx и API применяют rate limits.
3. API **синхронно** вызывает Message Safety Service (`POST /internal/safety/v1/messages/check`) — шаги текст и ссылки.
4. Далее — общая ветка вердикта (п. 5–8 ниже).
**Файловое сообщение:**
1. Frontend инициализирует **одно** вложение (`POST .../attachments/init`), получает **presigned PUT** в **S3-quarantine**, загружает байты **напрямую в S3**, затем вызывает `POST .../attachments/{attachment_id}/complete`.
2. Frontend отправляет `POST /api/v1/dialogs/{dialog_id}/messages` с `attachment_id` и `checksum` (поле `text` пустое).
3. Nginx и API применяют rate limits.
4. API **синхронно** вызывает Message Safety Service — шаг проверки файла (текст и ссылки пропускаются, если `text` пуст).
**Общая ветка вердикта (оба типа):**
5. **`403 deny`**: API удаляет quarantine (если был файл), выставляет `safety_status=blocked`, `delivery_status=rejected`, возвращает клиенту безопасную ошибку; в Bitrix24 ничего не уходит.
6. **`200 allow`**: API переносит файл в S3-data attachments (если был), сохраняет сообщение (`safety_status=allowed`, `delivery_status=accepted`) и фиксирует задачу доставки в Open Lines. После успешной отправки через `bitrix-local-app` статус становится `delivery_status=delivered`, API подтверждает клиенту финальный результат; `Dialog.status``waiting_for_company`. Если Bitrix24/S3/dependency недоступны после allow, статус становится `delivery_status=failed`, клиент получает безопасную ошибку зависимости.
7. **`203 pending` + `task_id`**: api-backend пишет checkpoint в `safety_tasks` и **регулярно синхронно** вызывает `GET /internal/safety/v1/messages/tasks/{task_id}` (backoff), пока не получит финальный вердикт или не истечёт `MESSAGE_SAFETY_TASK_POLL_MAX_SEC`. Пока идёт poll, **этот** клиентский `POST .../messages` ещё не завершён (соединение ждёт). Параллельные запросы других клиентов **не** блокируются — общей очереди анализа на api-backend нет.
- финальный **`200 allow`** → как п. 6, затем ответ клиенту;
- финальный **`403 deny`** → как п. 5, затем ответ клиенту;
- timeout / недоступность safety → `delivery_status=failed`, безопасная ошибка клиенту (`503` / `504`), quarantine не promote; recovery по `safety_tasks` — зона модуля.
Клиент на `POST .../messages` получает **только финальный** результат (или ошибку инфраструктуры), не промежуточное «обрабатывается».
### Надёжность доставки и recovery
- Для доставки в Bitrix24 используется transactional outbox/checkpoint в App DB: запись `Message` и запись намерения доставки фиксируются атомарно, а повторная отправка в `bitrix-local-app` идемпотентна по `message_id` / `Idempotency-Key`.
- `delivery_status=accepted` означает, что API принял сообщение и завершил safety allow, но ещё не получил подтверждение доставки в Open Lines. `delivery_status=delivered` выставляется только после успешного ответа `bitrix-local-app` о приёме сообщения для Bitrix24 Open Lines.
- Recovery по `han_app.safety_tasks` восстанавливает только сценарии, где Message Safety вернул `203 pending` и клиентский запрос оборвался из-за timeout/crash. Recovery job повторно опрашивает `message-safety` по `task_id`, затем идемпотентно выполняет promote/delete quarantine и обновляет `Message`/`MessageAttachment`.
- Объекты в S3-quarantine не удаляются при timeout safety до финального verdict; orphan-cleanup удаляет только просроченные объекты без активного `safety_tasks` или attachment metadata.
## Поток работы с чатом: Битрикс24 -> клиент
1. Оператор отвечает клиенту в Битрикс24 Open Lines.
2. Битрикс24 отправляет `ONIMCONNECTOR*` webhook/event в `bitrix-local-app`.
3. `bitrix-local-app` проверяет `application_token`, нормализует payload и сохраняет idempotent inbox.
4. `bitrix-local-app` обогащает событие данными из `dialog_sessions` и forward-ит в API, если `BITRIX_API_FORWARD_URL` включен. При недоступности API событие остаётся во внутреннем inbox, повторяется с backoff и после исчерпания retry попадает в DLQ; дубликаты определяются по `(external_chat_id, bitrix_message_id)`.
5. api-backend находит локальный диалог по `external_chat_id` (= `dialog_id`, см. [`arch-00-glossary.md`](arch-00-glossary.md)).
6. api-backend сохраняет входящее сообщение в App DB (`sender_type=company`, `delivery_status=delivered`). Файл оператора (если есть) — в бакет **S3-data attachments** (`han-chat-attachments`) с metadata в `MessageAttachment`; бакет **documents** зарезервирован для документов компании в профиле (post-MVP). По факту сообщения API обновляет `Dialog.status`: входящее от оператора → `waiting_for_client`, исходящее от клиента → `waiting_for_company` (значения — arch-00).
7. `bitrix-local-app` подтверждает доставку в Bitrix24 через `imconnector.send.status.delivery` после успешного сохранения события в api-backend или идемпотентного duplicate-ack.
8. api-backend публикует событие для frontend через **WebSocket** (`WS /api/v1/realtime`). Если realtime недоступен, frontend получает сообщение через polling `GET /api/v1/dialogs/{dialog_id}/messages?after=...`.
9. Frontend отображает сообщение оператора в чате.
10. При получении от `bitrix-local-app` доменного события `dialog.closed` (Bitrix24 `ONIMCONNECTORDIALOGFINISH`) API переводит `Dialog.status` в `closed`.
## Документы компании (post-MVP)
Доставка документов из Bitrix24 в приложение **не входит в MVP** — см. [`!Backlog.md`](../../HAN_chat/!Backlog.md), п. 9.
В MVP блок профиля «Документы» и API `GET /api/v1/me/documents` зарезервированы; список может быть пустым. Контракт endpoint — в [`arch-02-api-contracts.md`](arch-02-api-contracts.md).
## Профиль клиента
Профиль должен быть блочным.
Блок "Личные данные":
- ФИО;
- гражданство;
- номер телефона в РФ;
- зарубежный номер телефона;
- email.
Блок "Документы":
- перечень документов, отправленных клиенту компанией (в MVP — пустой до реализации бэклога);
- дата отправки;
- наименование документа;
- возможность скачать документ (после реализации доставки).
Редактирование данных профиля недоступно.
### Sync профиля и master для PII
App DB — **локальный кэш** для UI. Двусторонний sync — `bitrix-sync` (имена полей — [`arch-00-glossary.md`](arch-00-glossary.md)):
- **Auth-телефон:** master — Keycloak (`UserIdentity.phone_number`); изменения могут инициировать `contact.update` через триггеры.
- **Поля профиля для UI:** master — последнее успешно синхронизированное значение; основной входящий поток на MVP — правки сотрудником в Bitrix24 (webhook → App DB).
- **App → Bitrix:** триггеры `han_app``sync_queue` (`contact.update`).
- **Bitrix → App:** webhook робота → `bitrix-sync`; запись с GUC `han.sync_suppress` (без эхо в очередь).
- **Конфликт:** побеждает более позднее событие (`updated_at`, audit в `bitrix_sync`).
## Аудит скачиваний
При выдаче presigned URL на скачивание вложений чата (`GET /api/v1/dialogs/{dialog_id}/attachments/{attachment_id}/download-url`) и документов профиля (`GET /api/v1/documents/{document_id}/download-url`) api-backend пишет audit-событие в App DB:
| Поле | Значение |
|---|---|
| `event_type` | `attachment.download_url_issued` / `document.download_url_issued` |
| `user_id` | текущий пользователь из JWT |
| `resource_type` | `attachment` / `document` |
| `resource_id` | UUID сущности |
| `ux_session_id` | из заголовка `X-Ux-Session-Id` |
| `request_id` | из заголовка запроса |
| `ip`, `user_agent` | из proxy headers |
В audit **не** сохраняются presigned URL, содержимое файлов и PII. Формат таблицы — в модуле `database`.
## Realtime (кратко)
Детальный контракт — [`arch-02-api-contracts.md`](arch-02-api-contracts.md), раздел «Realtime».
- Transport: **только WebSocket** `WS /api/v1/realtime` (JWT). SSE в MVP **не** используется.
- Путь входит в `/api/*`; отдельный location `/realtime/*` в nginx **не** нужен.
- Fallback: polling `GET /api/v1/dialogs/{dialog_id}/messages?after=...`.
- События: новое сообщение, смена `delivery_status` / `safety_status`, смена `Dialog.status`.
## Принципы безопасности
- Все защищенные пользовательские API требуют валидный JWT.
- Без JWT доступны **только** read-only публичные endpoint: `GET /api/v1/public/*` (rate limit + CORS + кэш). Write-endpoint (`consents`, `session-start`, чат, профиль и т.д.) требуют JWT.
- Все внешние пользовательские соединения работают через HTTPS.
- HTTP допускается только для веб-домена как вход для редиректа на HTTPS. Для api домена HTTP не допускается.
- TLS завершается на reverse proxy; внутренний HTTP между контейнерами допускается только в закрытой backend-сети.
- TLS 1.0/1.1 и слабые шифры запрещены.
- HSTS обязателен после проверки домена и сертификата.
- INPUT-validation на api-backend
- использовать только Параметризованные SQL-запросы
- обязательное Экранирование вывода
- настройка CORS только на разрешённые домены (`security.cors.allowed_origins` в `app_settings`, см. arch-04)
- настройка Secure Headers (CSP, X-Frame-Options и др.)
- Доступ к профилю, диалогам, сообщениям, файлам и документам ограничен текущим `user_id`.
- Все запросы, содержащие в себе ссылку на сущность, которая относится к конкретному пользователю (ИД продукта, услуги, чата, документа и тп), проверяются backend_api на соответствие тому пользователю, от которого пришел запрос.
- Все публичные id создаются в формате UUID.
- Сервисные API защищаются внутренней сетью Docker/VPC плюс service token (канонический контракт — [`arch-02-api-contracts.md`](arch-02-api-contracts.md), «Service tokens (internal API)»; значения переменных — [`arch-04-settings-and-content.md`](arch-04-settings-and-content.md)).
- Rate limits применяются минимум на двух уровнях: edge-лимиты в `nginx` и пользовательские лимиты в API с состоянием в Redis.
- Входящие сообщения пользователя: синхронный `POST /internal/safety/v1/messages/check` → при `203` api-backend синхронно поллит `task_id` до финального `200`/`403` (или timeout); без очереди анализа на api-backend.
- Файлы пользователя до финального `allow` только в S3-quarantine; в S3-data — после `200 allow`.
- У клиента **нет** постоянных S3 credentials. Загрузка — **presigned PUT** в S3-quarantine, выданный `api-backend`; скачивание — **presigned GET**. Байты файла **не** проксируются через `api-backend`.
- `message-safety` — read-only к S3-quarantine, без прав записи в бакеты.
- Вызовы `message-safety` и `bitrix-local-app` защищены timeout budget и circuit breaker (см. arch-04).
- Все изменяемые параметры, телефоны, лимиты, mime types и флаги хранятся в настройках ([`arch-04-settings-and-content.md`](arch-04-settings-and-content.md)).
- PII-данные не пишутся в логи в открытом виде.
- Документы и файлы чата должны иметь контроль доступа и аудит скачиваний.
## Backend-репозиторий и инфраструктура
### Состав backend-контура
Минимальный целевой real-SMS контур на одной VM: `nginx`, `api-backend`, `message-safety`, `keycloak`, `sms-service`/worker, `bitrix-sync`, `bitrix-local-app`, `redis`, `otel-collector`. До SMS rollout сервисы SMS могут отсутствовать, но Keycloak обязан оставаться в mock mode.
### Предлагаемая структура backend-репозитория
```text
backend/
docker-compose.yml # корневой compose: nginx + include сервисов + networks/volumes
.env.example
nginx/
docker-compose.yml
nginx.conf
conf.d/
certs/
.gitkeep
api-backend/
app/
docker-compose.yml
tests/
pyproject.toml
Dockerfile
message-safety/
app/
docker-compose.yml
tests/
pyproject.toml
Dockerfile
bitrix-local-app/
app/
docker-compose.yml
deploy/
tests/
pyproject.toml
Dockerfile
bitrix-sync/
app/
docker-compose.yml
tests/
pyproject.toml
Dockerfile
keycloak/
docker-compose.yml
realm/
themes/
providers/
sms-service/
app/
migrations/
openapi.yaml
Dockerfile
redis/
docker-compose.yml
observability/
docker-compose.yml # сервис otel-collector
otel-collector.yaml
```
Детальная внутренняя структура каждого сервиса (`app/`, модули, миграции) определяется в профильных спецификациях модулей (TBD).
### Compose-контур
Корневой `backend/docker-compose.yml` подключает сервисные compose-файлы через `include`.
Публикация портов наружу разрешена только `nginx` (`80/443`). Остальные сервисы доступны через Docker-сети и private VPC.