Send magic-link email in the background (instant request response)
The SMTP send (connect → TLS → login → handoff to the relay) ran synchronously inside POST /api/auth/email/start, so the "Sending…" button waited the whole handshake. Move it to a FastAPI BackgroundTask: the token is created + committed, the request returns immediately, and the email sends off the request path. Reply stays identical (no account enumeration). Tests pass (TestClient runs the task). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+15
-7
@@ -22,7 +22,7 @@ from contextlib import contextmanager
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import FastAPI, HTTPException, Query, Request, Response
|
||||
from fastapi import BackgroundTasks, FastAPI, HTTPException, Query, Request, Response
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from pydantic import BaseModel
|
||||
@@ -74,6 +74,14 @@ def _current_user(conn: sqlite3.Connection, request: Request) -> sqlite3.Row | N
|
||||
return user
|
||||
|
||||
|
||||
def _send_link_safe(email: str, link: str) -> None:
|
||||
"""Send the magic link, swallowing failures (runs off the request path)."""
|
||||
try:
|
||||
email_send.send_magic_link(email, link)
|
||||
except Exception:
|
||||
pass # don't crash the worker; never surfaced to the caller anyway
|
||||
|
||||
|
||||
def _set_session_cookie(response: Response, token: str) -> None:
|
||||
response.set_cookie(
|
||||
SESSION_COOKIE, token, max_age=SESSION_MAX_AGE,
|
||||
@@ -285,10 +293,11 @@ def create_app() -> FastAPI:
|
||||
# --- Auth: passwordless magic link (Google added in Phase 2) ----------
|
||||
|
||||
@app.post("/api/auth/email/start")
|
||||
def auth_email_start(body: EmailStartRequest) -> dict:
|
||||
def auth_email_start(body: EmailStartRequest, background_tasks: BackgroundTasks) -> dict:
|
||||
email = auth.normalize_email(body.email)
|
||||
if not _EMAIL_RE.match(email):
|
||||
raise HTTPException(status_code=422, detail="Please enter a valid email address.")
|
||||
link = None
|
||||
with get_conn() as conn:
|
||||
# Light abuse guard: cap recent tokens per address (still reply OK).
|
||||
recent = conn.execute(
|
||||
@@ -300,11 +309,10 @@ def create_app() -> FastAPI:
|
||||
raw = auth.create_login_token(conn, email)
|
||||
conn.commit()
|
||||
link = f"{PUBLIC_BASE_URL}/auth/verify?token={raw}"
|
||||
try:
|
||||
email_send.send_magic_link(email, link)
|
||||
except Exception:
|
||||
pass # never leak send failures or whether the address exists
|
||||
# Always identical (no account enumeration).
|
||||
# Hand the (slow) SMTP send to a background task so the request returns
|
||||
# immediately. Reply is always identical (no account enumeration).
|
||||
if link:
|
||||
background_tasks.add_task(_send_link_safe, email, link)
|
||||
return {"ok": True}
|
||||
|
||||
@app.post("/api/auth/email/verify", response_model=SessionOut)
|
||||
|
||||
Reference in New Issue
Block a user