feat(T01): auth + user management + RBAC + base frontend + i18n
- Backend: FastAPI, JWT auth (HS256), bcrypt, RBAC middleware - User CRUD (admin-only), soft-delete, pagination - Pydantic BaseSettings config, async SQLAlchemy 2.0 - 50 backend tests, 88% coverage - Frontend: Next.js 14 App Router, Tailwind, design tokens - Login page, auth context, API client with auto-refresh - i18n: next-intl, DE/EN (31 keys each) - 6 base UI components (Button, Input, Card, Table, Modal, Toast) - 12 frontend tests, npm build success
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
"""FastAPI dependencies: pagination, current user extraction, RBAC role enforcement.
|
||||
"""
|
||||
|
||||
import uuid
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import Depends, HTTPException, Query, status
|
||||
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.database import get_db
|
||||
from app.models.user import User, UserRole
|
||||
from app.services.auth_service import get_user_by_id
|
||||
from app.utils.jwt import verify_access_token
|
||||
|
||||
security = HTTPBearer(auto_error=False)
|
||||
|
||||
|
||||
def get_pagination(
|
||||
page: int = Query(1, ge=1, description="Page number (1-indexed)"),
|
||||
page_size: int = Query(20, ge=1, le=100, description="Items per page"),
|
||||
) -> dict:
|
||||
"""Common pagination parameters."""
|
||||
return {"page": page, "page_size": page_size}
|
||||
|
||||
|
||||
async def get_current_user(
|
||||
credentials: Optional[HTTPAuthorizationCredentials] = Depends(security),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> User:
|
||||
"""Extract and verify the JWT bearer token, returning the current user.
|
||||
|
||||
Raises 401 if token is missing, invalid, or the user is not found / inactive.
|
||||
"""
|
||||
if credentials is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail={"error": {"code": "UNAUTHORIZED", "message": "Missing authentication token"}},
|
||||
)
|
||||
|
||||
token = credentials.credentials
|
||||
payload = verify_access_token(token)
|
||||
if payload is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail={"error": {"code": "INVALID_TOKEN", "message": "Invalid or expired access token"}},
|
||||
)
|
||||
|
||||
user_id_str = payload.get("sub")
|
||||
if not user_id_str:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail={"error": {"code": "INVALID_TOKEN", "message": "Token missing subject"}},
|
||||
)
|
||||
|
||||
try:
|
||||
user_uuid = uuid.UUID(user_id_str)
|
||||
except (ValueError, TypeError):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail={"error": {"code": "INVALID_TOKEN", "message": "Invalid user ID in token"}},
|
||||
)
|
||||
|
||||
user = await get_user_by_id(db, user_uuid)
|
||||
if user is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail={"error": {"code": "USER_NOT_FOUND", "message": "User not found"}},
|
||||
)
|
||||
|
||||
if not user.is_active:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail={"error": {"code": "ACCOUNT_DISABLED", "message": "Account is deactivated"}},
|
||||
)
|
||||
|
||||
return user
|
||||
|
||||
|
||||
def require_role(allowed_roles: list[str]):
|
||||
"""RBAC dependency factory: enforce that the current user has one of the allowed roles.
|
||||
|
||||
Usage:
|
||||
@router.get("/", dependencies=[Depends(require_role(["admin"]))])
|
||||
or:
|
||||
async def handler(user: User = Depends(require_role(["admin"]))):
|
||||
"""
|
||||
|
||||
async def role_checker(user: User = Depends(get_current_user)) -> User:
|
||||
user_role = user.role.value if isinstance(user.role, UserRole) else str(user.role)
|
||||
if user_role not in allowed_roles:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail={
|
||||
"error": {
|
||||
"code": "FORBIDDEN",
|
||||
"message": f"Role '{user_role}' is not permitted. Required: {allowed_roles}",
|
||||
}
|
||||
},
|
||||
)
|
||||
return user
|
||||
|
||||
return role_checker
|
||||
Reference in New Issue
Block a user