Files
nick-doc/04 - Flows/Password Reset Flow.md
2026-05-23 20:35:34 +03:30

6.0 KiB

title, tags, related_models, related_apis
title tags related_models related_apis
Password Reset Flow
flow
auth
password-reset
email
User
POST /api/auth/request-password-reset
POST /api/auth/reset-password-with-code

Password Reset Flow

Self-service password recovery: request a 6-digit code by email, submit it with the new password.

Actors

  • User who has forgotten their password.
  • Frontendfrontend/src/auth/view/jwt/jwt-reset-password-view.tsx (request) and jwt-update-password-view.tsx (submit new password).
  • BackendAuthController.requestPasswordReset and AuthController.resetPasswordWithCode in backend/src/services/auth/authController.ts.
  • MongoDBUser collection (passwordResetCode, passwordResetCodeExpires, refreshTokens).
  • Email serviceemailService.sendPasswordResetCodeEmail.

Preconditions

  • The account exists and status === "active" (deleted accounts are silently treated as non-existent).
  • The user has access to the email inbox associated with the account.
  • A 6-digit code is valid for 1 hour (authController.ts:556).

Step-by-step narrative

  1. User clicks "Forgot password?" on the sign-in page and lands at /auth/jwt/reset-password.
  2. User enters their email and submits.
  3. Frontend POSTs POST /api/auth/request-password-reset { email }.
  4. Backend authController.requestPasswordReset (:542-574):
    • User.findOne({ email, status: "active" }). If absent, returns 200 with the same generic message — no enumeration.
    • Generates a 6-digit code via authService.generateVerificationCode().
    • Saves passwordResetCode and passwordResetCodeExpires = now + 3_600_000 ms on the user.
    • Calls emailService.sendPasswordResetCodeEmail(email, firstName, code).
  5. Response: 200 "If an account with this email exists, a password reset code has been sent" regardless of outcome.
  6. User receives the email and enters the code + new password on /auth/jwt/update-password.
  7. Frontend POSTs POST /api/auth/reset-password-with-code { email, code, password }.
  8. Backend authController.resetPasswordWithCode (:611-657):
    • Validates code format /^\d{6}$/.
    • 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.
  9. 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:559 console.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 format400 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'.

Linked flows

Source files

  • Backend: backend/src/services/auth/authController.ts:542-657
  • Backend: backend/src/services/email/emailService.ts (sendPasswordResetCodeEmail)
  • Frontend: frontend/src/auth/view/jwt/jwt-reset-password-view.tsx
  • Frontend: frontend/src/auth/view/jwt/jwt-update-password-view.tsx
  • Frontend: frontend/src/auth/context/jwt/action.ts:181-200, :261-276