Files
nick-doc/03 - API Reference/User API.md

15 KiB
Raw Permalink Blame History

title, tags
title tags
User API
api
user
reference

User API

Last updated: 2026-05-29 — aligned with code (see Doc vs Code Audit Report)

Two routers are mounted for users:

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 (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:

{
  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:

{
  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.

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.tsuserController). 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:

{
  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:

{
  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, 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.