chore: repo cleanup + docs for admin API and DB schema (v1.2.8)

- remove stale docs/admin-frontend-spec.md (old Express/EJS admin), unused templates/ (SmartAdmin copy), dead scripts/sync-agents.cjs, committed dev.pid
- add docs/API.md (admin REST API reference) and docs/DATABASE.md (DB schema)
- refresh .env.example: drop stale ADMIN_PORT/SHOP_CONTAINER, add ADMIN_URL, HEALTH_PORT, DEFAULT_LANGUAGE, CHATBOT_API_*
- add npm run lint so Gitea workflows pass
- rebrand web-testing suite from APAW to telegram-shop
This commit is contained in:
NW
2026-08-09 00:22:22 +01:00
parent 338a700b3a
commit d7680357af
13 changed files with 517 additions and 1192 deletions

207
docs/API.md Normal file
View File

@@ -0,0 +1,207 @@
# Telegram Shop — Admin API Reference
Актуальная документация по REST API админ-панели (`admin-next/`, Next.js 16 + Prisma).
- **Базовый URL**: `http://<host>:3000`
- **Формат**: JSON (`Content-Type: application/json`)
- **Аутентификация**: cookie `admin_token` (httpOnly, 24ч), устанавливается через `POST /api/auth/login`
## Аутентификация
| Метод | Путь | Описание |
|---|---|---|
| POST | `/api/auth/login` | Вход по токену. Body: `{ "token": "..." }`. Rate-limit: 5 попыток / 15 мин на IP (429). Устанавливает cookie `admin_token` |
| GET | `/api/auth/session` | Проверка сессии. Ответ: `{ "role": "admin" \| "super_admin" }` (401 если нет/просрочен) |
| POST | `/api/auth/logout` | Выход, удаляет cookie |
### Роли
- `admin` — обычный администратор
- `super_admin` — доступ к seed-фразам, экспорту, импорту, очистке БД
Роль определяется при входе: если `SUPER_ADMIN_SECRET` не задан или равен `ADMIN_SECRET` — все админы получают `super_admin`. Иначе роль `super_admin` даёт только вход по `SUPER_ADMIN_SECRET`.
**Все эндпоинты ниже требуют cookie `admin_token`** (иначе 401). Эндпоинты с пометкой 🔒 требуют роль `super_admin` (иначе 403).
## Дашборд и статистика
### GET `/api/stats/dashboard`
Сводная статистика для дашборда:
- counts: `totalUsers`, `totalProducts`, `totalPurchases`, `totalSubcategories`, `bannedUsers`, `activeWallets`
- статусы покупок: `completedPurchases`, `pendingPurchases`, `cancelledPurchases`
- метрики: `totalRevenue` (сумма completed), `aov`, `conversionRate`
- временные ряды: `revenueByDay`, `purchasesByDay`, `newUsersByDay` (последние 30 дней)
- `topProducts`, `recentPurchases`, `recentUsers`
## Каталог
### GET `/api/catalog/tree`
Полное дерево каталога: локации, категории, подкатегории с `_count` (вложенные сущности).
### Локации
| Метод | Путь | Описание |
|---|---|---|
| GET | `/api/locations/bulk` | Список локаций (с `_count` категорий/товаров) |
| POST | `/api/locations/bulk` | Создать. Body: `{ country, city, district }` |
| PUT | `/api/locations/[id]` | Обновить. Body: `{ country?, city?, district? }` |
| PATCH | `/api/locations/[id]` | Переключить `is_active` (0/1) |
| DELETE | `/api/locations/[id]` | Удалить (409 если есть связанные сущности) |
### Категории
| Метод | Путь | Описание |
|---|---|---|
| GET | `/api/categories/bulk` | Список категорий (с локацией и `_count`) |
| POST | `/api/categories/bulk` | Создать. Body: `{ name, locationId }` |
| PUT | `/api/categories/[id]` | Обновить. Body: `{ name?, locationId? }` (409 при дубликате в локации) |
| PATCH | `/api/categories/[id]` | Переключить `is_active` |
| DELETE | `/api/categories/[id]` | Удалить (409 если есть связанные сущности) |
### Подкатегории
| Метод | Путь | Описание |
|---|---|---|
| GET | `/api/subcategories/bulk` | Список подкатегорий |
| POST | `/api/subcategories/bulk` | Создать. Body: `{ name, categoryId }` |
| PUT | `/api/subcategories/[id]` | Обновить. Body: `{ name?, categoryId? }` |
| PATCH | `/api/subcategories/[id]` | Переключить `is_active` |
| DELETE | `/api/subcategories/[id]` | Удалить |
### Товары
| Метод | Путь | Описание |
|---|---|---|
| GET | `/api/products/bulk` | Список. Query: `loc`, `cat`, `sub`, `search`, `page`, `limit` (≤100). Ответ: `{ data, total, page, limit }` |
| POST | `/api/products/add` | Создать. Body: `{ locationId, categoryId, subcategoryId?, name, description?, privateData?, price, quantityInStock?, photoUrl?, hiddenPhotoUrl?, hiddenCoordinates?, hiddenDescription?, isMono? }`. Обязательны: `locationId, categoryId, name, price`. `isMono=1``quantityInStock=999999` |
| GET | `/api/products/[id]` | Детали товара (с category, subcategory, location) |
| PUT | `/api/products/[id]` | Обновить (частично) |
| DELETE | `/api/products/[id]` | Удалить |
| POST | `/api/products/[id]/clone` | Клонировать (имя получает суффикс `(Copy)`) |
## Пользователи
| Метод | Путь | Описание |
|---|---|---|
| GET | `/api/users/bulk` | Список. Query: `search` (username/telegramId), `status` (0/2), `page`, `limit` (≤100). Ответ: `{ data, total, page, limit }` |
| GET | `/api/users/[id]` | Детали: пользователь + кошельки + последние 20 покупок + связанный лид |
| POST | `/api/users/[id]` | Обновить (username, country, city, district, notes и т.д.) |
| PATCH | `/api/users/[id]` | Переключить `status` (0=active, 2=blocked) |
| POST | `/api/users/[id]/adjust-balance` | Изменить баланс. Body: `{ amount: number, currency: "total_balance" \| "bonus_balance" }`. Пишет audit-запись |
| POST | `/api/users/batch-status` | Массово сменить статус. Body: `{ userIds: number[], newStatus: 0 \| 2 }` |
## Кошельки
| Метод | Путь | Описание |
|---|---|---|
| GET | `/api/wallets/bulk` | Пользователи, у которых есть кошельки. Query: `search`. Ответ: `{ users, totalWallets, totalBalance }` |
| GET | `/api/wallets/[userId]` | Кошельки конкретного пользователя |
| GET | `/api/wallets/overview` | Сводка: суммы и количество по типам (BTC/LTC/ETH/USDT/USDC), последние 20 комиссионных выплат |
| GET | `/api/wallets/seeds` | 🔒 Seed-фразы всех кошельков (пишет audit `seed_phrase_viewed`) |
| GET | `/api/wallets/export-seeds` | 🔒 Экспорт seed-фраз в CSV (Content-Disposition: attachment) |
| POST | `/api/wallets/record-payment` | Записать комиссионную выплату. Body: `{ paidAmount: number, note? }` |
## Покупки
| Метод | Путь | Описание |
|---|---|---|
| GET | `/api/purchases/bulk` | Список. Query: `status` (pending/completed/cancelled), `from`, `to` (YYYY-MM-DD), `page`, `limit` (≤100). Ответ: `{ data, total, page, limit }` |
| PATCH | `/api/purchases/[id]` | Сменить статус. Body: `{ status: "completed" \| "cancelled" }`. Только для `pending` |
| POST | `/api/purchases/batch-status` | Массово. Body: `{ purchaseIds: number[], status: "completed" \| "cancelled" }` (обновляет только pending) |
## Транзакции
| Метод | Путь | Описание |
|---|---|---|
| GET | `/api/transactions/bulk` | Список. Query: `userId`, `page`, `limit` (≤100, по умолчанию 20). Ответ: `{ data, total, page, limit }` |
## Лиды и чат
### Лиды
| Метод | Путь | Описание |
|---|---|---|
| GET | `/api/leads/bulk` | Список. Query: `search` (name/phone/email/telegram/telegramId), `status`, `page`, `limit` (≤100). Ответ: `{ data, total, page, limit }` |
| GET | `/api/leads/[id]` | Детали лида + связанный пользователь (баланс, покупки, кошельки) |
| PUT | `/api/leads/[id]` | Обновить (name, phone, email, telegram, status, notes, customFields и т.д.) |
| GET | `/api/leads/[id]/activity` | Активность: `{ hourly: number[24], yearly: { "YYYY-MM-DD": count }, total }` (по audit_log) |
| GET | `/api/leads/[id]/sessions` | Чат-сессии лида с распарсенными сообщениями |
### Чат (публичный виджет)
| Метод | Путь | Описание |
|---|---|---|
| POST | `/api/chat` | Отправить сообщение в чат-виджет. Body: `{ sessionId?, message, language?, device?, ip?, country? }`. Создаёт/обновляет `ChatSession`, извлекает данные лида (имя/телефон/email/telegram), вызывает ИИ-провайдера, при `chatbot_sleep_mode` отвечает sleep-сообщением |
### Оператор
| Метод | Путь | Описание |
|---|---|---|
| POST | `/api/operator` | Подключение/отключение оператора. Body: `{ sessionId, action: "connect" \| "disconnect", operatorName }`. Пишет audit `operator_connect` |
## Чатбот (настройки)
| Метод | Путь | Описание |
|---|---|---|
| GET | `/api/admin/chatbot` | Настройки чатбота из `site_settings` (ключи `chatbot_*`), API-ключ маскируется |
| PUT | `/api/admin/chatbot` | Обновить настройки. Валидация: `chatbot_temperature` 02, `chatbot_max_tokens` 504000, `chatbot_max_history` 150 |
| GET | `/api/chatbot/models?endpoint=&apiKey=` | Список моделей провайдера через OpenAI-совместимый `/models`. Без query — берёт сохранённые настройки. Поддерживает OpenAI (`data[]`) и Ollama (`models[]`) |
Ключи настроек чатбота (defaults):
`chatbot_enabled`, `chatbot_sleep_mode`, `chatbot_sleep_message`, `chatbot_system_prompt`, `chatbot_temperature` (0.7), `chatbot_max_tokens` (1024), `chatbot_max_history` (20), `chatbot_knowledge_base`, `chatbot_provider` (ollama), `chatbot_api_endpoint`, `chatbot_api_key`, `chatbot_model`.
## Аудит
| Метод | Путь | Описание |
|---|---|---|
| GET | `/api/audit/bulk` | Журнал аудита. Query: `page`, `limit` (≤200, по умолчанию 100), `userId`, `from`, `to`, `search`, `action`. Ответ: `{ data, total, page, limit }` |
## Локализация (i18n)
| Метод | Путь | Описание |
|---|---|---|
| GET | `/api/locales` | Все переводы: `{ en: {...}, es: {...}, de: {...} }` |
| PUT | `/api/locales` | Обновить ключ. Body: `{ lang, key, value }` |
## Настройки
| Метод | Путь | Описание |
|---|---|---|
| GET | `/api/settings` | Список настроек (секреты маскированы, список в `_masked`) |
| PUT | `/api/settings` | Обновить. Body: `{ key, value }`. Ответ: «Settings saved. Restart required.» |
| GET | `/api/settings/export` | Полный экспорт БД в JSON (users, wallets, purchases, categories, subcategories, locations, products, auditLogs, commissionPayments, userStates) |
| POST | `/api/settings/import` | 🔒 Импорт (заглушка: «Import not yet implemented») |
## Seed / Demo
| Метод | Путь | Описание |
|---|---|---|
| GET | `/api/seed/data` | Проверка: `{ seeded: boolean }` (есть ли пользователи) |
| POST | `/api/seed/demo` | Заполнить демо-данными. Body: `{ reauthToken }` (повторный ввод секрета) |
| POST | `/api/seed/clear` | 🔒 Очистить все данные. Body: `{ reauthToken }`. Пишет audit `clear_all` |
## Корневой
| Метод | Путь | Описание |
|---|---|---|
| GET | `/api` | Служебный ответ (health API) |
## Коды ошибок
| Код | Значение |
|---|---|
| 400 | Невалидный запрос / отсутствуют обязательные поля |
| 401 | Нет cookie `admin_token` или токен невалиден/просрочен |
| 403 | Недостаточно прав (нужен `super_admin`) |
| 404 | Сущность не найдена |
| 409 | Конфликт (дубликат, есть связанные записи) |
| 429 | Слишком много попыток входа |
| 500 | Внутренняя ошибка |
## Соглашения
- Пагинация: `page` (с 1), `limit` (кап зависит от эндпоинта: 50/100/200)
- Даты: `from`/`to` в формате `YYYY-MM-DD` (to инклюзивно до конца дня)
- `is_active`/`status` пользователей: `0` = активно, `2` = заблокировано
- Статусы покупок: `pending` / `completed` / `cancelled`
- Статусы лидов: `new` / `contacted` / `qualified` / `lost` / `spam`

