Files
nick-doc/03 - API Reference/User API.md
Siavash Sameni 7a616744f4 docs: complete code-reality alignment for remaining docs + reconcile issue set
Remaining docs updated to match code (the docs that the first pass had not covered):
- Flows: Chat, Referral, Rating, Registration, Google OAuth, Negotiation, Payout,
  Trezor Safekeeping — corrected endpoints, socket events, status enums, auth gaps
- API Reference: User API, Trezor API — admin route prefix/verb/status corrections,
  added undocumented endpoints (ton-proof challenge, profile email verify,
  GET /trezor/account, POST /trezor/verify-operation)
- Data Models: Chat, Notification, Payment, PointTransaction, User — corrected
  enums (PaymentProvider, escrowState, PointTransaction.type, User.status),
  90-day notification TTL, soft-delete semantics, wallet fields

Trezor "zero frontend" finding (audit C31/C32) corrected as STALE:
- Verified current code HAS a full frontend Trezor implementation (admin/trezor
  page, TrezorSettingsView, trezorConnector via @trezor/connect-web,
  TrezorSignDialog, actions/trezor.ts building the {message,signature} object)
- Fixed Trezor Safekeeping Flow doc (removed false "no frontend" warnings)
- Reclassified ISSUE-012 as invalid/superseded with explanation

Issue set reconciled to a single canonical numbering (ISSUE-001..054):
- Adopted the comprehensive 51-issue set (long-slug, fully indexed)
- Removed 35 superseded short-slug duplicates from the first pass
- Removed a duplicate ISSUE-046 file
- Added 3 issues the 51-set lacked: ISSUE-052 (completed-not-counted-in-stats),
  ISSUE-053 (axios 401-only interceptor), ISSUE-054 (rate limiter counts all attempts)
- Regenerated Issues Index: 53 open (14 critical, 39 major) + 1 invalid

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 15:15:02 +04:00

345 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: User API
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`.
- `/api/users/*` - the legacy router in [`backend/src/services/user/userRoutes.ts`](../../backend/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/*`](../../backend/src/services/address/addressRoutes.ts). 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:**
```json
{ "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):**
```ts
{
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:**
```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:** 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; // 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:**
- `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
**Description:** Returns the users the caller is allowed to chat with based on role:
- `buyer` → sees `seller` + `admin`
- `seller` → sees `buyer` + `admin`
- `admin` → sees everyone
**Auth required:** Bearer JWT
**Response 200:** `{ "success": true, "data": { "contacts": [...], "count": N } }`
### GET /api/users/search?q=<term>&role=<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/*`** (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
**Description:** Admin creates a user with a chosen role and verification state.
**Auth required:** Bearer JWT (admin)
**Request body:**
```ts
{
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 controllers 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:**
```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/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.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]].
### 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:**
```ts
{
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.