Files
han-app/architectory/arch-01-system-architecture.md
T
2026-07-09 11:03:44 +03:00

567 lines
47 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 на старте: **заглушка** — пользователь вводит фиксированный код из `.env` (`KEYCLOAK_OTP_MOCK_CODE`); SMS не отправляется. Интеграция с SMS-провайдерами — в бэклоге (см. [`!Backlog.md`](../../HAN_chat/!Backlog.md)).
- Популярный вопрос при выборе **автоматически отправляется как сообщение**; если пользователь не авторизован — сначала согласия и OTP, затем отправка.
- Перечень таблиц и миграций App DB проектирует модуль `database` (и владельцы схем других сервисов); arch фиксирует только **разделение схем** PostgreSQL и контракты между сервисами.
## Пользовательские сценарии
1. Клиент открывает мобильное или web-приложение и видит главный экран с приветствием, популярными вопросами и полем ввода.
2. Frontend определяет, нужна ли **новая UX-сессия**, и при необходимости отправляет событие **`session_start`** (см. «Аналитическая UX-сессия»). Клиент может изучить сервис без авторизации.
3. Если у клиента сохранён **действующий refresh token**, frontend выполняет silent refresh **без OTP** (см. «Поток возврата пользователя»).
4. Клиент нажимает популярный вопрос — frontend подставляет текст вопроса и **инициирует отправку сообщения** (тот же поток, что ручной ввод). Либо клиент вводит свой текст и отправляет.
5. Если клиент не авторизован, перед отправкой первого сообщения frontend показывает pop-up с согласиями и запускает OTP (см. «Поток авторизации»).
6. После успешной авторизации api-backend создаёт или находит локального пользователя по `keycloak_sub`, связывает ранее сохранённые согласия с `guest_session_id`, создаёт или обновляет профиль; триггер App DB ставит задачу в `sync_queue` для `bitrix-sync`.
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 авторизация по номеру телефона.
- 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`.
- 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` — отдельный DB-user на схему.
- Redis: rate limits, временные счетчики OTP и realtime/service coordination.
- 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`, `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 |
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]
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 -->|/api + /realtime| API
Keycloak --> DB
API --> DB
API --> Redis
API -->|upload / move / delete| S3Q
API -->|promote delivered 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/SSE or polling fallback| Client
API --> Obs
Safety --> Obs
Sync --> Obs
LocalApp --> Obs
LocalApp --> DB
```
## Архитектурные границы
### Frontend
Отвечает за:
- стартовый экран с приветствием, популярными вопросами, полем ввода, историей и профилем;
- гостевой режим до первого сообщения;
- показ pop-up с обязательными согласиями на обработку персональных данных и пользовательское соглашение, а также необязательным согласием на рекламные коммуникации;
- сбор данных устройства для передачи в backend;
- **управление аналитической UX-сессией** на клиенте: определение начала нового периода активности, хранение `ux_session_id` и `last_activity_at` **только в памяти**, отправка `session_start`, заголовок `X-Ux-Session-Id` во всех запросах;
- хранение access token и refresh token в безопасном хранилище после авторизации;
- **жизненный цикл access token**: проактивное обновление по расписанию (до истечения `exp`) и обработка **`401`** от `api-backend` (см. «Обновление access token (frontend)»);
- при открытии приложения: проверку refresh token → silent refresh через Keycloak **или** OTP-flow при истечении refresh token;
- отображение входящих сообщений от оператора;
- загрузку файлов в чат через 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 (`guest_session_id`, **`ux_session_id`**, **`client_ip`**, версии документов);
- профиль, структурированный блоками;
- API чата, истории, файлов и документов;
- realtime-доставку входящих сообщений клиенту;
- отправку сообщений клиента в Open Lines через Bitrix24 Local App;
- прием нормализованных входящих событий Open Lines от Bitrix24 Local App;
- хранение истории диалогов;
- запись данных профиля в App DB (синхронизация с Bitrix24 — триггеры → `sync_queue``bitrix-sync`, без участия api-backend);
- загрузку файлов из чата в S3-quarantine до проверки;
- синхронный вызов 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`: сохранение сообщения со статусом ожидания проверки, ответ клиенту «обрабатывается», опрос `GET /internal/safety/v1/messages/tasks/{task_id}` и доставка цепочки после финального `200` или cleanup после `403`;
- 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 по номеру телефона; проверка кода — в Keycloak (заглушка `KEYCLOAK_OTP_MOCK_*` или SMS-провайдер, см. arch-04 и «Поток авторизации»);
- хранение учетных записей;
- выдачу и обновление токенов;
- настройку realm, clients, roles, policies.
Парольная авторизация, magic link и социальные логины не входят в MVP.
### Nginx Reverse Proxy
Отвечает за:
- прием внешнего HTTPS-трафика;
- TLS termination;
- редирект HTTP на HTTPS (на веб-домене; для выделенного API-домена HTTP не допускается — см. «Принципы безопасности»);
- маршрутизацию `/api/*` и `/realtime/*` в api-backend;
- маршрутизацию `/auth/*` или выделенного auth-домена в Keycloak;
- маршрутизацию публичных `/bitrix/*` endpoint в `bitrix-local-app`;
- маршрутизацию `/bitrix/sync/*` webhook endpoint в `bitrix-sync`;
- защиту internal endpoint `bitrix-local-app` через private network или `nginx allowlist`;
- отсутствие публичной маршрутизации к `message-safety` — сервис доступен только из внутренней Docker-сети;
- передачу `X-Forwarded-For`, `X-Forwarded-Proto`, `X-Forwarded-Host`, `X-Request-ID`;
- базовые лимиты размера запроса и 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 (фоновый worker или internal loop).
api-backend не решает, sync или async нужна проверка: это определяет Message Safety Service по результатам фазы текста/ссылок и кэша файлов.
## Гостевая сессия (до JWT)
До OTP frontend работает в гостевом режиме с локально сгенерированным **`guest_session_id`** (UUID v4):
- создаётся при первом запуске приложения, хранится в secure storage устройства;
- передаётся в `POST /api/v1/consents` вместе с согласиями и device metadata;
- api-backend сохраняет согласия с привязкой к `guest_session_id` (TTL записи — 24 ч);
- после успешного OTP api-backend **связывает** записи согласий и device session с `UserIdentity` по `keycloak_sub`;
- `guest_session_id` не используется для доступа к защищённым ресурсам после выдачи JWT.
## Аналитическая 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` и сохраняет полученный `ux_session_id`;
- обновляет `last_activity_at` при пользовательской активности и при возврате из фона;
- при resume проверяет `(now - last_activity_at) > idle_timeout` → при превышении — новая сессия;
- передаёт **`X-Ux-Session-Id`** во **всех** запросах к backend (public и JWT).
**api-backend**:
1. принимает `session_start`, создаёт запись **`UxSession`**, возвращает `ux_session_id`;
2. пишет analytics/audit-событие `session_start` (без PII);
3. включает `ux_session_id` из заголовка в JSON-логи (если передан);
4. **не** блокирует запросы при отсутствии или неизвестном `ux_session_id` — это не auth.
`request_id` — один HTTP-запрос; `ux_session_id` — период UX-активности для аналитики и корреляции логов.
## Поток возврата пользователя (без OTP)
1. Клиент открывает приложение (UX-сессия определяется по правилам выше, независимо от auth).
2. Frontend проверяет наличие refresh token в secure storage.
3. Если refresh token **действителен** — frontend запрашивает новый access token у Keycloak (Refresh Token Grant), **OTP не показывается**.
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)
- Диалог создаётся **лениво** при первой отправке сообщения авторизованным клиентом.
- 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`.
## Поток авторизации (OTP)
Срабатывает, когда клиент **ещё не имеет действующего refresh token** (первый вход) или refresh token **истёк**. Если refresh token валиден — см. «Поток возврата пользователя».
1. Клиент находится в гостевом режиме (`guest_session_id` уже создан).
2. Клиент инициирует отправку сообщения (ручной ввод или популярный вопрос).
3. Frontend показывает pop-up с тремя согласиями.
4. Клиент обязан принять согласие на обработку персональных данных и пользовательское соглашение.
5. Клиент может опционально согласиться на рекламные коммуникации.
6. Если обязательные согласия не даны, отправка блокируется.
7. Frontend вызывает `POST /api/v1/consents` с `guest_session_id`, версиями документов, device metadata, IP/user agent (через backend).
8. Frontend запрашивает публичные настройки и показывает форму ввода номера телефона (единственный канал MVP).
9. Keycloak запускает OTP-flow по телефону: клиент вводит номер, инициируется «отправка» OTP (при заглушке SMS фактически не уходит — см. arch-04).
10. Лимиты OTP проверяются по **`app_settings`** (`otp.phone.*`).
11. Клиент вводит OTP и отправляет его в Keycloak.
12. **Keycloak проверяет корректность введённого OTP**:
- при **`KEYCLOAK_OTP_MOCK_ENABLED=true`** (MVP и любой режим с включённой заглушкой): введённое значение должно **совпадать** с `KEYCLOAK_OTP_MOCK_CODE` из `.env`;
- при **`KEYCLOAK_OTP_MOCK_ENABLED=false`** (после интеграции с SMS-провайдером, см. бэклог): введённое значение должно **совпадать** с одноразовым OTP, сгенерированным Keycloak и отправленным провайдером на телефон клиента (с учётом TTL и лимита попыток).
- при неверном коде Keycloak возвращает ошибку; frontend не получает tokens, шаг 13 не выполняется.
13. При успешной проверке frontend получает tokens через OIDC Authorization Code Flow with PKCE.
14. Frontend вызывает **`POST /api/v1/auth/bootstrap`** с JWT и `guest_session_id` (см. arch-02).
15. api-backend выполняет `find-or-create` пользователя, связывает согласия с `guest_session_id`, при необходимости привязывает `user_id` к текущей **`UxSession`** по `ux_session_id`.
16. Триггер App DB ставит задачу `contact.map_or_create` в `sync_queue`; `bitrix-sync` асинхронно находит или создает Contact в Битрикс24. Авторизация не должна синхронно зависеть от ответа Битрикс24 CRM.
17. Frontend создаёт диалог и отправляет отложенное сообщение (см. «Создание диалога» и поток чата).
## Поток работы с чатом: клиент -> Битрикс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`), загружает файл; api-backend сохраняет его в **S3-quarantine**.
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 (если был файл), возвращает клиенту безопасную ошибку; в Bitrix24 ничего не уходит.
6. **`200 allow`**: API переносит файл в S3-data (если был), сохраняет сообщение, отправляет в Bitrix24, подтверждает клиенту (realtime/polling).
7. **`203 pending` + `task_id`**: api-backend сохраняет сообщение со статусом ожидания проверки, отвечает клиенту, что сообщение обрабатывается; quarantine не трогает.
8. Фоновый процесс API опрашивает `GET /internal/safety/v1/messages/tasks/{task_id}`:
- финальный **`200 allow`** → S3-data, Bitrix24, статус «доставлено», realtime клиенту;
- финальный **`403 deny`** → удаление quarantine, статус «отклонено», уведомление клиенту;
- **`203 pending`** → повтор опроса с backoff.
## Поток работы с чатом: Битрикс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` включен.
5. api-backend находит локальный диалог по `external_chat_id` (= `dialog_id`, см. [`arch-00-glossary.md`](arch-00-glossary.md)).
6. api-backend сохраняет входящее сообщение в App DB (`sender_type=company`), а файл — в Selectel S3 (documents) с metadata в App DB. По факту сообщения API обновляет `Dialog.status`: входящее от оператора → `waiting_for_client`, исходящее от клиента → `waiting_for_company`.
7. `bitrix-local-app` подтверждает доставку в Bitrix24 через `imconnector.send.status.delivery`.
8. api-backend публикует событие для frontend через WebSocket/SSE. Если 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 .../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).
- Fallback: polling `GET /api/v1/dialogs/{dialog_id}/messages?after=...`.
- События: новое сообщение, смена статуса сообщения/диалога.
## Принципы безопасности
- Все защищенные пользовательские API требуют валидный JWT.
- Гостевые API доступны только для публичных настроек и стартового контента.
- Все внешние пользовательские соединения работают через HTTPS.
- HTTP допускается только для веб-домена как вход для редиректа на HTTPS. Для api домена HTTP не допускается.
- TLS завершается на reverse proxy; внутренний HTTP между контейнерами допускается только в закрытой backend-сети.
- TLS 1.0/1.1 и слабые шифры запрещены.
- HSTS обязателен после проверки домена и сертификата.
- INPUT-validation на api-backend
- использовать только Параметризованные SQL-запросы
- обязательное Экранирование вывода
- настройка CORS только на разрешенные домены (указать в .env)
- настройка Secure Headers (CSP, X-Frame-Options и др.)
- Доступ к профилю, диалогам, сообщениям, файлам и документам ограничен текущим `user_id`.
- Все запросы, содержащие в себе ссылку на сущность, которая относится к конкретному пользователю (ИД продукта, услуги, чата, документа и тп), проверяются backend_api на соответствие тому пользователю, от которого пришел запрос.
- Все публичные id создаются в формате UUID.
- Сервисные API защищаются внутренней сетью Docker/VPC плюс service token (перечень переменных — [`arch-04-settings-and-content.md`](arch-04-settings-and-content.md), «Service tokens (internal API)»).
- Rate limits применяются минимум на двух уровнях: edge-лимиты в `nginx` и пользовательские лимиты в API с состоянием в Redis.
- Входящие сообщения пользователя: синхронный `POST /internal/safety/v1/messages/check``200` | `403` | `203`; при `203` API опрашивает `task_id` до финального вердикта.
- Файлы пользователя до финального `allow` только в S3-quarantine; в S3-data — после `200 allow`.
- Клиент **не пишет** напрямую в S3; загрузка только через `api-backend`.
- `message-safety` — read-only к S3-quarantine, без прав записи в бакеты.
- Все изменяемые параметры, телефоны, лимиты, mime types и флаги хранятся в настройках ([`arch-04-settings-and-content.md`](arch-04-settings-and-content.md)).
- PII-данные не пишутся в логи в открытом виде.
- Документы и файлы чата должны иметь контроль доступа и аудит скачиваний.
## Backend-репозиторий и инфраструктура
### Состав backend-контура
Минимальный production-like контур на одной VM: `nginx`, `api-backend`, `message-safety`, `keycloak`, `bitrix-sync`, `bitrix-local-app`, `redis`, `otel-collector`. Managed PostgreSQL и Selectel S3 находятся вне Docker Compose.
### Предлагаемая структура 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/
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.