220
docs/DATABASE.md Normal file
View File

@@ -0,0 +1,220 @@
# Telegram Shop — Database Schema
Единая SQLite-база (`db/shop.db`), используемая **одновременно** ботом (`src/`, better-sqlite3) и админ-панелью (`admin-next/`, Prisma). Источник истины схемы: `admin-next/prisma/schema.prisma`.
Миграции бота: `src/migrations/` (001014, runner: `src/migrations/runner.js`).
## Схема
### users — пользователи Telegram
| Колонка | Тип | Описание |
|---|---|---|
| id | INTEGER PK | |
| telegram_id | TEXT UNIQUE | ID в Telegram |
| username | TEXT? | |
| country / city / district | TEXT? | География |
| status | INTEGER (0) | 0=active, 2=blocked |
| total_balance | REAL (0) | Основной баланс |
| bonus_balance | REAL (0) | Бонусный баланс |
| language | TEXT ('en') | en / es / de |
| language_set | INTEGER (0) | Флаг выбора языка |
| notes | TEXT? | Заметки админа (миграция 014) |
| created_at | DATETIME | |
Связи: `wallets`, `transactions`, `purchases`.
### crypto_wallets — криптокошельки
| Колонка | Тип | Описание |
|---|---|---|
| id | INTEGER PK | |
| user_id | INTEGER FK → users | |
| wallet_type | TEXT | BTC / LTC / ETH / USDT / USDC |
| address | TEXT | |
| derivation_path | TEXT? | |
| mnemonic | TEXT? | Seed-фраза (зашифрована) |
| balance | REAL (0) | |
| created_at | DATETIME | |
Уникальность: `(user_id, wallet_type)` — один кошелёк каждого типа на пользователя.
### transactions — транзакции
| Колонка | Тип | Описание |
|---|---|---|
| id | INTEGER PK | |
| user_id | INTEGER FK → users | |
| wallet_type | TEXT | |
| tx_hash | TEXT? | |
| amount | REAL | |
| created_at | DATETIME | |
### locations — локации
| Колонка | Тип | Описание |
|---|---|---|
| id | INTEGER PK | |
| country | TEXT | |
| city | TEXT | |
| district | TEXT ('') | |
| is_active | INTEGER (1) | 0/1 |
| created_at | DATETIME | |
Уникальность: `(country, city, district)`.
### categories — категории
| Колонка | Тип | Описание |
|---|---|---|
| id | INTEGER PK | |
| location_id | INTEGER FK → locations | |
| name | TEXT | |
| is_active | INTEGER (1) | |
| created_at | DATETIME | |
Уникальность: `(location_id, name)`.
### subcategories — подкатегории
| Колонка | Тип | Описание |
|---|---|---|
| id | INTEGER PK | |
| category_id | INTEGER FK → categories | |
| name | TEXT | |
| is_active | INTEGER (1) | |
| created_at | DATETIME | |
Уникальность: `(category_id, name)`.
### products — товары
| Колонка | Тип | Описание |
|---|---|---|
| id | INTEGER PK | |
| location_id | INTEGER FK → locations | |
| category_id | INTEGER FK → categories | |
| subcategory_id | INTEGER? FK → subcategories | |
| name | TEXT | |
| description | TEXT? | |
| private_data | TEXT? | Скрытый контент (выдаётся после покупки) |
| price | REAL | |
| quantity_in_stock | INTEGER (0) | Для mono-товаров = 999999 |
| photo_url | TEXT? | Публичное фото |
| hidden_photo_url | TEXT? | Скрытое фото |
| hidden_coordinates | TEXT? | Скрытые координаты |
| hidden_description | TEXT? | Скрытое описание |
| is_mono | INTEGER (0) | 1 = неограниченный товар |
| created_at | DATETIME | |
### purchases — покупки
| Колонка | Тип | Описание |
|---|---|---|
| id | INTEGER PK | |
| user_id | INTEGER FK → users | |
| product_id | INTEGER FK → products | |
| wallet_type | TEXT? | |
| tx_hash | TEXT? | |
| quantity | INTEGER | |
| total_price | REAL | |
| purchase_date | DATETIME | |
| status | TEXT ('pending') | pending / completed / cancelled |
### commission_payments — комиссионные выплаты
| Колонка | Тип | Описание |
|---|---|---|
| id | INTEGER PK | |
| total_balance_usd | REAL | |
| commission_rate | REAL | |
| commission_amount_usd | REAL | |
| paid_amount_usd | REAL | |
| wallet_count | INTEGER | |
| note | TEXT? | |
| created_at | DATETIME | |
### audit_log — журнал аудита
| Колонка | Тип | Описание |
|---|---|---|
| id | INTEGER PK | |
| action | TEXT | `balance_adjust`, `operator_connect`, `seed_phrase_viewed`, `clear_all` и др. |
| admin_id | TEXT | Роль или telegram_id |
| details | TEXT? | JSON-строка с контекстом |
| created_at | DATETIME | |
### chat_sessions — чат-сессии (чатбот)
| Колонка | Тип | Описание |
|---|---|---|
| id | INTEGER PK | |
| session_id | TEXT UNIQUE | |
| telegram_id | TEXT? | |
| lead_id | INTEGER? FK → leads | |
| messages | TEXT | JSON-массив `{role, content, timestamp}` |
| language | TEXT ('en') | |
| device / ip / country | TEXT? | |
| customer_profile | TEXT? | AI-профиль клиента (JSON) |
| is_active | BOOLEAN (true) | |
| operator_name | TEXT? | |
| auto_reply_disabled | BOOLEAN (false) | |
| operator_connected_at | DATETIME? | |
| created_at / updated_at | DATETIME | |
### leads — лиды
| Колонка | Тип | Описание |
|---|---|---|
| id | INTEGER PK | |
| telegram_id | TEXT? UNIQUE | |
| name / phone / email / telegram | TEXT? | |
| status | TEXT ('new') | new / contacted / qualified / lost / spam |
| verification | TEXT ('pending') | |
| notes | TEXT? | |
| custom_fields | TEXT ('{}') | JSON |
| geo_address | TEXT? | |
| ai_lead_score | REAL? | |
| created_at / updated_at | DATETIME | |
Связь: `chatSessions`.
### site_settings — настройки (ключ-значение)
| Колонка | Тип | Описание |
|---|---|---|
| id | INTEGER PK | |
| key | TEXT UNIQUE | Например `chatbot_*` |
| value | TEXT | |
| created_at / updated_at | DATETIME | |
### user_states — состояния пользователей бота
| Колонка | Тип | Описание |
|---|---|---|
| chat_id | TEXT PK | |
| state_data | TEXT? | JSON состояния |
| updated_at | INTEGER | Unix-время |
## Связи (ER-сводка)
```
users 1──N crypto_wallets
users 1──N transactions
users 1──N purchases
locations 1──N categories 1──N subcategories
locations 1──N products
categories 1──N products
subcategories 1──N products
products 1──N purchases
leads 1──N chat_sessions
leads 1──1 users (по telegram_id, не FK)
```
## Примечания
- **Бот и админка работают с одной БД**: любые изменения мгновенно видны обеим сторонам
- `users` и `leads`**отдельные таблицы**, связываются по `telegram_id` (не внешний ключ)
- `mnemonic` хранится зашифрованным (ключ `ENCRYPTION_KEY`)
- Каскадное удаление: wallets/transactions/purchases удаляются вместе с пользователем; products — вместе с location/category
- Миграции бота нумеруются `NNN_*.js`; Prisma-схема админки должна отражать ту же структуру

