from functools import lru_cache from pydantic import field_validator, model_validator from pydantic_settings import BaseSettings, SettingsConfigDict _INSECURE_SECRET_KEYS = frozenset( { "change-me", "change-me-to-a-long-random-string-in-production", "changeme", "secret", } ) class Settings(BaseSettings): model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8", extra="ignore") environment: str = "development" database_url: str secret_key: str access_token_expire_minutes: int = 60 * 24 payment_provider: str = "mock" payment_mock_secret: str = "dev-mock-payment-secret-change-me" razorpay_key_id: str | None = None razorpay_key_secret: str | None = None razorpay_webhook_secret: str | None = None order_payment_timeout_minutes: int = 15 ticket_verify_api_key: str | None = None @field_validator("secret_key") @classmethod def secret_key_must_be_strong_in_production(cls, value: str, info) -> str: environment = (info.data.get("environment") or "development").lower() if environment == "production" and value.strip().lower() in _INSECURE_SECRET_KEYS: raise ValueError("SECRET_KEY must be set to a long random value in production") if environment == "production" and len(value) < 32: raise ValueError("SECRET_KEY must be at least 32 characters in production") return value @field_validator("access_token_expire_minutes") @classmethod def token_ttl_reasonable(cls, value: int) -> int: if value < 5 or value > 60 * 24 * 14: raise ValueError("ACCESS_TOKEN_EXPIRE_MINUTES must be between 5 and 20160") return value cors_origins: str = "http://localhost:5173,http://127.0.0.1:5173" upload_dir: str = "uploads" storage_backend: str = "local" public_site_url: str = "http://localhost:5173" public_api_url: str = "http://localhost:8000" admin_email: str = "admin@desiclans.in" admin_password: str = "changeme" smtp_host: str | None = None smtp_port: int = 587 smtp_user: str | None = None smtp_password: str | None = None smtp_from: str | None = None smtp_use_tls: bool = True alert_notify_email: str | None = None alert_click_threshold: int = 50 aws_access_key_id: str | None = None aws_secret_access_key: str | None = None aws_region: str = "ap-south-1" s3_bucket: str | None = None s3_public_base_url: str | None = None @property def cors_origin_list(self) -> list[str]: return [origin.strip() for origin in self.cors_origins.split(",") if origin.strip()] @property def smtp_configured(self) -> bool: return bool(self.smtp_host and self.alert_notify_email) @model_validator(mode="after") def production_hardening(self) -> "Settings": if self.environment.lower() != "production": return self if self.payment_provider.lower() == "mock": raise ValueError("PAYMENT_PROVIDER must be razorpay in production") if not self.ticket_verify_api_key or len(self.ticket_verify_api_key) < 16: raise ValueError("TICKET_VERIFY_API_KEY is required in production (min 16 characters)") if self.admin_password.strip().lower() in {"changeme", "change-me", "password"}: raise ValueError("ADMIN_PASSWORD must be changed from the default in production") if not self.cors_origin_list: raise ValueError("CORS_ORIGINS must list at least one origin in production") if self.payment_provider.lower() == "razorpay": if not self.razorpay_key_id or not self.razorpay_key_secret or not self.razorpay_webhook_secret: raise ValueError( "RAZORPAY_KEY_ID, RAZORPAY_KEY_SECRET, and RAZORPAY_WEBHOOK_SECRET are required in production" ) return self @lru_cache def get_settings() -> Settings: return Settings()