docs: align flow docs with code reality + create 35 implementation issue files
Flow docs updated (11 files): - Delivery Confirmation: reversed actor roles (buyer generates, seller verifies), fixed endpoint paths (/delivery-code/generate, /delivery-code/verify) - Passkey (WebAuthn): removed stub/simulated-key claims; real @simplewebauthn/server attestation is implemented; refresh tokens are persisted - Dispute: corrected resolve schema (action enum), removed non-existent statuses, documented security gaps (no role guards on status/resolve/assign), route shadowing, all socket events are TODO stubs - Seller Offer: corrected all endpoint paths, removed 'active' status, documented withdraw dead code, missing seller history page, select-offer notification gap - Notification: corrected mark-all-read method+path, fixed GET /:id broken lookup, added unread-count-update socket event - Authentication: corrected rate limiter (counts all attempts), axios 403 not handled, deleteAccount wrong endpoint bug, changePassword no UI - Password Reset: corrected 6-digit code (not 8), documented no-complexity gap on reset-with-code vs token reset - Payment Flow DePay: /create→/save, removed phantom sub-routes, SIM_ bypass risk, PaymentProvider type gap, getProviderIntentEndpoint routing bug - Payment Flow SHKeeper: removed phantom polling endpoint, fixed release/refund paths - Purchase Request: added pending_payment/active statuses, fixed sellers/attachments endpoints, corrected socket events, PUT→PATCH bug - Escrow: documented dispute resolve does not touch escrow, route shadowing, confirm-delivery auth gap Issues created (35 files in Issues/): - 9 security issues (critical) including: dispute privilege escalation ×4, unauthenticated payment/scanner endpoints ×2, SIM_ production bypass, confirm-delivery ownership gap - 26 additional major/critical bugs covering broken endpoints, missing features, data integrity gaps, and frontend-backend mismatches Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,12 +2,22 @@
|
||||
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"]
|
||||
related_apis: ["POST /api/auth/request-password-reset", "POST /api/auth/reset-password-with-code", "POST /api/auth/reset-password"]
|
||||
---
|
||||
|
||||
> [!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: request a 6-digit code by email, submit it with the new password.
|
||||
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
|
||||
|
||||
@@ -30,16 +40,16 @@ Self-service password recovery: request a 6-digit code by email, submit it with
|
||||
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()`.
|
||||
- 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}$/`.
|
||||
- Validates code format `/^\d{6}$/` — a code of any other length (e.g., 8 digits) 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.
|
||||
- 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.
|
||||
@@ -59,7 +69,7 @@ sequenceDiagram
|
||||
FE->>BE: POST /api/auth/request-password-reset { email }
|
||||
BE->>DB: User.findOne({ email, status: "active" })
|
||||
alt user found
|
||||
BE->>BE: code = generateVerificationCode()
|
||||
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
|
||||
@@ -68,8 +78,9 @@ sequenceDiagram
|
||||
|
||||
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)
|
||||
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
|
||||
@@ -77,11 +88,26 @@ sequenceDiagram
|
||||
|
||||
## 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) |
|
||||
| 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}$/`. An 8-digit code will always fail.
|
||||
> - 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
|
||||
|
||||
@@ -100,7 +126,7 @@ sequenceDiagram
|
||||
## Error / edge cases
|
||||
|
||||
- **Unknown email** → always `200`, generic message. No enumeration.
|
||||
- **Invalid code format** → `400` from `isValidVerificationCode` guard before DB lookup.
|
||||
- **Invalid code format** → `400` from `isValidVerificationCode` guard before DB lookup. Note: the `authController.ts` comment mentions "8 digits" but the actual implementation generates and validates exactly 6 digits — any 8-digit code will be rejected.
|
||||
- **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).
|
||||
@@ -110,6 +136,17 @@ sequenceDiagram
|
||||
> [!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'`.
|
||||
|
||||
> [!bug] Controller comment says "8 digits" but code generates 6
|
||||
> The comment in `authController.ts` describes an 8-digit code, but `authService.generateVerificationCode()` uses `Math.floor(100000 + Math.random() * 900000)`, which produces a number in the range 100000–999999 (exactly 6 digits). `isValidVerificationCode()` enforces `/^\d{6}$/`. Any 8-digit value sent to `reset-password-with-code` will always be rejected. The comment is wrong; the 6-digit implementation and validation are correct and consistent.
|
||||
|
||||
## 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 |
|
||||
| Controller comment says 8 digits | Doc bug | Comment is wrong; code generates and validates exactly 6 digits |
|
||||
| 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.
|
||||
|
||||
Reference in New Issue
Block a user