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:
@@ -5,6 +5,7 @@ aliases: [Conversation, IChat, IMessage]
|
|||||||
---
|
---
|
||||||
|
|
||||||
# Chat
|
# Chat
|
||||||
|
> **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))
|
||||||
|
|
||||||
Conversation container with embedded messages. Used for buyer-seller direct chats, group chats, and support tickets. Each chat carries a list of participants, an embedded `messages[]` array (with reactions, replies, edit history), a denormalised `lastMessage` snapshot for list views, and per-user `unreadCounts`. A chat can be linked to any other entity through the `relatedTo` discriminator (currently `PurchaseRequest`, `SellerOffer`, or `Transaction`).
|
Conversation container with embedded messages. Used for buyer-seller direct chats, group chats, and support tickets. Each chat carries a list of participants, an embedded `messages[]` array (with reactions, replies, edit history), a denormalised `lastMessage` snapshot for list views, and per-user `unreadCounts`. A chat can be linked to any other entity through the `relatedTo` discriminator (currently `PurchaseRequest`, `SellerOffer`, or `Transaction`).
|
||||||
|
|
||||||
@@ -16,6 +17,9 @@ Conversation container with embedded messages. Used for buyer-seller direct chat
|
|||||||
> [!warning] Embedded messages
|
> [!warning] Embedded messages
|
||||||
> Messages live inside the chat document. Very long-running chats can grow past the 16 MB document limit. Treat this as a known constraint of the current schema.
|
> Messages live inside the chat document. Very long-running chats can grow past the 16 MB document limit. Treat this as a known constraint of the current schema.
|
||||||
|
|
||||||
|
> [!warning] `relatedTo` is NOT set via `POST /api/chat`
|
||||||
|
> Although `relatedTo` exists in the schema, it is **not accepted** by the `POST /api/chat` create endpoint. Purchase-request linkage is established server-side through the dedicated `POST /api/chat/purchase-request`, not by passing `relatedTo` to the generic create endpoint.
|
||||||
|
|
||||||
## Schema — Chat
|
## Schema — Chat
|
||||||
|
|
||||||
| Field | Type | Required | Default | Validation | Index | Description |
|
| Field | Type | Required | Default | Validation | Index | Description |
|
||||||
@@ -27,10 +31,10 @@ Conversation container with embedded messages. Used for buyer-seller direct chat
|
|||||||
| `participants[].role` | String | no | `member` | enum: `member` / `admin` / `owner` | — | Member role. |
|
| `participants[].role` | String | no | `member` | enum: `member` / `admin` / `owner` | — | Member role. |
|
||||||
| `participants[].joinedAt` | Date | no | `Date.now` | — | — | Join time. |
|
| `participants[].joinedAt` | Date | no | `Date.now` | — | — | Join time. |
|
||||||
| `participants[].lastSeen` | Date | no | — | — | — | Last activity. |
|
| `participants[].lastSeen` | Date | no | — | — | — | Last activity. |
|
||||||
| `participants[].leftAt` | Date | no | — | — | — | If left, when. |
|
| `participants[].leftAt` | Date | no | — | — | — | Set when the participant is removed (soft removal). |
|
||||||
| `participants[].isActive` | Boolean | no | `true` | — | — | Still a participant. |
|
| `participants[].isActive` | Boolean | no | `true` | — | — | Still a participant. Set to `false` on soft removal (subdocument is kept). |
|
||||||
| `messages[]` | Subdocument[] | no | `[]` | — | yes (`messages.timestamp`) | Embedded messages. |
|
| `messages[]` | Subdocument[] | no | `[]` | — | yes (`messages.timestamp`) | Embedded messages. |
|
||||||
| `relatedTo.type` | String | no | — | enum: `PurchaseRequest` / `SellerOffer` / `Transaction` | yes (compound) | Linked entity kind. |
|
| `relatedTo.type` | String | no | — | enum: `PurchaseRequest` / `SellerOffer` / `Transaction` | yes (compound) | Linked entity kind. **Not accepted via `POST /api/chat`** — set only via `POST /api/chat/purchase-request`. |
|
||||||
| `relatedTo.id` | ObjectId | no | — | — | yes (compound) | Linked entity id. |
|
| `relatedTo.id` | ObjectId | no | — | — | yes (compound) | Linked entity id. |
|
||||||
| `lastMessage.content` | String | no | — | — | — | Snapshot for list views. |
|
| `lastMessage.content` | String | no | — | — | — | Snapshot for list views. |
|
||||||
| `lastMessage.senderId` | ObjectId → [[User]] | no | — | — | — | Last sender. |
|
| `lastMessage.senderId` | ObjectId → [[User]] | no | — | — | — | Last sender. |
|
||||||
@@ -50,13 +54,16 @@ Conversation container with embedded messages. Used for buyer-seller direct chat
|
|||||||
> [!note] No top-level `timestamps`
|
> [!note] No top-level `timestamps`
|
||||||
> Unlike most models, this schema does not pass `{ timestamps: true }`. It uses its own `metadata.createdAt` / `metadata.updatedAt` instead, maintained by the pre-save hook.
|
> Unlike most models, this schema does not pass `{ timestamps: true }`. It uses its own `metadata.createdAt` / `metadata.updatedAt` instead, maintained by the pre-save hook.
|
||||||
|
|
||||||
|
> [!note] Soft removal of participants
|
||||||
|
> Removing a participant (via `DELETE /api/chat/:id/participants/:participantId`) does **not** delete the subdocument. It is a soft removal: `isActive` is set to `false` and `leftAt` is timestamped, preserving message attribution and history.
|
||||||
|
|
||||||
## Schema — Message (embedded)
|
## Schema — Message (embedded)
|
||||||
|
|
||||||
| Field | Type | Required | Default | Validation | Description |
|
| Field | Type | Required | Default | Validation | Description |
|
||||||
| --- | --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- | --- |
|
||||||
| `senderId` | ObjectId → [[User]] | yes | — | — | Author. |
|
| `senderId` | ObjectId → [[User]] | yes | — | — | Author. |
|
||||||
| `senderType` | String | no | `User` | — | Currently fixed. |
|
| `senderType` | String | no | `User` | — | Currently fixed. |
|
||||||
| `content` | String | yes | — | maxlength 5000 | Message body. |
|
| `content` | String | yes | — | **maxlength 5000** | Message body. Enforced at both schema and controller. |
|
||||||
| `messageType` | String | no | `text` | enum: `text` / `image` / `file` / `system` | Body kind. |
|
| `messageType` | String | no | `text` | enum: `text` / `image` / `file` / `system` | Body kind. |
|
||||||
| `fileUrl` | String | no | — | — | If file/image. |
|
| `fileUrl` | String | no | — | — | If file/image. |
|
||||||
| `fileName` | String | no | — | — | Original filename. |
|
| `fileName` | String | no | — | — | Original filename. |
|
||||||
@@ -65,10 +72,14 @@ Conversation container with embedded messages. Used for buyer-seller direct chat
|
|||||||
| `isRead` | Boolean | no | `false` | — | Read flag. |
|
| `isRead` | Boolean | no | `false` | — | Read flag. |
|
||||||
| `isEdited` | Boolean | no | `false` | — | Edited flag. |
|
| `isEdited` | Boolean | no | `false` | — | Edited flag. |
|
||||||
| `editedAt` | Date | no | — | — | When edited. |
|
| `editedAt` | Date | no | — | — | When edited. |
|
||||||
|
| `deletedAt` | Date | no | — | — | Set on soft-delete; `content` is cleared but the subdocument is kept. |
|
||||||
| `replyTo` | ObjectId | no | — | — | Reply target message id. |
|
| `replyTo` | ObjectId | no | — | — | Reply target message id. |
|
||||||
| `reactions[].userId` | ObjectId → [[User]] | no | — | — | Reacting user. |
|
| `reactions[].userId` | ObjectId → [[User]] | no | — | — | Reacting user. |
|
||||||
| `reactions[].reaction` | String | no | — | maxlength 10 | Emoji. |
|
| `reactions[].reaction` | String | no | — | maxlength 10 | Emoji. |
|
||||||
|
|
||||||
|
> [!note] Messages are soft-deleted
|
||||||
|
> Deleting a message sets `deletedAt` and clears `content` (the body becomes empty). The message subdocument is **not** physically removed from `messages[]`, and a `message-deleted` socket event is emitted.
|
||||||
|
|
||||||
## Virtuals
|
## Virtuals
|
||||||
|
|
||||||
| Virtual | Returns | Definition |
|
| Virtual | Returns | Definition |
|
||||||
@@ -97,7 +108,7 @@ Defined at `backend/src/models/Chat.ts:243-247`:
|
|||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `getUnreadCount(userId: Types.ObjectId): number` | Returns the unread counter for a participant. `backend/src/models/Chat.ts:264` |
|
| `getUnreadCount(userId: Types.ObjectId): number` | Returns the unread counter for a participant. `backend/src/models/Chat.ts:264` |
|
||||||
| `addMessage(messageData: Partial<IMessage>): IMessage` | Pushes a message, updates `lastMessage`, increments unread counters for everyone except the sender, and bumps `lastActivity`. `backend/src/models/Chat.ts:270` |
|
| `addMessage(messageData: Partial<IMessage>): IMessage` | Pushes a message, updates `lastMessage`, increments unread counters for everyone except the sender, and bumps `lastActivity`. `backend/src/models/Chat.ts:270` |
|
||||||
| `markAsRead(userId, messageIds?: Types.ObjectId[]): void` | Marks listed messages (or all) as read for the user, zeros their unread counter, and updates `lastSeen`. `backend/src/models/Chat.ts:308` |
|
| `markAsRead(userId, messageIds?: Types.ObjectId[]): void` | Marks listed messages (or all when `messageIds` is empty/omitted) as read for the user, zeros their unread counter, and updates `lastSeen`. `backend/src/models/Chat.ts:308` |
|
||||||
|
|
||||||
## Static Methods
|
## Static Methods
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ aliases: [User Notification, INotification]
|
|||||||
|
|
||||||
# Notification
|
# Notification
|
||||||
|
|
||||||
Per-user notification entry. Each row binds to one `userId` (stored as a string rather than ObjectId), carries a typed severity (`info` / `success` / `warning` / `error`) and a domain category, optionally references another entity via `relatedId`, and supports an `actionUrl` for deep-linking. Old notifications are auto-purged by a 90-day TTL index.
|
> **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))
|
||||||
|
|
||||||
|
Per-user notification entry. Each row binds to one `userId` (stored as a string rather than ObjectId), carries a typed severity (`info` / `success` / `warning` / `error`) and a domain category, optionally references another entity via `relatedId`, and supports an `actionUrl` for deep-linking. Old notifications are auto-purged by a 90-day TTL index (`createdAt` with `expireAfterSeconds = 7,776,000`).
|
||||||
|
|
||||||
> [!note] Source
|
> [!note] Source
|
||||||
> `backend/src/models/Notification.ts:18` — schema definition
|
> `backend/src/models/Notification.ts:18` — schema definition
|
||||||
@@ -15,6 +17,12 @@ Per-user notification entry. Each row binds to one `userId` (stored as a string
|
|||||||
> [!warning] String userId
|
> [!warning] String userId
|
||||||
> `userId` is a String, not an ObjectId, and is not declared with `ref`. Consumers must cast to `ObjectId` if they want to `populate()` it as a [[User]].
|
> `userId` is a String, not an ObjectId, and is not declared with `ref`. Consumers must cast to `ObjectId` if they want to `populate()` it as a [[User]].
|
||||||
|
|
||||||
|
> [!warning] `category` enum vs reality
|
||||||
|
> The schema enum is `purchase_request` / `offer` / `payment` / `delivery` / `system`, but in practice:
|
||||||
|
> - `notificationController.createNotification` defaults the category to **`'general'`** (`category = 'general'`) when the caller omits it. `'general'` is **not** in the schema enum — Mongoose enum validation will reject it on a strict save, so callers must supply a valid value or the write fails. Treat `'general'` as a value you may encounter in payloads even though it is not an enum member.
|
||||||
|
> - The frontend socket hook `use-notifications.ts` hardcodes `category: 'system'` for every realtime-injected notification, so most client-side notifications surface as `'system'` regardless of their true domain.
|
||||||
|
> - `NotificationService.notifyRequestStatusChanged` always writes `category: 'system'` for purchase-request status changes.
|
||||||
|
|
||||||
## Schema
|
## Schema
|
||||||
|
|
||||||
| Field | Type | Required | Default | Validation | Index | Description |
|
| Field | Type | Required | Default | Validation | Index | Description |
|
||||||
@@ -23,13 +31,13 @@ Per-user notification entry. Each row binds to one `userId` (stored as a string
|
|||||||
| `title` | String | yes | — | maxlength 200 | — | Headline. |
|
| `title` | String | yes | — | maxlength 200 | — | Headline. |
|
||||||
| `message` | String | yes | — | maxlength 1000 | — | Body. |
|
| `message` | String | yes | — | maxlength 1000 | — | Body. |
|
||||||
| `type` | String | yes | `info` | enum: `info` / `success` / `warning` / `error` | — | Severity. |
|
| `type` | String | yes | `info` | enum: `info` / `success` / `warning` / `error` | — | Severity. |
|
||||||
| `category` | String | yes | — | enum: `purchase_request` / `offer` / `payment` / `delivery` / `system` | yes (compound) | Domain bucket. |
|
| `category` | String | yes | — | enum: `purchase_request` / `offer` / `payment` / `delivery` / `system` | yes (compound) | Domain bucket. ⚠️ `notificationController` defaults to `'general'` (not in the enum) and the realtime socket hook + `notifyRequestStatusChanged` hardcode `'system'`. See warning above. |
|
||||||
| `relatedId` | String | no | — | — | yes | Id of the related entity (e.g. [[PurchaseRequest]]). |
|
| `relatedId` | String | no | — | — | yes | Id of the related entity (e.g. [[PurchaseRequest]]). |
|
||||||
| `metadata` | Mixed | no | — | — | — | Arbitrary payload. |
|
| `metadata` | Mixed | no | — | — | — | Arbitrary payload. |
|
||||||
| `actionUrl` | String | no | — | maxlength 500 | — | Deep link. |
|
| `actionUrl` | String | no | — | maxlength 500 | — | Deep link. |
|
||||||
| `isRead` | Boolean | no | `false` | — | yes (compound) | Read flag. |
|
| `isRead` | Boolean | no | `false` | — | yes (compound) | Read flag. |
|
||||||
| `readAt` | Date | no | — | — | — | When read. |
|
| `readAt` | Date | no | — | — | — | When read. |
|
||||||
| `createdAt` | Date | auto | — | — | yes (compound + TTL) | Mongoose timestamp. |
|
| `createdAt` | Date | auto | — | — | yes (compound + TTL) | Mongoose timestamp. Auto-deleted after 90 days by TTL index. |
|
||||||
| `updatedAt` | Date | auto | — | — | — | Mongoose timestamp. |
|
| `updatedAt` | Date | auto | — | — | — | Mongoose timestamp. |
|
||||||
|
|
||||||
The collection name is overridden to `notifications` via `collection: 'notifications'`.
|
The collection name is overridden to `notifications` via `collection: 'notifications'`.
|
||||||
@@ -46,7 +54,7 @@ Defined at `backend/src/models/Notification.ts:71-77`:
|
|||||||
- `{ userId: 1, isRead: 1 }` — unread badge.
|
- `{ userId: 1, isRead: 1 }` — unread badge.
|
||||||
- `{ userId: 1, category: 1 }` — category filter.
|
- `{ userId: 1, category: 1 }` — category filter.
|
||||||
- `{ relatedId: 1 }` — lookup by linked entity.
|
- `{ relatedId: 1 }` — lookup by linked entity.
|
||||||
- `{ createdAt: 1 }` with `expireAfterSeconds: 60 * 60 * 24 * 90` — auto-delete after 90 days.
|
- `{ createdAt: 1 }` with `expireAfterSeconds: 60 * 60 * 24 * 90` (7,776,000 s) — MongoDB TTL index; the database hard-deletes documents automatically after 90 days.
|
||||||
|
|
||||||
Plus the implicit index from `userId` having `index: true` at the field level.
|
Plus the implicit index from `userId` having `index: true` at the field level.
|
||||||
|
|
||||||
@@ -62,6 +70,13 @@ None defined.
|
|||||||
|
|
||||||
None defined.
|
None defined.
|
||||||
|
|
||||||
|
## Status-change notification coverage
|
||||||
|
|
||||||
|
`NotificationService.notifyRequestStatusChanged` maps a [[PurchaseRequest]] status to a human label via an internal `statusMessages` table. That table covers `pending`, `active`, `received_offers`, `in_negotiation`, `payment`, `processing`, `delivery`, `delivered`, `confirming`, `completed`, and `cancelled`.
|
||||||
|
|
||||||
|
> [!warning] Missing status templates
|
||||||
|
> The `pending_payment` and `seller_paid` [[PurchaseRequest]] statuses have **no entry** in the `statusMessages` table and no dedicated notification template. Transitions into these states do not produce a meaningful status-change notification (the label falls back to the raw status string, and several flows skip notification entirely). If you rely on notifications for `pending_payment` / `seller_paid`, they will not arrive as expected.
|
||||||
|
|
||||||
## Relationships
|
## Relationships
|
||||||
|
|
||||||
- **References**: [[User]] indirectly through `userId` (string); arbitrary entity via `relatedId`.
|
- **References**: [[User]] indirectly through `userId` (string); arbitrary entity via `relatedId`.
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ aliases: [Payment Record, Escrow, IPayment]
|
|||||||
|
|
||||||
# Payment
|
# Payment
|
||||||
|
|
||||||
|
> **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))
|
||||||
|
|
||||||
Records every monetary movement in the marketplace: buyer pay-ins, seller payouts, and refunds. The current model is centered on Request Network pay-in, in-house checkout metadata, on-chain transaction verification, escrow state, and provider request IDs. The `provider` and `direction` discriminators let one collection hold incoming buyer payments, outgoing seller releases, refunds, and legacy/other provider records.
|
Records every monetary movement in the marketplace: buyer pay-ins, seller payouts, and refunds. The current model is centered on Request Network pay-in, in-house checkout metadata, on-chain transaction verification, escrow state, and provider request IDs. The `provider` and `direction` discriminators let one collection hold incoming buyer payments, outgoing seller releases, refunds, and legacy/other provider records.
|
||||||
|
|
||||||
> [!note] Source
|
> [!note] Source
|
||||||
@@ -15,6 +17,22 @@ Records every monetary movement in the marketplace: buyer pay-ins, seller payout
|
|||||||
> [!warning] Mixed types
|
> [!warning] Mixed types
|
||||||
> `purchaseRequestId`, `sellerOfferId`, and `sellerId` use `Schema.Types.Mixed`. They are usually `ObjectId`s, but the template-checkout flow passes string ids that do not yet exist in the database, so the schema accepts both.
|
> `purchaseRequestId`, `sellerOfferId`, and `sellerId` use `Schema.Types.Mixed`. They are usually `ObjectId`s, but the template-checkout flow passes string ids that do not yet exist in the database, so the schema accepts both.
|
||||||
|
|
||||||
|
> [!warning] `provider` values (schema enum vs reality)
|
||||||
|
> The declared schema enum for `provider` is only `['request.network', 'other']`, yet production code writes additional values. The full set of providers that actually appear is: `request.network`, `shkeeper`, `decentralized`, `test`, `other`.
|
||||||
|
> - `paymentCoordinator.ts` and `RequestTemplateService.ts` create `Payment` docs with `provider: 'shkeeper'`.
|
||||||
|
> - The decentralized/on-chain flow uses `decentralized`.
|
||||||
|
> - ⚠️ **Frontend type bug:** the frontend `PaymentProvider` TypeScript type (`frontend/src/types/payment.ts`) is `'request.network' | 'test' | 'other'` — it is **missing `shkeeper` and `decentralized`**, so the client cannot represent payments created by those providers.
|
||||||
|
|
||||||
|
> [!warning] `confirmed` vs `completed` — stats undercount
|
||||||
|
> Payment stats (`paymentService.getPaymentStats`) only increment `successfulPayments` for status **`confirmed`**:
|
||||||
|
> ```ts
|
||||||
|
> case "confirmed": stats.successfulPayments += stat.count; break;
|
||||||
|
> ```
|
||||||
|
> The terminal SHKeeper / DePay state is **`completed`**, which has no case in the switch and is therefore **not** counted as a successful payment. ⚠️ This causes successful-payment stats to undercount any payment that reached `completed`.
|
||||||
|
|
||||||
|
> [!warning] `SIM_` payment-hash bypass — security concern
|
||||||
|
> In both `payment/paymentRoutes.ts` and `marketplace/routes.ts`, a `paymentHash` that starts with `SIM_` (or a short `0x...` hash under 64 chars) is treated as a simulated transaction and **skips on-chain verification entirely** (`isVerified = true`). There is **no environment guard** (e.g. no `NODE_ENV !== 'production'` check) around this branch, so the bypass is reachable in production. ⚠️ A caller can mark a payment verified without any real on-chain settlement.
|
||||||
|
|
||||||
## Schema
|
## Schema
|
||||||
|
|
||||||
| Field | Type | Required | Default | Validation | Index | Description |
|
| Field | Type | Required | Default | Validation | Index | Description |
|
||||||
@@ -25,7 +43,7 @@ Records every monetary movement in the marketplace: buyer pay-ins, seller payout
|
|||||||
| `sellerId` | Mixed (ObjectId or String) | yes | — | — | yes (compound) | Seller receiving (or template seller). |
|
| `sellerId` | Mixed (ObjectId or String) | yes | — | — | yes (compound) | Seller receiving (or template seller). |
|
||||||
| `amount.amount` | Number | yes | — | — | — | Numeric amount. |
|
| `amount.amount` | Number | yes | — | — | — | Numeric amount. |
|
||||||
| `amount.currency` | String | yes | `USDT` | — | — | Settlement currency. |
|
| `amount.currency` | String | yes | `USDT` | — | — | Settlement currency. |
|
||||||
| `provider` | String | no | `request.network` | enum: `request.network` / `other` | yes (compound, partial) | Payment processor. |
|
| `provider` | String | no | `request.network` | enum (declared): `request.network` / `other`. Values written in practice: `request.network`, `shkeeper`, `decentralized`, `request.network`, `test`, `other` | yes (compound, partial) | Payment processor. ⚠️ See provider note below — code writes `shkeeper` and `decentralized` even though they are not in the declared schema enum, and the frontend `PaymentProvider` type is missing both. |
|
||||||
| `direction` | String | no | `in` | enum: `in` / `out` / `refund` | yes (compound, partial) | Flow direction. |
|
| `direction` | String | no | `in` | enum: `in` / `out` / `refund` | yes (compound, partial) | Flow direction. |
|
||||||
| `blockchain.network` | String | no | — | — | — | Network identifier. |
|
| `blockchain.network` | String | no | — | — | — | Network identifier. |
|
||||||
| `blockchain.transactionHash` | String | no | — | — | yes (sparse) | On-chain tx hash. |
|
| `blockchain.transactionHash` | String | no | — | — | yes (sparse) | On-chain tx hash. |
|
||||||
@@ -35,8 +53,8 @@ Records every monetary movement in the marketplace: buyer pay-ins, seller payout
|
|||||||
| `blockchain.receiver` | String | no | — | — | — | Destination address. |
|
| `blockchain.receiver` | String | no | — | — | — | Destination address. |
|
||||||
| `blockchain.confirmedAt` | Date | no | — | — | — | When tx confirmed. |
|
| `blockchain.confirmedAt` | Date | no | — | — | — | When tx confirmed. |
|
||||||
| `blockchain.confirmations` | Number | no | `0` | — | — | Confirmation count. |
|
| `blockchain.confirmations` | Number | no | `0` | — | — | Confirmation count. |
|
||||||
| `status` | String | no | `pending` | enum: `pending` / `processing` / `confirmed` / `completed` / `failed` / `cancelled` / `refunded` | yes (compound) | Lifecycle status. |
|
| `status` | String | no | `pending` | enum: `pending` / `processing` / `confirmed` / `completed` / `failed` / `cancelled` / `refunded` | yes (compound) | Lifecycle status. ⚠️ `confirmed` vs `completed`: only `confirmed` is counted as a successful payment in stats. See status note below. |
|
||||||
| `escrowState` | String | no | — | enum: `funded` / `releasable` / `released` / `refunded` / `releasing` / `failed` / `cancelled` / `partial` | — | Escrow lifecycle. |
|
| `escrowState` | String | no | — | enum: `funded` / `releasable` / `released` / `refunded` / `releasing` / `failed` / `cancelled` / `partial` | — | Escrow lifecycle. Note the intermediate states `releasable` (delivery confirmed, ready to pay out) and `releasing` (payout in flight) between `funded` and `released`. |
|
||||||
| `providerPaymentId` | String | no | — | — | yes (sparse) | External provider id for idempotency. |
|
| `providerPaymentId` | String | no | — | — | yes (sparse) | External provider id for idempotency. |
|
||||||
| `metadata.userAgent` | String | no | — | — | — | Browser UA. |
|
| `metadata.userAgent` | String | no | — | — | — | Browser UA. |
|
||||||
| `metadata.ipAddress` | String | no | — | — | — | Client IP. |
|
| `metadata.ipAddress` | String | no | — | — | — | Client IP. |
|
||||||
|
|||||||
@@ -4,9 +4,19 @@ tags: [data-model, mongoose]
|
|||||||
aliases: [Point Ledger, Loyalty Transaction, IPointTransaction]
|
aliases: [Point Ledger, Loyalty Transaction, IPointTransaction]
|
||||||
---
|
---
|
||||||
|
|
||||||
|
> **Last updated:** 2026-05-29 — aligned with code (see Doc vs Code Audit Report)
|
||||||
|
|
||||||
# PointTransaction
|
# PointTransaction
|
||||||
|
|
||||||
Append-only ledger of loyalty point movements. Each row represents one earn / spend / expire event for a user, with a source attribution (`purchase` / `referral` / `bonus` / `admin` / `redemption`), the amount moved, and the resulting balance snapshot. Metadata is flexible to support different sources (order amount, commission, level changes, referenced [[PurchaseRequest]]).
|
> **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))
|
||||||
|
|
||||||
|
Append-only ledger of loyalty point movements. Each row represents one `earn` / `spend` / `expire` event for a user, with a source attribution (`purchase` / `referral` / `bonus` / `admin` / `redemption`), the amount moved, and the resulting balance snapshot. Metadata is flexible to support different sources (order amount, commission, level changes, referenced [[PurchaseRequest]]).
|
||||||
|
|
||||||
|
> [!warning] `type` enum is `earn` / `spend` / `expire` ONLY
|
||||||
|
> There is **no `refund` type** (nor any other value). The `enum` at `PointTransaction.ts:35` is exactly `['earn', 'spend', 'expire']`. Referral earns are identified by `source: 'referral'` + `type: 'earn'`, **not** by a dedicated type.
|
||||||
|
|
||||||
|
> [!danger] `expire` is defined but never produced
|
||||||
|
> The `expiresAt` field and the `'expire'` type exist in the schema, and there is a sparse `{ expiresAt: 1 }` index intended for expiry sweeps — but **no service, cron job, or TTL ever creates an `expire`-type transaction**. Point expiry is **not enforced** anywhere in the codebase today; points effectively never expire.
|
||||||
|
|
||||||
> [!note] Source
|
> [!note] Source
|
||||||
> `backend/src/models/PointTransaction.ts:25` — schema definition
|
> `backend/src/models/PointTransaction.ts:25` — schema definition
|
||||||
@@ -18,7 +28,7 @@ Append-only ledger of loyalty point movements. Each row represents one earn / sp
|
|||||||
| --- | --- | --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- | --- | --- |
|
||||||
| `user` | ObjectId → [[User]] | yes | — | — | yes (single + compound) | Owner of the transaction. |
|
| `user` | ObjectId → [[User]] | yes | — | — | yes (single + compound) | Owner of the transaction. |
|
||||||
| `type` | String | yes | — | enum: `earn` / `spend` / `expire` | yes (compound) | Movement direction. |
|
| `type` | String | yes | — | enum: `earn` / `spend` / `expire` | yes (compound) | Movement direction. |
|
||||||
| `source` | String | yes | — | enum: `purchase` / `referral` / `bonus` / `admin` / `redemption` | yes (compound) | Source bucket. |
|
| `source` | String | yes | — | enum: `purchase` / `referral` / `bonus` / `admin` / `redemption` | yes (compound) | Source bucket. **Referral earns are identified by `source='referral'` (with `type='earn'`), not by type.** Redemptions use `source='redemption'`; admin grants use `source='admin'`. |
|
||||||
| `amount` | Number | yes | — | — | — | Points moved (positive integer; semantics by `type`). |
|
| `amount` | Number | yes | — | — | — | Points moved (positive integer; semantics by `type`). |
|
||||||
| `balance` | Number | yes | — | — | — | Available balance after the move. |
|
| `balance` | Number | yes | — | — | — | Available balance after the move. |
|
||||||
| `order` | ObjectId → Order | no | — | — | — | Linked order id (legacy ref, see warning). |
|
| `order` | ObjectId → Order | no | — | — | — | Linked order id (legacy ref, see warning). |
|
||||||
@@ -67,7 +77,7 @@ None defined.
|
|||||||
|
|
||||||
## State Transitions
|
## State Transitions
|
||||||
|
|
||||||
No status field — entries are immutable once written. A consumer scans for `expiresAt < now` to create offsetting `type: 'expire'` rows.
|
No status field — entries are immutable once written. The schema anticipates a consumer scanning for `expiresAt < now` to create offsetting `type: 'expire'` rows, but **no such consumer exists**: nothing in the codebase ever writes an `expire` row, so in practice only `earn` and `spend` entries are ever created.
|
||||||
|
|
||||||
## Common Queries
|
## Common Queries
|
||||||
|
|
||||||
|
|||||||
@@ -6,12 +6,20 @@ aliases: [User Model, IUser, Account]
|
|||||||
|
|
||||||
# User
|
# User
|
||||||
|
|
||||||
|
> **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))
|
||||||
|
|
||||||
The core identity document for every actor in the marketplace: buyers, sellers, and admins. Stores credentials (password + WebAuthn passkeys), profile/preference data, referral bookkeeping, point balances, and a soft-delete status flag. Almost every other model carries an `ObjectId` reference back to `User`, so this collection is the relational hub of the system.
|
The core identity document for every actor in the marketplace: buyers, sellers, and admins. Stores credentials (password + WebAuthn passkeys), profile/preference data, referral bookkeeping, point balances, and a soft-delete status flag. Almost every other model carries an `ObjectId` reference back to `User`, so this collection is the relational hub of the system.
|
||||||
|
|
||||||
> [!note] Source
|
> [!note] Source
|
||||||
> `backend/src/models/User.ts:70` — schema definition
|
> `backend/src/models/User.ts:70` — schema definition
|
||||||
> `backend/src/models/User.ts:257` — model export
|
> `backend/src/models/User.ts:257` — model export
|
||||||
|
|
||||||
|
> [!note] Email change re-verification
|
||||||
|
> When a profile update (`PUT /api/user/profile`, `userController.updateUserProfile`) changes `email` to a new value, the controller sets `isEmailVerified = false`, generates a **6-digit** `emailVerificationCode` (valid 15 minutes), stores it on `emailVerificationCode` / `emailVerificationCodeExpires`, and emails the code to the new address. The user must then confirm via `POST /api/user/profile/email/verify` (or request a new code with `POST /api/user/profile/email/resend-verification`).
|
||||||
|
|
||||||
|
> [!note] Wallet ownership proof
|
||||||
|
> `PATCH /api/user/wallet-address` accepts both EVM and TON wallets. EVM addresses require an EIP-191 signature (`ethers.verifyMessage`); TON addresses are format-validated and may include an optional TonProof. A successful proof sets `profile.walletProofVerified = true` and `profile.walletProofTimestamp`.
|
||||||
|
|
||||||
## Schema
|
## Schema
|
||||||
|
|
||||||
| Field | Type | Required | Default | Validation | Index | Description |
|
| Field | Type | Required | Default | Validation | Index | Description |
|
||||||
@@ -21,7 +29,7 @@ The core identity document for every actor in the marketplace: buyers, sellers,
|
|||||||
| `firstName` | String | no | `"کاربر"` | trim | — | Persian default ("user"). |
|
| `firstName` | String | no | `"کاربر"` | trim | — | Persian default ("user"). |
|
||||||
| `lastName` | String | no | `"جدید"` | trim | — | Persian default ("new"). |
|
| `lastName` | String | no | `"جدید"` | trim | — | Persian default ("new"). |
|
||||||
| `role` | String | yes | `"buyer"` | enum: `admin` / `buyer` / `seller` | yes | Authorisation tier. |
|
| `role` | String | yes | `"buyer"` | enum: `admin` / `buyer` / `seller` | yes | Authorisation tier. |
|
||||||
| `isEmailVerified` | Boolean | no | `false` | — | — | Set to true after [[TempVerification]] is consumed. |
|
| `isEmailVerified` | Boolean | no | `false` | — | — | Set to true after the email verification code is consumed. ⚠️ Changing the email via `PUT /api/user/profile` **resets this to `false`** and dispatches a fresh **6-digit** verification code to the new address (see Email verification note below). |
|
||||||
| `authProvider` | String | yes | `"email"` | enum: `email` / `google` / `telegram` | yes | Provider used to create the account. Existing email/password accounts remain `email`; Telegram-only users are `telegram`. |
|
| `authProvider` | String | yes | `"email"` | enum: `email` / `google` / `telegram` | yes | Provider used to create the account. Existing email/password accounts remain `email`; Telegram-only users are `telegram`. |
|
||||||
| `telegramVerified` | Boolean | no | `false` | — | — | Set when Telegram identity has been signature-verified and linked through `TelegramLink`. |
|
| `telegramVerified` | Boolean | no | `false` | — | — | Set when Telegram identity has been signature-verified and linked through `TelegramLink`. |
|
||||||
| `emailVerificationToken` | String | no | — | — | — | Legacy token-based email verification. |
|
| `emailVerificationToken` | String | no | — | — | — | Legacy token-based email verification. |
|
||||||
@@ -48,7 +56,11 @@ The core identity document for every actor in the marketplace: buyers, sellers,
|
|||||||
| `profile.address.country` | String | no | — | — | — | — |
|
| `profile.address.country` | String | no | — | — | — | — |
|
||||||
| `profile.bio` | String | no | — | — | — | Free-form bio. |
|
| `profile.bio` | String | no | — | — | — | Free-form bio. |
|
||||||
| `profile.website` | String | no | — | — | — | Personal website URL. |
|
| `profile.website` | String | no | — | — | — | Personal website URL. |
|
||||||
| `profile.walletAddress` | String | no | — | — | — | On-chain wallet address. |
|
| `profile.walletAddress` | String | no | — | — | — | On-chain wallet address (EVM `0x…` or TON). Set via `PATCH /api/user/wallet-address`. |
|
||||||
|
| `profile.walletType` | String | no | — | enum: `evm` / `ton` | — | Which chain family the stored `walletAddress` belongs to. |
|
||||||
|
| `profile.walletProvider` | String | no | — | — | — | Wallet provider label (e.g. `evm`, `telegram-wallet`). Defaults to `telegram-wallet` for TON, `evm` otherwise. |
|
||||||
|
| `profile.walletProofVerified` | Boolean | no | — | — | — | True when ownership was proven — EIP-191 signature for EVM, or a verified TonProof for TON. |
|
||||||
|
| `profile.walletProofTimestamp` | Date | no | — | — | — | When the wallet proof was last verified (only set when `walletProofVerified` is true). |
|
||||||
| `profile.isPublic` | Boolean | no | `false` | — | — | Whether the profile is publicly visible. |
|
| `profile.isPublic` | Boolean | no | `false` | — | — | Whether the profile is publicly visible. |
|
||||||
| `preferences.language` | String | no | `"en"` | — | — | UI language. |
|
| `preferences.language` | String | no | `"en"` | — | — | UI language. |
|
||||||
| `preferences.currency` | String | no | `"USD"` | — | — | Display currency. |
|
| `preferences.currency` | String | no | `"USD"` | — | — | Display currency. |
|
||||||
@@ -57,7 +69,7 @@ The core identity document for every actor in the marketplace: buyers, sellers,
|
|||||||
| `preferences.notifications.push` | Boolean | no | `true` | — | — | Opt-in for push notifications. |
|
| `preferences.notifications.push` | Boolean | no | `true` | — | — | Opt-in for push notifications. |
|
||||||
| `status` | String | no | `"active"` | enum: `active` / `suspended` / `deleted` | yes | Soft-delete and moderation flag. |
|
| `status` | String | no | `"active"` | enum: `active` / `suspended` / `deleted` | yes | Soft-delete and moderation flag. |
|
||||||
| `lastLoginAt` | Date | no | — | — | — | Updated by auth middleware. |
|
| `lastLoginAt` | Date | no | — | — | — | Updated by auth middleware. |
|
||||||
| `refreshTokens[]` | String[] | no | `[]` | — | — | Outstanding JWT refresh tokens. |
|
| `refreshTokens[]` | String[] | no | `[]` | — | — | Array of currently active JWT refresh tokens. ⚠️ Reset to `[]` on password change and on password reset, which invalidates every outstanding session and forces re-login everywhere. |
|
||||||
| `referralCode` | String | no | — | — | unique, sparse | **Not yet implemented** in `User.ts` — planned for referral programme. |
|
| `referralCode` | String | no | — | — | unique, sparse | **Not yet implemented** in `User.ts` — planned for referral programme. |
|
||||||
| `referredBy` | ObjectId → User | no | — | — | yes | **Not yet implemented** in `User.ts` — planned for referral programme. |
|
| `referredBy` | ObjectId → User | no | — | — | yes | **Not yet implemented** in `User.ts` — planned for referral programme. |
|
||||||
| `points.total` | Number | no | `0` | — | — | **Not yet implemented** in `User.ts` — planned for loyalty system. |
|
| `points.total` | Number | no | `0` | — | — | **Not yet implemented** in `User.ts` — planned for loyalty system. |
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ tags: [api, admin, reference]
|
|||||||
|
|
||||||
# Admin API
|
# Admin API
|
||||||
|
|
||||||
> **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))
|
> **Last updated:** 2026-05-29 — aligned with code (see Doc vs Code Audit Report)
|
||||||
|
|
||||||
There is no single `/api/admin` namespace — admin-only endpoints are scattered across the service routers. This page catalogs them in one place. All require `Bearer JWT` with `req.user.role === 'admin'`. The two enforcement patterns are:
|
There is no single `/api/admin` namespace — admin-only endpoints are scattered across the service routers. This page catalogs them in one place. All require `Bearer JWT` with `req.user.role === 'admin'` unless explicitly noted otherwise. The two enforcement patterns are:
|
||||||
|
|
||||||
- Middleware: `authorizeRoles('admin')` after `authenticateToken` (used by the dispute, data-cleanup, blog routers).
|
- Middleware: `authorizeRoles('admin')` after `authenticateToken` (used by the dispute, data-cleanup, blog routers).
|
||||||
- Inline check inside the handler: `if (req.user.role !== 'admin') return 403` (used by user, points, payment routes).
|
- Inline check inside the handler: `if (req.user.role !== 'admin') return 403` (used by user, points, payment routes).
|
||||||
@@ -16,27 +16,31 @@ There is no single `/api/admin` namespace — admin-only endpoints are scattered
|
|||||||
|
|
||||||
See full descriptions in [[User API]].
|
See full descriptions in [[User API]].
|
||||||
|
|
||||||
|
> **Path note:** The frontend and backend both use `/api/users/admin/*` (plural). The singular `/api/user/admin/*` paths for create/delete/status/role/list are **unreachable** — they are not mounted in the backend. Use `/api/users/admin/*` for all user-management calls.
|
||||||
|
|
||||||
| Endpoint | Action |
|
| Endpoint | Action |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `POST /api/user/admin/create` | Create user with role/status |
|
| `POST /api/users/admin/create` | Create user with role/status |
|
||||||
| `DELETE /api/user/admin/:userId` | Soft delete user — sets `status='deleted'` (admins cannot delete each other) |
|
| `DELETE /api/users/admin/:userId` | Soft delete user — sets `status='deleted'` (admins cannot delete each other) |
|
||||||
| `PATCH /api/user/admin/:userId/status` | Activate / suspend |
|
| `PATCH /api/users/admin/:userId/status` | Activate / suspend |
|
||||||
| `PATCH /api/user/admin/:userId/toggle-status` | Flip active flag |
|
| `PATCH /api/users/admin/:userId/toggle-status` | Flip active flag |
|
||||||
| `PATCH /api/user/admin/:userId/role` | Change role |
|
| `PATCH /api/users/admin/:userId/role` | Change role |
|
||||||
| `GET /api/user/admin/list` | Paginated directory + stats |
|
| `GET /api/users/admin/list` | Paginated directory + stats |
|
||||||
| `GET /api/user/admin/:userId/dependencies` | Pre-delete dependency check |
|
| `GET /api/users/admin/:userId/dependencies` | Pre-delete dependency check |
|
||||||
| `GET /api/users/admin/stats` | Aggregate user analytics |
|
| `GET /api/users/admin/stats` | Aggregate user analytics |
|
||||||
| `GET /api/users/admin/:userId` | Full user detail (admin view) |
|
| `GET /api/users/admin/:userId` | Full user detail (admin view) |
|
||||||
| `PUT /api/users/admin/:userId` | Mass update user |
|
| `PUT /api/users/admin/:userId` | Mass update user |
|
||||||
| `PUT /api/users/admin/update/:email` | Mass update by email |
|
| `PUT /api/users/admin/update/:email` | Mass update by email |
|
||||||
| `PATCH /api/users/admin/:userId/password` | Force password reset (wipes refresh tokens) |
|
| `PATCH /api/users/admin/:userId/password` | Force password reset (wipes refresh tokens) |
|
||||||
| `POST /api/users/admin/:userId/resend-verification` | Resend verification email |
|
| `POST /api/users/admin/:userId/resend-verification` | Resend verification email (legacy route — uses 8-digit codes) |
|
||||||
|
|
||||||
|
> **Verification code length:** The endpoint `POST /api/users/admin/:userId/resend-verification` is served by the legacy userRoutes and generates **8-digit** codes. The new userController generates 6-digit codes and is reached via a different path. Both coexist; the legacy route takes precedence for this path.
|
||||||
|
|
||||||
**⚠️ KNOWN BUG — HTTP verb mismatch (status/role updates):** The frontend Redux actions for `updateUserStatus` and `updateUserRole` send `PUT` requests, but the backend registers these handlers under `PATCH`. These calls will receive `404 Method Not Found` responses until the frontend is corrected to use `PATCH`.
|
**⚠️ KNOWN BUG — HTTP verb mismatch (status/role updates):** The frontend Redux actions for `updateUserStatus` and `updateUserRole` send `PUT` requests, but the backend registers these handlers under `PATCH`. These calls will receive `404 Method Not Found` responses until the frontend is corrected to use `PATCH`.
|
||||||
|
|
||||||
**⚠️ KNOWN BUG — Status value mismatch:** The frontend sends `'inactive'` and `'pending'` as status values when updating user status. The backend only accepts `'active'`, `'suspended'`, or `'deleted'`. Sending `'inactive'` or `'pending'` will be rejected or silently ignored.
|
**⚠️ KNOWN BUG — Status value mismatch:** The frontend sends `'inactive'` and `'pending'` as status values when updating user status. The backend only accepts `'active'`, `'suspended'`, or `'deleted'`. Sending `'inactive'` or `'pending'` will be rejected or silently ignored.
|
||||||
|
|
||||||
**Hard vs. soft delete note:** The legacy route `DELETE /users/admin/:id` performs a **hard delete** (`findByIdAndDelete`). The current route `DELETE /api/user/admin/:userId` performs a **soft delete** (sets `status='deleted'`). Always use the current `/api/user/admin/:userId` route to preserve data integrity.
|
**Hard vs. soft delete note:** The legacy route `DELETE /users/admin/:id` performs a **hard delete** (`findByIdAndDelete`). The current route `DELETE /api/users/admin/:userId` performs a **soft delete** (sets `status='deleted'`). Always use the current `/api/users/admin/:userId` route to preserve data integrity.
|
||||||
|
|
||||||
## Listing / marketplace moderation
|
## Listing / marketplace moderation
|
||||||
|
|
||||||
@@ -80,6 +84,22 @@ See [[Payment API]].
|
|||||||
|
|
||||||
**⚠️ Path correction:** Release/refund routes do **not** include a `/shkeeper/` segment. The correct paths are `/api/payment/:id/release`, `/api/payment/:id/release/confirm`, etc. (Previously documented incorrectly as `/api/payment/shkeeper/:id/…`.)
|
**⚠️ Path correction:** Release/refund routes do **not** include a `/shkeeper/` segment. The correct paths are `/api/payment/:id/release`, `/api/payment/:id/release/confirm`, etc. (Previously documented incorrectly as `/api/payment/shkeeper/:id/…`.)
|
||||||
|
|
||||||
|
## Derived destinations & sweep
|
||||||
|
|
||||||
|
Frontend page: `/dashboard/admin/derived-destinations`. Backend registers 7 endpoints under `/api/payment/derived-destinations/*` with admin auth.
|
||||||
|
|
||||||
|
| Endpoint | Action |
|
||||||
|
| --- | --- |
|
||||||
|
| `GET /api/payment/derived-destinations` | List all derived destination addresses |
|
||||||
|
| `POST /api/payment/derived-destinations/sweep/trigger` | Trigger a sweep across all destinations |
|
||||||
|
| `POST /api/payment/derived-destinations/sweep/trigger/:id` | Trigger sweep for a single destination |
|
||||||
|
| `GET /api/payment/derived-destinations/sweep/cron/status` | Get sweep cron job status |
|
||||||
|
| `POST /api/payment/derived-destinations/sweep/cron/start` | Start the sweep cron job |
|
||||||
|
| `POST /api/payment/derived-destinations/sweep/cron/stop` | Stop the sweep cron job |
|
||||||
|
| `GET /api/payment/derived-destinations/sweep/history` | Sweep history log |
|
||||||
|
|
||||||
|
> Frontend action functions: `getDerivedDestinations`, `triggerSweep`, `triggerSingleSweep`, `getSweepCronStatus`, `startSweepCron`, `stopSweepCron`.
|
||||||
|
|
||||||
## Points (admin)
|
## Points (admin)
|
||||||
|
|
||||||
See [[Points API]].
|
See [[Points API]].
|
||||||
@@ -140,12 +160,58 @@ Router: [`backend/src/services/admin/dataCleanupRoutes.ts`](../../backend/src/se
|
|||||||
### GET /api/admin/scanner/status
|
### GET /api/admin/scanner/status
|
||||||
|
|
||||||
**Description:** Returns the current state of the blockchain scanner / wallet monitor.
|
**Description:** Returns the current state of the blockchain scanner / wallet monitor.
|
||||||
**⚠️ SECURITY BUG — NO AUTHENTICATION:** Despite being mounted under `/api/admin/` and documented as admin-only, this endpoint has **no** `authenticateToken` or `authorizeRoles` guard. Any unauthenticated request can read scanner state.
|
|
||||||
|
|
||||||
> ⚠️ **NOT IMPLEMENTED:** The following endpoints do not exist in the codebase:
|
> **⚠️ SECURITY BUG — NO AUTHENTICATION:** Despite being mounted under `/api/admin/`, this endpoint has **no** `authenticateToken` or `authorizeRoles` guard. Any unauthenticated request can read scanner state.
|
||||||
> - `GET /api/admin/settings/confirmation-thresholds/history` — only the current-values `GET /api/admin/settings/confirmation-thresholds` and per-chain `PATCH /api/admin/settings/confirmation-thresholds/:chainId` exist.
|
|
||||||
> - `POST /api/admin/rn/networks/reload` — the network registry cannot be reloaded at runtime via HTTP.
|
## Settings
|
||||||
> - `POST /api/admin/rn/networks/probe/:chainId` — no per-chain probe endpoint exists.
|
|
||||||
|
### AML settings
|
||||||
|
|
||||||
|
> **⚠️ RUNTIME-ONLY PERSISTENCE:** `PATCH /api/admin/settings/aml` updates `process.env` at runtime only. Changes are **lost on server restart**. There is no frontend page for these endpoints.
|
||||||
|
|
||||||
|
| Endpoint | Auth | Action |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `GET /api/admin/settings/aml` | admin | Read current AML settings |
|
||||||
|
| `PATCH /api/admin/settings/aml` | admin | Update AML settings (runtime only — not persisted to disk or DB) |
|
||||||
|
|
||||||
|
### Confirmation thresholds
|
||||||
|
|
||||||
|
Frontend page exists. Endpoints require admin auth.
|
||||||
|
|
||||||
|
| Endpoint | Action |
|
||||||
|
| --- | --- |
|
||||||
|
| `GET /api/admin/settings/confirmation-thresholds` | Get current confirmation thresholds for all chains |
|
||||||
|
| `PATCH /api/admin/settings/confirmation-thresholds/:chainId` | Update threshold for a specific chain |
|
||||||
|
|
||||||
|
> **Not implemented:** `GET /api/admin/settings/confirmation-thresholds/history` — history endpoint does not exist. `POST /api/admin/rn/networks/reload` and `POST /api/admin/rn/networks/probe/:chainId` do not exist.
|
||||||
|
|
||||||
|
## Payments awaiting confirmation
|
||||||
|
|
||||||
|
Frontend page exists.
|
||||||
|
|
||||||
|
| Endpoint | Auth | Action |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `GET /api/admin/payments/awaiting-confirmation` | admin | List payments pending blockchain confirmation |
|
||||||
|
|
||||||
|
## RN network registry
|
||||||
|
|
||||||
|
Frontend page exists.
|
||||||
|
|
||||||
|
| Endpoint | Auth | Action |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `GET /api/admin/rn/networks` | admin | List all registered RN networks |
|
||||||
|
|
||||||
|
## Blog admin
|
||||||
|
|
||||||
|
Backend registers 5 blog admin endpoints, all guarded by `authorizeRoles('admin')`. Frontend has action functions calling each.
|
||||||
|
|
||||||
|
| Endpoint | Action |
|
||||||
|
| --- | --- |
|
||||||
|
| `GET /api/blog/admin/posts` | List all blog posts (admin view, includes drafts) |
|
||||||
|
| `POST /api/blog/posts` | Create a new blog post |
|
||||||
|
| `GET /api/blog/admin/posts/:id` | Get a single blog post (admin view) |
|
||||||
|
| `PUT /api/blog/posts/:id` | Update a blog post |
|
||||||
|
| `DELETE /api/blog/posts/:id` | Delete a blog post |
|
||||||
|
|
||||||
## Analytics
|
## Analytics
|
||||||
|
|
||||||
|
|||||||
@@ -69,13 +69,11 @@ Model: [[Chat]]. Real-time delivery happens over Socket.IO rooms named `chat-<ch
|
|||||||
**Auth required:** Bearer JWT (participant)
|
**Auth required:** Bearer JWT (participant)
|
||||||
**Errors:** `403` not a participant, `404` not found.
|
**Errors:** `403` not a participant, `404` not found.
|
||||||
|
|
||||||
### PATCH /api/chat/:id/archive
|
### PUT /api/chat/:id/archive
|
||||||
|
|
||||||
**Description:** Toggle archived state for the caller (per-user flag). Calling this endpoint on an already-archived chat **unarchives** it (toggle semantics).
|
**Description:** Toggle archived state for the caller (per-user flag). Calling this endpoint on an already-archived chat **unarchives** it (toggle semantics).
|
||||||
**Auth required:** Bearer JWT (participant)
|
**Auth required:** Bearer JWT (participant)
|
||||||
|
|
||||||
> ⚠️ **KNOWN BUG** — The frontend `archiveConversation` helper sends `PUT /api/chat/:id/archive` but the backend route is registered as `PATCH`. The request will receive a `404` until the frontend is corrected to use `PATCH`.
|
|
||||||
|
|
||||||
### POST /api/chat/:id/participants
|
### POST /api/chat/:id/participants
|
||||||
|
|
||||||
**Description:** Add a participant to a group chat.
|
**Description:** Add a participant to a group chat.
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ Model: [[Dispute]]. A dispute references a [[PurchaseRequest]] plus optional [[P
|
|||||||
### GET /api/disputes/statistics
|
### GET /api/disputes/statistics
|
||||||
|
|
||||||
**Description:** Aggregated counts (open, by reason, average resolution time) for admin dashboards.
|
**Description:** Aggregated counts (open, by reason, average resolution time) for admin dashboards.
|
||||||
**Auth required:** Bearer JWT (admin)
|
**Auth required:** Bearer JWT (any authenticated user — backend applies `authenticateToken` only, no role restriction)
|
||||||
**Response 200:** `{ success, data: { open, byReason, avgResolutionHours, ... } }`
|
**Response 200:** `{ success, data: { open, byReason, avgResolutionHours, ... } }`
|
||||||
|
|
||||||
### GET /api/disputes/:id
|
### GET /api/disputes/:id
|
||||||
|
|||||||
@@ -500,6 +500,16 @@ Same result shape as above, but for a single destination.
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Frontend PaymentProvider type
|
||||||
|
|
||||||
|
`src/types/payment.ts` defines `PaymentProvider` as:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
type PaymentProvider = 'request.network' | 'test' | 'other';
|
||||||
|
```
|
||||||
|
|
||||||
|
> ⚠️ **Type gap (M37):** Despite both SHKeeper and the legacy wallet-direct (DePay/decentralized) flows being active in production, neither `'shkeeper'` nor `'decentralized'` appears in this union. Any frontend code that branches on `provider` will treat both as `'other'` or fall through a switch default. The backend stores the literal strings `"shkeeper"` and `"decentralized"` in the database; the mismatch exists only in the frontend type definition.
|
||||||
|
|
||||||
## Status model
|
## Status model
|
||||||
|
|
||||||
[[Payment]] uses the statuses below across all providers:
|
[[Payment]] uses the statuses below across all providers:
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ title: Trezor API
|
|||||||
tags: [api, payments, trezor, safekeeping]
|
tags: [api, payments, trezor, safekeeping]
|
||||||
---
|
---
|
||||||
|
|
||||||
|
> **Last updated:** 2026-05-29 — aligned with code (see Doc vs Code Audit Report)
|
||||||
|
|
||||||
# Trezor API
|
# Trezor API
|
||||||
|
|
||||||
The Trezor API is mounted at `/api/trezor`. It is optional support for hardware-backed safekeeping and does not replace Request Network checkout, the funds ledger, or the broader Safe/multisig custody roadmap.
|
The Trezor API is mounted at `/api/trezor`. It is optional support for hardware-backed safekeeping and does not replace Request Network checkout, the funds ledger, or the broader Safe/multisig custody roadmap.
|
||||||
@@ -80,10 +82,26 @@ Response:
|
|||||||
|
|
||||||
## GET /api/trezor/account
|
## GET /api/trezor/account
|
||||||
|
|
||||||
Returns the caller's active Trezor registration summary.
|
Returns the caller's active Trezor registration summary. If no Trezor has been registered for the authenticated user, returns `{ registered: false }` without an error.
|
||||||
|
|
||||||
Auth: bearer JWT
|
Auth: bearer JWT
|
||||||
|
|
||||||
|
Response when registered:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"registered": true,
|
||||||
|
"xpubFingerprint": "0x...",
|
||||||
|
"registrationAddress": "0x...",
|
||||||
|
"basePath": "m/44'/60'/0'",
|
||||||
|
"deviceLabel": "Office Trezor",
|
||||||
|
"nextAddressIndex": 3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
Response when absent:
|
Response when absent:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
@@ -148,7 +166,7 @@ Response:
|
|||||||
|
|
||||||
## POST /api/trezor/verify-operation
|
## POST /api/trezor/verify-operation
|
||||||
|
|
||||||
Verifies a signed operation intent against the admin's registered Trezor safekeeping address.
|
Admin-only standalone signature verification endpoint. Verifies a signed operation intent against the admin's registered Trezor safekeeping address without performing any release or refund. Use this to validate a Trezor proof before submitting it to the release/refund flow.
|
||||||
|
|
||||||
Auth: bearer JWT, admin
|
Auth: bearer JWT, admin
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ tags: [api, user, reference]
|
|||||||
|
|
||||||
# User API
|
# User API
|
||||||
|
|
||||||
|
> **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))
|
||||||
|
|
||||||
Two routers are mounted for users:
|
Two routers are mounted for users:
|
||||||
|
|
||||||
- `/api/user/*` - the new controller pattern in [`backend/src/services/user/userControllerRoutes.ts`](../../backend/src/services/user/userControllerRoutes.ts) wired to `userController`.
|
- `/api/user/*` - the new controller pattern in [`backend/src/services/user/userControllerRoutes.ts`](../../backend/src/services/user/userControllerRoutes.ts) wired to `userController`.
|
||||||
@@ -75,29 +77,78 @@ Avatar upload is handled by the [[File API]]:
|
|||||||
|
|
||||||
### GET /api/user/wallet-address
|
### GET /api/user/wallet-address
|
||||||
|
|
||||||
**Description:** Returns the caller's stored EVM wallet address (or `null`).
|
**Description:** Returns the caller's stored wallet address plus its chain type and provider (each `null` if unset).
|
||||||
**Auth required:** Bearer JWT
|
**Auth required:** Bearer JWT
|
||||||
**Response 200:** `{ "success": true, "data": { "walletAddress": "0x..." | null } }`
|
**Response 200:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"walletAddress": "0x..." , // or null
|
||||||
|
"walletType": "evm" , // "evm" | "ton" | null (the chain family)
|
||||||
|
"walletProvider": "evm" // e.g. "evm" | "telegram-wallet" | null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
(Earlier docs listed only `walletAddress`; the endpoint also returns `walletType` and `walletProvider`.)
|
||||||
|
|
||||||
### PATCH /api/user/wallet-address
|
### PATCH /api/user/wallet-address
|
||||||
|
|
||||||
**Description:** Verifies an EIP-191 signed message and stores `profile.walletAddress`. The server uses `ethers.verifyMessage(message, signature)` and rejects if the recovered address does not match.
|
**Description:** Stores a verified wallet address. Supports **both EVM and TON**:
|
||||||
|
- **EVM** (`walletType` omitted or not `'ton'`): the address must pass `ethers.isAddress`, and the body must include `signature` + `message`. The server runs `ethers.verifyMessage(message, signature)` (EIP-191) and rejects if the recovered address does not match.
|
||||||
|
- **TON** (`walletType: 'ton'`): the address is validated against a TON address regex. An optional `tonProof` payload is verified via `verifyTonProof`; if valid, `profile.walletProofVerified` is set to `true` and `profile.walletProofTimestamp` is stamped.
|
||||||
|
|
||||||
|
On success the server writes `profile.walletAddress`, `profile.walletType` (`'evm'` or `'ton'`), `profile.walletProvider`, and `profile.walletProofVerified`.
|
||||||
**Auth required:** Bearer JWT
|
**Auth required:** Bearer JWT
|
||||||
**Request body:**
|
**Request body:**
|
||||||
```ts
|
```ts
|
||||||
{
|
{
|
||||||
walletAddress: string; // 0x-prefixed 40-hex
|
walletAddress: string; // EVM 0x-address, or TON address
|
||||||
signature: string; // signed `message`
|
walletType?: "evm" | "ton"; // defaults to "evm"
|
||||||
message: string; // human-readable challenge text
|
walletProvider?: string; // defaults to "telegram-wallet" for ton, "evm" otherwise
|
||||||
|
// EVM only:
|
||||||
|
signature?: string; // required for EVM — signed `message`
|
||||||
|
message?: string; // required for EVM — human-readable challenge text
|
||||||
|
// TON only:
|
||||||
|
tonProof?: TonProofPayload; // optional; when valid sets walletProofVerified=true
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
**Response 200:** `{ "success": true, "data": { "walletAddress": "0x..." } }`
|
**Response 200:** `{ "success": true, "data": { "user": { /* sanitized user */ }, "walletProofVerified": boolean } }`
|
||||||
**Errors:**
|
**Errors:**
|
||||||
- `400` missing fields, malformed address, signature mismatch
|
- `400` missing/invalid fields, malformed address, EVM signature mismatch, invalid TON proof
|
||||||
- `404` user not found
|
- `404` user not found
|
||||||
|
|
||||||
The legacy alias `PATCH /api/users/wallet-address` performs the same logic.
|
The legacy alias `PATCH /api/users/wallet-address` performs the same logic.
|
||||||
|
|
||||||
|
### POST /api/user/wallet-address/ton-proof/challenge
|
||||||
|
|
||||||
|
**Description:** Generates a TON proof nonce/challenge for TON wallet address verification. The returned challenge is then signed by the client and submitted for verification.
|
||||||
|
**Auth required:** Bearer JWT
|
||||||
|
**Response 200:** `{ "success": true, "data": { /* challenge/nonce payload */ } }`
|
||||||
|
**Source:** Backend implements this endpoint for TON proof nonce generation.
|
||||||
|
|
||||||
|
## Email verification
|
||||||
|
|
||||||
|
### POST /api/user/profile/email/verify
|
||||||
|
|
||||||
|
**Description:** Re-verifies the caller's email address after an email change using a 6-digit code sent to the new address.
|
||||||
|
**Auth required:** Bearer JWT
|
||||||
|
**Request body:**
|
||||||
|
```ts
|
||||||
|
{
|
||||||
|
code: string; // 6-digit verification code
|
||||||
|
}
|
||||||
|
```
|
||||||
|
**Response 200:** `{ "success": true, "data": { /* updated user */ } }`
|
||||||
|
**Source:** `axios.ts` defines this endpoint; used after email change flow.
|
||||||
|
|
||||||
|
### POST /api/user/profile/email/resend-verification
|
||||||
|
|
||||||
|
**Description:** Resends the 6-digit email verification code to the caller's (new) email address.
|
||||||
|
**Auth required:** Bearer JWT
|
||||||
|
**Response 200:** `{ "success": true }`
|
||||||
|
**Source:** `axios.ts` defines this endpoint; used in email change / re-verification flow.
|
||||||
|
|
||||||
## Contacts and search
|
## Contacts and search
|
||||||
|
|
||||||
### GET /api/users/contacts
|
### GET /api/users/contacts
|
||||||
@@ -122,7 +173,17 @@ The legacy alias `PATCH /api/users/wallet-address` performs the same logic.
|
|||||||
|
|
||||||
## Admin: user management
|
## Admin: user management
|
||||||
|
|
||||||
These are duplicated across the two routers. The newer controller variants live under `/api/user/admin/*`; the legacy bodies live under `/api/users/admin/*`. All require `req.user.role === 'admin'` (the legacy routes check inline; the controller routes only check `authenticateToken` and the controller enforces the role).
|
> **Note on the two admin route groups (prefix inconsistency).** There are TWO parallel admin route groups:
|
||||||
|
> - **Singular `/api/user/admin/*`** — the NEW controller (`userControllerRoutes.ts` → `userController`). This is where create / delete / status / role / list / dependencies are actually *registered* on the new controller.
|
||||||
|
> - **Plural `/api/users/admin/*`** — the LEGACY router (`userRoutes.ts`), which also mounts admin sub-routes (status, role, password, single-user fetch/update, resend-verification, stats).
|
||||||
|
>
|
||||||
|
> ⚠️ **The frontend consistently calls the PLURAL `/api/users/admin/*`** (see `frontend/src/lib/axios.ts`, all paths under `endpoints.users.admin.*`). So the singular create/delete/status/role/list paths below are *documented*, but in practice the frontend hits the legacy plural group. Both are listed; treat the plural group as the frontend-effective reality.
|
||||||
|
>
|
||||||
|
> ⚠️ **Note on HTTP verbs (KNOWN BUG):** The frontend `updateUserStatus` and `updateUserRole` calls (`frontend/src/actions/user.ts`) use **`PUT`** (`PUT /api/users/admin/:id/status`, `PUT /api/users/admin/:id/role`). The backend registers these as **`PATCH`** only (both the legacy and new routers). The verbs do not match — treat `PATCH` as the authoritative backend verb; the `PUT` calls will not route.
|
||||||
|
>
|
||||||
|
> ⚠️ **Note on status values (KNOWN BUG):** The frontend `updateUserStatus` TypeScript type is `'active' | 'inactive' | 'pending'`. The backend `User.status` enum is `'active' | 'suspended' | 'deleted'`. So:
|
||||||
|
> - `'inactive'` and `'pending'` are **rejected/ignored** by the backend (the new controller only applies `status` when it is one of `active`/`suspended`/`deleted`).
|
||||||
|
> - `'suspended'` — the actually-usable suspend value — is **missing from the frontend type**, so the admin UI cannot send it.
|
||||||
|
|
||||||
### POST /api/user/admin/create
|
### POST /api/user/admin/create
|
||||||
|
|
||||||
@@ -144,31 +205,49 @@ These are duplicated across the two routers. The newer controller variants live
|
|||||||
**Response 201:** `{ success, data: { user } }`
|
**Response 201:** `{ success, data: { user } }`
|
||||||
**Errors:** `400` missing fields, `403` non-admin, `409` email exists.
|
**Errors:** `400` missing fields, `403` non-admin, `409` email exists.
|
||||||
|
|
||||||
### DELETE /api/user/admin/:userId
|
### DELETE /api/user/admin/:userId (new controller — SOFT delete)
|
||||||
|
|
||||||
**Description:** Hard-delete a user. Prevents self-deletion and deleting other admins.
|
**Description:** **Soft-delete** — sets `status = 'deleted'` via `findByIdAndUpdate` (the user document is retained). Only blocks **self-deletion** (`userId === req.user.id`).
|
||||||
**Auth required:** Bearer JWT (admin)
|
**Auth required:** Bearer JWT (admin)
|
||||||
**Response 200:** `{ success, data: { deletedUserId } }`
|
**Response 200:** `{ success, data: { deletedUserId } }`
|
||||||
**Errors:** `400` self-delete, `403` admin-on-admin, `404` not found.
|
**Errors:** `400` self-delete, `404` not found.
|
||||||
|
|
||||||
### PATCH /api/user/admin/:userId/status
|
> ⚠️ **Behavior diverges from the legacy DELETE — and a privilege concern.** The new controller’s soft-delete does **NOT** block an admin from deleting *other* admins (it only blocks deleting yourself). By contrast, the legacy `DELETE /api/users/admin/:id` (below) is a **HARD delete** (`findByIdAndDelete`, removes the document) and **does** block admin-on-admin deletion. The two endpoints behave differently in both deletion semantics (soft vs hard) and authorization (self-only vs admin-on-admin block).
|
||||||
|
|
||||||
**Description:** Activate / suspend a user.
|
### DELETE /api/users/admin/:id (legacy router — HARD delete)
|
||||||
|
|
||||||
|
**Description:** **Hard-delete** — permanently removes the user document via `findByIdAndDelete`. Blocks deleting other admins.
|
||||||
**Auth required:** Bearer JWT (admin)
|
**Auth required:** Bearer JWT (admin)
|
||||||
**Request body:** `{ isActive: boolean; reason?: string }`
|
**Errors:** `403` admin-on-admin, `404` not found.
|
||||||
**Response 200:** `{ success, data: { user: { _id, isActive, statusUpdatedAt } } }`
|
|
||||||
|
### PATCH /api/user/admin/:userId/status (and legacy PATCH /api/users/admin/:id/status)
|
||||||
|
|
||||||
|
**Description:** Update a user's status and/or email-verified flag. Registered on the new controller as `/api/user/admin/:userId/status`; the legacy plural `/api/users/admin/:id/status` is what the frontend actually calls.
|
||||||
|
**Auth required:** Bearer JWT (admin)
|
||||||
|
**Request body:**
|
||||||
|
```ts
|
||||||
|
{
|
||||||
|
status?: "active" | "suspended" | "deleted"; // applied only if one of these three values
|
||||||
|
isEmailVerified?: boolean; // new controller also accepts this — sets User.isEmailVerified
|
||||||
|
reason?: string;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
The new controller only writes `status` when it is exactly `active`, `suspended`, or `deleted`; any other value (e.g. the frontend's `inactive`/`pending`) is silently ignored. It additionally accepts an `isEmailVerified` boolean to flip the user's email-verified flag.
|
||||||
|
**Response 200:** `{ success, data: { user } }` (sanitized user without password)
|
||||||
|
**⚠️ Frontend discrepancy (KNOWN BUG):** Frontend calls this with the `PUT` verb and sends `status: 'active' | 'inactive' | 'pending'`; the backend registers `PATCH` and only honors `active`/`suspended`/`deleted`. See the admin routing note above.
|
||||||
|
|
||||||
### PATCH /api/user/admin/:userId/toggle-status
|
### PATCH /api/user/admin/:userId/toggle-status
|
||||||
|
|
||||||
**Description:** Flip active/suspended without explicit body.
|
**Description:** Flip active/suspended without explicit body.
|
||||||
**Auth required:** Bearer JWT (admin)
|
**Auth required:** Bearer JWT (admin)
|
||||||
|
|
||||||
### PATCH /api/user/admin/:userId/role
|
### PATCH /api/users/admin/:userId/role
|
||||||
|
|
||||||
**Description:** Change a user's role.
|
**Description:** Change a user's role.
|
||||||
**Auth required:** Bearer JWT (admin)
|
**Auth required:** Bearer JWT (admin)
|
||||||
**Request body:** `{ role: "buyer" | "seller" | "admin"; reason?: string }`
|
**Request body:** `{ role: "buyer" | "seller" | "admin"; reason?: string }`
|
||||||
**Errors:** `400` invalid role.
|
**Errors:** `400` invalid role.
|
||||||
|
**Frontend discrepancy:** Frontend calls this with `PUT` verb; backend only accepts `PATCH`.
|
||||||
|
|
||||||
### GET /api/user/admin/list
|
### GET /api/user/admin/list
|
||||||
|
|
||||||
@@ -184,8 +263,9 @@ These are duplicated across the two routers. The newer controller variants live
|
|||||||
|
|
||||||
### GET /api/users/admin/stats
|
### GET /api/users/admin/stats
|
||||||
|
|
||||||
**Description:** Aggregated user stats — total/active/verified counts, role distribution, activity buckets (24h / 7d / 30d).
|
**Description:** Aggregated user stats — total/active/verified counts, role distribution, activity buckets (24h / 7d / 30d). (Undocumented previously.)
|
||||||
**Auth required:** Bearer JWT (admin)
|
**Auth required:** Bearer JWT (admin)
|
||||||
|
**Note:** No frontend UI actually consumes this. The endpoint path exists in `axios.ts` (`endpoints.users.admin.stats`), but the admin overview computes its figures client-side from `getPurchaseRequests()`, not from this endpoint.
|
||||||
|
|
||||||
### GET /api/users/admin/:userId
|
### GET /api/users/admin/:userId
|
||||||
|
|
||||||
@@ -210,10 +290,12 @@ These are duplicated across the two routers. The newer controller variants live
|
|||||||
|
|
||||||
### POST /api/users/admin/:userId/resend-verification
|
### POST /api/users/admin/:userId/resend-verification
|
||||||
|
|
||||||
**Description:** Regenerate the 8-digit email verification code and re-send the verification email.
|
**Description:** Regenerate the email verification code and re-send the verification email.
|
||||||
**Auth required:** Bearer JWT (admin)
|
**Auth required:** Bearer JWT (admin)
|
||||||
**Errors:** `400` user already verified.
|
**Errors:** `400` user already verified.
|
||||||
|
|
||||||
|
> ⚠️ **Email code length inconsistency.** The legacy `userRoutes.ts` generates an **8-digit** code (`10000000 + Math.random() * 90000000`), while the new `userController` (used by `POST /api/user/profile/email/verify` and the email-change flow) generates a **6-digit** code (`crypto.randomInt(100000, 1000000)`). Code length therefore depends on which path issued it.
|
||||||
|
|
||||||
## Address book
|
## Address book
|
||||||
|
|
||||||
Source: [`backend/src/services/address/addressRoutes.ts`](../../backend/src/services/address/addressRoutes.ts), model: [[Address]].
|
Source: [`backend/src/services/address/addressRoutes.ts`](../../backend/src/services/address/addressRoutes.ts), model: [[Address]].
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ related_models: ["[[User]]", "[[TempVerification]]"]
|
|||||||
related_apis: ["[[Auth API]]", "POST /api/auth/login", "POST /api/auth/refresh-token", "POST /api/auth/logout"]
|
related_apis: ["[[Auth API]]", "POST /api/auth/login", "POST /api/auth/refresh-token", "POST /api/auth/logout"]
|
||||||
---
|
---
|
||||||
|
|
||||||
|
> **Last updated:** 2026-05-29 — aligned with code (see Doc vs Code Audit Report)
|
||||||
|
|
||||||
> [!caution] Audit note — last reviewed 2026-05-29
|
> [!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.
|
> 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.
|
||||||
|
|
||||||
@@ -52,7 +54,7 @@ End-to-end specification for **email + password** authentication, JWT issuance,
|
|||||||
> [!warning] Token storage is `localStorage`, not cookies
|
> [!warning] Token storage is `localStorage`, not cookies
|
||||||
> Tokens are persisted in `window.localStorage`. This is intentional (the API is a separate origin and the app is fully SPA-like), but it means tokens are reachable from any script running on the page. Mitigations in place: strict CSP via `helmet`, no third-party scripts in the auth views, and short access-token TTL with refresh rotation. There are **no httpOnly auth cookies**.
|
> Tokens are persisted in `window.localStorage`. This is intentional (the API is a separate origin and the app is fully SPA-like), but it means tokens are reachable from any script running on the page. Mitigations in place: strict CSP via `helmet`, no third-party scripts in the auth views, and short access-token TTL with refresh rotation. There are **no httpOnly auth cookies**.
|
||||||
|
|
||||||
16. **Axios interceptor** (`frontend/src/lib/axios.ts`) attaches `Authorization: Bearer ${accessToken}` to every subsequent request. On a `401` response, the interceptor automatically triggers the refresh flow described below. A `403` response (e.g., `EMAIL_NOT_VERIFIED`) is **not** retried via refresh — it is surfaced directly to the caller.
|
16. **Axios interceptor** (`frontend/src/lib/axios.ts`) attaches `Authorization: Bearer ${accessToken}` to every subsequent request. On a `401` response, the interceptor automatically triggers the refresh flow described below. A `403` response (e.g., `EMAIL_NOT_VERIFIED`) is **not** retried via refresh — it is surfaced directly to the caller. The interceptor only checks `status === 401` (`axios.ts:105`); 403 responses are not handled by the interceptor and propagate as errors.
|
||||||
17. **Socket.IO bootstrap**: After login, the dashboard layout connects to Socket.IO and emits `join-user-room`, `join-buyer-room`/`join-seller-room` based on `user.role`. See `backend/src/app.ts:83-126`.
|
17. **Socket.IO bootstrap**: After login, the dashboard layout connects to Socket.IO and emits `join-user-room`, `join-buyer-room`/`join-seller-room` based on `user.role`. See `backend/src/app.ts:83-126`.
|
||||||
|
|
||||||
## Sequence diagram
|
## Sequence diagram
|
||||||
@@ -102,6 +104,7 @@ sequenceDiagram
|
|||||||
| `POST` | `/api/auth/refresh-token` | `authRoutes.ts:24-27` → `authController.refreshToken` |
|
| `POST` | `/api/auth/refresh-token` | `authRoutes.ts:24-27` → `authController.refreshToken` |
|
||||||
| `POST` | `/api/auth/logout` | `authRoutes.ts:68` → `authController.logout` (protected) |
|
| `POST` | `/api/auth/logout` | `authRoutes.ts:68` → `authController.logout` (protected) |
|
||||||
| `GET` | `/api/auth/profile` | `authRoutes.ts:69` → `authController.getProfile` |
|
| `GET` | `/api/auth/profile` | `authRoutes.ts:69` → `authController.getProfile` |
|
||||||
|
| `DELETE` | `/api/auth/account` | `authRoutes.ts:86-89` → `authController.deleteAccount` (requires `password` in body, runs `deleteAccountValidation`) |
|
||||||
|
|
||||||
## Telegram first-class auth flow
|
## Telegram first-class auth flow
|
||||||
|
|
||||||
@@ -119,6 +122,10 @@ Telegram is now a peer auth provider alongside email/password, Google, and passk
|
|||||||
|
|
||||||
High-risk actions are unchanged: escrow release, refund, dispute-sensitive, and wallet-sensitive operations still use the existing protected backend authorization and step-up gates. Telegram auth only establishes the user session.
|
High-risk actions are unchanged: escrow release, refund, dispute-sensitive, and wallet-sensitive operations still use the existing protected backend authorization and step-up gates. Telegram auth only establishes the user session.
|
||||||
|
|
||||||
|
## Passkey auth flow
|
||||||
|
|
||||||
|
The frontend `registerPasskey` and `authenticateWithPasskey` actions call passkey API endpoints. All passkey API calls are proxied directly to the Express backend via the `next.config.ts` rewrite rule (`/api/:path*` → backend). There are no Next.js route handler files (`route.ts`) for passkey paths — requests travel: browser → Next.js dev server (rewrite) → Express backend.
|
||||||
|
|
||||||
## Database writes
|
## Database writes
|
||||||
|
|
||||||
- **`users` collection**: `lastLoginAt` updated; `refreshTokens` array gains one entry per successful login or refresh.
|
- **`users` collection**: `lastLoginAt` updated; `refreshTokens` array gains one entry per successful login or refresh.
|
||||||
@@ -146,7 +153,7 @@ The access token is short-lived. When a protected request returns `401 TOKEN_INV
|
|||||||
4. The new pair is written back to `localStorage` and the original failed request is retried.
|
4. The new pair is written back to `localStorage` and the original failed request is retried.
|
||||||
|
|
||||||
> [!note] 403 responses are not retried
|
> [!note] 403 responses are not retried
|
||||||
> The interceptor only triggers token refresh for `status === 401`. A `403` (e.g., `EMAIL_NOT_VERIFIED`) is passed through directly to the caller without attempting a refresh.
|
> The interceptor only triggers token refresh for `status === 401` (`axios.ts:105`). A `403` (e.g., `EMAIL_NOT_VERIFIED`) is passed through directly to the caller without attempting a refresh.
|
||||||
|
|
||||||
> [!warning] Refresh-token sequence diagram is truncated
|
> [!warning] Refresh-token sequence diagram is truncated
|
||||||
> The Mermaid diagram below is **incomplete** — it was truncated in the original source at the point where the backend checks that the refresh token exists in `user.refreshTokens`. The remaining steps (rotate tokens, persist, respond, retry original request) are described in prose above but are not yet rendered in the diagram.
|
> The Mermaid diagram below is **incomplete** — it was truncated in the original source at the point where the backend checks that the refresh token exists in `user.refreshTokens`. The remaining steps (rotate tokens, persist, respond, retry original request) are described in prose above but are not yet rendered in the diagram.
|
||||||
@@ -178,17 +185,17 @@ sequenceDiagram
|
|||||||
|
|
||||||
### deleteAccount
|
### deleteAccount
|
||||||
|
|
||||||
> [!bug] Account deletion is currently broken
|
> [!bug] Account deletion frontend calls wrong endpoint
|
||||||
> The frontend `deleteAccount` action calls `DELETE /user/profile`, which does **not exist** on the backend. The real backend endpoint is `DELETE /api/auth/account` (requires `password` in the request body and runs `deleteAccountValidation`). Until the frontend is updated to call the correct endpoint, account deletion will always fail with a 404 or routing error.
|
> The frontend `deleteAccount` action calls `DELETE /user/profile`, which does **not exist** on the backend. The real backend endpoint is `DELETE /api/auth/account` (`authRoutes.ts:86-89`), which requires a `password` field in the request body and runs `deleteAccountValidation` middleware. Until the frontend is updated to call the correct endpoint, account deletion will always fail with a 404 or routing error.
|
||||||
|
|
||||||
## Known issues summary
|
## Known issues summary
|
||||||
|
|
||||||
| Issue | Severity | Details |
|
| Issue | Severity | Details |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `deleteAccount` calls wrong endpoint | Bug | Frontend calls `DELETE /user/profile`; backend endpoint is `DELETE /api/auth/account` |
|
| `deleteAccount` calls wrong endpoint | Bug | Frontend calls `DELETE /user/profile`; correct backend endpoint is `DELETE /api/auth/account` (requires `password` in body) |
|
||||||
| No change-password UI | Gap | `POST /api/auth/change-password` and `changePassword()` action exist but no dashboard page renders the form |
|
| No change-password UI | Gap | `POST /api/auth/change-password` and `changePassword()` action exist but no dashboard page renders the form |
|
||||||
| Rate limiter counts all attempts | Clarification | Counter increments before password check — 5 total attempts (not 5 failures) triggers lockout |
|
| Rate limiter counts all attempts | Clarification | Counter increments before password check — 5 total attempts (not 5 failures) triggers lockout |
|
||||||
| Axios interceptor 403 passthrough | Clarification | Interceptor only auto-refreshes on 401; 403 errors are surfaced directly |
|
| Axios interceptor 401-only | Clarification | Interceptor only auto-refreshes on `status === 401` (`axios.ts:105`); 403 errors propagate directly to caller |
|
||||||
| Refresh-token diagram truncated | Doc debt | Mermaid diagram cut off mid-flow; prose description is authoritative |
|
| Refresh-token diagram truncated | Doc debt | Mermaid diagram cut off mid-flow; prose description is authoritative |
|
||||||
|
|
||||||
## Linked flows
|
## Linked flows
|
||||||
|
|||||||
@@ -2,10 +2,11 @@
|
|||||||
title: Chat Flow
|
title: Chat Flow
|
||||||
tags: [flow, chat, socket-io, messaging]
|
tags: [flow, chat, socket-io, messaging]
|
||||||
related_models: ["[[Chat]]", "[[Message]]", "[[User]]"]
|
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
|
# 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.
|
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).
|
- **Frontend** — `frontend/src/sections/chat/` (chat list, conversation view, message composer).
|
||||||
- **Backend** — `ChatService` (`backend/src/services/chat/ChatService.ts`), routes under `/api/chat`.
|
- **Backend** — `ChatService` (`backend/src/services/chat/ChatService.ts`), routes under `/api/chat`.
|
||||||
- **MongoDB** — `chats` collection with embedded `messages`, `participants`, `unreadCounts`, `settings`, `metadata`.
|
- **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
|
## Preconditions
|
||||||
|
|
||||||
@@ -32,8 +33,8 @@ stateDiagram-v2
|
|||||||
[*] --> Created: ChatService.createChat\n(or auto on first contact)
|
[*] --> Created: ChatService.createChat\n(or auto on first contact)
|
||||||
Created --> Active: messages flowing
|
Created --> Active: messages flowing
|
||||||
Active --> Active: send / read / typing
|
Active --> Active: send / read / typing
|
||||||
Active --> Archived: settings.isArchived=true
|
Active --> Archived: PATCH /api/chat/:id/archive (toggle)
|
||||||
Archived --> Active: unarchive
|
Archived --> Active: PATCH /api/chat/:id/archive (same endpoint toggles back)
|
||||||
Active --> [*]: chat deleted (rare)
|
Active --> [*]: chat deleted (rare)
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -41,25 +42,33 @@ stateDiagram-v2
|
|||||||
|
|
||||||
### Creation
|
### 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`):
|
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.
|
- 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`.
|
- 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'`).
|
- 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.
|
- 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`).
|
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.
|
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)
|
### 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}`.
|
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
|
### 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`):
|
9. `ChatService.sendMessage` (`:195-260`):
|
||||||
- Loads chat, verifies the sender is in `participants[]` and `isActive`.
|
- 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 }`.
|
- Builds `Message`: `{ senderId, content, messageType: 'text', fileUrl?, fileName?, fileSize?, replyTo?, timestamp, isRead: false, isEdited: false }`.
|
||||||
@@ -71,20 +80,64 @@ stateDiagram-v2
|
|||||||
|
|
||||||
### Attachments
|
### 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 }`).
|
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**.
|
||||||
12. The send-message call then includes `messageType: 'image' | 'file'` and the file metadata. Files are served from `/uploads`.
|
|
||||||
|
> [!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
|
### Read receipts
|
||||||
|
|
||||||
13. When the user opens a chat, frontend POSTs `POST /api/chat/:chatId/read` (optionally with `messageIds: string[]`).
|
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**.
|
||||||
14. `ChatService.markMessagesAsRead` (`:438-483`):
|
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).
|
- 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.
|
- Emits **`messages-read`** to `chat-{chatId}` so the sender sees the double-tick.
|
||||||
|
|
||||||
### Typing indicator
|
### Typing indicator
|
||||||
|
|
||||||
15. On `input` events, frontend emits `socket.emit('typing-start', { chatId, userId, userName })`; on idle/blur emits `typing-stop`.
|
17. 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.
|
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
|
## Sequence diagram
|
||||||
|
|
||||||
@@ -100,22 +153,28 @@ sequenceDiagram
|
|||||||
participant IO as Socket.IO
|
participant IO as Socket.IO
|
||||||
|
|
||||||
A->>FE_A: Open conversation
|
A->>FE_A: Open conversation
|
||||||
FE_A->>BE: POST /api/chat {type:direct, participantIds, relatedTo}
|
FE_A->>BE: POST /api/chat {type:direct, participantIds:[sellerId]}
|
||||||
BE->>DB: find-or-create Chat
|
BE->>DB: find-or-create Chat (caller auto-appended)
|
||||||
BE-->>FE_A: { chat }
|
BE-->>FE_A: { chat }
|
||||||
FE_A->>IO: emit 'join-chat-room' chatId
|
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)
|
FE_B->>IO: emit 'join-chat-room' chatId (when B opens too)
|
||||||
|
|
||||||
A->>FE_A: type & send
|
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->>DB: chat.addMessage and update metadata.lastActivity to now
|
||||||
BE->>IO: emit chat-{id} 'new-message'
|
BE->>IO: emit chat-{id} 'new-message'
|
||||||
IO-->>FE_A: 'new-message' (echo)
|
IO-->>FE_A: 'new-message' (echo)
|
||||||
IO-->>FE_B: 'new-message' (live)
|
IO-->>FE_B: 'new-message' (live)
|
||||||
BE->>IO: emit user-{B} 'chat-notification' (badge)
|
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
|
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->>DB: chat.markAsRead(B)
|
||||||
BE->>IO: emit chat-{id} 'messages-read'
|
BE->>IO: emit chat-{id} 'messages-read'
|
||||||
IO-->>FE_A: 'messages-read' (double-tick)
|
IO-->>FE_A: 'messages-read' (double-tick)
|
||||||
@@ -128,25 +187,49 @@ sequenceDiagram
|
|||||||
|
|
||||||
| Method | Endpoint | Purpose |
|
| 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` | List user's chats |
|
||||||
| `GET` | `/api/chat/:chatId/messages` | Paginated message history |
|
| `GET` | `/api/chat/:id/info` | Chat details + first 50 messages (page 1, limit 50) + participants |
|
||||||
| `POST` | `/api/chat/:chatId/messages` | Send message |
|
| `GET` | `/api/chat/:id/messages` | Paginated message history |
|
||||||
| `POST` | `/api/chat/:chatId/upload` | Upload attachment |
|
| `POST` | `/api/chat/:id/messages` | Send text message |
|
||||||
| `POST` | `/api/chat/:chatId/read` | Mark read |
|
| `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/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
|
## 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
|
## Socket events emitted
|
||||||
|
|
||||||
- **`new-message`** → `chat-{chatId}` (every message).
|
- **`new-message`** → `chat-{chatId}` (every message).
|
||||||
- **`chat-notification`** → `user-{recipientId}` for non-senders (badge).
|
- **`chat-notification`** → `user-{recipientId}` for non-senders (badge).
|
||||||
- **`messages-read`** → `chat-{chatId}` after read mark.
|
- **`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-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.
|
- **`new-message`** (system) for system welcome lines on chat creation.
|
||||||
|
|
||||||
## Side effects
|
## Side effects
|
||||||
@@ -161,11 +244,13 @@ sequenceDiagram
|
|||||||
- **Sender not a participant** → `403 "User is not a participant in this chat"` (`:209-211`).
|
- **Sender not a participant** → `403 "User is not a participant in this chat"` (`:209-211`).
|
||||||
- **Chat not found** → `404` on `getChatMessages`.
|
- **Chat not found** → `404` on `getChatMessages`.
|
||||||
- **Direct duplicate** → idempotent — `createChat` returns existing chat.
|
- **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.
|
- **Content too long** — backend rejects messages exceeding 5000 characters at both Mongoose schema and controller validation levels.
|
||||||
- **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.
|
- **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.
|
- **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.
|
- **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
|
> [!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.
|
> `ChatService.sendMessage` posts `chat-notification` with `senderName: "کاربر"` (`:248`) — the literal Persian word for "user". Resolve `senderName` from `participant.userId.firstName` for a better UX.
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ After the escrow is funded ([[PRD - Request Network In-House Checkout]] / [[Escr
|
|||||||
|
|
||||||
## Step-by-step narrative
|
## Step-by-step narrative
|
||||||
|
|
||||||
1. **Seller marks shipped** — from the seller step `frontend/src/sections/request/components/seller-steps/step-3-ship-goods.tsx`, clicks "Mark as shipped". This patches the request status to `delivery`. No code is generated at this point.
|
1. **Seller marks shipped** — from the seller step `frontend/src/sections/request/components/seller-steps/step-3-ship-goods.tsx`, clicks "Mark as shipped". The frontend action `updateDelivery` calls `PUT /api/marketplace/purchase-requests/:id/delivery`. The controller's `updateDeliveryInfo` sets `shippedAt` and advances status to `delivery`. No code is generated at this point.
|
||||||
2. **Buyer generates the delivery code** — once status is `delivery`, the buyer explicitly triggers `POST /api/marketplace/purchase-requests/:id/delivery-code/generate` (buyerId is enforced server-side). `DeliveryService.generateDeliveryCode(requestId)`:
|
2. **Buyer generates the delivery code** — once status is `delivery`, the buyer explicitly triggers `POST /api/marketplace/purchase-requests/:id/delivery-code/generate` (buyerId is enforced server-side). `DeliveryService.generateDeliveryCode(requestId)`:
|
||||||
- Generates a 6-digit code (`Math.floor(100000 + Math.random()*900000)`).
|
- Generates a 6-digit code (`Math.floor(100000 + Math.random()*900000)`).
|
||||||
- Sets `deliveryInfo.deliveryCode`, `deliveryCodeGeneratedAt = now`, `deliveryCodeExpiresAt = now + 7d`, `deliveryCodeUsed = false`.
|
- Sets `deliveryInfo.deliveryCode`, `deliveryCodeGeneratedAt = now`, `deliveryCodeExpiresAt = now + 7d`, `deliveryCodeUsed = false`.
|
||||||
@@ -34,13 +34,13 @@ After the escrow is funded ([[PRD - Request Network In-House Checkout]] / [[Escr
|
|||||||
- The code is displayed to the buyer in `frontend/src/sections/request/components/buyer-steps/step-5-receive-goods.tsx`.
|
- The code is displayed to the buyer in `frontend/src/sections/request/components/buyer-steps/step-5-receive-goods.tsx`.
|
||||||
3. **Buyer reads code to seller** — at hand-off the buyer reads the 6-digit code out loud (or shows it) to the seller.
|
3. **Buyer reads code to seller** — at hand-off the buyer reads the 6-digit code out loud (or shows it) to the seller.
|
||||||
4. **Seller enters code** — seller types the code into `frontend/src/sections/request/components/seller-steps/delivery-code-verification.tsx`.
|
4. **Seller enters code** — seller types the code into `frontend/src/sections/request/components/seller-steps/delivery-code-verification.tsx`.
|
||||||
5. **Verification** — `POST /api/marketplace/purchase-requests/:id/delivery-code/verify` with `{ code }` (selectedOffer.sellerId is enforced server-side):
|
5. **Verification** — `POST /api/marketplace/purchase-requests/:id/delivery-code/verify` with `{ code }` (selectedOffer.sellerId is enforced server-side). Handled by `DeliveryService.verifyDeliveryCode` (lines 180-212):
|
||||||
- Matches `code` against `deliveryInfo.deliveryCode`.
|
- Matches `code` against `deliveryInfo.deliveryCode`.
|
||||||
- Checks `deliveryCodeExpiresAt > now` and `deliveryCodeUsed === false`.
|
- Checks `deliveryCodeExpiresAt > now` and `deliveryCodeUsed === false`.
|
||||||
- On success: `deliveryInfo.deliveryCodeUsed = true; deliveryCodeUsedAt = now`. Status flips `delivery → delivered`.
|
- On success: `deliveryInfo.deliveryCodeUsed = true; deliveryCodeUsedAt = now`. Status flips `delivery → delivered`.
|
||||||
- Emits `purchase-request-update` `status-changed`.
|
- Emits `purchase-request-update` `status-changed`.
|
||||||
- Triggers buyer/seller notifications via `notifyDeliveryConfirmed` (see `PurchaseRequestService.ts:631-641`).
|
- Sends delivery-confirmed notifications to both buyer and seller directly within `DeliveryService.verifyDeliveryCode`.
|
||||||
6. **Alternative path — buyer fast-track** — the buyer can also call `PATCH .../confirm-delivery` to set status to `delivered` without any code (used when the code path fails, e.g. code expired or lost). **⚠️ Authorization gap:** this endpoint currently has no authorization check; any authenticated user can call it.
|
6. **Alternative path — buyer fast-track** — the buyer can also call `PATCH .../confirm-delivery` to set status to `delivered` without any code (used when the code path fails, e.g. code expired or lost). This endpoint emits only `purchase-request-update` with `status-changed` — it does **not** send delivery-specific notifications to either party. **⚠️ Authorization gap:** this endpoint currently has no authorization check; any authenticated user can call it.
|
||||||
7. **Optional auto-release timer** — once `status === 'delivered'`, a scheduled job can flip the request to `confirming` and then to `seller_paid` after a grace period (e.g. 48h). The auto-release worker is not yet implemented; today an admin completes the chain via [[Payout Flow]].
|
7. **Optional auto-release timer** — once `status === 'delivered'`, a scheduled job can flip the request to `confirming` and then to `seller_paid` after a grace period (e.g. 48h). The auto-release worker is not yet implemented; today an admin completes the chain via [[Payout Flow]].
|
||||||
|
|
||||||
## Sequence diagram
|
## Sequence diagram
|
||||||
@@ -56,14 +56,14 @@ sequenceDiagram
|
|||||||
participant IO as Socket.IO
|
participant IO as Socket.IO
|
||||||
|
|
||||||
S->>FE: Click "Mark as shipped"
|
S->>FE: Click "Mark as shipped"
|
||||||
FE->>BE: PATCH /api/marketplace/purchase-requests/{id} {status:"delivery"}
|
FE->>BE: PUT /api/marketplace/purchase-requests/{id}/delivery
|
||||||
BE->>DB: PurchaseRequest.status="delivery"
|
BE->>DB: PurchaseRequest.shippedAt=now, status="delivery"
|
||||||
Note over BE,DB: No code generated here
|
Note over BE,DB: No code generated here
|
||||||
|
|
||||||
B->>FE: View delivery code in step-5-receive-goods
|
B->>FE: View delivery code in step-5-receive-goods
|
||||||
FE->>BE: POST /api/marketplace/purchase-requests/{id}/delivery-code/generate
|
FE->>BE: POST /api/marketplace/purchase-requests/{id}/delivery-code/generate
|
||||||
BE->>DB: deliveryInfo.deliveryCode=XXXXXX\nexpires=+7d
|
BE->>DB: deliveryInfo.deliveryCode=XXXXXX\nexpires=+7d
|
||||||
BE->>IO: emit request-{id} 'delivery-code-generated'
|
BE->>IO: emit request-{id} 'delivery-code-generated' {code, expiresAt}
|
||||||
FE->>B: Display 6-digit code
|
FE->>B: Display 6-digit code
|
||||||
|
|
||||||
B->>S: At hand-off, read the 6-digit code aloud
|
B->>S: At hand-off, read the 6-digit code aloud
|
||||||
@@ -73,8 +73,8 @@ sequenceDiagram
|
|||||||
BE->>DB: set deliveryCodeUsed = true
|
BE->>DB: set deliveryCodeUsed = true
|
||||||
BE->>DB: set status = "delivered"
|
BE->>DB: set status = "delivered"
|
||||||
BE->>IO: emit request-{id} 'purchase-request-update' status-changed
|
BE->>IO: emit request-{id} 'purchase-request-update' status-changed
|
||||||
BE->>B: notifyDeliveryConfirmed
|
BE->>B: notifyDeliveryConfirmed (DeliveryService.verifyDeliveryCode)
|
||||||
BE->>S: notifyDeliveryConfirmed
|
BE->>S: notifyDeliveryConfirmed (DeliveryService.verifyDeliveryCode)
|
||||||
Note over BE: Auto-release timer (planned) → seller_paid → payout
|
Note over BE: Auto-release timer (planned) → seller_paid → payout
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -82,12 +82,12 @@ sequenceDiagram
|
|||||||
|
|
||||||
| Method | Endpoint | Purpose |
|
| Method | Endpoint | Purpose |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `PATCH` | `/api/marketplace/purchase-requests/:id` `{status:"delivery"}` | Seller marks shipped |
|
| `PUT` | `/api/marketplace/purchase-requests/:id/delivery` | Seller marks shipped (sets shippedAt, advances to `delivery`) |
|
||||||
| `GET` | `/api/marketplace/purchase-requests/:id/delivery-code` | Retrieve current code (buyer + seller) |
|
| `GET` | `/api/marketplace/purchase-requests/:id/delivery-code` | Retrieve current code (buyer + seller) |
|
||||||
| `POST` | `/api/marketplace/purchase-requests/:id/delivery-code/generate` | Buyer generates delivery code (buyer only) |
|
| `POST` | `/api/marketplace/purchase-requests/:id/delivery-code/generate` | Buyer generates delivery code (buyer only) |
|
||||||
| `POST` | `/api/marketplace/purchase-requests/:id/delivery-code/verify` | Seller verifies code (seller only) |
|
| `POST` | `/api/marketplace/purchase-requests/:id/delivery-code/verify` | Seller verifies code (seller only) |
|
||||||
| `GET` | `/api/marketplace/purchase-requests/:id/delivery-code/status` | Check code status (buyer + seller) |
|
| `GET` | `/api/marketplace/purchase-requests/:id/delivery-code/status` | Check code status (buyer + seller) |
|
||||||
| `PATCH` | `/api/marketplace/purchase-requests/:id/confirm-delivery` | Buyer fast-track confirm (no code) — ⚠️ no auth check |
|
| `PATCH` | `/api/marketplace/purchase-requests/:id/confirm-delivery` | Buyer fast-track confirm (no code) — ⚠️ no auth check, no delivery notifications |
|
||||||
|
|
||||||
### Phantom frontend actions (routes do NOT exist on backend)
|
### Phantom frontend actions (routes do NOT exist on backend)
|
||||||
|
|
||||||
@@ -101,24 +101,25 @@ These Redux/API actions exist in the frontend but call endpoints that return 404
|
|||||||
|
|
||||||
## Two paths to `delivered` status
|
## Two paths to `delivered` status
|
||||||
|
|
||||||
1. **Code path** — seller calls `POST .../delivery-code/verify` with the correct, unexpired code → status becomes `delivered`.
|
1. **Code path** — seller calls `POST .../delivery-code/verify` with the correct, unexpired code → status becomes `delivered`. Both buyer and seller receive delivery-confirmed notifications (sent by `DeliveryService.verifyDeliveryCode`).
|
||||||
2. **Fast-track path** — buyer calls `PATCH .../confirm-delivery` (no code required) → also becomes `delivered`. ⚠️ Currently no authorization check on this endpoint.
|
2. **Fast-track path** — buyer calls `PATCH .../confirm-delivery` (no code required) → also becomes `delivered`. ⚠️ Currently no authorization check on this endpoint, and no delivery-specific notifications are sent to either party.
|
||||||
|
|
||||||
## Database writes
|
## Database writes
|
||||||
|
|
||||||
- **`purchaserequests.deliveryInfo`** — `deliveryCode`, `deliveryCodeGeneratedAt`, `deliveryCodeExpiresAt`, `deliveryCodeUsed`, `deliveryCodeUsedAt`.
|
- **`purchaserequests.deliveryInfo`** — `deliveryCode`, `deliveryCodeGeneratedAt`, `deliveryCodeExpiresAt`, `deliveryCodeUsed`, `deliveryCodeUsedAt`.
|
||||||
|
- **`purchaserequests.shippedAt`** — set when seller calls `PUT .../delivery`.
|
||||||
- **`purchaserequests.status`** — `delivery` → `delivered` → (eventually `seller_paid` → `completed`).
|
- **`purchaserequests.status`** — `delivery` → `delivered` → (eventually `seller_paid` → `completed`).
|
||||||
- **`notifications`** — generated for both parties.
|
- **`notifications`** — generated for both parties (code path only).
|
||||||
|
|
||||||
## Socket events emitted
|
## Socket events emitted
|
||||||
|
|
||||||
- **`delivery-code-generated`** → `request-{id}` (with code, expiresAt).
|
- **`delivery-code-generated`** → `request-{id}` room (payload: `{ requestId, code, expiresAt, timestamp }`). **⚠️ Security note:** the full 6-digit code is included in the payload and broadcast to all subscribers in the room, including the seller. The buyer dashboard displays the code; the seller receives it via socket as well.
|
||||||
- **`delivery-update`** → `request-{id}` (`type: 'code-generated'`).
|
- **`delivery-update`** → `request-{id}` (`type: 'code-generated'`).
|
||||||
- **`purchase-request-update`** `status-changed` on `delivery → delivered`.
|
- **`purchase-request-update`** `status-changed` on `delivery → delivered`.
|
||||||
|
|
||||||
## Side effects
|
## Side effects
|
||||||
|
|
||||||
- The code is shown only to the **buyer** in their dashboard. The buyer verbally shares it with the seller — there is no backend push of the code to the seller.
|
- The code is displayed to the **buyer** in their dashboard. The buyer verbally shares it with the seller at hand-off. Note that the `delivery-code-generated` socket event also broadcasts the raw code to the entire request room (including the seller — see socket events section above).
|
||||||
- Triggers the path that eventually frees up the escrow (manual today via [[Payout Flow]], auto in the future).
|
- Triggers the path that eventually frees up the escrow (manual today via [[Payout Flow]], auto in the future).
|
||||||
|
|
||||||
## Error / edge cases
|
## Error / edge cases
|
||||||
@@ -143,9 +144,8 @@ These Redux/API actions exist in the frontend but call endpoints that return 404
|
|||||||
|
|
||||||
## Source files
|
## Source files
|
||||||
|
|
||||||
- Backend: `backend/src/services/delivery/DeliveryService.ts`
|
- Backend: `backend/src/services/delivery/DeliveryService.ts` (generateDeliveryCode, verifyDeliveryCode lines 180-212)
|
||||||
- Backend: `backend/src/services/marketplace/routes.ts` (delivery endpoints)
|
- Backend: `backend/src/services/marketplace/routes.ts` (delivery endpoints)
|
||||||
- Backend: `backend/src/services/marketplace/PurchaseRequestService.ts:631-641` (confirmation notifications)
|
|
||||||
- Frontend: `frontend/src/sections/request/components/seller-steps/step-3-ship-goods.tsx`
|
- Frontend: `frontend/src/sections/request/components/seller-steps/step-3-ship-goods.tsx`
|
||||||
- Frontend: `frontend/src/sections/request/components/seller-steps/delivery-code-verification.tsx`
|
- Frontend: `frontend/src/sections/request/components/seller-steps/delivery-code-verification.tsx`
|
||||||
- Frontend: `frontend/src/sections/request/components/buyer-steps/step-5-receive-goods.tsx`
|
- Frontend: `frontend/src/sections/request/components/buyer-steps/step-5-receive-goods.tsx`
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ related_apis: ["POST /api/disputes", "POST /api/disputes/:id/assign", "POST /api
|
|||||||
audit: "2026-05-29 — corrected against source: Dispute.ts, DisputeService.ts, routes/disputeRoutes.ts (dashboard), services/dispute/disputeRoutes.ts (release-hold), app.ts. Previous version had wrong resolution schema, invented status values, missing security issues, and incorrect socket-event description."
|
audit: "2026-05-29 — corrected against source: Dispute.ts, DisputeService.ts, routes/disputeRoutes.ts (dashboard), services/dispute/disputeRoutes.ts (release-hold), app.ts. Previous version had wrong resolution schema, invented status values, missing security issues, and incorrect socket-event description."
|
||||||
---
|
---
|
||||||
|
|
||||||
|
> **Last updated:** 2026-05-29 — aligned with code (see Doc vs Code Audit Report)
|
||||||
|
|
||||||
# Dispute Flow
|
# Dispute Flow
|
||||||
|
|
||||||
When something goes wrong (item not delivered, wrong item, seller misbehaviour), either party can open a **dispute**. A three-way chat (buyer, seller, admin mediator) is created automatically. After evidence is gathered, the admin **resolves** the dispute — selecting an action such as refund, replacement, compensation, warning, or ban.
|
When something goes wrong (item not delivered, wrong item, seller misbehaviour), either party can open a **dispute**. A three-way chat (buyer, seller, admin mediator) is created automatically. After evidence is gathered, the admin **resolves** the dispute — selecting an action such as refund, replacement, compensation, warning, or ban.
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ related_apis: ["POST /api/auth/google/signup", "POST /api/auth/google/signin"]
|
|||||||
|
|
||||||
# Google OAuth Flow
|
# Google OAuth 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))
|
||||||
|
|
||||||
Google sign-in/up integration using **Google Identity Services** (`accounts.google.com/gsi/client`). The flow short-circuits email verification because Google accounts are pre-verified.
|
Google sign-in/up integration using **Google Identity Services** (`accounts.google.com/gsi/client`). The flow short-circuits email verification because Google accounts are pre-verified.
|
||||||
|
|
||||||
## Actors
|
## Actors
|
||||||
@@ -33,8 +35,8 @@ Google sign-in/up integration using **Google Identity Services** (`accounts.goog
|
|||||||
4. On success the popup returns an **ID token** (a Google-signed JWT containing `email`, `email_verified`, `name`, `given_name`, `family_name`, `picture`, `sub`).
|
4. On success the popup returns an **ID token** (a Google-signed JWT containing `email`, `email_verified`, `name`, `given_name`, `family_name`, `picture`, `sub`).
|
||||||
5. Frontend calls `signUpWithGoogle({ googleToken, role, referralCode })` (`frontend/src/auth/context/jwt/action.ts:281-304`), which POSTs `POST /api/auth/google/signup`.
|
5. Frontend calls `signUpWithGoogle({ googleToken, role, referralCode })` (`frontend/src/auth/context/jwt/action.ts:281-304`), which POSTs `POST /api/auth/google/signup`.
|
||||||
6. Backend `authController.googleSignUp` (`:781-872`) calls `googleOAuthService.verifyGoogleToken(googleToken)`. The verifier uses `google-auth-library` to validate the JWT signature, expiry, audience (`client_id`), and issuer.
|
6. Backend `authController.googleSignUp` (`:781-872`) calls `googleOAuthService.verifyGoogleToken(googleToken)`. The verifier uses `google-auth-library` to validate the JWT signature, expiry, audience (`client_id`), and issuer.
|
||||||
7. **Duplicate check**: `User.findOne({ email: googleUser.email })` — if found returns `409 USER_EXISTS` so the user can use *sign-in* instead.
|
7. **Duplicate check**: `User.findOne({ email: googleUser.email })` — if the email already exists, returns **`409 USER_EXISTS`** so the user can use *sign-in* instead.
|
||||||
8. **New user creation** with `password` omitted, `isEmailVerified: true`, `status: "active"`, `profile.avatar = googleUser.picture`, role from the request.
|
8. **New user creation** with `password` omitted, `isEmailVerified: true`, `status: "active"`, `profile.avatar = googleUser.picture`, and the chosen `role` from the request.
|
||||||
9. **Referral attribution** (`authController.ts:817-838`): same logic as the email path — increment `referrer.referralStats.totalReferrals`, emit `referral-signup` on `user-${referrer._id}`.
|
9. **Referral attribution** (`authController.ts:817-838`): same logic as the email path — increment `referrer.referralStats.totalReferrals`, emit `referral-signup` on `user-${referrer._id}`.
|
||||||
10. Generate access + refresh tokens, push refresh into `user.refreshTokens[]`, respond with `{ user, tokens }`.
|
10. Generate access + refresh tokens, push refresh into `user.refreshTokens[]`, respond with `{ user, tokens }`.
|
||||||
11. Frontend stores tokens in `localStorage` and redirects to the dashboard.
|
11. Frontend stores tokens in `localStorage` and redirects to the dashboard.
|
||||||
@@ -44,12 +46,15 @@ Google sign-in/up integration using **Google Identity Services** (`accounts.goog
|
|||||||
1. User clicks the Google icon on `/auth/jwt/sign-in`.
|
1. User clicks the Google icon on `/auth/jwt/sign-in`.
|
||||||
2. Same GSI flow as sign-up — Google returns an ID token.
|
2. Same GSI flow as sign-up — Google returns an ID token.
|
||||||
3. Frontend calls `signInWithGoogle(googleToken)` → `POST /api/auth/google/signin`.
|
3. Frontend calls `signInWithGoogle(googleToken)` → `POST /api/auth/google/signin`.
|
||||||
4. Backend verifies the token, looks up `User.findOne({ email: googleUser.email })`. If no user, returns `404 USER_NOT_FOUND` ("please sign up first"). The frontend surfaces a localized prompt.
|
4. Backend verifies the token, then looks up `User.findOne({ email: googleUser.email, status: "active" })` (`authController.ts:1194`). Note the **`status: "active"` filter**: the query only matches active accounts. If no active user matches, returns **`404 USER_NOT_FOUND`** ("please sign up first"). The frontend surfaces a localized prompt.
|
||||||
5. On hit: `existingUser.lastLoginAt = now`; if `profile.avatar` is empty and Google has a picture, it is back-filled (`authController.ts:905-907`).
|
5. On hit: `existingUser.lastLoginAt = now`; if `profile.avatar` is empty and Google has a picture, it is back-filled (`authController.ts:905-907`).
|
||||||
6. Tokens issued and returned identically to email login.
|
6. Tokens issued and returned identically to email login.
|
||||||
|
|
||||||
> [!tip] Account linking is implicit by email
|
> [!warning] No account merge
|
||||||
> A user who originally signed up via email + password can sign in with Google as long as the email matches — no extra "link account" step. The backend simply reuses the existing user document. There is **no** separate `googleId` field stored today, so this is a one-way trust on `googleUser.email`.
|
> There is **no** account-merge step between a Telegram-only / email account and a Google account. The Google sign-in path simply looks up an **active** user by email and reuses that document if one exists; it does not reconcile, link, or merge distinct identities. There is **no** separate `googleId` field stored today, so matching is a one-way trust on `googleUser.email`.
|
||||||
|
|
||||||
|
> [!warning] Soft-deleted accounts get a generic 404 on Google sign-in
|
||||||
|
> Because the sign-in lookup filters by `status: "active"`, a user who registered via Google and was later **soft-deleted** (`status: "deleted"`) is invisible to the query. They receive the **same generic `404 USER_NOT_FOUND`** as a never-registered user — there is **no** distinct "account deleted" / "account disabled" error.
|
||||||
|
|
||||||
## Sequence diagram
|
## Sequence diagram
|
||||||
|
|
||||||
@@ -76,15 +81,19 @@ sequenceDiagram
|
|||||||
end
|
end
|
||||||
BE->>GA: verifyGoogleToken(googleToken)
|
BE->>GA: verifyGoogleToken(googleToken)
|
||||||
GA-->>BE: { email, name, picture, ... } or null
|
GA-->>BE: { email, name, picture, ... } or null
|
||||||
|
alt Sign-up
|
||||||
BE->>DB: User.findOne({ email })
|
BE->>DB: User.findOne({ email })
|
||||||
alt Sign-up: user exists
|
else Sign-in
|
||||||
|
BE->>DB: User.findOne({ email, status: "active" })
|
||||||
|
end
|
||||||
|
alt Sign-up: email exists
|
||||||
BE-->>FE: 409 USER_EXISTS
|
BE-->>FE: 409 USER_EXISTS
|
||||||
else Sign-up: new
|
else Sign-up: new
|
||||||
BE->>DB: User.create({ email, role, isEmailVerified:true, profile.avatar })
|
BE->>DB: User.create({ email, role, isEmailVerified:true, profile.avatar })
|
||||||
opt referral
|
opt referral
|
||||||
BE->>DB: increment referrer.referralStats
|
BE->>DB: increment referrer.referralStats
|
||||||
end
|
end
|
||||||
else Sign-in: user missing
|
else Sign-in: no active user (missing or soft-deleted)
|
||||||
BE-->>FE: 404 USER_NOT_FOUND
|
BE-->>FE: 404 USER_NOT_FOUND
|
||||||
else Sign-in: ok
|
else Sign-in: ok
|
||||||
BE->>DB: set user.lastLoginAt = now
|
BE->>DB: set user.lastLoginAt = now
|
||||||
@@ -120,8 +129,9 @@ sequenceDiagram
|
|||||||
## Error / edge cases
|
## Error / edge cases
|
||||||
|
|
||||||
- **Invalid Google token** (bad signature, wrong audience, expired) → `googleOAuthService` returns `null` → `401 INVALID_GOOGLE_TOKEN`.
|
- **Invalid Google token** (bad signature, wrong audience, expired) → `googleOAuthService` returns `null` → `401 INVALID_GOOGLE_TOKEN`.
|
||||||
- **User already exists during sign-up** → `409`; frontend prompts to use sign-in instead.
|
- **Email already exists during sign-up** → `409 USER_EXISTS`; frontend prompts to use sign-in instead.
|
||||||
- **User missing during sign-in** → `404`; frontend redirects to sign-up.
|
- **User does not exist during sign-in** → `404 USER_NOT_FOUND`; frontend redirects to sign-up.
|
||||||
|
- **Soft-deleted user signs in via Google** → `404 USER_NOT_FOUND` (generic, indistinguishable from "never registered") because the lookup filters by `status: "active"`.
|
||||||
- **Popup blocker** → GSI throws a client-side error caught in the view and surfaced as a toast.
|
- **Popup blocker** → GSI throws a client-side error caught in the view and surfaced as a toast.
|
||||||
- **Network failure to `accounts.google.com`** → GSI rejects; frontend retries on next click.
|
- **Network failure to `accounts.google.com`** → GSI rejects; frontend retries on next click.
|
||||||
- **`email_verified === false` on the Google token** → currently not enforced; the backend trusts any successful Google response. For an extra-strict mode, gate on `googleUser.email_verified === true` in `googleOAuthService`.
|
- **`email_verified === false` on the Google token** → currently not enforced; the backend trusts any successful Google response. For an extra-strict mode, gate on `googleUser.email_verified === true` in `googleOAuthService`.
|
||||||
|
|||||||
@@ -2,11 +2,13 @@
|
|||||||
title: Negotiation Flow
|
title: Negotiation Flow
|
||||||
tags: [flow, marketplace, negotiation, counter-offer, chat]
|
tags: [flow, marketplace, negotiation, counter-offer, chat]
|
||||||
related_models: ["[[SellerOffer]]", "[[PurchaseRequest]]", "[[Chat]]"]
|
related_models: ["[[SellerOffer]]", "[[PurchaseRequest]]", "[[Chat]]"]
|
||||||
related_apis: ["PATCH /api/marketplace/offers/:id", "POST /api/chat", "POST /api/chat/:chatId/messages"]
|
related_apis: ["PUT /api/marketplace/offers/:id", "POST /api/chat", "POST /api/chat/:chatId/messages"]
|
||||||
---
|
---
|
||||||
|
|
||||||
# Negotiation Flow
|
# Negotiation 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))
|
||||||
|
|
||||||
After an offer is submitted ([[Seller Offer Flow]]), the buyer and seller can negotiate the price/ETA via **counter-offers** exchanged through the chat. The request status moves to `in_negotiation`, and either party can finalise with accept/reject.
|
After an offer is submitted ([[Seller Offer Flow]]), the buyer and seller can negotiate the price/ETA via **counter-offers** exchanged through the chat. The request status moves to `in_negotiation`, and either party can finalise with accept/reject.
|
||||||
|
|
||||||
## Actors
|
## Actors
|
||||||
@@ -16,7 +18,7 @@ After an offer is submitted ([[Seller Offer Flow]]), the buyer and seller can ne
|
|||||||
- **Frontend** — chat component (`frontend/src/sections/chat/`) overlaid on the request view; offer-edit modal under `frontend/src/sections/request/components/buyer-steps/step-3-components/`.
|
- **Frontend** — chat component (`frontend/src/sections/chat/`) overlaid on the request view; offer-edit modal under `frontend/src/sections/request/components/buyer-steps/step-3-components/`.
|
||||||
- **Backend** — `ChatService.sendMessage` for chat lines, `SellerOfferService.updateOffer` for price/ETA edits, `PurchaseRequestService.updatePurchaseRequest` for the status flip.
|
- **Backend** — `ChatService.sendMessage` for chat lines, `SellerOfferService.updateOffer` for price/ETA edits, `PurchaseRequestService.updatePurchaseRequest` for the status flip.
|
||||||
- **MongoDB** — `chats`, `selleroffers`, `purchaserequests`.
|
- **MongoDB** — `chats`, `selleroffers`, `purchaserequests`.
|
||||||
- **Socket.IO** — `new-message`, `seller-offer-update`, `purchase-request-update`.
|
- **Socket.IO** — `new-message`, `purchase-request-update`.
|
||||||
|
|
||||||
## Preconditions
|
## Preconditions
|
||||||
|
|
||||||
@@ -24,6 +26,9 @@ After an offer is submitted ([[Seller Offer Flow]]), the buyer and seller can ne
|
|||||||
- The purchase request is `received_offers` or `in_negotiation`.
|
- The purchase request is `received_offers` or `in_negotiation`.
|
||||||
- Both parties are still active users.
|
- Both parties are still active users.
|
||||||
|
|
||||||
|
> [!info] Status vocabulary
|
||||||
|
> The negotiation drives the **PurchaseRequest** into the `in_negotiation` status. The **SellerOffer** moves only between `pending`, `accepted`, `rejected`, and `withdrawn` (`backend/src/models/SellerOffer.ts:80`). There is **no `'active'` SellerOffer status** — any documentation or UI that references an "active" offer is incorrect.
|
||||||
|
|
||||||
## Step-by-step narrative
|
## Step-by-step narrative
|
||||||
|
|
||||||
1. **Open negotiation chat** — when a buyer first clicks "Chat with seller" on an offer card, the frontend calls `POST /api/chat` to find-or-create a `direct` chat tied to the purchase request (`ChatService.createChat`, `chat.ts:90-192`). The chat's `relatedTo = { type: 'PurchaseRequest', id }` makes it discoverable from the request view.
|
1. **Open negotiation chat** — when a buyer first clicks "Chat with seller" on an offer card, the frontend calls `POST /api/chat` to find-or-create a `direct` chat tied to the purchase request (`ChatService.createChat`, `chat.ts:90-192`). The chat's `relatedTo = { type: 'PurchaseRequest', id }` makes it discoverable from the request view.
|
||||||
@@ -35,20 +40,26 @@ After an offer is submitted ([[Seller Offer Flow]]), the buyer and seller can ne
|
|||||||
|
|
||||||
3. **Buyer proposes a counter** — the buyer types a message like "Can you do $80 instead of $100?". Two patterns are used:
|
3. **Buyer proposes a counter** — the buyer types a message like "Can you do $80 instead of $100?". Two patterns are used:
|
||||||
- **Free-form** — just a chat message; the seller eyeballs the offer-edit screen and updates the price.
|
- **Free-form** — just a chat message; the seller eyeballs the offer-edit screen and updates the price.
|
||||||
- **Structured counter** — the buyer opens an "edit offer" modal that PATCHes `/api/marketplace/offers/{id}` with the new desired terms. This is currently a seller-only edit endpoint; structured buyer counters typically come as a system message in the chat (`messageType: 'system'`) referencing the new price.
|
- **Structured counter** — the buyer opens an "edit offer" modal that (via the frontend `updateOffer` action) sends `PUT /api/marketplace/offers/{id}` with the new desired terms. This is a seller-side edit endpoint; structured buyer counters typically come as a system message in the chat (`messageType: 'system'`) referencing the new price.
|
||||||
|
|
||||||
4. **Seller updates the offer** — `SellerOfferService.updateOffer` (`:271-295`):
|
4. **Seller updates the offer** — `SellerOfferService.updateOffer` (`:271-295`):
|
||||||
- `SellerOffer.findByIdAndUpdate(id, { ...updateData, updatedAt: now }, { new: true })`.
|
- `SellerOffer.findByIdAndUpdate(id, { ...updateData, updatedAt: now }, { new: true })`.
|
||||||
- Emits `purchase-request-update` with `eventType: 'offer-updated'` to `request-{requestId}` (`SellerOfferService.ts:284-288`) — both parties' open tabs refresh.
|
- Emits `purchase-request-update` with `eventType: 'offer-updated'` to `request-{requestId}` (`SellerOfferService.ts:284-288`) — both parties' open tabs refresh.
|
||||||
|
|
||||||
|
> [!bug] ⚠️ KNOWN BUG — PUT/PATCH method mismatch on offer edit
|
||||||
|
> The frontend `updateOffer` action (`frontend/src/actions/marketplace.ts:286-297`) sends **`PUT /marketplace/offers/:id`**, but the legacy backend router registers only **`PATCH /offers/:id`** (`backend/src/services/marketplace/routes.ts:1260`). No `PUT /offers/:id` handler is registered, so structured offer edits from the UI may **404**. Fix by aligning on a single method (register `PUT` on the backend, or switch the frontend to `PATCH`).
|
||||||
|
|
||||||
5. **Buyer accepts** -- clicks "Accept this offer", which kicks off [[PRD - Request Network In-House Checkout]] with the selected `sellerOfferId`. Payment confirmation flips offer -> `accepted` and request -> `payment`.
|
5. **Buyer accepts** -- clicks "Accept this offer", which kicks off [[PRD - Request Network In-House Checkout]] with the selected `sellerOfferId`. Payment confirmation flips offer -> `accepted` and request -> `payment`.
|
||||||
|
|
||||||
6. **Buyer rejects** — calls `PATCH /api/marketplace/offers/{id}` with `{ status: 'rejected' }`. `SellerOfferService.updateOfferStatus` (`:306-353`) sends `notifyOfferRejected` to the seller and stamps `rejectedAt` + `rejectionReason`.
|
6. **Buyer rejects** — the frontend `rejectOffer` action calls `PUT /api/marketplace/offers/{id}/status` with `{ status: 'rejected' }`. `SellerOfferService.updateOfferStatus` (`:306-353`) sends `notifyOfferRejected` to the seller and stamps `rejectedAt` + `rejectionReason`.
|
||||||
|
|
||||||
7. **Seller withdraws** — `withdrawOffer` (`:428-443`) only works while `status === 'pending'`. After rejection/acceptance, withdrawal is impossible.
|
7. **Seller withdraws** — there is **no dedicated `/withdraw` endpoint** (see warning below). The only way to withdraw is `PUT /api/marketplace/offers/{id}/status` with `{ status: 'withdrawn' }` (`routes.ts:1914`). `withdrawOffer` (`:428-443`) only works while `status === 'pending'`. After rejection/acceptance, withdrawal is impossible.
|
||||||
|
|
||||||
8. **Chat continues** — even after status flips, the chat remains open for clarifications (delivery details, dispute prep). See [[Chat Flow]] for message-level semantics.
|
8. **Chat continues** — even after status flips, the chat remains open for clarifications (delivery details, dispute prep). See [[Chat Flow]] for message-level semantics.
|
||||||
|
|
||||||
|
> [!warning] ⚠️ NOT IMPLEMENTED — `POST /api/marketplace/offers/:id/withdraw`
|
||||||
|
> No `POST .../offers/:id/withdraw` route is registered anywhere in the backend; calling it returns **404**. Withdrawal is performed exclusively through the status endpoint: `PUT /api/marketplace/offers/:id/status` with body `{ status: 'withdrawn' }`.
|
||||||
|
|
||||||
## Sequence diagram
|
## Sequence diagram
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
@@ -75,8 +86,8 @@ sequenceDiagram
|
|||||||
BE->>DB: PurchaseRequest.status = "in_negotiation"
|
BE->>DB: PurchaseRequest.status = "in_negotiation"
|
||||||
BE->>IO: emit request-{id} 'purchase-request-update' (status-changed)
|
BE->>IO: emit request-{id} 'purchase-request-update' (status-changed)
|
||||||
S->>FE_S: Open edit-offer modal, set new price
|
S->>FE_S: Open edit-offer modal, set new price
|
||||||
FE_S->>BE: PATCH /api/marketplace/offers/{id} {price:{amount:80}}
|
FE_S->>BE: PUT /api/marketplace/offers/{id} {price:{amount:80}} ⚠️ backend only registers PATCH
|
||||||
BE->>DB: SellerOffer update
|
BE->>DB: SellerOffer update (if PUT handled; else 404)
|
||||||
BE->>IO: emit request-{id} 'purchase-request-update' (offer-updated)
|
BE->>IO: emit request-{id} 'purchase-request-update' (offer-updated)
|
||||||
IO-->>FE_B: refresh offer card
|
IO-->>FE_B: refresh offer card
|
||||||
alt Buyer accepts
|
alt Buyer accepts
|
||||||
@@ -84,10 +95,15 @@ sequenceDiagram
|
|||||||
Note over BE: Webhook PAID flips offer→accepted, request→payment
|
Note over BE: Webhook PAID flips offer→accepted, request→payment
|
||||||
else Buyer rejects
|
else Buyer rejects
|
||||||
B->>FE_B: Click "Reject"
|
B->>FE_B: Click "Reject"
|
||||||
FE_B->>BE: PATCH /api/marketplace/offers/{id} {status:"rejected"}
|
FE_B->>BE: PUT /api/marketplace/offers/{id}/status {status:"rejected"}
|
||||||
BE->>DB: offer.status = "rejected"
|
BE->>DB: offer.status = "rejected"
|
||||||
BE->>BE: notifyOfferRejected(seller)
|
BE->>BE: notifyOfferRejected(seller)
|
||||||
IO-->>FE_S: 'new-notification'
|
IO-->>FE_S: 'new-notification'
|
||||||
|
else Seller withdraws
|
||||||
|
S->>FE_S: Click "Withdraw offer"
|
||||||
|
FE_S->>BE: PUT /api/marketplace/offers/{id}/status {status:"withdrawn"}
|
||||||
|
BE->>DB: offer.status = "withdrawn"
|
||||||
|
BE->>IO: emit request-{id} 'purchase-request-update' (offer-updated)
|
||||||
end
|
end
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -97,14 +113,16 @@ sequenceDiagram
|
|||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `POST` | `/api/chat` | Find-or-create negotiation chat |
|
| `POST` | `/api/chat` | Find-or-create negotiation chat |
|
||||||
| `POST` | `/api/chat/:chatId/messages` | Send chat message |
|
| `POST` | `/api/chat/:chatId/messages` | Send chat message |
|
||||||
| `PATCH` | `/api/marketplace/offers/:id` | Seller updates price / ETA / notes (counter) |
|
| `POST` | `/api/marketplace/purchase-requests/:id/offers` | Create offer (scoped) — `routes.ts:1163` |
|
||||||
|
| `GET` | `/api/marketplace/purchase-requests/:id/offers` | List offers for a request (scoped) — `routes.ts:1223` |
|
||||||
|
| `PUT` | `/api/marketplace/offers/:id` | Seller updates price / ETA / notes (counter). ⚠️ KNOWN BUG: frontend sends `PUT`, backend registers only `PATCH /offers/:id` (`routes.ts:1260`) → may 404. |
|
||||||
|
| `PUT` | `/api/marketplace/offers/:id/status` | Reject (`{ status: 'rejected' }`) and withdraw (`{ status: 'withdrawn' }`) — `routes.ts:1914`. There is no separate `/withdraw` endpoint. |
|
||||||
| `PATCH` | `/api/marketplace/purchase-requests/:id` | Status transition to `in_negotiation` |
|
| `PATCH` | `/api/marketplace/purchase-requests/:id` | Status transition to `in_negotiation` |
|
||||||
| `POST` | `/api/marketplace/offers/:id/withdraw` | Seller pulls the offer |
|
|
||||||
|
|
||||||
## Database writes
|
## Database writes
|
||||||
|
|
||||||
- **`chats`**: messages appended via `chat.addMessage`; `metadata.lastActivity` bumped; `unreadCounts` incremented for non-sender participants.
|
- **`chats`**: messages appended via `chat.addMessage`; `metadata.lastActivity` bumped; `unreadCounts` incremented for non-sender participants.
|
||||||
- **`selleroffers`**: counter changes update `price`, `deliveryTime`, `notes`, `updatedAt`.
|
- **`selleroffers`**: counter changes update `price`, `deliveryTime`, `notes`, `updatedAt`; status moves between `pending`/`accepted`/`rejected`/`withdrawn`.
|
||||||
- **`purchaserequests`**: status flips when first counter arrives.
|
- **`purchaserequests`**: status flips when first counter arrives.
|
||||||
- **`notifications`**: created per status change (accept/reject), not per chat message (chat has its own real-time channel).
|
- **`notifications`**: created per status change (accept/reject), not per chat message (chat has its own real-time channel).
|
||||||
|
|
||||||
@@ -122,9 +140,11 @@ sequenceDiagram
|
|||||||
|
|
||||||
## Error / edge cases
|
## Error / edge cases
|
||||||
|
|
||||||
|
- **Offer edit returns 404** — see the KNOWN BUG above (PUT vs PATCH method mismatch).
|
||||||
- **Sender not a chat participant** → `403 "User is not a participant in this chat"` (`ChatService.sendMessage:209-211`).
|
- **Sender not a chat participant** → `403 "User is not a participant in this chat"` (`ChatService.sendMessage:209-211`).
|
||||||
- **Counter on an offer the buyer doesn't own a request for** → blocked by the controller (the buyer must be the request's owner).
|
- **Counter on an offer the buyer doesn't own a request for** → blocked by the controller (the buyer must be the request's owner).
|
||||||
- **Counter on an `accepted` offer** → `updateOffer` does not enforce status; the schema/controller should reject. Current code allows the price change, which is dangerous post-payment. Recommended hardening: guard with `if (offer.status !== 'pending') throw`.
|
- **Counter on an `accepted` offer** → `updateOffer` does not enforce status; the schema/controller should reject. Current code allows the price change, which is dangerous post-payment. Recommended hardening: guard with `if (offer.status !== 'pending') throw`.
|
||||||
|
- **Withdraw after accept/reject** → `withdrawOffer` only acts while `status === 'pending'`, so withdrawal is rejected once the offer leaves that state.
|
||||||
- **Status regression attempt** (`in_negotiation → received_offers`) → blocked by `isValidStatusProgression` (`PurchaseRequestService.ts:31-50`).
|
- **Status regression attempt** (`in_negotiation → received_offers`) → blocked by `isValidStatusProgression` (`PurchaseRequestService.ts:31-50`).
|
||||||
- **Two simultaneous edits** — last-write-wins on `findByIdAndUpdate`; consider optimistic concurrency via `__v` if conflicts become an issue.
|
- **Two simultaneous edits** — last-write-wins on `findByIdAndUpdate`; consider optimistic concurrency via `__v` if conflicts become an issue.
|
||||||
- **Chat created in negotiation but buyer never pays** → orphan chat remains; the post-payment chat (in [[Chat Flow]]) reuses it because the find-or-create logic matches by participants + relatedTo.
|
- **Chat created in negotiation but buyer never pays** → orphan chat remains; the post-payment chat (in [[Chat Flow]]) reuses it because the find-or-create logic matches by participants + relatedTo.
|
||||||
@@ -141,8 +161,11 @@ sequenceDiagram
|
|||||||
|
|
||||||
## Source files
|
## Source files
|
||||||
|
|
||||||
- Backend: `backend/src/services/marketplace/SellerOfferService.ts:271-353`
|
- Backend: `backend/src/services/marketplace/SellerOfferService.ts:271-443`
|
||||||
|
- Backend: `backend/src/services/marketplace/routes.ts:1163-1278,1914` (offer routes)
|
||||||
- Backend: `backend/src/services/marketplace/PurchaseRequestService.ts:408-495`
|
- Backend: `backend/src/services/marketplace/PurchaseRequestService.ts:408-495`
|
||||||
- Backend: `backend/src/services/chat/ChatService.ts:90-260`
|
- Backend: `backend/src/services/chat/ChatService.ts:90-260`
|
||||||
|
- Backend: `backend/src/models/SellerOffer.ts:17,80` (status enum)
|
||||||
|
- Frontend: `frontend/src/actions/marketplace.ts:286-308` (`updateOffer`, `rejectOffer`)
|
||||||
- Frontend: `frontend/src/sections/request/components/buyer-steps/step-3-components/`
|
- Frontend: `frontend/src/sections/request/components/buyer-steps/step-3-components/`
|
||||||
- Frontend: `frontend/src/sections/chat/` (chat UI)
|
- Frontend: `frontend/src/sections/chat/` (chat UI)
|
||||||
|
|||||||
@@ -65,6 +65,10 @@ Cross-cutting flow that powers the bell icon, toast pop-ups, badge counters, and
|
|||||||
- `Notification.findOneAndUpdate({ _id, userId }, { isRead: true, readAt: now })`.
|
- `Notification.findOneAndUpdate({ _id, userId }, { isRead: true, readAt: now })`.
|
||||||
- After updating, the backend emits `unread-count-update` to `user-{userId}` so all open tabs (and other devices) immediately sync their badge counter.
|
- After updating, the backend emits `unread-count-update` to `user-{userId}` so all open tabs (and other devices) immediately sync their badge counter.
|
||||||
|
|
||||||
|
### Purchase request status coverage gap
|
||||||
|
|
||||||
|
`NotificationService.notifyRequestStatusChanged` handles many purchase-request statuses but does **not** emit notifications for `pending_payment` or `seller_paid`. If a buyer moves to `pending_payment` or a seller is marked `seller_paid`, no notification is created. This is a known coverage gap; add dedicated helper methods (or extend the switch-case) if those transitions need to surface to recipients.
|
||||||
|
|
||||||
### Preferences
|
### Preferences
|
||||||
|
|
||||||
- `User.preferences.notifications` (in the User schema) can hold per-category opt-outs (`emailNotifications`, `pushNotifications`, etc.). The current implementation does not enforce preferences at send-time — all enabled notifications fire. Add a check in `createNotification` to short-circuit when the user has opted out of a category.
|
- `User.preferences.notifications` (in the User schema) can hold per-category opt-outs (`emailNotifications`, `pushNotifications`, etc.). The current implementation does not enforce preferences at send-time — all enabled notifications fire. Add a check in `createNotification` to short-circuit when the user has opted out of a category.
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ related_models: ["[[User]]"]
|
|||||||
related_apis: ["POST /api/auth/request-password-reset", "POST /api/auth/reset-password-with-code", "POST /api/auth/reset-password"]
|
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
|
> [!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.
|
> 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.
|
||||||
|
|
||||||
@@ -47,7 +49,7 @@ The primary UI-driven path uses the **code-based** endpoint. The token-based end
|
|||||||
6. User receives the email and enters the code + new password on `/auth/jwt/update-password`.
|
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 }`.
|
7. Frontend POSTs `POST /api/auth/reset-password-with-code { email, code, password }`.
|
||||||
8. Backend `authController.resetPasswordWithCode` (`:611-657`):
|
8. Backend `authController.resetPasswordWithCode` (`:611-657`):
|
||||||
- Validates code format `/^\d{6}$/` — a code of any other length (e.g., 8 digits) will **always fail** here.
|
- 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`.
|
- `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.
|
- 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.
|
- Sets `user.password = hashed`, clears `passwordResetCode` and `passwordResetCodeExpires`, **wipes `user.refreshTokens = []`** to invalidate all existing sessions.
|
||||||
@@ -100,7 +102,7 @@ sequenceDiagram
|
|||||||
>
|
>
|
||||||
> **`POST /api/auth/reset-password-with-code`** (primary UI path)
|
> **`POST /api/auth/reset-password-with-code`** (primary UI path)
|
||||||
> - Uses a 6-digit numeric code delivered by email.
|
> - Uses a 6-digit numeric code delivered by email.
|
||||||
> - `isValidVerificationCode()` validates with `/^\d{6}$/`. An 8-digit code will always fail.
|
> - `isValidVerificationCode()` validates with `/^\d{6}$/`.
|
||||||
> - Has **no password complexity middleware**. Any string is accepted as the new password.
|
> - Has **no password complexity middleware**. Any string is accepted as the new password.
|
||||||
>
|
>
|
||||||
> **`POST /api/auth/reset-password`** (legacy token-based path)
|
> **`POST /api/auth/reset-password`** (legacy token-based path)
|
||||||
@@ -126,7 +128,7 @@ sequenceDiagram
|
|||||||
## Error / edge cases
|
## Error / edge cases
|
||||||
|
|
||||||
- **Unknown email** → always `200`, generic message. No enumeration.
|
- **Unknown email** → always `200`, generic message. No enumeration.
|
||||||
- **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.
|
- **Invalid code format** → `400` from `isValidVerificationCode` guard before DB lookup.
|
||||||
- **Expired code** (>1h) → `400 Invalid or expired reset code`.
|
- **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.
|
- **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).
|
- **User attempts reset on deleted account** → treated as unknown (no email sent, `200` returned).
|
||||||
@@ -136,15 +138,11 @@ sequenceDiagram
|
|||||||
> [!warning] Plaintext code in logs
|
> [!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'`.
|
> 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
|
## Known issues summary
|
||||||
|
|
||||||
| Issue | Severity | Details |
|
| 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 |
|
| 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 |
|
| Inconsistent complexity between reset endpoints | Security gap | Token-based reset enforces complexity; code-based reset does not |
|
||||||
|
|
||||||
## Linked flows
|
## Linked flows
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ related_models: ["[[Payment]]", "[[PurchaseRequest]]"]
|
|||||||
related_apis: ["POST /api/payment/decentralized/save", "POST /api/payment/decentralized/verify/:paymentId"]
|
related_apis: ["POST /api/payment/decentralized/save", "POST /api/payment/decentralized/verify/:paymentId"]
|
||||||
---
|
---
|
||||||
|
|
||||||
|
> **Last updated:** 2026-05-29 — aligned with code (see Doc vs Code Audit Report)
|
||||||
|
|
||||||
> [!caution] Audit — 2026-05-29
|
> [!caution] Audit — 2026-05-29
|
||||||
> This document was reviewed against the live codebase. **12 corrections applied** — endpoint paths, missing route bugs, TypeScript type gaps, a security issue, and stats undercounting. See inline ⚠️ callouts throughout.
|
> This document was reviewed against the live codebase. **12 corrections applied** — endpoint paths, missing route bugs, TypeScript type gaps, a security issue, and stats undercounting. See inline ⚠️ callouts throughout.
|
||||||
|
|
||||||
@@ -169,6 +171,28 @@ The following four Request Network payout/release/refund sub-paths are **not reg
|
|||||||
- **`payments`** — same model as the Request Network flow. `provider` distinguishes the source.
|
- **`payments`** — same model as the Request Network flow. `provider` distinguishes the source.
|
||||||
- **`selleroffers`**, **`purchaserequests`**, **`chats`**, **`notifications`** — identical funded-escrow cascade (offer accepted, others rejected, request → `payment`, chat created, notifications fanned out).
|
- **`selleroffers`**, **`purchaserequests`**, **`chats`**, **`notifications`** — identical funded-escrow cascade (offer accepted, others rejected, request → `payment`, chat created, notifications fanned out).
|
||||||
|
|
||||||
|
### Payment status values
|
||||||
|
|
||||||
|
| `status` | `escrowState` | Meaning |
|
||||||
|
|---|---|---|
|
||||||
|
| `pending` | — | Intent created, awaiting on-chain transfer |
|
||||||
|
| `completed` | `funded` | On-chain transfer verified (terminal success for DePay/wallet-direct) |
|
||||||
|
| `failed` | — | Transaction reverted or verification failed |
|
||||||
|
|
||||||
|
### escrowState values (backend-authoritative)
|
||||||
|
|
||||||
|
| `escrowState` | Meaning |
|
||||||
|
|---|---|
|
||||||
|
| `funded` | Escrow received the on-chain transfer |
|
||||||
|
| `releasable` | Escrow funds cleared for release to seller |
|
||||||
|
| `releasing` | Release to seller in progress (intermediate state) |
|
||||||
|
| `released` | Funds sent to seller |
|
||||||
|
| `refunding` | Refund to buyer in progress |
|
||||||
|
| `refunded` | Funds returned to buyer |
|
||||||
|
|
||||||
|
> [!note] `'completed'` is not counted as a successful payment in stats
|
||||||
|
> `paymentService.getPaymentStats` counts only `status === 'confirmed'` as `successfulPayments`. DePay/wallet-direct payments terminate at `'completed'`, so they are **excluded** from the success count. The aggregate must include `'completed'` alongside `'confirmed'` to avoid undercounting.
|
||||||
|
|
||||||
## Socket events emitted
|
## Socket events emitted
|
||||||
|
|
||||||
- **`payment-created`** (admin dashboard) on intent creation.
|
- **`payment-created`** (admin dashboard) on intent creation.
|
||||||
|
|||||||
@@ -2,11 +2,13 @@
|
|||||||
title: Payment Flow - SHKeeper
|
title: Payment Flow - SHKeeper
|
||||||
tags: [flow, payment, shkeeper, crypto, escrow, webhook]
|
tags: [flow, payment, shkeeper, crypto, escrow, webhook]
|
||||||
related_models: ["[[Payment]]", "[[PurchaseRequest]]", "[[SellerOffer]]"]
|
related_models: ["[[Payment]]", "[[PurchaseRequest]]", "[[SellerOffer]]"]
|
||||||
related_apis: ["POST /api/payment/shkeeper/create", "POST /api/payment/shkeeper/webhook", "POST /api/payment/:id/release", "POST /api/payment/:id/refund"]
|
related_apis: ["POST /api/payment/shkeeper/intents", "POST /api/payment/shkeeper/webhook", "POST /api/payment/:id/release", "POST /api/payment/:id/refund"]
|
||||||
---
|
---
|
||||||
|
|
||||||
|
> **Last updated:** 2026-05-29 — aligned with code (see Doc vs Code Audit Report)
|
||||||
|
|
||||||
> [!caution] Audit — 2026-05-29
|
> [!caution] Audit — 2026-05-29
|
||||||
> This document was reviewed against the live codebase. **2 corrections applied**: the non-existent HTTP polling endpoint has been removed (status updates arrive via socket only), and the release/refund/confirm paths have been corrected to remove the erroneous `/shkeeper/` segment.
|
> This document was reviewed against the live codebase. **3 corrections applied**: (1) the non-existent HTTP polling endpoint has been removed (status updates arrive via socket only), (2) the release/refund/confirm paths have been corrected to remove the erroneous `/shkeeper/` segment, and (3) the intent-creation endpoint corrected from `/shkeeper/create` to `/shkeeper/intents` and parallel stats/export paths documented.
|
||||||
|
|
||||||
# Payment Flow — SHKeeper (Crypto Pay-In)
|
# Payment Flow — SHKeeper (Crypto Pay-In)
|
||||||
|
|
||||||
@@ -66,7 +68,7 @@ stateDiagram-v2
|
|||||||
### Phase 1 — Create intent
|
### Phase 1 — Create intent
|
||||||
|
|
||||||
1. Buyer clicks "Pay" on the chosen offer (`/dashboard/buyer/requests/{id}` → step-3-select-and-pay).
|
1. Buyer clicks "Pay" on the chosen offer (`/dashboard/buyer/requests/{id}` → step-3-select-and-pay).
|
||||||
2. Frontend POSTs `POST /api/payment/shkeeper/create` with `{ purchaseRequestId, sellerOfferId, amount, token?, network? }`.
|
2. Frontend POSTs `POST /api/payment/shkeeper/intents` with `{ purchaseRequestId, sellerOfferId, amount, token?, network? }`.
|
||||||
3. Backend `createPayInIntent`:
|
3. Backend `createPayInIntent`:
|
||||||
- Validates ObjectIds (`shkeeperService.ts:55-71`). Special path for **template checkout** (string IDs starting with `template-checkout-`).
|
- Validates ObjectIds (`shkeeperService.ts:55-71`). Special path for **template checkout** (string IDs starting with `template-checkout-`).
|
||||||
- **Duplicate-guard #1** (`:75-116`): if there is already an active `Payment` (`status ∈ {pending, processing}`) for the same `{purchaseRequestId, sellerOfferId, buyerId}`, the existing record is reused — same `paymentUrl`, no new wallet allocation.
|
- **Duplicate-guard #1** (`:75-116`): if there is already an active `Payment` (`status ∈ {pending, processing}`) for the same `{purchaseRequestId, sellerOfferId, buyerId}`, the existing record is reused — same `paymentUrl`, no new wallet allocation.
|
||||||
@@ -127,7 +129,7 @@ stateDiagram-v2
|
|||||||
|
|
||||||
21. The buyer's checkout page subscribes to socket events (`payment-update`, `template-checkout-payment-confirmed`). When the status flips to `completed`, the UI transitions to "Payment received" and unlocks the next buyer step (await delivery).
|
21. The buyer's checkout page subscribes to socket events (`payment-update`, `template-checkout-payment-confirmed`). When the status flips to `completed`, the UI transitions to "Payment received" and unlocks the next buyer step (await delivery).
|
||||||
|
|
||||||
> [!warning] ⚠️ No HTTP polling endpoint — socket events only
|
> [!warning] No HTTP polling endpoint — socket events only
|
||||||
> `GET /api/payment/shkeeper/status/:paymentId` **does not exist** — there is no polling route in `shkeeperRoutes.ts`. Status transitions must be observed via Socket.IO events (`payment-update`, `template-checkout-payment-confirmed`). Any frontend code path that polls this URL will always receive 404. Remove HTTP polling and rely solely on the socket subscription.
|
> `GET /api/payment/shkeeper/status/:paymentId` **does not exist** — there is no polling route in `shkeeperRoutes.ts`. Status transitions must be observed via Socket.IO events (`payment-update`, `template-checkout-payment-confirmed`). Any frontend code path that polls this URL will always receive 404. Remove HTTP polling and rely solely on the socket subscription.
|
||||||
|
|
||||||
22. The seller's dashboard receives `seller-offer-update` `payment-completed` and surfaces the green "Order paid — start preparing" banner.
|
22. The seller's dashboard receives `seller-offer-update` `payment-completed` and surfaces the green "Order paid — start preparing" banner.
|
||||||
@@ -148,7 +150,7 @@ sequenceDiagram
|
|||||||
actor S as Seller
|
actor S as Seller
|
||||||
|
|
||||||
B->>FE: Choose offer, click "Pay"
|
B->>FE: Choose offer, click "Pay"
|
||||||
FE->>BE: POST /api/payment/shkeeper/create
|
FE->>BE: POST /api/payment/shkeeper/intents
|
||||||
BE->>DB: dedupe / upsert Payment(status:"pending")
|
BE->>DB: dedupe / upsert Payment(status:"pending")
|
||||||
BE->>R: getCachedWallet(amount, token, network, requestId)
|
BE->>R: getCachedWallet(amount, token, network, requestId)
|
||||||
alt cache hit
|
alt cache hit
|
||||||
@@ -183,18 +185,26 @@ sequenceDiagram
|
|||||||
|
|
||||||
## API calls
|
## API calls
|
||||||
|
|
||||||
| Method | Endpoint | Purpose | Source |
|
| Method | Endpoint | Purpose | Auth | Source |
|
||||||
|---|---|---|---|
|
|---|---|---|---|---|
|
||||||
| `POST` | `/api/payment/shkeeper/create` | Create pay-in intent | `shkeeperRoutes.ts` → `shkeeperService.createPayInIntent` |
|
| `POST` | `/api/payment/shkeeper/intents` | Create pay-in intent | Bearer JWT (buyer) | `shkeeperRoutes.ts` → `shkeeperService.createPayInIntent` |
|
||||||
| `POST` | `/api/payment/shkeeper/webhook` | Receive SHKeeper webhook | `shkeeperWebhook.handleShkeeperWebhook` |
|
| `POST` | `/api/payment/shkeeper/webhook` | Receive SHKeeper webhook | HMAC / API key | `shkeeperWebhook.handleShkeeperWebhook` |
|
||||||
| `POST` | `/api/payment/:id/release` | Release escrow to seller | `paymentRoutes.ts` |
|
| `POST` | `/api/payment/:id/release` | Release escrow to seller | Bearer JWT | `paymentRoutes.ts` |
|
||||||
| `POST` | `/api/payment/:id/release/confirm` | Confirm escrow release | `paymentRoutes.ts` |
|
| `POST` | `/api/payment/:id/release/confirm` | Confirm escrow release | Bearer JWT | `paymentRoutes.ts` |
|
||||||
| `POST` | `/api/payment/:id/refund` | Refund to buyer | `paymentRoutes.ts` |
|
| `POST` | `/api/payment/:id/refund` | Refund to buyer | Bearer JWT | `paymentRoutes.ts` |
|
||||||
| `POST` | `/api/payment/:id/refund/confirm` | Confirm buyer refund | `paymentRoutes.ts` |
|
| `POST` | `/api/payment/:id/refund/confirm` | Confirm buyer refund | Bearer JWT | `paymentRoutes.ts` |
|
||||||
| `POST` | `/api/payment/payments/:id/fetch-tx` | Manual transaction lookup | `paymentRoutes.ts` |
|
| `POST` | `/api/payment/payments/:id/fetch-tx` | Manual transaction lookup | Bearer JWT | `paymentRoutes.ts` |
|
||||||
| ~~`GET /api/payment/shkeeper/status/:paymentId`~~ | | ⚠️ **404 — does not exist.** Use socket events instead. | — |
|
| `GET` | `/api/payment/payments/stats` | Payment statistics (admin-gated strict) | Bearer JWT + admin role | `paymentRoutes.ts` |
|
||||||
|
| `GET` | `/api/payment/stats` | Payment statistics (no admin guard) | Bearer JWT | `paymentRoutes.ts` |
|
||||||
|
| ~~`GET /api/payment/shkeeper/status/:paymentId`~~ | | **404 — does not exist.** Use socket events instead. | — | — |
|
||||||
|
|
||||||
> [!warning] ⚠️ Release/refund path correction
|
> [!note] Two parallel stats paths
|
||||||
|
> Two separate stats endpoints exist with different auth levels:
|
||||||
|
> - `GET /api/payment/payments/stats` — admin-gated (strict role check); intended for admin dashboard.
|
||||||
|
> - `GET /api/payment/stats` — authenticated but no admin guard; accessible to any logged-in user.
|
||||||
|
> Similarly, export endpoints exist at two paths with different auth levels. Confirm which is appropriate for each consumer before wiring the frontend.
|
||||||
|
|
||||||
|
> [!warning] Release/refund path correction
|
||||||
> Previously documented paths included a `/shkeeper/` segment that does **not** exist in the router:
|
> Previously documented paths included a `/shkeeper/` segment that does **not** exist in the router:
|
||||||
> - ~~`POST /api/payment/shkeeper/:id/release`~~ → correct: `POST /api/payment/:id/release`
|
> - ~~`POST /api/payment/shkeeper/:id/release`~~ → correct: `POST /api/payment/:id/release`
|
||||||
> - ~~`POST /api/payment/shkeeper/:id/release/confirm`~~ → correct: `POST /api/payment/:id/release/confirm`
|
> - ~~`POST /api/payment/shkeeper/:id/release/confirm`~~ → correct: `POST /api/payment/:id/release/confirm`
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ related_apis: ["POST /api/payment/:id/release", "POST /api/payment/:id/release/c
|
|||||||
|
|
||||||
# Payout Flow
|
# Payout 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))
|
||||||
|
|
||||||
This page describes how escrowed funds leave Amanat custody after an order is complete or a dispute is resolved.
|
This page describes how escrowed funds leave Amanat custody after an order is complete or a dispute is resolved.
|
||||||
|
|
||||||
The current flow is no longer SHKeeper payout-task centric. Release and refund are instruction-based:
|
The current flow is no longer SHKeeper payout-task centric. Release and refund are instruction-based:
|
||||||
@@ -34,7 +36,7 @@ Today the custody signer can be an admin/Trezor path when enabled. The roadmap t
|
|||||||
- The release/refund amount is positive and does not exceed available ledger balance.
|
- The release/refund amount is positive and does not exceed available ledger balance.
|
||||||
- No active dispute hold blocks the operation, unless the operation is the explicit dispute resolution path.
|
- No active dispute hold blocks the operation, unless the operation is the explicit dispute resolution path.
|
||||||
- Recipient wallet is known and verified.
|
- Recipient wallet is known and verified.
|
||||||
- If `TREZOR_SAFEKEEPING_REQUIRED=true`, the confirm step includes the expected Trezor operation signature.
|
- If `TREZOR_SAFEKEEPING_REQUIRED=true`, the confirm step **must** include the expected Trezor operation signature (see gate below).
|
||||||
- Production target: Safe multisig execution is required for custody movement.
|
- Production target: Safe multisig execution is required for custody movement.
|
||||||
|
|
||||||
## Release Narrative
|
## Release Narrative
|
||||||
@@ -43,7 +45,7 @@ Today the custody signer can be an admin/Trezor path when enabled. The roadmap t
|
|||||||
2. Admin calls `POST /api/payment/:id/release` with optional partial amount.
|
2. Admin calls `POST /api/payment/:id/release` with optional partial amount.
|
||||||
3. Backend loads the `Payment`, validates ledger availability when `PAYMENT_LEDGER_ENFORCEMENT=true`, and returns an instruction payload.
|
3. Backend loads the `Payment`, validates ledger availability when `PAYMENT_LEDGER_ENFORCEMENT=true`, and returns an instruction payload.
|
||||||
4. Custody signer broadcasts the seller payment transaction.
|
4. Custody signer broadcasts the seller payment transaction.
|
||||||
5. Admin calls `POST /api/payment/:id/release/confirm` with `txHash` and optional Trezor proof.
|
5. Admin calls `POST /api/payment/:id/release/confirm` with `txHash` and (when safekeeping is enabled) a Trezor signature proof.
|
||||||
6. Backend verifies signer proof when required, confirms adapter state, appends a `release` ledger entry, and marks escrow released.
|
6. Backend verifies signer proof when required, confirms adapter state, appends a `release` ledger entry, and marks escrow released.
|
||||||
|
|
||||||
## Refund Narrative
|
## Refund Narrative
|
||||||
@@ -52,7 +54,7 @@ Today the custody signer can be an admin/Trezor path when enabled. The roadmap t
|
|||||||
2. Admin calls `POST /api/payment/:id/refund`.
|
2. Admin calls `POST /api/payment/:id/refund`.
|
||||||
3. Backend validates available funds and policy.
|
3. Backend validates available funds and policy.
|
||||||
4. Custody signer broadcasts the refund transaction.
|
4. Custody signer broadcasts the refund transaction.
|
||||||
5. Admin calls `POST /api/payment/:id/refund/confirm` with `txHash` and optional Trezor proof.
|
5. Admin calls `POST /api/payment/:id/refund/confirm` with `txHash` and (when safekeeping is enabled) a Trezor signature proof.
|
||||||
6. Backend appends a `refund` ledger entry and marks escrow refunded.
|
6. Backend appends a `refund` ledger entry and marks escrow refunded.
|
||||||
|
|
||||||
## Sequence Diagram
|
## Sequence Diagram
|
||||||
@@ -74,15 +76,19 @@ sequenceDiagram
|
|||||||
A->>C: Request Trezor/Safe execution
|
A->>C: Request Trezor/Safe execution
|
||||||
C->>BC: Broadcast transfer
|
C->>BC: Broadcast transfer
|
||||||
BC-->>C: txHash
|
BC-->>C: txHash
|
||||||
A->>BE: POST /confirm { txHash, signer proof }
|
A->>BE: POST /confirm { txHash, trezor proof if safekeeping }
|
||||||
BE->>BE: Verify proof if required
|
BE->>BE: Verify proof if required
|
||||||
BE->>DB: append release/refund ledger entry
|
BE->>DB: append release/refund ledger entry
|
||||||
BE->>DB: update Payment escrowState
|
BE->>DB: update Payment escrowState
|
||||||
BE-->>R: notification
|
BE-->>R: notification (no realtime socket listener — see gap below)
|
||||||
```
|
```
|
||||||
|
|
||||||
## API Calls
|
## API Calls
|
||||||
|
|
||||||
|
### Release / Refund (custody) — correct paths
|
||||||
|
|
||||||
|
These are mounted on `paymentControllerRouter` at `/api/payment` (`backend/src/services/payment/paymentControllerRoutes.ts:23-26`). Note: **no `/shkeeper/` segment**.
|
||||||
|
|
||||||
| Method | Endpoint | Purpose |
|
| Method | Endpoint | Purpose |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `POST` | `/api/payment/:id/release` | Build release instruction |
|
| `POST` | `/api/payment/:id/release` | Build release instruction |
|
||||||
@@ -92,6 +98,44 @@ sequenceDiagram
|
|||||||
| `GET` | `/api/admin/payments/awaiting-confirmation` | Admin view of payments blocked on confirmation depth |
|
| `GET` | `/api/admin/payments/awaiting-confirmation` | Admin view of payments blocked on confirmation depth |
|
||||||
| `GET` | `/api/payment/derived-destinations` | Admin view of derived destination sweep state |
|
| `GET` | `/api/payment/derived-destinations` | Admin view of derived destination sweep state |
|
||||||
|
|
||||||
|
### Request Network — actually implemented routes
|
||||||
|
|
||||||
|
Mounted at `/api/payment/request-network` (`app.ts:428` → `requestNetwork/requestNetworkRoutes.ts`). Only these exist:
|
||||||
|
|
||||||
|
| Method | Endpoint | Purpose |
|
||||||
|
|---|---|---|
|
||||||
|
| `POST` | `/api/payment/request-network/pay-in` | Create a pay-in intent (authenticated) — `requestNetworkRoutes.ts:111` |
|
||||||
|
| `POST` | `/api/payment/request-network/intents` | Create checkout intent — `requestNetworkRoutes.ts:289` |
|
||||||
|
| `GET` | `/api/payment/request-network/:paymentId/checkout` | In-house checkout block fetcher — `requestNetworkRoutes.ts:152` |
|
||||||
|
| `POST` | `/api/payment/request-network/webhook` | Provider webhook (raw body) — `requestNetworkRoutes.ts:330` |
|
||||||
|
|
||||||
|
> [!warning] ⚠️ NOT IMPLEMENTED — Request Network payout/release/refund sub-routes
|
||||||
|
> The following routes are **not registered anywhere** and return **404**:
|
||||||
|
> - `POST /api/payment/request-network/:id/payout/initiate`
|
||||||
|
> - `POST /api/payment/request-network/:id/payout/confirm`
|
||||||
|
> - `POST /api/payment/request-network/:id/release/confirm`
|
||||||
|
> - `POST /api/payment/request-network/:id/refund/confirm`
|
||||||
|
>
|
||||||
|
> Release and refund are handled exclusively by the custody routes under `/api/payment/:id/...` listed above — **not** under the `request-network` namespace.
|
||||||
|
|
||||||
|
## Custody-signer / Trezor safekeeping gate
|
||||||
|
|
||||||
|
> [!warning] Safekeeping gate blocks the legacy non-custodial helpers
|
||||||
|
> When `TREZOR_SAFEKEEPING_REQUIRED=true` (`backend/src/services/trezor/trezorService.ts:214`), the release/refund `confirm` endpoints require a Trezor operation signature in the request body.
|
||||||
|
>
|
||||||
|
> - The **active admin UI** path uses `TrezorSignDialog` (`frontend/src/components/trezor-sign-dialog/TrezorSignDialog.tsx`), wired into the awaiting-confirmation list view. It builds the signed payload via `getTrezorOperationMessage` + `trezorSignMessage` and posts `{ txHash, amount, trezor: { message, signature } }` through `confirmRelease` / `confirmRefund` (`frontend/src/actions/trezor.ts:108,133`). This path satisfies the gate.
|
||||||
|
> - The **legacy helpers** `confirmReleaseTx` / `confirmRefundTx` (`frontend/src/actions/payment.ts:487,503`) post only `{ txHash, ...extra }` — by default **no Trezor proof**. They have **no UI callers** today, but if used with safekeeping enabled the backend will **reject** the payout. Prefer the `TrezorSignDialog` flow; remove or retrofit the legacy helpers to attach the signature.
|
||||||
|
|
||||||
|
## Derived-destinations sweep
|
||||||
|
|
||||||
|
HD-wallet derived-destination sweep infrastructure exists but is **admin-tooling only**:
|
||||||
|
|
||||||
|
- Routes: `GET /api/payment/derived-destinations` (`app.ts:546` → `wallets/derivedDestinationRoutes`).
|
||||||
|
- Cron: `startSweepCron()` auto-starts only when `DERIVED_DESTINATION_SWEEP_AUTOSTART=true` (`app.ts:578-582`, `wallets/sweepService.ts`).
|
||||||
|
- Model: `DerivedDestination` with statuses `active`/`swept`/`sweeping`/`quarantined` (`models/DerivedDestination.ts:35`).
|
||||||
|
|
||||||
|
This is not part of the buyer/seller payout UX; it consolidates funds from per-payment derived addresses.
|
||||||
|
|
||||||
## Database Writes
|
## Database Writes
|
||||||
|
|
||||||
- **`payments`** -- status, `escrowState`, `blockchain.transactionHash`, signer metadata.
|
- **`payments`** -- status, `escrowState`, `blockchain.transactionHash`, signer metadata.
|
||||||
@@ -99,14 +143,24 @@ sequenceDiagram
|
|||||||
- **`purchaserequests`** -- terminal business state after release/refund completes.
|
- **`purchaserequests`** -- terminal business state after release/refund completes.
|
||||||
- **`notifications`** -- release/refund receipt to the relevant party.
|
- **`notifications`** -- release/refund receipt to the relevant party.
|
||||||
|
|
||||||
|
## Socket events emitted
|
||||||
|
|
||||||
|
> [!warning] Real-time payout/payment events have NO frontend listeners
|
||||||
|
> Two seller-facing socket events are emitted by the backend but **no frontend code subscribes to them**, so sellers receive no real-time notification:
|
||||||
|
> - **`payout-completed`** → `user-{sellerId}`, emitted after admin wallet payout (`backend/src/services/payment/decentralizedPaymentService.ts:911`). No frontend listener.
|
||||||
|
> - **`payment-received`** → `user-{sellerId}`, emitted on Web3 verify (`backend/src/services/payment/paymentRoutes.ts:622`) and from `marketplace/routes.ts:2611`. No frontend listener.
|
||||||
|
>
|
||||||
|
> Until the frontend socket layer registers handlers for these, sellers must refresh / poll to see payout and incoming-payment state. Persisted DB notifications still surface through the standard notification channel.
|
||||||
|
|
||||||
## Error / Edge Cases
|
## Error / Edge Cases
|
||||||
|
|
||||||
- **Insufficient ledger balance** -- reject instruction build/confirm.
|
- **Insufficient ledger balance** -- reject instruction build/confirm.
|
||||||
- **Active dispute hold** -- reject release/refund unless the operation is the explicit dispute outcome.
|
- **Active dispute hold** -- reject release/refund unless the operation is the explicit dispute outcome.
|
||||||
- **Missing signer proof** -- reject when `TREZOR_SAFEKEEPING_REQUIRED=true`.
|
- **Missing signer proof** -- reject confirm when `TREZOR_SAFEKEEPING_REQUIRED=true` (legacy `confirmReleaseTx`/`confirmRefundTx` helpers omit it — see gate above).
|
||||||
- **Custody tx sent but not confirmed in app** -- reconcile by tx hash and append the missing ledger entry once verified.
|
- **Custody tx sent but not confirmed in app** -- reconcile by tx hash and append the missing ledger entry once verified.
|
||||||
- **Partial split** -- build separate release and refund instructions whose sum does not exceed available balance.
|
- **Partial split** -- build separate release and refund instructions whose sum does not exceed available balance.
|
||||||
- **Payout reverted** -- leave escrow in failed/retryable state and do not append the terminal ledger entry.
|
- **Payout reverted** -- leave escrow in failed/retryable state and do not append the terminal ledger entry.
|
||||||
|
- **Wrong namespace** -- calling release/refund under `/api/payment/request-network/:id/...` returns 404 (those routes do not exist).
|
||||||
|
|
||||||
## Legacy SHKeeper Note
|
## Legacy SHKeeper Note
|
||||||
|
|
||||||
@@ -122,9 +176,15 @@ Older versions used SHKeeper payout tasks and scripts such as `fix-transaction-h
|
|||||||
|
|
||||||
## Source Files
|
## Source Files
|
||||||
|
|
||||||
|
- Backend: `backend/src/services/payment/paymentControllerRoutes.ts:23-26` (release/refund routes)
|
||||||
|
- Backend: `backend/src/services/payment/requestNetwork/requestNetworkRoutes.ts:111,152,289,330` (implemented RN routes)
|
||||||
- Backend: `backend/src/services/payment/orchestration/releaseRefundService.ts`
|
- Backend: `backend/src/services/payment/orchestration/releaseRefundService.ts`
|
||||||
- Backend: `backend/src/services/payment/ledger/fundsLedgerService.ts`
|
- Backend: `backend/src/services/payment/ledger/fundsLedgerService.ts`
|
||||||
- Backend: `backend/src/services/payment/adapters/requestNetworkAdapter.ts`
|
- Backend: `backend/src/services/payment/adapters/requestNetworkAdapter.ts`
|
||||||
- Backend: `backend/src/services/trezor/trezorService.ts`
|
- Backend: `backend/src/services/trezor/trezorService.ts:214` (safekeeping gate)
|
||||||
- Backend: `backend/src/services/dispute/releaseHoldService.ts`
|
- Backend: `backend/src/services/dispute/releaseHoldService.ts`
|
||||||
- Frontend: admin payment/release/refund surfaces under `frontend/src/sections/`
|
- Backend: `backend/src/services/payment/decentralizedPaymentService.ts:911` (`payout-completed` emit)
|
||||||
|
- Backend: `backend/src/services/payment/paymentRoutes.ts:622` (`payment-received` emit)
|
||||||
|
- Backend: `backend/src/services/payment/wallets/sweepService.ts`, `models/DerivedDestination.ts` (sweep infra)
|
||||||
|
- Frontend: `frontend/src/components/trezor-sign-dialog/TrezorSignDialog.tsx`, `frontend/src/actions/trezor.ts:108,133` (active Trezor confirm path)
|
||||||
|
- Frontend: `frontend/src/actions/payment.ts:487,503` (legacy `confirmReleaseTx`/`confirmRefundTx`, no Trezor proof)
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ related_models: ["[[PurchaseRequest]]", "[[Category]]", "[[Address]]", "[[Seller
|
|||||||
related_apis: ["POST /api/marketplace/purchase-requests", "GET /api/marketplace/purchase-requests", "PATCH /api/marketplace/purchase-requests/:id"]
|
related_apis: ["POST /api/marketplace/purchase-requests", "GET /api/marketplace/purchase-requests", "PATCH /api/marketplace/purchase-requests/:id"]
|
||||||
---
|
---
|
||||||
|
|
||||||
|
> **Last updated:** 2026-05-29 — aligned with code (see Doc vs Code Audit Report)
|
||||||
|
|
||||||
> [!warning] Audit — 2026-05-29
|
> [!warning] Audit — 2026-05-29
|
||||||
> This document was corrected against the live codebase. Key changes: status enum updated (added `pending_payment`, `active`; removed undocumented `finalized`/`archived`); urgency values expanded to include `urgent`; sellers endpoint corrected; attachment upload endpoint corrected; `request-cancelled` socket event removed (non-existent); `new-purchase-request` fan-out target corrected to shared `sellers` room; socket room join/leave events documented; description minimum corrected to 5 chars; PUT vs PATCH mismatch flagged as known bug; two frontend actions hitting non-existent backend endpoints flagged as not implemented.
|
> This document was corrected against the live codebase. Key changes: status enum updated (added `pending_payment`, `active`; removed undocumented `finalized`/`archived`); urgency values expanded to include `urgent`; sellers endpoint corrected; attachment upload endpoint corrected; `request-cancelled` socket event removed (non-existent); `new-purchase-request` fan-out target corrected to shared `sellers` room; socket room join/leave events documented; description minimum corrected to 5 chars; PUT vs PATCH mismatch flagged as known bug; two frontend actions hitting non-existent backend endpoints flagged as not implemented.
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,11 @@ related_apis: ["POST /api/marketplace/reviews", "GET /api/marketplace/reviews/:s
|
|||||||
|
|
||||||
# Rating Flow
|
# Rating 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))
|
||||||
|
|
||||||
|
> [!caution] Not deeply audited
|
||||||
|
> This flow was not deeply covered by the 2026-05-29 audit; endpoints should be verified against `reviewRoutes`/`marketplaceController` before relying on them for UAT.
|
||||||
|
|
||||||
After an order is `completed`, the buyer rates the seller and (optionally) leaves a review; the seller can rate the buyer in the reciprocal flow. Reviews are scoped by `subjectType` (`seller` | `template`) and constrained by the seller's `ShopSettings`.
|
After an order is `completed`, the buyer rates the seller and (optionally) leaves a review; the seller can rate the buyer in the reciprocal flow. Reviews are scoped by `subjectType` (`seller` | `template`) and constrained by the seller's `ShopSettings`.
|
||||||
|
|
||||||
## Actors
|
## Actors
|
||||||
|
|||||||
@@ -7,15 +7,17 @@ related_apis: ["POST /api/points/generate-referral-code", "GET /api/points/refer
|
|||||||
|
|
||||||
# Referral Flow
|
# Referral 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))
|
||||||
|
|
||||||
Each user can generate a personal referral code, share a short URL, and earn points/commission when their referees sign up and complete purchases. Codes can also be entered at sign-up time (Phase 1 attribution) — see [[Registration Flow]].
|
Each user can generate a personal referral code, share a short URL, and earn points/commission when their referees sign up and complete purchases. Codes can also be entered at sign-up time (Phase 1 attribution) — see [[Registration Flow]].
|
||||||
|
|
||||||
## Actors
|
## Actors
|
||||||
|
|
||||||
- **Referrer** — the user with the code.
|
- **Referrer** — the user with the code.
|
||||||
- **Referred user** — the new sign-up.
|
- **Referred user** — the new sign-up.
|
||||||
- **Backend** — `PointsService` (`backend/src/services/points/PointsService.ts`), points routes at `backend/src/routes/pointsRoutes.ts`.
|
- **Backend** — `PointsService` (`backend/src/services/points/PointsService.ts`), `authController` (`backend/src/services/auth/authController.ts`), points routes at `backend/src/routes/pointsRoutes.ts`.
|
||||||
- **MongoDB** — `users` (`referralCode`, `referredBy`, `referralStats`, `points`), `pointtransactions`, `levelconfigs`.
|
- **MongoDB** — `users` (`referralCode`, `referredBy`, `referralStats`, `points`), `pointtransactions`, `levelconfigs`.
|
||||||
- **Socket.IO** — `referral-signup` and `level-up` events.
|
- **Socket.IO** — `referral-signup` (auth domain) and `referral-reward` / `level-up` (points domain) events.
|
||||||
|
|
||||||
## Preconditions
|
## Preconditions
|
||||||
|
|
||||||
@@ -26,17 +28,19 @@ Each user can generate a personal referral code, share a short URL, and earn poi
|
|||||||
|
|
||||||
### 1. Code generation
|
### 1. Code generation
|
||||||
|
|
||||||
1. User opens `/dashboard/account/referrals`. If they don't have a code yet, they click "Generate code".
|
1. User opens the points dashboard. If they don't have a code yet, they receive one automatically (`getUserPoints` lazily generates one — `PointsService.ts:216-219`).
|
||||||
2. Frontend POSTs `POST /api/points/generate-referral-code`.
|
2. A manual `POST /api/points/generate-referral-code` is also available.
|
||||||
3. `PointsService.generateReferralCode(userId)` (`:12-31`):
|
3. `PointsService.generateReferralCode(userId)` (`:12-31`):
|
||||||
- Loops generating an 8-character code from `ABCDEFGHJKLMNPQRSTUVWXYZ23456789` until uniqueness is confirmed by `User.findOne({ referralCode })`.
|
- Loops generating an 8-character code from `ABCDEFGHJKLMNPQRSTUVWXYZ23456789` until uniqueness is confirmed by `User.findOne({ referralCode })`.
|
||||||
- Saves the code to the user.
|
- **ALWAYS overwrites** the user's existing code via `User.findByIdAndUpdate(userId, { referralCode: code })` (`:29`). There is **no idempotency / no `force` flag** — any param in the request body is ignored. Calling this endpoint rotates (replaces) the code every time, invalidating previously shared links.
|
||||||
- Returns it.
|
- Returns it.
|
||||||
4. Frontend renders the share URL `https://amn.gg/r/{code}` and a copy button.
|
4. Frontend renders the share URL `${NEXT_PUBLIC_API_URL}/r/${referralCode}` (pointing to the **backend** API URL, not a frontend URL) and a copy button. This is constructed in `frontend/src/sections/points/points-invite-friends.tsx:35-36`.
|
||||||
|
> [!warning] Share link points at the wrong base
|
||||||
|
> The link is built from `NEXT_PUBLIC_API_URL` (the backend) rather than the frontend origin. The `/r/:code` redirect on the backend then bounces the user to the frontend sign-up — so it functions, but the surfaced URL is the API host, which is not the intended public-facing brand URL.
|
||||||
|
|
||||||
### 2. Short-URL redirect
|
### 2. Short-URL redirect
|
||||||
|
|
||||||
5. When a friend clicks the short URL, `GET /r/:code` (`backend/src/app.ts:274-278`) redirects to `${FRONTEND_URL}/auth/jwt/sign-up?ref={code}`.
|
5. When a friend clicks the share URL, `GET /r/:code` (`backend/src/app.ts:274-278`) redirects to `${FRONTEND_URL}/auth/jwt/sign-up?ref={code}`.
|
||||||
6. The sign-up form reads `?ref=` and pre-fills the referral field (hidden or visible).
|
6. The sign-up form reads `?ref=` and pre-fills the referral field (hidden or visible).
|
||||||
|
|
||||||
### 3. Attribution at sign-up
|
### 3. Attribution at sign-up
|
||||||
@@ -44,26 +48,38 @@ Each user can generate a personal referral code, share a short URL, and earn poi
|
|||||||
7. During [[Registration Flow]] verification (or [[Google OAuth Flow]] sign-up), the controller looks up `User.findOne({ referralCode })`:
|
7. During [[Registration Flow]] verification (or [[Google OAuth Flow]] sign-up), the controller looks up `User.findOne({ referralCode })`:
|
||||||
- Sets `user.referredBy = referrer._id` on the new user.
|
- Sets `user.referredBy = referrer._id` on the new user.
|
||||||
- Increments `referrer.referralStats.totalReferrals`.
|
- Increments `referrer.referralStats.totalReferrals`.
|
||||||
- Emits **`referral-signup`** to `user-{referrerId}` with the referee's name, email, and updated total.
|
- Emits **`referral-signup`** to `user-{referrerId}` with the referee's name, email, and updated total — emitted from `authController.ts`, not from PointsService.
|
||||||
8. At this point the referee is **counted** but no points have changed hands yet. Points award on subsequent business events.
|
8. At this point the referee is **counted** but no points have changed hands yet. Points award on subsequent business events.
|
||||||
|
|
||||||
|
> [!danger] No self-referral guard
|
||||||
|
> There is **no check** preventing a user from using their own referral code. A user who enters their own code at sign-up (or any flow that sets `referredBy`) is not blocked at the controller or service level. This is a known gap — add a guard such as `if (referrer._id.equals(user._id)) return` in the email and Google sign-up paths.
|
||||||
|
|
||||||
### 4. Points awarding
|
### 4. Points awarding
|
||||||
|
|
||||||
9. `PointsService.addPoints(userId, amount, source, metadata)` (`:36-100`) is called by other services on triggering events:
|
9. The **only** caller that awards referral points is `marketplaceController.ts`, which invokes `PointsService.processReferralReward(id)` **only when an order transitions to `'completed'`** (`marketplaceController.ts:473-475`, inside `if (newStatus === 'completed')`). It is **NOT** triggered on `'delivered'`, `'delivery'`, `'seller_paid'`, or any other status.
|
||||||
- **Purchase completion** (intended): when a referred user finishes an order, the referrer should get a commission. The hook point is `PurchaseRequestService` `notifyTransactionCompleted` — the exact wiring is implementation-specific; the service exposes `source: 'purchase' | 'referral' | 'bonus' | 'admin'`.
|
10. `PointsService.processReferralReward(purchaseRequestId)` (`:372-429`):
|
||||||
- **Bonus**: ad-hoc admin grants.
|
- Loads the purchase request, finds the buyer and the buyer's `referredBy` referrer (returns `null` if either is missing).
|
||||||
10. Inside `addPoints`:
|
- Computes `referralPoints = Math.floor(amount * 0.02)` — a flat **2% commission** on the selected offer's price.
|
||||||
|
- Calls `PointsService.addPoints(referrerId, referralPoints, 'referral', {...})`.
|
||||||
|
- Recomputes `referrer.referralStats.activeReferrals` as a count of **ALL** users with `referredBy = referrer._id` (`:409-411`) — this includes referrals that never purchased; it is **not** scoped to converted referrals.
|
||||||
|
- Increments `referrer.referralStats.totalEarned`.
|
||||||
|
- Emits **`referral-reward`** to `user-{referrerId}` (`:417`).
|
||||||
|
11. Inside `addPoints` (`:36-113`):
|
||||||
- Transaction-scoped Mongo session.
|
- Transaction-scoped Mongo session.
|
||||||
- `user.points.total += amount; user.points.available += amount`.
|
- `user.points.total += amount; user.points.available += amount`.
|
||||||
- `PointTransaction.create({ type:'earn', source, amount, balance, metadata })`.
|
- `PointTransaction.create({ type:'earn', source, amount, balance, metadata })`. For `source === 'referral'`, `metadata.commission` is set to the amount.
|
||||||
- `updateUserLevel(userId, session)` recomputes the user's tier from `LevelConfig`.
|
- `updateUserLevel(userId, session)` recomputes the user's tier from `LevelConfig`.
|
||||||
- Emits **`level-up`** on `user-{userId}` if the level changed (`:91-99`).
|
- Emits **`level-up`** on `user-{userId}` if the level changed (`:91-99`).
|
||||||
11. Both the referrer and the referee may earn points (e.g. "give 100, get 100" growth model). The current code awards per `addPoints` call — design decision lives in the caller, not in PointsService.
|
12. Note: only the **referrer** earns points via this path. There is no "referee also earns" reward in the current code — the referee gets nothing automatically.
|
||||||
|
|
||||||
### 5. Redemption / payout
|
### 5. Redemption
|
||||||
|
|
||||||
12. Users see their balance under `/dashboard/account/points` and can spend via `POST /api/points/redeem` (e.g. for service-credit or discount codes).
|
13. Users see their balance under `/dashboard/points` and can spend via `POST /api/points/redeem` (applied as a discount against a specific purchase request).
|
||||||
13. `PointTransaction` records `type: 'spend'` with negative `amount`, keeping `balance` running.
|
14. `redeemPoints(userId, pointsToUse, purchaseRequestId)` (`:118-167`):
|
||||||
|
- Requires both `purchaseRequestId` and `pointsToUse` (controller returns `400` if either is missing or `pointsToUse <= 0`).
|
||||||
|
- Throws `Insufficient points` if `user.points.available < pointsToUse`.
|
||||||
|
- Decrements `available`, increments `used`, and records a `PointTransaction` with `type: 'spend'`, `source: 'redemption'`.
|
||||||
|
- The controller computes `discount = pointsToUse * 1000` (1 point = 1000 IRR, **always**) and returns `{ transaction, discount, remainingPoints }`. There are **no** `amount` / `purpose` / `newBalance` / `redemption` fields in the response.
|
||||||
|
|
||||||
## Sequence diagram
|
## Sequence diagram
|
||||||
|
|
||||||
@@ -77,11 +93,11 @@ sequenceDiagram
|
|||||||
participant DB as MongoDB
|
participant DB as MongoDB
|
||||||
participant IO as Socket.IO
|
participant IO as Socket.IO
|
||||||
|
|
||||||
R->>FE: Generate referral code
|
R->>FE: Generate referral code (or auto-assigned)
|
||||||
FE->>BE: POST /api/points/generate-referral-code
|
FE->>BE: POST /api/points/generate-referral-code
|
||||||
BE->>DB: User.findByIdAndUpdate(referralCode=...)
|
BE->>DB: User.findByIdAndUpdate(referralCode=...) (ALWAYS overwrites)
|
||||||
BE-->>FE: { code }
|
BE-->>FE: { referralCode }
|
||||||
R->>R: share https://amn.gg/r/{code}
|
R->>R: share ${NEXT_PUBLIC_API_URL}/r/{code} (backend URL)
|
||||||
|
|
||||||
N->>BE: GET /r/{code}
|
N->>BE: GET /r/{code}
|
||||||
BE-->>N: 302 → /auth/jwt/sign-up?ref={code}
|
BE-->>N: 302 → /auth/jwt/sign-up?ref={code}
|
||||||
@@ -89,67 +105,96 @@ sequenceDiagram
|
|||||||
FE->>BE: POST /api/auth/verify-email-code (with referralCode in TempVerification)
|
FE->>BE: POST /api/auth/verify-email-code (with referralCode in TempVerification)
|
||||||
BE->>DB: User.create
|
BE->>DB: User.create
|
||||||
BE->>DB: referrer.referralStats.totalReferrals += 1
|
BE->>DB: referrer.referralStats.totalReferrals += 1
|
||||||
BE->>IO: emit user-{R} 'referral-signup'
|
BE->>IO: emit user-{R} 'referral-signup' (authController)
|
||||||
|
|
||||||
Note over BE,DB: Later, when N completes a purchase
|
Note over BE,DB: ONLY when N's order reaches status 'completed'
|
||||||
BE->>BE: PointsService.addPoints(R, +X, 'referral', {referredUserId:N})
|
BE->>BE: marketplaceController → PointsService.processReferralReward(id)
|
||||||
BE->>DB: add X points to user balance
|
BE->>BE: addPoints(R, floor(amount*0.02), 'referral', {...})
|
||||||
BE->>DB: create PointTransaction record
|
BE->>DB: add points to balance + create PointTransaction
|
||||||
BE->>BE: updateUserLevel → maybe 'level-up'
|
BE->>BE: updateUserLevel → maybe 'level-up'
|
||||||
BE->>IO: emit user-{R} 'level-up'
|
BE->>IO: emit user-{R} 'level-up'
|
||||||
|
BE->>DB: activeReferrals = count(referredBy=R) (ALL, not just buyers)
|
||||||
|
BE->>IO: emit user-{R} 'referral-reward' (PointsService)
|
||||||
```
|
```
|
||||||
|
|
||||||
## API calls
|
## API calls
|
||||||
|
|
||||||
| Method | Endpoint | Purpose |
|
> [!note] All points routes require authentication
|
||||||
|---|---|---|
|
> `router.use(authenticateToken)` is applied to **every** route in `pointsRoutes.ts:8`. None of these endpoints — including `GET /api/points/levels` — are public.
|
||||||
| `POST` | `/api/points/generate-referral-code` | Generate or rotate referral code |
|
|
||||||
| `GET` | `/api/points/my-points` | Balance + level |
|
| Method | Endpoint | Auth | Body / Query | Response data |
|
||||||
| `GET` | `/api/points/transactions` | History |
|
|---|---|---|---|---|
|
||||||
| `GET` | `/api/points/referrals` | Referred users list |
|
| `POST` | `/api/points/generate-referral-code` | user | (ignored) | `{ referralCode }` — always rotates the code |
|
||||||
| `GET` | `/api/points/leaderboard` | Global top referrers |
|
| `GET` | `/api/points/my-points` | user | — | `{ points, referral, currentLevel, nextLevel }` |
|
||||||
| `GET` | `/api/points/levels` | Level config (public) |
|
| `GET` | `/api/points/transactions` | user | `page`, `limit`, `type` (`earn`/`spend`/`expire` only) | `{ transactions, pagination }` |
|
||||||
| `POST` | `/api/points/redeem` | Spend points |
|
| `GET` | `/api/points/referrals` | user | `page`, `limit` | `{ referrals, pagination }` |
|
||||||
| `POST` | `/api/points/admin/add` | Admin-only manual grant |
|
| `GET` | `/api/points/leaderboard` | user | `limit` only (**`period` is NOT supported**) | `{ leaderboard, total }` |
|
||||||
| `GET` | `/r/:code` | Short-URL redirect to sign-up |
|
| `GET` | `/api/points/levels` | user (**NOT public**) | — | `{ levels }` |
|
||||||
|
| `POST` | `/api/points/redeem` | user | `{ pointsToUse, purchaseRequestId }` (both required) | `{ transaction, discount, remainingPoints }` |
|
||||||
|
| `POST` | `/api/points/admin/add` | admin | `{ userId, amount, description }` | `{ transaction, user, levelChanged, newLevel }` |
|
||||||
|
| `GET` | `/r/:code` | public | — | `302` redirect to sign-up |
|
||||||
|
|
||||||
|
### Endpoint notes (verified against code)
|
||||||
|
|
||||||
|
- **`GET /api/points/transactions` — `type` filter** only accepts `earn`, `spend`, or `expire` (`PointsService.ts:250-265`). There is **no source-based filtering**: you cannot filter by `referral` / `purchase` / `admin` / `redemption`.
|
||||||
|
- **`GET /api/points/leaderboard` — the `period` filter (`all`/`month`/`week`) does not exist and is silently ignored.** `getLeaderboard(limit)` only honors `limit` and always returns all-time data sorted by `totalReferrals` then `totalEarned` (`PointsService.ts:434-479`).
|
||||||
|
- **`POST /api/points/admin/add`** reads `{ userId, amount, description }` (the field is `description`, **not** `reason`). However the `description` is **read but never persisted** — the controller calls `addPoints(userId, amount, 'admin', {})` with an empty metadata object (`pointsController.ts:209`), so admin-granted points store **no human-readable reason**. The stored description is the generic auto-generated `'admin'` label from `getTransactionDescription`.
|
||||||
|
|
||||||
## Database writes
|
## Database writes
|
||||||
|
|
||||||
- **`users`**: `referralCode` on generation, `referredBy` on referee creation, `referralStats.{totalReferrals, activeReferrals, totalEarned}` and `points.{total, available, level}` on point events.
|
- **`users`**: `referralCode` on generation/rotation, `referredBy` on referee creation, `referralStats.{totalReferrals, activeReferrals, totalEarned}` and `points.{total, available, used, level}` on point events. `activeReferrals` is set by `PointsService.processReferralReward` (`:409`) as a count of **all** users with `referredBy = referrer._id`, regardless of purchase history.
|
||||||
- **`pointtransactions`**: one document per earn/spend/refund.
|
- **`pointtransactions`**: one document per `earn` / `spend` event. (`expire` is defined in the schema but **never written** — see below.)
|
||||||
- **`levelconfigs`**: read-only at runtime (seeded at deploy).
|
- **`levelconfigs`**: read-only at runtime (seeded at deploy).
|
||||||
|
|
||||||
## Socket events emitted
|
## Socket events emitted
|
||||||
|
|
||||||
- **`referral-signup`** → `user-{referrerId}` on referee creation.
|
- **`referral-signup`** → `user-{referrerId}` on referee creation — emitted by `authController.ts`; this is an **auth-domain** event (NOT emitted by `PointsService`).
|
||||||
- **`level-up`** → `user-{userId}` when crossing a tier.
|
- **`referral-reward`** → `user-{referrerId}` when `PointsService.processReferralReward` runs — emitted by `PointsService.ts:417`; this is the **points-domain** event. (There is no `referral-signup` emitted from PointsService.)
|
||||||
- **`new-notification`** → standard notification channel for points-related milestones.
|
- **`level-up`** → `user-{userId}` when crossing a tier (`PointsService.ts:92`).
|
||||||
|
|
||||||
## Side effects
|
## Side effects
|
||||||
|
|
||||||
- The referee never sees the referrer's identity unless surfaced in UI.
|
- `points.available` is the spendable balance; `points.total` is the lifetime accumulation (used for level tiers); `points.used` tracks redeemed points.
|
||||||
- `points.available` is the spendable balance; `points.total` is the lifetime accumulation (used for level tiers).
|
- Transactions are wrapped in a Mongo session for atomicity (`addPoints:47-88`, `redeemPoints:123-153`).
|
||||||
- Transactions are wrapped in a Mongo session for atomicity (`addPoints:47-88`).
|
|
||||||
|
|
||||||
## Error / edge cases
|
## Error / edge cases
|
||||||
|
|
||||||
- **Code collision** — extremely unlikely with 32^8 ≈ 1.1 × 10¹² combinations; the while-loop in `generateReferralCode` is a hard guarantee.
|
- **Code collision** — extremely unlikely with 32^8 ≈ 1.1 × 10¹² combinations; the while-loop in `generateReferralCode` is a hard guarantee.
|
||||||
- **Self-referral** — not blocked at controller level. Add a check `if (referrer._id.equals(user._id)) return` in `verifyEmailWithCode` and `googleSignUp` to prevent gaming.
|
- **Self-referral** — **NOT blocked** at any level (see danger callout above). Known gap.
|
||||||
- **Referral code entered with leading/trailing spaces** — `.trim()` is applied (`authController.ts:74`, `:127`).
|
- **Code rotation on regenerate** — calling `generate-referral-code` again replaces the existing code, breaking previously shared links. There is no opt-out.
|
||||||
- **Referrer deleted** — `referredBy` still points to the deleted user; the new user is effectively un-attributed. Soft-delete preservation is acceptable.
|
- **Referrer deleted** — `referredBy` still points to the deleted user; the new user is effectively un-attributed.
|
||||||
- **Points overflow** — `Number` is sufficient up to 2⁵³; no overflow risk in practice.
|
- **Point expiry never enforced** — the `expiresAt` field and the `'expire'` transaction type exist in the schema, and there is a sparse index for expiry sweeps, but **no cron job, TTL index, or service ever creates `expire`-type transactions**. Points never actually expire today.
|
||||||
- **Race on level-up** — the Mongo session ensures `user.points` and `PointTransaction` are atomically updated, but two parallel `addPoints` calls might both trigger level-up emit. Idempotent in practice (frontend shows toast once).
|
- **`activeReferrals` semantics** — counts **all** referred users, not just those who completed a purchase. If conversion tracking is the intent, this counter is misleading.
|
||||||
- **`activeReferrals`** — defined in `referralStats` but no code path increments it currently. Define "active" (e.g. referee has at least one completed purchase) and update accordingly.
|
|
||||||
|
|
||||||
> [!tip] Track conversion, not just sign-ups
|
> [!tip] Track conversion, not just sign-ups
|
||||||
> `totalReferrals` is incremented on sign-up; consider also tracking `convertedReferrals` (completed purchases) to measure real growth value.
|
> `totalReferrals` is incremented on sign-up and `activeReferrals` counts all referees regardless of purchase; neither distinguishes converted referrals. Consider a dedicated `convertedReferrals` counter incremented only inside `processReferralReward`.
|
||||||
|
|
||||||
|
## Frontend coverage (known gaps)
|
||||||
|
|
||||||
|
The following routes are referenced conceptually but **do NOT exist** — navigating to them returns **404**:
|
||||||
|
|
||||||
|
- `/dashboard/points/referrals` — 404 (no page file)
|
||||||
|
- `/dashboard/points/transactions` — 404 (no page file)
|
||||||
|
- `/dashboard/points/levels` — 404 (no page file)
|
||||||
|
|
||||||
|
Only `/dashboard/points` (`frontend/src/app/dashboard/points/page.tsx`) exists.
|
||||||
|
|
||||||
|
The following frontend actions are defined in `frontend/src/actions/points.ts` but have **no UI callers** (dead code from the UI's perspective):
|
||||||
|
|
||||||
|
- `redeemPoints` — no caller.
|
||||||
|
- `generateReferralCode` — no caller (codes are auto-assigned server-side via `getUserPoints`).
|
||||||
|
- `getLevels` — no caller.
|
||||||
|
- `getReferrals` — no caller.
|
||||||
|
- `adminAddPoints` — no caller.
|
||||||
|
|
||||||
|
Only `getMyPoints`, `getTransactions`, and `getLeaderboard` are actually invoked by the UI (`points-main-view.tsx`, `points-leaderboard.tsx`).
|
||||||
|
|
||||||
## Linked flows
|
## Linked flows
|
||||||
|
|
||||||
- [[Registration Flow]] — attribution point.
|
- [[Registration Flow]] — attribution point.
|
||||||
- [[Google OAuth Flow]] — also supports `referralCode`.
|
- [[Google OAuth Flow]] — also supports `referralCode`.
|
||||||
- [[Notification Flow]] — `referral-signup`, `level-up`, and points events surface here.
|
- [[Notification Flow]] — `referral-signup`, `referral-reward`, `level-up` surface here.
|
||||||
- [[PRD - Request Network In-House Checkout]] / [[Escrow Flow]] — completion of a purchase is the canonical trigger for awarding referral commission.
|
- [[Escrow Flow]] — order reaching `'completed'` is the **sole** trigger for awarding referral commission.
|
||||||
|
|
||||||
## Source files
|
## Source files
|
||||||
|
|
||||||
@@ -158,7 +203,8 @@ sequenceDiagram
|
|||||||
- Backend: `backend/src/routes/pointsRoutes.ts`
|
- Backend: `backend/src/routes/pointsRoutes.ts`
|
||||||
- Backend: `backend/src/models/PointTransaction.ts`
|
- Backend: `backend/src/models/PointTransaction.ts`
|
||||||
- Backend: `backend/src/models/LevelConfig.ts`
|
- Backend: `backend/src/models/LevelConfig.ts`
|
||||||
- Backend: `backend/src/services/auth/authController.ts:411-433` (referral attribution on email signup)
|
- Backend: `backend/src/services/marketplace/marketplaceController.ts:473-475` (referral reward triggered ONLY on `'completed'`)
|
||||||
- Backend: `backend/src/services/auth/authController.ts:817-838` (referral on Google signup)
|
- Backend: `backend/src/services/auth/authController.ts` (referral attribution + `referral-signup` emit on email/Google signup)
|
||||||
- Backend: `backend/src/app.ts:274-278` (short-URL redirect)
|
- Backend: `backend/src/app.ts:274-278` (short-URL redirect)
|
||||||
- Frontend: `/dashboard/account/referrals` view (see `frontend/src/sections/account/`)
|
- Frontend: `frontend/src/sections/points/points-invite-friends.tsx:35-36` (builds share URL from `NEXT_PUBLIC_API_URL`)
|
||||||
|
- Frontend: `frontend/src/actions/points.ts` (action layer; several actions have no UI callers)
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ related_apis: ["POST /api/auth/register", "POST /api/auth/verify-email-code", "P
|
|||||||
|
|
||||||
# Registration Flow
|
# Registration 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))
|
||||||
|
|
||||||
End-to-end specification for **email + password** registration with role selection (buyer/seller), six-digit email verification, optional referral code attribution, and terms acceptance.
|
End-to-end specification for **email + password** registration with role selection (buyer/seller), six-digit email verification, optional referral code attribution, and terms acceptance.
|
||||||
|
|
||||||
## Actors
|
## Actors
|
||||||
@@ -53,10 +55,10 @@ stateDiagram-v2
|
|||||||
1. **User visits `/auth/jwt/sign-up`** (optionally with `?ref=ABCD1234` from the short-URL referral redirect implemented at `backend/src/app.ts:274-278`).
|
1. **User visits `/auth/jwt/sign-up`** (optionally with `?ref=ABCD1234` from the short-URL referral redirect implemented at `backend/src/app.ts:274-278`).
|
||||||
2. **User selects role** (buyer or seller), enters email, password (held in client state only), accepts the Terms checkbox, and clicks "Create account".
|
2. **User selects role** (buyer or seller), enters email, password (held in client state only), accepts the Terms checkbox, and clicks "Create account".
|
||||||
|
|
||||||
> [!tip] Password is **not** sent to `/register`
|
> [!bug] ⚠️ KNOWN BUG / quirk — the sign-up form does not collect the real password
|
||||||
> The password is only included in the second step (`/verify-email-code`). The intent: never hash and store a password for an unverified account. The TempVerification document carries `password: ''` until verification.
|
> `jwt-sign-up-view.tsx` `onSubmit` calls `signUp({ ..., password: '' })` with a **hard-coded empty string** (`jwt-sign-up-view.tsx:191`, with the inline comment `// You might need to add password field to form`). So the actual password is **not** collected on the sign-up form at all — it is collected at the **email-verification step** (`/verify-email-code`). The `TempVerification.password` field is effectively **unused** (it is set to `''` and never read as a real credential). The credential that ends up on the `User` is the one entered at verification.
|
||||||
|
|
||||||
3. **HTTP request**: `POST /api/auth/register` with `{ email, password?, firstName?, lastName?, role, referralCode? }`. (The frontend currently passes the password through, but the controller stores `''` regardless — see `authController.ts:123`.)
|
3. **HTTP request**: `POST /api/auth/register` with `{ email, password: '', firstName?, lastName?, role, referralCode? }`. The frontend passes `password: ''` (empty string) — see the quirk above. The controller persists this empty string into `TempVerification.password`, which is never used as a real credential.
|
||||||
4. **Validation middleware** `registerValidation` (`authValidation.ts`) checks email format, password complexity, and role enum.
|
4. **Validation middleware** `registerValidation` (`authValidation.ts`) checks email format, password complexity, and role enum.
|
||||||
5. **Duplicate check** (`authController.ts:55-64`): `User.findOne({ email })` — if found, returns `409 USER_EXISTS`.
|
5. **Duplicate check** (`authController.ts:55-64`): `User.findOne({ email })` — if found, returns `409 USER_EXISTS`.
|
||||||
6. **Idempotent temp record**: `TempVerification.findOne({ email })` — if present, the existing temp is **updated in place** (new name, role, referralCode, fresh 6-digit code, expiry pushed to now + 15 min).
|
6. **Idempotent temp record**: `TempVerification.findOne({ email })` — if present, the existing temp is **updated in place** (new name, role, referralCode, fresh 6-digit code, expiry pushed to now + 15 min).
|
||||||
@@ -74,10 +76,11 @@ stateDiagram-v2
|
|||||||
15. **Lookup**: `TempVerification.findOne({ email, emailVerificationCode: code, emailVerificationCodeExpires: { $gt: now } })` — if any field mismatches or the code is older than 15 minutes, returns `400`.
|
15. **Lookup**: `TempVerification.findOne({ email, emailVerificationCode: code, emailVerificationCodeExpires: { $gt: now } })` — if any field mismatches or the code is older than 15 minutes, returns `400`.
|
||||||
16. **Hash password**: `bcrypt.hash(password, 12)` via `authService.hashPassword()`.
|
16. **Hash password**: `bcrypt.hash(password, 12)` via `authService.hashPassword()`.
|
||||||
17. **Create `User`** (`authController.ts:400-435`): `email`, `password: hashedPassword`, `firstName`, `lastName`, `role`, `isEmailVerified: true`, `status: "active"`.
|
17. **Create `User`** (`authController.ts:400-435`): `email`, `password: hashedPassword`, `firstName`, `lastName`, `role`, `isEmailVerified: true`, `status: "active"`.
|
||||||
18. **Apply referral** (`authController.ts:411-433`): if `tempVerification.referralCode` exists, find the referrer by `User.findOne({ referralCode })`. If found:
|
18. **Apply referral** (`authController.ts:691-713`): `tempVerification.referralCode` (stored on the `TempVerification` document at registration and applied here at verification) is looked up via `User.findOne({ referralCode })`. If a referrer is found:
|
||||||
- `user.referredBy = referrer._id`
|
- `user.referredBy = referrer._id`
|
||||||
- `referrer.referralStats.totalReferrals += 1`
|
- `referrer.referralStats.totalReferrals += 1`
|
||||||
- Emit `referral-signup` on `user-${referrer._id}` Socket.IO room — see [[Referral Flow]] for the points-awarding side effect that happens later on the first purchase.
|
- Emit `referral-signup` on `user-${referrer._id}` Socket.IO room (`authController.ts:704`; the equivalent Google/other path emits at `authController.ts:1132`) — see [[Referral Flow]] for the points-awarding side effect that happens later on the first purchase.
|
||||||
|
- ⚠️ **No self-referral guard**: the code only checks `if (referrer)` — it never compares `referrer._id` to the newly created user. A user who somehow signs up with their own `referralCode` would be attributed as their own referrer.
|
||||||
19. **Persist user**, then **delete** the TempVerification document (`findByIdAndDelete`).
|
19. **Persist user**, then **delete** the TempVerification document (`findByIdAndDelete`).
|
||||||
20. **Token issuance**: identical to [[Authentication Flow]] — generate access + refresh, push the refresh into `user.refreshTokens[]`.
|
20. **Token issuance**: identical to [[Authentication Flow]] — generate access + refresh, push the refresh into `user.refreshTokens[]`.
|
||||||
21. **Response**: `{ user, tokens: { accessToken, refreshToken } }`. Frontend writes both into `localStorage` (`action.ts:228-235`) and routes the user into the appropriate dashboard (`/dashboard/buyer` or `/dashboard/seller`).
|
21. **Response**: `{ user, tokens: { accessToken, refreshToken } }`. Frontend writes both into `localStorage` (`action.ts:228-235`) and routes the user into the appropriate dashboard (`/dashboard/buyer` or `/dashboard/seller`).
|
||||||
@@ -139,9 +142,9 @@ sequenceDiagram
|
|||||||
|
|
||||||
## Database writes
|
## Database writes
|
||||||
|
|
||||||
- **`tempverifications` collection**: insert on first POST, in-place update on duplicate POST (`authController.ts:66-108`), delete on successful verification.
|
- **`tempverifications` collection**: insert on first POST (carrying `email`, `password: ''`, `firstName`, `lastName`, `role`, `referralCode`, code + expiry), in-place update on duplicate POST, delete on successful verification.
|
||||||
- **`users` collection**: full insert on successful verification (`authController.ts:400-435`). The first refresh token is appended in the same save.
|
- **`users` collection**: full insert on successful verification (`authController.ts:680-688`). The first refresh token is appended in the same save.
|
||||||
- **`users` collection (referrer)**: `referralStats.totalReferrals` incremented (`authController.ts:419`).
|
- **`users` collection (referrer)**: `referralStats.totalReferrals` incremented (`authController.ts:699`).
|
||||||
|
|
||||||
## Socket events emitted
|
## Socket events emitted
|
||||||
|
|
||||||
@@ -149,7 +152,7 @@ sequenceDiagram
|
|||||||
```
|
```
|
||||||
{ userId, userName, userEmail, timestamp, totalReferrals }
|
{ userId, userName, userEmail, timestamp, totalReferrals }
|
||||||
```
|
```
|
||||||
Source: `authController.ts:423-431`.
|
Source: `authController.ts:704-710` (and `:1132` on the parallel path).
|
||||||
|
|
||||||
## Side effects
|
## Side effects
|
||||||
|
|
||||||
@@ -168,6 +171,7 @@ sequenceDiagram
|
|||||||
- **Code format wrong (non-digits or wrong length)** → `400` from `isValidVerificationCode` guard before DB lookup.
|
- **Code format wrong (non-digits or wrong length)** → `400` from `isValidVerificationCode` guard before DB lookup.
|
||||||
- **Email delivery failure** → response still `201`/`200`; the user can hit "Resend" or check spam.
|
- **Email delivery failure** → response still `201`/`200`; the user can hit "Resend" or check spam.
|
||||||
- **Referral code that does not match any user** → silently ignored; the user is still created with `referredBy: undefined`.
|
- **Referral code that does not match any user** → silently ignored; the user is still created with `referredBy: undefined`.
|
||||||
|
- **Self-referral** → **not guarded**. The referral attribution (`authController.ts:691-713`) only checks that a referrer exists, never that it differs from the signing-up user.
|
||||||
- **Race condition: two parallel registrations for the same email** → MongoDB unique index on `User.email` ensures only one user document; the loser of the race sees `E11000` and returns `409 USER_EXISTS`.
|
- **Race condition: two parallel registrations for the same email** → MongoDB unique index on `User.email` ensures only one user document; the loser of the race sees `E11000` and returns `409 USER_EXISTS`.
|
||||||
- **Race condition: verify request arrives twice with the same code** → second request finds no TempVerification and returns `400`. The created `User` is the canonical record.
|
- **Race condition: verify request arrives twice with the same code** → second request finds no TempVerification and returns `400`. The created `User` is the canonical record.
|
||||||
- **Role tampering** → role is validated by `registerValidation` enum (`buyer | seller`). Admin role is created only via the bootstrap seed (`initializeAdminUser` in `app.ts:377`), never via this flow.
|
- **Role tampering** → role is validated by `registerValidation` enum (`buyer | seller`). Admin role is created only via the bootstrap seed (`initializeAdminUser` in `app.ts:377`), never via this flow.
|
||||||
|
|||||||
@@ -136,7 +136,7 @@ sequenceDiagram
|
|||||||
end
|
end
|
||||||
BE->>N: notifyNewOfferReceived
|
BE->>N: notifyNewOfferReceived
|
||||||
N->>IO: emit notification to buyer
|
N->>IO: emit notification to buyer
|
||||||
BE->>IO: emit seller new-offer
|
BE->>IO: emit new-offer to buyer-{buyerId}
|
||||||
BE-->>FE_S: 200 { offer }
|
BE-->>FE_S: 200 { offer }
|
||||||
IO-->>FE_B: notify buyer bell icon
|
IO-->>FE_B: notify buyer bell icon
|
||||||
B->>FE_B: Open request detail
|
B->>FE_B: Open request detail
|
||||||
@@ -171,6 +171,7 @@ sequenceDiagram
|
|||||||
## Socket events emitted
|
## Socket events emitted
|
||||||
|
|
||||||
- **`seller-offer-update`** with `eventType: 'new-offer'` → `seller-{sellerId}` (creator's other tabs).
|
- **`seller-offer-update`** with `eventType: 'new-offer'` → `seller-{sellerId}` (creator's other tabs).
|
||||||
|
- **`new-offer`** → `buyer-{buyerId}` room — emitted directly by `marketplaceController.ts` on offer creation; `use-marketplace-socket.ts` (lines 300, 497) listens on this event to update the buyer's offer list in real time.
|
||||||
- **`purchase-request-update`** with `eventType: 'offer-updated'` → `request-{requestId}` on edits (`SellerOfferService.ts:284-288`).
|
- **`purchase-request-update`** with `eventType: 'offer-updated'` → `request-{requestId}` on edits (`SellerOfferService.ts:284-288`).
|
||||||
- **`purchase-request-update`** → `request-{requestId}` when buyer calls `select-offer` (generic room event only — no per-seller notifications or events are sent to winning or losing sellers).
|
- **`purchase-request-update`** → `request-{requestId}` when buyer calls `select-offer` (generic room event only — no per-seller notifications or events are sent to winning or losing sellers).
|
||||||
- **`seller-offer-update`** with `eventType: 'payment-completed'` to winning seller, `'offer-rejected'` to losers (emitted by the webhook handler after payment confirmation).
|
- **`seller-offer-update`** with `eventType: 'payment-completed'` to winning seller, `'offer-rejected'` to losers (emitted by the webhook handler after payment confirmation).
|
||||||
|
|||||||
@@ -1,9 +1,19 @@
|
|||||||
|
> **Last updated:** 2026-05-29 — aligned with code (see Doc vs Code Audit Report)
|
||||||
|
|
||||||
# Trezor Safekeeping Flow
|
# Trezor Safekeeping Flow
|
||||||
|
|
||||||
This flow adds hardware-backed custody controls without replacing the current payment model. The backend never stores private keys. Trezor support starts as a single hardware signer and is designed to upgrade to multisig later.
|
This flow adds hardware-backed custody controls without replacing the current payment model. The backend never stores private keys. Trezor support starts as a single hardware signer and is designed to upgrade to multisig later.
|
||||||
|
|
||||||
Default mode: optional. Existing release/refund flows do not require Trezor proof unless `TREZOR_SAFEKEEPING_REQUIRED=true`.
|
Default mode: optional. Existing release/refund flows do not require Trezor proof unless `TREZOR_SAFEKEEPING_REQUIRED=true`.
|
||||||
|
|
||||||
|
> **Note (corrected 2026-05-29):** The frontend Trezor implementation **does exist** in current code — the 2026-05-29 audit's "zero frontend implementation" claim was based on an older snapshot. The active surface is:
|
||||||
|
> - `src/app/dashboard/admin/trezor/page.tsx` → `TrezorSettingsView` (registration + re-register UI)
|
||||||
|
> - `src/web3/trezor/trezorConnector.ts` → lazy-imports `@trezor/connect-web` (`trezorGetXpub`, `trezorGetAddress`, `trezorSignMessage`)
|
||||||
|
> - `src/components/trezor-sign-dialog/TrezorSignDialog.tsx` → build-instruction → sign-on-Trezor → enter-txHash → confirm
|
||||||
|
> - `src/actions/trezor.ts` → full API client (`getTrezorAccount`, `getTrezorRegistrationMessage`, `registerTrezor`, `getTrezorOperationMessage`, `confirmRelease`/`confirmRefund`) that **builds the `trezor: { message, signature }` object**
|
||||||
|
>
|
||||||
|
> The legacy `confirmReleaseTx`/`confirmRefundTx` helpers in `src/actions/payment.ts` post only `{ txHash }` (no `trezor` field), but they have **no UI callers** — the active admin release/refund path goes through `TrezorSignDialog` → `actions/trezor.ts`, which satisfies the `assertTrezorSignatureForOperation` guard when `TREZOR_SAFEKEEPING_REQUIRED=true`.
|
||||||
|
|
||||||
## Goals
|
## Goals
|
||||||
|
|
||||||
- Generate a fresh receive address per user/payment from a registered Trezor xpub.
|
- Generate a fresh receive address per user/payment from a registered Trezor xpub.
|
||||||
@@ -11,14 +21,19 @@ Default mode: optional. Existing release/refund flows do not require Trezor proo
|
|||||||
- Keep the Request Network payment adapter and legacy provider abstractions intact while adding custody controls.
|
- Keep the Request Network payment adapter and legacy provider abstractions intact while adding custody controls.
|
||||||
- Preserve the existing `Payment` model and orchestration surface.
|
- Preserve the existing `Payment` model and orchestration surface.
|
||||||
|
|
||||||
|
## Actors
|
||||||
|
|
||||||
|
- **Admin** — the only party who can request operation messages and submit verify-operation calls. The registered Trezor must belong to an admin account; the safekeeping guard validates against the admin's `TrezorAccount.registrationAddress`.
|
||||||
|
- **Any authenticated user** — may call `POST /api/trezor/register` (no role restriction on that endpoint).
|
||||||
|
|
||||||
## Registration
|
## Registration
|
||||||
|
|
||||||
1. User connects a Trezor in the frontend and exports an Ethereum account xpub, for example `m/44'/60'/0'`.
|
1. The Trezor owner (typically an admin) connects a Trezor and exports an Ethereum account xpub, for example `m/44'/60'/0'`.
|
||||||
2. Backend builds a registration challenge:
|
2. Backend builds a registration challenge:
|
||||||
- `GET /api/trezor/registration-message?xpub=...®istrationAddress=...`
|
- `GET /api/trezor/registration-message?xpub=...®istrationAddress=...`
|
||||||
3. The registration address must be the first derived address from the xpub:
|
3. The registration address must be the first derived address from the xpub:
|
||||||
- `m/44'/60'/0'/0/0`
|
- `m/44'/60'/0'/0/0`
|
||||||
4. User signs the challenge with that Trezor address.
|
4. The owner signs the challenge with that Trezor address.
|
||||||
5. Frontend submits:
|
5. Frontend submits:
|
||||||
- `POST /api/trezor/register`
|
- `POST /api/trezor/register`
|
||||||
- `xpub`
|
- `xpub`
|
||||||
@@ -30,14 +45,7 @@ Default mode: optional. Existing release/refund flows do not require Trezor proo
|
|||||||
- xpub is public, not private.
|
- xpub is public, not private.
|
||||||
- registration address matches xpub-derived index `0`.
|
- registration address matches xpub-derived index `0`.
|
||||||
- signature recovers the registration address.
|
- signature recovers the registration address.
|
||||||
7. Backend stores only:
|
7. Backend stores / updates the `TrezorAccount` record. **Upsert behaviour:** if a record already exists for the user, `xpub`, `basePath`, and `label` are updated, but `nextAddressIndex` and the existing `addresses` array are preserved via `$setOnInsert`. Old address records continue to reference the previous xpub — a xpub mismatch is therefore possible after re-registration.
|
||||||
- `userId`
|
|
||||||
- xpub fingerprint
|
|
||||||
- xpub
|
|
||||||
- base derivation path
|
|
||||||
- registration address
|
|
||||||
- next address index
|
|
||||||
- issued address records
|
|
||||||
|
|
||||||
## Address Generation
|
## Address Generation
|
||||||
|
|
||||||
@@ -51,6 +59,15 @@ POST /api/trezor/addresses/next
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Valid values for `purpose` (as enumerated in the schema):
|
||||||
|
|
||||||
|
| Value | Description |
|
||||||
|
|---|---|
|
||||||
|
| `deposit` | Incoming payment address |
|
||||||
|
| `release` | Address used in a release operation |
|
||||||
|
| `refund` | Address used in a refund operation |
|
||||||
|
| `other` | General-purpose address |
|
||||||
|
|
||||||
The backend derives non-hardened receive addresses from the registered xpub:
|
The backend derives non-hardened receive addresses from the registered xpub:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
@@ -59,9 +76,9 @@ m/44'/60'/0'/0/{index}
|
|||||||
|
|
||||||
If a `paymentId` already has an address, the endpoint returns the same address instead of incrementing the index.
|
If a `paymentId` already has an address, the endpoint returns the same address instead of incrementing the index.
|
||||||
|
|
||||||
## Transaction Approval
|
## Transaction Approval (Admin-only)
|
||||||
|
|
||||||
Before a release/refund confirmation, the admin asks the backend for the exact operation message:
|
`POST /api/trezor/operation-message` and `POST /api/trezor/verify-operation` are admin-only endpoints. Before a release/refund confirmation, the admin asks the backend for the exact operation message:
|
||||||
|
|
||||||
```http
|
```http
|
||||||
POST /api/trezor/operation-message
|
POST /api/trezor/operation-message
|
||||||
@@ -75,19 +92,17 @@ POST /api/trezor/operation-message
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
The Trezor signs that message. Release/refund confirmation then includes:
|
The Trezor signs that message and the admin submits it. **The frontend implements this flow** via `TrezorSignDialog`, which calls `getTrezorOperationMessage()`, prompts the Trezor to sign, and then submits the release/refund confirmation through `confirmRelease()` / `confirmRefund()` in `src/actions/trezor.ts` with the full payload:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"txHash": "0x...",
|
"txHash": "0x...",
|
||||||
"trezor": {
|
"amount": 100,
|
||||||
"message": "Amanat escrow Trezor transaction approval\n...",
|
"trezor": { "message": "<canonical operation message>", "signature": "0x..." }
|
||||||
"signature": "0x..."
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
When `TREZOR_SAFEKEEPING_REQUIRED=true`, `confirmReleaseRefundInstruction` verifies the signature before calling the payment adapter confirmation path.
|
The `trezor` object is included whenever a signature was produced, satisfying the backend `assertTrezorSignatureForOperation` guard. (The older `confirmReleaseTx`/`confirmRefundTx` helpers in `src/actions/payment.ts` post only `{ txHash }`, but they are unused legacy code with no UI callers.)
|
||||||
|
|
||||||
## Enforcement Flag
|
## Enforcement Flag
|
||||||
|
|
||||||
@@ -95,7 +110,7 @@ When `TREZOR_SAFEKEEPING_REQUIRED=true`, `confirmReleaseRefundInstruction` verif
|
|||||||
TREZOR_SAFEKEEPING_REQUIRED=false
|
TREZOR_SAFEKEEPING_REQUIRED=false
|
||||||
```
|
```
|
||||||
|
|
||||||
Default is permissive so existing Request Network release/refund flows continue to work. Set it to `true` only after registering the operating admin's Trezor account and testing the signing path. Any value other than the literal string `true` is treated as disabled.
|
Default is permissive so existing Request Network release/refund flows continue to work. Set it to `true` only after registering the operating admin's Trezor account (the frontend signing flow via `TrezorSignDialog` is already implemented). Any value other than the literal string `true` is treated as disabled.
|
||||||
|
|
||||||
## Safety Rules
|
## Safety Rules
|
||||||
|
|
||||||
|
|||||||
@@ -1,50 +0,0 @@
|
|||||||
---
|
|
||||||
issue: "001"
|
|
||||||
title: "PATCH /api/disputes/:id/status has no role guard — privilege escalation"
|
|
||||||
severity: critical
|
|
||||||
domain: dispute
|
|
||||||
labels: [security, backend, bug]
|
|
||||||
status: open
|
|
||||||
created: 2026-05-29
|
|
||||||
source: Doc vs Code Audit 2026-05-29
|
|
||||||
---
|
|
||||||
|
|
||||||
# 🔴 PATCH /api/disputes/:id/status has no role guard — privilege escalation
|
|
||||||
|
|
||||||
**Severity:** critical
|
|
||||||
**Domain:** dispute
|
|
||||||
**Labels:** security, backend, bug
|
|
||||||
|
|
||||||
## Description
|
|
||||||
|
|
||||||
`PATCH /api/disputes/:id/status` is mounted with only `authenticateToken` middleware — no `authorizeRoles('admin')` guard. Any authenticated buyer or seller who knows a dispute `_id` can change that dispute's status to `resolved`, `closed`, or any other value including states that release funds or trigger bans.
|
|
||||||
|
|
||||||
## Current Behavior
|
|
||||||
|
|
||||||
Any authenticated user (buyer or seller) can call:
|
|
||||||
```
|
|
||||||
PATCH /api/disputes/{disputeId}/status
|
|
||||||
{ "status": "resolved" }
|
|
||||||
```
|
|
||||||
and receive a 200 response. The dispute status is updated in MongoDB.
|
|
||||||
|
|
||||||
## Expected Behavior
|
|
||||||
|
|
||||||
Only users with `role: admin` should be permitted to change a dispute's status. Non-admin tokens should receive `403 Forbidden`.
|
|
||||||
|
|
||||||
## Reproduction Steps
|
|
||||||
|
|
||||||
1. Log in as a buyer or seller, obtain a JWT.
|
|
||||||
2. Find or create a dispute `_id`.
|
|
||||||
3. `PATCH /api/disputes/{id}/status` with `{ "status": "resolved" }` and the buyer/seller Bearer token.
|
|
||||||
4. Observe 200 and the status change in the DB.
|
|
||||||
|
|
||||||
## Affected Files
|
|
||||||
|
|
||||||
- `backend/src/routes/disputeRoutes.ts` — router missing `authorizeRoles('admin')` before `updateStatus` handler
|
|
||||||
- `backend/src/controllers/disputeController.ts` — `updateStatus` method
|
|
||||||
|
|
||||||
## References
|
|
||||||
|
|
||||||
- [Doc vs Code Audit Report](../09%20-%20Audits/Doc%20vs%20Code%20Audit%20Report%20-%202026-05-29.md) — Finding C16
|
|
||||||
- Related: [[ISSUE-002-dispute-resolve-no-role-guard]]
|
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
---
|
||||||
|
issue: 001
|
||||||
|
title: "PATCH /api/disputes/:id/status and POST /api/disputes/:id/resolve have no role guard — privilege escalation"
|
||||||
|
severity: critical
|
||||||
|
domain: Dispute
|
||||||
|
labels: [security, bug, backend, privilege-escalation]
|
||||||
|
status: open
|
||||||
|
created: 2026-05-29
|
||||||
|
source: Doc vs Code Audit 2026-05-29
|
||||||
|
---
|
||||||
|
|
||||||
|
# 🔴 PATCH /api/disputes/:id/status and POST /api/disputes/:id/resolve have no role guard — privilege escalation
|
||||||
|
|
||||||
|
**Severity:** critical
|
||||||
|
**Domain:** Dispute
|
||||||
|
**Labels:** security, bug, backend, privilege-escalation
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
Any authenticated buyer or seller can change dispute status to 'resolved', 'closed', or 'rejected', and can post a dispute resolution including action=ban_seller. Neither the dashboard updateStatus controller nor the resolveDispute controller call authorizeRoles('admin'). Only authenticateToken is applied on the router.
|
||||||
|
|
||||||
|
## Current Behavior
|
||||||
|
|
||||||
|
Any authenticated user with the dispute ID can call PATCH /api/disputes/:id/status or POST /api/disputes/:id/resolve and receive 200 with the mutation applied.
|
||||||
|
|
||||||
|
## Expected Behavior
|
||||||
|
|
||||||
|
Both endpoints should return 403 for non-admin users. authorizeRoles('admin') middleware should be applied at the route level.
|
||||||
|
|
||||||
|
## Affected Files
|
||||||
|
|
||||||
|
- `backend/src/routes/disputeRoutes.ts`
|
||||||
|
- `backend/src/controllers/disputeController.ts`
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Doc vs Code Audit Report](../09%20-%20Audits/Doc%20vs%20Code%20Audit%20Report%20-%202026-05-29.md)
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
---
|
|
||||||
issue: "002"
|
|
||||||
title: "POST /api/disputes/:id/resolve has no role guard — any user can resolve disputes and ban sellers"
|
|
||||||
severity: critical
|
|
||||||
domain: dispute
|
|
||||||
labels: [security, backend, bug]
|
|
||||||
status: open
|
|
||||||
created: 2026-05-29
|
|
||||||
source: Doc vs Code Audit 2026-05-29
|
|
||||||
---
|
|
||||||
|
|
||||||
# 🔴 POST /api/disputes/:id/resolve has no role guard — any user can resolve disputes and ban sellers
|
|
||||||
|
|
||||||
**Severity:** critical
|
|
||||||
**Domain:** dispute
|
|
||||||
**Labels:** security, backend, bug
|
|
||||||
|
|
||||||
## Description
|
|
||||||
|
|
||||||
The dashboard dispute router's `POST /api/disputes/:id/resolve` handler applies only `authenticateToken`. No `authorizeRoles('admin')` guard exists. Any authenticated user can post any resolution action including `action: 'ban_seller'`, `action: 'refund'`, or `action: 'no_action'`, bypassing all admin authority.
|
|
||||||
|
|
||||||
Note: the *releaseHold* router's `POST /api/disputes/:purchaseRequestId/resolve` correctly uses `authorizeRoles('admin')`, but the dashboard router does not.
|
|
||||||
|
|
||||||
## Current Behavior
|
|
||||||
|
|
||||||
A buyer or seller can call:
|
|
||||||
```
|
|
||||||
POST /api/disputes/{disputeId}/resolve
|
|
||||||
{ "action": "ban_seller", "notes": "malicious" }
|
|
||||||
```
|
|
||||||
The resolution is persisted with a 200 response.
|
|
||||||
|
|
||||||
## Expected Behavior
|
|
||||||
|
|
||||||
`POST /api/disputes/:id/resolve` must be protected by `authorizeRoles('admin')`. Non-admin tokens should receive `403`.
|
|
||||||
|
|
||||||
## Affected Files
|
|
||||||
|
|
||||||
- `backend/src/routes/disputeRoutes.ts` (dashboard router, mounted at `/api/disputes` first)
|
|
||||||
- `backend/src/controllers/disputeController.ts` — `resolveDispute` method
|
|
||||||
|
|
||||||
## References
|
|
||||||
|
|
||||||
- [Doc vs Code Audit Report](../09%20-%20Audits/Doc%20vs%20Code%20Audit%20Report%20-%202026-05-29.md) — Finding C17
|
|
||||||
- Related: [[ISSUE-001-dispute-status-no-role-guard]], [[ISSUE-003-dispute-route-shadowing]]
|
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
---
|
||||||
|
issue: 002
|
||||||
|
title: "POST /api/disputes/:id/assign has no role guard — any user can self-assign as admin"
|
||||||
|
severity: critical
|
||||||
|
domain: Dispute
|
||||||
|
labels: [security, bug, backend, privilege-escalation]
|
||||||
|
status: open
|
||||||
|
created: 2026-05-29
|
||||||
|
source: Doc vs Code Audit 2026-05-29
|
||||||
|
---
|
||||||
|
|
||||||
|
# 🔴 POST /api/disputes/:id/assign has no role guard — any user can self-assign as admin
|
||||||
|
|
||||||
|
**Severity:** critical
|
||||||
|
**Domain:** Dispute
|
||||||
|
**Labels:** security, bug, backend, privilege-escalation
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
The POST /api/disputes/:id/assign endpoint registers only authenticateToken. Any authenticated user can assign themselves or anyone else as the admin handler for a dispute. The admin check is absent at both the middleware and controller level.
|
||||||
|
|
||||||
|
## Current Behavior
|
||||||
|
|
||||||
|
Any authenticated buyer or seller can call POST /api/disputes/:id/assign and become the assigned admin for the dispute.
|
||||||
|
|
||||||
|
## Expected Behavior
|
||||||
|
|
||||||
|
Return 403 for non-admin tokens. Apply authorizeRoles('admin') at the route level.
|
||||||
|
|
||||||
|
## Affected Files
|
||||||
|
|
||||||
|
- `backend/src/routes/disputeRoutes.ts`
|
||||||
|
- `backend/src/controllers/disputeController.ts`
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Doc vs Code Audit Report](../09%20-%20Audits/Doc%20vs%20Code%20Audit%20Report%20-%202026-05-29.md)
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
---
|
|
||||||
issue: "003"
|
|
||||||
title: "Route shadowing: two dispute routers mounted at /api/disputes cause non-deterministic handler dispatch"
|
|
||||||
severity: critical
|
|
||||||
domain: dispute
|
|
||||||
labels: [backend, bug]
|
|
||||||
status: open
|
|
||||||
created: 2026-05-29
|
|
||||||
source: Doc vs Code Audit 2026-05-29
|
|
||||||
---
|
|
||||||
|
|
||||||
# 🔴 Route shadowing: two dispute routers mounted at /api/disputes cause non-deterministic handler dispatch
|
|
||||||
|
|
||||||
**Severity:** critical
|
|
||||||
**Domain:** dispute
|
|
||||||
**Labels:** backend, bug
|
|
||||||
|
|
||||||
## Description
|
|
||||||
|
|
||||||
In `backend/src/app.ts`, two separate dispute routers are mounted on the same path `/api/disputes`:
|
|
||||||
- Line ~521: `dashboardDisputeRoutes` (first — unguarded `POST /:id/resolve`, `PATCH /:id/status`)
|
|
||||||
- Line ~585: `releaseHold disputeRoutes` (second — admin-guarded `POST /:purchaseRequestId/resolve`, also `GET /:purchaseRequestId/status`)
|
|
||||||
|
|
||||||
Express evaluates in registration order. A `POST /api/disputes/{purchaseRequestId}/resolve` request will match the **dashboard router's** `POST /:id/resolve` handler first (since `:id` and `:purchaseRequestId` are identical route patterns). This executes the unguarded Dispute CRUD resolve instead of the admin-guarded escrow release-hold logic.
|
|
||||||
|
|
||||||
## Current Behavior
|
|
||||||
|
|
||||||
`POST /api/disputes/{purchaseRequestId}/resolve` executes the dashboard `resolveDispute` controller (updates the Dispute document only, no role guard) rather than the intended `releaseHold` handler (admin-only, clears escrow).
|
|
||||||
|
|
||||||
## Expected Behavior
|
|
||||||
|
|
||||||
The escrow-release resolve handler should be reachable at a distinct, unambiguous path (e.g., `/api/disputes/hold/:purchaseRequestId/resolve` or mounted at a different prefix).
|
|
||||||
|
|
||||||
## Affected Files
|
|
||||||
|
|
||||||
- `backend/src/app.ts` — two `app.use('/api/disputes', ...)` mount points
|
|
||||||
|
|
||||||
## References
|
|
||||||
|
|
||||||
- [Doc vs Code Audit Report](../09%20-%20Audits/Doc%20vs%20Code%20Audit%20Report%20-%202026-05-29.md) — Finding C18
|
|
||||||
- Related: [[ISSUE-002-dispute-resolve-no-role-guard]]
|
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
---
|
||||||
|
issue: 003
|
||||||
|
title: "Route shadowing: POST /api/disputes/:purchaseRequestId/resolve matches dashboard router first and executes wrong handler"
|
||||||
|
severity: critical
|
||||||
|
domain: Dispute
|
||||||
|
labels: [bug, backend, critical, escrow]
|
||||||
|
status: open
|
||||||
|
created: 2026-05-29
|
||||||
|
source: Doc vs Code Audit 2026-05-29
|
||||||
|
---
|
||||||
|
|
||||||
|
# 🔴 Route shadowing: POST /api/disputes/:purchaseRequestId/resolve matches dashboard router first and executes wrong handler
|
||||||
|
|
||||||
|
**Severity:** critical
|
||||||
|
**Domain:** Dispute
|
||||||
|
**Labels:** bug, backend, critical, escrow
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
Both the dashboard disputeRoutes and the releaseHold disputeRoutes are mounted at /api/disputes in app.ts. The dashboard router is mounted first (line 521). A POST /api/disputes/{purchaseRequestId}/resolve with a valid purchaseRequestId will match the dashboard router's POST /:id/resolve (Dispute CRUD resolve) before reaching the releaseHold router's escrow-unblocking resolve. The escrow hold is never cleared.
|
||||||
|
|
||||||
|
## Current Behavior
|
||||||
|
|
||||||
|
The dashboard router intercepts the request and executes Dispute model CRUD resolve only. Escrow hold is not cleared. Outcome is non-deterministic depending on whether the ID matches a Dispute _id.
|
||||||
|
|
||||||
|
## Expected Behavior
|
||||||
|
|
||||||
|
POST /api/disputes/:purchaseRequestId/resolve should reach the releaseHold handler and clear the escrow hold. Route registration order must be corrected or paths made unambiguous.
|
||||||
|
|
||||||
|
## Reproduction Steps
|
||||||
|
|
||||||
|
POST /api/disputes/{validPurchaseRequestId}/resolve with admin token — observe that escrow hold is NOT released, only the Dispute document is updated.
|
||||||
|
|
||||||
|
## Affected Files
|
||||||
|
|
||||||
|
- `backend/src/app.ts`
|
||||||
|
- `backend/src/routes/disputeRoutes.ts`
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Doc vs Code Audit Report](../09%20-%20Audits/Doc%20vs%20Code%20Audit%20Report%20-%202026-05-29.md)
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
---
|
|
||||||
issue: "004"
|
|
||||||
title: "fetch-tx, auto-fetch-missing, and debug payment endpoints have no authentication"
|
|
||||||
severity: critical
|
|
||||||
domain: payment
|
|
||||||
labels: [security, backend, bug]
|
|
||||||
status: open
|
|
||||||
created: 2026-05-29
|
|
||||||
source: Doc vs Code Audit 2026-05-29
|
|
||||||
---
|
|
||||||
|
|
||||||
# 🔴 fetch-tx, auto-fetch-missing, and debug payment endpoints have no authentication
|
|
||||||
|
|
||||||
**Severity:** critical
|
|
||||||
**Domain:** payment
|
|
||||||
**Labels:** security, backend, bug
|
|
||||||
|
|
||||||
## Description
|
|
||||||
|
|
||||||
Three backend payment endpoints are mounted with **no `authenticateToken` middleware**, despite being documented as admin-only:
|
|
||||||
|
|
||||||
1. `POST /api/payment/payments/:id/fetch-tx` — triggers on-chain transaction fetch for a payment
|
|
||||||
2. `POST /api/payment/payments/auto-fetch-missing` — triggers bulk on-chain fetch for all pending payments
|
|
||||||
3. `GET /api/payment/payments/:id/debug` — returns full payment document including blockchain metadata and wallet monitor state
|
|
||||||
|
|
||||||
Any unauthenticated caller (no Authorization header needed) can call all three endpoints.
|
|
||||||
|
|
||||||
## Current Behavior
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -X POST https://api.example.com/api/payment/payments/anyId/fetch-tx
|
|
||||||
# Returns 200 and triggers on-chain state write
|
|
||||||
```
|
|
||||||
|
|
||||||
## Expected Behavior
|
|
||||||
|
|
||||||
All three endpoints should require `authenticateToken` + `authorizeRoles('admin')` and return `401` without credentials.
|
|
||||||
|
|
||||||
## Affected Files
|
|
||||||
|
|
||||||
- `backend/src/routes/paymentRoutes.js` — route definitions for `fetch-tx`, `auto-fetch-missing`, `debug`
|
|
||||||
|
|
||||||
## References
|
|
||||||
|
|
||||||
- [Doc vs Code Audit Report](../09%20-%20Audits/Doc%20vs%20Code%20Audit%20Report%20-%202026-05-29.md) — Findings C28, M40
|
|
||||||
- Related: [[ISSUE-005-scanner-status-no-auth]]
|
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
---
|
||||||
|
issue: 004
|
||||||
|
title: "POST /api/disputes/:id/resolve (dashboard) does not trigger escrow release — only updates Dispute model"
|
||||||
|
severity: critical
|
||||||
|
domain: Dispute
|
||||||
|
labels: [bug, backend, escrow, major]
|
||||||
|
status: open
|
||||||
|
created: 2026-05-29
|
||||||
|
source: Doc vs Code Audit 2026-05-29
|
||||||
|
---
|
||||||
|
|
||||||
|
# 🔴 POST /api/disputes/:id/resolve (dashboard) does not trigger escrow release — only updates Dispute model
|
||||||
|
|
||||||
|
**Severity:** critical
|
||||||
|
**Domain:** Dispute
|
||||||
|
**Labels:** bug, backend, escrow, major
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
The API claims resolveDispute 'triggers refund/release/split escrow action.' DisputeService.resolveDispute only updates the Dispute document. The separate POST /api/disputes/:purchaseRequestId/resolve (releaseHold router) is required to actually unblock escrow. Due to the route shadowing bug, the correct handler may never be reached.
|
||||||
|
|
||||||
|
## Current Behavior
|
||||||
|
|
||||||
|
DisputeService.resolveDispute only updates the Dispute document. Escrow remains blocked until a separate correct API call is made to the releaseHold router.
|
||||||
|
|
||||||
|
## Expected Behavior
|
||||||
|
|
||||||
|
Dispute resolution should atomically update the Dispute record AND release/refund the escrow as indicated by the action field.
|
||||||
|
|
||||||
|
## Affected Files
|
||||||
|
|
||||||
|
- `backend/src/services/disputeService.ts`
|
||||||
|
- `backend/src/controllers/disputeController.ts`
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Doc vs Code Audit Report](../09%20-%20Audits/Doc%20vs%20Code%20Audit%20Report%20-%202026-05-29.md)
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
---
|
||||||
|
issue: 005
|
||||||
|
title: "POST /api/payment/payments/:id/fetch-tx, POST /api/payment/payments/auto-fetch-missing, and GET /api/payment/payments/:id/debug have no authentication middleware"
|
||||||
|
severity: critical
|
||||||
|
domain: Payment
|
||||||
|
labels: [security, bug, backend, critical, missing-auth]
|
||||||
|
status: open
|
||||||
|
created: 2026-05-29
|
||||||
|
source: Doc vs Code Audit 2026-05-29
|
||||||
|
---
|
||||||
|
|
||||||
|
# 🔴 POST /api/payment/payments/:id/fetch-tx, POST /api/payment/payments/auto-fetch-missing, and GET /api/payment/payments/:id/debug have no authentication middleware
|
||||||
|
|
||||||
|
**Severity:** critical
|
||||||
|
**Domain:** Payment
|
||||||
|
**Labels:** security, bug, backend, critical, missing-auth
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
Three payment utility/debug endpoints are mounted with zero authentication. Any unauthenticated caller can read full payment internals (including blockchain metadata and wallet monitor state) or trigger on-chain fetches and state writes. These are exploitable without credentials in production.
|
||||||
|
|
||||||
|
## Current Behavior
|
||||||
|
|
||||||
|
All three return 200 with full data when called without any Authorization header.
|
||||||
|
|
||||||
|
## Expected Behavior
|
||||||
|
|
||||||
|
All three endpoints should require at minimum authenticateToken, and ideally authorizeRoles('admin').
|
||||||
|
|
||||||
|
## Reproduction Steps
|
||||||
|
|
||||||
|
curl -X POST https://api.example.com/api/payment/payments/test123/fetch-tx — expect 401, currently returns 200.
|
||||||
|
|
||||||
|
## Affected Files
|
||||||
|
|
||||||
|
- `backend/src/routes/paymentRoutes.ts`
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Doc vs Code Audit Report](../09%20-%20Audits/Doc%20vs%20Code%20Audit%20Report%20-%202026-05-29.md)
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
---
|
|
||||||
issue: "005"
|
|
||||||
title: "GET /api/admin/scanner/status has no authentication despite /api/admin/ prefix"
|
|
||||||
severity: critical
|
|
||||||
domain: admin
|
|
||||||
labels: [security, backend, bug]
|
|
||||||
status: open
|
|
||||||
created: 2026-05-29
|
|
||||||
source: Doc vs Code Audit 2026-05-29
|
|
||||||
---
|
|
||||||
|
|
||||||
# 🔴 GET /api/admin/scanner/status has no authentication despite /api/admin/ prefix
|
|
||||||
|
|
||||||
**Severity:** critical
|
|
||||||
**Domain:** admin
|
|
||||||
**Labels:** security, backend, bug
|
|
||||||
|
|
||||||
## Description
|
|
||||||
|
|
||||||
`GET /api/admin/scanner/status` proxies to `AMN_SCANNER_URL` and returns scanner status data. Despite sitting under the `/api/admin/` prefix (which conventionally implies admin auth), this endpoint has **no `authenticateToken` middleware**. Any unauthenticated request returns scanner data.
|
|
||||||
|
|
||||||
## Current Behavior
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl https://api.example.com/api/admin/scanner/status
|
|
||||||
# Returns scanner data with 200, no credentials needed
|
|
||||||
```
|
|
||||||
|
|
||||||
## Expected Behavior
|
|
||||||
|
|
||||||
Should return `401` without a valid admin JWT.
|
|
||||||
|
|
||||||
## Affected Files
|
|
||||||
|
|
||||||
- `backend/src/routes/adminRoutes.js` — scanner proxy route definition
|
|
||||||
|
|
||||||
## References
|
|
||||||
|
|
||||||
- [Doc vs Code Audit Report](../09%20-%20Audits/Doc%20vs%20Code%20Audit%20Report%20-%202026-05-29.md) — Finding C29
|
|
||||||
- Related: [[ISSUE-004-payment-endpoints-no-auth]]
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
---
|
|
||||||
issue: "006"
|
|
||||||
title: "Frontend deleteAccount action calls DELETE /user/profile which does not exist"
|
|
||||||
severity: critical
|
|
||||||
domain: auth
|
|
||||||
labels: [frontend, bug]
|
|
||||||
status: open
|
|
||||||
created: 2026-05-29
|
|
||||||
source: Doc vs Code Audit 2026-05-29
|
|
||||||
---
|
|
||||||
|
|
||||||
# 🔴 Frontend deleteAccount action calls DELETE /user/profile which does not exist
|
|
||||||
|
|
||||||
**Severity:** critical
|
|
||||||
**Domain:** auth
|
|
||||||
**Labels:** frontend, bug
|
|
||||||
|
|
||||||
## Description
|
|
||||||
|
|
||||||
`frontend/src/actions/account.ts` (line ~144) calls:
|
|
||||||
```ts
|
|
||||||
axiosInstance.delete(endpoints.users.profile)
|
|
||||||
// resolves to DELETE /user/profile
|
|
||||||
```
|
|
||||||
|
|
||||||
There is no `DELETE` handler on `/user/profile` in the backend. The actual soft-delete endpoint is:
|
|
||||||
```
|
|
||||||
DELETE /api/auth/account
|
|
||||||
```
|
|
||||||
which requires a `password` field in the request body and runs `deleteAccountValidation`.
|
|
||||||
|
|
||||||
**Result:** Account deletion silently 404s from every UI path. Users cannot delete their accounts.
|
|
||||||
|
|
||||||
## Current Behavior
|
|
||||||
|
|
||||||
Clicking the delete account button in the dashboard sends `DELETE /user/profile` → 404. The account is not deleted.
|
|
||||||
|
|
||||||
## Expected Behavior
|
|
||||||
|
|
||||||
The action should send `DELETE /api/auth/account` with `{ password }` in the body. On success, the account status is set to `'deleted'` (soft delete) in MongoDB.
|
|
||||||
|
|
||||||
## Affected Files
|
|
||||||
|
|
||||||
- `frontend/src/actions/account.ts` — `deleteAccount` function
|
|
||||||
- `frontend/src/lib/axios.ts` — `endpoints.users.profile` key used for the path
|
|
||||||
|
|
||||||
## References
|
|
||||||
|
|
||||||
- [Doc vs Code Audit Report](../09%20-%20Audits/Doc%20vs%20Code%20Audit%20Report%20-%202026-05-29.md) — Finding C3
|
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
---
|
||||||
|
issue: 006
|
||||||
|
title: "GET /api/admin/scanner/status has no authentication middleware despite /api/admin/ prefix"
|
||||||
|
severity: critical
|
||||||
|
domain: Admin
|
||||||
|
labels: [security, bug, backend, critical, missing-auth]
|
||||||
|
status: open
|
||||||
|
created: 2026-05-29
|
||||||
|
source: Doc vs Code Audit 2026-05-29
|
||||||
|
---
|
||||||
|
|
||||||
|
# 🔴 GET /api/admin/scanner/status has no authentication middleware despite /api/admin/ prefix
|
||||||
|
|
||||||
|
**Severity:** critical
|
||||||
|
**Domain:** Admin
|
||||||
|
**Labels:** security, bug, backend, critical, missing-auth
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
The scanner status proxy endpoint at GET /api/admin/scanner/status proxies directly to AMN_SCANNER_URL without any authentication check, despite sitting under the /api/admin/ route prefix which conventionally requires admin auth.
|
||||||
|
|
||||||
|
## Current Behavior
|
||||||
|
|
||||||
|
Returns scanner data (200) to any unauthenticated request.
|
||||||
|
|
||||||
|
## Expected Behavior
|
||||||
|
|
||||||
|
Return 401 without Authorization header, 403 for non-admin token. Apply authenticateToken + authorizeRoles('admin').
|
||||||
|
|
||||||
|
## Reproduction Steps
|
||||||
|
|
||||||
|
curl https://api.example.com/api/admin/scanner/status — should return 401, currently returns scanner data.
|
||||||
|
|
||||||
|
## Affected Files
|
||||||
|
|
||||||
|
- `backend/src/routes/adminRoutes.ts`
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Doc vs Code Audit Report](../09%20-%20Audits/Doc%20vs%20Code%20Audit%20Report%20-%202026-05-29.md)
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
---
|
||||||
|
issue: 007
|
||||||
|
title: "Frontend deleteAccount action calls DELETE /user/profile which has no backend route — account deletion is broken"
|
||||||
|
severity: critical
|
||||||
|
domain: Authentication
|
||||||
|
labels: [bug, frontend, critical, broken-feature]
|
||||||
|
status: open
|
||||||
|
created: 2026-05-29
|
||||||
|
source: Doc vs Code Audit 2026-05-29
|
||||||
|
---
|
||||||
|
|
||||||
|
# 🔴 Frontend deleteAccount action calls DELETE /user/profile which has no backend route — account deletion is broken
|
||||||
|
|
||||||
|
**Severity:** critical
|
||||||
|
**Domain:** Authentication
|
||||||
|
**Labels:** bug, frontend, critical, broken-feature
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
The frontend deleteAccount action in src/actions/account.ts (line 144) calls axiosInstance.delete(endpoints.users.profile) which resolves to DELETE /user/profile. The actual soft-delete route is DELETE /api/auth/account (requires password in body, runs deleteAccountValidation). Account deletion silently returns 404 from every UI path.
|
||||||
|
|
||||||
|
## Current Behavior
|
||||||
|
|
||||||
|
DELETE /user/profile returns 404. Users cannot delete their accounts from the UI.
|
||||||
|
|
||||||
|
## Expected Behavior
|
||||||
|
|
||||||
|
deleteAccount action should call DELETE /api/auth/account with the user's password in the request body.
|
||||||
|
|
||||||
|
## Affected Files
|
||||||
|
|
||||||
|
- `frontend/src/actions/account.ts`
|
||||||
|
- `frontend/src/lib/axios.ts`
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Doc vs Code Audit Report](../09%20-%20Audits/Doc%20vs%20Code%20Audit%20Report%20-%202026-05-29.md)
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
---
|
|
||||||
issue: "007"
|
|
||||||
title: "SIM_ transaction bypass active in production — no NODE_ENV guard on wallet connection fallback"
|
|
||||||
severity: critical
|
|
||||||
domain: payment
|
|
||||||
labels: [security, frontend, backend, bug]
|
|
||||||
status: open
|
|
||||||
created: 2026-05-29
|
|
||||||
source: Doc vs Code Audit 2026-05-29
|
|
||||||
---
|
|
||||||
|
|
||||||
# 🔴 SIM_ transaction bypass active in production — no NODE_ENV guard on wallet connection fallback
|
|
||||||
|
|
||||||
**Severity:** critical
|
|
||||||
**Domain:** payment
|
|
||||||
**Labels:** security, frontend, backend, bug
|
|
||||||
|
|
||||||
## Description
|
|
||||||
|
|
||||||
`frontend/src/web3/context/web3-provider.tsx` (lines ~225 and ~232) generates `SIM_` prefixed transaction hashes when wallet connection fails, and passes these to the backend as real transaction hashes.
|
|
||||||
|
|
||||||
The backend's payment service skips all on-chain verification for any `paymentHash` starting with `SIM_`. This bypass is controlled **only by the hash prefix** — there is no `process.env.NODE_ENV === 'development'` check in either the frontend or backend.
|
|
||||||
|
|
||||||
In production, if a user's wallet connection times out or throws (e.g., network error, MetaMask not responding), the frontend will submit a `SIM_` hash. This can result in a payment record being created as `completed` without any actual on-chain transaction.
|
|
||||||
|
|
||||||
## Current Behavior
|
|
||||||
|
|
||||||
Wallet connection failure → frontend generates `SIM_xxxxxxxx` hash → sends to backend → backend skips on-chain verification → payment created as completed.
|
|
||||||
|
|
||||||
## Expected Behavior
|
|
||||||
|
|
||||||
- Frontend: `SIM_` hash generation should be gated on `process.env.NODE_ENV !== 'production'`
|
|
||||||
- Backend: `SIM_` bypass should additionally check an environment flag (e.g., `process.env.ALLOW_SIM_PAYMENTS !== 'true'`)
|
|
||||||
|
|
||||||
## Affected Files
|
|
||||||
|
|
||||||
- `frontend/src/web3/context/web3-provider.tsx` — lines ~225, ~232
|
|
||||||
- `backend/src/services/payment/` — SIM_ prefix check in payment verification logic
|
|
||||||
|
|
||||||
## References
|
|
||||||
|
|
||||||
- [Doc vs Code Audit Report](../09%20-%20Audits/Doc%20vs%20Code%20Audit%20Report%20-%202026-05-29.md) — Finding M39
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
---
|
|
||||||
issue: "008"
|
|
||||||
title: "sendFileMessage posts to wrong endpoint — file uploads always fail in chat"
|
|
||||||
severity: critical
|
|
||||||
domain: chat
|
|
||||||
labels: [frontend, bug]
|
|
||||||
status: open
|
|
||||||
created: 2026-05-29
|
|
||||||
source: Doc vs Code Audit 2026-05-29
|
|
||||||
---
|
|
||||||
|
|
||||||
# 🔴 sendFileMessage posts to wrong endpoint — file uploads always fail in chat
|
|
||||||
|
|
||||||
**Severity:** critical
|
|
||||||
**Domain:** chat
|
|
||||||
**Labels:** frontend, bug
|
|
||||||
|
|
||||||
## Description
|
|
||||||
|
|
||||||
`frontend/src/actions/chat.ts` (line ~386) sends file upload multipart form data to `endpoints.chat.sendMessage` which resolves to `POST /api/chat/:id/messages` — the text message endpoint.
|
|
||||||
|
|
||||||
The actual backend file upload endpoint is `POST /api/chat/:id/messages/file`.
|
|
||||||
|
|
||||||
The text-message handler expects a JSON body with a `content` string field, not a multipart payload. The file upload either fails or the attachment is silently discarded.
|
|
||||||
|
|
||||||
## Current Behavior
|
|
||||||
|
|
||||||
User picks a file in the chat input → `sendFileMessage` POSTs multipart to `/chat/:id/messages` → backend text handler rejects or ignores the multipart payload → file is never uploaded or stored.
|
|
||||||
|
|
||||||
## Expected Behavior
|
|
||||||
|
|
||||||
`sendFileMessage` should POST to `/api/chat/:id/messages/file` with the multipart form data. The response should include a message with an `attachments` array.
|
|
||||||
|
|
||||||
## Affected Files
|
|
||||||
|
|
||||||
- `frontend/src/actions/chat.ts` — `sendFileMessage` function uses `endpoints.chat.sendMessage`
|
|
||||||
- `frontend/src/lib/axios.ts` — no `endpoints.chat.sendFileMessage` entry exists; needs to be added as `/chat/:id/messages/file`
|
|
||||||
|
|
||||||
## References
|
|
||||||
|
|
||||||
- [Doc vs Code Audit Report](../09%20-%20Audits/Doc%20vs%20Code%20Audit%20Report%20-%202026-05-29.md) — Finding C19
|
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
---
|
||||||
|
issue: 008
|
||||||
|
title: "sendFileMessage posts to wrong endpoint — file uploads silently fail or corrupt text-message handler"
|
||||||
|
severity: critical
|
||||||
|
domain: Chat
|
||||||
|
labels: [bug, frontend, critical, broken-feature]
|
||||||
|
status: open
|
||||||
|
created: 2026-05-29
|
||||||
|
source: Doc vs Code Audit 2026-05-29
|
||||||
|
---
|
||||||
|
|
||||||
|
# 🔴 sendFileMessage posts to wrong endpoint — file uploads silently fail or corrupt text-message handler
|
||||||
|
|
||||||
|
**Severity:** critical
|
||||||
|
**Domain:** Chat
|
||||||
|
**Labels:** bug, frontend, critical, broken-feature
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
The frontend sendFileMessage action in src/actions/chat.ts (line 386) sends multipart form data to endpoints.chat.sendMessage which resolves to POST /api/chat/:id/messages. The actual file upload endpoint is POST /api/chat/:id/messages/file. The file payload hits the text-message handler which expects JSON with a string content field.
|
||||||
|
|
||||||
|
## Current Behavior
|
||||||
|
|
||||||
|
File uploads hit the text-message handler, which cannot process multipart payloads. File attachments are silently discarded or the request errors.
|
||||||
|
|
||||||
|
## Expected Behavior
|
||||||
|
|
||||||
|
sendFileMessage should POST multipart/form-data to /api/chat/:id/messages/file.
|
||||||
|
|
||||||
|
## Affected Files
|
||||||
|
|
||||||
|
- `frontend/src/actions/chat.ts`
|
||||||
|
- `frontend/src/lib/axios.ts`
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Doc vs Code Audit Report](../09%20-%20Audits/Doc%20vs%20Code%20Audit%20Report%20-%202026-05-29.md)
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
---
|
|
||||||
issue: "009"
|
|
||||||
title: "archiveConversation uses PUT but backend only accepts PATCH"
|
|
||||||
severity: major
|
|
||||||
domain: chat
|
|
||||||
labels: [frontend, bug]
|
|
||||||
status: open
|
|
||||||
created: 2026-05-29
|
|
||||||
source: Doc vs Code Audit 2026-05-29
|
|
||||||
---
|
|
||||||
|
|
||||||
# 🟠 archiveConversation uses PUT but backend only accepts PATCH
|
|
||||||
|
|
||||||
**Severity:** major
|
|
||||||
**Domain:** chat
|
|
||||||
**Labels:** frontend, bug
|
|
||||||
|
|
||||||
## Description
|
|
||||||
|
|
||||||
`frontend/src/actions/chat.ts` (line ~289) calls `axiosInstance.put(endpoints.chat.archive, ...)`. The backend registers this route as `PATCH /api/chat/:id/archive`. Express treats PUT and PATCH as distinct methods; PUT will not match the PATCH handler and returns 404/405.
|
|
||||||
|
|
||||||
## Current Behavior
|
|
||||||
|
|
||||||
Attempting to archive a conversation from the UI sends `PUT /api/chat/:id/archive` → 404. The chat is not archived.
|
|
||||||
|
|
||||||
## Expected Behavior
|
|
||||||
|
|
||||||
`archiveConversation` should use `axiosInstance.patch(...)` to match the backend's PATCH registration. The endpoint also has toggle semantics — calling it on an archived chat unarchives it.
|
|
||||||
|
|
||||||
## Affected Files
|
|
||||||
|
|
||||||
- `frontend/src/actions/chat.ts` — `archiveConversation` method verb (`put` → `patch`)
|
|
||||||
|
|
||||||
## References
|
|
||||||
|
|
||||||
- [Doc vs Code Audit Report](../09%20-%20Audits/Doc%20vs%20Code%20Audit%20Report%20-%202026-05-29.md) — Finding C20
|
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
---
|
||||||
|
issue: 009
|
||||||
|
title: "archiveConversation sends PUT but backend only accepts PATCH — all archive attempts fail"
|
||||||
|
severity: critical
|
||||||
|
domain: Chat
|
||||||
|
labels: [bug, frontend, critical, broken-feature]
|
||||||
|
status: open
|
||||||
|
created: 2026-05-29
|
||||||
|
source: Doc vs Code Audit 2026-05-29
|
||||||
|
---
|
||||||
|
|
||||||
|
# 🔴 archiveConversation sends PUT but backend only accepts PATCH — all archive attempts fail
|
||||||
|
|
||||||
|
**Severity:** critical
|
||||||
|
**Domain:** Chat
|
||||||
|
**Labels:** bug, frontend, critical, broken-feature
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
The frontend archiveConversation action (src/actions/chat.ts line 289) calls axiosInstance.put(). The backend registers PATCH /api/chat/:id/archive. HTTP method mismatch causes 404 or 405 on every archive attempt.
|
||||||
|
|
||||||
|
## Current Behavior
|
||||||
|
|
||||||
|
Every archive attempt returns 404/405. Chat archiving is non-functional.
|
||||||
|
|
||||||
|
## Expected Behavior
|
||||||
|
|
||||||
|
archiveConversation should call axiosInstance.patch().
|
||||||
|
|
||||||
|
## Affected Files
|
||||||
|
|
||||||
|
- `frontend/src/actions/chat.ts`
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Doc vs Code Audit Report](../09%20-%20Audits/Doc%20vs%20Code%20Audit%20Report%20-%202026-05-29.md)
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
---
|
|
||||||
issue: "010"
|
|
||||||
title: "Admin user status/role actions broken: wrong HTTP verb (PUT vs PATCH) and wrong status values"
|
|
||||||
severity: critical
|
|
||||||
domain: admin
|
|
||||||
labels: [frontend, bug]
|
|
||||||
status: open
|
|
||||||
created: 2026-05-29
|
|
||||||
source: Doc vs Code Audit 2026-05-29
|
|
||||||
---
|
|
||||||
|
|
||||||
# 🔴 Admin user status/role actions broken: wrong HTTP verb (PUT vs PATCH) and wrong status values
|
|
||||||
|
|
||||||
**Severity:** critical
|
|
||||||
**Domain:** admin
|
|
||||||
**Labels:** frontend, bug
|
|
||||||
|
|
||||||
## Description
|
|
||||||
|
|
||||||
Two separate bugs on the admin user management actions:
|
|
||||||
|
|
||||||
**Bug 1 — Wrong HTTP verb:**
|
|
||||||
`frontend/src/actions/user.ts`:
|
|
||||||
- `updateUserStatus` calls `axiosInstance.put(...)` — backend registers `PATCH`
|
|
||||||
- `updateUserRole` calls `axiosInstance.put(...)` — backend registers `PATCH`
|
|
||||||
|
|
||||||
Both will 404/405 in production since Express doesn't alias PUT to PATCH.
|
|
||||||
|
|
||||||
**Bug 2 — Wrong status values:**
|
|
||||||
`updateUserStatus` accepts and sends `'active' | 'inactive' | 'pending'`. The backend `User.status` enum only accepts `'active' | 'suspended' | 'deleted'`. Sending `'inactive'` or `'pending'` is silently rejected or ignored. `'suspended'` is completely absent from the frontend type.
|
|
||||||
|
|
||||||
## Current Behavior
|
|
||||||
|
|
||||||
- Clicking "Suspend user" in admin panel sends `PUT /api/users/admin/:userId/status` with `{ status: 'inactive' }` → 404 and wrong value
|
|
||||||
- Clicking "Update role" sends `PUT /api/users/admin/:userId/role` → 404
|
|
||||||
|
|
||||||
## Expected Behavior
|
|
||||||
|
|
||||||
- Use `axiosInstance.patch(...)` for both actions
|
|
||||||
- Status values should be `'active' | 'suspended' | 'deleted'` to match the backend enum
|
|
||||||
|
|
||||||
## Affected Files
|
|
||||||
|
|
||||||
- `frontend/src/actions/user.ts` — `updateUserStatus` (line ~162), `updateUserRole` (line ~175)
|
|
||||||
- `frontend/src/types/user.ts` (line ~159) — status union type needs to include `'suspended'` and remove `'inactive'`/`'pending'`
|
|
||||||
|
|
||||||
## References
|
|
||||||
|
|
||||||
- [Doc vs Code Audit Report](../09%20-%20Audits/Doc%20vs%20Code%20Audit%20Report%20-%202026-05-29.md) — Findings C26, C27
|
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
---
|
||||||
|
issue: 010
|
||||||
|
title: "Frontend admin updateUserStatus and updateUserRole use PUT but backend only accepts PATCH"
|
||||||
|
severity: critical
|
||||||
|
domain: User Management
|
||||||
|
labels: [bug, frontend, critical, admin, broken-feature]
|
||||||
|
status: open
|
||||||
|
created: 2026-05-29
|
||||||
|
source: Doc vs Code Audit 2026-05-29
|
||||||
|
---
|
||||||
|
|
||||||
|
# 🔴 Frontend admin updateUserStatus and updateUserRole use PUT but backend only accepts PATCH
|
||||||
|
|
||||||
|
**Severity:** critical
|
||||||
|
**Domain:** User Management
|
||||||
|
**Labels:** bug, frontend, critical, admin, broken-feature
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
user.ts line 162 calls axiosInstance.put() for updateUserStatus and line 175 calls axiosInstance.put() for updateUserRole. Backend registers these as PATCH /api/users/admin/:userId/status and PATCH /api/users/admin/:userId/role. PUT is not registered; calls return 404 or 405.
|
||||||
|
|
||||||
|
## Current Behavior
|
||||||
|
|
||||||
|
Admin status and role update actions fail with 404/405 silently.
|
||||||
|
|
||||||
|
## Expected Behavior
|
||||||
|
|
||||||
|
Both actions should use axiosInstance.patch().
|
||||||
|
|
||||||
|
## Affected Files
|
||||||
|
|
||||||
|
- `frontend/src/actions/user.ts`
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Doc vs Code Audit Report](../09%20-%20Audits/Doc%20vs%20Code%20Audit%20Report%20-%202026-05-29.md)
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
---
|
||||||
|
issue: 011
|
||||||
|
title: "Frontend updateUserStatus sends 'inactive'/'pending' status values that backend does not accept"
|
||||||
|
severity: critical
|
||||||
|
domain: User Management
|
||||||
|
labels: [bug, frontend, critical, admin, type-mismatch]
|
||||||
|
status: open
|
||||||
|
created: 2026-05-29
|
||||||
|
source: Doc vs Code Audit 2026-05-29
|
||||||
|
---
|
||||||
|
|
||||||
|
# 🔴 Frontend updateUserStatus sends 'inactive'/'pending' status values that backend does not accept
|
||||||
|
|
||||||
|
**Severity:** critical
|
||||||
|
**Domain:** User Management
|
||||||
|
**Labels:** bug, frontend, critical, admin, type-mismatch
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
TypeScript union type in user.ts line 159 is 'active' | 'inactive' | 'pending'. Backend User.status enum is active | suspended | deleted. Values 'inactive' and 'pending' are not valid on the backend and will be rejected or silently ignored. 'suspended' is absent from the frontend type.
|
||||||
|
|
||||||
|
## Current Behavior
|
||||||
|
|
||||||
|
Attempting to set user status to 'inactive' or 'pending' via the admin UI sends invalid values. The user's status is not actually updated.
|
||||||
|
|
||||||
|
## Expected Behavior
|
||||||
|
|
||||||
|
Frontend type should be 'active' | 'suspended' | 'deleted' to match the backend enum. Admin UI should offer 'suspended' as an option.
|
||||||
|
|
||||||
|
## Affected Files
|
||||||
|
|
||||||
|
- `frontend/src/actions/user.ts`
|
||||||
|
- `frontend/src/types/user.ts`
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Doc vs Code Audit Report](../09%20-%20Audits/Doc%20vs%20Code%20Audit%20Report%20-%202026-05-29.md)
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
---
|
|
||||||
issue: "011"
|
|
||||||
title: "updatePurchaseRequest sends PUT but backend only accepts PATCH"
|
|
||||||
severity: major
|
|
||||||
domain: purchase-request
|
|
||||||
labels: [frontend, bug]
|
|
||||||
status: open
|
|
||||||
created: 2026-05-29
|
|
||||||
source: Doc vs Code Audit 2026-05-29
|
|
||||||
---
|
|
||||||
|
|
||||||
# 🟠 updatePurchaseRequest sends PUT but backend only accepts PATCH
|
|
||||||
|
|
||||||
**Severity:** major
|
|
||||||
**Domain:** purchase-request
|
|
||||||
**Labels:** frontend, bug
|
|
||||||
|
|
||||||
## Description
|
|
||||||
|
|
||||||
`frontend/src/actions/marketplace.ts` (line ~71) calls `axiosInstance.put(endpoints.marketplace.requests.update)`. The backend registers `PATCH /marketplace/purchase-requests/:id` (routes.ts). Sending PUT results in 404/405 — edits to purchase requests silently fail.
|
|
||||||
|
|
||||||
## Current Behavior
|
|
||||||
|
|
||||||
Editing a purchase request from the buyer edit view sends `PUT /marketplace/purchase-requests/:id` → 404. The request is not updated.
|
|
||||||
|
|
||||||
## Expected Behavior
|
|
||||||
|
|
||||||
The action should use `axiosInstance.patch(...)`.
|
|
||||||
|
|
||||||
## Affected Files
|
|
||||||
|
|
||||||
- `frontend/src/actions/marketplace.ts` — `updatePurchaseRequest` function (verb: `put` → `patch`)
|
|
||||||
|
|
||||||
## References
|
|
||||||
|
|
||||||
- [Doc vs Code Audit Report](../09%20-%20Audits/Doc%20vs%20Code%20Audit%20Report%20-%202026-05-29.md) — Finding M18
|
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
---
|
||||||
|
issue: 012
|
||||||
|
title: "Trezor Safekeeping frontend — audit 'zero frontend' finding was STALE (feature exists)"
|
||||||
|
severity: info
|
||||||
|
domain: Trezor
|
||||||
|
labels: [invalid, stale-audit, trezor, frontend]
|
||||||
|
status: invalid
|
||||||
|
created: 2026-05-29
|
||||||
|
resolved: 2026-05-29
|
||||||
|
source: Doc vs Code Audit 2026-05-29
|
||||||
|
---
|
||||||
|
|
||||||
|
# ⚪ INVALID — Trezor Safekeeping frontend DOES exist (audit finding was stale)
|
||||||
|
|
||||||
|
**Severity:** info (was: critical)
|
||||||
|
**Domain:** Trezor
|
||||||
|
**Status:** INVALID — the audit's "zero frontend implementation" claim (findings C31/C32) was generated from an older code snapshot. The frontend Trezor implementation exists in current code.
|
||||||
|
|
||||||
|
## Why this is not a bug
|
||||||
|
|
||||||
|
A direct re-check of the current frontend on 2026-05-29 confirmed a complete Trezor implementation:
|
||||||
|
|
||||||
|
- `frontend/src/app/dashboard/admin/trezor/page.tsx` → renders `TrezorSettingsView` (registration + re-register UI)
|
||||||
|
- `frontend/src/sections/admin/trezor/trezor-settings-view.tsx` → settings/registration view (~14KB)
|
||||||
|
- `frontend/src/web3/trezor/trezorConnector.ts` → lazy-imports `@trezor/connect-web`; implements `trezorGetXpub`, `trezorGetAddress`, `trezorSignMessage`
|
||||||
|
- `frontend/src/components/trezor-sign-dialog/TrezorSignDialog.tsx` → full stepper: build instruction → sign on Trezor → enter txHash → confirm
|
||||||
|
- `frontend/src/actions/trezor.ts` → complete API client: `getTrezorAccount`, `getTrezorRegistrationMessage`, `registerTrezor`, `getTrezorOperationMessage`, `confirmRelease`, `confirmRefund` — and it **builds the `trezor: { message, signature }` object** in the confirmation body
|
||||||
|
|
||||||
|
The active admin release/refund path goes through `TrezorSignDialog` → `actions/trezor.ts`, which **does** satisfy the backend `assertTrezorSignatureForOperation` guard when `TREZOR_SAFEKEEPING_REQUIRED=true`.
|
||||||
|
|
||||||
|
## Residual note (not a blocker)
|
||||||
|
|
||||||
|
The legacy helpers `confirmReleaseTx` / `confirmRefundTx` in `frontend/src/actions/payment.ts` post only `{ txHash }` with no `trezor` field — but they have **no UI callers** and are dead code. Consider removing them to avoid confusion. Tracked as a minor cleanup, not a release blocker.
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Doc vs Code Audit Report](../09%20-%20Audits/Doc%20vs%20Code%20Audit%20Report%20-%202026-05-29.md) — findings C31, C32 (now superseded)
|
||||||
|
- Corrected doc: `04 - Flows/Trezor Safekeeping Flow.md`
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
---
|
|
||||||
issue: "012"
|
|
||||||
title: "updateOffer sends PUT but backend registers PATCH — offer edits fail"
|
|
||||||
severity: major
|
|
||||||
domain: seller-offer
|
|
||||||
labels: [frontend, bug]
|
|
||||||
status: open
|
|
||||||
created: 2026-05-29
|
|
||||||
source: Doc vs Code Audit 2026-05-29
|
|
||||||
---
|
|
||||||
|
|
||||||
# 🟠 updateOffer sends PUT but backend registers PATCH — offer edits fail
|
|
||||||
|
|
||||||
**Severity:** major
|
|
||||||
**Domain:** seller-offer
|
|
||||||
**Labels:** frontend, bug
|
|
||||||
|
|
||||||
## Description
|
|
||||||
|
|
||||||
`frontend/src/actions/marketplace.ts` (line ~289) calls `axiosInstance.put(endpoints.marketplace.offers.update)` mapping to `PUT /marketplace/offers/:id`. The backend registers `PATCH /offers/:id` (routes.ts line ~1260). Method mismatch → 404 or matched wrong route. `step-1-send-proposal.tsx` calls `updateOffer()` for proposal edits, so this path is actively exercised.
|
|
||||||
|
|
||||||
## Current Behavior
|
|
||||||
|
|
||||||
A seller editing an existing proposal sends `PUT /marketplace/offers/:id` which does not match the registered `PATCH` handler.
|
|
||||||
|
|
||||||
## Expected Behavior
|
|
||||||
|
|
||||||
`updateOffer` should use `axiosInstance.patch(...)`.
|
|
||||||
|
|
||||||
## Affected Files
|
|
||||||
|
|
||||||
- `frontend/src/actions/marketplace.ts` — `updateOffer` function
|
|
||||||
|
|
||||||
## References
|
|
||||||
|
|
||||||
- [Doc vs Code Audit Report](../09%20-%20Audits/Doc%20vs%20Code%20Audit%20Report%20-%202026-05-29.md) — Finding M28
|
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
---
|
||||||
|
issue: 013
|
||||||
|
title: "createProviderPaymentIntent always routes to request-network/intents regardless of provider argument"
|
||||||
|
severity: critical
|
||||||
|
domain: Payment
|
||||||
|
labels: [bug, frontend, critical, payment, routing]
|
||||||
|
status: open
|
||||||
|
created: 2026-05-29
|
||||||
|
source: Doc vs Code Audit 2026-05-29
|
||||||
|
---
|
||||||
|
|
||||||
|
# 🔴 createProviderPaymentIntent always routes to request-network/intents regardless of provider argument
|
||||||
|
|
||||||
|
**Severity:** critical
|
||||||
|
**Domain:** Payment
|
||||||
|
**Labels:** bug, frontend, critical, payment, routing
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
src/actions/payment.ts getProviderIntentEndpoint() ignores the provider parameter and always returns endpoints.payments.requestNetwork.intents ('/payment/request-network/intents'). Any checkout using provider='shkeeper' silently POSTs to the wrong backend service.
|
||||||
|
|
||||||
|
## Current Behavior
|
||||||
|
|
||||||
|
SHKeeper checkout silently POSTs to /payment/request-network/intents instead of /payment/shkeeper/intents, causing payment intent creation to fail or create a wrong-provider payment record.
|
||||||
|
|
||||||
|
## Expected Behavior
|
||||||
|
|
||||||
|
getProviderIntentEndpoint() should return the correct provider-specific endpoint based on the provider argument (e.g., endpoints.payments.shkeeper.intents for 'shkeeper').
|
||||||
|
|
||||||
|
## Reproduction Steps
|
||||||
|
|
||||||
|
Initiate a SHKeeper checkout and intercept network — observe the POST goes to /payment/request-network/intents not /payment/shkeeper/intents.
|
||||||
|
|
||||||
|
## Affected Files
|
||||||
|
|
||||||
|
- `frontend/src/actions/payment.ts`
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Doc vs Code Audit Report](../09%20-%20Audits/Doc%20vs%20Code%20Audit%20Report%20-%202026-05-29.md)
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
---
|
|
||||||
issue: "013"
|
|
||||||
title: "select-offer cascade overwrites withdrawn/rejected offers — missing status filter in updateMany"
|
|
||||||
severity: major
|
|
||||||
domain: seller-offer
|
|
||||||
labels: [backend, bug, data-integrity]
|
|
||||||
status: open
|
|
||||||
created: 2026-05-29
|
|
||||||
source: Doc vs Code Audit 2026-05-29
|
|
||||||
---
|
|
||||||
|
|
||||||
# 🟠 select-offer cascade overwrites withdrawn/rejected offers — missing status filter in updateMany
|
|
||||||
|
|
||||||
**Severity:** major
|
|
||||||
**Domain:** seller-offer
|
|
||||||
**Labels:** backend, bug, data-integrity
|
|
||||||
|
|
||||||
## Description
|
|
||||||
|
|
||||||
`POST /api/marketplace/purchase-requests/:id/select-offer` (routes.ts lines ~1386-1395) calls `SellerOffer.updateMany({ purchaseRequestId, _id: { $ne: offerId } }, { status: 'rejected' })` with **no status filter**. This overwrites offers that are already `'withdrawn'` or previously `'rejected'`, corrupting their status history.
|
|
||||||
|
|
||||||
By contrast, `SellerOfferService.acceptOffer()` (the service method used by `PUT /offers/:id/accept`) correctly filters with `status: { $in: ['pending', 'active'] }` before bulk-rejecting competitors.
|
|
||||||
|
|
||||||
## Current Behavior
|
|
||||||
|
|
||||||
1. Seller A submits offer → pending
|
|
||||||
2. Seller B submits offer → pending
|
|
||||||
3. Seller B withdraws offer → withdrawn
|
|
||||||
4. Buyer selects Seller A's offer via `POST .../select-offer`
|
|
||||||
5. Seller B's withdrawn offer is **overwritten to 'rejected'** — status history corrupted
|
|
||||||
|
|
||||||
## Expected Behavior
|
|
||||||
|
|
||||||
The `updateMany` in the `select-offer` route handler should add `status: { $in: ['pending'] }` to only reject currently-pending competing offers. Already-withdrawn or rejected offers should be left untouched.
|
|
||||||
|
|
||||||
## Affected Files
|
|
||||||
|
|
||||||
- `backend/src/routes/routes.ts` (or marketplaceController.ts) — `select-offer` route handler's `updateMany` call
|
|
||||||
|
|
||||||
## References
|
|
||||||
|
|
||||||
- [Doc vs Code Audit Report](../09%20-%20Audits/Doc%20vs%20Code%20Audit%20Report%20-%202026-05-29.md) — Finding M23
|
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
---
|
||||||
|
issue: 014
|
||||||
|
title: "PaymentProvider TypeScript type excludes 'shkeeper' and 'decentralized' causing UI fallthrough for main payment providers"
|
||||||
|
severity: critical
|
||||||
|
domain: Payment
|
||||||
|
labels: [bug, frontend, critical, payment, type-mismatch]
|
||||||
|
status: open
|
||||||
|
created: 2026-05-29
|
||||||
|
source: Doc vs Code Audit 2026-05-29
|
||||||
|
---
|
||||||
|
|
||||||
|
# 🔴 PaymentProvider TypeScript type excludes 'shkeeper' and 'decentralized' causing UI fallthrough for main payment providers
|
||||||
|
|
||||||
|
**Severity:** critical
|
||||||
|
**Domain:** Payment
|
||||||
|
**Labels:** bug, frontend, critical, payment, type-mismatch
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
src/types/payment.ts defines PaymentProvider as 'request.network' | 'test' | 'other'. The two primary production payment providers ('shkeeper' and 'decentralized') are absent from this union type. Frontend code that switches on PaymentProvider falls through to unknown/default state for the majority of production payments.
|
||||||
|
|
||||||
|
## Current Behavior
|
||||||
|
|
||||||
|
Provider-based conditional rendering, labels, and routing logic silently falls through to unknown state for SHKeeper and DePay payments.
|
||||||
|
|
||||||
|
## Expected Behavior
|
||||||
|
|
||||||
|
PaymentProvider type should include 'shkeeper' and 'decentralized' variants.
|
||||||
|
|
||||||
|
## Affected Files
|
||||||
|
|
||||||
|
- `frontend/src/types/payment.ts`
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Doc vs Code Audit Report](../09%20-%20Audits/Doc%20vs%20Code%20Audit%20Report%20-%202026-05-29.md)
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
---
|
|
||||||
issue: "014"
|
|
||||||
title: "select-offer sends no per-seller socket events or notifications to winning/losing sellers"
|
|
||||||
severity: major
|
|
||||||
domain: seller-offer
|
|
||||||
labels: [backend, missing-feature]
|
|
||||||
status: open
|
|
||||||
created: 2026-05-29
|
|
||||||
source: Doc vs Code Audit 2026-05-29
|
|
||||||
---
|
|
||||||
|
|
||||||
# 🟠 select-offer sends no per-seller socket events or notifications to winning/losing sellers
|
|
||||||
|
|
||||||
**Severity:** major
|
|
||||||
**Domain:** seller-offer
|
|
||||||
**Labels:** backend, missing-feature
|
|
||||||
|
|
||||||
## Description
|
|
||||||
|
|
||||||
`POST /api/marketplace/purchase-requests/:id/select-offer` (routes.ts lines ~1300-1438) emits only a single `purchase-request-update` event to the request room with `eventType: 'offer-selected'`. It does NOT:
|
|
||||||
- Call `notifyOfferAccepted` for the winning seller
|
|
||||||
- Call `notifyOfferRejected` for losing sellers
|
|
||||||
- Emit `seller-offer-update` events to individual seller rooms
|
|
||||||
|
|
||||||
These notifications only fire when using `PUT /offers/:id/accept` or `PUT /offers/:id/status` (via `SellerOfferService.updateOfferStatus`), not via the `select-offer` path used by the frontend.
|
|
||||||
|
|
||||||
## Current Behavior
|
|
||||||
|
|
||||||
Buyer selects an offer → winning seller gets no real-time notification → losing sellers get no notification.
|
|
||||||
|
|
||||||
## Expected Behavior
|
|
||||||
|
|
||||||
When a buyer selects an offer:
|
|
||||||
1. Winning seller receives a `seller-offer-update` event and a push notification
|
|
||||||
2. Losing sellers receive a `seller-offer-update` event and a notification
|
|
||||||
|
|
||||||
## Affected Files
|
|
||||||
|
|
||||||
- `backend/src/routes/routes.ts` — `select-offer` route handler, missing `notifyOfferAccepted` and `notifyOfferRejected` calls
|
|
||||||
|
|
||||||
## References
|
|
||||||
|
|
||||||
- [Doc vs Code Audit Report](../09%20-%20Audits/Doc%20vs%20Code%20Audit%20Report%20-%202026-05-29.md) — Finding M25
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
---
|
|
||||||
issue: "015"
|
|
||||||
title: "Seller offer withdraw has no HTTP route — withdrawOffer() service method is dead code"
|
|
||||||
severity: major
|
|
||||||
domain: seller-offer
|
|
||||||
labels: [backend, missing-feature]
|
|
||||||
status: open
|
|
||||||
created: 2026-05-29
|
|
||||||
source: Doc vs Code Audit 2026-05-29
|
|
||||||
---
|
|
||||||
|
|
||||||
# 🟠 Seller offer withdraw has no HTTP route — withdrawOffer() service method is dead code
|
|
||||||
|
|
||||||
**Severity:** major
|
|
||||||
**Domain:** seller-offer
|
|
||||||
**Labels:** backend, missing-feature
|
|
||||||
|
|
||||||
## Description
|
|
||||||
|
|
||||||
`SellerOfferService.withdrawOffer()` (SellerOfferService.ts lines ~427-443) exists and implements withdrawal logic, but no HTTP route calls it. The documented `POST /api/marketplace/offers/:id/withdraw` endpoint does not exist in `routes.ts` or `marketplaceController.ts`.
|
|
||||||
|
|
||||||
There is also no frontend `withdrawOffer()` action, no withdraw button in any seller step component, and no seller offers history page at `/dashboard/seller/marketplace/offers`.
|
|
||||||
|
|
||||||
The only workaround is `PUT /api/marketplace/offers/:id/status` with `{ status: 'withdrawn' }`, which has no guard ensuring the requester is the offer's seller.
|
|
||||||
|
|
||||||
## Current Behavior
|
|
||||||
|
|
||||||
Sellers cannot withdraw their pending offers through any UI path. Withdrawing via `PUT /offers/:id/status` is the only API path and has no ownership guard.
|
|
||||||
|
|
||||||
## Expected Behavior
|
|
||||||
|
|
||||||
1. Wire a `POST /api/marketplace/offers/:id/withdraw` route to `SellerOfferService.withdrawOffer()`
|
|
||||||
2. Add an ownership guard (only the offer's seller can withdraw)
|
|
||||||
3. Add a frontend withdraw button and action
|
|
||||||
|
|
||||||
## Affected Files
|
|
||||||
|
|
||||||
- `backend/src/routes/routes.ts` — missing `POST /offers/:id/withdraw` route
|
|
||||||
- `frontend/src/actions/marketplace.ts` — missing `withdrawOffer` action
|
|
||||||
- Frontend seller dashboard — missing offers list page
|
|
||||||
|
|
||||||
## References
|
|
||||||
|
|
||||||
- [Doc vs Code Audit Report](../09%20-%20Audits/Doc%20vs%20Code%20Audit%20Report%20-%202026-05-29.md) — Findings C9, M26
|
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
---
|
||||||
|
issue: 015
|
||||||
|
title: "Simulated transaction SIM_ bypass has no environment guard — can fire in production on wallet connection failure"
|
||||||
|
severity: critical
|
||||||
|
domain: Payment
|
||||||
|
labels: [security, bug, critical, payment, frontend, bypass]
|
||||||
|
status: open
|
||||||
|
created: 2026-05-29
|
||||||
|
source: Doc vs Code Audit 2026-05-29
|
||||||
|
---
|
||||||
|
|
||||||
|
# 🔴 Simulated transaction SIM_ bypass has no environment guard — can fire in production on wallet connection failure
|
||||||
|
|
||||||
|
**Severity:** critical
|
||||||
|
**Domain:** Payment
|
||||||
|
**Labels:** security, bug, critical, payment, frontend, bypass
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
src/web3/context/web3-provider.tsx lines 225 and 232 generate SIM_ prefixed transaction hashes when wallet connection fails. The backend skips on-chain verification for any paymentHash starting with 'SIM_' — controlled only by hash prefix, not an environment flag. The frontend generates SIM_ hashes in an error fallback path that can trigger in production.
|
||||||
|
|
||||||
|
## Current Behavior
|
||||||
|
|
||||||
|
In production, if a wallet connection times out or throws, the fallback generates a SIM_ hash that passes backend verification and creates a completed payment record without any real on-chain transaction.
|
||||||
|
|
||||||
|
## Expected Behavior
|
||||||
|
|
||||||
|
SIM_ hash generation should be guarded by process.env.NODE_ENV !== 'production' check. Backend SIM_ bypass should also be gated by NODE_ENV.
|
||||||
|
|
||||||
|
## Reproduction Steps
|
||||||
|
|
||||||
|
Simulate a wallet connection failure in staging — observe that a SIM_ hash is generated and check if a completed payment record is created in the database.
|
||||||
|
|
||||||
|
## Affected Files
|
||||||
|
|
||||||
|
- `frontend/src/web3/context/web3-provider.tsx`
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Doc vs Code Audit Report](../09%20-%20Audits/Doc%20vs%20Code%20Audit%20Report%20-%202026-05-29.md)
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
---
|
|
||||||
issue: "016"
|
|
||||||
title: "createProviderPaymentIntent always routes to request-network regardless of provider — SHKeeper checkout broken"
|
|
||||||
severity: critical
|
|
||||||
domain: payment
|
|
||||||
labels: [frontend, bug]
|
|
||||||
status: open
|
|
||||||
created: 2026-05-29
|
|
||||||
source: Doc vs Code Audit 2026-05-29
|
|
||||||
---
|
|
||||||
|
|
||||||
# 🔴 createProviderPaymentIntent always routes to request-network regardless of provider — SHKeeper checkout broken
|
|
||||||
|
|
||||||
**Severity:** critical
|
|
||||||
**Domain:** payment
|
|
||||||
**Labels:** frontend, bug
|
|
||||||
|
|
||||||
## Description
|
|
||||||
|
|
||||||
`frontend/src/actions/payment.ts` — `getProviderIntentEndpoint()` ignores its `provider` argument and always returns `endpoints.payments.requestNetwork.intents` (`/payment/request-network/intents`).
|
|
||||||
|
|
||||||
If any UI component passes `provider='shkeeper'` to `createProviderPaymentIntent()`, the intent creation silently POSTs to the Request Network endpoint instead of `/payment/shkeeper/intents`. The SHKeeper intents endpoint is defined in `axios.ts` but is never reached by this factory.
|
|
||||||
|
|
||||||
## Current Behavior
|
|
||||||
|
|
||||||
A SHKeeper checkout call to `createProviderPaymentIntent('shkeeper', ...)` POSTs to `/payment/request-network/intents`. The RN endpoint creates a Request Network intent, not a SHKeeper intent. The payment provider is silently misrouted.
|
|
||||||
|
|
||||||
## Expected Behavior
|
|
||||||
|
|
||||||
`getProviderIntentEndpoint('shkeeper')` should return `endpoints.payments.shkeeper.intents`. The function should switch on the provider argument.
|
|
||||||
|
|
||||||
## Affected Files
|
|
||||||
|
|
||||||
- `frontend/src/actions/payment.ts` — `getProviderIntentEndpoint()` function (~line 444)
|
|
||||||
|
|
||||||
## References
|
|
||||||
|
|
||||||
- [Doc vs Code Audit Report](../09%20-%20Audits/Doc%20vs%20Code%20Audit%20Report%20-%202026-05-29.md) — Finding M38
|
|
||||||
- Related: [[ISSUE-017-payment-provider-type-missing-values]]
|
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
---
|
||||||
|
issue: 016
|
||||||
|
title: "updatePurchaseRequest uses PUT but backend only registers PATCH — all purchase request edits fail"
|
||||||
|
severity: major
|
||||||
|
domain: Purchase Request
|
||||||
|
labels: [bug, frontend, major, broken-feature]
|
||||||
|
status: open
|
||||||
|
created: 2026-05-29
|
||||||
|
source: Doc vs Code Audit 2026-05-29
|
||||||
|
---
|
||||||
|
|
||||||
|
# 🟠 updatePurchaseRequest uses PUT but backend only registers PATCH — all purchase request edits fail
|
||||||
|
|
||||||
|
**Severity:** major
|
||||||
|
**Domain:** Purchase Request
|
||||||
|
**Labels:** bug, frontend, major, broken-feature
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
The frontend updatePurchaseRequest action (marketplace.ts line 71) calls axiosInstance.put against '/marketplace/purchase-requests/:id'. Backend registers PATCH (not PUT) on /purchase-requests/:id. PUT returns 404 from the controller router.
|
||||||
|
|
||||||
|
## Current Behavior
|
||||||
|
|
||||||
|
Purchase request edits from the buyer edit view fail with 404/405.
|
||||||
|
|
||||||
|
## Expected Behavior
|
||||||
|
|
||||||
|
updatePurchaseRequest should call axiosInstance.patch().
|
||||||
|
|
||||||
|
## Affected Files
|
||||||
|
|
||||||
|
- `frontend/src/actions/marketplace.ts`
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Doc vs Code Audit Report](../09%20-%20Audits/Doc%20vs%20Code%20Audit%20Report%20-%202026-05-29.md)
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
---
|
|
||||||
issue: "017"
|
|
||||||
title: "PaymentProvider TypeScript type missing 'shkeeper' and 'decentralized' values"
|
|
||||||
severity: major
|
|
||||||
domain: payment
|
|
||||||
labels: [frontend, bug, typescript]
|
|
||||||
status: open
|
|
||||||
created: 2026-05-29
|
|
||||||
source: Doc vs Code Audit 2026-05-29
|
|
||||||
---
|
|
||||||
|
|
||||||
# 🟠 PaymentProvider TypeScript type missing 'shkeeper' and 'decentralized' values
|
|
||||||
|
|
||||||
**Severity:** major
|
|
||||||
**Domain:** payment
|
|
||||||
**Labels:** frontend, bug, typescript
|
|
||||||
|
|
||||||
## Description
|
|
||||||
|
|
||||||
`frontend/src/types/payment.ts` defines:
|
|
||||||
```ts
|
|
||||||
type PaymentProvider = 'request.network' | 'test' | 'other'
|
|
||||||
```
|
|
||||||
|
|
||||||
The backend accepts `'shkeeper'`, `'decentralized'`, and `'other'` as `provider` values on Payment records. The two most-used production providers (`shkeeper`, `decentralized`) are absent from the TypeScript union.
|
|
||||||
|
|
||||||
Any frontend code that switches on `payment.provider` will fall through to a default/unknown branch for all SHKeeper and DePay payments, causing incorrect UI rendering (wrong labels, missing payment method icons, etc.).
|
|
||||||
|
|
||||||
## Current Behavior
|
|
||||||
|
|
||||||
SHKeeper and DePay payments in the payment list and payment detail views may show as "Unknown provider" or trigger TypeScript errors at compile time.
|
|
||||||
|
|
||||||
## Expected Behavior
|
|
||||||
|
|
||||||
```ts
|
|
||||||
type PaymentProvider = 'request.network' | 'shkeeper' | 'decentralized' | 'test' | 'other'
|
|
||||||
```
|
|
||||||
|
|
||||||
## Affected Files
|
|
||||||
|
|
||||||
- `frontend/src/types/payment.ts` — `PaymentProvider` type definition (~line 15)
|
|
||||||
|
|
||||||
## References
|
|
||||||
|
|
||||||
- [Doc vs Code Audit Report](../09%20-%20Audits/Doc%20vs%20Code%20Audit%20Report%20-%202026-05-29.md) — Finding M37
|
|
||||||
- Related: [[ISSUE-016-payment-provider-routing-always-request-network]]
|
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
---
|
||||||
|
issue: 017
|
||||||
|
title: "updateOffer uses PUT /marketplace/offers/:id but backend registers PATCH /offers/:id — offer edits fail"
|
||||||
|
severity: major
|
||||||
|
domain: Seller Offer
|
||||||
|
labels: [bug, frontend, major, broken-feature]
|
||||||
|
status: open
|
||||||
|
created: 2026-05-29
|
||||||
|
source: Doc vs Code Audit 2026-05-29
|
||||||
|
---
|
||||||
|
|
||||||
|
# 🟠 updateOffer uses PUT /marketplace/offers/:id but backend registers PATCH /offers/:id — offer edits fail
|
||||||
|
|
||||||
|
**Severity:** major
|
||||||
|
**Domain:** Seller Offer
|
||||||
|
**Labels:** bug, frontend, major, broken-feature
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
Frontend updateOffer action (src/actions/marketplace.ts line 289) uses axiosInstance.put() against /marketplace/offers/:id. Backend registers router.patch('/offers/:id') at routes.ts line 1260. Method mismatch. step-1-send-proposal.tsx actively calls updateOffer() for existing offer edits.
|
||||||
|
|
||||||
|
## Current Behavior
|
||||||
|
|
||||||
|
Offer price/ETA/notes edits from the seller proposal form fail silently or 404.
|
||||||
|
|
||||||
|
## Expected Behavior
|
||||||
|
|
||||||
|
updateOffer should use axiosInstance.patch().
|
||||||
|
|
||||||
|
## Affected Files
|
||||||
|
|
||||||
|
- `frontend/src/actions/marketplace.ts`
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Doc vs Code Audit Report](../09%20-%20Audits/Doc%20vs%20Code%20Audit%20Report%20-%202026-05-29.md)
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
---
|
||||||
|
issue: 018
|
||||||
|
title: "select-offer updateMany has no status filter — overwrites withdrawn/rejected offers back to 'rejected' corrupting status history"
|
||||||
|
severity: major
|
||||||
|
domain: Seller Offer
|
||||||
|
labels: [bug, backend, major, data-integrity]
|
||||||
|
status: open
|
||||||
|
created: 2026-05-29
|
||||||
|
source: Doc vs Code Audit 2026-05-29
|
||||||
|
---
|
||||||
|
|
||||||
|
# 🟠 select-offer updateMany has no status filter — overwrites withdrawn/rejected offers back to 'rejected' corrupting status history
|
||||||
|
|
||||||
|
**Severity:** major
|
||||||
|
**Domain:** Seller Offer
|
||||||
|
**Labels:** bug, backend, major, data-integrity
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
The POST /purchase-requests/:id/select-offer route handler (routes.ts lines 1386-1395) uses updateMany with only {purchaseRequestId, _id: {$ne: offerId}} — no status filter. This can overwrite already-withdrawn or previously-rejected offers' status back to 'rejected', corrupting their status history. SellerOfferService.acceptOffer() correctly filters by status: {$in: ['pending', 'active']}.
|
||||||
|
|
||||||
|
## Current Behavior
|
||||||
|
|
||||||
|
Selecting an offer via the select-offer endpoint corrupts previously-withdrawn offer records by setting their status back to 'rejected'.
|
||||||
|
|
||||||
|
## Expected Behavior
|
||||||
|
|
||||||
|
The select-offer updateMany call should include a status filter: {$in: ['pending']} to only reject pending offers, matching the service-layer behavior.
|
||||||
|
|
||||||
|
## Reproduction Steps
|
||||||
|
|
||||||
|
Create a request with one withdrawn offer and one pending offer. Select the pending offer via POST /purchase-requests/:id/select-offer. Verify the withdrawn offer's status is now 'rejected'.
|
||||||
|
|
||||||
|
## Affected Files
|
||||||
|
|
||||||
|
- `backend/src/routes/routes.ts`
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Doc vs Code Audit Report](../09%20-%20Audits/Doc%20vs%20Code%20Audit%20Report%20-%202026-05-29.md)
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
---
|
|
||||||
issue: "018"
|
|
||||||
title: "Trezor Safekeeping has zero frontend implementation — all backend endpoints unreachable from UI"
|
|
||||||
severity: critical
|
|
||||||
domain: trezor
|
|
||||||
labels: [frontend, missing-feature]
|
|
||||||
status: open
|
|
||||||
created: 2026-05-29
|
|
||||||
source: Doc vs Code Audit 2026-05-29
|
|
||||||
---
|
|
||||||
|
|
||||||
# 🔴 Trezor Safekeeping has zero frontend implementation — all backend endpoints unreachable from UI
|
|
||||||
|
|
||||||
**Severity:** critical
|
|
||||||
**Domain:** trezor
|
|
||||||
**Labels:** frontend, missing-feature
|
|
||||||
|
|
||||||
## Description
|
|
||||||
|
|
||||||
A comprehensive search of all `.ts` and `.tsx` files in `frontend/src/` finds **zero calls** to any Trezor backend endpoint. There is no:
|
|
||||||
- Trezor registration page
|
|
||||||
- xpub input UI
|
|
||||||
- Trezor Connect SDK import
|
|
||||||
- Admin Trezor signing panel
|
|
||||||
- Any action calling `/api/trezor/*`
|
|
||||||
|
|
||||||
The only Trezor reference in the entire frontend is a brand logo in `wallet-icons.ts`.
|
|
||||||
|
|
||||||
The documented 12-step challenge-sign-submit flow exists entirely in the backend but has no frontend surface at any step.
|
|
||||||
|
|
||||||
Additionally, `confirmReleaseTx` and `confirmRefundTx` in `frontend/src/actions/payment.ts` post `{ txHash, ...extra }` with **no `trezor` object** (message + signature). With `TREZOR_SAFEKEEPING_REQUIRED=true`, every admin release/refund from the UI will be rejected by the backend's `assertTrezorSignatureForOperation` guard.
|
|
||||||
|
|
||||||
## Current Behavior
|
|
||||||
|
|
||||||
- No UI exists for Trezor registration
|
|
||||||
- Admin release/refund with `TREZOR_SAFEKEEPING_REQUIRED=true` always fails (missing signature payload)
|
|
||||||
- All Trezor API endpoints are only testable via curl/Postman
|
|
||||||
|
|
||||||
## Expected Behavior
|
|
||||||
|
|
||||||
A complete frontend implementation covering:
|
|
||||||
1. Trezor registration page (xpub input, challenge-sign-submit flow)
|
|
||||||
2. Operation signing UI for admin release/refund (call `POST /api/trezor/operation-message`, prompt sign, attach `trezor` object to confirm body)
|
|
||||||
|
|
||||||
## Affected Files
|
|
||||||
|
|
||||||
- `frontend/src/actions/payment.ts` — `confirmReleaseTx`, `confirmRefundTx` missing `trezor` field
|
|
||||||
- Missing: Trezor registration page component
|
|
||||||
- Missing: Admin Trezor signing integration in dispute/payment admin panels
|
|
||||||
|
|
||||||
## References
|
|
||||||
|
|
||||||
- [Doc vs Code Audit Report](../09%20-%20Audits/Doc%20vs%20Code%20Audit%20Report%20-%202026-05-29.md) — Findings C31, C32
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
---
|
|
||||||
issue: "019"
|
|
||||||
title: "Request Network admin payout/release/refund sub-routes do not exist in backend"
|
|
||||||
severity: major
|
|
||||||
domain: payment
|
|
||||||
labels: [backend, missing-feature]
|
|
||||||
status: open
|
|
||||||
created: 2026-05-29
|
|
||||||
source: Doc vs Code Audit 2026-05-29
|
|
||||||
---
|
|
||||||
|
|
||||||
# 🟠 Request Network admin payout/release/refund sub-routes do not exist in backend
|
|
||||||
|
|
||||||
**Severity:** major
|
|
||||||
**Domain:** payment
|
|
||||||
**Labels:** backend, missing-feature
|
|
||||||
|
|
||||||
## Description
|
|
||||||
|
|
||||||
`frontend/src/actions/payment.ts` exports four functions that hit non-existent backend endpoints:
|
|
||||||
|
|
||||||
| Function | Calls | Status |
|
|
||||||
|---|---|---|
|
|
||||||
| `initiateRequestNetworkPayout()` | `POST /api/payment/request-network/:id/payout/initiate` | 404 |
|
|
||||||
| `confirmRequestNetworkPayout()` | `POST /api/payment/request-network/:id/payout/confirm` | 404 |
|
|
||||||
| `confirmRequestNetworkRelease()` | `POST /api/payment/request-network/:id/release/confirm` | 404 |
|
|
||||||
| `confirmRequestNetworkRefund()` | `POST /api/payment/request-network/:id/refund/confirm` | 404 |
|
|
||||||
|
|
||||||
The backend only implements: `POST /api/payment/request-network/intents`, `GET /api/payment/request-network/:paymentId/checkout`, `POST /api/payment/request-network/webhook`.
|
|
||||||
|
|
||||||
## Current Behavior
|
|
||||||
|
|
||||||
All four admin RN payout/release/refund actions return 404. Admin has no way to complete or refund a Request Network payment through the UI.
|
|
||||||
|
|
||||||
## Expected Behavior
|
|
||||||
|
|
||||||
Backend should implement the four sub-routes, or the frontend actions should be mapped to the actual release/refund mechanism.
|
|
||||||
|
|
||||||
## Affected Files
|
|
||||||
|
|
||||||
- `frontend/src/actions/payment.ts` — `initiateRequestNetworkPayout`, `confirmRequestNetworkPayout`, `confirmRequestNetworkRelease`, `confirmRequestNetworkRefund`
|
|
||||||
- Backend: missing `request-network/:id/payout/*`, `release/confirm`, `refund/confirm` routes
|
|
||||||
|
|
||||||
## References
|
|
||||||
|
|
||||||
- [Doc vs Code Audit Report](../09%20-%20Audits/Doc%20vs%20Code%20Audit%20Report%20-%202026-05-29.md) — Finding M34
|
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
---
|
||||||
|
issue: 019
|
||||||
|
title: "SellerOffer.status 'active' does not exist in schema enum but is referenced in docs and code comments"
|
||||||
|
severity: major
|
||||||
|
domain: Seller Offer
|
||||||
|
labels: [bug, backend, major, data-model]
|
||||||
|
status: open
|
||||||
|
created: 2026-05-29
|
||||||
|
source: Doc vs Code Audit 2026-05-29
|
||||||
|
---
|
||||||
|
|
||||||
|
# 🟠 SellerOffer.status 'active' does not exist in schema enum but is referenced in docs and code comments
|
||||||
|
|
||||||
|
**Severity:** major
|
||||||
|
**Domain:** Seller Offer
|
||||||
|
**Labels:** bug, backend, major, data-model
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
SellerOffer Mongoose schema (SellerOffer.ts line 80) and TypeScript interface (line 17) enumerate only 'pending | accepted | rejected | withdrawn'. Attempting to save status='active' on a SellerOffer will throw a Mongoose ValidationError. Any code path that sets status='active' on a SellerOffer will fail at runtime.
|
||||||
|
|
||||||
|
## Current Behavior
|
||||||
|
|
||||||
|
Any attempt to set a SellerOffer to 'active' throws Mongoose ValidationError.
|
||||||
|
|
||||||
|
## Expected Behavior
|
||||||
|
|
||||||
|
Either add 'active' to the SellerOffer status enum if it is a real state, or remove all references to it from code comments and remove the documented state machine entry.
|
||||||
|
|
||||||
|
## Affected Files
|
||||||
|
|
||||||
|
- `backend/src/models/SellerOffer.ts`
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Doc vs Code Audit Report](../09%20-%20Audits/Doc%20vs%20Code%20Audit%20Report%20-%202026-05-29.md)
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
---
|
|
||||||
issue: "020"
|
|
||||||
title: "POST /api/disputes/:id/assign has no role guard — any user can self-assign as mediator"
|
|
||||||
severity: major
|
|
||||||
domain: dispute
|
|
||||||
labels: [security, backend, bug]
|
|
||||||
status: open
|
|
||||||
created: 2026-05-29
|
|
||||||
source: Doc vs Code Audit 2026-05-29
|
|
||||||
---
|
|
||||||
|
|
||||||
# 🟠 POST /api/disputes/:id/assign has no role guard — any user can self-assign as mediator
|
|
||||||
|
|
||||||
**Severity:** major
|
|
||||||
**Domain:** dispute
|
|
||||||
**Labels:** security, backend, bug
|
|
||||||
|
|
||||||
## Description
|
|
||||||
|
|
||||||
`POST /api/disputes/:id/assign` is mounted with only `authenticateToken`. Any authenticated buyer or seller can assign themselves as the mediator/admin for any open dispute.
|
|
||||||
|
|
||||||
## Current Behavior
|
|
||||||
|
|
||||||
```bash
|
|
||||||
POST /api/disputes/{disputeId}/assign
|
|
||||||
Authorization: Bearer <buyer-jwt>
|
|
||||||
{ "adminId": "<buyer-user-id>" }
|
|
||||||
```
|
|
||||||
Returns 200 and sets the dispute's assigned mediator to the buyer.
|
|
||||||
|
|
||||||
## Expected Behavior
|
|
||||||
|
|
||||||
Should require `authorizeRoles('admin')`. Non-admin tokens should receive `403`.
|
|
||||||
|
|
||||||
## Affected Files
|
|
||||||
|
|
||||||
- `backend/src/routes/disputeRoutes.ts` — missing role guard on the assign route
|
|
||||||
|
|
||||||
## References
|
|
||||||
|
|
||||||
- [Doc vs Code Audit Report](../09%20-%20Audits/Doc%20vs%20Code%20Audit%20Report%20-%202026-05-29.md)
|
|
||||||
- Related: [[ISSUE-001-dispute-status-no-role-guard]], [[ISSUE-002-dispute-resolve-no-role-guard]]
|
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
---
|
||||||
|
issue: 020
|
||||||
|
title: "select-offer does not send per-seller socket events or notifications to winning or losing sellers"
|
||||||
|
severity: major
|
||||||
|
domain: Seller Offer
|
||||||
|
labels: [bug, backend, major, notifications, socket]
|
||||||
|
status: open
|
||||||
|
created: 2026-05-29
|
||||||
|
source: Doc vs Code Audit 2026-05-29
|
||||||
|
---
|
||||||
|
|
||||||
|
# 🟠 select-offer does not send per-seller socket events or notifications to winning or losing sellers
|
||||||
|
|
||||||
|
**Severity:** major
|
||||||
|
**Domain:** Seller Offer
|
||||||
|
**Labels:** bug, backend, major, notifications, socket
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
POST /purchase-requests/:id/select-offer route (routes.ts lines 1300-1438) emits only a single purchase-request-update event to the request room. It does NOT call notifyOfferAccepted, does NOT call notifyOfferRejected for losing sellers, and does NOT emit seller-offer-update events. Those notifications only fire via SellerOfferService.updateOfferStatus().
|
||||||
|
|
||||||
|
## Current Behavior
|
||||||
|
|
||||||
|
After a buyer selects an offer via select-offer, the winning seller receives no notification and losing sellers receive no rejection notification.
|
||||||
|
|
||||||
|
## Expected Behavior
|
||||||
|
|
||||||
|
The select-offer path should emit per-seller socket events and notifications equivalent to what SellerOfferService.acceptOffer() does — notify the winning seller and each losing seller.
|
||||||
|
|
||||||
|
## Affected Files
|
||||||
|
|
||||||
|
- `backend/src/routes/routes.ts`
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Doc vs Code Audit Report](../09%20-%20Audits/Doc%20vs%20Code%20Audit%20Report%20-%202026-05-29.md)
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
---
|
|
||||||
issue: "021"
|
|
||||||
title: "Axios interceptor only retriggers token refresh for 401, not 403"
|
|
||||||
severity: major
|
|
||||||
domain: auth
|
|
||||||
labels: [frontend, bug]
|
|
||||||
status: open
|
|
||||||
created: 2026-05-29
|
|
||||||
source: Doc vs Code Audit 2026-05-29
|
|
||||||
---
|
|
||||||
|
|
||||||
# 🟠 Axios interceptor only retriggers token refresh for 401, not 403
|
|
||||||
|
|
||||||
**Severity:** major
|
|
||||||
**Domain:** auth
|
|
||||||
**Labels:** frontend, bug
|
|
||||||
|
|
||||||
## Description
|
|
||||||
|
|
||||||
`frontend/src/lib/axios.ts` (line ~105) only triggers the token refresh flow for `status === 401`:
|
|
||||||
```ts
|
|
||||||
if (status === 401 && !isAuthRoute && !originalRequest?._retry) {
|
|
||||||
// trigger refresh
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
A `403` response (e.g., `EMAIL_NOT_VERIFIED`, a blocked account, or an under-privileged action) is not intercepted — it propagates as an unhandled error. Depending on how calling components handle errors, this may result in a blank screen or silent failure rather than an appropriate user message.
|
|
||||||
|
|
||||||
## Current Behavior
|
|
||||||
|
|
||||||
Backend returns `403 EMAIL_NOT_VERIFIED` → interceptor does not retry or refresh → error propagates to the component. Some components may not handle this gracefully.
|
|
||||||
|
|
||||||
## Expected Behavior
|
|
||||||
|
|
||||||
The interceptor (or a separate error handler) should:
|
|
||||||
- On `403`: **not** attempt a token refresh (a 403 is an authorization failure, not an expired token)
|
|
||||||
- But should surface the error clearly to the user (e.g., redirect to verify-email page for `EMAIL_NOT_VERIFIED` errors)
|
|
||||||
|
|
||||||
## Affected Files
|
|
||||||
|
|
||||||
- `frontend/src/lib/axios.ts` — response interceptor
|
|
||||||
|
|
||||||
## References
|
|
||||||
|
|
||||||
- [Doc vs Code Audit Report](../09%20-%20Audits/Doc%20vs%20Code%20Audit%20Report%20-%202026-05-29.md) — Finding M1
|
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
---
|
||||||
|
issue: 021
|
||||||
|
title: "POST /api/marketplace/offers/:id/withdraw HTTP route does not exist — seller withdraw is dead code"
|
||||||
|
severity: major
|
||||||
|
domain: Seller Offer
|
||||||
|
labels: [missing-feature, backend, frontend, major]
|
||||||
|
status: open
|
||||||
|
created: 2026-05-29
|
||||||
|
source: Doc vs Code Audit 2026-05-29
|
||||||
|
---
|
||||||
|
|
||||||
|
# 🟠 POST /api/marketplace/offers/:id/withdraw HTTP route does not exist — seller withdraw is dead code
|
||||||
|
|
||||||
|
**Severity:** major
|
||||||
|
**Domain:** Seller Offer
|
||||||
|
**Labels:** missing-feature, backend, frontend, major
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
SellerOfferService.withdrawOffer() method exists (lines 427-443) but no HTTP route calls it. The only way to withdraw is via PUT /offers/:id/status with {status:'withdrawn'} which applies no pending-only guard. No frontend withdraw button or action exists.
|
||||||
|
|
||||||
|
## Current Behavior
|
||||||
|
|
||||||
|
Sellers have no UI path to withdraw an offer. withdrawOffer() service method is unreachable via HTTP. The route-level withdrawal via PUT /status has no transition guard.
|
||||||
|
|
||||||
|
## Expected Behavior
|
||||||
|
|
||||||
|
A dedicated withdraw endpoint should be registered, calling withdrawOffer() which enforces the pending-only guard. Or the PUT /offers/:id/status path should enforce status transition guards.
|
||||||
|
|
||||||
|
## Affected Files
|
||||||
|
|
||||||
|
- `backend/src/routes/routes.ts`
|
||||||
|
- `frontend/src/actions/marketplace.ts`
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Doc vs Code Audit Report](../09%20-%20Audits/Doc%20vs%20Code%20Audit%20Report%20-%202026-05-29.md)
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
---
|
||||||
|
issue: 022
|
||||||
|
title: "GET /api/payment/payments/:id/debug has no authentication — full payment data exposed without credentials"
|
||||||
|
severity: major
|
||||||
|
domain: Payment
|
||||||
|
labels: [security, bug, backend, major, missing-auth]
|
||||||
|
status: open
|
||||||
|
created: 2026-05-29
|
||||||
|
source: Doc vs Code Audit 2026-05-29
|
||||||
|
---
|
||||||
|
|
||||||
|
# 🟠 GET /api/payment/payments/:id/debug has no authentication — full payment data exposed without credentials
|
||||||
|
|
||||||
|
**Severity:** major
|
||||||
|
**Domain:** Payment
|
||||||
|
**Labels:** security, bug, backend, major, missing-auth
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
GET /api/payment/payments/:id/debug returns payment document plus walletMonitor status without any authentication middleware. Backend code explicitly flags this as a security issue.
|
||||||
|
|
||||||
|
## Current Behavior
|
||||||
|
|
||||||
|
Any unauthenticated caller can read full payment data including blockchain metadata.
|
||||||
|
|
||||||
|
## Expected Behavior
|
||||||
|
|
||||||
|
Should require authenticateToken + authorizeRoles('admin').
|
||||||
|
|
||||||
|
## Affected Files
|
||||||
|
|
||||||
|
- `backend/src/routes/paymentRoutes.ts`
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Doc vs Code Audit Report](../09%20-%20Audits/Doc%20vs%20Code%20Audit%20Report%20-%202026-05-29.md)
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
---
|
|
||||||
issue: "022"
|
|
||||||
title: "Login rate limiter counts all attempts (not just failures) — users can be locked out after correct logins"
|
|
||||||
severity: major
|
|
||||||
domain: auth
|
|
||||||
labels: [backend, bug]
|
|
||||||
status: open
|
|
||||||
created: 2026-05-29
|
|
||||||
source: Doc vs Code Audit 2026-05-29
|
|
||||||
---
|
|
||||||
|
|
||||||
# 🟠 Login rate limiter counts all attempts (not just failures) — users can be locked out after correct logins
|
|
||||||
|
|
||||||
**Severity:** major
|
|
||||||
**Domain:** auth
|
|
||||||
**Labels:** backend, bug
|
|
||||||
|
|
||||||
## Description
|
|
||||||
|
|
||||||
`rateLimitService.checkLoginAttempts()` calls `checkLimit()` which calls `redisService.incr` — incrementing the counter on **every invocation**, before password comparison. The counter is only reset after a full successful login (password verified + session created).
|
|
||||||
|
|
||||||
With the limit at 5 attempts/15 min, a user who makes 4 correct logins in quick succession (e.g., testing on multiple devices) followed by 1 wrong password will be locked out immediately, even though they never "failed" 5 times in the intended sense.
|
|
||||||
|
|
||||||
## Current Behavior
|
|
||||||
|
|
||||||
5 total login attempts within 15 minutes (any combination of correct/incorrect passwords) triggers `429 TOO_MANY_ATTEMPTS`.
|
|
||||||
|
|
||||||
## Expected Behavior
|
|
||||||
|
|
||||||
The counter should only increment on **failed** password comparison, not on every attempt. Alternatively, the behaviour should be clearly documented so UX can warn users appropriately.
|
|
||||||
|
|
||||||
## Affected Files
|
|
||||||
|
|
||||||
- `backend/src/services/auth/rateLimitService.ts` — `checkLoginAttempts` / `checkLimit` — counter increment should move to after password comparison in `authController.ts`
|
|
||||||
|
|
||||||
## References
|
|
||||||
|
|
||||||
- [Doc vs Code Audit Report](../09%20-%20Audits/Doc%20vs%20Code%20Audit%20Report%20-%202026-05-29.md) — Finding M3
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
---
|
|
||||||
issue: "023"
|
|
||||||
title: "changePassword action exists but no dashboard UI page exposes it"
|
|
||||||
severity: major
|
|
||||||
domain: auth
|
|
||||||
labels: [frontend, missing-feature]
|
|
||||||
status: open
|
|
||||||
created: 2026-05-29
|
|
||||||
source: Doc vs Code Audit 2026-05-29
|
|
||||||
---
|
|
||||||
|
|
||||||
# 🟠 changePassword action exists but no dashboard UI page exposes it
|
|
||||||
|
|
||||||
**Severity:** major
|
|
||||||
**Domain:** auth
|
|
||||||
**Labels:** frontend, missing-feature
|
|
||||||
|
|
||||||
## Description
|
|
||||||
|
|
||||||
`frontend/src/actions/account.ts` (line ~263) defines `changePassword()` which calls `POST /api/auth/change-password`. The backend endpoint exists and `changePasswordValidation` enforces password complexity (uppercase + lowercase + digit). However, **no dashboard page or component renders a change-password form**. The feature is API-only.
|
|
||||||
|
|
||||||
## Current Behavior
|
|
||||||
|
|
||||||
Users have no UI path to change their password after login. The only password reset mechanism is the email-based reset flow.
|
|
||||||
|
|
||||||
## Expected Behavior
|
|
||||||
|
|
||||||
A "Change Password" section in the account settings dashboard (e.g., under `/dashboard/account`) that calls `changePassword()` with `{ currentPassword, newPassword }`.
|
|
||||||
|
|
||||||
## Affected Files
|
|
||||||
|
|
||||||
- Missing: Change password form component in `/dashboard/account` or `/dashboard/account/security`
|
|
||||||
- `frontend/src/actions/account.ts` — `changePassword` function (implemented, no callers)
|
|
||||||
|
|
||||||
## References
|
|
||||||
|
|
||||||
- [Doc vs Code Audit Report](../09%20-%20Audits/Doc%20vs%20Code%20Audit%20Report%20-%202026-05-29.md) — Finding M4
|
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
---
|
||||||
|
issue: 023
|
||||||
|
title: "GET /api/payment/export has no admin role guard at route level — any authenticated user can export all payment data"
|
||||||
|
severity: major
|
||||||
|
domain: Payment
|
||||||
|
labels: [security, bug, backend, major, privilege-escalation]
|
||||||
|
status: open
|
||||||
|
created: 2026-05-29
|
||||||
|
source: Doc vs Code Audit 2026-05-29
|
||||||
|
---
|
||||||
|
|
||||||
|
# 🟠 GET /api/payment/export has no admin role guard at route level — any authenticated user can export all payment data
|
||||||
|
|
||||||
|
**Severity:** major
|
||||||
|
**Domain:** Payment
|
||||||
|
**Labels:** security, bug, backend, major, privilege-escalation
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
GET /api/payment/export (controller-pattern route) has only authenticateToken — no admin guard at the router level. The parallel /api/payment/payments/export route has an admin role guard. The frontend hits the non-admin-gated path. Any authenticated buyer can export all payment records.
|
||||||
|
|
||||||
|
## Current Behavior
|
||||||
|
|
||||||
|
Non-admin buyers can call GET /api/payment/export and receive payment export data for all users.
|
||||||
|
|
||||||
|
## Expected Behavior
|
||||||
|
|
||||||
|
GET /api/payment/export should apply authorizeRoles('admin') at the route level.
|
||||||
|
|
||||||
|
## Affected Files
|
||||||
|
|
||||||
|
- `backend/src/routes/paymentRoutes.ts`
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Doc vs Code Audit Report](../09%20-%20Audits/Doc%20vs%20Code%20Audit%20Report%20-%202026-05-29.md)
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
---
|
||||||
|
issue: 024
|
||||||
|
title: "GET /api/payment/stats has no admin role guard — any authenticated user can read aggregate payment stats"
|
||||||
|
severity: major
|
||||||
|
domain: Payment
|
||||||
|
labels: [security, bug, backend, major, privilege-escalation]
|
||||||
|
status: open
|
||||||
|
created: 2026-05-29
|
||||||
|
source: Doc vs Code Audit 2026-05-29
|
||||||
|
---
|
||||||
|
|
||||||
|
# 🟠 GET /api/payment/stats has no admin role guard — any authenticated user can read aggregate payment stats
|
||||||
|
|
||||||
|
**Severity:** major
|
||||||
|
**Domain:** Payment
|
||||||
|
**Labels:** security, bug, backend, major, privilege-escalation
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
GET /api/payment/stats (controller-pattern route) requires only authenticateToken. The /api/payment/payments/stats route requires admin role. Frontend uses the non-admin-gated path.
|
||||||
|
|
||||||
|
## Current Behavior
|
||||||
|
|
||||||
|
Any authenticated buyer can read aggregate payment platform statistics.
|
||||||
|
|
||||||
|
## Expected Behavior
|
||||||
|
|
||||||
|
Stats endpoint should be admin-only or return only caller-scoped data for non-admins.
|
||||||
|
|
||||||
|
## Affected Files
|
||||||
|
|
||||||
|
- `backend/src/routes/paymentRoutes.ts`
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Doc vs Code Audit Report](../09%20-%20Audits/Doc%20vs%20Code%20Audit%20Report%20-%202026-05-29.md)
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
---
|
|
||||||
issue: "024"
|
|
||||||
title: "POST /api/auth/reset-password-with-code accepts weak passwords — no complexity validation"
|
|
||||||
severity: major
|
|
||||||
domain: auth
|
|
||||||
labels: [backend, security, bug]
|
|
||||||
status: open
|
|
||||||
created: 2026-05-29
|
|
||||||
source: Doc vs Code Audit 2026-05-29
|
|
||||||
---
|
|
||||||
|
|
||||||
# 🟠 POST /api/auth/reset-password-with-code accepts weak passwords — no complexity validation
|
|
||||||
|
|
||||||
**Severity:** major
|
|
||||||
**Domain:** auth
|
|
||||||
**Labels:** backend, security, bug
|
|
||||||
|
|
||||||
## Description
|
|
||||||
|
|
||||||
`POST /api/auth/reset-password-with-code` has **no `passwordResetValidation` middleware** (`authRoutes.ts` line ~54-57). The controller only validates that email, code, and password fields are present, and that the code is 6 digits.
|
|
||||||
|
|
||||||
Passwords like `'123456'`, `'aaaaaa'`, or `'password'` are accepted.
|
|
||||||
|
|
||||||
By contrast, the legacy `POST /api/auth/reset-password` (token-based) is wired with `passwordResetValidation` which enforces `/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/` — at least one uppercase, one lowercase, one digit.
|
|
||||||
|
|
||||||
## Current Behavior
|
|
||||||
|
|
||||||
`POST /api/auth/reset-password-with-code` with `{ email, code: "123456", password: "aaaaaa" }` → 200, password reset to weak value.
|
|
||||||
|
|
||||||
## Expected Behavior
|
|
||||||
|
|
||||||
Apply `passwordResetValidation` (or equivalent inline validation) to `reset-password-with-code` as well.
|
|
||||||
|
|
||||||
## Affected Files
|
|
||||||
|
|
||||||
- `backend/src/routes/authRoutes.ts` — line ~54-57, add `passwordResetValidation` middleware
|
|
||||||
- `backend/src/shared/middleware/authValidation.ts` — `passwordResetValidation` definition
|
|
||||||
|
|
||||||
## References
|
|
||||||
|
|
||||||
- [Doc vs Code Audit Report](../09%20-%20Audits/Doc%20vs%20Code%20Audit%20Report%20-%202026-05-29.md) — Finding M6
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
---
|
|
||||||
issue: "025"
|
|
||||||
title: "All dispute socket events are commented-out TODO stubs — no real-time updates in dispute flow"
|
|
||||||
severity: major
|
|
||||||
domain: dispute
|
|
||||||
labels: [backend, missing-feature]
|
|
||||||
status: open
|
|
||||||
created: 2026-05-29
|
|
||||||
source: Doc vs Code Audit 2026-05-29
|
|
||||||
---
|
|
||||||
|
|
||||||
# 🟠 All dispute socket events are commented-out TODO stubs — no real-time updates in dispute flow
|
|
||||||
|
|
||||||
**Severity:** major
|
|
||||||
**Domain:** dispute
|
|
||||||
**Labels:** backend, missing-feature
|
|
||||||
|
|
||||||
## Description
|
|
||||||
|
|
||||||
Every `socket.io` emit block in `DisputeService` is currently commented out as a TODO. No real-time updates fire for any dispute lifecycle event:
|
|
||||||
- Dispute created
|
|
||||||
- Admin assigned
|
|
||||||
- Status changed
|
|
||||||
- Evidence uploaded
|
|
||||||
- Resolution posted
|
|
||||||
|
|
||||||
The dispute flow is CRUD-only. Any UI component that relies on socket events for real-time dispute state will never receive updates.
|
|
||||||
|
|
||||||
## Current Behavior
|
|
||||||
|
|
||||||
All dispute state changes are only visible after a manual page refresh.
|
|
||||||
|
|
||||||
## Expected Behavior
|
|
||||||
|
|
||||||
Implement the socket emit calls for key dispute events:
|
|
||||||
- `dispute-created` → to buyer, seller, and admin rooms
|
|
||||||
- `dispute-status-changed` → to involved parties
|
|
||||||
- `dispute-resolved` → to buyer and seller rooms
|
|
||||||
|
|
||||||
## Affected Files
|
|
||||||
|
|
||||||
- `backend/src/services/dispute/disputeService.ts` — all commented-out `io.to(...).emit(...)` blocks
|
|
||||||
|
|
||||||
## References
|
|
||||||
|
|
||||||
- [Doc vs Code Audit Report](../09%20-%20Audits/Doc%20vs%20Code%20Audit%20Report%20-%202026-05-29.md)
|
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
---
|
||||||
|
issue: 025
|
||||||
|
title: "GET /api/disputes/statistics has no admin role guard — any authenticated user can access aggregate dispute KPIs"
|
||||||
|
severity: major
|
||||||
|
domain: Dispute
|
||||||
|
labels: [security, bug, backend, major, privilege-escalation]
|
||||||
|
status: open
|
||||||
|
created: 2026-05-29
|
||||||
|
source: Doc vs Code Audit 2026-05-29
|
||||||
|
---
|
||||||
|
|
||||||
|
# 🟠 GET /api/disputes/statistics has no admin role guard — any authenticated user can access aggregate dispute KPIs
|
||||||
|
|
||||||
|
**Severity:** major
|
||||||
|
**Domain:** Dispute
|
||||||
|
**Labels:** security, bug, backend, major, privilege-escalation
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
Backend registers GET /api/disputes/statistics with authenticateToken only. No authorizeRoles(admin) guard is applied at the route or controller level. Any authenticated non-admin user can access aggregate dispute platform data.
|
||||||
|
|
||||||
|
## Current Behavior
|
||||||
|
|
||||||
|
Non-admin authenticated users can call GET /api/disputes/statistics and receive platform-wide KPI data.
|
||||||
|
|
||||||
|
## Expected Behavior
|
||||||
|
|
||||||
|
Return 403 for non-admin tokens. Apply authorizeRoles('admin') at the route level.
|
||||||
|
|
||||||
|
## Affected Files
|
||||||
|
|
||||||
|
- `backend/src/routes/disputeRoutes.ts`
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Doc vs Code Audit Report](../09%20-%20Audits/Doc%20vs%20Code%20Audit%20Report%20-%202026-05-29.md)
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
---
|
||||||
|
issue: 026
|
||||||
|
title: "GET /notifications/:id only returns user's most-recent notification — all others return 404 erroneously"
|
||||||
|
severity: major
|
||||||
|
domain: Notification
|
||||||
|
labels: [bug, backend, major, broken-feature]
|
||||||
|
status: open
|
||||||
|
created: 2026-05-29
|
||||||
|
source: Doc vs Code Audit 2026-05-29
|
||||||
|
---
|
||||||
|
|
||||||
|
# 🟠 GET /notifications/:id only returns user's most-recent notification — all others return 404 erroneously
|
||||||
|
|
||||||
|
**Severity:** major
|
||||||
|
**Domain:** Notification
|
||||||
|
**Labels:** bug, backend, major, broken-feature
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
The backend getNotificationById controller calls getUserNotifications(userId, 1, 1) — fetching page 1 with limit 1 — then does an in-memory _id string match. Any notification that is not the single most-recent record for that user always returns 404, regardless of ownership.
|
||||||
|
|
||||||
|
## Current Behavior
|
||||||
|
|
||||||
|
GET /notifications/:id returns 404 for all notifications except the user's most recently created one.
|
||||||
|
|
||||||
|
## Expected Behavior
|
||||||
|
|
||||||
|
getNotificationById should perform a direct MongoDB query by _id and userId: Notification.findOne({_id, userId}).
|
||||||
|
|
||||||
|
## Affected Files
|
||||||
|
|
||||||
|
- `backend/src/controllers/notificationController.ts`
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Doc vs Code Audit Report](../09%20-%20Audits/Doc%20vs%20Code%20Audit%20Report%20-%202026-05-29.md)
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
---
|
|
||||||
issue: "026"
|
|
||||||
title: "'completed' payment status not counted in successfulPayments stats — admin dashboard undercounts"
|
|
||||||
severity: major
|
|
||||||
domain: payment
|
|
||||||
labels: [backend, bug]
|
|
||||||
status: open
|
|
||||||
created: 2026-05-29
|
|
||||||
source: Doc vs Code Audit 2026-05-29
|
|
||||||
---
|
|
||||||
|
|
||||||
# 🟠 'completed' payment status not counted in successfulPayments stats — admin dashboard undercounts
|
|
||||||
|
|
||||||
**Severity:** major
|
|
||||||
**Domain:** payment
|
|
||||||
**Labels:** backend, bug
|
|
||||||
|
|
||||||
## Description
|
|
||||||
|
|
||||||
`paymentService.getPaymentStats()` aggregate counts only `'confirmed'` as `successfulPayments`. `'completed'` is excluded from this count.
|
|
||||||
|
|
||||||
Most SHKeeper payments follow the terminal path: `pending → processing → completed`. `'confirmed'` is a separate RN-specific intermediate state. This means the vast majority of successfully completed payments (SHKeeper + DePay) are **invisible in the `successfulPayments` count** in the admin stats endpoint.
|
|
||||||
|
|
||||||
## Current Behavior
|
|
||||||
|
|
||||||
Admin dashboard shows a `successfulPayments` count that excludes all `'completed'` status payments. For a platform where SHKeeper is the primary payment provider, this count is close to 0 even when hundreds of payments have succeeded.
|
|
||||||
|
|
||||||
## Expected Behavior
|
|
||||||
|
|
||||||
`successfulPayments` should count payments in both `'confirmed'` and `'completed'` status, or the aggregate should be documented with a clear note about which statuses are terminal success states.
|
|
||||||
|
|
||||||
## Affected Files
|
|
||||||
|
|
||||||
- `backend/src/services/payment/paymentService.ts` — `getPaymentStats()` aggregate pipeline
|
|
||||||
|
|
||||||
## References
|
|
||||||
|
|
||||||
- [Doc vs Code Audit Report](../09%20-%20Audits/Doc%20vs%20Code%20Audit%20Report%20-%202026-05-29.md) — Finding M36
|
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
---
|
||||||
|
issue: 027
|
||||||
|
title: "confirm-delivery endpoint has no ownership check — any authenticated user can confirm delivery on any request"
|
||||||
|
severity: major
|
||||||
|
domain: Delivery
|
||||||
|
labels: [security, bug, backend, major, authorization]
|
||||||
|
status: open
|
||||||
|
created: 2026-05-29
|
||||||
|
source: Doc vs Code Audit 2026-05-29
|
||||||
|
---
|
||||||
|
|
||||||
|
# 🟠 confirm-delivery endpoint has no ownership check — any authenticated user can confirm delivery on any request
|
||||||
|
|
||||||
|
**Severity:** major
|
||||||
|
**Domain:** Delivery
|
||||||
|
**Labels:** security, bug, backend, major, authorization
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
marketplaceController.confirmDelivery (line 782) checks dispute gate and status === 'delivery' but does NOT verify the caller is the buyer of the request. Any authenticated user who knows a purchaseRequestId in 'delivery' status can call PATCH /confirm-delivery and advance it to 'delivered'.
|
||||||
|
|
||||||
|
## Current Behavior
|
||||||
|
|
||||||
|
Sellers, admins, or any authenticated third party can call confirm-delivery and mark a request as delivered without the buyer's involvement.
|
||||||
|
|
||||||
|
## Expected Behavior
|
||||||
|
|
||||||
|
confirmDelivery should verify req.user.id === purchaseRequest.buyerId before proceeding.
|
||||||
|
|
||||||
|
## Affected Files
|
||||||
|
|
||||||
|
- `backend/src/controllers/marketplaceController.ts`
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Doc vs Code Audit Report](../09%20-%20Audits/Doc%20vs%20Code%20Audit%20Report%20-%202026-05-29.md)
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
---
|
|
||||||
issue: "027"
|
|
||||||
title: "GET /api/notifications/:id always 404s for non-latest notifications — broken in-memory lookup"
|
|
||||||
severity: major
|
|
||||||
domain: notification
|
|
||||||
labels: [backend, bug]
|
|
||||||
status: open
|
|
||||||
created: 2026-05-29
|
|
||||||
source: Doc vs Code Audit 2026-05-29
|
|
||||||
---
|
|
||||||
|
|
||||||
# 🟠 GET /api/notifications/:id always 404s for non-latest notifications — broken in-memory lookup
|
|
||||||
|
|
||||||
**Severity:** major
|
|
||||||
**Domain:** notification
|
|
||||||
**Labels:** backend, bug
|
|
||||||
|
|
||||||
## Description
|
|
||||||
|
|
||||||
The `getNotificationById` controller does NOT perform a direct MongoDB `findById` lookup. Instead it calls `getUserNotifications(userId, 1, 1)` — fetching only the user's single most-recent notification — and then does an **in-memory `_id` string comparison**.
|
|
||||||
|
|
||||||
Any notification that is not the user's absolute latest record returns `404`, regardless of ownership. This makes the endpoint completely unreliable for any consumer that tries to fetch a specific notification by ID.
|
|
||||||
|
|
||||||
## Current Behavior
|
|
||||||
|
|
||||||
`GET /api/notifications/abc123` returns the notification only if `abc123` happens to be the user's most recently created notification. For all others: 404.
|
|
||||||
|
|
||||||
## Expected Behavior
|
|
||||||
|
|
||||||
`getNotificationById` should do a direct `Notification.findOne({ _id: id, userId })` query.
|
|
||||||
|
|
||||||
## Affected Files
|
|
||||||
|
|
||||||
- `backend/src/services/notification/notificationService.ts` (or controller) — `getNotificationById` / `getUserNotifications` call
|
|
||||||
|
|
||||||
## References
|
|
||||||
|
|
||||||
- [Doc vs Code Audit Report](../09%20-%20Audits/Doc%20vs%20Code%20Audit%20Report%20-%202026-05-29.md) — Finding C22
|
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
---
|
||||||
|
issue: 028
|
||||||
|
title: "delivery-code-generated socket event broadcasts raw 6-digit code to entire request room including seller"
|
||||||
|
severity: major
|
||||||
|
domain: Delivery
|
||||||
|
labels: [security, bug, backend, major, delivery]
|
||||||
|
status: open
|
||||||
|
created: 2026-05-29
|
||||||
|
source: Doc vs Code Audit 2026-05-29
|
||||||
|
---
|
||||||
|
|
||||||
|
# 🟠 delivery-code-generated socket event broadcasts raw 6-digit code to entire request room including seller
|
||||||
|
|
||||||
|
**Severity:** major
|
||||||
|
**Domain:** Delivery
|
||||||
|
**Labels:** security, bug, backend, major, delivery
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
DeliveryService.generateDeliveryCode emits 'delivery-code-generated' with the raw 6-digit code to the room request-{id}. Both buyer and seller are subscribers of this room. A seller with socket access can intercept the code before physical handoff, defeating the security purpose of the code-based handoff verification.
|
||||||
|
|
||||||
|
## Current Behavior
|
||||||
|
|
||||||
|
DeliveryService.ts line 55 broadcasts {requestId, code, expiresAt, timestamp} to all room subscribers. Seller receives the code via socket before physically receiving the goods.
|
||||||
|
|
||||||
|
## Expected Behavior
|
||||||
|
|
||||||
|
The code should only be emitted to the buyer's personal room (user-{buyerId}), not the shared request room.
|
||||||
|
|
||||||
|
## Affected Files
|
||||||
|
|
||||||
|
- `backend/src/services/deliveryService.ts`
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Doc vs Code Audit Report](../09%20-%20Audits/Doc%20vs%20Code%20Audit%20Report%20-%202026-05-29.md)
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
---
|
|
||||||
issue: "028"
|
|
||||||
title: "GET /api/payment/export has no admin role guard — any authenticated user can export payment data"
|
|
||||||
severity: major
|
|
||||||
domain: payment
|
|
||||||
labels: [security, backend, bug]
|
|
||||||
status: open
|
|
||||||
created: 2026-05-29
|
|
||||||
source: Doc vs Code Audit 2026-05-29
|
|
||||||
---
|
|
||||||
|
|
||||||
# 🟠 GET /api/payment/export has no admin role guard — any authenticated user can export payment data
|
|
||||||
|
|
||||||
**Severity:** major
|
|
||||||
**Domain:** payment
|
|
||||||
**Labels:** security, backend, bug
|
|
||||||
|
|
||||||
## Description
|
|
||||||
|
|
||||||
Two parallel export endpoints exist:
|
|
||||||
- `GET /api/payment/payments/export` — has `authorizeRoles('admin')` guard (correct)
|
|
||||||
- `GET /api/payment/export` (controller-pattern route) — only has `authenticateToken`, **no admin guard**
|
|
||||||
|
|
||||||
The frontend hits `/payment/export` (the controller-pattern route without the admin guard). Any authenticated buyer can export payment records.
|
|
||||||
|
|
||||||
## Current Behavior
|
|
||||||
|
|
||||||
`GET /api/payment/export` with any valid user JWT → 200 with payment export data.
|
|
||||||
|
|
||||||
## Expected Behavior
|
|
||||||
|
|
||||||
`GET /api/payment/export` should require `authorizeRoles('admin')`, or the frontend should be pointed at `/api/payment/payments/export`.
|
|
||||||
|
|
||||||
## Affected Files
|
|
||||||
|
|
||||||
- Backend: controller-pattern route for `GET /payment/export` — missing `authorizeRoles('admin')`
|
|
||||||
- `frontend/src/lib/axios.ts` — `endpoints.payments.export` maps to the wrong route
|
|
||||||
|
|
||||||
## References
|
|
||||||
|
|
||||||
- [Doc vs Code Audit Report](../09%20-%20Audits/Doc%20vs%20Code%20Audit%20Report%20-%202026-05-29.md) — Finding M31
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
---
|
|
||||||
issue: "029"
|
|
||||||
title: "Frontend delivery actions regenerate/attempts/stats call non-existent backend endpoints"
|
|
||||||
severity: major
|
|
||||||
domain: delivery
|
|
||||||
labels: [frontend, missing-feature]
|
|
||||||
status: open
|
|
||||||
created: 2026-05-29
|
|
||||||
source: Doc vs Code Audit 2026-05-29
|
|
||||||
---
|
|
||||||
|
|
||||||
# 🟠 Frontend delivery actions regenerate/attempts/stats call non-existent backend endpoints
|
|
||||||
|
|
||||||
**Severity:** major
|
|
||||||
**Domain:** delivery
|
|
||||||
**Labels:** frontend, missing-feature
|
|
||||||
|
|
||||||
## Description
|
|
||||||
|
|
||||||
Three frontend delivery actions hit non-existent backend routes:
|
|
||||||
|
|
||||||
| Action | Calls | Status |
|
|
||||||
|---|---|---|
|
|
||||||
| `regenerateDeliveryCode` | `POST /delivery-code/regenerate` | 404 (falls back to `/generate`) |
|
|
||||||
| `getDeliveryAttempts` | `GET /delivery-code/attempts` | 404, throws |
|
|
||||||
| `getDeliveryStats` | `GET /delivery/stats` | 404, throws |
|
|
||||||
|
|
||||||
`regenerateDeliveryCode` silently falls back to the generate endpoint on 404. The other two throw unhandled errors if any component calls them.
|
|
||||||
|
|
||||||
## Current Behavior
|
|
||||||
|
|
||||||
- Code "regeneration" actually calls generate (new code, ignores regenerate semantic)
|
|
||||||
- Any UI showing delivery attempt count or stats shows nothing or throws
|
|
||||||
|
|
||||||
## Expected Behavior
|
|
||||||
|
|
||||||
Either implement the backend routes, or remove the phantom actions and handle their use cases differently.
|
|
||||||
|
|
||||||
## Affected Files
|
|
||||||
|
|
||||||
- `frontend/src/actions/delivery.ts` — `regenerateDeliveryCode`, `getDeliveryAttempts`, `getDeliveryStats`
|
|
||||||
- Backend: missing routes for `/delivery-code/regenerate`, `/delivery-code/attempts`, `/delivery/stats`
|
|
||||||
|
|
||||||
## References
|
|
||||||
|
|
||||||
- [Doc vs Code Audit Report](../09%20-%20Audits/Doc%20vs%20Code%20Audit%20Report%20-%202026-05-29.md) — Finding M15
|
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
---
|
||||||
|
issue: 029
|
||||||
|
title: "No brute-force protection on delivery code verification endpoint — 900,000 combinations are enumerable"
|
||||||
|
severity: major
|
||||||
|
domain: Delivery
|
||||||
|
labels: [security, bug, backend, major, brute-force]
|
||||||
|
status: open
|
||||||
|
created: 2026-05-29
|
||||||
|
source: Doc vs Code Audit 2026-05-29
|
||||||
|
---
|
||||||
|
|
||||||
|
# 🟠 No brute-force protection on delivery code verification endpoint — 900,000 combinations are enumerable
|
||||||
|
|
||||||
|
**Severity:** major
|
||||||
|
**Domain:** Delivery
|
||||||
|
**Labels:** security, bug, backend, major, brute-force
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
The 6-digit delivery code verify endpoint (routes.ts lines 2790-2847) has no rate limiting, lockout counter, or attempt count maximum. Failed attempts are recorded to deliveryInfo.deliveryAttempts[] but no enforcement exists. A malicious actor could attempt all 900,000 combinations without being blocked.
|
||||||
|
|
||||||
|
## Current Behavior
|
||||||
|
|
||||||
|
Unlimited guesses are permitted. No rate limiting or lockout is applied to the verify endpoint.
|
||||||
|
|
||||||
|
## Expected Behavior
|
||||||
|
|
||||||
|
After N failed attempts (e.g., 5), the endpoint should return 429 or lock the code for a period. The deliveryAttempts[] array already tracks attempts — enforcement just needs to be added.
|
||||||
|
|
||||||
|
## Affected Files
|
||||||
|
|
||||||
|
- `backend/src/routes/routes.ts`
|
||||||
|
- `backend/src/services/deliveryService.ts`
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Doc vs Code Audit Report](../09%20-%20Audits/Doc%20vs%20Code%20Audit%20Report%20-%202026-05-29.md)
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
---
|
|
||||||
issue: "030"
|
|
||||||
title: "PATCH /confirm-delivery has no ownership check — any authenticated user can confirm delivery"
|
|
||||||
severity: major
|
|
||||||
domain: delivery
|
|
||||||
labels: [backend, security, bug]
|
|
||||||
status: open
|
|
||||||
created: 2026-05-29
|
|
||||||
source: Doc vs Code Audit 2026-05-29
|
|
||||||
---
|
|
||||||
|
|
||||||
# 🟠 PATCH /confirm-delivery has no ownership check — any authenticated user can confirm delivery
|
|
||||||
|
|
||||||
**Severity:** major
|
|
||||||
**Domain:** delivery
|
|
||||||
**Labels:** backend, security, bug
|
|
||||||
|
|
||||||
## Description
|
|
||||||
|
|
||||||
`PATCH /api/marketplace/purchase-requests/:id/confirm-delivery` (the buyer fast-track path to `'delivered'` status) has no ownership or role check. Any authenticated user who knows a purchase request ID can mark it as delivered without possessing the delivery code.
|
|
||||||
|
|
||||||
## Current Behavior
|
|
||||||
|
|
||||||
`PATCH /purchase-requests/{anyId}/confirm-delivery` with any valid JWT → 200, status set to `'delivered'`.
|
|
||||||
|
|
||||||
## Expected Behavior
|
|
||||||
|
|
||||||
Should verify `req.user.id === request.buyerId` — only the buyer of that specific request should be able to confirm delivery via this fast-track path.
|
|
||||||
|
|
||||||
## Affected Files
|
|
||||||
|
|
||||||
- `backend/src/routes/controllerRoutes.ts` or `routes.ts` — `confirm-delivery` handler missing ownership guard
|
|
||||||
|
|
||||||
## References
|
|
||||||
|
|
||||||
- [Doc vs Code Audit Report](../09%20-%20Audits/Doc%20vs%20Code%20Audit%20Report%20-%202026-05-29.md)
|
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
---
|
||||||
|
issue: 030
|
||||||
|
title: "POST /api/payment/payments/cleanup-pending admin check is inside handler only — no middleware-level enforcement"
|
||||||
|
severity: major
|
||||||
|
domain: Admin
|
||||||
|
labels: [security, bug, backend, major, missing-auth]
|
||||||
|
status: open
|
||||||
|
created: 2026-05-29
|
||||||
|
source: Doc vs Code Audit 2026-05-29
|
||||||
|
---
|
||||||
|
|
||||||
|
# 🟠 POST /api/payment/payments/cleanup-pending admin check is inside handler only — no middleware-level enforcement
|
||||||
|
|
||||||
|
**Severity:** major
|
||||||
|
**Domain:** Admin
|
||||||
|
**Labels:** security, bug, backend, major, missing-auth
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
POST /api/payment/payments/cleanup-pending registers only authenticateToken at the route level. Admin check is inside the handler. Any authenticated non-admin who discovers this endpoint can attempt to call it; the in-handler check is the only defense against unauthorized bulk deletion of pending payments.
|
||||||
|
|
||||||
|
## Current Behavior
|
||||||
|
|
||||||
|
Non-admin authenticated users can call the endpoint; admin gate fires inside handler code rather than at middleware level.
|
||||||
|
|
||||||
|
## Expected Behavior
|
||||||
|
|
||||||
|
Apply authorizeRoles('admin') middleware at the route level before the handler runs.
|
||||||
|
|
||||||
|
## Affected Files
|
||||||
|
|
||||||
|
- `backend/src/routes/paymentRoutes.ts`
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Doc vs Code Audit Report](../09%20-%20Audits/Doc%20vs%20Code%20Audit%20Report%20-%202026-05-29.md)
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
---
|
|
||||||
issue: "031"
|
|
||||||
title: "Points/referral system missing 5 frontend pages — redemption, levels, referrals, transactions, admin"
|
|
||||||
severity: major
|
|
||||||
domain: points
|
|
||||||
labels: [frontend, missing-feature]
|
|
||||||
status: open
|
|
||||||
created: 2026-05-29
|
|
||||||
source: Doc vs Code Audit 2026-05-29
|
|
||||||
---
|
|
||||||
|
|
||||||
# 🟠 Points/referral system missing 5 frontend pages — redemption, levels, referrals, transactions, admin
|
|
||||||
|
|
||||||
**Severity:** major
|
|
||||||
**Domain:** points
|
|
||||||
**Labels:** frontend, missing-feature
|
|
||||||
|
|
||||||
## Description
|
|
||||||
|
|
||||||
The following routes return 404 because no frontend pages exist:
|
|
||||||
|
|
||||||
| Route | Backend Endpoint | Status |
|
|
||||||
|---|---|---|
|
|
||||||
| `/dashboard/points/referrals` | `GET /api/points/referrals` | Page missing |
|
|
||||||
| `/dashboard/points/transactions` | `GET /api/points/transactions` | Page missing |
|
|
||||||
| `/dashboard/points/levels` | `GET /api/points/levels` | Page missing |
|
|
||||||
| `/dashboard/points/redeem` (or any UI) | `POST /api/points/redeem` | No redemption UI anywhere |
|
|
||||||
| Admin points management | `POST /api/points/admin/add` | No admin page |
|
|
||||||
|
|
||||||
`redeemPoints()` and `generateReferralCode()` actions are defined but have no call sites in any component.
|
|
||||||
|
|
||||||
## Current Behavior
|
|
||||||
|
|
||||||
All points features beyond the basic balance display are inaccessible from the UI.
|
|
||||||
|
|
||||||
## Expected Behavior
|
|
||||||
|
|
||||||
Implement frontend pages for: referral history, transaction history, levels display, points redemption flow, and admin points management.
|
|
||||||
|
|
||||||
## Affected Files
|
|
||||||
|
|
||||||
- Missing pages in `frontend/src/app/dashboard/points/`
|
|
||||||
- `frontend/src/actions/points.ts` — `redeemPoints`, `generateReferralCode` (defined, no callers)
|
|
||||||
|
|
||||||
## References
|
|
||||||
|
|
||||||
- [Doc vs Code Audit Report](../09%20-%20Audits/Doc%20vs%20Code%20Audit%20Report%20-%202026-05-29.md)
|
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
---
|
||||||
|
issue: 031
|
||||||
|
title: "POST /api/points/admin/add admin check is inside handler only — no middleware-level enforcement"
|
||||||
|
severity: major
|
||||||
|
domain: Admin
|
||||||
|
labels: [security, bug, backend, major, missing-auth]
|
||||||
|
status: open
|
||||||
|
created: 2026-05-29
|
||||||
|
source: Doc vs Code Audit 2026-05-29
|
||||||
|
---
|
||||||
|
|
||||||
|
# 🟠 POST /api/points/admin/add admin check is inside handler only — no middleware-level enforcement
|
||||||
|
|
||||||
|
**Severity:** major
|
||||||
|
**Domain:** Admin
|
||||||
|
**Labels:** security, bug, backend, major, missing-auth
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
POST /api/points/admin/add registers authenticateToken only at the route level. Admin role check runs inside the handler. This means the handler code runs before the role is verified, creating potential for edge-case bypass.
|
||||||
|
|
||||||
|
## Current Behavior
|
||||||
|
|
||||||
|
The handler code begins executing for any authenticated user before the role check fires.
|
||||||
|
|
||||||
|
## Expected Behavior
|
||||||
|
|
||||||
|
Apply authorizeRoles('admin') middleware at the route level.
|
||||||
|
|
||||||
|
## Affected Files
|
||||||
|
|
||||||
|
- `backend/src/routes/pointsRoutes.ts`
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Doc vs Code Audit Report](../09%20-%20Audits/Doc%20vs%20Code%20Audit%20Report%20-%202026-05-29.md)
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
---
|
||||||
|
issue: 032
|
||||||
|
title: "Admin delete user via legacy endpoint performs hard delete (findByIdAndDelete) instead of soft delete"
|
||||||
|
severity: major
|
||||||
|
domain: User Management
|
||||||
|
labels: [bug, frontend, backend, major, data-integrity]
|
||||||
|
status: open
|
||||||
|
created: 2026-05-29
|
||||||
|
source: Doc vs Code Audit 2026-05-29
|
||||||
|
---
|
||||||
|
|
||||||
|
# 🟠 Admin delete user via legacy endpoint performs hard delete (findByIdAndDelete) instead of soft delete
|
||||||
|
|
||||||
|
**Severity:** major
|
||||||
|
**Domain:** User Management
|
||||||
|
**Labels:** bug, frontend, backend, major, data-integrity
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
Frontend deleteUser function calls the legacy /users/admin/:id DELETE route which performs findByIdAndDelete (hard delete). The new controller at /api/user/admin/:userId performs a soft delete (status='deleted'). The frontend comment says 'soft delete' but calls the hard-delete route. User records and all associated data are permanently destroyed.
|
||||||
|
|
||||||
|
## Current Behavior
|
||||||
|
|
||||||
|
Admin 'delete user' action permanently destroys the user record from the database via findByIdAndDelete.
|
||||||
|
|
||||||
|
## Expected Behavior
|
||||||
|
|
||||||
|
Frontend should call the new controller endpoint /api/user/admin/:userId for soft delete, or the legacy route should be updated to perform a soft delete.
|
||||||
|
|
||||||
|
## Affected Files
|
||||||
|
|
||||||
|
- `frontend/src/actions/user.ts`
|
||||||
|
- `frontend/src/lib/axios.ts`
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Doc vs Code Audit Report](../09%20-%20Audits/Doc%20vs%20Code%20Audit%20Report%20-%202026-05-29.md)
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
---
|
|
||||||
issue: "032"
|
|
||||||
title: "SHKeeper release/refund doc paths include erroneous /shkeeper/ segment"
|
|
||||||
severity: major
|
|
||||||
domain: payment
|
|
||||||
labels: [backend, bug]
|
|
||||||
status: open
|
|
||||||
created: 2026-05-29
|
|
||||||
source: Doc vs Code Audit 2026-05-29
|
|
||||||
---
|
|
||||||
|
|
||||||
# 🟠 SHKeeper release/refund doc paths include erroneous /shkeeper/ segment
|
|
||||||
|
|
||||||
**Severity:** major
|
|
||||||
**Domain:** payment
|
|
||||||
**Labels:** backend, bug
|
|
||||||
|
|
||||||
## Description
|
|
||||||
|
|
||||||
The SHKeeper Payment Flow was documented with `/shkeeper/` in the release/refund paths. The actual backend routes are:
|
|
||||||
|
|
||||||
| Documented (wrong) | Actual (correct) |
|
|
||||||
|---|---|
|
|
||||||
| `POST /api/payment/shkeeper/:id/release` | `POST /api/payment/:id/release` |
|
|
||||||
| `POST /api/payment/shkeeper/:id/release/confirm` | `POST /api/payment/:id/release/confirm` |
|
|
||||||
| `POST /api/payment/shkeeper/:id/refund` | `POST /api/payment/:id/refund` |
|
|
||||||
| `POST /api/payment/shkeeper/:id/refund/confirm` | `POST /api/payment/:id/refund/confirm` |
|
|
||||||
|
|
||||||
The frontend `endpoints.payments.details` maps to `/payment/:id` (correct), so the frontend is unaffected. The issue is in the documentation and any external integration or test harness built from the docs.
|
|
||||||
|
|
||||||
## Current Behavior
|
|
||||||
|
|
||||||
Calling any `/shkeeper/` path returns 404.
|
|
||||||
|
|
||||||
## Expected Behavior
|
|
||||||
|
|
||||||
Documentation and any test harnesses should use paths without the `/shkeeper/` segment.
|
|
||||||
|
|
||||||
## Affected Files
|
|
||||||
|
|
||||||
- Doc file updated: `04 - Flows/Payment Flow - SHKeeper.md`
|
|
||||||
|
|
||||||
## References
|
|
||||||
|
|
||||||
- [Doc vs Code Audit Report](../09%20-%20Audits/Doc%20vs%20Code%20Audit%20Report%20-%202026-05-29.md) — Finding C30
|
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
---
|
||||||
|
issue: 033
|
||||||
|
title: "Admin can delete other admin accounts via new controller — legacy admin-on-admin protection does not apply"
|
||||||
|
severity: major
|
||||||
|
domain: User Management
|
||||||
|
labels: [security, bug, backend, major, privilege-escalation]
|
||||||
|
status: open
|
||||||
|
created: 2026-05-29
|
||||||
|
source: Doc vs Code Audit 2026-05-29
|
||||||
|
---
|
||||||
|
|
||||||
|
# 🟠 Admin can delete other admin accounts via new controller — legacy admin-on-admin protection does not apply
|
||||||
|
|
||||||
|
**Severity:** major
|
||||||
|
**Domain:** User Management
|
||||||
|
**Labels:** security, bug, backend, major, privilege-escalation
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
The new controller (DELETE /api/user/admin/:userId) only blocks self-deletion. It does not prevent an admin from deleting other admin accounts. The legacy route (DELETE /api/users/admin/:userId) blocks admin-on-admin deletion. The two routes have divergent authorization logic.
|
||||||
|
|
||||||
|
## Current Behavior
|
||||||
|
|
||||||
|
An admin can delete other admin accounts via the new controller endpoint without a 403 error.
|
||||||
|
|
||||||
|
## Expected Behavior
|
||||||
|
|
||||||
|
DELETE /api/user/admin/:userId should check if target user has role=admin and return 403 (matching legacy route behavior).
|
||||||
|
|
||||||
|
## Affected Files
|
||||||
|
|
||||||
|
- `backend/src/controllers/userController.ts`
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Doc vs Code Audit Report](../09%20-%20Audits/Doc%20vs%20Code%20Audit%20Report%20-%202026-05-29.md)
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
---
|
|
||||||
issue: "033"
|
|
||||||
title: "GET seller offer history has no HTTP route — getOffersBySeller() is unreachable dead code"
|
|
||||||
severity: major
|
|
||||||
domain: seller-offer
|
|
||||||
labels: [backend, missing-feature]
|
|
||||||
status: open
|
|
||||||
created: 2026-05-29
|
|
||||||
source: Doc vs Code Audit 2026-05-29
|
|
||||||
---
|
|
||||||
|
|
||||||
# 🟠 GET seller offer history has no HTTP route — getOffersBySeller() is unreachable dead code
|
|
||||||
|
|
||||||
**Severity:** major
|
|
||||||
**Domain:** seller-offer
|
|
||||||
**Labels:** backend, missing-feature
|
|
||||||
|
|
||||||
## Description
|
|
||||||
|
|
||||||
`SellerOfferService.getOffersBySeller()` exists in the service layer but no HTTP route exposes it. The documented endpoint `GET /api/marketplace/offers/seller/:sellerId` does not exist in `routes.ts` or `marketplaceController.ts`.
|
|
||||||
|
|
||||||
Notification action URLs that point to `/dashboard/seller/marketplace/offers` are also broken — that frontend page does not exist.
|
|
||||||
|
|
||||||
## Current Behavior
|
|
||||||
|
|
||||||
- Sellers have no way to view their own offer history via the API
|
|
||||||
- Notification deep-links to the offers page return 404
|
|
||||||
|
|
||||||
## Expected Behavior
|
|
||||||
|
|
||||||
1. Register `GET /api/marketplace/offers/seller/:sellerId` (or equivalent scoped route) calling `getOffersBySeller()`
|
|
||||||
2. Create the frontend page at `/dashboard/seller/marketplace/offers`
|
|
||||||
3. Fix notification `actionUrl` to point to the real page
|
|
||||||
|
|
||||||
## Affected Files
|
|
||||||
|
|
||||||
- `backend/src/routes/routes.ts` — missing `GET /offers/seller/:sellerId` route
|
|
||||||
- Missing: `frontend/src/app/dashboard/shops/` or similar seller offers list page
|
|
||||||
|
|
||||||
## References
|
|
||||||
|
|
||||||
- [Doc vs Code Audit Report](../09%20-%20Audits/Doc%20vs%20Code%20Audit%20Report%20-%202026-05-29.md) — Finding M27
|
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
---
|
||||||
|
issue: 034
|
||||||
|
title: "All dispute socket.io emit blocks are TODO stubs — no real-time updates fire for any dispute event"
|
||||||
|
severity: major
|
||||||
|
domain: Dispute
|
||||||
|
labels: [missing-feature, backend, major, socket, dispute]
|
||||||
|
status: open
|
||||||
|
created: 2026-05-29
|
||||||
|
source: Doc vs Code Audit 2026-05-29
|
||||||
|
---
|
||||||
|
|
||||||
|
# 🟠 All dispute socket.io emit blocks are TODO stubs — no real-time updates fire for any dispute event
|
||||||
|
|
||||||
|
**Severity:** major
|
||||||
|
**Domain:** Dispute
|
||||||
|
**Labels:** missing-feature, backend, major, socket, dispute
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
Every socket.io emit block in DisputeService is commented out as TODO. No real-time updates fire for dispute creation, admin assignment, status changes, evidence uploads, or resolution. The flow doc describes real-time presence as a working feature.
|
||||||
|
|
||||||
|
## Current Behavior
|
||||||
|
|
||||||
|
Zero socket events are emitted from DisputeService. All real-time dispute notifications are silent.
|
||||||
|
|
||||||
|
## Expected Behavior
|
||||||
|
|
||||||
|
Socket events should be emitted for dispute lifecycle events to keep all parties informed in real time.
|
||||||
|
|
||||||
|
## Affected Files
|
||||||
|
|
||||||
|
- `backend/src/services/disputeService.ts`
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Doc vs Code Audit Report](../09%20-%20Audits/Doc%20vs%20Code%20Audit%20Report%20-%202026-05-29.md)
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
---
|
|
||||||
issue: "034"
|
|
||||||
title: "SellerOffer 'active' status does not exist in schema — saves with this value throw ValidationError"
|
|
||||||
severity: major
|
|
||||||
domain: seller-offer
|
|
||||||
labels: [backend, bug]
|
|
||||||
status: open
|
|
||||||
created: 2026-05-29
|
|
||||||
source: Doc vs Code Audit 2026-05-29
|
|
||||||
---
|
|
||||||
|
|
||||||
# 🟠 SellerOffer 'active' status does not exist in schema — saves with this value throw ValidationError
|
|
||||||
|
|
||||||
**Severity:** major
|
|
||||||
**Domain:** seller-offer
|
|
||||||
**Labels:** backend, bug
|
|
||||||
|
|
||||||
## Description
|
|
||||||
|
|
||||||
The Seller Offer Flow doc lists `'active'` as a valid `SellerOffer.status`. The Mongoose schema and TypeScript interface only enumerate:
|
|
||||||
```
|
|
||||||
'pending' | 'accepted' | 'rejected' | 'withdrawn'
|
|
||||||
```
|
|
||||||
|
|
||||||
Any code path that attempts to set `SellerOffer.status = 'active'` will throw a Mongoose `ValidationError`. The `createOffer()` service correctly checks `PurchaseRequest.status === 'active'` (a different model's status), but `SellerOffer.status = 'active'` is never valid.
|
|
||||||
|
|
||||||
## Current Behavior
|
|
||||||
|
|
||||||
`SellerOffer.save()` with `status: 'active'` → Mongoose ValidationError. (Currently no code path actually tries to do this — the bug is latent but would be triggered by misreading the documentation.)
|
|
||||||
|
|
||||||
## Expected Behavior
|
|
||||||
|
|
||||||
Remove `'active'` from all `SellerOffer` status documentation. The valid states are `pending | accepted | rejected | withdrawn`.
|
|
||||||
|
|
||||||
## Affected Files
|
|
||||||
|
|
||||||
- Doc file updated: `04 - Flows/Seller Offer Flow.md` and `02 - Data Models/SellerOffer.md`
|
|
||||||
|
|
||||||
## References
|
|
||||||
|
|
||||||
- [Doc vs Code Audit Report](../09%20-%20Audits/Doc%20vs%20Code%20Audit%20Report%20-%202026-05-29.md) — Finding M22
|
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
---
|
||||||
|
issue: 035
|
||||||
|
title: "Frontend getPaymentStatus and confirmPayment call non-existent endpoints GET /payment/:id/status and POST /payment/:id/confirm"
|
||||||
|
severity: major
|
||||||
|
domain: Payment
|
||||||
|
labels: [bug, frontend, major, broken-feature, dispute]
|
||||||
|
status: open
|
||||||
|
created: 2026-05-29
|
||||||
|
source: Doc vs Code Audit 2026-05-29
|
||||||
|
---
|
||||||
|
|
||||||
|
# 🟠 Frontend getPaymentStatus and confirmPayment call non-existent endpoints GET /payment/:id/status and POST /payment/:id/confirm
|
||||||
|
|
||||||
|
**Severity:** major
|
||||||
|
**Domain:** Payment
|
||||||
|
**Labels:** bug, frontend, major, broken-feature, dispute
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
Frontend getPaymentStatus() builds URL as /payment/:id/status and confirmPayment() builds /payment/:id/confirm. Neither endpoint is registered in the backend. getPaymentStatus is actively called from dispute/payment-details-card.tsx line 101 — the 'Verify' button always returns 404.
|
||||||
|
|
||||||
|
## Current Behavior
|
||||||
|
|
||||||
|
The 'Verify' button in the dispute payment panel always returns 404. confirmPayment() is broken.
|
||||||
|
|
||||||
|
## Expected Behavior
|
||||||
|
|
||||||
|
Either implement /payment/:id/status and /payment/:id/confirm backend routes, or fix the frontend to use the correct existing payment detail endpoint.
|
||||||
|
|
||||||
|
## Affected Files
|
||||||
|
|
||||||
|
- `frontend/src/actions/payment.ts`
|
||||||
|
- `frontend/src/sections/dispute/components/payment-details-card.tsx`
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Doc vs Code Audit Report](../09%20-%20Audits/Doc%20vs%20Code%20Audit%20Report%20-%202026-05-29.md)
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
---
|
|
||||||
issue: "035"
|
|
||||||
title: "Dispute payment card 'Verify' button always 404s — getPaymentStatus calls non-existent endpoint"
|
|
||||||
severity: major
|
|
||||||
domain: payment
|
|
||||||
labels: [frontend, bug]
|
|
||||||
status: open
|
|
||||||
created: 2026-05-29
|
|
||||||
source: Doc vs Code Audit 2026-05-29
|
|
||||||
---
|
|
||||||
|
|
||||||
# 🟠 Dispute payment card 'Verify' button always 404s — getPaymentStatus calls non-existent endpoint
|
|
||||||
|
|
||||||
**Severity:** major
|
|
||||||
**Domain:** payment
|
|
||||||
**Labels:** frontend, bug
|
|
||||||
|
|
||||||
## Description
|
|
||||||
|
|
||||||
`frontend/src/sections/dispute/components/payment-details-card.tsx` (line ~101) calls `getPaymentStatus()` which builds URL as `GET /payment/:id/status`. No `/status` sub-route exists on any payment route in the backend.
|
|
||||||
|
|
||||||
The 'Verify' button in the dispute panel is permanently broken in production.
|
|
||||||
|
|
||||||
## Current Behavior
|
|
||||||
|
|
||||||
Clicking 'Verify' on the dispute payment card → `GET /payment/{id}/status` → 404.
|
|
||||||
|
|
||||||
## Expected Behavior
|
|
||||||
|
|
||||||
Either:
|
|
||||||
1. Implement `GET /api/payment/:id/status` on the backend, or
|
|
||||||
2. Update the component to use the existing `GET /api/payment/:id` endpoint for payment detail fetching
|
|
||||||
|
|
||||||
## Affected Files
|
|
||||||
|
|
||||||
- `frontend/src/sections/dispute/components/payment-details-card.tsx` — line ~101
|
|
||||||
- `frontend/src/actions/payment.ts` — `getPaymentStatus` function
|
|
||||||
|
|
||||||
## References
|
|
||||||
|
|
||||||
- [Doc vs Code Audit Report](../09%20-%20Audits/Doc%20vs%20Code%20Audit%20Report%20-%202026-05-29.md) — Finding C13
|
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
---
|
||||||
|
issue: 036
|
||||||
|
title: "cancelPayment action sends DELETE /payment/:id but no DELETE route exists on any payment endpoint"
|
||||||
|
severity: major
|
||||||
|
domain: Payment
|
||||||
|
labels: [bug, frontend, major, broken-feature]
|
||||||
|
status: open
|
||||||
|
created: 2026-05-29
|
||||||
|
source: Doc vs Code Audit 2026-05-29
|
||||||
|
---
|
||||||
|
|
||||||
|
# 🟠 cancelPayment action sends DELETE /payment/:id but no DELETE route exists on any payment endpoint
|
||||||
|
|
||||||
|
**Severity:** major
|
||||||
|
**Domain:** Payment
|
||||||
|
**Labels:** bug, frontend, major, broken-feature
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
cancelPayment() in src/actions/payment.ts sends DELETE /payment/:id. Backend has no DELETE method on any payment route. The web3 context version is a local state reset, but the action-layer version makes a real HTTP DELETE that will 404.
|
||||||
|
|
||||||
|
## Current Behavior
|
||||||
|
|
||||||
|
cancelPayment() via the action layer returns 404.
|
||||||
|
|
||||||
|
## Expected Behavior
|
||||||
|
|
||||||
|
Either implement a DELETE /payment/:id backend route for cancellation, or remove/replace the action-layer cancelPayment with correct API call.
|
||||||
|
|
||||||
|
## Affected Files
|
||||||
|
|
||||||
|
- `frontend/src/actions/payment.ts`
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Doc vs Code Audit Report](../09%20-%20Audits/Doc%20vs%20Code%20Audit%20Report%20-%202026-05-29.md)
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
---
|
||||||
|
issue: 037
|
||||||
|
title: "Frontend initiateRequestNetworkPayout, confirmRequestNetworkPayout, confirmRequestNetworkRelease, confirmRequestNetworkRefund call non-existent backend routes"
|
||||||
|
severity: major
|
||||||
|
domain: Payment
|
||||||
|
labels: [missing-feature, bug, frontend, major, payment, request-network]
|
||||||
|
status: open
|
||||||
|
created: 2026-05-29
|
||||||
|
source: Doc vs Code Audit 2026-05-29
|
||||||
|
---
|
||||||
|
|
||||||
|
# 🟠 Frontend initiateRequestNetworkPayout, confirmRequestNetworkPayout, confirmRequestNetworkRelease, confirmRequestNetworkRefund call non-existent backend routes
|
||||||
|
|
||||||
|
**Severity:** major
|
||||||
|
**Domain:** Payment
|
||||||
|
**Labels:** missing-feature, bug, frontend, major, payment, request-network
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
Four frontend actions in src/actions/payment.ts call /api/payment/request-network/:id/payout/initiate, /payout/confirm, /release/confirm, and /refund/confirm. None of these sub-paths exist in the backend. Admin Request Network payout/release/refund operations are completely broken.
|
||||||
|
|
||||||
|
## Current Behavior
|
||||||
|
|
||||||
|
All four RN admin payout/release/refund actions return 404.
|
||||||
|
|
||||||
|
## Expected Behavior
|
||||||
|
|
||||||
|
Backend should implement the four Request Network admin payout/release/refund endpoints, or the frontend actions should be updated to match existing backend routes.
|
||||||
|
|
||||||
|
## Affected Files
|
||||||
|
|
||||||
|
- `frontend/src/actions/payment.ts`
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Doc vs Code Audit Report](../09%20-%20Audits/Doc%20vs%20Code%20Audit%20Report%20-%202026-05-29.md)
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user