mantimon-tcg/backend/app/api/deps.py
Cal Corum f82bc8aa1f Fix OAuth callback redirect and deps import
- Discord OAuth callback now redirects to frontend with tokens in URL fragment
  (more secure than query params - fragment not sent to server)
- Error responses also redirect with error in query params
- Fix deps.py import: get_session_dependency -> get_session

Part of Phase 2 Authentication completion.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-28 00:17:27 -06:00

173 lines
4.8 KiB
Python

"""FastAPI dependencies for Mantimon TCG API.
This module provides dependency injection functions for authentication
and database access in API endpoints.
Usage:
from app.api.deps import get_current_user, get_db
@router.get("/me")
async def get_me(
user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
return user
Dependencies:
- get_db: Async database session
- get_current_user: Authenticated user from JWT (required)
- get_optional_user: Authenticated user or None
- get_current_premium_user: User with active premium
"""
from typing import Annotated
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from sqlalchemy.ext.asyncio import AsyncSession
from app.db import get_session
from app.db.models import User
from app.services.jwt_service import verify_access_token
from app.services.user_service import user_service
# OAuth2 scheme for extracting Bearer token from Authorization header
# tokenUrl is for OpenAPI docs - points to where tokens are obtained
oauth2_scheme = OAuth2PasswordBearer(
tokenUrl="/api/auth/token", # For OpenAPI docs
auto_error=True, # Raise 401 if no token
)
oauth2_scheme_optional = OAuth2PasswordBearer(
tokenUrl="/api/auth/token",
auto_error=False, # Return None if no token
)
async def get_db() -> AsyncSession:
"""Get async database session.
Yields:
AsyncSession for database operations.
Example:
@router.get("/items")
async def get_items(db: AsyncSession = Depends(get_db)):
...
"""
async with get_session() as session:
yield session
async def get_current_user(
token: Annotated[str, Depends(oauth2_scheme)],
db: Annotated[AsyncSession, Depends(get_db)],
) -> User:
"""Get the current authenticated user from JWT token.
Validates the access token and fetches the user from database.
Args:
token: JWT access token from Authorization header.
db: Database session.
Returns:
The authenticated User.
Raises:
HTTPException: 401 if token is invalid or user not found.
Example:
@router.get("/me")
async def get_me(user: User = Depends(get_current_user)):
return {"email": user.email}
"""
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
# Verify token and extract user ID
user_id = verify_access_token(token)
if user_id is None:
raise credentials_exception
# Fetch user from database
user = await user_service.get_by_id(db, user_id)
if user is None:
raise credentials_exception
return user
async def get_optional_user(
token: Annotated[str | None, Depends(oauth2_scheme_optional)],
db: Annotated[AsyncSession, Depends(get_db)],
) -> User | None:
"""Get the current user if authenticated, or None.
Useful for endpoints that work both with and without authentication,
but may provide additional features for authenticated users.
Args:
token: JWT access token or None.
db: Database session.
Returns:
The authenticated User, or None if not authenticated.
Example:
@router.get("/cards")
async def get_cards(user: User | None = Depends(get_optional_user)):
if user:
# Show user's collection
else:
# Show public cards
"""
if token is None:
return None
user_id = verify_access_token(token)
if user_id is None:
return None
return await user_service.get_by_id(db, user_id)
async def get_current_premium_user(
user: Annotated[User, Depends(get_current_user)],
) -> User:
"""Get the current user and verify they have premium.
Args:
user: The authenticated user.
Returns:
The authenticated User with active premium.
Raises:
HTTPException: 403 if user doesn't have active premium.
Example:
@router.post("/decks")
async def create_unlimited_decks(
user: User = Depends(get_current_premium_user)
):
# Only premium users can have unlimited decks
...
"""
if not user.has_active_premium:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Premium subscription required",
)
return user
# Type aliases for cleaner endpoint signatures
CurrentUser = Annotated[User, Depends(get_current_user)]
OptionalUser = Annotated[User | None, Depends(get_optional_user)]
PremiumUser = Annotated[User, Depends(get_current_premium_user)]
DbSession = Annotated[AsyncSession, Depends(get_db)]