Spaces:
Runtime error
Runtime error
| """ | |
| Supabase JWT verification β FastAPI dependency. | |
| When SUPABASE_URL + SUPABASE_ANON_KEY are configured, verifies the Bearer | |
| token from Authorization header using Supabase's JWKS endpoint. | |
| When not configured (local dev / SQLite mode), returns an anonymous user so | |
| all routes work without auth. | |
| """ | |
| from __future__ import annotations | |
| import httpx | |
| from typing import Optional | |
| from fastapi import Header, HTTPException | |
| from jose import jwt, JWTError | |
| from .config import get_settings, is_supabase_configured | |
| # In-memory JWKS cache (refreshed on startup) | |
| _jwks_cache: dict | None = None | |
| ANON_USER = {'id': 'anonymous', 'name': 'Investigator', 'email': ''} | |
| async def _fetch_jwks() -> dict: | |
| global _jwks_cache | |
| if _jwks_cache is not None: | |
| return _jwks_cache | |
| s = get_settings() | |
| url = f"{s['supabase_url']}/auth/v1/.well-known/jwks.json" | |
| try: | |
| async with httpx.AsyncClient(timeout=5.0) as client: | |
| resp = await client.get(url) | |
| resp.raise_for_status() | |
| _jwks_cache = resp.json() | |
| return _jwks_cache | |
| except Exception as exc: | |
| print(f"[auth] JWKS fetch failed: {exc}") | |
| return {} | |
| def _decode_token(token: str, jwks: dict) -> dict: | |
| """Decode a Supabase JWT and return user claims.""" | |
| keys = jwks.get('keys', []) | |
| if keys: | |
| # jose accepts the full JWKS dict as the key argument | |
| return jwt.decode( | |
| token, jwks, | |
| algorithms=['RS256', 'ES256'], | |
| options={'verify_aud': False}, # Supabase audience varies by project | |
| ) | |
| jwt_secret = get_settings().get('jwt_secret', '') | |
| if jwt_secret: | |
| return jwt.decode( | |
| token, | |
| jwt_secret, | |
| algorithms=['HS256'], | |
| options={'verify_aud': False}, | |
| ) | |
| raise JWTError("No JWKS keys or SUPABASE_JWT_SECRET available") | |
| async def get_current_user( | |
| authorization: Optional[str] = Header(default=None), | |
| ) -> dict: | |
| """ | |
| FastAPI dependency β returns current user dict: | |
| { id, name, email } | |
| If Supabase is not configured or no token provided, returns ANON_USER. | |
| Routes can use this for audit attribution without requiring auth. | |
| """ | |
| if not is_supabase_configured(): | |
| return ANON_USER | |
| if not authorization or not authorization.startswith('Bearer '): | |
| return ANON_USER | |
| token = authorization[7:].strip() | |
| if token.startswith("guest:"): | |
| parts = token.split(":") | |
| guest_id = parts[1] if len(parts) > 1 else "guest_unknown" | |
| name_slice = guest_id[6:12] if len(guest_id) > 12 else guest_id | |
| return { | |
| 'id': guest_id, | |
| 'name': f"Guest ({name_slice})", | |
| 'email': '', | |
| } | |
| try: | |
| jwks = await _fetch_jwks() | |
| payload = _decode_token(token, jwks) | |
| meta = payload.get('user_metadata', {}) or {} | |
| return { | |
| 'id': payload.get('sub', 'unknown'), | |
| 'name': meta.get('full_name') or meta.get('name') or payload.get('email', 'Investigator'), | |
| 'email': payload.get('email', ''), | |
| } | |
| except JWTError as exc: | |
| # Expired / tampered token β reject with 401 when auth is configured | |
| raise HTTPException(status_code=401, detail=f"Invalid token: {exc}") | |
| async def get_current_user_optional( | |
| authorization: Optional[str] = Header(default=None), | |
| ) -> dict: | |
| """ | |
| Soft auth β never raises 401. Used on endpoints that are readable | |
| without auth but write attribution when authenticated. | |
| """ | |
| try: | |
| return await get_current_user(authorization) | |
| except HTTPException: | |
| return ANON_USER | |