auth_middleware.py 2.4 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576
  1. from __future__ import annotations
  2. import re
  3. from collections.abc import Awaitable, Callable
  4. from fastapi import Request
  5. from starlette.concurrency import run_in_threadpool
  6. from starlette.middleware.base import BaseHTTPMiddleware
  7. from starlette.responses import JSONResponse, Response
  8. from api.services.auth import ADMIN_ROLE, SESSION_COOKIE_NAME, resolve_session
  9. _PUBLIC_PATHS = {
  10. "/api/auth/login",
  11. }
  12. _AUTHENTICATED_USER_PATHS = {
  13. ("GET", "/api/auth/me"),
  14. ("POST", "/api/auth/logout"),
  15. }
  16. _NORMAL_USER_PATHS = {
  17. ("GET", "/api/category-tree"),
  18. ("GET", "/api/demand-grade"),
  19. ("GET", "/api/video-discovery/demands"),
  20. ("GET", "/api/video-discovery/feedback"),
  21. ("POST", "/api/video-discovery/feedback"),
  22. }
  23. _NORMAL_USER_PATTERNS = (
  24. re.compile(r"^/api/video-discovery/demands/\d+$"),
  25. re.compile(r"^/api/demand-grade/\d+/videos$"),
  26. )
  27. def normal_user_can_access(method: str, path: str) -> bool:
  28. if (method, path) in _AUTHENTICATED_USER_PATHS:
  29. return True
  30. if (method, path) in _NORMAL_USER_PATHS:
  31. return True
  32. return method == "GET" and any(pattern.fullmatch(path) for pattern in _NORMAL_USER_PATTERNS)
  33. def _is_protected_path(path: str) -> bool:
  34. return (
  35. path == "/api"
  36. or path.startswith("/api/")
  37. or path in {"/docs", "/redoc", "/openapi.json"}
  38. )
  39. class AuthenticationMiddleware(BaseHTTPMiddleware):
  40. """Authenticate API requests and enforce the two fixed application roles."""
  41. async def dispatch(
  42. self,
  43. request: Request,
  44. call_next: Callable[[Request], Awaitable[Response]],
  45. ) -> Response:
  46. path = request.url.path
  47. method = request.method.upper()
  48. if method == "OPTIONS" or not _is_protected_path(path) or path in _PUBLIC_PATHS:
  49. return await call_next(request)
  50. token = request.cookies.get(SESSION_COOKIE_NAME)
  51. user = await run_in_threadpool(resolve_session, token) if token else None
  52. if user is None:
  53. return JSONResponse(
  54. status_code=401,
  55. content={"detail": "authentication required"},
  56. )
  57. request.state.current_user = user
  58. if user["role"] == ADMIN_ROLE or normal_user_can_access(method, path):
  59. return await call_next(request)
  60. return JSONResponse(
  61. status_code=403,
  62. content={"detail": "permission denied"},
  63. )