from __future__ import annotations import re from collections.abc import Awaitable, Callable from fastapi import Request from starlette.concurrency import run_in_threadpool from starlette.middleware.base import BaseHTTPMiddleware from starlette.responses import JSONResponse, Response from api.services.auth import ADMIN_ROLE, SESSION_COOKIE_NAME, resolve_session _PUBLIC_PATHS = { "/api/auth/login", } _AUTHENTICATED_USER_PATHS = { ("GET", "/api/auth/me"), ("POST", "/api/auth/logout"), } _NORMAL_USER_PATHS = { ("GET", "/api/category-tree"), ("GET", "/api/demand-grade"), ("GET", "/api/video-discovery/demands"), } _NORMAL_USER_PATTERNS = ( re.compile(r"^/api/video-discovery/demands/\d+$"), re.compile(r"^/api/demand-grade/\d+/videos$"), ) def normal_user_can_access(method: str, path: str) -> bool: if (method, path) in _AUTHENTICATED_USER_PATHS: return True if (method, path) in _NORMAL_USER_PATHS: return True return method == "GET" and any(pattern.fullmatch(path) for pattern in _NORMAL_USER_PATTERNS) def _is_protected_path(path: str) -> bool: return ( path == "/api" or path.startswith("/api/") or path in {"/docs", "/redoc", "/openapi.json"} ) class AuthenticationMiddleware(BaseHTTPMiddleware): """Authenticate API requests and enforce the two fixed application roles.""" async def dispatch( self, request: Request, call_next: Callable[[Request], Awaitable[Response]], ) -> Response: path = request.url.path method = request.method.upper() if method == "OPTIONS" or not _is_protected_path(path) or path in _PUBLIC_PATHS: return await call_next(request) token = request.cookies.get(SESSION_COOKIE_NAME) user = await run_in_threadpool(resolve_session, token) if token else None if user is None: return JSONResponse( status_code=401, content={"detail": "authentication required"}, ) request.state.current_user = user if user["role"] == ADMIN_ROLE or normal_user_can_access(method, path): return await call_next(request) return JSONResponse( status_code=403, content={"detail": "permission denied"}, )