Initial commit: nick docs
This commit is contained in:
118
03 - API Reference/AI API.md
Normal file
118
03 - API Reference/AI API.md
Normal file
@@ -0,0 +1,118 @@
|
||||
---
|
||||
title: AI API
|
||||
tags: [api, ai, reference]
|
||||
---
|
||||
|
||||
# AI API
|
||||
|
||||
Endpoints live under `/api/ai/*`. The router is [`backend/src/services/ai/aiRoutes.ts`](../../backend/src/services/ai/aiRoutes.ts) and delegates to [`aiController`](../../backend/src/services/ai/aiController.ts), which wraps the OpenAI SDK.
|
||||
|
||||
The AI service is intentionally generic — there are no resource-specific endpoints (no "generate product description" route). Instead, four general-purpose endpoints (`generate`, `analyze`, `translate`, `assist`) accept a `purpose` field that the controller maps to a system prompt template. The frontend uses these for:
|
||||
|
||||
- Product description generation (purpose: `description`)
|
||||
- Content moderation (purpose: `moderation` on `/analyze`)
|
||||
- Product recommendations (purpose: `recommend` on `/generate`)
|
||||
- In-app helper (`/assist`)
|
||||
|
||||
> Note: the router currently exposes these endpoints **without** `authenticateToken`. Front-end usage assumes the user is logged in. If you need to add auth, do it in the router. OpenAI cost-protection and rate-limiting are handled inside `aiController` via the Redis-backed `rateLimitService`.
|
||||
|
||||
## Endpoints
|
||||
|
||||
### POST /api/ai/generate
|
||||
|
||||
**Description:** Generate free-form text. Used for product descriptions, marketing copy, recommendation rationales.
|
||||
**Auth required:** No (intended for authenticated frontends)
|
||||
**Request body:**
|
||||
```ts
|
||||
{
|
||||
prompt: string;
|
||||
purpose?: "description" | "recommend" | "summary" | "generic"; // default "generic"
|
||||
context?: Record<string, unknown>; // injected into the system prompt
|
||||
maxTokens?: number; // default 512, capped server-side
|
||||
temperature?: number; // 0..2, default 0.7
|
||||
language?: "en" | "fa" | "ar"; // default "en"
|
||||
}
|
||||
```
|
||||
**Response 200:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"text": "...",
|
||||
"model": "gpt-4o-mini",
|
||||
"tokensUsed": { "prompt": 120, "completion": 340, "total": 460 }
|
||||
}
|
||||
}
|
||||
```
|
||||
**Errors:** `400` invalid params, `429` quota exceeded, `502` upstream OpenAI error.
|
||||
|
||||
### POST /api/ai/analyze
|
||||
|
||||
**Description:** Analyze a piece of text. Used for moderation (flag offensive content), sentiment, keyword extraction.
|
||||
**Auth required:** No
|
||||
**Request body:**
|
||||
```ts
|
||||
{
|
||||
text: string;
|
||||
purpose?: "moderation" | "sentiment" | "keywords" | "summary"; // default "moderation"
|
||||
language?: "en" | "fa" | "ar";
|
||||
}
|
||||
```
|
||||
**Response 200 (moderation example):**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"purpose": "moderation",
|
||||
"flagged": false,
|
||||
"categories": { "hate": 0.001, "harassment": 0.0 },
|
||||
"suggestion": null
|
||||
}
|
||||
}
|
||||
```
|
||||
**Response 200 (sentiment example):**
|
||||
```json
|
||||
{ "success": true, "data": { "purpose": "sentiment", "score": 0.62, "label": "positive" } }
|
||||
```
|
||||
|
||||
### POST /api/ai/translate
|
||||
|
||||
**Description:** Translate `text` between languages.
|
||||
**Auth required:** No
|
||||
**Request body:**
|
||||
```ts
|
||||
{
|
||||
text: string;
|
||||
from?: "en" | "fa" | "ar" | "auto"; // default "auto"
|
||||
to: "en" | "fa" | "ar";
|
||||
}
|
||||
```
|
||||
**Response 200:** `{ success, data: { translated: "...", detectedFrom: "en" } }`
|
||||
|
||||
### POST /api/ai/assist
|
||||
|
||||
**Description:** In-app conversational assistant. Maintains short-term context per call (the client passes prior turns explicitly).
|
||||
**Auth required:** No
|
||||
**Request body:**
|
||||
```ts
|
||||
{
|
||||
messages: Array<{ role: "user" | "assistant" | "system"; content: string }>;
|
||||
purpose?: "help" | "search" | "navigate"; // tunes the system prompt
|
||||
context?: { userId?: string; route?: string; locale?: string };
|
||||
}
|
||||
```
|
||||
**Response 200:** `{ success, data: { reply: { role: "assistant", content: "..." }, tokensUsed } }`
|
||||
|
||||
## Configuration
|
||||
|
||||
The OpenAI client is configured via env:
|
||||
|
||||
- `OPENAI_API_KEY` (required)
|
||||
- `OPENAI_MODEL` (default `gpt-4o-mini`)
|
||||
- `OPENAI_BASE_URL` (override for self-hosted proxies)
|
||||
|
||||
## Related
|
||||
|
||||
- [[AI Integration]]
|
||||
- [[Moderation Pipeline]]
|
||||
- [[Rate Limiting]]
|
||||
173
03 - API Reference/API Overview.md
Normal file
173
03 - API Reference/API Overview.md
Normal file
@@ -0,0 +1,173 @@
|
||||
---
|
||||
title: API Overview
|
||||
tags: [api, reference, overview]
|
||||
---
|
||||
|
||||
# API Overview
|
||||
|
||||
The AMN backend is an Express.js + TypeScript service that exposes a REST API plus a Socket.IO real-time channel. Every HTTP endpoint is mounted under `/api/<service>/...` in [`backend/src/app.ts`](../../backend/src/app.ts) and follows a consistent response envelope, authentication scheme, and error model.
|
||||
|
||||
This page is the entry point for the API. See the individual service pages for endpoint details:
|
||||
|
||||
- [[Authentication API]] - register/login/passkeys/Google OAuth
|
||||
- [[User API]] - profile, wallet, admin user management
|
||||
- [[Marketplace API]] - purchase requests, seller offers, templates, shop, reviews
|
||||
- [[Payment API]] - SHKeeper, Web3, DePay, payouts
|
||||
- [[Chat API]] - conversations and messages
|
||||
- [[Notification API]] - in-app notifications
|
||||
- [[Dispute API]] - dispute resolution
|
||||
- [[Blog API]] - blog posts (admin-managed)
|
||||
- [[Admin API]] - user management, data cleanup, manual ops
|
||||
- [[Points API]] - loyalty points, levels, referrals
|
||||
- [[AI API]] - OpenAI-backed text endpoints
|
||||
- [[File API]] - upload, delete, serve
|
||||
- [[Socket Events]] - real-time events
|
||||
- [[Error Codes]] - status mapping and error shape
|
||||
|
||||
## Base URLs
|
||||
|
||||
| Environment | Base URL |
|
||||
| --- | --- |
|
||||
| Local development | `http://localhost:5001/api` |
|
||||
| Staging / dev | `https://dev.amn.gg/api` |
|
||||
| Production | `https://amn.gg/api` |
|
||||
|
||||
The base port is set via `PORT` env var; in `development` it defaults to `5001`. CORS is restricted to `process.env.FRONTEND_URL` and credentials are allowed (`cors({ origin, credentials: true })` in `app.ts`).
|
||||
|
||||
Health check (not under `/api`): `GET /health` → `{ success, message, timestamp, environment, version }`.
|
||||
|
||||
API discovery endpoint: `GET /api` → returns a map of available service prefixes.
|
||||
|
||||
## Versioning
|
||||
|
||||
The API is currently un-versioned (there is no `/v1` in paths). Breaking changes are communicated via release notes in the repository. The version string is exposed on `/health` and is sourced from `package.json` (`process.env.npm_package_version`). At the time of writing the deployed version is in the `4.x` line.
|
||||
|
||||
## Authentication
|
||||
|
||||
All protected endpoints use a stateless JWT in the standard HTTP Authorization header:
|
||||
|
||||
```
|
||||
Authorization: Bearer <accessToken>
|
||||
```
|
||||
|
||||
The token is verified by `authenticateToken` in [`backend/src/shared/middleware/auth.ts`](../../backend/src/shared/middleware/auth.ts) using `config.jwtSecret`. The decoded payload is normalised into `req.user = { id, email, role }` regardless of which key carried it (`id`, `userId`, `_id`, `sub`).
|
||||
|
||||
Tokens are issued by `POST /api/auth/login`, `POST /api/auth/google/signin`, `POST /api/auth/google/signup`, `POST /api/auth/passkey/authenticate`, and `POST /api/auth/refresh-token`. Refresh tokens are stored on the [[User]] document (`refreshTokens` array) so an admin password change wipes them and forces re-login.
|
||||
|
||||
Role-based access uses `authorizeRoles('admin', ...)` after `authenticateToken`. Three roles exist: `buyer`, `seller`, `admin`.
|
||||
|
||||
WebAuthn / Passkey flows live under `/api/auth/passkey/*` and exchange a challenge for an assertion; on success a regular JWT pair is returned (`tokens.accessToken`, `tokens.refreshToken`). See [[Authentication Flow]] for the full lifecycle.
|
||||
|
||||
## Standard response envelope
|
||||
|
||||
The canonical helper is `ResponseHandler` in [`backend/src/shared/utils/response-handler.ts`](../../backend/src/shared/utils/response-handler.ts). Successful responses look like:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Success",
|
||||
"data": { /* payload */ },
|
||||
"statusCode": 200,
|
||||
"timestamp": "2026-05-23T10:00:00.000Z",
|
||||
"path": "/api/marketplace/purchase-requests",
|
||||
"method": "GET",
|
||||
"meta": { /* optional */ }
|
||||
}
|
||||
```
|
||||
|
||||
Paginated responses add a `pagination` block:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": [ /* items */ ],
|
||||
"pagination": {
|
||||
"page": 1,
|
||||
"limit": 20,
|
||||
"total": 137,
|
||||
"totalPages": 7,
|
||||
"hasMore": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Error responses use the same envelope with `success: false`:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"message": "Validation failed",
|
||||
"error": "Validation Error",
|
||||
"statusCode": 400,
|
||||
"timestamp": "2026-05-23T10:00:00.000Z",
|
||||
"path": "/api/auth/register",
|
||||
"method": "POST",
|
||||
"data": [ /* validation details */ ]
|
||||
}
|
||||
```
|
||||
|
||||
Caveat: not every endpoint uses `ResponseHandler`. Legacy routes (mainly under `/api/users`, `/api/marketplace` legacy, `/api/payment/decentralized`, parts of `/api/payment/shkeeper`) return ad-hoc shapes such as `{ error: string }` on failure or bare objects on success. When in doubt consult the per-endpoint response section in the service pages. The error middleware in [`shared/middleware/errorHandler.ts`](../../backend/src/shared/middleware/errorHandler.ts) covers uncaught exceptions and emits `{ success: false, error, statusCode, stack? }`.
|
||||
|
||||
## Pagination conventions
|
||||
|
||||
Most list endpoints accept `?page=<n>&limit=<n>` query params:
|
||||
|
||||
- `page` defaults to `1`, 1-based.
|
||||
- `limit` defaults vary (20 for notifications/marketplace, 50 for user lists) and is capped (e.g. 100 for templates).
|
||||
- Server computes `total`, `totalPages`, and `hasMore` and returns them under `pagination`.
|
||||
- Some endpoints also accept `offset` (e.g. payment lists) as a raw skip count.
|
||||
|
||||
Sort parameters: list endpoints commonly accept `sortBy` (default `createdAt`) and `sortOrder` (`asc` | `desc`, default `desc`).
|
||||
|
||||
## Common query params
|
||||
|
||||
| Parameter | Used by | Meaning |
|
||||
| --- | --- | --- |
|
||||
| `search` | users, templates, posts | Case-insensitive regex over title/name/email fields |
|
||||
| `status` | payments, requests, disputes, notifications | Filter by entity state |
|
||||
| `role` | users, contacts | Filter by `buyer`/`seller`/`admin` |
|
||||
| `isActive` | users, templates | Boolean filter |
|
||||
| `categoryId` | templates, requests | MongoId category filter |
|
||||
| `unreadOnly` | notifications | Only unread items |
|
||||
| `format` | payment export | `json` (default) or `csv` |
|
||||
|
||||
## Rate limiting
|
||||
|
||||
Rate limiting is currently **disabled** in the deployed code (`app.ts` logs `🔓 Rate limiting COMPLETELY DISABLED for personal use`). The intended policy when re-enabled is **100 requests per 15-minute window per IP**, applied per `/api/*` route. The Express `trust proxy` setting is enabled in production so the real client IP is read from `X-Forwarded-For` (Nginx terminator).
|
||||
|
||||
Redis-backed rate limiting helpers exist in `src/services/redis/rateLimitService.ts` and are used by sensitive auth flows (password reset, email verification) even with the global limiter off.
|
||||
|
||||
## CORS
|
||||
|
||||
CORS is configured globally in `app.ts`:
|
||||
|
||||
```ts
|
||||
cors({
|
||||
origin: process.env.FRONTEND_URL,
|
||||
credentials: true,
|
||||
methods: ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"],
|
||||
allowedHeaders: ["Content-Type", "Authorization", "X-Requested-With"],
|
||||
})
|
||||
```
|
||||
|
||||
Only the configured `FRONTEND_URL` may make cross-origin requests with credentials. The SHKeeper configuration endpoint (`GET /api/payment/shkeeper/config`) overrides this with `Access-Control-Allow-Origin: *` because it is consumed by the SHKeeper payment widget hosted on another domain.
|
||||
|
||||
Uploaded files served from `/uploads/*` use `helmet({ crossOriginResourcePolicy: { policy: "cross-origin" } })` so they can be embedded from the frontend domain.
|
||||
|
||||
## Real-time channel
|
||||
|
||||
Socket.IO runs on the same HTTP server. The frontend should connect to the API origin with credentials. Clients join rooms via `join-user-room`, `join-request-room`, `join-seller-room`, `join-buyer-room`, `join-chat-room`. Full catalog: [[Socket Events]].
|
||||
|
||||
## Standard HTTP status codes
|
||||
|
||||
- `200 OK` - success
|
||||
- `201 Created` - resource created
|
||||
- `400 Bad Request` - validation error
|
||||
- `401 Unauthorized` - missing/invalid token
|
||||
- `403 Forbidden` - role/ownership failure
|
||||
- `404 Not Found` - resource or route missing
|
||||
- `409 Conflict` - duplicate (email, review)
|
||||
- `423 Locked` - account locked (auth flows)
|
||||
- `500 Internal Server Error` - unhandled exception
|
||||
|
||||
See [[Error Codes]] for the app-specific codes returned in the `error` field.
|
||||
143
03 - API Reference/Admin API.md
Normal file
143
03 - API Reference/Admin API.md
Normal file
@@ -0,0 +1,143 @@
|
||||
---
|
||||
title: Admin API
|
||||
tags: [api, admin, reference]
|
||||
---
|
||||
|
||||
# Admin API
|
||||
|
||||
There is no single `/api/admin` namespace — admin-only endpoints are scattered across the service routers. This page catalogs them in one place. All require `Bearer JWT` with `req.user.role === 'admin'`. The two enforcement patterns are:
|
||||
|
||||
- Middleware: `authorizeRoles('admin')` after `authenticateToken` (used by the dispute, data-cleanup, blog routers).
|
||||
- Inline check inside the handler: `if (req.user.role !== 'admin') return 403` (used by user, points, payment routes).
|
||||
|
||||
## User management
|
||||
|
||||
See full descriptions in [[User API]].
|
||||
|
||||
| Endpoint | Action |
|
||||
| --- | --- |
|
||||
| `POST /api/user/admin/create` | Create user with role/status |
|
||||
| `DELETE /api/user/admin/:userId` | Delete user (admins cannot delete each other) |
|
||||
| `PATCH /api/user/admin/:userId/status` | Activate / suspend |
|
||||
| `PATCH /api/user/admin/:userId/toggle-status` | Flip active flag |
|
||||
| `PATCH /api/user/admin/:userId/role` | Change role |
|
||||
| `GET /api/user/admin/list` | Paginated directory + stats |
|
||||
| `GET /api/user/admin/:userId/dependencies` | Pre-delete dependency check |
|
||||
| `GET /api/users/admin/stats` | Aggregate user analytics |
|
||||
| `GET /api/users/admin/:userId` | Full user detail (admin view) |
|
||||
| `PUT /api/users/admin/:userId` | Mass update user |
|
||||
| `PUT /api/users/admin/update/:email` | Mass update by email |
|
||||
| `PATCH /api/users/admin/:userId/password` | Force password reset (wipes refresh tokens) |
|
||||
| `POST /api/users/admin/:userId/resend-verification` | Resend verification email |
|
||||
|
||||
## Listing / marketplace moderation
|
||||
|
||||
See [[Marketplace API]]. Admins can use most marketplace endpoints with elevated privileges (e.g. delete any purchase request, override offer status). Specific admin-only actions:
|
||||
|
||||
| Endpoint | Action |
|
||||
| --- | --- |
|
||||
| `PUT /api/marketplace/offers/:id/status` | Direct status mutation including admin overrides |
|
||||
| `POST /api/marketplace/purchase-requests/:id/release-payment` | Force escrow release |
|
||||
| `PATCH /api/marketplace/purchase-requests/:id/status` (any → any) | Override request state machine |
|
||||
|
||||
Template approval is implicit: admins use the same template CRUD endpoints with override privileges.
|
||||
|
||||
## Dispute mediation
|
||||
|
||||
See [[Dispute API]].
|
||||
|
||||
| Endpoint | Action |
|
||||
| --- | --- |
|
||||
| `POST /api/disputes/:id/assign` | Assign moderator |
|
||||
| `PATCH /api/disputes/:id/status` | Update status |
|
||||
| `POST /api/disputes/:id/resolve` | Final decision (buyer / seller / split) |
|
||||
| `GET /api/disputes/statistics` | Admin dashboard data |
|
||||
|
||||
## Manual payment operations
|
||||
|
||||
See [[Payment API]].
|
||||
|
||||
| Endpoint | Action |
|
||||
| --- | --- |
|
||||
| `POST /api/payment/payments/cleanup-pending` | Delete stale pending payments |
|
||||
| `POST /api/payment/payments/:id/fetch-tx` | Re-query chain for missing tx hash |
|
||||
| `POST /api/payment/payments/auto-fetch-missing` | Batch tx-hash backfill |
|
||||
| `POST /api/payment/shkeeper/:id/release` | Build escrow-release tx |
|
||||
| `POST /api/payment/shkeeper/:id/release/confirm` | Confirm release tx hash |
|
||||
| `POST /api/payment/shkeeper/:id/refund` | Build refund tx |
|
||||
| `POST /api/payment/shkeeper/:id/refund/confirm` | Confirm refund tx hash |
|
||||
| `POST /api/payment/shkeeper/payout` | Create payout task |
|
||||
| `GET /api/payment/shkeeper/webhook-stats` | Webhook telemetry |
|
||||
| `POST /api/payment/decentralized/admin-payout` | Direct admin-wallet payout |
|
||||
|
||||
## Points (admin)
|
||||
|
||||
See [[Points API]].
|
||||
|
||||
| Endpoint | Action |
|
||||
| --- | --- |
|
||||
| `POST /api/points/admin/add` | Manually grant / deduct points for a user |
|
||||
|
||||
## Data cleanup
|
||||
|
||||
Router: [`backend/src/services/admin/dataCleanupRoutes.ts`](../../backend/src/services/admin/dataCleanupRoutes.ts). Mounted under `/api/admin/cleanup/*`. The router applies `authenticateToken` + `authorizeRoles('admin')` to every endpoint.
|
||||
|
||||
### GET /api/admin/cleanup/stats
|
||||
|
||||
**Description:** Per-collection document counts and sizes.
|
||||
**Response 200:** `{ success, data: { collections: [{ name, count, sizeBytes }] } }`
|
||||
|
||||
### GET /api/admin/cleanup/collections
|
||||
|
||||
**Description:** List collections that can be cleaned and the supported flags.
|
||||
**Response 200:** `{ success, data: { collections, options } }`
|
||||
|
||||
### POST /api/admin/cleanup/clean
|
||||
|
||||
**Description:** Bulk delete records. Defaults to `dryRun: true` and `keepAdmins: true`.
|
||||
**Request body:**
|
||||
```ts
|
||||
{
|
||||
collections?: string[]; // default ["all"]
|
||||
dryRun?: boolean; // default true
|
||||
keepAdmins?: boolean; // default true
|
||||
olderThanDays?: number; // optional age filter
|
||||
confirm?: "DELETE_ALL_DATA"; // required for actual deletion
|
||||
}
|
||||
```
|
||||
**Response 200:** `{ success, data: { deletedCounts, dryRun } }`
|
||||
|
||||
### DELETE /api/admin/cleanup/user/:userId
|
||||
|
||||
**Description:** Cascade-delete all data for a specific user (GDPR). Requires `?confirm=DELETE_USER_DATA` for real execution.
|
||||
**Query params:** `dryRun=true|false`, `confirm=DELETE_USER_DATA`
|
||||
|
||||
### POST /api/admin/cleanup/temp
|
||||
|
||||
**Description:** Purge temporary data older than N hours (verification codes, file temp uploads).
|
||||
**Request body:** `{ olderThanHours?: number }` (default 24)
|
||||
|
||||
### POST /api/admin/cleanup/seed-templates
|
||||
|
||||
**Description:** Re-runs the request templates seeder (production safe; idempotent).
|
||||
|
||||
### POST /api/admin/cleanup/seed-all
|
||||
|
||||
**Description:** Seeds users, addresses, and templates in dependency order. Used to bootstrap a fresh staging environment.
|
||||
|
||||
## Analytics
|
||||
|
||||
There is no dedicated analytics router. Admin dashboards stitch together:
|
||||
|
||||
- `GET /api/users/admin/stats` (user metrics)
|
||||
- `GET /api/payment/stats` (payment aggregates)
|
||||
- `GET /api/disputes/statistics` (dispute KPIs)
|
||||
- `GET /api/admin/cleanup/stats` (collection sizes)
|
||||
- `GET /api/payment/shkeeper/webhook-stats` (provider health)
|
||||
- `GET /api/payment/shkeeper/wallet-monitor/status` (chain monitor)
|
||||
|
||||
## Related
|
||||
|
||||
- [[Admin Console Architecture]]
|
||||
- [[Authorization Model]]
|
||||
- [[Error Codes]]
|
||||
305
03 - API Reference/Authentication API.md
Normal file
305
03 - API Reference/Authentication API.md
Normal file
@@ -0,0 +1,305 @@
|
||||
---
|
||||
title: Authentication API
|
||||
tags: [api, auth, reference]
|
||||
---
|
||||
|
||||
# Authentication API
|
||||
|
||||
All endpoints are mounted under `/api/auth/*` in `backend/src/app.ts`. The routes file is [`backend/src/services/auth/authRoutes.ts`](../../backend/src/services/auth/authRoutes.ts) and the WebAuthn sub-routes are in [`passkeyRoutes.ts`](../../backend/src/services/auth/passkeyRoutes.ts). Controller logic lives in [`authController.ts`](../../backend/src/services/auth/authController.ts) and [`authService.ts`](../../backend/src/services/auth/authService.ts).
|
||||
|
||||
Two distinct identities are involved: a [[User]] (`models/User.ts`) and a [[TempVerification]] document that holds pending registration data until the email code is confirmed. Tokens are signed JWTs (access + refresh) created in `authService`. See [[Authentication Flow]] for the high-level lifecycle diagram.
|
||||
|
||||
## Registration
|
||||
|
||||
### POST /api/auth/register
|
||||
|
||||
**Description:** Start a new registration. Creates a [[TempVerification]] document and emails an 8-digit verification code. The actual [[User]] is only created once the code is verified.
|
||||
**Auth required:** No
|
||||
**Request body:**
|
||||
```ts
|
||||
{
|
||||
email: string;
|
||||
firstName?: string; // default "کاربر"
|
||||
lastName?: string; // default "جدید"
|
||||
role?: "buyer" | "seller"; // default "buyer"
|
||||
password?: string; // accepted now or at verify-email-code time
|
||||
referralCode?: string; // optional, links to referrer for [[Points API]]
|
||||
}
|
||||
```
|
||||
**Response 200:**
|
||||
```json
|
||||
{ "success": true, "message": "Verification code sent ...", "data": { "email": "x@y.z" } }
|
||||
```
|
||||
**Errors:** `400` validation, `409 USER_EXISTS` if email already registered.
|
||||
**Side effects:**
|
||||
- Upserts a `TempVerification` row.
|
||||
- Sends `emailService.sendVerificationCodeEmail`.
|
||||
- No socket emission yet (user does not exist).
|
||||
**Source:** `authController.register`
|
||||
|
||||
### POST /api/auth/verify-email-code
|
||||
|
||||
**Description:** Confirms the registration code. Creates the [[User]], deletes the `TempVerification`, processes any referral, issues JWT tokens, and starts the session in Redis.
|
||||
**Auth required:** No
|
||||
**Request body:**
|
||||
```ts
|
||||
{
|
||||
email: string;
|
||||
code: string; // 8 digits
|
||||
password?: string; // required if not provided at register
|
||||
}
|
||||
```
|
||||
**Response 200:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Email verified",
|
||||
"data": {
|
||||
"user": { "_id": "...", "email": "...", "role": "buyer", ... },
|
||||
"tokens": { "accessToken": "...", "refreshToken": "..." }
|
||||
}
|
||||
}
|
||||
```
|
||||
**Errors:** `400` invalid/expired code, `404` no pending verification, `409` email already taken.
|
||||
**Side effects:**
|
||||
- Creates [[User]], deletes `TempVerification`.
|
||||
- Calls [[Points API]] referral hook when `referralCode` was supplied (emits `referral-signup` to `user-<referrerId>`).
|
||||
- Stores refresh token in `user.refreshTokens`.
|
||||
- Welcome email via `emailService.sendWelcomeEmail`.
|
||||
|
||||
### GET /api/auth/verify-email/:token
|
||||
|
||||
**Description:** Legacy URL-based verification (link in email). Marks `isEmailVerified=true`.
|
||||
**Auth required:** No
|
||||
**Response 200:** `{ "success": true, "message": "Email verified successfully" }`
|
||||
**Errors:** `400` invalid/expired token.
|
||||
|
||||
### POST /api/auth/resend-verification
|
||||
|
||||
**Description:** Re-issues the 8-digit code for a pending or unverified user.
|
||||
**Auth required:** No
|
||||
**Request body:** `{ email: string }`
|
||||
**Response 200:** `{ "success": true, "message": "Verification code resent" }`
|
||||
**Errors:** `400` invalid email, `404` no user/temp record, `429` rate-limited (Redis).
|
||||
|
||||
### POST /api/auth/force-verify-user
|
||||
|
||||
**Description:** Development-only helper to mark a user verified without going through email.
|
||||
**Auth required:** No (intended for dev only — gate with env in prod)
|
||||
|
||||
## Login & sessions
|
||||
|
||||
### POST /api/auth/login
|
||||
|
||||
**Description:** Email/password login. Validates credentials with bcrypt, signs JWT pair, stores refresh token on user, returns sanitized user object.
|
||||
**Auth required:** No
|
||||
**Request body:**
|
||||
```ts
|
||||
{
|
||||
email: string;
|
||||
password: string;
|
||||
}
|
||||
```
|
||||
**Response 200:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Login successful",
|
||||
"data": {
|
||||
"user": { "_id": "...", "email": "...", "role": "buyer", "firstName": "..." },
|
||||
"tokens": { "accessToken": "...", "refreshToken": "..." }
|
||||
}
|
||||
}
|
||||
```
|
||||
**Errors:**
|
||||
- `400` validation
|
||||
- `401` invalid credentials
|
||||
- `403` email not verified
|
||||
- `423` account locked (after repeated failures, tracked in Redis via `rateLimitService`)
|
||||
**Side effects:**
|
||||
- Updates `user.lastLoginAt`.
|
||||
- Pushes refresh token onto `user.refreshTokens`.
|
||||
- Redis session start via `sessionService`.
|
||||
|
||||
### POST /api/auth/refresh-token
|
||||
|
||||
**Description:** Exchanges a refresh token for a new access token. Rotates the refresh token.
|
||||
**Auth required:** No (refresh token in body)
|
||||
**Request body:** `{ refreshToken: string }`
|
||||
**Response 200:** `{ "success": true, "data": { "tokens": { "accessToken": "...", "refreshToken": "..." } } }`
|
||||
**Errors:** `401` token expired / not present in user record, `403` user disabled.
|
||||
|
||||
### POST /api/auth/logout
|
||||
|
||||
**Description:** Removes the current refresh token from the user record and clears the Redis session.
|
||||
**Auth required:** Bearer JWT
|
||||
**Response 200:** `{ "success": true, "message": "Logged out" }`
|
||||
**Side effects:** `redisService` session removed.
|
||||
|
||||
## Google OAuth
|
||||
|
||||
### POST /api/auth/google/signup
|
||||
|
||||
**Description:** Verifies a Google ID token, creates a new [[User]] (no password), optionally links a referral, returns JWT tokens.
|
||||
**Auth required:** No
|
||||
**Request body:**
|
||||
```ts
|
||||
{
|
||||
googleToken: string;
|
||||
role?: "buyer" | "seller";
|
||||
referralCode?: string;
|
||||
}
|
||||
```
|
||||
**Response 200:** Same shape as `verify-email-code` response.
|
||||
**Errors:** `400` invalid Google token, `409` email already registered (suggest sign-in instead).
|
||||
|
||||
### POST /api/auth/google/signin
|
||||
|
||||
**Description:** Verifies a Google ID token and signs in an existing user. Will not create a new account.
|
||||
**Auth required:** No
|
||||
**Request body:** `{ googleToken: string }`
|
||||
**Response 200:** `{ success, data: { user, tokens } }`
|
||||
**Errors:** `400` invalid token, `404` no user with that Google email.
|
||||
|
||||
## Passkey / WebAuthn
|
||||
|
||||
Routes are nested under `/api/auth/` via `passkeyRoutes`. Service: `passkeyService.ts`.
|
||||
|
||||
### POST /api/auth/passkey/authenticate/challenge
|
||||
|
||||
**Description:** Generates a sign-in challenge that the browser/authenticator will sign. No `userId` required — the assertion will identify the user.
|
||||
**Auth required:** No
|
||||
**Response 200:** `{ "success": true, "challenge": { /* PublicKeyCredentialRequestOptions */ } }`
|
||||
|
||||
### POST /api/auth/passkey/authenticate
|
||||
|
||||
**Description:** Verifies the WebAuthn assertion and, on success, returns a JWT pair.
|
||||
**Auth required:** No
|
||||
**Request body:** `{ challenge, assertion }` (assertion is the browser's `navigator.credentials.get()` output).
|
||||
**Response 200:** `{ "success": true, "userId": "...", "user": { ... }, "tokens": { ... } }`
|
||||
**Errors:** `400` missing fields, `404` `Passkey not found`, `500` verification error.
|
||||
|
||||
### POST /api/auth/passkey/register/challenge
|
||||
|
||||
**Description:** Generates a registration challenge for the authenticated user.
|
||||
**Auth required:** Bearer JWT
|
||||
**Response 200:** `{ "success": true, "challenge": { /* PublicKeyCredentialCreationOptions */ } }`
|
||||
|
||||
### POST /api/auth/passkey/register
|
||||
|
||||
**Description:** Verifies a new passkey registration and stores the credential on the user.
|
||||
**Auth required:** Bearer JWT
|
||||
**Request body:** `{ challenge, credential }`
|
||||
**Response 200:** `{ "success": true, "message": "Passkey registered successfully" }`
|
||||
|
||||
### GET /api/auth/passkey/list
|
||||
|
||||
**Description:** Returns the calling user's registered passkeys (id, label, created date).
|
||||
**Auth required:** Bearer JWT
|
||||
**Response 200:** `{ "success": true, "passkeys": [...] }`
|
||||
|
||||
### DELETE /api/auth/passkey/:passkeyId
|
||||
|
||||
**Description:** Removes a passkey by id.
|
||||
**Auth required:** Bearer JWT
|
||||
**Response 200:** `{ "success": true, "message": "Passkey removed successfully" }`
|
||||
|
||||
## Password management
|
||||
|
||||
### POST /api/auth/request-password-reset
|
||||
|
||||
**Description:** Generates a reset token, stores it on the user, and emails a reset link plus a numeric code.
|
||||
**Auth required:** No
|
||||
**Request body:** `{ email: string }`
|
||||
**Response 200:** `{ "success": true, "message": "Password reset email sent" }` (always returns success to avoid email enumeration).
|
||||
**Side effects:** `emailService.sendPasswordResetEmail`; rate-limited per IP via Redis.
|
||||
|
||||
### POST /api/auth/reset-password
|
||||
|
||||
**Description:** Sets a new password using a token from the reset email. Wipes refresh tokens.
|
||||
**Auth required:** No
|
||||
**Request body:**
|
||||
```ts
|
||||
{
|
||||
token: string;
|
||||
password: string; // 6+ chars, mixed case + digit
|
||||
}
|
||||
```
|
||||
**Response 200:** `{ "success": true, "message": "Password updated" }`
|
||||
**Errors:** `400` invalid/expired token or weak password.
|
||||
|
||||
### POST /api/auth/reset-password-with-code
|
||||
|
||||
**Description:** Alternative reset flow using a numeric code instead of a tokenised URL.
|
||||
**Auth required:** No
|
||||
**Request body:** `{ email, code, password }`
|
||||
**Response 200:** `{ "success": true }`
|
||||
|
||||
### POST /api/auth/change-password
|
||||
|
||||
**Description:** Authenticated password change.
|
||||
**Auth required:** Bearer JWT
|
||||
**Request body:**
|
||||
```ts
|
||||
{
|
||||
currentPassword: string;
|
||||
newPassword: string; // 6+ chars, mixed case + digit
|
||||
}
|
||||
```
|
||||
**Response 200:** `{ "success": true, "message": "Password updated" }`
|
||||
**Errors:** `400` validation, `401` wrong current password.
|
||||
**Side effects:** Clears `user.refreshTokens` (forces re-login on other devices).
|
||||
|
||||
## Current user / profile
|
||||
|
||||
### GET /api/auth/profile
|
||||
|
||||
**Description:** Returns the full sanitized [[User]] document for the caller.
|
||||
**Auth required:** Bearer JWT
|
||||
**Response 200:** `{ "success": true, "data": { /* User */ } }`
|
||||
|
||||
### PUT /api/auth/profile (and POST /api/auth/update-profile)
|
||||
|
||||
**Description:** Updates the caller's profile (first/last name, phone, bio, website, language/currency preferences).
|
||||
**Auth required:** Bearer JWT
|
||||
**Request body:** Partial profile, see `updateProfileValidation`:
|
||||
```ts
|
||||
{
|
||||
firstName?: string; // 2-50
|
||||
lastName?: string; // 2-50
|
||||
profile?: {
|
||||
phone?: string; // E.164-ish
|
||||
bio?: string; // <=500
|
||||
website?: string; // URL
|
||||
};
|
||||
preferences?: {
|
||||
language?: "en" | "fa" | "ar";
|
||||
currency?: "USD" | "EUR" | "IRR" | "AED";
|
||||
};
|
||||
}
|
||||
```
|
||||
**Response 200:** Updated user.
|
||||
**Errors:** `400` validation.
|
||||
|
||||
## Account deletion
|
||||
|
||||
### DELETE /api/auth/account
|
||||
|
||||
**Description:** Permanently deletes the caller's account after re-authenticating with password.
|
||||
**Auth required:** Bearer JWT
|
||||
**Request body:** `{ password: string }`
|
||||
**Response 200:** `{ "success": true, "message": "Account deleted" }`
|
||||
**Errors:** `401` bad password.
|
||||
**Side effects:** Removes [[User]] document, clears Redis session, cascades configured by `dataCleanupService`.
|
||||
|
||||
## Error codes summary
|
||||
|
||||
| HTTP | App code | Meaning |
|
||||
| --- | --- | --- |
|
||||
| 400 | `Validation Error` | `express-validator` rejected the body |
|
||||
| 401 | — | Bad credentials / missing token |
|
||||
| 403 | — | Email not verified or insufficient role |
|
||||
| 409 | `USER_EXISTS` | Email already in use |
|
||||
| 423 | — | Account temporarily locked after failed logins |
|
||||
|
||||
See [[Error Codes]] for the global error shape.
|
||||
125
03 - API Reference/Blog API.md
Normal file
125
03 - API Reference/Blog API.md
Normal file
@@ -0,0 +1,125 @@
|
||||
---
|
||||
title: Blog API
|
||||
tags: [api, blog, reference]
|
||||
---
|
||||
|
||||
# Blog API
|
||||
|
||||
Endpoints live under `/api/blog/*`. The router is [`backend/src/routes/blogRoutes.ts`](../../backend/src/routes/blogRoutes.ts) and delegates to [`blogController`](../../backend/src/services/blog/blogController.ts).
|
||||
|
||||
Public endpoints (post listing and reads) are open. Mutating endpoints (create/update/delete) require **admin** role — enforced via `authenticateToken` + `authorizeRoles('admin')`.
|
||||
|
||||
Model: [[BlogPost]]. Image uploads use the [[File API]] (`POST /api/files/upload/blog-images`).
|
||||
|
||||
## Public reads
|
||||
|
||||
### GET /api/blog/posts
|
||||
|
||||
**Description:** Paginated list of published blog posts.
|
||||
**Auth required:** No
|
||||
**Query params:**
|
||||
- `page` (default 1)
|
||||
- `limit` (default 10)
|
||||
- `category`
|
||||
- `tag`
|
||||
- `search` (matches title/excerpt/content)
|
||||
- `sortBy` (default `publishedAt`)
|
||||
- `sortOrder` (`asc` | `desc`, default `desc`)
|
||||
**Response 200:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"posts": [BlogPost, ...],
|
||||
"pagination": { "page": 1, "limit": 10, "total": 25, "totalPages": 3, "hasMore": true }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### GET /api/blog/posts/featured
|
||||
|
||||
**Description:** Posts with `isFeatured: true`. Used by the home page hero strip.
|
||||
**Auth required:** No
|
||||
**Response 200:** `{ success, data: { posts } }`
|
||||
|
||||
### GET /api/blog/posts/recent
|
||||
|
||||
**Description:** Most recent N published posts. Used by the footer / sidebar.
|
||||
**Auth required:** No
|
||||
**Query params:** `limit` (default 5)
|
||||
|
||||
### GET /api/blog/posts/search
|
||||
|
||||
**Description:** Full-text search across title/excerpt/content/tags.
|
||||
**Auth required:** No
|
||||
**Query params:** `q` (required), `page`, `limit`
|
||||
**Errors:** `400` if `q` is missing or shorter than 2 characters.
|
||||
|
||||
### GET /api/blog/posts/:slug
|
||||
|
||||
**Description:** Get a single published post by URL slug.
|
||||
**Auth required:** No
|
||||
**Response 200:** `{ success, data: { post: BlogPost } }`
|
||||
**Errors:** `404` not found or not published.
|
||||
**Side effects:** Increments `viewsCount`.
|
||||
|
||||
## Admin (mutating)
|
||||
|
||||
All endpoints below require `Bearer JWT` with `role: "admin"`.
|
||||
|
||||
### GET /api/blog/admin/posts
|
||||
|
||||
**Description:** Admin list including drafts and unpublished posts.
|
||||
**Auth required:** Bearer JWT (admin)
|
||||
**Query params:** `page`, `limit`, `status` (`draft` | `published` | `archived`), `search`
|
||||
**Response 200:** `{ success, data: { posts, pagination } }`
|
||||
|
||||
### GET /api/blog/admin/posts/:id
|
||||
|
||||
**Description:** Get a post by id (admin view, includes draft fields).
|
||||
**Auth required:** Bearer JWT (admin)
|
||||
|
||||
### POST /api/blog/posts
|
||||
|
||||
**Description:** Create a post.
|
||||
**Auth required:** Bearer JWT (admin)
|
||||
**Request body:**
|
||||
```ts
|
||||
{
|
||||
title: string;
|
||||
slug?: string; // auto-generated from title when omitted
|
||||
excerpt?: string;
|
||||
content: string; // Markdown / rich text
|
||||
coverImage?: string; // URL from [[File API]]
|
||||
category?: string;
|
||||
tags?: string[];
|
||||
status?: "draft" | "published"; // default "draft"
|
||||
isFeatured?: boolean;
|
||||
publishedAt?: string; // ISO date, used when status === "published"
|
||||
videoUrl?: string;
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
```
|
||||
**Response 201:** `{ success, data: { post } }`
|
||||
|
||||
### PUT /api/blog/posts/:id
|
||||
|
||||
**Description:** Update a post. Partial; the body can contain any of the create fields.
|
||||
**Auth required:** Bearer JWT (admin)
|
||||
|
||||
### DELETE /api/blog/posts/:id
|
||||
|
||||
**Description:** Hard-delete a post.
|
||||
**Auth required:** Bearer JWT (admin)
|
||||
**Response 200:** `{ success: true, message: "Post deleted" }`
|
||||
|
||||
## Comments
|
||||
|
||||
The codebase does not currently expose a comments API — comments are intentionally disabled. If/when added, they will appear under `/api/blog/posts/:id/comments`.
|
||||
|
||||
## Related
|
||||
|
||||
- [[BlogPost]]
|
||||
- [[File API]] (for cover and inline image uploads)
|
||||
- `backend/BLOG_API_ENDPOINTS.md` (legacy in-repo notes)
|
||||
- `backend/BLOG_VIDEO_EXAMPLES.md`
|
||||
144
03 - API Reference/Chat API.md
Normal file
144
03 - API Reference/Chat API.md
Normal file
@@ -0,0 +1,144 @@
|
||||
---
|
||||
title: Chat API
|
||||
tags: [api, chat, reference]
|
||||
---
|
||||
|
||||
# Chat API
|
||||
|
||||
All chat endpoints live under `/api/chat/*`. The router is [`backend/src/services/chat/chatRoutes.ts`](../../backend/src/services/chat/chatRoutes.ts), controller is `chatController`, service is `ChatService`. Every endpoint requires `Bearer JWT` — the router applies `authenticateToken` globally.
|
||||
|
||||
Model: [[Chat]]. Real-time delivery happens over Socket.IO rooms named `chat-<chatId>`. Clients must call `join-chat-room` after connecting. See [[Socket Events]] for `new-message`, `messages-read`, `message-edited`, `message-deleted`, `participants-added`, `participant-removed`, and `user-typing` payloads.
|
||||
|
||||
## Conversations
|
||||
|
||||
### POST /api/chat
|
||||
|
||||
**Description:** Create a generic chat between two or more participants.
|
||||
**Auth required:** Bearer JWT
|
||||
**Request body:**
|
||||
```ts
|
||||
{
|
||||
participantIds: string[]; // 2+ user ids (caller is added automatically)
|
||||
type?: "direct" | "group"; // default "direct"
|
||||
title?: string; // group only
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
```
|
||||
**Response 201:** `{ success: true, data: { chat: { _id, participants, type, ... } } }`
|
||||
**Errors:** `400` need 2+ participants, `404` participant not found.
|
||||
|
||||
### POST /api/chat/purchase-request
|
||||
|
||||
**Description:** Create (or fetch the existing) chat tied to a specific [[PurchaseRequest]] between buyer and seller. Idempotent.
|
||||
**Auth required:** Bearer JWT
|
||||
**Request body:** `{ purchaseRequestId: string; sellerId: string }`
|
||||
**Response 200/201:** `{ success: true, data: { chat } }`
|
||||
|
||||
### POST /api/chat/support
|
||||
|
||||
**Description:** Open a support chat. Adds the caller and the support/admin pool. Used by the in-app help widget.
|
||||
**Auth required:** Bearer JWT
|
||||
**Request body:** `{ subject?: string; initialMessage?: string }`
|
||||
**Response 201:** `{ success: true, data: { chat } }`
|
||||
|
||||
### GET /api/chat
|
||||
|
||||
**Description:** List the caller's chats, ordered by latest message. Includes unread counts.
|
||||
**Auth required:** Bearer JWT
|
||||
**Query params:** `page`, `limit`, `type` (`direct` | `group` | `support`)
|
||||
**Response 200:** `{ success, data: { chats: [...], pagination } }`
|
||||
|
||||
### GET /api/chat/stats
|
||||
|
||||
**Description:** Aggregate counts (total chats, total messages, total unread).
|
||||
**Auth required:** Bearer JWT
|
||||
|
||||
### GET /api/chat/:id/info
|
||||
|
||||
**Description:** Full chat metadata: participants (populated), title, type, created-at, last message, unread per user.
|
||||
**Auth required:** Bearer JWT (participant)
|
||||
**Errors:** `403` not a participant, `404` not found.
|
||||
|
||||
### PATCH /api/chat/:id/archive
|
||||
|
||||
**Description:** Toggle archived state for the caller (per-user flag).
|
||||
**Auth required:** Bearer JWT (participant)
|
||||
|
||||
### POST /api/chat/:id/participants
|
||||
|
||||
**Description:** Add a participant to a group chat.
|
||||
**Auth required:** Bearer JWT (creator/admin)
|
||||
**Request body:** `{ userId: string }`
|
||||
**Side effects:** Emits `participants-added` on `chat-<id>`.
|
||||
|
||||
### DELETE /api/chat/:id/participants/:participantId
|
||||
|
||||
**Description:** Remove a participant from a group chat.
|
||||
**Auth required:** Bearer JWT (creator/admin)
|
||||
**Side effects:** Emits `participant-removed` on `chat-<id>`.
|
||||
|
||||
## Messages
|
||||
|
||||
### GET /api/chat/:id/messages
|
||||
|
||||
**Description:** Paginated message history (newest first by default).
|
||||
**Auth required:** Bearer JWT (participant)
|
||||
**Query params:** `page`, `limit`, `before` (cursor by createdAt)
|
||||
|
||||
### POST /api/chat/:id/messages
|
||||
|
||||
**Description:** Send a text message.
|
||||
**Auth required:** Bearer JWT (participant)
|
||||
**Request body:**
|
||||
```ts
|
||||
{
|
||||
content: string;
|
||||
type?: "text" | "system"; // default "text"
|
||||
replyToMessageId?: string;
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
```
|
||||
**Response 201:** `{ success, data: { message } }`
|
||||
**Side effects:** Emits `new-message` on `chat-<chatId>`; increments unread for other participants.
|
||||
|
||||
### POST /api/chat/:id/messages/file
|
||||
|
||||
**Description:** Send a message with a binary attachment. Multipart form (`file` field). Maxes follow the [[File API]] limits.
|
||||
**Auth required:** Bearer JWT (participant)
|
||||
**Form fields:**
|
||||
- `file` - the file blob
|
||||
- `content?` - optional caption
|
||||
- `replyToMessageId?` - optional
|
||||
|
||||
**Response 201:** `{ success, data: { message: { attachments: [{ url, filename, mimeType, size }] } } }`
|
||||
|
||||
### PATCH /api/chat/:id/messages/read
|
||||
|
||||
**Description:** Mark all unread messages up to the latest as read for the caller.
|
||||
**Auth required:** Bearer JWT (participant)
|
||||
**Response 200:** `{ success, data: { modifiedCount } }`
|
||||
**Side effects:** Emits `messages-read` on `chat-<id>`.
|
||||
|
||||
### PUT /api/chat/:id/messages/:messageId
|
||||
|
||||
**Description:** Edit an existing message (author only, within edit window).
|
||||
**Auth required:** Bearer JWT (message author)
|
||||
**Request body:** `{ content: string }`
|
||||
**Side effects:** Emits `message-edited` on `chat-<id>`.
|
||||
|
||||
### DELETE /api/chat/:id/messages/:messageId
|
||||
|
||||
**Description:** Soft-delete a message (author or admin). The record stays but `deletedAt` is set and `content` is cleared.
|
||||
**Auth required:** Bearer JWT (author or admin)
|
||||
**Side effects:** Emits `message-deleted` on `chat-<id>`.
|
||||
|
||||
## Typing indicator
|
||||
|
||||
Typing indicators are not REST endpoints — clients emit `typing-start` and `typing-stop` over Socket.IO. The server broadcasts `user-typing` to the chat room. Full details in [[Socket Events]].
|
||||
|
||||
## Related
|
||||
|
||||
- [[Chat]]
|
||||
- [[Chat Flow]]
|
||||
- [[File API]] (for attachment uploads)
|
||||
- [[Notification API]] (for out-of-band notifications when a chat message is received and the user is offline)
|
||||
122
03 - API Reference/Dispute API.md
Normal file
122
03 - API Reference/Dispute API.md
Normal file
@@ -0,0 +1,122 @@
|
||||
---
|
||||
title: Dispute API
|
||||
tags: [api, dispute, reference]
|
||||
---
|
||||
|
||||
# Dispute API
|
||||
|
||||
Endpoints live under `/api/disputes/*`. The router is [`backend/src/routes/disputeRoutes.ts`](../../backend/src/routes/disputeRoutes.ts) and delegates to `DisputeController` ([`backend/src/controllers/disputeController.ts`](../../backend/src/controllers/disputeController.ts)). The router applies `authenticateToken` globally — every endpoint requires `Bearer JWT`.
|
||||
|
||||
Model: [[Dispute]]. A dispute references a [[PurchaseRequest]] plus optional [[Payment]] and is the input to the mediation workflow that ends in either a `resolved_buyer` or `resolved_seller` decision and triggers an escrow release or refund via the [[Payment API]].
|
||||
|
||||
## Create
|
||||
|
||||
### POST /api/disputes
|
||||
|
||||
**Description:** Open a dispute against a purchase request.
|
||||
**Auth required:** Bearer JWT (buyer or seller participant in the request)
|
||||
**Request body:**
|
||||
```ts
|
||||
{
|
||||
purchaseRequestId: string;
|
||||
reason: "not_delivered" | "wrong_item" | "damaged" | "quality" | "other";
|
||||
description: string;
|
||||
evidence?: string[]; // URLs from [[File API]]
|
||||
paymentId?: string;
|
||||
}
|
||||
```
|
||||
**Response 201:** `{ success: true, data: { dispute } }`
|
||||
**Errors:** `400` validation, `403` not a participant of the request, `409` dispute already open for this request.
|
||||
**Side effects:**
|
||||
- Notifies the counter-party via `POST /api/notifications` (`new-notification` socket event).
|
||||
- Pauses any in-flight payout (sets a hold flag on the related [[Payment]]).
|
||||
|
||||
## Read
|
||||
|
||||
### GET /api/disputes
|
||||
|
||||
**Description:** List disputes the caller can see (their own as buyer/seller, all for admins).
|
||||
**Auth required:** Bearer JWT
|
||||
**Query params:**
|
||||
- `status` (`open` | `under_review` | `resolved_buyer` | `resolved_seller` | `closed`)
|
||||
- `purchaseRequestId`
|
||||
- `page`, `limit`, `sortBy`, `sortOrder`
|
||||
**Response 200:** `{ success, data: { disputes, pagination } }`
|
||||
|
||||
### GET /api/disputes/statistics
|
||||
|
||||
**Description:** Aggregated counts (open, by reason, average resolution time) for admin dashboards.
|
||||
**Auth required:** Bearer JWT (admin)
|
||||
**Response 200:** `{ success, data: { open, byReason, avgResolutionHours, ... } }`
|
||||
|
||||
### GET /api/disputes/:id
|
||||
|
||||
**Description:** Full dispute including evidence list, messages, assigned admin, decision (if any).
|
||||
**Auth required:** Bearer JWT (participant or admin)
|
||||
**Errors:** `403` not allowed, `404` not found.
|
||||
|
||||
## Admin operations
|
||||
|
||||
### POST /api/disputes/:id/assign
|
||||
|
||||
**Description:** Assign an admin moderator to the dispute. Sets `assignedAdminId` and transitions status to `under_review`.
|
||||
**Auth required:** Bearer JWT (admin)
|
||||
**Request body:** `{ adminId: string }`
|
||||
**Side effects:** Notifies all participants.
|
||||
|
||||
### PATCH /api/disputes/:id/status
|
||||
|
||||
**Description:** Generic status update (e.g. close without resolution).
|
||||
**Auth required:** Bearer JWT (admin)
|
||||
**Request body:** `{ status: string; note?: string }`
|
||||
|
||||
### POST /api/disputes/:id/resolve
|
||||
|
||||
**Description:** Final adjudication. Records the decision and triggers the appropriate escrow action.
|
||||
**Auth required:** Bearer JWT (admin)
|
||||
**Request body:**
|
||||
```ts
|
||||
{
|
||||
decision: "buyer" | "seller" | "split";
|
||||
refundAmount?: number; // required when "split"
|
||||
releaseAmount?: number; // required when "split"
|
||||
reasoning: string;
|
||||
}
|
||||
```
|
||||
**Response 200:** `{ success, data: { dispute, paymentAction } }`
|
||||
**Side effects:**
|
||||
- `decision === "buyer"` → triggers `POST /api/payment/shkeeper/:id/refund` flow.
|
||||
- `decision === "seller"` → triggers `POST /api/payment/shkeeper/:id/release` flow.
|
||||
- `decision === "split"` → admin executes both partial release and partial refund manually.
|
||||
- Notifies both participants and updates [[PurchaseRequest]] status to `disputed_resolved`.
|
||||
|
||||
## Evidence and messages
|
||||
|
||||
### POST /api/disputes/:id/evidence
|
||||
|
||||
**Description:** Attach additional evidence (image / document URLs from the [[File API]]) to the dispute. Either party can call.
|
||||
**Auth required:** Bearer JWT (participant or admin)
|
||||
**Request body:**
|
||||
```ts
|
||||
{
|
||||
url: string;
|
||||
description?: string;
|
||||
type?: "image" | "document" | "video";
|
||||
}
|
||||
```
|
||||
**Response 200:** `{ success, data: { dispute } }` with the evidence appended.
|
||||
|
||||
### Messages
|
||||
|
||||
Direct messages between disputants and the admin moderator are handled via a dedicated [[Chat]] created automatically when the dispute is opened (`type: "dispute"`). Use the [[Chat API]] endpoints (`POST /api/chat/:id/messages`, `GET /api/chat/:id/messages`) once you have the `chatId` from the dispute payload.
|
||||
|
||||
## Real-time
|
||||
|
||||
Dispute mutations emit notifications via `POST /api/notifications` which delivers `new-notification` socket events to each participant's `user-<userId>` room. See [[Socket Events]] for payload shape.
|
||||
|
||||
## Related
|
||||
|
||||
- [[Dispute]]
|
||||
- [[Dispute Resolution Flow]]
|
||||
- [[Payment API]]
|
||||
- [[Chat API]]
|
||||
110
03 - API Reference/Error Codes.md
Normal file
110
03 - API Reference/Error Codes.md
Normal file
@@ -0,0 +1,110 @@
|
||||
---
|
||||
title: Error Codes
|
||||
tags: [api, errors, reference]
|
||||
---
|
||||
|
||||
# Error Codes
|
||||
|
||||
This page documents the error shape returned by the AMN backend and the HTTP status mapping used across services.
|
||||
|
||||
## Standard error shape
|
||||
|
||||
The canonical helper is `ResponseHandler.error` in [`backend/src/shared/utils/response-handler.ts`](../../backend/src/shared/utils/response-handler.ts). Modern routes return:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"message": "Validation failed",
|
||||
"error": "Validation Error",
|
||||
"statusCode": 400,
|
||||
"timestamp": "2026-05-23T10:00:00.000Z",
|
||||
"path": "/api/auth/register",
|
||||
"method": "POST",
|
||||
"data": [ /* field-level details for validation errors */ ]
|
||||
}
|
||||
```
|
||||
|
||||
Uncaught errors are formatted by [`shared/middleware/errorHandler.ts`](../../backend/src/shared/middleware/errorHandler.ts):
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": "Internal Server Error",
|
||||
"statusCode": 500,
|
||||
"stack": "..." // only in NODE_ENV=development
|
||||
}
|
||||
```
|
||||
|
||||
Legacy routes (chiefly `/api/users` legacy paths, `/api/marketplace` legacy paths, `/api/payment/decentralized/*`, parts of `/api/payment/shkeeper/*`) return ad-hoc shapes such as `{ "error": "..." }` or `{ "success": false, "message": "..." }`. Treat any non-`2xx` response as an error and read whichever of `error` / `message` is present.
|
||||
|
||||
## HTTP status mapping
|
||||
|
||||
| Status | When | Examples |
|
||||
| --- | --- | --- |
|
||||
| `200 OK` | Successful read or mutation | Most `GET`s, idempotent `PUT`s/`PATCH`s |
|
||||
| `201 Created` | Resource created | `POST /api/marketplace/purchase-requests`, `POST /api/auth/register` (when user created), `POST /api/marketplace/reviews` |
|
||||
| `202 Accepted` | Async accepted (provider webhooks) | SHKeeper webhook acknowledgement |
|
||||
| `204 No Content` | Mutations with no body to return | Rare — most endpoints return the updated object |
|
||||
| `400 Bad Request` | Validation failure, malformed input | `express-validator` errors, bad MongoIds, missing fields |
|
||||
| `401 Unauthorized` | Missing or invalid JWT | `Access token required`, `Invalid or expired token` |
|
||||
| `403 Forbidden` | Authenticated but not allowed | Role check failed, email not verified, ownership check failed |
|
||||
| `404 Not Found` | Resource (or route) missing | `notFoundHandler`, `Resource not found` from `ResponseHandler.notFound` |
|
||||
| `409 Conflict` | Duplicate / state collision | `USER_EXISTS`, duplicate review, dispute already open |
|
||||
| `423 Locked` | Account temporarily locked | After repeated failed logins (Redis-tracked) |
|
||||
| `429 Too Many Requests` | Rate limit hit | Currently issued only by per-feature Redis limits (auth / AI); global limiter is disabled |
|
||||
| `500 Internal Server Error` | Unhandled exception | Caught by `errorHandler`; included stack trace in dev |
|
||||
| `502 Bad Gateway` | Upstream provider failure | OpenAI / SHKeeper unreachable |
|
||||
|
||||
## Application error codes
|
||||
|
||||
The `error` field of the response envelope contains a human-readable category. Currently used codes:
|
||||
|
||||
| Code | Meaning | Returned by |
|
||||
| --- | --- | --- |
|
||||
| `Validation Error` | `express-validator` rejected the body | All `*Validation` middlewares |
|
||||
| `Not Found` | Generic resource lookup miss | `ResponseHandler.notFound` |
|
||||
| `Unauthorized` | No token / bad token | `authenticateToken`, `ResponseHandler.unauthorized` |
|
||||
| `Forbidden` | Role/ownership check failed | `authorizeRoles`, `ResponseHandler.forbidden` |
|
||||
| `Internal Server Error` | Catch-all 500 | `ResponseHandler.internalError`, `errorHandler` |
|
||||
| `USER_EXISTS` | Email already registered | `POST /api/auth/register` |
|
||||
|
||||
For auth-specific 4xx responses the body's `message` carries the user-facing text (often Persian/Farsi for legacy reasons):
|
||||
|
||||
- `"کاربری با این ایمیل قبلاً ثبتنام کرده است"` - email already in use (409)
|
||||
- `"کد تحویل نادرست است"` - wrong delivery code (400)
|
||||
- `"شما مجاز به ایجاد کد تحویل برای این درخواست نیستید"` - not the buyer (403)
|
||||
|
||||
## Mongoose-specific mappings
|
||||
|
||||
Handled in `errorHandler`:
|
||||
|
||||
| Mongoose error | Mapped status | Body `message` |
|
||||
| --- | --- | --- |
|
||||
| `ValidationError` | 400 | `Validation Error` |
|
||||
| `MongoError` code `11000` | 409 | `Duplicate resource` |
|
||||
| `JsonWebTokenError` | 401 | `Invalid token` |
|
||||
| `TokenExpiredError` | 401 | `Token expired` |
|
||||
|
||||
## Webhook-specific
|
||||
|
||||
| Provider | Endpoint | Status on success | Status on signature mismatch |
|
||||
| --- | --- | --- | --- |
|
||||
| SHKeeper pay-in | `POST /api/payment/shkeeper/webhook` | 200 `{ success: true }` | 401 `{ success: false }` (then ignored) |
|
||||
| SHKeeper payout | `POST /api/payment/shkeeper/payout/webhook` | 200 / 400 with `{ success, message, data }` | 400 |
|
||||
| Generic payment callback | `POST /api/payment/callback` | 200 `{ success: true, message }` | 400 |
|
||||
|
||||
If a webhook is acknowledged with non-2xx, the provider re-delivers (SHKeeper retries every 60 seconds).
|
||||
|
||||
## Client guidance
|
||||
|
||||
1. Always parse `response.json()` even on non-2xx — both `message` and `error` are useful for UX surface text.
|
||||
2. For optimistic UI, treat `409` as "your action raced — refresh".
|
||||
3. For `401`, attempt one silent refresh-token call before redirecting to sign-in.
|
||||
4. For `403`, do not retry — the user lacks permission.
|
||||
5. For `423`, surface the lockout window from the `message`/`data` and direct the user to password reset.
|
||||
|
||||
## Related
|
||||
|
||||
- [[API Overview]]
|
||||
- [[Authentication API]]
|
||||
- [[Rate Limiting]]
|
||||
111
03 - API Reference/File API.md
Normal file
111
03 - API Reference/File API.md
Normal file
@@ -0,0 +1,111 @@
|
||||
---
|
||||
title: File API
|
||||
tags: [api, file, reference]
|
||||
---
|
||||
|
||||
# File API
|
||||
|
||||
Endpoints live under `/api/files/*`. The router is [`backend/src/services/file/fileRoutes.ts`](../../backend/src/services/file/fileRoutes.ts), delegating to `fileController` and `fileService`. Multer is used for multipart parsing and uploaded files are written under `uploads/<subfolder>`.
|
||||
|
||||
All endpoints require `Bearer JWT`. Static serving is wired in `app.ts` at `/uploads/*` with `helmet({ crossOriginResourcePolicy: { policy: "cross-origin" } })` so files can be embedded from the frontend domain.
|
||||
|
||||
## Multer configuration
|
||||
|
||||
`fileService.getUploadMiddleware(options)` produces a Multer instance per route. Options:
|
||||
|
||||
- `subfolder` - where the file lands under `uploads/`
|
||||
- `fieldName` - form field name
|
||||
- `fileTypes` - allowed MIME types (default: any)
|
||||
- `maxFiles` - default 1
|
||||
- `maxFileSizeMB` - default 10 MB
|
||||
|
||||
Global body limits in `app.ts` are `10mb` for `express.json` and `express.urlencoded`.
|
||||
|
||||
## Upload
|
||||
|
||||
### POST /api/files/upload/avatar
|
||||
|
||||
**Description:** Upload an avatar image. Lands in `uploads/temp/` and is then moved when the user persists it on their profile.
|
||||
**Auth required:** Bearer JWT
|
||||
**Form fields:** `avatar` (image; JPEG / PNG / GIF / WebP)
|
||||
**Response 200:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"url": "/uploads/temp/avatar-1716459200000.jpg",
|
||||
"filename": "avatar-1716459200000.jpg",
|
||||
"mimeType": "image/jpeg",
|
||||
"size": 51234
|
||||
}
|
||||
}
|
||||
```
|
||||
**Errors:** `400` bad type / too large, `401` not authenticated.
|
||||
**Side effects:** None — caller is responsible for `PUT /api/user/profile` to persist the URL.
|
||||
|
||||
### POST /api/files/upload/file
|
||||
|
||||
**Description:** Generic single-file upload (any MIME), lands in `uploads/temp/`.
|
||||
**Auth required:** Bearer JWT
|
||||
**Form fields:** `file`
|
||||
**Response 200:** `{ success, data: { url, filename, mimeType, size } }`
|
||||
|
||||
### POST /api/files/upload/files
|
||||
|
||||
**Description:** Multi-file upload (up to 5).
|
||||
**Auth required:** Bearer JWT
|
||||
**Form fields:** `files` (repeated)
|
||||
**Response 200:** `{ success, data: { files: [{ url, filename, ... }] } }`
|
||||
|
||||
### POST /api/files/upload/request-template-images
|
||||
|
||||
**Description:** Up to 10 images for a [[RequestTemplate]]. Lands in `uploads/request-templates/`.
|
||||
**Auth required:** Bearer JWT
|
||||
**Form fields:** `images` (repeated; JPEG / PNG / GIF / WebP)
|
||||
**Response 200:** `{ success, data: { files: [...] } }`
|
||||
|
||||
### POST /api/files/upload/blog-images
|
||||
|
||||
**Description:** Up to 10 images for a [[BlogPost]]. Lands in `uploads/blog/`.
|
||||
**Auth required:** Bearer JWT
|
||||
**Form fields:** `images` (repeated; JPEG / PNG / GIF / WebP)
|
||||
**Response 200:** `{ success, data: { files: [...] } }`
|
||||
|
||||
## Delete
|
||||
|
||||
### DELETE /api/files/delete
|
||||
|
||||
**Description:** Delete a file by relative path.
|
||||
**Auth required:** Bearer JWT
|
||||
**Request body:** `{ filePath: string }` (e.g. `"temp/avatar-1716459200000.jpg"`)
|
||||
**Response 200:** `{ success, message: "File deleted" }`
|
||||
**Errors:** `400` invalid path (must stay inside `uploads/`), `404` file missing.
|
||||
|
||||
## Inspect
|
||||
|
||||
### GET /api/files/info/:filePath
|
||||
|
||||
**Description:** Returns metadata for a file (URL-encoded `filePath` segment).
|
||||
**Auth required:** Bearer JWT
|
||||
**Response 200:** `{ success, data: { url, size, mimeType, createdAt } }`
|
||||
|
||||
### GET /api/files/stats
|
||||
|
||||
**Description:** Aggregate upload statistics (counts and sizes per subfolder).
|
||||
**Auth required:** Bearer JWT (admin gating planned per TODO in source)
|
||||
**Response 200:** `{ success, data: { subfolders: [{ name, count, sizeBytes }], total: { count, sizeBytes } } }`
|
||||
|
||||
## Serving
|
||||
|
||||
- `GET /uploads/<path>` - static file delivery (no auth, public read). Suitable for embedding avatars and blog/template images.
|
||||
- The server logs `❌ File not found:` for missing paths and `✅ Serving file:` on hits — useful when debugging frontend image refs.
|
||||
|
||||
The on-disk root resolves from `config.uploadPath`. In production this defaults to `/app/uploads`; in development to `<repo>/uploads`. Both are normalised to an absolute path before being passed to `express.static`.
|
||||
|
||||
## Related
|
||||
|
||||
- [[File Storage Architecture]]
|
||||
- [[User API]] (avatar consumer)
|
||||
- [[Marketplace API]] (template image consumer)
|
||||
- [[Blog API]] (blog image consumer)
|
||||
- [[Chat API]] (message attachments use a separate multipart route under chat)
|
||||
499
03 - API Reference/Marketplace API.md
Normal file
499
03 - API Reference/Marketplace API.md
Normal file
@@ -0,0 +1,499 @@
|
||||
---
|
||||
title: Marketplace API
|
||||
tags: [api, marketplace, reference]
|
||||
---
|
||||
|
||||
# Marketplace API
|
||||
|
||||
All marketplace endpoints live under `/api/marketplace/*`. The router is composed of several files mounted from `app.ts`:
|
||||
|
||||
- New controller-pattern routes: [`backend/src/services/marketplace/controllerRoutes.ts`](../../backend/src/services/marketplace/controllerRoutes.ts) (`marketplaceControllerRouter`)
|
||||
- Legacy + delivery-code routes: [`backend/src/services/marketplace/routes.ts`](../../backend/src/services/marketplace/routes.ts) (`marketplaceRouter`)
|
||||
- Request templates: [`requestTemplateRoutes.ts`](../../backend/src/services/marketplace/requestTemplateRoutes.ts) (`/api/marketplace/request-templates/*`)
|
||||
- Shop settings: [`shopSettingsRoutes.ts`](../../backend/src/services/marketplace/shopSettingsRoutes.ts) (`/api/marketplace/shop/*`)
|
||||
- Reviews: [`reviewRoutes.ts`](../../backend/src/services/marketplace/reviewRoutes.ts) (`/api/marketplace/reviews/*`)
|
||||
|
||||
Core models: [[PurchaseRequest]], [[SellerOffer]], [[Category]], [[RequestTemplate]], [[ShopSettings]], [[Review]]. Real-time updates are emitted on the `request-<id>`, `seller-<id>`, `buyer-<id>`, and `sellers`/`buyers` rooms — see [[Socket Events]].
|
||||
|
||||
## Categories
|
||||
|
||||
### GET /api/marketplace/categories
|
||||
|
||||
**Description:** Flat list of all categories.
|
||||
**Auth required:** No
|
||||
**Response 200:** `{ success: true, data: [Category, ...] }`
|
||||
|
||||
### GET /api/marketplace/categories/tree
|
||||
|
||||
**Description:** Nested tree based on `parentId`.
|
||||
**Auth required:** No
|
||||
|
||||
### GET /api/marketplace/categories/:id
|
||||
|
||||
**Description:** Single category by id.
|
||||
**Auth required:** No
|
||||
**Errors:** `404` not found.
|
||||
|
||||
## Sellers
|
||||
|
||||
### GET /api/marketplace/sellers
|
||||
|
||||
**Description:** Public seller directory (users with `role: 'seller'`). Includes aggregated stats (completed orders, ratings).
|
||||
**Auth required:** No
|
||||
|
||||
### GET /api/marketplace/request-templates/sellers
|
||||
|
||||
**Description:** Sellers who currently expose at least one active [[RequestTemplate]] (used by the shop discovery page).
|
||||
**Auth required:** No
|
||||
|
||||
### GET /api/marketplace/request-templates/sellers/:sellerId
|
||||
|
||||
**Description:** Public shop profile for a seller, including their templates and shop settings.
|
||||
**Auth required:** No
|
||||
|
||||
## Purchase Requests
|
||||
|
||||
The buyer-facing CRUD plus seller-side workflow endpoints. Model: [[PurchaseRequest]].
|
||||
|
||||
### POST /api/marketplace/purchase-requests
|
||||
|
||||
**Description:** Create a purchase request (new controller path).
|
||||
**Auth required:** Bearer JWT (buyer)
|
||||
**Request body:**
|
||||
```ts
|
||||
{
|
||||
title: string;
|
||||
description: string;
|
||||
categoryId: string;
|
||||
productLink?: string;
|
||||
size?: string;
|
||||
color?: string;
|
||||
quantity?: number; // default 1
|
||||
budget?: { min?: number; max?: number; currency: "USD" | "EUR" | "IRR" };
|
||||
urgency?: "low" | "medium" | "high";
|
||||
deliveryInfo?: {
|
||||
deliveryType: "physical" | "online";
|
||||
addressId?: string; // when physical
|
||||
email?: string; // when online
|
||||
};
|
||||
preferredSellerIds?: string[];
|
||||
attachments?: string[]; // URLs from [[File API]]
|
||||
}
|
||||
```
|
||||
**Response 201:** `{ success, data: { purchaseRequest } }`
|
||||
**Side effects:** Emits `new-purchase-request` to the `sellers` room (broadcast).
|
||||
**Source:** `marketplaceController.createPurchaseRequest`
|
||||
|
||||
### POST /api/marketplace/purchase-requests/bulk
|
||||
|
||||
**Description:** Bulk creation (used by the template checkout flow).
|
||||
**Auth required:** Bearer JWT
|
||||
**Request body:** `{ items: Array<PurchaseRequestInput> }`
|
||||
|
||||
### GET /api/marketplace/purchase-requests
|
||||
|
||||
**Description:** List requests with filters. Buyers see their own; sellers see ones routed to them; admins see all.
|
||||
**Auth required:** Bearer JWT
|
||||
**Query params:** `status`, `categoryId`, `urgency`, `search`, `page`, `limit`, `sortBy`, `sortOrder`
|
||||
|
||||
### GET /api/marketplace/purchase-requests/my
|
||||
|
||||
**Description:** Shortcut for the caller's own purchase requests.
|
||||
**Auth required:** Bearer JWT
|
||||
|
||||
### GET /api/marketplace/purchase-requests/:id
|
||||
|
||||
**Description:** Single request with populated category and selected offer.
|
||||
**Auth required:** No (public read for shareable links)
|
||||
**Errors:** `404` not found.
|
||||
|
||||
### PATCH /api/marketplace/purchase-requests/:id
|
||||
|
||||
**Description:** Buyer edits draft / pending request fields.
|
||||
**Auth required:** Bearer JWT (owner)
|
||||
|
||||
### PATCH /api/marketplace/purchase-requests/:id/status
|
||||
|
||||
**Description:** Transition the request status (`draft` → `pending` → `payment` → `processing` → `delivery` → `delivered` → `seller_paid` → `completed`, or `cancelled`).
|
||||
**Auth required:** Bearer JWT (owner or admin)
|
||||
**Request body:** `{ status: string }`
|
||||
**Side effects:** Emits `purchase-request-update` to `request-<id>`.
|
||||
|
||||
### DELETE /api/marketplace/purchase-requests/:id
|
||||
|
||||
**Description:** Cancel/delete a request that has no committed payment.
|
||||
**Auth required:** Bearer JWT (owner)
|
||||
|
||||
### GET /api/marketplace/purchase-requests/:id/workflow-steps
|
||||
|
||||
**Description:** Returns the ordered workflow steps + current pointer used by the frontend stepper.
|
||||
**Auth required:** No
|
||||
|
||||
### GET /api/marketplace/purchase-requests/:id/payment-status
|
||||
|
||||
**Description:** Returns the latest payment + escrow state for the request.
|
||||
**Auth required:** Bearer JWT
|
||||
|
||||
### POST /api/marketplace/purchase-requests/:id/sync-payment-status
|
||||
|
||||
**Description:** Force a re-check against the payment provider ([[Payment API]]).
|
||||
**Auth required:** Bearer JWT
|
||||
|
||||
### POST /api/marketplace/purchase-requests/:id/confirm-payment
|
||||
|
||||
**Description:** Legacy buyer-confirmation endpoint kept for old clients.
|
||||
**Auth required:** Bearer JWT
|
||||
|
||||
### POST /api/marketplace/purchase-requests/:id/release-payment
|
||||
|
||||
**Description:** Triggers admin escrow release (mirror of `POST /api/payment/shkeeper/:id/release`).
|
||||
**Auth required:** Bearer JWT (admin)
|
||||
|
||||
### PUT /api/marketplace/purchase-requests/:id/delivery (controller route)
|
||||
|
||||
**Description:** Seller submits shipping details (carrier, tracking number, expected date).
|
||||
**Auth required:** Bearer JWT (selected seller)
|
||||
|
||||
### POST /api/marketplace/purchase-requests/:id/update-delivery (legacy)
|
||||
|
||||
**Description:** Older equivalent retained for compatibility.
|
||||
|
||||
### POST /api/marketplace/requests/:id/start-delivery
|
||||
|
||||
**Description:** Marks the request as `delivery` and notifies the buyer.
|
||||
**Auth required:** Bearer JWT (seller)
|
||||
**Side effects:** Emits `purchase-request-update` event.
|
||||
|
||||
### PATCH /api/marketplace/purchase-requests/:id/confirm-delivery
|
||||
|
||||
**Description:** Buyer confirms goods received (transitions to `delivered`).
|
||||
**Auth required:** Bearer JWT (buyer)
|
||||
|
||||
### POST /api/marketplace/purchase-requests/:id/final-approval
|
||||
|
||||
**Description:** Buyer's final approval that releases escrow to the seller.
|
||||
**Auth required:** Bearer JWT (buyer)
|
||||
|
||||
### GET /api/marketplace/buyers/:buyerId/purchase-requests
|
||||
|
||||
**Description:** Admin/seller view of a buyer's request history.
|
||||
**Auth required:** Bearer JWT
|
||||
|
||||
## Delivery codes
|
||||
|
||||
Six-digit codes the buyer hands to the seller at handover. Backed by `deliveryService`.
|
||||
|
||||
### POST /api/marketplace/purchase-requests/:id/delivery-code/generate
|
||||
|
||||
**Description:** Buyer generates a delivery code. Request must be in `delivery` status.
|
||||
**Auth required:** Bearer JWT (buyer)
|
||||
**Response 200:** `{ success: true, data: { deliveryCode: "123456" } }`
|
||||
**Errors:** `400` wrong status, `403` not buyer, `404` request not found.
|
||||
**Side effects:** Emits `delivery-code-generated` on `request-<id>`.
|
||||
|
||||
### POST /api/marketplace/purchase-requests/:id/delivery-code/verify
|
||||
|
||||
**Description:** Seller verifies the code. On success the status moves to `delivered`.
|
||||
**Auth required:** Bearer JWT (selected seller)
|
||||
**Request body:** `{ code: string }` (exactly 6 chars)
|
||||
**Errors:** `400` bad code, `403` not the selected seller.
|
||||
**Side effects:** Emits `delivery-confirmed` and `buyer-confirmed-delivery`.
|
||||
|
||||
### GET /api/marketplace/purchase-requests/:id/delivery-code
|
||||
|
||||
**Description:** Buyer or seller fetches the current code metadata.
|
||||
**Auth required:** Bearer JWT
|
||||
|
||||
### GET /api/marketplace/purchase-requests/:id/delivery-code/status
|
||||
|
||||
**Description:** Returns `{ isValid, hasDeliveryCode, ... }` so the UI can decide whether to show "regenerate".
|
||||
**Auth required:** Bearer JWT
|
||||
|
||||
## Seller Offers
|
||||
|
||||
Model: [[SellerOffer]].
|
||||
|
||||
### POST /api/marketplace/purchase-requests/:id/offers
|
||||
|
||||
**Description:** Submit an offer against a purchase request.
|
||||
**Auth required:** Bearer JWT (seller)
|
||||
**Request body:**
|
||||
```ts
|
||||
{
|
||||
price: { amount: number; currency: "USD" | "EUR" | "IRR" };
|
||||
deliveryEstimate: { days: number; note?: string };
|
||||
notes?: string;
|
||||
attachments?: string[];
|
||||
}
|
||||
```
|
||||
**Response 201:** `{ success, data: { offer } }`
|
||||
**Side effects:** Emits `new-offer` to `buyer-<buyerId>` and `seller-offer-update` to `seller-<sellerId>`.
|
||||
|
||||
### PUT /api/marketplace/purchase-requests/:id/offers (legacy)
|
||||
|
||||
**Description:** Older offer-update endpoint kept for compatibility.
|
||||
|
||||
### GET /api/marketplace/purchase-requests/:id/offers
|
||||
|
||||
**Description:** List all offers for a request.
|
||||
**Auth required:** No (buyers and prospective sellers compare offers)
|
||||
|
||||
### GET /api/marketplace/purchase-requests/:id/has-offer
|
||||
|
||||
**Description:** Returns `{ hasOffer: boolean }` for the caller (seller-side helper).
|
||||
**Auth required:** Bearer JWT (seller)
|
||||
|
||||
### GET /api/marketplace/purchase-requests/:id/offers/:sellerId
|
||||
|
||||
**Description:** Fetch a specific seller's offer on a request.
|
||||
**Auth required:** No
|
||||
|
||||
### PATCH /api/marketplace/offers/:id
|
||||
|
||||
**Description:** Seller edits their pending offer (price, delivery estimate, notes).
|
||||
**Auth required:** Bearer JWT (offer owner)
|
||||
|
||||
### DELETE /api/marketplace/offers/:id
|
||||
|
||||
**Description:** Seller withdraws their offer.
|
||||
**Auth required:** Bearer JWT (offer owner)
|
||||
|
||||
### PUT /api/marketplace/offers/:id/status
|
||||
|
||||
**Description:** Direct status mutation (admin override / counter-offer states).
|
||||
**Auth required:** Bearer JWT
|
||||
**Request body:** `{ status: "pending" | "accepted" | "rejected" | "withdrawn" | "countered" }`
|
||||
|
||||
### POST /api/marketplace/purchase-requests/:id/select-offer
|
||||
|
||||
**Description:** Buyer selects/accepts an offer; this triggers payment intent creation in [[Payment API]] and rejects all other offers automatically once payment lands.
|
||||
**Auth required:** Bearer JWT (buyer)
|
||||
**Request body:** `{ offerId: string }`
|
||||
**Side effects:**
|
||||
- Updates [[PurchaseRequest]] `selectedOfferId`, status moves toward `payment`.
|
||||
- Emits `seller-offer-update` to all sellers for the request.
|
||||
|
||||
### POST /api/marketplace/offers/:id/accept (legacy)
|
||||
|
||||
**Description:** Older synonym retained for backward compatibility with old clients.
|
||||
|
||||
### DELETE /api/marketplace/offers/:id (controller route)
|
||||
|
||||
**Description:** Controller-pattern delete that also notifies the buyer.
|
||||
|
||||
## Request Templates
|
||||
|
||||
A [[RequestTemplate]] is a re-usable "shop product" a seller can publish. Buyers convert templates into actual purchase requests via the shareable link, individually or in bulk (cart checkout).
|
||||
|
||||
### POST /api/marketplace/request-templates
|
||||
|
||||
**Description:** Create a new template.
|
||||
**Auth required:** Bearer JWT (seller)
|
||||
**Request body:**
|
||||
```ts
|
||||
{
|
||||
title: string; // 1-200 chars
|
||||
description: string; // 1-2000
|
||||
categoryId: string; // MongoId
|
||||
productLink?: string; // valid URL
|
||||
size?: string; // <=100
|
||||
color?: string; // <=100
|
||||
quantity?: number; // 1-10000
|
||||
budget?: { min?: number; max?: number; currency: "USD" | "EUR" | "IRR" };
|
||||
urgency?: "low" | "medium" | "high";
|
||||
deliveryInfo?: { deliveryType: "physical" | "online"; email?: string };
|
||||
maxUsage?: number | null; // 0/null = unlimited
|
||||
expiresAt?: string | null; // ISO date
|
||||
images?: string[]; // URLs from [[File API]]
|
||||
}
|
||||
```
|
||||
**Response 201:** `{ data: { template } }` with a generated `shareableLink`.
|
||||
|
||||
### GET /api/marketplace/request-templates
|
||||
|
||||
**Description:** Paginated list of the caller's templates.
|
||||
**Auth required:** Bearer JWT (seller)
|
||||
**Query params:** `page`, `limit` (1-100), `isActive`, `categoryId`, `search`
|
||||
|
||||
### GET /api/marketplace/request-templates/stats
|
||||
|
||||
**Description:** Aggregate counts of the caller's templates (active, expired, usage).
|
||||
**Auth required:** Bearer JWT (seller)
|
||||
|
||||
### GET /api/marketplace/request-templates/:id
|
||||
|
||||
**Description:** Full template by id (owner view).
|
||||
**Auth required:** Bearer JWT (owner)
|
||||
|
||||
### PUT /api/marketplace/request-templates/:id
|
||||
|
||||
**Description:** Update the template. Same body as create.
|
||||
**Auth required:** Bearer JWT (owner)
|
||||
|
||||
### DELETE /api/marketplace/request-templates/:id
|
||||
|
||||
**Description:** Delete a template.
|
||||
**Auth required:** Bearer JWT (owner)
|
||||
|
||||
### PATCH /api/marketplace/request-templates/:id/toggle-status
|
||||
|
||||
**Description:** Toggle `isActive`.
|
||||
**Auth required:** Bearer JWT (owner)
|
||||
|
||||
### GET /api/marketplace/request-templates/public/:shareableLink
|
||||
|
||||
**Description:** Public read of a template via its shareable slug. Used by the shop preview page.
|
||||
**Auth required:** No
|
||||
**Errors:** `404` link not found or template inactive/expired.
|
||||
|
||||
### POST /api/marketplace/request-templates/:shareableLink/convert
|
||||
|
||||
**Description:** Buyer converts the template into a real [[PurchaseRequest]].
|
||||
**Auth required:** Bearer JWT (buyer)
|
||||
**Request body:** Overrides (quantity, address, etc.)
|
||||
**Response 201:** `{ data: { purchaseRequest } }`
|
||||
|
||||
### POST /api/marketplace/request-templates/batch-convert
|
||||
|
||||
**Description:** Convert several templates at once (cart checkout).
|
||||
**Auth required:** Bearer JWT (buyer)
|
||||
**Request body:**
|
||||
```ts
|
||||
{
|
||||
items: Array<{
|
||||
shareableLink: string;
|
||||
quantity: number; // 1-100
|
||||
sellerId: string; // MongoId
|
||||
}>;
|
||||
status?: "pending" | "pending_payment" | "active";
|
||||
}
|
||||
```
|
||||
|
||||
### POST /api/marketplace/request-templates/complete-payment
|
||||
|
||||
**Description:** Marks the requests created by `batch-convert` as paid (called after a successful checkout).
|
||||
**Auth required:** Bearer JWT
|
||||
**Request body:**
|
||||
```ts
|
||||
{
|
||||
requestIds: string[]; // 1+ MongoIds
|
||||
newStatus?: "pending" | "active" | "processing";
|
||||
paymentData?: Record<string, unknown>;
|
||||
}
|
||||
```
|
||||
|
||||
## Shop Settings
|
||||
|
||||
Per-seller storefront preferences. Model: [[ShopSettings]].
|
||||
|
||||
### GET /api/marketplace/shop/settings/:sellerId
|
||||
|
||||
**Description:** Public shop settings for the given seller (used by the shop landing page).
|
||||
**Auth required:** No
|
||||
**Response 200:** `{ data: ShopSettings }`
|
||||
|
||||
### GET /api/marketplace/shop/settings
|
||||
|
||||
**Description:** The authenticated seller's own settings.
|
||||
**Auth required:** Bearer JWT (seller)
|
||||
|
||||
### PUT /api/marketplace/shop/settings
|
||||
|
||||
**Description:** Update shop settings (banner, bio, policies, `allowSellerReviews`, `allowTemplateReviews`, ...).
|
||||
**Auth required:** Bearer JWT (seller)
|
||||
|
||||
## Reviews
|
||||
|
||||
Model: [[Review]]. Reviews can target a seller or a template. Subject must be `seller` or `template`.
|
||||
|
||||
### GET /api/marketplace/reviews/:subjectType/:subjectId
|
||||
|
||||
**Description:** Published reviews + aggregate stats. Honours `allowSellerReviews` / `allowTemplateReviews` from [[ShopSettings]].
|
||||
**Auth required:** No
|
||||
**Query params:** `page` (default 1), `limit` (default 10)
|
||||
**Response 200:**
|
||||
```json
|
||||
{
|
||||
"data": [Review, ...],
|
||||
"pagination": { "page": 1, "limit": 10, "total": 42, "pages": 5 },
|
||||
"stats": { "count": 42, "avg": 4.6, "one": 1, "two": 0, "three": 3, "four": 10, "five": 28 }
|
||||
}
|
||||
```
|
||||
**Errors:** `400` bad subjectType / invalid id, `403` reviews disabled by seller.
|
||||
|
||||
### GET /api/marketplace/reviews/:subjectType/:subjectId/summary
|
||||
|
||||
**Description:** Stats only (no review list).
|
||||
**Auth required:** No
|
||||
|
||||
### POST /api/marketplace/reviews
|
||||
|
||||
**Description:** Submit a review. The server computes `isVerifiedBuyer` when `purchaseRequestId` is given and the request is in a terminal state (`delivery`, `delivered`, `seller_paid`, `completed`).
|
||||
**Auth required:** Bearer JWT
|
||||
**Request body:**
|
||||
```ts
|
||||
{
|
||||
subjectType: "seller" | "template";
|
||||
subjectId: string;
|
||||
rating: 1 | 2 | 3 | 4 | 5;
|
||||
comment?: string;
|
||||
purchaseRequestId?: string;
|
||||
}
|
||||
```
|
||||
**Errors:** `400` validation, `403` reviews disabled, `404` template not found, `409` duplicate review.
|
||||
**Response 201:** `{ data: Review, stats: { ... } }`
|
||||
|
||||
## Payments (legacy under marketplace)
|
||||
|
||||
These routes are duplicates of the main [[Payment API]] kept under `/api/marketplace/payments/*` for backward compatibility with the early frontend. Prefer the canonical endpoints.
|
||||
|
||||
### POST /api/marketplace/payments
|
||||
### GET /api/marketplace/payments
|
||||
### GET /api/marketplace/payments/:paymentId
|
||||
### PATCH /api/marketplace/payments/:paymentId
|
||||
|
||||
See [[Payment API]] for the canonical descriptions.
|
||||
|
||||
## Verify Web3 payment (legacy)
|
||||
|
||||
### POST /api/marketplace/payments/verify
|
||||
|
||||
**Description:** Legacy Web3 verification endpoint that records a transaction and moves the purchase request to `processing`. Modern flows use `POST /api/payment/shkeeper/confirm-transaction` instead.
|
||||
**Auth required:** Bearer JWT
|
||||
**Request body:**
|
||||
```ts
|
||||
{
|
||||
purchaseRequestId: string;
|
||||
sellerOfferId: string;
|
||||
buyerId: string;
|
||||
sellerId: string;
|
||||
amount: number;
|
||||
currency: "USDT" | string;
|
||||
paymentHash: string;
|
||||
paymentMethod?: string;
|
||||
token?: string;
|
||||
network?: string;
|
||||
escrowWalletAddress?: string;
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
```
|
||||
**Side effects:** Creates a [[Payment]] record, updates [[PurchaseRequest]] status, emits `payment-received` to `user-<sellerId>`.
|
||||
|
||||
## Real-time events
|
||||
|
||||
Most marketplace mutations fan out via `global.io` to the rooms below — see [[Socket Events]] for payloads:
|
||||
|
||||
- `purchase-request-update` → `request-<id>`
|
||||
- `new-purchase-request` → `sellers`
|
||||
- `new-offer` → `buyer-<buyerId>`
|
||||
- `seller-offer-update` → `seller-<sellerId>` (and global on payment confirm)
|
||||
- `delivery-code-generated` / `delivery-confirmed` / `delivery-update` → `request-<id>`
|
||||
- `request-cancelled` → `user-<buyerId>`, `user-<sellerId>`
|
||||
- `transaction-completed` → `user-<buyerId>`, `user-<sellerId>`
|
||||
|
||||
## Related
|
||||
|
||||
- [[Purchase Request Flow]]
|
||||
- [[Seller Offer Flow]]
|
||||
- [[Template Checkout Flow]]
|
||||
- [[Delivery Code Flow]]
|
||||
110
03 - API Reference/Notification API.md
Normal file
110
03 - API Reference/Notification API.md
Normal file
@@ -0,0 +1,110 @@
|
||||
---
|
||||
title: Notification API
|
||||
tags: [api, notification, reference]
|
||||
---
|
||||
|
||||
# Notification API
|
||||
|
||||
Endpoints live under `/api/notifications/*`. Two routers are mounted:
|
||||
|
||||
- New controller pattern: [`notificationControllerRoutes.ts`](../../backend/src/services/notification/notificationControllerRoutes.ts) (controller-backed, requires auth)
|
||||
- Legacy: [`notification/routes.ts`](../../backend/src/services/notification/routes.ts) (no auth gate — userId is passed in query/body)
|
||||
|
||||
Both routers are mounted at `/api`, so the paths collide; the controller router wins for the shared paths (it is mounted first). The legacy router is still used by background scripts and admin tools that have no JWT context.
|
||||
|
||||
Model: [[Notification]]. Real-time delivery is via `new-notification` and `unread-count-update` Socket.IO events on `user-<userId>`. See [[Socket Events]].
|
||||
|
||||
## List
|
||||
|
||||
### GET /api/notifications
|
||||
|
||||
**Description:** Paginated notifications for the caller.
|
||||
**Auth required:** Bearer JWT (controller route); legacy variant takes `?userId=...`.
|
||||
**Query params:**
|
||||
- `page` (default 1)
|
||||
- `limit` (default 20)
|
||||
- `unreadOnly` (`true` | `false`, default `false`)
|
||||
**Response 200:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"notifications": [Notification, ...],
|
||||
"pagination": { "page": 1, "limit": 20, "total": 42, "hasMore": true }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### GET /api/notifications/unread-count
|
||||
|
||||
**Description:** Just the integer unread count.
|
||||
**Auth required:** Bearer JWT
|
||||
**Response 200:** `{ "success": true, "data": { "unreadCount": 5 } }`
|
||||
|
||||
### GET /api/notifications/:id
|
||||
|
||||
**Description:** Single notification by id. Available on the controller router.
|
||||
**Auth required:** Bearer JWT
|
||||
**Errors:** `404` not found, `403` not owner.
|
||||
|
||||
## Mutations
|
||||
|
||||
### PATCH /api/notifications/:id/read
|
||||
|
||||
**Description:** Mark one notification as read.
|
||||
**Auth required:** Bearer JWT (controller); legacy variant requires `{ userId }` in body.
|
||||
**Response 200:** `{ success: true, data: { /* updated notification */ } }`
|
||||
**Side effects:** Emits `unread-count-update` to `user-<userId>`.
|
||||
|
||||
### PATCH /api/notifications/mark-all-read
|
||||
|
||||
**Description:** Mark every notification for the caller as read.
|
||||
**Auth required:** Bearer JWT
|
||||
**Response 200:** `{ "success": true, "data": { "modifiedCount": 12 } }`
|
||||
|
||||
### PATCH /api/notifications/bulk/mark-read
|
||||
|
||||
**Description:** Mark a list of notifications as read.
|
||||
**Auth required:** Bearer JWT
|
||||
**Request body:** `{ ids: string[] }`
|
||||
**Response 200:** `{ success, data: { modifiedCount } }`
|
||||
|
||||
### DELETE /api/notifications/:id
|
||||
|
||||
**Description:** Delete a notification.
|
||||
**Auth required:** Bearer JWT
|
||||
**Errors:** `404` not found.
|
||||
|
||||
### DELETE /api/notifications/bulk/delete
|
||||
|
||||
**Description:** Bulk delete.
|
||||
**Auth required:** Bearer JWT
|
||||
**Request body:** `{ ids: string[] }`
|
||||
|
||||
### POST /api/notifications
|
||||
|
||||
**Description:** Create a notification. Primarily used by other services and admin tools.
|
||||
**Auth required:** Bearer JWT (controller); legacy variant is open.
|
||||
**Request body:**
|
||||
```ts
|
||||
{
|
||||
userId: string;
|
||||
type: string; // e.g. "order_update", "chat_message", "payment_received"
|
||||
title: string;
|
||||
body?: string;
|
||||
data?: Record<string, unknown>;
|
||||
channel?: "in_app" | "email" | "push";
|
||||
}
|
||||
```
|
||||
**Response 201:** `{ success, data: { notification } }`
|
||||
**Side effects:** Emits `new-notification` to `user-<userId>`; also increments unread count via `unread-count-update`.
|
||||
|
||||
## Preferences
|
||||
|
||||
Notification preferences live on [[User]] (`preferences.notifications.email | sms | push`). They are read and written through the [[User API]] (`GET /api/user/profile`, `PUT /api/user/profile`).
|
||||
|
||||
## Related
|
||||
|
||||
- [[Notification]]
|
||||
- [[Notification Flow]]
|
||||
- [[Socket Events]]
|
||||
387
03 - API Reference/Payment API.md
Normal file
387
03 - API Reference/Payment API.md
Normal file
@@ -0,0 +1,387 @@
|
||||
---
|
||||
title: Payment API
|
||||
tags: [api, payment, reference, shkeeper]
|
||||
---
|
||||
|
||||
# Payment API
|
||||
|
||||
The payment surface is split across four routers, all mounted under `/api/payment/*`:
|
||||
|
||||
| Path prefix | File | Purpose |
|
||||
| --- | --- | --- |
|
||||
| `/api/payment/*` | [`paymentControllerRoutes.ts`](../../backend/src/services/payment/paymentControllerRoutes.ts) | New controller pattern (CRUD + configuration) |
|
||||
| `/api/payment/*` | [`paymentRoutes.ts`](../../backend/src/services/payment/paymentRoutes.ts) | Additional legacy endpoints (tx fetch, exports) |
|
||||
| `/api/payment/decentralized/*` | [`decentralizedPaymentRoutes.ts`](../../backend/src/services/payment/decentralizedPaymentRoutes.ts) | DePay / Web3 confirmations |
|
||||
| `/api/payment/shkeeper/*` | [`shkeeper/shkeeperRoutes.ts`](../../backend/src/services/payment/shkeeper/shkeeperRoutes.ts) | SHKeeper pay-in, webhook, release/refund |
|
||||
| `/api/payment/shkeeper/payout*` | [`shkeeper/shkeeperPayoutRoutes.ts`](../../backend/src/services/payment/shkeeper/shkeeperPayoutRoutes.ts) | SHKeeper payouts to sellers |
|
||||
|
||||
Core model: [[Payment]]. Coordination logic to avoid race conditions when multiple sources update the same payment is in `paymentCoordinator.ts`.
|
||||
|
||||
## Configuration / health
|
||||
|
||||
### POST /api/payment/configuration
|
||||
|
||||
**Description:** Returns the payment provider configuration the SHKeeper widget needs (accepted blockchains, escrow receiver address, redirect URLs, webhook URL).
|
||||
**Auth required:** No
|
||||
**Request body:** `{ amount?, currency?, purchaseRequestId? }` (used to scope returned config)
|
||||
**Response 200:**
|
||||
```json
|
||||
{
|
||||
"accept": [{ "blockchain": "bsc", "token": "0x55d3...", "receiver": "0xa30..." }],
|
||||
"redirect": { "success": "...", "cancel": "..." },
|
||||
"webhook": "https://.../api/payment/shkeeper/webhook"
|
||||
}
|
||||
```
|
||||
|
||||
### GET /api/payment/health
|
||||
|
||||
**Description:** Lightweight health probe.
|
||||
**Auth required:** No
|
||||
**Response 200:** `{ success, message, endpoints: { shkeeper, decentralized, health } }`
|
||||
|
||||
### GET /api/payment/shkeeper/config
|
||||
|
||||
**Description:** Same payload as `/configuration` but tailored for the SHKeeper-hosted widget; includes explicit CORS `*` headers.
|
||||
**Auth required:** No
|
||||
|
||||
## Payment records (CRUD)
|
||||
|
||||
### POST /api/payment
|
||||
|
||||
**Description:** Create a payment record (manual entry — usually the SHKeeper intent path is preferred).
|
||||
**Auth required:** Bearer JWT
|
||||
**Request body:**
|
||||
```ts
|
||||
{
|
||||
purchaseRequestId: string;
|
||||
sellerOfferId: string;
|
||||
buyerId: string;
|
||||
sellerId: string;
|
||||
amount: { amount: number; currency: string };
|
||||
blockchain?: { network: string; token: string };
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
```
|
||||
**Response 201:** `{ /* Payment document */ }`
|
||||
|
||||
### PUT /api/payment/:id
|
||||
|
||||
**Description:** Update a payment record (status, transactionHash, metadata).
|
||||
**Auth required:** Bearer JWT
|
||||
**Request body:**
|
||||
```ts
|
||||
{
|
||||
status?: "pending" | "processing" | "completed" | "failed" | "cancelled";
|
||||
transactionHash?: string;
|
||||
blockchain?: { ... };
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
```
|
||||
|
||||
### GET /api/payment
|
||||
|
||||
**Description:** List the caller's payments (defaults to `completed,success` status).
|
||||
**Auth required:** Bearer JWT
|
||||
**Query params:** `status`, `limit` (default 50), `offset` (default 0)
|
||||
|
||||
### GET /api/payment/:id
|
||||
|
||||
**Description:** Fetch a payment by id.
|
||||
**Auth required:** Bearer JWT
|
||||
**Errors:** `404` not found.
|
||||
|
||||
### GET /api/payment/:id/debug
|
||||
|
||||
**Description:** Debug bundle including the raw payment, blockchain metadata, and wallet-monitor status.
|
||||
**Auth required:** Bearer JWT
|
||||
**Notes:** Intended for admin / development.
|
||||
|
||||
### GET /api/payment/user/:userId
|
||||
|
||||
**Description:** Payments for a specific user (admin or self).
|
||||
**Auth required:** Bearer JWT
|
||||
**Query params:** `status`, `limit`, `offset`
|
||||
|
||||
### GET /api/payment/stats / GET /api/payment/stats/:userId
|
||||
|
||||
**Description:** Aggregated counts and sums per status.
|
||||
**Auth required:** Bearer JWT
|
||||
|
||||
### GET /api/payment/export / GET /api/payment/export/:userId
|
||||
|
||||
**Description:** Export payments as `json` or `csv`.
|
||||
**Auth required:** Bearer JWT
|
||||
**Query params:** `format=json|csv`
|
||||
|
||||
### POST /api/payment/payments/cleanup-pending
|
||||
|
||||
**Description:** Admin cleanup of stale `pending` payments.
|
||||
**Auth required:** Bearer JWT (admin)
|
||||
**Response 200:** `{ success, deletedCount, message }`
|
||||
|
||||
### POST /api/payment/payments/:id/fetch-tx
|
||||
|
||||
**Description:** Re-queries the blockchain to fetch the missing `transactionHash` for a completed payment.
|
||||
**Auth required:** Bearer JWT
|
||||
**Response 200:** `{ success, transactionHash, network, source, message }`
|
||||
|
||||
### POST /api/payment/payments/auto-fetch-missing
|
||||
|
||||
**Description:** Batch tx-hash backfill across the database.
|
||||
**Auth required:** Bearer JWT
|
||||
**Request body:** `{ limit?: number }` (default 10)
|
||||
|
||||
### POST /api/payment/callback
|
||||
|
||||
**Description:** Generic payment callback (called by the older client SDK).
|
||||
**Auth required:** No (verified by `paymentRef` matching)
|
||||
**Request body:** `{ paymentId, transactionHash, status, data }`
|
||||
|
||||
### POST /api/payment/verify
|
||||
|
||||
**Description:** Frontend verification endpoint used by the Web3 flow. Confirms a payment and updates the related [[PurchaseRequest]].
|
||||
**Auth required:** Bearer JWT
|
||||
|
||||
## SHKeeper - Pay-in
|
||||
|
||||
### POST /api/payment/shkeeper/intents
|
||||
|
||||
**Description:** Creates a SHKeeper pay-in intent. The server provisions an invoice on SHKeeper, stores a [[Payment]] with `provider: "shkeeper"`, `direction: "in"`, returns the hosted-widget URL.
|
||||
**Auth required:** Bearer JWT (buyer)
|
||||
**Request body:**
|
||||
```ts
|
||||
{
|
||||
purchaseRequestId: string;
|
||||
sellerOfferId: string;
|
||||
amount: number;
|
||||
sellerId: string;
|
||||
token?: string; // default "USDT"
|
||||
network?: string; // default "bsc"
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
```
|
||||
**Response 200:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"paymentId": "...",
|
||||
"paymentUrl": "https://pay.amn.gg/invoice/...",
|
||||
"externalId": "AMN_...",
|
||||
"expiresAt": "2026-05-23T11:00:00.000Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
**Errors:** `400` missing fields, `401` not authenticated, `500` SHKeeper error.
|
||||
**Side effects:** Emits `payment-created` globally and to the request room.
|
||||
|
||||
### POST /api/payment/shkeeper/webhook
|
||||
|
||||
**Description:** SHKeeper posts here when an invoice changes state. Handles both raw-string and JSON bodies and verifies the HMAC signature (`x-shkeeper-signature` against the raw body using `SHKEEPER_WEBHOOK_SECRET`).
|
||||
**Auth required:** No (signature-protected)
|
||||
**Body:** The SHKeeper callback envelope (`external_id`, `crypto`, `addr`, `fiat`, `balance_fiat`, `balance_crypto`, `paid`, `status`, `transactions[]`).
|
||||
**Response 200:** `{ success: true }`
|
||||
**Side effects:**
|
||||
- Updates the matching [[Payment]] to `completed` (`OVERPAID` and `PAID` both count).
|
||||
- Releases or rejects [[SellerOffer]] siblings (the chosen offer becomes `accepted`, others `rejected`).
|
||||
- Updates [[PurchaseRequest]] status to `payment` / `processing`.
|
||||
- Emits `seller-offer-update` to each affected seller room and `purchase-request-update` to the request room.
|
||||
|
||||
### POST /api/payment/shkeeper/confirm-transaction
|
||||
|
||||
**Description:** Manual fallback when the webhook misses — the frontend calls this after the buyer signs the EVM transaction directly. Coordinated through `PaymentCoordinator` to avoid double updates.
|
||||
**Auth required:** Bearer JWT
|
||||
**Request body:** `{ paymentId, transactionHash, network? }`
|
||||
**Response 200:** `{ success, message, data: { paymentId, transactionHash, status } }`
|
||||
**Side effects:** Closes the SHKeeper invoice session, then runs the same offer/request updates as the webhook.
|
||||
|
||||
### POST /api/payment/shkeeper/test
|
||||
|
||||
**Description:** Smoke-tests the real SHKeeper API. Development only.
|
||||
**Auth required:** No
|
||||
|
||||
### POST /api/payment/shkeeper/callback-test (and GET equivalent)
|
||||
|
||||
**Description:** Echo-style endpoints used during webhook configuration.
|
||||
**Auth required:** No
|
||||
|
||||
### POST /api/payment/shkeeper/create-test-payment
|
||||
|
||||
**Description:** Inserts a sample [[Payment]] row to exercise the webhook handler.
|
||||
**Auth required:** No
|
||||
|
||||
### POST /api/payment/shkeeper/trigger-webhook
|
||||
|
||||
**Description:** Sends a fake webhook payload to the local webhook endpoint for end-to-end testing.
|
||||
**Auth required:** Bearer JWT
|
||||
|
||||
### GET /api/payment/shkeeper/wallet-monitor/status
|
||||
|
||||
**Description:** Returns the wallet-monitor state (`isMonitoring`, watched wallet addresses).
|
||||
**Auth required:** No
|
||||
|
||||
### GET /api/payment/shkeeper/auto-webhook/status
|
||||
|
||||
**Description:** Returns the auto-webhook fallback monitor state.
|
||||
**Auth required:** No
|
||||
|
||||
### GET /api/payment/shkeeper/webhook-stats
|
||||
|
||||
**Description:** Counters for webhook deliveries (success / failure / duplicates).
|
||||
**Auth required:** Bearer JWT (admin)
|
||||
|
||||
## SHKeeper - Release / Refund (escrow)
|
||||
|
||||
These build an admin-signed transaction off-chain and require a follow-up confirm with the broadcast tx hash. Source: `shkeeperService.buildAdminSignedTxPayload` and `confirmAdminTx`.
|
||||
|
||||
### POST /api/payment/shkeeper/:id/release
|
||||
|
||||
**Description:** Prepares the admin-signed payload to release escrow to the seller. Returns the raw payload — the admin client signs and broadcasts.
|
||||
**Auth required:** Bearer JWT (admin)
|
||||
**Response 200:** `{ success: true, data: { /* tx payload */ } }`
|
||||
|
||||
### POST /api/payment/shkeeper/:id/release/confirm
|
||||
|
||||
**Description:** Records the broadcast transaction hash for the release; marks the payment as released, updates [[PurchaseRequest]] to `seller_paid` and emits `purchase-request-update` (`type: payment_released`).
|
||||
**Auth required:** Bearer JWT (admin)
|
||||
**Request body:** `{ txHash: string }`
|
||||
**Errors:** `400` missing `txHash`.
|
||||
|
||||
### POST /api/payment/shkeeper/:id/refund
|
||||
|
||||
**Description:** Mirror of release, but returns the escrow to the buyer.
|
||||
**Auth required:** Bearer JWT (admin)
|
||||
|
||||
### POST /api/payment/shkeeper/:id/refund/confirm
|
||||
|
||||
**Description:** Records the refund tx hash; emits `purchase-request-update` (`type: payment_refunded`).
|
||||
**Auth required:** Bearer JWT (admin)
|
||||
**Request body:** `{ txHash: string }`
|
||||
|
||||
## SHKeeper - Payouts
|
||||
|
||||
Payouts are SHKeeper-side outbound transfers (admin pays the seller from a hot wallet).
|
||||
|
||||
### POST /api/payment/shkeeper/payout
|
||||
|
||||
**Description:** Create a payout task. The server creates a [[Payment]] row with `direction: "out"` and provider task id, then returns the SHKeeper task descriptor.
|
||||
**Auth required:** Bearer JWT (admin)
|
||||
**Request body:**
|
||||
```ts
|
||||
{
|
||||
purchaseRequestId: string;
|
||||
sellerOfferId: string;
|
||||
buyerId: string;
|
||||
sellerId: string;
|
||||
amount: number;
|
||||
recipientAddress: string;
|
||||
token?: string; // default "USDT"
|
||||
network?: string; // default "bsc"
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
```
|
||||
**Response 200:** `{ success, data: { payoutId, taskId, status }, message }`
|
||||
**Side effects:** `emitGlobalEvent('payout-created', { ... })`.
|
||||
**Errors:** `400` for each missing required field, `500` upstream error.
|
||||
|
||||
### GET /api/payment/shkeeper/payout/status/:taskId
|
||||
|
||||
**Description:** Polls SHKeeper for the current task status.
|
||||
**Auth required:** Bearer JWT
|
||||
**Response 200:** `{ success, data: { /* status payload */ } }`
|
||||
|
||||
### POST /api/payment/shkeeper/payout/webhook
|
||||
|
||||
**Description:** SHKeeper webhook for payout state changes. Handled by `processPayoutWebhookEvent`. Emits `payout-completed` (or `payout-updated`) global socket events on success.
|
||||
**Auth required:** No (signature checked)
|
||||
**Response 200/400:** `{ success, message, data }`
|
||||
|
||||
## DePay / Web3 (decentralized)
|
||||
|
||||
### POST /api/payment/decentralized/save
|
||||
|
||||
**Description:** Persists a Web3-initiated payment record.
|
||||
**Auth required:** No
|
||||
**Request body:**
|
||||
```ts
|
||||
{
|
||||
purchaseRequestId: string;
|
||||
buyerId: string;
|
||||
sellerId?: string;
|
||||
amount: number;
|
||||
currency?: string;
|
||||
transactionHash?: string;
|
||||
network?: string;
|
||||
token?: string;
|
||||
walletAddress?: string;
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
```
|
||||
|
||||
### GET /api/payment/decentralized/status/:paymentId
|
||||
|
||||
**Description:** Returns the latest status for a decentralized payment.
|
||||
**Auth required:** No
|
||||
|
||||
### PUT /api/payment/decentralized/update
|
||||
|
||||
**Description:** Update a decentralized payment's status / confirmations.
|
||||
**Auth required:** No
|
||||
**Request body:** `{ paymentId, status, confirmations? }`
|
||||
|
||||
### GET /api/payment/decentralized/receiver
|
||||
|
||||
**Description:** Returns the configured escrow receiver wallet address.
|
||||
**Auth required:** No
|
||||
|
||||
### GET /api/payment/decentralized/history/:userId
|
||||
|
||||
**Description:** Decentralized payment history for a user.
|
||||
**Auth required:** No
|
||||
|
||||
### POST /api/payment/decentralized/verify/:paymentId
|
||||
|
||||
**Description:** Re-verifies a single decentralized payment against the chain.
|
||||
**Auth required:** No
|
||||
|
||||
### POST /api/payment/decentralized/verify-all-pending
|
||||
|
||||
**Description:** Iterates all `pending` decentralized payments and re-verifies them.
|
||||
**Auth required:** No (typically called by a cron)
|
||||
|
||||
### POST /api/payment/decentralized/admin-payout
|
||||
|
||||
**Description:** Pay a seller directly from an admin hot wallet (no SHKeeper).
|
||||
**Auth required:** Bearer JWT (admin)
|
||||
**Request body:**
|
||||
```ts
|
||||
{
|
||||
purchaseRequestId: string;
|
||||
receiverWalletAddress: string;
|
||||
amount: number;
|
||||
currency?: string; // default "USDT"
|
||||
network?: string; // default "BSC"
|
||||
}
|
||||
```
|
||||
**Response 200:** `{ success, data: { /* payout receipt */ } }`
|
||||
|
||||
## Status model
|
||||
|
||||
[[Payment]] uses the statuses below across all providers:
|
||||
|
||||
- `pending` - intent created, awaiting on-chain settlement
|
||||
- `processing` - settlement seen, awaiting confirmations
|
||||
- `completed` - confirmed, escrow funded
|
||||
- `failed` - intentionally failed (expired, declined, refused)
|
||||
- `cancelled` - cancelled by user/admin
|
||||
- `released` - escrow released to seller (`shkeeper` flow)
|
||||
- `refunded` - escrow returned to buyer
|
||||
|
||||
Escrow state (`escrowState`): `unfunded` → `funded` → `released` | `refunded`.
|
||||
|
||||
## Related
|
||||
|
||||
- [[Payment Flow]]
|
||||
- [[Escrow Flow]]
|
||||
- [[SHKeeper Webhook Flow]]
|
||||
- [[Socket Events]]
|
||||
140
03 - API Reference/Points API.md
Normal file
140
03 - API Reference/Points API.md
Normal file
@@ -0,0 +1,140 @@
|
||||
---
|
||||
title: Points API
|
||||
tags: [api, points, reference]
|
||||
---
|
||||
|
||||
# Points API
|
||||
|
||||
Endpoints live under `/api/points/*`. The router is [`backend/src/routes/pointsRoutes.ts`](../../backend/src/routes/pointsRoutes.ts), delegating to [`PointsController`](../../backend/src/controllers/pointsController.ts) and `PointsService` ([`backend/src/services/points/PointsService.ts`](../../backend/src/services/points/PointsService.ts)). The router applies `authenticateToken` globally — every endpoint requires `Bearer JWT`.
|
||||
|
||||
Models: [[PointTransaction]], [[LevelConfig]]. Points are minted automatically by the platform (referral signup, successful purchases, reviews) and can be redeemed for discounts or marketplace credits. Levels progress as the user's lifetime points cross configured thresholds.
|
||||
|
||||
## Balance and history
|
||||
|
||||
### GET /api/points/my-points
|
||||
|
||||
**Description:** Caller's current balance, lifetime totals, current level, referral code, and progress to next level.
|
||||
**Auth required:** Bearer JWT
|
||||
**Response 200:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"balance": 320,
|
||||
"lifetimePoints": 1280,
|
||||
"level": { "name": "silver", "tier": 2, "minPoints": 1000, "perks": [...] },
|
||||
"nextLevel": { "name": "gold", "tier": 3, "minPoints": 2500, "pointsToGo": 1220 },
|
||||
"referralCode": "ABCD1234",
|
||||
"referralsCount": 4
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### GET /api/points/transactions
|
||||
|
||||
**Description:** Paginated point ledger.
|
||||
**Auth required:** Bearer JWT
|
||||
**Query params:**
|
||||
- `page` (default 1), `limit` (default 20)
|
||||
- `type` (`earn` | `redeem` | `referral` | `purchase` | `review` | `admin_grant` | `admin_deduct`)
|
||||
- `from` / `to` (ISO dates)
|
||||
**Response 200:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"transactions": [PointTransaction, ...],
|
||||
"pagination": { "page": 1, "limit": 20, "total": 17, "hasMore": false }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### GET /api/points/referrals
|
||||
|
||||
**Description:** Users referred by the caller plus the points earned from each.
|
||||
**Auth required:** Bearer JWT
|
||||
**Response 200:** `{ success, data: { referrals: [{ userId, name, joinedAt, pointsEarned, status }] } }`
|
||||
|
||||
### GET /api/points/levels
|
||||
|
||||
**Description:** Public list of every configured level (from [[LevelConfig]]). Used by the marketing / levels page.
|
||||
**Auth required:** Bearer JWT (but data is non-sensitive)
|
||||
**Response 200:** `{ success, data: { levels: [LevelConfig, ...] } }`
|
||||
|
||||
### GET /api/points/leaderboard
|
||||
|
||||
**Description:** Top referrers by referral count and points earned. Used for community displays.
|
||||
**Auth required:** Bearer JWT
|
||||
**Query params:** `limit` (default 10), `period` (`all` | `month` | `week`)
|
||||
**Response 200:** `{ success, data: { entries: [{ userId, name, avatar, referrals, pointsEarned }] } }`
|
||||
|
||||
## Mutations
|
||||
|
||||
### POST /api/points/redeem
|
||||
|
||||
**Description:** Redeem points for a marketplace credit / discount. Server validates available balance and configured redemption rate.
|
||||
**Auth required:** Bearer JWT
|
||||
**Request body:**
|
||||
```ts
|
||||
{
|
||||
amount: number; // points to redeem
|
||||
purpose?: "wallet_credit" | "discount_code";
|
||||
purchaseRequestId?: string; // when applying to an in-progress purchase
|
||||
}
|
||||
```
|
||||
**Response 200:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"transaction": { /* PointTransaction */ },
|
||||
"redemption": { "creditAmount": 3.20, "currency": "USD", "code": "DISC-..." },
|
||||
"newBalance": 0
|
||||
}
|
||||
}
|
||||
```
|
||||
**Errors:** `400` insufficient balance, validation; `409` redemption already claimed.
|
||||
|
||||
### POST /api/points/generate-referral-code
|
||||
|
||||
**Description:** Generates (or rotates) the caller's unique referral code. Idempotent if the user already has one and `force` is not set.
|
||||
**Auth required:** Bearer JWT
|
||||
**Request body:** `{ force?: boolean }`
|
||||
**Response 200:** `{ success, data: { referralCode: "ABCD1234", link: "https://amn.gg/r/ABCD1234" } }`
|
||||
|
||||
The short link redirect (`GET /r/:code`) is mounted at the app root in `app.ts` and forwards to `${FRONTEND_URL}/auth/jwt/sign-up?ref=<code>`.
|
||||
|
||||
## Admin
|
||||
|
||||
### POST /api/points/admin/add
|
||||
|
||||
**Description:** Manually grant (or deduct, with negative amount) points to a user. Logged as a `PointTransaction` with `type: "admin_grant"` / `"admin_deduct"`.
|
||||
**Auth required:** Bearer JWT (admin)
|
||||
**Request body:**
|
||||
```ts
|
||||
{
|
||||
userId: string;
|
||||
amount: number; // positive = grant; negative = deduct
|
||||
reason: string; // mandatory free-text reason
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
```
|
||||
**Response 200:** `{ success, data: { transaction, newBalance } }`
|
||||
**Errors:** `403` non-admin, `404` target user not found, `400` would go below zero.
|
||||
|
||||
## Side effects and real-time
|
||||
|
||||
`PointsService` emits Socket.IO events on level-up and referral rewards:
|
||||
|
||||
- `level-up` on `user-<userId>` when a transaction crosses a level threshold.
|
||||
- `referral-reward` on `user-<referrerId>` when a referred user triggers a reward.
|
||||
|
||||
See [[Socket Events]] for payload shape.
|
||||
|
||||
## Related
|
||||
|
||||
- [[PointTransaction]]
|
||||
- [[LevelConfig]]
|
||||
- [[Points and Levels Flow]]
|
||||
- [[Referral Flow]]
|
||||
- `backend/POINTS_MIGRATION.md` (legacy migration notes)
|
||||
152
03 - API Reference/Socket Events.md
Normal file
152
03 - API Reference/Socket Events.md
Normal file
@@ -0,0 +1,152 @@
|
||||
---
|
||||
title: Socket Events
|
||||
tags: [api, socket, realtime, reference]
|
||||
---
|
||||
|
||||
# Socket Events
|
||||
|
||||
The backend runs a Socket.IO server on the same HTTP port as the REST API. It is initialised in [`backend/src/app.ts`](../../backend/src/app.ts) and exposed globally as `global.io`. Helper functions for emitting events from services live in [`backend/src/infrastructure/socket/socketService.ts`](../../backend/src/infrastructure/socket/socketService.ts):
|
||||
|
||||
```ts
|
||||
emitGlobalEvent(event, data); // io.emit
|
||||
emitToRoom(room, event, data); // io.to(room).emit
|
||||
setSocketServer(server);
|
||||
getSocketServer();
|
||||
```
|
||||
|
||||
## Connection
|
||||
|
||||
```ts
|
||||
import { io } from "socket.io-client";
|
||||
|
||||
const socket = io(process.env.NEXT_PUBLIC_API_URL!, {
|
||||
withCredentials: true,
|
||||
transports: ["websocket"],
|
||||
});
|
||||
```
|
||||
|
||||
The CORS policy mirrors the REST API: only `FRONTEND_URL` is allowed, and credentials are enabled. There is no token-based handshake — the server identifies users by the `join-*` events the client emits after connecting. (A future improvement would authenticate via the JWT on handshake; for now, ownership is checked at the REST layer when a mutation triggers an event.)
|
||||
|
||||
The server logs `🔌 User connected: <socketId>` on connect and `🔌 User disconnected: <socketId>` on disconnect. There is no broadcast `user-offline` on disconnect — see "Online status" below.
|
||||
|
||||
## Rooms
|
||||
|
||||
Rooms are the targeting mechanism. The client joins/leaves rooms with these events:
|
||||
|
||||
| Client → Server | Room joined |
|
||||
| --- | --- |
|
||||
| `join-user-room` (userId) | `user-<userId>` |
|
||||
| `join-request-room` (requestId) | `request-<requestId>` |
|
||||
| `leave-request-room` (requestId) | leaves `request-<requestId>` |
|
||||
| `join-seller-room` (sellerId) | `seller-<sellerId>` + `sellers` (global) |
|
||||
| `leave-seller-room` (sellerId) | leaves both |
|
||||
| `join-buyer-room` (buyerId) | `buyer-<buyerId>` + `buyers` (global) |
|
||||
| `leave-buyer-room` (buyerId) | leaves both |
|
||||
| `join-chat-room` (chatId) | `chat-<chatId>` |
|
||||
| `leave-chat-room` (chatId) | leaves `chat-<chatId>` |
|
||||
| `user-online` (userId) | joins `user-<userId>`, broadcasts `user-status-change` |
|
||||
| `typing-start` ({ chatId, userId, userName }) | broadcasts `user-typing` to `chat-<chatId>` |
|
||||
| `typing-stop` ({ chatId, userId }) | broadcasts `user-typing` (isTyping=false) |
|
||||
|
||||
Joining a room is unauthenticated — clients are expected to only join their own rooms. Sensitive data is filtered at the REST layer that emits the event.
|
||||
|
||||
## Server → Client events
|
||||
|
||||
Grouped by the service that emits them.
|
||||
|
||||
### Marketplace
|
||||
|
||||
| Event | Room | Payload | Source |
|
||||
| --- | --- | --- | --- |
|
||||
| `new-purchase-request` | `sellers` | `PurchaseRequest` document | `marketplaceController.createPurchaseRequest` |
|
||||
| `new-offer` | `buyer-<buyerId>` | `{ requestId, offer, sellerId }` | `marketplaceController.createSellerOffer` |
|
||||
| `seller-offer-update` | `seller-<sellerId>` (and global on payment confirm) | `{ sellerId, requestId, eventType: "payment-completed" \| "offer-rejected" \| "offer-accepted", data: { offerId, isSelected, paymentId?, transactionHash?, reason? } }` | `marketplaceController`, `shkeeperRoutes`, `shkeeperWebhook`, `SellerOfferService` |
|
||||
| `purchase-request-update` | `request-<requestId>` | `{ type, requestId, status?, paymentId?, txHash?, provider? }` | `marketplaceController`, `PurchaseRequestService`, `shkeeperRoutes`, `paymentCoordinator` |
|
||||
| `request-cancelled` | `user-<buyerId>`, `user-<sellerId>` | `{ requestId, reason }` | `PurchaseRequestService` |
|
||||
| `transaction-completed` | `user-<buyerId>`, `user-<sellerId>` | `{ requestId, paymentId, amount, currency }` | `marketplaceController` |
|
||||
| `delivery-code-generated` | `request-<requestId>` | `{ requestId, deliveryCode }` (only the seller UI uses this) | `DeliveryService` |
|
||||
| `delivery-update` | `request-<requestId>` | `{ requestId, status, carrier?, trackingNumber? }` | `DeliveryService` |
|
||||
| `delivery-confirmed` | `request-<requestId>` | `{ requestId }` | `DeliveryService` |
|
||||
| `buyer-confirmed-delivery` | `user-<sellerId>` | `{ requestId, buyerId }` | `DeliveryService` |
|
||||
| `template-checkout-payment-confirmed` | global + `template-checkout-<id>` | `{ checkoutId, requestIds, paymentId }` | `templateCheckoutWebhook`, `paymentCoordinator` |
|
||||
| `template-checkout-payment-pending` | global | `{ checkoutId }` | `templateCheckoutWebhook` |
|
||||
| `template-checkout-payment-failed` | global | `{ checkoutId, reason }` | `templateCheckoutWebhook` |
|
||||
|
||||
### Payment
|
||||
|
||||
| Event | Room | Payload | Source |
|
||||
| --- | --- | --- | --- |
|
||||
| `payment-created` | global | `{ paymentId, provider, requestId, buyerId, sellerId, amount, currency }` | `shkeeperService`, `decentralizedPaymentService` |
|
||||
| `payment-received` | `user-<sellerId>` | `{ purchaseRequestId, amount, currency, buyerId }` | `paymentRoutes`, `marketplace/routes` |
|
||||
| `payment-update` | global + room-specific | `{ paymentId, status, escrowState?, txHash? }` | `paymentCoordinator` |
|
||||
| `payout-created` | global | `{ payoutId, taskId, sellerId, amount, currency }` | `shkeeperPayoutService` |
|
||||
| `payout-completed` | global, `user-<sellerId>` | `{ payoutId, taskId, txHash }` | `shkeeperPayoutService`, `decentralizedPaymentService` |
|
||||
| `payout-updated` | global | `{ payoutId, status }` | `shkeeperPayoutService` |
|
||||
|
||||
### Chat
|
||||
|
||||
| Event | Room | Payload |
|
||||
| --- | --- | --- |
|
||||
| `new-message` | `chat-<chatId>` | `{ chatId, message: { _id, content, senderId, createdAt, attachments? }, senderId }` |
|
||||
| `messages-read` | `chat-<chatId>` | `{ chatId, userId, upToMessageId, modifiedCount }` |
|
||||
| `message-edited` | `chat-<chatId>` | `{ chatId, messageId, content, editedAt }` |
|
||||
| `message-deleted` | `chat-<chatId>` | `{ chatId, messageId, deletedAt }` |
|
||||
| `participants-added` | `chat-<chatId>` | `{ chatId, addedUserIds }` |
|
||||
| `participant-removed` | `chat-<chatId>` | `{ chatId, removedUserId }` |
|
||||
| `user-typing` | `chat-<chatId>` | `{ userId, userName?, isTyping }` |
|
||||
| `user-status-change` | broadcast | `{ userId, status: "online", lastSeen }` |
|
||||
|
||||
Sources: [`ChatService.ts`](../../backend/src/services/chat/ChatService.ts), [`chatController.ts`](../../backend/src/services/chat/chatController.ts), and `app.ts` socket handlers.
|
||||
|
||||
### Notification
|
||||
|
||||
| Event | Room | Payload |
|
||||
| --- | --- | --- |
|
||||
| `new-notification` | `user-<userId>` | `{ notification: { _id, type, title, body, data, createdAt } }` |
|
||||
| `unread-count-update` | `user-<userId>` | `{ unreadCount }` |
|
||||
|
||||
Source: [`NotificationService.ts`](../../backend/src/services/notification/NotificationService.ts).
|
||||
|
||||
### Points
|
||||
|
||||
| Event | Room | Payload |
|
||||
| --- | --- | --- |
|
||||
| `level-up` | `user-<userId>` | `{ oldLevel, newLevel, lifetimePoints, perks }` |
|
||||
| `referral-reward` | `user-<referrerId>` | `{ referredUserId, points, transactionId }` |
|
||||
| `referral-signup` | `user-<referrerId>` | `{ referredUserId, name, joinedAt }` |
|
||||
|
||||
Sources: [`PointsService.ts`](../../backend/src/services/points/PointsService.ts), [`authController.ts`](../../backend/src/services/auth/authController.ts).
|
||||
|
||||
## Online status
|
||||
|
||||
A client emits `user-online` after connecting; the server broadcasts `user-status-change` (status `"online"`). There is currently **no** matching offline emit on disconnect because the server does not track which userId belongs to which socketId. To implement presence, store the mapping on connect and emit `user-status-change` (status `"offline"`) in the disconnect handler.
|
||||
|
||||
## Reconnection
|
||||
|
||||
Socket.IO defaults are used: exponential backoff starting at 1s, capped at 5s, with jitter. The recommended client policy is:
|
||||
|
||||
1. On reconnect, re-emit every `join-*` for the rooms the user cares about.
|
||||
2. Re-fetch any state that may have moved while disconnected (`GET /api/notifications/unread-count`, the active purchase request, chat history with `?before=<lastSeen>`).
|
||||
3. Treat the first 5 seconds after reconnect as "catching up" in the UI.
|
||||
|
||||
The server does not buffer missed events; if delivery guarantees matter, fall back to REST.
|
||||
|
||||
## Server-side helpers (for service authors)
|
||||
|
||||
```ts
|
||||
import { emitGlobalEvent, emitToRoom } from "@/infrastructure/socket/socketService";
|
||||
|
||||
emitGlobalEvent("new-purchase-request", request);
|
||||
emitToRoom(`request-${requestId}`, "purchase-request-update", { type: "status_changed", status });
|
||||
```
|
||||
|
||||
Use `emitToRoom` whenever you can — it limits the broadcast surface area. Reserve `emitGlobalEvent` for events that genuinely need every connected client (rare).
|
||||
|
||||
## Related
|
||||
|
||||
- [[Authentication API]] (token issuance)
|
||||
- [[Chat API]]
|
||||
- [[Notification API]]
|
||||
- [[Marketplace API]]
|
||||
- [[Payment API]]
|
||||
- [[Real-time Architecture]]
|
||||
262
03 - API Reference/User API.md
Normal file
262
03 - API Reference/User API.md
Normal file
@@ -0,0 +1,262 @@
|
||||
---
|
||||
title: User API
|
||||
tags: [api, user, reference]
|
||||
---
|
||||
|
||||
# User API
|
||||
|
||||
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 EVM wallet address (or `null`).
|
||||
**Auth required:** Bearer JWT
|
||||
**Response 200:** `{ "success": true, "data": { "walletAddress": "0x..." | null } }`
|
||||
|
||||
### PATCH /api/user/wallet-address
|
||||
|
||||
**Description:** Verifies an EIP-191 signed message and stores `profile.walletAddress`. The server uses `ethers.verifyMessage(message, signature)` and rejects if the recovered address does not match.
|
||||
**Auth required:** Bearer JWT
|
||||
**Request body:**
|
||||
```ts
|
||||
{
|
||||
walletAddress: string; // 0x-prefixed 40-hex
|
||||
signature: string; // signed `message`
|
||||
message: string; // human-readable challenge text
|
||||
}
|
||||
```
|
||||
**Response 200:** `{ "success": true, "data": { "walletAddress": "0x..." } }`
|
||||
**Errors:**
|
||||
- `400` missing fields, malformed address, signature mismatch
|
||||
- `404` user not found
|
||||
|
||||
The legacy alias `PATCH /api/users/wallet-address` performs the same logic.
|
||||
|
||||
## 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
|
||||
|
||||
These are duplicated across the two routers. The newer controller variants live under `/api/user/admin/*`; the legacy bodies live under `/api/users/admin/*`. All require `req.user.role === 'admin'` (the legacy routes check inline; the controller routes only check `authenticateToken` and the controller enforces the role).
|
||||
|
||||
### 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
|
||||
|
||||
**Description:** Hard-delete a user. Prevents self-deletion and deleting other admins.
|
||||
**Auth required:** Bearer JWT (admin)
|
||||
**Response 200:** `{ success, data: { deletedUserId } }`
|
||||
**Errors:** `400` self-delete, `403` admin-on-admin, `404` not found.
|
||||
|
||||
### PATCH /api/user/admin/:userId/status
|
||||
|
||||
**Description:** Activate / suspend a user.
|
||||
**Auth required:** Bearer JWT (admin)
|
||||
**Request body:** `{ isActive: boolean; reason?: string }`
|
||||
**Response 200:** `{ success, data: { user: { _id, isActive, statusUpdatedAt } } }`
|
||||
|
||||
### PATCH /api/user/admin/:userId/toggle-status
|
||||
|
||||
**Description:** Flip active/suspended without explicit body.
|
||||
**Auth required:** Bearer JWT (admin)
|
||||
|
||||
### PATCH /api/user/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.
|
||||
|
||||
### 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).
|
||||
**Auth required:** Bearer JWT (admin)
|
||||
|
||||
### 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 8-digit email verification code and re-send the verification email.
|
||||
**Auth required:** Bearer JWT (admin)
|
||||
**Errors:** `400` user already verified.
|
||||
|
||||
## 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.
|
||||
Reference in New Issue
Block a user