- 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>
173 lines
4.8 KiB
Python
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)]
|