"""FastAPI dependencies: DB session, current user (JWT via cookie or bearer), CSRF, rate limiting.""" from typing import Optional from fastapi import Depends, Header, Request, Response, Cookie, HTTPException, status from fastapi.security.utils import get_authorization_scheme_param from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.core.config import settings from app.core.security import decode_token, rate_limit from app.db.session import AsyncSessionLocal async def get_db() -> AsyncSession: async with AsyncSessionLocal() as session: yield session async def get_current_user(request: Request, response: Response, authorization: Optional[str] = Header(None), mailer_session: Optional[str] = Cookie(None), db: AsyncSession = Depends(get_db)) -> "User": # noqa: F821 """Authenticate via JWT in HttpOnly cookie (web UI) or Authorization header (API/tests).""" from app.models import User token = None if authorization: scheme, param = get_authorization_scheme_param(authorization) if scheme.lower() == "bearer": token = param if not token and mailer_session: token = mailer_session if not token: raise HTTPException(status_code=401, detail="Not authenticated") payload = decode_token(token) if not payload: raise HTTPException(status_code=401, detail="Invalid or expired session") user = await db.get(User, int(payload["sub"])) if not user or not user.is_active: raise HTTPException(status_code=401, detail="User not found or inactive") return user async def get_optional_user(request: Request, db: AsyncSession = Depends(get_db)): from app.models import User token = request.cookies.get(settings.SESSION_COOKIE_NAME) authz = request.headers.get("authorization", "") if authz.lower().startswith("bearer "): token = authz[7:] if not token: return None payload = decode_token(token) if not payload: return None return await db.get(User, int(payload["sub"])) def require_csrf(request: Request, x_csrf_token: Optional[str] = Header(None)) -> None: """Double-submit cookie CSRF check for state-changing API calls from the browser.""" if request.method in ("GET", "HEAD", "OPTIONS"): return # Exempt token-based API access (Authorization header present) and OAuth callback. if request.headers.get("authorization", "").lower().startswith("bearer "): return if request.url.path.startswith("/auth/google/callback"): return cookie = request.cookies.get(settings.CSRF_COOKIE_NAME, "") if not cookie or not x_csrf_token or cookie != x_csrf_token: raise HTTPException(status_code=403, detail="CSRF token missing or invalid") def rate_limit_user(request: Request, user=Depends(get_current_user)): key = f"u:{user.id}:{request.url.path.split('?')[0]}" allowed, remaining = rate_limit(key) if not allowed: raise HTTPException(status_code=429, detail="Rate limit exceeded; try again shortly") return True