From 7a616744f4ecc15225d458e6a3eb7c645c601e59 Mon Sep 17 00:00:00 2001 From: Siavash Sameni Date: Fri, 29 May 2026 15:15:02 +0400 Subject: [PATCH] docs: complete code-reality alignment for remaining docs + reconcile issue set MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- 02 - Data Models/Chat.md | 21 ++- 02 - Data Models/Notification.md | 23 ++- 02 - Data Models/Payment.md | 24 ++- 02 - Data Models/PointTransaction.md | 16 +- 02 - Data Models/User.md | 18 +- 03 - API Reference/Admin API.md | 98 +++++++++-- 03 - API Reference/Chat API.md | 4 +- 03 - API Reference/Dispute API.md | 2 +- 03 - API Reference/Payment API.md | 10 ++ 03 - API Reference/Trezor API.md | 22 ++- 03 - API Reference/User API.md | 120 ++++++++++--- 04 - Flows/Authentication Flow.md | 19 ++- 04 - Flows/Chat Flow.md | 143 ++++++++++++---- 04 - Flows/Delivery Confirmation Flow.md | 36 ++-- 04 - Flows/Dispute Flow.md | 2 + 04 - Flows/Google OAuth Flow.md | 30 ++-- 04 - Flows/Negotiation Flow.md | 47 +++-- 04 - Flows/Notification Flow.md | 4 + 04 - Flows/Password Reset Flow.md | 12 +- 04 - Flows/Payment Flow - DePay & Web3.md | 24 +++ 04 - Flows/Payment Flow - SHKeeper.md | 42 +++-- 04 - Flows/Payout Flow.md | 76 ++++++++- 04 - Flows/Purchase Request Flow.md | 2 + 04 - Flows/Rating Flow.md | 5 + 04 - Flows/Referral Flow.md | 160 +++++++++++------- 04 - Flows/Registration Flow.md | 22 ++- 04 - Flows/Seller Offer Flow.md | 3 +- 04 - Flows/Trezor Safekeeping Flow.md | 53 +++--- .../ISSUE-001-dispute-status-no-role-guard.md | 50 ------ ...-status-and-post-api-disputes-id-resolv.md | 37 ++++ ...ISSUE-002-dispute-resolve-no-role-guard.md | 45 ----- ...assign-has-no-role-guard-any-user-can-s.md | 37 ++++ Issues/ISSUE-003-dispute-route-shadowing.md | 41 ----- ...api-disputes-purchaserequestid-resolve-.md | 41 +++++ Issues/ISSUE-004-payment-endpoints-no-auth.md | 46 ----- ...resolve-dashboard-does-not-trigger-escr.md | 37 ++++ ...ents-id-fetch-tx-post-api-payment-payme.md | 40 +++++ Issues/ISSUE-005-scanner-status-no-auth.md | 40 ----- ...ISSUE-006-delete-account-wrong-endpoint.md | 49 ------ ...-status-has-no-authentication-middlewar.md | 40 +++++ ...t-action-calls-delete-user-profile-whic.md | 37 ++++ Issues/ISSUE-007-sim-bypass-no-env-guard.md | 42 ----- ...SUE-008-chat-file-upload-wrong-endpoint.md | 41 ----- ...-to-wrong-endpoint-file-uploads-silentl.md | 37 ++++ Issues/ISSUE-009-archive-chat-wrong-method.md | 36 ---- ...ends-put-but-backend-only-accepts-patch.md | 36 ++++ ...admin-user-status-wrong-values-and-verb.md | 49 ------ ...userstatus-and-updateuserrole-use-put-b.md | 36 ++++ ...atus-sends-inactive-pending-status-valu.md | 37 ++++ ...11-update-purchase-request-put-vs-patch.md | 36 ---- ...ro-frontend-implementation-all-admin-re.md | 38 +++++ Issues/ISSUE-012-update-offer-put-vs-patch.md | 36 ---- ...intent-always-routes-to-request-network.md | 40 +++++ ...fer-no-status-filter-corrupts-withdrawn.md | 42 ----- ...cript-type-excludes-shkeeper-and-decent.md | 36 ++++ ...14-select-offer-no-seller-notifications.md | 43 ----- ...015-seller-offer-withdraw-no-http-route.md | 44 ----- ...-sim-bypass-has-no-environment-guard-ca.md | 40 +++++ ...provider-routing-always-request-network.md | 39 ----- ...-uses-put-but-backend-only-registers-pa.md | 36 ++++ ...17-payment-provider-type-missing-values.md | 46 ----- ...marketplace-offers-id-but-backend-regis.md | 36 ++++ ...ny-has-no-status-filter-overwrites-with.md | 40 +++++ ...E-018-trezor-no-frontend-implementation.md | 53 ------ ...n-payout-release-refund-not-implemented.md | 46 ----- ...tive-does-not-exist-in-schema-enum-but-.md | 36 ++++ .../ISSUE-020-dispute-assign-no-role-guard.md | 42 ----- ...-send-per-seller-socket-events-or-notif.md | 36 ++++ ...E-021-axios-interceptor-403-not-handled.md | 45 ----- ...offers-id-withdraw-http-route-does-not-.md | 37 ++++ ...nts-id-debug-has-no-authentication-full.md | 36 ++++ ...SSUE-022-rate-limit-counts-all-attempts.md | 38 ----- Issues/ISSUE-023-change-password-no-ui.md | 37 ---- ...t-has-no-admin-role-guard-at-route-leve.md | 36 ++++ ...-has-no-admin-role-guard-any-authentica.md | 36 ++++ ...-password-with-code-no-complexity-check.md | 41 ----- ...SUE-025-dispute-socket-events-all-stubs.md | 46 ----- ...istics-has-no-admin-role-guard-any-auth.md | 36 ++++ ...only-returns-user-s-most-recent-notific.md | 36 ++++ ...-payment-completed-not-counted-in-stats.md | 38 ----- ...oint-has-no-ownership-check-any-authent.md | 36 ++++ ...ISSUE-027-get-notification-by-id-broken.md | 38 ----- ...ed-socket-event-broadcasts-raw-6-digit-.md | 36 ++++ ...ISSUE-028-payment-export-no-admin-guard.md | 41 ----- ...livery-attempts-stats-phantom-endpoints.md | 46 ----- ...tion-on-delivery-code-verification-endp.md | 37 ++++ ...SSUE-030-confirm-delivery-no-auth-guard.md | 36 ---- ...ents-cleanup-pending-admin-check-is-ins.md | 36 ++++ ...ISSUE-031-points-missing-frontend-pages.md | 47 ----- ...-add-admin-check-is-inside-handler-only.md | 36 ++++ ...-legacy-endpoint-performs-hard-delete-f.md | 37 ++++ ...032-shkeeper-release-refund-wrong-paths.md | 45 ----- ...r-admin-accounts-via-new-controller-leg.md | 36 ++++ ...-033-seller-offer-history-route-missing.md | 42 ----- ...-emit-blocks-are-todo-stubs-no-real-tim.md | 36 ++++ ...-034-seller-offer-active-status-invalid.md | 41 ----- ...atus-and-confirmpayment-call-non-existe.md | 37 ++++ ...E-035-payment-dispute-verify-button-404.md | 41 ----- ...sends-delete-payment-id-but-no-delete-r.md | 36 ++++ ...estnetworkpayout-confirmrequestnetworkp.md | 36 ++++ ...ment-stub-actions-call-non-existent-bac.md | 37 ++++ ...ode-endpoint-has-no-password-complexity.md | 36 ++++ ...-has-no-ui-component-change-password-fe.md | 36 ++++ ...serequests-calls-marketplace-purchase-r.md | 37 ++++ ...cestats-calls-marketplace-purchase-requ.md | 37 ++++ ...ttempts-and-getdeliverystats-call-non-e.md | 36 ++++ ...purchase-requests-id-final-approval-cre.md | 36 ++++ ...end-sends-participants-string-array-but.md | 36 ++++ ...erhistory-seller-offer-history-page-doe.md | 37 ++++ ...ent-and-per-id-token-sweep-endpoints-fo.md | 36 ++++ ...kregistry-and-probechain-call-backend-e.md | 36 ++++ ...ionthresholdhistory-calls-get-api-admin.md | 36 ++++ ...frontend-pages-do-not-exist-redemption-.md | 36 ++++ ...ion-is-absent-users-can-refer-themselve.md | 36 ++++ ...ot-counted-in-successful-payments-stats.md | 36 ++++ ...y-handles-401-not-403-for-token-refresh.md | 36 ++++ ...r-counts-all-attempts-not-only-failures.md | 37 ++++ Issues/Issues Index.md | 109 ++++++------ 118 files changed, 2833 insertions(+), 1788 deletions(-) delete mode 100644 Issues/ISSUE-001-dispute-status-no-role-guard.md create mode 100644 Issues/ISSUE-001-patch-api-disputes-id-status-and-post-api-disputes-id-resolv.md delete mode 100644 Issues/ISSUE-002-dispute-resolve-no-role-guard.md create mode 100644 Issues/ISSUE-002-post-api-disputes-id-assign-has-no-role-guard-any-user-can-s.md delete mode 100644 Issues/ISSUE-003-dispute-route-shadowing.md create mode 100644 Issues/ISSUE-003-route-shadowing-post-api-disputes-purchaserequestid-resolve-.md delete mode 100644 Issues/ISSUE-004-payment-endpoints-no-auth.md create mode 100644 Issues/ISSUE-004-post-api-disputes-id-resolve-dashboard-does-not-trigger-escr.md create mode 100644 Issues/ISSUE-005-post-api-payment-payments-id-fetch-tx-post-api-payment-payme.md delete mode 100644 Issues/ISSUE-005-scanner-status-no-auth.md delete mode 100644 Issues/ISSUE-006-delete-account-wrong-endpoint.md create mode 100644 Issues/ISSUE-006-get-api-admin-scanner-status-has-no-authentication-middlewar.md create mode 100644 Issues/ISSUE-007-frontend-deleteaccount-action-calls-delete-user-profile-whic.md delete mode 100644 Issues/ISSUE-007-sim-bypass-no-env-guard.md delete mode 100644 Issues/ISSUE-008-chat-file-upload-wrong-endpoint.md create mode 100644 Issues/ISSUE-008-sendfilemessage-posts-to-wrong-endpoint-file-uploads-silentl.md delete mode 100644 Issues/ISSUE-009-archive-chat-wrong-method.md create mode 100644 Issues/ISSUE-009-archiveconversation-sends-put-but-backend-only-accepts-patch.md delete mode 100644 Issues/ISSUE-010-admin-user-status-wrong-values-and-verb.md create mode 100644 Issues/ISSUE-010-frontend-admin-updateuserstatus-and-updateuserrole-use-put-b.md create mode 100644 Issues/ISSUE-011-frontend-updateuserstatus-sends-inactive-pending-status-valu.md delete mode 100644 Issues/ISSUE-011-update-purchase-request-put-vs-patch.md create mode 100644 Issues/ISSUE-012-trezor-safekeeping-zero-frontend-implementation-all-admin-re.md delete mode 100644 Issues/ISSUE-012-update-offer-put-vs-patch.md create mode 100644 Issues/ISSUE-013-createproviderpaymentintent-always-routes-to-request-network.md delete mode 100644 Issues/ISSUE-013-select-offer-no-status-filter-corrupts-withdrawn.md create mode 100644 Issues/ISSUE-014-paymentprovider-typescript-type-excludes-shkeeper-and-decent.md delete mode 100644 Issues/ISSUE-014-select-offer-no-seller-notifications.md delete mode 100644 Issues/ISSUE-015-seller-offer-withdraw-no-http-route.md create mode 100644 Issues/ISSUE-015-simulated-transaction-sim-bypass-has-no-environment-guard-ca.md delete mode 100644 Issues/ISSUE-016-payment-provider-routing-always-request-network.md create mode 100644 Issues/ISSUE-016-updatepurchaserequest-uses-put-but-backend-only-registers-pa.md delete mode 100644 Issues/ISSUE-017-payment-provider-type-missing-values.md create mode 100644 Issues/ISSUE-017-updateoffer-uses-put-marketplace-offers-id-but-backend-regis.md create mode 100644 Issues/ISSUE-018-select-offer-updatemany-has-no-status-filter-overwrites-with.md delete mode 100644 Issues/ISSUE-018-trezor-no-frontend-implementation.md delete mode 100644 Issues/ISSUE-019-rn-payout-release-refund-not-implemented.md create mode 100644 Issues/ISSUE-019-selleroffer-status-active-does-not-exist-in-schema-enum-but-.md delete mode 100644 Issues/ISSUE-020-dispute-assign-no-role-guard.md create mode 100644 Issues/ISSUE-020-select-offer-does-not-send-per-seller-socket-events-or-notif.md delete mode 100644 Issues/ISSUE-021-axios-interceptor-403-not-handled.md create mode 100644 Issues/ISSUE-021-post-api-marketplace-offers-id-withdraw-http-route-does-not-.md create mode 100644 Issues/ISSUE-022-get-api-payment-payments-id-debug-has-no-authentication-full.md delete mode 100644 Issues/ISSUE-022-rate-limit-counts-all-attempts.md delete mode 100644 Issues/ISSUE-023-change-password-no-ui.md create mode 100644 Issues/ISSUE-023-get-api-payment-export-has-no-admin-role-guard-at-route-leve.md create mode 100644 Issues/ISSUE-024-get-api-payment-stats-has-no-admin-role-guard-any-authentica.md delete mode 100644 Issues/ISSUE-024-reset-password-with-code-no-complexity-check.md delete mode 100644 Issues/ISSUE-025-dispute-socket-events-all-stubs.md create mode 100644 Issues/ISSUE-025-get-api-disputes-statistics-has-no-admin-role-guard-any-auth.md create mode 100644 Issues/ISSUE-026-get-notifications-id-only-returns-user-s-most-recent-notific.md delete mode 100644 Issues/ISSUE-026-payment-completed-not-counted-in-stats.md create mode 100644 Issues/ISSUE-027-confirm-delivery-endpoint-has-no-ownership-check-any-authent.md delete mode 100644 Issues/ISSUE-027-get-notification-by-id-broken.md create mode 100644 Issues/ISSUE-028-delivery-code-generated-socket-event-broadcasts-raw-6-digit-.md delete mode 100644 Issues/ISSUE-028-payment-export-no-admin-guard.md delete mode 100644 Issues/ISSUE-029-delivery-attempts-stats-phantom-endpoints.md create mode 100644 Issues/ISSUE-029-no-brute-force-protection-on-delivery-code-verification-endp.md delete mode 100644 Issues/ISSUE-030-confirm-delivery-no-auth-guard.md create mode 100644 Issues/ISSUE-030-post-api-payment-payments-cleanup-pending-admin-check-is-ins.md delete mode 100644 Issues/ISSUE-031-points-missing-frontend-pages.md create mode 100644 Issues/ISSUE-031-post-api-points-admin-add-admin-check-is-inside-handler-only.md create mode 100644 Issues/ISSUE-032-admin-delete-user-via-legacy-endpoint-performs-hard-delete-f.md delete mode 100644 Issues/ISSUE-032-shkeeper-release-refund-wrong-paths.md create mode 100644 Issues/ISSUE-033-admin-can-delete-other-admin-accounts-via-new-controller-leg.md delete mode 100644 Issues/ISSUE-033-seller-offer-history-route-missing.md create mode 100644 Issues/ISSUE-034-all-dispute-socket-io-emit-blocks-are-todo-stubs-no-real-tim.md delete mode 100644 Issues/ISSUE-034-seller-offer-active-status-invalid.md create mode 100644 Issues/ISSUE-035-frontend-getpaymentstatus-and-confirmpayment-call-non-existe.md delete mode 100644 Issues/ISSUE-035-payment-dispute-verify-button-404.md create mode 100644 Issues/ISSUE-036-cancelpayment-action-sends-delete-payment-id-but-no-delete-r.md create mode 100644 Issues/ISSUE-037-frontend-initiaterequestnetworkpayout-confirmrequestnetworkp.md create mode 100644 Issues/ISSUE-038-multiple-frontend-payment-stub-actions-call-non-existent-bac.md create mode 100644 Issues/ISSUE-039-reset-password-with-code-endpoint-has-no-password-complexity.md create mode 100644 Issues/ISSUE-040-changepassword-action-has-no-ui-component-change-password-fe.md create mode 100644 Issues/ISSUE-041-frontend-searchpurchaserequests-calls-marketplace-purchase-r.md create mode 100644 Issues/ISSUE-042-frontend-getmarketplacestats-calls-marketplace-purchase-requ.md create mode 100644 Issues/ISSUE-043-frontend-getdeliveryattempts-and-getdeliverystats-call-non-e.md create mode 100644 Issues/ISSUE-044-post-api-marketplace-purchase-requests-id-final-approval-cre.md create mode 100644 Issues/ISSUE-045-addparticipants-frontend-sends-participants-string-array-but.md create mode 100644 Issues/ISSUE-046-frontend-getsellerofferhistory-seller-offer-history-page-doe.md create mode 100644 Issues/ISSUE-047-frontend-cron-management-and-per-id-token-sweep-endpoints-fo.md create mode 100644 Issues/ISSUE-048-frontend-reloadnetworkregistry-and-probechain-call-backend-e.md create mode 100644 Issues/ISSUE-049-frontend-getconfirmationthresholdhistory-calls-get-api-admin.md create mode 100644 Issues/ISSUE-050-points-referral-five-frontend-pages-do-not-exist-redemption-.md create mode 100644 Issues/ISSUE-051-self-referral-prevention-is-absent-users-can-refer-themselve.md create mode 100644 Issues/ISSUE-052-payment-completed-status-not-counted-in-successful-payments-stats.md create mode 100644 Issues/ISSUE-053-axios-interceptor-only-handles-401-not-403-for-token-refresh.md create mode 100644 Issues/ISSUE-054-login-rate-limiter-counts-all-attempts-not-only-failures.md diff --git a/02 - Data Models/Chat.md b/02 - Data Models/Chat.md index fb84783..9006b48 100644 --- a/02 - Data Models/Chat.md +++ b/02 - Data Models/Chat.md @@ -5,6 +5,7 @@ aliases: [Conversation, IChat, IMessage] --- # 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`). @@ -16,6 +17,9 @@ Conversation container with embedded messages. Used for buyer-seller direct chat > [!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. +> [!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 | 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[].joinedAt` | Date | no | `Date.now` | — | — | Join time. | | `participants[].lastSeen` | Date | no | — | — | — | Last activity. | -| `participants[].leftAt` | Date | no | — | — | — | If left, when. | -| `participants[].isActive` | Boolean | no | `true` | — | — | Still a participant. | +| `participants[].leftAt` | Date | no | — | — | — | Set when the participant is removed (soft removal). | +| `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. | -| `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. | | `lastMessage.content` | String | no | — | — | — | Snapshot for list views. | | `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` > 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) | Field | Type | Required | Default | Validation | Description | | --- | --- | --- | --- | --- | --- | | `senderId` | ObjectId → [[User]] | yes | — | — | Author. | | `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. | | `fileUrl` | String | no | — | — | If file/image. | | `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. | | `isEdited` | Boolean | no | `false` | — | Edited flag. | | `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. | | `reactions[].userId` | ObjectId → [[User]] | no | — | — | Reacting user. | | `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 | 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` | | `addMessage(messageData: Partial): 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 diff --git a/02 - Data Models/Notification.md b/02 - Data Models/Notification.md index 6c2fa42..71535bf 100644 --- a/02 - Data Models/Notification.md +++ b/02 - Data Models/Notification.md @@ -6,7 +6,9 @@ aliases: [User Notification, INotification] # 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 > `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 > `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 | 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. | | `message` | String | yes | — | maxlength 1000 | — | Body. | | `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]]). | | `metadata` | Mixed | no | — | — | — | Arbitrary payload. | | `actionUrl` | String | no | — | maxlength 500 | — | Deep link. | | `isRead` | Boolean | no | `false` | — | yes (compound) | Read flag. | | `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. | 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, category: 1 }` — category filter. - `{ 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. @@ -62,6 +70,13 @@ 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 - **References**: [[User]] indirectly through `userId` (string); arbitrary entity via `relatedId`. diff --git a/02 - Data Models/Payment.md b/02 - Data Models/Payment.md index 1570c1e..9be8055 100644 --- a/02 - Data Models/Payment.md +++ b/02 - Data Models/Payment.md @@ -6,6 +6,8 @@ aliases: [Payment Record, Escrow, IPayment] # 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. > [!note] Source @@ -15,6 +17,22 @@ Records every monetary movement in the marketplace: buyer pay-ins, seller payout > [!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. +> [!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 | 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). | | `amount.amount` | Number | yes | — | — | — | Numeric amount. | | `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. | | `blockchain.network` | String | no | — | — | — | Network identifier. | | `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.confirmedAt` | Date | no | — | — | — | When tx confirmed. | | `blockchain.confirmations` | Number | no | `0` | — | — | Confirmation count. | -| `status` | String | no | `pending` | enum: `pending` / `processing` / `confirmed` / `completed` / `failed` / `cancelled` / `refunded` | yes (compound) | Lifecycle status. | -| `escrowState` | String | no | — | enum: `funded` / `releasable` / `released` / `refunded` / `releasing` / `failed` / `cancelled` / `partial` | — | Escrow lifecycle. | +| `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. 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. | | `metadata.userAgent` | String | no | — | — | — | Browser UA. | | `metadata.ipAddress` | String | no | — | — | — | Client IP. | diff --git a/02 - Data Models/PointTransaction.md b/02 - Data Models/PointTransaction.md index 9b6b9ca..6d30a83 100644 --- a/02 - Data Models/PointTransaction.md +++ b/02 - Data Models/PointTransaction.md @@ -4,9 +4,19 @@ tags: [data-model, mongoose] aliases: [Point Ledger, Loyalty Transaction, IPointTransaction] --- +> **Last updated:** 2026-05-29 — aligned with code (see Doc vs Code Audit Report) + # 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 > `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. | | `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`). | | `balance` | Number | yes | — | — | — | Available balance after the move. | | `order` | ObjectId → Order | no | — | — | — | Linked order id (legacy ref, see warning). | @@ -67,7 +77,7 @@ None defined. ## 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 diff --git a/02 - Data Models/User.md b/02 - Data Models/User.md index 660b0e5..4e80c0c 100644 --- a/02 - Data Models/User.md +++ b/02 - Data Models/User.md @@ -6,12 +6,20 @@ aliases: [User Model, IUser, Account] # 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. > [!note] Source > `backend/src/models/User.ts:70` — schema definition > `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 | 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"). | | `lastName` | String | no | `"جدید"` | trim | — | Persian default ("new"). | | `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`. | | `telegramVerified` | Boolean | no | `false` | — | — | Set when Telegram identity has been signature-verified and linked through `TelegramLink`. | | `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.bio` | String | no | — | — | — | Free-form bio. | | `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. | | `preferences.language` | String | no | `"en"` | — | — | UI language. | | `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. | | `status` | String | no | `"active"` | enum: `active` / `suspended` / `deleted` | yes | Soft-delete and moderation flag. | | `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. | | `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. | diff --git a/03 - API Reference/Admin API.md b/03 - API Reference/Admin API.md index f75207a..906994f 100644 --- a/03 - API Reference/Admin API.md +++ b/03 - API Reference/Admin API.md @@ -5,9 +5,9 @@ tags: [api, admin, reference] # 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). - 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]]. +> **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 | | --- | --- | -| `POST /api/user/admin/create` | Create user with role/status | -| `DELETE /api/user/admin/:userId` | Soft delete user — sets `status='deleted'` (admins cannot delete each other) | -| `PATCH /api/user/admin/:userId/status` | Activate / suspend | -| `PATCH /api/user/admin/:userId/toggle-status` | Flip active flag | -| `PATCH /api/user/admin/:userId/role` | Change role | -| `GET /api/user/admin/list` | Paginated directory + stats | -| `GET /api/user/admin/:userId/dependencies` | Pre-delete dependency check | +| `POST /api/users/admin/create` | Create user with role/status | +| `DELETE /api/users/admin/:userId` | Soft delete user — sets `status='deleted'` (admins cannot delete each other) | +| `PATCH /api/users/admin/:userId/status` | Activate / suspend | +| `PATCH /api/users/admin/:userId/toggle-status` | Flip active flag | +| `PATCH /api/users/admin/:userId/role` | Change role | +| `GET /api/users/admin/list` | Paginated directory + stats | +| `GET /api/users/admin/:userId/dependencies` | Pre-delete dependency check | | `GET /api/users/admin/stats` | Aggregate user analytics | | `GET /api/users/admin/:userId` | Full user detail (admin view) | | `PUT /api/users/admin/:userId` | Mass update user | | `PUT /api/users/admin/update/:email` | Mass update by email | | `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 — 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 @@ -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/…`.) +## 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) See [[Points API]]. @@ -140,12 +160,58 @@ Router: [`backend/src/services/admin/dataCleanupRoutes.ts`](../../backend/src/se ### GET /api/admin/scanner/status **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: -> - `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. -> - `POST /api/admin/rn/networks/probe/:chainId` — no per-chain probe endpoint exists. +> **⚠️ 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. + +## Settings + +### 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 diff --git a/03 - API Reference/Chat API.md b/03 - API Reference/Chat API.md index 03922c4..2650ff2 100644 --- a/03 - API Reference/Chat API.md +++ b/03 - API Reference/Chat API.md @@ -69,13 +69,11 @@ Model: [[Chat]]. Real-time delivery happens over Socket.IO rooms named `chat- ⚠️ **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 **Description:** Add a participant to a group chat. diff --git a/03 - API Reference/Dispute API.md b/03 - API Reference/Dispute API.md index 00f6b41..af4f861 100644 --- a/03 - API Reference/Dispute API.md +++ b/03 - API Reference/Dispute API.md @@ -79,7 +79,7 @@ Model: [[Dispute]]. A dispute references a [[PurchaseRequest]] plus optional [[P ### GET /api/disputes/statistics **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, ... } }` ### GET /api/disputes/:id diff --git a/03 - API Reference/Payment API.md b/03 - API Reference/Payment API.md index 01392e5..245ff28 100644 --- a/03 - API Reference/Payment API.md +++ b/03 - API Reference/Payment API.md @@ -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 [[Payment]] uses the statuses below across all providers: diff --git a/03 - API Reference/Trezor API.md b/03 - API Reference/Trezor API.md index 126d4e0..cc3ae60 100644 --- a/03 - API Reference/Trezor API.md +++ b/03 - API Reference/Trezor API.md @@ -3,6 +3,8 @@ title: Trezor API tags: [api, payments, trezor, safekeeping] --- +> **Last updated:** 2026-05-29 — aligned with code (see Doc vs Code Audit Report) + # 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. @@ -80,10 +82,26 @@ Response: ## 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 +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: ```json @@ -148,7 +166,7 @@ Response: ## 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 diff --git a/03 - API Reference/User API.md b/03 - API Reference/User API.md index 2b25337..988bc0e 100644 --- a/03 - API Reference/User API.md +++ b/03 - API Reference/User API.md @@ -5,6 +5,8 @@ tags: [api, user, reference] # 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: - `/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 -**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 -**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 -**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 **Request body:** ```ts { - walletAddress: string; // 0x-prefixed 40-hex - signature: string; // signed `message` - message: string; // human-readable challenge text + walletAddress: string; // EVM 0x-address, or TON address + walletType?: "evm" | "ton"; // defaults to "evm" + 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:** -- `400` missing fields, malformed address, signature mismatch +- `400` missing/invalid fields, malformed address, EVM signature mismatch, invalid TON proof - `404` user not found 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 ### GET /api/users/contacts @@ -122,7 +173,17 @@ The legacy alias `PATCH /api/users/wallet-address` performs the same logic. ## 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 @@ -144,31 +205,49 @@ These are duplicated across the two routers. The newer controller variants live **Response 201:** `{ success, data: { user } }` **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) **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) -**Request body:** `{ isActive: boolean; reason?: string }` -**Response 200:** `{ success, data: { user: { _id, isActive, statusUpdatedAt } } }` +**Errors:** `403` admin-on-admin, `404` not found. + +### 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 **Description:** Flip active/suspended without explicit body. **Auth required:** Bearer JWT (admin) -### PATCH /api/user/admin/:userId/role +### PATCH /api/users/admin/:userId/role **Description:** Change a user's role. **Auth required:** Bearer JWT (admin) **Request body:** `{ role: "buyer" | "seller" | "admin"; reason?: string }` **Errors:** `400` invalid role. +**Frontend discrepancy:** Frontend calls this with `PUT` verb; backend only accepts `PATCH`. ### 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 -**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) +**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 @@ -210,10 +290,12 @@ These are duplicated across the two routers. The newer controller variants live ### 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) **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 Source: [`backend/src/services/address/addressRoutes.ts`](../../backend/src/services/address/addressRoutes.ts), model: [[Address]]. diff --git a/04 - Flows/Authentication Flow.md b/04 - Flows/Authentication Flow.md index b4178f8..64259bd 100644 --- a/04 - Flows/Authentication Flow.md +++ b/04 - Flows/Authentication Flow.md @@ -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"] --- +> **Last updated:** 2026-05-29 — aligned with code (see Doc vs Code Audit Report) + > [!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. @@ -52,7 +54,7 @@ End-to-end specification for **email + password** authentication, JWT issuance, > [!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**. -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`. ## Sequence diagram @@ -102,6 +104,7 @@ sequenceDiagram | `POST` | `/api/auth/refresh-token` | `authRoutes.ts:24-27` → `authController.refreshToken` | | `POST` | `/api/auth/logout` | `authRoutes.ts:68` → `authController.logout` (protected) | | `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 @@ -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. +## 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 - **`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. > [!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 > 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 -> [!bug] Account deletion is currently broken -> 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. +> [!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` (`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 | 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 | | 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 | ## Linked flows diff --git a/04 - Flows/Chat Flow.md b/04 - Flows/Chat Flow.md index da14b99..9456a0c 100644 --- a/04 - Flows/Chat Flow.md +++ b/04 - Flows/Chat Flow.md @@ -2,10 +2,11 @@ title: Chat Flow tags: [flow, chat, socket-io, messaging] related_models: ["[[Chat]]", "[[Message]]", "[[User]]"] -related_apis: ["POST /api/chat", "POST /api/chat/:chatId/messages", "GET /api/chat/:chatId/messages", "POST /api/chat/:chatId/read"] +related_apis: ["POST /api/chat", "POST /api/chat/:id/messages", "GET /api/chat/:id/messages", "PATCH /api/chat/:id/messages/read"] --- # Chat Flow +> **Last updated:** 2026-05-29 — aligned with code (see [Doc vs Code Audit Report](../09%20-%20Audits/Doc%20vs%20Code%20Audit%20Report%20-%202026-05-29.md)) Real-time messaging between buyer & seller (direct), three-way dispute mediation (group), and user-to-support (support). Backed by `Chat` documents with embedded `messages[]` and a Socket.IO room per chat for live updates. @@ -18,7 +19,7 @@ Real-time messaging between buyer & seller (direct), three-way dispute mediation - **Frontend** — `frontend/src/sections/chat/` (chat list, conversation view, message composer). - **Backend** — `ChatService` (`backend/src/services/chat/ChatService.ts`), routes under `/api/chat`. - **MongoDB** — `chats` collection with embedded `messages`, `participants`, `unreadCounts`, `settings`, `metadata`. -- **Socket.IO** — events `new-message`, `chat-notification`, `messages-read`, `user-typing`, `user-status-change`. +- **Socket.IO** — events `new-message`, `chat-notification`, `messages-read`, `user-typing`, `user-status-change`, `message-deleted`. ## Preconditions @@ -32,8 +33,8 @@ stateDiagram-v2 [*] --> Created: ChatService.createChat\n(or auto on first contact) Created --> Active: messages flowing Active --> Active: send / read / typing - Active --> Archived: settings.isArchived=true - Archived --> Active: unarchive + Active --> Archived: PATCH /api/chat/:id/archive (toggle) + Archived --> Active: PATCH /api/chat/:id/archive (same endpoint toggles back) Active --> [*]: chat deleted (rare) ``` @@ -41,25 +42,33 @@ stateDiagram-v2 ### Creation -1. **Direct chat (find-or-create)** — when buyer clicks "Chat with seller" on a request detail, frontend POSTs `POST /api/chat` with `{ type: 'direct', participantIds: [buyer, seller], relatedTo: { type: 'PurchaseRequest', id } }`. +1. **Direct chat (find-or-create)** — when buyer clicks "Chat with seller" on a request detail, frontend POSTs `POST /api/chat` with `{ type: 'direct', participantIds: [sellerId] }`. The endpoint requires **exactly 1 external `participantId`**; the authenticated caller is auto-appended to make 2. + + > [!warning] `relatedTo` is NOT accepted on `POST /api/chat` + > Despite the schema carrying a `relatedTo` discriminator, the create endpoint ignores/does not accept a `relatedTo` payload. Purchase-request linkage is performed server-side via the dedicated `POST /api/chat/purchase-request` (see step 5), not by passing `relatedTo` to `POST /api/chat`. + 2. `ChatService.createChat` (`ChatService.ts:90-192`): - For `direct` with exactly 2 participants, runs `Chat.findOne({ type: 'direct', 'participants.userId': { $all: [...] }, 'participants.isActive': true })` and returns the existing chat if found. - Otherwise creates a new `Chat` with `participants` (each with `role:'member'`, `joinedAt`, `isActive:true`), zeroed `unreadCounts`, default `settings`, `metadata.createdBy`. - Appends a system welcome message (`messageType: 'system'`). - - If `relatedTo.type === 'PurchaseRequest'`, also writes `"چت برای درخواست خرید \"{title}\" ایجاد شد"` system line. - Re-loads with `populate('participants.userId', 'firstName lastName profile.avatar email')` for the response. 3. **Group chat (dispute)** — same pattern, but `type: 'group'`, all three participants (buyer, seller, admin) added (admin is added later by `DisputeService.assignAdmin`). 4. **Support chat** — `ChatService.createSupportChat(userId)` (`:41-88`) auto-discovers `User.findOne({ email: 'support@amn.gg' })` and creates a `type: 'support'` chat with a welcome message. Idempotent. -5. **Post-payment auto-chat** — when payment is confirmed, the payment-state cascade ensures a direct chat exists between buyer and winning seller. +5. **Post-payment / purchase-request auto-chat** — `POST /api/chat/purchase-request` exists on the backend and creates/links a direct chat for a purchase request. When payment is confirmed, the payment-state cascade ensures a direct chat exists between buyer and winning seller. **No frontend action is wired to `POST /api/chat/purchase-request`** — this direct chat is created server-side. ### Joining the room (real-time) 6. On chat page mount, the frontend emits `socket.emit('join-chat-room', chatId)` (`backend/src/app.ts:130-133`). The socket joins room `chat-{chatId}`. -7. Optionally `socket.emit('user-online', userId)` so other clients see green status (`app.ts:161-169`). +7. **`join-user-room` and `user-online` are SEPARATE events** (do not conflate them): + - `socket.emit('join-user-room', userId)` makes the socket join the personal `user-{userId}` room (so it can receive `chat-notification`). + - `socket.emit('user-online', userId)` broadcasts a `user-status-change` (online) to other clients. + + > [!warning] No offline broadcast on disconnect — stale "online" status + > On socket disconnect, **no offline `user-status-change` is emitted**. Other users keep seeing a stale "online" indicator for a peer who has actually left. Document this as a known gap. ### Sending a message -8. User types and hits send. Frontend POSTs `POST /api/chat/:chatId/messages` with `{ content, messageType?, fileUrl?, fileName?, fileSize?, replyTo? }`. +8. User types and hits send. Frontend POSTs `POST /api/chat/:id/messages` with `{ content, messageType?, fileUrl?, fileName?, fileSize?, replyTo? }`. Backend enforces a **5000-character maximum** on `content` at both Mongoose schema and controller validation levels. 9. `ChatService.sendMessage` (`:195-260`): - Loads chat, verifies the sender is in `participants[]` and `isActive`. - Builds `Message`: `{ senderId, content, messageType: 'text', fileUrl?, fileName?, fileSize?, replyTo?, timestamp, isRead: false, isEdited: false }`. @@ -71,20 +80,64 @@ stateDiagram-v2 ### Attachments -11. To attach a file, the user picks a file → frontend calls `chatService.uploadChatFile(chatId, file)` (or the equivalent `POST /api/chat/:chatId/upload`) — backend persists the upload via `fileService` (returns `{ fileUrl, fileName, fileSize }`). -12. The send-message call then includes `messageType: 'image' | 'file'` and the file metadata. Files are served from `/uploads`. +11. **File upload endpoint:** the real endpoint is **`POST /api/chat/:id/messages/file`** (multipart/form-data). The flow previously referenced `POST /api/chat/:chatId/upload`, which **does NOT exist**. + + > [!bug] ⚠️ KNOWN BUG — file uploads broken + > The frontend `chatService.sendFileMessage` currently POSTs to the **text** message endpoint (`POST /api/chat/:id/messages`) instead of `POST /api/chat/:id/messages/file`. As a result file uploads are broken — they hit the wrong endpoint. + +12. When working correctly, the backend handles the multipart payload at `POST /api/chat/:id/messages/file`, persists the upload via `fileService` (returns `{ fileUrl, fileName, fileSize }`), and records the message with `messageType: 'image' | 'file'`. + + > [!warning] ⚠️ Security concern — anonymous file access + > Uploaded files are stored under `uploads/chat/` and served with **anonymous access**. Sensitive attachments (KYC docs, dispute evidence) are fetchable by any user who has the URL. Consider signed URLs or per-user authorisation. + +### Editing a message + +13. Editing a message uses a body of `{ content }` (max 5000 chars). Edits are only allowed within a **15-minute edit window** — edits attempted after that return **400**. + + > [!bug] ⚠️ KNOWN BUG — edits fail / are ignored + > The frontend `editMessage` action sends `{ text }`, but the backend expects `{ content }`. The mismatched field name means edits fail or are silently ignored. + +### Deleting a message (soft-delete) + +14. Message DELETE **soft-deletes**: it sets `deletedAt`, clears the message `content`, and emits **`message-deleted`** to `chat-{chatId}`. The subdocument is not physically removed. ### Read receipts -13. When the user opens a chat, frontend POSTs `POST /api/chat/:chatId/read` (optionally with `messageIds: string[]`). -14. `ChatService.markMessagesAsRead` (`:438-483`): +15. When the user opens a chat, frontend marks messages read via **`PATCH /api/chat/:id/messages/read`** (note: **PATCH**, not POST; there is no `POST /api/chat/:chatId/read`). The body may carry `messageIds: string[]`; if `messageIds` is **empty or omitted, ALL messages are marked read**. +16. `ChatService.markMessagesAsRead` (`:438-483`): - Calls `chat.markAsRead(userId, messageObjectIds)` (schema method that flips `isRead` on the relevant messages and zeros the user's `unreadCounts` entry). - Emits **`messages-read`** to `chat-{chatId}` so the sender sees the double-tick. ### Typing indicator -15. On `input` events, frontend emits `socket.emit('typing-start', { chatId, userId, userName })`; on idle/blur emits `typing-stop`. -16. Backend `app.ts:142-158` relays to `chat-{chatId}` as `user-typing` (excluding the sender). No DB persistence. +17. On `input` events, frontend emits `socket.emit('typing-start', { chatId, userId, userName })`; on idle/blur emits `typing-stop`. +18. Backend `app.ts:142-158` relays to `chat-{chatId}` as `user-typing` (excluding the sender). No DB persistence. Limited to **5 typing indicators per 10 seconds**. + +### Participants (add / remove / role) + +19. **Add a participant** — real endpoint `POST /api/chat/:id/participants` expects a body of **`{ userId }` (a single id)**. + + > [!bug] ⚠️ KNOWN BUG — add participant payload mismatch + > The frontend `addParticipants` action sends `{ participants: string[] }` (an array), but the backend expects `{ userId }` (a single id). The shapes do not match. + +20. **Remove / leave** — to remove a participant (or have a user leave), use `DELETE /api/chat/:id/participants/:participantId`. Removal is a **soft removal**: the participant subdocument is kept with `isActive=false` and a `leftAt` timestamp. + + > [!bug] ⚠️ KNOWN BUG — leave action 404s + > `PUT /chat/:id/leave` **does NOT exist** on the backend. The frontend `leaveConversation` action targets that path and therefore **404s**. Use `DELETE /api/chat/:id/participants/:participantId` instead. + +21. **List participants** — + + > [!bug] ⚠️ KNOWN BUG — getParticipants 404s + > `GET /chat/:id/participants` **does NOT exist** — the backend only exposes `POST` (add) and `DELETE` (remove) on that path. The frontend `getParticipants` action 404s. Participants must be read from **`GET /api/chat/:id/info`** instead. + +22. **Change a participant role** — + + > [!bug] ⚠️ NOT IMPLEMENTED — updateParticipantRole + > `PUT /chat/:id/participants/:participantId` **does NOT exist** on the backend. The frontend `updateParticipantRole` action has no backend counterpart. + +### Chat info + +23. `getChatInfo` → `GET /api/chat/:id/info` returns chat details **plus only the first 50 messages** (page 1, limit 50) — **not** the full message history. Use the paginated `GET /api/chat/:id/messages` to load older messages. ## Sequence diagram @@ -100,22 +153,28 @@ sequenceDiagram participant IO as Socket.IO A->>FE_A: Open conversation - FE_A->>BE: POST /api/chat {type:direct, participantIds, relatedTo} - BE->>DB: find-or-create Chat + FE_A->>BE: POST /api/chat {type:direct, participantIds:[sellerId]} + BE->>DB: find-or-create Chat (caller auto-appended) BE-->>FE_A: { chat } FE_A->>IO: emit 'join-chat-room' chatId + FE_A->>IO: emit 'join-user-room' userId (separate from user-online) FE_B->>IO: emit 'join-chat-room' chatId (when B opens too) A->>FE_A: type & send - FE_A->>BE: POST /api/chat/{id}/messages {content} + FE_A->>BE: POST /api/chat/{id}/messages {content} (max 5000 chars) BE->>DB: chat.addMessage and update metadata.lastActivity to now BE->>IO: emit chat-{id} 'new-message' IO-->>FE_A: 'new-message' (echo) IO-->>FE_B: 'new-message' (live) BE->>IO: emit user-{B} 'chat-notification' (badge) + A->>FE_A: attach file + FE_A->>BE: POST /api/chat/{id}/messages/file (multipart/form-data) + BE->>DB: chat.addMessage with fileUrl/fileName/fileSize + BE->>IO: emit chat-{id} 'new-message' + B->>FE_B: opens chat - FE_B->>BE: POST /api/chat/{id}/read + FE_B->>BE: PATCH /api/chat/{id}/messages/read (empty messageIds = all) BE->>DB: chat.markAsRead(B) BE->>IO: emit chat-{id} 'messages-read' IO-->>FE_A: 'messages-read' (double-tick) @@ -128,25 +187,49 @@ sequenceDiagram | Method | Endpoint | Purpose | |---|---|---| -| `POST` | `/api/chat` | Find-or-create chat | +| `POST` | `/api/chat` | Find-or-create chat (exactly 1 external `participantId`; caller auto-appended; `relatedTo` NOT accepted) | | `GET` | `/api/chat` | List user's chats | -| `GET` | `/api/chat/:chatId/messages` | Paginated message history | -| `POST` | `/api/chat/:chatId/messages` | Send message | -| `POST` | `/api/chat/:chatId/upload` | Upload attachment | -| `POST` | `/api/chat/:chatId/read` | Mark read | +| `GET` | `/api/chat/:id/info` | Chat details + first 50 messages (page 1, limit 50) + participants | +| `GET` | `/api/chat/:id/messages` | Paginated message history | +| `POST` | `/api/chat/:id/messages` | Send text message | +| `POST` | `/api/chat/:id/messages/file` | Send file attachment (multipart/form-data) | +| `PATCH` | `/api/chat/:id/messages/read` | Mark read (empty/omitted `messageIds` marks ALL read) | +| `PUT` | `/api/chat/:id/messages/:messageId` | Edit message — body `{ content }`, 15-min edit window | +| `DELETE` | `/api/chat/:id/messages/:messageId` | Soft-delete a message (`deletedAt`, content cleared, emits `message-deleted`) | +| `POST` | `/api/chat/:id/participants` | Add a participant — body `{ userId }` (single) | +| `DELETE` | `/api/chat/:id/participants/:participantId` | Remove / leave (soft: `isActive=false`, `leftAt`) | | `POST` | `/api/chat/support` | Create/get support chat | +| `POST` | `/api/chat/purchase-request` | Create/link direct chat for a purchase request (no frontend action wired) | +| `PATCH` | `/api/chat/:id/archive` | Toggle archived state (archive **and** unarchive via same endpoint) | + +> [!bug] Frontend actions that target non-existent or mismatched backend endpoints +> - `leaveConversation` → `PUT /chat/:id/leave` — **does NOT exist** (404). Use `DELETE /api/chat/:id/participants/:participantId`. +> - `getParticipants` → `GET /chat/:id/participants` — **does NOT exist** (404). Use `GET /api/chat/:id/info`. +> - `updateParticipantRole` → `PUT /chat/:id/participants/:participantId` — **NOT IMPLEMENTED** on backend. +> - `editMessage` → sends `{ text }` but backend expects `{ content }` — edits fail/ignored. +> - `addParticipants` → sends `{ participants: string[] }` but backend expects `{ userId }` (single). +> - `sendFileMessage` → POSTs to the text endpoint instead of `POST /api/chat/:id/messages/file` — file uploads broken. + +## Rate limits & constraints + +- **Messages:** 20 messages / minute per user per chat. +- **Typing indicators:** 5 / 10 seconds. +- **Message dedup:** 5-minute window (duplicate sends within the window are de-duplicated). +- **Edit window:** 15 minutes — edits after that return **400**. +- **Message length:** 5000-character maximum (schema + controller). ## Database writes -- **`chats`**: insert on create; `$push` into `messages[]`; `$set` `metadata.lastActivity`; `unreadCounts.$.count` increment per recipient; `settings.isArchived` toggled; `participants.$.isActive` flipped on leave. +- **`chats`**: insert on create; `$push` into `messages[]`; `$set` `metadata.lastActivity`; `unreadCounts.$.count` increment per recipient; `settings.isArchived` toggled (archive/unarchive); message soft-delete sets `deletedAt` + clears `content`; participant removal sets `participants.$.isActive=false` + `participants.$.leftAt`. ## Socket events emitted - **`new-message`** → `chat-{chatId}` (every message). - **`chat-notification`** → `user-{recipientId}` for non-senders (badge). - **`messages-read`** → `chat-{chatId}` after read mark. +- **`message-deleted`** → `chat-{chatId}` after a message soft-delete. - **`user-typing`** → `chat-{chatId}` (relayed by `app.ts`). -- **`user-status-change`** → broadcast when `user-online` is emitted. +- **`user-status-change`** → broadcast when `user-online` is emitted (online only; **no offline broadcast on disconnect**). - **`new-message`** (system) for system welcome lines on chat creation. ## Side effects @@ -161,11 +244,13 @@ sequenceDiagram - **Sender not a participant** → `403 "User is not a participant in this chat"` (`:209-211`). - **Chat not found** → `404` on `getChatMessages`. - **Direct duplicate** → idempotent — `createChat` returns existing chat. -- **Empty content** — currently allowed (system messages are typically non-empty though); add a min-length validator if needed. -- **Files served from `/uploads`** — anonymous access is allowed. Sensitive attachments (KYC docs, dispute evidence) should be protected by signed URLs or per-user authorisation; currently any user with the URL can fetch. +- **Content too long** — backend rejects messages exceeding 5000 characters at both Mongoose schema and controller validation levels. +- **Edit after 15 minutes** → `400`. +- **Files served from `uploads/chat/`** — anonymous access is allowed. Sensitive attachments (KYC docs, dispute evidence) should be protected by signed URLs or per-user authorisation; currently any user with the URL can fetch. +- **Stale online status** — no offline broadcast on disconnect; peers may show "online" for a user who has left. - **Long conversations** — `getChatMessages` slices an in-memory copy of the messages array. For >10k messages this is inefficient. Use aggregation `$slice` or a separate collection. - **Race on `markAsRead`** — two parallel reads may double-zero the counter, which is harmless. -- **Typing indicator spam** — clients should debounce `typing-start` and emit `typing-stop` on a 2s idle. +- **Typing indicator spam** — clients should debounce `typing-start` and emit `typing-stop` on idle (rate-limited to 5/10s server-side regardless). > [!warning] Notification message uses placeholder sender name > `ChatService.sendMessage` posts `chat-notification` with `senderName: "کاربر"` (`:248`) — the literal Persian word for "user". Resolve `senderName` from `participant.userId.firstName` for a better UX. diff --git a/04 - Flows/Delivery Confirmation Flow.md b/04 - Flows/Delivery Confirmation Flow.md index 367803b..6b4bcca 100644 --- a/04 - Flows/Delivery Confirmation Flow.md +++ b/04 - Flows/Delivery Confirmation Flow.md @@ -26,7 +26,7 @@ After the escrow is funded ([[PRD - Request Network In-House Checkout]] / [[Escr ## 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)`: - Generates a 6-digit code (`Math.floor(100000 + Math.random()*900000)`). - 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`. 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`. -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`. - Checks `deliveryCodeExpiresAt > now` and `deliveryCodeUsed === false`. - On success: `deliveryInfo.deliveryCodeUsed = true; deliveryCodeUsedAt = now`. Status flips `delivery → delivered`. - Emits `purchase-request-update` `status-changed`. - - Triggers buyer/seller notifications via `notifyDeliveryConfirmed` (see `PurchaseRequestService.ts:631-641`). -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. + - 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). 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]]. ## Sequence diagram @@ -56,14 +56,14 @@ sequenceDiagram participant IO as Socket.IO S->>FE: Click "Mark as shipped" - FE->>BE: PATCH /api/marketplace/purchase-requests/{id} {status:"delivery"} - BE->>DB: PurchaseRequest.status="delivery" + FE->>BE: PUT /api/marketplace/purchase-requests/{id}/delivery + BE->>DB: PurchaseRequest.shippedAt=now, status="delivery" Note over BE,DB: No code generated here B->>FE: View delivery code in step-5-receive-goods FE->>BE: POST /api/marketplace/purchase-requests/{id}/delivery-code/generate 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 B->>S: At hand-off, read the 6-digit code aloud @@ -73,8 +73,8 @@ sequenceDiagram BE->>DB: set deliveryCodeUsed = true BE->>DB: set status = "delivered" BE->>IO: emit request-{id} 'purchase-request-update' status-changed - BE->>B: notifyDeliveryConfirmed - BE->>S: notifyDeliveryConfirmed + BE->>B: notifyDeliveryConfirmed (DeliveryService.verifyDeliveryCode) + BE->>S: notifyDeliveryConfirmed (DeliveryService.verifyDeliveryCode) Note over BE: Auto-release timer (planned) → seller_paid → payout ``` @@ -82,12 +82,12 @@ sequenceDiagram | 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) | | `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) | | `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) @@ -101,24 +101,25 @@ These Redux/API actions exist in the frontend but call endpoints that return 404 ## Two paths to `delivered` status -1. **Code path** — seller calls `POST .../delivery-code/verify` with the correct, unexpired code → status becomes `delivered`. -2. **Fast-track path** — buyer calls `PATCH .../confirm-delivery` (no code required) → also becomes `delivered`. ⚠️ Currently no authorization check on this endpoint. +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, and no delivery-specific notifications are sent to either party. ## Database writes - **`purchaserequests.deliveryInfo`** — `deliveryCode`, `deliveryCodeGeneratedAt`, `deliveryCodeExpiresAt`, `deliveryCodeUsed`, `deliveryCodeUsedAt`. +- **`purchaserequests.shippedAt`** — set when seller calls `PUT .../delivery`. - **`purchaserequests.status`** — `delivery` → `delivered` → (eventually `seller_paid` → `completed`). -- **`notifications`** — generated for both parties. +- **`notifications`** — generated for both parties (code path only). ## 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'`). - **`purchase-request-update`** `status-changed` on `delivery → delivered`. ## 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). ## Error / edge cases @@ -143,9 +144,8 @@ These Redux/API actions exist in the frontend but call endpoints that return 404 ## 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/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/delivery-code-verification.tsx` - Frontend: `frontend/src/sections/request/components/buyer-steps/step-5-receive-goods.tsx` diff --git a/04 - Flows/Dispute Flow.md b/04 - Flows/Dispute Flow.md index 244b090..979679a 100644 --- a/04 - Flows/Dispute Flow.md +++ b/04 - Flows/Dispute Flow.md @@ -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." --- +> **Last updated:** 2026-05-29 — aligned with code (see Doc vs Code Audit Report) + # 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. diff --git a/04 - Flows/Google OAuth Flow.md b/04 - Flows/Google OAuth Flow.md index 7ad90c3..c720a1b 100644 --- a/04 - Flows/Google OAuth Flow.md +++ b/04 - Flows/Google OAuth Flow.md @@ -7,6 +7,8 @@ related_apis: ["POST /api/auth/google/signup", "POST /api/auth/google/signin"] # 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. ## 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`). 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. -7. **Duplicate check**: `User.findOne({ email: googleUser.email })` — if found 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. +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`, 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}`. 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. @@ -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`. 2. Same GSI flow as sign-up — Google returns an ID token. 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`). 6. Tokens issued and returned identically to email login. -> [!tip] Account linking is implicit by email -> 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`. +> [!warning] No account merge +> 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 @@ -76,15 +81,19 @@ sequenceDiagram end BE->>GA: verifyGoogleToken(googleToken) GA-->>BE: { email, name, picture, ... } or null - BE->>DB: User.findOne({ email }) - alt Sign-up: user exists + alt Sign-up + BE->>DB: User.findOne({ email }) + else Sign-in + BE->>DB: User.findOne({ email, status: "active" }) + end + alt Sign-up: email exists BE-->>FE: 409 USER_EXISTS else Sign-up: new BE->>DB: User.create({ email, role, isEmailVerified:true, profile.avatar }) opt referral BE->>DB: increment referrer.referralStats end - else Sign-in: user missing + else Sign-in: no active user (missing or soft-deleted) BE-->>FE: 404 USER_NOT_FOUND else Sign-in: ok BE->>DB: set user.lastLoginAt = now @@ -120,8 +129,9 @@ sequenceDiagram ## Error / edge cases - **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. -- **User missing during sign-in** → `404`; frontend redirects to sign-up. +- **Email already exists during sign-up** → `409 USER_EXISTS`; frontend prompts to use sign-in instead. +- **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. - **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`. diff --git a/04 - Flows/Negotiation Flow.md b/04 - Flows/Negotiation Flow.md index 14a2c28..314bb95 100644 --- a/04 - Flows/Negotiation Flow.md +++ b/04 - Flows/Negotiation Flow.md @@ -2,11 +2,13 @@ title: Negotiation Flow tags: [flow, marketplace, negotiation, counter-offer, 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 +> **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. ## 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/`. - **Backend** — `ChatService.sendMessage` for chat lines, `SellerOfferService.updateOffer` for price/ETA edits, `PurchaseRequestService.updatePurchaseRequest` for the status flip. - **MongoDB** — `chats`, `selleroffers`, `purchaserequests`. -- **Socket.IO** — `new-message`, `seller-offer-update`, `purchase-request-update`. +- **Socket.IO** — `new-message`, `purchase-request-update`. ## 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`. - 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 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: - **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`): - `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. +> [!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`. -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. +> [!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 ```mermaid @@ -75,8 +86,8 @@ sequenceDiagram BE->>DB: PurchaseRequest.status = "in_negotiation" BE->>IO: emit request-{id} 'purchase-request-update' (status-changed) S->>FE_S: Open edit-offer modal, set new price - FE_S->>BE: PATCH /api/marketplace/offers/{id} {price:{amount:80}} - BE->>DB: SellerOffer update + FE_S->>BE: PUT /api/marketplace/offers/{id} {price:{amount:80}} ⚠️ backend only registers PATCH + BE->>DB: SellerOffer update (if PUT handled; else 404) BE->>IO: emit request-{id} 'purchase-request-update' (offer-updated) IO-->>FE_B: refresh offer card alt Buyer accepts @@ -84,10 +95,15 @@ sequenceDiagram Note over BE: Webhook PAID flips offer→accepted, request→payment else Buyer rejects 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->>BE: notifyOfferRejected(seller) 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 ``` @@ -97,14 +113,16 @@ sequenceDiagram |---|---|---| | `POST` | `/api/chat` | Find-or-create negotiation chat | | `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` | -| `POST` | `/api/marketplace/offers/:id/withdraw` | Seller pulls the offer | ## Database writes - **`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. - **`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 +- **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`). - **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`. +- **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`). - **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. @@ -141,8 +161,11 @@ sequenceDiagram ## 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/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/chat/` (chat UI) diff --git a/04 - Flows/Notification Flow.md b/04 - Flows/Notification Flow.md index 367cd5f..9751f65 100644 --- a/04 - Flows/Notification Flow.md +++ b/04 - Flows/Notification Flow.md @@ -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 })`. - 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 - `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. diff --git a/04 - Flows/Password Reset Flow.md b/04 - Flows/Password Reset Flow.md index 896acce..6923cae 100644 --- a/04 - Flows/Password Reset Flow.md +++ b/04 - Flows/Password Reset Flow.md @@ -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"] --- +> **Last updated:** 2026-05-29 — aligned with code (see Doc vs Code Audit Report) + > [!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. @@ -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`. 7. Frontend POSTs `POST /api/auth/reset-password-with-code { email, code, password }`. 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`. - 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. @@ -100,7 +102,7 @@ sequenceDiagram > > **`POST /api/auth/reset-password-with-code`** (primary UI path) > - Uses a 6-digit numeric code delivered by email. -> - `isValidVerificationCode()` validates with `/^\d{6}$/`. An 8-digit code will always fail. +> - `isValidVerificationCode()` validates with `/^\d{6}$/`. > - Has **no password complexity middleware**. Any string is accepted as the new password. > > **`POST /api/auth/reset-password`** (legacy token-based path) @@ -126,7 +128,7 @@ sequenceDiagram ## Error / edge cases - **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`. - **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). @@ -136,15 +138,11 @@ sequenceDiagram > [!warning] Plaintext code in logs > Same as [[Registration Flow]]: the reset code is `console.log`-ed by the controller in all environments. Restrict log access in production or gate the log behind `NODE_ENV !== 'production'`. -> [!bug] Controller comment says "8 digits" but code generates 6 -> The comment in `authController.ts` describes an 8-digit code, but `authService.generateVerificationCode()` uses `Math.floor(100000 + Math.random() * 900000)`, which produces a number in the range 100000–999999 (exactly 6 digits). `isValidVerificationCode()` enforces `/^\d{6}$/`. Any 8-digit value sent to `reset-password-with-code` will always be rejected. The comment is wrong; the 6-digit implementation and validation are correct and consistent. - ## Known issues summary | Issue | Severity | Details | |---|---|---| | No password complexity on code-based reset | Security gap | `POST /api/auth/reset-password-with-code` has no complexity middleware; weak passwords accepted | -| Controller comment says 8 digits | Doc bug | Comment is wrong; code generates and validates exactly 6 digits | | Inconsistent complexity between reset endpoints | Security gap | Token-based reset enforces complexity; code-based reset does not | ## Linked flows diff --git a/04 - Flows/Payment Flow - DePay & Web3.md b/04 - Flows/Payment Flow - DePay & Web3.md index f2c4992..41b6aa4 100644 --- a/04 - Flows/Payment Flow - DePay & Web3.md +++ b/04 - Flows/Payment Flow - DePay & Web3.md @@ -5,6 +5,8 @@ related_models: ["[[Payment]]", "[[PurchaseRequest]]"] 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 > 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. - **`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 - **`payment-created`** (admin dashboard) on intent creation. diff --git a/04 - Flows/Payment Flow - SHKeeper.md b/04 - Flows/Payment Flow - SHKeeper.md index 4c93251..68bb82a 100644 --- a/04 - Flows/Payment Flow - SHKeeper.md +++ b/04 - Flows/Payment Flow - SHKeeper.md @@ -2,11 +2,13 @@ title: Payment Flow - SHKeeper tags: [flow, payment, shkeeper, crypto, escrow, webhook] 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 -> 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) @@ -66,7 +68,7 @@ stateDiagram-v2 ### Phase 1 — Create intent 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`: - 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. @@ -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). - > [!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. 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 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->>R: getCachedWallet(amount, token, network, requestId) alt cache hit @@ -183,18 +185,26 @@ sequenceDiagram ## API calls -| Method | Endpoint | Purpose | Source | -|---|---|---|---| -| `POST` | `/api/payment/shkeeper/create` | Create pay-in intent | `shkeeperRoutes.ts` → `shkeeperService.createPayInIntent` | -| `POST` | `/api/payment/shkeeper/webhook` | Receive SHKeeper webhook | `shkeeperWebhook.handleShkeeperWebhook` | -| `POST` | `/api/payment/:id/release` | Release escrow to seller | `paymentRoutes.ts` | -| `POST` | `/api/payment/:id/release/confirm` | Confirm escrow release | `paymentRoutes.ts` | -| `POST` | `/api/payment/:id/refund` | Refund to buyer | `paymentRoutes.ts` | -| `POST` | `/api/payment/:id/refund/confirm` | Confirm buyer refund | `paymentRoutes.ts` | -| `POST` | `/api/payment/payments/:id/fetch-tx` | Manual transaction lookup | `paymentRoutes.ts` | -| ~~`GET /api/payment/shkeeper/status/:paymentId`~~ | | ⚠️ **404 — does not exist.** Use socket events instead. | — | +| Method | Endpoint | Purpose | Auth | Source | +|---|---|---|---|---| +| `POST` | `/api/payment/shkeeper/intents` | Create pay-in intent | Bearer JWT (buyer) | `shkeeperRoutes.ts` → `shkeeperService.createPayInIntent` | +| `POST` | `/api/payment/shkeeper/webhook` | Receive SHKeeper webhook | HMAC / API key | `shkeeperWebhook.handleShkeeperWebhook` | +| `POST` | `/api/payment/:id/release` | Release escrow to seller | Bearer JWT | `paymentRoutes.ts` | +| `POST` | `/api/payment/:id/release/confirm` | Confirm escrow release | Bearer JWT | `paymentRoutes.ts` | +| `POST` | `/api/payment/:id/refund` | Refund to buyer | Bearer JWT | `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 | Bearer JWT | `paymentRoutes.ts` | +| `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: > - ~~`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` diff --git a/04 - Flows/Payout Flow.md b/04 - Flows/Payout Flow.md index e826a3b..fc9c574 100644 --- a/04 - Flows/Payout Flow.md +++ b/04 - Flows/Payout Flow.md @@ -7,6 +7,8 @@ related_apis: ["POST /api/payment/:id/release", "POST /api/payment/:id/release/c # 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. 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. - No active dispute hold blocks the operation, unless the operation is the explicit dispute resolution path. - 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. ## 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. 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. -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. ## 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`. 3. Backend validates available funds and policy. 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. ## Sequence Diagram @@ -74,15 +76,19 @@ sequenceDiagram A->>C: Request Trezor/Safe execution C->>BC: Broadcast transfer 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->>DB: append release/refund ledger entry BE->>DB: update Payment escrowState - BE-->>R: notification + BE-->>R: notification (no realtime socket listener — see gap below) ``` ## 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 | |---|---|---| | `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/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 - **`payments`** -- status, `escrowState`, `blockchain.transactionHash`, signer metadata. @@ -99,14 +143,24 @@ sequenceDiagram - **`purchaserequests`** -- terminal business state after release/refund completes. - **`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 - **Insufficient ledger balance** -- reject instruction build/confirm. - **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. - **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. +- **Wrong namespace** -- calling release/refund under `/api/payment/request-network/:id/...` returns 404 (those routes do not exist). ## Legacy SHKeeper Note @@ -122,9 +176,15 @@ Older versions used SHKeeper payout tasks and scripts such as `fix-transaction-h ## 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/ledger/fundsLedgerService.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` -- 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) diff --git a/04 - Flows/Purchase Request Flow.md b/04 - Flows/Purchase Request Flow.md index c21dd68..f378b87 100644 --- a/04 - Flows/Purchase Request Flow.md +++ b/04 - Flows/Purchase Request Flow.md @@ -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"] --- +> **Last updated:** 2026-05-29 — aligned with code (see Doc vs Code Audit Report) + > [!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. diff --git a/04 - Flows/Rating Flow.md b/04 - Flows/Rating Flow.md index 8d50c15..908562a 100644 --- a/04 - Flows/Rating Flow.md +++ b/04 - Flows/Rating Flow.md @@ -7,6 +7,11 @@ related_apis: ["POST /api/marketplace/reviews", "GET /api/marketplace/reviews/:s # 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`. ## Actors diff --git a/04 - Flows/Referral Flow.md b/04 - Flows/Referral Flow.md index 65e4180..0960abf 100644 --- a/04 - Flows/Referral Flow.md +++ b/04 - Flows/Referral Flow.md @@ -7,15 +7,17 @@ related_apis: ["POST /api/points/generate-referral-code", "GET /api/points/refer # 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]]. ## Actors - **Referrer** — the user with the code. - **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`. -- **Socket.IO** — `referral-signup` and `level-up` events. +- **Socket.IO** — `referral-signup` (auth domain) and `referral-reward` / `level-up` (points domain) events. ## Preconditions @@ -26,17 +28,19 @@ Each user can generate a personal referral code, share a short URL, and earn poi ### 1. Code generation -1. User opens `/dashboard/account/referrals`. If they don't have a code yet, they click "Generate code". -2. Frontend POSTs `POST /api/points/generate-referral-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. A manual `POST /api/points/generate-referral-code` is also available. 3. `PointsService.generateReferralCode(userId)` (`:12-31`): - 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. -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 -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). ### 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 })`: - Sets `user.referredBy = referrer._id` on the new user. - 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. +> [!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 -9. `PointsService.addPoints(userId, amount, source, metadata)` (`:36-100`) is called by other services on triggering events: - - **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'`. - - **Bonus**: ad-hoc admin grants. -10. Inside `addPoints`: +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. +10. `PointsService.processReferralReward(purchaseRequestId)` (`:372-429`): + - Loads the purchase request, finds the buyer and the buyer's `referredBy` referrer (returns `null` if either is missing). + - 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. - `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`. - 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. `PointTransaction` records `type: 'spend'` with negative `amount`, keeping `balance` running. +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). +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 @@ -77,11 +93,11 @@ sequenceDiagram participant DB as MongoDB 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 - BE->>DB: User.findByIdAndUpdate(referralCode=...) - BE-->>FE: { code } - R->>R: share https://amn.gg/r/{code} + BE->>DB: User.findByIdAndUpdate(referralCode=...) (ALWAYS overwrites) + BE-->>FE: { referralCode } + R->>R: share ${NEXT_PUBLIC_API_URL}/r/{code} (backend URL) N->>BE: GET /r/{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) BE->>DB: User.create 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 - BE->>BE: PointsService.addPoints(R, +X, 'referral', {referredUserId:N}) - BE->>DB: add X points to user balance - BE->>DB: create PointTransaction record + Note over BE,DB: ONLY when N's order reaches status 'completed' + BE->>BE: marketplaceController → PointsService.processReferralReward(id) + BE->>BE: addPoints(R, floor(amount*0.02), 'referral', {...}) + BE->>DB: add points to balance + create PointTransaction BE->>BE: updateUserLevel → maybe '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 -| Method | Endpoint | Purpose | -|---|---|---| -| `POST` | `/api/points/generate-referral-code` | Generate or rotate referral code | -| `GET` | `/api/points/my-points` | Balance + level | -| `GET` | `/api/points/transactions` | History | -| `GET` | `/api/points/referrals` | Referred users list | -| `GET` | `/api/points/leaderboard` | Global top referrers | -| `GET` | `/api/points/levels` | Level config (public) | -| `POST` | `/api/points/redeem` | Spend points | -| `POST` | `/api/points/admin/add` | Admin-only manual grant | -| `GET` | `/r/:code` | Short-URL redirect to sign-up | +> [!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. + +| Method | Endpoint | Auth | Body / Query | Response data | +|---|---|---|---|---| +| `POST` | `/api/points/generate-referral-code` | user | (ignored) | `{ referralCode }` — always rotates the code | +| `GET` | `/api/points/my-points` | user | — | `{ points, referral, currentLevel, nextLevel }` | +| `GET` | `/api/points/transactions` | user | `page`, `limit`, `type` (`earn`/`spend`/`expire` only) | `{ transactions, pagination }` | +| `GET` | `/api/points/referrals` | user | `page`, `limit` | `{ referrals, pagination }` | +| `GET` | `/api/points/leaderboard` | user | `limit` only (**`period` is NOT supported**) | `{ leaderboard, total }` | +| `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 -- **`users`**: `referralCode` on generation, `referredBy` on referee creation, `referralStats.{totalReferrals, activeReferrals, totalEarned}` and `points.{total, available, level}` on point events. -- **`pointtransactions`**: one document per earn/spend/refund. +- **`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` event. (`expire` is defined in the schema but **never written** — see below.) - **`levelconfigs`**: read-only at runtime (seeded at deploy). ## Socket events emitted -- **`referral-signup`** → `user-{referrerId}` on referee creation. -- **`level-up`** → `user-{userId}` when crossing a tier. -- **`new-notification`** → standard notification channel for points-related milestones. +- **`referral-signup`** → `user-{referrerId}` on referee creation — emitted by `authController.ts`; this is an **auth-domain** event (NOT emitted by `PointsService`). +- **`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.) +- **`level-up`** → `user-{userId}` when crossing a tier (`PointsService.ts:92`). ## 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). -- Transactions are wrapped in a Mongo session for atomicity (`addPoints:47-88`). +- `points.available` is the spendable balance; `points.total` is the lifetime accumulation (used for level tiers); `points.used` tracks redeemed points. +- Transactions are wrapped in a Mongo session for atomicity (`addPoints:47-88`, `redeemPoints:123-153`). ## Error / edge cases - **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. -- **Referral code entered with leading/trailing spaces** — `.trim()` is applied (`authController.ts:74`, `:127`). -- **Referrer deleted** — `referredBy` still points to the deleted user; the new user is effectively un-attributed. Soft-delete preservation is acceptable. -- **Points overflow** — `Number` is sufficient up to 2⁵³; no overflow risk in practice. -- **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`** — defined in `referralStats` but no code path increments it currently. Define "active" (e.g. referee has at least one completed purchase) and update accordingly. +- **Self-referral** — **NOT blocked** at any level (see danger callout above). Known gap. +- **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. +- **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. +- **`activeReferrals` semantics** — counts **all** referred users, not just those who completed a purchase. If conversion tracking is the intent, this counter is misleading. > [!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 - [[Registration Flow]] — attribution point. - [[Google OAuth Flow]] — also supports `referralCode`. -- [[Notification Flow]] — `referral-signup`, `level-up`, and points events surface here. -- [[PRD - Request Network In-House Checkout]] / [[Escrow Flow]] — completion of a purchase is the canonical trigger for awarding referral commission. +- [[Notification Flow]] — `referral-signup`, `referral-reward`, `level-up` surface here. +- [[Escrow Flow]] — order reaching `'completed'` is the **sole** trigger for awarding referral commission. ## Source files @@ -158,7 +203,8 @@ sequenceDiagram - Backend: `backend/src/routes/pointsRoutes.ts` - Backend: `backend/src/models/PointTransaction.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/auth/authController.ts:817-838` (referral on Google signup) +- Backend: `backend/src/services/marketplace/marketplaceController.ts:473-475` (referral reward triggered ONLY on `'completed'`) +- 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) -- 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) diff --git a/04 - Flows/Registration Flow.md b/04 - Flows/Registration Flow.md index af5b46c..d8a90b5 100644 --- a/04 - Flows/Registration Flow.md +++ b/04 - Flows/Registration Flow.md @@ -7,6 +7,8 @@ related_apis: ["POST /api/auth/register", "POST /api/auth/verify-email-code", "P # 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. ## 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`). 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` -> 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. +> [!bug] ⚠️ KNOWN BUG / quirk — the sign-up form does not collect the real password +> `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. 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). @@ -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`. 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"`. -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` - `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`). 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`). @@ -139,9 +142,9 @@ sequenceDiagram ## Database writes -- **`tempverifications` collection**: insert on first POST, in-place update on duplicate POST (`authController.ts:66-108`), 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 (referrer)**: `referralStats.totalReferrals` incremented (`authController.ts:419`). +- **`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:680-688`). The first refresh token is appended in the same save. +- **`users` collection (referrer)**: `referralStats.totalReferrals` incremented (`authController.ts:699`). ## Socket events emitted @@ -149,7 +152,7 @@ sequenceDiagram ``` { userId, userName, userEmail, timestamp, totalReferrals } ``` - Source: `authController.ts:423-431`. + Source: `authController.ts:704-710` (and `:1132` on the parallel path). ## Side effects @@ -168,6 +171,7 @@ sequenceDiagram - **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. - **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: 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. diff --git a/04 - Flows/Seller Offer Flow.md b/04 - Flows/Seller Offer Flow.md index be17c49..86c492b 100644 --- a/04 - Flows/Seller Offer Flow.md +++ b/04 - Flows/Seller Offer Flow.md @@ -136,7 +136,7 @@ sequenceDiagram end BE->>N: notifyNewOfferReceived 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 } IO-->>FE_B: notify buyer bell icon B->>FE_B: Open request detail @@ -171,6 +171,7 @@ sequenceDiagram ## Socket events emitted - **`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`** → `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). diff --git a/04 - Flows/Trezor Safekeeping Flow.md b/04 - Flows/Trezor Safekeeping Flow.md index c5dc9a3..4354c51 100644 --- a/04 - Flows/Trezor Safekeeping Flow.md +++ b/04 - Flows/Trezor Safekeeping Flow.md @@ -1,9 +1,19 @@ +> **Last updated:** 2026-05-29 — aligned with code (see Doc vs Code Audit Report) + # 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. 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 - 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. - 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 -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: - `GET /api/trezor/registration-message?xpub=...®istrationAddress=...` 3. The registration address must be the first derived address from the xpub: - `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: - `POST /api/trezor/register` - `xpub` @@ -30,14 +45,7 @@ Default mode: optional. Existing release/refund flows do not require Trezor proo - xpub is public, not private. - registration address matches xpub-derived index `0`. - signature recovers the registration address. -7. Backend stores only: - - `userId` - - xpub fingerprint - - xpub - - base derivation path - - registration address - - next address index - - issued address records +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. ## 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: ```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. -## 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 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 { "txHash": "0x...", - "trezor": { - "message": "Amanat escrow Trezor transaction approval\n...", - "signature": "0x..." - } + "amount": 100, + "trezor": { "message": "", "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 @@ -95,7 +110,7 @@ When `TREZOR_SAFEKEEPING_REQUIRED=true`, `confirmReleaseRefundInstruction` verif 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 diff --git a/Issues/ISSUE-001-dispute-status-no-role-guard.md b/Issues/ISSUE-001-dispute-status-no-role-guard.md deleted file mode 100644 index c2c38b1..0000000 --- a/Issues/ISSUE-001-dispute-status-no-role-guard.md +++ /dev/null @@ -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]] diff --git a/Issues/ISSUE-001-patch-api-disputes-id-status-and-post-api-disputes-id-resolv.md b/Issues/ISSUE-001-patch-api-disputes-id-status-and-post-api-disputes-id-resolv.md new file mode 100644 index 0000000..7f3f3a3 --- /dev/null +++ b/Issues/ISSUE-001-patch-api-disputes-id-status-and-post-api-disputes-id-resolv.md @@ -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) diff --git a/Issues/ISSUE-002-dispute-resolve-no-role-guard.md b/Issues/ISSUE-002-dispute-resolve-no-role-guard.md deleted file mode 100644 index 68e0fee..0000000 --- a/Issues/ISSUE-002-dispute-resolve-no-role-guard.md +++ /dev/null @@ -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]] diff --git a/Issues/ISSUE-002-post-api-disputes-id-assign-has-no-role-guard-any-user-can-s.md b/Issues/ISSUE-002-post-api-disputes-id-assign-has-no-role-guard-any-user-can-s.md new file mode 100644 index 0000000..e1c1143 --- /dev/null +++ b/Issues/ISSUE-002-post-api-disputes-id-assign-has-no-role-guard-any-user-can-s.md @@ -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) diff --git a/Issues/ISSUE-003-dispute-route-shadowing.md b/Issues/ISSUE-003-dispute-route-shadowing.md deleted file mode 100644 index 7cf7086..0000000 --- a/Issues/ISSUE-003-dispute-route-shadowing.md +++ /dev/null @@ -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]] diff --git a/Issues/ISSUE-003-route-shadowing-post-api-disputes-purchaserequestid-resolve-.md b/Issues/ISSUE-003-route-shadowing-post-api-disputes-purchaserequestid-resolve-.md new file mode 100644 index 0000000..2079d23 --- /dev/null +++ b/Issues/ISSUE-003-route-shadowing-post-api-disputes-purchaserequestid-resolve-.md @@ -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) diff --git a/Issues/ISSUE-004-payment-endpoints-no-auth.md b/Issues/ISSUE-004-payment-endpoints-no-auth.md deleted file mode 100644 index ae78d9d..0000000 --- a/Issues/ISSUE-004-payment-endpoints-no-auth.md +++ /dev/null @@ -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]] diff --git a/Issues/ISSUE-004-post-api-disputes-id-resolve-dashboard-does-not-trigger-escr.md b/Issues/ISSUE-004-post-api-disputes-id-resolve-dashboard-does-not-trigger-escr.md new file mode 100644 index 0000000..6565db3 --- /dev/null +++ b/Issues/ISSUE-004-post-api-disputes-id-resolve-dashboard-does-not-trigger-escr.md @@ -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) diff --git a/Issues/ISSUE-005-post-api-payment-payments-id-fetch-tx-post-api-payment-payme.md b/Issues/ISSUE-005-post-api-payment-payments-id-fetch-tx-post-api-payment-payme.md new file mode 100644 index 0000000..eaac740 --- /dev/null +++ b/Issues/ISSUE-005-post-api-payment-payments-id-fetch-tx-post-api-payment-payme.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) diff --git a/Issues/ISSUE-005-scanner-status-no-auth.md b/Issues/ISSUE-005-scanner-status-no-auth.md deleted file mode 100644 index 4b620cd..0000000 --- a/Issues/ISSUE-005-scanner-status-no-auth.md +++ /dev/null @@ -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]] diff --git a/Issues/ISSUE-006-delete-account-wrong-endpoint.md b/Issues/ISSUE-006-delete-account-wrong-endpoint.md deleted file mode 100644 index 7a17b93..0000000 --- a/Issues/ISSUE-006-delete-account-wrong-endpoint.md +++ /dev/null @@ -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 diff --git a/Issues/ISSUE-006-get-api-admin-scanner-status-has-no-authentication-middlewar.md b/Issues/ISSUE-006-get-api-admin-scanner-status-has-no-authentication-middlewar.md new file mode 100644 index 0000000..321e3f0 --- /dev/null +++ b/Issues/ISSUE-006-get-api-admin-scanner-status-has-no-authentication-middlewar.md @@ -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) diff --git a/Issues/ISSUE-007-frontend-deleteaccount-action-calls-delete-user-profile-whic.md b/Issues/ISSUE-007-frontend-deleteaccount-action-calls-delete-user-profile-whic.md new file mode 100644 index 0000000..56e845a --- /dev/null +++ b/Issues/ISSUE-007-frontend-deleteaccount-action-calls-delete-user-profile-whic.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) diff --git a/Issues/ISSUE-007-sim-bypass-no-env-guard.md b/Issues/ISSUE-007-sim-bypass-no-env-guard.md deleted file mode 100644 index 53cfe03..0000000 --- a/Issues/ISSUE-007-sim-bypass-no-env-guard.md +++ /dev/null @@ -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 diff --git a/Issues/ISSUE-008-chat-file-upload-wrong-endpoint.md b/Issues/ISSUE-008-chat-file-upload-wrong-endpoint.md deleted file mode 100644 index d60992b..0000000 --- a/Issues/ISSUE-008-chat-file-upload-wrong-endpoint.md +++ /dev/null @@ -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 diff --git a/Issues/ISSUE-008-sendfilemessage-posts-to-wrong-endpoint-file-uploads-silentl.md b/Issues/ISSUE-008-sendfilemessage-posts-to-wrong-endpoint-file-uploads-silentl.md new file mode 100644 index 0000000..b2d5d65 --- /dev/null +++ b/Issues/ISSUE-008-sendfilemessage-posts-to-wrong-endpoint-file-uploads-silentl.md @@ -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) diff --git a/Issues/ISSUE-009-archive-chat-wrong-method.md b/Issues/ISSUE-009-archive-chat-wrong-method.md deleted file mode 100644 index 6ae07c9..0000000 --- a/Issues/ISSUE-009-archive-chat-wrong-method.md +++ /dev/null @@ -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 diff --git a/Issues/ISSUE-009-archiveconversation-sends-put-but-backend-only-accepts-patch.md b/Issues/ISSUE-009-archiveconversation-sends-put-but-backend-only-accepts-patch.md new file mode 100644 index 0000000..47814cc --- /dev/null +++ b/Issues/ISSUE-009-archiveconversation-sends-put-but-backend-only-accepts-patch.md @@ -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) diff --git a/Issues/ISSUE-010-admin-user-status-wrong-values-and-verb.md b/Issues/ISSUE-010-admin-user-status-wrong-values-and-verb.md deleted file mode 100644 index d1e2342..0000000 --- a/Issues/ISSUE-010-admin-user-status-wrong-values-and-verb.md +++ /dev/null @@ -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 diff --git a/Issues/ISSUE-010-frontend-admin-updateuserstatus-and-updateuserrole-use-put-b.md b/Issues/ISSUE-010-frontend-admin-updateuserstatus-and-updateuserrole-use-put-b.md new file mode 100644 index 0000000..4681012 --- /dev/null +++ b/Issues/ISSUE-010-frontend-admin-updateuserstatus-and-updateuserrole-use-put-b.md @@ -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) diff --git a/Issues/ISSUE-011-frontend-updateuserstatus-sends-inactive-pending-status-valu.md b/Issues/ISSUE-011-frontend-updateuserstatus-sends-inactive-pending-status-valu.md new file mode 100644 index 0000000..3d65084 --- /dev/null +++ b/Issues/ISSUE-011-frontend-updateuserstatus-sends-inactive-pending-status-valu.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) diff --git a/Issues/ISSUE-011-update-purchase-request-put-vs-patch.md b/Issues/ISSUE-011-update-purchase-request-put-vs-patch.md deleted file mode 100644 index e03f6a5..0000000 --- a/Issues/ISSUE-011-update-purchase-request-put-vs-patch.md +++ /dev/null @@ -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 diff --git a/Issues/ISSUE-012-trezor-safekeeping-zero-frontend-implementation-all-admin-re.md b/Issues/ISSUE-012-trezor-safekeeping-zero-frontend-implementation-all-admin-re.md new file mode 100644 index 0000000..dd1b950 --- /dev/null +++ b/Issues/ISSUE-012-trezor-safekeeping-zero-frontend-implementation-all-admin-re.md @@ -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` diff --git a/Issues/ISSUE-012-update-offer-put-vs-patch.md b/Issues/ISSUE-012-update-offer-put-vs-patch.md deleted file mode 100644 index 4ada354..0000000 --- a/Issues/ISSUE-012-update-offer-put-vs-patch.md +++ /dev/null @@ -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 diff --git a/Issues/ISSUE-013-createproviderpaymentintent-always-routes-to-request-network.md b/Issues/ISSUE-013-createproviderpaymentintent-always-routes-to-request-network.md new file mode 100644 index 0000000..b6a66e3 --- /dev/null +++ b/Issues/ISSUE-013-createproviderpaymentintent-always-routes-to-request-network.md @@ -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) diff --git a/Issues/ISSUE-013-select-offer-no-status-filter-corrupts-withdrawn.md b/Issues/ISSUE-013-select-offer-no-status-filter-corrupts-withdrawn.md deleted file mode 100644 index ebded8d..0000000 --- a/Issues/ISSUE-013-select-offer-no-status-filter-corrupts-withdrawn.md +++ /dev/null @@ -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 diff --git a/Issues/ISSUE-014-paymentprovider-typescript-type-excludes-shkeeper-and-decent.md b/Issues/ISSUE-014-paymentprovider-typescript-type-excludes-shkeeper-and-decent.md new file mode 100644 index 0000000..512b7f4 --- /dev/null +++ b/Issues/ISSUE-014-paymentprovider-typescript-type-excludes-shkeeper-and-decent.md @@ -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) diff --git a/Issues/ISSUE-014-select-offer-no-seller-notifications.md b/Issues/ISSUE-014-select-offer-no-seller-notifications.md deleted file mode 100644 index ce92b8c..0000000 --- a/Issues/ISSUE-014-select-offer-no-seller-notifications.md +++ /dev/null @@ -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 diff --git a/Issues/ISSUE-015-seller-offer-withdraw-no-http-route.md b/Issues/ISSUE-015-seller-offer-withdraw-no-http-route.md deleted file mode 100644 index 030e63d..0000000 --- a/Issues/ISSUE-015-seller-offer-withdraw-no-http-route.md +++ /dev/null @@ -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 diff --git a/Issues/ISSUE-015-simulated-transaction-sim-bypass-has-no-environment-guard-ca.md b/Issues/ISSUE-015-simulated-transaction-sim-bypass-has-no-environment-guard-ca.md new file mode 100644 index 0000000..2c529d0 --- /dev/null +++ b/Issues/ISSUE-015-simulated-transaction-sim-bypass-has-no-environment-guard-ca.md @@ -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) diff --git a/Issues/ISSUE-016-payment-provider-routing-always-request-network.md b/Issues/ISSUE-016-payment-provider-routing-always-request-network.md deleted file mode 100644 index 4bed742..0000000 --- a/Issues/ISSUE-016-payment-provider-routing-always-request-network.md +++ /dev/null @@ -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]] diff --git a/Issues/ISSUE-016-updatepurchaserequest-uses-put-but-backend-only-registers-pa.md b/Issues/ISSUE-016-updatepurchaserequest-uses-put-but-backend-only-registers-pa.md new file mode 100644 index 0000000..bbc8037 --- /dev/null +++ b/Issues/ISSUE-016-updatepurchaserequest-uses-put-but-backend-only-registers-pa.md @@ -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) diff --git a/Issues/ISSUE-017-payment-provider-type-missing-values.md b/Issues/ISSUE-017-payment-provider-type-missing-values.md deleted file mode 100644 index 8223d13..0000000 --- a/Issues/ISSUE-017-payment-provider-type-missing-values.md +++ /dev/null @@ -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]] diff --git a/Issues/ISSUE-017-updateoffer-uses-put-marketplace-offers-id-but-backend-regis.md b/Issues/ISSUE-017-updateoffer-uses-put-marketplace-offers-id-but-backend-regis.md new file mode 100644 index 0000000..1f0a2e4 --- /dev/null +++ b/Issues/ISSUE-017-updateoffer-uses-put-marketplace-offers-id-but-backend-regis.md @@ -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) diff --git a/Issues/ISSUE-018-select-offer-updatemany-has-no-status-filter-overwrites-with.md b/Issues/ISSUE-018-select-offer-updatemany-has-no-status-filter-overwrites-with.md new file mode 100644 index 0000000..f6a1f63 --- /dev/null +++ b/Issues/ISSUE-018-select-offer-updatemany-has-no-status-filter-overwrites-with.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) diff --git a/Issues/ISSUE-018-trezor-no-frontend-implementation.md b/Issues/ISSUE-018-trezor-no-frontend-implementation.md deleted file mode 100644 index e23e169..0000000 --- a/Issues/ISSUE-018-trezor-no-frontend-implementation.md +++ /dev/null @@ -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 diff --git a/Issues/ISSUE-019-rn-payout-release-refund-not-implemented.md b/Issues/ISSUE-019-rn-payout-release-refund-not-implemented.md deleted file mode 100644 index b7170af..0000000 --- a/Issues/ISSUE-019-rn-payout-release-refund-not-implemented.md +++ /dev/null @@ -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 diff --git a/Issues/ISSUE-019-selleroffer-status-active-does-not-exist-in-schema-enum-but-.md b/Issues/ISSUE-019-selleroffer-status-active-does-not-exist-in-schema-enum-but-.md new file mode 100644 index 0000000..116a485 --- /dev/null +++ b/Issues/ISSUE-019-selleroffer-status-active-does-not-exist-in-schema-enum-but-.md @@ -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) diff --git a/Issues/ISSUE-020-dispute-assign-no-role-guard.md b/Issues/ISSUE-020-dispute-assign-no-role-guard.md deleted file mode 100644 index 59dbf24..0000000 --- a/Issues/ISSUE-020-dispute-assign-no-role-guard.md +++ /dev/null @@ -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 -{ "adminId": "" } -``` -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]] diff --git a/Issues/ISSUE-020-select-offer-does-not-send-per-seller-socket-events-or-notif.md b/Issues/ISSUE-020-select-offer-does-not-send-per-seller-socket-events-or-notif.md new file mode 100644 index 0000000..f29714e --- /dev/null +++ b/Issues/ISSUE-020-select-offer-does-not-send-per-seller-socket-events-or-notif.md @@ -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) diff --git a/Issues/ISSUE-021-axios-interceptor-403-not-handled.md b/Issues/ISSUE-021-axios-interceptor-403-not-handled.md deleted file mode 100644 index 07dad69..0000000 --- a/Issues/ISSUE-021-axios-interceptor-403-not-handled.md +++ /dev/null @@ -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 diff --git a/Issues/ISSUE-021-post-api-marketplace-offers-id-withdraw-http-route-does-not-.md b/Issues/ISSUE-021-post-api-marketplace-offers-id-withdraw-http-route-does-not-.md new file mode 100644 index 0000000..ecaf438 --- /dev/null +++ b/Issues/ISSUE-021-post-api-marketplace-offers-id-withdraw-http-route-does-not-.md @@ -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) diff --git a/Issues/ISSUE-022-get-api-payment-payments-id-debug-has-no-authentication-full.md b/Issues/ISSUE-022-get-api-payment-payments-id-debug-has-no-authentication-full.md new file mode 100644 index 0000000..09a75ef --- /dev/null +++ b/Issues/ISSUE-022-get-api-payment-payments-id-debug-has-no-authentication-full.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) diff --git a/Issues/ISSUE-022-rate-limit-counts-all-attempts.md b/Issues/ISSUE-022-rate-limit-counts-all-attempts.md deleted file mode 100644 index 62135d9..0000000 --- a/Issues/ISSUE-022-rate-limit-counts-all-attempts.md +++ /dev/null @@ -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 diff --git a/Issues/ISSUE-023-change-password-no-ui.md b/Issues/ISSUE-023-change-password-no-ui.md deleted file mode 100644 index 231da86..0000000 --- a/Issues/ISSUE-023-change-password-no-ui.md +++ /dev/null @@ -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 diff --git a/Issues/ISSUE-023-get-api-payment-export-has-no-admin-role-guard-at-route-leve.md b/Issues/ISSUE-023-get-api-payment-export-has-no-admin-role-guard-at-route-leve.md new file mode 100644 index 0000000..f6bcccf --- /dev/null +++ b/Issues/ISSUE-023-get-api-payment-export-has-no-admin-role-guard-at-route-leve.md @@ -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) diff --git a/Issues/ISSUE-024-get-api-payment-stats-has-no-admin-role-guard-any-authentica.md b/Issues/ISSUE-024-get-api-payment-stats-has-no-admin-role-guard-any-authentica.md new file mode 100644 index 0000000..641b951 --- /dev/null +++ b/Issues/ISSUE-024-get-api-payment-stats-has-no-admin-role-guard-any-authentica.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) diff --git a/Issues/ISSUE-024-reset-password-with-code-no-complexity-check.md b/Issues/ISSUE-024-reset-password-with-code-no-complexity-check.md deleted file mode 100644 index d94b75c..0000000 --- a/Issues/ISSUE-024-reset-password-with-code-no-complexity-check.md +++ /dev/null @@ -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 diff --git a/Issues/ISSUE-025-dispute-socket-events-all-stubs.md b/Issues/ISSUE-025-dispute-socket-events-all-stubs.md deleted file mode 100644 index 0b9a522..0000000 --- a/Issues/ISSUE-025-dispute-socket-events-all-stubs.md +++ /dev/null @@ -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) diff --git a/Issues/ISSUE-025-get-api-disputes-statistics-has-no-admin-role-guard-any-auth.md b/Issues/ISSUE-025-get-api-disputes-statistics-has-no-admin-role-guard-any-auth.md new file mode 100644 index 0000000..0983c50 --- /dev/null +++ b/Issues/ISSUE-025-get-api-disputes-statistics-has-no-admin-role-guard-any-auth.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) diff --git a/Issues/ISSUE-026-get-notifications-id-only-returns-user-s-most-recent-notific.md b/Issues/ISSUE-026-get-notifications-id-only-returns-user-s-most-recent-notific.md new file mode 100644 index 0000000..d2dad6e --- /dev/null +++ b/Issues/ISSUE-026-get-notifications-id-only-returns-user-s-most-recent-notific.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) diff --git a/Issues/ISSUE-026-payment-completed-not-counted-in-stats.md b/Issues/ISSUE-026-payment-completed-not-counted-in-stats.md deleted file mode 100644 index a512909..0000000 --- a/Issues/ISSUE-026-payment-completed-not-counted-in-stats.md +++ /dev/null @@ -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 diff --git a/Issues/ISSUE-027-confirm-delivery-endpoint-has-no-ownership-check-any-authent.md b/Issues/ISSUE-027-confirm-delivery-endpoint-has-no-ownership-check-any-authent.md new file mode 100644 index 0000000..c7a3980 --- /dev/null +++ b/Issues/ISSUE-027-confirm-delivery-endpoint-has-no-ownership-check-any-authent.md @@ -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) diff --git a/Issues/ISSUE-027-get-notification-by-id-broken.md b/Issues/ISSUE-027-get-notification-by-id-broken.md deleted file mode 100644 index 1281206..0000000 --- a/Issues/ISSUE-027-get-notification-by-id-broken.md +++ /dev/null @@ -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 diff --git a/Issues/ISSUE-028-delivery-code-generated-socket-event-broadcasts-raw-6-digit-.md b/Issues/ISSUE-028-delivery-code-generated-socket-event-broadcasts-raw-6-digit-.md new file mode 100644 index 0000000..1aaef48 --- /dev/null +++ b/Issues/ISSUE-028-delivery-code-generated-socket-event-broadcasts-raw-6-digit-.md @@ -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) diff --git a/Issues/ISSUE-028-payment-export-no-admin-guard.md b/Issues/ISSUE-028-payment-export-no-admin-guard.md deleted file mode 100644 index f75754f..0000000 --- a/Issues/ISSUE-028-payment-export-no-admin-guard.md +++ /dev/null @@ -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 diff --git a/Issues/ISSUE-029-delivery-attempts-stats-phantom-endpoints.md b/Issues/ISSUE-029-delivery-attempts-stats-phantom-endpoints.md deleted file mode 100644 index 33d8c86..0000000 --- a/Issues/ISSUE-029-delivery-attempts-stats-phantom-endpoints.md +++ /dev/null @@ -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 diff --git a/Issues/ISSUE-029-no-brute-force-protection-on-delivery-code-verification-endp.md b/Issues/ISSUE-029-no-brute-force-protection-on-delivery-code-verification-endp.md new file mode 100644 index 0000000..360fcc6 --- /dev/null +++ b/Issues/ISSUE-029-no-brute-force-protection-on-delivery-code-verification-endp.md @@ -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) diff --git a/Issues/ISSUE-030-confirm-delivery-no-auth-guard.md b/Issues/ISSUE-030-confirm-delivery-no-auth-guard.md deleted file mode 100644 index 1ad9293..0000000 --- a/Issues/ISSUE-030-confirm-delivery-no-auth-guard.md +++ /dev/null @@ -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) diff --git a/Issues/ISSUE-030-post-api-payment-payments-cleanup-pending-admin-check-is-ins.md b/Issues/ISSUE-030-post-api-payment-payments-cleanup-pending-admin-check-is-ins.md new file mode 100644 index 0000000..6a1f7bb --- /dev/null +++ b/Issues/ISSUE-030-post-api-payment-payments-cleanup-pending-admin-check-is-ins.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) diff --git a/Issues/ISSUE-031-points-missing-frontend-pages.md b/Issues/ISSUE-031-points-missing-frontend-pages.md deleted file mode 100644 index cb82915..0000000 --- a/Issues/ISSUE-031-points-missing-frontend-pages.md +++ /dev/null @@ -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) diff --git a/Issues/ISSUE-031-post-api-points-admin-add-admin-check-is-inside-handler-only.md b/Issues/ISSUE-031-post-api-points-admin-add-admin-check-is-inside-handler-only.md new file mode 100644 index 0000000..b9e2dd4 --- /dev/null +++ b/Issues/ISSUE-031-post-api-points-admin-add-admin-check-is-inside-handler-only.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) diff --git a/Issues/ISSUE-032-admin-delete-user-via-legacy-endpoint-performs-hard-delete-f.md b/Issues/ISSUE-032-admin-delete-user-via-legacy-endpoint-performs-hard-delete-f.md new file mode 100644 index 0000000..0d23533 --- /dev/null +++ b/Issues/ISSUE-032-admin-delete-user-via-legacy-endpoint-performs-hard-delete-f.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) diff --git a/Issues/ISSUE-032-shkeeper-release-refund-wrong-paths.md b/Issues/ISSUE-032-shkeeper-release-refund-wrong-paths.md deleted file mode 100644 index 514e74b..0000000 --- a/Issues/ISSUE-032-shkeeper-release-refund-wrong-paths.md +++ /dev/null @@ -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 diff --git a/Issues/ISSUE-033-admin-can-delete-other-admin-accounts-via-new-controller-leg.md b/Issues/ISSUE-033-admin-can-delete-other-admin-accounts-via-new-controller-leg.md new file mode 100644 index 0000000..50816fd --- /dev/null +++ b/Issues/ISSUE-033-admin-can-delete-other-admin-accounts-via-new-controller-leg.md @@ -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) diff --git a/Issues/ISSUE-033-seller-offer-history-route-missing.md b/Issues/ISSUE-033-seller-offer-history-route-missing.md deleted file mode 100644 index 485fc0d..0000000 --- a/Issues/ISSUE-033-seller-offer-history-route-missing.md +++ /dev/null @@ -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 diff --git a/Issues/ISSUE-034-all-dispute-socket-io-emit-blocks-are-todo-stubs-no-real-tim.md b/Issues/ISSUE-034-all-dispute-socket-io-emit-blocks-are-todo-stubs-no-real-tim.md new file mode 100644 index 0000000..9a171a9 --- /dev/null +++ b/Issues/ISSUE-034-all-dispute-socket-io-emit-blocks-are-todo-stubs-no-real-tim.md @@ -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) diff --git a/Issues/ISSUE-034-seller-offer-active-status-invalid.md b/Issues/ISSUE-034-seller-offer-active-status-invalid.md deleted file mode 100644 index e766372..0000000 --- a/Issues/ISSUE-034-seller-offer-active-status-invalid.md +++ /dev/null @@ -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 diff --git a/Issues/ISSUE-035-frontend-getpaymentstatus-and-confirmpayment-call-non-existe.md b/Issues/ISSUE-035-frontend-getpaymentstatus-and-confirmpayment-call-non-existe.md new file mode 100644 index 0000000..fd89830 --- /dev/null +++ b/Issues/ISSUE-035-frontend-getpaymentstatus-and-confirmpayment-call-non-existe.md @@ -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) diff --git a/Issues/ISSUE-035-payment-dispute-verify-button-404.md b/Issues/ISSUE-035-payment-dispute-verify-button-404.md deleted file mode 100644 index 87acec4..0000000 --- a/Issues/ISSUE-035-payment-dispute-verify-button-404.md +++ /dev/null @@ -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 diff --git a/Issues/ISSUE-036-cancelpayment-action-sends-delete-payment-id-but-no-delete-r.md b/Issues/ISSUE-036-cancelpayment-action-sends-delete-payment-id-but-no-delete-r.md new file mode 100644 index 0000000..f153c42 --- /dev/null +++ b/Issues/ISSUE-036-cancelpayment-action-sends-delete-payment-id-but-no-delete-r.md @@ -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) diff --git a/Issues/ISSUE-037-frontend-initiaterequestnetworkpayout-confirmrequestnetworkp.md b/Issues/ISSUE-037-frontend-initiaterequestnetworkpayout-confirmrequestnetworkp.md new file mode 100644 index 0000000..d6fd665 --- /dev/null +++ b/Issues/ISSUE-037-frontend-initiaterequestnetworkpayout-confirmrequestnetworkp.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) diff --git a/Issues/ISSUE-038-multiple-frontend-payment-stub-actions-call-non-existent-bac.md b/Issues/ISSUE-038-multiple-frontend-payment-stub-actions-call-non-existent-bac.md new file mode 100644 index 0000000..611767a --- /dev/null +++ b/Issues/ISSUE-038-multiple-frontend-payment-stub-actions-call-non-existent-bac.md @@ -0,0 +1,37 @@ +--- +issue: 038 +title: "Multiple frontend payment stub actions call non-existent backend endpoints: /payment/history, /payment/methods, /payment/validate, /payment/transactions, /payment/escrow/balance" +severity: major +domain: Payment +labels: [missing-feature, major, frontend, payment] +status: open +created: 2026-05-29 +source: Doc vs Code Audit 2026-05-29 +--- + +# 🟠 Multiple frontend payment stub actions call non-existent backend endpoints: /payment/history, /payment/methods, /payment/validate, /payment/transactions, /payment/escrow/balance + +**Severity:** major +**Domain:** Payment +**Labels:** missing-feature, major, frontend, payment + +## Description + +Frontend defines getPaymentHistory, getPaymentMethods, validatePayment, getTransactionHistory, getEscrowBalance — all calling endpoints that have no backend implementation. Any dashboard widget invoking these actions will receive 404 and silently fail or show empty state. + +## Current Behavior + +All five actions return 404 when called. + +## Expected Behavior + +Either implement backend routes for these endpoints or remove the stub actions. At minimum, verify no production UI calls them. + +## Affected Files + +- `frontend/src/actions/payment.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) diff --git a/Issues/ISSUE-039-reset-password-with-code-endpoint-has-no-password-complexity.md b/Issues/ISSUE-039-reset-password-with-code-endpoint-has-no-password-complexity.md new file mode 100644 index 0000000..89ca0ab --- /dev/null +++ b/Issues/ISSUE-039-reset-password-with-code-endpoint-has-no-password-complexity.md @@ -0,0 +1,36 @@ +--- +issue: 039 +title: "reset-password-with-code endpoint has no password complexity validation — accepts weak passwords rejected by token-based reset" +severity: major +domain: Authentication +labels: [security, bug, backend, major, auth] +status: open +created: 2026-05-29 +source: Doc vs Code Audit 2026-05-29 +--- + +# 🟠 reset-password-with-code endpoint has no password complexity validation — accepts weak passwords rejected by token-based reset + +**Severity:** major +**Domain:** Authentication +**Labels:** security, bug, backend, major, auth + +## Description + +POST /api/auth/reset-password-with-code has no validation middleware (authRoutes.ts:54-56). A new password of '123456' or 'aaaaaa' is accepted. POST /api/auth/reset-password uses passwordResetValidation enforcing uppercase+lowercase+digit. Inconsistent security between the two reset paths. + +## Current Behavior + +Code-based password reset accepts any non-empty password without complexity requirements. + +## Expected Behavior + +POST /api/auth/reset-password-with-code should apply the same passwordResetValidation middleware as the token-based reset. + +## Affected Files + +- `backend/src/routes/authRoutes.ts` + +## References + +- [Doc vs Code Audit Report](../09%20-%20Audits/Doc%20vs%20Code%20Audit%20Report%20-%202026-05-29.md) diff --git a/Issues/ISSUE-040-changepassword-action-has-no-ui-component-change-password-fe.md b/Issues/ISSUE-040-changepassword-action-has-no-ui-component-change-password-fe.md new file mode 100644 index 0000000..c9c5162 --- /dev/null +++ b/Issues/ISSUE-040-changepassword-action-has-no-ui-component-change-password-fe.md @@ -0,0 +1,36 @@ +--- +issue: 040 +title: "changePassword action has no UI component — change password feature is untestable from the UI" +severity: major +domain: Authentication +labels: [missing-feature, frontend, major] +status: open +created: 2026-05-29 +source: Doc vs Code Audit 2026-05-29 +--- + +# 🟠 changePassword action has no UI component — change password feature is untestable from the UI + +**Severity:** major +**Domain:** Authentication +**Labels:** missing-feature, frontend, major + +## Description + +The changePassword action is implemented in action.ts (line 263) and POST /api/auth/change-password exists on the backend, but no dashboard page or view component calls it. There is no 'Change Password' UI anywhere under /dashboard. + +## Current Behavior + +Users cannot change their password through the UI. The feature only exists at the API level. + +## Expected Behavior + +A change-password form should exist in the user dashboard settings. + +## Affected Files + +- `frontend/src/actions/account.ts` + +## References + +- [Doc vs Code Audit Report](../09%20-%20Audits/Doc%20vs%20Code%20Audit%20Report%20-%202026-05-29.md) diff --git a/Issues/ISSUE-041-frontend-searchpurchaserequests-calls-marketplace-purchase-r.md b/Issues/ISSUE-041-frontend-searchpurchaserequests-calls-marketplace-purchase-r.md new file mode 100644 index 0000000..b9c3ef6 --- /dev/null +++ b/Issues/ISSUE-041-frontend-searchpurchaserequests-calls-marketplace-purchase-r.md @@ -0,0 +1,37 @@ +--- +issue: 041 +title: "Frontend searchPurchaseRequests calls /marketplace/purchase-requests/search which does not exist in backend" +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 +--- + +# 🟠 Frontend searchPurchaseRequests calls /marketplace/purchase-requests/search which does not exist in backend + +**Severity:** major +**Domain:** Purchase Request +**Labels:** bug, frontend, major, broken-feature + +## Description + +Frontend defines searchPurchaseRequests pointing to /marketplace/purchase-requests/search. No /search sub-path is registered in backend. Search/filter should be handled via query parameters on the list endpoint GET /purchase-requests. + +## Current Behavior + +Calling searchPurchaseRequests produces a 404. + +## Expected Behavior + +searchPurchaseRequests should use GET /marketplace/purchase-requests with filter query parameters instead of a /search sub-path. + +## Affected Files + +- `frontend/src/actions/marketplace.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) diff --git a/Issues/ISSUE-042-frontend-getmarketplacestats-calls-marketplace-purchase-requ.md b/Issues/ISSUE-042-frontend-getmarketplacestats-calls-marketplace-purchase-requ.md new file mode 100644 index 0000000..e0e0182 --- /dev/null +++ b/Issues/ISSUE-042-frontend-getmarketplacestats-calls-marketplace-purchase-requ.md @@ -0,0 +1,37 @@ +--- +issue: 042 +title: "Frontend getMarketplaceStats calls /marketplace/purchase-requests/stats which has no backend handler" +severity: major +domain: Purchase Request +labels: [missing-feature, frontend, major] +status: open +created: 2026-05-29 +source: Doc vs Code Audit 2026-05-29 +--- + +# 🟠 Frontend getMarketplaceStats calls /marketplace/purchase-requests/stats which has no backend handler + +**Severity:** major +**Domain:** Purchase Request +**Labels:** missing-feature, frontend, major + +## Description + +Frontend defines getMarketplaceStats calling /marketplace/purchase-requests/stats. No /stats sub-path under purchase-requests is registered in backend. Any dashboard page calling this will receive a 404. + +## Current Behavior + +getMarketplaceStats always returns 404. + +## Expected Behavior + +Backend should implement GET /marketplace/purchase-requests/stats, or the frontend action should be removed and any UI using it should use an alternative. + +## Affected Files + +- `frontend/src/actions/marketplace.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) diff --git a/Issues/ISSUE-043-frontend-getdeliveryattempts-and-getdeliverystats-call-non-e.md b/Issues/ISSUE-043-frontend-getdeliveryattempts-and-getdeliverystats-call-non-e.md new file mode 100644 index 0000000..1714ffe --- /dev/null +++ b/Issues/ISSUE-043-frontend-getdeliveryattempts-and-getdeliverystats-call-non-e.md @@ -0,0 +1,36 @@ +--- +issue: 043 +title: "Frontend getDeliveryAttempts and getDeliveryStats call non-existent backend endpoints" +severity: major +domain: Delivery +labels: [missing-feature, frontend, major] +status: open +created: 2026-05-29 +source: Doc vs Code Audit 2026-05-29 +--- + +# 🟠 Frontend getDeliveryAttempts and getDeliveryStats call non-existent backend endpoints + +**Severity:** major +**Domain:** Delivery +**Labels:** missing-feature, frontend, major + +## Description + +getDeliveryAttempts calls /delivery-code/attempts and getDeliveryStats calls /delivery/stats. Neither path is registered in backend. Delivery attempt data exists in deliveryInfo.deliveryAttempts[] but no HTTP route exposes it. + +## Current Behavior + +Both actions return 404. Any UI calling them silently fails. + +## Expected Behavior + +Either implement the backend routes or remove the frontend actions and any UI depending on them. + +## Affected Files + +- `frontend/src/actions/delivery.ts` + +## References + +- [Doc vs Code Audit Report](../09%20-%20Audits/Doc%20vs%20Code%20Audit%20Report%20-%202026-05-29.md) diff --git a/Issues/ISSUE-044-post-api-marketplace-purchase-requests-id-final-approval-cre.md b/Issues/ISSUE-044-post-api-marketplace-purchase-requests-id-final-approval-cre.md new file mode 100644 index 0000000..a24f39b --- /dev/null +++ b/Issues/ISSUE-044-post-api-marketplace-purchase-requests-id-final-approval-cre.md @@ -0,0 +1,36 @@ +--- +issue: 044 +title: "POST /api/marketplace/purchase-requests/:id/final-approval creates dummy payment for testing if no real payment exists — testing backdoor in production code" +severity: major +domain: Purchase Request +labels: [security, bug, backend, major, escrow, bypass] +status: open +created: 2026-05-29 +source: Doc vs Code Audit 2026-05-29 +--- + +# 🟠 POST /api/marketplace/purchase-requests/:id/final-approval creates dummy payment for testing if no real payment exists — testing backdoor in production code + +**Severity:** major +**Domain:** Purchase Request +**Labels:** security, bug, backend, major, escrow, bypass + +## Description + +The final-approval endpoint in routes.ts (lines 1561-1592) contains logic that creates a dummy Payment document when no real payment is found and the request is in 'delivered' or 'delivery' status. This testing backdoor is undocumented and bypasses the payment integrity check in production. + +## Current Behavior + +Any request in delivered/delivery status can be final-approved without a real payment by triggering this code path, effectively releasing escrow for unpaid orders. + +## Expected Behavior + +The dummy payment creation should be guarded by NODE_ENV !== 'production' or removed entirely from production code. + +## 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) diff --git a/Issues/ISSUE-045-addparticipants-frontend-sends-participants-string-array-but.md b/Issues/ISSUE-045-addparticipants-frontend-sends-participants-string-array-but.md new file mode 100644 index 0000000..242f66d --- /dev/null +++ b/Issues/ISSUE-045-addparticipants-frontend-sends-participants-string-array-but.md @@ -0,0 +1,36 @@ +--- +issue: 045 +title: "addParticipants frontend sends { participants: string[] } array but backend expects { userId: string } single user" +severity: major +domain: Chat +labels: [bug, frontend, major, chat] +status: open +created: 2026-05-29 +source: Doc vs Code Audit 2026-05-29 +--- + +# 🟠 addParticipants frontend sends { participants: string[] } array but backend expects { userId: string } single user + +**Severity:** major +**Domain:** Chat +**Labels:** bug, frontend, major, chat + +## Description + +The frontend addParticipants action (chat.ts line 425) sends { participants: string[] } as the body. The API documents POST /api/chat/:id/participants with body { userId: string } — a single user. Backend expects a single userId, not an array. Bulk participant addition will be silently handled incorrectly. + +## Current Behavior + +addParticipants sends an array payload that the backend does not expect. Participant addition may fail or be ignored. + +## Expected Behavior + +Frontend should send { userId: string } and call the endpoint once per participant, or backend should be updated to accept an array. + +## 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) diff --git a/Issues/ISSUE-046-frontend-getsellerofferhistory-seller-offer-history-page-doe.md b/Issues/ISSUE-046-frontend-getsellerofferhistory-seller-offer-history-page-doe.md new file mode 100644 index 0000000..c6b3bc4 --- /dev/null +++ b/Issues/ISSUE-046-frontend-getsellerofferhistory-seller-offer-history-page-doe.md @@ -0,0 +1,37 @@ +--- +issue: 046 +title: "Frontend getSellerOfferHistory / seller offer history page does not exist — notification links to /dashboard/seller/marketplace/offers are broken" +severity: major +domain: Seller Offer +labels: [missing-feature, frontend, backend, major] +status: open +created: 2026-05-29 +source: Doc vs Code Audit 2026-05-29 +--- + +# 🟠 Frontend getSellerOfferHistory / seller offer history page does not exist — notification links to /dashboard/seller/marketplace/offers are broken + +**Severity:** major +**Domain:** Seller Offer +**Labels:** missing-feature, frontend, backend, major + +## Description + +No frontend page exists at /dashboard/seller/marketplace/offers. No getSellerOffers() action exists. The backend route GET /api/marketplace/offers/seller/:sellerId also does not exist (getOffersBySeller() service method is dead code via HTTP). Backend notification actionUrls pointing to this path produce broken links. + +## Current Behavior + +Notification links to the seller offer history are broken. Sellers have no way to view their offer history. + +## Expected Behavior + +A seller offer history page should exist at /dashboard/seller/marketplace/offers, backed by a proper backend list endpoint for the seller's own offers. + +## Affected Files + +- `backend/src/routes/routes.ts` +- `backend/src/services/sellerOfferService.ts` + +## References + +- [Doc vs Code Audit Report](../09%20-%20Audits/Doc%20vs%20Code%20Audit%20Report%20-%202026-05-29.md) diff --git a/Issues/ISSUE-047-frontend-cron-management-and-per-id-token-sweep-endpoints-fo.md b/Issues/ISSUE-047-frontend-cron-management-and-per-id-token-sweep-endpoints-fo.md new file mode 100644 index 0000000..6174210 --- /dev/null +++ b/Issues/ISSUE-047-frontend-cron-management-and-per-id-token-sweep-endpoints-fo.md @@ -0,0 +1,36 @@ +--- +issue: 047 +title: "Frontend cron management and per-id token sweep endpoints for derived-destinations are not in backend inventory" +severity: major +domain: Admin +labels: [missing-feature, backend, major, admin] +status: open +created: 2026-05-29 +source: Doc vs Code Audit 2026-05-29 +--- + +# 🟠 Frontend cron management and per-id token sweep endpoints for derived-destinations are not in backend inventory + +**Severity:** major +**Domain:** Admin +**Labels:** missing-feature, backend, major, admin + +## Description + +Frontend derived-destinations actions call GET /api/payment/derived-destinations/cron/status, POST /cron/start, POST /cron/stop, and POST /:id/sweep. Backend lists only the bulk sweep and /:id/sweep-native. The cron management and per-id token sweep may be unimplemented. The UI page calls getSweepCronStatus on mount. + +## Current Behavior + +Opening /dashboard/admin/derived-destinations likely triggers 404 on cron status request on mount. + +## Expected Behavior + +Backend should implement cron status, start, stop, and per-destination token sweep endpoints, or the frontend should be updated to match what is implemented. + +## Affected Files + +- `frontend/src/actions/derived-destinations.ts` + +## References + +- [Doc vs Code Audit Report](../09%20-%20Audits/Doc%20vs%20Code%20Audit%20Report%20-%202026-05-29.md) diff --git a/Issues/ISSUE-048-frontend-reloadnetworkregistry-and-probechain-call-backend-e.md b/Issues/ISSUE-048-frontend-reloadnetworkregistry-and-probechain-call-backend-e.md new file mode 100644 index 0000000..ad727e4 --- /dev/null +++ b/Issues/ISSUE-048-frontend-reloadnetworkregistry-and-probechain-call-backend-e.md @@ -0,0 +1,36 @@ +--- +issue: 048 +title: "Frontend reloadNetworkRegistry and probeChain call backend endpoints that do not exist" +severity: major +domain: Admin +labels: [missing-feature, backend, major, admin] +status: open +created: 2026-05-29 +source: Doc vs Code Audit 2026-05-29 +--- + +# 🟠 Frontend reloadNetworkRegistry and probeChain call backend endpoints that do not exist + +**Severity:** major +**Domain:** Admin +**Labels:** missing-feature, backend, major, admin + +## Description + +Frontend network-registry actions call POST /api/admin/rn/networks/reload and POST /api/admin/rn/networks/probe/:chainId. Backend only has GET /api/admin/rn/networks. Reload and probe buttons in the network registry UI silently fail. + +## Current Behavior + +Reload Registry and Probe Chain UI buttons return 404. + +## Expected Behavior + +Backend should implement reload and probe endpoints, or the frontend buttons should be removed/disabled. + +## Affected Files + +- `frontend/src/actions/network-registry.ts` + +## References + +- [Doc vs Code Audit Report](../09%20-%20Audits/Doc%20vs%20Code%20Audit%20Report%20-%202026-05-29.md) diff --git a/Issues/ISSUE-049-frontend-getconfirmationthresholdhistory-calls-get-api-admin.md b/Issues/ISSUE-049-frontend-getconfirmationthresholdhistory-calls-get-api-admin.md new file mode 100644 index 0000000..4efad19 --- /dev/null +++ b/Issues/ISSUE-049-frontend-getconfirmationthresholdhistory-calls-get-api-admin.md @@ -0,0 +1,36 @@ +--- +issue: 049 +title: "Frontend getConfirmationThresholdHistory calls GET /api/admin/settings/confirmation-thresholds/history which does not exist in backend" +severity: major +domain: Admin +labels: [missing-feature, backend, major, admin] +status: open +created: 2026-05-29 +source: Doc vs Code Audit 2026-05-29 +--- + +# 🟠 Frontend getConfirmationThresholdHistory calls GET /api/admin/settings/confirmation-thresholds/history which does not exist in backend + +**Severity:** major +**Domain:** Admin +**Labels:** missing-feature, backend, major, admin + +## Description + +Frontend confirmation-thresholds action defines getConfirmationThresholdHistory() calling /admin/settings/confirmation-thresholds/history. Backend only lists GET (current values) and PATCH per-chain. No history endpoint is registered. + +## Current Behavior + +getConfirmationThresholdHistory() returns 404. + +## Expected Behavior + +Backend should implement a history endpoint for threshold changes, or the frontend action should be removed. + +## Affected Files + +- `frontend/src/actions/confirmation-thresholds.ts` + +## References + +- [Doc vs Code Audit Report](../09%20-%20Audits/Doc%20vs%20Code%20Audit%20Report%20-%202026-05-29.md) diff --git a/Issues/ISSUE-050-points-referral-five-frontend-pages-do-not-exist-redemption-.md b/Issues/ISSUE-050-points-referral-five-frontend-pages-do-not-exist-redemption-.md new file mode 100644 index 0000000..d2a5833 --- /dev/null +++ b/Issues/ISSUE-050-points-referral-five-frontend-pages-do-not-exist-redemption-.md @@ -0,0 +1,36 @@ +--- +issue: 050 +title: "Points/Referral: five frontend pages do not exist — redemption, levels, referrals, transactions, admin-add all untestable via UI" +severity: major +domain: Points +labels: [missing-feature, frontend, major, points] +status: open +created: 2026-05-29 +source: Doc vs Code Audit 2026-05-29 +--- + +# 🟠 Points/Referral: five frontend pages do not exist — redemption, levels, referrals, transactions, admin-add all untestable via UI + +**Severity:** major +**Domain:** Points +**Labels:** missing-feature, frontend, major, points + +## Description + +The following routes 404: /dashboard/points/referrals, /dashboard/points/transactions, /dashboard/points/levels. redeemPoints is never called from any component. generateReferralCode is never called. adminAddPoints has no admin UI page. + +## Current Behavior + +All five features are untestable via UI. Backend endpoints exist but are inaccessible through the product. + +## Expected Behavior + +Frontend pages should be implemented for the above routes and wired to the corresponding backend endpoints. + +## Affected Files + +- `frontend/src/app/dashboard/points/` + +## References + +- [Doc vs Code Audit Report](../09%20-%20Audits/Doc%20vs%20Code%20Audit%20Report%20-%202026-05-29.md) diff --git a/Issues/ISSUE-051-self-referral-prevention-is-absent-users-can-refer-themselve.md b/Issues/ISSUE-051-self-referral-prevention-is-absent-users-can-refer-themselve.md new file mode 100644 index 0000000..6fbbbc7 --- /dev/null +++ b/Issues/ISSUE-051-self-referral-prevention-is-absent-users-can-refer-themselve.md @@ -0,0 +1,36 @@ +--- +issue: 051 +title: "Self-referral prevention is absent — users can refer themselves for points" +severity: major +domain: Points +labels: [security, bug, backend, major, points] +status: open +created: 2026-05-29 +source: Doc vs Code Audit 2026-05-29 +--- + +# 🟠 Self-referral prevention is absent — users can refer themselves for points + +**Severity:** major +**Domain:** Points +**Labels:** security, bug, backend, major, points + +## Description + +authController.ts referral attribution logic at lines 704 and 1132 has no self-referral check. Any user who obtains their own referral code and uses it during sign-up will receive a referral reward on their own account. + +## Current Behavior + +Self-referral is possible. Users can earn referral rewards by using their own code. + +## Expected Behavior + +Before applying referral attribution, verify that the referrer's userId !== the new user's userId. If they match, skip the reward. + +## Affected Files + +- `backend/src/controllers/authController.ts` + +## References + +- [Doc vs Code Audit Report](../09%20-%20Audits/Doc%20vs%20Code%20Audit%20Report%20-%202026-05-29.md) diff --git a/Issues/ISSUE-052-payment-completed-status-not-counted-in-successful-payments-stats.md b/Issues/ISSUE-052-payment-completed-status-not-counted-in-successful-payments-stats.md new file mode 100644 index 0000000..060629b --- /dev/null +++ b/Issues/ISSUE-052-payment-completed-status-not-counted-in-successful-payments-stats.md @@ -0,0 +1,36 @@ +--- +issue: 052 +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. Most SHKeeper/DePay payments follow the terminal path `pending → processing → completed`, so the bulk of successful payments are invisible in the success count. + +## Current Behavior + +Admin dashboard `successfulPayments` count excludes all `'completed'` payments. For a platform where SHKeeper is the primary provider, this count reads close to zero even after many successful payments. + +## Expected Behavior + +`successfulPayments` should count both `'confirmed'` and `'completed'` (the terminal success states), or the stat should be clearly documented as confirmed-only. + +## 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 diff --git a/Issues/ISSUE-053-axios-interceptor-only-handles-401-not-403-for-token-refresh.md b/Issues/ISSUE-053-axios-interceptor-only-handles-401-not-403-for-token-refresh.md new file mode 100644 index 0000000..134b117 --- /dev/null +++ b/Issues/ISSUE-053-axios-interceptor-only-handles-401-not-403-for-token-refresh.md @@ -0,0 +1,36 @@ +--- +issue: 053 +title: "Axios interceptor only retriggers token refresh for 401, not 403" +severity: major +domain: Authentication +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:** Authentication +**Labels:** frontend, bug + +## Description + +`frontend/src/lib/axios.ts` (line ~105) only triggers the token-refresh flow for `status === 401`. A `403` response (e.g. `EMAIL_NOT_VERIFIED`, blocked account, under-privileged action) is not intercepted — it propagates as an unhandled error and some components may not handle it gracefully. + +## Current Behavior + +Backend returns `403` → interceptor neither refreshes nor surfaces a meaningful state → error propagates raw to the calling component. + +## Expected Behavior + +A `403` should NOT trigger a token refresh (it is an authorization failure, not an expired token), but it should be surfaced clearly — e.g. redirect to the verify-email page for `EMAIL_NOT_VERIFIED`. The doc's claim that the interceptor "handles 401/403" should match the code. + +## Affected Files + +- `frontend/src/lib/axios.ts` — response interceptor (~line 105) + +## References + +- [Doc vs Code Audit Report](../09%20-%20Audits/Doc%20vs%20Code%20Audit%20Report%20-%202026-05-29.md) — Finding M1 diff --git a/Issues/ISSUE-054-login-rate-limiter-counts-all-attempts-not-only-failures.md b/Issues/ISSUE-054-login-rate-limiter-counts-all-attempts-not-only-failures.md new file mode 100644 index 0000000..ec119da --- /dev/null +++ b/Issues/ISSUE-054-login-rate-limiter-counts-all-attempts-not-only-failures.md @@ -0,0 +1,37 @@ +--- +issue: 054 +title: "Login rate limiter counts all attempts (not just failures) — users locked out after correct logins" +severity: major +domain: Authentication +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 locked out after correct logins + +**Severity:** major +**Domain:** Authentication +**Labels:** backend, bug + +## Description + +`rateLimitService.checkLoginAttempts()` calls `checkLimit()` → `redisService.incr`, incrementing the counter on **every** login invocation, before password comparison. The counter only resets after a fully successful login. So 5 total attempts within 15 min (any mix of correct/incorrect passwords) triggers the lockout — not 5 failures as the docs imply. + +## Current Behavior + +5 total login attempts within 15 minutes → `429 TOO_MANY_ATTEMPTS`, even if some attempts used the correct password. + +## Expected Behavior + +The counter should increment only on a **failed** password comparison, not on every attempt. Otherwise document the actual behaviour so UX warns users appropriately. + +## Affected Files + +- `backend/src/services/auth/rateLimitService.ts` — `checkLoginAttempts` / `checkLimit` +- `backend/src/controllers/authController.ts` — move the increment to after password comparison + +## References + +- [Doc vs Code Audit Report](../09%20-%20Audits/Doc%20vs%20Code%20Audit%20Report%20-%202026-05-29.md) — Finding M3 diff --git a/Issues/Issues Index.md b/Issues/Issues Index.md index fe56363..40e6328 100644 --- a/Issues/Issues Index.md +++ b/Issues/Issues Index.md @@ -1,59 +1,70 @@ # Issues Index -> Generated from Doc vs Code Audit — 2026-05-29 -> **35 open issues** | 🔴 14 critical · 🟠 19 major · 🟡 2 minor +> Generated from Doc vs Code Audit — 2026-05-29 · last reconciled 2026-05-29 +> **53 open issues** | 🔴 14 critical · 🟠 39 major · 🟡 0 minor · ⚪ 1 invalid (stale audit) ## 🔴 Critical -- [[ISSUE-001-dispute-status-no-role-guard|PATCH /api/disputes/:id/status no role guard — privilege escalation]] — `dispute` · security -- [[ISSUE-002-dispute-resolve-no-role-guard|POST /api/disputes/:id/resolve no role guard — any user can resolve + ban sellers]] — `dispute` · security -- [[ISSUE-003-dispute-route-shadowing|Route shadowing: two dispute routers at /api/disputes — wrong handler fires]] — `dispute` -- [[ISSUE-004-payment-endpoints-no-auth|fetch-tx, auto-fetch-missing, debug payment endpoints have no authentication]] — `payment` · security -- [[ISSUE-005-scanner-status-no-auth|GET /api/admin/scanner/status has no authentication]] — `admin` · security -- [[ISSUE-006-delete-account-wrong-endpoint|Frontend deleteAccount calls DELETE /user/profile — endpoint doesn't exist]] — `auth` -- [[ISSUE-007-sim-bypass-no-env-guard|SIM_ transaction bypass active in production — no NODE_ENV guard]] — `payment` · security -- [[ISSUE-008-chat-file-upload-wrong-endpoint|sendFileMessage posts to wrong endpoint — chat file uploads always fail]] — `chat` -- [[ISSUE-010-admin-user-status-wrong-values-and-verb|Admin user status/role broken: wrong HTTP verb + wrong status values]] — `admin` -- [[ISSUE-016-payment-provider-routing-always-request-network|createProviderPaymentIntent always routes to request-network — SHKeeper broken]] — `payment` -- [[ISSUE-018-trezor-no-frontend-implementation|Trezor Safekeeping has zero frontend implementation]] — `trezor` -- [[ISSUE-020-dispute-assign-no-role-guard|POST /api/disputes/:id/assign no role guard — any user can self-assign mediator]] — `dispute` · security -- [[ISSUE-030-confirm-delivery-no-auth-guard|PATCH /confirm-delivery no ownership check — any user can confirm delivery]] — `delivery` · security -- [[ISSUE-035-payment-dispute-verify-button-404|Dispute 'Verify' button always 404s — getPaymentStatus hits non-existent endpoint]] — `payment` +- [[ISSUE-001-patch-api-disputes-id-status-and-post-api-disputes-id-resolv|PATCH /api/disputes/:id/status and POST /api/disputes/:id/resolve have no role guard — privilege escalation]] — `Dispute` +- [[ISSUE-002-post-api-disputes-id-assign-has-no-role-guard-any-user-can-s|POST /api/disputes/:id/assign has no role guard — any user can self-assign as admin]] — `Dispute` +- [[ISSUE-003-route-shadowing-post-api-disputes-purchaserequestid-resolve-|Route shadowing: POST /api/disputes/:purchaseRequestId/resolve matches dashboard router first and executes wrong handler]] — `Dispute` +- [[ISSUE-004-post-api-disputes-id-resolve-dashboard-does-not-trigger-escr|POST /api/disputes/:id/resolve (dashboard) does not trigger escrow release — only updates Dispute model]] — `Dispute` +- [[ISSUE-005-post-api-payment-payments-id-fetch-tx-post-api-payment-payme|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]] — `Payment` +- [[ISSUE-006-get-api-admin-scanner-status-has-no-authentication-middlewar|GET /api/admin/scanner/status has no authentication middleware despite /api/admin/ prefix]] — `Admin` +- [[ISSUE-007-frontend-deleteaccount-action-calls-delete-user-profile-whic|Frontend deleteAccount action calls DELETE /user/profile which has no backend route — account deletion is broken]] — `Authentication` +- [[ISSUE-008-sendfilemessage-posts-to-wrong-endpoint-file-uploads-silentl|sendFileMessage posts to wrong endpoint — file uploads silently fail or corrupt text-message handler]] — `Chat` +- [[ISSUE-009-archiveconversation-sends-put-but-backend-only-accepts-patch|archiveConversation sends PUT but backend only accepts PATCH — all archive attempts fail]] — `Chat` +- [[ISSUE-010-frontend-admin-updateuserstatus-and-updateuserrole-use-put-b|Frontend admin updateUserStatus and updateUserRole use PUT but backend only accepts PATCH]] — `User Management` +- [[ISSUE-011-frontend-updateuserstatus-sends-inactive-pending-status-valu|Frontend updateUserStatus sends 'inactive'/'pending' status values that backend does not accept]] — `User Management` +- [[ISSUE-013-createproviderpaymentintent-always-routes-to-request-network|createProviderPaymentIntent always routes to request-network/intents regardless of provider argument]] — `Payment` +- [[ISSUE-014-paymentprovider-typescript-type-excludes-shkeeper-and-decent|PaymentProvider TypeScript type excludes 'shkeeper' and 'decentralized' causing UI fallthrough for main payment providers]] — `Payment` +- [[ISSUE-015-simulated-transaction-sim-bypass-has-no-environment-guard-ca|Simulated transaction SIM_ bypass has no environment guard — can fire in production on wallet connection failure]] — `Payment` ## 🟠 Major -- [[ISSUE-009-archive-chat-wrong-method|archiveConversation uses PUT but backend only accepts PATCH]] — `chat` -- [[ISSUE-011-update-purchase-request-put-vs-patch|updatePurchaseRequest sends PUT but backend only accepts PATCH]] — `purchase-request` -- [[ISSUE-012-update-offer-put-vs-patch|updateOffer sends PUT but backend registers PATCH]] — `seller-offer` -- [[ISSUE-013-select-offer-no-status-filter-corrupts-withdrawn|select-offer cascade overwrites withdrawn offers — missing status filter]] — `seller-offer` · data-integrity -- [[ISSUE-014-select-offer-no-seller-notifications|select-offer sends no per-seller notifications to winning/losing sellers]] — `seller-offer` -- [[ISSUE-015-seller-offer-withdraw-no-http-route|Seller offer withdraw has no HTTP route — withdrawOffer() is dead code]] — `seller-offer` -- [[ISSUE-017-payment-provider-type-missing-values|PaymentProvider TypeScript type missing 'shkeeper' and 'decentralized']] — `payment` -- [[ISSUE-019-rn-payout-release-refund-not-implemented|Request Network admin payout/release/refund sub-routes do not exist]] — `payment` -- [[ISSUE-021-axios-interceptor-403-not-handled|Axios interceptor only retriggers token refresh for 401, not 403]] — `auth` -- [[ISSUE-022-rate-limit-counts-all-attempts|Login rate limiter counts all attempts — users locked out after correct logins]] — `auth` -- [[ISSUE-023-change-password-no-ui|changePassword action exists but no dashboard UI page]] — `auth` -- [[ISSUE-024-reset-password-with-code-no-complexity-check|POST /api/auth/reset-password-with-code accepts weak passwords]] — `auth` · security -- [[ISSUE-025-dispute-socket-events-all-stubs|All dispute socket events are TODO stubs — no real-time updates]] — `dispute` -- [[ISSUE-026-payment-completed-not-counted-in-stats|'completed' payment not counted in successfulPayments — admin dashboard undercounts]] — `payment` -- [[ISSUE-027-get-notification-by-id-broken|GET /api/notifications/:id always 404s for non-latest notifications]] — `notification` -- [[ISSUE-028-payment-export-no-admin-guard|GET /api/payment/export has no admin guard — any user can export payments]] — `payment` · security -- [[ISSUE-029-delivery-attempts-stats-phantom-endpoints|Frontend delivery actions regenerate/attempts/stats hit non-existent endpoints]] — `delivery` -- [[ISSUE-031-points-missing-frontend-pages|Points/referral missing 5 frontend pages — redemption, levels, referrals, transactions, admin]] — `points` -- [[ISSUE-032-shkeeper-release-refund-wrong-paths|SHKeeper release/refund doc paths include erroneous /shkeeper/ segment]] — `payment` -- [[ISSUE-033-seller-offer-history-route-missing|GET seller offer history has no HTTP route — getOffersBySeller() is dead code]] — `seller-offer` -- [[ISSUE-034-seller-offer-active-status-invalid|SellerOffer 'active' status invalid — saves throw ValidationError]] — `seller-offer` +- [[ISSUE-016-updatepurchaserequest-uses-put-but-backend-only-registers-pa|updatePurchaseRequest uses PUT but backend only registers PATCH — all purchase request edits fail]] — `Purchase Request` +- [[ISSUE-017-updateoffer-uses-put-marketplace-offers-id-but-backend-regis|updateOffer uses PUT /marketplace/offers/:id but backend registers PATCH /offers/:id — offer edits fail]] — `Seller Offer` +- [[ISSUE-018-select-offer-updatemany-has-no-status-filter-overwrites-with|select-offer updateMany has no status filter — overwrites withdrawn/rejected offers back to 'rejected' corrupting status history]] — `Seller Offer` +- [[ISSUE-019-selleroffer-status-active-does-not-exist-in-schema-enum-but-|SellerOffer.status 'active' does not exist in schema enum but is referenced in docs and code comments]] — `Seller Offer` +- [[ISSUE-020-select-offer-does-not-send-per-seller-socket-events-or-notif|select-offer does not send per-seller socket events or notifications to winning or losing sellers]] — `Seller Offer` +- [[ISSUE-021-post-api-marketplace-offers-id-withdraw-http-route-does-not-|POST /api/marketplace/offers/:id/withdraw HTTP route does not exist — seller withdraw is dead code]] — `Seller Offer` +- [[ISSUE-022-get-api-payment-payments-id-debug-has-no-authentication-full|GET /api/payment/payments/:id/debug has no authentication — full payment data exposed without credentials]] — `Payment` +- [[ISSUE-023-get-api-payment-export-has-no-admin-role-guard-at-route-leve|GET /api/payment/export has no admin role guard at route level — any authenticated user can export all payment data]] — `Payment` +- [[ISSUE-024-get-api-payment-stats-has-no-admin-role-guard-any-authentica|GET /api/payment/stats has no admin role guard — any authenticated user can read aggregate payment stats]] — `Payment` +- [[ISSUE-025-get-api-disputes-statistics-has-no-admin-role-guard-any-auth|GET /api/disputes/statistics has no admin role guard — any authenticated user can access aggregate dispute KPIs]] — `Dispute` +- [[ISSUE-026-get-notifications-id-only-returns-user-s-most-recent-notific|GET /notifications/:id only returns user's most-recent notification — all others return 404 erroneously]] — `Notification` +- [[ISSUE-027-confirm-delivery-endpoint-has-no-ownership-check-any-authent|confirm-delivery endpoint has no ownership check — any authenticated user can confirm delivery on any request]] — `Delivery` +- [[ISSUE-028-delivery-code-generated-socket-event-broadcasts-raw-6-digit-|delivery-code-generated socket event broadcasts raw 6-digit code to entire request room including seller]] — `Delivery` +- [[ISSUE-029-no-brute-force-protection-on-delivery-code-verification-endp|No brute-force protection on delivery code verification endpoint — 900,000 combinations are enumerable]] — `Delivery` +- [[ISSUE-030-post-api-payment-payments-cleanup-pending-admin-check-is-ins|POST /api/payment/payments/cleanup-pending admin check is inside handler only — no middleware-level enforcement]] — `Admin` +- [[ISSUE-031-post-api-points-admin-add-admin-check-is-inside-handler-only|POST /api/points/admin/add admin check is inside handler only — no middleware-level enforcement]] — `Admin` +- [[ISSUE-032-admin-delete-user-via-legacy-endpoint-performs-hard-delete-f|Admin delete user via legacy endpoint performs hard delete (findByIdAndDelete) instead of soft delete]] — `User Management` +- [[ISSUE-033-admin-can-delete-other-admin-accounts-via-new-controller-leg|Admin can delete other admin accounts via new controller — legacy admin-on-admin protection does not apply]] — `User Management` +- [[ISSUE-034-all-dispute-socket-io-emit-blocks-are-todo-stubs-no-real-tim|All dispute socket.io emit blocks are TODO stubs — no real-time updates fire for any dispute event]] — `Dispute` +- [[ISSUE-035-frontend-getpaymentstatus-and-confirmpayment-call-non-existe|Frontend getPaymentStatus and confirmPayment call non-existent endpoints GET /payment/:id/status and POST /payment/:id/confirm]] — `Payment` +- [[ISSUE-036-cancelpayment-action-sends-delete-payment-id-but-no-delete-r|cancelPayment action sends DELETE /payment/:id but no DELETE route exists on any payment endpoint]] — `Payment` +- [[ISSUE-037-frontend-initiaterequestnetworkpayout-confirmrequestnetworkp|Frontend initiateRequestNetworkPayout, confirmRequestNetworkPayout, confirmRequestNetworkRelease, confirmRequestNetworkRefund call non-existent backend routes]] — `Payment` +- [[ISSUE-038-multiple-frontend-payment-stub-actions-call-non-existent-bac|Multiple frontend payment stub actions call non-existent backend endpoints: /payment/history, /payment/methods, /payment/validate, /payment/transactions, /payment/escrow/balance]] — `Payment` +- [[ISSUE-039-reset-password-with-code-endpoint-has-no-password-complexity|reset-password-with-code endpoint has no password complexity validation — accepts weak passwords rejected by token-based reset]] — `Authentication` +- [[ISSUE-040-changepassword-action-has-no-ui-component-change-password-fe|changePassword action has no UI component — change password feature is untestable from the UI]] — `Authentication` +- [[ISSUE-041-frontend-searchpurchaserequests-calls-marketplace-purchase-r|Frontend searchPurchaseRequests calls /marketplace/purchase-requests/search which does not exist in backend]] — `Purchase Request` +- [[ISSUE-042-frontend-getmarketplacestats-calls-marketplace-purchase-requ|Frontend getMarketplaceStats calls /marketplace/purchase-requests/stats which has no backend handler]] — `Purchase Request` +- [[ISSUE-043-frontend-getdeliveryattempts-and-getdeliverystats-call-non-e|Frontend getDeliveryAttempts and getDeliveryStats call non-existent backend endpoints]] — `Delivery` +- [[ISSUE-044-post-api-marketplace-purchase-requests-id-final-approval-cre|POST /api/marketplace/purchase-requests/:id/final-approval creates dummy payment for testing if no real payment exists — testing backdoor in production code]] — `Purchase Request` +- [[ISSUE-045-addparticipants-frontend-sends-participants-string-array-but|addParticipants frontend sends { participants: string[] } array but backend expects { userId: string } single user]] — `Chat` +- [[ISSUE-046-frontend-getsellerofferhistory-seller-offer-history-page-doe|Frontend getSellerOfferHistory / seller offer history page does not exist — notification links to /dashboard/seller/marketplace/offers are broken]] — `Seller Offer` +- [[ISSUE-047-frontend-cron-management-and-per-id-token-sweep-endpoints-fo|Frontend cron management and per-id token sweep endpoints for derived-destinations are not in backend inventory]] — `Admin` +- [[ISSUE-048-frontend-reloadnetworkregistry-and-probechain-call-backend-e|Frontend reloadNetworkRegistry and probeChain call backend endpoints that do not exist]] — `Admin` +- [[ISSUE-049-frontend-getconfirmationthresholdhistory-calls-get-api-admin|Frontend getConfirmationThresholdHistory calls GET /api/admin/settings/confirmation-thresholds/history which does not exist in backend]] — `Admin` +- [[ISSUE-050-points-referral-five-frontend-pages-do-not-exist-redemption-|Points/Referral: five frontend pages do not exist — redemption, levels, referrals, transactions, admin-add all untestable via UI]] — `Points` +- [[ISSUE-051-self-referral-prevention-is-absent-users-can-refer-themselve|Self-referral prevention is absent — users can refer themselves for points]] — `Points` +- [[ISSUE-052-payment-completed-status-not-counted-in-successful-payments-stats|'completed' payment status not counted in successfulPayments stats — admin dashboard undercounts]] — `Payment` +- [[ISSUE-053-axios-interceptor-only-handles-401-not-403-for-token-refresh|Axios interceptor only retriggers token refresh for 401, not 403]] — `Authentication` +- [[ISSUE-054-login-rate-limiter-counts-all-attempts-not-only-failures|Login rate limiter counts all attempts (not just failures) — users locked out after correct logins]] — `Authentication` -## Security Issues Summary +## ⚪ Invalid / Superseded (audit was stale vs current code) + +- [[ISSUE-012-trezor-safekeeping-zero-frontend-implementation-all-admin-re|Trezor Safekeeping "zero frontend" — INVALID: the frontend Trezor implementation exists in current code (TrezorSettingsView, trezorConnector, TrezorSignDialog, actions/trezor.ts). Audit findings C31/C32 were from an older snapshot.]] — `Trezor` + +## 🟡 Minor -| # | Issue | Severity | -|---|---|---| -| 001 | Dispute status PATCH — no role guard (privilege escalation) | 🔴 Critical | -| 002 | Dispute resolve POST — no role guard (ban_seller without auth) | 🔴 Critical | -| 004 | Payment fetch-tx/auto-fetch/debug — no authentication | 🔴 Critical | -| 005 | Admin scanner status — no authentication | 🔴 Critical | -| 007 | SIM_ bypass active in production | 🔴 Critical | -| 020 | Dispute assign — no role guard | 🔴 Critical | -| 030 | confirm-delivery — no ownership check | 🔴 Critical | -| 024 | reset-password-with-code — no complexity validation | 🟠 Major | -| 028 | Payment export — no admin guard | 🟠 Major |