727 lines
29 KiB
Markdown
727 lines
29 KiB
Markdown
|
|
# Полная документация 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 сессии (если есть) для привязки к заказу, но не требуют авторизации строго. Фронтенд проверяет авторизацию перед оформлением заказа.
|