Files
han-app/architectory/arch-04-settings-and-content.md
T

366 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# arch-04. Настройки и изменяемые параметры
> **`.env`** — инфраструктура и секреты. **`app_settings`** (App DB) — единственный источник бизнес-настроек. Имена полей и enum — [`arch-00-glossary.md`](arch-00-glossary.md). Docker Compose — [`arch-03-docker-compose-blueprint.md`](arch-03-docker-compose-blueprint.md).
## Цель
Параметры разделены по слоям:
| Слой | Где | Что |
|---|---|---|
| **Инфраструктура** | `.env` | подключения, URL, секреты, nginx/TLS, service tokens |
| **Бизнес-логика** | таблица **`app_settings`** | лимиты, флаги, телефоны, типы файлов, CORS, consent URLs |
| **Контент** | `text_resources`, `popular_questions` | тексты UI |
Managed PostgreSQL **поднимается до** развёртывания приложения. Бизнес-настройки **не дублируются** в `.env`: seed в `app_settings` выполняется миграцией/скриптом модуля `database` **до** первого запуска `api-backend`.
## Источники настроек
### `.env` — только инфраструктура
Корневой `backend/.env` читается сервисами compose. В репозитории — `.env.example`, не `.env`.
**Допустимо в `.env`:**
- URL сервисов, публичные endpoint, порты;
- строки подключения PostgreSQL, Redis, Keycloak DB;
- секреты: S3, Bitrix OAuth, service tokens, webhook-тokens;
- параметры **nginx/TLS** и edge rate limits (`NGINX_RATE_LIMIT_*`);
- идентификация Keycloak: realm, audience, public/internal URL;
- **OTP-заглушка MVP** (`KEYCLOAK_OTP_MOCK_*`) — infra/dev-секрет, не бизнес-настройка;
- технические таймауты worker-ов (`MESSAGE_SAFETY_*`, интервалы `bitrix-sync`).
**Запрещено в `.env` (→ только `app_settings`):**
- включение/отключение OTP, OTP-лимиты для UI/продукта;
- телефон оператора, consent URLs/versions;
- лимиты приложения (сообщения, download URL, login);
- типы/размер файлов чата, UX idle timeout;
- CORS origins, feature flags frontend.
### `app_settings` — бизнес-настройки (App DB)
**Единственный источник правды** для параметров, которые:
- меняет продукт/оператор без redeploy;
- отдаются в `GET /api/v1/public/app-config` (публичные ключи);
- используются `api-backend` (и при необходимости другими сервисами) в runtime.
Позже — редактирование через админку; на MVP — seed-миграция.
### `text_resources` / `popular_questions`
Контент UI — отдельные таблицы (не `app_settings`). Ключи MVP — TBD (спецификация frontend).
---
## Требования к таблице `app_settings`
Схема: **`han_app`**. Детальная DDL — модуль `database`; arch фиксирует контракт.
### Колонки (минимум)
| Колонка | Тип | Назначение |
|---|---|---|
| `setting_key` | `varchar`, PK | Канонический ключ (`auth.phone.enabled`, см. ниже) |
| `setting_value` | `text`, NOT NULL | Значение (строка; парсинг по типу) |
| `value_type` | `enum` | `boolean` \| `integer` \| `string` \| `duration` \| `string_list` |
| `is_public` | `boolean` | Разрешён в `GET /api/v1/public/app-config` |
| `description` | `text`, nullable | Комментарий для админки/ops |
| `updated_at` | `timestamptz` | Последнее изменение |
| `record_status` | `char(1)` | Soft delete: `'A'` active |
### Правила
1. **Seed обязателен** до первого запуска `api-backend` в новой среде (миграция или idempotent seed-скрипт).
2. **`api-backend`** загружает настройки при старте; допускается in-memory cache с инвалидацией по `updated_at` (реализация — модуль).
3. Отсутствие **обязательного** ключа при старте → сервис **не** переходит в `ready` (fail-fast).
4. Публичные ключи (`is_public=true`) отдаются только через **строгий DTO** `app-config`, не raw dump таблицы.
5. Секреты и infra **не** хранятся в `app_settings`.
### Ключи MVP (seed)
Полный пример значений — раздел «Seed MVP» ниже. Группы:
| Группа | Ключи |
|---|---|
| Auth | `auth.phone.enabled`, `auth.password.enabled` |
| OTP (продукт; потребитель — Keycloak SPI через settings bridge api-backend) | `otp.phone.max_send_attempts_per_24h`, `otp.phone.min_seconds_between_attempts` |
| Оператор | `operator.call.phone` |
| Consent | `consent.personal_data.*`, `consent.user_agreement.*`, `consent.marketing.*` |
| Файлы чата | `chat.attachments.*` |
| Rate limits (app) | `rate_limit.message_send.*`, `rate_limit.download_url.*`, `rate_limit.public_endpoints.*`, `rate_limit.login.*` |
| UX | `ux.session.idle_timeout_minutes` |
| Security | `security.cors.allowed_origins`, `security.public_cache.max_age_seconds` |
### Seed MVP
```text
auth.phone.enabled=true
auth.password.enabled=false
otp.phone.max_send_attempts_per_24h=3
otp.phone.min_seconds_between_attempts=30
operator.call.phone=+74999591007
consent.personal_data.required=true
consent.personal_data.document_url=https://www.han0107.ru/privacy/persdata-agree-mobile
consent.personal_data.version=2026-06-10
consent.user_agreement.required=true
consent.user_agreement.document_url=https://www.han0107.ru/user-agreement
consent.user_agreement.version=2026-06-10
consent.marketing.required=false
consent.marketing.version=2026-06-10
chat.attachments.allowed_extensions=jpg,jpeg,png,webp,heic,heif,pdf
chat.attachments.allowed_mime_types=image/jpeg,image/png,image/webp,image/heic,image/heif,application/pdf
chat.attachments.disallowed_extensions=svg,doc,docx,xls,xlsx,csv
chat.attachments.max_size_mb=5
chat.attachments.storage=selectel_s3
chat.attachments.upload_mode=presigned_put
chat.attachments.safety_scan_required=true
chat.attachments.presigned_upload_ttl_seconds=600
rate_limit.message_send.per_user=30/minute
rate_limit.message_send.per_dialog=20/minute
rate_limit.download_url.per_user=60/hour
rate_limit.public_endpoints.per_ip=60/minute
rate_limit.login.per_ip=10/minute
ux.session.idle_timeout_minutes=30
security.cors.allowed_origins=https://tohin.ru
security.public_cache.max_age_seconds=3600
```
---
## Пример `.env.example`
Только инфраструктура. Бизнес-параметры — в seed `app_settings`.
```text
# =============================================================================
# Общие
# =============================================================================
APP_ENV=production-like
API_PORT=8000
LOG_LEVEL=INFO
# =============================================================================
# Managed PostgreSQL
# =============================================================================
HAN_PG_HOST=<managed-pg-private-host>
HAN_PG_PORT=5433
HAN_PG_DATABASE=han_chat
DATABASE_URL=postgresql+asyncpg://han_app:change-me@<HAN_PG_HOST>:<HAN_PG_PORT>/<HAN_PG_DATABASE>
BITRIX_DATABASE_URL=postgresql://bitrix_local_app:change-me@<HAN_PG_HOST>:<HAN_PG_PORT>/<HAN_PG_DATABASE>
BITRIX_SYNC_APP_DATABASE_URL=postgresql://bitrix_sync_user:change-me@<HAN_PG_HOST>:<HAN_PG_PORT>/<HAN_PG_DATABASE>
BITRIX_SYNC_DATABASE_URL=postgresql://bitrix_sync_user:change-me@<HAN_PG_HOST>:<HAN_PG_PORT>/<HAN_PG_DATABASE>
MESSAGE_SAFETY_DATABASE_URL=postgresql://message_safety_app:change-me@<HAN_PG_HOST>:<HAN_PG_PORT>/<HAN_PG_DATABASE>
KEYCLOAK_DB_URL=jdbc:postgresql://<HAN_PG_HOST>:<HAN_PG_PORT>/<HAN_PG_DATABASE>?user=keycloak_user&password=change-me&currentSchema=keycloak
KC_DB_URL_PROPERTIES=currentSchema=keycloak
# Selectel PgBouncer 5433: pool_mode=session; search_path задаётся на уровне ролей.
# Не добавлять options=-csearch_path: pooler отклоняет этот startup parameter.
# =============================================================================
# Публичные URL (HTTPS)
# =============================================================================
PUBLIC_WEB_URL=https://tohin.ru
PUBLIC_API_URL=https://tohin.ru/api
PUBLIC_AUTH_URL=https://tohin.ru/auth
# =============================================================================
# nginx (edge, TLS, rate limits)
# =============================================================================
NGINX_HTTP_PORT=80
NGINX_HTTPS_PORT=443
TLS_CERT_PATH=/etc/nginx/certs/fullchain.pem
TLS_KEY_PATH=/etc/nginx/certs/privkey.pem
NGINX_TLS_PROTOCOLS=TLSv1.2 TLSv1.3
NGINX_HSTS_MAX_AGE=31536000
NGINX_CLIENT_MAX_BODY_SIZE=8m
NGINX_RATE_LIMIT_API=60r/m
NGINX_RATE_LIMIT_AUTH=10r/m
NGINX_RATE_LIMIT_DOWNLOADS=30r/m
NGINX_RATE_LIMIT_PUBLIC=60r/m
NGINX_RATE_LIMIT_POLLING=60r/m
# =============================================================================
# Keycloak (infra; OTP-заглушка — dev/MVP)
# =============================================================================
KEYCLOAK_PUBLIC_URL=https://tohin.ru/auth
KEYCLOAK_INTERNAL_URL=http://keycloak:8080
KEYCLOAK_REALM=han-chat
KEYCLOAK_AUDIENCE=han-chat-api
KEYCLOAK_OTP_MOCK_ENABLED=true
KEYCLOAK_OTP_MOCK_CODE=1234
# =============================================================================
# Redis (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_REALTIME_URL=redis://redis:6379/1
MESSAGE_SAFETY_REDIS_URL=redis://redis:6379/2
# =============================================================================
# Service tokens (internal API) — все переменные только в backend/.env
# =============================================================================
MESSAGE_SAFETY_SERVICE_TOKEN=change-me
BITRIX_LOCAL_APP_INTERNAL_TOKEN=change-me
BITRIX_API_INBOX_TOKEN=change-me
BITRIX_INTERNAL_API_TOKEN=change-me
BITRIX_API_FORWARD_TOKEN=change-me
BITRIX_SYNC_SERVICE_TOKEN=change-me
KEYCLOAK_SETTINGS_BRIDGE_TOKEN=change-me
# =============================================================================
# api-backend (интеграции + resilience I2)
# =============================================================================
BITRIX_LOCAL_APP_BASE_URL=http://bitrix-local-app:8080
BITRIX_API_INBOX_PATH=/internal/openlines/v1/inbox
MESSAGE_SAFETY_URL=http://message-safety:8080
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_ENABLED=true
BITRIX_SYNC_CRM_BASE_URL=https://han0107.bitrix24.ru
BITRIX_SYNC_CRM_WEBHOOK_URL=change-me
BITRIX_SYNC_CONTACT_MAP_INTERVAL_SEC=60
BITRIX_SYNC_CONTACT_UPDATE_INTERVAL_SEC=30
BITRIX_SYNC_CRM_MAX_CONCURRENCY=2
BITRIX_SYNC_CONTACT_LIST_BATCH_SIZE=50
BITRIX_SYNC_WEBHOOK_TOKEN=change-me
# =============================================================================
# bitrix-local-app
# =============================================================================
BITRIX_CLIENT_ID=change-me
BITRIX_CLIENT_SECRET=change-me
BITRIX_CONNECTOR_ID=han_mobile_app
BITRIX_CONNECTOR_NAME=HAN Mobile App
BITRIX_OPEN_LINE_ID=8
BITRIX_PUBLIC_BASE_URL=https://tohin.ru/bitrix
BITRIX_API_FORWARD_URL=http://api-backend:8000/internal/openlines/v1/inbox
BITRIX_APPLICATION_TOKEN=change-me
# =============================================================================
# message-safety (technical)
# =============================================================================
# POST check timeout; poll interval/max — бюджет sync-wait внутри POST .../messages (G4)
MESSAGE_SAFETY_POST_TIMEOUT_SEC=5
MESSAGE_SAFETY_TASK_POLL_INTERVAL_SEC=2
MESSAGE_SAFETY_TASK_POLL_MAX_SEC=300
MESSAGE_SAFETY_FILE_SCAN_TIMEOUT_SEC=60
MESSAGE_SAFETY_RULES_VERSION=2026-01-01
# =============================================================================
# Frontend (nginx)
# =============================================================================
FRONTEND_STATIC_PATH=/usr/share/nginx/html
FRONTEND_DEV_PROXY_ENABLED=false
EXPO_DEV_SERVER_URL=http://host.docker.internal:8081
# =============================================================================
# Selectel S3
# =============================================================================
SELECTEL_S3_ENDPOINT_URL=https://s3.storage.selcloud.ru
SELECTEL_S3_BUCKET_DOCUMENTS=han-chat-documents
SELECTEL_S3_BUCKET_ATTACHMENTS=han-chat-attachments
SELECTEL_S3_BUCKET_QUARANTINE=han-chat-quarantine
SELECTEL_S3_ACCESS_KEY=change-me
SELECTEL_S3_SECRET_KEY=change-me
SELECTEL_S3_QUARANTINE_READ_ACCESS_KEY=change-me
SELECTEL_S3_QUARANTINE_READ_SECRET_KEY=change-me
# =============================================================================
# Observability
# =============================================================================
OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317
```
S3-клиенты используют только virtual-hosted addressing
(`https://<bucket>.s3.storage.selcloud.ru/<object-key>`). Это часть контракта
presigned URL и CORS Selectel; path-style адресация не поддерживается приложением.
Все переменные — **только** в `backend/.env`. Отдельного хранилища нет.
**Webhook-токены** (публичные callback, не service API): `BITRIX_APPLICATION_TOKEN`, `BITRIX_SYNC_WEBHOOK_TOKEN`.
## Namespace переменных Bitrix
- `bitrix-local-app`: `BITRIX_CLIENT_*`, `BITRIX_CONNECTOR_*`, `BITRIX_PUBLIC_BASE_URL`, `BITRIX_DATABASE_URL`, `BITRIX_API_FORWARD_URL`, `BITRIX_APPLICATION_TOKEN` + service tokens.
- `api-backend`: `BITRIX_LOCAL_APP_BASE_URL`, `MESSAGE_SAFETY_URL`, circuit/timeout vars, Redis `/0`/`/1` + service tokens; **бизнес-настройки** — из `app_settings`. OTP counters **не** ведёт.
- `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_ENABLED`
| Значение | Поведение |
|---|---|
| `true` (default) | `bitrix-sync` обрабатывает `sync_queue` и принимает CRM webhook |
| `false` | синхронизация с Bitrix24 CRM не выполняется; сервис стартует в no-op/degraded режиме; чат Open Lines через `bitrix-local-app` **не** затрагивается |
В MVP выбран режим **no-op service**: контейнер `bitrix-sync` стартует, `/health/live` отвечает успешно, `/health/ready` возвращает degraded/not-ready с явной причиной `sync_disabled`, worker не обрабатывает `sync_queue`, webhook CRM возвращает безопасный `503` или `202 ignored` по контракту модуля. Это сохраняет единый compose-контур и не влияет на чат Open Lines.
## Keycloak settings bridge для OTP
Product limits OTP (`otp.phone.*`) хранятся в `app_settings`, но Keycloak не получает прямой доступ к схеме `han_app`.
MVP-механизм:
1. `api-backend` читает публичные/служебные настройки из `app_settings` и кэширует их.
2. Для Keycloak SPI доступен internal endpoint `GET /internal/settings/v1/otp` в Docker/VPC-сети, защищённый service token.
3. Keycloak SPI читает `otp.phone.max_send_attempts_per_24h` и `otp.phone.min_seconds_between_attempts` через этот endpoint с локальным cache TTL.
4. При недоступности settings bridge SPI использует последнее валидное cache-значение; если cache пустой — fail-closed и не выдаёт OTP.
Счётчики попыток OTP остаются в зоне Keycloak/SPI, не в `api-backend`.
## Разрешённые типы файлов чата (MVP)
Источник значений — ключи **`app_settings`** (раздел «Seed MVP»). Остальные arch-* **ссылаются сюда**.
| Ключ | MVP-значение |
|---|---|
| `chat.attachments.allowed_extensions` | `jpg`, `jpeg`, `png`, `webp`, `heic`, `heif`, `pdf` |
| `chat.attachments.allowed_mime_types` | `image/jpeg`, `image/png`, `image/webp`, `image/heic`, `image/heif`, `application/pdf` |
| `chat.attachments.max_size_mb` | `5` |
Правило: файл принимается только если **и** расширение, **и** MIME в allow-list. Детальная проверка — модуль `message-safety`.
Публичный UI: `GET /api/v1/public/app-config` (строгий DTO, без секретов).
## Публичный config endpoint
`GET /api/v1/public/app-config` — только ключи с `is_public=true` из `app_settings`:
- OTP по телефону (`auth.phone.enabled`);
- номер оператора;
- типы файлов и max size;
- `ux.session.idle_timeout_minutes`;
- feature flags;
- публичные лимиты для подсказок UI.
Секреты, service tokens, внутренние URL **не** возвращаются. DTO явный, не сериализация всей таблицы. Rate limit: 60/min per IP. `Cache-Control: public, max-age` из `security.public_cache.max_age_seconds`.
## Публичный content endpoint
`GET /api/v1/public/content``text_resources` для текущего языка. Те же требования безопасности, что у config.
## Nginx и HTTPS
Infra-переменные — `.env.example` (`NGINX_*`, `TLS_*`). Edge rate limits (`NGINX_RATE_LIMIT_*`) **не** дублируют `rate_limit.*` из `app_settings`: nginx — защита периметра, app — бизнес-лимиты в backend.
Реализация — [`arch-03-docker-compose-blueprint.md`](arch-03-docker-compose-blueprint.md).