feat(auth): self-service password reset (backend + web)
Add a full password-reset flow — the prerequisite for the Android app (docs/android/PLAN.md §8.2), which hands off to the website for reset rather than shipping a native screen. Backend: - password_resets table: stores only the sha256 hash of an opaque 32-byte token (mirrors user_invites / mobile_refresh_tokens), single-use, ~1h TTL. - model/passwordResets + users.getActiveByEmail (email is non-unique, so a request can match several accounts, each emailed its own link). - mailer.sendPasswordReset (fails soft when email is unconfigured). - Endpoints: POST /auth/password/forgot (always a generic 200 — no account enumeration), GET|POST /auth/password/reset/:token. Confirming rotates the hash and revokes every session (web cutoff + mobile refresh tokens); it does not auto-login, so a 2FA account still passes TOTP next sign-in. Also serves SSO-only accounts (null hash) as their set-initial-password path. - Dedicated request/confirm rate limiters. Swagger regenerated. Web: - ForgotPassword + ResetPassword pages, routes /account/forgot and /account/reset/:token, and a "Forgot your password?" link on the login page. Tests: test/passwordResets.test.js (5). All server tests pass; client builds; end-to-end smoketest against MariaDB passes (no-enumeration, single-use, hash rotation, session revoke, login with the new password). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NgyHnrNa8WwG3doxvxjuCr
This commit is contained in:
@@ -34,6 +34,14 @@ async function getById(id) {
|
||||
return sanitize(await usersDb.findById(id))
|
||||
}
|
||||
|
||||
// Raw rows (incl. email/status) for every active account on an email address.
|
||||
// Server-side only (password-reset request); email is non-unique so this may
|
||||
// return several. Never sent to a client.
|
||||
async function getActiveByEmail(email) {
|
||||
if (!email) return []
|
||||
return usersDb.findActiveByEmail(String(email).trim())
|
||||
}
|
||||
|
||||
// Raw row incl. totp_secret — server-side only (TOTP setup/verify). Never sent
|
||||
// to a client; sanitize() strips the secret from anything user-facing.
|
||||
async function getRawById(id) {
|
||||
@@ -109,6 +117,7 @@ module.exports = {
|
||||
isDuplicateUsername,
|
||||
getRawByUsername,
|
||||
getById,
|
||||
getActiveByEmail,
|
||||
getRawById,
|
||||
validatePassword,
|
||||
list,
|
||||
|
||||
Reference in New Issue
Block a user