Grok version

This commit is contained in:
mi
2026-07-09 12:39:52 +03:00
parent ea8bb6181a
commit 8835677860
7 changed files with 353 additions and 155 deletions
+91 -65
View File
@@ -30,7 +30,7 @@ HAN Chat - приложение для мигрантов, где стартов
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`.
6. После успешной авторизации frontend с JWT вызывает **`POST /auth/bootstrap`** (в теле — принятые согласия): api-backend создаёт или находит локального пользователя по `keycloak_sub`, сохраняет согласия на `user_id`, создаёт или обновляет профиль; триггер App DB ставит задачу в `sync_queue` для `bitrix-sync`. Затем при необходимости — `POST /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 и отображается в чате приложения.
@@ -44,11 +44,11 @@ HAN Chat - приложение для мигрантов, где стартов
- 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`.
- 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` — отдельный DB-user на схему.
- Redis: rate limits, временные счетчики OTP и realtime/service coordination.
- 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.
@@ -95,12 +95,13 @@ flowchart LR
Client -->|HTTPS REST + Realtime| Nginx
Nginx -->|/auth| Keycloak
Nginx -->|/api + /realtime| API
Nginx -->|/api (REST + WS realtime)| API
Keycloak --> DB
API --> DB
API --> Redis
API -->|upload / move / delete| S3Q
API -->|promote delivered files| S3Data
Client -->|presigned PUT| S3Q
API -->|presign / HeadObject / move / delete| S3Q
API -->|promote chat files| S3Data
API -->|internal check message| Safety
Safety --> DB
Safety --> Redis
@@ -113,7 +114,7 @@ flowchart LR
Bitrix -->|ONIMCONNECTOR*| LocalApp
LocalApp -->|imconnector.send.messages/status| Bitrix
LocalApp -->|normalized inbox events| API
API -->|WebSocket/SSE or polling fallback| Client
API -->|WebSocket or polling fallback| Client
API --> Obs
Safety --> Obs
Sync --> Obs
@@ -131,12 +132,12 @@ flowchart LR
- гостевой режим до первого сообщения;
- показ pop-up с обязательными согласиями на обработку персональных данных и пользовательское соглашение, а также необязательным согласием на рекламные коммуникации;
- сбор данных устройства для передачи в backend;
- **управление аналитической UX-сессией** на клиенте: определение начала нового периода активности, хранение `ux_session_id` и `last_activity_at` **только в памяти**, отправка `session_start`, заголовок `X-Ux-Session-Id` во всех запросах;
- **управление аналитической 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;
- отображение входящих сообщений от оператора;
- загрузку файлов в чат через backend;
- загрузку файлов в чат: `init` → presigned PUT в S3 → `complete` (байты не через api-backend);
- работу с текстовыми мнемониками;
- отправку `traceparent`/correlation id в backend.
@@ -155,7 +156,7 @@ Frontend не должен:
- локальную регистрацию пользователя приложения: `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`**, версии документов);
- хранение согласий пользователя в App DB (**`user_id`**, **`ux_session_id`**, **`client_ip`**, версии документов) — только после JWT;
- профиль, структурированный блоками;
- API чата, истории, файлов и документов;
- realtime-доставку входящих сообщений клиенту;
@@ -163,11 +164,15 @@ Frontend не должен:
- прием нормализованных входящих событий Open Lines от Bitrix24 Local App;
- хранение истории диалогов;
- запись данных профиля в App DB (синхронизация с Bitrix24 — триггеры → `sync_queue``bitrix-sync`, без участия api-backend);
- загрузку файлов из чата в S3-quarantine до проверки;
- выдачу **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`: сохранение сообщения со статусом ожидания проверки, ответ клиенту «обрабатывается», опрос `GET /internal/safety/v1/messages/tasks/{task_id}` и доставка цепочки после финального `200` или cleanup после `403`;
- при `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 для сообщений, пользовательских и сервисных операций;
- аудит пользовательских действий;
- единые ошибки и валидацию входных данных.
@@ -218,12 +223,25 @@ Frontend не должен:
- OTP-only регистрацию и вход;
- OTP по номеру телефона; проверка кода — в Keycloak (заглушка `KEYCLOAK_OTP_MOCK_*` или SMS-провайдер, см. arch-04 и «Поток авторизации»);
- продуктовые лимиты OTP (`otp.phone.*` из `app_settings`) через authenticator/SPI; счётчики попыток — в зоне Keycloak (Redis DB Keycloak/SPI или in-memory Keycloak), **не** в `api-backend`;
- хранение учетных записей;
- выдачу и обновление токенов;
- настройку realm, clients, roles, policies.
- выдачу и обновление токенов (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-провайдер | Keycloak → SMS (post-MVP) | Доставка OTP; на MVP — mock code из `.env` |
Confidential **backend client** Keycloak (client credentials) в MVP **не обязателен**: S2S между нашими сервисами идёт по service tokens, не через Keycloak. Client можно завести заранее в realm как optional для будущих admin/ops сценариев.
### Nginx Reverse Proxy
Отвечает за:
@@ -231,13 +249,13 @@ Frontend не должен:
- прием внешнего HTTPS-трафика;
- TLS termination;
- редирект HTTP на HTTPS (на веб-домене; для выделенного API-домена HTTP не допускается — см. «Принципы безопасности»);
- маршрутизацию `/api/*` и `/realtime/*` в api-backend;
- маршрутизацию `/api/*` в api-backend (включая `WS /api/v1/realtime`);
- маршрутизацию `/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`;
- передачу `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 и скрытие технологических заголовков;
@@ -266,48 +284,49 @@ Frontend не должен:
- сохранение сообщений, истории диалогов (CRM sync — зона `bitrix-sync`, не api-backend);
- доставку в Bitrix24 Open Lines и realtime клиенту;
- проверку JWT, согласий, edge rate limits;
- polling `task_id` на стороне клиента — только api-backend (фоновый worker или internal loop).
- polling `task_id` на стороне клиента запрещён — только api-backend, и только **внутри** обработки `POST .../messages` (sync wait до финального вердикта).
api-backend не решает, sync или async нужна проверка: это определяет Message Safety Service по результатам фазы текста/ссылок и кэша файлов.
api-backend не решает, sync или async нужна проверка внутри Message Safety: это определяет Message Safety Service. Но для клиента `POST .../messages` всегда завершается финальным allow/deny (или ошибкой timeout/зависимости).
## Гостевая сессия (до JWT)
## Гостевой режим (до JWT)
До OTP frontend работает в гостевом режиме с локально сгенерированным **`guest_session_id`** (UUID v4):
До OTP frontend работает **локально** без записи согласий и UX-сессии в App DB:
- создаётся при первом запуске приложения, хранится в 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.
- UI главного экрана, популярные вопросы и публичный контент — через `GET /api/v1/public/*` (без JWT);
- pop-up согласий показывается **до** OTP, но факт принятия хранится **только на клиенте** до получения tokens;
- **`POST /consents`**, **`POST /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 и **не** механизм авторизации.
**UX-сессия** — период непрерывной активности **авторизованного** пользователя в приложении для аналитики и сквозной трассировки. Это **не** сессия Keycloak, **не** refresh/access token и **не** механизм авторизации.
### Роли компонентов
**Frontend** (источник истины по правилам сессии):
- хранит `ux_session_id` и `last_activity_at` **только в памяти** (не в localStorage/secure storage);
- при новой сессии вызывает `POST /api/v1/analytics/session-start` и сохраняет полученный `ux_session_id`;
- вызывает `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` → при превышении — новая сессия;
- передаёт **`X-Ux-Session-Id`** во **всех** запросах к backend (public и JWT).
- при resume проверяет `(now - last_activity_at) > idle_timeout` → при превышении — новая сессия (снова с JWT);
- передаёт **`X-Ux-Session-Id`** во **всех** JWT-запросах к backend, пока сессия активна.
**api-backend**:
1. принимает `session_start`, создаёт запись **`UxSession`**, возвращает `ux_session_id`;
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` — это не auth.
4. отсутствие `ux_session_id` **не** блокирует API (кроме endpoint, где id обязателен по контракту) — это не auth, но сам `session-start` без JWT недоступен.
`request_id` — один HTTP-запрос; `ux_session_id` — период UX-активности для аналитики и корреляции логов.
## Поток возврата пользователя (без OTP)
1. Клиент открывает приложение (UX-сессия определяется по правилам выше, независимо от auth).
1. Клиент открывает приложение. Пока нет JWT — гостевой UI; `session-start` не вызывается.
2. Frontend проверяет наличие refresh token в secure storage.
3. Если refresh token **действителен** — frontend запрашивает новый access token у Keycloak (Refresh Token Grant), **OTP не показывается**.
3. Если refresh token **действителен** — frontend запрашивает новый access token у Keycloak (Refresh Token Grant), **OTP не показывается**, затем при необходимости `POST /analytics/session-start`.
4. Frontend работает как авторизованный пользователь (история, профиль, чат).
5. Если refresh token **отсутствует или истёк** — клиент остаётся в гостевом режиме до сценария, требующего auth; при первом сообщении — «Поток авторизации» с OTP.
@@ -343,35 +362,39 @@ api-backend не решает, sync или async нужна проверка: э
## Создание диалога (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` и использует его далее.
- 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. Клиент находится в гостевом режиме (`guest_session_id` уже создан).
1. Клиент в гостевом режиме (только UI + `GET /api/v1/public/*`).
2. Клиент инициирует отправку сообщения (ручной ввод или популярный вопрос).
3. Frontend показывает pop-up с тремя согласиями.
3. Frontend показывает pop-up с тремя согласиями; факт принятия хранится **локально** до OTP.
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**:
7. Frontend запрашивает публичные настройки и показывает форму ввода номера телефона (единственный канал MVP).
8. Keycloak запускает OTP-flow по телефону: клиент вводит номер, инициируется «отправка» OTP (при заглушке SMS фактически не уходит — см. arch-04).
9. Лимиты OTP на **edge**`nginx` (`NGINX_RATE_LIMIT_AUTH`); продуктовые лимиты `otp.phone.*` из `app_settings` применяются на стороне **Keycloak authenticator / SPI** (или обёртки OTP), не в `api-backend`. До интеграции SMS (mock OTP) достаточно edge + mock code.
10. Клиент вводит OTP и отправляет его в Keycloak.
11. **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 создаёт диалог и отправляет отложенное сообщение (см. «Создание диалога» и поток чата).
- при неверном коде 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
@@ -386,20 +409,21 @@ api-backend не решает, sync или async нужна проверка: э
**Файловое сообщение:**
1. Frontend инициализирует **одно** вложение (`POST .../attachments/init`), загружает файл; api-backend сохраняет его в **S3-quarantine**.
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 (если был файл), возвращает клиенту безопасную ошибку; в 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.
5. **`403 deny`**: API удаляет quarantine (если был файл), выставляет `safety_status=blocked`, `delivery_status=rejected`, возвращает клиенту безопасную ошибку; в Bitrix24 ничего не уходит.
6. **`200 allow`**: API переносит файл в S3-data attachments (если был), сохраняет сообщение (`safety_status=allowed`, `delivery_status=delivered`), отправляет в Bitrix24, подтверждает клиенту; `Dialog.status``waiting_for_company`.
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` получает **только финальный** результат (или ошибку инфраструктуры), не промежуточное «обрабатывается».
## Поток работы с чатом: Битрикс24 -> клиент
@@ -408,9 +432,9 @@ api-backend не решает, sync или async нужна проверка: э
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`.
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`.
8. api-backend публикует событие для frontend через WebSocket/SSE. Если realtime недоступен, frontend получает сообщение через polling `GET /api/v1/dialogs/{dialog_id}/messages?after=...`.
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`.
@@ -453,7 +477,7 @@ App DB — **локальный кэш** для UI. Двусторонний syn
## Аудит скачиваний
При выдаче presigned URL на скачивание (`GET .../download-url`, вложения чата) api-backend пишет audit-событие в App DB:
При выдаче 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:
| Поле | Значение |
|---|---|
@@ -471,14 +495,15 @@ App DB — **локальный кэш** для UI. Двусторонний syn
Детальный контракт — [`arch-02-api-contracts.md`](arch-02-api-contracts.md), раздел «Realtime».
- Transport: WebSocket `WS /api/v1/realtime` (JWT).
- 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.
- Гостевые API доступны только для публичных настроек и стартового контента.
- Без 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-сети.
@@ -487,17 +512,18 @@ App DB — **локальный кэш** для UI. Двусторонний syn
- INPUT-validation на api-backend
- использовать только Параметризованные SQL-запросы
- обязательное Экранирование вывода
- настройка CORS только на разрешенные домены (указать в .env)
- настройка 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-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` до финального вердикта.
- Входящие сообщения пользователя: синхронный `POST /internal/safety/v1/messages/check`при `203` api-backend синхронно поллит `task_id` до финального `200`/`403` (или timeout); без очереди анализа на api-backend.
- Файлы пользователя до финального `allow` только в S3-quarantine; в S3-data — после `200 allow`.
- Клиент **не пишет** напрямую в S3; загрузка только через `api-backend`.
- У клиента **нет** постоянных 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-данные не пишутся в логи в открытом виде.
- Документы и файлы чата должны иметь контроль доступа и аудит скачиваний.