from dataclasses import dataclass from typing import Annotated from fastapi import Cookie, Depends, Header from sqlalchemy.orm import Session from app.config import get_settings from app.db.session import get_db, set_space_context from app.errors import forbidden, unauthorized from app.identity.models import Session as UserSession from app.identity.models import User from app.identity.service import get_active_session from app.security import constant_time_equals from app.spaces.models import FinancialSpace, Membership from app.spaces.service import list_spaces_for_user Db = Annotated[Session, Depends(get_db)] @dataclass(frozen=True) class RequestContext: user: User session: UserSession space: FinancialSpace membership: Membership def current_session( db: Db, session_token: Annotated[str | None, Cookie(alias=get_settings().session_cookie_name)] = None, ) -> tuple[User, UserSession]: if not session_token: raise unauthorized("IDENTITY_SESSION_MISSING", "Authentication required") session = get_active_session(db, token=session_token) user = db.get(User, session.user_id) if user is None or user.status != "active": raise unauthorized("IDENTITY_USER_INACTIVE", "Authentication required") return user, session CurrentSession = Annotated[tuple[User, UserSession], Depends(current_session)] def current_context(db: Db, auth: CurrentSession) -> RequestContext: user, session = auth spaces = list_spaces_for_user(db, user=user) if not spaces: raise forbidden("SPACE_MEMBERSHIP_REQUIRED", "No financial space is available") space, membership = spaces[0] if space.status != "active": raise forbidden("SPACE_BLOCKED", "Financial space is unavailable") set_space_context(db, space.id) return RequestContext(user=user, session=session, space=space, membership=membership) Context = Annotated[RequestContext, Depends(current_context)] def require_csrf( context: Context, csrf_header: Annotated[str | None, Header(alias="X-CSRF-Token")] = None, ) -> None: if not csrf_header or not constant_time_equals(context.session.csrf_token, csrf_header): raise forbidden("IDENTITY_CSRF_INVALID", "CSRF token is missing or invalid") Csrf = Annotated[None, Depends(require_csrf)]