343 lines
15 KiB
Markdown
343 lines
15 KiB
Markdown
---
|
||
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.
|
||
>
|
||
> ✅ **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.
|