Auto-derive CORS_ORIGINS from UMBRA_URL in production

In production, CORS_ORIGINS now defaults to UMBRA_URL so deployers only
need to set the external URL once. In development it defaults to
http://localhost:5173 (Vite dev server). Explicit CORS_ORIGINS env var
is still respected as an override for multi-origin or custom setups.

This means a production .env only needs: ENVIRONMENT, SECRET_KEY,
UMBRA_URL, and DB credentials. COOKIE_SECURE and CORS_ORIGINS both
auto-derive.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Kyle 2026-03-02 17:53:26 +08:00
parent cad1ca00c7
commit dadd19bc30
2 changed files with 16 additions and 8 deletions

View File

@ -15,10 +15,7 @@ SECRET_KEY=your-secret-key-change-in-production
# development | production — controls Swagger/ReDoc visibility and cookie defaults # development | production — controls Swagger/ReDoc visibility and cookie defaults
ENVIRONMENT=development ENVIRONMENT=development
# CORS allowed origins (comma-separated, default: http://localhost:5173) # Public URL — used for ntfy click links and auto-derives CORS_ORIGINS in production
# CORS_ORIGINS=https://umbra.example.com
# Public URL for ntfy notification click links (default: http://localhost)
# UMBRA_URL=https://umbra.example.com # UMBRA_URL=https://umbra.example.com
# Timezone (applied to backend + db containers via env_file) # Timezone (applied to backend + db containers via env_file)
@ -35,3 +32,7 @@ OPENWEATHERMAP_API_KEY=your-openweathermap-api-key
# COOKIE_SECURE auto-derives from ENVIRONMENT (production → true). # COOKIE_SECURE auto-derives from ENVIRONMENT (production → true).
# Only set explicitly to override, e.g. false for a non-TLS prod behind a proxy. # Only set explicitly to override, e.g. false for a non-TLS prod behind a proxy.
# COOKIE_SECURE=false # COOKIE_SECURE=false
# CORS_ORIGINS auto-derives from UMBRA_URL in production, http://localhost:5173 in dev.
# Only set explicitly if you need a different origin or multiple origins.
# CORS_ORIGINS=https://custom-domain.example.com

View File

@ -20,10 +20,11 @@ class Settings(BaseSettings):
# TOTP issuer name shown in authenticator apps # TOTP issuer name shown in authenticator apps
TOTP_ISSUER: str = "UMBRA" TOTP_ISSUER: str = "UMBRA"
# CORS allowed origins (comma-separated) # CORS allowed origins (comma-separated). Auto-derives from UMBRA_URL in
CORS_ORIGINS: str = "http://localhost:5173" # production or http://localhost:5173 in development. Set explicitly to override.
CORS_ORIGINS: str | None = None
# Public-facing URL used in ntfy notification click links # Public-facing URL used in ntfy notification click links and CORS derivation
UMBRA_URL: str = "http://localhost" UMBRA_URL: str = "http://localhost"
# Concurrent session limit per user (oldest evicted when exceeded) # Concurrent session limit per user (oldest evicted when exceeded)
@ -36,10 +37,16 @@ class Settings(BaseSettings):
) )
@model_validator(mode="after") @model_validator(mode="after")
def derive_cookie_secure(self) -> "Settings": def derive_defaults(self) -> "Settings":
if self.COOKIE_SECURE is None: if self.COOKIE_SECURE is None:
self.COOKIE_SECURE = self.ENVIRONMENT == "production" self.COOKIE_SECURE = self.ENVIRONMENT == "production"
if self.CORS_ORIGINS is None:
if self.ENVIRONMENT == "production":
self.CORS_ORIGINS = self.UMBRA_URL
else:
self.CORS_ORIGINS = "http://localhost:5173"
assert self.COOKIE_SECURE is not None # type narrowing assert self.COOKIE_SECURE is not None # type narrowing
assert self.CORS_ORIGINS is not None
return self return self