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
- User visits
/forgot-password, submits email. - 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_resetsrow with a 48-char token, 1-hour expiry. Emails the reset link via the app's ownsendEmail()usingSYSTEM_FROM_EMAIL.
- User clicks link →
/reset-password?token=<token>. - User submits new password (min 8 chars).
- 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).
- 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.