commit ea8bb6181a2db94a97bfe00c1b75d3f2add6af2b Author: mi Date: Thu Jul 9 11:03:44 2026 +0300 Initial commit diff --git a/HAN_chat_specification.rar b/HAN_chat_specification.rar new file mode 100644 index 0000000..e7f4c63 Binary files /dev/null and b/HAN_chat_specification.rar differ diff --git a/architectory/README.md b/architectory/README.md new file mode 100644 index 0000000..fc86e0c --- /dev/null +++ b/architectory/README.md @@ -0,0 +1,56 @@ +# Архитектура HAN Chat + +Канонический набор архитектурных документов проекта. Описывает границы системы, интеграции, контракты API, инфраструктуру, настройки и процесс разработки. + +Детальная **схема таблиц App DB**, **правила проверки файлов** (`file_rules` и др.) и **OpenAPI-файлы** — зона ответственности соответствующих модулей; архитектура задаёт только границы, контракты и общие правила. + +## Состав документов + +| Документ | Содержание | +|---|---| +| [`arch-00-glossary.md`](arch-00-glossary.md) | Канонические имена: сущности, поля, id, enum, бакеты S3, env | +| [`arch-01-system-architecture.md`](arch-01-system-architecture.md) | Общая архитектура: компоненты, сценарии, потоки данных, безопасность | +| [`arch-02-api-contracts.md`](arch-02-api-contracts.md) | Реестр API-контрактов, realtime, гостевая сессия, OpenAPI, аудит | +| [`arch-03-docker-compose-blueprint.md`](arch-03-docker-compose-blueprint.md) | Требования к Docker Compose, nginx, сетям, TLS и rate limits | +| [`arch-04-settings-and-content.md`](arch-04-settings-and-content.md) | `.env` (infra), таблица `app_settings`, service tokens, типы файлов | +| [`arch-05-agent-development-process.md`](arch-05-agent-development-process.md) | Правила разработки модулей отдельными агентами | + +## Как читать + +1. Начните с **arch-01** — общая картина и зафиксированные решения MVP. +2. При работе с API — **arch-02**; при деплое — **arch-03**; при настройках — **arch-04**. +3. Спорные **имена** полей, id, enum, бакетов — **arch-00** (не правила и не лимиты). +4. Перед разработкой модуля — **arch-05** и релевантные разделы arch-01/arch-02. + +## Приоритет документов + +При конфликте требований: + +1. **arch-00** — только **имена** (поля, id, enum, бакеты, env); не правила и не лимиты. +2. **arch-01** — границы сервисов, сценарии, sync, безопасность. +3. **arch-02** — HTTP-контракты и направление вызовов. +4. **arch-03** — инфраструктура и nginx. +5. **arch-04** — env, `app_settings`, публичные DTO. +6. **arch-05** — процесс разработки. + +Профильные спецификации модулей уточняют реализацию внутри этих границ. Если границы не позволяют эффективно реализовать модуль, то агент, разрабатывающий модуль, может предложить внести изменения в архитектуру. + +## Разрешение конфликтов + +- Имена полей, бакетов, статусов → **arch-00**, затем синхронизация arch-*. +- Endpoint или auth → **arch-02**, при необходимости arch-01/arch-03. +- Новая интеграция → сначала **arch-02**. +- Compose, nginx, TLS → **arch-03**. + +## В бэклоге (не MVP) + +| Тема | Где зафиксировано | +|---|---| +| Доставка документов компании из Bitrix24 в приложение (`bitrix-sync` → `api-backend`, уведомление клиента) | [`!Backlog.md`](../../HAN_chat/!Backlog.md), п. 9; arch-01 — заглушка UI «Документы» | +| Интеграция с SMS-провайдерами (отправка OTP, отключение `KEYCLOAK_OTP_MOCK_*`) | [`!Backlog.md`](../../HAN_chat/!Backlog.md), п. 10 | + +## Обновление документации + +- Изменение MVP → arch-01 + arch-02 (+ arch-03/arch-04 при необходимости). +- Новый env или ключ `app_settings` → arch-04. +- Новый термин → arch-00, затем поиск по arch-*. diff --git a/architectory/arch-00-glossary.md b/architectory/arch-00-glossary.md new file mode 100644 index 0000000..f80d331 --- /dev/null +++ b/architectory/arch-00-glossary.md @@ -0,0 +1,123 @@ +# arch-00. Глоссарий и единый словарь терминов + +## Назначение + +Канонические **имена** сущностей, полей, идентификаторов, enum-значений, бакетов S3, env-переменных и терминов проекта. + +При расхождении имён приоритет у настоящего словаря. + +## Bucket Selectel S3 + +| Логическое имя | env-переменная | Пример физического бакета | +|---|---|---| +| **S3-quarantine** | `SELECTEL_S3_BUCKET_QUARANTINE` | `han-chat-quarantine` | +| **S3-data** (attachments) | `SELECTEL_S3_BUCKET_ATTACHMENTS` | `han-chat-attachments` | +| **S3-data** (documents) | `SELECTEL_S3_BUCKET_DOCUMENTS` | `han-chat-documents` | + +В тексте: **S3-quarantine** — временное хранилище до вердикта Message Safety; **S3-data** — проверенные файлы (attachments и documents — два физических бакета). + +## Сущности App DB (основные) + +| Имя | Схема | Назначение (кратко) | +|---|---|---| +| `UserIdentity` | `han_app` | Локальный пользователь, связь с Keycloak | +| `UxSession` | `han_app` | Аналитическая UX-сессия (период активности пользователя) | +| `UserConsent` | `han_app` | Запись о принятии согласий (до OTP) | +| `ClientProfile` | `han_app` | Кэш профиля для UI | +| `Dialog` | `han_app` | Диалог клиента с Open Lines | +| `Message` | `han_app` | Сообщение в диалоге | +| `MessageAttachment` | `han_app` | Вложение к сообщению | +| `sync_queue` | `han_app` | Очередь sync App → Bitrix24 | +| `entity_external_mapping` | `han_app` | Маппинг App entity ↔ Bitrix entity | +| `app_settings` | `han_app` | Бизнес-настройки | +| `text_resources` | `han_app` | Тексты UI по мнемоникам | +| `popular_questions` | `han_app` | Популярные вопросы главного экрана | +| `dialog_sessions` | `bitrix_local` | Маппинг чата Open Lines | + +## Идентификаторы + +| Имя | Где используется | +|---|---| +| `dialog_id` | UUID диалога в приложении; **равен** `external_chat_id` в Open Lines | +| `external_chat_id` | Идентификатор чата для `bitrix-local-app` / `imconnector` | +| `keycloak_sub` | Subject JWT Keycloak; ключ `UserIdentity` | +| `guest_session_id` | UUID гостевой сессии до OTP | +| `ux_session_id` | UUID **аналитической UX-сессии**; заголовок `X-Ux-Session-Id` | +| `bitrix_contact_id` | ID Contact в Bitrix24 CRM | +| `bitrix_chat_id` | ID чата Open Lines в Bitrix24 | +| `session_id` | ID сессии Open Lines (поле `dialog_sessions`; не путать с `ux_session_id`) | +| `task_id` | ID async-проверки Message Safety | +| `request_id` | Корреляция HTTP-запроса (заголовок `X-Request-ID`) | + +Публичные id сущностей — **UUID**. + + +## `UxSession` (аналитическая UX-сессия) + +### Определение + +**`UxSession`** — период **непрерывной активности** пользователя в приложении (web / iOS / Android) для **аналитики** и **сквозной корреляции** логов и событий. + +- Идентификатор периода — **`ux_session_id`** (UUID). +- Начало периода фиксируется событием **`session_start`** (**ровно один раз** на период). +- Запись создаётся в App DB при `POST /api/v1/analytics/session-start` (см. arch-02). +- Frontend передаёт **`X-Ux-Session-Id`** во всех запросах к backend, пока сессия активна. + +**`UxSession` не является механизмом авторизации.** Отсутствие или неизвестный `ux_session_id` **не** блокирует API (кроме endpoint, где id обязателен по контракту, напр. `POST /api/v1/consents`). + +### Когда начинается **новая** `UxSession` +Новый **`ux_session_id`** + событие **`session_start`** — **только** если: +1. **`first_launch`** — приложение открыто, в памяти **нет** `ux_session_id`. +2. **`cold_start`** — после kill app или закрытия вкладки браузера (память очищена). +3. **`idle_timeout`** — возврат спустя **более N минут** (`ux.session.idle_timeout_minutes` в `app_settings`, default **30**). + +### Когда **та же** `UxSession` продолжается +- возврат из фона **в пределах** idle timeout (напр. через 5 минут — **без** нового `session_start`); +- успешный OTP или refresh access token; +- навигация между экранами внутри приложения. + +## `Message` — enum и поля + +| Имя | Допустимые значения | +|---|---| +| `Message.sender_type` | `client`, `company` | +| `Message.safety_status` | `pending`, `allowed`, `blocked` (`needs_review` — зарезервирован, MVP не используется) | +| `Message.text` | текст сообщения; пустая строка для файлового сообщения | +| `content_kind` (логическое) | `text`, `file` — тип исходящего сообщения клиента (MVP) | + +Семантика `allow` / `deny` / `pending` в `message-safety` и HTTP-коды — [`arch-02-api-contracts.md`](arch-02-api-contracts.md). + +## `MessageAttachment.scan_status` + +| Значение | Смысл | +|---|---| +| `pending` | Файл в S3-quarantine, проверка не завершена | +| `clean` | Проверка завершена, allow | +| `infected` | Проверка завершена, deny | +| `failed` | Ошибка инфраструктуры проверки | + +## Мнемоники internal API + +Префикс: **`/internal/{service_mnemonic}/v1/`**. Health: **`/health/*`**. + +| `{service_mnemonic}` | Сервис | +|---|---| +| `safety` | `message-safety` | +| `openlines` | `bitrix-local-app`, приёмник inbox на `api-backend` | +| `sync` | `bitrix-sync` | + +## Bitrix24 Open Lines + +| Имя | Значение | +|---|---| +| `BITRIX_CONNECTOR_ID` / connector | `han_mobile_app` | +| `BITRIX_OPEN_LINE_ID` | `8` | +| Канонический URL коннектора | `https://han0107.bitrix24.ru/contact_center/connector/?ID=han_mobile_app&LINE=8` | + +## Термины чата + +| Термин | `Message.sender_type` / направление | +|---|---| +| клиент | `client`; исходящее сообщение | +| оператор | `company`; входящее сообщение | +| сообщение пользователя | исходящее; проверяет Message Safety | diff --git a/architectory/arch-01-system-architecture.md b/architectory/arch-01-system-architecture.md new file mode 100644 index 0000000..9812124 --- /dev/null +++ b/architectory/arch-01-system-architecture.md @@ -0,0 +1,566 @@ +# 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. diff --git a/architectory/arch-02-api-contracts.md b/architectory/arch-02-api-contracts.md new file mode 100644 index 0000000..078dbea --- /dev/null +++ b/architectory/arch-02-api-contracts.md @@ -0,0 +1,395 @@ +# arch-02. API-контракты и связность взаимодействий + +> Термины — в [`arch-00-glossary.md`](arch-00-glossary.md). Общая схема — в [`arch-01-system-architecture.md`](arch-01-system-architecture.md). + +## Назначение + +Этот документ — канонический реестр API-контрактов между frontend, backend-сервисами и внешними системами. Его цель — контролировать связность: если сервис описан как участник сценария, здесь должен быть указан контракт, направление вызова, владелец и потребитель. Когда появятся профильные спецификации модулей, они могут дублировать здесь зафиксированные контракты для удобства разработки. + +## Правила связности + +- Любой новый endpoint, webhook, worker-contract или внешний вызов сначала добавляется в этот файл; при появлении профильного документа модуля-владельца — дублируется там для детализации реализации. +- Публичные пользовательские API находятся под `/api/v1`; internal API не публикуются наружу через `nginx`. +- Internal HTTP API между backend-сервисами используют единую маску: **`/internal/{service_mnemonic}/v1/{resource}`**, где `{service_mnemonic}` — короткое имя владельца endpoint (см. [`arch-00-glossary.md`](arch-00-glossary.md), «Мнемоники internal API»). Health-check остаётся на `/health/*`. +- OpenAPI 3.1 обязателен для HTTP-контрактов `api-backend`, `message-safety`, `bitrix-sync` и `bitrix-local-app` — файлы `{service}/openapi.yaml` в репозитории сервиса (см. раздел «OpenAPI»); для Bitrix24 REST фиксируются используемые методы и payload-мэппинг. +- Все service-to-service вызовы передают `X-Request-ID` и по возможности W3C `traceparent`. +- Frontend передаёт **`X-Ux-Session-Id`** во всех запросах к `api-backend`, когда UX-сессия активна (рекомендуется для аналитики и логов; **не** является auth). +- Все internal API защищаются service token и закрытой Docker/VPC-сетью. + +## Service tokens (internal API) + +Все internal endpoint (`/internal/*`) доступны **только** из Docker/VPC-сети и требуют service token. Endpoint не публикуются через `nginx` (исключение — ops внутри VPC). + +| Переменная | Кто проверяет | Кто передаёт | Endpoint | Заголовок | +|---|---|---|---|---| +| `MESSAGE_SAFETY_SERVICE_TOKEN` | `message-safety` | `api-backend` | `POST/GET /internal/safety/v1/*` | `X-Service-Token` | +| `BITRIX_INTERNAL_API_TOKEN` | `bitrix-local-app` | `api-backend` | `POST/GET /internal/openlines/v1/*` | `Authorization: Bearer` | +| `BITRIX_LOCAL_APP_INTERNAL_TOKEN` | — | `api-backend` (исходящий) | то же | `Authorization: Bearer` | +| `BITRIX_API_INBOX_TOKEN` | `api-backend` | `bitrix-local-app` | `POST /internal/openlines/v1/inbox` | `Authorization: Bearer` | +| `BITRIX_API_FORWARD_TOKEN` | — | `bitrix-local-app` (исходящий) | то же | `Authorization: Bearer` | +| `BITRIX_SYNC_SERVICE_TOKEN` | `bitrix-sync` | ops / мониторинг | `GET /internal/sync/v1/*` | `Authorization: Bearer` или `X-Service-Token` | + +Пары значений (должны совпадать): + +- `BITRIX_LOCAL_APP_INTERNAL_TOKEN` (api-backend) = `BITRIX_INTERNAL_API_TOKEN` (bitrix-local-app) +- `BITRIX_API_FORWARD_TOKEN` (bitrix-local-app) = `BITRIX_API_INBOX_TOKEN` (api-backend) + +Генерация: `openssl rand -hex 32`. Секреты не коммитить. + +**Не путать с webhook-токенами** (публичные callback от Bitrix24, не internal service API): + +| Переменная | Назначение | +|---|---| +| `BITRIX_APPLICATION_TOKEN` | проверка событий Bitrix24 → `bitrix-local-app` `/bitrix/handler` | +| `BITRIX_SYNC_WEBHOOK_TOKEN` | проверка webhook Bitrix24 → `bitrix-sync` `/bitrix/sync/webhook/contact` | + +## Frontend ↔ api-backend + +| Контракт | Владелец | Потребитель | Назначение | Auth | +|---|---|---|---|---| +| `GET /api/v1/public/app-config` | `api-backend` | Expo frontend | Публичные настройки: OTP, оператор, лимиты, файлы, **UX idle timeout** | public + CORS/rate limit | +| `GET /api/v1/public/content` | `api-backend` | Expo frontend | Тексты по мнемоникам и популярные вопросы | public + CORS/rate limit | +| `POST /api/v1/consents` | `api-backend` | Expo frontend | Сохранение согласий перед OTP; тело включает `guest_session_id`, версии документов, device metadata | public + `guest_session_id` + rate limit | +| `POST /api/v1/analytics/session-start` | `api-backend` | Expo frontend | Событие `session_start`, новая `UxSession` | public + rate limit | +| `POST /api/v1/auth/bootstrap` | `api-backend` | Expo frontend | После OTP: `find-or-create` пользователя, связь согласий, привязка `user_id` к `UxSession` | JWT | +| `POST /api/v1/dialogs` | `api-backend` | Expo frontend | Создание диалога перед первым сообщением (в т.ч. после популярного вопроса) | JWT + idempotency | +| `GET /api/v1/me` | `api-backend` | Expo frontend | Профиль текущего клиента | JWT | +| `GET /api/v1/me/documents` | `api-backend` | Expo frontend | Список документов (MVP: может быть пустым; доставка — post-MVP) | JWT | +| `GET /api/v1/documents/{document_id}` | `api-backend` | Expo frontend | Метаданные документа (post-MVP) | JWT | +| `GET /api/v1/documents/{document_id}/download-url` | `api-backend` | Expo frontend | Presigned URL; обязателен audit | JWT | +| `GET /api/v1/dialogs` | `api-backend` | Expo frontend | История диалогов | JWT | +| `GET /api/v1/dialogs/{dialog_id}` | `api-backend` | Expo frontend | Карточка диалога | JWT | +| `GET /api/v1/dialogs/{dialog_id}/messages` | `api-backend` | Expo frontend | История сообщений, polling fallback | JWT | +| `POST /api/v1/dialogs/{dialog_id}/messages` | `api-backend` | Expo frontend | Отправка сообщения клиента (MVP: `content_kind` `text` или `file`, см. ниже) | JWT + idempotency + safety | +| `POST /api/v1/dialogs/{dialog_id}/attachments/init` | `api-backend` | Expo frontend | Инициализация загрузки в S3-quarantine | JWT | +| `POST /api/v1/dialogs/{dialog_id}/attachments/{attachment_id}/complete` | `api-backend` | Expo frontend | Завершение загрузки и фиксация checksum/metadata | JWT | +| `WS /api/v1/realtime` | `api-backend` | Expo frontend | Realtime-события чата, статусы доставки, unread | JWT | + +Единый формат ошибки: + +```json +{ + "error": { + "code": "profile_not_found", + "message": "Profile was not found", + "request_id": "01J00000000000000000000000", + "details": {} + } +} +``` + +### `POST /api/v1/consents` (тело запроса) + +```json +{ + "guest_session_id": "550e8400-e29b-41d4-a716-446655440000", + "consents": { + "personal_data": { "accepted": true, "version": "2026-06-10" }, + "user_agreement": { "accepted": true, "version": "2026-06-10" }, + "marketing": { "accepted": false, "version": "2026-06-10" } + }, + "device": { + "platform": "ios", + "app_version": "1.0.0", + "device_id": "..." + } +} +``` + +После OTP api-backend связывает запись с `UserIdentity` по `guest_session_id` (в рамках `POST /api/v1/auth/bootstrap`). TTL guest-записи — 24 ч. + +### `POST /api/v1/analytics/session-start` (событие `session_start`) + +Вызывается frontend **только** при начале **новой** UX-сессии (см. arch-01, «Аналитическая UX-сессия»). **Не** привязан к OTP и JWT. + +**Заголовки:** `X-Request-ID` (опционально). + +**Тело:** + +```json +{ + "start_reason": "first_launch", + "guest_session_id": "550e8400-e29b-41d4-a716-446655440000", + "device": { + "platform": "web", + "app_version": "1.0.0", + "device_id": "..." + } +} +``` + +- `start_reason` — обязательно: `first_launch` | `cold_start` | `idle_timeout`; +- `guest_session_id` — опционально (если уже создан в гостевом режиме). + +**Ответ `201`:** + +```json +{ + "ux_session_id": "660e8400-e29b-41d4-a716-446655440001", + "started_at": "2026-07-08T12:00:00Z" +} +``` + +Frontend сохраняет `ux_session_id` **в памяти** и передаёт **`X-Ux-Session-Id`** в последующих запросах. + +**Повторный вызов в рамках той же UX-сессии не требуется** (возврат из фона в пределах idle timeout). + +### `POST /api/v1/auth/bootstrap` (после OTP) + +Вызывается **один раз** после успешного OTP и получения JWT. **Не** создаёт UX-сессию. + +**Заголовки:** `Authorization: Bearer `, `X-Ux-Session-Id` (рекомендуется). + +**Тело:** + +```json +{ + "guest_session_id": "550e8400-e29b-41d4-a716-446655440000", + "ux_session_id": "660e8400-e29b-41d4-a716-446655440001" +} +``` + +**Ответ `200`:** `{ "user_id": "uuid", "profile_ready": true }`. + +**Ошибки:** `401` (JWT), `403` (согласия не приняты / guest session истёк). + +### Создание диалога + +- `POST /api/v1/dialogs` — idempotency key в заголовке; ответ `{ "dialog_id": "uuid", "status": "open" }`. +- Обязателен перед первым `POST .../messages` (включая популярный вопрос после auth). +- `dialog_id` = `external_chat_id` (см. [`arch-00-glossary.md`](arch-00-glossary.md)). + +### Формат исходящего сообщения клиента (MVP) + +Имена `content_kind`, полей — [`arch-00-glossary.md`](arch-00-glossary.md). Правила: + +| `content_kind` | Тело `POST .../messages` | `Message.text` | `MessageAttachment` | +|---|---|---|---| +| `text` | непустой `text`; без вложения | текст | 0 записей | +| `file` | `attachment_id` + `checksum`; `text` пустой | пустая строка | ровно 1 запись | + +- непустой `text` **и** вложение → **`400`** `mixed_content_not_allowed` (до `message-safety`); +- пустое сообщение → **`403`** `empty_message`; +- более одного вложения → **`400`** `too_many_attachments`; +- файловое сообщение в Bitrix24: `message.files` (signed URL), `message.text` пустой. + +Post-MVP: допускается «текст + файлы» отдельной версией API. + +## Realtime (`WS /api/v1/realtime`) + +Transport: **WebSocket** over HTTPS (`wss://`), JWT в query `?access_token=` или subprotocol (реализация — в модуле `api-backend`). + +**Подключение:** + +1. Клиент открывает WS с валидным access token. +2. Сервер отправляет `{ "type": "connected", "server_time": "ISO8601" }`. +3. Клиент отправляет подписку: + +```json +{ "type": "subscribe", "dialog_ids": ["uuid"] } +``` + +4. Сервер отвечает `{ "type": "subscribed", "dialog_ids": ["uuid"] }`. + +**События сервер → клиент:** + +| `type` | Назначение | Ключевые поля | +|---|---|---| +| `message.new` | Новое сообщение в диалоге | `dialog_id`, `message` (DTO как в REST) | +| `message.status` | Смена статуса доставки/safety | `dialog_id`, `message_id`, `status` | +| `dialog.status` | Смена статуса диалога | `dialog_id`, `status` | + +**Reconnect:** + +- exponential backoff: 1s → 2s → 4s → … max 30s; +- после reconnect — повтор `subscribe` с актуальным списком `dialog_ids`; +- при недоступности WS > 30s — fallback на polling `GET .../messages?after=`. + +**Ping:** сервер может слать `{ "type": "ping" }` каждые 30s; клиент отвечает `{ "type": "pong" }`. + +## OpenAPI + +| Сервис | Файл | Публикуется наружу | +|---|---|---| +| `api-backend` | `api-backend/openapi.yaml` | да (`/api/v1/*`, health) | +| `message-safety` | `message-safety/openapi.yaml` | нет (internal) | +| `bitrix-local-app` | `bitrix-local-app/openapi.yaml` | частично (`/bitrix/*`, health) | +| `bitrix-sync` | `bitrix-sync/openapi.yaml` | нет (internal + webhook) | + +Правила: + +- breaking change публичного API → новый path-prefix (`/api/v2`) + запись в arch-02; +- internal API версионируется тем же правилом (`/internal/{mnemonic}/v2/...`); +- OpenAPI генерируется или поддерживается вручную — на усмотрение модуля, но файл обязателен в DoD (arch-05). + +## Frontend ↔ Keycloak + +| Контракт | Владелец | Потребитель | Назначение | +|---|---|---|---| +| OIDC Authorization Code Flow with PKCE | Keycloak | Expo frontend | OTP-only login, token issue, refresh | +| OIDC Refresh Token Grant | Keycloak | Expo frontend | Обновление access token без OTP при действующем refresh token | +| OIDC logout | Keycloak | Expo frontend | Завершение сессии Keycloak, очистка tokens | +| JWKS / discovery | Keycloak | Expo frontend, `api-backend` | Проверка issuer, audience и ключей | + +Frontend не обращается напрямую к Keycloak DB и не хранит парольные credentials. Парольная авторизация в MVP отключена. + +**OTP (Keycloak):** единственный канал первичной авторизации — телефон. OTP-flow нужен, когда refresh token отсутствует или истёк. При действующем refresh token frontend использует **Refresh Token Grant** и не показывает OTP. После ввода кода **Keycloak проверяет OTP**: при `KEYCLOAK_OTP_MOCK_ENABLED=true` — сверка с `KEYCLOAK_OTP_MOCK_CODE` (`.env`); при `false` — сверка с OTP от SMS-провайдера (post-MVP, [`!Backlog.md`](../../HAN_chat/!Backlog.md)). `api-backend` OTP не проверяет, только JWT. + +### Жизненный цикл access token (frontend) + +Детали — arch-01, «Обновление access token (frontend)». Кратко: + +| Механизм | Когда | Действие | +|---|---|---| +| **Scheduler** | за ~60 с до `exp` access token | Refresh Token Grant → новые tokens, перепланировать таймер | +| **401 interceptor** | `api-backend` / WS отклонил access token | single-flight refresh → **один** retry запроса | +| **Открытие приложения** | cold start / resume | silent refresh, если refresh token ещё действителен | + +Правила: + +- refresh выполняет **только frontend** (Keycloak token endpoint); `api-backend` на `401` **не** обновляет токен; +- параллельные запросы при refresh — очередь + single-flight; +- провал refresh → очистка tokens, гостевой режим, OTP при следующем защищённом действии; +- успешный refresh **не** создаёт UX-сессию (`session_start`). + +**401 от `api-backend`:** единый формат ошибки (см. выше); типичный `code`: `unauthorized` / `token_expired` — frontend трактует как сигнал к refresh+retry (если refresh token ещё валиден). + +## api-backend ↔ message-safety + +| Контракт | Владелец | Потребитель | Назначение | Защита | +|---|---|---|---|---| +| `POST /internal/safety/v1/messages/check` | `message-safety` | `api-backend` | Синхронная проверка текста, ссылок и файлов по cache/rules | internal network + `X-Service-Token` | +| `GET /internal/safety/v1/messages/tasks/{task_id}` | `message-safety` | `api-backend` | Опрос async-проверки файлов | internal network + `X-Service-Token` | +| Read S3-quarantine | Selectel S3 | `message-safety` | Чтение файла worker-ом при cache miss | read-only key | + +HTTP-семантика: `200 allow`, `403 deny`, `203 pending`. `api-backend` не выбирает sync/async режим, а только интерпретирует ответ. + +Маппинг в App DB (`Message.safety_status` — см. [`arch-00-glossary.md`](arch-00-glossary.md)): + +| HTTP / `message-safety` | `Message.safety_status` | Финальный? | +|---|---|---| +| `200` / `allow` | `allowed` | да | +| `403` / `deny` | `blocked` | да | +| `203` / `pending` | `pending` | нет | + +## api-backend ↔ bitrix-local-app (Open Lines) + +Мнемоника сервиса: **`openlines`**. Endpoint Open Lines на стороне `bitrix-local-app` и приёмник событий на стороне `api-backend` используют один префикс `/internal/openlines/v1/`. + +| Контракт | Владелец | Потребитель | Назначение | Защита | +|---|---|---|---|---| +| `POST /internal/openlines/v1/messages` | `bitrix-local-app` | `api-backend` | Отправка сообщения клиента в Bitrix24 Open Lines | Bearer `BITRIX_INTERNAL_API_TOKEN` | +| `GET /internal/openlines/v1/dialogs/{external_chat_id}` | `bitrix-local-app` | `api-backend` | Получение маппинга `dialog_id` ↔ `bitrix_chat_id` | Bearer token | +| `GET /internal/openlines/v1/status` | `bitrix-local-app` | ops / `api-backend` | Статус OAuth и `imconnector.status` | Bearer token | +| `POST /internal/openlines/v1/setup/retry` | `bitrix-local-app` | ops | Повтор register/activate/event.bind | Bearer token | +| `POST /internal/openlines/v1/inbox` | `api-backend` | `bitrix-local-app` | Forward нормализованных событий оператора | Bearer `BITRIX_API_INBOX_TOKEN` | + +`external_chat_id` всегда равен `dialog_id` приложения. `bitrix-sync` не участвует в hot path чата. + +## api-backend ↔ bitrix-sync + +**без синхронного HTTP** в пользовательских сценариях. Связь — PostgreSQL-триггеры в App DB → очередь `han_app.sync_queue` + прямой доступ `bitrix-sync` к `han_app` для write-back. `api-backend` **не создаёт** задачи синхронизации вручную. + +### Очередь, триггеры и write-back (основной контракт MVP) + +| Контракт | Тип | Владелец | Потребитель | Назначение | +|---|---|---|---|---| +| `han_app.sync_queue` | PostgreSQL | триггеры `han_app` (миграции App DB) | `bitrix-sync` | Асинхронная очередь App DB → Bitrix24: триггер ставит задачу при изменении отслеживаемых полей | +| `han.sync_suppress` (GUC) | PostgreSQL session | `bitrix-sync` | триггеры `han_app` | Подавление эхо-задач при записи данных от Bitrix24 в App DB | +| `ClientProfile.bitrix_contact_id` | PostgreSQL | `bitrix-sync` | App DB | Маппинг профиля на CRM Contact после map/create | +| `han_app.entity_external_mapping` | PostgreSQL | `bitrix-sync` | App DB | Универсальный маппинг App entity ↔ Bitrix entity (MVP: Contact) | +| Обновление `sync_queue.status` | PostgreSQL | `bitrix-sync` | App DB | `processed` / `failed` / `dead_letter`, retry metadata | + +Типы задач MVP (`sync_queue.task_type`): + +- `contact.map_or_create` — матчинг/создание Contact, запись `bitrix_contact_id`, флаг регистрации в Bitrix24; +- `contact.update` — push изменений профиля в Bitrix24. + +`bitrix-sync` **не создаёт** `UserIdentity` / `ClientProfile` в auth-flow; вход worker — задачи из `sync_queue`, созданные триггерами. + +### Internal HTTP `bitrix-sync` (ops, не hot path) + +| Контракт | Владелец | Потребитель | Назначение | Защита | +|---|---|---|---|---| +| `GET /internal/sync/v1/status` | `bitrix-sync` | ops / мониторинг | Глубина очереди, dead letter, последний успешный run | internal network + `BITRIX_SYNC_SERVICE_TOKEN` | + +Повтор dead letter и ручной replay в MVP — через БД/ops-процедуры; отдельный HTTP replay-endpoint — post-MVP. + +## bitrix-local-app ↔ Bitrix24 + +| Контракт | Направление | Назначение | +|---|---|---| +| `GET/POST /bitrix/install` | Bitrix24 → `bitrix-local-app` | Установка local app, OAuth lifecycle | +| `GET/POST /bitrix/handler` | Bitrix24 → `bitrix-local-app` | `ONIMCONNECTOR*`, `ONAPPINSTALL`, `ONAPPUNINSTALL` | +| `imconnector.register` | `bitrix-local-app` → Bitrix24 | Регистрация `han_mobile_app` | +| `imconnector.activate` | `bitrix-local-app` → Bitrix24 | Привязка к линии 8 | +| `event.bind` | `bitrix-local-app` → Bitrix24 | Подписка на события коннектора | +| `imconnector.send.messages` | `bitrix-local-app` → Bitrix24 | Доставка сообщения клиента оператору | +| `imconnector.send.status.delivery` | `bitrix-local-app` → Bitrix24 | Подтверждение доставки входящего события | + +## bitrix-sync ↔ Bitrix24 CRM + +| Контракт | Направление | Назначение | +|---|---|---| +| `crm.contact.get/list/add/update` | `bitrix-sync` → Bitrix24 | Поиск, создание и обновление Contact | +| `POST /bitrix/sync/webhook/contact` | Bitrix24 (робот) → `bitrix-sync` | Исходящий webhook при изменении полей Contact, зарегистрированного в приложении | +| PostgreSQL schema `bitrix_sync` | `bitrix-sync` ↔ PostgreSQL | Worker state, field mapping, retry/dead letter audit | +| PostgreSQL schema `han_app` | `bitrix-sync` ↔ PostgreSQL | Очередь `sync_queue`, маппинг ID, обновление профиля (Bitrix → App) | + +Очередь `han_app.sync_queue` и write-back — в разделе «api-backend ↔ bitrix-sync» выше. + +`bitrix-sync` использует `BITRIX_SYNC_APP_DATABASE_URL` для `han_app` + `bitrix_sync`, только `BITRIX_SYNC_CRM_*` для Bitrix24 CRM REST и не читает OAuth-токены `bitrix-local-app`. + +## api-backend ↔ внешние хранилища + +| Контракт | Внешний сервис | Назначение | +|---|---|---| +| PostgreSQL schema `han_app` | Managed PostgreSQL | App DB: пользователи, профили, диалоги, сообщения, настройки, sync_queue, audit | +| Selectel S3 `han-chat-quarantine` | Selectel S3 | Временное хранение вложений клиента до verdict | +| Selectel S3 `han-chat-attachments` | Selectel S3 | Проверенные файлы чата | +| Selectel S3 `han-chat-documents` | Selectel S3 | Документы компании для клиента | +| Redis | `redis` | rate limits, OTP counters, coordination/realtime state | + +## Observability-контракты + +| Контракт | Владелец | Потребители | Назначение | +|---|---|---|---| +| OTLP gRPC/HTTP | `otel-collector` (`observability`) | backend-сервисы | Приём traces/logs/metrics | +| JSON stdout logs | каждый сервис | platform logs / оператор | Техническая диагностика; **`ux_session_id`** из `X-Ux-Session-Id`, если передан | +| Audit / analytics events в App DB | `api-backend` | аналитика, расследования | `session_start` и чувствительные действия без PII | + +### Analytics: `session_start` + +При `POST /api/v1/analytics/session-start` api-backend создаёт запись: + +| Поле | Пример | +|---|---| +| `event_type` | `session_start` | +| `ux_session_id` | UUID новой UX-сессии | +| `start_reason` | `first_launch` / `cold_start` / `idle_timeout` | +| `guest_session_id` | UUID или null | +| `user_id` | null (до auth bootstrap) | +| `request_id` | из `X-Request-ID` | +| `ip`, `user_agent` | из proxy headers | + +Raw OTP и полный номер телефона в audit **не** пишутся. + +### Audit: выдача download URL + +При `GET /api/v1/documents/{document_id}/download-url` и аналогичных endpoint вложений чата api-backend создаёт запись: + +| Поле | Пример | +|---|---| +| `event_type` | `document.download_url_issued`, `attachment.download_url_issued` | +| `user_id` | UUID пользователя | +| `resource_type` | `document` / `attachment` | +| `resource_id` | UUID ресурса | +| `ux_session_id` | из `X-Ux-Session-Id` | +| `request_id` | из `X-Request-ID` | +| `ip`, `user_agent` | из proxy headers | + +Presigned URL и содержимое файла в audit **не** пишутся. Структура таблицы — модуль `database`. + +## Health-контракты + +Все backend-сервисы имеют `GET /health/live` и `GET /health/ready`. Наружу публикуются только health endpoints, которые нужны `nginx`/Bitrix24; internal services проверяются через Docker/VPC-сеть. diff --git a/architectory/arch-03-docker-compose-blueprint.md b/architectory/arch-03-docker-compose-blueprint.md new file mode 100644 index 0000000..12e957a --- /dev/null +++ b/architectory/arch-03-docker-compose-blueprint.md @@ -0,0 +1,445 @@ +# arch-03. Docker Compose blueprint + +> Термины (имена бакетов S3, идентификаторы) — в [`arch-00-glossary.md`](arch-00-glossary.md). Контракт Message Safety Service — в [`arch-02-api-contracts.md`](arch-02-api-contracts.md), раздел «api-backend ↔ message-safety». Переменные окружения и настройки — в [`arch-04-settings-and-content.md`](arch-04-settings-and-content.md). + +## Назначение + +Этот документ описывает целевой Docker Compose контур для первой production-like среды. Он не заменяет будущий `docker-compose.yml`, но задает требования, которым он должен соответствовать. + +Требования к безопасности на уровне приложения и данных — в [`arch-01-system-architecture.md`](arch-01-system-architecture.md), раздел **«Принципы безопасности»**. Настоящий документ описывает только инфраструктурную реализацию этих принципов в compose/nginx: TLS, маршрутизация, сетевые границы, rate limits на edge. Значения переменных окружения — в [`arch-04-settings-and-content.md`](arch-04-settings-and-content.md). Дублировать прикладные требования (JWT, валидация, CORS в API, PII в логах и т.п.) здесь не нужно — они остаются в `arch-01`. + +## Единый compose-контур (обязательно) + +Это зафиксированное архитектурное требование, а не рекомендация. + +### Принцип единого входа + +- Весь backend-контур поднимается **одной командой** `docker compose up -d` из корня репозитория (`backend/`). +- Корневой `docker-compose.yml` — единственный источник правды для production-like среды. Отдельных compose-файлов для production-деплоя отдельных сервисов не должно быть. +- **Один `nginx`** поднимается из корневого `docker-compose.yml` и является единой публичной точкой входа с маршрутизацией на все сервисы: + - `/api/*`, `/realtime/*` → `api-backend`; + - `/auth/*` → `keycloak`; + - `/bitrix/*` (public: `/bitrix/handler`, `/bitrix/install`, `/bitrix/placement`, `/health/*` для `bitrix-local-app`) → `bitrix-local-app`; + - `/bitrix/sync/*` (public: webhook CRM sync для `bitrix-sync`) → `bitrix-sync`; + - web-сборка frontend или прокси на dev-сервер; + - `/internal/openlines/*`, `/internal/safety/*`, `/internal/sync/*` **не публикуются** наружу — доступны только из внутренней Docker-сети. +- Никакой другой `nginx` (ни в контейнере сервиса, ни на хосте) не терминирует внешний HTTPS для backend-контура. Site-конфиг `tohin.ru` на хосте, если используется, должен проксировать весь трафик на корневой `nginx` контейнера, а не на порты отдельных сервисов напрямую. + +### Структура compose через `include` + +Каждый сервис описывается в собственном `docker-compose.yml` внутри папки сервиса и подключается в корневой файл директивой `include`: + +```text +backend/ + docker-compose.yml # корневой: nginx + include сервисов + общие networks/volumes + .env + nginx/ + docker-compose.yml # описание сервиса nginx (или секция в корневом) + nginx.conf + conf.d/ + certs/ + .gitkeep + api-backend/ + docker-compose.yml # описание сервиса api-backend + message-safety/ + docker-compose.yml # описание сервиса message-safety + bitrix-sync/ + docker-compose.yml # описание сервиса bitrix-sync + bitrix-local-app/ + docker-compose.yml # описание сервиса bitrix-local-app + keycloak/ + docker-compose.yml # описание сервиса keycloak (или секция в корневом) + observability/ + docker-compose.yml # otel-collector и т.п. +``` + +Корневой `backend/docker-compose.yml` (принципиальная схема): + +```yaml +name: han-chat + +include: + - nginx/docker-compose.yml + - api-backend/docker-compose.yml + - message-safety/docker-compose.yml + - bitrix-sync/docker-compose.yml + - bitrix-local-app/docker-compose.yml + - keycloak/docker-compose.yml + - redis/docker-compose.yml + - observability/docker-compose.yml + +networks: + public: + backend: + observability: + +volumes: + redis-data: + nginx-certs: +``` + +### Правила для сервисных compose-файлов + +- Сервисный `docker-compose.yml` описывает **только** сервис(ы) своего модуля: образ, build context, `environment` (через `${VAR}` из корневого `.env`), порты (только внутренние, кроме случаев ниже), `depends_on`, healthcheck, подключение к сетям `public`/`backend`/`observability` (объявленным в корневом файле). +- Сервисный файл **не объявляет** сети и volumes верхнего уровня — они объявляются в корневом `docker-compose.yml`. Сервис только ссылается на них через `networks:` / `volumes:` (external-стиль не нужен, т.к. `include` объединяет файлы в один проект). +- Публикация портов наружу (`ports:`) разрешена **только** для `nginx` (80/443). Все остальные сервисы используют `expose:` для внутренних портов и общаются через Docker-сети. +- `bitrix-local-app` не публикует `8080` на хост (даже на `127.0.0.1`) — он доступен `api-backend` и `nginx` через сеть `backend`/`public`. Ранее применявшийся `127.0.0.1:8080:8080` считаем устаревшим; проверки через curl на `127.0.0.1:8080` заменяются на `docker compose exec bitrix-local-app` или прокси через `nginx`. +- Каждый сервисный compose-файл должен запускаться и в составе корневого контура, и автономно (`docker compose -f bitrix-local-app/docker-compose.yml up`) для локальной разработки сервиса — при условии, что переменные окружения заданы. Для автономного запуска сервис может объявлять заглушки сетей/volumes, но в составе корневого контура они переопределяются общими. + +### Команды разработки + +```text +docker compose up -d +docker compose logs -f nginx +docker compose logs -f api-backend +docker compose logs -f message-safety +docker compose logs -f bitrix-sync +docker compose logs -f bitrix-local-app +docker compose exec api-backend alembic upgrade head +docker compose exec api-backend pytest +docker compose exec api-backend ruff check . +docker compose exec api-backend ruff format . +``` + +## Сервисы + +### nginx + +Reverse proxy и единственная публичная точка входа в Docker Compose контур. + +Требования: + +- публикует наружу только `80` и `443` (см. политику HTTP ниже); +- принимает внешний HTTPS-трафик; +- выполняет TLS termination на reverse proxy; внутренний HTTP между контейнерами — только в закрытой Docker-сети `backend`; +- **политика HTTP/HTTPS по доменам** (каноническое правило — [`arch-01-system-architecture.md`](arch-01-system-architecture.md), «Принципы безопасности»): + - **веб-домен** (frontend, SPA, статика): `listen 80` допускается **только** для безусловного редиректа `301`/`308` на HTTPS; обработка бизнес-логики по HTTP запрещена; + - **API-домен** (если выделен отдельный host, напр. `api.example.ru`): **не** слушает порт `80`; только `listen 443 ssl`; HTTP-запросы к API-домену недоступны; + - **единый домен MVP** (напр. `tohin.ru` с путями `/api/*`, `/auth/*`, web): считается веб-доменом; порт `80` — только redirect на HTTPS для всего server block; после редиректа весь пользовательский трафик — HTTPS; + - **auth** на том же host, что API (`/auth/*`): следует политике host (redirect-only на :80 или HTTPS-only для выделенного API-host); + - **Bitrix callbacks** (`/bitrix/*`, `/bitrix/sync/*`): только HTTPS; порт `80` не обслуживает эти location — только redirect; +- маршрутизирует `/api/*` и `/realtime/*` в `api-backend`; +- маршрутизирует `/auth/*` в `keycloak` или проксирует отдельный auth-домен; +- маршрутизирует публичные `/bitrix/*` endpoint в `bitrix-local-app`; +- маршрутизирует `/bitrix/sync/*` webhook endpoint в `bitrix-sync`; +- закрывает `/internal/*` (в т.ч. `bitrix-local-app`, `message-safety`, `bitrix-sync` ops) от публичного доступа — только private network Docker/VPC; +- **не публикует** `message-safety` наружу; +- **production-like / production**: отдаёт **статическую сборку Expo web** из volume или каталога (`/usr/share/nginx/html` или аналог); `index.html` + assets, SPA fallback `try_files $uri /index.html`; +- **local dev** (опционально): при `FRONTEND_DEV_PROXY_ENABLED=true` проксирует `/` на Expo dev server (`EXPO_DEV_SERVER_URL`, напр. `http://host.docker.internal:8081`); +- передает upstream-сервисам `Host`, `X-Real-IP`, `X-Forwarded-For`, `X-Forwarded-Proto`, `X-Forwarded-Host`, `X-Request-ID`; +- задает разумные `proxy_connect_timeout`, `proxy_read_timeout`, `client_max_body_size`; +- применяет edge rate limits для auth, API и download endpoints; +- ограничивает частоту соединений и размер тела запроса; +- разрешает только TLS 1.2/1.3 и запрещает слабые шифры; +- добавляет HSTS и базовые security headers; +- скрывает заголовки, раскрывающие внутренние технологии; +- кэширует публичные endpoint настроек и контента; +- не проксирует наружу managed PostgreSQL, `redis`, `otel-collector` (БД вне compose, в VPC); + +### api-backend + +Python FastAPI backend. + +Требования: + +- запускается после доступности managed PostgreSQL, `keycloak`, `redis`; +- применяет настройки из `.env`; +- отдает `/health/live` и `/health/ready`; +- корректно работает за reverse proxy и доверяет proxy headers только от `nginx`; +- применяет API-level rate limits с состоянием в Redis; +- вызывает message safety pipeline для сообщений до отправки в Open Lines; +- вызывает `bitrix-local-app` для отправки сообщений в Open Lines; +- принимает forward нормализованных событий оператора от `bitrix-local-app`; +- поддерживает realtime endpoint для сообщений оператора; +- работает с Selectel S3 для файлов и документов; +- экспортирует traces/logs в `otel-collector`; +- не хранит состояние внутри контейнера. + +### message-safety + +Отдельный backend-сервис проверки входящих сообщений пользователя. HTTP-контракт — в [`arch-02-api-contracts.md`](arch-02-api-contracts.md), раздел «api-backend ↔ message-safety». + +Требования: + +- запускается после доступности managed PostgreSQL (схема `message_safety`), `redis`; +- **не публикуется** через `nginx` — доступен только из внутренней Docker-сети; +- отдаёт `/health/live` и `/health/ready` (ready проверяет PostgreSQL, Redis, workers, read-доступ к S3-quarantine); +- exposing endpoints: `POST /internal/safety/v1/messages/check`, `GET /internal/safety/v1/messages/tasks/{task_id}` (internal Docker network + `X-Service-Token` / `MESSAGE_SAFETY_SERVICE_TOKEN`); +- read-only доступ к S3-quarantine (отдельный access key без прав записи); +- использует отдельную схему `message_safety` в managed PostgreSQL и отдельный DB-user; +- использует Redis (отдельная DB, напр. `redis://redis:6379/2`) для verdict cache и rate limits; +- запускает async workers для file scan из S3-quarantine; +- экспортирует traces/logs в `otel-collector`; +- таймауты: POST check 5 s, GET task 2 s, file scan 60 s (см. [`arch-04-settings-and-content.md`](arch-04-settings-and-content.md), переменные `MESSAGE_SAFETY_*`). + +### bitrix-sync + +Python worker/service **двусторонней** синхронизации App DB ↔ Bitrix24 CRM. + +Требования: + +- запускается после готовности managed PostgreSQL, `redis`; +- читает задачи из `han_app.sync_queue` (заполняется триггерами App DB); +- имеет прямой доступ к `han_app` (`BITRIX_SYNC_APP_DATABASE_URL`) и схеме `bitrix_sync`; +- выполняет map/create Contact по телефону (интервал `BITRIX_SYNC_CONTACT_MAP_INTERVAL_SEC`, default 60); +- push обновлений Contact (интервал `BITRIX_SYNC_CONTACT_UPDATE_INTERVAL_SEC`, default 30); +- принимает webhook `POST /bitrix/sync/webhook/contact` от роботов Bitrix24; +- при записи в App DB от Bitrix использует GUC `han.sync_suppress=true`; +- поддерживает graceful shutdown и rate limiting Bitrix REST; +- не блокирует пользовательский API при ошибках Битрикс24; +- не участвует в OTP-flow, не создаёт `UserIdentity`/`ClientProfile`; +- **не участвует** в hot path чата Open Lines. +- `bitrix-sync` должен быть подключаем через .env (если отключили, то синхронизация с битрикс24 не проводится; если не отключили - проводится) + +### bitrix-local-app + +Локальное приложение Bitrix24 и custom connector `han_mobile_app`. + +Требования: + +- публикует наружу только `/bitrix/handler`, `/bitrix/install`, `/bitrix/placement`, `/health/live`, `/health/ready`; +- принимает `ONAPPINSTALL` и `ONIMCONNECTOR*` события от Bitrix24; +- регистрирует и активирует connector `han_mobile_app` для открытой линии 8; +- хранит OAuth-токены Bitrix24, inbox событий и `dialog_sessions` в managed PostgreSQL, схема `bitrix_local`; +- предоставляет internal API `POST /internal/openlines/v1/messages` и `GET /internal/openlines/v1/dialogs/{external_chat_id}` для api-backend; +- защищает internal API через `Authorization: Bearer {BITRIX_INTERNAL_API_TOKEN}`; +- forward-ит нормализованные события Open Lines в API, если задан `BITRIX_API_FORWARD_URL`; +- не хранит бизнес-данные приложения и не пишет напрямую в App DB. + +### Managed PostgreSQL + +**Во всех средах** (production, production-like, local dev) данные хранятся в **managed PostgreSQL** провайдера. Контейнер PostgreSQL в Docker Compose **не используется** — ни для production, ни для локальной разработки. + +Прикладные данные, Keycloak, `bitrix-sync`, `bitrix-local-app` и `message-safety` подключаются к одной managed базе по URL из `.env` (`HAN_PG_HOST`, `HAN_PG_PORT`, `HAN_PG_DATABASE` и схемо-специфичные `*_DATABASE_URL`). + +Требования: + +- подключение только из приватной сети VPC (VM → managed PostgreSQL); +- одна managed база: схемы `han_app`, `bitrix_sync`, `message_safety`, `bitrix_local`, `keycloak`; +- отдельные DB-пользователи с доступом только к своей схеме; исключение: `bitrix_sync_user` дополнительно имеет ограниченный GRANT на `han_app` (`sync_queue`, `entity_external_mapping`, tracked columns профиля — детали схемы TBD в спецификации database); +- TLS к managed PostgreSQL обязателен; +- миграции Alembic выполняются отдельной командой при деплое; +- бэкапы и PITR — на стороне провайдера. + +### keycloak + +Identity provider. + +Требования: + +- отдельный realm для приложения; +- отдельный frontend client с PKCE; +- backend client для service-to-service сценариев; +- публичный issuer должен соответствовать HTTPS URL, видимому frontend-приложению; +- включены proxy settings для работы за `nginx`; +- импорт realm в local/dev; +- использует managed PostgreSQL, схема `keycloak` (см. раздел «Managed PostgreSQL» выше); +- healthcheck. + +### redis + +Очереди, кеш, rate limiting. + +Требования: + +- не использовать как единственное надежное хранилище бизнес-событий; +- хранить счетчики API-level rate limits; +- поддерживать TTL для лимитных ключей; +- sync_queue хранится в PostgreSQL (`han_app`), Redis может использоваться для wake-up/locking/queue optimization. + +### otel-collector + +Принимает telemetry от сервисов. + +Требования: + +- OTLP HTTP/gRPC receiver; +- экспорт traces/logs в stdout или платформенный collector; +- единые resource attributes: `service.name`, `deployment.environment`. + +## Networks + +Рекомендуемые сети: + +- `public`: `nginx`, frontend dev access, внешний HTTPS entrypoint. +- `backend`: API, `message-safety`, `bitrix-sync`, `bitrix-local-app`, `redis` (managed PostgreSQL — вне compose, в VPC). +- `observability`: otel-collector. + +Базы данных, Redis, Keycloak internal port и API internal port не должны публиковаться наружу. `message-safety` доступен только внутри сети `backend`. Основной пользовательский путь должен идти через `nginx` и HTTPS. + +## Volumes + +Минимальные volumes (production на одной VM): + +- `redis-data` (опционально, если нужна персистентность); +- certbot / TLS volumes для `nginx`. + +Данные PostgreSQL **не** хранятся в Docker volumes — только managed PostgreSQL вне compose. + +## Переменные окружения + +Корневой `backend/.env` читается всеми сервисами compose через `${VAR}` в сервисных `docker-compose.yml`. Канонический `.env.example`, service tokens, `app_settings` — в [`arch-04-settings-and-content.md`](arch-04-settings-and-content.md). + +## HTTPS и TLS + +Соответствует [`arch-01-system-architecture.md`](arch-01-system-architecture.md), «Принципы безопасности» (HTTPS, TLS, HSTS). Инфраструктурная реализация: + +### Домены и HTTP + +| Host | Порт 80 | Порт 443 | Примечание | +|---|---|---|---| +| Веб-домен (frontend) | только `301`/`308` → HTTPS | HTTPS, бизнес-логика | MVP: `tohin.ru` / `app.example.ru` | +| API-домен (если выделен) | **не слушает** | только HTTPS | Post-MVP: `api.example.ru` | +| Bitrix callbacks (`/bitrix/*`, `/bitrix/sync/*`) | не обслуживает API; только redirect на том же host | HTTPS | webhook и install URL | + +Правила: + +- все внешние пользовательские соединения — **HTTPS**; +- HTTP допускается **только** на веб-домене как вход для редиректа на HTTPS; +- для **выделенного API-домена** HTTP **не допускается** (нет listener на :80); +- при едином домене MVP redirect на :80 применяется ко всему host, включая `/api/*` и `/auth/*`, после редиректа — только HTTPS. + +### TLS и заголовки + +- cookies в web-клиенте: `Secure`, `HttpOnly`, корректный `SameSite`; +- OIDC redirect URI в Keycloak — HTTPS; +- `KEYCLOAK_PUBLIC_URL`, issuer и frontend auth discovery URL совпадают по схеме, host и path; +- backend формирует внешние ссылки с учётом `X-Forwarded-Proto=https`; +- HSTS включается в production-like среде **после** проверки доменов и сертификатов; +- TLS 1.0/1.1 запрещены; минимум TLS 1.2, предпочтительно TLS 1.3; +- слабые шифры запрещены на уровне `nginx`; +- `nginx` скрывает `Server`, `X-Powered-By` и аналогичные технологические заголовки; +- security headers: `Strict-Transport-Security`, `X-Content-Type-Options`, `Referrer-Policy`, `Content-Security-Policy` для web-приложения; +- секретный ключ сертификата не коммитится в репозиторий; +- использовать сертификаты доверенного CA; автоматизировать выпуск и продление (Let's Encrypt + reload `nginx`); +- закрыть прямой доступ к внутренним портам контейнеров извне. + +## Nginx routing для Bitrix24 Local App + +`nginx` должен поддерживать отдельные server/location rules для `bitrix-local-app`. + +Рекомендуемая схема: + +- **веб-домен** (MVP: `tohin.ru` или `app.example.ru`): `/api/*`, `/auth/*`, `/realtime/*`, web frontend; `:80` → redirect HTTPS; `:443` — TLS + маршрутизация; +- **выделенный API-домен** (post-MVP, опционально): отдельный `server { listen 443 ssl; ... }` **без** `listen 80`; только `/api/*`, `/realtime/*`; +- домен или path `/bitrix/*` → `bitrix-local-app`; `/bitrix/sync/*` → `bitrix-sync`; +- `GET/POST /bitrix/handler` и `GET/POST /bitrix/install` доступны публично для Bitrix24; +- `/bitrix/placement` доступен публично как заглушка UI настроек коннектора; +- `/health/live` и `/health/ready` для `bitrix-local-app` доступны только там, где это нужно для healthcheck и проверки Bitrix form URL; +- `/internal/openlines/v1/*` не публикуется наружу или защищается allowlist/private network плюс `Authorization: Bearer {BITRIX_INTERNAL_API_TOKEN}`; +- для `/bitrix/*` callbacks кэширование отключено; +- для `/bitrix/*` callbacks включены отдельные rate limits, но они не должны блокировать легитимные webhook-повторы Bitrix24. + +## Nginx routing для bitrix-sync (CRM webhook) + +`nginx` маршрутизирует публичные webhook CRM sync в `bitrix-sync`: + +- `POST /bitrix/sync/webhook/contact` — исходящий webhook от роботов Bitrix24 при изменении Contact; +- проверка `BITRIX_SYNC_WEBHOOK_TOKEN` выполняется в `bitrix-sync`; +- кэширование отключено; rate limits не должны блокировать легитимные повторы Bitrix24; +- `/internal/sync/v1/*` не публикуется наружу (только internal network + `BITRIX_SYNC_SERVICE_TOKEN`). + +## Rate limits и защита от abuse + +Rate limits должны быть распределены по двум слоям. + +`nginx`: + +- ограничивает частоту запросов до попадания в API; +- держит отдельные зоны лимитов для `/auth`, `/api`, public endpoints, fallback polling и download endpoints; +- ограничивает `client_max_body_size`; +- ограничивает загрузку файлов лимитом 5 МБ; `client_max_body_size` должен быть чуть выше бизнес-лимита для учета overhead запроса; +- применяет `limit_req` для endpoint авторизации и fallback polling; +- для публичных endpoint использует лимит не выше 60 запросов в минуту с одного IP, если настройки не говорят иначе; +- возвращает `429` при превышении лимитов; +- не должен использоваться для сложных пользовательских правил, завязанных на `user_id`. + +API: + +- применяет лимиты после проверки JWT; +- считает лимиты по `user_id`, IP, route, dialog id и service client; +- хранит быстрые счетчики в Redis; +- пишет значимые превышения в audit/App DB; +- возвращает `Retry-After`, если клиент может повторить запрос позже. + +Проверка сообщений на prompt injection и вредоносные действия не должна выполняться в `nginx`: это задача отдельного сервиса `message-safety`, вызываемого из `api-backend` (см. [`arch-02-api-contracts.md`](arch-02-api-contracts.md)). + +## WAF + +WAF можно подключить внешним слоем перед `nginx` без изменения бизнес-кода, если соблюдены требования: + +- `nginx` и API корректно работают с цепочкой proxy headers и доверяют real IP только от доверенных прокси; +- CORS разрешает только доверенные домены; +- публичные endpoint имеют rate limits и кэширование даже без WAF; +- схема TLS termination согласована с тем, где завершается TLS: WAF/CDN, load balancer или `nginx`; +- WAF не должен подменять тело запросов и ответы API без явной необходимости. + +WAF не заменяет обязательные лимиты, валидацию схем, авторизацию и аудит внутри приложения. + +## Публичные endpoint + +`GET /api/v1/public/app-config` и `GET /api/v1/public/content` являются публичными, поэтому для них обязательны: + +- `limit_req` на уровне `nginx`, базово 60 запросов в минуту с одного IP; +- агрессивное кэширование на уровне `nginx` или CDN; +- заголовок `Cache-Control: public, max-age=3600`; +- строгая DTO-схема ответа на backend, без сериализации всех строк таблицы настроек; +- CORS только для доверенных доменов приложения; +- отсутствие секретов, внутренних URL, service tokens и приватных feature flags в ответе. + +Подробнее — [`arch-04-settings-and-content.md`](arch-04-settings-and-content.md), разделы «Публичный config endpoint» и «Публичный content endpoint». + +## Healthchecks + +Минимальные проверки: + +- `nginx`: на веб-домене — `301` с `:80` на HTTPS; на API-домене (если выделен) — `:80` не слушает; `:443` — HTTP 200/301 и успешная TLS handshake; +- `api-backend`: HTTP 200 от `/health/ready`; +- `message-safety`: HTTP 200 от `/health/ready` (проверяет PostgreSQL, Redis, workers, read S3-quarantine); +- `bitrix-sync`: процесс жив, подключение к App DB доступно; +- `bitrix-local-app`: HTTP 200 от `/health/live`, readiness показывает наличие OAuth-токенов после установки приложения; +- `keycloak`: health endpoint Keycloak; readiness — подключение к managed PostgreSQL; +- `redis`: `redis-cli ping`; + +## Порядок запуска + +1. `redis` (managed PostgreSQL должна быть доступна до старта зависимых сервисов). +2. `keycloak`. +3. `otel-collector`. +4. `message-safety`. +5. `api-backend`. +6. `bitrix-local-app`. +7. `bitrix-sync`. +8. `nginx`. + +`depends_on` не заменяет проверку готовности. Сервисы должны уметь ждать зависимости или корректно завершаться с понятной ошибкой. `api-backend` должен ждать готовности `message-safety` (healthcheck), т.к. отправка сообщения синхронно зависит от `POST /internal/safety/v1/messages/check`. + +## Развёртывание на одной VM + +Production-контур на `tohin.ru`: + +1. VM и managed PostgreSQL в одном VPC/кластере провайдера. +2. Managed PostgreSQL без публичного IP; security group разрешает подключение только с VM. +3. `docker compose up -d` на VM поднимает все сервисы кроме БД. +4. Сервисы подключаются к managed PostgreSQL по приватному FQDN/IP. + +## Production-замечания + +### Frontend (Expo web) + +| Режим | Поведение nginx | +|---|---| +| production-like / production | Статика Expo web (`expo export` / EAS web build), `FRONTEND_DEV_PROXY_ENABLED=false` | +| local dev | Опционально proxy на Expo dev server, `FRONTEND_DEV_PROXY_ENABLED=true` | + +Переменные — [`arch-04-settings-and-content.md`](arch-04-settings-and-content.md), блок «Frontend (nginx)». + +Docker Compose на одной VM — production-контур первого этапа. Позже при росте нагрузки можно отдельно решить: + +- вынос Redis в managed cache; +- managed object storage; +- secret manager; +- TLS, reverse proxy или managed ingress; +- backup и restore; +- централизованный мониторинг; +- горизонтальное масштабирование API и worker. diff --git a/architectory/arch-04-settings-and-content.md b/architectory/arch-04-settings-and-content.md new file mode 100644 index 0000000..1798ef5 --- /dev/null +++ b/architectory/arch-04-settings-and-content.md @@ -0,0 +1,322 @@ +# arch-04. Настройки и изменяемые параметры + +> **`.env`** — инфраструктура и секреты. **`app_settings`** (App DB) — единственный источник бизнес-настроек. Имена полей и enum — [`arch-00-glossary.md`](arch-00-glossary.md). Docker Compose — [`arch-03-docker-compose-blueprint.md`](arch-03-docker-compose-blueprint.md). + +## Цель + +Параметры разделены по слоям: + +| Слой | Где | Что | +|---|---|---| +| **Инфраструктура** | `.env` | подключения, URL, секреты, nginx/TLS, service tokens | +| **Бизнес-логика** | таблица **`app_settings`** | лимиты, флаги, телефоны, типы файлов, CORS, consent URLs | +| **Контент** | `text_resources`, `popular_questions` | тексты UI | + +Managed PostgreSQL **поднимается до** развёртывания приложения. Бизнес-настройки **не дублируются** в `.env`: seed в `app_settings` выполняется миграцией/скриптом модуля `database` **до** первого запуска `api-backend`. + +## Источники настроек + +### `.env` — только инфраструктура + +Корневой `backend/.env` читается сервисами compose. В репозитории — `.env.example`, не `.env`. + +**Допустимо в `.env`:** + +- URL сервисов, публичные endpoint, порты; +- строки подключения PostgreSQL, Redis, Keycloak DB; +- секреты: S3, Bitrix OAuth, service tokens, webhook-тokens; +- параметры **nginx/TLS** и edge rate limits (`NGINX_RATE_LIMIT_*`); +- идентификация Keycloak: realm, audience, public/internal URL; +- **OTP-заглушка MVP** (`KEYCLOAK_OTP_MOCK_*`) — infra/dev-секрет, не бизнес-настройка; +- технические таймауты worker-ов (`MESSAGE_SAFETY_*`, интервалы `bitrix-sync`). + +**Запрещено в `.env` (→ только `app_settings`):** + +- включение/отключение OTP, OTP-лимиты для UI/продукта; +- телефон оператора, consent URLs/versions; +- лимиты приложения (сообщения, download URL, login); +- типы/размер файлов чата, UX idle timeout; +- CORS origins, feature flags frontend. + +### `app_settings` — бизнес-настройки (App DB) + +**Единственный источник правды** для параметров, которые: + +- меняет продукт/оператор без redeploy; +- отдаются в `GET /api/v1/public/app-config` (публичные ключи); +- используются `api-backend` (и при необходимости другими сервисами) в runtime. + +Позже — редактирование через админку; на MVP — seed-миграция. + +### `text_resources` / `popular_questions` + +Контент UI — отдельные таблицы (не `app_settings`). Ключи MVP — TBD (спецификация frontend). + +--- + +## Требования к таблице `app_settings` + +Схема: **`han_app`**. Детальная DDL — модуль `database`; arch фиксирует контракт. + +### Колонки (минимум) + +| Колонка | Тип | Назначение | +|---|---|---| +| `setting_key` | `varchar`, PK | Канонический ключ (`auth.phone.enabled`, см. ниже) | +| `setting_value` | `text`, NOT NULL | Значение (строка; парсинг по типу) | +| `value_type` | `enum` | `boolean` \| `integer` \| `string` \| `duration` \| `string_list` | +| `is_public` | `boolean` | Разрешён в `GET /api/v1/public/app-config` | +| `description` | `text`, nullable | Комментарий для админки/ops | +| `updated_at` | `timestamptz` | Последнее изменение | +| `record_status` | `char(1)` | Soft delete: `'A'` active | + +### Правила + +1. **Seed обязателен** до первого запуска `api-backend` в новой среде (миграция или idempotent seed-скрипт). +2. **`api-backend`** загружает настройки при старте; допускается in-memory cache с инвалидацией по `updated_at` (реализация — модуль). +3. Отсутствие **обязательного** ключа при старте → сервис **не** переходит в `ready` (fail-fast). +4. Публичные ключи (`is_public=true`) отдаются только через **строгий DTO** `app-config`, не raw dump таблицы. +5. Секреты и infra **не** хранятся в `app_settings`. + +### Ключи MVP (seed) + +Полный пример значений — раздел «Seed MVP» ниже. Группы: + +| Группа | Ключи | +|---|---| +| Auth | `auth.phone.enabled`, `auth.password.enabled` | +| OTP (продукт) | `otp.phone.max_send_attempts_per_24h`, `otp.phone.min_seconds_between_attempts` | +| Оператор | `operator.call.phone` | +| Consent | `consent.personal_data.*`, `consent.user_agreement.*`, `consent.marketing.*` | +| Файлы чата | `chat.attachments.*` | +| Rate limits (app) | `rate_limit.message_send.*`, `rate_limit.download_url.*`, `rate_limit.public_endpoints.*`, `rate_limit.login.*` | +| UX | `ux.session.idle_timeout_minutes` | +| Security | `security.cors.allowed_origins`, `security.public_cache.max_age_seconds` | + +### Seed MVP + +```text +auth.phone.enabled=true +auth.password.enabled=false + +otp.phone.max_send_attempts_per_24h=3 +otp.phone.min_seconds_between_attempts=30 + +operator.call.phone=+74999591007 + +consent.personal_data.required=true +consent.personal_data.document_url=https://www.han0107.ru/privacy/persdata-agree-mobile +consent.personal_data.version=2026-06-10 +consent.user_agreement.required=true +consent.user_agreement.document_url=https://www.han0107.ru/user-agreement +consent.user_agreement.version=2026-06-10 +consent.marketing.required=false +consent.marketing.version=2026-06-10 + +chat.attachments.allowed_extensions=jpg,jpeg,png,webp,heic,heif,pdf +chat.attachments.allowed_mime_types=image/jpeg,image/png,image/webp,image/heic,image/heif,application/pdf +chat.attachments.disallowed_extensions=svg,doc,docx,xls,xlsx,csv +chat.attachments.max_size_mb=5 +chat.attachments.storage=selectel_s3 +chat.attachments.upload_mode=backend_controlled_upload +chat.attachments.safety_scan_required=true + +rate_limit.message_send.per_user=30/minute +rate_limit.message_send.per_dialog=20/minute +rate_limit.download_url.per_user=60/hour +rate_limit.public_endpoints.per_ip=60/minute +rate_limit.login.per_ip=10/minute + +ux.session.idle_timeout_minutes=30 + +security.cors.allowed_origins=https://tohin.ru,https://app.example.ru +security.public_cache.max_age_seconds=3600 +``` + +--- + +## Пример `.env.example` + +Только инфраструктура. Бизнес-параметры — в seed `app_settings`. + +```text +# ============================================================================= +# Общие +# ============================================================================= +APP_ENV=production-like +API_PORT=8000 +LOG_LEVEL=INFO + +# ============================================================================= +# Managed PostgreSQL +# ============================================================================= +HAN_PG_HOST= +HAN_PG_PORT=6432 +HAN_PG_DATABASE=han_chat + +DATABASE_URL=postgresql+asyncpg://han_app:change-me@:/?options=-csearch_path%3Dhan_app +BITRIX_DATABASE_URL=postgresql://bitrix_local_app:change-me@:/?options=-csearch_path%3Dbitrix_local +BITRIX_SYNC_APP_DATABASE_URL=postgresql://bitrix_sync_user:change-me@:/?options=-csearch_path%3Dbitrix_sync%2Chan_app +BITRIX_SYNC_DATABASE_URL=postgresql://bitrix_sync_user:change-me@:/?options=-csearch_path%3Dbitrix_sync +MESSAGE_SAFETY_DATABASE_URL=postgresql://message_safety_app:change-me@:/?options=-csearch_path%3Dmessage_safety +KEYCLOAK_DB_URL=jdbc:postgresql://:/?user=keycloak_user&password=change-me¤tSchema=keycloak +KC_DB_URL_PROPERTIES=currentSchema=keycloak + +# ============================================================================= +# Публичные URL (HTTPS) +# ============================================================================= +PUBLIC_WEB_URL=https://app.example.ru +PUBLIC_API_URL=https://app.example.ru/api +PUBLIC_AUTH_URL=https://app.example.ru/auth + +# ============================================================================= +# nginx (edge, TLS, rate limits) +# ============================================================================= +NGINX_HTTP_PORT=80 +NGINX_HTTPS_PORT=443 +TLS_CERT_PATH=/etc/nginx/certs/fullchain.pem +TLS_KEY_PATH=/etc/nginx/certs/privkey.pem +NGINX_TLS_PROTOCOLS=TLSv1.2 TLSv1.3 +NGINX_HSTS_MAX_AGE=31536000 +NGINX_CLIENT_MAX_BODY_SIZE=8m +NGINX_RATE_LIMIT_API=60r/m +NGINX_RATE_LIMIT_AUTH=10r/m +NGINX_RATE_LIMIT_DOWNLOADS=30r/m +NGINX_RATE_LIMIT_PUBLIC=60r/m +NGINX_RATE_LIMIT_POLLING=60r/m + +# ============================================================================= +# Keycloak (infra; OTP-заглушка — dev/MVP) +# ============================================================================= +KEYCLOAK_PUBLIC_URL=https://app.example.ru/auth +KEYCLOAK_INTERNAL_URL=http://keycloak:8080 +KEYCLOAK_REALM=han-chat +KEYCLOAK_AUDIENCE=han-chat-api +KEYCLOAK_OTP_MOCK_ENABLED=true +KEYCLOAK_OTP_MOCK_CODE=1234 + +# ============================================================================= +# Redis +# ============================================================================= +REDIS_URL=redis://redis:6379/0 + +# ============================================================================= +# Service tokens (internal API) — все переменные только в backend/.env +# ============================================================================= +MESSAGE_SAFETY_SERVICE_TOKEN=change-me +BITRIX_LOCAL_APP_INTERNAL_TOKEN=change-me +BITRIX_API_INBOX_TOKEN=change-me +BITRIX_INTERNAL_API_TOKEN=change-me +BITRIX_API_FORWARD_TOKEN=change-me +BITRIX_SYNC_SERVICE_TOKEN=change-me + +# ============================================================================= +# api-backend (интеграции) +# ============================================================================= +BITRIX_LOCAL_APP_BASE_URL=http://bitrix-local-app:8080 +BITRIX_API_INBOX_PATH=/internal/openlines/v1/inbox +MESSAGE_SAFETY_URL=http://message-safety:8080 + +# ============================================================================= +# bitrix-sync +# ============================================================================= +BITRIX_SYNC_CRM_BASE_URL=https://han0107.bitrix24.ru +BITRIX_SYNC_CRM_WEBHOOK_URL=change-me +BITRIX_SYNC_CONTACT_MAP_INTERVAL_SEC=60 +BITRIX_SYNC_CONTACT_UPDATE_INTERVAL_SEC=30 +BITRIX_SYNC_CRM_MAX_CONCURRENCY=2 +BITRIX_SYNC_CONTACT_LIST_BATCH_SIZE=50 +BITRIX_SYNC_WEBHOOK_TOKEN=change-me + +# ============================================================================= +# bitrix-local-app +# ============================================================================= +BITRIX_CLIENT_ID=change-me +BITRIX_CLIENT_SECRET=change-me +BITRIX_CONNECTOR_ID=han_mobile_app +BITRIX_CONNECTOR_NAME=HAN Mobile App +BITRIX_OPEN_LINE_ID=8 +BITRIX_PUBLIC_BASE_URL=https://tohin.ru/bitrix +BITRIX_API_FORWARD_URL=http://api-backend:8000/internal/openlines/v1/inbox +BITRIX_APPLICATION_TOKEN=change-me + +# ============================================================================= +# message-safety (technical) +# ============================================================================= +MESSAGE_SAFETY_POST_TIMEOUT_SEC=5 +MESSAGE_SAFETY_TASK_POLL_INTERVAL_SEC=2 +MESSAGE_SAFETY_TASK_POLL_MAX_SEC=300 +MESSAGE_SAFETY_RULES_VERSION=2026-01-01 + +# ============================================================================= +# Frontend (nginx) +# ============================================================================= +FRONTEND_STATIC_PATH=/usr/share/nginx/html +FRONTEND_DEV_PROXY_ENABLED=false +EXPO_DEV_SERVER_URL=http://host.docker.internal:8081 + +# ============================================================================= +# Selectel S3 +# ============================================================================= +SELECTEL_S3_ENDPOINT_URL=https://s3.storage.selcloud.ru +SELECTEL_S3_BUCKET_DOCUMENTS=han-chat-documents +SELECTEL_S3_BUCKET_ATTACHMENTS=han-chat-attachments +SELECTEL_S3_BUCKET_QUARANTINE=han-chat-quarantine +SELECTEL_S3_ACCESS_KEY=change-me +SELECTEL_S3_SECRET_KEY=change-me +SELECTEL_S3_QUARANTINE_READ_ACCESS_KEY=change-me +SELECTEL_S3_QUARANTINE_READ_SECRET_KEY=change-me + +# ============================================================================= +# Observability +# ============================================================================= +OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317 +``` + +Все переменные — **только** в `backend/.env`. Отдельного хранилища нет. + +**Webhook-токены** (публичные callback, не service API): `BITRIX_APPLICATION_TOKEN`, `BITRIX_SYNC_WEBHOOK_TOKEN`. + +## Namespace переменных Bitrix + +- `bitrix-local-app`: `BITRIX_CLIENT_*`, `BITRIX_CONNECTOR_*`, `BITRIX_PUBLIC_BASE_URL`, `BITRIX_DATABASE_URL`, `BITRIX_API_FORWARD_URL`, `BITRIX_APPLICATION_TOKEN` + service tokens. +- `api-backend`: `BITRIX_LOCAL_APP_BASE_URL`, `MESSAGE_SAFETY_URL` + service tokens; **бизнес-настройки** — из `app_settings`. +- `bitrix-sync`: `BITRIX_SYNC_*`, `BITRIX_SYNC_WEBHOOK_TOKEN`, `BITRIX_SYNC_SERVICE_TOKEN`. +- `bitrix-sync` не читает `BITRIX_CLIENT_ID` / `BITRIX_CLIENT_SECRET`. + +## Разрешённые типы файлов чата (MVP) + +Источник значений — ключи **`app_settings`** (раздел «Seed MVP»). Остальные arch-* **ссылаются сюда**. + +| Ключ | MVP-значение | +|---|---| +| `chat.attachments.allowed_extensions` | `jpg`, `jpeg`, `png`, `webp`, `heic`, `heif`, `pdf` | +| `chat.attachments.allowed_mime_types` | `image/jpeg`, `image/png`, `image/webp`, `image/heic`, `image/heif`, `application/pdf` | +| `chat.attachments.max_size_mb` | `5` | + +Правило: файл принимается только если **и** расширение, **и** MIME в allow-list. Детальная проверка — модуль `message-safety`. + +Публичный UI: `GET /api/v1/public/app-config` (строгий DTO, без секретов). + +## Публичный config endpoint + +`GET /api/v1/public/app-config` — только ключи с `is_public=true` из `app_settings`: + +- OTP по телефону (`auth.phone.enabled`); +- номер оператора; +- типы файлов и max size; +- `ux.session.idle_timeout_minutes`; +- feature flags; +- публичные лимиты для подсказок UI. + +Секреты, service tokens, внутренние URL **не** возвращаются. DTO явный, не сериализация всей таблицы. Rate limit: 60/min per IP. `Cache-Control: public, max-age` из `security.public_cache.max_age_seconds`. + +## Публичный content endpoint + +`GET /api/v1/public/content` — `text_resources` для текущего языка. Те же требования безопасности, что у config. + +## Nginx и HTTPS + +Infra-переменные — `.env.example` (`NGINX_*`, `TLS_*`). Edge rate limits (`NGINX_RATE_LIMIT_*`) **не** дублируют `rate_limit.*` из `app_settings`: nginx — защита периметра, app — бизнес-лимиты в backend. + +Реализация — [`arch-03-docker-compose-blueprint.md`](arch-03-docker-compose-blueprint.md). diff --git a/architectory/arch-05-agent-development-process.md b/architectory/arch-05-agent-development-process.md new file mode 100644 index 0000000..51531d5 --- /dev/null +++ b/architectory/arch-05-agent-development-process.md @@ -0,0 +1,96 @@ +# arch-05. Правила разработки модулей отдельными агентами + +> Термины — в [`arch-00-glossary.md`](arch-00-glossary.md). Настоящий документ описывает процесс разработки и не переопределяет архитектуру. Иерархия приоритета — в [`README.md`](README.md), раздел «Разрешение конфликтов». + +## Цель + +Этот документ задает единый процесс разработки, чтобы отдельные агенты создавали совместимые части приложения без расхождения архитектуры. + +## Общие правила +- Каждый агент работает только в границах назначенного модуля. +- Перед разработкой агент читает [`README.md`](README.md), архитектурные документы и профильный документ назначенного модуля. +- Любое изменение публичного API сопровождается обновлением OpenAPI. +- Любое изменение структуры данных сопровождается миграцией. +- Все **бизнес-параметры** — в таблице `app_settings`; **infra и секреты** — в `.env`. +- Нельзя hardcode-ить телефоны, лимиты, тексты, mime types, feature flags и параметры Битрикс24. +- Модули, принимающие пользовательский ввод, должны учитывать rate limits и security/safety проверки. + +## Правила базы данных + +- Перечень таблиц, полей, индексов и миграций **определяет модуль-владелец** (`database`, `api-backend`, `bitrix-sync`, `message-safety`, `bitrix-local-app`), а не arch-*. +- Архитектура фиксирует **разделение схем** и общие подходы к ведению баз данных, которые должны соблюдаться при проработке модулей. +- У каждой основной **прикладной** сущности должен быть `record_status`. Базовые статусы: `A` — active, `D` — deleted. +- Физическое удаление строк прикладных сущностей запрещено. Если нужно удалить сущность, сервис меняет `record_status` с `A` на `D`. +- При смене статуса на `D` сервис обязан заполнить `status_changed_at` и `status_change_reason`. +- Все сервисы при чтении бизнес-данных по умолчанию запрашивают только `record_status = 'A'`. +- Исключения допускаются только для аудита, админки, технического восстановления и миграций. +- Прикладные сущности, имеют `id`, `created_at`, `updated_at`, `updater_user_id`. +- Системные таблицы (`app_settings`, `text_resources`, `popular_questions`, `sync_queue`, audit, справочники) могут использовать `user_id = NULL` или отдельное поле `actor_type` — по спецификации модуля `database`. +- Для списков использовать справочники. Если значения в столбце могут принимать определенный набор значений, записывать их через ИД (sequence) и создавать справочник с расшифровкой ИД. Это существенно позволит экономить на размере таблиц. +- Для часто используемых фильтров добавляются индексы. +- Миграции не должны удалять данные без отдельного согласования. +- Все юзеры должны иметь ИД, которое указывается в `updater_user_id` которое они меняют. + + +## API + +Правила: + +- endpoint naming должен следовать `arch-02-api-contracts.md`; +- response schema не должна раскрывать внутренние поля; +- ошибки возвращаются в едином формате; +- для пользовательских данных всегда используется текущий user context из JWT; +- frontend не передает `client_profile_id` для доступа к своим данным; +- профиль в MVP не редактируется через `PATCH /me`; +- сообщения оператора должны приходить в frontend через realtime или polling fallback. + +## Логирование и OTP + +Каждый модуль должен: + +- использовать общий формат JSON-логов; +- добавлять `module`, `event`, `request_id`, `trace_id`; +- для `api-backend` добавлять **`ux_session_id`** в JSON-логи, если передан заголовок `X-Ux-Session-Id`; +- не логировать access token, refresh token, raw OTP, документы, полные PII; +- хранить факт отправки OTP через `provider_message_id`, `sent_at`, `destination_masked`, `otp_hash`, попытки и итог проверки. + +Raw OTP запрещено хранить в открытом виде: это временный секрет. Доказательство отправки и проверки строится на аудите, delivery id провайдера и hash-проверке. + +## Тесты + +Минимум для каждого модуля: + +- happy path; +- ошибки авторизации и доступа; +- rate limits, если модуль принимает пользовательский ввод; +- soft delete и фильтрация `record_status = 'A'`, если модуль работает с БД; +- idempotency, если операция может повториться; +- отсутствие секретов и PII в логах. + +Дополнительно: + + + +## Definition of Done + +Модуль считается готовым, если: + +- реализованы сценарии из задачи; +- обновлен `{service}/openapi.yaml`, если менялся HTTP API; +- созданы миграции, если менялась БД; +- добавлены тесты; +- сервис запускается в Docker Compose; +- все изменяемые параметры вынесены из кода; +- логи содержат `request_id`, `trace_id` и **`ux_session_id`** (если передан в запросе); +- нет секретов, raw OTP и PII в логах; +- soft delete соблюден; +- агент указал, какие документы архитектуры были затронуты. + +## Правила изменения архитектуры + +Если агент видит, что текущая архитектура мешает задаче, он должен: + +1. описать проблему; +2. предложить минимальное изменение; +3. указать затронутые документы; +4. не делать широкий рефакторинг без подтверждения.