Newer
Older
gnexus-tasks / backend / app / config.py
"""Конфигурация приложения. Все значения приходят из окружения (см. .env.example)."""

from functools import lru_cache

from pydantic import model_validator
from pydantic_settings import BaseSettings, SettingsConfigDict

# Дефолт в репозитории — подделываемая кука, если при деплое забыть SESSION_SECRET.
# Падаем на старте вместо молчаливо небезопасной сессии (остальные секреты и так
# обязательны — без них gauth не работает).
DEFAULT_SESSION_SECRET = "change-me-session-secret"


class Settings(BaseSettings):
    model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8", extra="ignore")

    # SSO (gnexus-gauth)
    gauth_base_url: str = "https://auth.gnexus.space"
    gauth_client_id: str = ""
    gauth_client_secret: str = ""
    gauth_redirect_uri: str = "http://localhost:8134/auth/callback"
    # Секрет подписи webhook от gnexus-auth (показывается при создании webhook
    # в админке SSO). Пусто — приём webhook выключен (эндпоинт отвечает 503).
    gauth_webhook_secret: str = ""

    # Database
    database_url: str = "postgresql+psycopg://gntodo:gntodo@localhost:5432/gntodo"

    # Session cookie
    session_secret: str = DEFAULT_SESSION_SECRET
    session_cookie_name: str = "gntodo_session"

    # Ollama (M2)
    ollama_base_url: str = "http://192.168.1.130:11434"
    ollama_model: str = "qwen3.5:2b-q4_K_M"

    # Хранилище вложений (M2)
    attachments_path: str = "data/attachments"

    # Кэш favicon сайтов проектов (0.63)
    favicons_path: str = "data/favicons"

    # Web Push (ТЗ 3.21, 0.88): пара VAPID для отправки уведомлений, печатает
    # `uv run python -m app.vapid`. Пусто — уведомления тихо выключены:
    # приложение и тесты работают без ключей. Валидатора-падения здесь нет
    # намеренно — в отличие от SESSION_SECRET это не дыра в безопасности, а
    # опциональная функция, и падать на старте из-за неё нельзя.
    vapid_public_key: str = ""
    vapid_private_key: str = ""
    # Контакт отправителя для push-сервиса (mailto:… или https://…): часть
    # сервисов без него отказывает, поэтому пустое значение подменяем дефолтом.
    vapid_subject: str = ""

    # Часовой пояс напоминаний (ТЗ 3.21, 0.89): «сегодня» считается в нём.
    # Один на инстанс — упрощение, вопрос о per-user поясе открыт (8.13).
    reminder_timezone: str = "Europe/Kyiv"

    # BugTrail (ТЗ 3.22, 0.92): трекер багов владельца, из ссылки на отчёт
    # собирается черновик задачи. `bugtrail_url` — база API (по ней и ходим),
    # `_panel_url` — база панели (человеческие ссылки в описании), пусто —
    # берётся API-база. Пустой `bugtrail_url` выключает импорт (эндпоинт 503).
    bugtrail_url: str = "https://bugtrail.gnexus.space"
    bugtrail_panel_url: str = ""

    @property
    def push_enabled(self) -> bool:
        return bool(self.vapid_public_key and self.vapid_private_key)

    @property
    def bugtrail_enabled(self) -> bool:
        return bool(self.bugtrail_url)

    @property
    def bugtrail_panel(self) -> str:
        """База панели трекера для ссылок в тексте задачи (fallback — API-база)."""
        return (self.bugtrail_panel_url or self.bugtrail_url).rstrip("/")

    @model_validator(mode="after")
    def _session_secret_must_be_set(self) -> "Settings":
        if self.session_secret == DEFAULT_SESSION_SECRET:
            raise ValueError(
                "SESSION_SECRET не задан: дефолтный секрет из репозитория позволяет "
                "подделать куку сессии. Задайте SESSION_SECRET в .env."
            )
        return self


@lru_cache
def get_settings() -> Settings:
    return Settings()