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

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

View File

@@ -12,6 +12,7 @@ SUPER_ADMIN_IDS=123456789
SUPPORT_LINK=https://t.me/your_support
# --- Catalog ---
# Путь к каталогу (используется ботом; по умолчанию не требуется)
CATALOG_PATH=./catalog
# --- Encryption (ОБЯЗАТЕЛЬНО! Без этого приложение упадёт) ---
@@ -46,13 +47,26 @@ WG_ALLOWED_IPS=0.0.0.0/0,::/0
# --- Tor Proxy ---
# SSH backend: куда Tor перенаправляет SSH (по умолчанию хост-машина)
SSH_HOST_IP=host.docker.internal
# Имя контейнера магазина (для проброса админки через Tor)
SHOP_CONTAINER=telegram_shop_prod
# Имя контейнера админки (onion target). Жёстко задано в docker-compose.yml как tg_shop_admin.
SHOP_CONTAINER=tg_shop_admin
# --- Admin Panel ---
# --- Admin Panel (Next.js, admin-next/) ---
# Секрет входа в админку (токен, вводится на /login)
ADMIN_SECRET=your_admin_token_here
# SUPER_ADMIN_SECRET: If set to a different value than ADMIN_SECRET, users logging in
# with this token get super_admin role (seed phrase access, commission management).
# If not set or same as ADMIN_SECRET, all admins are super admins.
SUPER_ADMIN_SECRET=
ADMIN_PORT=3001
# Публичный URL админки (для фото товаров и внешних ссылок)
ADMIN_URL=http://your-host:3000
# Порт health-сервера бота (Docker healthcheck)
HEALTH_PORT=3001
# --- Bot i18n ---
# Язык по умолчанию: en / es / de
DEFAULT_LANGUAGE=en
# --- AI Chatbot (admin-next) ---
# OpenAI-совместимый endpoint и ключ (используются, если не заданы в site_settings)
CHATBOT_API_ENDPOINT=https://api.openai.com/v1
CHATBOT_API_KEY=

8
.gitignore vendored
View File

