from functools import lru_cache from pydantic_settings import BaseSettings, SettingsConfigDict class Settings(BaseSettings): database_url: str = "postgresql+asyncpg://drivers:drivers@localhost:5432/drivers" bot_token: str = "" bot_username: str = "" api_base_url: str = "http://localhost:8000" webapp_url: str = "http://localhost:8000" public_webapp_url: str | None = None app_host: str = "0.0.0.0" app_port: int = 8000 app_env: str = "production" cors_origins: str = "" internal_api_token: str = "" vapid_public_key: str = "" vapid_private_key: str = "" secret_key: str = "" redis_url: str = "" allow_dev_auth: bool = False ocr_provider: str = "tesseract" ocr_languages: str = "eng+rus+kor" admin_telegram_ids: str = "" admin_bootstrap_token: str = "" admin_notification_chat_id: str = "" admin_notify_new_users: bool = True admin_notify_sto_applications: bool = True admin_notify_security_events: bool = True admin_notify_system_errors: bool = True model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8", extra="ignore") @property def cors_origin_list(self) -> list[str]: return [item.strip().rstrip("/") for item in self.cors_origins.split(",") if item.strip()] @property def effective_webapp_url(self) -> str: return (self.public_webapp_url or self.webapp_url).rstrip("/") @property def is_production(self) -> bool: return self.app_env.lower() == "production" @property def admin_telegram_id_list(self) -> list[int]: values: list[int] = [] for item in self.admin_telegram_ids.split(","): item = item.strip() if not item: continue values.append(int(item)) return values def validate_webapp_url_for_telegram(self) -> None: url = self.effective_webapp_url if self.is_production and not url.startswith("https://"): raise RuntimeError("WEBAPP_URL/PUBLIC_WEBAPP_URL must be public HTTPS in production") forbidden = ("localhost", "127.0.0.1", "0.0.0.0", "http://") if self.is_production and any(item in url for item in forbidden): raise RuntimeError("Telegram Mini App URL must not use localhost, internal IP, or http://") def validate_production_settings(self) -> None: if not self.is_production: return if self.allow_dev_auth: raise RuntimeError("ALLOW_DEV_AUTH must be false in production") if not self.bot_token or self.bot_token == "change-me": raise RuntimeError("BOT_TOKEN is required in production") if not self.internal_api_token or self.internal_api_token.startswith("change-"): raise RuntimeError("INTERNAL_API_TOKEN must be a real secret in production") if not self.secret_key or self.secret_key.startswith("change-"): raise RuntimeError("SECRET_KEY must be configured in production") if not self.redis_url: raise RuntimeError("REDIS_URL is required in production for rate limiting and queues") if bool(self.vapid_public_key) != bool(self.vapid_private_key): raise RuntimeError("VAPID_PUBLIC_KEY and VAPID_PRIVATE_KEY must be configured together") if not self.cors_origin_list: raise RuntimeError("CORS_ORIGINS is required in production") self.validate_webapp_url_for_telegram() @lru_cache def get_settings() -> Settings: return Settings() settings = get_settings()