"""Auth dependencies: current user and CSRF enforcement (SPEC §6.1, §10.3).""" from __future__ import annotations from typing import Annotated from fastapi import Depends, Request from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.api.errors import raise_api_error from app.db import get_session from app.identity.cookies import CSRF_HEADER, csrf_cookie_name, read_session_token from app.identity.sessions import Session, resolve_session from app.models import User async def current_session(request: Request) -> Session: """Resolve the caller's session or raise 401.""" token = read_session_token(request) session = await resolve_session(token) if session is None: raise_api_error(401, "not_authenticated", "authentication required") return session async def current_user( session: Annotated[Session, Depends(current_session)], db: Annotated[AsyncSession, Depends(get_session)], ) -> User: """Load the authenticated :class:`User` (I-7: scoped by the session's user_id).""" user = await db.scalar(select(User).where(User.id == session.user_id)) if user is None or user.blocked_at is not None: # Session references a deleted or blocked user; treat as unauthenticated. # (Blocking also revokes the sessions; this covers any request in flight.) raise_api_error(401, "not_authenticated", "authentication required") return user async def require_csrf(request: Request) -> None: """Enforce the double-submit CSRF token on mutating requests (SPEC §10.3).""" if request.method in ("GET", "HEAD", "OPTIONS"): return header = request.headers.get(CSRF_HEADER, "") cookie = request.cookies.get(csrf_cookie_name(request), "") if not header or not cookie or header != cookie: raise_api_error(403, "csrf_failed", "missing or invalid CSRF token") async def current_admin(user: Annotated[User, Depends(current_user)]) -> User: """Load the authenticated user and require the administrator flag.""" if not user.is_admin: raise_api_error(403, "not_admin", "administrator privileges required") return user CurrentUser = Annotated[User, Depends(current_user)] CurrentAdmin = Annotated[User, Depends(current_admin)] CurrentSession = Annotated[Session, Depends(current_session)]