Remaining docs updated to match code (the docs that the first pass had not covered):
- Flows: Chat, Referral, Rating, Registration, Google OAuth, Negotiation, Payout,
Trezor Safekeeping — corrected endpoints, socket events, status enums, auth gaps
- API Reference: User API, Trezor API — admin route prefix/verb/status corrections,
added undocumented endpoints (ton-proof challenge, profile email verify,
GET /trezor/account, POST /trezor/verify-operation)
- Data Models: Chat, Notification, Payment, PointTransaction, User — corrected
enums (PaymentProvider, escrowState, PointTransaction.type, User.status),
90-day notification TTL, soft-delete semantics, wallet fields
Trezor "zero frontend" finding (audit C31/C32) corrected as STALE:
- Verified current code HAS a full frontend Trezor implementation (admin/trezor
page, TrezorSettingsView, trezorConnector via @trezor/connect-web,
TrezorSignDialog, actions/trezor.ts building the {message,signature} object)
- Fixed Trezor Safekeeping Flow doc (removed false "no frontend" warnings)
- Reclassified ISSUE-012 as invalid/superseded with explanation
Issue set reconciled to a single canonical numbering (ISSUE-001..054):
- Adopted the comprehensive 51-issue set (long-slug, fully indexed)
- Removed 35 superseded short-slug duplicates from the first pass
- Removed a duplicate ISSUE-046 file
- Added 3 issues the 51-set lacked: ISSUE-052 (completed-not-counted-in-stats),
ISSUE-053 (axios 401-only interceptor), ISSUE-054 (rate limiter counts all attempts)
- Regenerated Issues Index: 53 open (14 critical, 39 major) + 1 invalid
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
160 lines
8.4 KiB
Markdown
160 lines
8.4 KiB
Markdown
---
|
|
title: Password Reset Flow
|
|
tags: [flow, auth, password-reset, email]
|
|
related_models: ["[[User]]"]
|
|
related_apis: ["POST /api/auth/request-password-reset", "POST /api/auth/reset-password-with-code", "POST /api/auth/reset-password"]
|
|
---
|
|
|
|
> **Last updated:** 2026-05-29 — aligned with code (see Doc vs Code Audit Report)
|
|
|
|
> [!caution] Audit note — last reviewed 2026-05-29
|
|
> Several discrepancies between frontend code, backend code, and this document were identified and corrected in this revision. See inline ⚠️ markers for known bugs and mismatches.
|
|
|
|
# Password Reset Flow
|
|
|
|
Self-service password recovery. There are **two separate reset endpoints** with different security characteristics:
|
|
|
|
| Endpoint | Mechanism | Password complexity enforced? |
|
|
|---|---|---|
|
|
| `POST /api/auth/reset-password-with-code` | 6-digit emailed code | **No** — no validation middleware |
|
|
| `POST /api/auth/reset-password` | Token-based (link in email) | **Yes** — `passwordResetValidation` requires uppercase + lowercase + digit |
|
|
|
|
The primary UI-driven path uses the **code-based** endpoint. The token-based endpoint is a legacy/alternative variant.
|
|
|
|
## Actors
|
|
|
|
- **User** who has forgotten their password.
|
|
- **Frontend** — `frontend/src/auth/view/jwt/jwt-reset-password-view.tsx` (request) and `jwt-update-password-view.tsx` (submit new password).
|
|
- **Backend** — `AuthController.requestPasswordReset` and `AuthController.resetPasswordWithCode` in `backend/src/services/auth/authController.ts`.
|
|
- **MongoDB** — `User` collection (`passwordResetCode`, `passwordResetCodeExpires`, `refreshTokens`).
|
|
- **Email service** — `emailService.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()` (`Math.floor(100000 + Math.random() * 900000)`).
|
|
- 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}$/` — codes of any other length will **always fail** here.
|
|
- `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. **No password complexity validation is applied** — weak passwords such as `123456` or `aaaaaa` are accepted without error.
|
|
- 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
|
|
|
|
```mermaid
|
|
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() [6 digits]
|
|
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->>BE: isValidVerificationCode(code) [/^\d{6}$/]
|
|
BE->>DB: User.findOne({ email, code, expires>now })
|
|
BE->>BE: bcrypt.hash(password, 12) [no complexity check]
|
|
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 | Notes |
|
|
|---|---|---|---|
|
|
| `POST` | `/api/auth/request-password-reset` | `authRoutes.ts:44-47` | Sends 6-digit code by email |
|
|
| `POST` | `/api/auth/reset-password-with-code` | `authRoutes.ts:54-56` | Code-based; **no complexity validation** |
|
|
| `POST` | `/api/auth/reset-password` | `authRoutes.ts:49-52` | Token-based variant; enforces complexity via `passwordResetValidation` |
|
|
|
|
## Two-endpoint comparison
|
|
|
|
> [!important] Code-based vs token-based reset endpoints
|
|
>
|
|
> **`POST /api/auth/reset-password-with-code`** (primary UI path)
|
|
> - Uses a 6-digit numeric code delivered by email.
|
|
> - `isValidVerificationCode()` validates with `/^\d{6}$/`.
|
|
> - Has **no password complexity middleware**. Any string is accepted as the new password.
|
|
>
|
|
> **`POST /api/auth/reset-password`** (legacy token-based path)
|
|
> - Uses a URL token (link in email) rather than a short code.
|
|
> - Enforces password complexity via `passwordResetValidation` middleware (requires uppercase, lowercase, and a digit).
|
|
>
|
|
> The two endpoints provide inconsistent security guarantees. Users who reset via the code flow can set a weak password that would be rejected by the token flow.
|
|
|
|
## 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 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'`.
|
|
|
|
## Known issues summary
|
|
|
|
| Issue | Severity | Details |
|
|
|---|---|---|
|
|
| No password complexity on code-based reset | Security gap | `POST /api/auth/reset-password-with-code` has no complexity middleware; weak passwords accepted |
|
|
| Inconsistent complexity between reset endpoints | Security gap | Token-based reset enforces complexity; code-based reset does not |
|
|
|
|
## Linked flows
|
|
|
|
- [[Authentication Flow]] — user re-signs-in after reset.
|
|
- [[Registration Flow]] — same code-generation utility.
|
|
|
|
## 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`
|