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