606 lines
62 KiB
Markdown
606 lines
62 KiB
Markdown
# arch-01. Общая архитектура системы
|
||
|
||
> Термины — в [`arch-00-glossary.md`](arch-00-glossary.md).
|
||
> Приоритет документов — в [`README.md`](README.md).
|
||
> Безопасность размещения на VM, OS-роли, SSH, секреты и production-деплой — в [`arch-06-service-hosting-security.md`](arch-06-service-hosting-security.md).
|
||
|
||
## Назначение
|
||
|
||
HAN Chat - приложение для мигрантов, где стартовый экран знакомит клиента с сервисом и предлагает задать вопрос. Авторизация не требуется при первом входе: она запрашивается при попытке отправить первое сообщение, потому что в переписке могут обрабатываться персональные данные.
|
||
|
||
## Зафиксированные решения MVP
|
||
|
||
- Авторизация: только OTP по **номеру телефона** (email-канал в MVP не используется).
|
||
- Вторая сторона чата: Битрикс24 Open Lines.
|
||
- Master source auth-данных: Keycloak; профиль в UI — кэш App DB с двусторонней sync через `bitrix-sync`.
|
||
- Диалог приложения соответствует диалогу в Битрикс24 Open Lines.
|
||
- Лиды и сделки в MVP не используются.
|
||
- Файлы production-хранилища: Selectel S3, бакет **S3-data** (логическое имя; физически два бакета — `han-chat-attachments` для файлов чата и `han-chat-documents` для документов компании).
|
||
- Файлы до проверки: Selectel S3, бакет **S3-quarantine**; после `200 allow` — перенос в S3-data (attachments). Имена бакетов — [`arch-00-glossary.md`](arch-00-glossary.md); права доступа — ниже и в «Принципы безопасности».
|
||
- Мультиязычность в первом релизе не нужна, но тексты должны храниться по мнемоникам для будущих переводов.
|
||
- Среда на первом этапе одна и проектируется как боевая.
|
||
- Вложения чата MVP: **только изображения и PDF** — см. [`arch-04-settings-and-content.md`](arch-04-settings-and-content.md), «Разрешённые типы файлов чата».
|
||
- SMS OTP вводится поэтапно: до production rollout действует явный mock (`KEYCLOAK_OTP_MOCK_ENABLED=true`); целевой real mode — Keycloak генерирует/локально проверяет OTP и создаёт durable order в `sms-service`, а worker асинхронно вызывает i-Digital Direct. Контракт и gates — [`module-11-idgtl-sms.md`](../VM1_app/documentation/module-11-idgtl-sms.md).
|
||
- Популярный вопрос при выборе **автоматически отправляется как сообщение**; если пользователь не авторизован — сначала согласия и OTP, затем отправка.
|
||
- Notification Center v1 использует два контура: G — общие read-only гостевые кампании, P — персональные уведомления с состоянием в App DB. Виды, CTA, кнопки и палитра задаются каталогом данных.
|
||
- Инструкция `install_app` всегда открывается во внешней новой вкладке; iframe/модалка для неё не используется.
|
||
- Перечень таблиц и миграций схемы `han_app` проектирует `module-01-api-backend` и его migration owner; владельцы остальных сервисов проектируют свои схемы. Arch фиксирует только **разделение схем** PostgreSQL и контракты между сервисами.
|
||
|
||
## Пользовательские сценарии
|
||
|
||
1. Клиент открывает мобильное или web-приложение и видит главный экран с приветствием, популярными вопросами и полем ввода.
|
||
2. Frontend определяет, нужна ли **новая UX-сессия**, но до JWT не вызывает backend write-endpoint: клиент может изучить сервис без авторизации через guest UI и `GET /api/v1/public/*`.
|
||
3. Если у клиента сохранён **действующий refresh token**, frontend выполняет silent refresh **без OTP** (см. «Поток возврата пользователя»).
|
||
4. Клиент нажимает популярный вопрос — frontend подставляет текст вопроса и **инициирует отправку сообщения** (тот же поток, что ручной ввод). Либо клиент вводит свой текст и отправляет.
|
||
5. Если клиент не авторизован, перед отправкой первого сообщения frontend показывает pop-up с согласиями и запускает OTP (см. «Поток авторизации»).
|
||
6. После успешной авторизации frontend с JWT вызывает **`POST /api/v1/auth/bootstrap`** (в теле — принятые согласия): api-backend создаёт или находит локального пользователя по `keycloak_sub`, сохраняет согласия на `user_id`, создаёт или обновляет профиль; триггер App DB ставит задачу в `sync_queue` для `bitrix-sync`. Затем, если у frontend нет активной UX-сессии или она истекла, вызывается **`POST /api/v1/analytics/session-start`**.
|
||
7. api-backend выполняет find-or-create диалога (см. «Создание диалога») и отправляет сообщение (текст популярного вопроса или введённый клиентом).
|
||
8. Сообщение клиента проходит Message Safety и через Bitrix24 Local App направляется в Битрикс24 Open Lines.
|
||
9. Ответ оператора из Битрикс24 Open Lines поступает через Bitrix24 Local App в api-backend и отображается в чате приложения.
|
||
10. Клиент может открыть историю диалогов.
|
||
11. Клиент может открыть профиль, где данные структурированы блоками: «Личные данные» и «Документы». В дальнейшем могут добавляться новые блоки.
|
||
12. Редактирование профиля из профиля недоступно. Для изменения данных клиент переходит в чат и пишет запрос оператору.
|
||
|
||
## Компоненты верхнего уровня
|
||
|
||
- Expo App: единая frontend-кодовая база для iOS, Android и web.
|
||
- Keycloak: identity provider, OTP-only авторизация по номеру телефона.
|
||
- SMS Service: internal durable order API, шаблоны и бессрочный журнал SMS; отдельный worker вызывает i-Digital Direct, callback обновляет только журнал.
|
||
- api-backend: Python-приложение с REST API, realtime-доставкой сообщений и бизнес-логикой.
|
||
- Notification producers: сервисы приватной сети, создающие/отменяющие персональные уведомления через Internal API с отдельным Bearer token на `source`; `producer_test` используется только для smoke API.
|
||
- Nginx Reverse Proxy: независимые точки входа ВМ1 и ВМ2; ВМ1 обслуживает приложение/Open Lines, ВМ2 — CRM webhook `bitrix-sync` и private Message Safety API.
|
||
- Message Safety Service: отдельный сервис ВМ2 проверки исходящих сообщений; target v2 → `200 allow` | `403 deny` | `202 pending` + `Location`.
|
||
- Bitrix24 Local App: локальное приложение, custom connector `han_mobile_app` для Bitrix24 Open Lines: чат, OAuth, webhook-события, маппинг `dialog_id` ↔ `bitrix_chat_id`.
|
||
- Bitrix24 sync service: двусторонняя синхронизация App DB ↔ Битрикс24 CRM (Contact на MVP; маппинг ID, очередь через триггеры, webhook от роботов Bitrix24).
|
||
- Managed PostgreSQL (приватная сеть, одна база): схемы `han_app`, `bitrix_sync`, `bitrix_local`, `keycloak`, `message_safety`, `sms` — отдельный DB-user на схему.
|
||
- Redis: rate limits API и realtime/service coordination (**не** OTP counters — они в Keycloak/SPI);
|
||
- S3-data: production-хранилище проверенных файлов чата (`han-chat-attachments`) и документов компании (`han-chat-documents`).
|
||
- S3-quarantine: временное хранилище загруженных файлов до вердикта Message Safety Service (`han-chat-quarantine`); read-only для `message-safety`.
|
||
- observability: JSON-логи в stdout, `request_id`, `trace_id`, **`ux_session_id`** (если передан), базовая трассировка через OpenTelemetry Collector.
|
||
|
||
## Инфраструктура развёртывания (зафиксировано)
|
||
|
||
Production-like backend разделён на два контура в одной private network/VPC:
|
||
|
||
- **ВМ1 HAN Chat**: edge `nginx`, `api-backend`, `keycloak`, `sms-service`/worker, `bitrix-local-app`, Redis DB0/DB1 и локальный `otel-collector`;
|
||
- **ВМ2 Processing**: собственный public/private `nginx`, `message-safety` API/worker, `clamd`/`freshclam`, `bitrix-sync`, отдельный Redis Safety и локальный `otel-collector`;
|
||
- каждая VM имеет один root Compose project и отдельный root-owned systemd deployment unit;
|
||
- ВМ1 и ВМ2 имеют независимые public DNS/TLS ingress на своих nginx; ВМ2 публикует только exact CRM webhook;
|
||
- ВМ1 вызывает ВМ2 по private HTTPS с проверкой internal CA, service token, cloud SG и host firewall;
|
||
- Битрикс24 вызывает public nginx ВМ2 напрямую; CRM webhook не проходит через ВМ1 и не создаёт на ней трафик/зависимость.
|
||
|
||
ВМ2 является независимым контуром вспомогательных сервисов. При её недоступности отправка пользовательских сообщений и CRM sync приостанавливаются, но чтение истории, auth, realtime и приём сообщений оператора на ВМ1 продолжаются. Недоступность ВМ1 не мешает ВМ2 принимать CRM webhook и выполнять накопленные workflows. Fail-open для Message Safety запрещён.
|
||
|
||
Базы данных — **managed PostgreSQL** того же провайдера в **том же облачном кластере/VPC**, **без публичного доступа** из интернета. VM подключается к БД только по приватной сети.
|
||
|
||
Размещение нескольких сервисов на одной VM не делает их одним доверенным контуром. Для каждого контейнера сохраняются least privilege, отдельные секреты, минимальные Docker networks и запрет доступа к Docker socket/host root. Обязательный baseline VM и контейнеров — [`arch-06-service-hosting-security.md`](arch-06-service-hosting-security.md).
|
||
|
||
Схема данных в managed PostgreSQL (перечень таблиц внутри схем — в модульных спецификациях, не в arch-*):
|
||
|
||
| База / схема | Сервисы | Назначение схемы |
|
||
|---|---|---|
|
||
| одна база / `han_app` | `api-backend`, `bitrix-sync` (ограниченный GRANT) | прикладные данные приложения, очередь sync, audit |
|
||
| одна база / `bitrix_sync` | `bitrix-sync` | worker state, retry/dead letter, sync audit |
|
||
| одна база / `message_safety` | `message-safety` | verdict caches, safety tasks/audit, immutable versioned runtime config |
|
||
| одна база / `bitrix_local` | `bitrix-local-app` | OAuth, inbox, `dialog_sessions` |
|
||
| одна база / `keycloak` | Keycloak | учётные записи, realm, сессии IdP |
|
||
| одна база / `sms` | `sms-service`, `sms-worker` | шаблоны, runtime settings, бессрочный журнал отправки/доставки SMS |
|
||
|
||
Redis разделён по deployment boundary: DB0/DB1 остаются на ВМ1, отдельный Redis Safety находится на ВМ2. Оба являются ephemeral/coordination слоями; PostgreSQL остаётся durable source of truth. Selectel S3 — внешнее object storage: три бакета (`han-chat-quarantine`, `han-chat-attachments`, `han-chat-documents`); см. [`arch-00-glossary.md`](arch-00-glossary.md).
|
||
|
||
```mermaid
|
||
flowchart LR
|
||
client[Client] --> edge[VM1_EdgeNginx]
|
||
edge --> api[VM1_ApiBackend]
|
||
bitrix[Bitrix24] -->|"CRM webhook HTTPS"| publicGateway[VM2_PublicNginx]
|
||
publicGateway --> sync[BitrixSync]
|
||
api -->|"HTTPS 8443 + service token"| privateGateway[VM2_PrivateListener]
|
||
privateGateway --> safety[MessageSafetyApi]
|
||
safety --> worker[SafetyWorker]
|
||
worker --> clamd[Clamd]
|
||
worker --> s3q[S3Quarantine]
|
||
worker --> pg[ManagedPostgreSQL]
|
||
sync --> pg
|
||
sync --> bitrix
|
||
collector[VM2_OtelCollector] --> signoz[PrivateSigNoz]
|
||
```
|
||
|
||
## Контекстная схема
|
||
|
||
```mermaid
|
||
flowchart LR
|
||
Client[Expo Mobile/Web App]
|
||
Nginx[Nginx Reverse Proxy]
|
||
Keycloak[Keycloak OTP]
|
||
SMS[SMS Service]
|
||
SMSWorker[SMS Worker]
|
||
Direct[i-Digital Direct]
|
||
API[Python api-backend]
|
||
Safety[Message Safety Service]
|
||
DB[(PostgreSQL)]
|
||
Redis[(Redis)]
|
||
Sync[Bitrix24 sync service]
|
||
LocalApp[Bitrix24 Local App]
|
||
Bitrix[Bitrix24 CRM]
|
||
S3Data[(S3-data: attachments + documents)]
|
||
S3Q[(S3-quarantine)]
|
||
Obs[observability]
|
||
|
||
Client -->|HTTPS REST + Realtime| Nginx
|
||
Nginx -->|/auth| Keycloak
|
||
Nginx -->|exact POST /callbacks/idgtl/sms| SMS
|
||
Nginx -->|"/api REST + WS realtime"| API
|
||
Keycloak --> DB
|
||
Keycloak -->|durable SMS order| SMS
|
||
SMS --> DB
|
||
SMSWorker --> DB
|
||
SMSWorker -->|HTTPS POST /api/v1/message| Direct
|
||
Direct -->|delivery callback| Nginx
|
||
API --> DB
|
||
API --> Redis
|
||
Client -->|presigned PUT| S3Q
|
||
API -->|presign / HeadObject / move / delete| S3Q
|
||
API -->|promote chat files| S3Data
|
||
API -->|internal check message| Safety
|
||
Safety --> DB
|
||
Safety --> Redis
|
||
Safety -->|read scan| S3Q
|
||
API -->|send messages| LocalApp
|
||
API -->|App DB writes| DB
|
||
Sync -->|sync_queue + profile| DB
|
||
Sync -->|CRM Contact REST| Bitrix
|
||
Bitrix -->|robot webhook| Sync
|
||
Bitrix -->|ONIMCONNECTOR*| LocalApp
|
||
LocalApp -->|imconnector.send.messages/status| Bitrix
|
||
LocalApp -->|normalized inbox events| API
|
||
API -->|WebSocket or polling fallback| Client
|
||
API --> Obs
|
||
Safety --> Obs
|
||
Sync --> Obs
|
||
LocalApp --> Obs
|
||
LocalApp --> DB
|
||
```
|
||
|
||
## Архитектурные границы
|
||
|
||
### Frontend
|
||
|
||
Отвечает за:
|
||
|
||
- стартовый экран с приветствием, популярными вопросами, полем ввода, историей и профилем;
|
||
- гостевой режим до первого сообщения;
|
||
- показ pop-up с обязательными согласиями на обработку персональных данных (со ссылками на согласие и политику ПД) и пользовательское соглашение, а также необязательным согласием на рекламные коммуникации;
|
||
- сбор данных устройства для передачи в backend;
|
||
- **управление аналитической UX-сессией** на клиенте: после получения JWT — `session_start`, хранение `ux_session_id` и `last_activity_at` **только в памяти**, заголовок `X-Ux-Session-Id` в JWT-запросах;
|
||
- хранение access token и refresh token в безопасном хранилище после авторизации;
|
||
- **жизненный цикл access token**: проактивное обновление по расписанию (до истечения `exp`) и обработка **`401`** от `api-backend` (см. «Обновление access token (frontend)»);
|
||
- при открытии приложения: проверку refresh token → silent refresh через Keycloak **или** OTP-flow при истечении refresh token;
|
||
- отображение входящих сообщений от оператора;
|
||
- загрузку файлов в чат: `init` → presigned PUT в S3 → `complete` (байты не через api-backend);
|
||
- работу с текстовыми мнемониками;
|
||
- отправку `traceparent`/correlation id в backend.
|
||
|
||
Frontend не должен:
|
||
|
||
- хранить бизнес-логику синхронизации с Битрикс24;
|
||
- принимать решения о доступе к чужим документам или диалогам;
|
||
- обращаться напрямую к Битрикс24, Selectel S3 или базе данных.
|
||
|
||
### api-backend
|
||
|
||
Отвечает за:
|
||
|
||
- публичные настройки приложения для frontend;
|
||
- проверку JWT от Keycloak для защищенных операций; при истёкшем или невалидном access token — **`401`** (refresh выполняет frontend, не backend);
|
||
- локальную регистрацию пользователя приложения: `find-or-create` `UserIdentity` по `keycloak_sub`, создание минимального `ClientProfile` для нового пользователя, обновление `last_login_at` для существующего (после OTP — см. `POST /api/v1/auth/bootstrap`);
|
||
- **приём события `session_start`**: запись `UxSession`, audit/analytics-событие; **не** используется для контроля доступа;
|
||
- валидация данных получаемых от frontend (соответствие типов данных, проверка обязательности полей, проверка формата данных, диапазоны значений, размер полей) через Pydantic
|
||
- хранение согласий пользователя в App DB (**`user_id`**, nullable **`ux_session_id`**, **`client_ip`**, версии документов) — только после JWT; при bootstrap `ux_session_id=NULL` допустим, потому что новая UX-сессия создаётся следующим запросом;
|
||
- профиль, структурированный блоками;
|
||
- API чата, истории, файлов и документов;
|
||
- realtime-доставку входящих сообщений клиенту;
|
||
- отправку сообщений клиента в Open Lines через Bitrix24 Local App;
|
||
- прием нормализованных входящих событий Open Lines от Bitrix24 Local App;
|
||
- хранение истории диалогов;
|
||
- запись данных профиля в App DB (синхронизация с Bitrix24 — триггеры → `sync_queue` → `bitrix-sync`, без участия api-backend);
|
||
- выдачу **presigned URL** на загрузку в S3-quarantine, проверку объекта при `complete`, promote/delete после вердикта;
|
||
- вызов Message Safety v2 (`POST /internal/safety/v2/messages/check`) и интерпретацию `200 allow`, `403 deny`, `202 pending`;
|
||
- при `200`: перенос файлов quarantine → S3-data, сохранение сообщения, отправка в Bitrix24;
|
||
- при `403`: удаление файлов из quarantine, безопасный ответ клиенту;
|
||
- при `202`: api-backend **синхронно поллит** `Location` до финального `200`/`403`, terminal failed или timeout, затем 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 для сообщений, пользовательских и сервисных операций;
|
||
- аудит пользовательских действий;
|
||
- публичный каталог/гостевые кампании, JWT API Notification Center и Internal Create/Cancel; дедупликацию по бессрочной паре `(source, external_id)`;
|
||
- применение каталога уведомлений без ветвления по `notification_type`, пользовательские действия, документы и события `notification.created|updated|closed`;
|
||
- expire job и очистку upload drafts. При скрытии TTL задаёт `date_expired` только если оно отсутствует; существующая дата не меняется;
|
||
- единые ошибки и валидацию входных данных.
|
||
|
||
### Bitrix24 Local App
|
||
|
||
Отвечает за Open Lines (чат):
|
||
|
||
- регистрацию локального приложения Bitrix24;
|
||
- OAuth lifecycle Bitrix24 и хранение токенов портала;
|
||
- регистрацию и активацию custom connector `han_mobile_app` для открытой линии 8;
|
||
- прием публичных событий Bitrix24 `ONIMCONNECTOR*` на `/bitrix/handler`;
|
||
- нормализацию событий Open Lines в доменные события HAN;
|
||
- хранение `dialog_sessions`: связка `external_chat_id` (=`dialog_id` приложения) ↔ `bitrix_chat_id` ↔ `session_id`;
|
||
- хранение локального inbox до готовности API;
|
||
- internal API для api-backend: `POST /internal/openlines/v1/messages`, `GET /internal/openlines/v1/dialogs/{external_chat_id}`;
|
||
- forward нормализованных событий оператора в API (`BITRIX_API_FORWARD_URL`);
|
||
- вызовы `imconnector.send.messages` и `imconnector.send.status.delivery`.
|
||
|
||
Не отвечает за:
|
||
|
||
- CRM Contact mapping и синхронизацию прочих CRM-сущностей;
|
||
- сохранение сообщений и истории чата в App DB;
|
||
- realtime-доставку в Expo App;
|
||
- бизнес-логику профиля и документов.
|
||
|
||
### Bitrix24 sync service
|
||
|
||
Отвечает за асинхронную двустороннюю синхронизацию данных между App DB и Битрикс24 CRM по контракту [`module-07-bitrix-sync.md`](../VM2_services/documentation/module-07-bitrix-sync.md):
|
||
|
||
- **канонический mapping** и его историю в `bitrix_sync.entity_external_mapping`; App DB не хранит CRM Contact ID;
|
||
- **App DB → Bitrix24:** durable workflow для `contact.map_or_create`, `contact.update`, `contact.deactivate`;
|
||
- **исправление связи:** audited административный запрос запускает `contact.rebind`; прямой `UPDATE` mapping запрещён;
|
||
- **Bitrix24 → App DB:** durable webhook inbox, coalescing и reconciliation; запись профиля с transaction-local GUC `han.sync_suppress`;
|
||
- mastership по полям: телефон — App/Keycloak, `NAME`/citizenship/email — Битрикс24;
|
||
- batch, общий portal rate limiter, leases/fencing, retry до 24 часов и technical DLQ;
|
||
- business conflicts через смарт-процесс Битрикс24, technical failures через SigNoz;
|
||
- прямой доступ к схеме `han_app` и собственной `bitrix_sync`.
|
||
|
||
Не отвечает за:
|
||
|
||
- hot path чата Open Lines;
|
||
- OAuth lifecycle локального приложения Bitrix24;
|
||
- создание `UserIdentity` / `ClientProfile` в auth-flow;
|
||
- хранение `dialog_sessions`.
|
||
|
||
### Keycloak
|
||
|
||
Отвечает за:
|
||
|
||
- OTP-only регистрацию и вход;
|
||
- OTP по номеру телефона; генерация и локальная проверка кода, challenge lifecycle, limits и verify audit — в Keycloak;
|
||
- в real mode — заказ в `sms-service` по закрытому `POST /internal/sms/v1/send`; Keycloak ждёт только `200/202` + `sms_message_id`, не вызывает Direct и не читает provider statuses;
|
||
- продуктовые лимиты OTP (`otp.phone.*` из `app_settings`) через authenticator/SPI и settings bridge `api-backend` (см. arch-04); durable counters/challenges/events — в provider-owned таблицах schema `keycloak`, **не** в Redis и не в `api-backend`;
|
||
- хранение учетных записей;
|
||
- выдачу и обновление токенов (access + refresh);
|
||
- настройку realm, clients, roles, policies;
|
||
- публикацию OIDC discovery и JWKS для проверки JWT.
|
||
|
||
Парольная авторизация, magic link и социальные логины не входят в MVP.
|
||
|
||
**Взаимодействия (MVP):**
|
||
|
||
| С кем | Направление | Назначение |
|
||
|---|---|---|
|
||
| Expo frontend | Frontend → Keycloak (`/auth/*` через nginx) | OTP login (Authorization Code + PKCE), Refresh Token Grant, logout |
|
||
| `api-backend` | api-backend → Keycloak JWKS/discovery | Валидация access token (issuer, audience, подпись); **не** вызывает Admin API в hot path |
|
||
| Managed PostgreSQL | Keycloak → схема `keycloak` | Пользователи IdP, сессии, realm |
|
||
| `sms-service` | Keycloak → `sms-service` (real mode) | Durable order; service token, idempotency key и `sms_message_id` |
|
||
|
||
Confidential **backend client** Keycloak (client credentials) в MVP **не обязателен**: S2S между нашими сервисами идёт по service tokens, не через Keycloak. Client можно завести заранее в realm как optional для будущих admin/ops сценариев.
|
||
|
||
### Nginx Reverse Proxy (целевая двух-VM топология)
|
||
|
||
Отвечает за:
|
||
|
||
- прием внешнего HTTPS-трафика;
|
||
- TLS termination;
|
||
- редирект HTTP на HTTPS (на веб-домене; для выделенного API-домена HTTP не допускается — см. «Принципы безопасности»);
|
||
- маршрутизацию `/api/*` в api-backend (включая `WS /api/v1/realtime`);
|
||
- маршрутизацию `/auth/*` или выделенного auth-домена в Keycloak;
|
||
- на nginx ВМ1 — маршрутизацию только `/bitrix/handler`, `/bitrix/install`, `/bitrix/placement` в `bitrix-local-app`;
|
||
- на отдельном public nginx ВМ2 — маршрутизацию только exact `/bitrix/sync/webhook/contact` и `/bitrix/sync/webhook/alert` в `bitrix-sync`; ВМ1 эти paths не проксирует;
|
||
- маршрутизацию только exact `POST /callbacks/idgtl/sms` в `sms-service` по HTTPS, с allowlist актуального IP Direct и без логирования Basic Authorization;
|
||
- защиту internal endpoint `bitrix-local-app` через private network или `nginx allowlist`;
|
||
- отсутствие публичной маршрутизации к `message-safety`: `api-backend` ВМ1 вызывает private nginx ВМ2 `:8443` по HTTPS с internal CA и service token; Docker DNS/HTTP допустим только внутри ВМ2 за gateway;
|
||
- передачу `X-Forwarded-For`, `X-Forwarded-Proto`, `X-Forwarded-Host`, `X-Request-ID` (если клиент не прислал `X-Request-ID` — nginx **генерирует** UUID и прокидывает upstream);
|
||
- базовые лимиты размера запроса и timeout;
|
||
- грубые edge rate limits по IP, route и зоне риска;
|
||
- TLS 1.2/1.3, HSTS, security headers и скрытие технологических заголовков;
|
||
- кэширование публичных endpoint настроек и контента;
|
||
- запрет доступа к внутренним сервисам и техническим портам извне.
|
||
|
||
### Message Safety Service
|
||
|
||
Отвечает за:
|
||
|
||
- проверку **входящих сообщений от пользователя** (текст, ссылки, файлы);
|
||
- **внутреннюю** orchestration: синхронно текст и ссылки; при необходимости — async-проверка файлов;
|
||
- HTTP-контракт для api-backend:
|
||
- `200` — синхронная проверка завершена, **allow**;
|
||
- `403` — синхронная проверка завершена, **deny**;
|
||
- `202` + `task_id`/`Location` — нужна async-проверка;
|
||
- task GET: `200 allow` | `403 deny` | `202 pending` | terminal failed `503`;
|
||
- SHA-256 хеширование и lookup кэша вердиктов;
|
||
- отдельный pipeline проверки ссылок;
|
||
- запись verdict caches, `safety_task`, audit и immutable `config_versions` в схеме `message_safety`; runtime role не активирует config;
|
||
- target internal API: `POST /internal/safety/v2/messages/check`, `GET /internal/safety/v2/messages/tasks/{task_id}`.
|
||
|
||
Не отвечает за:
|
||
|
||
- загрузку файлов клиентом, presigned URL, перемещение quarantine → S3-data, удаление из quarantine;
|
||
- сохранение сообщений, истории диалогов (CRM sync — зона `bitrix-sync`, не api-backend);
|
||
- доставку в Bitrix24 Open Lines и realtime клиенту;
|
||
- проверку JWT, согласий, edge rate limits;
|
||
- polling `task_id` на стороне клиента запрещён — только api-backend, и только **внутри** обработки `POST .../messages` (sync wait до финального вердикта).
|
||
|
||
api-backend не решает, sync или async нужна проверка внутри Message Safety: это определяет Message Safety Service. Но для клиента `POST .../messages` всегда завершается финальным allow/deny (или ошибкой timeout/зависимости).
|
||
|
||
## Гостевой режим (до JWT)
|
||
|
||
До OTP frontend работает **локально** без записи согласий и UX-сессии в App DB:
|
||
|
||
- UI главного экрана, популярные вопросы и публичный контент — через `GET /api/v1/public/*` (без JWT);
|
||
- pop-up согласий показывается **до** OTP, но факт принятия хранится **только на клиенте** до получения tokens;
|
||
- **`POST /api/v1/consents`**, **`POST /api/v1/analytics/session-start`** и остальные write/API чата — **только с JWT**;
|
||
- опциональный локальный `guest_session_id` (UUID в secure storage) может использоваться frontend для своей аналитики/идемпотентности UI, но **не** является auth и **не** открывает backend write-endpoint.
|
||
|
||
## Аналитическая UX-сессия (`ux_session_id`)
|
||
|
||
**UX-сессия** — период непрерывной активности **авторизованного** пользователя в приложении для аналитики и сквозной трассировки. Это **не** сессия Keycloak, **не** refresh/access token и **не** механизм авторизации.
|
||
|
||
### Роли компонентов
|
||
|
||
**Frontend** (источник истины по правилам сессии):
|
||
|
||
- хранит `ux_session_id` и `last_activity_at` **только в памяти** (не в localStorage/secure storage);
|
||
- вызывает `POST /api/v1/analytics/session-start` **только при наличии JWT** (после OTP или silent refresh);
|
||
- в гостевом режиме `session-start` **не** вызывается;
|
||
- при новой сессии сохраняет полученный `ux_session_id`;
|
||
- обновляет `last_activity_at` при пользовательской активности и при возврате из фона;
|
||
- при resume проверяет `(now - last_activity_at) > idle_timeout` → при превышении — новая сессия (снова с JWT);
|
||
- передаёт **`X-Ux-Session-Id`** во **всех** JWT-запросах к backend, пока сессия активна.
|
||
|
||
**api-backend**:
|
||
|
||
1. принимает `session_start` **только с валидным JWT**, создаёт запись **`UxSession`** с `user_id`, возвращает `ux_session_id`;
|
||
2. пишет analytics/audit-событие `session_start` (без PII);
|
||
3. включает `ux_session_id` из заголовка в JSON-логи (если передан);
|
||
4. отсутствие `ux_session_id` **не** блокирует API (кроме endpoint, где id обязателен по контракту) — это не auth, но сам `session-start` без JWT недоступен.
|
||
|
||
`request_id` — один HTTP-запрос; `ux_session_id` — период UX-активности для аналитики и корреляции логов.
|
||
|
||
## Поток возврата пользователя (без OTP)
|
||
|
||
1. Клиент открывает приложение. Пока нет JWT — гостевой UI; `session-start` не вызывается.
|
||
2. Frontend проверяет наличие refresh token в secure storage.
|
||
3. Если refresh token **действителен** — frontend запрашивает новый access token у Keycloak (Refresh Token Grant), **OTP не показывается**, затем при необходимости начала новой UX-сессии вызывает `POST /api/v1/analytics/session-start`.
|
||
4. Frontend работает как авторизованный пользователь (история, профиль, чат).
|
||
5. Если refresh token **отсутствует или истёк** — клиент остаётся в гостевом режиме до сценария, требующего auth; при первом сообщении — «Поток авторизации» с OTP.
|
||
|
||
## Обновление access token (frontend)
|
||
|
||
Пока refresh token **действителен**, frontend **сам** поддерживает актуальный access token — **не** полагаясь только на открытие приложения и **не** дожидаясь истечения refresh token (использует его для обновления access token заранее).
|
||
|
||
### Проактивное обновление по расписанию
|
||
|
||
1. После получения tokens (OTP или refresh) frontend сохраняет access token, refresh token и момент истечения access token (`exp` из JWT или `expires_in` из ответа Keycloak).
|
||
2. Запускает таймер/scheduler: обновить access token **до** наступления `exp` (рекомендуемый запас — **60 с** до `exp`; константа модуля frontend).
|
||
3. По срабатыванию таймера — **Refresh Token Grant** к Keycloak, сохранение новой пары tokens, перепланирование следующего обновления.
|
||
4. **Single-flight:** параллельные refresh-запросы не дублируются (один in-flight refresh, остальные ждут результат).
|
||
5. Успешный refresh access token **не** создаёт новую UX-сессию и **не** вызывает `session_start`.
|
||
|
||
### Обработка `401` от `api-backend`
|
||
|
||
Если запрос с access token вернул **`401`** (токен уже истёк или отклонён):
|
||
|
||
1. HTTP-клиент frontend **один раз** инициирует Refresh Token Grant (если refresh ещё не выполняется — через тот же single-flight).
|
||
2. При успехе — подставляет новый access token и **повторяет исходный запрос** (без бесконечных retry).
|
||
3. При неудаче refresh (`invalid_grant`, истёк refresh token, ошибка Keycloak) — очищает tokens, переводит UI в **гостевой режим**; повторная авторизация — через OTP при следующем защищённом действии.
|
||
4. Запросы, пришедшие во время in-flight refresh, **ставятся в очередь** и выполняются после успешного обновления (или отклоняются при провале refresh).
|
||
5. Тот же принцип — для **WebSocket** `/api/v1/realtime`: при ошибке auth — refresh и переподключение с новым access token.
|
||
|
||
### Разделение ответственности
|
||
|
||
| Компонент | Поведение |
|
||
|---|---|
|
||
| **Frontend** | scheduler refresh, intercept `401`, retry, single-flight, хранение tokens |
|
||
| **Keycloak** | выдача и ротация tokens (Refresh Token Grant) |
|
||
| **api-backend** | проверка JWT; при невалидном/expired access token — **`401`**, refresh **не** выполняет |
|
||
|
||
## Создание диалога (MVP)
|
||
|
||
- У пользователя **не более одного активного** диалога: статус `open` | `waiting_for_company` | `waiting_for_client`. Закрытые (`closed`) остаются в истории.
|
||
- `POST /api/v1/dialogs`: если активный диалог уже есть — возвращает его (`200` / idempotent), новый не создаёт; иначе создаёт (`201`, `status=open`).
|
||
- Диалог создаётся **лениво** при первой отправке сообщения авторизованным клиентом.
|
||
- Frontend перед `POST .../messages` вызывает `POST /api/v1/dialogs` (заголовок `Idempotency-Key`), получает `dialog_id` и использует его далее.
|
||
- Популярный вопрос: после auth тот же порядок — `POST /dialogs` → `POST .../messages` с текстом вопроса.
|
||
- `dialog_id` = `external_chat_id` для Open Lines (см. [`arch-00-glossary.md`](arch-00-glossary.md), «Идентификаторы»).
|
||
- При первой доставке в Bitrix24 `bitrix-local-app` создаёт запись `dialog_sessions`.
|
||
- Новый активный диалог после `closed` — снова через `POST /dialogs` (когда продукт это разрешит; MVP: один активный в любой момент).
|
||
|
||
## Поток авторизации (OTP)
|
||
|
||
Срабатывает, когда клиент **ещё не имеет действующего refresh token** (первый вход) или refresh token **истёк**. Если refresh token валиден — см. «Поток возврата пользователя».
|
||
1. Клиент в гостевом режиме (только UI + `GET /api/v1/public/*`).
|
||
2. Клиент инициирует отправку сообщения (ручной ввод или популярный вопрос).
|
||
3. Frontend показывает pop-up с тремя согласиями; факт принятия хранится **локально** до OTP.
|
||
4. Клиент обязан принять согласие на обработку персональных данных и пользовательское соглашение.
|
||
5. Клиент может опционально согласиться на рекламные коммуникации.
|
||
6. Если обязательные согласия не даны, отправка блокируется.
|
||
7. Frontend запрашивает публичные настройки и показывает форму ввода номера телефона (единственный канал MVP).
|
||
8. Keycloak запускает OTP-flow: в mock mode challenge сразу активен без SMS; в real mode Keycloak создаёт `ordering`, генерирует OTP, заказывает SMS в `sms-service` и активирует challenge только после durable order.
|
||
9. Лимиты OTP на **edge** — `nginx`; продуктовые `otp.phone.*` применяет Keycloak. HTTP retry одного durable order использует прежние challenge/idempotency key и не увеличивает send counter.
|
||
10. Клиент вводит OTP и отправляет его в Keycloak.
|
||
11. **Keycloak проверяет корректность введённого OTP**:
|
||
- при **`KEYCLOAK_OTP_MOCK_ENABLED=true`** (MVP и любой режим с включённой заглушкой): введённое значение должно **совпадать** с `KEYCLOAK_OTP_MOCK_CODE` из `.env`;
|
||
- при **`KEYCLOAK_OTP_MOCK_ENABLED=false`**: значение сверяется локально с HMAC OTP, сгенерированного Keycloak и переданного в закрытом заказе `sms-service`; статусы Direct и callback на verify не влияют.
|
||
- при неверном коде Keycloak возвращает ошибку; frontend не получает tokens, шаг 12 не выполняется.
|
||
12. При успешной проверке frontend получает tokens через OIDC Authorization Code Flow with PKCE.
|
||
13. Frontend с JWT вызывает **`POST /api/v1/auth/bootstrap`** — в теле передаёт локально принятые согласия и device metadata (см. arch-02). api-backend атомарно: `find-or-create` по JWT `sub` (`keycloak_sub`), телефон из JWT claims (не из body) → сохранение `UserConsent` на `user_id` с nullable `ux_session_id` (на bootstrap обычно `NULL`) → минимальный профиль.
|
||
14. Frontend вызывает **`POST /api/v1/analytics/session-start`** (если нужна новая UX-сессия) и далее работает с `X-Ux-Session-Id`.
|
||
15. Триггер App DB ставит задачу `contact.map_or_create` в `sync_queue`; `bitrix-sync` асинхронно находит или создает Contact в Битрикс24. Авторизация не должна синхронно зависеть от ответа Битрикс24 CRM.
|
||
16. Frontend создаёт диалог и отправляет отложенное сообщение (см. «Создание диалога» и поток чата).
|
||
|
||
Отдельный **`POST /api/v1/consents`** после первого входа нужен, когда пользователь заново принимает обновлённые версии документов (не часть OTP-flow).
|
||
|
||
## Поток работы с чатом: клиент -> Битрикс24
|
||
|
||
В MVP сообщение клиента — **`content_kind`** `text` или `file`, не оба (см. [`arch-02-api-contracts.md`](arch-02-api-contracts.md), «Формат исходящего сообщения»).
|
||
|
||
**Текстовое сообщение:**
|
||
|
||
1. Frontend вызывает `POST /api/v1/dialogs` (если `dialog_id` ещё нет), затем отправляет `POST /api/v1/dialogs/{dialog_id}/messages` с непустым `text` (без вложения).
|
||
2. Nginx и API применяют rate limits.
|
||
3. API вызывает Message Safety v2 (`POST /internal/safety/v2/messages/check`) — шаги text/local links.
|
||
4. Далее — общая ветка вердикта (п. 5–8 ниже).
|
||
|
||
**Файловое сообщение:**
|
||
|
||
1. Frontend инициализирует **одно** вложение (`POST .../attachments/init`), получает **presigned PUT** в **S3-quarantine**, загружает байты **напрямую в S3**, затем вызывает `POST .../attachments/{attachment_id}/complete`.
|
||
2. Frontend отправляет `POST /api/v1/dialogs/{dialog_id}/messages` с `attachment_id` и `checksum` (поле `text` пустое).
|
||
3. Nginx и API применяют rate limits.
|
||
4. API **синхронно** вызывает Message Safety Service — шаг проверки файла (текст и ссылки пропускаются, если `text` пуст).
|
||
|
||
**Общая ветка вердикта (оба типа):**
|
||
|
||
5. **`403 deny`**: API удаляет quarantine (если был файл), выставляет `safety_status=blocked`, `delivery_status=rejected`, возвращает клиенту безопасную ошибку; в Bitrix24 ничего не уходит.
|
||
6. **`200 allow`**: API переносит файл в S3-data attachments (если был), сохраняет сообщение (`safety_status=allowed`, `delivery_status=accepted`) и фиксирует задачу доставки в Open Lines. После успешной отправки через `bitrix-local-app` статус становится `delivery_status=delivered`, API подтверждает клиенту финальный результат; `Dialog.status` → `waiting_for_company`. Если Bitrix24/S3/dependency недоступны после allow, статус становится `delivery_status=failed`, клиент получает безопасную ошибку зависимости.
|
||
7. **`202 pending`**: api-backend пишет checkpoint с `Location` и синхронно поллит его с `Retry-After`, пока не получит финальный вердикт/terminal failure или не истечёт budget. Public POST остаётся открытым; другие запросы не блокируются.
|
||
- финальный **`200 allow`** → как п. 6, затем ответ клиенту;
|
||
- финальный **`403 deny`** → как п. 5, затем ответ клиенту;
|
||
- timeout / недоступность safety → `delivery_status=failed`, безопасная ошибка клиенту (`503` / `504`), quarantine не promote; recovery по `safety_tasks` — зона модуля.
|
||
|
||
Клиент на `POST .../messages` получает **только финальный** результат (или ошибку инфраструктуры), не промежуточное «обрабатывается».
|
||
|
||
### Надёжность доставки и recovery
|
||
|
||
- Для доставки в Bitrix24 используется transactional outbox/checkpoint в App DB: запись `Message` и запись намерения доставки фиксируются атомарно, а повторная отправка в `bitrix-local-app` идемпотентна по `message_id` / `Idempotency-Key`.
|
||
- `delivery_status=accepted` означает, что API принял сообщение и завершил safety allow, но ещё не получил подтверждение доставки в Open Lines. `delivery_status=delivered` выставляется только после успешного ответа `bitrix-local-app` о приёме сообщения для Bitrix24 Open Lines.
|
||
- Recovery по `han_app.safety_tasks` восстанавливает сценарии `202 pending` после timeout/crash, опрашивает сохранённый `Location`, затем идемпотентно выполняет conditional promote/delete и обновляет App DB.
|
||
- Объекты в S3-quarantine не удаляются при timeout safety до финального verdict; orphan-cleanup удаляет только просроченные объекты без активного `safety_tasks` или attachment metadata.
|
||
|
||
## Поток работы с чатом: Битрикс24 -> клиент
|
||
|
||
1. Оператор отвечает клиенту в Битрикс24 Open Lines.
|
||
2. Битрикс24 отправляет `ONIMCONNECTOR*` webhook/event в `bitrix-local-app`.
|
||
3. `bitrix-local-app` проверяет `application_token`, нормализует payload и сохраняет idempotent inbox.
|
||
4. `bitrix-local-app` обогащает событие данными из `dialog_sessions` и forward-ит в API, если `BITRIX_API_FORWARD_URL` включен. При недоступности API событие остаётся во внутреннем inbox, повторяется с backoff и после исчерпания retry попадает в DLQ; дубликаты определяются по `(external_chat_id, bitrix_message_id)`.
|
||
5. api-backend находит локальный диалог по `external_chat_id` (= `dialog_id`, см. [`arch-00-glossary.md`](arch-00-glossary.md)).
|
||
6. api-backend сохраняет входящее сообщение в App DB (`sender_type=company`, `delivery_status=delivered`). Файл оператора (если есть) — в бакет **S3-data attachments** (`han-chat-attachments`) с metadata в `MessageAttachment`; бакет **documents** зарезервирован для документов компании в профиле (post-MVP). По факту сообщения API обновляет `Dialog.status`: входящее от оператора → `waiting_for_client`, исходящее от клиента → `waiting_for_company` (значения — arch-00).
|
||
7. `bitrix-local-app` подтверждает доставку в Bitrix24 через `imconnector.send.status.delivery` после успешного сохранения события в api-backend или идемпотентного duplicate-ack.
|
||
8. api-backend публикует событие для frontend через **WebSocket** (`WS /api/v1/realtime`). Если realtime недоступен, frontend получает сообщение через polling `GET /api/v1/dialogs/{dialog_id}/messages?after=...`.
|
||
9. Frontend отображает сообщение оператора в чате.
|
||
10. При получении от `bitrix-local-app` доменного события `dialog.closed` (Bitrix24 `ONIMCONNECTORDIALOGFINISH`) API переводит `Dialog.status` в `closed`.
|
||
|
||
## Документы компании
|
||
|
||
Notification Center v1 регистрирует переданные продюсером объекты `han-chat-documents` в реестре `documents` и связывает их с уведомлением. Это первый действующий канал наполнения будущего общего блока профиля; доставка из Bitrix24 остаётся вне scope.
|
||
|
||
Скачивание выполняется owner-only по короткому presigned GET с audit. Для вида с `hide_on_document_download=true` первое скачивание **любого** связанного документа атомарно скрывает уведомление; последующие скачивания не меняют состояние. Если `date_expired` уже задано, оно сохраняется; TTL скрытия устанавливает дату только при её отсутствии.
|
||
|
||
## Профиль клиента
|
||
|
||
Профиль должен быть блочным.
|
||
|
||
Блок "Личные данные":
|
||
|
||
- ФИО;
|
||
- гражданство;
|
||
- номер телефона в РФ;
|
||
- зарубежный номер телефона;
|
||
- email.
|
||
|
||
Блок "Документы":
|
||
|
||
- перечень документов, отправленных клиенту компанией (в MVP — пустой до реализации бэклога);
|
||
- дата отправки;
|
||
- наименование документа;
|
||
- возможность скачать документ (после реализации доставки).
|
||
|
||
Редактирование данных профиля недоступно.
|
||
|
||
### Sync профиля и master для PII
|
||
|
||
App DB — **локальный кэш** для UI. Двусторонний sync — `bitrix-sync` (имена полей — [`arch-00-glossary.md`](arch-00-glossary.md)):
|
||
|
||
- **Auth-телефон:** master — Keycloak (`UserIdentity.phone_number`); только его фактическое изменение инициирует `contact.update`.
|
||
- **ФИО, гражданство, email:** master — Битрикс24; App хранит последний успешно полученный snapshot для UI и не отправляет эти поля обратно.
|
||
- **App → Bitrix:** триггеры `han_app` → `sync_queue` (`contact.map_or_create`, `contact.update`, `contact.deactivate`).
|
||
- **Bitrix → App:** durable webhook inbox + reconciliation; запись с `SET LOCAL han.sync_suppress='true'` без эхо.
|
||
- **Конфликт:** универсального правила «последнее событие побеждает» нет; применяется field mastership. Несовпадение identity/mapping создаёт business alert и не перезаписывает профиль.
|
||
|
||
## Аудит скачиваний
|
||
|
||
При выдаче presigned URL на скачивание вложений чата (`GET /api/v1/dialogs/{dialog_id}/attachments/{attachment_id}/download-url`) и документов профиля (`GET /api/v1/documents/{document_id}/download-url`) api-backend пишет audit-событие в App DB:
|
||
|
||
| Поле | Значение |
|
||
|---|---|
|
||
| `event_type` | `attachment.download_url_issued` / `document.download_url_issued` |
|
||
| `user_id` | текущий пользователь из JWT |
|
||
| `resource_type` | `attachment` / `document` |
|
||
| `resource_id` | UUID сущности |
|
||
| `ux_session_id` | из заголовка `X-Ux-Session-Id` |
|
||
| `request_id` | из заголовка запроса |
|
||
| `ip`, `user_agent` | из proxy headers |
|
||
|
||
В audit **не** сохраняются presigned URL, содержимое файлов и PII. Формат таблицы — в модуле `database`.
|
||
|
||
## Realtime (кратко)
|
||
|
||
Детальный контракт — [`arch-02-api-contracts.md`](arch-02-api-contracts.md), раздел «Realtime».
|
||
|
||
- Transport: **только WebSocket** `WS /api/v1/realtime` (JWT). SSE в MVP **не** используется.
|
||
- Путь входит в `/api/*`; отдельный location `/realtime/*` в nginx **не** нужен.
|
||
- Fallback: polling `GET /api/v1/dialogs/{dialog_id}/messages?after=...`.
|
||
- События: новое сообщение, смена `delivery_status` / `safety_status`, смена `Dialog.status`.
|
||
- Подписка расширена опциональным `notifications` (default `false`); канал пользователя передаёт `notification.created`, `notification.updated`, `notification.closed`, включая эхо инициатору. После reconnect источник истины — REST.
|
||
|
||
## Принципы безопасности
|
||
|
||
- Все защищенные пользовательские API требуют валидный JWT.
|
||
- Компрометация одного сервиса не должна автоматически давать host root, Docker daemon, секреты или сетевой доступ соседних сервисов; требования к VM и production-деплою — в [`arch-06-service-hosting-security.md`](arch-06-service-hosting-security.md).
|
||
- Без JWT доступны **только** read-only публичные endpoint: `GET /api/v1/public/*` (rate limit + CORS + кэш). Write-endpoint (`consents`, `session-start`, чат, профиль и т.д.) требуют JWT.
|
||
- Все внешние пользовательские соединения работают через HTTPS.
|
||
- HTTP допускается только для веб-домена как вход для редиректа на HTTPS. Для api домена HTTP не допускается.
|
||
- TLS завершается на reverse proxy; внутренний HTTP между контейнерами допускается только в закрытой backend-сети.
|
||
- TLS 1.0/1.1 и слабые шифры запрещены.
|
||
- HSTS обязателен после проверки домена и сертификата.
|
||
- INPUT-validation на api-backend
|
||
- использовать только Параметризованные SQL-запросы
|
||
- обязательное Экранирование вывода
|
||
- настройка CORS только на разрешённые домены (`security.cors.allowed_origins` в `app_settings`, см. arch-04)
|
||
- настройка Secure Headers (CSP, X-Frame-Options и др.)
|
||
- Доступ к профилю, диалогам, сообщениям, файлам и документам ограничен текущим `user_id`.
|
||
- Все запросы, содержащие в себе ссылку на сущность, которая относится к конкретному пользователю (ИД продукта, услуги, чата, документа и тп), проверяются backend_api на соответствие тому пользователю, от которого пришел запрос.
|
||
- Все публичные id создаются в формате UUID.
|
||
- Сервисные API защищаются внутренней сетью Docker/VPC плюс service token (канонический контракт — [`arch-02-api-contracts.md`](arch-02-api-contracts.md), «Service tokens (internal API)»; значения переменных — [`arch-04-settings-and-content.md`](arch-04-settings-and-content.md)).
|
||
- Rate limits применяются минимум на двух уровнях: edge-лимиты в `nginx` и пользовательские лимиты в API с состоянием в Redis.
|
||
- Исходящие сообщения пользователя: internal `POST /internal/safety/v2/messages/check` → при `202` api-backend синхронно поллит `Location` до финального `200`/`403`, terminal failed `503` или timeout; public API не становится async.
|
||
- Файлы пользователя до финального `allow` только в S3-quarantine; в S3-data — после `200 allow`.
|
||
- У клиента **нет** постоянных S3 credentials. Загрузка — **presigned PUT** в S3-quarantine, выданный `api-backend`; скачивание — **presigned GET**. Байты файла **не** проксируются через `api-backend`.
|
||
- `message-safety` — read-only к S3-quarantine, без прав записи в бакеты.
|
||
- Вызовы `message-safety` и `bitrix-local-app` защищены timeout budget и circuit breaker (см. arch-04).
|
||
- Все изменяемые параметры, телефоны, лимиты, mime types и флаги хранятся в настройках ([`arch-04-settings-and-content.md`](arch-04-settings-and-content.md)).
|
||
- PII-данные не пишутся в логи в открытом виде.
|
||
- Документы и файлы чата должны иметь контроль доступа и аудит скачиваний.
|
||
|
||
## Backend-репозиторий и инфраструктура
|
||
|
||
### Состав backend-контура
|
||
|
||
Минимальный целевой real-SMS контур разделён на два stack:
|
||
|
||
- ВМ1: `nginx`, `api-backend`, `keycloak`, `sms-service`/worker, `bitrix-local-app`, Redis DB0/DB1, `otel-collector`;
|
||
- ВМ2: nginx с public webhook/private internal server blocks, `message-safety` API/worker, `clamd`/`freshclam`, `bitrix-sync`, Redis Safety, `otel-collector`.
|
||
|
||
До SMS rollout сервисы SMS могут отсутствовать, но Keycloak обязан оставаться в mock mode.
|
||
|
||
### Предлагаемая структура backend-репозитория
|
||
|
||
Каноническая структура root Compose, service includes, networks и mounts задаётся в [`arch-03-docker-compose-blueprint.md`](arch-03-docker-compose-blueprint.md). Детальная внутренняя структура сервиса определяется его профильной спецификацией.
|
||
|
||
### Compose-контуры
|
||
|
||
`backend/docker-compose.yml` является единственным root Compose ВМ1; `processing/docker-compose.yml` — единственным root Compose ВМ2. Оба используют `include` и отдельные root-owned systemd units. Cross-host Docker network не используется.
|
||
|
||
На каждой VM host ports публикует только её nginx. ВМ1 публикует `80/443` своего application host. ВМ2 публикует `80/443` отдельного webhook host и private `8443`; public server block ВМ2 допускает только exact CRM webhook, private listener доступен только SG ВМ1/ops.
|