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

22 KiB
Raw Blame History

arch-04. Настройки и изменяемые параметры

.env — инфраструктура и секреты. app_settings (App DB) — единственный источник бизнес-настроек. Имена полей и enum — arch-00-glossary.md. Docker Compose — arch-03-docker-compose-blueprint.md.

Цель

Параметры разделены по слоям:

Слой Где Что
Инфраструктура .env подключения, URL, секреты, nginx/TLS, service tokens
Бизнес-логика таблица app_settings лимиты, флаги, телефоны, типы файлов, CORS, consent URLs
Настройки SMS runtime таблица sms.sms_setting sender default, provider timeouts, callback flag, worker intervals
Контент 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 mock (KEYCLOAK_OTP_MOCK_*); mock обязателен до прохождения real-SMS rollout gates и запрещён как незаявленный fallback;
  • технические параметры сервисов, пока профильная спецификация не определила service-owned settings; для sms-service runtime-параметры уже вынесены в sms.sms_setting.

Запрещено в .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-миграция.

Контент 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, otp.phone.max_verify_attempts, otp.phone.code_length, otp.phone.ttl_seconds, otp.phone.sms_order_timeout_ms
Оператор 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

auth.phone.enabled=true
auth.password.enabled=false

otp.phone.max_send_attempts_per_24h=3
otp.phone.min_seconds_between_attempts=30
otp.phone.max_verify_attempts=5
otp.phone.code_length=6
otp.phone.ttl_seconds=60
otp.phone.sms_order_timeout_ms=3000

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

Service-owned настройки sms-service

Параметры, изменение которых не меняет Compose, секреты, URL или сетевую топологию, хранятся в sms.sms_setting, а не в .env.

Ключи и seed:

provider.idgtl.default_sender_name=<approved>
provider.idgtl.connect_timeout_ms=3000
provider.idgtl.request_timeout_ms=70000
provider.idgtl.callback_enabled=true
worker.poll_interval_ms=500
worker.lease_seconds=90

В .env остаются только SMS_DATABASE_URL, URL внутренних/внешних сервисов, service tokens, Direct API key и callback credentials. Детальный контракт — module-11-idgtl-sms.md.

<approved> — обязательный deployment placeholder, а не допустимое production-значение. Перед real mode должны существовать active approved template auth_otp с точными placeholders code/ttl_min и согласованный senderName. Отсутствие template/sender делает readiness false.


Пример .env.example

Только инфраструктура. Бизнес-параметры — в seed app_settings.

# =============================================================================
# Общие
# =============================================================================
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>
SMS_DATABASE_URL=postgresql://sms_user: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 (mock остаётся true до controlled SMS cutover)
# =============================================================================
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
KEYCLOAK_SMS_SERVICE_URL=http://sms-service:8080

# =============================================================================
# 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
SMS_SERVICE_TOKEN=change-me
KEYCLOAK_SMS_SERVICE_TOKEN=change-me

# =============================================================================
# SMS provider (URL и секреты; runtime-параметры — sms.sms_setting)
# =============================================================================
IDGTL_SMS_BASE_URL=https://direct.i-dgtl.ru
IDGTL_SMS_API_KEY=change-me
IDGTL_SMS_CALLBACK_PUBLIC_URL=https://tohin.ru/callbacks/idgtl/sms
IDGTL_SMS_CALLBACK_USERNAME=change-me
IDGTL_SMS_CALLBACK_PASSWORD=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. Отдельного хранилища нет.

Для production change-me, <...>, примерные sender/template/API key/callback credentials отклоняются validate-env. IDGTL_SMS_API_KEY — выданный Direct готовый TOKEN_1 для Basic, без повторного Base64. Реальный статический egress IP хранится в deployment inventory, а не env; если он не обеспечен NAT/сетевой конфигурацией, KEYCLOAK_OTP_MOCK_ENABLED=false запрещён.

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

OTP settings (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 читает limits, otp.phone.code_length, otp.phone.ttl_seconds и otp.phone.sms_order_timeout_ms через этот endpoint с локальным cache.
  4. При недоступности settings bridge SPI использует последнее валидное cache-значение; если cache пустой — fail-closed и не выдаёт OTP.

Challenge сохраняет snapshot TTL, длины кода и settings_version; изменение settings влияет только на новые challenges. Счётчики попыток 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/contenttext_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.