User.findOne({ email, passwordResetCode: code, passwordResetCodeExpires: { $gt: now }, status: "active" }). Mismatch → 400 Invalid or expired reset code.
Hashes the new password with bcrypt cost 12.
Sets user.password = hashed, clears passwordResetCode and passwordResetCodeExpires, wipes user.refreshTokens = [] to invalidate all existing sessions.
Saves.
Response: 200 "Password reset successfully". Frontend redirects to /auth/jwt/sign-in for a fresh login.
Sequence diagram
sequenceDiagram
autonumber
actor U as User
participant FE as Frontend
participant BE as Backend
participant DB as MongoDB
participant MAIL as Email Service
U->>FE: Click "Forgot password", enter email
FE->>BE: POST /api/auth/request-password-reset { email }
BE->>DB: User.findOne({ email, status: "active" })
alt user found
BE->>BE: code = generateVerificationCode()
BE->>DB: user.passwordResetCode = code\nexpires = +1h
BE->>MAIL: sendPasswordResetCodeEmail(email, firstName, code)
MAIL-->>U: Email with 6-digit code
end
BE-->>FE: 200 "if account exists, code sent"
U->>FE: Enter code + new password
FE->>BE: POST /api/auth/reset-password-with-code { email, code, password }
BE->>DB: User.findOne({ email, code, expires>now })
BE->>BE: bcrypt.hash(password, 12)
BE->>DB: user.password = hash\nuser.refreshTokens = []\nclear reset fields
BE-->>FE: 200 "Password reset successfully"
FE-->>U: Redirect /auth/jwt/sign-in
API calls
Method
Endpoint
Source
POST
/api/auth/request-password-reset
authRoutes.ts:44-47
POST
/api/auth/reset-password-with-code
authRoutes.ts:54-56
POST
/api/auth/reset-password
authRoutes.ts:49-52 (legacy token-based variant)
Database writes
users collection: on request, sets passwordResetCode + passwordResetCodeExpires. On submit, replaces password, clears reset fields, and empties refreshTokens.
Socket events emitted
None.
Side effects
Email: one transactional message containing the 6-digit code.
Server-side log: authController.ts:559console.log includes the generated code in plain text — same hardening note as Registration Flow.
Session invalidation: All refresh tokens cleared → all devices forced to re-login after password change. Access tokens still valid until expiry (typically minutes).
Error / edge cases
Unknown email → always 200, generic message. No enumeration.
Invalid code format → 400 from isValidVerificationCode guard before DB lookup.
Expired code (>1h) → 400 Invalid or expired reset code.
Multiple parallel requests → each overwrites the previous passwordResetCode; the latest email wins, prior codes silently invalidated.
User attempts reset on deleted account → treated as unknown (no email sent, 200 returned).
Email delivery failure → response still 200; user can request again.
Access tokens still valid post-reset → unavoidable with stateless JWT; mitigated by short TTL. Critical operations should re-verify password.
[!warning] Plaintext code in logs
Same as Registration Flow: the reset code is console.log-ed by the controller in all environments. Restrict log access in production or gate the log behind NODE_ENV !== 'production'.