Newer
Older
gn-synapse / app / settings_registry.py
"""Реестр настраиваемых через UI параметров Synapse.

.env задаёт дефолты, admin UI перебивает их оверрайдами в таблице
app_settings (app/settings_store.py). Значения секретов хранятся в БД
write-only: никогда не возвращаются через API. Каналы Telegram/SMTP пока
заглушки (#29) — конфиги сохраняем заранее.
"""

from dataclasses import dataclass


@dataclass(frozen=True)
class SettingDef:
    key: str
    section: str  # routing | push | telegram | smtp
    label: str
    kind: str  # "str" | "int" | "bool" | "secret"
    options: tuple[str, ...] = ()  # для enum — допустимые значения
    help: str = ""


EDITABLE: dict[str, SettingDef] = {d.key: d for d in (
    SettingDef(
        "routing_match_mode", "routing", "Режим маршрутизации", "str",
        options=("all", "first"),
        help="all — все подошедшие правила; first — только первое по weight",
    ),
    SettingDef(
        "dedup_window_seconds", "routing", "Окно дедупликации, сек", "int",
        help="Дубль с тем же dedup_key внутри окна вернёт id первого события",
    ),
    SettingDef(
        "retention_days", "routing", "Ретеншн, дней", "int",
        help="События старше N дней удаляются; 0 — отключить окно",
    ),
    SettingDef(
        "vapid_public_key", "push", "VAPID public key", "str",
        help="URLsafe base64 публичный ключ (p256dh)",
    ),
    SettingDef(
        "vapid_private_key", "push", "VAPID private key", "secret",
        help="Секрет хранится в БД write-only, в API возвращается только факт «задано»",
    ),
    SettingDef(
        "push_subject", "push", "Subject (отправитель)", "str",
        help="Контакты отправителя для веб-пушей, например mailto:ops@gnexus.space",
    ),
    SettingDef(
        "telegram_bot_token", "telegram", "Токен тг-бота", "secret",
        help="Канал ещё не реализован (#29) — токен сохраняется впрок",
    ),
    SettingDef("smtp_host", "smtp", "SMTP host", "str"),
    SettingDef("smtp_port", "smtp", "SMTP port", "int"),
    SettingDef("smtp_user", "smtp", "SMTP user", "str"),
    SettingDef("smtp_password", "smtp", "SMTP password", "secret"),
    SettingDef(
        "smtp_from", "smtp", "From-адрес", "str",
        help='Например "Synapse <synapse@gnexus.space>"',
    ),
)}

SECRET_KINDS = ("secret",)

INT_RANGES = {
    "dedup_window_seconds": (1, 10**6),
    "retention_days": (0, 366),
    "smtp_port": (1, 65535),
}


def is_secret(defn: SettingDef) -> bool:
    return defn.kind in SECRET_KINDS


def validate(defn: SettingDef, raw: str) -> str | None:
    """Валидация значения из UI; None — ок, иначе текст ошибки."""
    if defn.kind == "int":
        # диапазоны все неотрицательные — минус не нужен; "" сразу отвергаем
        if not raw.isdigit():
            return f"{defn.key}: значение должно быть числом"
        low, high = INT_RANGES.get(defn.key, (0, 10**9))
        if not low <= int(raw) <= high:
            return f"{defn.key}: значение вне допустимого диапазона {low}..{high}"
    if defn.kind == "bool" and raw not in ("true", "false"):
        return f"{defn.key}: значение должно быть true/false"
    if defn.options and raw not in defn.options:
        return f"{defn.key}: допустимо {' или '.join(defn.options)}"
    return None