View File

@@ -1,750 +0,0 @@
# Админ-панель Telegram Shop — Техническое описание фронтенда и API
> **Назначение документа**: комплексное ТЗ для отдела фронтенда по реализации полноценного админ-кабинета поверх существующего бэкенда.
> **Версия бэкенда**: v1.2.4 (2026-08-05)
> **Дата**: 2026-08-05
> **Статус**: актуально на момент передачи
---
## Оглавление
1. [Обзор системы](#1-обзор-системы)
2. [Технологический стек](#2-технологический-стек)
3. [Архитектура развёртывания](#3-архитектура-развёртывания)
4. [Доступ и безопасность](#4-доступ-и-безопасность)
5. [Схема базы данных](#5-схема-базы-данных)
6. [Карта эндпоинтов (полная)](#6-карта-эндпоинтов-полная)
7. [Спецификация экранов](#7-спецификация-экранов)
8. [Роли и права](#8-роли-и-права)
9. [Формат данных](#9-формат-данных)
10. [Существующие ограничения и подводные камни](#10-существующие-ограничения-и-подводные-камни)
11. [Требования к новому фронтенду](#11-требования-к-новому-фронтенду)
12. [API-контракты JSON (дополнить бэкенду)](#12-api-контракты-json-дополнить-бэкенду)
13. [Чек-лист приёмки](#13-чек-лист-приёмки)
---
## 1. Обзор системы
Telegram Shop — это **Telegram-бот-магазин** (Node.js) с **встроенной админ-панелью** на Express + EJS (шаблон SmartAdmin). Продажи идут через Telegram-бота, админка управляет всем магазином через браузер.
Бизнес-сущности:
| Сущность | Описание |
|---|---|
| **Users** | Покупатели из Telegram (id, ник, локация, статус, балансы, язык) |
| **Locations** | Гео-иерархия: Страна → Город → Район (округа) |
| **Categories / Subcategories** | Категории товаров, привязанные к локации; подкатегории внутри категории |
| **Products** | Товары: цена, остаток, публичное фото, скрытый контент (фото/координаты/описание), private-заметки, цифровые (mono) |
| **Purchases** | Заказы: пользователь, товар, валюта, tx_hash, количество, сумма, статус |
| **Crypto Wallets** | Криптокошельки пользователей (BTC/LTC/ETH/USDT/USDC), с зашифрованными seed-фразами |
| **Transactions** | Ончейн-транзакции пользователей |
| **Commission payments** | Платежи комиссии владельцу платформы (SaaS-модель) |
| **Audit log** | Журнал действий админов |
| **Locales (i18n)** | JSON-файлы переводов бота (en/es/de), редактируются из админки |
**Ключевой факт**: бот и админка живут в **одном процессе** и используют **одну SQLite-базу** (`db/shop.db`) — поэтому любые изменения в админке мгновенно видны боту и наоборот. Отдельного REST API **нет** — админка рендерит EJS на сервере и ходит в БД напрямую.
---
## 2. Технологический стек
### Текущий (серверная часть — НЕ трогать без отдельной задачи)
| Слой | Технология |
|---|---|
| Язык | Node.js ≥ 18 (ES Modules, `"type": "module"`) |
| Web-фреймворк | Express 4 |
| Шаблонизация | EJS + `express-ejs-layouts` (layout: `views/layout.ejs`) |
| Шаблон UI | SmartAdmin (собственные партиалы в `views/partials/`, SCSS в `public/sass/`, сборка `smartapp.min.css`) |
| БД | SQLite через `better-sqlite3` (обёртка `db.runAsync/allAsync/getAsync` в `src/config/database.js`) |
| Загрузка файлов | Multer (только картинки jpeg/png/webp/gif, ≤ 10 МБ) в `uploads/` |
| QR | `qrcode` (для seed-фраз) |
| CDN-ресурсы | Bootstrap 5.3.2, FontAwesome 6.4.2 с jsDelivr (только в legacy-`layout.js`) |
### Текущий фронтенд (что есть)
- **Два параллельных набора вьюх**:
1. **EJS-шаблоны** (`views/*.ejs`) — используются роутами (dashboard, users, wallets, purchases, audit, settings, categories, locations, payment-wallets, seed, locales, catalog, products, product-edit, user-detail).
2. **JS-рендер-функции** (`views/*.js`) — legacy/дублирующий слой, возвращающий HTML-строки (`layout()`, `renderCatalog()`, `renderWalletLayout()` и т.д.). **На практике роуты рендерят EJS; JS-слой частично устарел** (дублирует catalog и wallets). Новый фронтенд их не использует.
- Статика: Bootstrap 5 (SmartAdmin-тема), ApexCharts, jQuery, FontAwesome, собственные скрипты `public/scripts/`.
- AJAX-вызовы есть только в нескольких местах (catalog-модалка, wallets-обновление балансов, locales).
---
## 3. Архитектура развёртывания
```
Internet
├── HTTPS/LAN ───────────────► telegram_shop_prod :3001 (Express: бот + админка)
└── Tor Network ──► tor-proxy ──► HiddenService :80 ──► telegram_shop_prod :3001
(admin onion-адрес, работа без HTTPS)
```
- Единственный контейнер `telegram_shop_prod` (node:22-alpine) слушает порт `3001` (`ADMIN_PORT`).
- Доступ к админке: напрямую `http://host:3001` или через `.onion` (Tor Browser).
- Volume: `db/` (SQLite), `uploads/` (фото товаров, отдаются через `/uploads`), `.env` (только чтение).
- WireGuard опционален (`WG_ENABLED`).
**Следствие для фронтенда**: админка доступна по **HTTP без HTTPS** (в т.ч. через Tor). Это накладывает жёсткие ограничения:
- нельзя использовать Secure-куки;
- нельзя полагаться на Origin-проверку (Tor) — CSRF реализован вручную (см. §10);
- весь JS/CSS должен быть либо локальным, либо закешированным (CDN может быть недоступен из Tor).
---
## 4. Доступ и безопасность
### 4.1. Аутентификация (важно для фронта)
Реализована в `src/admin/auth.js`. **Не JWT, а самописный HMAC-токен**:
| Параметр | Значение |
|---|---|
| Кука | `admin_token` (httpOnly, `sameSite=false`, maxAge 24ч) |
| Формат токена | `base64(payload).hmac_sha256(payload)` |
| Payload | `{ role, jti, iat, exp }` |
| Секрет | `ADMIN_SECRET` (env) |
| Вход | POST `/login` с полем `token` (пароль = админ-токен) |
| Выход | GET `/logout` |
Роли (определяются **каким секретом залогинились**):
| Роль | Условие | Права |
|---|---|---|
| `admin` | токен = `ADMIN_SECRET` | Все страницы, кроме seed-выгрузки |
| `super_admin` | токен = `SUPER_ADMIN_SECRET` (если задан и ≠ ADMIN_SECRET) | + просмотр/экспорт seed-фраз, комиссии |
При `SUPER_ADMIN_SECRET` не заданном — **все админы считаются супер-админами** (`config.SUPER_ADMIN_IDS` по умолчанию = `ADMIN_IDS`, но роли в админке задаются именно через секреты, не через `ADMIN_IDS`).
Rate-limit логина: 5 попыток / 15 минут с IP.
**Деструктивные действия** (`/seed/*`) требуют повторного ввода токена в поле `reauth_token` (middleware `requireReAuth`).
### 4.2. Защита статики и загрузок
- `/uploads/*` — только после `requireAuth`, заголовки `X-Content-Type-Options: nosniff` + `Content-Disposition: attachment` (фото не рендерятся inline из uploads, только скачивание).
- Файлы uploads именуются `{timestamp}-{hex}.{ext}`, MIME-белый список.
- CSRF: заглушка (`csrf.js` возвращает пустой токен и no-op) — **сознательно отключено ради совместимости с Tor**. См. §10.
### 4.3. Существующие уязвимости, которые фронтенд не должен усугублять
1. Нет реальной CSRF-защиты — все POST-формы уязвимы к cross-site-запросам. В новом фронте использовать токены, если бэкенд их вернёт, иначе как минимум не убирать подтверждения на деструктив.
2. Некоторые вьюхи выводят данные с ручным `esc()`/`escapeHtml()`; часть EJS использует `<%= %>` (экранируется) — но есть места с `JSON.stringify` прямо в `<script>` (инъекция через данные). Новому фронту: **все данные из API экранировать на клиенте**.
3. Пароль админа (ADMIN_SECRET) отправляется как обычное поле формы.
4. Seed-фразы — сверхчувствительные данные; показывать только super_admin, с аудитом (logAudit: `seed_phrase_viewed`, `seed_phrase_qr_viewed`, `csv_seed_export`), желательно с подтверждением.
---
## 5. Схема базы данных
Источник истины: `src/migrations/*.js` (001012). SQLite, FK включены, WAL-режим.
### 5.1. `users`
| Колонка | Тип | Описание |
|---|---|---|
| `id` | INTEGER PK AUTOINCREMENT | Внутренний ID |
| `telegram_id` | TEXT UNIQUE NOT NULL | ID в Telegram (строка!) |
| `username` | TEXT | Ник |
| `country` / `city` / `district` | TEXT | Гео из бота (снимок) |
| `status` | INTEGER DEFAULT 0 | `0`=активен, `2`=заблокирован, прочее=удалён |
| `total_balance` | REAL DEFAULT 0 | Основной баланс в USD |
| `bonus_balance` | REAL DEFAULT 0 | Бонусный баланс в USD |
| `language` | TEXT DEFAULT 'en' | Язык бота (`en`/`es`/`de`) |
| `language_set` | INTEGER DEFAULT 0 | Флаг выбора языка |
| `created_at` | DATETIME | Регистрация |
### 5.2. `crypto_wallets`
| Колонка | Тип | Описание |
|---|---|---|
| `id` | INTEGER PK | |
| `user_id` | INTEGER FK→users (CASCADE) | |
| `wallet_type` | TEXT | BTC/LTC/ETH/USDT/USDC (+ архивные с суффиксом `#N`, отфильтровываются `NOT LIKE '%#_%'`) |
| `address` | TEXT | Адрес |
| `derivation_path` | TEXT | Derivation path |
| `mnemonic` | TEXT | **Зашифрована** (`encrypt(mnemonic, user_id)`) |
| `balance` | REAL DEFAULT 0 | Кэш баланса (обновляется с блокчейна) |
| `created_at` | DATETIME | |
| UNIQUE | (user_id, wallet_type) | |
### 5.3. `transactions`
| Колонка | Тип | Описание |
|---|---|---|
| `id` | INTEGER PK | |
| `user_id` | INTEGER FK | |
| `wallet_type` | TEXT | |
| `tx_hash` | TEXT | |
| `amount` | REAL | |
| `created_at` | DATETIME | |
### 5.4. `locations`
| Колонка | Тип | Описание |
|---|---|---|
| `id` | INTEGER PK | |
| `country` | TEXT NOT NULL | |
| `city` | TEXT NOT NULL | |
| `district` | TEXT NOT NULL | Район (может быть пустой строкой) |
| `is_active` | INTEGER NOT NULL DEFAULT 1 | Отключённые локации скрыты из бота |
| `created_at` | DATETIME | |
| UNIQUE | (country, city, district) | |
### 5.5. `categories`
| Колонка | Тип | Описание |
|---|---|---|
| `id` | INTEGER PK | |
| `location_id` | INTEGER FK→locations (CASCADE) | |
| `name` | TEXT NOT NULL | |
| `is_active` | INTEGER NOT NULL DEFAULT 1 | |
| `created_at` | DATETIME | |
| UNIQUE | (location_id, name) | |
### 5.6. `subcategories`
| Колонка | Тип | Описание |
|---|---|---|
| `id` | INTEGER PK | |
| `category_id` | INTEGER FK→categories (CASCADE) | |
| `name` | TEXT NOT NULL | |
| `is_active` | INTEGER NOT NULL DEFAULT 1 | |
| `created_at` | DATETIME | |
| UNIQUE | (category_id, name) | |
### 5.7. `products`
| Колонка | Тип | Описание |
|---|---|---|
| `id` | INTEGER PK | |
| `location_id` | INTEGER FK→locations (CASCADE) | |
| `category_id` | INTEGER FK→categories (CASCADE) | |
| `subcategory_id` | INTEGER FK→subcategories (SET NULL) | |
| `name` | TEXT NOT NULL | |
| `description` | TEXT | Публичное описание |
| `private_data` | TEXT | Внутренние заметки (не показывать) |
| `price` | REAL NOT NULL CHECK (price > 0) | USD |
| `quantity_in_stock` | INTEGER DEFAULT 0 | Остаток |
| `photo_url` | TEXT | Публичное фото (URL или `/uploads/...`) |
| `hidden_photo_url` | TEXT | Фото после покупки |
| `hidden_coordinates` | TEXT | Координаты после покупки (`lat,lng`) |
| `hidden_description` | TEXT | Текст после покупки |
| `is_mono` | INTEGER DEFAULT 0 | **Цифровой товар**: бесконечный остаток (`1` ⇒ stock=999999, в боте продаётся без складских проверок) |
| `created_at` | DATETIME | |
### 5.8. `purchases`
| Колонка | Тип | Описание |
|---|---|---|
| `id` | INTEGER PK | |
| `user_id` | INTEGER FK→users (CASCADE) | |
| `product_id` | INTEGER FK→products (CASCADE) | |
| `wallet_type` | TEXT | Валюта оплаты |
| `tx_hash` | TEXT | Хэш транзакции |
| `quantity` | INTEGER CHECK (quantity > 0) | |
| `total_price` | REAL CHECK (total_price > 0) | USD |
| `purchase_date` | DATETIME | |
| `status` | TEXT DEFAULT 'pending' | `pending` / `completed` / `cancelled` |
### 5.9. `commission_payments`
| Колонка | Тип | Описание |
|---|---|---|
| `id` | INTEGER PK | |
| `total_balance_usd` | REAL NOT NULL | Снимок баланса на момент платежа |
| `commission_rate` | REAL NOT NULL | % комиссии |
| `commission_amount_usd` | REAL NOT NULL | Начисленная комиссия |
| `paid_amount_usd` | REAL NOT NULL | Фактически уплачено |
| `wallet_count` | INTEGER NOT NULL | Кошельков на момент |
| `note` | TEXT | |
| `created_at` | DATETIME | |
### 5.10. `audit_log`
| Колонка | Тип | Описание |
|---|---|---|
| `id` | INTEGER PK | |
| `action` | TEXT NOT NULL | Слаг действия: `balance_adjust`, `csv_seed_export`, `seed_phrase_viewed`, `seed_phrase_qr_viewed`, `login` и т.д. |
| `admin_id` | TEXT NOT NULL | Роль или ID |
| `details` | TEXT | JSON-строка с контекстом |
| `created_at` | DATETIME | |
### 5.11. `user_states`
| Колонка | Тип | Описание |
|---|---|---|
| `chat_id` | TEXT PK | |
| `state_data` | TEXT | JSON состояния диалога бота |
| `updated_at` | INTEGER | |
---
## 6. Карта эндпоинтов (полная)
Базовый URL: `http://<host>:3001`. **Все маршруты, кроме `/login`, `/logout`, `/health`, требуют куки `admin_token`** (redirect на `/login`).
Легенда: 🔓 — только super_admin · ⚠️ — деструктивно, требует `requireReAuth` · 📁 — загрузка файла · 🔁 — редирект после POST.
### 6.1. Служебные
| Метод | Путь | Назначение |
|---|---|---|
| GET | `/health` | Health-check → `{ status:'ok', uptime }` |
| GET | `/login` | Страница логина (поле `token`) |
| POST | `/login` | Вход; body `{ token }`; 429 при rate-limit; 401 неверный токен |
| GET | `/logout` | Выход + инвалидация jti |
| GET | `/error` (рендер) | Страница ошибки (глобальный обработчик) |
### 6.2. Dashboard (роут: `routes/dashboard.js`)
| Метод | Путь | Ответ |
|---|---|---|
| GET | `/` | EJS `dashboard` со статами и данными графиков |
Query-параметры-флеши: `?seeded=1`, `?cleared=1` (показывают сообщения).
**Данные, передаваемые в шаблон** (это и есть «API» дашборда):
| Поле | Тип | Содержимое |
|---|---|---|
| `stats.totalUsers` | int | Всего пользователей |
| `stats.totalProducts` | int | Всего товаров |
| `stats.totalPurchases` | int | Всего покупок |
| `stats.totalRevenue` | float | Выручка по `status='completed'` |
| `stats.totalSubcategories` | int | Подкатегорий |
| `stats.aov` | float | Средний чек (completed) |
| `stats.conversionRate` | float % (1 знак) | Купившие / все пользователи × 100 |
| `stats.completedPurchases` / `pendingPurchases` / `cancelledPurchases` | int | Сплит по статусам |
| `chartData.days` | string[] | Последние 7 дат `YYYY-MM-DD` |
| `chartData.days30` | string[] | Последние 30 дат |
| `chartData.revenueData` | number[] | Выручка по дням (7д) |
| `chartData.revenueData30` | number[] | Выручка по дням (30д) |
| `chartData.usersData` | number[] | Новые пользователи по дням (7д) |
| `chartData.topProducts` | `{name, qty, revenue}[]` | Топ-5 товаров за 30д (по кол-ву) |
| `chartData.topSpenders` | `{username, spent}[]` | Топ-5 покупателей за 30д |
| `chartData.revenueByCategory` | `{name, revenue}[]` | Выручка по категориям за 30д |
| `chartData.topCountries` | `{country, count}[]` | Топ-5 стран из locations |
| `activities` | `{type, id, username, item, total_price, date}` (или `{type:'audit', ...}`) | Последние 10 completed-покупок; если пусто — последние 10 audit_log |
| `wallets` | `{wallet_type, count, total}[]` | Сводка кошельков по валютам (верхний регистр) |
| `commission` | `{enabled, rate, due, currentCommission, lastPaidAmount, totalBalance}` | SaaS-комиссия: начислено/оплачено/к оплате |
### 6.3. Catalog (роут: `routes/catalog.js` — дерево; `routes/catalogProducts.js` — товары)
Дерево: Страна → Город → Район(location) → Категории → Подкатегории.
| Метод | Путь | Body / Param | Назначение |
|---|---|---|---|
| GET | `/catalog` | query: `?loc=`, `?cat=`, `?sub=` (фильтры), `?msg=&msg_type=` (флеш) | EJS `catalog` + `treeHtml` + `products` |
| POST | `/catalog/locations` | `country, city, district?` | + локация |
| POST | `/catalog/locations/add-city` | `country, city` | + город (district='') |
| POST | `/catalog/locations/add-district` | `country, city, district` | + район |
| POST | `/catalog/locations/:id/update` | `country, city, district` | Переименование |
| POST | `/catalog/locations/:id/toggle` | — | Вкл/выкл `is_active` |
| POST | `/catalog/locations/:id/delete` | — | Удаление (запрещено при наличии категорий/товаров) |
| POST | `/catalog/categories` | `name, location_id` | + категория |
| POST | `/catalog/categories/json` | JSON `{name, location_id}` | + категория (AJAX, возвращает объект категории) |
| POST | `/catalog/categories/:id/update` | `name, location_id?` | Переименование/перенос |
| POST | `/catalog/categories/:id/toggle` | — | Вкл/выкл |
| POST | `/catalog/categories/:id/delete` | — | Удаление (блокируется при товарах) |
| POST | `/catalog/categories/:id/subcategories` | `name` | + подкатегория |
| POST | `/catalog/categories/:id/subcategories/json` | JSON `{name}` | + подкатегория (AJAX) |
| POST | `/catalog/subcategories/:id/update` | `name` | Переименование |
| POST | `/catalog/subcategories/:id/toggle` | — | Вкл/выкл |
| POST | `/catalog/subcategories/:id/delete` | — | Удаление (блокируется при товарах) |
| POST | `/catalog/products` | multipart: поля товара + `photo_file`, `hidden_photo_file` | + товар |
| POST | `/catalog/products/:id/edit` | multipart: поля товара + файлы | Обновление |
| POST | `/catalog/products/:id/delete` | — | Удаление товара |
| GET | `/catalog/products/:id/json` | — | **AJAX**: товар с `country, city, district, category_name, subcategory_name` |
**Поля формы товара** (обязательные: `name, price>0, description, photo`):
`name, price, quantity_in_stock, description, photo_url, hidden_photo_url, hidden_coordinates, hidden_description, private_data, category_id, subcategory_id?, location_id?, is_mono`
Логика:
- `is_mono=1``quantity_in_stock` принудительно `999999`.
- Если `location_id` не передан — берётся `location_id` категории.
- Файл заменяет URL (`photo_url`), если загружен.
- JSON-ответы: `{error: '...'}` при 400.
### 6.4. Products (роут: `routes/products.js` — упрощённый, дублирует catalog)
| Метод | Путь | Назначение |
|---|---|---|
| GET | `/products` | EJS `products` (таблица до 100 товаров) |
| POST | `/products` | + товар (без файлов; `photo_url` обязателен) |
| GET | `/products/:id/edit` | EJS `product-edit` |
| POST | `/products/:id/update` | Обновление |
| POST | `/products/:id/delete` | Удаление |
> **Рекомендация фронту**: считать **catalog** основным экраном товаров; `/products` — legacy-дубль.
### 6.5. Categories (роут: `routes/categories.js` — плоские таблицы)
| Метод | Путь | Назначение |
|---|---|---|
| GET | `/categories` | EJS `categories` + `subcategoriesByCategory`, флеши `?error=`, `?success=` |
| POST | `/categories` | + категория (`name, location_id`) |
| POST | `/categories/:id/update` | `name, location_id` |
| POST | `/categories/:id/toggle` | Вкл/выкл |
| POST | `/categories/:id/delete` | Удаление (блок при товарах; удаляет и подкатегории) |
| POST | `/categories/:id/subcategories` | + подкатегория |
| POST | `/categories/subcategories/:id/update` | `name` |
| POST | `/categories/subcategories/:id/toggle` | Вкл/выкл |
| POST | `/categories/subcategories/:id/delete` | Удаление (блок при товарах) |
### 6.6. Locations (роут: `routes/locations.js`)
| Метод | Путь | Назначение |
|---|---|---|
| GET | `/locations` | EJS `locations` с `category_count`, `product_count` |
| POST | `/locations` | + локация |
| POST | `/locations/:id/update` | `country, city, district` |
| POST | `/locations/:id/toggle` | Вкл/выкл |
| POST | `/locations/:id/delete` | Блок при категориях/товарах |
### 6.7. Users (роут: `routes/users.js`)
| Метод | Путь | Назначение |
|---|---|---|
| GET | `/users` | EJS `users` (последние 100, `ORDER BY id DESC`) |
| GET | `/users/:id` | EJS `user-detail` + последние 20 покупок с `product_name` |
| POST | `/users/:id/toggle-status` | Бан/разбан (`status` 0↔2) |
| POST | `/users/:id/adjust-balance` | `amount, currency` (`total_balance`|`bonus_balance`); пишет audit `balance_adjust`; редирект на `/users/:id` |
### 6.8. Wallets (роут: `routes/wallets.js`) 🔓 для seed-части
| Метод | Путь | Назначение |
|---|---|---|
| GET | `/wallets` | EJS `wallets`; query `?user=`, `?seeds=1`; полный срез: `users` (с `wallet_count`), `wallets` выбранного юзера, `stats` (см. ниже) |
| POST | `/wallets/record-payment` | `paid_amount, note` → запись в `commission_payments`; редирект `/wallets?payment=recorded` |
| POST | `/wallets/export-seeds` 🔓 | CSV всех кошельков с расшифрованными seed-фразами; audit `csv_seed_export` |
| GET | `/wallets/refresh-balances/:userId` | **AJAX**: обновить балансы с блокчейна; ответ JSON см. ниже |
| GET | `/wallets/seed/:walletId` 🔓 | **AJAX**: расшифрованная seed-фраза + derivation; audit `seed_phrase_viewed` |
| GET | `/wallets/seed-qr/:walletId` 🔓 | **AJAX**: PNG QR-код seed-фразы (image/png); audit `seed_phrase_qr_viewed` |
**Формат `stats`** (передаётся в шаблон wallets):
| Поле | Тип | Описание |
|---|---|---|
| `totals` | `{btc,ltc,eth,usdt,usdc}` | Суммы балансов по валютам |
| `walletCounts` | `{...}` | Кол-во кошельков по валютам |
| `usdValues` | `{...}` | USD-оценка по валютам (курсы с CoinGecko) |
| `totalUsd` | float | Итог в USD |
| `prices` | `{btc,ltc,eth}` | Текущие курсы |
| `totalWallets` | int | Активных кошельков |
| `commissionRate` | float | % |
| `currentCommission` | float | Ставка × totalUsd |
| `lastPaidAmount` | float | Последняя оплата |
| `commissionDue` | float | max(0, current last) |
| `commissionEnabled` | bool | |
| `commissionWallets` | `{BTC,LTC,USDT,USDC,ETH}` | Адреса для оплаты комиссии |
| `totalUsers` | int | |
| `payments` | `commission_payments[]` | Последние 20 платежей |
| `seedsPaid` | bool | lastPaid ≥ current |
**Формат `refresh-balances` (JSON)**:
```json
{
"wallets": [{ "id", "wallet_type", "address", "balance", "usdValue", "created_at" }],
"totalBalance": 123.45,
"balances": { "BTC": { "amount", "usdValue" }, ... },
"timestamp": "ISO",
"error": "Blockchain API unavailable — showing cached balances" // опционально
}
```
**Формат `seed/:walletId` (JSON)**: `{ walletId, walletType, address, derivationPath, mnemonic, userId, username }` (404 `{error}` при отсутствии).
**Логика разблокировки seed**: кнопка «Unlock» доступна, только если `seedsPaid` (комиссия оплачена). Параметр `?seeds=1` открывает таблицу всех seed-фраз.
### 6.9. Purchases (роут: `routes/purchases.js`)
| Метод | Путь | Назначение |
|---|---|---|
| GET | `/purchases` | EJS `purchases` (последние 200, JOIN product_name) |
### 6.10. Audit (роут: `routes/audit.js`)
| Метод | Путь | Назначение |
|---|---|---|
| GET | `/audit` | EJS `audit` (последние 200 записей `audit_log`) |
### 6.11. Settings (роут: `routes/settings.js`)
| Метод | Путь | Назначение |
|---|---|---|
| GET | `/settings` | EJS `settings`; флеши `?saved=1`, `?error=1` |
| POST | `/settings` | Обновление `.env` |
**Редактируемые ключи** (только они; остальные строки `.env` не трогаются):
`BOT_TOKEN, SUPPORT_LINK, ADMIN_IDS, SUPER_ADMIN_IDS, WG_ENABLED, WG_ENDPOINT, WG_ADDRESS, WG_PUBLIC_KEY, WG_DNS, ADMIN_PORT, ADMIN_URL, CATALOG_PATH, GITEA_API_URL`
**Никогда не перезаписываются** (показываются как `•••••••`): `ENCRYPTION_KEY, ADMIN_SECRET, GITEA_TOKEN, WG_PRIVATE_KEY, WG_PRESHARED_KEY, COMMISSION_*`.
⚠️ Изменения применяются только **после рестарта контейнера** (сообщение об этом на странице).
### 6.12. Payment Wallets (роут: `routes/paymentWallets.js`)
| Метод | Путь | Назначение |
|---|---|---|
| GET | `/payment-wallets` | EJS `payment-wallets`**только чтение** конфига `COMMISSION_WALLETS` (адреса для уплаты комиссии владельцу) |
### 6.13. Seed & Reset (роут: `routes/seed.js`) ⚠️ requireReAuth
| Метод | Путь | Назначение |
|---|---|---|
| GET | `/seed` | EJS `seed` (форма с подтверждением токеном) |
| POST | `/seed/seed-demo` | **Полная очистка БД + заливка демо-данных**. Body: `reauth_token` |
| POST | `/seed/clear-all` | **Полная очистка всех таблиц**. Body: `reauth_token` |
Очищаемые таблицы: `purchases, transactions, crypto_wallets, audit_log, user_states, products, subcategories, categories, users, locations, commission_payments` + сброс `sqlite_sequence`.
### 6.14. Locales (роут: `routes/locales.js`)
| Метод | Путь | Назначение |
|---|---|---|
| GET | `/locales` | EJS `locales` + `sections` (ключи верхнего уровня из `en.json`) |
| POST | `/locales/save` | JSON `{lang, key, value}` (точечный путь, напр. `menu.shop`); запись в `src/i18n/locales/{lang}.json`; 400 при неверном `lang` |
Языки: `en`, `es`, `de` (`AVAILABLE_LANGUAGES`). Структура файлов — вложенные объекты; ключ в запросе — dot-path.
---
## 7. Спецификация экранов
### 7.1. Login (`/login`)
- Одно поле `token` (пароль-токен), кнопка Submit.
- Ошибки: `401 Invalid token`, `429 Too many attempts...` (15 мин).
- После успеха — redirect `/`.
- Подсказка: если `SUPER_ADMIN_SECRET` задан — вход с ним даёт роль super_admin.
### 7.2. Dashboard (`/`)
KPI-карточки:
- Пользователи всего, Товары, Покупки всего, Выручка ($, completed), Средний чек ($), Конверсия (%), Сплит по статусам (completed/pending/cancelled), Подкатегории.
Графики (данные готовы в `chartData`):
1. Выручка 7 дней (line/area) — `days` × `revenueData`.
2. Выручка 30 дней (line/area) — `days30` × `revenueData30`.
3. Новые пользователи 7 дней (bar) — `days` × `usersData`.
4. Топ-5 товаров (bar) — `topProducts`.
5. Топ-5 покупателей (bar) — `topSpenders`.
6. Выручка по категориям (pie/donut) — `revenueByCategory`.
7. Топ-5 стран (bar) — `topCountries`.
Блоки: Активность (последние покупки/аудит), Сводка кошельков по валютам, SaaS-комиссия (enabled/rate/due/последняя оплата).
### 7.3. Catalog (`/catalog`) — самый сложный экран
Две колонки:
- **Слева — дерево**: accordion Страна → Город → Район → Категория → Подкатегория. У каждого узла:
- счётчик товаров (включая подкатегории),
- бейдж «Disabled» у неактивных,
- inline-формы: переименовать, вкл/выкл (⏻), добавить ребёнка, удалить (✕, с confirm).
- **Справа — таблица товаров** (до 200): ID, фото (40×40), название, категория, подкатегория, цена $, остаток (∞ для mono), действия: ✎ Edit (модалка), ✕ Delete (confirm).
- Фильтры через клик по узлам дерева (`?loc=`, `?cat=`, `?sub=`).
- **Модалка товара** (Add/Edit): каскадные селекты Страна→Город→Район→(Категория фильтруется по району)→Подкатегория; inline-создание категории/подкатегории через `fetch` к `/json`-эндпоинтам; чекбокс «Digital Product (infinite stock)» блокирует поле остатка; загрузка 2 фото (public, hidden) или URL; поля скрытого контента (фото, координаты, описание) и private_data.
- Флеш-сообщения: `?msg=` + `?msg_type=` (success/error/info).
### 7.4. Products (`/products`) — legacy
Простая таблица (ID, фото, название, категория, цена, остаток, действия) + формы Add/Edit на отдельных страницах. **Фронту: экран можно заменить ссылкой на Catalog.**
### 7.5. Categories (`/categories`)
Плоские таблицы: категории (с локацией и product_count) + подкатегории (сгруппированы по категориям). Действия: add/edit/toggle/delete. Ошибки через `?error=` (с счётчиком блокирующих товаров), успех через `?success=`.
### 7.6. Locations (`/locations`)
Таблица: Страна/Город/Район, category_count, product_count, is_active, действия add/edit/toggle/delete. Ошибки/успехи как у Categories. Заблокированное удаление — с подсказкой «Remove categories/products first».
### 7.7. Users (`/users`, `/users/:id`)
- Список (100): ID, Telegram ID, username, страна, город, статус (badge Active/Blocked/Deleted), баланс $, кнопки View и Ban/Unban.
- Детальная (`/users/:id`): профиль + таблица покупок (20) + **форма корректировки баланса** (amount + выбор `total_balance`/`bonus_balance`) + audit-запись.
### 7.8. Wallets (`/wallets`)
- Слева: поиск + список пользователей (статус, число кошельков), выбор `?user=`.
- Справа: карточка балансов выбранного (Main/Bonus/Available/Status), таблица кошельков (тип, адрес — клик=копировать, баланс 8 знаков, дата), кнопка «Refresh balances» (fetch `/wallets/refresh-balances/:userId`).
- **Owner Summary** (для всех): Total USD, Users, Active Wallets, Commission Due; таблица «Balances by Currency»; блок комиссии: rate, total, full commission, last paid, due, форма «Record Payment» (paid_amount, note), адреса для оплаты (клик=копировать); таблица Payment History (delta между платежами).
- **Seed Phrases** (super_admin, если комиссия оплачена): кнопка «Unlock», таблица всех seed-фраз (user, type, address, derivation, mnemonic — клик=копировать), кнопка «Export All Seeds as CSV» (`/wallets/export-seeds`). Если не оплачено — предупреждение и заблокированная кнопка.
### 7.9. Purchases (`/purchases`)
Таблица (200): ID, user (ссылка), product, qty, цена, валюта, дата, статус (badge completed/pending/failed). **Нет фильтров и действий — только просмотр.**
### 7.10. Audit (`/audit`)
Таблица (200): action, admin_id, details (JSON-строка), created_at. Только просмотр.
### 7.11. Settings (`/settings`)
Форма с секциями:
- Bot: `BOT_TOKEN` (placeholder-маска), `SUPPORT_LINK`, `ADMIN_IDS`, `SUPER_ADMIN_IDS`.
- WireGuard: `WG_ENABLED` (checkbox), `WG_ENDPOINT`, `WG_ADDRESS`, `WG_PUBLIC_KEY`, `WG_DNS`.
- Admin: `ADMIN_PORT`, `ADMIN_URL`, `CATALOG_PATH`, `GITEA_API_URL`.
- Только для чтения (маскированные `•••••••`): `ENCRYPTION_KEY`, `ADMIN_SECRET`, `GITEA_TOKEN`, `WG_PRIVATE_KEY`, `WG_PRESHARED_KEY`, `COMMISSION_*`.
- Предупреждение: «Restart the application to apply changes».
### 7.12. Payment Wallets (`/payment-wallets`)
Таблица адресов комиссии (BTC/LTC/USDT/USDC/ETH) — только чтение; ссылка на Settings.
### 7.13. Locales (`/locales`)
- Таблица: строки = ключи из `en.json` (секции-заголовки), колонки = языки en/es/de.
- Редактирование значения: inline (fetch POST `/locales/save` `{lang, key, value}`).
- Требование: 3 языка всегда синхронны по набору ключей; добавление ключа в одном — добавить в остальных (200+ ключей).
### 7.14. Seed & Reset (`/seed`)
Две опасные кнопки: «Seed Demo Data» и «Clear All Data». Обе требуют повторный ввод токена (`reauth_token`). В UI — двойное подтверждение (модалка + поле токена), предупреждение о необратимости.
---
## 8. Роли и права
| Возможность | admin | super_admin |
|---|---|---|
| Все страницы, кроме seed-выгрузки | ✅ | ✅ |
| Просмотр/экспорт seed-фраз (`/wallets/seed*`, `export-seeds`) | ❌ (401/403) | ✅ |
| Запись комиссионных платежей | ✅ (форма на /wallets) | ✅ |
| Seed & Reset (`/seed/*`) | ✅ (с reauth) | ✅ (с reauth) |
Middleware: `requireAuth` (все роуты после `/login`), `requireSuperAuth` (seed-эндпоинты), `requireReAuth` (seed/reset).
---
## 9. Формат данных
- Деньги: USD, `REAL`. Выводить `$X.XX`.
- Крипто-балансы: до 8 знаков, обрезать хвостовые нули.
- Даты: `DATETIME` SQLite (`YYYY-MM-DD HH:MM:SS`, UTC). На клиенте конвертировать в локальное время.
- Фото: абсолютный путь (например `/uploads/...`) или внешний URL. В таблицах — превью 4050px.
- Status пользователя: `0` активен, `2` заблокирован, иное — удалён.
- Status покупки: `pending` | `completed` | `cancelled`.
- `is_mono`: `1` = цифровой (бесконечный остаток, показывать ∞).
- Флеш-сообщения: через query-параметры (`?msg=`, `?error=`, `?success=`, `?seeded=1`, `?cleared=1`, `?saved=1`, `?payment=recorded`).
---
## 10. Существующие ограничения и подводные камни
1. **Нет REST API.** Всё — SSR EJS + формы + редкие fetch. Для полноценного SPA бэкенду нужны JSON-эндпоинты (предложены в §12) — **отдел фронтенда не должен проектировать свой API без согласования с бэкендом**.
2. **CSRF отключён** (заглушка). Новая админка не должна полагаться на Origin/SameSite; если оставляем формы — обязательны серверные токены или хотя бы подтверждения на деструктив.
3. **Работа через Tor**: нельзя полагаться на внешние CDN (Bootstrap/FontAwesome/ApexCharts должны быть локальными), нельзя требовать HTTPS/WSS, куки `Secure` не работают.
4. **Нет пагинации** нигде (LIMIT 100/200 фиксирован). При росте данных списки «режутся» — фронт может предложить пагинацию, но это потребует новых query-параметров на бэкенде.
5. **Дублирование каталога**: `/catalog` (богатый) и `/products` (бедный) — менять оба или убрать один (решение за продуктом).
6. **Фото из uploads отдаются с `Content-Disposition: attachment`** — их нельзя показывать `<img>` inline из `/uploads/` (скачаются как файл). В таблицах товаров фото берутся из `photo_url`, который может указывать на внешний URL или `/uploads/...` — при `/uploads` превью не отобразится в браузере. Для галереи товаров нужно либо отдельный inline-эндпоинт, либо хранить публичные фото вне uploads (например, статика `public/img/products/`).
7. **Нет нотификаций/вебхуков** — dashboard статичен до перезагрузки.
8. **Settings перезаписывают `.env`** — изменения требуют рестарта; форма должна явно это сообщать.
9. **Seed/Reset опасны** — полная очистка БД; в новом UI обязателен confirm + reauth (уже реализовано на бэкенде).
10. **i18n-файлы — живые данные** админки; редактирование без синхронизации ключей ломает бота (fallback на `en`).
11. Legacy JS-слой (`views/*.js`) частично дублирует EJS — **не использовать как источник истины**, ссылаться на роуты и EJS.
---
## 11. Требования к новому фронтенду
### 11.1. Целевой стек (рекомендация)
- SPA на Vue 3 или React (или продолжение SSR EJS, если команда не готова к SPA).
- Все ассеты **локальные** (сборка в `public/dist`), без CDN — совместимость с Tor.
- Адаптивность: админкой пользуются с десктопа и планшета.
- Тёмная/светлая тема (SmartAdmin уже имеет темы — можно переиспользовать SCSS-переменные).
- Charts: ApexCharts (уже есть в проекте) или ECharts.
### 11.2. Обязательные экраны (приоритет)
1. **Dashboard** — KPI + 7 графиков (данные в §6.2).
2. **Catalog** — дерево + таблица товаров + модалка товара с каскадными селектами и загрузкой 2 фото.
3. **Users + User detail** — список, бан/разбан, корректировка баланса.
4. **Wallets** — пользователь + кошельки + Owner Summary + комиссия + seed (super_admin).
5. **Purchases** — таблица со статусами.
6. **Audit** — журнал.
7. **Categories / Locations** — CRUD + toggle.
8. **Settings** — форма .env с масками секретов.
9. **Locales** — редактор переводов на 3 языка.
10. **Seed & Reset** — подтверждённая деструктивная панель.
11. **Login / Logout**.
### 11.3. UI/UX-требования
- Все деструктивные действия — confirm-модалка (не `window.confirm`).
- Все флеш-сообщения — toast/alert с автоскрытием.
- Каскадные select'ы — с загрузкой и disabled-состояниями.
- Копирование адресов/seed — по клику с визуальной обратной связью «Copied!».
- Пустые состояния («No products found», «No users», «No wallets yet»).
- Загрузочные спиннеры на fetch-запросах.
- Ошибки API (400/401/403/404/500) — понятные сообщения пользователю.
- Валидация форм на клиенте (price>0, name required, photo required, amount>0) + серверная.
- Хлебные крошки, поиск по меню (уже есть в SmartAdmin).
- Индикация роли (admin/super_admin) в шапке; скрывать seed-раздел для обычных админов.
### 11.4. Интеграционные требования
- Авторизация: работа с кукой `admin_token`; при 401/403 от `/wallets/seed*` — редирект на `/login` или сообщение о недостатке прав.
- Все POST-формы в SPA отправлять с `Content-Type: application/x-www-form-urlencoded` или `multipart/form-data` в зависимости от бэкенд-роута (см. §6).
- Не вводить собственный роутинг API-путей — строго использовать карту §6 (или согласованные новые из §12).
- Реализовать обработку флеш-query-параметров на клиенте.
### 11.5. Что НЕ делать
- Не разворачивать отдельный фронтенд-сервер (админка живёт в контейнере бота; статика отдаётся из `src/admin/public`).
- Не менять схему БД без миграций и без бэкенда.
- Не показывать seed-фразы не-super_admin.
- Не загружать фото в произвольные пути — только через существующий multipart-роут `/catalog/products` (+ file input) или `/products`.
---
## 12. API-контракты JSON (дополнить бэкенду)
Для SPA-режима предлагаются **новые** JSON-эндпоинты (требуют реализации на бэкенде — вне зоны ответственности фронта, но контракты фиксируем заранее). Все — под `requireAuth`, префикс `/api`.
| Метод | Путь | Ответ |
|---|---|---|
| GET | `/api/stats` | Агрегат дашборда (поля из §6.2) |
| GET | `/api/users?search=&page=&limit=` | Пагинированный список пользователей |
| GET | `/api/users/:id` | Профиль + балансы |
| POST | `/api/users/:id/toggle-status` | `{ok:true}` |
| POST | `/api/users/:id/adjust-balance` | body `{amount, currency}``{ok:true, newBalance}` |
| GET | `/api/catalog/tree` | Дерево (страна→город→район→категория→подкатегория) с is_active и счётчиками |
| GET | `/api/products?loc=&cat=&sub=&page=&limit=` | Пагинированный список товаров |
| GET | `/api/products/:id` | Товар целиком (вкл. скрытый контент и private_data) |
| POST | `/api/products` | Создание (JSON или multipart) |
| PUT | `/api/products/:id` | Обновление |
| DELETE | `/api/products/:id` | Удаление |
| GET | `/api/locations` | Список с counts |
| POST/PUT/DELETE | `/api/locations...` | CRUD |
| GET | `/api/categories` / `/api/subcategories` | Списки с counts |
| POST/PUT/DELETE | `/api/categories...` | CRUD |
| GET | `/api/purchases?status=&page=&limit=` | Пагинированные покупки с фильтром по статусу |
| PATCH | `/api/purchases/:id/status` | Смена статуса (требует согласования с ботом — статусы влияют на доставку контента!) |
| GET | `/api/wallets/overview` | Owner summary + комиссия + платежи |
| GET | `/api/wallets?userId=` | Кошельки пользователя |
| POST | `/api/wallets/refresh` | body `{userId}` → свежие балансы |
| POST | `/api/wallets/record-payment` | body `{paidAmount, note}` |
| GET | `/api/wallets/seeds` 🔓 | Список seed-фраз |
| GET | `/api/wallets/seeds/:id` 🔓 | Одна seed-фраза |
| GET | `/api/wallets/seeds/:id/qr` 🔓 | PNG QR |
| GET | `/api/audit?page=&limit=` | Пагинированный аудит |
| GET/POST | `/api/locales` | Чтение/сохранение ключей (как `/locales/save`) |
| GET/PUT | `/api/settings` | Чтение маскированных / запись разрешённых ключей |
| POST | `/api/seed/demo`, `/api/seed/clear` ⚠️ | с `reauth_token` |
| GET | `/api/session` | Текущая роль (admin/super_admin) для условного рендера |
⚠️ **Важно**: PATCH статуса покупки должен быть синхронизирован с `purchaseService` (доставка скрытого контента происходит при `completed`).
---
## 13. Чек-лист приёмки
- [ ] Логин/логаут работают; роль super_admin корректно отображается.
- [ ] Dashboard: все KPI и 7 графиков рендерятся без ошибок.
- [ ] Catalog: дерево полностью управляемо (add/rename/toggle/delete на всех 4 уровнях), товары CRUD с загрузкой 2 фото, каскадные селекты, mono-флаг.
- [ ] Users: список, детальная, бан/разбан, корректировка баланса с audit.
- [ ] Wallets: выбор пользователя, обновление балансов (fetch), Owner Summary, запись комиссии, история платежей, seed-блок с правами и QR/CSV.
- [ ] Purchases: таблица со статусами и ссылками на пользователей.
- [ ] Audit: журнал читается, details парсится.
- [ ] Categories/Locations: CRUD с блокировками удаления и понятными ошибками.
- [ ] Settings: сохраняются только разрешённые ключи; секреты маскированы.
- [ ] Locales: inline-редактирование всех 3 языков, dot-path ключей.
- [ ] Seed & Reset: двойное подтверждение, reauth-токен, предупреждения.
- [ ] Все деструктивные действия с confirm; все флеши; все пустые состояния.
- [ ] Работает через Tor (без CDN-зависимостей), тёмная тема.
- [ ] Нет утечек seed/private_data/масок в DOM для не-super_admin.
- [ ] Нет XSS: все данные экранированы на клиенте (в т.ч. из `JSON` в `<script>`).
---
*Документ подготовлен на основе фактического кода: `src/admin/server.js`, `src/admin/routes/*.js`, `src/admin/auth.js`, `src/admin/views/*.ejs`, `src/migrations/*.js`, `src/config/*.js`, `src/services/*.js`. При изменении бэкенда — перегенерировать разделы 67.*