feat: add password reset ("forgot password") flow
Build & Push Docker Image / build (push) Successful in 4m12s

Add a self-service password reset to the login flow:

- Login page now shows a "Passwort vergessen?" link under the password field
- New /forgot-password page requests a reset email by address
- New /reset-password page sets a new password from an emailed token
- Backend: POST /auth/forgot-password and /auth/reset-password with
  dedicated rate limiters; tokens stored as SHA-256 hashes with a 1h expiry
- Generic responses avoid leaking account existence or SMTP/SSO state;
  SSO-only accounts are skipped
- New sendPasswordResetEmail mailer + email/auth i18n keys (de + en)
- DB migration: reset_token_hash, reset_token_expires, reset_requested_at

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-26 23:40:11 +02:00
parent 2f65e53a24
commit db82cd944f
9 changed files with 500 additions and 1 deletions
+19
View File
@@ -384,6 +384,25 @@ export async function initDatabase() {
}
}
// Password reset: store a SHA-256 hash of the reset token (never the raw token)
if (!(await db.columnExists('users', 'reset_token_hash'))) {
await db.exec('ALTER TABLE users ADD COLUMN reset_token_hash TEXT DEFAULT NULL');
}
if (!(await db.columnExists('users', 'reset_token_expires'))) {
if (isPostgres) {
await db.exec('ALTER TABLE users ADD COLUMN reset_token_expires TIMESTAMP DEFAULT NULL');
} else {
await db.exec('ALTER TABLE users ADD COLUMN reset_token_expires DATETIME DEFAULT NULL');
}
}
if (!(await db.columnExists('users', 'reset_requested_at'))) {
if (isPostgres) {
await db.exec('ALTER TABLE users ADD COLUMN reset_requested_at TIMESTAMP DEFAULT NULL');
} else {
await db.exec('ALTER TABLE users ADD COLUMN reset_requested_at DATETIME DEFAULT NULL');
}
}
// Federation sync: add deleted + updated_at to federated_rooms
if (!(await db.columnExists('federated_rooms', 'deleted'))) {
await db.exec('ALTER TABLE federated_rooms ADD COLUMN deleted INTEGER DEFAULT 0');