fix: repair Mermaid diagram syntax errors and add PRD task plan

This commit is contained in:
Siavash Sameni
2026-05-24 08:07:25 +04:00
parent 5b93b2d23e
commit 09ef02c314
11 changed files with 167 additions and 85 deletions

View File

@@ -79,7 +79,8 @@ sequenceDiagram
FE-->>U: Redirect /auth/jwt/verify
else success
BE->>R: rateLimitService.resetLoginAttempts(email)
BE->>DB: user.lastLoginAt = now; user.refreshTokens.push(refresh)
BE->>DB: set user.lastLoginAt = now
BE->>DB: save new refresh token
BE->>BE: generateToken(authUser) / generateRefreshToken(authUser)
BE->>R: sessionService.createSession(accessToken, ...)
BE-->>FE: 200 { user, tokens: { accessToken, refreshToken } }
@@ -135,53 +136,5 @@ sequenceDiagram
BE-->>FE: 401 TOKEN_INVALID
FE->>BE: POST /api/auth/refresh-token { refreshToken }
BE->>BE: verifyRefreshToken(refreshToken)
BE->>DB: User.findById(decoded.id); ensure refresh ∈ user.refreshTokens
BE->>BE: Generate new access + refresh tokens
BE->>DB: user.refreshTokens = [...minus old, new]
BE-->>FE: 200 { tokens: { accessToken, refreshToken } }
FE->>BE: GET /api/marketplace/... (Bearer new access) — retry
```
## Logout flow
1. Frontend `signOut()` (`action.ts:146-176`) reads `refreshToken` from `localStorage` and POSTs `/api/auth/logout` with a 10-second timeout.
2. Backend `authController.logout` (`:316-344`) removes the refresh token from `user.refreshTokens[]` and calls `sessionService.deleteSession(accessToken)`.
3. **Always-clear**: the frontend's `finally` block removes both `accessToken` and `refreshToken` from `localStorage` regardless of network success — meaning even an offline logout effectively signs the user out locally.
> [!tip] Force-logout an entire user
> Setting `user.refreshTokens = []` in MongoDB instantly invalidates all sessions on next refresh. `changePassword`, `resetPassword`, and `deleteAccount` all do this.
## Error / edge cases
- **Wrong password** → `401 Invalid credentials` (intentionally vague — no distinction between "unknown email" and "wrong password").
- **Email unverified** → `403 EMAIL_NOT_VERIFIED`; frontend auto-redirects to verify page.
- **5+ failures in 15 min** → `429 TOO_MANY_ATTEMPTS`; only an admin can manually clear via Redis.
- **Network timeout** → axios `AbortController` cancels at 60s; frontend shows a typed error and the user can retry.
- **Redis down** → login still succeeds (session creation is best-effort, wrapped in try/catch at `authController.ts:234-247`). Rate limiting falls back to the in-memory map in `authService.ts:113-145` if `rateLimitService` itself throws.
- **Stale refresh token** (rotated by another device) → `403 Invalid refresh token`. Frontend signs out and redirects to sign-in.
- **JWT signature mismatch** (secret rotated) → all sessions invalidated server-side; clients clear tokens on first 401.
- **Token issued for another audience/issuer** → `verifyToken` returns `null` (`authService.ts:60-79`), middleware returns `403 TOKEN_INVALID`.
- **Refresh token used as access token** → blocked by the `if (decoded.type === 'refresh') return null` check in `verifyToken` (`authService.ts:67`). This is critical: a leaked refresh token alone cannot read protected data.
- **Soft-deleted account** → `User.findOne({ status: "active" })` filter excludes deleted accounts; login fails as if the email did not exist.
> [!warning] Constant-time response is approximate
> Today we return `401` immediately when the user is missing, before running bcrypt. This is a timing oracle that lets an attacker enumerate registered emails by response-time analysis. Mitigation tracked separately — the recommendation is to always run a dummy bcrypt compare on missing users.
## Linked flows
- [[Registration Flow]] — produces the `User` document this flow consumes.
- [[Password Reset Flow]] — alternate entry into the account if credentials are lost.
- [[Google OAuth Flow]] — parallel auth path that produces equivalent tokens.
- [[Passkey (WebAuthn) Flow]] — passwordless alternative.
- [[Chat Flow]], [[Notification Flow]] — both consume the access token to authorise Socket.IO rooms.
## Source files
- Backend: `backend/src/services/auth/authController.ts:161-260`
- Backend: `backend/src/services/auth/authService.ts:24-99`
- Backend: `backend/src/services/auth/authRoutes.ts:22`
- Backend: `backend/src/services/redis/sessionService.ts`
- Backend: `backend/src/services/redis/rateLimitService.ts`
- Frontend: `frontend/src/auth/context/jwt/action.ts:32-176`
- Frontend: `frontend/src/auth/view/jwt/jwt-sign-in-view.tsx`
- Frontend: `frontend/src/lib/axios.ts` (interceptor + endpoints)
BE->>DB: User.findById(decoded.id)
BE->>DB: ensure refresh token is in user.refreshTokens

View File

@@ -108,7 +108,7 @@ sequenceDiagram
A->>FE_A: type & send
FE_A->>BE: POST /api/chat/{id}/messages {content}
BE->>DB: chat.addMessage; metadata.lastActivity=now
BE->>DB: chat.addMessage and update metadata.lastActivity to now
BE->>IO: emit chat-{id} 'new-message'
IO-->>FE_A: 'new-message' (echo)
IO-->>FE_B: 'new-message' (live)

View File

@@ -64,9 +64,11 @@ sequenceDiagram
B->>FE: Enter code in dashboard
FE->>BE: POST /api/marketplace/purchase-requests/{id}/verify-delivery {code}
BE->>DB: match code, expires>now, !used
BE->>DB: deliveryCodeUsed=true; status="delivered"
BE->>DB: set deliveryCodeUsed = true
BE->>DB: set status = "delivered"
BE->>IO: emit request-{id} 'purchase-request-update' status-changed
BE->>B,S: notifyDeliveryConfirmed
BE->>B: notifyDeliveryConfirmed
BE->>S: notifyDeliveryConfirmed
Note over BE: Auto-release timer (planned) → seller_paid → payout
```