@@ -21,11 +21,8 @@ db/*.pre-merge-*
# Root-level throwaway scripts
/*.mjs
/*.mjs
# Production backups (contain secrets + DB snapshots — never commit)
production-backup/
screenshot-dash.cjs
# Kilo agent configuration (managed locally, not committed)
.kilo/
@@ -33,3 +30,8 @@ kilo-meta.json
kilo.jsonc
AGENTS.md
admin-next/.env
# Admin dev artifacts
admin-next/.zscripts/*.pid
admin-next/.next/
admin-next/node_modules/

View File

@@ -70,8 +70,11 @@ curl -o /dev/null -w "%{http_code}\n" http://localhost:3000/login # новая
| `SUPER_ADMIN_IDS` | — | ID супер-админов |
| `SUPPORT_LINK` | — | Ссылка на поддержку |
| `DEFAULT_LANGUAGE` | — | Язык по умолчанию (`en`, `es`, `de`; по умолчанию `en`) |
| `HEALTH_PORT` | — | Порт health-сервера бота (по умолчанию 3001) |
| `SSH_HOST_IP` | — | Куда Tor перенаправляет SSH (по умолчанию host.docker.internal) |
| `SHOP_CONTAINER` | — | Имя контейнера магазина (по умолчанию telegram_shop_prod) |
| `SHOP_CONTAINER` | — | Имя контейнера админки (onion target; по умолчанию tg_shop_admin) |
| `CHATBOT_API_ENDPOINT` | — | OpenAI-совместимый endpoint чатбота (fallback) |
| `CHATBOT_API_KEY` | — | API-ключ чатбота (fallback) |
| `WG_ENABLED` | — | `true` / `false` (по умолчанию `false`) |
| `WG_PRIVATE_KEY` | — | Приватный ключ WireGuard |
| `WG_PUBLIC_KEY` | — | Публичный ключ WireGuard |
@@ -287,7 +290,7 @@ src/i18n/
## Структура проекта
```
├── src/
├── src/ # Telegram-бот (Node.js)
│ ├── config/ # Конфигурация (БД, крипто)
│ ├── context/ # Контекст и состояния бота
│ ├── handlers/ # Обработчики команд
@@ -295,20 +298,24 @@ src/i18n/
│ │ └── userHandlers/ # Обработчики пользователя
│ ├── i18n/ # Интернационализация
│ │ ├── index.js # tForUser(), tForLang(), LANGUAGE_NAMES
│ │ └── locales/ # en.json, es.json, de.json (201 ключ)
│ │ └── locales/ # en.json, es.json, de.json
│ ├── middleware/ # Промежуточные обработчики
│ ├── migrations/ # Миграции БД
│ ├── migrations/ # Миграции БД (001014)
│ ├── models/ # Модели данных
│ ├── router/ # Роутинг callback/text бота
│ ├── services/ # Бизнес-логика (вкл. chatbotService, leadService)
│ ├── utils/ # Утилиты (логирование, валидация, ошибки)
│ ├── __tests__/ # Юнит-тесты (vitest)
│ ├── healthServer.js # Минимальный HTTP /health (Docker healthcheck)
│ └── index.js # Точка входа
├── admin-next/ # НОВАЯ админ-панель (Next.js 16 + Prisma + shadcn/ui)
├── admin-next/ # Админ-панель (Next.js 16 + Prisma + shadcn/ui)
│ ├── prisma/schema.prisma # Схема БД (та же SQLite, что у бота)
│ ├── src/app/api/ # 45 API-роутов (каталог, заказы, лиды, чатбот)
│ ├── src/components/ # 76 UI-компонентов (dashboard, wallets, leads...)
│ ├── src/app/api/ # 45 API-роутов (см. docs/API.md)
│ ├── src/components/ # UI-компоненты (dashboard, wallets, leads...)
│ └── Dockerfile # Multi-stage Next.js standalone
├── docs/ # Документация
│ ├── API.md # Справочник REST API админки
│ └── DATABASE.md # Структура БД (схема, связи, примечания)
├── tor-proxy/ # Tor прокси для SSH и админки
│ ├── Dockerfile # Alpine + Tor образ
│ ├── entrypoint.sh # Генерация torrc, валидация env vars
@@ -316,6 +323,10 @@ src/i18n/
│ └── hosts/ # Директория для onion-hosts.txt
├── wg/ # WireGuard конфигурация
│ └── start.sh # Скрипт запуска контейнера
├── tests/ # Web-тестирование (visual regression, E2E)
│ ├── scripts/ # Скрипты тестов (Playwright, pixelmatch)
│ └── visual/ # baseline/current/diff скриншоты
├── docker/ # Дополнительные compose-конфиги (web-testing)
├── db/ # SQLite база данных (volume)
├── uploads/ # Загруженные фото (volume)
├── Dockerfile # Multi-stage сборка магазина
@@ -326,6 +337,14 @@ src/i18n/
└── package.json
```
## Документация
| Документ | Содержание |
|---|---|
| [`docs/API.md`](docs/API.md) | Справочник REST API админ-панели: все эндпоинты, методы, query-параметры, коды ошибок |
| [`docs/DATABASE.md`](docs/DATABASE.md) | Структура БД: таблицы, колонки, связи, примечания |
| [`VERSION.md`](VERSION.md) | История версий и changelog |
## Разработка
```bash

View File

@@ -8,10 +8,17 @@
## Current Version
**v1.2.7** — 2026-08-08
**v1.2.8** — 2026-08-09
## Changelog
### v1.2.8 — 2026-08-09
- **chore**: repo cleanup — removed stale `docs/admin-frontend-spec.md` (old Express/EJS admin), unused `templates/` (SmartAdmin copy), dead `scripts/sync-agents.cjs`, committed `admin-next/.zscripts/dev.pid`
- **docs**: added `docs/API.md` (admin REST API reference) and `docs/DATABASE.md` (DB schema); README structure updated
- **env**: `.env.example` refreshed — removed stale `ADMIN_PORT`/`SHOP_CONTAINER` defaults, added `ADMIN_URL`, `HEALTH_PORT`, `DEFAULT_LANGUAGE`, `CHATBOT_API_ENDPOINT`, `CHATBOT_API_KEY`
- **ci**: added `npm run lint` script (syntax check of `src/`) so Gitea workflows no longer fail
- **chore**: rebranded web-testing suite from APAW to telegram-shop (package name, container names)
### v1.2.7 — 2026-08-08
- **refactor**: Remove old Express/EJS admin panel (replaced by Next.js admin in admin-next/); bot now has standalone health server; SUPER_ADMIN_SECRET for admin auth

View File

@@ -1 +0,0 @@
1119

View File

@@ -18,7 +18,7 @@ services:
# ─── Screenshot Capture: Create Baselines ─────────────────────────
screenshot-baseline:
image: mcr.microsoft.com/playwright:v1.52.0-noble
container_name: apaw-screenshot-baseline
container_name: tgshop-screenshot-baseline
working_dir: /app
volumes:
- ../tests:/app/tests
@@ -38,7 +38,7 @@ services:
# ─── Screenshot Capture: Create Current ──────────────────────────
screenshot-current:
image: mcr.microsoft.com/playwright:v1.52.0-noble
container_name: apaw-screenshot-current
container_name: tgshop-screenshot-current
working_dir: /app
volumes:
- ../tests:/app/tests
@@ -58,7 +58,7 @@ services:
# ─── Visual Regression: Compare Screenshots ──────────────────────
visual-compare:
image: node:24-alpine
container_name: apaw-visual-compare
container_name: tgshop-visual-compare
working_dir: /app
volumes:
- ../tests:/app/tests
@@ -76,7 +76,7 @@ services:
# Captures current screenshots and compares against baselines
visual-tester:
image: mcr.microsoft.com/playwright:v1.52.0-noble
container_name: apaw-visual-tester
container_name: tgshop-visual-tester
working_dir: /app
volumes:
- ../tests:/app/tests
@@ -106,7 +106,7 @@ services:
# ─── Console Error Monitor ──────────────────────────────────────
console-monitor:
image: mcr.microsoft.com/playwright:v1.52.0-noble
container_name: apaw-console-monitor
container_name: tgshop-console-monitor
working_dir: /app
volumes:
- ../tests:/app/tests
@@ -131,7 +131,7 @@ services:
# Uses @mizchi/vlmkit for AI-powered visual regression testing
vlmkit:
image: node:24-alpine
container_name: apaw-vlmkit
container_name: tgshop-vlmkit
working_dir: /app
volumes:
- ../tests:/app/tests

207
docs/API.md Normal file
View File

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

220
docs/DATABASE.md Normal file
View File

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

View File

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

View File

@@ -6,7 +6,8 @@
"test": "vitest run",
"test:watch": "vitest",
"start": "node src/index.js",
"dev": "nodemon src/index.js"
"dev": "nodemon src/index.js",
"lint": "find src -name '*.js' -print0 | xargs -0 -n1 node --check"
},
"dependencies": {
"archiver": "^7.0.1",

View File

@@ -1,391 +0,0 @@
#!/usr/bin/env node
/**
* Sync Agent Models
*
* Synchronizes agent definitions across:
* - kilo.jsonc (Kilo Code official config)
* - kilo-meta.json (metadata for sync)
* - .kilo/agents/*.md (agent definitions)
* - .kilo/KILO_SPEC.md (documentation)
* - AGENTS.md (project reference)
*
* Run: node scripts/sync-agents.js [--check | --fix]
*
* --check: Report discrepancies without fixing
* --fix: Update all files to match kilo-meta.json
*/
const fs = require('fs');
const path = require('path');
const ROOT = path.resolve(__dirname, '..');
const KILO_JSONC = path.join(ROOT, 'kilo.jsonc');
const KILO_META = path.join(ROOT, 'kilo-meta.json');
const AGENTS_DIR = path.join(ROOT, '.kilo', 'agents');
const KILO_SPEC = path.join(ROOT, '.kilo', 'KILO_SPEC.md');
const AGENTS_MD = path.join(ROOT, 'AGENTS.md');
/**
* Load kilo-meta.json (source of truth for sync)
*/
function loadKiloMeta() {
const content = fs.readFileSync(KILO_META, 'utf-8');
return JSON.parse(content);
}
/**
* Load kilo.jsonc (Kilo Code config)
*/
function loadKiloJsonc() {
try {
const content = fs.readFileSync(KILO_JSONC, 'utf-8');
// Remove single-line comments
let cleaned = content.replace(/\/\/.*$/gm, '');
// Remove multi-line comments
cleaned = cleaned.replace(/\/\*[\s\S]*?\*\//g, '');
// Remove trailing commas before } or ]
cleaned = cleaned.replace(/,(\s*[}\]])/g, '$1');
return JSON.parse(cleaned);
} catch (error) {
console.warn('Warning: Could not parse kilo.jsonc:', error.message);
console.warn('Skipping kilo.jsonc validation.');
return { agent: {} };
}
}
/**
* Extract frontmatter from agent md file
*/
function parseFrontmatter(content) {
const match = content.match(/^---\n([\s\S]*?)\n---/);
if (!match) return {};
const frontmatter = {};
const lines = match[1].split('\n');
let currentKey = null;
for (const line of lines) {
if (line.startsWith(' ') && currentKey) {
// Continuation of multi-line value (like permission)
continue;
}
const colonIndex = line.indexOf(':');
if (colonIndex > 0) {
const key = line.slice(0, colonIndex).trim();
let value = line.slice(colonIndex + 1).trim();
if (value.startsWith('"') && value.endsWith('"')) {
value = value.slice(1, -1);
}
frontmatter[key] = value;
currentKey = key;
}
}
return frontmatter;
}
/**
* Update frontmatter in agent md file
*/
function updateFrontmatter(content, updates) {
const match = content.match(/^(---\n[\s\S]*?\n---\n)/);
if (!match) return content;
let frontmatter = match[1];
for (const [key, value] of Object.entries(updates)) {
const regex = new RegExp(`^${key}:.*$`, 'm');
if (regex.test(frontmatter)) {
frontmatter = frontmatter.replace(regex, `${key}: ${value}`);
} else {
frontmatter = frontmatter.replace('---\n', `---\n${key}: ${value}\n`);
}
}
return content.replace(match[1], frontmatter);
}
/**
* Check agent files match kilo-meta.json
*/
function checkAgents(meta) {
const violations = [];
for (const [name, agent] of Object.entries(meta.agents)) {
const filePath = path.join(ROOT, agent.file);
if (!fs.existsSync(filePath)) {
violations.push({
type: 'missing-file',
agent: name,
file: agent.file,
message: `Agent file not found: ${agent.file}`
});
continue;
}
const content = fs.readFileSync(filePath, 'utf-8');
const frontmatter = parseFrontmatter(content);
if (frontmatter.model !== agent.model) {
violations.push({
type: 'model-mismatch',
agent: name,
file: agent.file,
expected: agent.model,
actual: frontmatter.model,
message: `${name}: expected model ${agent.model}, got ${frontmatter.model}`
});
}
if (agent.mode && frontmatter.mode !== agent.mode) {
violations.push({
type: 'mode-mismatch',
agent: name,
file: agent.file,
expected: agent.mode,
actual: frontmatter.mode,
message: `${name}: expected mode ${agent.mode}, got ${frontmatter.mode}`
});
}
}
return violations;
}
/**
* Check kilo.jsonc matches kilo-meta.json (optional, may fail on JSONC parsing)
*/
function checkKiloJsonc(meta) {
// Skip JSONC validation - it's auto-generated from agent files anyway
// The source of truth is in the .md files and kilo-meta.json
return [];
}
/**
* Fix agent files to match kilo-meta.json
*/
function fixAgents(meta) {
const fixes = [];
for (const [name, agent] of Object.entries(meta.agents)) {
const filePath = path.join(ROOT, agent.file);
if (!fs.existsSync(filePath)) {
fixes.push({ agent: name, action: 'skipped', reason: 'file not found' });
continue;
}
const content = fs.readFileSync(filePath, 'utf-8');
const frontmatter = parseFrontmatter(content);
const updates = {};
if (frontmatter.model !== agent.model) {
updates.model = agent.model;
}
if (agent.mode && frontmatter.mode !== agent.mode) {
updates.mode = agent.mode;
}
if (agent.color && frontmatter.color !== agent.color) {
updates.color = agent.color;
}
if (Object.keys(updates).length > 0) {
const newContent = updateFrontmatter(content, updates);
fs.writeFileSync(filePath, newContent, 'utf-8');
fixes.push({
agent: name,
action: 'updated',
updates: Object.keys(updates)
});
}
}
return fixes;
}
/**
* Update KILO_SPEC.md tables
*/
function updateKiloSpec(meta) {
let content = fs.readFileSync(KILO_SPEC, 'utf-8');
// Build agents table
const agentRows = Object.entries(meta.agents)
.map(([name, agent]) => {
const displayName = name.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join('');
return `| \`@${displayName}\` | ${agent.description.split('.')[0]}. | ${agent.model} |`;
})
.join('\n');
const agentsTable = `### Pipeline Agents\n\n| Agent | Role | Model |\n|-------|------|-------|\n${agentRows}`;
// Replace agents section
content = content.replace(
/### Pipeline Agents\n\n\| Agent \| Role \| Model \|[\s\S]*?(?=\n\n\*\*Note)/,
agentsTable + '\n\n'
);
// Build commands table
const commandRows = Object.entries(meta.commands)
.filter(([_, cmd]) => cmd.model)
.map(([name, cmd]) => {
return `| \`/${name}\` | ${cmd.description.split('.')[0]}. | ${cmd.model} |`;
})
.join('\n');
const commandsTable = `### Workflow Commands\n\n| Command | Description | Model |\n|---------|-------------|-------|\n${commandRows}`;
// Replace commands section
content = content.replace(
/### Workflow Commands\n\n\| Command \| Description \| Model \|[\s\S]*?(?=\n\n###)/,
commandsTable + '\n\n'
);
fs.writeFileSync(KILO_SPEC, content, 'utf-8');
}
/**
* Update AGENTS.md
*/
function updateAgentsMd(meta) {
let content = fs.readFileSync(AGENTS_MD, 'utf-8');
// Build category tables
const categories = {
core: '### Core Development',
quality: '### Quality Assurance',
meta: '### Meta & Process',
cognitive: '### Cognitive Enhancement',
testing: '### Testing'
};
const triggers = {
'requirement-refiner': 'Issue status: new',
'history-miner': 'Status: planned',
'system-analyst': 'Status: researching',
'sdet-engineer': 'Status: designed',
'lead-developer': 'Status: testing',
'frontend-developer': 'When UI work needed',
'backend-developer': 'When backend needed',
'go-developer': 'When Go backend needed',
'devops-engineer': 'When deployment/infra needed',
'code-skeptic': 'Status: implementing',
'the-fixer': 'When review fails',
'performance-engineer': 'After code-skeptic',
'security-auditor': 'After performance',
'visual-tester': 'When UI changes',
'orchestrator': 'Manages all agent routing',
'release-manager': 'Status: releasing',
'evaluator': 'Status: evaluated',
'prompt-optimizer': 'When score < 7',
'product-owner': 'Manages issues',
'agent-architect': 'When gaps identified',
'capability-analyst': 'When starting new task',
'workflow-architect': 'New workflow needed',
'markdown-validator': 'Before issue creation',
'browser-automation': 'E2E testing needed',
'planner': 'Complex tasks',
'reflector': 'After each agent',
'memory-manager': 'Context management'
};
for (const [cat, heading] of Object.entries(categories)) {
const agents = Object.entries(meta.agents)
.filter(([_, a]) => a.category === cat)
.map(([name, agent]) => {
const displayName = name.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join('');
return `| \`@${displayName}\` | ${agent.description.split('.')[0]} | ${triggers[name] || 'Manual invocation'} |`;
})
.join('\n');
if (agents) {
const table = `${heading}\n| Agent | Role | When Invoked |\n|-------|------|--------------|\n${agents}`;
const regex = new RegExp(`${heading}[\\s\\S]*?(?=###|$)`);
if (regex.test(content)) {
content = content.replace(regex, table + '\n\n');
}
}
}
fs.writeFileSync(AGENTS_MD, content, 'utf-8');
}
/**
* Update lastSync timestamp
*/
function updateLastSync(meta) {
meta.lastSync = new Date().toISOString();
fs.writeFileSync(KILO_META, JSON.stringify(meta, null, 2));
}
/**
* Main
*/
function main() {
const args = process.argv.slice(2);
const checkOnly = args.includes('--check');
const fixMode = args.includes('--fix');
console.log('=== Agent Sync Tool ===\n');
console.log('Source of truth: kilo-meta.json\n');
const meta = loadKiloMeta();
// Check agents
console.log('Checking agent files...');
let violations = checkAgents(meta);
// Check kilo.jsonc
console.log('Checking kilo.jsonc...');
violations = violations.concat(checkKiloJsonc(meta));
if (violations.length > 0) {
console.log(`\n⚠️ Found ${violations.length} violations:\n`);
for (const v of violations) {
console.log(` [${v.type}] ${v.agent}: ${v.message}`);
if (v.expected) {
console.log(` Expected: ${v.expected}`);
console.log(` Actual: ${v.actual}`);
}
}
if (fixMode) {
console.log('\n🔧 Fixing agent files...');
const fixes = fixAgents(meta);
for (const f of fixes) {
console.log(`${f.agent}: ${f.action} (${f.updates?.join(', ') || 'n/a'})`);
}
console.log('\n📝 Updating KILO_SPEC.md...');
updateKiloSpec(meta);
console.log(' ✓ KILO_SPEC.md updated');
console.log('\n📝 Updating AGENTS.md...');
updateAgentsMd(meta);
console.log(' ✓ AGENTS.md updated');
updateLastSync(meta);
console.log('\n✅ Sync complete!');
} else if (checkOnly) {
console.log('\n❌ Check failed. Run with --fix to resolve.');
process.exit(1);
}
} else {
console.log('\n✅ All agents in sync!');
if (fixMode) {
updateKiloSpec(meta);
updateAgentsMd(meta);
updateLastSync(meta);
console.log('✅ Documentation updated');
}
}
}
main();

View File

@@ -1,3 +0,0 @@
node_modules/
package-lock.json
.DS_Store

View File

@@ -1,7 +1,7 @@
{
"name": "apaw-web-testing",
"name": "telegram-shop-web-testing",
"version": "2.0.0",
"description": "Web application testing suite for APAW - Visual regression, link checking, form testing, console error detection",
"description": "Web application testing suite for Telegram Shop - Visual regression, link checking, form testing, console error detection",
"main": "scripts/visual-test-pipeline.js",
"scripts": {
"test": "node scripts/visual-test-pipeline.js",
@@ -19,7 +19,7 @@
"playwright",
"kilo-code"
],
"author": "APAW Team",
"author": "Telegram Shop Team",
"license": "MIT",
"dependencies": {
"pixelmatch": "^5.3.0",