"""Configuration container for environment variables.
Two rules this file exists to enforce, both of which are easy to lose:
* Nothing secret has a hardcoded default. An unset DB_PASS fails loudly at
connect time rather than silently using a password baked into the repo.
* An unset feature variable means "that feature is off", not "crash". The app
must run end-to-end - locally and in CI - with no accounts registered at any
third party. SMTP_HOST below is the worked example; copy the stance when you
add Stripe, an OAuth client, or anything else with a credential.
Read at import time, so load_dotenv() has to run before the class body.
"""
import os
from typing import Optional
from dotenv import load_dotenv
# Existing environment variables win, so App Engine's injected config is never
# overwritten by a stray .env on a developer's machine.
load_dotenv(override=False)
class Config:
# "prod" means "deployed behind HTTPS", not "the production environment":
# it is what sets SESSION_COOKIE_SECURE and arms the weak-SECRET_KEY guard
# in main.py. The integration deployment sets it too.
ENV: str = os.getenv("ENV", "local")
# The organisation manage.py targets when no --org is given.
DEFAULT_ORG_SLUG: str = os.getenv("DEFAULT_ORG_SLUG", "myapp")
# --- database ---------------------------------------------------------
# Local dev / CI: TCP to localhost (Cloud SQL Auth Proxy or local Postgres)
# App Engine: Unix socket, set via app.yaml
DB_USER: Optional[str] = os.getenv("DB_USER", "myapp")
DB_PASS: Optional[str] = os.getenv("DB_PASS", "")
DB_NAME: Optional[str] = os.getenv("DB_NAME", "myapp")
DB_PORT: str = os.getenv("DB_PORT", "5432")
INSTANCE_UNIX_SOCKET: Optional[str] = os.getenv("INSTANCE_UNIX_SOCKET", "")
# Signs the session cookie, which is the credential that says a request is
# authenticated. main.py refuses to start in prod if this is still a
# placeholder.
SECRET_KEY: str = os.getenv("SECRET_KEY", "dev-only-not-for-production")
# The scheme+host that security-sensitive external links (password reset,
# email verification, a payment provider's return URL) must be built from.
# Left blank, endpoints/crud.public_base_url() derives it from the request -
# but only for loopback hosts, so a deployment cannot have its links poisoned
# through the attacker-controllable Host header.
PUBLIC_BASE_URL: str = os.getenv("PUBLIC_BASE_URL", "")
# --- outbound email ---------------------------------------------------
# The worked example of "unset config = feature off". SMTP_HOST unset means
# "no mail server": send_mail logs the message instead of sending, so local
# dev and CI run any flow that mails without a live server.
# STARTTLS upgrade a plaintext connection to TLS (the usual port 587)
# SSL implicit TLS from the first byte (port 465); overrides STARTTLS
SMTP_HOST: str = os.getenv("SMTP_HOST", "")
SMTP_PORT: str = os.getenv("SMTP_PORT", "587")
SMTP_USER: str = os.getenv("SMTP_USER", "")
SMTP_PASS: str = os.getenv("SMTP_PASS", "")
SMTP_FROM: str = os.getenv("SMTP_FROM", "")
SMTP_STARTTLS: bool = os.getenv("SMTP_STARTTLS", "true").lower() == "true"
SMTP_SSL: bool = os.getenv("SMTP_SSL", "false").lower() == "true"
# --- Google sign-in (OAuth 2.0 / OIDC) --------------------------------
# Both unset means "Sign in with Google" is off: the button is hidden and
# the /auth/google routes bounce to /login - the same stance SMTP_HOST takes.
# Register an OAuth client in the Google console with the redirect URI
# <PUBLIC_BASE_URL>/auth/google/callback.
GOOGLE_CLIENT_ID: str = os.getenv("GOOGLE_CLIENT_ID", "").strip()
GOOGLE_CLIENT_SECRET: str = os.getenv("GOOGLE_CLIENT_SECRET", "").strip()
# --- your application's config goes below ------------------------------
# Follow the split the two files above draw:
# a host, an id, a public address, a flag -> app.yaml (committed)
# anything that authenticates -> env_secrets.yaml (gitignored,
# rendered by CI)
# and .strip() every secret, so a trailing newline from a piped value is not
# baked into a comparison.