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_current_user_csrf(user: "User" = Depends(get_current_user), _: None = Depends(require_csrf)) -> "User": """Combined dependency: authenticate first (401 for anonymous), then CSRF (403). Used by state-changing endpoints in routers where CSRF must be enforced but anonymous callers should still receive the semantically-correct 401. """ 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") async def get_current_user_csrf(user: "User" = Depends(get_current_user), _: None = Depends(require_csrf)) -> "User": """Combined dependency: authenticate first (401 for anonymous), then CSRF (403). Used by state-changing endpoints in routers where CSRF must be enforced but anonymous callers should still receive the semantically-correct 401. """ return user 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)