from pydantic import Field from pydantic_settings import BaseSettings, SettingsConfigDict def build_database_url( *, user: str, password: str, host: str, port: int, database: str, ) -> str: return f"postgresql+asyncpg://{user}:{password}@{host}:{port}/{database}" class Settings(BaseSettings): app_env: str = "development" database_url_override: str | None = Field(default=None, alias="DATABASE_URL") postgres_user: str = "headquarter" postgres_password: str = "headquarter" postgres_host: str = "postgres" postgres_port: int = 5432 postgres_db: str = "headquarter" # Domain configuration api_domain: str = "localhost" web_domain: str = "localhost" authentik_domain: str = "authentik.local" # Public URLs (constructed from domains if not explicitly set) api_public_url: str | None = None web_public_url: str | None = None # Authentik configuration - no hardcoded URLs authentik_client_id: str = "headquarter-web" authentik_client_secret: str = "change-me" # Authentik application slug used in URLs (e.g., "headquarter-web") # This is different from the OAuth client_id which may be a UUID authentik_application_slug: str = "headquarter-web" authentik_authorize_url: str | None = None authentik_token_url: str | None = None authentik_jwks_url: str | None = None authentik_issuer: str | None = None authentik_audience: str = "headquarter-web" # Session configuration session_secret: str = "change-me-session-secret" session_ttl_hours: int = 24 # Internal JWT configuration (used for service-to-service tokens) jwt_algorithm: str = "HS256" access_token_ttl_minutes: int = 15 refresh_token_ttl_days: int = 7 # Repository storage repo_base_path: str = "/data/repos" # Tool instance storage instance_base_path: str = "/data/instances" model_config = SettingsConfigDict(env_file=".env", extra="ignore", populate_by_name=True) @property def database_url(self) -> str: if self.database_url_override: return self.database_url_override return build_database_url( user=self.postgres_user, password=self.postgres_password, host=self.postgres_host, port=self.postgres_port, database=self.postgres_db, ) @property def api_base_url(self) -> str: if self.api_public_url: return self.api_public_url protocol = "https" if self.app_env == "production" else "http" port = "" if self.app_env == "production" else ":8000" return f"{protocol}://{self.api_domain}{port}" @property def web_base_url(self) -> str: if self.web_public_url: return self.web_public_url protocol = "https" if self.app_env == "production" else "http" port = "" if self.app_env == "production" else ":3000" return f"{protocol}://{self.web_domain}{port}" @property def authentik_base_url(self) -> str: protocol = "https" if self.app_env == "production" else "http" return f"{protocol}://{self.authentik_domain}" @property def resolved_authentik_authorize_url(self) -> str: if self.authentik_authorize_url: return self.authentik_authorize_url return f"{self.authentik_base_url}/application/o/authorize/" @property def resolved_authentik_token_url(self) -> str: if self.authentik_token_url: return self.authentik_token_url return f"{self.authentik_base_url}/application/o/token/" @property def resolved_authentik_jwks_url(self) -> str: if self.authentik_jwks_url: return self.authentik_jwks_url return f"{self.authentik_base_url}/application/o/{self.authentik_application_slug}/jwks/" @property def resolved_authentik_issuer(self) -> str: if self.authentik_issuer: return self.authentik_issuer return f"{self.authentik_base_url}/application/o/{self.authentik_application_slug}/" @property def cookie_secure(self) -> bool: return self.app_env == "production" @property def cookie_samesite(self) -> str: if self.app_env == "production": return "strict" return "lax" @property def cookie_domain(self) -> str | None: """Return the parent domain for cross-subdomain cookies. E.g., api.example.com and app.example.com both share .example.com """ if self.app_env != "production": return None # Extract parent domain from api_domain # e.g., "api.headquarter.commumedia.org" -> ".headquarter.commumedia.org" parts = self.api_domain.split(".") if len(parts) >= 3: return "." + ".".join(parts[1:]) return None