Catsy / app /auth.py
Domince's picture
feat: support access_token query param for apk download
995425f
Raw
History Blame Contribute Delete
4.56 kB
"""
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