Files
voidea/app/core/dependencies.py
T

176 lines
5.0 KiB
Python

"""FastAPI dependencies for VoIdea.
Common dependencies used across API endpoints.
"""
from typing import Annotated
from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from jose import JWTError
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.config import get_settings
from app.core.database import async_session_maker
from app.core.security import decode_token
from app.models.user import User
settings = get_settings()
security = HTTPBearer()
async def get_db() -> AsyncSession:
"""Get database session."""
async with async_session_maker() as session:
try:
yield session
await session.commit()
except Exception:
await session.rollback()
raise
finally:
await session.close()
async def get_current_user(
credentials: Annotated[HTTPAuthorizationCredentials, Depends(security)],
) -> User:
"""Get current authenticated user from JWT token.
Args:
credentials: Bearer token credentials
Returns:
User model instance
Raises:
HTTPException: If token invalid or expired
"""
try:
payload = decode_token(credentials.credentials)
if payload is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid token",
headers={"WWW-Authenticate": "Bearer"},
)
if payload.get("type") != "access":
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid token type",
headers={"WWW-Authenticate": "Bearer"},
)
user_id = payload.get("sub")
if user_id is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Token missing user ID",
headers={"WWW-Authenticate": "Bearer"},
)
except JWTError:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
async with async_session_maker() as db:
result = await db.execute(
select(User).where(User.id == user_id)
)
user = result.scalar_one_or_none()
if user is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found",
)
if not user.is_active:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Account is disabled",
)
return user
async def require_admin(
user: Annotated[User, Depends(get_current_user)],
) -> User:
"""Require admin or owner role."""
if not user.is_superuser and not user.is_owner:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Admin access required",
)
return user
async def require_owner(
user: Annotated[User, Depends(get_current_user)],
) -> User:
"""Require owner role (system-level operations)."""
if not user.is_owner:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Owner access required",
)
return user
def require_permission(perm_name: str):
"""Factory: require a specific moderator permission or admin/owner.
Usage:
@router.get("/admin/feedback")
async def list_feedback(
user: Annotated[User, Depends(require_permission("can_manage_feedback"))],
):
...
"""
async def _check(
user: Annotated[User, Depends(get_current_user)],
) -> User:
if user.is_owner or user.role == "admin":
return user
if user.role == "moderator" and user.permissions:
if user.permissions.get(perm_name, False):
return user
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=f"Permission '{perm_name}' required",
)
return _check
async def get_optional_user(
credentials: Annotated[HTTPAuthorizationCredentials | None, Depends(HTTPBearer(auto_error=False))],
) -> User | None:
"""Get current user if authenticated, None otherwise."""
if credentials is None:
return None
try:
payload = decode_token(credentials.credentials)
if payload is None or payload.get("type") != "access":
return None
user_id = payload.get("sub")
if user_id is None:
return None
except JWTError:
return None
async with async_session_maker() as db:
result = await db.execute(
select(User).where(User.id == user_id)
)
user = result.scalar_one_or_none()
if user and user.is_active:
return user
return None