Password Reset

Self-service flow via emailed token.

Files

  • Schema: src/lib/db/schema/password-resets.ts (token PK, user_id, expires_at, used_at).
  • Request endpoint: POST /api/auth/forgot-password.
  • Complete endpoint: POST /api/auth/reset-password.
  • UIs: /forgot-password, /reset-password.

Flow

  1. User visits /forgot-password, submits email.
  2. Server POST /api/auth/forgot-password:
    • Looks up user by email.
    • Always returns { ok: true } — does not reveal whether the email exists.
    • If user exists: inserts password_resets row with a 48-char token, 1-hour expiry. Emails the reset link via the app's own sendEmail() using SYSTEM_FROM_EMAIL.
  3. User clicks link → /reset-password?token=<token>.
  4. User submits new password (min 8 chars).
  5. Server POST /api/auth/reset-password { token, password }:
    • Looks up token — rejects if unknown, expired, or used.
    • Hashes password with bcrypt cost 12.
    • Updates users.password_hash.
    • Marks token used_at.
    • Writes audit log (user.password_reset).
  6. UI redirects to /login.

Security

  • 1-hour TTL — much shorter than email verification.
  • Single-use via used_at.
  • 48-char random token (~276 bits).
  • Enumeration-resistant: endpoint always returns success regardless of email existence.
  • Note: reset does not invalidate existing sessions. Consider adding a session version column or issuing a new JWT secret per user if sensitive data is involved.

Env

  • SYSTEM_FROM_EMAIL — verified SES identity for the reset email.