| """ |
| FastAPI dependency injection functions. |
| |
| Provides common dependencies for authentication, authorization, |
| and service access across all API endpoints. |
| """ |
| from typing import Optional |
| from fastapi import Depends, HTTPException, status, Query |
| from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials |
|
|
| from app.config import get_settings, Settings |
| from app.core.security import decode_access_token |
| from app.core import AuthenticationError, AuthorizationError |
| from app.models.user import AccountType, TokenPayload |
| from app.services.supabase_client import SupabaseService, get_supabase_service |
|
|
|
|
| |
| security = HTTPBearer(auto_error=False) |
|
|
|
|
| async def get_settings_dep() -> Settings: |
| """Dependency to get application settings.""" |
| return get_settings() |
|
|
|
|
| async def get_supabase( |
| settings: Settings = Depends(get_settings_dep) |
| ) -> SupabaseService: |
| """Dependency to get Supabase service instance.""" |
| return get_supabase_service() |
|
|
|
|
| async def get_current_user_optional( |
| credentials: Optional[HTTPAuthorizationCredentials] = Depends(security), |
| ) -> Optional[dict]: |
| """ |
| Get current user from JWT token if present. |
| Returns None if no token provided (for public endpoints). |
| """ |
| if not credentials: |
| return None |
| |
| token = credentials.credentials |
| payload = decode_access_token(token) |
| |
| if not payload: |
| return None |
| |
| return { |
| "id": payload.get("sub"), |
| "email": payload.get("email"), |
| "account_type": payload.get("account_type"), |
| "organization_id": payload.get("organization_id") |
| } |
|
|
|
|
|
|
|
|
| async def get_current_user( |
| credentials: Optional[HTTPAuthorizationCredentials] = Depends(security), |
| token_query: Optional[str] = Query(None, alias="token") |
| ) -> dict: |
| """ |
| Get current authenticated user from JWT token (Header or Query param). |
| Raises HTTPException if not authenticated. |
| """ |
| token = None |
| if credentials: |
| token = credentials.credentials |
| elif token_query: |
| token = token_query |
| |
| if not token: |
| raise HTTPException( |
| status_code=status.HTTP_401_UNAUTHORIZED, |
| detail="Not authenticated", |
| headers={"WWW-Authenticate": "Bearer"}, |
| ) |
| |
| payload = decode_access_token(token) |
| if not payload: |
| raise HTTPException( |
| status_code=status.HTTP_401_UNAUTHORIZED, |
| detail="Invalid or expired token", |
| headers={"WWW-Authenticate": "Bearer"}, |
| ) |
|
|
| return { |
| "id": payload.get("sub"), |
| "email": payload.get("email"), |
| "account_type": payload.get("account_type"), |
| "organization_id": payload.get("organization_id") |
| } |
|
|
| async def get_current_user_with_db( |
| current_user: dict = Depends(get_current_user), |
| supabase: SupabaseService = Depends(get_supabase) |
| ) -> dict: |
| """ |
| Enhanced version of get_current_user that fetches organization_id from DB |
| if it's missing in the JWT payload (e.g. newly linked account). |
| """ |
| if current_user.get("organization_id"): |
| return current_user |
| |
| |
| user_id = current_user["id"] |
| account_type = current_user["account_type"] |
| |
| if account_type == AccountType.TEAM.value: |
| orgs = await supabase.select("organizations", filters={"owner_id": user_id}) |
| if orgs: |
| current_user["organization_id"] = orgs[0]["id"] |
| elif account_type == AccountType.COACH.value: |
| |
| staff = await supabase.select("organizations_staff", filters={"user_id": user_id}) |
| if staff: |
| current_user["organization_id"] = staff[0]["organization_id"] |
| elif account_type == AccountType.PLAYER.value: |
| |
| players = await supabase.select("players", filters={"user_id": user_id}) |
| if players: |
| |
| org_players = [p for p in players if p.get("organization_id")] |
| if org_players: |
| current_user["organization_id"] = org_players[0]["organization_id"] |
|
|
| return current_user |
|
|
|
|
| async def require_team_account( |
| current_user: dict = Depends(get_current_user_with_db), |
| ) -> dict: |
| """ |
| Dependency that requires a TEAM or COACH account type. |
| Use for team-only endpoints. |
| """ |
| allowed_types = [AccountType.TEAM.value, AccountType.COACH.value] |
| if current_user.get("account_type") not in allowed_types: |
| raise HTTPException( |
| status_code=status.HTTP_403_FORBIDDEN, |
| detail="This endpoint requires a TEAM or COACH account", |
| ) |
| return current_user |
|
|
|
|
| async def require_organization_admin( |
| current_user: dict = Depends(get_current_user_with_db), |
| ) -> dict: |
| """ |
| Dependency that requires a TEAM account type (Organization Owner). |
| Use for critical administrative tasks like staff linking and settings. |
| """ |
| if current_user.get("account_type") != AccountType.TEAM.value: |
| raise HTTPException( |
| status_code=status.HTTP_403_FORBIDDEN, |
| detail="This operation requires organization owner administrative privileges", |
| ) |
| return current_user |
|
|
| async def require_personal_account( |
| current_user: dict = Depends(get_current_user_with_db), |
| ) -> dict: |
| """ |
| Dependency that requires a PERSONAL account type. |
| Use for personal-only endpoints. |
| """ |
| if current_user.get("account_type") != AccountType.PERSONAL.value: |
| raise HTTPException( |
| status_code=status.HTTP_403_FORBIDDEN, |
| detail="This endpoint requires a PERSONAL account", |
| ) |
| return current_user |
|
|
|
|
| async def require_linked_account( |
| current_user: dict = Depends(get_current_user_with_db), |
| ) -> dict: |
| """ |
| Dependency that requires the user to be linked to an organization. |
| """ |
| if not current_user.get("organization_id"): |
| |
| |
| if current_user.get("account_type") != AccountType.TEAM.value: |
| raise HTTPException( |
| status_code=status.HTTP_403_FORBIDDEN, |
| detail="You must be linked to a team to access this feature", |
| ) |
| return current_user |
|
|
|
|
| async def require_staff_member( |
| current_user: dict = Depends(get_current_user_with_db), |
| ) -> dict: |
| """ |
| Dependency that requires the user to be a COACH or TEAM owner who is linked to an organization. |
| Used for features delegated to coaching staff (match upload, scheduling, stats). |
| """ |
| allowed_types = [AccountType.COACH.value, AccountType.TEAM.value] |
| if current_user.get("account_type") not in allowed_types: |
| raise HTTPException( |
| status_code=status.HTTP_403_FORBIDDEN, |
| detail="This feature is managed by the Team Owner or Coaching Staff", |
| ) |
| if not current_user.get("organization_id"): |
| |
| |
| if current_user.get("account_type") != AccountType.TEAM.value: |
| raise HTTPException( |
| status_code=status.HTTP_403_FORBIDDEN, |
| detail="Your account has not been linked to a team yet.", |
| ) |
| return current_user |
|
|
| def require_owner_or_admin(resource_owner_id: str): |
| """ |
| Factory for dependency that checks if user owns a resource. |
| |
| Usage: |
| @router.delete("/items/{item_id}") |
| async def delete_item( |
| item_id: str, |
| _: dict = Depends(require_owner_or_admin(item.owner_id)) |
| ): |
| ... |
| """ |
| async def dependency(current_user: dict = Depends(get_current_user)) -> dict: |
| if current_user.get("id") != resource_owner_id: |
| raise HTTPException( |
| status_code=status.HTTP_403_FORBIDDEN, |
| detail="You don't have permission to access this resource", |
| ) |
| return current_user |
| return dependency |
|
|