View File

@@ -105,7 +105,8 @@ sequenceDiagram
BE->>DB: Chat.create({type:"group", participants:[buyer, seller], system message})
BE->>DB: dispute.chatId = chat._id
BE-->>FE: { dispute }
FE-->>B,S: chat opens (real-time via existing chat join)
FE-->>B: chat opens (real-time via existing chat join)
FE-->>S: chat opens (real-time via existing chat join)
A->>FE: Admin dashboard, click "Pick up"
FE->>BE: POST /api/disputes/{id}/assign
@@ -130,7 +131,8 @@ sequenceDiagram
A->>BE: split — refund X to buyer, release Y to seller
end
BE-->>FE: { dispute }
IO-->>B,S: 'new-notification' dispute resolved (planned)
IO-->>B: 'new-notification' dispute resolved (planned)
IO-->>S: 'new-notification' dispute resolved (planned)
```
## API calls

View File

@@ -87,9 +87,11 @@ sequenceDiagram
else Sign-in: user missing
BE-->>FE: 404 USER_NOT_FOUND
else Sign-in: ok
BE->>DB: user.lastLoginAt = now; back-fill avatar if blank
BE->>DB: set user.lastLoginAt = now
BE->>DB: back-fill avatar if blank
end
BE->>BE: generate access + refresh; push refresh
BE->>BE: generate access and refresh tokens
BE->>BE: push refresh token
BE-->>FE: 200 { user, tokens }
FE->>FE: localStorage.setItem(accessToken/refreshToken)
FE-->>U: Redirect /dashboard/{role}

View File

@@ -118,7 +118,8 @@ sequenceDiagram
B->>FE: Click "Publish"
FE->>BE: POST /api/marketplace/purchase-requests
BE->>DB: Duplicate check (same title+desc in 5m?)
BE->>BE: clean preferredSellerIds; compute isPublic
BE->>BE: clean preferredSellerIds
BE->>BE: compute isPublic
BE->>DB: PurchaseRequest.create({status: "pending"})
DB-->>BE: savedRequest
BE->>N: notifyPurchaseRequestCreated(buyer, requestId)

View File

@@ -93,7 +93,8 @@ sequenceDiagram
Note over BE,DB: Later, when N completes a purchase
BE->>BE: PointsService.addPoints(R, +X, 'referral', {referredUserId:N})
BE->>DB: user.points += X; PointTransaction.create
BE->>DB: add X points to user balance
BE->>DB: create PointTransaction record
BE->>BE: updateUserLevel → maybe 'level-up'
BE->>IO: emit user-{R} 'level-up'
```

