189 lines
6.3 KiB
Python
189 lines
6.3 KiB
Python
from __future__ import annotations
|
|
|
|
import os
|
|
from dataclasses import dataclass
|
|
from datetime import timedelta
|
|
from functools import lru_cache
|
|
|
|
from typing import Optional
|
|
|
|
from services.security import JWTSettings
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class AdminBootstrapSettings:
|
|
"""Default administrator bootstrap configuration."""
|
|
|
|
email: str
|
|
username: str
|
|
password: str
|
|
roles: tuple[str, ...]
|
|
force_reset: bool
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class SessionSettings:
|
|
"""Cookie and header configuration for session token transport."""
|
|
|
|
access_cookie_name: str
|
|
refresh_cookie_name: str
|
|
cookie_secure: bool
|
|
cookie_domain: Optional[str]
|
|
cookie_path: str
|
|
header_name: str
|
|
header_prefix: str
|
|
allow_header_fallback: bool
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class Settings:
|
|
"""Application configuration sourced from environment variables."""
|
|
|
|
jwt_secret_key: str = "change-me"
|
|
jwt_algorithm: str = "HS256"
|
|
jwt_access_token_minutes: int = 15
|
|
jwt_refresh_token_days: int = 7
|
|
session_access_cookie_name: str = "calminer_access_token"
|
|
session_refresh_cookie_name: str = "calminer_refresh_token"
|
|
session_cookie_secure: bool = False
|
|
session_cookie_domain: Optional[str] = None
|
|
session_cookie_path: str = "/"
|
|
session_header_name: str = "Authorization"
|
|
session_header_prefix: str = "Bearer"
|
|
session_allow_header_fallback: bool = True
|
|
admin_email: str = "admin@calminer.local"
|
|
admin_username: str = "admin"
|
|
admin_password: str = "ChangeMe123!"
|
|
admin_roles: tuple[str, ...] = ("admin",)
|
|
admin_force_reset: bool = False
|
|
|
|
@classmethod
|
|
def from_environment(cls) -> "Settings":
|
|
"""Construct settings from environment variables."""
|
|
|
|
return cls(
|
|
jwt_secret_key=os.getenv("CALMINER_JWT_SECRET", "change-me"),
|
|
jwt_algorithm=os.getenv("CALMINER_JWT_ALGORITHM", "HS256"),
|
|
jwt_access_token_minutes=cls._int_from_env(
|
|
"CALMINER_JWT_ACCESS_MINUTES", 15
|
|
),
|
|
jwt_refresh_token_days=cls._int_from_env(
|
|
"CALMINER_JWT_REFRESH_DAYS", 7
|
|
),
|
|
session_access_cookie_name=os.getenv(
|
|
"CALMINER_SESSION_ACCESS_COOKIE", "calminer_access_token"
|
|
),
|
|
session_refresh_cookie_name=os.getenv(
|
|
"CALMINER_SESSION_REFRESH_COOKIE", "calminer_refresh_token"
|
|
),
|
|
session_cookie_secure=cls._bool_from_env(
|
|
"CALMINER_SESSION_COOKIE_SECURE", False
|
|
),
|
|
session_cookie_domain=os.getenv("CALMINER_SESSION_COOKIE_DOMAIN"),
|
|
session_cookie_path=os.getenv("CALMINER_SESSION_COOKIE_PATH", "/"),
|
|
session_header_name=os.getenv(
|
|
"CALMINER_SESSION_HEADER_NAME", "Authorization"
|
|
),
|
|
session_header_prefix=os.getenv(
|
|
"CALMINER_SESSION_HEADER_PREFIX", "Bearer"
|
|
),
|
|
session_allow_header_fallback=cls._bool_from_env(
|
|
"CALMINER_SESSION_ALLOW_HEADER_FALLBACK", True
|
|
),
|
|
admin_email=os.getenv(
|
|
"CALMINER_SEED_ADMIN_EMAIL", "admin@calminer.local"
|
|
),
|
|
admin_username=os.getenv(
|
|
"CALMINER_SEED_ADMIN_USERNAME", "admin"
|
|
),
|
|
admin_password=os.getenv(
|
|
"CALMINER_SEED_ADMIN_PASSWORD", "ChangeMe123!"
|
|
),
|
|
admin_roles=cls._parse_admin_roles(
|
|
os.getenv("CALMINER_SEED_ADMIN_ROLES")
|
|
),
|
|
admin_force_reset=cls._bool_from_env(
|
|
"CALMINER_SEED_FORCE", False
|
|
),
|
|
)
|
|
|
|
@staticmethod
|
|
def _int_from_env(name: str, default: int) -> int:
|
|
raw_value = os.getenv(name)
|
|
if raw_value is None:
|
|
return default
|
|
try:
|
|
return int(raw_value)
|
|
except ValueError:
|
|
return default
|
|
|
|
@staticmethod
|
|
def _bool_from_env(name: str, default: bool) -> bool:
|
|
raw_value = os.getenv(name)
|
|
if raw_value is None:
|
|
return default
|
|
lowered = raw_value.strip().lower()
|
|
if lowered in {"1", "true", "yes", "on"}:
|
|
return True
|
|
if lowered in {"0", "false", "no", "off"}:
|
|
return False
|
|
return default
|
|
|
|
@staticmethod
|
|
def _parse_admin_roles(raw_value: str | None) -> tuple[str, ...]:
|
|
if not raw_value:
|
|
return ("admin",)
|
|
parts = [segment.strip()
|
|
for segment in raw_value.split(",") if segment.strip()]
|
|
if "admin" not in parts:
|
|
parts.insert(0, "admin")
|
|
seen: set[str] = set()
|
|
ordered: list[str] = []
|
|
for role_name in parts:
|
|
if role_name not in seen:
|
|
ordered.append(role_name)
|
|
seen.add(role_name)
|
|
return tuple(ordered)
|
|
|
|
def jwt_settings(self) -> JWTSettings:
|
|
"""Build runtime JWT settings compatible with token helpers."""
|
|
|
|
return JWTSettings(
|
|
secret_key=self.jwt_secret_key,
|
|
algorithm=self.jwt_algorithm,
|
|
access_token_ttl=timedelta(minutes=self.jwt_access_token_minutes),
|
|
refresh_token_ttl=timedelta(days=self.jwt_refresh_token_days),
|
|
)
|
|
|
|
def session_settings(self) -> SessionSettings:
|
|
"""Provide transport configuration for session tokens."""
|
|
|
|
return SessionSettings(
|
|
access_cookie_name=self.session_access_cookie_name,
|
|
refresh_cookie_name=self.session_refresh_cookie_name,
|
|
cookie_secure=self.session_cookie_secure,
|
|
cookie_domain=self.session_cookie_domain,
|
|
cookie_path=self.session_cookie_path,
|
|
header_name=self.session_header_name,
|
|
header_prefix=self.session_header_prefix,
|
|
allow_header_fallback=self.session_allow_header_fallback,
|
|
)
|
|
|
|
def admin_bootstrap_settings(self) -> AdminBootstrapSettings:
|
|
"""Return configured admin bootstrap settings."""
|
|
|
|
return AdminBootstrapSettings(
|
|
email=self.admin_email,
|
|
username=self.admin_username,
|
|
password=self.admin_password,
|
|
roles=self.admin_roles,
|
|
force_reset=self.admin_force_reset,
|
|
)
|
|
|
|
|
|
@lru_cache(maxsize=1)
|
|
def get_settings() -> Settings:
|
|
"""Return cached application settings."""
|
|
|
|
return Settings.from_environment()
|