From dadd19bc3058d7b3c08640498cd2b067a9923d28 Mon Sep 17 00:00:00 2001 From: Kyle Pope Date: Mon, 2 Mar 2026 17:53:26 +0800 Subject: [PATCH] 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 --- .env.example | 9 +++++---- backend/app/config.py | 15 +++++++++++---- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/.env.example b/.env.example index 68add17..9ac77b4 100644 --- a/.env.example +++ b/.env.example @@ -15,10 +15,7 @@ SECRET_KEY=your-secret-key-change-in-production # development | production — controls Swagger/ReDoc visibility and cookie defaults ENVIRONMENT=development -# CORS allowed origins (comma-separated, default: http://localhost:5173) -# CORS_ORIGINS=https://umbra.example.com - -# Public URL for ntfy notification click links (default: http://localhost) +# Public URL — used for ntfy click links and auto-derives CORS_ORIGINS in production # UMBRA_URL=https://umbra.example.com # 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). # Only set explicitly to override, e.g. false for a non-TLS prod behind a proxy. # 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 diff --git a/backend/app/config.py b/backend/app/config.py index fd72db5..592af82 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -20,10 +20,11 @@ class Settings(BaseSettings): # TOTP issuer name shown in authenticator apps TOTP_ISSUER: str = "UMBRA" - # CORS allowed origins (comma-separated) - CORS_ORIGINS: str = "http://localhost:5173" + # CORS allowed origins (comma-separated). Auto-derives from UMBRA_URL in + # 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" # Concurrent session limit per user (oldest evicted when exceeded) @@ -36,10 +37,16 @@ class Settings(BaseSettings): ) @model_validator(mode="after") - def derive_cookie_secure(self) -> "Settings": + def derive_defaults(self) -> "Settings": if self.COOKIE_SECURE is None: 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.CORS_ORIGINS is not None return self