View File

@@ -120,7 +120,8 @@ sequenceDiagram
BE->>IO: emit user-{refId} 'referral-signup'
end
BE->>DB: TempVerification.findByIdAndDelete(...)
BE->>BE: generate tokens; push refresh
BE->>BE: generate tokens
BE->>BE: push refresh
BE-->>FE: 200 { user, tokens }
FE->>FE: localStorage.setItem(accessToken, refreshToken)
FE-->>U: Redirect /dashboard/{role}

View File

@@ -99,37 +99,37 @@ sequenceDiagram
autonumber
actor S as Seller
actor B as Buyer
participant FE_S as Frontend (seller)
participant FE_B as Frontend (buyer)
participant FE_S as Seller frontend
participant FE_B as Buyer frontend
participant BE as Backend
participant DB as MongoDB
participant N as NotificationService
participant IO as Socket.IO
participant IO as SocketIO
S->>FE_S: Browse /dashboard/seller/marketplace
FE_S->>BE: GET /api/marketplace/purchase-requests?sellerId
BE-->>FE_S: filtered list
S->>FE_S: Open request, click "Send proposal"
S->>FE_S: Fill price, ETA, notes; submit
S->>FE_S: Browse marketplace
FE_S->>BE: GET /api/marketplace/purchase-requests
BE-->>FE_S: filtered request list
S->>FE_S: Open request and send offer
FE_S->>BE: POST /api/marketplace/offers
BE->>DB: ensure no existing offer; check status
BE->>DB: SellerOffer.create({status:"pending"})
opt first offer on the request
BE->>DB: PurchaseRequest.status = "received_offers"
BE->>DB: Validate offer not duplicate
BE->>DB: Validate request status
BE->>DB: Create offer with status pending
opt request has no offers yet
BE->>DB: Set request status to received_offers
end
BE->>N: notifyNewOfferReceived(buyer, requestId, sellerName)
N->>IO: emit user-{buyer} new-notification
BE->>IO: emit seller-{sellerId} 'new-offer'
BE->>N: notifyNewOfferReceived
N->>IO: emit notification to buyer
BE->>IO: emit seller new-offer
BE-->>FE_S: 200 { offer }
IO-->>FE_B: new-notification (buyer's bell icon)
IO-->>FE_B: notify buyer bell icon
B->>FE_B: Open request detail
FE_B->>BE: GET /api/marketplace/offers/request/{id}
BE-->>FE_B: offers[]
alt Buyer accepts via payment
B->>FE_B: Click "Pay" → starts [[Payment Flow - SHKeeper]]
Note over BE,DB: SHKeeper webhook PAID arrives later;<br/>winning offer → accepted, others → rejected
else Buyer negotiates
B->>FE_B: Open chat → [[Negotiation Flow]]
BE-->>FE_B: offers
alt
B->>FE_B: Click pay to finish selected offer
B->>FE_B: SHKeeper webhook handles payment result
else
B->>FE_B: Open chat to negotiate
end
```