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,43 @@
|
||||
"""Password reset tokens (SPEC §6.1).
|
||||
|
||||
Single-use, 30-minute, Redis-backed tokens. Only issued when the account has a
|
||||
recovery email on file. Actual delivery of the token to the recovery address
|
||||
requires an app-level mailer (not part of M0/M1); until then the token is logged
|
||||
server-side. See docs/open-questions.md.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import secrets
|
||||
import uuid
|
||||
|
||||
from app.redis_client import aw, get_redis
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_PREFIX = "pwreset:"
|
||||
_TTL = 30 * 60 # 30 minutes
|
||||
|
||||
|
||||
async def issue_token(user_id: uuid.UUID, recovery_email: str) -> str:
|
||||
"""Create a single-use reset token bound to ``user_id`` and return it."""
|
||||
token = secrets.token_urlsafe(32)
|
||||
await get_redis().set(_PREFIX + token, str(user_id), ex=_TTL)
|
||||
# TODO(spec): deliver via the recovery address once an app mailer exists.
|
||||
logger.info("password reset token issued for user=%s recovery=%s", user_id, recovery_email)
|
||||
return token
|
||||
|
||||
|
||||
async def consume_token(token: str) -> uuid.UUID | None:
|
||||
"""Atomically consume a reset token, returning the user_id (or None)."""
|
||||
if not token:
|
||||
return None
|
||||
redis = get_redis()
|
||||
raw = await aw(redis.getdel(_PREFIX + token)) # single-use: fetch and delete atomically
|
||||
if raw is None:
|
||||
return None
|
||||
try:
|
||||
return uuid.UUID(raw)
|
||||
except ValueError:
|
||||
return None
|
||||
Reference in New Issue
Block a user