105 lines
3.9 KiB
Python
105 lines
3.9 KiB
Python
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()
|