docs(admin): frontend spec for admin panel + endpoint architecture (issue #131)
This commit is contained in:
750
docs/admin-frontend-spec.md
Normal file
750
docs/admin-frontend-spec.md
Normal file
@@ -0,0 +1,750 @@
|
||||
# Админ-панель 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` (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`|`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. В таблицах — превью 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. Существующие ограничения и подводные камни
|
||||
|
||||
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`. При изменении бэкенда — перегенерировать разделы 6–7.*
|
||||
Reference in New Issue
Block a user