Files
webmail-client/backend/app/identity/reset.py
T
lukasandClaude Opus 4.8 a5ec0d21cb 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>
2026-07-17 23:20:34 +02:00

44 lines
1.4 KiB
Python

"""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