--- 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=&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. > > ✅ **Since backend `14c231e` (v2.8.50):** `toggle-status` and `dependencies` are also reachable under the plural prefix (`/api/users/admin/:userId/toggle-status`, `/api/users/admin/:userId/dependencies`) — the legacy router delegates them to the new controller, so the frontend's plural calls now route. > > ✅ **Fixed (frontend `d7a2a86`, v2.8.50):** the old PUT-verb and `{status: 'inactive'}` mismatches are gone — `updateUserStatus` now sends `PATCH` with `{ isActive: boolean }`, which is what the legacy plural status route reads. ### 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 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:** ```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.