Files
han-app/architectory/arch-04-settings-and-content.md
T
2026-07-09 11:03:44 +03:00

323 lines
16 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 (продукт) | `otp.phone.max_send_attempts_per_24h`, `otp.phone.min_seconds_between_attempts` |
| Оператор | `operator.call.phone` |
| Consent | `consent.personal_data.*`, `consent.user_agreement.*`, `consent.marketing.*` |
| Файлы чата | `chat.attachments.*` |
| Rate limits (app) | `rate_limit.message_send.*`, `rate_limit.download_url.*`, `rate_limit.public_endpoints.*`, `rate_limit.login.*` |
| UX | `ux.session.idle_timeout_minutes` |
| Security | `security.cors.allowed_origins`, `security.public_cache.max_age_seconds` |
### Seed MVP
```text
auth.phone.enabled=true
auth.password.enabled=false
otp.phone.max_send_attempts_per_24h=3
otp.phone.min_seconds_between_attempts=30
operator.call.phone=+74999591007
consent.personal_data.required=true
consent.personal_data.document_url=https://www.han0107.ru/privacy/persdata-agree-mobile
consent.personal_data.version=2026-06-10
consent.user_agreement.required=true
consent.user_agreement.document_url=https://www.han0107.ru/user-agreement
consent.user_agreement.version=2026-06-10
consent.marketing.required=false
consent.marketing.version=2026-06-10
chat.attachments.allowed_extensions=jpg,jpeg,png,webp,heic,heif,pdf
chat.attachments.allowed_mime_types=image/jpeg,image/png,image/webp,image/heic,image/heif,application/pdf
chat.attachments.disallowed_extensions=svg,doc,docx,xls,xlsx,csv
chat.attachments.max_size_mb=5
chat.attachments.storage=selectel_s3
chat.attachments.upload_mode=backend_controlled_upload
chat.attachments.safety_scan_required=true
rate_limit.message_send.per_user=30/minute
rate_limit.message_send.per_dialog=20/minute
rate_limit.download_url.per_user=60/hour
rate_limit.public_endpoints.per_ip=60/minute
rate_limit.login.per_ip=10/minute
ux.session.idle_timeout_minutes=30
security.cors.allowed_origins=https://tohin.ru,https://app.example.ru
security.public_cache.max_age_seconds=3600
```
---
## Пример `.env.example`
Только инфраструктура. Бизнес-параметры — в seed `app_settings`.
```text
# =============================================================================
# Общие
# =============================================================================
APP_ENV=production-like
API_PORT=8000
LOG_LEVEL=INFO
# =============================================================================
# Managed PostgreSQL
# =============================================================================
HAN_PG_HOST=<managed-pg-private-host>
HAN_PG_PORT=6432
HAN_PG_DATABASE=han_chat
DATABASE_URL=postgresql+asyncpg://han_app:change-me@<HAN_PG_HOST>:<HAN_PG_PORT>/<HAN_PG_DATABASE>?options=-csearch_path%3Dhan_app
BITRIX_DATABASE_URL=postgresql://bitrix_local_app:change-me@<HAN_PG_HOST>:<HAN_PG_PORT>/<HAN_PG_DATABASE>?options=-csearch_path%3Dbitrix_local
BITRIX_SYNC_APP_DATABASE_URL=postgresql://bitrix_sync_user:change-me@<HAN_PG_HOST>:<HAN_PG_PORT>/<HAN_PG_DATABASE>?options=-csearch_path%3Dbitrix_sync%2Chan_app
BITRIX_SYNC_DATABASE_URL=postgresql://bitrix_sync_user:change-me@<HAN_PG_HOST>:<HAN_PG_PORT>/<HAN_PG_DATABASE>?options=-csearch_path%3Dbitrix_sync
MESSAGE_SAFETY_DATABASE_URL=postgresql://message_safety_app:change-me@<HAN_PG_HOST>:<HAN_PG_PORT>/<HAN_PG_DATABASE>?options=-csearch_path%3Dmessage_safety
KEYCLOAK_DB_URL=jdbc:postgresql://<HAN_PG_HOST>:<HAN_PG_PORT>/<HAN_PG_DATABASE>?user=keycloak_user&password=change-me&currentSchema=keycloak
KC_DB_URL_PROPERTIES=currentSchema=keycloak
# =============================================================================
# Публичные URL (HTTPS)
# =============================================================================
PUBLIC_WEB_URL=https://app.example.ru
PUBLIC_API_URL=https://app.example.ru/api
PUBLIC_AUTH_URL=https://app.example.ru/auth
# =============================================================================
# nginx (edge, TLS, rate limits)
# =============================================================================
NGINX_HTTP_PORT=80
NGINX_HTTPS_PORT=443
TLS_CERT_PATH=/etc/nginx/certs/fullchain.pem
TLS_KEY_PATH=/etc/nginx/certs/privkey.pem
NGINX_TLS_PROTOCOLS=TLSv1.2 TLSv1.3
NGINX_HSTS_MAX_AGE=31536000
NGINX_CLIENT_MAX_BODY_SIZE=8m
NGINX_RATE_LIMIT_API=60r/m
NGINX_RATE_LIMIT_AUTH=10r/m
NGINX_RATE_LIMIT_DOWNLOADS=30r/m
NGINX_RATE_LIMIT_PUBLIC=60r/m
NGINX_RATE_LIMIT_POLLING=60r/m
# =============================================================================
# Keycloak (infra; OTP-заглушка — dev/MVP)
# =============================================================================
KEYCLOAK_PUBLIC_URL=https://app.example.ru/auth
KEYCLOAK_INTERNAL_URL=http://keycloak:8080
KEYCLOAK_REALM=han-chat
KEYCLOAK_AUDIENCE=han-chat-api
KEYCLOAK_OTP_MOCK_ENABLED=true
KEYCLOAK_OTP_MOCK_CODE=1234
# =============================================================================
# Redis
# =============================================================================
REDIS_URL=redis://redis:6379/0
# =============================================================================
# Service tokens (internal API) — все переменные только в backend/.env
# =============================================================================
MESSAGE_SAFETY_SERVICE_TOKEN=change-me
BITRIX_LOCAL_APP_INTERNAL_TOKEN=change-me
BITRIX_API_INBOX_TOKEN=change-me
BITRIX_INTERNAL_API_TOKEN=change-me
BITRIX_API_FORWARD_TOKEN=change-me
BITRIX_SYNC_SERVICE_TOKEN=change-me
# =============================================================================
# api-backend (интеграции)
# =============================================================================
BITRIX_LOCAL_APP_BASE_URL=http://bitrix-local-app:8080
BITRIX_API_INBOX_PATH=/internal/openlines/v1/inbox
MESSAGE_SAFETY_URL=http://message-safety:8080
# =============================================================================
# bitrix-sync
# =============================================================================
BITRIX_SYNC_CRM_BASE_URL=https://han0107.bitrix24.ru
BITRIX_SYNC_CRM_WEBHOOK_URL=change-me
BITRIX_SYNC_CONTACT_MAP_INTERVAL_SEC=60
BITRIX_SYNC_CONTACT_UPDATE_INTERVAL_SEC=30
BITRIX_SYNC_CRM_MAX_CONCURRENCY=2
BITRIX_SYNC_CONTACT_LIST_BATCH_SIZE=50
BITRIX_SYNC_WEBHOOK_TOKEN=change-me
# =============================================================================
# bitrix-local-app
# =============================================================================
BITRIX_CLIENT_ID=change-me
BITRIX_CLIENT_SECRET=change-me
BITRIX_CONNECTOR_ID=han_mobile_app
BITRIX_CONNECTOR_NAME=HAN Mobile App
BITRIX_OPEN_LINE_ID=8
BITRIX_PUBLIC_BASE_URL=https://tohin.ru/bitrix
BITRIX_API_FORWARD_URL=http://api-backend:8000/internal/openlines/v1/inbox
BITRIX_APPLICATION_TOKEN=change-me
# =============================================================================
# message-safety (technical)
# =============================================================================
MESSAGE_SAFETY_POST_TIMEOUT_SEC=5
MESSAGE_SAFETY_TASK_POLL_INTERVAL_SEC=2
MESSAGE_SAFETY_TASK_POLL_MAX_SEC=300
MESSAGE_SAFETY_RULES_VERSION=2026-01-01
# =============================================================================
# Frontend (nginx)
# =============================================================================
FRONTEND_STATIC_PATH=/usr/share/nginx/html
FRONTEND_DEV_PROXY_ENABLED=false
EXPO_DEV_SERVER_URL=http://host.docker.internal:8081
# =============================================================================
# Selectel S3
# =============================================================================
SELECTEL_S3_ENDPOINT_URL=https://s3.storage.selcloud.ru
SELECTEL_S3_BUCKET_DOCUMENTS=han-chat-documents
SELECTEL_S3_BUCKET_ATTACHMENTS=han-chat-attachments
SELECTEL_S3_BUCKET_QUARANTINE=han-chat-quarantine
SELECTEL_S3_ACCESS_KEY=change-me
SELECTEL_S3_SECRET_KEY=change-me
SELECTEL_S3_QUARANTINE_READ_ACCESS_KEY=change-me
SELECTEL_S3_QUARANTINE_READ_SECRET_KEY=change-me
# =============================================================================
# Observability
# =============================================================================
OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317
```
Все переменные — **только** в `backend/.env`. Отдельного хранилища нет.
**Webhook-токены** (публичные callback, не service API): `BITRIX_APPLICATION_TOKEN`, `BITRIX_SYNC_WEBHOOK_TOKEN`.
## Namespace переменных Bitrix
- `bitrix-local-app`: `BITRIX_CLIENT_*`, `BITRIX_CONNECTOR_*`, `BITRIX_PUBLIC_BASE_URL`, `BITRIX_DATABASE_URL`, `BITRIX_API_FORWARD_URL`, `BITRIX_APPLICATION_TOKEN` + service tokens.
- `api-backend`: `BITRIX_LOCAL_APP_BASE_URL`, `MESSAGE_SAFETY_URL` + service tokens; **бизнес-настройки** — из `app_settings`.
- `bitrix-sync`: `BITRIX_SYNC_*`, `BITRIX_SYNC_WEBHOOK_TOKEN`, `BITRIX_SYNC_SERVICE_TOKEN`.
- `bitrix-sync` не читает `BITRIX_CLIENT_ID` / `BITRIX_CLIENT_SECRET`.
## Разрешённые типы файлов чата (MVP)
Источник значений — ключи **`app_settings`** (раздел «Seed MVP»). Остальные arch-* **ссылаются сюда**.
| Ключ | MVP-значение |
|---|---|
| `chat.attachments.allowed_extensions` | `jpg`, `jpeg`, `png`, `webp`, `heic`, `heif`, `pdf` |
| `chat.attachments.allowed_mime_types` | `image/jpeg`, `image/png`, `image/webp`, `image/heic`, `image/heif`, `application/pdf` |
| `chat.attachments.max_size_mb` | `5` |
Правило: файл принимается только если **и** расширение, **и** MIME в allow-list. Детальная проверка — модуль `message-safety`.
Публичный UI: `GET /api/v1/public/app-config` (строгий DTO, без секретов).
## Публичный config endpoint
`GET /api/v1/public/app-config` — только ключи с `is_public=true` из `app_settings`:
- OTP по телефону (`auth.phone.enabled`);
- номер оператора;
- типы файлов и max size;
- `ux.session.idle_timeout_minutes`;
- feature flags;
- публичные лимиты для подсказок UI.
Секреты, service tokens, внутренние URL **не** возвращаются. DTO явный, не сериализация всей таблицы. Rate limit: 60/min per IP. `Cache-Control: public, max-age` из `security.public_cache.max_age_seconds`.
## Публичный content endpoint
`GET /api/v1/public/content``text_resources` для текущего языка. Те же требования безопасности, что у config.
## Nginx и HTTPS
Infra-переменные — `.env.example` (`NGINX_*`, `TLS_*`). Edge rate limits (`NGINX_RATE_LIMIT_*`) **не** дублируют `rate_limit.*` из `app_settings`: nginx — защита периметра, app — бизнес-лимиты в backend.
Реализация — [`arch-03-docker-compose-blueprint.md`](arch-03-docker-compose-blueprint.md).