"""Bearer-token auth shared across routers. Two modes, picked at startup from env: - API_BEARER_TOKEN set → enforce Bearer auth on all write/compute paths - API_BEARER_TOKEN unset (dev) → no auth, log a one-time warning Read endpoints (`/networth`, `/scenarios`, ...) skip auth entirely so the frontend can render without juggling tokens during dev. Lock those down later via Authentik-fronted ingress when we deploy. """ from __future__ import annotations import hmac import logging import os from fastapi import Header, HTTPException log = logging.getLogger(__name__) _warned_unauth = False def _read_token() -> str | None: return os.environ.get("API_BEARER_TOKEN") or os.environ.get("RECOMPUTE_BEARER_TOKEN") async def require_bearer(authorization: str | None = Header(default=None)) -> None: """FastAPI dependency: enforce bearer auth IF API_BEARER_TOKEN is set.""" expected = _read_token() if not expected: global _warned_unauth if not _warned_unauth: log.warning("API_BEARER_TOKEN unset — write endpoints are open. " "Set it before exposing this service.") _warned_unauth = True return if not authorization or not authorization.startswith("Bearer "): raise HTTPException(status_code=401, detail="Missing bearer token") token = authorization.removeprefix("Bearer ") if not hmac.compare_digest(token, expected): raise HTTPException(status_code=401, detail="Invalid token")