changes for md
This commit is contained in:
@@ -1,24 +1,471 @@
|
||||
# API Changes Required for Backend
|
||||
# Complete Backend API Documentation
|
||||
|
||||
## Overview
|
||||
|
||||
Frontend has been updated with two new features:
|
||||
1. **Region/Location system** — catalog filtering by region
|
||||
2. **Auth/Login system** — Telegram-based authentication required before payment
|
||||
|
||||
Base URLs:
|
||||
- Dexar: `https://api.dexarmarket.ru:445`
|
||||
- Novo: `https://api.novo.market:444`
|
||||
> **Last updated:** February 2026
|
||||
> **Frontend:** Angular 21 · Dual-brand (Dexar + Novo)
|
||||
> **Covers:** Catalog, Cart, Payments, Reviews, Regions, Auth, i18n, BackOffice
|
||||
|
||||
---
|
||||
|
||||
## 1. Region / Location Endpoints
|
||||
## Base URLs
|
||||
|
||||
### 1.1 `GET /regions` — List available regions
|
||||
| Brand | Dev | Production |
|
||||
|--------|----------------------------------|----------------------------------|
|
||||
| Dexar | `https://api.dexarmarket.ru:445` | `https://api.dexarmarket.ru:445` |
|
||||
| Novo | `https://api.novo.market:444` | `https://api.novo.market:444` |
|
||||
|
||||
Returns the list of regions where the marketplace operates.
|
||||
---
|
||||
|
||||
**Response** `200 OK`
|
||||
## Global HTTP Headers
|
||||
|
||||
The frontend **automatically attaches** two custom headers to **every API request** via an interceptor. The backend should read these headers and use them to filter/translate responses accordingly.
|
||||
|
||||
| Header | Example Value | Description |
|
||||
|---------------|---------------|------------------------------------------------------------|
|
||||
| `X-Region` | `moscow` | Region ID selected by the user. **Absent** = global (all). |
|
||||
| `X-Language` | `ru` | Active UI language: `ru`, `en`, or `hy`. |
|
||||
|
||||
### Backend behavior
|
||||
|
||||
- **`X-Region`**: If present, filter items/categories to only those available in that region. If absent, return everything (global catalog).
|
||||
- **`X-Language`**: If present, return translated `name`, `description`, etc. for categories/items when translations exist. If absent or `ru`, use russians defaults.
|
||||
|
||||
### CORS requirements for these headers
|
||||
|
||||
```
|
||||
Access-Control-Allow-Headers: Content-Type, X-Region, X-Language
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 1. Health Check
|
||||
|
||||
### `GET /ping`
|
||||
|
||||
Simple health check.
|
||||
|
||||
**Response `200`:**
|
||||
```json
|
||||
{ "message": "pong" }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Catalog — Categories
|
||||
|
||||
### `GET /category`
|
||||
|
||||
Returns all top-level categories. Respects `X-Region` and `X-Language` headers.
|
||||
|
||||
**Response `200`:**
|
||||
```json
|
||||
[
|
||||
{
|
||||
"categoryID": 1,
|
||||
"name": "Электроника",
|
||||
"parentID": 0,
|
||||
"icon": "https://...",
|
||||
"wideBanner": "https://...",
|
||||
"itemCount": 42,
|
||||
"priority": 10,
|
||||
|
||||
"id": "cat_abc123",
|
||||
"visible": true,
|
||||
"img": "https://...",
|
||||
"projectId": "proj_xyz",
|
||||
"subcategories": [
|
||||
{
|
||||
"id": "sub_001",
|
||||
"name": "Смартфоны",
|
||||
"visible": true,
|
||||
"priority": 5,
|
||||
"img": "https://...",
|
||||
"categoryId": "cat_abc123",
|
||||
"parentId": "cat_abc123",
|
||||
"itemCount": 20,
|
||||
"hasItems": true,
|
||||
"subcategories": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
**Category object:**
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|------------------|---------------|----------|----------------------------------------------------|
|
||||
| `categoryID` | number | yes | Legacy numeric ID |
|
||||
| `name` | string | yes | Category display name (translated if `X-Language`) |
|
||||
| `parentID` | number | yes | Parent category ID (`0` = top-level) |
|
||||
| `icon` | string | no | Category icon URL |
|
||||
| `wideBanner` | string | no | Wide banner image URL |
|
||||
| `itemCount` | number | no | Number of items in category |
|
||||
| `priority` | number | no | Sort priority (higher = first) |
|
||||
| `id` | string | no | BackOffice string ID |
|
||||
| `visible` | boolean | no | Whether category is shown (`true` default) |
|
||||
| `img` | string | no | BackOffice image URL (maps to `icon`) |
|
||||
| `projectId` | string | no | BackOffice project reference |
|
||||
| `subcategories` | Subcategory[] | no | Nested subcategories |
|
||||
|
||||
**Subcategory object:**
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|------------------|---------------|----------|------------------------------------|
|
||||
| `id` | string | yes | Subcategory ID |
|
||||
| `name` | string | yes | Display name |
|
||||
| `visible` | boolean | no | Whether visible |
|
||||
| `priority` | number | no | Sort priority |
|
||||
| `img` | string | no | Image URL |
|
||||
| `categoryId` | string | yes | Parent category ID |
|
||||
| `parentId` | string | yes | Direct parent ID |
|
||||
| `itemCount` | number | no | Number of items |
|
||||
| `hasItems` | boolean | no | Whether has any items |
|
||||
| `subcategories` | Subcategory[] | no | Nested children |
|
||||
|
||||
---
|
||||
|
||||
### `GET /category/:categoryID`
|
||||
|
||||
Returns items in a specific category. Respects `X-Region` and `X-Language` headers.
|
||||
|
||||
**Query params:**
|
||||
|
||||
| Param | Type | Default | Description |
|
||||
|----------|--------|---------|--------------------|
|
||||
| `count` | number | `50` | Items per page |
|
||||
| `skip` | number | `0` | Offset for paging |
|
||||
|
||||
**Response `200`:** Array of [Item](#item-object) objects.
|
||||
|
||||
---
|
||||
|
||||
## 3. Items
|
||||
|
||||
### `GET /item/:itemID`
|
||||
|
||||
Returns a single item. Respects `X-Region` and `X-Language` headers.
|
||||
|
||||
**Response `200`:** A single [Item](#item-object) object.
|
||||
|
||||
---
|
||||
|
||||
### `GET /searchitems`
|
||||
|
||||
Full-text search across items. Respects `X-Region` and `X-Language` headers.
|
||||
|
||||
**Query params:**
|
||||
|
||||
| Param | Type | Default | Description |
|
||||
|----------|--------|---------|----------------------|
|
||||
| `search` | string | — | Search query (required) |
|
||||
| `count` | number | `50` | Items per page |
|
||||
| `skip` | number | `0` | Offset for paging |
|
||||
|
||||
**Response `200`:**
|
||||
```json
|
||||
{
|
||||
"items": [ /* Item objects */ ],
|
||||
"total": 128,
|
||||
"count": 50,
|
||||
"skip": 0
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `GET /randomitems`
|
||||
|
||||
Returns random items for carousel/recommendations. Respects `X-Region` and `X-Language` headers.
|
||||
|
||||
**Query params:**
|
||||
|
||||
| Param | Type | Default | Description |
|
||||
|------------|--------|---------|------------------------------------|
|
||||
| `count` | number | `5` | Number of items to return |
|
||||
| `category` | number | — | Optional: limit to this category |
|
||||
|
||||
**Response `200`:** Array of [Item](#item-object) objects.
|
||||
|
||||
---
|
||||
|
||||
### Item Object
|
||||
|
||||
The backend can return items in **either** legacy format or BackOffice format. The frontend normalizes both.
|
||||
|
||||
```json
|
||||
{
|
||||
"categoryID": 1,
|
||||
"itemID": 123,
|
||||
"name": "iPhone 15 Pro",
|
||||
"photos": [{ "url": "https://..." }],
|
||||
"description": "Описание товара",
|
||||
"currency": "RUB",
|
||||
"price": 89990,
|
||||
"discount": 10,
|
||||
"remainings": "high",
|
||||
"rating": 4.5,
|
||||
"callbacks": [
|
||||
{
|
||||
"rating": 5,
|
||||
"content": "Отличный товар!",
|
||||
"userID": "user_123",
|
||||
"timestamp": "2026-02-01T12:00:00Z"
|
||||
}
|
||||
],
|
||||
"questions": [
|
||||
{
|
||||
"question": "Есть ли гарантия?",
|
||||
"answer": "Да, 12 месяцев",
|
||||
"upvotes": 5,
|
||||
"downvotes": 0
|
||||
}
|
||||
],
|
||||
|
||||
"id": "item_abc123",
|
||||
"visible": true,
|
||||
"priority": 10,
|
||||
"imgs": ["https://img1.jpg", "https://img2.jpg"],
|
||||
"tags": ["new", "popular"],
|
||||
"badges": ["bestseller", "sale"],
|
||||
"simpleDescription": "Краткое описание",
|
||||
"descriptionFields": [
|
||||
{ "key": "Процессор", "value": "A17 Pro" },
|
||||
{ "key": "Память", "value": "256 GB" }
|
||||
],
|
||||
"subcategoryId": "sub_001",
|
||||
"translations": {
|
||||
"en": {
|
||||
"name": "iPhone 15 Pro",
|
||||
"simpleDescription": "Short description",
|
||||
"description": [
|
||||
{ "key": "Processor", "value": "A17 Pro" }
|
||||
]
|
||||
},
|
||||
"hy": {
|
||||
"name": "iPhone 15 Pro",
|
||||
"simpleDescription": "Կարcheck check check"
|
||||
}
|
||||
},
|
||||
"comments": [
|
||||
{
|
||||
"id": "cmt_001",
|
||||
"text": "Отличный товар!",
|
||||
"author": "user_123",
|
||||
"stars": 5,
|
||||
"createdAt": "2026-02-01T12:00:00Z"
|
||||
}
|
||||
],
|
||||
"quantity": 50
|
||||
}
|
||||
```
|
||||
|
||||
**Full Item fields:**
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|---------------------|-------------------|----------|------------------------------------------------------------|
|
||||
| `categoryID` | number | yes | Category this item belongs to |
|
||||
| `itemID` | number | yes | Legacy numeric item ID |
|
||||
| `name` | string | yes | Item display name |
|
||||
| `photos` | Photo[] | no | Legacy photo array `[{ url }]` |
|
||||
| `description` | string | yes | Text description |
|
||||
| `currency` | string | yes | Currency code (default: `RUB`) |
|
||||
| `price` | number | yes | Price in the currency's smallest display unit |
|
||||
| `discount` | number | yes | Discount percentage (`0`–`100`) |
|
||||
| `remainings` | string | no | Stock level: `high`, `medium`, `low`, `out` |
|
||||
| `rating` | number | yes | Average rating (`0`–`5`) |
|
||||
| `callbacks` | Review[] | no | Legacy reviews (alias for reviews) |
|
||||
| `questions` | Question[] | no | Q&A entries |
|
||||
| `id` | string | no | BackOffice string ID |
|
||||
| `visible` | boolean | no | Whether item is visible (`true` default) |
|
||||
| `priority` | number | no | Sort priority (higher = first) |
|
||||
| `imgs` | string[] | no | BackOffice image URLs (maps to `photos`) |
|
||||
| `tags` | string[] | no | Item tags for filtering |
|
||||
| `badges` | string[] | no | Display badges (`bestseller`, `sale`, etc.) |
|
||||
| `simpleDescription` | string | no | Short plain-text description |
|
||||
| `descriptionFields` | DescriptionField[]| no | Structured `[{ key, value }]` descriptions |
|
||||
| `subcategoryId` | string | no | BackOffice subcategory reference |
|
||||
| `translations` | Record | no | Translations keyed by lang code (see below) |
|
||||
| `comments` | Comment[] | no | BackOffice comments format |
|
||||
| `quantity` | number | no | Numeric stock count (maps to `remainings` on frontend) |
|
||||
|
||||
**Nested types:**
|
||||
|
||||
| Type | Fields |
|
||||
|--------------------|-----------------------------------------------------------------|
|
||||
| `Photo` | `url: string`, `photo?: string`, `video?: string`, `type?: string` |
|
||||
| `DescriptionField` | `key: string`, `value: string` |
|
||||
| `Comment` | `id?: string`, `text: string`, `author?: string`, `stars?: number`, `createdAt?: string` |
|
||||
| `Review` | `rating?: number`, `content?: string`, `userID?: string`, `answer?: string`, `timestamp?: string` |
|
||||
| `Question` | `question: string`, `answer: string`, `upvotes: number`, `downvotes: number` |
|
||||
| `ItemTranslation` | `name?: string`, `simpleDescription?: string`, `description?: DescriptionField[]` |
|
||||
|
||||
---
|
||||
|
||||
## 4. Cart
|
||||
|
||||
### `POST /cart` — Add item to cart
|
||||
|
||||
**Request body:**
|
||||
```json
|
||||
{ "itemID": 123, "quantity": 1 }
|
||||
```
|
||||
|
||||
**Response `200`:**
|
||||
```json
|
||||
{ "message": "Added to cart" }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `PATCH /cart` — Update item quantity
|
||||
|
||||
**Request body:**
|
||||
```json
|
||||
{ "itemID": 123, "quantity": 3 }
|
||||
```
|
||||
|
||||
**Response `200`:**
|
||||
```json
|
||||
{ "message": "Updated" }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `DELETE /cart` — Remove items from cart
|
||||
|
||||
**Request body:** Array of item IDs
|
||||
```json
|
||||
[123, 456]
|
||||
```
|
||||
|
||||
**Response `200`:**
|
||||
```json
|
||||
{ "message": "Removed" }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `GET /cart` — Get cart contents
|
||||
|
||||
**Response `200`:** Array of [Item](#item-object) objects (each with `quantity` field).
|
||||
|
||||
---
|
||||
|
||||
## 5. Payments (SBP / QR)
|
||||
|
||||
### `POST /cart` — Create payment (SBP QR)
|
||||
|
||||
> Note: Same endpoint as add-to-cart but with different body schema.
|
||||
|
||||
**Request body:**
|
||||
```json
|
||||
{
|
||||
"amount": 89990,
|
||||
"currency": "RUB",
|
||||
"siteuserID": "tg_123456789",
|
||||
"siteorderID": "order_abc123",
|
||||
"redirectUrl": "",
|
||||
"telegramUsername": "john_doe",
|
||||
"items": [
|
||||
{ "itemID": 123, "price": 89990, "name": "iPhone 15 Pro" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Response `200`:**
|
||||
```json
|
||||
{
|
||||
"qrId": "qr_abc123",
|
||||
"qrStatus": "CREATED",
|
||||
"qrExpirationDate": "2026-02-28T13:00:00Z",
|
||||
"payload": "https://qr.nspk.ru/...",
|
||||
"qrUrl": "https://qr.nspk.ru/..."
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `GET /qr/payment/:qrId` — Check payment status
|
||||
|
||||
**Response `200`:**
|
||||
```json
|
||||
{
|
||||
"additionalInfo": "",
|
||||
"paymentPurpose": "Order #order_abc123",
|
||||
"amount": 89990,
|
||||
"code": "SUCCESS",
|
||||
"createDate": "2026-02-28T12:00:00Z",
|
||||
"currency": "RUB",
|
||||
"order": "order_abc123",
|
||||
"paymentStatus": "COMPLETED",
|
||||
"qrId": "qr_abc123",
|
||||
"transactionDate": "2026-02-28T12:01:00Z",
|
||||
"transactionId": 999,
|
||||
"qrExpirationDate": "2026-02-28T13:00:00Z",
|
||||
"phoneNumber": "+7XXXXXXXXXX"
|
||||
}
|
||||
```
|
||||
|
||||
| `paymentStatus` values | Meaning |
|
||||
|------------------------|---------------------------|
|
||||
| `CREATED` | QR generated, not paid |
|
||||
| `WAITING` | Payment in progress |
|
||||
| `COMPLETED` | Payment successful |
|
||||
| `EXPIRED` | QR code expired |
|
||||
| `CANCELLED` | Payment cancelled |
|
||||
|
||||
---
|
||||
|
||||
### `POST /purchase-email` — Submit email after payment
|
||||
|
||||
**Request body:**
|
||||
```json
|
||||
{
|
||||
"email": "user@example.com",
|
||||
"telegramUserId": "123456789",
|
||||
"items": [
|
||||
{ "itemID": 123, "name": "iPhone 15 Pro", "price": 89990, "currency": "RUB" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Response `200`:**
|
||||
```json
|
||||
{ "message": "Email sent" }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Reviews / Comments
|
||||
|
||||
### `POST /comment` — Submit a review
|
||||
|
||||
**Request body:**
|
||||
```json
|
||||
{
|
||||
"itemID": 123,
|
||||
"rating": 5,
|
||||
"comment": "Great product!",
|
||||
"username": "john_doe",
|
||||
"userId": 123456789,
|
||||
"timestamp": "2026-02-28T12:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
**Response `200`:**
|
||||
```json
|
||||
{ "message": "Review submitted" }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Regions
|
||||
|
||||
### `GET /regions` — List available regions
|
||||
|
||||
Returns regions where the marketplace operates.
|
||||
|
||||
**Response `200`:**
|
||||
```json
|
||||
[
|
||||
{
|
||||
@@ -46,52 +493,44 @@ Returns the list of regions where the marketplace operates.
|
||||
```
|
||||
|
||||
**Region object:**
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|---------------|--------|----------|------------------------------|
|
||||
|---------------|--------|----------|--------------------------|
|
||||
| `id` | string | yes | Unique region identifier |
|
||||
| `city` | string | yes | City name (display) |
|
||||
| `country` | string | yes | Country name (display) |
|
||||
| `countryCode` | string | yes | ISO 3166-1 alpha-2 code |
|
||||
| `timezone` | string | no | IANA timezone string |
|
||||
| `country` | string | yes | Country name |
|
||||
| `countryCode` | string | yes | ISO 3166-1 alpha-2 |
|
||||
| `timezone` | string | no | IANA timezone |
|
||||
|
||||
> If this endpoint is unavailable, the frontend falls back to 6 hardcoded regions (Moscow, SPB, Yerevan, Minsk, Almaty, Tbilisi).
|
||||
> **Fallback:** If this endpoint is down, the frontend uses 6 hardcoded defaults: Moscow, SPB, Yerevan, Minsk, Almaty, Tbilisi.
|
||||
|
||||
---
|
||||
|
||||
### 1.2 Region Query Parameter on Existing Endpoints
|
||||
|
||||
The following **existing** endpoints now accept an optional `?region=` query parameter:
|
||||
|
||||
| Endpoint | Example |
|
||||
|---------------------------------|----------------------------------------------|
|
||||
| `GET /category` | `GET /category?region=moscow` |
|
||||
| `GET /category/:id` | `GET /category/5?count=50&skip=0®ion=spb` |
|
||||
| `GET /item/:id` | `GET /item/123?region=yerevan` |
|
||||
| `GET /searchitems` | `GET /searchitems?search=phone®ion=moscow` |
|
||||
| `GET /randomitems` | `GET /randomitems?count=5®ion=almaty` |
|
||||
|
||||
**Behavior:**
|
||||
- If `region` param is **present** → return only items/categories available in that region
|
||||
- If `region` param is **absent** → return all items globally (current behavior, no change)
|
||||
- The `region` value matches the `id` field from the `/regions` response
|
||||
|
||||
---
|
||||
|
||||
## 2. Auth / Login Endpoints
|
||||
## 8. Authentication (Telegram Login)
|
||||
|
||||
Authentication is **Telegram-based** with **cookie sessions** (HttpOnly, Secure, SameSite=None).
|
||||
|
||||
All auth endpoints must support CORS with `credentials: true`.
|
||||
All auth endpoints must include `withCredentials: true` CORS support.
|
||||
|
||||
### 2.1 `GET /auth/session` — Check current session
|
||||
### Auth flow
|
||||
|
||||
Called on every page load to check if the user has an active session.
|
||||
```
|
||||
1. User clicks "Checkout" → not authenticated → login dialog shown
|
||||
2. User clicks "Log in with Telegram" → opens https://t.me/{bot}?start=auth_{callback}
|
||||
3. User starts the bot in Telegram
|
||||
4. Bot sends user data → backend /auth/telegram/callback
|
||||
5. Backend creates session → sets Set-Cookie
|
||||
6. Frontend polls GET /auth/session every 3s
|
||||
7. Session detected → dialog closes → checkout proceeds
|
||||
```
|
||||
|
||||
**Request:**
|
||||
- Cookies: session cookie (set by backend)
|
||||
- CORS: `withCredentials: true`
|
||||
---
|
||||
|
||||
**Response `200 OK`** (authenticated):
|
||||
### `GET /auth/session` — Check current session
|
||||
|
||||
**Request:** Cookies only (session cookie set by backend).
|
||||
|
||||
**Response `200`** (authenticated):
|
||||
```json
|
||||
{
|
||||
"sessionId": "sess_abc123",
|
||||
@@ -103,7 +542,7 @@ Called on every page load to check if the user has an active session.
|
||||
}
|
||||
```
|
||||
|
||||
**Response `200 OK`** (expired session):
|
||||
**Response `200`** (expired):
|
||||
```json
|
||||
{
|
||||
"sessionId": "sess_abc123",
|
||||
@@ -115,37 +554,29 @@ Called on every page load to check if the user has an active session.
|
||||
}
|
||||
```
|
||||
|
||||
**Response `401 Unauthorized`** (no session / invalid cookie):
|
||||
**Response `401`** (no session):
|
||||
```json
|
||||
{
|
||||
"error": "No active session"
|
||||
}
|
||||
{ "error": "No active session" }
|
||||
```
|
||||
|
||||
**AuthSession object:**
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|------------------|---------|----------|------------------------------------------|
|
||||
|------------------|---------|----------|--------------------------------------------|
|
||||
| `sessionId` | string | yes | Unique session ID |
|
||||
| `telegramUserId` | number | yes | Telegram user ID |
|
||||
| `username` | string? | no | Telegram @username (can be null) |
|
||||
| `displayName` | string | yes | User display name (first_name + last_name) |
|
||||
| `active` | boolean | yes | Whether session is currently valid |
|
||||
| `displayName` | string | yes | User display name (first + last) |
|
||||
| `active` | boolean | yes | Whether session is valid |
|
||||
| `expiresAt` | string | yes | ISO 8601 expiration datetime |
|
||||
|
||||
---
|
||||
|
||||
### 2.2 `GET /auth/telegram/callback` — Telegram bot auth callback
|
||||
### `GET /auth/telegram/callback` — Telegram bot auth callback
|
||||
|
||||
This is the URL the Telegram bot redirects to after the user starts the bot.
|
||||
Called by the Telegram bot after user authenticates.
|
||||
|
||||
**Flow:**
|
||||
1. Frontend generates link: `https://t.me/{botUsername}?start=auth_{encodedCallbackUrl}`
|
||||
2. User clicks → opens Telegram → starts the bot
|
||||
3. Bot sends user data to this callback endpoint
|
||||
4. Backend creates session, sets `Set-Cookie` header
|
||||
5. Frontend polls `GET /auth/session` every 3 seconds to detect when session becomes active
|
||||
|
||||
**Request** (from Telegram bot / webhook):
|
||||
**Request body (from bot):**
|
||||
```json
|
||||
{
|
||||
"id": 123456789,
|
||||
@@ -158,7 +589,7 @@ This is the URL the Telegram bot redirects to after the user starts the bot.
|
||||
}
|
||||
```
|
||||
|
||||
**Response:** Should set a session cookie and return:
|
||||
**Response:** Must set a session cookie and return:
|
||||
```json
|
||||
{
|
||||
"sessionId": "sess_abc123",
|
||||
@@ -167,93 +598,89 @@ This is the URL the Telegram bot redirects to after the user starts the bot.
|
||||
```
|
||||
|
||||
**Cookie requirements:**
|
||||
|
||||
| Attribute | Value | Notes |
|
||||
|------------|----------------|------------------------------------------|
|
||||
|------------|----------------|--------------------------------------------|
|
||||
| `HttpOnly` | `true` | Not accessible via JS |
|
||||
| `Secure` | `true` | HTTPS only |
|
||||
| `SameSite` | `None` | Required for cross-origin (API ≠ frontend) |
|
||||
| `Path` | `/` | |
|
||||
| `Max-Age` | `86400` (24h) | Or as needed |
|
||||
| `Domain` | API domain | |
|
||||
|
||||
> **Important:** Since the API domain differs from the frontend domain, `SameSite=None` + `Secure=true` is required for the cookie to be sent cross-origin.
|
||||
|
||||
---
|
||||
|
||||
### 2.3 `POST /auth/logout` — End session
|
||||
### `POST /auth/logout` — End session
|
||||
|
||||
**Request:**
|
||||
- Cookies: session cookie
|
||||
- CORS: `withCredentials: true`
|
||||
- Body: `{}` (empty)
|
||||
**Request:** Cookies only, empty body `{}`
|
||||
|
||||
**Response `200 OK`:**
|
||||
**Response `200`:**
|
||||
```json
|
||||
{ "message": "Logged out" }
|
||||
```
|
||||
|
||||
Must clear/invalidate the session cookie.
|
||||
|
||||
---
|
||||
|
||||
### Session refresh
|
||||
|
||||
The frontend re-checks the session **60 seconds before `expiresAt`**. If the backend supports sliding expiration, it can reset the cookie's `Max-Age` on each `GET /auth/session`.
|
||||
|
||||
---
|
||||
|
||||
## 9. i18n / Translations
|
||||
|
||||
The frontend supports 3 languages: **Russian (ru)**, **English (en)**, **Armenian (hy)**.
|
||||
|
||||
The active language is sent via the `X-Language` HTTP header on every request.
|
||||
|
||||
### What the backend should do with `X-Language`
|
||||
|
||||
1. **Categories & items**: If `translations` field exists for the requested language, return the translated `name`, `description`, etc. OR the backend can apply translations server-side and return already-translated fields.
|
||||
|
||||
2. **The `translations` field** on items (optional approach):
|
||||
```json
|
||||
{
|
||||
"message": "Logged out"
|
||||
"translations": {
|
||||
"en": {
|
||||
"name": "iPhone 15 Pro",
|
||||
"simpleDescription": "Short desc in English",
|
||||
"description": [{ "key": "Processor", "value": "A17 Pro" }]
|
||||
},
|
||||
"hy": {
|
||||
"name": "iPhone 15 Pro",
|
||||
"simpleDescription": "Կarcheck check"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Should clear/invalidate the session cookie.
|
||||
3. **Recommended approach**: Read `X-Language` header and return the `name`/`description` in that language directly. If no translation exists, return the Russian default.
|
||||
|
||||
---
|
||||
|
||||
## 3. CORS Configuration
|
||||
## 10. CORS Configuration
|
||||
|
||||
For auth cookies to work cross-origin, the backend CORS config must include:
|
||||
For auth cookies and custom headers to work, the backend CORS config must include:
|
||||
|
||||
```
|
||||
Access-Control-Allow-Origin: https://dexarmarket.ru (NOT *)
|
||||
Access-Control-Allow-Origin: https://dexarmarket.ru (NOT wildcard *)
|
||||
Access-Control-Allow-Credentials: true
|
||||
Access-Control-Allow-Headers: Content-Type, Authorization
|
||||
Access-Control-Allow-Headers: Content-Type, X-Region, X-Language
|
||||
Access-Control-Allow-Methods: GET, POST, PATCH, DELETE, OPTIONS
|
||||
```
|
||||
|
||||
> `Access-Control-Allow-Origin` **cannot** be `*` when `Allow-Credentials: true`. Must be the exact frontend origin.
|
||||
> **Important:** `Access-Control-Allow-Origin` cannot be `*` when `Allow-Credentials: true`. Must be the exact frontend origin.
|
||||
|
||||
For Novo, also allow `https://novo.market`.
|
||||
**Allowed origins:**
|
||||
- `https://dexarmarket.ru`
|
||||
- `https://novo.market`
|
||||
- `http://localhost:4200` (dev)
|
||||
- `http://localhost:4201` (dev, Novo)
|
||||
|
||||
---
|
||||
|
||||
## 4. Session Refresh Behavior
|
||||
|
||||
The frontend automatically re-checks the session **60 seconds before `expiresAt`**. If the backend supports session extension (sliding expiration), it can re-set the cookie with a fresh `Max-Age` on every `GET /auth/session` call.
|
||||
|
||||
---
|
||||
|
||||
## 5. Auth Gate — Checkout Flow
|
||||
|
||||
The checkout button (`POST /cart` payment) now requires authentication:
|
||||
- If the user is **not logged in** → frontend shows a Telegram login dialog instead of proceeding
|
||||
- If the user **is logged in** → checkout proceeds normally
|
||||
- The session cookie is sent automatically with the payment request
|
||||
|
||||
No backend changes needed for the payment endpoint itself — just ensure it reads the session cookie if needed for order association.
|
||||
|
||||
---
|
||||
|
||||
## Summary of New Endpoints
|
||||
|
||||
| Method | Path | Purpose | Auth Required |
|
||||
|--------|----------------------------|-----------------------------|---------------|
|
||||
| `GET` | `/regions` | List available regions | No |
|
||||
| `GET` | `/auth/session` | Check current session | Cookie |
|
||||
| `GET` | `/auth/telegram/callback` | Telegram bot auth callback | No (from bot) |
|
||||
| `POST` | `/auth/logout` | End session | Cookie |
|
||||
|
||||
## Summary of Modified Endpoints
|
||||
|
||||
| Method | Path | Change |
|
||||
|--------|-------------------|---------------------------------------|
|
||||
| `GET` | `/category` | Added optional `?region=` param |
|
||||
| `GET` | `/category/:id` | Added optional `?region=` param |
|
||||
| `GET` | `/item/:id` | Added optional `?region=` param |
|
||||
| `GET` | `/searchitems` | Added optional `?region=` param |
|
||||
| `GET` | `/randomitems` | Added optional `?region=` param |
|
||||
|
||||
---
|
||||
|
||||
## Telegram Bot Setup
|
||||
## 11. Telegram Bot Setup
|
||||
|
||||
Each brand needs its own bot:
|
||||
- **Dexar:** `@dexarmarket_bot`
|
||||
@@ -262,5 +689,38 @@ Each brand needs its own bot:
|
||||
The bot should:
|
||||
1. Listen for `/start auth_{callbackUrl}` command
|
||||
2. Extract the callback URL
|
||||
3. Send the user's Telegram data (id, first_name, username, etc.) to that callback URL
|
||||
3. Send the user's Telegram data (`id`, `first_name`, `username`, etc.) to that callback URL
|
||||
4. The callback URL is `{apiUrl}/auth/telegram/callback`
|
||||
|
||||
---
|
||||
|
||||
## Complete Endpoint Reference
|
||||
|
||||
### New endpoints
|
||||
|
||||
| Method | Path | Description | Auth |
|
||||
|--------|---------------------------|----------------------------|----------|
|
||||
| `GET` | `/regions` | List available regions | No |
|
||||
| `GET` | `/auth/session` | Check current session | Cookie |
|
||||
| `GET` | `/auth/telegram/callback` | Telegram bot auth callback | No (bot) |
|
||||
| `POST` | `/auth/logout` | End session | Cookie |
|
||||
|
||||
### Existing endpoints
|
||||
|
||||
| Method | Path | Description | Auth | Headers |
|
||||
|----------|-----------------------|-------------------------|------|--------------------|
|
||||
| `GET` | `/ping` | Health check | No | — |
|
||||
| `GET` | `/category` | List categories | No | X-Region, X-Language |
|
||||
| `GET` | `/category/:id` | Items in category | No | X-Region, X-Language |
|
||||
| `GET` | `/item/:id` | Single item | No | X-Region, X-Language |
|
||||
| `GET` | `/searchitems` | Search items | No | X-Region, X-Language |
|
||||
| `GET` | `/randomitems` | Random items | No | X-Region, X-Language |
|
||||
| `POST` | `/cart` | Add to cart / Payment | No* | — |
|
||||
| `PATCH` | `/cart` | Update cart quantity | No* | — |
|
||||
| `DELETE` | `/cart` | Remove from cart | No* | — |
|
||||
| `GET` | `/cart` | Get cart contents | No* | — |
|
||||
| `POST` | `/comment` | Submit review | No | — |
|
||||
| `GET` | `/qr/payment/:qrId` | Check payment status | No | — |
|
||||
| `POST` | `/purchase-email` | Submit email after pay | No | — |
|
||||
|
||||
> \* Cart/payment endpoints may use the session cookie if available for order association, but don't strictly require auth. The frontend enforces auth before checkout.
|
||||
|
||||
726
docs/API_DOCS_RU.md
Normal file
726
docs/API_DOCS_RU.md
Normal file
@@ -0,0 +1,726 @@
|
||||
# Полная документация Backend API
|
||||
|
||||
> **Последнее обновление:** Февраль 2026
|
||||
> **Фронтенд:** Angular 21 · Два бренда (Dexar + Novo)
|
||||
> **Охватывает:** Каталог, Корзина, Оплата, Отзывы, Регионы, Авторизация, i18n, BackOffice
|
||||
|
||||
---
|
||||
|
||||
## Базовые URL
|
||||
|
||||
| Бренд | Dev | Production |
|
||||
|--------|----------------------------------|----------------------------------|
|
||||
| Dexar | `https://api.dexarmarket.ru:445` | `https://api.dexarmarket.ru:445` |
|
||||
| Novo | `https://api.novo.market:444` | `https://api.novo.market:444` |
|
||||
|
||||
---
|
||||
|
||||
## Глобальные HTTP-заголовки
|
||||
|
||||
Фронтенд **автоматически добавляет** два кастомных заголовка к **каждому API-запросу** через interceptor. Бэкенд должен читать эти заголовки и использовать для фильтрации/перевода ответов.
|
||||
|
||||
| Заголовок | Пример значения | Описание |
|
||||
|---------------|-----------------|-------------------------------------------------------------------|
|
||||
| `X-Region` | `moscow` | ID региона, выбранного пользователем. **Отсутствует** = все регионы. |
|
||||
| `X-Language` | `ru` | Активный язык интерфейса: `ru`, `en` или `hy`. |
|
||||
|
||||
### Поведение бэкенда
|
||||
|
||||
- **`X-Region`**: Если присутствует — фильтровать товары/категории только по этому региону. Если отсутствует — возвращать всё (глобальный каталог).
|
||||
- **`X-Language`**: Если присутствует — возвращать переведённые `name`, `description` и т.д., если переводы существуют. Если отсутствует или `ru` — возвращать на русском (по умолчанию).
|
||||
|
||||
### Требования CORS для этих заголовков
|
||||
|
||||
```
|
||||
Access-Control-Allow-Headers: Content-Type, X-Region, X-Language
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 1. Проверка состояния
|
||||
|
||||
### `GET /ping`
|
||||
|
||||
Простая проверка работоспособности.
|
||||
|
||||
**Ответ `200`:**
|
||||
```json
|
||||
{ "message": "pong" }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Каталог — Категории
|
||||
|
||||
### `GET /category`
|
||||
|
||||
Возвращает все категории верхнего уровня. Учитывает заголовки `X-Region` и `X-Language`.
|
||||
|
||||
**Ответ `200`:**
|
||||
```json
|
||||
[
|
||||
{
|
||||
"categoryID": 1,
|
||||
"name": "Электроника",
|
||||
"parentID": 0,
|
||||
"icon": "https://...",
|
||||
"wideBanner": "https://...",
|
||||
"itemCount": 42,
|
||||
"priority": 10,
|
||||
|
||||
"id": "cat_abc123",
|
||||
"visible": true,
|
||||
"img": "https://...",
|
||||
"projectId": "proj_xyz",
|
||||
"subcategories": [
|
||||
{
|
||||
"id": "sub_001",
|
||||
"name": "Смартфоны",
|
||||
"visible": true,
|
||||
"priority": 5,
|
||||
"img": "https://...",
|
||||
"categoryId": "cat_abc123",
|
||||
"parentId": "cat_abc123",
|
||||
"itemCount": 20,
|
||||
"hasItems": true,
|
||||
"subcategories": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
**Объект Category:**
|
||||
|
||||
| Поле | Тип | Обязат. | Описание |
|
||||
|------------------|---------------|---------|-----------------------------------------------------|
|
||||
| `categoryID` | number | да | Числовой ID (legacy) |
|
||||
| `name` | string | да | Название категории (переведённое если `X-Language`) |
|
||||
| `parentID` | number | да | ID родительской категории (`0` = верхний уровень) |
|
||||
| `icon` | string | нет | URL иконки категории |
|
||||
| `wideBanner` | string | нет | URL широкого баннера |
|
||||
| `itemCount` | number | нет | Количество товаров в категории |
|
||||
| `priority` | number | нет | Приоритет сортировки (больше = выше) |
|
||||
| `id` | string | нет | Строковый ID из BackOffice |
|
||||
| `visible` | boolean | нет | Видима ли категория (по умолч. `true`) |
|
||||
| `img` | string | нет | URL изображения из BackOffice (маппится на `icon`) |
|
||||
| `projectId` | string | нет | Ссылка на проект в BackOffice |
|
||||
| `subcategories` | Subcategory[] | нет | Вложенные подкатегории |
|
||||
|
||||
**Объект Subcategory:**
|
||||
|
||||
| Поле | Тип | Обязат. | Описание |
|
||||
|------------------|---------------|---------|----------------------------------|
|
||||
| `id` | string | да | ID подкатегории |
|
||||
| `name` | string | да | Отображаемое название |
|
||||
| `visible` | boolean | нет | Видима ли |
|
||||
| `priority` | number | нет | Приоритет сортировки |
|
||||
| `img` | string | нет | URL изображения |
|
||||
| `categoryId` | string | да | ID родительской категории |
|
||||
| `parentId` | string | да | ID прямого родителя |
|
||||
| `itemCount` | number | нет | Количество товаров |
|
||||
| `hasItems` | boolean | нет | Есть ли товары |
|
||||
| `subcategories` | Subcategory[] | нет | Вложенные дочерние подкатегории |
|
||||
|
||||
---
|
||||
|
||||
### `GET /category/:categoryID`
|
||||
|
||||
Возвращает товары определённой категории. Учитывает заголовки `X-Region` и `X-Language`.
|
||||
|
||||
**Query-параметры:**
|
||||
|
||||
| Параметр | Тип | По умолч. | Описание |
|
||||
|----------|--------|-----------|-----------------------|
|
||||
| `count` | number | `50` | Товаров на страницу |
|
||||
| `skip` | number | `0` | Смещение для пагинации |
|
||||
|
||||
**Ответ `200`:** Массив объектов [Item](#объект-item).
|
||||
|
||||
---
|
||||
|
||||
## 3. Товары
|
||||
|
||||
### `GET /item/:itemID`
|
||||
|
||||
Возвращает один товар. Учитывает заголовки `X-Region` и `X-Language`.
|
||||
|
||||
**Ответ `200`:** Один объект [Item](#объект-item).
|
||||
|
||||
---
|
||||
|
||||
### `GET /searchitems`
|
||||
|
||||
Полнотекстовый поиск по товарам. Учитывает заголовки `X-Region` и `X-Language`.
|
||||
|
||||
**Query-параметры:**
|
||||
|
||||
| Параметр | Тип | По умолч. | Описание |
|
||||
|----------|--------|-----------|-------------------------------|
|
||||
| `search` | string | — | Поисковый запрос (обязателен) |
|
||||
| `count` | number | `50` | Товаров на страницу |
|
||||
| `skip` | number | `0` | Смещение для пагинации |
|
||||
|
||||
**Ответ `200`:**
|
||||
```json
|
||||
{
|
||||
"items": [ /* объекты Item */ ],
|
||||
"total": 128,
|
||||
"count": 50,
|
||||
"skip": 0
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `GET /randomitems`
|
||||
|
||||
Возвращает случайные товары для карусели/рекомендаций. Учитывает заголовки `X-Region` и `X-Language`.
|
||||
|
||||
**Query-параметры:**
|
||||
|
||||
| Параметр | Тип | По умолч. | Описание |
|
||||
|------------|--------|-----------|--------------------------------------|
|
||||
| `count` | number | `5` | Количество товаров |
|
||||
| `category` | number | — | Ограничить данной категорией (опц.) |
|
||||
|
||||
**Ответ `200`:** Массив объектов [Item](#объект-item).
|
||||
|
||||
---
|
||||
|
||||
### Объект Item
|
||||
|
||||
Бэкенд может возвращать товары в **любом** из двух форматов — legacy или BackOffice. Фронтенд нормализует оба варианта.
|
||||
|
||||
```json
|
||||
{
|
||||
"categoryID": 1,
|
||||
"itemID": 123,
|
||||
"name": "iPhone 15 Pro",
|
||||
"photos": [{ "url": "https://..." }],
|
||||
"description": "Описание товара",
|
||||
"currency": "RUB",
|
||||
"price": 89990,
|
||||
"discount": 10,
|
||||
"remainings": "high",
|
||||
"rating": 4.5,
|
||||
"callbacks": [
|
||||
{
|
||||
"rating": 5,
|
||||
"content": "Отличный товар!",
|
||||
"userID": "user_123",
|
||||
"timestamp": "2026-02-01T12:00:00Z"
|
||||
}
|
||||
],
|
||||
"questions": [
|
||||
{
|
||||
"question": "Есть ли гарантия?",
|
||||
"answer": "Да, 12 месяцев",
|
||||
"upvotes": 5,
|
||||
"downvotes": 0
|
||||
}
|
||||
],
|
||||
|
||||
"id": "item_abc123",
|
||||
"visible": true,
|
||||
"priority": 10,
|
||||
"imgs": ["https://img1.jpg", "https://img2.jpg"],
|
||||
"tags": ["new", "popular"],
|
||||
"badges": ["bestseller", "sale"],
|
||||
"simpleDescription": "Краткое описание",
|
||||
"descriptionFields": [
|
||||
{ "key": "Процессор", "value": "A17 Pro" },
|
||||
{ "key": "Память", "value": "256 GB" }
|
||||
],
|
||||
"subcategoryId": "sub_001",
|
||||
"translations": {
|
||||
"en": {
|
||||
"name": "iPhone 15 Pro",
|
||||
"simpleDescription": "Short description",
|
||||
"description": [
|
||||
{ "key": "Processor", "value": "A17 Pro" }
|
||||
]
|
||||
},
|
||||
"hy": {
|
||||
"name": "iPhone 15 Pro",
|
||||
"simpleDescription": "Կարcheck check"
|
||||
}
|
||||
},
|
||||
"comments": [
|
||||
{
|
||||
"id": "cmt_001",
|
||||
"text": "Отличный товар!",
|
||||
"author": "user_123",
|
||||
"stars": 5,
|
||||
"createdAt": "2026-02-01T12:00:00Z"
|
||||
}
|
||||
],
|
||||
"quantity": 50
|
||||
}
|
||||
```
|
||||
|
||||
**Все поля Item:**
|
||||
|
||||
| Поле | Тип | Обязат. | Описание |
|
||||
|---------------------|-------------------|---------|-----------------------------------------------------------|
|
||||
| `categoryID` | number | да | Категория, к которой принадлежит товар |
|
||||
| `itemID` | number | да | Числовой ID товара (legacy) |
|
||||
| `name` | string | да | Название товара |
|
||||
| `photos` | Photo[] | нет | Массив фотографий `[{ url }]` (legacy) |
|
||||
| `description` | string | да | Текстовое описание |
|
||||
| `currency` | string | да | Код валюты (по умолч. `RUB`) |
|
||||
| `price` | number | да | Цена |
|
||||
| `discount` | number | да | Процент скидки (`0`–`100`) |
|
||||
| `remainings` | string | нет | Уровень остатка: `high`, `medium`, `low`, `out` |
|
||||
| `rating` | number | да | Средний рейтинг (`0`–`5`) |
|
||||
| `callbacks` | Review[] | нет | Отзывы (legacy формат) |
|
||||
| `questions` | Question[] | нет | Вопросы и ответы |
|
||||
| `id` | string | нет | Строковый ID из BackOffice |
|
||||
| `visible` | boolean | нет | Виден ли товар (по умолч. `true`) |
|
||||
| `priority` | number | нет | Приоритет сортировки (больше = выше) |
|
||||
| `imgs` | string[] | нет | URL картинок из BackOffice (маппится на `photos`) |
|
||||
| `tags` | string[] | нет | Теги для фильтрации |
|
||||
| `badges` | string[] | нет | Бейджи (`bestseller`, `sale` и т.д.) |
|
||||
| `simpleDescription` | string | нет | Краткое текстовое описание |
|
||||
| `descriptionFields` | DescriptionField[]| нет | Структурированное описание `[{ key, value }]` |
|
||||
| `subcategoryId` | string | нет | Ссылка на подкатегорию из BackOffice |
|
||||
| `translations` | Record | нет | Переводы по ключу языка (см. ниже) |
|
||||
| `comments` | Comment[] | нет | Комментарии в формате BackOffice |
|
||||
| `quantity` | number | нет | Числовое кол-во на складе (маппится на `remainings`) |
|
||||
|
||||
**Вложенные типы:**
|
||||
|
||||
| Тип | Поля |
|
||||
|--------------------|-----------------------------------------------------------------|
|
||||
| `Photo` | `url: string`, `photo?: string`, `video?: string`, `type?: string` |
|
||||
| `DescriptionField` | `key: string`, `value: string` |
|
||||
| `Comment` | `id?: string`, `text: string`, `author?: string`, `stars?: number`, `createdAt?: string` |
|
||||
| `Review` | `rating?: number`, `content?: string`, `userID?: string`, `answer?: string`, `timestamp?: string` |
|
||||
| `Question` | `question: string`, `answer: string`, `upvotes: number`, `downvotes: number` |
|
||||
| `ItemTranslation` | `name?: string`, `simpleDescription?: string`, `description?: DescriptionField[]` |
|
||||
|
||||
---
|
||||
|
||||
## 4. Корзина
|
||||
|
||||
### `POST /cart` — Добавить товар в корзину
|
||||
|
||||
**Тело запроса:**
|
||||
```json
|
||||
{ "itemID": 123, "quantity": 1 }
|
||||
```
|
||||
|
||||
**Ответ `200`:**
|
||||
```json
|
||||
{ "message": "Added to cart" }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `PATCH /cart` — Обновить количество товара
|
||||
|
||||
**Тело запроса:**
|
||||
```json
|
||||
{ "itemID": 123, "quantity": 3 }
|
||||
```
|
||||
|
||||
**Ответ `200`:**
|
||||
```json
|
||||
{ "message": "Updated" }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `DELETE /cart` — Удалить товары из корзины
|
||||
|
||||
**Тело запроса:** Массив ID товаров
|
||||
```json
|
||||
[123, 456]
|
||||
```
|
||||
|
||||
**Ответ `200`:**
|
||||
```json
|
||||
{ "message": "Removed" }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `GET /cart` — Получить содержимое корзины
|
||||
|
||||
**Ответ `200`:** Массив объектов [Item](#объект-item) (каждый с полем `quantity`).
|
||||
|
||||
---
|
||||
|
||||
## 5. Оплата (СБП / QR)
|
||||
|
||||
### `POST /cart` — Создать платёж (СБП QR)
|
||||
|
||||
> Примечание: Тот же эндпоинт что и добавление в корзину, но с другой схемой тела запроса.
|
||||
|
||||
**Тело запроса:**
|
||||
```json
|
||||
{
|
||||
"amount": 89990,
|
||||
"currency": "RUB",
|
||||
"siteuserID": "tg_123456789",
|
||||
"siteorderID": "order_abc123",
|
||||
"redirectUrl": "",
|
||||
"telegramUsername": "john_doe",
|
||||
"items": [
|
||||
{ "itemID": 123, "price": 89990, "name": "iPhone 15 Pro" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Ответ `200`:**
|
||||
```json
|
||||
{
|
||||
"qrId": "qr_abc123",
|
||||
"qrStatus": "CREATED",
|
||||
"qrExpirationDate": "2026-02-28T13:00:00Z",
|
||||
"payload": "https://qr.nspk.ru/...",
|
||||
"qrUrl": "https://qr.nspk.ru/..."
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `GET /qr/payment/:qrId` — Проверить статус оплаты
|
||||
|
||||
**Ответ `200`:**
|
||||
```json
|
||||
{
|
||||
"additionalInfo": "",
|
||||
"paymentPurpose": "Order #order_abc123",
|
||||
"amount": 89990,
|
||||
"code": "SUCCESS",
|
||||
"createDate": "2026-02-28T12:00:00Z",
|
||||
"currency": "RUB",
|
||||
"order": "order_abc123",
|
||||
"paymentStatus": "COMPLETED",
|
||||
"qrId": "qr_abc123",
|
||||
"transactionDate": "2026-02-28T12:01:00Z",
|
||||
"transactionId": 999,
|
||||
"qrExpirationDate": "2026-02-28T13:00:00Z",
|
||||
"phoneNumber": "+7XXXXXXXXXX"
|
||||
}
|
||||
```
|
||||
|
||||
| Значение `paymentStatus` | Значение |
|
||||
|--------------------------|------------------------------|
|
||||
| `CREATED` | QR создан, не оплачен |
|
||||
| `WAITING` | Оплата в процессе |
|
||||
| `COMPLETED` | Оплата успешна |
|
||||
| `EXPIRED` | QR-код истёк |
|
||||
| `CANCELLED` | Оплата отменена |
|
||||
|
||||
---
|
||||
|
||||
### `POST /purchase-email` — Отправить email после оплаты
|
||||
|
||||
**Тело запроса:**
|
||||
```json
|
||||
{
|
||||
"email": "user@example.com",
|
||||
"telegramUserId": "123456789",
|
||||
"items": [
|
||||
{ "itemID": 123, "name": "iPhone 15 Pro", "price": 89990, "currency": "RUB" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Ответ `200`:**
|
||||
```json
|
||||
{ "message": "Email sent" }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Отзывы / Комментарии
|
||||
|
||||
### `POST /comment` — Оставить отзыв
|
||||
|
||||
**Тело запроса:**
|
||||
```json
|
||||
{
|
||||
"itemID": 123,
|
||||
"rating": 5,
|
||||
"comment": "Отличный товар!",
|
||||
"username": "john_doe",
|
||||
"userId": 123456789,
|
||||
"timestamp": "2026-02-28T12:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
**Ответ `200`:**
|
||||
```json
|
||||
{ "message": "Review submitted" }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Регионы
|
||||
|
||||
### `GET /regions` — Список доступных регионов
|
||||
|
||||
Возвращает регионы, в которых работает маркетплейс.
|
||||
|
||||
**Ответ `200`:**
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "moscow",
|
||||
"city": "Москва",
|
||||
"country": "Россия",
|
||||
"countryCode": "RU",
|
||||
"timezone": "Europe/Moscow"
|
||||
},
|
||||
{
|
||||
"id": "spb",
|
||||
"city": "Санкт-Петербург",
|
||||
"country": "Россия",
|
||||
"countryCode": "RU",
|
||||
"timezone": "Europe/Moscow"
|
||||
},
|
||||
{
|
||||
"id": "yerevan",
|
||||
"city": "Ереван",
|
||||
"country": "Армения",
|
||||
"countryCode": "AM",
|
||||
"timezone": "Asia/Yerevan"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
**Объект Region:**
|
||||
|
||||
| Поле | Тип | Обязат. | Описание |
|
||||
|---------------|--------|---------|----------------------------------|
|
||||
| `id` | string | да | Уникальный идентификатор региона |
|
||||
| `city` | string | да | Название города |
|
||||
| `country` | string | да | Название страны |
|
||||
| `countryCode` | string | да | Код страны ISO 3166-1 alpha-2 |
|
||||
| `timezone` | string | нет | Часовой пояс IANA |
|
||||
|
||||
> **Фоллбэк:** Если эндпоинт недоступен, фронтенд использует 6 захардкоженных значений: Москва, СПб, Ереван, Минск, Алматы, Тбилиси.
|
||||
|
||||
---
|
||||
|
||||
## 8. Авторизация (вход через Telegram)
|
||||
|
||||
Авторизация **через Telegram** с **cookie-сессиями** (HttpOnly, Secure, SameSite=None).
|
||||
|
||||
Все auth-эндпоинты должны поддерживать CORS с `credentials: true`.
|
||||
|
||||
### Процесс авторизации
|
||||
|
||||
```
|
||||
1. Пользователь нажимает «Оформить заказ» → не авторизован → показывается диалог входа
|
||||
2. Нажимает «Войти через Telegram» → открывается https://t.me/{bot}?start=auth_{callback}
|
||||
3. Пользователь запускает бота в Telegram
|
||||
4. Бот отправляет данные пользователя → бэкенд /auth/telegram/callback
|
||||
5. Бэкенд создаёт сессию → устанавливает Set-Cookie
|
||||
6. Фронтенд опрашивает GET /auth/session каждые 3 секунды
|
||||
7. Сессия обнаружена → диалог закрывается → оформление заказа продолжается
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `GET /auth/session` — Проверить текущую сессию
|
||||
|
||||
**Запрос:** Только cookie (сессионная cookie, установленная бэкендом).
|
||||
|
||||
**Ответ `200`** (авторизован):
|
||||
```json
|
||||
{
|
||||
"sessionId": "sess_abc123",
|
||||
"telegramUserId": 123456789,
|
||||
"username": "john_doe",
|
||||
"displayName": "John Doe",
|
||||
"active": true,
|
||||
"expiresAt": "2026-03-01T12:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
**Ответ `200`** (сессия истекла):
|
||||
```json
|
||||
{
|
||||
"sessionId": "sess_abc123",
|
||||
"telegramUserId": 123456789,
|
||||
"username": "john_doe",
|
||||
"displayName": "John Doe",
|
||||
"active": false,
|
||||
"expiresAt": "2026-02-27T12:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
**Ответ `401`** (нет сессии):
|
||||
```json
|
||||
{ "error": "No active session" }
|
||||
```
|
||||
|
||||
**Объект AuthSession:**
|
||||
|
||||
| Поле | Тип | Обязат. | Описание |
|
||||
|------------------|---------|---------|-------------------------------------------|
|
||||
| `sessionId` | string | да | Уникальный ID сессии |
|
||||
| `telegramUserId` | number | да | ID пользователя в Telegram |
|
||||
| `username` | string? | нет | @username в Telegram (может быть null) |
|
||||
| `displayName` | string | да | Отображаемое имя (имя + фамилия) |
|
||||
| `active` | boolean | да | Действительна ли сессия |
|
||||
| `expiresAt` | string | да | Дата истечения в формате ISO 8601 |
|
||||
|
||||
---
|
||||
|
||||
### `GET /auth/telegram/callback` — Callback авторизации Telegram-бота
|
||||
|
||||
Вызывается Telegram-ботом после авторизации пользователя.
|
||||
|
||||
**Тело запроса (от бота):**
|
||||
```json
|
||||
{
|
||||
"id": 123456789,
|
||||
"first_name": "John",
|
||||
"last_name": "Doe",
|
||||
"username": "john_doe",
|
||||
"photo_url": "https://t.me/i/userpic/...",
|
||||
"auth_date": 1709100000,
|
||||
"hash": "abc123def456..."
|
||||
}
|
||||
```
|
||||
|
||||
**Ответ:** Должен установить cookie сессии и вернуть:
|
||||
```json
|
||||
{
|
||||
"sessionId": "sess_abc123",
|
||||
"message": "Authenticated successfully"
|
||||
}
|
||||
```
|
||||
|
||||
**Требования к cookie:**
|
||||
|
||||
| Атрибут | Значение | Примечание |
|
||||
|------------|----------------|-----------------------------------------------------|
|
||||
| `HttpOnly` | `true` | Недоступна из JavaScript |
|
||||
| `Secure` | `true` | Только HTTPS |
|
||||
| `SameSite` | `None` | Обязательно для cross-origin (API ≠ фронтенд) |
|
||||
| `Path` | `/` | |
|
||||
| `Max-Age` | `86400` (24ч) | Или по необходимости |
|
||||
|
||||
---
|
||||
|
||||
### `POST /auth/logout` — Завершить сессию
|
||||
|
||||
**Запрос:** Только cookie, пустое тело `{}`
|
||||
|
||||
**Ответ `200`:**
|
||||
```json
|
||||
{ "message": "Logged out" }
|
||||
```
|
||||
|
||||
Должен очистить/инвалидировать cookie сессии.
|
||||
|
||||
---
|
||||
|
||||
### Обновление сессии
|
||||
|
||||
Фронтенд повторно проверяет сессию за **60 секунд до `expiresAt`**. Если бэкенд поддерживает скользящий срок действия (sliding expiration), можно обновлять `Max-Age` cookie при каждом вызове `GET /auth/session`.
|
||||
|
||||
---
|
||||
|
||||
## 9. i18n / Переводы
|
||||
|
||||
Фронтенд поддерживает 3 языка: **Русский (ru)**, **Английский (en)**, **Армянский (hy)**.
|
||||
|
||||
Активный язык отправляется через HTTP-заголовок `X-Language` с каждым запросом.
|
||||
|
||||
### Что бэкенд должен делать с `X-Language`
|
||||
|
||||
1. **Категории и товары**: Если для запрошенного языка есть поле `translations`, вернуть переведённые `name`, `description` и т.д. ИЛИ бэкенд может применять переводы на стороне сервера и возвращать уже переведённые поля.
|
||||
|
||||
2. **Поле `translations`** на товарах (опциональный подход):
|
||||
```json
|
||||
{
|
||||
"translations": {
|
||||
"en": {
|
||||
"name": "iPhone 15 Pro",
|
||||
"simpleDescription": "Short desc in English",
|
||||
"description": [{ "key": "Processor", "value": "A17 Pro" }]
|
||||
},
|
||||
"hy": {
|
||||
"name": "iPhone 15 Pro",
|
||||
"simpleDescription": "Կarcheck check"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. **Рекомендуемый подход**: Читать заголовок `X-Language` и возвращать `name`/`description` на этом языке напрямую. Если перевода нет — возвращать русский вариант по умолчанию.
|
||||
|
||||
---
|
||||
|
||||
## 10. Настройка CORS
|
||||
|
||||
Для работы auth-cookie и кастомных заголовков конфигурация CORS бэкенда должна включать:
|
||||
|
||||
```
|
||||
Access-Control-Allow-Origin: https://dexarmarket.ru (НЕ wildcard *)
|
||||
Access-Control-Allow-Credentials: true
|
||||
Access-Control-Allow-Headers: Content-Type, X-Region, X-Language
|
||||
Access-Control-Allow-Methods: GET, POST, PATCH, DELETE, OPTIONS
|
||||
```
|
||||
|
||||
> **Важно:** `Access-Control-Allow-Origin` не может быть `*` при `Allow-Credentials: true`. Должен быть точный origin фронтенда.
|
||||
|
||||
**Разрешённые origins:**
|
||||
- `https://dexarmarket.ru`
|
||||
- `https://novo.market`
|
||||
- `http://localhost:4200` (dev)
|
||||
- `http://localhost:4201` (dev, Novo)
|
||||
|
||||
---
|
||||
|
||||
## 11. Настройка Telegram-бота
|
||||
|
||||
Каждому бренду нужен свой бот:
|
||||
- **Dexar:** `@dexarmarket_bot`
|
||||
- **Novo:** `@novomarket_bot`
|
||||
|
||||
Бот должен:
|
||||
1. Слушать команду `/start auth_{callbackUrl}`
|
||||
2. Извлечь callback URL
|
||||
3. Отправить данные пользователя (`id`, `first_name`, `username` и т.д.) на этот callback URL
|
||||
4. Callback URL: `{apiUrl}/auth/telegram/callback`
|
||||
|
||||
---
|
||||
|
||||
## Полный справочник эндпоинтов
|
||||
|
||||
### Новые эндпоинты
|
||||
|
||||
| Метод | Путь | Описание | Авторизация |
|
||||
|--------|---------------------------|---------------------------------|-------------|
|
||||
| `GET` | `/regions` | Список доступных регионов | Нет |
|
||||
| `GET` | `/auth/session` | Проверка текущей сессии | Cookie |
|
||||
| `GET` | `/auth/telegram/callback` | Callback авторизации через бота | Нет (бот) |
|
||||
| `POST` | `/auth/logout` | Завершение сессии | Cookie |
|
||||
|
||||
### Существующие эндпоинты
|
||||
|
||||
| Метод | Путь | Описание | Авт. | Заголовки |
|
||||
|----------|-----------------------|---------------------------|------|--------------------|
|
||||
| `GET` | `/ping` | Проверка состояния | Нет | — |
|
||||
| `GET` | `/category` | Список категорий | Нет | X-Region, X-Language |
|
||||
| `GET` | `/category/:id` | Товары категории | Нет | X-Region, X-Language |
|
||||
| `GET` | `/item/:id` | Один товар | Нет | X-Region, X-Language |
|
||||
| `GET` | `/searchitems` | Поиск товаров | Нет | X-Region, X-Language |
|
||||
| `GET` | `/randomitems` | Случайные товары | Нет | X-Region, X-Language |
|
||||
| `POST` | `/cart` | Добавить в корзину / Оплата | Нет* | — |
|
||||
| `PATCH` | `/cart` | Обновить кол-во | Нет* | — |
|
||||
| `DELETE` | `/cart` | Удалить из корзины | Нет* | — |
|
||||
| `GET` | `/cart` | Содержимое корзины | Нет* | — |
|
||||
| `POST` | `/comment` | Оставить отзыв | Нет | — |
|
||||
| `GET` | `/qr/payment/:qrId` | Статус оплаты | Нет | — |
|
||||
| `POST` | `/purchase-email` | Отправить email после оплаты | Нет | — |
|
||||
|
||||
> \* Эндпоинты корзины/оплаты могут использовать cookie сессии (если есть) для привязки к заказу, но не требуют авторизации строго. Фронтенд проверяет авторизацию перед оформлением заказа.
|
||||
@@ -4,6 +4,7 @@ import { provideHttpClient, withInterceptors } from '@angular/common/http';
|
||||
|
||||
import { routes } from './app.routes';
|
||||
import { cacheInterceptor } from './interceptors/cache.interceptor';
|
||||
import { apiHeadersInterceptor } from './interceptors/api-headers.interceptor';
|
||||
import { provideServiceWorker } from '@angular/service-worker';
|
||||
|
||||
export const appConfig: ApplicationConfig = {
|
||||
@@ -15,7 +16,7 @@ export const appConfig: ApplicationConfig = {
|
||||
withInMemoryScrolling({ scrollPositionRestoration: 'top' })
|
||||
),
|
||||
provideHttpClient(
|
||||
withInterceptors([cacheInterceptor])
|
||||
withInterceptors([apiHeadersInterceptor, cacheInterceptor])
|
||||
),
|
||||
provideServiceWorker('ngsw-worker.js', {
|
||||
enabled: !isDevMode(),
|
||||
|
||||
37
src/app/interceptors/api-headers.interceptor.ts
Normal file
37
src/app/interceptors/api-headers.interceptor.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { HttpInterceptorFn } from '@angular/common/http';
|
||||
import { inject } from '@angular/core';
|
||||
import { LocationService } from '../services/location.service';
|
||||
import { LanguageService } from '../services/language.service';
|
||||
import { environment } from '../../environments/environment';
|
||||
|
||||
/**
|
||||
* Interceptor that attaches X-Region and X-Language headers
|
||||
* to every outgoing request aimed at our API.
|
||||
*
|
||||
* The backend reads these headers to:
|
||||
* - filter catalog by region
|
||||
* - return translated content in the requested language
|
||||
*/
|
||||
export const apiHeadersInterceptor: HttpInterceptorFn = (req, next) => {
|
||||
// Only attach headers to our own API requests
|
||||
if (!req.url.startsWith(environment.apiUrl)) {
|
||||
return next(req);
|
||||
}
|
||||
|
||||
const locationService = inject(LocationService);
|
||||
const languageService = inject(LanguageService);
|
||||
|
||||
const regionId = locationService.regionId(); // '' when global
|
||||
const lang = languageService.currentLanguage(); // 'ru' | 'en' | 'hy'
|
||||
|
||||
let headers = req.headers;
|
||||
|
||||
if (regionId) {
|
||||
headers = headers.set('X-Region', regionId);
|
||||
}
|
||||
if (lang) {
|
||||
headers = headers.set('X-Language', lang);
|
||||
}
|
||||
|
||||
return next(req.clone({ headers }));
|
||||
};
|
||||
Reference in New Issue
Block a user