Files
data-entry-app/backend/app/core/config.py
T

126 lines
5.7 KiB
Python
Raw Normal View History

2026-04-25 20:43:37 +12:00
import os
from dataclasses import dataclass
2026-04-27 21:53:36 +12:00
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())
2026-05-10 09:46:07 +12:00
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"}
2026-04-25 20:43:37 +12:00
@dataclass(frozen=True)
class Settings:
app_name: str
2026-05-10 09:46:07 +12:00
app_env: str
host: str
port: int
log_level: str
log_verbose: bool
2026-04-25 20:43:37 +12:00
database_url: str
2026-04-25 22:51:36 +12:00
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
2026-04-27 21:53:36 +12:00
cors_allow_origins: tuple[str, ...]
cors_allow_origin_regex: str
2026-05-10 09:46:07 +12:00
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
2026-04-25 20:43:37 +12:00
@classmethod
def from_env(cls) -> "Settings":
2026-05-10 09:46:07 +12:00
settings = cls(
2026-04-25 20:43:37 +12:00
app_name=os.getenv("APP_NAME", "Data Entry App API"),
2026-05-10 09:46:07 +12:00
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"),
2026-04-25 20:43:37 +12:00
database_url=os.getenv("DATABASE_URL", "sqlite:///./data_entry_app.db"),
2026-04-25 22:51:36 +12:00
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"),
2026-04-27 21:53:36 +12:00
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),
2026-05-10 09:46:07 +12:00
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"),
2026-04-25 20:43:37 +12:00
)
2026-05-10 09:46:07 +12:00
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")
2026-04-25 20:43:37 +12:00
settings = Settings.from_env()