Updates
This commit is contained in:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user