| """ |
| 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 |
|
|