Spaces:
Running
Running
| """HTTP Basic auth for the API. | |
| Gate is a single ASGI middleware that protects `/api/*` (the data + functionality). | |
| `/health`, the static SPA shell, and `/portal` stay open so the login screen can | |
| load and Vercel health checks pass. Credentials come from APERTURE_USER / | |
| APERTURE_PASS (defaults: demo / aperture2026 for the demo deployment). | |
| Stateless: the SPA stores base64(user:pass) and sends it on every request; there | |
| is no server-side session to manage on serverless. | |
| """ | |
| from __future__ import annotations | |
| import base64 | |
| import secrets | |
| from starlette.requests import Request | |
| from starlette.responses import JSONResponse | |
| def _check(authorization: str | None, user: str, pwd: str) -> bool: | |
| if not authorization or not authorization.lower().startswith("basic "): | |
| return False | |
| try: | |
| decoded = base64.b64decode(authorization.split(" ", 1)[1]).decode("utf-8") | |
| u, _, p = decoded.partition(":") | |
| except Exception: | |
| return False | |
| return secrets.compare_digest(u, user) and secrets.compare_digest(p, pwd) | |
| def make_auth_middleware(user: str, pwd: str): | |
| async def auth_middleware(request: Request, call_next): | |
| path = request.url.path | |
| # Only guard the API; allow preflight, health, SPA shell, and portal. | |
| if request.method == "OPTIONS" or not path.startswith("/api/"): | |
| return await call_next(request) | |
| if _check(request.headers.get("authorization"), user, pwd): | |
| return await call_next(request) | |
| # NOTE: deliberately NO `WWW-Authenticate: Basic` header — that triggers the | |
| # browser's native credential popup. The React SPA handles 401 itself and | |
| # shows its own login screen, so we return a plain JSON 401. | |
| return JSONResponse({"detail": "Authentication required"}, status_code=401) | |
| return auth_middleware | |