import os from dataclasses import dataclass from pathlib import Path DEFAULT_SQLITE_PATH = (Path(__file__).resolve().parents[2] / "data_entry_app.db").as_posix() DEFAULT_DATABASE_URL = f"sqlite:///{DEFAULT_SQLITE_PATH}" DEFAULT_CORS_ALLOW_ORIGIN_REGEX = ( r"^https?://(" r"localhost|127\.0\.0\.1|" r"10\.\d{1,3}\.\d{1,3}\.\d{1,3}|" r"192\.168\.\d{1,3}\.\d{1,3}|" r"172\.(1[6-9]|2\d|3[0-1])\.\d{1,3}\.\d{1,3}" r")(:\d+)?$" ) def _parse_csv_env(value: str) -> tuple[str, ...]: return tuple(part.strip() for part in value.split(",") if part.strip()) def _env_flag(name: str, default: bool = False) -> bool: value = os.getenv(name) if value is None: return default return value.strip().lower() in {"1", "true", "yes", "on"} @dataclass(frozen=True) class Settings: app_name: str app_env: str host: str port: int log_level: str log_verbose: bool database_url: str client_name: str client_email: str client_password: str client_tenant_id: str admin_name: str admin_email: str admin_password: str auth_secret: str cors_allow_origins: tuple[str, ...] cors_allow_origin_regex: str session_ttl_seconds: int session_cookie_name: str admin_session_cookie_name: str session_cookie_secure: bool session_cookie_samesite: str session_cookie_domain: str | None request_body_max_bytes: int login_rate_limit_attempts: int login_rate_limit_window_seconds: int trusted_hosts: tuple[str, ...] docs_enabled: bool @classmethod def from_env(cls) -> "Settings": settings = cls( app_name=os.getenv("APP_NAME", "Hunter App"), app_env=os.getenv("APP_ENV", os.getenv("ENVIRONMENT", "development")), host=os.getenv("HOST", "0.0.0.0"), port=int(os.getenv("PORT", "8000")), log_level=os.getenv("LOG_LEVEL", "DEBUG" if os.getenv("LOG_VERBOSE") in {"1", "true", "TRUE", "yes", "on"} else "INFO"), log_verbose=_env_flag("LOG_VERBOSE"), # Keep the default SQLite location stable regardless of the current # working directory so local dev does not silently fork data into # multiple `data_entry_app.db` files. database_url=os.getenv("DATABASE_URL", DEFAULT_DATABASE_URL), client_name=os.getenv("CLIENT_NAME", "Hunter Premium Produce"), client_email=os.getenv("CLIENT_EMAIL", "operator@example.com"), client_password=os.getenv("CLIENT_PASSWORD", "changeme"), client_tenant_id=os.getenv("CLIENT_TENANT_ID", "hunter-premium-produce"), admin_name=os.getenv("ADMIN_NAME", "Lean 101"), admin_email=os.getenv("ADMIN_EMAIL", "admin@lean101.local"), admin_password=os.getenv("ADMIN_PASSWORD", "lean101-admin"), auth_secret=os.getenv("AUTH_SECRET", "lean-101-local-dev-secret"), cors_allow_origins=_parse_csv_env( os.getenv( "CORS_ALLOW_ORIGINS", "http://localhost:5173,http://localhost:5174,http://127.0.0.1:5173,http://127.0.0.1:5174", ) ), cors_allow_origin_regex=os.getenv("CORS_ALLOW_ORIGIN_REGEX", DEFAULT_CORS_ALLOW_ORIGIN_REGEX), session_ttl_seconds=int(os.getenv("SESSION_TTL_SECONDS", str(60 * 60 * 12))), session_cookie_name=os.getenv("SESSION_COOKIE_NAME", "client_session"), admin_session_cookie_name=os.getenv("ADMIN_SESSION_COOKIE_NAME", "admin_session"), session_cookie_secure=_env_flag("SESSION_COOKIE_SECURE"), session_cookie_samesite=os.getenv("SESSION_COOKIE_SAMESITE", "lax").lower(), session_cookie_domain=os.getenv("SESSION_COOKIE_DOMAIN", "").strip() or None, request_body_max_bytes=int(os.getenv("REQUEST_BODY_MAX_BYTES", str(1024 * 1024))), login_rate_limit_attempts=int(os.getenv("LOGIN_RATE_LIMIT_ATTEMPTS", "8")), login_rate_limit_window_seconds=int(os.getenv("LOGIN_RATE_LIMIT_WINDOW_SECONDS", "300")), trusted_hosts=_parse_csv_env(os.getenv("TRUSTED_HOSTS", "localhost,127.0.0.1,testserver")), docs_enabled=_env_flag("DOCS_ENABLED", default=os.getenv("APP_ENV", os.getenv("ENVIRONMENT", "development")).lower() != "production"), ) settings._validate() return settings def _validate(self) -> None: if self.session_cookie_samesite not in {"lax", "strict", "none"}: raise ValueError("SESSION_COOKIE_SAMESITE must be one of: lax, strict, none") is_production = self.app_env.lower() == "production" if not is_production: return if self.client_password in {"changeme", "", "replace-with-strong-password"}: raise ValueError("CLIENT_PASSWORD must be set to a non-default value in production") if self.admin_password in {"lean101-admin", "", "replace-with-strong-password"}: raise ValueError("ADMIN_PASSWORD must be set to a non-default value in production") if self.auth_secret in {"lean-101-local-dev-secret", "change-me-in-production", "", "replace-with-a-long-random-secret"}: raise ValueError("AUTH_SECRET must be set to a strong production secret") if len(self.auth_secret) < 32: raise ValueError("AUTH_SECRET must be at least 32 characters in production") if not self.session_cookie_secure: raise ValueError("SESSION_COOKIE_SECURE must be enabled in production") if not self.cors_allow_origins: raise ValueError("CORS_ALLOW_ORIGINS must explicitly list production origins") if "localhost" in ",".join(self.cors_allow_origins).lower(): raise ValueError("CORS_ALLOW_ORIGINS cannot include localhost in production") if self.cors_allow_origin_regex == DEFAULT_CORS_ALLOW_ORIGIN_REGEX: raise ValueError("CORS_ALLOW_ORIGIN_REGEX must be overridden or blank in production") if self.docs_enabled: raise ValueError("DOCS_ENABLED must be false in production") settings = Settings.from_env()