""" Authentication Dependencies What: Provides the shared FastAPI auth dependencies used by protected routes. This module validates bearer tokens, resolves user roles, and exposes reusable role guards for admin-only and staff-only endpoints. How: `get_current_user()` validates the incoming JWT through the Supabase Auth client. Role guards then resolve the user's role from JWT metadata first, with a lazy fallback to the `user_profiles` table when metadata is missing. Usage: Imported by routers and used with `Depends(...)` on protected endpoints to require an authenticated user, admin access, or staff-only access before request handlers are allowed to run. What it does: - Validates Authorization headers and bearer tokens - Returns the authenticated Supabase user object - Resolves role from user metadata, app metadata, or DB fallback - Exposes `require_admin`, `require_staff`, and `require_staff_only` What it does NOT do: - Does not implement login or signup flows - Does not contain router handlers - Does not own business logic for auth responses - Does not persist user profile changes """ from fastapi import Header, HTTPException, Depends, Query from app.database import get_db, create_db_client def get_auth_client(): """Return an isolated auth client so auth checks do not taint admin API calls.""" return create_db_client().auth def get_user_from_token(token: str, auth_client=None): """Validate a raw bearer token and return the authenticated Supabase user.""" if not token: raise HTTPException(status_code=401, detail="Missing access token") auth_client = auth_client or get_auth_client() try: user_res = auth_client.get_user(token) if not user_res.user: raise Exception("Invalid token") return user_res.user except Exception as e: raise HTTPException(status_code=401, detail=f"Unauthorized: {str(e)}") def get_current_user( authorization: str = Header(None), access_token: str = Query(None), auth_client=Depends(get_auth_client) ): """ Validates the Supabase JWT by sending it directly to Supabase Auth. Ensures that only logged-in users or staff can access protected endpoints. Accepts either an Authorization header or an access_token query parameter. """ if not authorization and not access_token: raise HTTPException(status_code=401, detail="Missing authorization") if authorization and authorization.startswith("Bearer "): token = authorization.split(" ")[1] elif access_token: token = access_token else: raise HTTPException(status_code=401, detail="Invalid Authorization format") return get_user_from_token(token, auth_client=auth_client) def get_optional_current_user(authorization: str = Header(None), auth_client=Depends(get_auth_client)): """Return the authenticated user when a bearer token is present, else None.""" if not authorization: return None return get_current_user(authorization=authorization, auth_client=auth_client) def resolve_role_from_user(user): """Resolve a user's role from JWT metadata, then DB as a lazy fallback.""" role = (user.user_metadata or {}).get("role") if role: return str(role).strip().lower() role = (user.app_metadata or {}).get("role") if role: return str(role).strip().lower() db = get_db() res = db.table("user_profiles").select("role").eq("id", str(user.id)).limit(1).execute() if res.data: role = res.data[0].get("role") return str(role).strip().lower() if role else None return None def require_admin(user=Depends(get_current_user)): """Ensure the authenticated user has admin role metadata.""" role = resolve_role_from_user(user) if role != "admin": raise HTTPException(status_code=403, detail="Admin access required.") return user def require_staff(user=Depends(get_current_user)): """Ensure the authenticated user has staff or admin role metadata.""" role = resolve_role_from_user(user) if role not in {"staff", "admin"}: raise HTTPException(status_code=403, detail="Staff access required.") return user def require_staff_only(user=Depends(get_current_user)): """Ensure the authenticated user has staff role metadata only.""" role = resolve_role_from_user(user) if role != "staff": raise HTTPException(status_code=403, detail="Staff access required.") return user