Initial commit: webmail client (M0/M1)
Three-pane webmail client. - Backend: FastAPI + async SQLAlchemy + arq worker (Postgres/Redis), IMAP/SMTP sync, threading, search, drafts/outbox with undo, attachments, 2FA. - Frontend: React 18 + Vite + TypeScript + TanStack Query + Tailwind; three-pane UI with rich-text composer, light/dark themes, resizable panels. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
"""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:
|
||||
# Session references a deleted user; treat as unauthenticated.
|
||||
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")
|
||||
|
||||
|
||||
CurrentUser = Annotated[User, Depends(current_user)]
|
||||
CurrentSession = Annotated[Session, Depends(current_session)]
|
||||
Reference in New Issue
Block a user