pounce/backend/app/api/deps.py

126 lines
3.5 KiB
Python

"""API dependencies."""
from typing import Annotated, Optional
from fastapi import Depends, HTTPException, Request, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from sqlalchemy.ext.asyncio import AsyncSession
from app.database import get_db
from app.services.auth import AuthService
from app.models.user import User
from app.security import AUTH_COOKIE_NAME
# Security scheme
security_optional = HTTPBearer(auto_error=False)
async def get_current_user(
request: Request,
credentials: Annotated[Optional[HTTPAuthorizationCredentials], Depends(security_optional)],
db: Annotated[AsyncSession, Depends(get_db)],
) -> User:
"""Get current authenticated user from JWT token."""
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
token: Optional[str] = None
if credentials is not None:
token = credentials.credentials
if not token:
token = request.cookies.get(AUTH_COOKIE_NAME)
if not token:
raise credentials_exception
payload = AuthService.decode_token(token)
if payload is None:
raise credentials_exception
user_id_str = payload.get("sub")
if user_id_str is None:
raise credentials_exception
try:
user_id = int(user_id_str)
except (ValueError, TypeError):
raise credentials_exception
user = await AuthService.get_user_by_id(db, user_id)
if user is None:
raise credentials_exception
if not user.is_active:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="User account is disabled",
)
return user
async def get_current_active_user(
current_user: Annotated[User, Depends(get_current_user)],
) -> User:
"""Ensure user is active."""
if not current_user.is_active:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Inactive user",
)
return current_user
async def get_current_user_optional(
request: Request,
credentials: Annotated[Optional[HTTPAuthorizationCredentials], Depends(security_optional)],
db: Annotated[AsyncSession, Depends(get_db)],
) -> Optional[User]:
"""Get current user if authenticated, otherwise return None.
This allows endpoints to work for both authenticated and anonymous users,
potentially showing different content based on auth status.
"""
token: Optional[str] = None
if credentials is not None:
token = credentials.credentials
if not token:
token = request.cookies.get(AUTH_COOKIE_NAME)
if not token:
return None
payload = AuthService.decode_token(token)
if payload is None:
return None
user_id_str = payload.get("sub")
if user_id_str is None:
return None
try:
user_id = int(user_id_str)
except (ValueError, TypeError):
return None
user = await AuthService.get_user_by_id(db, user_id)
if user is None or not user.is_active:
return None
return user
# Type aliases for cleaner annotations
CurrentUser = Annotated[User, Depends(get_current_user)]
ActiveUser = Annotated[User, Depends(get_current_active_user)]
OptionalUser = Annotated[Optional[User], Depends(get_current_user_optional)]
CurrentUserOptional = OptionalUser # Alias for backward compatibility
Database = Annotated[AsyncSession, Depends(get_db)]