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