clean up stage 1
This commit is contained in:
@@ -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.
|
||||
@@ -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 сессии (если есть) для привязки к заказу, но не требуют авторизации строго. Фронтенд проверяет авторизацию перед оформлением заказа.
|
||||
@@ -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
|
||||
@@ -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`
|
||||
@@ -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
|
||||
@@ -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. Обновить контактную информацию (телефоны, адреса)
|
||||
|
||||
## Примечания
|
||||
|
||||
- Оба бренда используют одну кодовую базу
|
||||
- Все компоненты автоматически адаптируются под выбранный бренд
|
||||
- Легко добавить новые бренды по той же схеме
|
||||
@@ -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/)
|
||||
@@ -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. Логирование действий для доказательной базы
|
||||
|
||||
**Все юридические и информационные требования выполнены!** ✅
|
||||
@@ -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! 🌟
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user