Remaining docs updated to match code (the docs that the first pass had not covered):
- Flows: Chat, Referral, Rating, Registration, Google OAuth, Negotiation, Payout,
Trezor Safekeeping — corrected endpoints, socket events, status enums, auth gaps
- API Reference: User API, Trezor API — admin route prefix/verb/status corrections,
added undocumented endpoints (ton-proof challenge, profile email verify,
GET /trezor/account, POST /trezor/verify-operation)
- Data Models: Chat, Notification, Payment, PointTransaction, User — corrected
enums (PaymentProvider, escrowState, PointTransaction.type, User.status),
90-day notification TTL, soft-delete semantics, wallet fields
Trezor "zero frontend" finding (audit C31/C32) corrected as STALE:
- Verified current code HAS a full frontend Trezor implementation (admin/trezor
page, TrezorSettingsView, trezorConnector via @trezor/connect-web,
TrezorSignDialog, actions/trezor.ts building the {message,signature} object)
- Fixed Trezor Safekeeping Flow doc (removed false "no frontend" warnings)
- Reclassified ISSUE-012 as invalid/superseded with explanation
Issue set reconciled to a single canonical numbering (ISSUE-001..054):
- Adopted the comprehensive 51-issue set (long-slug, fully indexed)
- Removed 35 superseded short-slug duplicates from the first pass
- Removed a duplicate ISSUE-046 file
- Added 3 issues the 51-set lacked: ISSUE-052 (completed-not-counted-in-stats),
ISSUE-053 (axios 401-only interceptor), ISSUE-054 (rate limiter counts all attempts)
- Regenerated Issues Index: 53 open (14 critical, 39 major) + 1 invalid
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
15 KiB
title, tags
| title | tags | |||
|---|---|---|---|---|
| User API |
|
User API
Last updated: 2026-05-29 — aligned with code (see Doc vs Code Audit Report)
Two routers are mounted for users:
/api/user/*- the new controller pattern inbackend/src/services/user/userControllerRoutes.tswired touserController./api/users/*- the legacy router inbackend/src/services/user/userRoutes.ts(kept for backward compatibility, primarily used by the admin console).
Address-book CRUD lives on its own service: /api/addresses/*. All endpoints require Bearer JWT unless noted. Source of truth model: User.
Profile (current user)
GET /api/user/profile
Description: Returns the caller's profile. Auth required: Bearer JWT Response 200:
{ "success": true, "data": { /* User without password / verification tokens */ } }
Source: userController.getCurrentUserProfile
PUT /api/user/profile
Description: Updates the caller's profile. Auth required: Bearer JWT Request body (whitelisted fields):
{
firstName?: string;
lastName?: string;
name?: string; // alias for profile.name
phone?: string;
bio?: string;
website?: string;
photoURL?: string; // also mirrored to profile.avatar
isPublic?: boolean;
address?: {
street?: string;
city?: string;
state?: string;
country?: string;
postalCode?: string;
};
preferences?: {
language?: "en" | "fa" | "ar";
currency?: "USD" | "EUR" | "IRR" | "AED";
notifications?: { email?: boolean; sms?: boolean; push?: boolean };
};
}
Response 200: Updated user.
GET /api/users/profile
Description: Legacy equivalent of the above. Returns the full sanitized user document. Auth required: Bearer JWT
GET /api/users/profile/:userId
Description: Public profile by id. If profile.isPublic === false and caller is not the owner or admin, only firstName, lastName, avatar, role are returned.
Auth required: Bearer JWT
Avatar upload
Avatar upload is handled by the File API:
POST /api/files/upload/avatar (multipart avatar) returns a URL that the caller then writes to profile.avatar via PUT /api/user/profile.
Wallet address
GET /api/user/wallet-address
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..." , // 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: Stores a verified wallet address. Supports both EVM and TON:
- EVM (
walletTypeomitted or not'ton'): the address must passethers.isAddress, and the body must includesignature+message. The server runsethers.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 optionaltonProofpayload is verified viaverifyTonProof; if valid,profile.walletProofVerifiedis set totrueandprofile.walletProofTimestampis stamped.
On success the server writes profile.walletAddress, profile.walletType ('evm' or 'ton'), profile.walletProvider, and profile.walletProofVerified.
Auth required: Bearer JWT
Request body:
{
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": { "user": { /* sanitized user */ }, "walletProofVerified": boolean } }
Errors:
400missing/invalid fields, malformed address, EVM signature mismatch, invalid TON proof404user 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:
{
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
Description: Returns the users the caller is allowed to chat with based on role:
buyer→ seesseller+adminseller→ seesbuyer+adminadmin→ sees everyone Auth required: Bearer JWT Response 200:{ "success": true, "data": { "contacts": [...], "count": N } }
GET /api/users/search?q=&role=
Description: Case-insensitive search across firstName/lastName/email. Returns up to 20 active users.
Auth required: Bearer JWT
Errors: 400 if q.length < 2.
GET /api/users?role=...&isActive=true&search=...&page=1&limit=50
Description: Paginated user directory (legacy, no admin gate). See pagination conventions in API Overview. Auth required: Bearer JWT
Admin: user management
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/*(seefrontend/src/lib/axios.ts, all paths underendpoints.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
updateUserStatusandupdateUserRolecalls (frontend/src/actions/user.ts) usePUT(PUT /api/users/admin/:id/status,PUT /api/users/admin/:id/role). The backend registers these asPATCHonly (both the legacy and new routers). The verbs do not match — treatPATCHas the authoritative backend verb; thePUTcalls will not route.⚠️ Note on status values (KNOWN BUG): The frontend
updateUserStatusTypeScript type is'active' | 'inactive' | 'pending'. The backendUser.statusenum is'active' | 'suspended' | 'deleted'. So:
'inactive'and'pending'are rejected/ignored by the backend (the new controller only appliesstatuswhen it is one ofactive/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
Description: Admin creates a user with a chosen role and verification state. Auth required: Bearer JWT (admin) Request body:
{
email: string;
password: string;
firstName: string;
lastName: string;
role?: "buyer" | "seller" | "admin"; // default "buyer"
isActive?: boolean; // default true
isVerified?: boolean; // default false
profile?: { /* free-form */ };
}
Response 201: { success, data: { user } }
Errors: 400 missing fields, 403 non-admin, 409 email exists.
DELETE /api/user/admin/:userId (new controller — SOFT delete)
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, 404 not found.
⚠️ 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).
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)
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:
{
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/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
Description: Paginated admin user list with filters.
Auth required: Bearer JWT (admin)
Query params: role, isActive, isVerified, search, page, limit, sortBy, sortOrder
Response 200: { success, data: { users, pagination, stats: { totalUsers, activeUsers, verifiedUsers, buyers, sellers, admins } } }
GET /api/user/admin/:userId/dependencies
Description: Returns a count of related entities (purchase requests, offers, payments) before a destructive admin operation. Auth required: Bearer JWT (admin)
GET /api/users/admin/stats
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
Description: Fetch a single user by id (admin view, includes preferences and last-login). Auth required: Bearer JWT (admin)
PUT /api/users/admin/:userId
Description: Mass update a user document (admin override). Auth required: Bearer JWT (admin)
PUT /api/users/admin/update/:email
Description: Same as above but keyed on email. Auth required: Bearer JWT (admin)
PATCH /api/users/admin/:userId/password
Description: Admin forces a new password. Wipes refreshTokens so all sessions are invalidated.
Auth required: Bearer JWT (admin)
Request body: { newPassword: string; reason?: string }
POST /api/users/admin/:userId/resend-verification
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.tsgenerates an 8-digit code (10000000 + Math.random() * 90000000), while the newuserController(used byPOST /api/user/profile/email/verifyand 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, model: Address.
GET /api/addresses
Description: List the caller's addresses.
Auth required: Bearer JWT
Response 200: { success: true, data: [Address, ...] }
POST /api/addresses
Description: Create a new address. First address auto-becomes primary. Auth required: Bearer JWT Request body:
{
fullName: string;
phone: string;
street: string;
city: string;
state?: string;
country: string;
postalCode?: string;
isPrimary?: boolean;
notes?: string;
}
PUT /api/addresses/:addressId
Description: Update an address.
Auth required: Bearer JWT
Errors: 404 not owned by user.
DELETE /api/addresses/:addressId
Description: Delete an address. If it was the primary, another address is promoted. Auth required: Bearer JWT
PATCH /api/addresses/:addressId/primary
Description: Promote an address to primary; demotes the previous primary. Auth required: Bearer JWT
See Address for the schema, and Marketplace API for how purchase requests reference an address.