Initial commit
This commit is contained in:
Binary file not shown.
@@ -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-*.
|
||||||
@@ -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 |
|
||||||
@@ -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.
|
||||||
@@ -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 <access_token>`, `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=<cursor>`.
|
||||||
|
|
||||||
|
**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-сеть.
|
||||||
@@ -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.
|
||||||
@@ -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=<managed-pg-private-host>
|
||||||
|
HAN_PG_PORT=6432
|
||||||
|
HAN_PG_DATABASE=han_chat
|
||||||
|
|
||||||
|
DATABASE_URL=postgresql+asyncpg://han_app:change-me@<HAN_PG_HOST>:<HAN_PG_PORT>/<HAN_PG_DATABASE>?options=-csearch_path%3Dhan_app
|
||||||
|
BITRIX_DATABASE_URL=postgresql://bitrix_local_app:change-me@<HAN_PG_HOST>:<HAN_PG_PORT>/<HAN_PG_DATABASE>?options=-csearch_path%3Dbitrix_local
|
||||||
|
BITRIX_SYNC_APP_DATABASE_URL=postgresql://bitrix_sync_user:change-me@<HAN_PG_HOST>:<HAN_PG_PORT>/<HAN_PG_DATABASE>?options=-csearch_path%3Dbitrix_sync%2Chan_app
|
||||||
|
BITRIX_SYNC_DATABASE_URL=postgresql://bitrix_sync_user:change-me@<HAN_PG_HOST>:<HAN_PG_PORT>/<HAN_PG_DATABASE>?options=-csearch_path%3Dbitrix_sync
|
||||||
|
MESSAGE_SAFETY_DATABASE_URL=postgresql://message_safety_app:change-me@<HAN_PG_HOST>:<HAN_PG_PORT>/<HAN_PG_DATABASE>?options=-csearch_path%3Dmessage_safety
|
||||||
|
KEYCLOAK_DB_URL=jdbc:postgresql://<HAN_PG_HOST>:<HAN_PG_PORT>/<HAN_PG_DATABASE>?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).
|
||||||
@@ -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. не делать широкий рефакторинг без подтверждения.
|
||||||
Reference in New Issue
Block a user