docs: complete code-reality alignment for remaining docs + reconcile issue set

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>
This commit is contained in:
Siavash Sameni
2026-05-29 15:15:02 +04:00
parent 9698ec5809
commit 7a616744f4
118 changed files with 2833 additions and 1788 deletions

View File

@@ -2,10 +2,11 @@
title: Chat Flow
tags: [flow, chat, socket-io, messaging]
related_models: ["[[Chat]]", "[[Message]]", "[[User]]"]
related_apis: ["POST /api/chat", "POST /api/chat/:chatId/messages", "GET /api/chat/:chatId/messages", "POST /api/chat/:chatId/read"]
related_apis: ["POST /api/chat", "POST /api/chat/:id/messages", "GET /api/chat/:id/messages", "PATCH /api/chat/:id/messages/read"]
---
# Chat Flow
> **Last updated:** 2026-05-29 — aligned with code (see [Doc vs Code Audit Report](../09%20-%20Audits/Doc%20vs%20Code%20Audit%20Report%20-%202026-05-29.md))
Real-time messaging between buyer & seller (direct), three-way dispute mediation (group), and user-to-support (support). Backed by `Chat` documents with embedded `messages[]` and a Socket.IO room per chat for live updates.
@@ -18,7 +19,7 @@ Real-time messaging between buyer & seller (direct), three-way dispute mediation
- **Frontend** — `frontend/src/sections/chat/` (chat list, conversation view, message composer).
- **Backend** — `ChatService` (`backend/src/services/chat/ChatService.ts`), routes under `/api/chat`.
- **MongoDB** — `chats` collection with embedded `messages`, `participants`, `unreadCounts`, `settings`, `metadata`.
- **Socket.IO** — events `new-message`, `chat-notification`, `messages-read`, `user-typing`, `user-status-change`.
- **Socket.IO** — events `new-message`, `chat-notification`, `messages-read`, `user-typing`, `user-status-change`, `message-deleted`.
## Preconditions
@@ -32,8 +33,8 @@ stateDiagram-v2
[*] --> Created: ChatService.createChat\n(or auto on first contact)
Created --> Active: messages flowing
Active --> Active: send / read / typing
Active --> Archived: settings.isArchived=true
Archived --> Active: unarchive
Active --> Archived: PATCH /api/chat/:id/archive (toggle)
Archived --> Active: PATCH /api/chat/:id/archive (same endpoint toggles back)
Active --> [*]: chat deleted (rare)
```
@@ -41,25 +42,33 @@ stateDiagram-v2
### Creation
1. **Direct chat (find-or-create)** — when buyer clicks "Chat with seller" on a request detail, frontend POSTs `POST /api/chat` with `{ type: 'direct', participantIds: [buyer, seller], relatedTo: { type: 'PurchaseRequest', id } }`.
1. **Direct chat (find-or-create)** — when buyer clicks "Chat with seller" on a request detail, frontend POSTs `POST /api/chat` with `{ type: 'direct', participantIds: [sellerId] }`. The endpoint requires **exactly 1 external `participantId`**; the authenticated caller is auto-appended to make 2.
> [!warning] `relatedTo` is NOT accepted on `POST /api/chat`
> Despite the schema carrying a `relatedTo` discriminator, the create endpoint ignores/does not accept a `relatedTo` payload. Purchase-request linkage is performed server-side via the dedicated `POST /api/chat/purchase-request` (see step 5), not by passing `relatedTo` to `POST /api/chat`.
2. `ChatService.createChat` (`ChatService.ts:90-192`):
- For `direct` with exactly 2 participants, runs `Chat.findOne({ type: 'direct', 'participants.userId': { $all: [...] }, 'participants.isActive': true })` and returns the existing chat if found.
- Otherwise creates a new `Chat` with `participants` (each with `role:'member'`, `joinedAt`, `isActive:true`), zeroed `unreadCounts`, default `settings`, `metadata.createdBy`.
- Appends a system welcome message (`messageType: 'system'`).
- If `relatedTo.type === 'PurchaseRequest'`, also writes `"چت برای درخواست خرید \"{title}\" ایجاد شد"` system line.
- Re-loads with `populate('participants.userId', 'firstName lastName profile.avatar email')` for the response.
3. **Group chat (dispute)** — same pattern, but `type: 'group'`, all three participants (buyer, seller, admin) added (admin is added later by `DisputeService.assignAdmin`).
4. **Support chat**`ChatService.createSupportChat(userId)` (`:41-88`) auto-discovers `User.findOne({ email: 'support@amn.gg' })` and creates a `type: 'support'` chat with a welcome message. Idempotent.
5. **Post-payment auto-chat** — when payment is confirmed, the payment-state cascade ensures a direct chat exists between buyer and winning seller.
5. **Post-payment / purchase-request auto-chat**`POST /api/chat/purchase-request` exists on the backend and creates/links a direct chat for a purchase request. When payment is confirmed, the payment-state cascade ensures a direct chat exists between buyer and winning seller. **No frontend action is wired to `POST /api/chat/purchase-request`** — this direct chat is created server-side.
### Joining the room (real-time)
6. On chat page mount, the frontend emits `socket.emit('join-chat-room', chatId)` (`backend/src/app.ts:130-133`). The socket joins room `chat-{chatId}`.
7. Optionally `socket.emit('user-online', userId)` so other clients see green status (`app.ts:161-169`).
7. **`join-user-room` and `user-online` are SEPARATE events** (do not conflate them):
- `socket.emit('join-user-room', userId)` makes the socket join the personal `user-{userId}` room (so it can receive `chat-notification`).
- `socket.emit('user-online', userId)` broadcasts a `user-status-change` (online) to other clients.
> [!warning] No offline broadcast on disconnect — stale "online" status
> On socket disconnect, **no offline `user-status-change` is emitted**. Other users keep seeing a stale "online" indicator for a peer who has actually left. Document this as a known gap.
### Sending a message
8. User types and hits send. Frontend POSTs `POST /api/chat/:chatId/messages` with `{ content, messageType?, fileUrl?, fileName?, fileSize?, replyTo? }`.
8. User types and hits send. Frontend POSTs `POST /api/chat/:id/messages` with `{ content, messageType?, fileUrl?, fileName?, fileSize?, replyTo? }`. Backend enforces a **5000-character maximum** on `content` at both Mongoose schema and controller validation levels.
9. `ChatService.sendMessage` (`:195-260`):
- Loads chat, verifies the sender is in `participants[]` and `isActive`.
- Builds `Message`: `{ senderId, content, messageType: 'text', fileUrl?, fileName?, fileSize?, replyTo?, timestamp, isRead: false, isEdited: false }`.
@@ -71,20 +80,64 @@ stateDiagram-v2
### Attachments
11. To attach a file, the user picks a file → frontend calls `chatService.uploadChatFile(chatId, file)` (or the equivalent `POST /api/chat/:chatId/upload`) — backend persists the upload via `fileService` (returns `{ fileUrl, fileName, fileSize }`).
12. The send-message call then includes `messageType: 'image' | 'file'` and the file metadata. Files are served from `/uploads`.
11. **File upload endpoint:** the real endpoint is **`POST /api/chat/:id/messages/file`** (multipart/form-data). The flow previously referenced `POST /api/chat/:chatId/upload`, which **does NOT exist**.
> [!bug] ⚠️ KNOWN BUG — file uploads broken
> The frontend `chatService.sendFileMessage` currently POSTs to the **text** message endpoint (`POST /api/chat/:id/messages`) instead of `POST /api/chat/:id/messages/file`. As a result file uploads are broken — they hit the wrong endpoint.
12. When working correctly, the backend handles the multipart payload at `POST /api/chat/:id/messages/file`, persists the upload via `fileService` (returns `{ fileUrl, fileName, fileSize }`), and records the message with `messageType: 'image' | 'file'`.
> [!warning] ⚠️ Security concern — anonymous file access
> Uploaded files are stored under `uploads/chat/` and served with **anonymous access**. Sensitive attachments (KYC docs, dispute evidence) are fetchable by any user who has the URL. Consider signed URLs or per-user authorisation.
### Editing a message
13. Editing a message uses a body of `{ content }` (max 5000 chars). Edits are only allowed within a **15-minute edit window** — edits attempted after that return **400**.
> [!bug] ⚠️ KNOWN BUG — edits fail / are ignored
> The frontend `editMessage` action sends `{ text }`, but the backend expects `{ content }`. The mismatched field name means edits fail or are silently ignored.
### Deleting a message (soft-delete)
14. Message DELETE **soft-deletes**: it sets `deletedAt`, clears the message `content`, and emits **`message-deleted`** to `chat-{chatId}`. The subdocument is not physically removed.
### Read receipts
13. When the user opens a chat, frontend POSTs `POST /api/chat/:chatId/read` (optionally with `messageIds: string[]`).
14. `ChatService.markMessagesAsRead` (`:438-483`):
15. When the user opens a chat, frontend marks messages read via **`PATCH /api/chat/:id/messages/read`** (note: **PATCH**, not POST; there is no `POST /api/chat/:chatId/read`). The body may carry `messageIds: string[]`; if `messageIds` is **empty or omitted, ALL messages are marked read**.
16. `ChatService.markMessagesAsRead` (`:438-483`):
- Calls `chat.markAsRead(userId, messageObjectIds)` (schema method that flips `isRead` on the relevant messages and zeros the user's `unreadCounts` entry).
- Emits **`messages-read`** to `chat-{chatId}` so the sender sees the double-tick.
### Typing indicator
15. On `input` events, frontend emits `socket.emit('typing-start', { chatId, userId, userName })`; on idle/blur emits `typing-stop`.
16. Backend `app.ts:142-158` relays to `chat-{chatId}` as `user-typing` (excluding the sender). No DB persistence.
17. On `input` events, frontend emits `socket.emit('typing-start', { chatId, userId, userName })`; on idle/blur emits `typing-stop`.
18. Backend `app.ts:142-158` relays to `chat-{chatId}` as `user-typing` (excluding the sender). No DB persistence. Limited to **5 typing indicators per 10 seconds**.
### Participants (add / remove / role)
19. **Add a participant** — real endpoint `POST /api/chat/:id/participants` expects a body of **`{ userId }` (a single id)**.
> [!bug] ⚠️ KNOWN BUG — add participant payload mismatch
> The frontend `addParticipants` action sends `{ participants: string[] }` (an array), but the backend expects `{ userId }` (a single id). The shapes do not match.
20. **Remove / leave** — to remove a participant (or have a user leave), use `DELETE /api/chat/:id/participants/:participantId`. Removal is a **soft removal**: the participant subdocument is kept with `isActive=false` and a `leftAt` timestamp.
> [!bug] ⚠️ KNOWN BUG — leave action 404s
> `PUT /chat/:id/leave` **does NOT exist** on the backend. The frontend `leaveConversation` action targets that path and therefore **404s**. Use `DELETE /api/chat/:id/participants/:participantId` instead.
21. **List participants**
> [!bug] ⚠️ KNOWN BUG — getParticipants 404s
> `GET /chat/:id/participants` **does NOT exist** — the backend only exposes `POST` (add) and `DELETE` (remove) on that path. The frontend `getParticipants` action 404s. Participants must be read from **`GET /api/chat/:id/info`** instead.
22. **Change a participant role**
> [!bug] ⚠️ NOT IMPLEMENTED — updateParticipantRole
> `PUT /chat/:id/participants/:participantId` **does NOT exist** on the backend. The frontend `updateParticipantRole` action has no backend counterpart.
### Chat info
23. `getChatInfo``GET /api/chat/:id/info` returns chat details **plus only the first 50 messages** (page 1, limit 50) — **not** the full message history. Use the paginated `GET /api/chat/:id/messages` to load older messages.
## Sequence diagram
@@ -100,22 +153,28 @@ sequenceDiagram
participant IO as Socket.IO
A->>FE_A: Open conversation
FE_A->>BE: POST /api/chat {type:direct, participantIds, relatedTo}
BE->>DB: find-or-create Chat
FE_A->>BE: POST /api/chat {type:direct, participantIds:[sellerId]}
BE->>DB: find-or-create Chat (caller auto-appended)
BE-->>FE_A: { chat }
FE_A->>IO: emit 'join-chat-room' chatId
FE_A->>IO: emit 'join-user-room' userId (separate from user-online)
FE_B->>IO: emit 'join-chat-room' chatId (when B opens too)
A->>FE_A: type & send
FE_A->>BE: POST /api/chat/{id}/messages {content}
FE_A->>BE: POST /api/chat/{id}/messages {content} (max 5000 chars)
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)
BE->>IO: emit user-{B} 'chat-notification' (badge)
A->>FE_A: attach file
FE_A->>BE: POST /api/chat/{id}/messages/file (multipart/form-data)
BE->>DB: chat.addMessage with fileUrl/fileName/fileSize
BE->>IO: emit chat-{id} 'new-message'
B->>FE_B: opens chat
FE_B->>BE: POST /api/chat/{id}/read
FE_B->>BE: PATCH /api/chat/{id}/messages/read (empty messageIds = all)
BE->>DB: chat.markAsRead(B)
BE->>IO: emit chat-{id} 'messages-read'
IO-->>FE_A: 'messages-read' (double-tick)
@@ -128,25 +187,49 @@ sequenceDiagram
| Method | Endpoint | Purpose |
|---|---|---|
| `POST` | `/api/chat` | Find-or-create chat |
| `POST` | `/api/chat` | Find-or-create chat (exactly 1 external `participantId`; caller auto-appended; `relatedTo` NOT accepted) |
| `GET` | `/api/chat` | List user's chats |
| `GET` | `/api/chat/:chatId/messages` | Paginated message history |
| `POST` | `/api/chat/:chatId/messages` | Send message |
| `POST` | `/api/chat/:chatId/upload` | Upload attachment |
| `POST` | `/api/chat/:chatId/read` | Mark read |
| `GET` | `/api/chat/:id/info` | Chat details + first 50 messages (page 1, limit 50) + participants |
| `GET` | `/api/chat/:id/messages` | Paginated message history |
| `POST` | `/api/chat/:id/messages` | Send text message |
| `POST` | `/api/chat/:id/messages/file` | Send file attachment (multipart/form-data) |
| `PATCH` | `/api/chat/:id/messages/read` | Mark read (empty/omitted `messageIds` marks ALL read) |
| `PUT` | `/api/chat/:id/messages/:messageId` | Edit message — body `{ content }`, 15-min edit window |
| `DELETE` | `/api/chat/:id/messages/:messageId` | Soft-delete a message (`deletedAt`, content cleared, emits `message-deleted`) |
| `POST` | `/api/chat/:id/participants` | Add a participant — body `{ userId }` (single) |
| `DELETE` | `/api/chat/:id/participants/:participantId` | Remove / leave (soft: `isActive=false`, `leftAt`) |
| `POST` | `/api/chat/support` | Create/get support chat |
| `POST` | `/api/chat/purchase-request` | Create/link direct chat for a purchase request (no frontend action wired) |
| `PATCH` | `/api/chat/:id/archive` | Toggle archived state (archive **and** unarchive via same endpoint) |
> [!bug] Frontend actions that target non-existent or mismatched backend endpoints
> - `leaveConversation` → `PUT /chat/:id/leave` — **does NOT exist** (404). Use `DELETE /api/chat/:id/participants/:participantId`.
> - `getParticipants` → `GET /chat/:id/participants` — **does NOT exist** (404). Use `GET /api/chat/:id/info`.
> - `updateParticipantRole` → `PUT /chat/:id/participants/:participantId` — **NOT IMPLEMENTED** on backend.
> - `editMessage` → sends `{ text }` but backend expects `{ content }` — edits fail/ignored.
> - `addParticipants` → sends `{ participants: string[] }` but backend expects `{ userId }` (single).
> - `sendFileMessage` → POSTs to the text endpoint instead of `POST /api/chat/:id/messages/file` — file uploads broken.
## Rate limits & constraints
- **Messages:** 20 messages / minute per user per chat.
- **Typing indicators:** 5 / 10 seconds.
- **Message dedup:** 5-minute window (duplicate sends within the window are de-duplicated).
- **Edit window:** 15 minutes — edits after that return **400**.
- **Message length:** 5000-character maximum (schema + controller).
## Database writes
- **`chats`**: insert on create; `$push` into `messages[]`; `$set` `metadata.lastActivity`; `unreadCounts.$.count` increment per recipient; `settings.isArchived` toggled; `participants.$.isActive` flipped on leave.
- **`chats`**: insert on create; `$push` into `messages[]`; `$set` `metadata.lastActivity`; `unreadCounts.$.count` increment per recipient; `settings.isArchived` toggled (archive/unarchive); message soft-delete sets `deletedAt` + clears `content`; participant removal sets `participants.$.isActive=false` + `participants.$.leftAt`.
## Socket events emitted
- **`new-message`** → `chat-{chatId}` (every message).
- **`chat-notification`** → `user-{recipientId}` for non-senders (badge).
- **`messages-read`** → `chat-{chatId}` after read mark.
- **`message-deleted`** → `chat-{chatId}` after a message soft-delete.
- **`user-typing`** → `chat-{chatId}` (relayed by `app.ts`).
- **`user-status-change`** → broadcast when `user-online` is emitted.
- **`user-status-change`** → broadcast when `user-online` is emitted (online only; **no offline broadcast on disconnect**).
- **`new-message`** (system) for system welcome lines on chat creation.
## Side effects
@@ -161,11 +244,13 @@ sequenceDiagram
- **Sender not a participant** → `403 "User is not a participant in this chat"` (`:209-211`).
- **Chat not found** → `404` on `getChatMessages`.
- **Direct duplicate** → idempotent — `createChat` returns existing chat.
- **Empty content** — currently allowed (system messages are typically non-empty though); add a min-length validator if needed.
- **Files served from `/uploads`** — anonymous access is allowed. Sensitive attachments (KYC docs, dispute evidence) should be protected by signed URLs or per-user authorisation; currently any user with the URL can fetch.
- **Content too long** — backend rejects messages exceeding 5000 characters at both Mongoose schema and controller validation levels.
- **Edit after 15 minutes** → `400`.
- **Files served from `uploads/chat/`** — anonymous access is allowed. Sensitive attachments (KYC docs, dispute evidence) should be protected by signed URLs or per-user authorisation; currently any user with the URL can fetch.
- **Stale online status** — no offline broadcast on disconnect; peers may show "online" for a user who has left.
- **Long conversations** — `getChatMessages` slices an in-memory copy of the messages array. For >10k messages this is inefficient. Use aggregation `$slice` or a separate collection.
- **Race on `markAsRead`** — two parallel reads may double-zero the counter, which is harmless.
- **Typing indicator spam** — clients should debounce `typing-start` and emit `typing-stop` on a 2s idle.
- **Typing indicator spam** — clients should debounce `typing-start` and emit `typing-stop` on idle (rate-limited to 5/10s server-side regardless).
> [!warning] Notification message uses placeholder sender name
> `ChatService.sendMessage` posts `chat-notification` with `senderName: "کاربر"` (`:248`) — the literal Persian word for "user". Resolve `senderName` from `participant.userId.firstName` for a better UX.