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 datetime import datetime, timezone
|
||||||
from pathlib import Path
|
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.middleware.cors import CORSMiddleware
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
@@ -74,6 +74,14 @@ def _current_user(conn: sqlite3.Connection, request: Request) -> sqlite3.Row | N
|
|||||||
return user
|
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:
|
def _set_session_cookie(response: Response, token: str) -> None:
|
||||||
response.set_cookie(
|
response.set_cookie(
|
||||||
SESSION_COOKIE, token, max_age=SESSION_MAX_AGE,
|
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) ----------
|
# --- Auth: passwordless magic link (Google added in Phase 2) ----------
|
||||||
|
|
||||||
@app.post("/api/auth/email/start")
|
@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)
|
email = auth.normalize_email(body.email)
|
||||||
if not _EMAIL_RE.match(email):
|
if not _EMAIL_RE.match(email):
|
||||||
raise HTTPException(status_code=422, detail="Please enter a valid email address.")
|
raise HTTPException(status_code=422, detail="Please enter a valid email address.")
|
||||||
|
link = None
|
||||||
with get_conn() as conn:
|
with get_conn() as conn:
|
||||||
# Light abuse guard: cap recent tokens per address (still reply OK).
|
# Light abuse guard: cap recent tokens per address (still reply OK).
|
||||||
recent = conn.execute(
|
recent = conn.execute(
|
||||||
@@ -300,11 +309,10 @@ def create_app() -> FastAPI:
|
|||||||
raw = auth.create_login_token(conn, email)
|
raw = auth.create_login_token(conn, email)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
link = f"{PUBLIC_BASE_URL}/auth/verify?token={raw}"
|
link = f"{PUBLIC_BASE_URL}/auth/verify?token={raw}"
|
||||||
try:
|
# Hand the (slow) SMTP send to a background task so the request returns
|
||||||
email_send.send_magic_link(email, link)
|
# immediately. Reply is always identical (no account enumeration).
|
||||||
except Exception:
|
if link:
|
||||||
pass # never leak send failures or whether the address exists
|
background_tasks.add_task(_send_link_safe, email, link)
|
||||||
# Always identical (no account enumeration).
|
|
||||||
return {"ok": True}
|
return {"ok": True}
|
||||||
|
|
||||||
@app.post("/api/auth/email/verify", response_model=SessionOut)
|
@app.post("/api/auth/email/verify", response_model=SessionOut)
|
||||||
|
|||||||
Reference in New Issue
Block a user