Files
telegram-shop/docs/admin-frontend-spec.md

751 lines
52 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Админ-панель 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.*