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.✅ Since backend
14c231e(v2.8.50):toggle-statusanddependenciesare 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 —updateUserStatusnow sendsPATCHwith{ 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:
{
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.