Grok version
This commit is contained in:
+10
-1
@@ -48,9 +48,18 @@
|
|||||||
|---|---|
|
|---|---|
|
||||||
| Доставка документов компании из Bitrix24 в приложение (`bitrix-sync` → `api-backend`, уведомление клиента) | [`!Backlog.md`](../../HAN_chat/!Backlog.md), п. 9; arch-01 — заглушка UI «Документы» |
|
| Доставка документов компании из 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 |
|
| Интеграция с SMS-провайдерами (отправка OTP, отключение `KEYCLOAK_OTP_MOCK_*`) | [`!Backlog.md`](../../HAN_chat/!Backlog.md), п. 10 |
|
||||||
|
| Изоляция `bitrix-sync` на отдельную VM | [`!Backlog.md`](../../HAN_chat/!Backlog.md), п. 8 |
|
||||||
|
|
||||||
|
|
||||||
|
## Открытые пробелы
|
||||||
|
|
||||||
|
| # | Пробел | Статус |
|
||||||
|
|---|---|---|
|
||||||
|
| G8 | Явный список `is_public=true` для ключей `app_settings` | Отложить до оформления сервисов; seed в модуле `database` |
|
||||||
|
|
||||||
## Обновление документации
|
## Обновление документации
|
||||||
|
|
||||||
- Изменение MVP → arch-01 + arch-02 (+ arch-03/arch-04 при необходимости).
|
- Изменение MVP → arch-01 + arch-02 (+ arch-03/arch-04 при необходимости).
|
||||||
- Новый env или ключ `app_settings` → arch-04.
|
- Новый env или ключ `app_settings` → arch-04.
|
||||||
- Новый термин → arch-00, затем поиск по arch-*.
|
- Новый термин / enum → arch-00, затем поиск по arch-*.
|
||||||
|
- Закрытие пробела → убрать из «Открытые пробелы» и отразить решение в arch-*.
|
||||||
|
|||||||
@@ -22,12 +22,13 @@
|
|||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `UserIdentity` | `han_app` | Локальный пользователь, связь с Keycloak |
|
| `UserIdentity` | `han_app` | Локальный пользователь, связь с Keycloak |
|
||||||
| `UxSession` | `han_app` | Аналитическая UX-сессия (период активности пользователя) |
|
| `UxSession` | `han_app` | Аналитическая UX-сессия (период активности пользователя) |
|
||||||
| `UserConsent` | `han_app` | Запись о принятии согласий (до OTP) |
|
| `UserConsent` | `han_app` | Запись о принятии согласий (после OTP, привязка к `user_id`) |
|
||||||
| `ClientProfile` | `han_app` | Кэш профиля для UI |
|
| `ClientProfile` | `han_app` | Кэш профиля для UI |
|
||||||
| `Dialog` | `han_app` | Диалог клиента с Open Lines |
|
| `Dialog` | `han_app` | Диалог клиента с Open Lines |
|
||||||
| `Message` | `han_app` | Сообщение в диалоге |
|
| `Message` | `han_app` | Сообщение в диалоге |
|
||||||
| `MessageAttachment` | `han_app` | Вложение к сообщению |
|
| `MessageAttachment` | `han_app` | Вложение к сообщению |
|
||||||
| `sync_queue` | `han_app` | Очередь sync App → Bitrix24 |
|
| `sync_queue` | `han_app` | Очередь sync App → Bitrix24 |
|
||||||
|
| `safety_tasks` | `han_app` | Checkpoint sync-wait Message Safety (`task_id`) для recovery |
|
||||||
| `entity_external_mapping` | `han_app` | Маппинг App entity ↔ Bitrix entity |
|
| `entity_external_mapping` | `han_app` | Маппинг App entity ↔ Bitrix entity |
|
||||||
| `app_settings` | `han_app` | Бизнес-настройки |
|
| `app_settings` | `han_app` | Бизнес-настройки |
|
||||||
| `text_resources` | `han_app` | Тексты UI по мнемоникам |
|
| `text_resources` | `han_app` | Тексты UI по мнемоникам |
|
||||||
@@ -40,8 +41,9 @@
|
|||||||
|---|---|
|
|---|---|
|
||||||
| `dialog_id` | UUID диалога в приложении; **равен** `external_chat_id` в Open Lines |
|
| `dialog_id` | UUID диалога в приложении; **равен** `external_chat_id` в Open Lines |
|
||||||
| `external_chat_id` | Идентификатор чата для `bitrix-local-app` / `imconnector` |
|
| `external_chat_id` | Идентификатор чата для `bitrix-local-app` / `imconnector` |
|
||||||
| `keycloak_sub` | Subject JWT Keycloak; ключ `UserIdentity` |
|
| `keycloak_sub` | Subject JWT Keycloak (`sub`); ключ `UserIdentity` |
|
||||||
| `guest_session_id` | UUID гостевой сессии до OTP |
|
| `phone_number` | Auth-телефон пользователя; master — Keycloak; в App DB пишется из JWT claims при `bootstrap`, не из body клиента |
|
||||||
|
| `guest_session_id` | Опциональный локальный UUID на устройстве (UI); **не** auth и **не** открывает write API |
|
||||||
| `ux_session_id` | UUID **аналитической UX-сессии**; заголовок `X-Ux-Session-Id` |
|
| `ux_session_id` | UUID **аналитической UX-сессии**; заголовок `X-Ux-Session-Id` |
|
||||||
| `bitrix_contact_id` | ID Contact в Bitrix24 CRM |
|
| `bitrix_contact_id` | ID Contact в Bitrix24 CRM |
|
||||||
| `bitrix_chat_id` | ID чата Open Lines в Bitrix24 |
|
| `bitrix_chat_id` | ID чата Open Lines в Bitrix24 |
|
||||||
@@ -60,10 +62,10 @@
|
|||||||
|
|
||||||
- Идентификатор периода — **`ux_session_id`** (UUID).
|
- Идентификатор периода — **`ux_session_id`** (UUID).
|
||||||
- Начало периода фиксируется событием **`session_start`** (**ровно один раз** на период).
|
- Начало периода фиксируется событием **`session_start`** (**ровно один раз** на период).
|
||||||
- Запись создаётся в App DB при `POST /api/v1/analytics/session-start` (см. arch-02).
|
- Запись создаётся в App DB при `POST /api/v1/analytics/session-start` (**только с JWT**, см. arch-02).
|
||||||
- Frontend передаёт **`X-Ux-Session-Id`** во всех запросах к backend, пока сессия активна.
|
- Frontend передаёт **`X-Ux-Session-Id`** во всех JWT-запросах к backend, пока сессия активна.
|
||||||
|
|
||||||
**`UxSession` не является механизмом авторизации.** Отсутствие или неизвестный `ux_session_id` **не** блокирует API (кроме endpoint, где id обязателен по контракту, напр. `POST /api/v1/consents`).
|
**`UxSession` не является механизмом авторизации.** Отсутствие или неизвестный `ux_session_id` **не** блокирует API (кроме endpoint, где id обязателен по контракту). Сам endpoint `session-start` без JWT недоступен.
|
||||||
|
|
||||||
### Когда начинается **новая** `UxSession`
|
### Когда начинается **новая** `UxSession`
|
||||||
Новый **`ux_session_id`** + событие **`session_start`** — **только** если:
|
Новый **`ux_session_id`** + событие **`session_start`** — **только** если:
|
||||||
@@ -76,17 +78,41 @@
|
|||||||
- успешный OTP или refresh access token;
|
- успешный OTP или refresh access token;
|
||||||
- навигация между экранами внутри приложения.
|
- навигация между экранами внутри приложения.
|
||||||
|
|
||||||
|
## `Dialog.status`
|
||||||
|
|
||||||
|
| Значение | Смысл |
|
||||||
|
|---|---|
|
||||||
|
| `open` | Диалог создан, сообщений ещё нет (сразу после `POST /dialogs`) |
|
||||||
|
| `waiting_for_company` | Последнее значимое событие — исходящее от клиента; ждём оператора |
|
||||||
|
| `waiting_for_client` | Последнее значимое событие — входящее от оператора; ждём клиента |
|
||||||
|
| `closed` | Диалог закрыт в Open Lines (`dialog.closed` / `ONIMCONNECTORDIALOGFINISH`) |
|
||||||
|
|
||||||
|
Переходы задаёт `api-backend` (см. arch-01, потоки чата). Значения — **строковые enum** в API и App DB (не sequence-справочник).
|
||||||
|
|
||||||
## `Message` — enum и поля
|
## `Message` — enum и поля
|
||||||
|
|
||||||
| Имя | Допустимые значения |
|
| Имя | Допустимые значения |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `Message.sender_type` | `client`, `company` |
|
| `Message.sender_type` | `client`, `company` |
|
||||||
| `Message.safety_status` | `pending`, `allowed`, `blocked` (`needs_review` — зарезервирован, MVP не используется) |
|
| `Message.safety_status` | `pending`, `allowed`, `blocked` (`needs_review` — зарезервирован, MVP не используется) |
|
||||||
|
| `Message.delivery_status` | `accepted`, `processing`, `delivered`, `rejected`, `failed` |
|
||||||
| `Message.text` | текст сообщения; пустая строка для файлового сообщения |
|
| `Message.text` | текст сообщения; пустая строка для файлового сообщения |
|
||||||
| `content_kind` (логическое) | `text`, `file` — тип исходящего сообщения клиента (MVP) |
|
| `content_kind` (логическое) | `text`, `file` — тип исходящего сообщения клиента (MVP) |
|
||||||
|
|
||||||
Семантика `allow` / `deny` / `pending` в `message-safety` и HTTP-коды — [`arch-02-api-contracts.md`](arch-02-api-contracts.md).
|
Семантика `allow` / `deny` / `pending` в `message-safety` и HTTP-коды — [`arch-02-api-contracts.md`](arch-02-api-contracts.md).
|
||||||
|
|
||||||
|
### `Message.delivery_status` (смысл)
|
||||||
|
|
||||||
|
| Значение | Когда |
|
||||||
|
|---|---|
|
||||||
|
| `accepted` | Сообщение принято API, safety ещё не завершена или только начата |
|
||||||
|
| `processing` | Внутренний/transient на время sync-wait safety; клиенту на `POST .../messages` не отдаётся как финальный ответ |
|
||||||
|
| `delivered` | Финальный `allow`, сообщение ушло в Open Lines (или входящее от оператора сохранено) |
|
||||||
|
| `rejected` | Финальный `deny` от Message Safety |
|
||||||
|
| `failed` | Инфраструктурная ошибка доставки (Bitrix/S3), не safety-deny |
|
||||||
|
|
||||||
|
Realtime-событие `message.status` передаёт актуальные `safety_status` и/или `delivery_status`.
|
||||||
|
|
||||||
## `MessageAttachment.scan_status`
|
## `MessageAttachment.scan_status`
|
||||||
|
|
||||||
| Значение | Смысл |
|
| Значение | Смысл |
|
||||||
@@ -96,6 +122,11 @@
|
|||||||
| `infected` | Проверка завершена, deny |
|
| `infected` | Проверка завершена, deny |
|
||||||
| `failed` | Ошибка инфраструктуры проверки |
|
| `failed` | Ошибка инфраструктуры проверки |
|
||||||
|
|
||||||
|
## Строковые enum vs справочники
|
||||||
|
|
||||||
|
- **Строковые enum** (значения в API/контрактах): `Dialog.status`, `Message.sender_type`, `Message.safety_status`, `Message.delivery_status`, `MessageAttachment.scan_status`, `content_kind`, `start_reason`.
|
||||||
|
- **Справочники (sequence ID)** — для больших/изменяемых списков UI и доменных классификаторов (типы документов post-MVP, причины и т.п.); правило — [`arch-05-agent-development-process.md`](arch-05-agent-development-process.md).
|
||||||
|
|
||||||
## Мнемоники internal API
|
## Мнемоники internal API
|
||||||
|
|
||||||
Префикс: **`/internal/{service_mnemonic}/v1/`**. Health: **`/health/*`**.
|
Префикс: **`/internal/{service_mnemonic}/v1/`**. Health: **`/health/*`**.
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ HAN Chat - приложение для мигрантов, где стартов
|
|||||||
3. Если у клиента сохранён **действующий refresh token**, frontend выполняет silent refresh **без OTP** (см. «Поток возврата пользователя»).
|
3. Если у клиента сохранён **действующий refresh token**, frontend выполняет silent refresh **без OTP** (см. «Поток возврата пользователя»).
|
||||||
4. Клиент нажимает популярный вопрос — frontend подставляет текст вопроса и **инициирует отправку сообщения** (тот же поток, что ручной ввод). Либо клиент вводит свой текст и отправляет.
|
4. Клиент нажимает популярный вопрос — frontend подставляет текст вопроса и **инициирует отправку сообщения** (тот же поток, что ручной ввод). Либо клиент вводит свой текст и отправляет.
|
||||||
5. Если клиент не авторизован, перед отправкой первого сообщения frontend показывает pop-up с согласиями и запускает OTP (см. «Поток авторизации»).
|
5. Если клиент не авторизован, перед отправкой первого сообщения frontend показывает pop-up с согласиями и запускает OTP (см. «Поток авторизации»).
|
||||||
6. После успешной авторизации api-backend создаёт или находит локального пользователя по `keycloak_sub`, связывает ранее сохранённые согласия с `guest_session_id`, создаёт или обновляет профиль; триггер App DB ставит задачу в `sync_queue` для `bitrix-sync`.
|
6. После успешной авторизации frontend с JWT вызывает **`POST /auth/bootstrap`** (в теле — принятые согласия): api-backend создаёт или находит локального пользователя по `keycloak_sub`, сохраняет согласия на `user_id`, создаёт или обновляет профиль; триггер App DB ставит задачу в `sync_queue` для `bitrix-sync`. Затем при необходимости — `POST /analytics/session-start`.
|
||||||
7. api-backend выполняет find-or-create диалога (см. «Создание диалога») и отправляет сообщение (текст популярного вопроса или введённый клиентом).
|
7. api-backend выполняет find-or-create диалога (см. «Создание диалога») и отправляет сообщение (текст популярного вопроса или введённый клиентом).
|
||||||
8. Сообщение клиента проходит Message Safety и через Bitrix24 Local App направляется в Битрикс24 Open Lines.
|
8. Сообщение клиента проходит Message Safety и через Bitrix24 Local App направляется в Битрикс24 Open Lines.
|
||||||
9. Ответ оператора из Битрикс24 Open Lines поступает через Bitrix24 Local App в api-backend и отображается в чате приложения.
|
9. Ответ оператора из Битрикс24 Open Lines поступает через Bitrix24 Local App в api-backend и отображается в чате приложения.
|
||||||
@@ -44,11 +44,11 @@ HAN Chat - приложение для мигрантов, где стартов
|
|||||||
- Keycloak: identity provider, OTP-only авторизация по номеру телефона.
|
- Keycloak: identity provider, OTP-only авторизация по номеру телефона.
|
||||||
- api-backend: Python-приложение с REST API, realtime-доставкой сообщений и бизнес-логикой.
|
- api-backend: Python-приложение с REST API, realtime-доставкой сообщений и бизнес-логикой.
|
||||||
- Nginx Reverse Proxy: единая публичная точка входа, HTTPS termination и маршрутизация на Keycloak/API/frontend web/Bitrix24.
|
- Nginx Reverse Proxy: единая публичная точка входа, HTTPS termination и маршрутизация на Keycloak/API/frontend web/Bitrix24.
|
||||||
- Message Safety Service: отдельный сервис проверки входящих сообщений; синхронный вызов из API → `200 allow` | `403 deny` | `203 pending` + `task_id`.
|
- Message Safety Service: отдельный сервис проверки входящих сообщений; вызов из API → `200 allow` | `403 deny` | `203 pending` + `task_id` (при `203` api-backend синхронно поллит task до финального вердикта, без очереди анализа на api-backend).
|
||||||
- Bitrix24 Local App: локальное приложение, custom connector `han_mobile_app` для Bitrix24 Open Lines: чат, OAuth, webhook-события, маппинг `dialog_id` ↔ `bitrix_chat_id`.
|
- Bitrix24 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).
|
- 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 на схему.
|
- Managed PostgreSQL (приватная сеть, одна база): схемы `han_app`, `bitrix_sync`, `bitrix_local`, `keycloak`, `message_safety` — отдельный DB-user на схему.
|
||||||
- Redis: rate limits, временные счетчики OTP и realtime/service coordination.
|
- Redis: rate limits API и realtime/service coordination (**не** OTP counters — они в Keycloak/SPI);
|
||||||
- S3-data: production-хранилище проверенных файлов чата (`han-chat-attachments`) и документов компании (`han-chat-documents`).
|
- S3-data: production-хранилище проверенных файлов чата (`han-chat-attachments`) и документов компании (`han-chat-documents`).
|
||||||
- S3-quarantine: временное хранилище загруженных файлов до вердикта Message Safety Service (`han-chat-quarantine`); read-only для `message-safety`.
|
- S3-quarantine: временное хранилище загруженных файлов до вердикта Message Safety Service (`han-chat-quarantine`); read-only для `message-safety`.
|
||||||
- observability: JSON-логи в stdout, `request_id`, `trace_id`, **`ux_session_id`** (если передан), базовая трассировка через OpenTelemetry Collector.
|
- observability: JSON-логи в stdout, `request_id`, `trace_id`, **`ux_session_id`** (если передан), базовая трассировка через OpenTelemetry Collector.
|
||||||
@@ -95,12 +95,13 @@ flowchart LR
|
|||||||
|
|
||||||
Client -->|HTTPS REST + Realtime| Nginx
|
Client -->|HTTPS REST + Realtime| Nginx
|
||||||
Nginx -->|/auth| Keycloak
|
Nginx -->|/auth| Keycloak
|
||||||
Nginx -->|/api + /realtime| API
|
Nginx -->|/api (REST + WS realtime)| API
|
||||||
Keycloak --> DB
|
Keycloak --> DB
|
||||||
API --> DB
|
API --> DB
|
||||||
API --> Redis
|
API --> Redis
|
||||||
API -->|upload / move / delete| S3Q
|
Client -->|presigned PUT| S3Q
|
||||||
API -->|promote delivered files| S3Data
|
API -->|presign / HeadObject / move / delete| S3Q
|
||||||
|
API -->|promote chat files| S3Data
|
||||||
API -->|internal check message| Safety
|
API -->|internal check message| Safety
|
||||||
Safety --> DB
|
Safety --> DB
|
||||||
Safety --> Redis
|
Safety --> Redis
|
||||||
@@ -113,7 +114,7 @@ flowchart LR
|
|||||||
Bitrix -->|ONIMCONNECTOR*| LocalApp
|
Bitrix -->|ONIMCONNECTOR*| LocalApp
|
||||||
LocalApp -->|imconnector.send.messages/status| Bitrix
|
LocalApp -->|imconnector.send.messages/status| Bitrix
|
||||||
LocalApp -->|normalized inbox events| API
|
LocalApp -->|normalized inbox events| API
|
||||||
API -->|WebSocket/SSE or polling fallback| Client
|
API -->|WebSocket or polling fallback| Client
|
||||||
API --> Obs
|
API --> Obs
|
||||||
Safety --> Obs
|
Safety --> Obs
|
||||||
Sync --> Obs
|
Sync --> Obs
|
||||||
@@ -131,12 +132,12 @@ flowchart LR
|
|||||||
- гостевой режим до первого сообщения;
|
- гостевой режим до первого сообщения;
|
||||||
- показ pop-up с обязательными согласиями на обработку персональных данных и пользовательское соглашение, а также необязательным согласием на рекламные коммуникации;
|
- показ pop-up с обязательными согласиями на обработку персональных данных и пользовательское соглашение, а также необязательным согласием на рекламные коммуникации;
|
||||||
- сбор данных устройства для передачи в backend;
|
- сбор данных устройства для передачи в backend;
|
||||||
- **управление аналитической UX-сессией** на клиенте: определение начала нового периода активности, хранение `ux_session_id` и `last_activity_at` **только в памяти**, отправка `session_start`, заголовок `X-Ux-Session-Id` во всех запросах;
|
- **управление аналитической UX-сессией** на клиенте: после получения JWT — `session_start`, хранение `ux_session_id` и `last_activity_at` **только в памяти**, заголовок `X-Ux-Session-Id` в JWT-запросах;
|
||||||
- хранение access token и refresh token в безопасном хранилище после авторизации;
|
- хранение access token и refresh token в безопасном хранилище после авторизации;
|
||||||
- **жизненный цикл access token**: проактивное обновление по расписанию (до истечения `exp`) и обработка **`401`** от `api-backend` (см. «Обновление access token (frontend)»);
|
- **жизненный цикл access token**: проактивное обновление по расписанию (до истечения `exp`) и обработка **`401`** от `api-backend` (см. «Обновление access token (frontend)»);
|
||||||
- при открытии приложения: проверку refresh token → silent refresh через Keycloak **или** OTP-flow при истечении refresh token;
|
- при открытии приложения: проверку refresh token → silent refresh через Keycloak **или** OTP-flow при истечении refresh token;
|
||||||
- отображение входящих сообщений от оператора;
|
- отображение входящих сообщений от оператора;
|
||||||
- загрузку файлов в чат через backend;
|
- загрузку файлов в чат: `init` → presigned PUT в S3 → `complete` (байты не через api-backend);
|
||||||
- работу с текстовыми мнемониками;
|
- работу с текстовыми мнемониками;
|
||||||
- отправку `traceparent`/correlation id в backend.
|
- отправку `traceparent`/correlation id в backend.
|
||||||
|
|
||||||
@@ -155,7 +156,7 @@ Frontend не должен:
|
|||||||
- локальную регистрацию пользователя приложения: `find-or-create` `UserIdentity` по `keycloak_sub`, создание минимального `ClientProfile` для нового пользователя, обновление `last_login_at` для существующего (после OTP — см. `POST /api/v1/auth/bootstrap`);
|
- локальную регистрацию пользователя приложения: `find-or-create` `UserIdentity` по `keycloak_sub`, создание минимального `ClientProfile` для нового пользователя, обновление `last_login_at` для существующего (после OTP — см. `POST /api/v1/auth/bootstrap`);
|
||||||
- **приём события `session_start`**: запись `UxSession`, audit/analytics-событие; **не** используется для контроля доступа;
|
- **приём события `session_start`**: запись `UxSession`, audit/analytics-событие; **не** используется для контроля доступа;
|
||||||
- валидация данных получаемых от frontend (соответствие типов данных, проверка обязательности полей, проверка формата данных, диапазоны значений, размер полей) через Pydantic
|
- валидация данных получаемых от frontend (соответствие типов данных, проверка обязательности полей, проверка формата данных, диапазоны значений, размер полей) через Pydantic
|
||||||
- хранение согласий пользователя в App DB (`guest_session_id`, **`ux_session_id`**, **`client_ip`**, версии документов);
|
- хранение согласий пользователя в App DB (**`user_id`**, **`ux_session_id`**, **`client_ip`**, версии документов) — только после JWT;
|
||||||
- профиль, структурированный блоками;
|
- профиль, структурированный блоками;
|
||||||
- API чата, истории, файлов и документов;
|
- API чата, истории, файлов и документов;
|
||||||
- realtime-доставку входящих сообщений клиенту;
|
- realtime-доставку входящих сообщений клиенту;
|
||||||
@@ -163,11 +164,15 @@ Frontend не должен:
|
|||||||
- прием нормализованных входящих событий Open Lines от Bitrix24 Local App;
|
- прием нормализованных входящих событий Open Lines от Bitrix24 Local App;
|
||||||
- хранение истории диалогов;
|
- хранение истории диалогов;
|
||||||
- запись данных профиля в App DB (синхронизация с Bitrix24 — триггеры → `sync_queue` → `bitrix-sync`, без участия api-backend);
|
- запись данных профиля в App DB (синхронизация с Bitrix24 — триггеры → `sync_queue` → `bitrix-sync`, без участия api-backend);
|
||||||
- загрузку файлов из чата в S3-quarantine до проверки;
|
- выдачу **presigned URL** на загрузку в S3-quarantine, проверку объекта при `complete`, promote/delete после вердикта;
|
||||||
- синхронный вызов Message Safety Service (`POST /internal/safety/v1/messages/check`) и интерпретацию ответа: `200 allow`, `403 deny`, `203 pending` + `task_id`;
|
- синхронный вызов Message Safety Service (`POST /internal/safety/v1/messages/check`) и интерпретацию ответа: `200 allow`, `403 deny`, `203 pending` + `task_id`;
|
||||||
- при `200`: перенос файлов quarantine → S3-data, сохранение сообщения, отправка в Bitrix24;
|
- при `200`: перенос файлов quarantine → S3-data, сохранение сообщения, отправка в Bitrix24;
|
||||||
- при `403`: удаление файлов из quarantine, безопасный ответ клиенту;
|
- при `403`: удаление файлов из quarantine, безопасный ответ клиенту;
|
||||||
- при `203`: сохранение сообщения со статусом ожидания проверки, ответ клиенту «обрабатывается», опрос `GET /internal/safety/v1/messages/tasks/{task_id}` и доставка цепочки после финального `200` или cleanup после `403`;
|
- при `203`: api-backend **синхронно поллит** `GET /internal/safety/v1/messages/tasks/{task_id}` до финального `200`/`403` (timeout budget — arch-04), затем promote/Bitrix или cleanup, и только после этого отвечает клиенту финальным результатом;
|
||||||
|
- это **ожидание в рамках одного клиентского HTTP-соединения**, а не общая очередь: другие запросы обрабатываются параллельно (workers/async);
|
||||||
|
- решение «быстрая проверка / долгая» принимает только `message-safety`; на api-backend **нет** очереди анализа сообщений;
|
||||||
|
- запись checkpoint в `safety_tasks` (App DB) на время poll — для recovery при timeout/crash (I1);
|
||||||
|
- circuit breaker + timeout budget на вызовы `message-safety` и `bitrix-local-app` (I2);
|
||||||
- auth-aware rate limits для сообщений, пользовательских и сервисных операций;
|
- auth-aware rate limits для сообщений, пользовательских и сервисных операций;
|
||||||
- аудит пользовательских действий;
|
- аудит пользовательских действий;
|
||||||
- единые ошибки и валидацию входных данных.
|
- единые ошибки и валидацию входных данных.
|
||||||
@@ -218,12 +223,25 @@ Frontend не должен:
|
|||||||
|
|
||||||
- OTP-only регистрацию и вход;
|
- OTP-only регистрацию и вход;
|
||||||
- OTP по номеру телефона; проверка кода — в Keycloak (заглушка `KEYCLOAK_OTP_MOCK_*` или SMS-провайдер, см. arch-04 и «Поток авторизации»);
|
- OTP по номеру телефона; проверка кода — в Keycloak (заглушка `KEYCLOAK_OTP_MOCK_*` или SMS-провайдер, см. arch-04 и «Поток авторизации»);
|
||||||
|
- продуктовые лимиты OTP (`otp.phone.*` из `app_settings`) через authenticator/SPI; счётчики попыток — в зоне Keycloak (Redis DB Keycloak/SPI или in-memory Keycloak), **не** в `api-backend`;
|
||||||
- хранение учетных записей;
|
- хранение учетных записей;
|
||||||
- выдачу и обновление токенов;
|
- выдачу и обновление токенов (access + refresh);
|
||||||
- настройку realm, clients, roles, policies.
|
- настройку realm, clients, roles, policies;
|
||||||
|
- публикацию OIDC discovery и JWKS для проверки JWT.
|
||||||
|
|
||||||
Парольная авторизация, magic link и социальные логины не входят в MVP.
|
Парольная авторизация, magic link и социальные логины не входят в MVP.
|
||||||
|
|
||||||
|
**Взаимодействия (MVP):**
|
||||||
|
|
||||||
|
| С кем | Направление | Назначение |
|
||||||
|
|---|---|---|
|
||||||
|
| Expo frontend | Frontend → Keycloak (`/auth/*` через nginx) | OTP login (Authorization Code + PKCE), Refresh Token Grant, logout |
|
||||||
|
| `api-backend` | api-backend → Keycloak JWKS/discovery | Валидация access token (issuer, audience, подпись); **не** вызывает Admin API в hot path |
|
||||||
|
| Managed PostgreSQL | Keycloak → схема `keycloak` | Пользователи IdP, сессии, realm |
|
||||||
|
| SMS-провайдер | Keycloak → SMS (post-MVP) | Доставка OTP; на MVP — mock code из `.env` |
|
||||||
|
|
||||||
|
Confidential **backend client** Keycloak (client credentials) в MVP **не обязателен**: S2S между нашими сервисами идёт по service tokens, не через Keycloak. Client можно завести заранее в realm как optional для будущих admin/ops сценариев.
|
||||||
|
|
||||||
### Nginx Reverse Proxy
|
### Nginx Reverse Proxy
|
||||||
|
|
||||||
Отвечает за:
|
Отвечает за:
|
||||||
@@ -231,13 +249,13 @@ Frontend не должен:
|
|||||||
- прием внешнего HTTPS-трафика;
|
- прием внешнего HTTPS-трафика;
|
||||||
- TLS termination;
|
- TLS termination;
|
||||||
- редирект HTTP на HTTPS (на веб-домене; для выделенного API-домена HTTP не допускается — см. «Принципы безопасности»);
|
- редирект HTTP на HTTPS (на веб-домене; для выделенного API-домена HTTP не допускается — см. «Принципы безопасности»);
|
||||||
- маршрутизацию `/api/*` и `/realtime/*` в api-backend;
|
- маршрутизацию `/api/*` в api-backend (включая `WS /api/v1/realtime`);
|
||||||
- маршрутизацию `/auth/*` или выделенного auth-домена в Keycloak;
|
- маршрутизацию `/auth/*` или выделенного auth-домена в Keycloak;
|
||||||
- маршрутизацию публичных `/bitrix/*` endpoint в `bitrix-local-app`;
|
- маршрутизацию публичных `/bitrix/*` endpoint в `bitrix-local-app`;
|
||||||
- маршрутизацию `/bitrix/sync/*` webhook endpoint в `bitrix-sync`;
|
- маршрутизацию `/bitrix/sync/*` webhook endpoint в `bitrix-sync`;
|
||||||
- защиту internal endpoint `bitrix-local-app` через private network или `nginx allowlist`;
|
- защиту internal endpoint `bitrix-local-app` через private network или `nginx allowlist`;
|
||||||
- отсутствие публичной маршрутизации к `message-safety` — сервис доступен только из внутренней Docker-сети;
|
- отсутствие публичной маршрутизации к `message-safety` — сервис доступен только из внутренней Docker-сети;
|
||||||
- передачу `X-Forwarded-For`, `X-Forwarded-Proto`, `X-Forwarded-Host`, `X-Request-ID`;
|
- передачу `X-Forwarded-For`, `X-Forwarded-Proto`, `X-Forwarded-Host`, `X-Request-ID` (если клиент не прислал `X-Request-ID` — nginx **генерирует** UUID и прокидывает upstream);
|
||||||
- базовые лимиты размера запроса и timeout;
|
- базовые лимиты размера запроса и timeout;
|
||||||
- грубые edge rate limits по IP, route и зоне риска;
|
- грубые edge rate limits по IP, route и зоне риска;
|
||||||
- TLS 1.2/1.3, HSTS, security headers и скрытие технологических заголовков;
|
- TLS 1.2/1.3, HSTS, security headers и скрытие технологических заголовков;
|
||||||
@@ -266,48 +284,49 @@ Frontend не должен:
|
|||||||
- сохранение сообщений, истории диалогов (CRM sync — зона `bitrix-sync`, не api-backend);
|
- сохранение сообщений, истории диалогов (CRM sync — зона `bitrix-sync`, не api-backend);
|
||||||
- доставку в Bitrix24 Open Lines и realtime клиенту;
|
- доставку в Bitrix24 Open Lines и realtime клиенту;
|
||||||
- проверку JWT, согласий, edge rate limits;
|
- проверку JWT, согласий, edge rate limits;
|
||||||
- polling `task_id` на стороне клиента — только api-backend (фоновый worker или internal loop).
|
- polling `task_id` на стороне клиента запрещён — только api-backend, и только **внутри** обработки `POST .../messages` (sync wait до финального вердикта).
|
||||||
|
|
||||||
api-backend не решает, sync или async нужна проверка: это определяет Message Safety Service по результатам фазы текста/ссылок и кэша файлов.
|
api-backend не решает, sync или async нужна проверка внутри Message Safety: это определяет Message Safety Service. Но для клиента `POST .../messages` всегда завершается финальным allow/deny (или ошибкой timeout/зависимости).
|
||||||
|
|
||||||
## Гостевая сессия (до JWT)
|
## Гостевой режим (до JWT)
|
||||||
|
|
||||||
До OTP frontend работает в гостевом режиме с локально сгенерированным **`guest_session_id`** (UUID v4):
|
До OTP frontend работает **локально** без записи согласий и UX-сессии в App DB:
|
||||||
|
|
||||||
- создаётся при первом запуске приложения, хранится в secure storage устройства;
|
- UI главного экрана, популярные вопросы и публичный контент — через `GET /api/v1/public/*` (без JWT);
|
||||||
- передаётся в `POST /api/v1/consents` вместе с согласиями и device metadata;
|
- pop-up согласий показывается **до** OTP, но факт принятия хранится **только на клиенте** до получения tokens;
|
||||||
- api-backend сохраняет согласия с привязкой к `guest_session_id` (TTL записи — 24 ч);
|
- **`POST /consents`**, **`POST /analytics/session-start`** и остальные write/API чата — **только с JWT**;
|
||||||
- после успешного OTP api-backend **связывает** записи согласий и device session с `UserIdentity` по `keycloak_sub`;
|
- опциональный локальный `guest_session_id` (UUID в secure storage) может использоваться frontend для своей аналитики/идемпотентности UI, но **не** является auth и **не** открывает backend write-endpoint.
|
||||||
- `guest_session_id` не используется для доступа к защищённым ресурсам после выдачи JWT.
|
|
||||||
|
|
||||||
## Аналитическая UX-сессия (`ux_session_id`)
|
## Аналитическая UX-сессия (`ux_session_id`)
|
||||||
|
|
||||||
**UX-сессия** — период непрерывной активности пользователя в приложении для аналитики и сквозной трассировки. Это **не** сессия Keycloak, **не** refresh/access token и **не** механизм авторизации.
|
**UX-сессия** — период непрерывной активности **авторизованного** пользователя в приложении для аналитики и сквозной трассировки. Это **не** сессия Keycloak, **не** refresh/access token и **не** механизм авторизации.
|
||||||
|
|
||||||
### Роли компонентов
|
### Роли компонентов
|
||||||
|
|
||||||
**Frontend** (источник истины по правилам сессии):
|
**Frontend** (источник истины по правилам сессии):
|
||||||
|
|
||||||
- хранит `ux_session_id` и `last_activity_at` **только в памяти** (не в localStorage/secure storage);
|
- хранит `ux_session_id` и `last_activity_at` **только в памяти** (не в localStorage/secure storage);
|
||||||
- при новой сессии вызывает `POST /api/v1/analytics/session-start` и сохраняет полученный `ux_session_id`;
|
- вызывает `POST /api/v1/analytics/session-start` **только при наличии JWT** (после OTP или silent refresh);
|
||||||
|
- в гостевом режиме `session-start` **не** вызывается;
|
||||||
|
- при новой сессии сохраняет полученный `ux_session_id`;
|
||||||
- обновляет `last_activity_at` при пользовательской активности и при возврате из фона;
|
- обновляет `last_activity_at` при пользовательской активности и при возврате из фона;
|
||||||
- при resume проверяет `(now - last_activity_at) > idle_timeout` → при превышении — новая сессия;
|
- при resume проверяет `(now - last_activity_at) > idle_timeout` → при превышении — новая сессия (снова с JWT);
|
||||||
- передаёт **`X-Ux-Session-Id`** во **всех** запросах к backend (public и JWT).
|
- передаёт **`X-Ux-Session-Id`** во **всех** JWT-запросах к backend, пока сессия активна.
|
||||||
|
|
||||||
**api-backend**:
|
**api-backend**:
|
||||||
|
|
||||||
1. принимает `session_start`, создаёт запись **`UxSession`**, возвращает `ux_session_id`;
|
1. принимает `session_start` **только с валидным JWT**, создаёт запись **`UxSession`** с `user_id`, возвращает `ux_session_id`;
|
||||||
2. пишет analytics/audit-событие `session_start` (без PII);
|
2. пишет analytics/audit-событие `session_start` (без PII);
|
||||||
3. включает `ux_session_id` из заголовка в JSON-логи (если передан);
|
3. включает `ux_session_id` из заголовка в JSON-логи (если передан);
|
||||||
4. **не** блокирует запросы при отсутствии или неизвестном `ux_session_id` — это не auth.
|
4. отсутствие `ux_session_id` **не** блокирует API (кроме endpoint, где id обязателен по контракту) — это не auth, но сам `session-start` без JWT недоступен.
|
||||||
|
|
||||||
`request_id` — один HTTP-запрос; `ux_session_id` — период UX-активности для аналитики и корреляции логов.
|
`request_id` — один HTTP-запрос; `ux_session_id` — период UX-активности для аналитики и корреляции логов.
|
||||||
|
|
||||||
## Поток возврата пользователя (без OTP)
|
## Поток возврата пользователя (без OTP)
|
||||||
|
|
||||||
1. Клиент открывает приложение (UX-сессия определяется по правилам выше, независимо от auth).
|
1. Клиент открывает приложение. Пока нет JWT — гостевой UI; `session-start` не вызывается.
|
||||||
2. Frontend проверяет наличие refresh token в secure storage.
|
2. Frontend проверяет наличие refresh token в secure storage.
|
||||||
3. Если refresh token **действителен** — frontend запрашивает новый access token у Keycloak (Refresh Token Grant), **OTP не показывается**.
|
3. Если refresh token **действителен** — frontend запрашивает новый access token у Keycloak (Refresh Token Grant), **OTP не показывается**, затем при необходимости `POST /analytics/session-start`.
|
||||||
4. Frontend работает как авторизованный пользователь (история, профиль, чат).
|
4. Frontend работает как авторизованный пользователь (история, профиль, чат).
|
||||||
5. Если refresh token **отсутствует или истёк** — клиент остаётся в гостевом режиме до сценария, требующего auth; при первом сообщении — «Поток авторизации» с OTP.
|
5. Если refresh token **отсутствует или истёк** — клиент остаётся в гостевом режиме до сценария, требующего auth; при первом сообщении — «Поток авторизации» с OTP.
|
||||||
|
|
||||||
@@ -343,35 +362,39 @@ api-backend не решает, sync или async нужна проверка: э
|
|||||||
|
|
||||||
## Создание диалога (MVP)
|
## Создание диалога (MVP)
|
||||||
|
|
||||||
|
- У пользователя **не более одного активного** диалога: статус `open` | `waiting_for_company` | `waiting_for_client`. Закрытые (`closed`) остаются в истории.
|
||||||
|
- `POST /api/v1/dialogs`: если активный диалог уже есть — возвращает его (`200` / idempotent), новый не создаёт; иначе создаёт (`201`, `status=open`).
|
||||||
- Диалог создаётся **лениво** при первой отправке сообщения авторизованным клиентом.
|
- Диалог создаётся **лениво** при первой отправке сообщения авторизованным клиентом.
|
||||||
- Frontend перед `POST .../messages` вызывает `POST /api/v1/dialogs` (idempotency key), получает `dialog_id` и использует его далее.
|
- Frontend перед `POST .../messages` вызывает `POST /api/v1/dialogs` (заголовок `Idempotency-Key`), получает `dialog_id` и использует его далее.
|
||||||
- Популярный вопрос: после auth тот же порядок — `POST /dialogs` → `POST .../messages` с текстом вопроса.
|
- Популярный вопрос: после auth тот же порядок — `POST /dialogs` → `POST .../messages` с текстом вопроса.
|
||||||
- `dialog_id` = `external_chat_id` для Open Lines (см. [`arch-00-glossary.md`](arch-00-glossary.md), «Идентификаторы»).
|
- `dialog_id` = `external_chat_id` для Open Lines (см. [`arch-00-glossary.md`](arch-00-glossary.md), «Идентификаторы»).
|
||||||
- При первой доставке в Bitrix24 `bitrix-local-app` создаёт запись `dialog_sessions`.
|
- При первой доставке в Bitrix24 `bitrix-local-app` создаёт запись `dialog_sessions`.
|
||||||
|
- Новый активный диалог после `closed` — снова через `POST /dialogs` (когда продукт это разрешит; MVP: один активный в любой момент).
|
||||||
|
|
||||||
## Поток авторизации (OTP)
|
## Поток авторизации (OTP)
|
||||||
|
|
||||||
Срабатывает, когда клиент **ещё не имеет действующего refresh token** (первый вход) или refresh token **истёк**. Если refresh token валиден — см. «Поток возврата пользователя».
|
Срабатывает, когда клиент **ещё не имеет действующего refresh token** (первый вход) или refresh token **истёк**. Если refresh token валиден — см. «Поток возврата пользователя».
|
||||||
1. Клиент находится в гостевом режиме (`guest_session_id` уже создан).
|
1. Клиент в гостевом режиме (только UI + `GET /api/v1/public/*`).
|
||||||
2. Клиент инициирует отправку сообщения (ручной ввод или популярный вопрос).
|
2. Клиент инициирует отправку сообщения (ручной ввод или популярный вопрос).
|
||||||
3. Frontend показывает pop-up с тремя согласиями.
|
3. Frontend показывает pop-up с тремя согласиями; факт принятия хранится **локально** до OTP.
|
||||||
4. Клиент обязан принять согласие на обработку персональных данных и пользовательское соглашение.
|
4. Клиент обязан принять согласие на обработку персональных данных и пользовательское соглашение.
|
||||||
5. Клиент может опционально согласиться на рекламные коммуникации.
|
5. Клиент может опционально согласиться на рекламные коммуникации.
|
||||||
6. Если обязательные согласия не даны, отправка блокируется.
|
6. Если обязательные согласия не даны, отправка блокируется.
|
||||||
7. Frontend вызывает `POST /api/v1/consents` с `guest_session_id`, версиями документов, device metadata, IP/user agent (через backend).
|
7. Frontend запрашивает публичные настройки и показывает форму ввода номера телефона (единственный канал MVP).
|
||||||
8. Frontend запрашивает публичные настройки и показывает форму ввода номера телефона (единственный канал MVP).
|
8. Keycloak запускает OTP-flow по телефону: клиент вводит номер, инициируется «отправка» OTP (при заглушке SMS фактически не уходит — см. arch-04).
|
||||||
9. Keycloak запускает OTP-flow по телефону: клиент вводит номер, инициируется «отправка» OTP (при заглушке SMS фактически не уходит — см. arch-04).
|
9. Лимиты OTP на **edge** — `nginx` (`NGINX_RATE_LIMIT_AUTH`); продуктовые лимиты `otp.phone.*` из `app_settings` применяются на стороне **Keycloak authenticator / SPI** (или обёртки OTP), не в `api-backend`. До интеграции SMS (mock OTP) достаточно edge + mock code.
|
||||||
10. Лимиты OTP проверяются по **`app_settings`** (`otp.phone.*`).
|
10. Клиент вводит OTP и отправляет его в Keycloak.
|
||||||
11. Клиент вводит OTP и отправляет его в Keycloak.
|
11. **Keycloak проверяет корректность введённого OTP**:
|
||||||
12. **Keycloak проверяет корректность введённого OTP**:
|
|
||||||
- при **`KEYCLOAK_OTP_MOCK_ENABLED=true`** (MVP и любой режим с включённой заглушкой): введённое значение должно **совпадать** с `KEYCLOAK_OTP_MOCK_CODE` из `.env`;
|
- при **`KEYCLOAK_OTP_MOCK_ENABLED=true`** (MVP и любой режим с включённой заглушкой): введённое значение должно **совпадать** с `KEYCLOAK_OTP_MOCK_CODE` из `.env`;
|
||||||
- при **`KEYCLOAK_OTP_MOCK_ENABLED=false`** (после интеграции с SMS-провайдером, см. бэклог): введённое значение должно **совпадать** с одноразовым OTP, сгенерированным Keycloak и отправленным провайдером на телефон клиента (с учётом TTL и лимита попыток).
|
- при **`KEYCLOAK_OTP_MOCK_ENABLED=false`** (после интеграции с SMS-провайдером, см. бэклог): введённое значение должно **совпадать** с одноразовым OTP, сгенерированным Keycloak и отправленным провайдером на телефон клиента (с учётом TTL и лимита попыток).
|
||||||
- при неверном коде Keycloak возвращает ошибку; frontend не получает tokens, шаг 13 не выполняется.
|
- при неверном коде Keycloak возвращает ошибку; frontend не получает tokens, шаг 12 не выполняется.
|
||||||
13. При успешной проверке frontend получает tokens через OIDC Authorization Code Flow with PKCE.
|
12. При успешной проверке frontend получает tokens через OIDC Authorization Code Flow with PKCE.
|
||||||
14. Frontend вызывает **`POST /api/v1/auth/bootstrap`** с JWT и `guest_session_id` (см. arch-02).
|
13. Frontend с JWT вызывает **`POST /api/v1/auth/bootstrap`** — в теле передаёт локально принятые согласия и device metadata (см. arch-02). api-backend атомарно: `find-or-create` по JWT `sub` (`keycloak_sub`), телефон из JWT claims (не из body) → сохранение `UserConsent` на `user_id` → минимальный профиль.
|
||||||
15. api-backend выполняет `find-or-create` пользователя, связывает согласия с `guest_session_id`, при необходимости привязывает `user_id` к текущей **`UxSession`** по `ux_session_id`.
|
14. Frontend вызывает **`POST /api/v1/analytics/session-start`** (если нужна новая UX-сессия) и далее работает с `X-Ux-Session-Id`.
|
||||||
16. Триггер App DB ставит задачу `contact.map_or_create` в `sync_queue`; `bitrix-sync` асинхронно находит или создает Contact в Битрикс24. Авторизация не должна синхронно зависеть от ответа Битрикс24 CRM.
|
15. Триггер App DB ставит задачу `contact.map_or_create` в `sync_queue`; `bitrix-sync` асинхронно находит или создает Contact в Битрикс24. Авторизация не должна синхронно зависеть от ответа Битрикс24 CRM.
|
||||||
17. Frontend создаёт диалог и отправляет отложенное сообщение (см. «Создание диалога» и поток чата).
|
16. Frontend создаёт диалог и отправляет отложенное сообщение (см. «Создание диалога» и поток чата).
|
||||||
|
|
||||||
|
Отдельный **`POST /api/v1/consents`** после первого входа нужен, когда пользователь заново принимает обновлённые версии документов (не часть OTP-flow).
|
||||||
|
|
||||||
## Поток работы с чатом: клиент -> Битрикс24
|
## Поток работы с чатом: клиент -> Битрикс24
|
||||||
|
|
||||||
@@ -386,20 +409,21 @@ api-backend не решает, sync или async нужна проверка: э
|
|||||||
|
|
||||||
**Файловое сообщение:**
|
**Файловое сообщение:**
|
||||||
|
|
||||||
1. Frontend инициализирует **одно** вложение (`POST .../attachments/init`), загружает файл; api-backend сохраняет его в **S3-quarantine**.
|
1. Frontend инициализирует **одно** вложение (`POST .../attachments/init`), получает **presigned PUT** в **S3-quarantine**, загружает байты **напрямую в S3**, затем вызывает `POST .../attachments/{attachment_id}/complete`.
|
||||||
2. Frontend отправляет `POST /api/v1/dialogs/{dialog_id}/messages` с `attachment_id` и `checksum` (поле `text` пустое).
|
2. Frontend отправляет `POST /api/v1/dialogs/{dialog_id}/messages` с `attachment_id` и `checksum` (поле `text` пустое).
|
||||||
3. Nginx и API применяют rate limits.
|
3. Nginx и API применяют rate limits.
|
||||||
4. API **синхронно** вызывает Message Safety Service — шаг проверки файла (текст и ссылки пропускаются, если `text` пуст).
|
4. API **синхронно** вызывает Message Safety Service — шаг проверки файла (текст и ссылки пропускаются, если `text` пуст).
|
||||||
|
|
||||||
**Общая ветка вердикта (оба типа):**
|
**Общая ветка вердикта (оба типа):**
|
||||||
|
|
||||||
5. **`403 deny`**: API удаляет quarantine (если был файл), возвращает клиенту безопасную ошибку; в Bitrix24 ничего не уходит.
|
5. **`403 deny`**: API удаляет quarantine (если был файл), выставляет `safety_status=blocked`, `delivery_status=rejected`, возвращает клиенту безопасную ошибку; в Bitrix24 ничего не уходит.
|
||||||
6. **`200 allow`**: API переносит файл в S3-data (если был), сохраняет сообщение, отправляет в Bitrix24, подтверждает клиенту (realtime/polling).
|
6. **`200 allow`**: API переносит файл в S3-data attachments (если был), сохраняет сообщение (`safety_status=allowed`, `delivery_status=delivered`), отправляет в Bitrix24, подтверждает клиенту; `Dialog.status` → `waiting_for_company`.
|
||||||
7. **`203 pending` + `task_id`**: api-backend сохраняет сообщение со статусом ожидания проверки, отвечает клиенту, что сообщение обрабатывается; quarantine не трогает.
|
7. **`203 pending` + `task_id`**: api-backend пишет checkpoint в `safety_tasks` и **регулярно синхронно** вызывает `GET /internal/safety/v1/messages/tasks/{task_id}` (backoff), пока не получит финальный вердикт или не истечёт `MESSAGE_SAFETY_TASK_POLL_MAX_SEC`. Пока идёт poll, **этот** клиентский `POST .../messages` ещё не завершён (соединение ждёт). Параллельные запросы других клиентов **не** блокируются — общей очереди анализа на api-backend нет.
|
||||||
8. Фоновый процесс API опрашивает `GET /internal/safety/v1/messages/tasks/{task_id}`:
|
- финальный **`200 allow`** → как п. 6, затем ответ клиенту;
|
||||||
- финальный **`200 allow`** → S3-data, Bitrix24, статус «доставлено», realtime клиенту;
|
- финальный **`403 deny`** → как п. 5, затем ответ клиенту;
|
||||||
- финальный **`403 deny`** → удаление quarantine, статус «отклонено», уведомление клиенту;
|
- timeout / недоступность safety → `delivery_status=failed`, безопасная ошибка клиенту (`503` / `504`), quarantine не promote; recovery по `safety_tasks` — зона модуля.
|
||||||
- **`203 pending`** → повтор опроса с backoff.
|
|
||||||
|
Клиент на `POST .../messages` получает **только финальный** результат (или ошибку инфраструктуры), не промежуточное «обрабатывается».
|
||||||
|
|
||||||
## Поток работы с чатом: Битрикс24 -> клиент
|
## Поток работы с чатом: Битрикс24 -> клиент
|
||||||
|
|
||||||
@@ -408,9 +432,9 @@ api-backend не решает, sync или async нужна проверка: э
|
|||||||
3. `bitrix-local-app` проверяет `application_token`, нормализует payload и сохраняет idempotent inbox.
|
3. `bitrix-local-app` проверяет `application_token`, нормализует payload и сохраняет idempotent inbox.
|
||||||
4. `bitrix-local-app` обогащает событие данными из `dialog_sessions` и forward-ит в API, если `BITRIX_API_FORWARD_URL` включен.
|
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)).
|
5. api-backend находит локальный диалог по `external_chat_id` (= `dialog_id`, см. [`arch-00-glossary.md`](arch-00-glossary.md)).
|
||||||
6. api-backend сохраняет входящее сообщение в App DB (`sender_type=company`), а файл — в Selectel S3 (documents) с metadata в App DB. По факту сообщения API обновляет `Dialog.status`: входящее от оператора → `waiting_for_client`, исходящее от клиента → `waiting_for_company`.
|
6. api-backend сохраняет входящее сообщение в App DB (`sender_type=company`, `delivery_status=delivered`). Файл оператора (если есть) — в бакет **S3-data attachments** (`han-chat-attachments`) с metadata в `MessageAttachment`; бакет **documents** зарезервирован для документов компании в профиле (post-MVP). По факту сообщения API обновляет `Dialog.status`: входящее от оператора → `waiting_for_client`, исходящее от клиента → `waiting_for_company` (значения — arch-00).
|
||||||
7. `bitrix-local-app` подтверждает доставку в Bitrix24 через `imconnector.send.status.delivery`.
|
7. `bitrix-local-app` подтверждает доставку в Bitrix24 через `imconnector.send.status.delivery`.
|
||||||
8. api-backend публикует событие для frontend через WebSocket/SSE. Если realtime недоступен, frontend получает сообщение через polling `GET /api/v1/dialogs/{dialog_id}/messages?after=...`.
|
8. api-backend публикует событие для frontend через **WebSocket** (`WS /api/v1/realtime`). Если realtime недоступен, frontend получает сообщение через polling `GET /api/v1/dialogs/{dialog_id}/messages?after=...`.
|
||||||
9. Frontend отображает сообщение оператора в чате.
|
9. Frontend отображает сообщение оператора в чате.
|
||||||
10. При получении от `bitrix-local-app` доменного события `dialog.closed` (Bitrix24 `ONIMCONNECTORDIALOGFINISH`) API переводит `Dialog.status` в `closed`.
|
10. При получении от `bitrix-local-app` доменного события `dialog.closed` (Bitrix24 `ONIMCONNECTORDIALOGFINISH`) API переводит `Dialog.status` в `closed`.
|
||||||
|
|
||||||
@@ -453,7 +477,7 @@ App DB — **локальный кэш** для UI. Двусторонний syn
|
|||||||
|
|
||||||
## Аудит скачиваний
|
## Аудит скачиваний
|
||||||
|
|
||||||
При выдаче presigned URL на скачивание (`GET .../download-url`, вложения чата) api-backend пишет audit-событие в App DB:
|
При выдаче presigned URL на скачивание вложений чата (`GET /api/v1/dialogs/{dialog_id}/attachments/{attachment_id}/download-url`) и документов профиля (`GET /api/v1/documents/{document_id}/download-url`) api-backend пишет audit-событие в App DB:
|
||||||
|
|
||||||
| Поле | Значение |
|
| Поле | Значение |
|
||||||
|---|---|
|
|---|---|
|
||||||
@@ -471,14 +495,15 @@ App DB — **локальный кэш** для UI. Двусторонний syn
|
|||||||
|
|
||||||
Детальный контракт — [`arch-02-api-contracts.md`](arch-02-api-contracts.md), раздел «Realtime».
|
Детальный контракт — [`arch-02-api-contracts.md`](arch-02-api-contracts.md), раздел «Realtime».
|
||||||
|
|
||||||
- Transport: WebSocket `WS /api/v1/realtime` (JWT).
|
- Transport: **только WebSocket** `WS /api/v1/realtime` (JWT). SSE в MVP **не** используется.
|
||||||
|
- Путь входит в `/api/*`; отдельный location `/realtime/*` в nginx **не** нужен.
|
||||||
- Fallback: polling `GET /api/v1/dialogs/{dialog_id}/messages?after=...`.
|
- Fallback: polling `GET /api/v1/dialogs/{dialog_id}/messages?after=...`.
|
||||||
- События: новое сообщение, смена статуса сообщения/диалога.
|
- События: новое сообщение, смена `delivery_status` / `safety_status`, смена `Dialog.status`.
|
||||||
|
|
||||||
## Принципы безопасности
|
## Принципы безопасности
|
||||||
|
|
||||||
- Все защищенные пользовательские API требуют валидный JWT.
|
- Все защищенные пользовательские API требуют валидный JWT.
|
||||||
- Гостевые API доступны только для публичных настроек и стартового контента.
|
- Без JWT доступны **только** read-only публичные endpoint: `GET /api/v1/public/*` (rate limit + CORS + кэш). Write-endpoint (`consents`, `session-start`, чат, профиль и т.д.) требуют JWT.
|
||||||
- Все внешние пользовательские соединения работают через HTTPS.
|
- Все внешние пользовательские соединения работают через HTTPS.
|
||||||
- HTTP допускается только для веб-домена как вход для редиректа на HTTPS. Для api домена HTTP не допускается.
|
- HTTP допускается только для веб-домена как вход для редиректа на HTTPS. Для api домена HTTP не допускается.
|
||||||
- TLS завершается на reverse proxy; внутренний HTTP между контейнерами допускается только в закрытой backend-сети.
|
- TLS завершается на reverse proxy; внутренний HTTP между контейнерами допускается только в закрытой backend-сети.
|
||||||
@@ -487,17 +512,18 @@ App DB — **локальный кэш** для UI. Двусторонний syn
|
|||||||
- INPUT-validation на api-backend
|
- INPUT-validation на api-backend
|
||||||
- использовать только Параметризованные SQL-запросы
|
- использовать только Параметризованные SQL-запросы
|
||||||
- обязательное Экранирование вывода
|
- обязательное Экранирование вывода
|
||||||
- настройка CORS только на разрешенные домены (указать в .env)
|
- настройка CORS только на разрешённые домены (`security.cors.allowed_origins` в `app_settings`, см. arch-04)
|
||||||
- настройка Secure Headers (CSP, X-Frame-Options и др.)
|
- настройка Secure Headers (CSP, X-Frame-Options и др.)
|
||||||
- Доступ к профилю, диалогам, сообщениям, файлам и документам ограничен текущим `user_id`.
|
- Доступ к профилю, диалогам, сообщениям, файлам и документам ограничен текущим `user_id`.
|
||||||
- Все запросы, содержащие в себе ссылку на сущность, которая относится к конкретному пользователю (ИД продукта, услуги, чата, документа и тп), проверяются backend_api на соответствие тому пользователю, от которого пришел запрос.
|
- Все запросы, содержащие в себе ссылку на сущность, которая относится к конкретному пользователю (ИД продукта, услуги, чата, документа и тп), проверяются backend_api на соответствие тому пользователю, от которого пришел запрос.
|
||||||
- Все публичные id создаются в формате UUID.
|
- Все публичные id создаются в формате UUID.
|
||||||
- Сервисные API защищаются внутренней сетью Docker/VPC плюс service token (перечень переменных — [`arch-04-settings-and-content.md`](arch-04-settings-and-content.md), «Service tokens (internal API)»).
|
- Сервисные 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.
|
- Rate limits применяются минимум на двух уровнях: edge-лимиты в `nginx` и пользовательские лимиты в API с состоянием в Redis.
|
||||||
- Входящие сообщения пользователя: синхронный `POST /internal/safety/v1/messages/check` → `200` | `403` | `203`; при `203` API опрашивает `task_id` до финального вердикта.
|
- Входящие сообщения пользователя: синхронный `POST /internal/safety/v1/messages/check` → при `203` api-backend синхронно поллит `task_id` до финального `200`/`403` (или timeout); без очереди анализа на api-backend.
|
||||||
- Файлы пользователя до финального `allow` только в S3-quarantine; в S3-data — после `200 allow`.
|
- Файлы пользователя до финального `allow` только в S3-quarantine; в S3-data — после `200 allow`.
|
||||||
- Клиент **не пишет** напрямую в S3; загрузка только через `api-backend`.
|
- У клиента **нет** постоянных S3 credentials. Загрузка — **presigned PUT** в S3-quarantine, выданный `api-backend`; скачивание — **presigned GET**. Байты файла **не** проксируются через `api-backend`.
|
||||||
- `message-safety` — read-only к S3-quarantine, без прав записи в бакеты.
|
- `message-safety` — read-only к S3-quarantine, без прав записи в бакеты.
|
||||||
|
- Вызовы `message-safety` и `bitrix-local-app` защищены timeout budget и circuit breaker (см. arch-04).
|
||||||
- Все изменяемые параметры, телефоны, лимиты, mime types и флаги хранятся в настройках ([`arch-04-settings-and-content.md`](arch-04-settings-and-content.md)).
|
- Все изменяемые параметры, телефоны, лимиты, mime types и флаги хранятся в настройках ([`arch-04-settings-and-content.md`](arch-04-settings-and-content.md)).
|
||||||
- PII-данные не пишутся в логи в открытом виде.
|
- PII-данные не пишутся в логи в открытом виде.
|
||||||
- Документы и файлы чата должны иметь контроль доступа и аудит скачиваний.
|
- Документы и файлы чата должны иметь контроль доступа и аудит скачиваний.
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
- 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/*`.
|
- 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-мэппинг.
|
- 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`.
|
- Все service-to-service вызовы передают `X-Request-ID` и по возможности W3C `traceparent`.
|
||||||
- Frontend передаёт **`X-Ux-Session-Id`** во всех запросах к `api-backend`, когда UX-сессия активна (рекомендуется для аналитики и логов; **не** является auth).
|
- Frontend передаёт **`X-Ux-Session-Id`** во всех JWT-запросах к `api-backend`, когда UX-сессия активна (рекомендуется для аналитики и логов; **не** является auth). `session-start` и `consents` требуют JWT.
|
||||||
- Все internal API защищаются service token и закрытой Docker/VPC-сетью.
|
- Все internal API защищаются service token и закрытой Docker/VPC-сетью.
|
||||||
|
|
||||||
## Service tokens (internal API)
|
## Service tokens (internal API)
|
||||||
@@ -49,20 +49,21 @@
|
|||||||
|---|---|---|---|---|
|
|---|---|---|---|---|
|
||||||
| `GET /api/v1/public/app-config` | `api-backend` | Expo frontend | Публичные настройки: OTP, оператор, лимиты, файлы, **UX idle timeout** | public + CORS/rate limit |
|
| `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 |
|
| `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/consents` | `api-backend` | Expo frontend | Повторное сохранение согласий (новые версии документов); привязка к `user_id` | JWT + rate limit |
|
||||||
| `POST /api/v1/analytics/session-start` | `api-backend` | Expo frontend | Событие `session_start`, новая `UxSession` | public + rate limit |
|
| `POST /api/v1/analytics/session-start` | `api-backend` | Expo frontend | Событие `session_start`, новая `UxSession` (только для авторизованного пользователя) | JWT + rate limit |
|
||||||
| `POST /api/v1/auth/bootstrap` | `api-backend` | Expo frontend | После OTP: `find-or-create` пользователя, связь согласий, привязка `user_id` к `UxSession` | JWT |
|
| `POST /api/v1/auth/bootstrap` | `api-backend` | Expo frontend | После OTP: `find-or-create` пользователя + сохранение согласий из тела запроса | JWT |
|
||||||
| `POST /api/v1/dialogs` | `api-backend` | Expo frontend | Создание диалога перед первым сообщением (в т.ч. после популярного вопроса) | JWT + idempotency |
|
| `POST /api/v1/dialogs` | `api-backend` | Expo frontend | Создание диалога перед первым сообщением (в т.ч. после популярного вопроса) | JWT + idempotency |
|
||||||
| `GET /api/v1/me` | `api-backend` | Expo frontend | Профиль текущего клиента | JWT |
|
| `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/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}` | `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/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` | `api-backend` | Expo frontend | История диалогов | JWT |
|
||||||
| `GET /api/v1/dialogs/{dialog_id}` | `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 |
|
| `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}/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/init` | `api-backend` | Expo frontend | Инициализация загрузки; ответ: `attachment_id` + **presigned PUT** в S3-quarantine | JWT |
|
||||||
| `POST /api/v1/dialogs/{dialog_id}/attachments/{attachment_id}/complete` | `api-backend` | Expo frontend | Завершение загрузки и фиксация checksum/metadata | JWT |
|
| `POST /api/v1/dialogs/{dialog_id}/attachments/{attachment_id}/complete` | `api-backend` | Expo frontend | Подтверждение загрузки, проверка объекта в quarantine, фиксация checksum/metadata | JWT |
|
||||||
|
| `GET /api/v1/dialogs/{dialog_id}/attachments/{attachment_id}/download-url` | `api-backend` | Expo frontend | Presigned URL вложения чата; обязателен audit | JWT |
|
||||||
| `WS /api/v1/realtime` | `api-backend` | Expo frontend | Realtime-события чата, статусы доставки, unread | JWT |
|
| `WS /api/v1/realtime` | `api-backend` | Expo frontend | Realtime-события чата, статусы доставки, unread | JWT |
|
||||||
|
|
||||||
Единый формат ошибки:
|
Единый формат ошибки:
|
||||||
@@ -78,11 +79,27 @@
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### `POST /api/v1/consents` (тело запроса)
|
### `POST /api/v1/auth/bootstrap` (после OTP)
|
||||||
|
|
||||||
|
Вызывается **один раз** после успешного OTP и получения JWT. Создаёт локального пользователя и **сразу** сохраняет согласия из тела (атомарно в одной транзакции). **Не** создаёт UX-сессию — для этого `POST /analytics/session-start`.
|
||||||
|
|
||||||
|
**Заголовки:** `Authorization: Bearer <access_token>` — **единственный** источник идентичности пользователя.
|
||||||
|
|
||||||
|
**Как привязываются согласия (не через тело JSON):**
|
||||||
|
|
||||||
|
| Источник | Поле | Назначение |
|
||||||
|
|---|---|---|
|
||||||
|
| JWT claim `sub` | → `UserIdentity.keycloak_sub` | find-or-create пользователя; FK для `UserConsent.user_id` |
|
||||||
|
| JWT claim телефона | → `UserIdentity.phone_number` | номер, подтверждённый OTP в Keycloak (master auth-телефона) |
|
||||||
|
| Тело `consents` | версии / accepted | что именно принял пользователь |
|
||||||
|
| Тело `device` | metadata | устройство; не идентичность |
|
||||||
|
|
||||||
|
Телефон **не** передаётся в JSON body: клиент мог бы подставить чужой номер. api-backend читает телефон из **claims access token** Keycloak (канонический claim — по настройке realm; типично `phone_number` или `preferred_username` в E.164). Если claim отсутствует — **`400`** `phone_claim_missing`.
|
||||||
|
|
||||||
|
**Тело:**
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"guest_session_id": "550e8400-e29b-41d4-a716-446655440000",
|
|
||||||
"consents": {
|
"consents": {
|
||||||
"personal_data": { "accepted": true, "version": "2026-06-10" },
|
"personal_data": { "accepted": true, "version": "2026-06-10" },
|
||||||
"user_agreement": { "accepted": true, "version": "2026-06-10" },
|
"user_agreement": { "accepted": true, "version": "2026-06-10" },
|
||||||
@@ -96,20 +113,41 @@
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
После OTP api-backend связывает запись с `UserIdentity` по `guest_session_id` (в рамках `POST /api/v1/auth/bootstrap`). TTL guest-записи — 24 ч.
|
**Порядок после OTP:** `POST /auth/bootstrap` → `POST /analytics/session-start` → чат.
|
||||||
|
|
||||||
|
**Ответ `200`:** `{ "user_id": "uuid", "profile_ready": true }`.
|
||||||
|
|
||||||
|
**Ошибки:** `401` (JWT), `403` `consents_required` (обязательные согласия не `accepted: true`), `400` (невалидные версии/тело / нет phone claim).
|
||||||
|
|
||||||
|
### `POST /api/v1/consents` (повторное принятие)
|
||||||
|
|
||||||
|
Не часть OTP-flow. Используется, когда уже есть `UserIdentity` и нужно зафиксировать **новые версии** документов (или повторное принятие).
|
||||||
|
|
||||||
|
**Заголовки:** `Authorization: Bearer <access_token>`, `X-Ux-Session-Id` (если есть).
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"consents": {
|
||||||
|
"personal_data": { "accepted": true, "version": "2026-06-10" },
|
||||||
|
"user_agreement": { "accepted": true, "version": "2026-06-10" },
|
||||||
|
"marketing": { "accepted": false, "version": "2026-06-10" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
api-backend сохраняет согласия с привязкой к **`user_id`** из JWT. Пользователь должен уже существовать (`bootstrap` выполнен), иначе **`404`** / **`409`** по контракту модуля. Обязательные согласия без `accepted: true` → **`403`** `consents_required`.
|
||||||
|
|
||||||
### `POST /api/v1/analytics/session-start` (событие `session_start`)
|
### `POST /api/v1/analytics/session-start` (событие `session_start`)
|
||||||
|
|
||||||
Вызывается frontend **только** при начале **новой** UX-сессии (см. arch-01, «Аналитическая UX-сессия»). **Не** привязан к OTP и JWT.
|
Вызывается frontend **только** при начале **новой** UX-сессии у **авторизованного** пользователя (есть валидный JWT и обычно уже выполнен `bootstrap`). **Не** вызывается в гостевом режиме.
|
||||||
|
|
||||||
**Заголовки:** `X-Request-ID` (опционально).
|
**Заголовки:** `Authorization: Bearer <access_token>`, `X-Request-ID` (опционально).
|
||||||
|
|
||||||
**Тело:**
|
**Тело:**
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"start_reason": "first_launch",
|
"start_reason": "first_launch",
|
||||||
"guest_session_id": "550e8400-e29b-41d4-a716-446655440000",
|
|
||||||
"device": {
|
"device": {
|
||||||
"platform": "web",
|
"platform": "web",
|
||||||
"app_version": "1.0.0",
|
"app_version": "1.0.0",
|
||||||
@@ -118,8 +156,7 @@
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
- `start_reason` — обязательно: `first_launch` | `cold_start` | `idle_timeout`;
|
- `start_reason` — обязательно: `first_launch` | `cold_start` | `idle_timeout`.
|
||||||
- `guest_session_id` — опционально (если уже создан в гостевом режиме).
|
|
||||||
|
|
||||||
**Ответ `201`:**
|
**Ответ `201`:**
|
||||||
|
|
||||||
@@ -134,31 +171,26 @@ Frontend сохраняет `ux_session_id` **в памяти** и переда
|
|||||||
|
|
||||||
**Повторный вызов в рамках той же UX-сессии не требуется** (возврат из фона в пределах idle timeout).
|
**Повторный вызов в рамках той же UX-сессии не требуется** (возврат из фона в пределах idle timeout).
|
||||||
|
|
||||||
### `POST /api/v1/auth/bootstrap` (после OTP)
|
**Ошибки:** `401` без/с невалидным JWT.
|
||||||
|
|
||||||
Вызывается **один раз** после успешного 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 /api/v1/dialogs` — заголовок **`Idempotency-Key`** (обязателен); ответ `{ "dialog_id": "uuid", "status": "open" | "waiting_for_company" | "waiting_for_client" }`.
|
||||||
- Обязателен перед первым `POST .../messages` (включая популярный вопрос после auth).
|
- **Один активный диалог** на пользователя: если уже есть диалог со статусом не `closed`, endpoint возвращает его (idempotent), новый не создаёт.
|
||||||
|
- Обязателен перед первым `POST .../messages` (включая популярный вопрос после auth), если у клиента ещё нет `dialog_id`.
|
||||||
- `dialog_id` = `external_chat_id` (см. [`arch-00-glossary.md`](arch-00-glossary.md)).
|
- `dialog_id` = `external_chat_id` (см. [`arch-00-glossary.md`](arch-00-glossary.md)).
|
||||||
|
|
||||||
|
### Idempotency (G1)
|
||||||
|
|
||||||
|
| Правило | Значение |
|
||||||
|
|---|---|
|
||||||
|
| Заголовок | `Idempotency-Key` (UUID или opaque string ≤ 128 символов) |
|
||||||
|
| Endpoint | `POST /api/v1/dialogs`, `POST /api/v1/dialogs/{dialog_id}/messages` (и другие mutating POST по OpenAPI) |
|
||||||
|
| Хранение | Redis (DB `/0` api-backend): ключ → ответ / request fingerprint |
|
||||||
|
| TTL | **24 часа** |
|
||||||
|
| Повтор с тем же ключом и тем же телом | тот же HTTP-ответ, без повторного side-effect |
|
||||||
|
| Повтор с тем же ключом и **другим** телом | **`409`** `idempotency_key_reused` |
|
||||||
|
|
||||||
### Формат исходящего сообщения клиента (MVP)
|
### Формат исходящего сообщения клиента (MVP)
|
||||||
|
|
||||||
Имена `content_kind`, полей — [`arch-00-glossary.md`](arch-00-glossary.md). Правила:
|
Имена `content_kind`, полей — [`arch-00-glossary.md`](arch-00-glossary.md). Правила:
|
||||||
@@ -169,12 +201,28 @@ Frontend сохраняет `ux_session_id` **в памяти** и переда
|
|||||||
| `file` | `attachment_id` + `checksum`; `text` пустой | пустая строка | ровно 1 запись |
|
| `file` | `attachment_id` + `checksum`; `text` пустой | пустая строка | ровно 1 запись |
|
||||||
|
|
||||||
- непустой `text` **и** вложение → **`400`** `mixed_content_not_allowed` (до `message-safety`);
|
- непустой `text` **и** вложение → **`400`** `mixed_content_not_allowed` (до `message-safety`);
|
||||||
- пустое сообщение → **`403`** `empty_message`;
|
- пустое сообщение (нет `text` и нет `attachment_id`) → **`400`** `empty_message`;
|
||||||
- более одного вложения → **`400`** `too_many_attachments`;
|
- более одного вложения → **`400`** `too_many_attachments`;
|
||||||
- файловое сообщение в Bitrix24: `message.files` (signed URL), `message.text` пустой.
|
- файловое сообщение в Bitrix24: `message.files` (signed URL), `message.text` пустой.
|
||||||
|
|
||||||
Post-MVP: допускается «текст + файлы» отдельной версией API.
|
Post-MVP: допускается «текст + файлы» отдельной версией API.
|
||||||
|
|
||||||
|
### Загрузка вложения (MVP)
|
||||||
|
|
||||||
|
Байты файла идут **напрямую в S3-quarantine** по короткоживущему **presigned URL**. `api-backend` не проксирует тело файла: выдаёт URL, проверяет результат, управляет lifecycle (promote/delete).
|
||||||
|
|
||||||
|
1. `POST .../attachments/init` (JWT) → `{ attachment_id, upload_url, upload_headers?, expires_at }` — `upload_url` = **presigned PUT** (или POST policy) в **S3-quarantine**; ключ объекта и ограничения (bucket, key prefix, `Content-Type`, max size) задаёт `api-backend`.
|
||||||
|
2. Frontend загружает байты **напрямую в Selectel S3** по `upload_url` (не через `api-backend`).
|
||||||
|
3. `POST .../attachments/{attachment_id}/complete` с `checksum` (SHA-256) → api-backend проверяет наличие объекта в quarantine (HeadObject / размер / checksum), фиксирует metadata, `scan_status=pending`.
|
||||||
|
4. `POST .../messages` с `attachment_id` + `checksum` → Message Safety.
|
||||||
|
|
||||||
|
Правила безопасности:
|
||||||
|
|
||||||
|
- у клиента **нет** постоянных S3 access keys — только одноразовый/короткий presigned URL;
|
||||||
|
- presigned URL разрешает запись **только** в выделенный key в S3-quarantine (не в S3-data);
|
||||||
|
- TTL URL короткий (константа модуля / `app_settings`, ориентир минуты);
|
||||||
|
- скачивание из S3-data — отдельные **presigned GET** через `.../download-url` (с audit).
|
||||||
|
|
||||||
## Realtime (`WS /api/v1/realtime`)
|
## Realtime (`WS /api/v1/realtime`)
|
||||||
|
|
||||||
Transport: **WebSocket** over HTTPS (`wss://`), JWT в query `?access_token=` или subprotocol (реализация — в модуле `api-backend`).
|
Transport: **WebSocket** over HTTPS (`wss://`), JWT в query `?access_token=` или subprotocol (реализация — в модуле `api-backend`).
|
||||||
@@ -196,8 +244,8 @@ Transport: **WebSocket** over HTTPS (`wss://`), JWT в query `?access_token=` и
|
|||||||
| `type` | Назначение | Ключевые поля |
|
| `type` | Назначение | Ключевые поля |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `message.new` | Новое сообщение в диалоге | `dialog_id`, `message` (DTO как в REST) |
|
| `message.new` | Новое сообщение в диалоге | `dialog_id`, `message` (DTO как в REST) |
|
||||||
| `message.status` | Смена статуса доставки/safety | `dialog_id`, `message_id`, `status` |
|
| `message.status` | Смена `safety_status` / `delivery_status` | `dialog_id`, `message_id`, `safety_status`, `delivery_status` |
|
||||||
| `dialog.status` | Смена статуса диалога | `dialog_id`, `status` |
|
| `dialog.status` | Смена `Dialog.status` | `dialog_id`, `status` |
|
||||||
|
|
||||||
**Reconnect:**
|
**Reconnect:**
|
||||||
|
|
||||||
@@ -224,16 +272,30 @@ Transport: **WebSocket** over HTTPS (`wss://`), JWT в query `?access_token=` и
|
|||||||
|
|
||||||
## Frontend ↔ Keycloak
|
## Frontend ↔ Keycloak
|
||||||
|
|
||||||
|
Keycloak **обязателен** в production-like контуре с первого запуска (OTP, tokens, JWKS).
|
||||||
|
|
||||||
| Контракт | Владелец | Потребитель | Назначение |
|
| Контракт | Владелец | Потребитель | Назначение |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| OIDC Authorization Code Flow with PKCE | Keycloak | Expo frontend | OTP-only login, token issue, refresh |
|
| OIDC Authorization Code Flow with PKCE | Keycloak | Expo frontend | OTP-only login, token issue |
|
||||||
| OIDC Refresh Token Grant | Keycloak | Expo frontend | Обновление access token без OTP при действующем refresh token |
|
| OIDC Refresh Token Grant | Keycloak | Expo frontend | Обновление access token без OTP при действующем refresh token |
|
||||||
| OIDC logout | Keycloak | Expo frontend | Завершение сессии Keycloak, очистка tokens |
|
| OIDC logout | Keycloak | Expo frontend | Завершение сессии Keycloak, очистка tokens |
|
||||||
| JWKS / discovery | Keycloak | Expo frontend, `api-backend` | Проверка issuer, audience и ключей |
|
| OIDC Discovery (`/.well-known/openid-configuration`) | Keycloak | Expo frontend, `api-backend` | issuer, token/jwks endpoints |
|
||||||
|
| JWKS | Keycloak | `api-backend` | Проверка подписи access token (issuer, audience, exp) |
|
||||||
|
| OTP authenticator / SPI | Keycloak | — | Проверка OTP; product limits `otp.phone.*`; mock или SMS |
|
||||||
|
| PostgreSQL schema `keycloak` | Keycloak | Managed PostgreSQL | Учётные записи IdP |
|
||||||
|
|
||||||
Frontend не обращается напрямую к Keycloak DB и не хранит парольные credentials. Парольная авторизация в MVP отключена.
|
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.
|
**`api-backend` ↔ Keycloak:** только **валидация JWT** по JWKS/discovery (кэш ключей). Admin REST / User API Keycloak в hot path **не** используются. Телефон и `sub` для `bootstrap` берутся из claims access token.
|
||||||
|
|
||||||
|
**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)). Счётчики и product limits OTP — **только** Keycloak/SPI (+ nginx edge); `api-backend` OTP **не** проверяет и **не** ведёт OTP counters в Redis.
|
||||||
|
|
||||||
|
**Clients в realm (MVP):**
|
||||||
|
|
||||||
|
| Client | Тип | Назначение |
|
||||||
|
|---|---|---|
|
||||||
|
| Frontend (Expo) | public + PKCE | login / refresh / logout |
|
||||||
|
| Backend confidential | optional | не нужен для hot path; зарезервирован под будущие admin/ops S2S |
|
||||||
|
|
||||||
### Жизненный цикл access token (frontend)
|
### Жизненный цикл access token (frontend)
|
||||||
|
|
||||||
@@ -258,19 +320,31 @@ Frontend не обращается напрямую к Keycloak DB и не хр
|
|||||||
|
|
||||||
| Контракт | Владелец | Потребитель | Назначение | Защита |
|
| Контракт | Владелец | Потребитель | Назначение | Защита |
|
||||||
|---|---|---|---|---|
|
|---|---|---|---|---|
|
||||||
| `POST /internal/safety/v1/messages/check` | `message-safety` | `api-backend` | Синхронная проверка текста, ссылок и файлов по cache/rules | internal network + `X-Service-Token` |
|
| `POST /internal/safety/v1/messages/check` | `message-safety` | `api-backend` | Проверка текста, ссылок и файлов | internal network + `X-Service-Token` |
|
||||||
| `GET /internal/safety/v1/messages/tasks/{task_id}` | `message-safety` | `api-backend` | Опрос async-проверки файлов | internal network + `X-Service-Token` |
|
| `GET /internal/safety/v1/messages/tasks/{task_id}` | `message-safety` | `api-backend` | Опрос до финального вердикта **внутри** того же `POST .../messages` | internal network + `X-Service-Token` |
|
||||||
| Read S3-quarantine | Selectel S3 | `message-safety` | Чтение файла worker-ом при cache miss | read-only key |
|
| Read S3-quarantine | Selectel S3 | `message-safety` | Чтение файла worker-ом при cache miss | read-only key |
|
||||||
|
|
||||||
HTTP-семантика: `200 allow`, `403 deny`, `203 pending`. `api-backend` не выбирает sync/async режим, а только интерпретирует ответ.
|
HTTP-семантика от `message-safety`: `200 allow`, `403 deny`, `203 pending`.
|
||||||
|
|
||||||
Маппинг в App DB (`Message.safety_status` — см. [`arch-00-glossary.md`](arch-00-glossary.md)):
|
Поведение `api-backend`:
|
||||||
|
|
||||||
| HTTP / `message-safety` | `Message.safety_status` | Финальный? |
|
1. Синхронно вызывает `POST .../check`, получает один из трёх кодов.
|
||||||
|---|---|---|
|
2. При `200` / `403` — сразу завершает сценарий и отвечает клиенту.
|
||||||
| `200` / `allow` | `allowed` | да |
|
3. При `203` — **не ставит задачу в свою очередь анализа**; регулярно и синхронно поллит `GET .../tasks/{task_id}` до `200`/`403` или timeout (`MESSAGE_SAFETY_TASK_POLL_MAX_SEC`), затем отвечает клиенту.
|
||||||
| `403` / `deny` | `blocked` | да |
|
4. Решение «проверка быстрая или долгая» — только у `message-safety`. Ожидание poll держит **одно** клиентское HTTP-соединение; это не блокирует обработку других запросов (параллельные workers/async).
|
||||||
| `203` / `pending` | `pending` | нет |
|
|
||||||
|
Checkpoint: на время poll — запись в **`safety_tasks`** (`han_app`) для recovery при crash/timeout (I1), не очередь анализа.
|
||||||
|
|
||||||
|
Маппинг в App DB (`Message.safety_status` / `delivery_status` — см. [`arch-00-glossary.md`](arch-00-glossary.md)):
|
||||||
|
|
||||||
|
| HTTP / `message-safety` | `Message.safety_status` | `Message.delivery_status` (после завершения `POST .../messages`) | Финальный для клиента? |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `200` / `allow` | `allowed` | `delivered` (после успешной отправки в Open Lines) | да |
|
||||||
|
| `403` / `deny` | `blocked` | `rejected` | да |
|
||||||
|
| `203` → затем `200`/`403` | как финальный | как финальный | да (после sync-wait) |
|
||||||
|
| timeout / circuit open | `pending` или `blocked` по политике модуля | `failed` | да (ошибка инфраструктуры) |
|
||||||
|
|
||||||
|
Circuit breaker + timeout budget (I2): при открытом circuit на `message-safety` — не слать сообщение в Bitrix; вернуть клиенту безопасную ошибку зависимости.
|
||||||
|
|
||||||
## api-backend ↔ bitrix-local-app (Open Lines)
|
## api-backend ↔ bitrix-local-app (Open Lines)
|
||||||
|
|
||||||
@@ -286,6 +360,38 @@ HTTP-семантика: `200 allow`, `403 deny`, `203 pending`. `api-backend`
|
|||||||
|
|
||||||
`external_chat_id` всегда равен `dialog_id` приложения. `bitrix-sync` не участвует в hot path чата.
|
`external_chat_id` всегда равен `dialog_id` приложения. `bitrix-sync` не участвует в hot path чата.
|
||||||
|
|
||||||
|
### Inbox: входящие сообщения и файлы оператора (G5)
|
||||||
|
|
||||||
|
`POST /internal/openlines/v1/inbox` — нормализованное событие от `bitrix-local-app`. Минимальный контракт MVP:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"event_type": "message.new",
|
||||||
|
"external_chat_id": "uuid",
|
||||||
|
"bitrix_message_id": "string",
|
||||||
|
"occurred_at": "2026-07-09T12:00:00Z",
|
||||||
|
"message": {
|
||||||
|
"text": "текст оператора или пустая строка",
|
||||||
|
"files": [
|
||||||
|
{
|
||||||
|
"name": "scan.pdf",
|
||||||
|
"mime_type": "application/pdf",
|
||||||
|
"size_bytes": 12345,
|
||||||
|
"download_url": "https://..."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Правила:
|
||||||
|
|
||||||
|
- `event_type`: `message.new` | `dialog.closed` (и др. по OpenAPI модуля);
|
||||||
|
- idempotency по `(external_chat_id, bitrix_message_id)` на стороне `api-backend`;
|
||||||
|
- файлы оператора: api-backend скачивает по `download_url` (timeout budget) и сохраняет в **S3-data attachments** + `MessageAttachment`; MIME/size — те же продуктовые лимиты чата (`chat.attachments.*`) или отдельный allow-list модуля (зафиксировать в OpenAPI);
|
||||||
|
- пустой `text` и пустой `files` → reject события;
|
||||||
|
- детальная JSON Schema — в `bitrix-local-app/openapi.yaml` и `api-backend/openapi.yaml`.
|
||||||
|
|
||||||
## api-backend ↔ bitrix-sync
|
## api-backend ↔ bitrix-sync
|
||||||
|
|
||||||
**без синхронного HTTP** в пользовательских сценариях. Связь — PostgreSQL-триггеры в App DB → очередь `han_app.sync_queue` + прямой доступ `bitrix-sync` к `han_app` для write-back. `api-backend` **не создаёт** задачи синхронизации вручную.
|
**без синхронного HTTP** в пользовательских сценариях. Связь — PostgreSQL-триггеры в App DB → очередь `han_app.sync_queue` + прямой доступ `bitrix-sync` к `han_app` для write-back. `api-backend` **не создаёт** задачи синхронизации вручную.
|
||||||
@@ -348,7 +454,7 @@ HTTP-семантика: `200 allow`, `403 deny`, `203 pending`. `api-backend`
|
|||||||
| Selectel S3 `han-chat-quarantine` | Selectel S3 | Временное хранение вложений клиента до verdict |
|
| Selectel S3 `han-chat-quarantine` | Selectel S3 | Временное хранение вложений клиента до verdict |
|
||||||
| Selectel S3 `han-chat-attachments` | Selectel S3 | Проверенные файлы чата |
|
| Selectel S3 `han-chat-attachments` | Selectel S3 | Проверенные файлы чата |
|
||||||
| Selectel S3 `han-chat-documents` | Selectel S3 | Документы компании для клиента |
|
| Selectel S3 `han-chat-documents` | Selectel S3 | Документы компании для клиента |
|
||||||
| Redis | `redis` | rate limits, OTP counters, coordination/realtime state |
|
| Redis | `redis` | rate limits API (`/0`), realtime/coordination (опц. `/1`); **не** OTP counters |
|
||||||
|
|
||||||
## Observability-контракты
|
## Observability-контракты
|
||||||
|
|
||||||
@@ -367,8 +473,8 @@ HTTP-семантика: `200 allow`, `403 deny`, `203 pending`. `api-backend`
|
|||||||
| `event_type` | `session_start` |
|
| `event_type` | `session_start` |
|
||||||
| `ux_session_id` | UUID новой UX-сессии |
|
| `ux_session_id` | UUID новой UX-сессии |
|
||||||
| `start_reason` | `first_launch` / `cold_start` / `idle_timeout` |
|
| `start_reason` | `first_launch` / `cold_start` / `idle_timeout` |
|
||||||
| `guest_session_id` | UUID или null |
|
| `user_id` | из JWT |
|
||||||
| `user_id` | null (до auth bootstrap) |
|
| `guest_session_id` | не используется (endpoint только с JWT) |
|
||||||
| `request_id` | из `X-Request-ID` |
|
| `request_id` | из `X-Request-ID` |
|
||||||
| `ip`, `user_agent` | из proxy headers |
|
| `ip`, `user_agent` | из proxy headers |
|
||||||
|
|
||||||
@@ -376,7 +482,7 @@ Raw OTP и полный номер телефона в audit **не** пишут
|
|||||||
|
|
||||||
### Audit: выдача download URL
|
### Audit: выдача download URL
|
||||||
|
|
||||||
При `GET /api/v1/documents/{document_id}/download-url` и аналогичных endpoint вложений чата api-backend создаёт запись:
|
При `GET /api/v1/documents/{document_id}/download-url` и `GET /api/v1/dialogs/{dialog_id}/attachments/{attachment_id}/download-url` api-backend создаёт запись:
|
||||||
|
|
||||||
| Поле | Пример |
|
| Поле | Пример |
|
||||||
|---|---|
|
|---|---|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
- Весь backend-контур поднимается **одной командой** `docker compose up -d` из корня репозитория (`backend/`).
|
- Весь backend-контур поднимается **одной командой** `docker compose up -d` из корня репозитория (`backend/`).
|
||||||
- Корневой `docker-compose.yml` — единственный источник правды для production-like среды. Отдельных compose-файлов для production-деплоя отдельных сервисов не должно быть.
|
- Корневой `docker-compose.yml` — единственный источник правды для production-like среды. Отдельных compose-файлов для production-деплоя отдельных сервисов не должно быть.
|
||||||
- **Один `nginx`** поднимается из корневого `docker-compose.yml` и является единой публичной точкой входа с маршрутизацией на все сервисы:
|
- **Один `nginx`** поднимается из корневого `docker-compose.yml` и является единой публичной точкой входа с маршрутизацией на все сервисы:
|
||||||
- `/api/*`, `/realtime/*` → `api-backend`;
|
- `/api/*` → `api-backend` (REST и `WS /api/v1/realtime`; отдельный path `/realtime/*` **не** используется);
|
||||||
- `/auth/*` → `keycloak`;
|
- `/auth/*` → `keycloak`;
|
||||||
- `/bitrix/*` (public: `/bitrix/handler`, `/bitrix/install`, `/bitrix/placement`, `/health/*` для `bitrix-local-app`) → `bitrix-local-app`;
|
- `/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`;
|
- `/bitrix/sync/*` (public: webhook CRM sync для `bitrix-sync`) → `bitrix-sync`;
|
||||||
@@ -118,7 +118,7 @@ Reverse proxy и единственная публичная точка вход
|
|||||||
- **единый домен MVP** (напр. `tohin.ru` с путями `/api/*`, `/auth/*`, web): считается веб-доменом; порт `80` — только redirect на HTTPS для всего server block; после редиректа весь пользовательский трафик — HTTPS;
|
- **единый домен MVP** (напр. `tohin.ru` с путями `/api/*`, `/auth/*`, web): считается веб-доменом; порт `80` — только redirect на HTTPS для всего server block; после редиректа весь пользовательский трафик — HTTPS;
|
||||||
- **auth** на том же host, что API (`/auth/*`): следует политике host (redirect-only на :80 или HTTPS-only для выделенного API-host);
|
- **auth** на том же host, что API (`/auth/*`): следует политике host (redirect-only на :80 или HTTPS-only для выделенного API-host);
|
||||||
- **Bitrix callbacks** (`/bitrix/*`, `/bitrix/sync/*`): только HTTPS; порт `80` не обслуживает эти location — только redirect;
|
- **Bitrix callbacks** (`/bitrix/*`, `/bitrix/sync/*`): только HTTPS; порт `80` не обслуживает эти location — только redirect;
|
||||||
- маршрутизирует `/api/*` и `/realtime/*` в `api-backend`;
|
- маршрутизирует `/api/*` в `api-backend` (включая WebSocket upgrade для `/api/v1/realtime`);
|
||||||
- маршрутизирует `/auth/*` в `keycloak` или проксирует отдельный auth-домен;
|
- маршрутизирует `/auth/*` в `keycloak` или проксирует отдельный auth-домен;
|
||||||
- маршрутизирует публичные `/bitrix/*` endpoint в `bitrix-local-app`;
|
- маршрутизирует публичные `/bitrix/*` endpoint в `bitrix-local-app`;
|
||||||
- маршрутизирует `/bitrix/sync/*` webhook endpoint в `bitrix-sync`;
|
- маршрутизирует `/bitrix/sync/*` webhook endpoint в `bitrix-sync`;
|
||||||
@@ -127,6 +127,7 @@ Reverse proxy и единственная публичная точка вход
|
|||||||
- **production-like / production**: отдаёт **статическую сборку Expo web** из volume или каталога (`/usr/share/nginx/html` или аналог); `index.html` + assets, SPA fallback `try_files $uri /index.html`;
|
- **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`);
|
- **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`;
|
- передает upstream-сервисам `Host`, `X-Real-IP`, `X-Forwarded-For`, `X-Forwarded-Proto`, `X-Forwarded-Host`, `X-Request-ID`;
|
||||||
|
- если входящий запрос **без** `X-Request-ID`, nginx **генерирует** UUID и устанавливает заголовок до proxy_pass (I3);
|
||||||
- задает разумные `proxy_connect_timeout`, `proxy_read_timeout`, `client_max_body_size`;
|
- задает разумные `proxy_connect_timeout`, `proxy_read_timeout`, `client_max_body_size`;
|
||||||
- применяет edge rate limits для auth, API и download endpoints;
|
- применяет edge rate limits для auth, API и download endpoints;
|
||||||
- ограничивает частоту соединений и размер тела запроса;
|
- ограничивает частоту соединений и размер тела запроса;
|
||||||
@@ -188,8 +189,8 @@ Python worker/service **двусторонней** синхронизации Ap
|
|||||||
- поддерживает graceful shutdown и rate limiting Bitrix REST;
|
- поддерживает graceful shutdown и rate limiting Bitrix REST;
|
||||||
- не блокирует пользовательский API при ошибках Битрикс24;
|
- не блокирует пользовательский API при ошибках Битрикс24;
|
||||||
- не участвует в OTP-flow, не создаёт `UserIdentity`/`ClientProfile`;
|
- не участвует в OTP-flow, не создаёт `UserIdentity`/`ClientProfile`;
|
||||||
- **не участвует** в hot path чата Open Lines.
|
- **не участвует** в hot path чата Open Lines;
|
||||||
- `bitrix-sync` должен быть подключаем через .env (если отключили, то синхронизация с битрикс24 не проводится; если не отключили - проводится)
|
- включается/отключается флагом **`BITRIX_SYNC_ENABLED`** в `.env` (default `true`): при `false` сервис не стартует или работает в no-op (синхронизация с Bitrix24 CRM не выполняется).
|
||||||
|
|
||||||
### bitrix-local-app
|
### bitrix-local-app
|
||||||
|
|
||||||
@@ -223,29 +224,33 @@ Python worker/service **двусторонней** синхронизации Ap
|
|||||||
|
|
||||||
### keycloak
|
### keycloak
|
||||||
|
|
||||||
Identity provider.
|
Identity provider. **Обязателен** в compose-контуре с первого запуска.
|
||||||
|
|
||||||
Требования:
|
Требования:
|
||||||
|
|
||||||
- отдельный realm для приложения;
|
- отдельный realm для приложения;
|
||||||
- отдельный frontend client с PKCE;
|
- отдельный frontend client с PKCE (обязателен);
|
||||||
- backend client для service-to-service сценариев;
|
- confidential backend client — **optional** (не используется в hot path MVP; S2S между сервисами — service tokens);
|
||||||
- публичный issuer должен соответствовать HTTPS URL, видимому frontend-приложению;
|
- публичный issuer должен соответствовать HTTPS URL, видимому frontend-приложению;
|
||||||
- включены proxy settings для работы за `nginx`;
|
- включены proxy settings для работы за `nginx`;
|
||||||
- импорт realm в local/dev;
|
- импорт realm в local/dev;
|
||||||
- использует managed PostgreSQL, схема `keycloak` (см. раздел «Managed PostgreSQL» выше);
|
- использует managed PostgreSQL, схема `keycloak` (см. раздел «Managed PostgreSQL» выше);
|
||||||
- healthcheck.
|
- OTP mock / SMS SPI — см. arch-04;
|
||||||
|
- healthcheck;
|
||||||
|
- взаимодействия — [`arch-02-api-contracts.md`](arch-02-api-contracts.md), «Frontend ↔ Keycloak», и [`arch-01-system-architecture.md`](arch-01-system-architecture.md), «Keycloak».
|
||||||
|
|
||||||
### redis
|
### redis
|
||||||
|
|
||||||
Очереди, кеш, rate limiting.
|
Кэш, rate limiting, coordination (не единственное хранилище бизнес-событий).
|
||||||
|
|
||||||
Требования:
|
Требования:
|
||||||
|
|
||||||
- не использовать как единственное надежное хранилище бизнес-событий;
|
- не использовать как единственное надежное хранилище бизнес-событий;
|
||||||
- хранить счетчики API-level rate limits;
|
- хранить счетчики API-level rate limits и idempotency keys (`api-backend`);
|
||||||
- поддерживать TTL для лимитных ключей;
|
- поддерживать TTL для лимитных и idempotency ключей;
|
||||||
- sync_queue хранится в PostgreSQL (`han_app`), Redis может использоваться для wake-up/locking/queue optimization.
|
- **не** хранить OTP counters для `api-backend` (OTP — зона Keycloak/SPI);
|
||||||
|
- sync_queue хранится в PostgreSQL (`han_app`), Redis может использоваться для wake-up/locking/queue optimization;
|
||||||
|
- разделение DB index (I4): см. arch-04 (`REDIS_URL`, `MESSAGE_SAFETY_REDIS_URL`).
|
||||||
|
|
||||||
### otel-collector
|
### otel-collector
|
||||||
|
|
||||||
@@ -261,9 +266,9 @@ Identity provider.
|
|||||||
|
|
||||||
Рекомендуемые сети:
|
Рекомендуемые сети:
|
||||||
|
|
||||||
- `public`: `nginx`, frontend dev access, внешний HTTPS entrypoint.
|
- `public`: `nginx`, `keycloak` (для прокси `/auth/*`), frontend static/dev access, внешний HTTPS entrypoint.
|
||||||
- `backend`: API, `message-safety`, `bitrix-sync`, `bitrix-local-app`, `redis` (managed PostgreSQL — вне compose, в VPC).
|
- `backend`: `api-backend`, `message-safety`, `bitrix-sync`, `bitrix-local-app`, `keycloak`, `redis` (managed PostgreSQL — вне compose, в VPC).
|
||||||
- `observability`: otel-collector.
|
- `observability`: `otel-collector` + сервисы, экспортирующие telemetry.
|
||||||
|
|
||||||
Базы данных, Redis, Keycloak internal port и API internal port не должны публиковаться наружу. `message-safety` доступен только внутри сети `backend`. Основной пользовательский путь должен идти через `nginx` и HTTPS.
|
Базы данных, Redis, Keycloak internal port и API internal port не должны публиковаться наружу. `message-safety` доступен только внутри сети `backend`. Основной пользовательский путь должен идти через `nginx` и HTTPS.
|
||||||
|
|
||||||
@@ -320,8 +325,9 @@ Identity provider.
|
|||||||
|
|
||||||
Рекомендуемая схема:
|
Рекомендуемая схема:
|
||||||
|
|
||||||
- **веб-домен** (MVP: `tohin.ru` или `app.example.ru`): `/api/*`, `/auth/*`, `/realtime/*`, web frontend; `:80` → redirect HTTPS; `:443` — TLS + маршрутизация;
|
- **веб-домен** (MVP: `tohin.ru` или `app.example.ru`): `/api/*` (REST + WS realtime), `/auth/*`, web frontend; `:80` → redirect HTTPS; `:443` — TLS + маршрутизация;
|
||||||
- **выделенный API-домен** (post-MVP, опционально): отдельный `server { listen 443 ssl; ... }` **без** `listen 80`; только `/api/*`, `/realtime/*`;
|
- **выделенный API-домен** (post-MVP, опционально): отдельный `server { listen 443 ssl; ... }` **без** `listen 80`; только `/api/*`;
|
||||||
|
- для `location` WebSocket (`/api/v1/realtime`): `proxy_http_version 1.1`, `Upgrade`/`Connection` headers, увеличенный `proxy_read_timeout`;
|
||||||
- домен или path `/bitrix/*` → `bitrix-local-app`; `/bitrix/sync/*` → `bitrix-sync`;
|
- домен или path `/bitrix/*` → `bitrix-local-app`; `/bitrix/sync/*` → `bitrix-sync`;
|
||||||
- `GET/POST /bitrix/handler` и `GET/POST /bitrix/install` доступны публично для Bitrix24;
|
- `GET/POST /bitrix/handler` и `GET/POST /bitrix/install` доступны публично для Bitrix24;
|
||||||
- `/bitrix/placement` доступен публично как заглушка UI настроек коннектора;
|
- `/bitrix/placement` доступен публично как заглушка UI настроек коннектора;
|
||||||
@@ -396,7 +402,7 @@ WAF не заменяет обязательные лимиты, валидац
|
|||||||
- `nginx`: на веб-домене — `301` с `:80` на HTTPS; на API-домене (если выделен) — `:80` не слушает; `:443` — HTTP 200/301 и успешная TLS handshake;
|
- `nginx`: на веб-домене — `301` с `:80` на HTTPS; на API-домене (если выделен) — `:80` не слушает; `:443` — HTTP 200/301 и успешная TLS handshake;
|
||||||
- `api-backend`: HTTP 200 от `/health/ready`;
|
- `api-backend`: HTTP 200 от `/health/ready`;
|
||||||
- `message-safety`: HTTP 200 от `/health/ready` (проверяет PostgreSQL, Redis, workers, read S3-quarantine);
|
- `message-safety`: HTTP 200 от `/health/ready` (проверяет PostgreSQL, Redis, workers, read S3-quarantine);
|
||||||
- `bitrix-sync`: процесс жив, подключение к App DB доступно;
|
- `bitrix-sync`: HTTP 200 от `/health/live` и `/health/ready` (ready — PostgreSQL + доступ к `sync_queue`);
|
||||||
- `bitrix-local-app`: HTTP 200 от `/health/live`, readiness показывает наличие OAuth-токенов после установки приложения;
|
- `bitrix-local-app`: HTTP 200 от `/health/live`, readiness показывает наличие OAuth-токенов после установки приложения;
|
||||||
- `keycloak`: health endpoint Keycloak; readiness — подключение к managed PostgreSQL;
|
- `keycloak`: health endpoint Keycloak; readiness — подключение к managed PostgreSQL;
|
||||||
- `redis`: `redis-cli ping`;
|
- `redis`: `redis-cli ping`;
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ Managed PostgreSQL **поднимается до** развёртывания п
|
|||||||
| Группа | Ключи |
|
| Группа | Ключи |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Auth | `auth.phone.enabled`, `auth.password.enabled` |
|
| Auth | `auth.phone.enabled`, `auth.password.enabled` |
|
||||||
| OTP (продукт) | `otp.phone.max_send_attempts_per_24h`, `otp.phone.min_seconds_between_attempts` |
|
| OTP (продукт; потребитель — Keycloak SPI, не api-backend) | `otp.phone.max_send_attempts_per_24h`, `otp.phone.min_seconds_between_attempts` |
|
||||||
| Оператор | `operator.call.phone` |
|
| Оператор | `operator.call.phone` |
|
||||||
| Consent | `consent.personal_data.*`, `consent.user_agreement.*`, `consent.marketing.*` |
|
| Consent | `consent.personal_data.*`, `consent.user_agreement.*`, `consent.marketing.*` |
|
||||||
| Файлы чата | `chat.attachments.*` |
|
| Файлы чата | `chat.attachments.*` |
|
||||||
@@ -118,7 +118,7 @@ chat.attachments.allowed_mime_types=image/jpeg,image/png,image/webp,image/heic,i
|
|||||||
chat.attachments.disallowed_extensions=svg,doc,docx,xls,xlsx,csv
|
chat.attachments.disallowed_extensions=svg,doc,docx,xls,xlsx,csv
|
||||||
chat.attachments.max_size_mb=5
|
chat.attachments.max_size_mb=5
|
||||||
chat.attachments.storage=selectel_s3
|
chat.attachments.storage=selectel_s3
|
||||||
chat.attachments.upload_mode=backend_controlled_upload
|
chat.attachments.upload_mode=presigned_put
|
||||||
chat.attachments.safety_scan_required=true
|
chat.attachments.safety_scan_required=true
|
||||||
|
|
||||||
rate_limit.message_send.per_user=30/minute
|
rate_limit.message_send.per_user=30/minute
|
||||||
@@ -196,9 +196,14 @@ KEYCLOAK_OTP_MOCK_ENABLED=true
|
|||||||
KEYCLOAK_OTP_MOCK_CODE=1234
|
KEYCLOAK_OTP_MOCK_CODE=1234
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Redis
|
# Redis (I4: раздельные DB index)
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
# /0 — api-backend: rate limits, idempotency
|
||||||
|
# /1 — api-backend realtime/coordination (опционально; можно совместить с /0)
|
||||||
|
# /2 — message-safety: verdict cache / workers
|
||||||
REDIS_URL=redis://redis:6379/0
|
REDIS_URL=redis://redis:6379/0
|
||||||
|
REDIS_REALTIME_URL=redis://redis:6379/1
|
||||||
|
MESSAGE_SAFETY_REDIS_URL=redis://redis:6379/2
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Service tokens (internal API) — все переменные только в backend/.env
|
# Service tokens (internal API) — все переменные только в backend/.env
|
||||||
@@ -211,15 +216,21 @@ BITRIX_API_FORWARD_TOKEN=change-me
|
|||||||
BITRIX_SYNC_SERVICE_TOKEN=change-me
|
BITRIX_SYNC_SERVICE_TOKEN=change-me
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# api-backend (интеграции)
|
# api-backend (интеграции + resilience I2)
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
BITRIX_LOCAL_APP_BASE_URL=http://bitrix-local-app:8080
|
BITRIX_LOCAL_APP_BASE_URL=http://bitrix-local-app:8080
|
||||||
BITRIX_API_INBOX_PATH=/internal/openlines/v1/inbox
|
BITRIX_API_INBOX_PATH=/internal/openlines/v1/inbox
|
||||||
MESSAGE_SAFETY_URL=http://message-safety:8080
|
MESSAGE_SAFETY_URL=http://message-safety:8080
|
||||||
|
MESSAGE_SAFETY_CIRCUIT_FAILURE_THRESHOLD=5
|
||||||
|
MESSAGE_SAFETY_CIRCUIT_OPEN_SEC=30
|
||||||
|
BITRIX_LOCAL_APP_CIRCUIT_FAILURE_THRESHOLD=5
|
||||||
|
BITRIX_LOCAL_APP_CIRCUIT_OPEN_SEC=30
|
||||||
|
BITRIX_LOCAL_APP_HTTP_TIMEOUT_SEC=10
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# bitrix-sync
|
# bitrix-sync
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
BITRIX_SYNC_ENABLED=true
|
||||||
BITRIX_SYNC_CRM_BASE_URL=https://han0107.bitrix24.ru
|
BITRIX_SYNC_CRM_BASE_URL=https://han0107.bitrix24.ru
|
||||||
BITRIX_SYNC_CRM_WEBHOOK_URL=change-me
|
BITRIX_SYNC_CRM_WEBHOOK_URL=change-me
|
||||||
BITRIX_SYNC_CONTACT_MAP_INTERVAL_SEC=60
|
BITRIX_SYNC_CONTACT_MAP_INTERVAL_SEC=60
|
||||||
@@ -243,6 +254,7 @@ BITRIX_APPLICATION_TOKEN=change-me
|
|||||||
# =============================================================================
|
# =============================================================================
|
||||||
# message-safety (technical)
|
# message-safety (technical)
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
# POST check timeout; poll interval/max — бюджет sync-wait внутри POST .../messages (G4)
|
||||||
MESSAGE_SAFETY_POST_TIMEOUT_SEC=5
|
MESSAGE_SAFETY_POST_TIMEOUT_SEC=5
|
||||||
MESSAGE_SAFETY_TASK_POLL_INTERVAL_SEC=2
|
MESSAGE_SAFETY_TASK_POLL_INTERVAL_SEC=2
|
||||||
MESSAGE_SAFETY_TASK_POLL_MAX_SEC=300
|
MESSAGE_SAFETY_TASK_POLL_MAX_SEC=300
|
||||||
@@ -280,10 +292,17 @@ OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317
|
|||||||
## Namespace переменных Bitrix
|
## 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.
|
- `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`.
|
- `api-backend`: `BITRIX_LOCAL_APP_BASE_URL`, `MESSAGE_SAFETY_URL`, circuit/timeout vars, Redis `/0`/`/1` + service tokens; **бизнес-настройки** — из `app_settings`. OTP counters **не** ведёт.
|
||||||
- `bitrix-sync`: `BITRIX_SYNC_*`, `BITRIX_SYNC_WEBHOOK_TOKEN`, `BITRIX_SYNC_SERVICE_TOKEN`.
|
- `bitrix-sync`: `BITRIX_SYNC_ENABLED`, `BITRIX_SYNC_*`, `BITRIX_SYNC_WEBHOOK_TOKEN`, `BITRIX_SYNC_SERVICE_TOKEN`.
|
||||||
- `bitrix-sync` не читает `BITRIX_CLIENT_ID` / `BITRIX_CLIENT_SECRET`.
|
- `bitrix-sync` не читает `BITRIX_CLIENT_ID` / `BITRIX_CLIENT_SECRET`.
|
||||||
|
|
||||||
|
### `BITRIX_SYNC_ENABLED`
|
||||||
|
|
||||||
|
| Значение | Поведение |
|
||||||
|
|---|---|
|
||||||
|
| `true` (default) | `bitrix-sync` обрабатывает `sync_queue` и принимает CRM webhook |
|
||||||
|
| `false` | синхронизация с Bitrix24 CRM не выполняется (сервис не стартует или no-op); чат Open Lines через `bitrix-local-app` **не** затрагивается |
|
||||||
|
|
||||||
## Разрешённые типы файлов чата (MVP)
|
## Разрешённые типы файлов чата (MVP)
|
||||||
|
|
||||||
Источник значений — ключи **`app_settings`** (раздел «Seed MVP»). Остальные arch-* **ссылаются сюда**.
|
Источник значений — ключи **`app_settings`** (раздел «Seed MVP»). Остальные arch-* **ссылаются сюда**.
|
||||||
|
|||||||
@@ -26,7 +26,8 @@
|
|||||||
- Исключения допускаются только для аудита, админки, технического восстановления и миграций.
|
- Исключения допускаются только для аудита, админки, технического восстановления и миграций.
|
||||||
- Прикладные сущности, имеют `id`, `created_at`, `updated_at`, `updater_user_id`.
|
- Прикладные сущности, имеют `id`, `created_at`, `updated_at`, `updater_user_id`.
|
||||||
- Системные таблицы (`app_settings`, `text_resources`, `popular_questions`, `sync_queue`, audit, справочники) могут использовать `user_id = NULL` или отдельное поле `actor_type` — по спецификации модуля `database`.
|
- Системные таблицы (`app_settings`, `text_resources`, `popular_questions`, `sync_queue`, audit, справочники) могут использовать `user_id = NULL` или отдельное поле `actor_type` — по спецификации модуля `database`.
|
||||||
- Для списков использовать справочники. Если значения в столбце могут принимать определенный набор значений, записывать их через ИД (sequence) и создавать справочник с расшифровкой ИД. Это существенно позволит экономить на размере таблиц.
|
- Для **строковых enum из arch-00** (`Dialog.status`, `Message.safety_status`, `Message.delivery_status`, `sender_type`, `scan_status` и т.п.) справочник sequence **не** обязателен: значения фиксированы контрактом API.
|
||||||
|
- Для больших/изменяемых списков (типы документов post-MVP, причины, классификаторы UI) — справочники с ID (sequence) и расшифровкой.
|
||||||
- Для часто используемых фильтров добавляются индексы.
|
- Для часто используемых фильтров добавляются индексы.
|
||||||
- Миграции не должны удалять данные без отдельного согласования.
|
- Миграции не должны удалять данные без отдельного согласования.
|
||||||
- Все юзеры должны иметь ИД, которое указывается в `updater_user_id` которое они меняют.
|
- Все юзеры должны иметь ИД, которое указывается в `updater_user_id` которое они меняют.
|
||||||
@@ -67,11 +68,11 @@ Raw OTP запрещено хранить в открытом виде: это
|
|||||||
- idempotency, если операция может повториться;
|
- idempotency, если операция может повториться;
|
||||||
- отсутствие секретов и PII в логах.
|
- отсутствие секретов и PII в логах.
|
||||||
|
|
||||||
Дополнительно:
|
Дополнительно для модулей с async/worker:
|
||||||
|
|
||||||
|
- идемпотентность обработки задач очереди;
|
||||||
|
- поведение при повторной доставке webhook;
|
||||||
## Definition of Done
|
- таймауты и retry/backoff.## Definition of Done
|
||||||
|
|
||||||
Модуль считается готовым, если:
|
Модуль считается готовым, если:
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user