| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081 |
- 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/growth-category-tree"),
- ("GET", "/api/demand-grade"),
- ("GET", "/api/video-discovery/demands"),
- ("GET", "/api/video-discovery/runs"),
- ("GET", "/api/video-discovery/feedback"),
- ("POST", "/api/video-discovery/feedback"),
- }
- _NORMAL_USER_PATTERNS = (
- re.compile(r"^/api/growth-category-tree/\d+/channel-contents$"),
- re.compile(r"^/api/growth-channel-content/[^/]+/view$"),
- re.compile(r"^/api/video-discovery/demands/\d+$"),
- re.compile(r"^/api/video-discovery/runs/[^/]+$"),
- re.compile(r"^/api/video-discovery/runs/[^/]+/searches$"),
- re.compile(r"^/api/video-discovery/runs/[^/]+/candidates$"),
- 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"},
- )
|