| import base64 |
| import hashlib |
| import json |
| import logging |
|
|
| from fastapi import Depends, HTTPException, Request |
| from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer |
|
|
| logger = logging.getLogger(__name__) |
|
|
| security = HTTPBearer(auto_error=False) |
|
|
|
|
| async def get_user_id( |
| credentials: HTTPAuthorizationCredentials | None = Depends(security), |
| ) -> str | None: |
| """Extract Supabase user_id from the Bearer JWT, or None for anonymous users.""" |
| if credentials is None: |
| return None |
| try: |
| parts = credentials.credentials.split(".") |
| if len(parts) != 3: |
| return None |
| payload = parts[1] |
| padding = 4 - len(payload) % 4 |
| if padding != 4: |
| payload += "=" * padding |
| decoded = base64.urlsafe_b64decode(payload) |
| claims = json.loads(decoded) |
| return claims.get("sub") |
| except Exception: |
| logger.debug("Failed to decode JWT", exc_info=True) |
| return None |
|
|
|
|
| async def require_user_id( |
| credentials: HTTPAuthorizationCredentials | None = Depends(security), |
| ) -> str: |
| """Like get_user_id but raises 401 if no valid JWT is present.""" |
| uid = await get_user_id(credentials) |
| if not uid: |
| raise HTTPException(status_code=401, detail="Authentication required") |
| return uid |
|
|
|
|
| async def get_user_id_from_api_key(request: Request) -> str | None: |
| """Authenticate via X-API-Key header. Returns user_id or None.""" |
| api_key = request.headers.get("X-API-Key") |
| if not api_key: |
| return None |
| key_hash = hashlib.sha256(api_key.encode()).hexdigest() |
| try: |
| from app.services.supabase import get_supabase |
| supabase = get_supabase() |
| result = supabase.table("api_keys").select("user_id").eq("key_hash", key_hash).execute() |
| if result.data: |
| uid = result.data[0]["user_id"] |
| supabase.table("api_keys").update({"last_used_at": "now()"}).eq("key_hash", key_hash).execute() |
| return uid |
| except Exception: |
| pass |
| return None |
|
|
|
|
| async def require_user_or_api_key( |
| credentials: HTTPAuthorizationCredentials | None = Depends(security), |
| request: Request = None, |
| ) -> str: |
| """Accepts either a Bearer JWT or X-API-Key header.""" |
| uid = await get_user_id(credentials) |
| if uid: |
| return uid |
| uid = await get_user_id_from_api_key(request) |
| if uid: |
| return uid |
| raise HTTPException(status_code=401, detail="Authentication required") |
|
|