clean up stage 1

This commit is contained in:
sdarbinyan
2026-07-05 00:38:21 +04:00
parent eaa830af3f
commit 58b5a4e996
195 changed files with 66 additions and 13717 deletions

View File

@@ -1,726 +0,0 @@
# Complete Backend API Documentation
> **Last updated:** February 2026
> **Frontend:** Angular 21 · Dual-brand (Dexar + Novo)
> **Covers:** Catalog, Cart, Payments, Reviews, Regions, Auth, i18n, BackOffice
---
## Base URLs
| 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` |
---
## 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
[
{
"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 object:**
| Field | Type | Required | Description |
|---------------|--------|----------|--------------------------|
| `id` | string | yes | Unique region identifier |
| `city` | string | yes | City name (display) |
| `country` | string | yes | Country name |
| `countryCode` | string | yes | ISO 3166-1 alpha-2 |
| `timezone` | string | no | IANA timezone |
> **Fallback:** If this endpoint is down, the frontend uses 6 hardcoded defaults: Moscow, SPB, Yerevan, Minsk, Almaty, Tbilisi.
---
## 8. Authentication (Telegram Login)
Authentication is **Telegram-based** with **cookie sessions** (HttpOnly, Secure, SameSite=None).
All auth endpoints must include `withCredentials: true` CORS support.
### Auth flow
```
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
```
---
### `GET /auth/session` — Check current session
**Request:** Cookies only (session cookie set by backend).
**Response `200`** (authenticated):
```json
{
"sessionId": "sess_abc123",
"telegramUserId": 123456789,
"username": "john_doe",
"displayName": "John Doe",
"active": true,
"expiresAt": "2026-03-01T12:00:00Z"
}
```
**Response `200`** (expired):
```json
{
"sessionId": "sess_abc123",
"telegramUserId": 123456789,
"username": "john_doe",
"displayName": "John Doe",
"active": false,
"expiresAt": "2026-02-27T12:00:00Z"
}
```
**Response `401`** (no session):
```json
{ "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 + last) |
| `active` | boolean | yes | Whether session is valid |
| `expiresAt` | string | yes | ISO 8601 expiration datetime |
---
### `GET /auth/telegram/callback` — Telegram bot auth callback
Called by the Telegram bot after user authenticates.
**Request body (from bot):**
```json
{
"id": 123456789,
"first_name": "John",
"last_name": "Doe",
"username": "john_doe",
"photo_url": "https://t.me/i/userpic/...",
"auth_date": 1709100000,
"hash": "abc123def456..."
}
```
**Response:** Must set a session cookie and return:
```json
{
"sessionId": "sess_abc123",
"message": "Authenticated successfully"
}
```
**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 |
---
### `POST /auth/logout` — End session
**Request:** Cookies only, empty body `{}`
**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
{
"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. **Recommended approach**: Read `X-Language` header and return the `name`/`description` in that language directly. If no translation exists, return the Russian default.
---
## 10. CORS Configuration
For auth cookies and custom headers to work, the backend CORS config must include:
```
Access-Control-Allow-Origin: https://dexarmarket.ru (NOT 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
```
> **Important:** `Access-Control-Allow-Origin` cannot be `*` when `Allow-Credentials: true`. Must be the exact frontend origin.
**Allowed origins:**
- `https://dexarmarket.ru`
- `https://novo.market`
- `http://localhost:4200` (dev)
- `http://localhost:4201` (dev, Novo)
---
## 11. Telegram Bot Setup
Each brand needs its own bot:
- **Dexar:** `@dexarmarket_bot`
- **Novo:** `@novomarket_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
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.

View File

@@ -1,726 +0,0 @@
# Полная документация 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 сессии (если есть) для привязки к заказу, но не требуют авторизации строго. Фронтенд проверяет авторизацию перед оформлением заказа.

View File

@@ -1,824 +0,0 @@
# Авторизация через Telegram — Backend & Bot
> Всё что нужно Go-разработчику для реализации авторизации.
> Фронтенд **полностью готов** и ждёт эти эндпоинты.
---
## Статус
| Компонент | Готов? |
|-----------|--------|
| Frontend (Angular) — диалог, QR, поллинг, корзина | ✅ Готов |
| Telegram бот (обработка `/start`) | ❌ Нужно |
| Backend — 6 HTTP-эндпоинтов | ❌ Нужно |
| Хранилище сессий + QR-токенов | ❌ Нужно |
| CORS для cookie-based запросов | ❌ Нужно |
---
## Архитектура
Два сценария авторизации:
### Сценарий 1: Прямой вход (кнопка "Войти через Telegram")
Пользователь нажимает кнопку → открывается Telegram → бот выдаёт кнопку "Войти на сайт" → callback ставит cookie → фронтенд поллит `/auth/session`.
### Сценарий 2: QR-логин с десктопа (основной)
```
ДЕСКТОП БРАУЗЕР СЕРВЕР (Go) TELEGRAM
│ │ │
│ 1. POST /auth/qr/create │ │
│ ─────────────────────────────> │ │
│ { token: "abc", url: "..." } │ │
│ <───────────────────────────── │ │
│ │ │
│ 2. Показать QR: │ │
│ t.me/Bot?start=login_abc │ │
│ │ │
│ ПОЛЬЗОВАТЕЛЬ СКАНИРУЕТ ТЕЛЕФОНОМ │
│ │ │
│ │ 3. /start login_abc │
│ │ <────────────────────────│
│ │ │
│ │ Бот → POST /auth/qr/confirm
│ │ Бот → "✅ Вы вошли!" │
│ │ ────────────────────────>│
│ │ │
│ 4. GET /auth/qr/poll?token=abc │ │
│ (каждые 3 сек) │ │
│ ─────────────────────────────> │ │
│ { status: "confirmed", │ │
│ session: {...} } │ │
│ + Set-Cookie: dx_session=... │ │
│ <───────────────────────────── │ │
│ │ │
│ 5. POST /websession/{sessionId} │ │
│ [{ itemID, quantity, ... }] │ ← корзина │
│ ─────────────────────────────> │ │
│ │ │
│ 6. Готово! Авторизован + корзина│ │
```
---
## Бренды и боты
| Бренд | Username бота | Домен фронтенда | API сервер | Cookie Domain |
|-------|---------------|------------------|------------|---------------|
| Dexar | `DexarSupport_bot` | `dexarmarket.ru` | `api.dexarmarket.ru:445` | `.dexarmarket.ru` |
| Novo | `novomarket_bot` | `novo.market` | `api.novo.market:444` | `.novo.market` |
Бот создаётся через https://t.me/BotFather → `/newbot`. Сохранить `BOT_TOKEN`.
---
## Хранилище
### Структура: Сессия
```go
type Session struct {
SessionID string `json:"sessionId"`
TelegramUserID int64 `json:"telegramUserId"`
Username *string `json:"username"` // может быть null
DisplayName string `json:"displayName"`
Active bool `json:"active"`
ExpiresAt time.Time `json:"expiresAt"`
}
```
**TTL:** 24 часа.
### Структура: QR-токен (одноразовый)
```go
type AuthToken struct {
Token string `json:"token"`
Status string `json:"status"` // "pending" | "confirmed" | "expired"
SessionID string `json:"sessionId"` // заполняется после подтверждения ботом
CreatedAt time.Time `json:"createdAt"`
ExpiresAt time.Time `json:"expiresAt"`
}
```
**TTL:** 5 минут.
### Варианты хранения
**Redis (рекомендуется):**
```go
// Сессия
redisClient.Set(ctx, "session:"+s.SessionID, json, 24*time.Hour)
// QR-токен
redisClient.Set(ctx, "auth_token:"+t.Token, json, 5*time.Minute)
```
**sync.Map (для MVP):**
```go
var sessions sync.Map
var authTokens sync.Map
// Очистка устаревших токенов — запустить горутину при старте
func cleanupExpiredTokens() {
ticker := time.NewTicker(1 * time.Minute)
for range ticker.C {
authTokens.Range(func(key, value any) bool {
t := value.(AuthToken)
if time.Now().After(t.ExpiresAt) {
authTokens.Delete(key)
}
return true
})
}
}
```
---
## HTTP-эндпоинты
### 1. `POST /auth/qr/create`
Фронтенд вызывает при открытии диалога логина. Создаёт одноразовый QR-токен.
```go
func handleQrCreate(w http.ResponseWriter, r *http.Request) {
// 1. Сгенерировать криптографически безопасный токен
tokenBytes := make([]byte, 32)
if _, err := rand.Read(tokenBytes); err != nil {
http.Error(w, "internal error", 500)
return
}
token := base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(tokenBytes)
// 2. Определить бота по origin
botUsername := getBotForOrigin(r.Header.Get("Origin"))
// 3. Сохранить токен
authToken := AuthToken{
Token: token,
Status: "pending",
CreatedAt: time.Now(),
ExpiresAt: time.Now().Add(5 * time.Minute),
}
saveAuthToken(authToken)
// 4. Ответить
qrURL := fmt.Sprintf("https://t.me/%s?start=login_%s", botUsername, token)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{
"token": token,
"url": qrURL,
})
}
func getBotForOrigin(origin string) string {
if strings.Contains(origin, "novo.market") {
return "novomarket_bot"
}
return "DexarSupport_bot"
}
```
**Ответ:**
```json
{ "token": "dG9rZW4tYWJj....", "url": "https://t.me/DexarSupport_bot?start=login_dG9rZW4tYWJj...." }
```
> Telegram ограничивает `start` до 64 символов. `login_` (6) + base64url из 32 байт (43) = 49 ✅
---
### 2. `GET /auth/qr/poll?token={token}`
Фронтенд вызывает каждые 3 секунды. Когда бот подтвердил — возвращает сессию и ставит cookie.
```go
func handleQrPoll(w http.ResponseWriter, r *http.Request) {
tokenStr := r.URL.Query().Get("token")
if tokenStr == "" {
http.Error(w, "missing token", 400)
return
}
authToken, ok := getAuthToken(tokenStr)
if !ok {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"status": "expired"})
return
}
switch authToken.Status {
case "pending":
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"status": "pending"})
case "confirmed":
session, err := getSession(authToken.SessionID)
if err != nil {
http.Error(w, "session not found", 500)
return
}
// Cookie в ДЕСКТОПНЫЙ браузер
domain := getDomainForOrigin(r.Header.Get("Origin"))
http.SetCookie(w, &http.Cookie{
Name: "dx_session",
Value: session.SessionID,
Path: "/",
HttpOnly: true,
Secure: true,
SameSite: http.SameSiteNoneMode,
MaxAge: 86400,
Domain: domain,
})
// Удалить использованный токен
deleteAuthToken(tokenStr)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"status": "confirmed",
"session": session,
})
}
}
func getDomainForOrigin(origin string) string {
if strings.Contains(origin, "novo.market") {
return ".novo.market"
}
return ".dexarmarket.ru"
}
```
**Ответы:**
| Статус | JSON |
|--------|------|
| Ждём | `{ "status": "pending" }` |
| Подтверждено | `{ "status": "confirmed", "session": { sessionId, telegramUserId, username, displayName, active, expiresAt } }` + `Set-Cookie` |
| Истекло | `{ "status": "expired" }` |
---
### 3. `POST /auth/qr/confirm` (внутренний, для бота)
Бот вызывает когда пользователь отсканировал QR. Привязывает сессию к токену.
```go
func handleQrConfirm(w http.ResponseWriter, r *http.Request) {
// Проверить секрет бота
if r.Header.Get("X-Bot-Secret") != os.Getenv("BOT_INTERNAL_SECRET") {
http.Error(w, "forbidden", 403)
return
}
var req struct {
Token string `json:"token"`
User struct {
ID int64 `json:"id"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Username string `json:"username"`
} `json:"telegram_user"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "bad request", 400)
return
}
authToken, ok := getAuthToken(req.Token)
if !ok || authToken.Status != "pending" {
http.Error(w, "token not found or already used", 404)
return
}
// Создать сессию
displayName := req.User.FirstName
if req.User.LastName != "" {
displayName += " " + req.User.LastName
}
var username *string
if req.User.Username != "" {
username = &req.User.Username
}
session := Session{
SessionID: uuid.New().String(),
TelegramUserID: req.User.ID,
Username: username,
DisplayName: displayName,
Active: true,
ExpiresAt: time.Now().Add(24 * time.Hour),
}
saveSession(session)
// Привязать сессию к токену
authToken.Status = "confirmed"
authToken.SessionID = session.SessionID
saveAuthToken(*authToken)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
}
```
**Запрос от бота:**
```json
{
"token": "dG9rZW4tYWJj...",
"telegram_user": {
"id": 123456789,
"first_name": "Иван",
"last_name": "Петров",
"username": "ivan_petrov"
}
}
```
---
### 4. `GET /auth/session`
Фронтенд вызывает для проверки текущей сессии. Читает cookie.
```go
func handleGetSession(w http.ResponseWriter, r *http.Request) {
cookie, err := r.Cookie("dx_session")
if err != nil {
http.Error(w, "unauthorized", 401)
return
}
session, err := getSession(cookie.Value)
if err != nil {
http.Error(w, "unauthorized", 401)
return
}
if time.Now().After(session.ExpiresAt) {
session.Active = false
saveSession(session)
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(session)
}
```
**Формат ответа (200)** — фронтенд ожидает **точно эти поля**:
```json
{
"sessionId": "550e8400-e29b-41d4-a716-446655440000",
"telegramUserId": 123456789,
"username": "ivan_petrov",
"displayName": "Иван Петров",
"active": true,
"expiresAt": "2026-03-25T14:30:00Z"
}
```
| Поле | Тип | Обязательно | Примечание |
|------|-----|-------------|------------|
| `sessionId` | string (UUID) | да | Используется для `/websession/{sessionId}` |
| `telegramUserId` | number | да | Telegram user ID |
| `username` | string / null | нет | Telegram @username |
| `displayName` | string | да | "Имя Фамилия" — показывается в UI |
| `active` | boolean | да | `false` = истекла |
| `expiresAt` | string (ISO 8601) | да | Фронтенд перепроверяет за 60 сек до |
**Ошибка:** любой HTTP не-200 → фронтенд считает "не авторизован".
---
### 5. `GET /auth/telegram/callback`
Для прямого входа (по кнопке в Telegram, не через QR). Открывается в браузере.
```go
func handleTelegramCallback(w http.ResponseWriter, r *http.Request) {
token := r.URL.Query().Get("token")
if token == "" {
http.Error(w, "missing token", 400)
return
}
session, err := getSession(token)
if err != nil || !session.Active {
http.Error(w, "invalid or expired token", 401)
return
}
domain := getDomainForOrigin(r.Header.Get("Origin"))
if domain == "" {
domain = ".dexarmarket.ru" // fallback для прямого перехода
}
http.SetCookie(w, &http.Cookie{
Name: "dx_session",
Value: session.SessionID,
Path: "/",
HttpOnly: true,
Secure: true,
SameSite: http.SameSiteNoneMode,
MaxAge: 86400,
Domain: domain,
})
// Редирект на сайт
http.Redirect(w, r, "https://dexarmarket.ru", http.StatusFound)
}
```
---
### 6. `POST /auth/logout`
```go
func handleLogout(w http.ResponseWriter, r *http.Request) {
cookie, err := r.Cookie("dx_session")
if err == nil {
deleteSession(cookie.Value)
}
http.SetCookie(w, &http.Cookie{
Name: "dx_session",
Value: "",
Path: "/",
HttpOnly: true,
Secure: true,
SameSite: http.SameSiteNoneMode,
MaxAge: -1,
Domain: ".dexarmarket.ru",
})
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"message":"ok"}`))
}
```
---
## Cookie-параметры
| Параметр | Значение | Почему |
|----------|----------|--------|
| `Name` | `dx_session` | |
| `SameSite` | `None` | Фронтенд на `dexarmarket.ru`, API на `api.dexarmarket.ru:445` — разные origins |
| `Secure` | `true` | Обязательно при `SameSite=None` |
| `Domain` | `.dexarmarket.ru` | Доступна и на `dexarmarket.ru` и на `api.dexarmarket.ru` |
| `HttpOnly` | `true` | Недоступна из JS — защита от XSS |
| `MaxAge` | `86400` | 24 часа |
---
## CORS
Фронтенд шлёт `withCredentials: true`. Бэкенд обязан вернуть правильные заголовки.
```go
func corsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
origin := r.Header.Get("Origin")
allowed := map[string]bool{
"https://dexarmarket.ru": true,
"https://www.dexarmarket.ru": true,
"https://novo.market": true,
"https://www.novo.market": true,
"http://localhost:4200": true,
}
if allowed[origin] {
w.Header().Set("Access-Control-Allow-Origin", origin) // НЕ "*"
w.Header().Set("Access-Control-Allow-Credentials", "true")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
}
if r.Method == "OPTIONS" {
w.WriteHeader(200)
return
}
next.ServeHTTP(w, r)
})
}
```
> **Критично:** `Access-Control-Allow-Origin` не может быть `"*"` при `withCredentials`. Вернуть конкретный origin.
---
## Роутинг
```go
mux := http.NewServeMux()
// Существующие
mux.HandleFunc("GET /items/{id}", handleGetItem)
mux.HandleFunc("GET /category", handleGetCategories)
mux.HandleFunc("POST /websession/{id}", handleWebSession)
mux.HandleFunc("POST /websession/{id}/qr", handleCreateQR)
mux.HandleFunc("GET /websession/{id}/{qrId}", handleCheckPayment)
// Auth — прямой вход
mux.HandleFunc("GET /auth/session", handleGetSession)
mux.HandleFunc("GET /auth/telegram/callback", handleTelegramCallback)
mux.HandleFunc("POST /auth/logout", handleLogout)
// Auth — QR-логин
mux.HandleFunc("POST /auth/qr/create", handleQrCreate)
mux.HandleFunc("GET /auth/qr/poll", handleQrPoll)
mux.HandleFunc("POST /auth/qr/confirm", handleQrConfirm)
handler := corsMiddleware(mux)
http.ListenAndServeTLS(":445", "cert.pem", "key.pem", handler)
```
---
## Telegram-бот
### Обработчик `/start`
```go
const (
confirmURL = "http://localhost:8080/auth/qr/confirm"
botInternalSecret = os.Getenv("BOT_INTERNAL_SECRET")
)
func handleStart(update tgbotapi.Update) {
text := update.Message.Text
user := update.Message.From
switch {
case strings.HasPrefix(text, "/start login_"):
handleQrLogin(update, user, strings.TrimPrefix(text, "/start login_"))
case strings.HasPrefix(text, "/start auth"):
handleDirectAuth(update, user)
default:
sendWelcome(update)
}
}
```
### QR-логин (основной)
```go
func handleQrLogin(update tgbotapi.Update, user *tgbotapi.User, token string) {
reqBody := map[string]interface{}{
"token": token,
"telegram_user": map[string]interface{}{
"id": user.ID,
"first_name": user.FirstName,
"last_name": user.LastName,
"username": user.UserName,
},
}
bodyBytes, _ := json.Marshal(reqBody)
req, _ := http.NewRequest("POST", confirmURL, bytes.NewReader(bodyBytes))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-Bot-Secret", botInternalSecret)
resp, err := http.DefaultClient.Do(req)
if err != nil || resp.StatusCode != 200 {
msg := tgbotapi.NewMessage(update.Message.Chat.ID,
"❌ Не удалось войти. QR-код мог устареть. Попробуйте обновить страницу и отсканировать новый QR.")
bot.Send(msg)
return
}
defer resp.Body.Close()
displayName := buildDisplayName(user)
msg := tgbotapi.NewMessage(update.Message.Chat.ID,
fmt.Sprintf("✅ Вы вошли на сайт как %s!\n\nМожете вернуться в браузер — страница обновится автоматически.", displayName))
bot.Send(msg)
}
func buildDisplayName(user *tgbotapi.User) string {
name := user.FirstName
if user.LastName != "" {
name += " " + user.LastName
}
return name
}
```
### Прямой вход (кнопка, для обратной совместимости)
```go
func handleDirectAuth(update tgbotapi.Update, user *tgbotapi.User) {
session := Session{
SessionID: uuid.New().String(),
TelegramUserID: user.ID,
Username: stringPtr(user.UserName),
DisplayName: buildDisplayName(user),
Active: true,
ExpiresAt: time.Now().Add(24 * time.Hour),
}
saveSession(session)
callbackURL := "https://api.dexarmarket.ru:445/auth/telegram/callback"
loginURL := callbackURL + "?token=" + session.SessionID
msg := tgbotapi.NewMessage(update.Message.Chat.ID, "Нажмите кнопку чтобы войти:")
msg.ReplyMarkup = tgbotapi.NewInlineKeyboardMarkup(
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonURL("🔐 Войти на сайт", loginURL),
),
)
bot.Send(msg)
}
```
### Запуск бота (long polling)
```go
func main() {
bot, _ := tgbotapi.NewBotAPI(os.Getenv("BOT_TOKEN"))
u := tgbotapi.NewUpdate(0)
u.Timeout = 60
updates := bot.GetUpdatesChan(u)
for update := range updates {
if update.Message != nil && strings.HasPrefix(update.Message.Text, "/start") {
handleStart(update)
}
}
}
```
---
## Синхронизация корзины
Сразу после QR-логина фронтенд автоматически отправляет корзину:
```
POST /websession/{sessionId}
```
Тело — массив:
```json
[
{
"itemID": 123,
"quantity": 2,
"colour": "#ff0000",
"size": "XL",
"price": 1500
}
]
```
| Поле | Тип | Примечание |
|------|-----|------------|
| `itemID` | number | ID товара |
| `quantity` | number | Количество |
| `colour` | string | CSS hex (`#ff0000`). Бэкенд отдаёт `0xff0000`, фронтенд конвертирует |
| `size` | string | `"default"` если размер один |
| `price` | number | Финальная цена **с учётом скидки** |
> Этот эндпоинт (`POST /websession/{id}`) уже существует. Ничего менять не нужно, просто учитывать что он вызывается сразу после успешного логина.
---
## Безопасность
### Криптографический токен
```go
tokenBytes := make([]byte, 32) // 256 бит
crypto/rand.Read(tokenBytes)
token := base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(tokenBytes)
```
**НЕ использовать:** `math/rand`, UUID, timestamp.
### Токен одноразовый
- После `confirmed` → удалить при первом успешном `poll`
- После 5 минут → автоудаление (TTL)
- Повторный `poll``"expired"`
### Защита `/auth/qr/confirm`
```go
if r.Header.Get("X-Bot-Secret") != os.Getenv("BOT_INTERNAL_SECRET") {
http.Error(w, "forbidden", 403)
return
}
```
Дополнительно: можно ограничить по IP (`127.0.0.1`) если бот на том же сервере.
### Rate limiting для `/auth/qr/create`
Не более **5 токенов в минуту** с одного IP:
```go
var ipCounts sync.Map
func rateLimitQrCreate(ip string) bool {
key := ip + ":" + time.Now().Format("2006-01-02T15:04")
val, _ := ipCounts.LoadOrStore(key, new(int32))
count := atomic.AddInt32(val.(*int32), 1)
return count <= 5
}
```
---
## Переменные окружения
```env
BOT_TOKEN=123456:ABC-DEF...
BOT_INTERNAL_SECRET=случайная-строка-минимум-32-символа
FRONTEND_URL=https://dexarmarket.ru
SESSION_TTL=24h
REDIS_URL=localhost:6379
```
`BOT_INTERNAL_SECRET` должен совпадать в env сервера и env бота.
---
## Тестирование
### curl-тесты
**1. Создание токена:**
```bash
curl -X POST https://api.dexarmarket.ru:445/auth/qr/create \
-H "Origin: https://dexarmarket.ru"
# → { "token": "dG9r...", "url": "https://t.me/DexarSupport_bot?start=login_dG9r..." }
```
**2. Поллинг (до подтверждения):**
```bash
curl "https://api.dexarmarket.ru:445/auth/qr/poll?token=dG9r..."
# → { "status": "pending" }
```
**3. Подтверждение (имитация бота):**
```bash
curl -X POST https://api.dexarmarket.ru:445/auth/qr/confirm \
-H "Content-Type: application/json" \
-H "X-Bot-Secret: ваш-секрет" \
-d '{"token":"dG9r...","telegram_user":{"id":123,"first_name":"Тест","last_name":"","username":"testuser"}}'
# → { "status": "ok" }
```
**4. Поллинг (после подтверждения):**
```bash
curl -v "https://api.dexarmarket.ru:445/auth/qr/poll?token=dG9r..."
# → { "status": "confirmed", "session": {...} } + Set-Cookie: dx_session=...
```
**5. E2E:**
1. Открыть маркетплейс → добавить товар в корзину
2. Нажать "Оформить заказ" → появляется диалог с QR
3. Отсканировать QR телефоном → Telegram → бот: "✅ Вы вошли!"
4. Через 3 сек диалог закрывается → авторизован
5. Корзина синхронизирована (`POST /websession/{sessionId}`)
### Отладка
| Проблема | Где смотреть |
|----------|-------------|
| QR не показывается | `POST /auth/qr/create` — ошибка? CORS? |
| QR отсканирован, ничего не происходит | Бот получил `/start login_...`? Бот вызвал `confirm`? |
| Бот пишет "❌ QR устарел" | Токен expired? 5 минут прошло? |
| Поллинг "pending" бесконечно | Бот не вызвал `confirm`. Логи бота |
| Поллинг "confirmed" но cookie нет | `SameSite`, `Secure`, `Domain`, CORS |
---
## Чеклист
### Бэкенд (Go)
- [ ] Структура `Session` + `AuthToken`, функции save/get/delete
- [ ] `POST /auth/qr/create` — генерация токена
- [ ] `GET /auth/qr/poll?token=...` — статус + cookie при confirmed
- [ ] `POST /auth/qr/confirm` — приём от бота с `X-Bot-Secret`
- [ ] `GET /auth/session` — чтение cookie, JSON сессии
- [ ] `GET /auth/telegram/callback?token=...` — cookie + редирект
- [ ] `POST /auth/logout` — удаление сессии и cookie
- [ ] TTL 5 мин для токенов, 24ч для сессий
- [ ] Rate limiting `/auth/qr/create` (5/мин/IP)
- [ ] Очистка устаревших токенов
- [ ] CORS middleware
- [ ] `BOT_INTERNAL_SECRET` в env
### Telegram бот
- [ ] Обработка `/start login_{token}``POST /auth/qr/confirm`
- [ ] Обработка `/start auth` → создание сессии + кнопка "Войти"
- [ ] Сообщения: "✅ Вы вошли" / "❌ QR устарел"
- [ ] `BOT_INTERNAL_SECRET` в env (совпадает с сервером)
- [ ] `BOT_TOKEN` в env

View File

@@ -1,156 +0,0 @@
# Dexar Market - Deployment Guide
## Prerequisites
- Ubuntu/Debian server with root access
- Domain: dexarmarket.ru
- Node.js 18+ installed
## Quick Deployment
### 1. Build locally
```bash
npm install
npm run build
```
Output: `dist/dexarmarket/browser/`
**VERIFY BUILD LOCALLY:**
```bash
cd dist/dexarmarket/browser
ls -la
```
You MUST see `index.html`, chunk files, `assets/` folder, etc.
### 2. Upload to server
```bash
scp -r dist/dexarmarket/browser/* user@your-server:/var/www/dexarmarket/browser/
```
### 3. Set permissions on server
```bash
sudo chown -R www-data:www-data /var/www/dexarmarket
sudo chmod -R 755 /var/www/dexarmarket
sudo nginx -t
sudo systemctl reload nginx
```
## Initial Server Setup (one-time)
### Install and configure Nginx
```bash
sudo apt update
sudo apt install nginx -y
sudo mkdir -p /var/www/dexarmarket/browser
```
Copy `nginx.conf` content to `/etc/nginx/sites-available/dexarmarket`:
```bash
sudo nano /etc/nginx/sites-available/dexarmarket
```
Then enable it:
```bash
sudo ln -s /etc/nginx/sites-available/dexarmarket /etc/nginx/sites-enabled/
sudo rm /etc/nginx/sites-enabled/default
sudo nginx -t
sudo systemctl reload nginx
```
### Setup SSL (recommended)
```bash
sudo apt install certbot python3-certbot-nginx -y
sudo certbot --nginx -d dexarmarket.ru -d www.dexarmarket.ru
```
## Common Issues & Solutions
### ❌ 404 Error - Files Not Found
**Check 1: Verify files on server**
```bash
ls -la /var/www/dexarmarket/browser/
```
Should show: `index.html`, `chunk-*.js`, `assets/`, etc.
**If empty:**
```bash
# Re-upload files
scp -r dist/dexarmarket/browser/* user@your-server:/var/www/dexarmarket/browser/
```
**Check 2: Verify permissions**
```bash
namei -l /var/www/dexarmarket/browser/index.html
```
All directories need `x` (execute) permission.
**Fix permissions:**
```bash
sudo chown -R www-data:www-data /var/www/dexarmarket
sudo chmod -R 755 /var/www/dexarmarket
```
**Check 3: Test nginx config**
```bash
sudo nginx -t
```
Should say "syntax is ok" and "test is successful".
**Check 4: View nginx error log**
```bash
sudo tail -f /var/log/nginx/error.log
```
This shows the actual error!
### ❌ 502 Bad Gateway - API Issues
**This means the API backend is down or unreachable.**
**Check 1: Is API accessible?**
```bash
curl -v https://api.dexarmarket.ru:445/ping
```
**Check 2: Port 445 problem**
Port 445 is unusual for HTTPS and may be blocked by firewalls. Standard HTTPS uses port 443.
**Check 3: CORS issues**
The API must allow requests from `https://dexarmarket.ru`. Check API CORS configuration.
**Check 4: SSL certificate**
```bash
curl -k https://api.dexarmarket.ru:445/ping
```
If this works but without `-k` doesn't, SSL cert is invalid.
### ✅ Final Verification Checklist
On server, run all these:
```bash
# 1. Files exist
ls -la /var/www/dexarmarket/browser/index.html
# 2. Nginx config is valid
sudo nginx -t
# 3. Nginx is running
sudo systemctl status nginx
# 4. Site is enabled
ls -la /etc/nginx/sites-enabled/ | grep dexarmarket
# 5. Test API from server
curl -v https://api.dexarmarket.ru:445/ping
# 6. Check logs
sudo tail -20 /var/log/nginx/error.log
sudo tail -20 /var/log/nginx/access.log
```
### Debug Steps
If still having issues:
1. Check browser console (F12 → Console tab) - shows JavaScript errors
2. Check browser network tab (F12 → Network tab) - shows failed requests
3. Check exact error message in nginx logs
4. Test locally: `cd dist/dexarmarket/browser && python3 -m http.server 8000`

View File

@@ -1,140 +0,0 @@
# Dexar Market - Implementation Summary
## ✅ Completed Features
### 1. **Data Models** (`src/app/models/`)
- **Category Model**: Hierarchical category structure
- **Item Model**: Complete product data including photos/videos, pricing, reviews, Q&A
### 2. **Services** (`src/app/services/`)
- **API Service**: All endpoint integrations
- Health check (`/ping`)
- Categories (`/category`)
- Category items with pagination (`/category/:id`)
- Search with pagination (`/items`)
- Cart operations (GET, POST, DELETE)
- **Cart Service**: Reactive state management using Angular signals
- Add/remove items
- Real-time cart count
- Automatic total price calculation
### 3. **Pages** (`src/app/pages/`)
#### **Home Page** (`/`)
- Display all categories in grid layout
- Show subcategories
- Responsive category cards
#### **Category Page** (`/category/:id`)
- **Infinite Scroll**: Automatically loads more items on scroll
- Product grid with images, pricing, ratings
- Discount badges
- Stock status indicators
- Add to cart functionality
#### **Search Page** (`/search`)
- **Real-time search** with debounce (300ms)
- **Infinite Scroll** for results
- Same product display as category page
- Empty state handling
#### **Item Detail Page** (`/item/:id`)
- Photo/video gallery with thumbnails
- Full product information
- Pricing with discount display
- Reviews section with ratings
- Q&A section with voting counts (👍👎)
- Add to cart
#### **Cart Page** (`/cart`)
- List all cart items with details
- Remove individual items
- Clear entire cart
- Real-time total calculation
- Empty state with call-to-action
- Checkout button (placeholder)
### 4. **Components** (`src/app/components/`)
#### **Header Component**
- Sticky navigation
- Cart icon with badge showing item count
- Mobile-responsive hamburger menu
- Active route highlighting
### 5. **Routing & Configuration**
- Lazy-loaded routes for performance
- HTTP client configured
- All pages connected and navigable
### 6. **Responsive Design**
- Mobile-first approach
- Breakpoints at 768px and 968px
- Adaptive layouts for all screen sizes
- Touch-friendly interface
## 🎨 Design Features
- **Color Scheme**: Purple gradient theme (#667eea primary)
- **Smooth Animations**: Hover effects, transitions
- **Modern UI**: Card-based layouts, rounded corners
- **Custom Scrollbar**: Themed scrollbar styling
- **Loading States**: Spinners and skeleton states
- **Error Handling**: User-friendly error messages
## 📱 Performance Optimizations
1. **Infinite Scroll**: Loads 20 items at a time
2. **Lazy Loading**: Route-based code splitting
3. **Image Lazy Loading**: Native lazy loading for images
4. **Debounced Search**: Prevents excessive API calls
5. **Angular Signals**: Efficient reactivity
## 🔧 Technical Stack
- Angular 20 (standalone components)
- TypeScript
- RxJS for reactive programming
- SCSS for styling
- Angular Signals for state management
## 📦 API Integration
All endpoints from the provided documentation are integrated:
- ✅ GET /ping
- ✅ GET /category
- ✅ GET /category/:categoryID
- ✅ GET /items (search)
- ✅ GET /cart
- ✅ POST /cart
- ✅ DELETE /cart
## 🚀 How to Run
```bash
# Install dependencies (if needed)
npm install
# Start development server
ng serve
# Open browser
http://localhost:4200
```
## 📝 Notes
- **Item Detail Limitation**: Currently fetches items from cart for demo. In production, you may want to add a dedicated `/item/:id` endpoint or cache category results.
- **Checkout**: Placeholder button ready for payment integration
- **No Authentication**: As per requirements, no user management implemented
- **API Base URL**: Configured as `https://api.dexarmarket.ru`
## 🎯 Ready for Production
The application is production-ready with:
- Type-safe TypeScript
- Modular architecture
- Responsive design
- Error handling
- Performance optimizations
- Clean, maintainable code

View File

@@ -1,146 +0,0 @@
# Multi-Brand Configuration
Этот проект поддерживает несколько брендов с разными темами и конфигурациями.
## Доступные бренды
### 1. Dexar Market (фиолетовый)
- **Цвета**: Фиолетовый/пурпурный (#667eea, #764ba2)
- **Домен**: dexarmarket.ru
- **Email**: info@dexarmarket.ru
### 2. novo Market (зеленый)
- **Цвета**: Зеленый (#10b981, #14b8a6)
- **Домен**: novomarket.ru (будет настроено)
- **Email**: info@novomarket.ru (будет настроено)
## Команды запуска
### Dexar Market (разработка)
```bash
ng serve
# или
ng serve --configuration=development
```
### novo Market (разработка)
```bash
ng serve --configuration=novo
```
### Сборка для продакшена
#### Dexar Market
```bash
ng build --configuration=production
```
Результат: `dist/dexarmarket/`
#### novo Market
```bash
ng build --configuration=novo-production
```
Результат: `dist/novomarket/`
## Структура файлов
```
src/
├── environments/
│ ├── environment.ts # Dexar Development
│ ├── environment.production.ts # Dexar Production
│ ├── environment.novo.ts # novo Development
│ └── environment.novo.production.ts # novo Production
├── styles/
│ └── themes/
│ ├── dexar.theme.scss # Dexar цвета (фиолетовый)
│ └── novo.theme.scss # novo цвета (зеленый)
```
## Что настраивается через Environment
В файлах environment можно настроить:
```typescript
{
brandName: 'Название бренда',
brandFullName: 'Полное название бренда',
theme: 'dexar' | 'novo',
apiUrl: 'URL API',
logo: 'Путь к логотипу',
contactEmail: 'Email контактов',
supportEmail: 'Email поддержки',
domain: 'Домен сайта',
telegram: 'Telegram канал',
phones: {
russia: 'Телефон в России',
armenia: 'Телефон в Армении'
}
}
```
## CSS Переменные
Темы используют CSS переменные, которые можно изменить:
```scss
:root {
--primary-color: #10b981; // Основной цвет
--primary-hover: #059669; // Hover эффект
--secondary-color: #14b8a6; // Вторичный цвет
--gradient-primary: linear-gradient(...);
--gradient-hero: linear-gradient(...);
// и другие...
}
```
## Обновление для нового бренда
### Что нужно обновить для novo Market:
1.**Environment файлы** - созданы
2.**Темы (SCSS)** - созданы (зеленые цвета)
3.**Angular.json конфигурации** - настроены
4.**Логотипы и изображения** - добавить в `public/assets/images/`
5.**Реквизиты компании** - обновить когда будут готовы
6.**Домен и SSL** - настроить при деплое
7.**API endpoint** - обновить когда будет готов
## Деплой
### Dexar Market
```bash
ng build --configuration=production
# Deploy dist/dexarmarket/ to dexarmarket.ru
```
### novo Market
```bash
ng build --configuration=novo-production
# Deploy dist/novomarket/ to novomarket.ru
```
## Отличия брендов
| Параметр | Dexar Market | novo Market |
|----------|--------------|-------------|
| Основной цвет | Фиолетовый (#667eea) | Зеленый (#10b981) |
| Название | Dexar Market | novo Market |
| Домен | dexarmarket.ru | novomarket.ru |
| Email | info@dexarmarket.ru | info@novomarket.ru |
| Telegram | @dexarmarket | @novomarket |
| Реквизиты | Текущие | Будут обновлены |
## Следующие шаги для novo Market
1. Добавить логотип novo Market (`public/assets/images/novo-logo.svg`)
2. Обновить реквизиты компании в правовых документах
3. Настроить API endpoint для novo
4. Настроить домен и SSL сертификаты
5. Обновить контактную информацию (телефоны, адреса)
## Примечания
- Оба бренда используют одну кодовую базу
- Все компоненты автоматически адаптируются под выбранный бренд
- Легко добавить новые бренды по той же схеме

View File

@@ -1,206 +0,0 @@
# PWA Setup Guide
## ✅ Implemented Features
### 1. Service Worker
- **Caching Strategy**: Aggressive prefetch for app shell
- **API Caching**: Freshness strategy with 1-hour cache (max 100 requests)
- **Image Caching**: Performance strategy with 7-day cache (max 50 images)
- **Configuration**: `ngsw-config.json`
### 2. Web App Manifests
- **Dexar**: `public/manifest.webmanifest` (purple theme #a855f7)
- **Novo**: `public/manifest.novo.webmanifest` (green theme #10b981)
- **Features**:
- Installable on mobile/desktop
- Standalone display mode
- 8 icon sizes (72px to 512px)
- Russian language metadata
### 3. Offline Support
- App shell loads instantly from cache
- API responses cached for 1 hour
- Product images cached for 7 days
- Automatic background updates
## 🚀 Testing PWA Functionality
### Local Testing with Production Build
```bash
# Build for production
npm run build -- --configuration=production
# Serve the production build
npx http-server dist/dexarmarket -p 4200 -c-1
# For Novo brand
npx http-server dist/novomarket -p 4201 -c-1
```
### Chrome DevTools Testing
1. Open `http://localhost:4200`
2. Open DevTools (F12)
3. Go to **Application** tab
4. Check:
- **Service Workers**: Should show registered worker
- **Cache Storage**: Should show `ngsw:/:db`, `ngsw:/:assets`
- **Manifest**: Should show app details
### Install Prompt Testing
1. Open app in Chrome/Edge
2. Click the **install icon** in address bar ()
3. Confirm installation
4. App opens as standalone window
5. Check Start Menu/Home Screen for app icon
### Offline Testing
1. Open app while online
2. Navigate through pages (loads assets)
3. Open DevTools → Network → Toggle **Offline**
4. Refresh page - should still work!
5. Navigate to cached pages - should load instantly
## 📱 Mobile Testing
### Android Chrome
1. Open app URL
2. Chrome shows "Add to Home Screen" banner
3. Install and open - works like native app
4. Splash screen with your logo/colors
### iOS Safari
1. Open app URL
2. Tap Share → "Add to Home Screen"
3. Icon appears on home screen
4. Opens in full-screen mode
## 🔧 Configuration Details
### Service Worker Caching Strategy
```json
{
"app": {
"installMode": "prefetch", // Download immediately
"updateMode": "prefetch" // Auto-update in background
},
"assets": {
"installMode": "lazy", // Load on-demand
"updateMode": "prefetch"
},
"api-cache": {
"strategy": "freshness", // Network first, fallback to cache
"maxAge": "1h" // Keep for 1 hour
},
"product-images": {
"strategy": "performance", // Cache first, update in background
"maxAge": "7d" // Keep for 7 days
}
}
```
### Manifest Differences
| Property | Dexar | Novo |
|----------|-------|------|
| Theme Color | #a855f7 (purple) | #10b981 (green) |
| Name | Dexar Market | Novo Market |
| Icons | Default Angular | Default Angular |
| Background | White (#ffffff) | White (#ffffff) |
## 🎨 Custom Icons (Recommended)
Replace the default Angular icons with brand-specific ones:
```bash
public/icons/
├── icon-72x72.png # Smallest (splash screen)
├── icon-96x96.png
├── icon-128x128.png
├── icon-144x144.png
├── icon-152x152.png # iOS home screen
├── icon-192x192.png # Android home screen
├── icon-384x384.png
└── icon-512x512.png # Largest (splash, install prompt)
```
**Design Guidelines**:
- Use solid background color (purple for Dexar, green for Novo)
- Center white logo/icon
- Keep design simple (shows at small sizes)
- Export as PNG with transparency or solid background
## 🔄 Update Strategy
### How Updates Work
1. User visits app
2. Service worker checks for updates
3. New version downloads in background
4. User refreshes → gets updated version
5. Old cache automatically cleared
### Force Update (Development)
```bash
# Clear all caches
chrome://serviceworker-internals/ # Unregister worker
chrome://settings/clearBrowserData # Clear cache
# Or in code (add to app.config.ts)
navigator.serviceWorker.getRegistrations().then(registrations => {
registrations.forEach(reg => reg.unregister());
});
```
## 📊 Performance Benefits
### Before PWA
- Initial load: ~2-3s (network dependent)
- Subsequent loads: ~1-2s
- Offline: ❌ Not available
### After PWA
- Initial load: ~2-3s (first visit)
- Subsequent loads: **~200-500ms** (cached)
- Offline: ✅ **Fully functional**
- Install: ✅ **Native app experience**
## 🐛 Troubleshooting
### Service Worker Not Registering
- Check console for errors
- Ensure HTTPS (or localhost)
- Clear browser cache and reload
### Old Version Not Updating
- Hard refresh: `Ctrl+Shift+R` (Windows) or `Cmd+Shift+R` (Mac)
- Unregister worker in DevTools
- Wait 24 hours (automatic update)
### Manifest Not Loading
- Check `index.html` has `<link rel="manifest">`
- Verify manifest path is correct
- Check manifest JSON is valid (no syntax errors)
### Icons Not Showing
- Check icon paths in manifest
- Ensure icons exist in `public/icons/`
- Verify icon sizes match manifest
## 📚 Next Steps
1. **Custom Icons**: Create brand-specific icons for both themes
2. **Push Notifications**: Add user engagement (requires backend)
3. **Background Sync**: Queue offline orders, sync when online
4. **Analytics**: Track PWA installs, offline usage
5. **A2HS Prompt**: Show custom "Install App" banner
## 🔗 Resources
- [PWA Checklist](https://web.dev/pwa-checklist/)
- [Angular PWA Guide](https://angular.dev/ecosystem/service-workers)
- [Manifest Generator](https://www.simicart.com/manifest-generator.html/)
- [Icon Generator](https://realfavicongenerator.net/)

View File

@@ -1,181 +0,0 @@
# Рекомендации по работе с платежными ссылками
## Требования Райффайзенбанка для оплаты по ссылке
### ✅ Что уже реализовано:
1. **Реквизиты организации** - полностью заполнены
2. **Правила оплаты** - подробная страница с требованиями ЦБ РФ, PCI DSS, 3D-Secure
3. **Политика возврата** - полная информация о возврате физических и цифровых товаров
4. **Публичная оферта** - модель маркетплейса, разграничение ответственности
5. **Политика конфиденциальности** - обработка персональных данных (152-ФЗ)
6. **Чекбокс согласия в корзине** - со ссылками на:
- Публичную оферту
- Политику возврата
- Условия гарантии
- Политику конфиденциальности
7. **Логотипы платежных систем**:
- МИР (обязательно!)
- Visa
- Mastercard
- Размещены в футере и на странице оплаты
---
## 📧 Рекомендации при отправке платежной ссылки покупателю
### Шаблон письма/сообщения:
```
Здравствуйте, [Имя покупателя]!
Ваш заказ №[НОМЕР] оформлен.
Для оплаты перейдите по ссылке:
[ПЛАТЕЖНАЯ ССЫЛКА]
Сумма к оплате: [СУММА] ₽
Перед оплатой, пожалуйста, ознакомьтесь с условиями:
• Публичная оферта: https://dexarmarket.ru/public-offer
• Политика возврата: https://dexarmarket.ru/return-policy
• Условия гарантии: https://dexarmarket.ru/guarantee
• Политика конфиденциальности: https://dexarmarket.ru/privacy-policy
Оплачивая заказ, вы подтверждаете, что ознакомились и согласны с данными условиями.
---
С уважением,
Команда Dexarmarket
Техподдержка: Info@dexarmarket.ru
Телефон: +7 (926) 459-31-57
```
### ✅ Важно получить подтверждение от покупателя!
**Вариант 1 - Автоматическое подтверждение:**
После оплаты отправить покупателю:
```
Спасибо за оплату заказа №[НОМЕР]!
Вы подтвердили согласие с:
✓ Публичной офертой
✓ Политикой возврата
✓ Условиями гарантии
✓ Политикой конфиденциальности
Чек отправлен на email: [EMAIL]
Статус заказа можно отслеживать в личном кабинете.
```
**Вариант 2 - Ручное подтверждение (желательно):**
Перед отправкой ссылки запросить:
```
Для оформления заказа подтвердите, пожалуйста, что вы ознакомились с условиями
(https://dexarmarket.ru/public-offer) и согласны с ними.
Ответьте "Согласен" или "Подтверждаю" для продолжения.
```
---
## 🛡️ Защита от оспаривания платежей (Chargeback)
### Что сохранять для доказательной базы:
1. **Переписка с покупателем:**
- Скриншоты чатов
- Email переписка
- SMS/WhatsApp сообщения с подтверждением
2. **Логи действий покупателя:**
- IP-адрес при оформлении заказа
- Timestamp (дата и время)
- Согласие с чекбоксом (если есть личный кабинет)
3. **Документы об отправке:**
- Трек-номер посылки
- Подтверждение доставки
- Подпись получателя (если есть)
4. **Платежная информация:**
- Номер транзакции
- Дата и время оплаты
- Сумма платежа
---
## 🔒 Дополнительные меры безопасности
### 1. Двухфакторное подтверждение
Для крупных заказов (>10 000 ₽) рекомендуется:
- Звонок покупателю для подтверждения заказа
- Запись разговора (с уведомлением клиента)
### 2. Проверка благонадежности
Для новых покупателей:
- Проверить совпадение адреса доставки с регионом телефона
- При подозрительных заказах запросить фото документа
### 3. Страхование рисков
- Оформить договор с платежным провайдером на защиту от мошенничества
- Использовать холдирование средств (72 часа на проверку)
---
## 📊 Статистика оспариваний
**Риски по категориям товаров:**
- Электроника: ~2-5% оспариваний
- Одежда: ~1-3%
- Цифровые товары: ~0.5-2%
- Продукты питания: ~0.1-0.5%
**Причины оспариваний:**
1. "Не получил товар" (40%)
2. "Товар не соответствует описанию" (30%)
3. "Не заказывал" (20%)
4. "Дубликат платежа" (10%)
---
## ✅ Чек-лист готовности к работе с Райффайзенбанком
- [x] Реквизиты организации заполнены
- [x] Правила оплаты на русском языке
- [x] Политика возврата опубликована
- [x] Публичная оферта опубликована
- [x] Политика конфиденциальности опубликована
- [x] Логотип МИР размещен на сайте
- [x] Чекбокс согласия с условиями в корзине
- [x] Ссылки на все документы в чекбоксе
- [ ] Настроен процесс отправки платежных ссылок с условиями
- [ ] Настроен процесс получения подтверждений от покупателей
- [ ] Настроена система логирования действий пользователей
- [ ] Подготовлена база для работы с оспариваниями
---
## 📞 Контакты для связи с банком
**АО "Райффайзенбанк"**
- Сайт: https://www.raiffeisen.ru
- Требования к сайтам: https://www.raiffeisen.ru/common/img/uploaded/files/business/treb_k_saity.pdf
- Техподдержка эквайринга: указывается при подключении
**Платежная система МИР**
- Требования к использованию логотипа: https://mironline.ru/support/merchantam/brand/
- Обязательно размещение логотипа при приеме карт МИР
---
## 🚀 Статус проекта
**Готовность к подключению эквайринга: 95%**
Осталось реализовать:
1. Автоматизацию отправки ссылок с условиями
2. Систему получения подтверждений от покупателей
3. Логирование действий для доказательной базы
**Все юридические и информационные требования выполнены!**

View File

@@ -1,423 +0,0 @@
# Project Recommendations & Roadmap
## 📊 Current Status: 9.2/10
Your project is production-ready with excellent architecture! Here's what to focus on next:
---
## ✅ Recently Completed (January 2026)
1. **Phone Number Collection**
- Real-time formatting (+7 XXX XXX-XX-XX)
- Comprehensive validation (11 digits)
- Raw digits sent to API
2. **HTML Structure Unification**
- Single template for both themes
- CSS-only differentiation (Novo/Dexar)
- Eliminated code duplication
3. **PWA Implementation**
- Service worker with smart caching
- Dual manifests (brand-specific)
- Offline support
- Installable app
4. **Code Quality**
- Removed 3 duplicate methods
- Fixed SCSS syntax errors
- Optimized cart component
---
## 🎯 Priority Roadmap
### 🔥 HIGH PRIORITY (Next 2 Weeks)
#### 1. Custom PWA Icons
**Why**: Branding, professionalism
**Effort**: 2-3 hours
**Impact**: High visibility
**Action Items**:
```bash
# Create 8 icon sizes for each brand:
# Dexar: Purple (#a855f7) background + white logo
# Novo: Green (#10b981) background + white logo
public/icons/dexar/
├── icon-72x72.png
├── icon-512x512.png
└── ...
public/icons/novo/
├── icon-72x72.png
└── ...
# Update manifests to point to brand folders
```
**Tools**: Figma, Photoshop, or [RealFaviconGenerator](https://realfavicongenerator.net/)
---
#### 2. Unit Testing
**Why**: Code reliability, easier refactoring
**Effort**: 1-2 weeks
**Impact**: Development velocity, bug reduction
**Target Coverage**: 80%+
**Priority Test Files**:
```typescript
// 1. Services (highest ROI)
cart.service.spec.ts // Test signal updates, cart logic
api.service.spec.ts // Mock HTTP calls
telegram.service.spec.ts // Test WebApp initialization
// 2. Components (critical paths)
cart.component.spec.ts // Payment flow, validation
header.component.spec.ts // Cart count, navigation
item-detail.component.spec.ts // Add to cart, variant selection
// 3. Interceptors
cache.interceptor.spec.ts // Verify caching logic
```
**Quick Start**:
```bash
# Generate test with Angular CLI
ng test --code-coverage
# Write first test
describe('CartService', () => {
it('should add item to cart', () => {
service.addToCart(mockItem, mockVariant);
expect(service.cartItems().length).toBe(1);
});
});
```
---
#### 3. Error Boundary & User Feedback
**Why**: Graceful failures, better UX
**Effort**: 1 day
**Impact**: User trust, reduced support tickets
**Implementation**:
```typescript
// src/app/services/error-handler.service.ts
@Injectable({ providedIn: 'root' })
export class ErrorHandlerService {
showError(message: string) {
// Show toast notification
// Log to analytics
// Optionally send to backend
}
}
// Usage in cart.component.ts
this.apiService.createPayment(data).subscribe({
next: (response) => { /* handle success */ },
error: (err) => {
this.errorHandler.showError(
'Не удалось создать платеж. Попробуйте позже.'
);
console.error(err);
}
});
```
**Add Toast Library**:
```bash
npm install ngx-toastr --save
```
---
### ⚡ MEDIUM PRIORITY (Next Month)
#### 4. E2E Testing
**Why**: Catch integration bugs, confidence in releases
**Effort**: 3-5 days
**Impact**: Release quality
**Recommended**: [Playwright](https://playwright.dev/) (better than Cypress for modern apps)
```bash
npm install @playwright/test --save-dev
npx playwright install
```
**Critical Test Scenarios**:
1. Browse categories → View item → Add to cart → Checkout
2. Search product → Filter results → Add to cart
3. Empty cart → Add items → Remove items
4. Payment flow (mock SBP QR code response)
5. Email/phone validation on success screen
---
#### 5. Analytics Integration
**Why**: Data-driven decisions, understand users
**Effort**: 1 day
**Impact**: Business insights
**Recommended Setup**:
```typescript
// Yandex Metrica (best for Russian market)
<!-- index.html -->
<script>
(function(m,e,t,r,i,k,a){
// Yandex Metrica snippet
})(window, document, "yandex_metrica_callbacks2");
</script>
// Track events
yaCounter12345678.reachGoal('ADD_TO_CART', {
product_id: item.id,
price: variant.price
});
```
**Key Metrics to Track**:
- Product views
- Add to cart events
- Checkout initiation
- Payment success/failure
- Search queries
- PWA installs
---
#### 6. Performance Optimization
**Why**: Better UX, SEO, conversion rates
**Effort**: 2-3 days
**Impact**: User satisfaction
**Action Items**:
```typescript
// 1. Image Optimization
// Use WebP format with fallbacks
<picture>
<source srcset="image.webp" type="image/webp">
<img src="image.jpg" alt="Product">
</picture>
// 2. Lazy Load Images
<img loading="lazy" src="product.jpg">
// 3. Preload Critical Assets
// index.html
<link rel="preload" href="logo.svg" as="image">
// 4. Virtual Scrolling for Long Lists
// npm install @angular/cdk
<cdk-virtual-scroll-viewport itemSize="150">
@for (item of items; track item.id) {
<div>{{ item.title }}</div>
}
</cdk-virtual-scroll-viewport>
```
**Measure First**:
```bash
# Lighthouse audit
npm install -g lighthouse
lighthouse http://localhost:4200 --view
# Target scores:
# Performance: 90+
# Accessibility: 95+
# Best Practices: 100
# SEO: 90+
```
---
### 🔮 FUTURE ENHANCEMENTS (Next Quarter)
#### 7. Push Notifications
**Why**: Re-engage users, promote offers
**Effort**: 1 week (needs backend)
**Impact**: Retention, sales
**Requirements**:
- Firebase Cloud Messaging (FCM)
- Backend endpoint to send notifications
- User permission flow
---
#### 8. Background Sync
**Why**: Queue orders offline, sync when online
**Effort**: 2-3 days
**Impact**: Offline-first experience
```typescript
// Register background sync
navigator.serviceWorker.ready.then(registration => {
registration.sync.register('sync-orders');
});
// ngsw-config.json - already set up!
// Your PWA is ready for this
```
---
#### 9. Advanced Features
**Effort**: Varies
**Impact**: Competitive advantage
- **Product Recommendations**: "You might also like..."
- **Recently Viewed**: Track browsing history
- **Wishlist**: Save items for later
- **Price Alerts**: Notify when price drops
- **Social Sharing**: Share products on Telegram/VK
- **Dark Mode**: Theme switcher
- **Multi-language**: Support English, etc.
---
## 🛠️ Technical Debt & Improvements
### Quick Wins (< 1 hour each)
1. **Environment Variables for API URLs**
```typescript
// Don't hardcode API URLs
// Use environment.apiUrl consistently
```
2. **Content Security Policy (CSP)**
```nginx
# nginx.conf
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline';";
```
3. **Rate Limiting**
```typescript
// Prevent API spam
import { debounceTime } from 'rxjs';
searchQuery$.pipe(
debounceTime(300)
).subscribe(/* search */);
```
4. **Loading States**
```html
<!-- Show skeletons while loading -->
@if (loading()) {
<div class="skeleton"></div>
} @else {
<div>{{ content }}</div>
}
```
5. **SEO Meta Tags**
```typescript
// Use Angular's Meta service
constructor(private meta: Meta) {}
ngOnInit() {
this.meta.updateTag({
name: 'description',
content: this.product.description
});
}
```
---
## 📈 Success Metrics
### Before Optimizations
- Test Coverage: ~10%
- Lighthouse Score: ~85
- Error Tracking: Console only
- Analytics: None
- PWA: ❌
### After Optimizations (Target)
- Test Coverage: **80%+**
- Lighthouse Score: **95+**
- Error Tracking: ✅ Centralized
- Analytics: ✅ Yandex Metrica
- PWA: ✅ **Fully functional**
- User Engagement: **+30%** (with push notifications)
---
## 🎓 Learning Resources
### Testing
- [Angular Testing Guide](https://angular.dev/guide/testing)
- [Testing Library](https://testing-library.com/docs/angular-testing-library/intro/)
### Performance
- [Web.dev Performance](https://web.dev/performance/)
- [Angular Performance Checklist](https://github.com/mgechev/angular-performance-checklist)
### PWA
- [PWA Workshop](https://web.dev/learn/pwa/)
- [Workbox](https://developer.chrome.com/docs/workbox/) (service worker library)
### Analytics
- [Yandex Metrica Guide](https://yandex.ru/support/metrica/)
- [Google Analytics 4](https://developers.google.com/analytics/devguides/collection/ga4)
---
## 💡 Pro Tips
1. **Ship Frequently**: Deploy small updates often
2. **Monitor Production**: Set up error tracking (Sentry, Rollbar)
3. **User Feedback**: Add feedback button in app
4. **A/B Testing**: Test different checkout flows
5. **Mobile First**: 70%+ of e-commerce is mobile
6. **Accessibility**: Test with screen readers
7. **Security**: Regular dependency updates (`npm audit fix`)
---
## 🚀 Next Actions (This Week)
```bash
# Day 1: PWA Icons
1. Design icons for both brands
2. Update manifests
3. Test installation on mobile
# Day 2-3: Error Handling
1. Install ngx-toastr
2. Add ErrorHandlerService
3. Update all API calls with error handling
# Day 4-5: First Unit Tests
1. Set up testing utilities
2. Write tests for CartService
3. Write tests for cart validation logic
4. Run coverage report: npm test -- --code-coverage
# Weekend: Analytics
1. Set up Yandex Metrica
2. Add tracking to key events
3. Monitor dashboard
```
---
## 💬 Questions?
If you need help with any of these tasks:
1. Ask for specific code examples
2. Request architectural guidance
3. Need library recommendations
4. Want code reviews
Your project is already excellent - these improvements will make it world-class! 🌟

View File

@@ -1,327 +0,0 @@
# Telegram UserAuth Backend Contract
This document extracts the existing Telegram login flow into a repo-neutral contract for reuse in other projects.
The UI behavior, payloads, polling cadence, and session model stay the same. Only route names and cookie naming are generalized.
## Endpoint Renaming
| Current app contract | Reusable contract |
|---|---|
| `GET /auth/session` | `GET /userauth/session` |
| `POST /auth/qr/create` | `POST /userauth/qr/create` |
| `GET /auth/qr/poll?token=...` | `GET /userauth/qr/poll?token=...` |
| `POST /auth/qr/confirm` | `POST /userauth/qr/confirm` |
| `GET /auth/telegram/callback` | `GET /userauth/telegram/callback` |
| `POST /auth/logout` | `POST /userauth/logout` |
| `POST /websession/{sessionId}` | `POST /usersession/{sessionId}` |
| Cookie `dx_session` | Cookie `userauth_session` |
## Flow Summary
There are two supported flows.
### 1. Direct login from button
1. Frontend opens `https://t.me/{botUsername}?start=auth_{callbackUrl}`.
2. Telegram bot creates a session and sends the user a login button.
3. The button points to `GET /userauth/telegram/callback?token={sessionId}`.
4. Backend sets `userauth_session` cookie and redirects back to the storefront.
5. Frontend calls `GET /userauth/session` and becomes authenticated.
### 2. QR login from desktop
1. Frontend opens dialog.
2. Frontend calls `POST /userauth/qr/create`.
3. Backend returns `{ token, url }` where `url` is a Telegram deep link.
4. Frontend renders a QR from that URL.
5. User scans QR and bot calls `POST /userauth/qr/confirm`.
6. Frontend polls `GET /userauth/qr/poll?token=...` every 3 seconds.
7. When status becomes `confirmed`, backend returns session payload and sets the cookie.
8. Frontend syncs local cart using `POST /usersession/{sessionId}`.
## Session Shape
The frontend expects this exact response shape for the authenticated session.
```json
{
"sessionId": "550e8400-e29b-41d4-a716-446655440000",
"telegramUserId": 123456789,
"username": "ivan_petrov",
"displayName": "Ivan Petrov",
"active": true,
"expiresAt": "2026-05-21T14:30:00Z"
}
```
| Field | Type | Required | Notes |
|---|---|---|---|
| `sessionId` | string | yes | Session identifier used in cart sync |
| `telegramUserId` | number | yes | Telegram user ID |
| `username` | string or null | no | Telegram username |
| `displayName` | string | yes | User-facing full name |
| `active` | boolean | yes | `false` means expired session |
| `expiresAt` | ISO 8601 string | yes | Used by frontend refresh scheduling |
Recommended TTL:
- Session TTL: 24 hours
- QR token TTL: 5 minutes
## HTTP Contract
### `POST /userauth/qr/create`
Creates a one-time QR login token when the dialog opens.
Request body:
```json
{}
```
Response `200`:
```json
{
"token": "dG9rZW4tYWJjMTIz",
"url": "https://t.me/userauth_bot?start=login_dG9rZW4tYWJjMTIz"
}
```
Requirements:
- Generate a cryptographically secure token.
- Save token with status `pending`.
- Return a Telegram deep link in `url`.
- Rate limit to 5 requests per minute per IP.
### `GET /userauth/qr/poll?token={token}`
Called every 3 seconds until confirmation or expiration.
Possible responses:
Pending:
```json
{ "status": "pending" }
```
Confirmed:
```json
{
"status": "confirmed",
"session": {
"sessionId": "550e8400-e29b-41d4-a716-446655440000",
"telegramUserId": 123456789,
"username": "ivan_petrov",
"displayName": "Ivan Petrov",
"active": true,
"expiresAt": "2026-05-21T14:30:00Z"
}
}
```
Expired:
```json
{ "status": "expired" }
```
Behavior:
- If confirmed, set cookie `userauth_session` in the response.
- Delete or invalidate the QR token after the first successful confirmed poll.
- If token is unknown or expired, return `status: "expired"`.
### `POST /userauth/qr/confirm`
Internal endpoint called by the Telegram bot after the user scans the QR code.
Required header:
```text
X-Bot-Secret: <shared secret between bot and backend>
```
Request body:
```json
{
"token": "dG9rZW4tYWJjMTIz",
"telegram_user": {
"id": 123456789,
"first_name": "Ivan",
"last_name": "Petrov",
"username": "ivan_petrov"
}
}
```
Response `200`:
```json
{ "status": "ok" }
```
Behavior:
- Validate `X-Bot-Secret`.
- Validate token exists and is still `pending`.
- Create a user session.
- Store session ID on the QR token.
- Mark QR token as `confirmed`.
### `GET /userauth/session`
Returns the currently active session based on the cookie.
Frontend behavior depends on this endpoint in two places:
- initial auth check on app startup
- fallback polling if QR token creation fails
Response `200`:
```json
{
"sessionId": "550e8400-e29b-41d4-a716-446655440000",
"telegramUserId": 123456789,
"username": "ivan_petrov",
"displayName": "Ivan Petrov",
"active": true,
"expiresAt": "2026-05-21T14:30:00Z"
}
```
Error handling:
- Any non-200 response is treated by the frontend as unauthenticated.
### `GET /userauth/telegram/callback?token={sessionId}`
Used for direct Telegram login from the primary button flow.
Behavior:
- Read the `token` query param.
- Resolve it to a valid active session.
- Set cookie `userauth_session`.
- Redirect user to the storefront URL.
### `POST /userauth/logout`
Clears the backend session and expires the cookie.
Request body:
```json
{}
```
Response `200`:
```json
{ "message": "ok" }
```
### `POST /usersession/{sessionId}`
Synchronizes local cart immediately after successful login.
Request body:
```json
[
{
"itemID": 123,
"quantity": 2,
"colour": "#ff0000",
"size": "XL",
"price": 1500
}
]
```
Notes:
- This payload is unchanged from the existing implementation.
- `price` is already discounted on the frontend side.
- The frontend skips the call if cart is empty.
## Telegram Deep Link Format
Direct login link format:
```text
https://t.me/{botUsername}?start=auth_{urlEncodedCallbackUrl}
```
QR login link format:
```text
https://t.me/{botUsername}?start=login_{qrToken}
```
Important limit:
- Telegram limits the `start` payload to 64 characters.
- A base64url encoding of 32 random bytes plus `login_` fits safely.
## Cookie Requirements
Use these cookie settings for the frontend to work correctly across site and API origins.
| Property | Value |
|---|---|
| Name | `userauth_session` |
| Path | `/` |
| HttpOnly | `true` |
| Secure | `true` |
| SameSite | `None` |
| MaxAge | `86400` |
| Domain | your shared parent domain, for example `.example.com` |
## CORS Requirements
Because the frontend sends credentials, backend must return an explicit origin.
Required headers:
```text
Access-Control-Allow-Origin: https://your-frontend.example
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: GET, POST, OPTIONS
Access-Control-Allow-Headers: Content-Type
```
Do not use `*` for `Access-Control-Allow-Origin` together with credentials.
## Frontend Runtime Expectations
The current dialog behavior is fixed and should be preserved by backend responses.
- QR polling interval: every 3 seconds
- QR expiration on frontend: after 100 checks
- If QR creation fails, frontend falls back to direct login URL and session polling
- After login, frontend closes the dialog and re-checks session
## Minimal Backend Checklist
- Implement all six `userauth` endpoints and the `usersession` sync endpoint.
- Store sessions for 24 hours.
- Store QR tokens for 5 minutes.
- Protect `POST /userauth/qr/confirm` with `X-Bot-Secret`.
- Set `userauth_session` cookie on confirmed QR poll and direct callback.
- Return the exact session JSON shape.
- Support credentialed CORS.
## Bot Checklist
- Handle `/start login_{token}` and call `POST /userauth/qr/confirm`.
- Handle `/start auth_{callbackUrl}` and provide a button that opens the callback URL.
- Send success and expiration messages back to the user.
- Share the same `X-Bot-Secret` value with backend.

View File

@@ -1,193 +0,0 @@
# 🔧 Troubleshooting Guide for 404 and 502 Errors
## Quick Diagnosis
Run these commands on your Ubuntu server to diagnose the issue:
```bash
# 1. Check if files exist
ls -la /var/www/dexarmarket/browser/index.html
# 2. Check nginx config syntax
sudo nginx -t
# 3. Check nginx error logs (THIS IS MOST IMPORTANT!)
sudo tail -30 /var/log/nginx/error.log
# 4. Check if nginx is running
sudo systemctl status nginx
# 5. Test API from server
curl -v https://api.dexarmarket.ru:445/ping
```
## Error: 404 Not Found
### Cause: Files not uploaded or wrong path
**Solution 1: Verify files are on server**
```bash
ls -la /var/www/dexarmarket/browser/
```
Should show:
- `index.html`
- `main-*.js`
- `chunk-*.js`
- `polyfills-*.js`
- `styles-*.css`
- `assets/` folder
**If files are missing:**
```bash
# From your local machine:
cd F:\dx\marketplace\Dexarmarket
npm run build
scp -r dist/dexarmarket/browser/* user@your-server:/var/www/dexarmarket/browser/
```
**Solution 2: Fix permissions**
```bash
sudo chown -R www-data:www-data /var/www/dexarmarket
sudo chmod -R 755 /var/www/dexarmarket
```
**Solution 3: Check nginx config is loaded**
```bash
# Check which config is active
ls -la /etc/nginx/sites-enabled/
# Should show symlink to dexarmarket config
# If not:
sudo ln -s /etc/nginx/sites-available/dexarmarket /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
```
**Solution 4: Verify nginx root path**
```bash
sudo cat /etc/nginx/sites-available/dexarmarket | grep root
```
Should show: `root /var/www/dexarmarket/browser;`
## Error: 502 Bad Gateway
### This means the API backend (https://api.dexarmarket.ru:445) is unreachable
**Solution 1: Check if API is running**
```bash
# From Ubuntu server:
curl -v https://api.dexarmarket.ru:445/ping
# If this fails, your API backend is down!
```
**Solution 2: Port 445 is blocked**
Port 445 is typically blocked by many firewalls because it's used for SMB file sharing.
**Check from browser console (F12):**
- Open browser Developer Tools (F12)
- Go to Console tab
- Look for errors like: `net::ERR_CONNECTION_REFUSED` or `net::ERR_SSL_PROTOCOL_ERROR`
**Possible fixes:**
- Use standard port 443 for HTTPS
- Or use port 8443, 8080, or other non-standard but common ports
- Configure firewall to allow port 445
**Solution 3: CORS issues**
The API must have CORS headers allowing requests from `https://dexarmarket.ru`
Check API response headers:
```bash
curl -v -H "Origin: https://dexarmarket.ru" https://api.dexarmarket.ru:445/ping
```
Should include headers like:
```
Access-Control-Allow-Origin: https://dexarmarket.ru
```
**Solution 4: SSL Certificate issues**
```bash
# Test with SSL verification disabled
curl -k https://api.dexarmarket.ru:445/ping
# If this works but normal curl doesn't, SSL cert is invalid
```
## Still Not Working?
### Get detailed error information:
**1. Browser Console (JavaScript errors)**
```
F12 → Console tab
Look for red errors
```
**2. Browser Network Tab (Failed requests)**
```
F12 → Network tab
Reload page
Look for red (failed) requests
Click on failed request to see details
```
**3. Nginx Error Log (Server-side errors)**
```bash
sudo tail -50 /var/log/nginx/error.log
```
**4. Nginx Access Log (See what requests come in)**
```bash
sudo tail -50 /var/log/nginx/access.log
```
**5. Test Build Locally**
```bash
cd F:\dx\marketplace\Dexarmarket\dist\dexarmarket\browser
python -m http.server 8000
# Visit http://localhost:8000
```
If local test works, the issue is with deployment, not the build.
## Common Mistakes
**Uploading to wrong directory**
- Correct: `/var/www/dexarmarket/browser/`
- Wrong: `/var/www/dexarmarket/` (missing browser/)
**Wrong permissions**
```bash
# Must be readable by www-data
sudo chown -R www-data:www-data /var/www/dexarmarket
sudo chmod -R 755 /var/www/dexarmarket
```
**Nginx config not reloaded**
```bash
# After ANY change to nginx config:
sudo nginx -t
sudo systemctl reload nginx
```
**Old files cached**
```bash
# Clear browser cache: Ctrl+Shift+R (hard refresh)
```
**API port blocked**
- Port 445 is unusual and often blocked
- Consider using port 443 (standard HTTPS)
## Contact Information for Support
When asking for help, provide:
1. Output of `sudo nginx -t`
2. Last 30 lines of nginx error log: `sudo tail -30 /var/log/nginx/error.log`
3. Browser console errors (F12 → Console)
4. Result of `curl -v https://api.dexarmarket.ru:445/ping` from server
5. Screenshot of browser Network tab showing failed request

View File

@@ -1,551 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Telegram Login Dialog</title>
<style>
:root {
--bg-page: linear-gradient(135deg, #f4f7fb 0%, #e8eef4 100%);
--bg-card: #ffffff;
--bg-hover: #f0f0f0;
--text-primary: #1a1a1a;
--text-secondary: #666666;
--accent-color: #497671;
--accent-light: rgba(73, 118, 113, 0.1);
--telegram: #2aabee;
--telegram-hover: #229ed9;
--border: #e8e8e8;
--shadow: 0 20px 60px rgba(0, 0, 0, 0.15);
}
* {
box-sizing: border-box;
}
body {
margin: 0;
min-height: 100vh;
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
color: var(--text-primary);
background: var(--bg-page);
}
.page {
min-height: 100vh;
display: grid;
grid-template-columns: minmax(320px, 448px) minmax(320px, 560px);
gap: 32px;
padding: 40px 32px;
align-items: center;
justify-content: center;
}
.panel {
background: rgba(255, 255, 255, 0.72);
border: 1px solid rgba(255, 255, 255, 0.8);
border-radius: 28px;
padding: 24px;
box-shadow: 0 18px 50px rgba(38, 52, 73, 0.12);
backdrop-filter: blur(14px);
}
.info h1 {
margin: 0 0 12px;
font-size: 32px;
line-height: 1.1;
}
.info p {
margin: 0 0 18px;
color: var(--text-secondary);
line-height: 1.6;
}
.state-switcher {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin: 20px 0 24px;
}
.state-switcher button {
border: 1px solid #cfd8e3;
border-radius: 999px;
background: #fff;
color: var(--text-primary);
padding: 10px 14px;
font-size: 14px;
cursor: pointer;
transition: 0.2s ease;
}
.state-switcher button.active {
border-color: var(--accent-color);
background: var(--accent-light);
color: var(--accent-color);
}
.api-grid {
display: grid;
gap: 12px;
margin-top: 20px;
}
.api-card {
background: #fff;
border: 1px solid #eef2f7;
border-radius: 16px;
padding: 14px 16px;
}
.api-card strong {
display: block;
margin-bottom: 6px;
font-size: 14px;
}
.api-card code {
display: inline-block;
padding: 2px 8px;
border-radius: 999px;
background: #f3f7fb;
color: #21425f;
font-size: 13px;
}
.api-card p {
margin: 8px 0 0;
font-size: 13px;
}
.login-overlay {
position: relative;
min-height: 700px;
border-radius: 28px;
background: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(4px);
display: flex;
align-items: center;
justify-content: center;
animation: fadeIn 0.2s ease;
padding: 16px;
}
.login-dialog {
position: relative;
background: var(--bg-card);
border-radius: 20px;
padding: 32px 28px;
max-width: 400px;
width: 100%;
text-align: center;
box-shadow: var(--shadow);
animation: scaleIn 0.25s ease;
}
.close-btn {
position: absolute;
top: 12px;
right: 12px;
width: 32px;
height: 32px;
border: none;
border-radius: 50%;
background: var(--bg-hover);
color: var(--text-secondary);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
}
.close-btn:hover {
background: #e0e0e0;
color: #333;
}
.login-icon {
margin: 0 auto 16px;
width: 72px;
height: 72px;
border-radius: 50%;
background: var(--accent-light);
color: var(--accent-color);
display: flex;
align-items: center;
justify-content: center;
}
.login-dialog h2 {
margin: 0 0 8px;
font-size: 20px;
font-weight: 700;
color: var(--text-primary);
}
.login-desc {
margin: 0 0 24px;
font-size: 14px;
color: var(--text-secondary);
line-height: 1.5;
}
.telegram-btn {
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
width: 100%;
padding: 14px 24px;
border: none;
border-radius: 12px;
background: var(--telegram);
color: #fff;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
}
.telegram-btn:hover {
background: var(--telegram-hover);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(42, 171, 238, 0.3);
}
.telegram-btn:active {
transform: translateY(0);
}
.tg-icon {
flex-shrink: 0;
}
.qr-section {
margin-top: 20px;
}
.qr-hint {
margin: 0 0 12px;
font-size: 13px;
color: #999;
}
.qr-container {
display: inline-flex;
padding: 12px;
background: #fff;
border-radius: 12px;
border: 1px solid var(--border);
}
.qr-container img {
display: block;
border-radius: 4px;
}
.qr-loading {
align-items: center;
justify-content: center;
width: 204px;
height: 204px;
}
.qr-loading .spinner,
.login-status .spinner {
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
.qr-loading .spinner {
width: 32px;
height: 32px;
border: 3px solid #e0e0e0;
border-top-color: var(--accent-color);
}
.qr-expired {
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8px;
width: 204px;
height: 204px;
cursor: pointer;
color: #999;
transition: color 0.2s ease;
}
.qr-expired:hover {
color: var(--accent-color);
}
.qr-expired span {
font-size: 13px;
}
.login-note {
margin: 16px 0 0;
font-size: 12px;
color: #999;
line-height: 1.4;
}
.login-status {
display: none;
align-items: center;
justify-content: center;
gap: 10px;
padding: 16px;
color: var(--text-secondary);
font-size: 14px;
}
.login-status .spinner {
width: 20px;
height: 20px;
border: 2px solid #e0e0e0;
border-top-color: var(--accent-color);
}
.dialog-content[data-state="checking"] .login-status {
display: flex;
}
.dialog-content[data-state="checking"] .action-block,
.dialog-content[data-state="loading"] .qr-ready,
.dialog-content[data-state="loading"] .qr-expired,
.dialog-content[data-state="expired"] .qr-ready,
.dialog-content[data-state="expired"] .qr-loading,
.dialog-content[data-state="error"] .qr-loading,
.dialog-content[data-state="error"] .qr-expired,
.dialog-content[data-state="checking"] .qr-section,
.dialog-content[data-state="checking"] .login-note {
display: none;
}
.dialog-content[data-state="ready"] .qr-loading,
.dialog-content[data-state="ready"] .qr-expired,
.dialog-content[data-state="ready"] .qr-error,
.dialog-content[data-state="loading"] .qr-ready,
.dialog-content[data-state="loading"] .qr-expired,
.dialog-content[data-state="loading"] .qr-error,
.dialog-content[data-state="expired"] .qr-loading,
.dialog-content[data-state="expired"] .qr-ready,
.dialog-content[data-state="expired"] .qr-error,
.dialog-content[data-state="error"] .qr-loading,
.dialog-content[data-state="error"] .qr-expired {
display: none;
}
.dialog-content[data-state="error"] .qr-ready {
display: inline-flex;
}
.metadata {
margin-top: 22px;
padding-top: 18px;
border-top: 1px solid #e9edf2;
font-size: 13px;
color: var(--text-secondary);
}
.metadata ul {
margin: 10px 0 0;
padding-left: 18px;
}
.metadata li + li {
margin-top: 6px;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes scaleIn {
from {
opacity: 0;
transform: scale(0.95);
}
to {
opacity: 1;
transform: scale(1);
}
}
@keyframes spin {
to { transform: rotate(360deg); }
}
@media (max-width: 980px) {
.page {
grid-template-columns: 1fr;
padding: 24px 16px 32px;
}
.login-overlay {
min-height: 560px;
}
}
@media (max-width: 480px) {
.panel {
border-radius: 22px;
padding: 18px;
}
.login-dialog {
padding: 24px 20px;
border-radius: 16px;
}
.qr-container img {
width: 140px;
height: 140px;
}
.qr-loading,
.qr-expired {
width: 164px;
height: 164px;
}
}
</style>
</head>
<body>
<main class="page">
<section class="panel info">
<h1>Telegram Login Dialog</h1>
<p>
Standalone extraction of the current login popup: same layout, same visual states,
same QR flow, but with reusable neutral endpoint names for moving into a separate repo.
</p>
<div class="state-switcher" aria-label="Dialog state switcher">
<button class="active" data-state-btn="ready" type="button">Ready</button>
<button data-state-btn="loading" type="button">QR Loading</button>
<button data-state-btn="checking" type="button">Checking</button>
<button data-state-btn="expired" type="button">Expired</button>
<button data-state-btn="error" type="button">Fallback</button>
</div>
<div class="api-grid">
<div class="api-card">
<strong>Start QR session</strong>
<code>POST /userauth/qr/create</code>
<p>Returns a one-time token and Telegram deeplink for the QR image.</p>
</div>
<div class="api-card">
<strong>Poll QR confirmation</strong>
<code>GET /userauth/qr/poll?token=...</code>
<p>Returns pending, confirmed, or expired. On confirmed, also returns the user session.</p>
</div>
<div class="api-card">
<strong>Read current session</strong>
<code>GET /userauth/session</code>
<p>Cookie-based session lookup used for initial auth check and fallback polling.</p>
</div>
<div class="api-card">
<strong>Sync cart after login</strong>
<code>POST /usersession/{sessionId}</code>
<p>Existing cart payload is preserved. Only the namespace is generalized for reuse.</p>
</div>
</div>
<div class="metadata">
<strong>Behavior kept intact</strong>
<ul>
<li>Open Telegram directly from the primary button.</li>
<li>Show QR immediately and poll every 3 seconds.</li>
<li>Expire the QR after 100 checks and allow manual refresh.</li>
<li>Re-check cookie session if QR creation fails.</li>
</ul>
</div>
</section>
<section class="panel">
<div class="login-overlay">
<div class="login-dialog">
<button class="close-btn" type="button" aria-label="Close dialog">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M18 6L6 18M6 6l12 12"></path>
</svg>
</button>
<div class="dialog-content" data-state="ready" id="dialog-content">
<div class="login-icon">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"></path>
</svg>
</div>
<h2>Login required</h2>
<p class="login-desc">Please log in via Telegram to proceed with your order.</p>
<div class="login-status checking">
<div class="spinner"></div>
<span>Checking...</span>
</div>
<div class="action-block">
<button class="telegram-btn" type="button">
<svg class="tg-icon" width="22" height="22" viewBox="0 0 24 24" fill="currentColor">
<path d="M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z"></path>
</svg>
Log in with Telegram
</button>
<div class="qr-section">
<p class="qr-hint">Or scan the QR code</p>
<div class="qr-container qr-loading">
<div class="spinner"></div>
</div>
<div class="qr-container qr-ready">
<img
src="https://api.qrserver.com/v1/create-qr-code/?size=180x180&data=https%3A%2F%2Ft.me%2Fuserauth_bot%3Fstart%3Dlogin_sample_token"
alt="QR Code"
width="180"
height="180"
loading="eager"
/>
</div>
<div class="qr-container qr-expired" role="button" tabindex="0" aria-label="Refresh QR code">
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M1 4v6h6M23 20v-6h-6"></path>
<path d="M20.49 9A9 9 0 0 0 5.64 5.64L1 10m22 4l-4.64 4.36A9 9 0 0 1 3.51 15"></path>
</svg>
<span>QR code expired. Click to refresh</span>
</div>
</div>
<p class="login-note">You will be redirected back after login.</p>
</div>
</div>
</div>
</div>
</section>
</main>
<script>
const content = document.getElementById('dialog-content');
const buttons = document.querySelectorAll('[data-state-btn]');
buttons.forEach((button) => {
button.addEventListener('click', () => {
const state = button.getAttribute('data-state-btn');
content.setAttribute('data-state', state);
buttons.forEach((candidate) => candidate.classList.remove('active'));
button.classList.add('active');
});
});
</script>
</body>
</html>