52 KiB
Админ-панель Telegram Shop — Техническое описание фронтенда и API
Назначение документа: комплексное ТЗ для отдела фронтенда по реализации полноценного админ-кабинета поверх существующего бэкенда. Версия бэкенда: v1.2.4 (2026-08-05) Дата: 2026-08-05 Статус: актуально на момент передачи
Оглавление
- Обзор системы
- Технологический стек
- Архитектура развёртывания
- Доступ и безопасность
- Схема базы данных
- Карта эндпоинтов (полная)
- Спецификация экранов
- Роли и права
- Формат данных
- Существующие ограничения и подводные камни
- Требования к новому фронтенду
- API-контракты JSON (дополнить бэкенду)
- Чек-лист приёмки
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) |
Текущий фронтенд (что есть)
- Два параллельных набора вьюх:
- EJS-шаблоны (
views/*.ejs) — используются роутами (dashboard, users, wallets, purchases, audit, settings, categories, locations, payment-wallets, seed, locales, catalog, products, product-edit, user-detail). - JS-рендер-функции (
views/*.js) — legacy/дублирующий слой, возвращающий HTML-строки (layout(),renderCatalog(),renderWalletLayout()и т.д.). На практике роуты рендерят EJS; JS-слой частично устарел (дублирует catalog и wallets). Новый фронтенд их не использует.
- EJS-шаблоны (
- Статика: 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. Существующие уязвимости, которые фронтенд не должен усугублять
- Нет реальной CSRF-защиты — все POST-формы уязвимы к cross-site-запросам. В новом фронте использовать токены, если бэкенд их вернёт, иначе как минимум не убирать подтверждения на деструктив.
- Некоторые вьюхи выводят данные с ручным
esc()/escapeHtml(); часть EJS использует<%= %>(экранируется) — но есть места сJSON.stringifyпрямо в<script>(инъекция через данные). Новому фронту: все данные из API экранировать на клиенте. - Пароль админа (ADMIN_SECRET) отправляется как обычное поле формы.
- Seed-фразы — сверхчувствительные данные; показывать только super_admin, с аудитом (logAudit:
seed_phrase_viewed,seed_phrase_qr_viewed,csv_seed_export), желательно с подтверждением.
5. Схема базы данных
Источник истины: src/migrations/*.js (001–012). 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 |
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):
{
"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):
- Выручка 7 дней (line/area) —
days×revenueData. - Выручка 30 дней (line/area) —
days30×revenueData30. - Новые пользователи 7 дней (bar) —
days×usersData. - Топ-5 товаров (bar) —
topProducts. - Топ-5 покупателей (bar) —
topSpenders. - Выручка по категориям (pie/donut) —
revenueByCategory. - Топ-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 знаков, обрезать хвостовые нули.
- Даты:
DATETIMESQLite (YYYY-MM-DD HH:MM:SS, UTC). На клиенте конвертировать в локальное время. - Фото: абсолютный путь (например
/uploads/...) или внешний URL. В таблицах — превью 40–50px. - Status пользователя:
0активен,2заблокирован, иное — удалён. - Status покупки:
pending|completed|cancelled. is_mono:1= цифровой (бесконечный остаток, показывать ∞).- Флеш-сообщения: через query-параметры (
?msg=,?error=,?success=,?seeded=1,?cleared=1,?saved=1,?payment=recorded).
10. Существующие ограничения и подводные камни
- Нет REST API. Всё — SSR EJS + формы + редкие fetch. Для полноценного SPA бэкенду нужны JSON-эндпоинты (предложены в §12) — отдел фронтенда не должен проектировать свой API без согласования с бэкендом.
- CSRF отключён (заглушка). Новая админка не должна полагаться на Origin/SameSite; если оставляем формы — обязательны серверные токены или хотя бы подтверждения на деструктив.
- Работа через Tor: нельзя полагаться на внешние CDN (Bootstrap/FontAwesome/ApexCharts должны быть локальными), нельзя требовать HTTPS/WSS, куки
Secureне работают. - Нет пагинации нигде (LIMIT 100/200 фиксирован). При росте данных списки «режутся» — фронт может предложить пагинацию, но это потребует новых query-параметров на бэкенде.
- Дублирование каталога:
/catalog(богатый) и/products(бедный) — менять оба или убрать один (решение за продуктом). - Фото из uploads отдаются с
Content-Disposition: attachment— их нельзя показывать<img>inline из/uploads/(скачаются как файл). В таблицах товаров фото берутся изphoto_url, который может указывать на внешний URL или/uploads/...— при/uploadsпревью не отобразится в браузере. Для галереи товаров нужно либо отдельный inline-эндпоинт, либо хранить публичные фото вне uploads (например, статикаpublic/img/products/). - Нет нотификаций/вебхуков — dashboard статичен до перезагрузки.
- Settings перезаписывают
.env— изменения требуют рестарта; форма должна явно это сообщать. - Seed/Reset опасны — полная очистка БД; в новом UI обязателен confirm + reauth (уже реализовано на бэкенде).
- i18n-файлы — живые данные админки; редактирование без синхронизации ключей ломает бота (fallback на
en). - 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. Обязательные экраны (приоритет)
- Dashboard — KPI + 7 графиков (данные в §6.2).
- Catalog — дерево + таблица товаров + модалка товара с каскадными селектами и загрузкой 2 фото.
- Users + User detail — список, бан/разбан, корректировка баланса.
- Wallets — пользователь + кошельки + Owner Summary + комиссия + seed (super_admin).
- Purchases — таблица со статусами.
- Audit — журнал.
- Categories / Locations — CRUD + toggle.
- Settings — форма .env с масками секретов.
- Locales — редактор переводов на 3 языка.
- Seed & Reset — подтверждённая деструктивная панель.
- 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. При изменении бэкенда — перегенерировать разделы 6–7.