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:
2026-07-17 23:20:34 +02:00
co-authored by Claude Opus 4.8
commit a5ec0d21cb
174 changed files with 22239 additions and 0 deletions
+50
View File
@@ -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)]