import sys from pydantic import model_validator from pydantic_settings import BaseSettings, SettingsConfigDict class Settings(BaseSettings): DATABASE_URL: str = "postgresql+asyncpg://postgres:postgres@localhost:5432/umbra" SECRET_KEY: str = "your-secret-key-change-in-production" ENVIRONMENT: str = "development" COOKIE_SECURE: bool | None = None # Auto-derives from ENVIRONMENT if not explicitly set OPENWEATHERMAP_API_KEY: str = "" # Session config — sliding window SESSION_MAX_AGE_DAYS: int = 7 # Sliding window: inactive sessions expire after 7 days SESSION_TOKEN_HARD_CEILING_DAYS: int = 30 # Absolute token lifetime for itsdangerous max_age # MFA token config (short-lived token bridging password OK → TOTP verification) MFA_TOKEN_MAX_AGE_SECONDS: int = 300 # 5 minutes # TOTP issuer name shown in authenticator apps TOTP_ISSUER: str = "UMBRA" # CORS allowed origins (comma-separated). Auto-derives from UMBRA_URL in # production or http://localhost:5173 in development. Set explicitly to override. CORS_ORIGINS: str | None = None # Public-facing URL used in ntfy notification click links and CORS derivation UMBRA_URL: str = "http://localhost" # Concurrent session limit per user (oldest evicted when exceeded) MAX_SESSIONS_PER_USER: int = 10 # WebAuthn / Passkey configuration WEBAUTHN_RP_ID: str = "localhost" # eTLD+1 domain, e.g. "umbra.ghost6.xyz" WEBAUTHN_RP_NAME: str = "UMBRA" WEBAUTHN_ORIGIN: str = "http://localhost" # Full origin with scheme, e.g. "https://umbra.ghost6.xyz" WEBAUTHN_CHALLENGE_TTL: int = 60 # Challenge token lifetime in seconds model_config = SettingsConfigDict( env_file=".env", env_file_encoding="utf-8", case_sensitive=True ) @model_validator(mode="after") def derive_defaults(self) -> "Settings": if self.COOKIE_SECURE is None: self.COOKIE_SECURE = self.ENVIRONMENT == "production" if self.CORS_ORIGINS is None: if self.ENVIRONMENT == "production": self.CORS_ORIGINS = self.UMBRA_URL else: self.CORS_ORIGINS = "http://localhost:5173" assert self.COOKIE_SECURE is not None # type narrowing assert self.CORS_ORIGINS is not None # Validate WebAuthn origin includes scheme (S-04) if not self.WEBAUTHN_ORIGIN.startswith(("http://", "https://")): raise ValueError("WEBAUTHN_ORIGIN must include scheme (http:// or https://)") return self settings = Settings() print( f"INFO: COOKIE_SECURE={settings.COOKIE_SECURE} " f"(ENVIRONMENT={settings.ENVIRONMENT})", file=sys.stderr, ) if settings.SECRET_KEY == "your-secret-key-change-in-production": if settings.ENVIRONMENT != "development": print( "FATAL: Default SECRET_KEY detected in non-development environment. " "Set a unique SECRET_KEY in .env immediately.", file=sys.stderr, ) sys.exit(1) else: print( "WARNING: Using default SECRET_KEY. Set SECRET_KEY in .env for production.", file=sys.stderr, )