This commit is contained in:
2026-05-10 09:46:07 +12:00
parent cfc193b713
commit 2f2466ecac
81 changed files with 2571 additions and 413 deletions
+69 -1
View File
@@ -16,9 +16,21 @@ 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
@@ -30,11 +42,27 @@ class Settings:
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":
return cls(
settings = cls(
app_name=os.getenv("APP_NAME", "Data Entry App API"),
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"),
database_url=os.getenv("DATABASE_URL", "sqlite:///./data_entry_app.db"),
client_name=os.getenv("CLIENT_NAME", "Hunter Premium Produce"),
client_email=os.getenv("CLIENT_EMAIL", "operator@example.com"),
@@ -51,7 +79,47 @@ class Settings:
)
),
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()