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:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user