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

57 KiB
Raw Blame History

arch-01. Общая архитектура системы

Термины — в arch-00-glossary.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; права доступа — ниже и в «Принципы безопасности».
  • Мультиязычность в первом релизе не нужна, но тексты должны храниться по мнемоникам для будущих переводов.
  • Среда на первом этапе одна и проектируется как боевая.
  • Вложения чата MVP: только изображения и PDF — см. 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.
  • Популярный вопрос при выборе автоматически отправляется как сообщение; если пользователь не авторизован — сначала согласия и 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_idbitrix_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.

Контекстная схема

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_queuebitrix-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_idsession_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 /dialogsPOST .../messages с текстом вопроса.
  • dialog_id = external_chat_id для Open Lines (см. 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 на edgenginx; продуктовые 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, «Формат исходящего сообщения»).

Текстовое сообщение:

  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 пуст).

Общая ветка вердикта (оба типа):

  1. 403 deny: API удаляет quarantine (если был файл), выставляет safety_status=blocked, delivery_status=rejected, возвращает клиенту безопасную ошибку; в Bitrix24 ничего не уходит.
  2. 200 allow: API переносит файл в S3-data attachments (если был), сохраняет сообщение (safety_status=allowed, delivery_status=accepted) и фиксирует задачу доставки в Open Lines. После успешной отправки через bitrix-local-app статус становится delivery_status=delivered, API подтверждает клиенту финальный результат; Dialog.statuswaiting_for_company. Если Bitrix24/S3/dependency недоступны после allow, статус становится delivery_status=failed, клиент получает безопасную ошибку зависимости.
  3. 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).
  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, п. 9.

В MVP блок профиля «Документы» и API GET /api/v1/me/documents зарезервированы; список может быть пустым. Контракт endpoint — в arch-02-api-contracts.md.

Профиль клиента

Профиль должен быть блочным.

Блок "Личные данные":

  • ФИО;
  • гражданство;
  • номер телефона в РФ;
  • зарубежный номер телефона;
  • email.

Блок "Документы":

  • перечень документов, отправленных клиенту компанией (в MVP — пустой до реализации бэклога);
  • дата отправки;
  • наименование документа;
  • возможность скачать документ (после реализации доставки).

Редактирование данных профиля недоступно.

Sync профиля и master для PII

App DB — локальный кэш для UI. Двусторонний sync — bitrix-sync (имена полей — arch-00-glossary.md):

  • Auth-телефон: master — Keycloak (UserIdentity.phone_number); изменения могут инициировать contact.update через триггеры.
  • Поля профиля для UI: master — последнее успешно синхронизированное значение; основной входящий поток на MVP — правки сотрудником в Bitrix24 (webhook → App DB).
  • App → Bitrix: триггеры han_appsync_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, раздел «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, «Service tokens (internal API)»; значения переменных — 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).
  • 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-репозитория

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.