Compare commits
1 Commits
main
...
refactorin
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8ff01f5f68 |
44
.env.example
44
.env.example
@@ -1,44 +0,0 @@
|
|||||||
# ============================================================
|
|
||||||
# Telegram Shop - Environment Configuration (TEMPLATE)
|
|
||||||
# ============================================================
|
|
||||||
# Копируй этот файл в .env и заполни реальными значениями.
|
|
||||||
# ВНИМАНИЕ: .env файлы НЕ коммитятся — они в .gitignore.
|
|
||||||
# ============================================================
|
|
||||||
|
|
||||||
# --- Telegram Bot ---
|
|
||||||
BOT_TOKEN=your_bot_token_here
|
|
||||||
ADMIN_IDS=123456789,987654321
|
|
||||||
SUPER_ADMIN_IDS=123456789
|
|
||||||
SUPPORT_LINK=https://t.me/your_support
|
|
||||||
|
|
||||||
# --- Catalog ---
|
|
||||||
CATALOG_PATH=./catalog
|
|
||||||
|
|
||||||
# --- Encryption (ОБЯЗАТЕЛЬНО! Без этого приложение упадёт) ---
|
|
||||||
# Сгенерируй надёжный ключ: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
|
|
||||||
ENCRYPTION_KEY=
|
|
||||||
|
|
||||||
# --- Commission ---
|
|
||||||
COMMISSION_ENABLED=true
|
|
||||||
COMMISSION_PERCENT=5
|
|
||||||
|
|
||||||
# --- Commission Wallets ---
|
|
||||||
COMMISSION_WALLET_BTC=
|
|
||||||
COMMISSION_WALLET_LTC=
|
|
||||||
COMMISSION_WALLET_USDT=
|
|
||||||
COMMISSION_WALLET_USDC=
|
|
||||||
COMMISSION_WALLET_ETH=
|
|
||||||
|
|
||||||
# --- WireGuard ---
|
|
||||||
WG_ENABLED=false
|
|
||||||
WG_PRIVATE_KEY=
|
|
||||||
WG_PUBLIC_KEY=
|
|
||||||
WG_PRESHARED_KEY=
|
|
||||||
WG_ENDPOINT=
|
|
||||||
WG_ADDRESS=
|
|
||||||
WG_DNS=
|
|
||||||
WG_ALLOWED_IPS=0.0.0.0/0,::/0
|
|
||||||
|
|
||||||
# --- Gitea API (для CI/CD и пайплайна) ---
|
|
||||||
GITEA_API_URL=https://git.softuniq.eu/api/v1
|
|
||||||
GITEA_TOKEN=
|
|
||||||
44
.gitignore
vendored
44
.gitignore
vendored
@@ -1,43 +1 @@
|
|||||||
# Dependencies
|
db
|
||||||
node_modules/
|
|
||||||
|
|
||||||
# Environment
|
|
||||||
.env
|
|
||||||
.env.*
|
|
||||||
!.env.example
|
|
||||||
|
|
||||||
# Secrets & sensitive data
|
|
||||||
docker-compose.override.yml
|
|
||||||
wg/
|
|
||||||
dump/
|
|
||||||
dump.zip
|
|
||||||
*.csv
|
|
||||||
|
|
||||||
# Database
|
|
||||||
db/
|
|
||||||
*.db
|
|
||||||
|
|
||||||
# Logs
|
|
||||||
*.log
|
|
||||||
|
|
||||||
# OS
|
|
||||||
.DS_Store
|
|
||||||
|
|
||||||
# Kilo Code — entire directory (agents, skills, rules, workflows, etc.)
|
|
||||||
.kilo/
|
|
||||||
|
|
||||||
# Kilo Code — project-level config files (not part of telegram-shop source)
|
|
||||||
kilo-meta.json
|
|
||||||
kilo.jsonc
|
|
||||||
AGENTS.md
|
|
||||||
|
|
||||||
# Architect generated maps
|
|
||||||
.architect/
|
|
||||||
|
|
||||||
# Local workspace / worktrees
|
|
||||||
.work/
|
|
||||||
|
|
||||||
# Python cache
|
|
||||||
__pycache__/
|
|
||||||
*.pyc
|
|
||||||
*.pyo
|
|
||||||
44
Dockerfile
44
Dockerfile
@@ -1,43 +1,11 @@
|
|||||||
FROM node:22-alpine AS builder
|
FROM node:22
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY package*.json ./
|
COPY package*.json /app/
|
||||||
RUN npm install && npm cache clean --force
|
COPY src/ /app/src/
|
||||||
|
#COPY db/shop.db /app/shop.db
|
||||||
|
|
||||||
# --- Runtime image ---
|
RUN npm install
|
||||||
FROM node:22-alpine
|
|
||||||
|
|
||||||
# Install runtime dependencies
|
CMD ["node", "src/index.js"]
|
||||||
RUN apk update && \
|
|
||||||
apk add --no-cache --repository https://dl-cdn.alpinelinux.org/alpine/edge/community \
|
|
||||||
wireguard-tools \
|
|
||||||
&& apk add --no-cache \
|
|
||||||
iptables \
|
|
||||||
iproute2 \
|
|
||||||
openresolv \
|
|
||||||
bash \
|
|
||||||
curl \
|
|
||||||
&& rm -rf /var/cache/apk/*
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# Copy node_modules from builder
|
|
||||||
COPY --from=builder /app/node_modules ./node_modules
|
|
||||||
COPY package*.json ./
|
|
||||||
|
|
||||||
# Copy application source
|
|
||||||
COPY ./src ./src
|
|
||||||
|
|
||||||
# Copy startup script
|
|
||||||
COPY ./wg/start.sh /app/start.sh
|
|
||||||
RUN chmod +x /app/start.sh
|
|
||||||
|
|
||||||
# Create db directory
|
|
||||||
RUN mkdir -p /app/db
|
|
||||||
|
|
||||||
# Health check: bot responds to /health on port 3000
|
|
||||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
|
|
||||||
CMD curl -sf http://localhost:3001/health || exit 1
|
|
||||||
|
|
||||||
CMD ["/bin/bash", "/app/start.sh"]
|
|
||||||
|
|||||||
155
README.md
155
README.md
@@ -1,139 +1,53 @@
|
|||||||
# Универсальный Телеграмм Магазин
|
**Универсальный Телеграмм Магазин**
|
||||||
|
|
||||||
**Описание проекта**:
|
**Описание проекта**:
|
||||||
"Универсальный Телеграмм Магазин" — это телеграмм-бот для организации онлайн-продаж через Telegram. Проект предоставляет полный цикл управления магазином, включая работу с криптовалютами, управление товарами и пользователями, а также интеграцию с VPN через WireGuard для безопасных транзакций.
|
"Универсальный Телеграмм Магазин" — это телеграмм-бот, предназначенный для организации и управления онлайн-продажами товаров и услуг через популярную платформу Telegram. Магазин включает функционал как для пользователей, так и для администраторов, обеспечивая удобное взаимодействие с товарами, балансами, кошельками и покупками.
|
||||||
|
|
||||||
**Основные технологии**:
|
|
||||||
- Node.js + Telegraf для работы с Telegram API
|
|
||||||
- SQLite для хранения данных
|
|
||||||
- Docker для контейнеризации
|
|
||||||
- WireGuard для защищенных соединений
|
|
||||||
- Поддержка криптокошельков (Bitcoin, Ethereum, Litecoin)
|
|
||||||
|
|
||||||
Проект включает несколько ключевых разделов для удобной работы пользователей и администраторов, а также позволяет интегрировать систему криптокошельков для расчетов, управления товарами и отслеживания покупок.
|
Проект включает несколько ключевых разделов для удобной работы пользователей и администраторов, а также позволяет интегрировать систему криптокошельков для расчетов, управления товарами и отслеживания покупок.
|
||||||
|
|
||||||
### Основной функционал
|
### Цели проекта:
|
||||||
|
- Создание удобного и универсального интерфейса для покупок через Telegram.
|
||||||
#### Для пользователей:
|
- Обеспечение безопасности и простоты транзакций с использованием криптовалют и традиционных средств.
|
||||||
- Просмотр товаров по категориям с фильтрацией по локациям
|
- Внедрение эффективной системы управления для администраторов, с возможностью мониторинга пользователей, товаров, кошельков и комиссий.
|
||||||
- Совершение покупок с использованием криптовалют
|
- Реализация системы профилей с возможностью редактирования, управления балансами и удаления аккаунтов.
|
||||||
- Управление криптокошельками (создание, пополнение, просмотр баланса)
|
|
||||||
- Просмотр истории транзакций и покупок
|
|
||||||
- Настройка профиля (локация, контактные данные)
|
|
||||||
- Подключение к защищенному VPN через WireGuard для безопасных транзакций
|
|
||||||
|
|
||||||
#### Для администраторов:
|
|
||||||
- Полное управление товарами и категориями
|
|
||||||
- Управление пользователями (блокировка, редактирование балансов)
|
|
||||||
- Контроль транзакций и комиссий
|
|
||||||
- Создание дампов базы данных с автоматическим списанием комиссии (% от балансов кошельков)
|
|
||||||
- Управление локациями и настройками VPN
|
|
||||||
- Мониторинг активности пользователей
|
|
||||||
- SaaS система с автоматическим расчетом комиссий:
|
|
||||||
- Комиссия за оборот по магазину перед выгрузкой кошельков
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Установка и запуск
|
### Структура проекта:
|
||||||
|
|
||||||
#### Требования:
|
#### 1. **Пользовательский раздел**
|
||||||
- Node.js 18+
|
Пользователи могут:
|
||||||
- Docker и Docker Compose
|
- Просматривать и покупать товары, управлять своим балансом.
|
||||||
- Telegram Bot Token
|
- Следить за историей покупок.
|
||||||
- SQLite connection string
|
- Пополнять свои криптокошельки.
|
||||||
- WireGuard конфигурация
|
- Управлять своим профилем, изменяя локацию и удаляя аккаунт.
|
||||||
|
|
||||||
#### 1. Установка зависимостей:
|
#### 2. **Административный раздел**
|
||||||
```bash
|
Администраторы могут:
|
||||||
npm install
|
- Управлять пользователями: блокировать, удалять и редактировать балансы.
|
||||||
```
|
- Управлять товарами: добавлять, редактировать, удалять товары и категории.
|
||||||
|
- Управлять кошельками: контролировать пополнения и комиссионные платежи.
|
||||||
#### 2. Настройка конфигурации:
|
- Создавать дампы для переноса базы данных магазина.
|
||||||
Создайте файл `.env` в корне проекта со следующим содержимым:
|
|
||||||
```env
|
|
||||||
TELEGRAM_BOT_TOKEN=your_bot_token
|
|
||||||
MONGO_URI=mongodb://localhost:27017/telegram_shop
|
|
||||||
WIREGUARD_CONFIG_PATH=./wg/config/wg0.conf
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 3. Запуск через Docker:
|
|
||||||
```bash
|
|
||||||
docker-compose up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 4. Настройка WireGuard:
|
|
||||||
1. Сгенерируйте ключи:
|
|
||||||
```bash
|
|
||||||
wg genkey | tee privatekey | wg pubkey > publickey
|
|
||||||
```
|
|
||||||
2. Настройте wg0.conf:
|
|
||||||
```ini
|
|
||||||
[Interface]
|
|
||||||
PrivateKey = <your_private_key>
|
|
||||||
Address = 10.0.0.1/24
|
|
||||||
ListenPort = 51820
|
|
||||||
|
|
||||||
[Peer]
|
|
||||||
PublicKey = <client_public_key>
|
|
||||||
AllowedIPs = 10.0.0.2/32
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 5. Запуск бота:
|
|
||||||
```bash
|
|
||||||
npm start
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Структура проекта
|
### Основной функционал:
|
||||||
|
|
||||||
```
|
#### 1. **Покупки и товары**
|
||||||
├── src/
|
- **Продукты**: Пользователи могут выбирать товары по категориям, проверять наличие средств и совершать покупки.
|
||||||
│ ├── config/ # Конфигурация приложения
|
- **Профиль**: В разделе профиля можно изменять локацию, а также удалять аккаунт.
|
||||||
│ ├── context/ # Контекст и состояния бота
|
- **История покупок**: Пользователи могут отслеживать свои покупки с описанием товаров и статусов.
|
||||||
│ ├── handlers/ # Обработчики команд
|
- **Кошельки**: Возможность добавлять новые криптокошельки, пополнять их через QR-коды и просматривать историю транзакций.
|
||||||
│ ├── models/ # Модели данных
|
|
||||||
│ ├── services/ # Бизнес-логика
|
|
||||||
│ ├── utils/ # Вспомогательные утилиты
|
|
||||||
│ └── index.js # Точка входа
|
|
||||||
├── wg/ # Конфигурация WireGuard
|
|
||||||
├── docker-compose.yml # Docker конфигурация
|
|
||||||
└── Dockerfile # Docker образ
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
#### 2. **Администрирование**
|
||||||
|
- **Управление пользователями**: Администратор может просматривать информацию о пользователях, управлять их балансами, блокировать или удалять аккаунты.
|
||||||
|
- **Управление товарами**: Добавление новых товаров, редактирование существующих и управление их категориями.
|
||||||
|
- **Создание дампов**: Администратор может создать дамп магазина, чтобы перенести данные на другой сервер или сохранить их для архивации.
|
||||||
|
|
||||||
### Разработка
|
#### 3. **Работа с криптовалютами**
|
||||||
|
- Поддержка различных типов криптокошельков (биткойн, эфириум, лайткоин и другие).
|
||||||
#### Запуск в режиме разработки:
|
- Проверка баланса кошельков через общедоступные API.
|
||||||
```bash
|
- Управление комиссионными, которые необходимы для загрузки дампа магазина.
|
||||||
npm run dev
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Линтинг и форматирование:
|
|
||||||
```bash
|
|
||||||
npm run lint
|
|
||||||
npm run format
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Тестирование:
|
|
||||||
```bash
|
|
||||||
npm test
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Лицензия
|
|
||||||
|
|
||||||
Проект распространяется под лицензией MIT. Подробнее см. в файле LICENSE.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Контакты
|
|
||||||
|
|
||||||
По вопросам сотрудничества и поддержки:
|
|
||||||
- Email: support@telegram-shop.com
|
|
||||||
- Telegram: @telegram_shop_support
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -173,3 +87,4 @@ npm test
|
|||||||
|
|
||||||
### Заключение:
|
### Заключение:
|
||||||
**Универсальный Телеграмм Магазин** предоставляет эффективное решение для организации торговых процессов в Telegram, с возможностью работы с криптовалютами и традиционными средствами. Проект ориентирован на пользователей, которые ценят удобство, безопасность и скорость совершения покупок. Для администраторов — это мощный инструмент для управления товаром, пользователями и финансовыми потоками магазина.
|
**Универсальный Телеграмм Магазин** предоставляет эффективное решение для организации торговых процессов в Telegram, с возможностью работы с криптовалютами и традиционными средствами. Проект ориентирован на пользователей, которые ценят удобство, безопасность и скорость совершения покупок. Для администраторов — это мощный инструмент для управления товаром, пользователями и финансовыми потоками магазина.
|
||||||
|
|
||||||
|
|||||||
@@ -1,35 +1,17 @@
|
|||||||
version: "3.3"
|
version: "3.3"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
telegram_shop_prod:
|
telegram_shop_prod:
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: ./Dockerfile
|
dockerfile: ./Dockerfile
|
||||||
network: host
|
hostname: telegram_shop_prod
|
||||||
hostname: telegram_shop_prod
|
container_name: telegram_shop_prod
|
||||||
container_name: telegram_shop_prod
|
restart: always
|
||||||
ports:
|
environment:
|
||||||
- "3001:3001"
|
- BOT_TOKEN=7626758249:AAEdcbXJpW1VsnJJtc8kZ5VBsYMFR242wgk
|
||||||
restart: always
|
- ADMIN_IDS=732563549,390431690,217546867
|
||||||
env_file:
|
- SUPPORT_LINK=https://t.me/neroworm
|
||||||
- .env
|
- CATALOG_PATH=./catalog
|
||||||
volumes:
|
volumes:
|
||||||
- ./db:/app/db/ # Синхронизация базы данных (persistence)
|
- ./db:/app/db/
|
||||||
- ./uploads:/app/uploads/ # Uploaded product photos
|
|
||||||
- ./wg/start.sh:/app/start.sh # Монтируем start.sh (генерирует wg0.conf из env)
|
|
||||||
cap_add: # Минимальные привилегии, необходимые только для WireGuard
|
|
||||||
- NET_ADMIN
|
|
||||||
sysctls:
|
|
||||||
- net.ipv4.conf.all.src_valid_mark=1 # Необходимо для маршрутизации
|
|
||||||
dns:
|
|
||||||
- 8.8.8.8
|
|
||||||
- 1.1.1.1
|
|
||||||
mem_limit: 512m
|
|
||||||
cpus: "1.0"
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "curl", "-sf", "http://localhost:3001/health"]
|
|
||||||
interval: 30s
|
|
||||||
timeout: 10s
|
|
||||||
retries: 3
|
|
||||||
start_period: 60s
|
|
||||||
networks:
|
|
||||||
default:
|
|
||||||
2606
package-lock.json
generated
2606
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
12
package.json
12
package.json
@@ -9,22 +9,18 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"archiver": "^7.0.1",
|
"archiver": "^7.0.1",
|
||||||
"axios": "^1.7.7",
|
"axios": "^1.7.7",
|
||||||
"better-sqlite3": "^11.10.0",
|
|
||||||
"bip39": "^3.1.0",
|
"bip39": "^3.1.0",
|
||||||
"bitcoinjs-lib": "^6.1.6",
|
"bitcoinjs-lib": "^6.1.6",
|
||||||
"cookie-parser": "^1.4.6",
|
"crypto-js": "^4.2.0",
|
||||||
"csv-writer": "^1.6.0",
|
|
||||||
"decompress": "^4.2.1",
|
"decompress": "^4.2.1",
|
||||||
"dotenv": "^16.3.1",
|
"dotenv": "^16.3.1",
|
||||||
"ecpair": "^2.1.0",
|
"ecpair": "^2.1.0",
|
||||||
"ethereumjs-util": "^7.1.5",
|
"ethereumjs-util": "^7.1.5",
|
||||||
"express": "^4.21.0",
|
|
||||||
"hdkey": "^2.1.0",
|
"hdkey": "^2.1.0",
|
||||||
"multer": "^2.2.0",
|
|
||||||
"node-telegram-bot-api": "^0.64.0",
|
"node-telegram-bot-api": "^0.64.0",
|
||||||
"pino": "^8.21.0",
|
"sqlite3": "^5.1.6",
|
||||||
"pino-pretty": "^13.1.3",
|
"tiny-secp256k1": "^2.2.3",
|
||||||
"tiny-secp256k1": "^2.2.3"
|
"tronweb": "^5.3.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"nodemon": "^3.0.2"
|
"nodemon": "^3.0.2"
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
|
|
||||||
describe('SQL Injection Prevention', () => {
|
|
||||||
it('should reject invalid table name in checkColumnExists', () => {
|
|
||||||
const ALLOWED_TABLES = new Set([
|
|
||||||
'users', 'crypto_wallets', 'transactions', 'products',
|
|
||||||
'purchases', 'locations', 'categories'
|
|
||||||
]);
|
|
||||||
|
|
||||||
expect(ALLOWED_TABLES.has('users')).toBe(true);
|
|
||||||
expect(ALLOWED_TABLES.has('DROP TABLE users;--')).toBe(false);
|
|
||||||
expect(ALLOWED_TABLES.has('1=1')).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should filter out disallowed user fields', () => {
|
|
||||||
const ALLOWED_USER_FIELDS = new Set([
|
|
||||||
'telegram_id', 'username', 'country', 'city',
|
|
||||||
'district', 'status', 'total_balance', 'bonus_balance'
|
|
||||||
]);
|
|
||||||
|
|
||||||
const maliciousData = {
|
|
||||||
telegram_id: '123',
|
|
||||||
username: 'test',
|
|
||||||
admin: true,
|
|
||||||
role: 'superadmin'
|
|
||||||
};
|
|
||||||
|
|
||||||
const safeFields = Object.keys(maliciousData).filter(key => ALLOWED_USER_FIELDS.has(key));
|
|
||||||
|
|
||||||
expect(safeFields).toEqual(['telegram_id', 'username']);
|
|
||||||
expect(safeFields).not.toContain('admin');
|
|
||||||
expect(safeFields).not.toContain('role');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
import crypto from 'crypto';
|
|
||||||
import config from '../config/config.js';
|
|
||||||
|
|
||||||
const TOKEN_SECRET = process.env.ADMIN_SECRET || config.ADMIN_IDS[0] || 'change-me';
|
|
||||||
const COOKIE_NAME = 'admin_token';
|
|
||||||
const MAX_AGE = 24 * 60 * 60 * 1000;
|
|
||||||
|
|
||||||
function signToken(data) {
|
|
||||||
const payload = JSON.stringify({ ...data, exp: Date.now() + MAX_AGE });
|
|
||||||
const b64 = Buffer.from(payload).toString('base64');
|
|
||||||
const sig = crypto.createHmac('sha256', TOKEN_SECRET).update(b64).digest('hex');
|
|
||||||
return `${b64}.${sig}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function verifyToken(token) {
|
|
||||||
try {
|
|
||||||
const [b64, sig] = token.split('.');
|
|
||||||
const expected = crypto.createHmac('sha256', TOKEN_SECRET).update(b64).digest('hex');
|
|
||||||
if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) return null;
|
|
||||||
const payload = JSON.parse(Buffer.from(b64, 'base64').toString());
|
|
||||||
if (payload.exp < Date.now()) return null;
|
|
||||||
return payload;
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function requireAuth(req, res, next) {
|
|
||||||
const token = req.cookies?.[COOKIE_NAME];
|
|
||||||
if (!token) return res.redirect('/login');
|
|
||||||
const payload = verifyToken(token);
|
|
||||||
if (!payload) {
|
|
||||||
res.clearCookie(COOKIE_NAME);
|
|
||||||
return res.redirect('/login');
|
|
||||||
}
|
|
||||||
req.admin = payload;
|
|
||||||
next();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function handleLogin(req, res) {
|
|
||||||
const { token } = req.body || {};
|
|
||||||
if (token !== TOKEN_SECRET) {
|
|
||||||
return res.status(401).send(renderLogin('Invalid token'));
|
|
||||||
}
|
|
||||||
const signed = signToken({ role: 'admin' });
|
|
||||||
res.cookie(COOKIE_NAME, signed, {
|
|
||||||
httpOnly: true,
|
|
||||||
sameSite: 'strict',
|
|
||||||
maxAge: MAX_AGE,
|
|
||||||
secure: false
|
|
||||||
});
|
|
||||||
res.redirect('/');
|
|
||||||
}
|
|
||||||
|
|
||||||
export function handleLogout(req, res) {
|
|
||||||
res.clearCookie(COOKIE_NAME);
|
|
||||||
res.redirect('/login');
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderLogin(error) {
|
|
||||||
return `<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Admin Login</title>
|
|
||||||
<link rel="stylesheet" href="/admin/style.css">
|
|
||||||
</head>
|
|
||||||
<body class="login-page">
|
|
||||||
<div class="login-box">
|
|
||||||
<h1>Admin Panel</h1>
|
|
||||||
${error ? `<p class="error">${error}</p>` : ''}
|
|
||||||
<form method="POST" action="/login">
|
|
||||||
<label for="token">Admin Token</label>
|
|
||||||
<input type="password" id="token" name="token" required autofocus>
|
|
||||||
<button type="submit">Login</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export { renderLogin };
|
|
||||||
@@ -1,332 +0,0 @@
|
|||||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
||||||
|
|
||||||
:root {
|
|
||||||
--bg: #f5f5f5;
|
|
||||||
--card: #fff;
|
|
||||||
--text: #1a1a1a;
|
|
||||||
--muted: #666;
|
|
||||||
--primary: #2563eb;
|
|
||||||
--danger: #dc2626;
|
|
||||||
--success: #16a34a;
|
|
||||||
--border: #e5e5e5;
|
|
||||||
--radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
||||||
background: var(--bg);
|
|
||||||
color: var(--text);
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.topnav {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 1rem;
|
|
||||||
padding: 0.75rem 1.5rem;
|
|
||||||
background: var(--card);
|
|
||||||
border-bottom: 1px solid var(--border);
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.brand { font-weight: 700; font-size: 1.1rem; }
|
|
||||||
|
|
||||||
.nav-links { display: flex; gap: 0.25rem; flex-wrap: wrap; }
|
|
||||||
|
|
||||||
.nav-links a {
|
|
||||||
padding: 0.4rem 0.75rem;
|
|
||||||
text-decoration: none;
|
|
||||||
color: var(--muted);
|
|
||||||
border-radius: var(--radius);
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-links a:hover, .nav-links a.active {
|
|
||||||
background: var(--primary);
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logout-btn {
|
|
||||||
margin-left: auto;
|
|
||||||
padding: 0.4rem 0.75rem;
|
|
||||||
background: var(--danger);
|
|
||||||
color: #fff;
|
|
||||||
text-decoration: none;
|
|
||||||
border-radius: var(--radius);
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content { max-width: 1200px; margin: 0 auto; padding: 1.5rem; }
|
|
||||||
|
|
||||||
h1 { font-size: 1.5rem; margin-bottom: 1rem; }
|
|
||||||
h2 { font-size: 1.2rem; margin-bottom: 0.75rem; }
|
|
||||||
h3 { font-size: 1rem; margin: 1rem 0 0.5rem; }
|
|
||||||
|
|
||||||
.stats-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
||||||
gap: 1rem;
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-card {
|
|
||||||
background: var(--card);
|
|
||||||
padding: 1.25rem;
|
|
||||||
border-radius: var(--radius);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-value { display: block; font-size: 1.75rem; font-weight: 700; color: var(--primary); }
|
|
||||||
.stat-label { display: block; font-size: 0.85rem; color: var(--muted); margin-top: 0.25rem; }
|
|
||||||
|
|
||||||
table {
|
|
||||||
width: 100%;
|
|
||||||
border-collapse: collapse;
|
|
||||||
background: var(--card);
|
|
||||||
border-radius: var(--radius);
|
|
||||||
overflow: hidden;
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
}
|
|
||||||
|
|
||||||
th, td { padding: 0.6rem 0.75rem; text-align: left; font-size: 0.9rem; }
|
|
||||||
th { background: #fafafa; font-weight: 600; border-bottom: 2px solid var(--border); }
|
|
||||||
td { border-bottom: 1px solid var(--border); }
|
|
||||||
tr:last-child td { border-bottom: none; }
|
|
||||||
tr:hover td { background: #f9fafb; }
|
|
||||||
|
|
||||||
.badge {
|
|
||||||
display: inline-block;
|
|
||||||
padding: 0.15rem 0.5rem;
|
|
||||||
border-radius: 999px;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.badge-active { background: #dcfce7; color: var(--success); }
|
|
||||||
.badge-banned { background: #fee2e2; color: var(--danger); }
|
|
||||||
|
|
||||||
.btn, .btn-sm, button {
|
|
||||||
display: inline-block;
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
background: var(--primary);
|
|
||||||
color: #fff;
|
|
||||||
border: none;
|
|
||||||
border-radius: var(--radius);
|
|
||||||
cursor: pointer;
|
|
||||||
text-decoration: none;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-sm { padding: 0.25rem 0.5rem; font-size: 0.8rem; }
|
|
||||||
.btn:hover, .btn-sm:hover, button:hover { opacity: 0.9; }
|
|
||||||
.btn-danger, .btn-danger:hover { background: var(--danger); }
|
|
||||||
.btn-success, .btn-success:hover { background: var(--success); }
|
|
||||||
.btn-secondary { background: var(--muted); }
|
|
||||||
|
|
||||||
.form, .inline-form {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.75rem;
|
|
||||||
max-width: 500px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inline-form { flex-direction: row; flex-wrap: wrap; align-items: end; max-width: none; }
|
|
||||||
|
|
||||||
.form label { font-weight: 600; font-size: 0.9rem; }
|
|
||||||
|
|
||||||
input, select, textarea {
|
|
||||||
padding: 0.5rem;
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: var(--radius);
|
|
||||||
font-size: 0.9rem;
|
|
||||||
font-family: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
textarea { min-height: 80px; resize: vertical; }
|
|
||||||
|
|
||||||
.form-section {
|
|
||||||
background: var(--card);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: var(--radius);
|
|
||||||
padding: 1rem;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-section summary {
|
|
||||||
font-weight: 600;
|
|
||||||
cursor: pointer;
|
|
||||||
margin-bottom: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-card {
|
|
||||||
background: var(--card);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: var(--radius);
|
|
||||||
padding: 1.25rem;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-card p { margin-bottom: 0.4rem; }
|
|
||||||
|
|
||||||
.flash {
|
|
||||||
padding: 0.75rem 1rem;
|
|
||||||
border-radius: var(--radius);
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flash-info { background: #dbeafe; color: #1e40af; }
|
|
||||||
.flash-error { background: #fee2e2; color: #991b1b; }
|
|
||||||
|
|
||||||
.login-page {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
min-height: 100vh;
|
|
||||||
background: #e5e7eb;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-box {
|
|
||||||
background: var(--card);
|
|
||||||
padding: 2rem;
|
|
||||||
border-radius: var(--radius);
|
|
||||||
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
|
||||||
width: 100%;
|
|
||||||
max-width: 360px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-box h1 { text-align: center; margin-bottom: 1.5rem; }
|
|
||||||
.login-box label { display: block; margin-bottom: 0.25rem; font-weight: 600; }
|
|
||||||
.login-box input { width: 100%; margin-bottom: 1rem; }
|
|
||||||
.login-box button { width: 100%; }
|
|
||||||
|
|
||||||
.error { color: var(--danger); text-align: center; margin-bottom: 1rem; }
|
|
||||||
|
|
||||||
code { background: #f1f5f9; padding: 0.1rem 0.3rem; border-radius: 3px; font-size: 0.85em; }
|
|
||||||
pre { font-size: 0.8rem; white-space: pre-wrap; word-break: break-all; max-width: 300px; }
|
|
||||||
|
|
||||||
.muted { color: var(--muted); font-size: 0.85rem; }
|
|
||||||
|
|
||||||
.settings-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.seed-card {
|
|
||||||
background: var(--card);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: var(--radius);
|
|
||||||
padding: 1.5rem;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.seed-card.danger {
|
|
||||||
border-color: var(--danger);
|
|
||||||
border-width: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.seed-card h2 { margin-bottom: 0.5rem; }
|
|
||||||
.seed-card p { margin-bottom: 1rem; color: var(--muted); }
|
|
||||||
|
|
||||||
.catalog-layout {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 320px 1fr;
|
|
||||||
gap: 1.5rem;
|
|
||||||
align-items: start;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 900px) {
|
|
||||||
.catalog-layout { grid-template-columns: 1fr; }
|
|
||||||
}
|
|
||||||
|
|
||||||
.catalog-tree h2 { margin-bottom: 0.75rem; }
|
|
||||||
|
|
||||||
.catalog-main { min-width: 0; }
|
|
||||||
|
|
||||||
.tree-node { margin-left: 0; }
|
|
||||||
|
|
||||||
.tree-toggle {
|
|
||||||
cursor: pointer;
|
|
||||||
user-select: none;
|
|
||||||
padding: 0.4rem 0;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.3rem;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tree-toggle:hover { background: #f0f4ff; }
|
|
||||||
|
|
||||||
.tree-toggle .arrow {
|
|
||||||
display: inline-block;
|
|
||||||
font-size: 0.7rem;
|
|
||||||
transition: transform 0.15s;
|
|
||||||
width: 12px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tree-toggle .arrow.open { transform: rotate(90deg); }
|
|
||||||
|
|
||||||
.tree-children { display: none; }
|
|
||||||
|
|
||||||
.tree-children.open { display: block; }
|
|
||||||
|
|
||||||
.tree-children {
|
|
||||||
margin-left: 1.5rem;
|
|
||||||
border-left: 2px solid var(--border);
|
|
||||||
padding-left: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tree-count {
|
|
||||||
font-size: 0.75rem;
|
|
||||||
color: #888;
|
|
||||||
margin-left: 0.3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tree-actions {
|
|
||||||
margin-left: auto;
|
|
||||||
display: flex;
|
|
||||||
gap: 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tree-add {
|
|
||||||
margin: 0.3rem 0 0.3rem 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal {
|
|
||||||
position: fixed;
|
|
||||||
top: 0; left: 0; right: 0; bottom: 0;
|
|
||||||
background: rgba(0,0,0,0.5);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
z-index: 1000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-content {
|
|
||||||
background: var(--card);
|
|
||||||
border-radius: var(--radius);
|
|
||||||
padding: 1.5rem;
|
|
||||||
max-width: 560px;
|
|
||||||
width: 90%;
|
|
||||||
max-height: 90vh;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-row {
|
|
||||||
display: flex;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-row input { flex: 1; }
|
|
||||||
|
|
||||||
@media (max-width: 640px) {
|
|
||||||
.topnav { flex-direction: column; align-items: flex-start; }
|
|
||||||
.logout-btn { margin-left: 0; }
|
|
||||||
.stats-grid { grid-template-columns: 1fr 1fr; }
|
|
||||||
table { font-size: 0.8rem; }
|
|
||||||
th, td { padding: 0.4rem; }
|
|
||||||
.catalog-layout { grid-template-columns: 1fr; }
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import { Router } from 'express';
|
|
||||||
import db from '../../config/database.js';
|
|
||||||
import { renderAuditLog } from '../views/audit.js';
|
|
||||||
|
|
||||||
const router = Router();
|
|
||||||
|
|
||||||
router.get('/', async (req, res) => {
|
|
||||||
const entries = await db.allAsync(
|
|
||||||
'SELECT * FROM audit_log ORDER BY created_at DESC LIMIT 200'
|
|
||||||
);
|
|
||||||
res.send(renderAuditLog(entries));
|
|
||||||
});
|
|
||||||
|
|
||||||
export default router;
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
import { Router } from 'express';
|
|
||||||
import db from '../../config/database.js';
|
|
||||||
import { renderCatalog } from '../views/catalog.js';
|
|
||||||
|
|
||||||
const router = Router();
|
|
||||||
|
|
||||||
router.get('/', async (req, res) => {
|
|
||||||
const { loc, cat, sub, msg, msg_type } = req.query;
|
|
||||||
const [locations, categories, subcategories] = await Promise.all([
|
|
||||||
db.allAsync('SELECT * FROM locations ORDER BY country, city, district'),
|
|
||||||
db.allAsync(`SELECT c.*, (SELECT COUNT(*) FROM products WHERE category_id=c.id) as pc,
|
|
||||||
(SELECT COUNT(*) FROM subcategories WHERE category_id=c.id) as sc FROM categories c`),
|
|
||||||
db.allAsync(`SELECT s.*, (SELECT COUNT(*) FROM products WHERE subcategory_id=s.id) as pc
|
|
||||||
FROM subcategories s`)
|
|
||||||
]);
|
|
||||||
let psql = `SELECT p.*, c.name as cn, s.name as sn FROM products p
|
|
||||||
LEFT JOIN categories c ON p.category_id=c.id LEFT JOIN subcategories s ON p.subcategory_id=s.id WHERE 1=1`;
|
|
||||||
const params = [];
|
|
||||||
if (loc) { psql += ' AND p.location_id=?'; params.push(loc); }
|
|
||||||
if (cat) { psql += ' AND p.category_id=?'; params.push(cat); }
|
|
||||||
if (sub) { psql += ' AND p.subcategory_id=?'; params.push(sub); }
|
|
||||||
const products = await db.allAsync(psql + ' ORDER BY p.id DESC LIMIT 200', params);
|
|
||||||
const cl = {}; for (const c of categories) (cl[c.location_id]??=[]).push(c);
|
|
||||||
const sc = {}; for (const s of subcategories) (sc[s.category_id]??=[]).push(s);
|
|
||||||
const tree = {};
|
|
||||||
for (const l of locations) {
|
|
||||||
(tree[l.country]??={cities:{}}).cities[l.city]??={districts:{}};
|
|
||||||
tree[l.country].cities[l.city].districts[l.district] = {
|
|
||||||
id: l.id, cats: (cl[l.id]||[]).map(c=>({...c, subs: sc[c.id]||[]}))
|
|
||||||
};
|
|
||||||
}
|
|
||||||
res.send(renderCatalog(tree, products, { loc, cat, sub }, categories, subcategories, msg||'', msg_type||'info'));
|
|
||||||
});
|
|
||||||
|
|
||||||
router.post('/locations', async (req, res) => {
|
|
||||||
const { country, city, district } = req.body;
|
|
||||||
if (!country || !city) return res.redirect('/catalog?msg=Country+and+city+required&msg_type=error');
|
|
||||||
await db.runAsync('INSERT INTO locations (country,city,district) VALUES (?,?,?)',
|
|
||||||
[country.trim(), city.trim(), (district||'').trim()]);
|
|
||||||
res.redirect('/catalog?msg=Location+added&msg_type=success');
|
|
||||||
});
|
|
||||||
|
|
||||||
router.post('/locations/:id/delete', async (req, res) => {
|
|
||||||
const c = await db.getAsync('SELECT COUNT(*) as n FROM categories WHERE location_id=?', [req.params.id]);
|
|
||||||
if (c?.n > 0) return res.redirect('/catalog?msg=Cannot+delete+has+categories&msg_type=error');
|
|
||||||
await db.runAsync('DELETE FROM locations WHERE id=?', [req.params.id]);
|
|
||||||
res.redirect('/catalog?msg=Location+deleted&msg_type=success');
|
|
||||||
});
|
|
||||||
|
|
||||||
router.post('/locations/add-city', async (req, res) => {
|
|
||||||
const { country, city } = req.body;
|
|
||||||
if (!country || !city) return res.redirect('/catalog?msg=Country+and+city+required&msg_type=error');
|
|
||||||
await db.runAsync('INSERT INTO locations (country,city,district) VALUES (?,?,?)',
|
|
||||||
[country.trim(), city.trim(), '']);
|
|
||||||
res.redirect('/catalog?msg=City+added&msg_type=success');
|
|
||||||
});
|
|
||||||
|
|
||||||
router.post('/locations/add-district', async (req, res) => {
|
|
||||||
const { country, city, district } = req.body;
|
|
||||||
if (!country || !city || !district) return res.redirect('/catalog?msg=All+fields+required&msg_type=error');
|
|
||||||
await db.runAsync('INSERT INTO locations (country,city,district) VALUES (?,?,?)',
|
|
||||||
[country.trim(), city.trim(), district.trim()]);
|
|
||||||
res.redirect('/catalog?msg=District+added&msg_type=success');
|
|
||||||
});
|
|
||||||
|
|
||||||
router.post('/categories', async (req, res) => {
|
|
||||||
const { name, location_id } = req.body;
|
|
||||||
if (!name || !location_id) return res.redirect('/catalog?msg=Name+and+location+required&msg_type=error');
|
|
||||||
await db.runAsync('INSERT INTO categories (name,location_id) VALUES (?,?)', [name.trim(), location_id]);
|
|
||||||
res.redirect('/catalog?msg=Category+added&msg_type=success');
|
|
||||||
});
|
|
||||||
|
|
||||||
router.post('/categories/:id/delete', async (req, res) => {
|
|
||||||
const c = await db.getAsync('SELECT COUNT(*) as n FROM products WHERE category_id=?', [req.params.id]);
|
|
||||||
if (c?.n > 0) return res.redirect('/catalog?msg=Cannot+delete+has+products&msg_type=error');
|
|
||||||
await db.runAsync('DELETE FROM subcategories WHERE category_id=?', [req.params.id]);
|
|
||||||
await db.runAsync('DELETE FROM categories WHERE id=?', [req.params.id]);
|
|
||||||
res.redirect('/catalog?msg=Category+deleted&msg_type=success');
|
|
||||||
});
|
|
||||||
|
|
||||||
router.post('/categories/:id/subcategories', async (req, res) => {
|
|
||||||
const { name } = req.body;
|
|
||||||
if (!name) return res.redirect('/catalog?msg=Name+required&msg_type=error');
|
|
||||||
await db.runAsync('INSERT INTO subcategories (category_id,name) VALUES (?,?)', [req.params.id, name.trim()]);
|
|
||||||
res.redirect('/catalog?msg=Subcategory+added&msg_type=success');
|
|
||||||
});
|
|
||||||
|
|
||||||
router.post('/subcategories/:id/delete', async (req, res) => {
|
|
||||||
const c = await db.getAsync('SELECT COUNT(*) as n FROM products WHERE subcategory_id=?', [req.params.id]);
|
|
||||||
if (c?.n > 0) return res.redirect('/catalog?msg=Cannot+delete+has+products&msg_type=error');
|
|
||||||
await db.runAsync('DELETE FROM subcategories WHERE id=?', [req.params.id]);
|
|
||||||
res.redirect('/catalog?msg=Subcategory+deleted&msg_type=success');
|
|
||||||
});
|
|
||||||
|
|
||||||
export default router;
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
import { Router } from 'express';
|
|
||||||
import multer from 'multer';
|
|
||||||
import { join, dirname } from 'path';
|
|
||||||
import { fileURLToPath } from 'url';
|
|
||||||
import db from '../../config/database.js';
|
|
||||||
|
|
||||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
||||||
const uploadsDir = join(__dirname, '..', '..', '..', 'uploads');
|
|
||||||
const storage = multer.diskStorage({
|
|
||||||
destination: uploadsDir,
|
|
||||||
filename: (req, file, cb) => cb(null, `${Date.now()}-${file.originalname}`)
|
|
||||||
});
|
|
||||||
const upload = multer({ storage, limits: { fileSize: 10 * 1024 * 1024 } });
|
|
||||||
const router = Router();
|
|
||||||
|
|
||||||
router.post('/products', upload.fields([{ name: 'photo_file' }, { name: 'hidden_photo_file' }]), async (req, res) => {
|
|
||||||
const { name, price, quantity_in_stock, description, photo_url, hidden_photo_url,
|
|
||||||
hidden_coordinates, hidden_description, private_data, category_id, subcategory_id } = req.body;
|
|
||||||
if (!name || !price || !category_id) return res.redirect('/catalog?msg=Name+price+category+required&msg_type=error');
|
|
||||||
const pu = req.files?.photo_file?.[0] ? `/uploads/${req.files.photo_file[0].filename}` : (photo_url || '');
|
|
||||||
const hu = req.files?.hidden_photo_file?.[0] ? `/uploads/${req.files.hidden_photo_file[0].filename}` : (hidden_photo_url || '');
|
|
||||||
const locRow = await db.getAsync('SELECT location_id FROM categories WHERE id=?', [category_id]);
|
|
||||||
await db.runAsync(`INSERT INTO products (name,price,quantity_in_stock,description,photo_url,hidden_photo_url,
|
|
||||||
hidden_coordinates,hidden_description,private_data,category_id,subcategory_id,location_id)
|
|
||||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?)`,
|
|
||||||
[name.trim(), parseFloat(price), parseInt(quantity_in_stock)||0, description||'', pu, hu,
|
|
||||||
hidden_coordinates||'', hidden_description||'', private_data||'', category_id, subcategory_id||null, locRow?.location_id||null]);
|
|
||||||
res.redirect('/catalog?msg=Product+added&msg_type=success');
|
|
||||||
});
|
|
||||||
|
|
||||||
router.post('/products/:id/edit', upload.fields([{ name: 'photo_file' }, { name: 'hidden_photo_file' }]), async (req, res) => {
|
|
||||||
const { name, price, quantity_in_stock, description, photo_url, hidden_photo_url,
|
|
||||||
hidden_coordinates, hidden_description, private_data, category_id, subcategory_id } = req.body;
|
|
||||||
if (!name || !price || !category_id) return res.redirect('/catalog?msg=Name+price+category+required&msg_type=error');
|
|
||||||
const pu = req.files?.photo_file?.[0] ? `/uploads/${req.files.photo_file[0].filename}` : (photo_url || '');
|
|
||||||
const hu = req.files?.hidden_photo_file?.[0] ? `/uploads/${req.files.hidden_photo_file[0].filename}` : (hidden_photo_url || '');
|
|
||||||
const locRow = await db.getAsync('SELECT location_id FROM categories WHERE id=?', [category_id]);
|
|
||||||
await db.runAsync(`UPDATE products SET name=?,price=?,quantity_in_stock=?,description=?,photo_url=?,hidden_photo_url=?,
|
|
||||||
hidden_coordinates=?,hidden_description=?,private_data=?,category_id=?,subcategory_id=?,location_id=? WHERE id=?`,
|
|
||||||
[name.trim(), parseFloat(price), parseInt(quantity_in_stock)||0, description||'', pu, hu,
|
|
||||||
hidden_coordinates||'', hidden_description||'', private_data||'', category_id, subcategory_id||null, locRow?.location_id||null, req.params.id]);
|
|
||||||
res.redirect('/catalog?msg=Product+updated&msg_type=success');
|
|
||||||
});
|
|
||||||
|
|
||||||
router.post('/products/:id/delete', async (req, res) => {
|
|
||||||
await db.runAsync('DELETE FROM products WHERE id=?', [req.params.id]);
|
|
||||||
res.redirect('/catalog?msg=Product+deleted&msg_type=success');
|
|
||||||
});
|
|
||||||
|
|
||||||
router.get('/products/:id/json', async (req, res) => {
|
|
||||||
const p = await db.getAsync('SELECT * FROM products WHERE id=?', [req.params.id]);
|
|
||||||
if (!p) return res.status(404).json({ error: 'Not found' });
|
|
||||||
res.json(p);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default router;
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
import { Router } from 'express';
|
|
||||||
import db from '../../config/database.js';
|
|
||||||
import { renderCategoryList } from '../views/categories.js';
|
|
||||||
|
|
||||||
const router = Router();
|
|
||||||
|
|
||||||
router.get('/', async (req, res) => {
|
|
||||||
const [categories, locations, subcategories] = await Promise.all([
|
|
||||||
db.allAsync(`SELECT c.*, l.country, l.city, l.district,
|
|
||||||
(SELECT COUNT(*) FROM products WHERE category_id = c.id) as product_count
|
|
||||||
FROM categories c LEFT JOIN locations l ON c.location_id = l.id ORDER BY c.id`),
|
|
||||||
db.allAsync('SELECT id, country, city, district FROM locations ORDER BY country, city'),
|
|
||||||
db.allAsync(`SELECT s.*,
|
|
||||||
(SELECT COUNT(*) FROM products WHERE subcategory_id = s.id) as product_count
|
|
||||||
FROM subcategories s ORDER BY s.category_id, s.name`),
|
|
||||||
]);
|
|
||||||
res.send(renderCategoryList(categories, locations, subcategories));
|
|
||||||
});
|
|
||||||
|
|
||||||
router.post('/', async (req, res) => {
|
|
||||||
const { name, location_id } = req.body;
|
|
||||||
await db.runAsync('INSERT INTO categories (name, location_id) VALUES (?, ?)', [name, location_id || null]);
|
|
||||||
res.redirect('/categories');
|
|
||||||
});
|
|
||||||
|
|
||||||
router.post('/:id/update', async (req, res) => {
|
|
||||||
const { name, location_id } = req.body;
|
|
||||||
await db.runAsync('UPDATE categories SET name = ?, location_id = ? WHERE id = ?',
|
|
||||||
[name, location_id || null, req.params.id]);
|
|
||||||
res.redirect('/categories');
|
|
||||||
});
|
|
||||||
|
|
||||||
router.post('/:id/delete', async (req, res) => {
|
|
||||||
const count = await db.getAsync('SELECT COUNT(*) as cnt FROM products WHERE category_id = ?', [req.params.id]);
|
|
||||||
if (count && count.cnt > 0) {
|
|
||||||
return res.redirect('/categories?error=Cannot+delete+category+with+products');
|
|
||||||
}
|
|
||||||
await db.runAsync('DELETE FROM categories WHERE id = ?', [req.params.id]);
|
|
||||||
res.redirect('/categories');
|
|
||||||
});
|
|
||||||
|
|
||||||
router.post('/:id/subcategories', async (req, res) => {
|
|
||||||
const { name } = req.body;
|
|
||||||
await db.runAsync('INSERT INTO subcategories (category_id, name) VALUES (?, ?)',
|
|
||||||
[req.params.id, name]);
|
|
||||||
res.redirect('/categories');
|
|
||||||
});
|
|
||||||
|
|
||||||
router.post('/subcategories/:id/delete', async (req, res) => {
|
|
||||||
const count = await db.getAsync(
|
|
||||||
'SELECT COUNT(*) as cnt FROM products WHERE subcategory_id = ?',
|
|
||||||
[req.params.id]
|
|
||||||
);
|
|
||||||
if (count && count.cnt > 0) {
|
|
||||||
return res.redirect('/categories?error=Cannot+delete+subcategory+with+products');
|
|
||||||
}
|
|
||||||
await db.runAsync('DELETE FROM subcategories WHERE id = ?', [req.params.id]);
|
|
||||||
res.redirect('/categories');
|
|
||||||
});
|
|
||||||
|
|
||||||
export default router;
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
import { Router } from 'express';
|
|
||||||
import db from '../../config/database.js';
|
|
||||||
import { renderDashboard } from '../views/dashboard.js';
|
|
||||||
|
|
||||||
const router = Router();
|
|
||||||
|
|
||||||
router.get('/', async (req, res) => {
|
|
||||||
const [[{ totalUsers }], [{ totalProducts }], [{ totalPurchases }], [{ totalRevenue }], [{ totalSubcategories }]] = await Promise.all([
|
|
||||||
db.allAsync('SELECT COUNT(*) as totalUsers FROM users'),
|
|
||||||
db.allAsync('SELECT COUNT(*) as totalProducts FROM products'),
|
|
||||||
db.allAsync('SELECT COUNT(*) as totalPurchases FROM purchases'),
|
|
||||||
db.allAsync('SELECT COALESCE(SUM(total_price), 0) as totalRevenue FROM purchases WHERE status = ?', ['completed']),
|
|
||||||
db.allAsync('SELECT COUNT(*) as totalSubcategories FROM subcategories'),
|
|
||||||
]);
|
|
||||||
|
|
||||||
let message = '';
|
|
||||||
if (req.query.seeded) message = 'Demo data seeded successfully!';
|
|
||||||
if (req.query.cleared) message = 'All data cleared successfully!';
|
|
||||||
|
|
||||||
res.send(renderDashboard({ totalUsers, totalProducts, totalPurchases, totalRevenue, totalSubcategories }, message));
|
|
||||||
});
|
|
||||||
|
|
||||||
export default router;
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
import { Router } from 'express';
|
|
||||||
import db from '../../config/database.js';
|
|
||||||
import { renderLocationList } from '../views/locations.js';
|
|
||||||
|
|
||||||
const router = Router();
|
|
||||||
|
|
||||||
router.get('/', async (req, res) => {
|
|
||||||
const locations = await db.allAsync(`SELECT l.*,
|
|
||||||
(SELECT COUNT(*) FROM categories WHERE location_id = l.id) as category_count,
|
|
||||||
(SELECT COUNT(*) FROM products WHERE location_id = l.id) as product_count
|
|
||||||
FROM locations l ORDER BY l.country, l.city, l.district`);
|
|
||||||
res.send(renderLocationList(locations));
|
|
||||||
});
|
|
||||||
|
|
||||||
router.post('/', async (req, res) => {
|
|
||||||
const { country, city, district } = req.body;
|
|
||||||
await db.runAsync(
|
|
||||||
'INSERT INTO locations (country, city, district) VALUES (?, ?, ?)',
|
|
||||||
[country, city, district || '']
|
|
||||||
);
|
|
||||||
res.redirect('/locations');
|
|
||||||
});
|
|
||||||
|
|
||||||
router.post('/:id/delete', async (req, res) => {
|
|
||||||
const count = await db.getAsync(
|
|
||||||
'SELECT COUNT(*) as cnt FROM categories WHERE location_id = ?',
|
|
||||||
[req.params.id]
|
|
||||||
);
|
|
||||||
if (count && count.cnt > 0) {
|
|
||||||
return res.redirect('/locations?error=Cannot+delete+location+with+categories');
|
|
||||||
}
|
|
||||||
await db.runAsync('DELETE FROM locations WHERE id = ?', [req.params.id]);
|
|
||||||
res.redirect('/locations');
|
|
||||||
});
|
|
||||||
|
|
||||||
export default router;
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import { Router } from 'express';
|
|
||||||
import config from '../../config/config.js';
|
|
||||||
import { renderPaymentWallets } from '../views/paymentWallets.js';
|
|
||||||
|
|
||||||
const router = Router();
|
|
||||||
|
|
||||||
router.get('/', (req, res) => {
|
|
||||||
res.send(renderPaymentWallets({
|
|
||||||
commissionEnabled: config.COMMISSION_ENABLED,
|
|
||||||
commissionPercent: config.COMMISSION_PERCENT,
|
|
||||||
wallets: config.COMMISSION_WALLETS,
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
|
|
||||||
export default router;
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
import { Router } from 'express';
|
|
||||||
import db from '../../config/database.js';
|
|
||||||
import { renderProductList, renderProductEdit } from '../views/products.js';
|
|
||||||
|
|
||||||
const router = Router();
|
|
||||||
|
|
||||||
router.get('/', async (req, res) => {
|
|
||||||
const [products, categories, subcategories] = await Promise.all([
|
|
||||||
db.allAsync(`SELECT p.*, c.name as category_name, s.name as subcategory_name
|
|
||||||
FROM products p
|
|
||||||
LEFT JOIN categories c ON p.category_id = c.id
|
|
||||||
LEFT JOIN subcategories s ON p.subcategory_id = s.id
|
|
||||||
ORDER BY p.id DESC LIMIT 100`),
|
|
||||||
db.allAsync('SELECT id, name FROM categories ORDER BY name'),
|
|
||||||
db.allAsync('SELECT id, name, category_id FROM subcategories ORDER BY name'),
|
|
||||||
]);
|
|
||||||
res.send(renderProductList(products, categories, subcategories));
|
|
||||||
});
|
|
||||||
|
|
||||||
router.post('/', async (req, res) => {
|
|
||||||
const { name, price, quantity_in_stock, description, photo_url, category_id, subcategory_id } = req.body;
|
|
||||||
await db.runAsync(
|
|
||||||
`INSERT INTO products (name, price, quantity_in_stock, description, photo_url, category_id, subcategory_id, location_id)
|
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, (SELECT location_id FROM categories WHERE id = ?))`,
|
|
||||||
[name, price, quantity_in_stock || 0, description || '', photo_url || '', category_id, subcategory_id || null, category_id]
|
|
||||||
);
|
|
||||||
res.redirect('/products');
|
|
||||||
});
|
|
||||||
|
|
||||||
router.get('/:id/edit', async (req, res) => {
|
|
||||||
const [product, categories, subcategories] = await Promise.all([
|
|
||||||
db.getAsync('SELECT * FROM products WHERE id = ?', [req.params.id]),
|
|
||||||
db.allAsync('SELECT id, name FROM categories ORDER BY name'),
|
|
||||||
db.allAsync('SELECT id, name, category_id FROM subcategories ORDER BY name'),
|
|
||||||
]);
|
|
||||||
if (!product) return res.status(404).send('Product not found');
|
|
||||||
res.send(renderProductEdit(product, categories, subcategories));
|
|
||||||
});
|
|
||||||
|
|
||||||
router.post('/:id/update', async (req, res) => {
|
|
||||||
const { name, price, quantity_in_stock, description, photo_url, category_id, subcategory_id } = req.body;
|
|
||||||
await db.runAsync(
|
|
||||||
`UPDATE products SET name=?, price=?, quantity_in_stock=?, description=?, photo_url=?, category_id=?, subcategory_id=?
|
|
||||||
WHERE id=?`,
|
|
||||||
[name, price, quantity_in_stock || 0, description || '', photo_url || '', category_id, subcategory_id || null, req.params.id]
|
|
||||||
);
|
|
||||||
res.redirect('/products');
|
|
||||||
});
|
|
||||||
|
|
||||||
router.post('/:id/delete', async (req, res) => {
|
|
||||||
await db.runAsync('DELETE FROM products WHERE id = ?', [req.params.id]);
|
|
||||||
res.redirect('/products');
|
|
||||||
});
|
|
||||||
|
|
||||||
export default router;
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import { Router } from 'express';
|
|
||||||
import db from '../../config/database.js';
|
|
||||||
import { renderPurchaseList } from '../views/purchases.js';
|
|
||||||
|
|
||||||
const router = Router();
|
|
||||||
|
|
||||||
router.get('/', async (req, res) => {
|
|
||||||
const purchases = await db.allAsync(
|
|
||||||
`SELECT p.*, pr.name as product_name FROM purchases p
|
|
||||||
LEFT JOIN products pr ON p.product_id = pr.id
|
|
||||||
ORDER BY p.purchase_date DESC LIMIT 200`
|
|
||||||
);
|
|
||||||
res.send(renderPurchaseList(purchases));
|
|
||||||
});
|
|
||||||
|
|
||||||
export default router;
|
|
||||||
@@ -1,140 +0,0 @@
|
|||||||
import { Router } from 'express';
|
|
||||||
import db from '../../config/database.js';
|
|
||||||
import { renderSeedPage } from '../views/seed.js';
|
|
||||||
|
|
||||||
const router = Router();
|
|
||||||
|
|
||||||
router.get('/', (req, res) => {
|
|
||||||
res.send(renderSeedPage());
|
|
||||||
});
|
|
||||||
|
|
||||||
router.post('/seed-demo', async (req, res) => {
|
|
||||||
try {
|
|
||||||
const delTables = ['purchases', 'transactions', 'crypto_wallets', 'audit_log',
|
|
||||||
'user_states', 'products', 'subcategories', 'categories', 'users', 'locations'];
|
|
||||||
for (const t of delTables) {
|
|
||||||
await db.runAsync(`DELETE FROM ${t}`);
|
|
||||||
}
|
|
||||||
await db.runAsync("DELETE FROM sqlite_sequence WHERE name != '_meta'");
|
|
||||||
|
|
||||||
await db.runAsync(`INSERT INTO locations (country, city, district) VALUES
|
|
||||||
('Russia', 'Moscow', 'Center'),
|
|
||||||
('Russia', 'Saint Petersburg', 'North'),
|
|
||||||
('Germany', 'Berlin', 'Mitte')`);
|
|
||||||
const locs = await db.allAsync('SELECT id, city FROM locations');
|
|
||||||
const locMoscow = locs.find(l => l.city === 'Moscow').id;
|
|
||||||
const locSPb = locs.find(l => l.city === 'Saint Petersburg').id;
|
|
||||||
const locBerlin = locs.find(l => l.city === 'Berlin').id;
|
|
||||||
|
|
||||||
await db.runAsync('INSERT INTO categories (location_id, name) VALUES (?, ?), (?, ?), (?, ?), (?, ?), (?, ?)',
|
|
||||||
[locMoscow, 'Digital', locMoscow, 'Physical', locSPb, 'Premium', locSPb, 'VIP', locBerlin, 'Standard']);
|
|
||||||
const cats = await db.allAsync('SELECT id, name FROM categories');
|
|
||||||
const catDigital = cats.find(c => c.name === 'Digital').id;
|
|
||||||
const catPhysical = cats.find(c => c.name === 'Physical').id;
|
|
||||||
const catPremium = cats.find(c => c.name === 'Premium').id;
|
|
||||||
const catVIP = cats.find(c => c.name === 'VIP').id;
|
|
||||||
const catStandard = cats.find(c => c.name === 'Standard').id;
|
|
||||||
|
|
||||||
await db.runAsync(`INSERT INTO subcategories (category_id, name) VALUES
|
|
||||||
(?, 'VPN'), (?, 'Accounts'), (?, 'Software'),
|
|
||||||
(?, 'Hardware'), (?, 'Accessories'),
|
|
||||||
(?, 'Annual'), (?, 'Monthly'),
|
|
||||||
(?, 'Lifetime'), (?, 'Express'),
|
|
||||||
(?, 'Basic'), (?, 'Starter')`,
|
|
||||||
[catDigital, catDigital, catDigital,
|
|
||||||
catPhysical, catPhysical,
|
|
||||||
catPremium, catPremium,
|
|
||||||
catVIP, catVIP,
|
|
||||||
catStandard, catStandard]);
|
|
||||||
const subs = await db.allAsync('SELECT id, name, category_id FROM subcategories');
|
|
||||||
const subVPN = subs.find(s => s.name === 'VPN' && s.category_id === catDigital).id;
|
|
||||||
const subAccounts = subs.find(s => s.name === 'Accounts' && s.category_id === catDigital).id;
|
|
||||||
const subHardware = subs.find(s => s.name === 'Hardware' && s.category_id === catPhysical).id;
|
|
||||||
const subAnnual = subs.find(s => s.name === 'Annual' && s.category_id === catPremium).id;
|
|
||||||
const subLifetime = subs.find(s => s.name === 'Lifetime' && s.category_id === catVIP).id;
|
|
||||||
const subMonthly = subs.find(s => s.name === 'Monthly' && s.category_id === catPremium).id;
|
|
||||||
const subBasic = subs.find(s => s.name === 'Basic' && s.category_id === catStandard).id;
|
|
||||||
const subExpress = subs.find(s => s.name === 'Express' && s.category_id === catVIP).id;
|
|
||||||
const subStarter = subs.find(s => s.name === 'Starter' && s.category_id === catStandard).id;
|
|
||||||
const subSoftware = subs.find(s => s.name === 'Software' && s.category_id === catDigital).id;
|
|
||||||
|
|
||||||
await db.runAsync(`INSERT INTO users (telegram_id, username, country, city, district, status, total_balance, bonus_balance) VALUES
|
|
||||||
('1001', 'alice', 'Russia', 'Moscow', 'Center', 1, 150.00, 25.00),
|
|
||||||
('1002', 'bob', 'Russia', 'Moscow', 'Center', 1, 85.50, 10.00),
|
|
||||||
('1003', 'charlie', 'Russia', 'Saint Petersburg', 'North', 1, 320.75, 50.00),
|
|
||||||
('1004', 'diana', 'Germany', 'Berlin', 'Mitte', 1, 45.00, 5.00),
|
|
||||||
('1005', 'evan', 'Germany', 'Berlin', 'Mitte', 0, 0.00, 0.00)`);
|
|
||||||
const users = await db.allAsync('SELECT id, username FROM users');
|
|
||||||
const uAlice = users.find(u => u.username === 'alice').id;
|
|
||||||
const uBob = users.find(u => u.username === 'bob').id;
|
|
||||||
const uCharlie = users.find(u => u.username === 'charlie').id;
|
|
||||||
const uDiana = users.find(u => u.username === 'diana').id;
|
|
||||||
|
|
||||||
await db.runAsync(`INSERT INTO products (location_id, category_id, subcategory_id, name, description, price, quantity_in_stock, photo_url) VALUES
|
|
||||||
(?, ?, ?, 'VPN Subscription 30d', 'Premium VPN access for 30 days', 9.99, 100, ''),
|
|
||||||
(?, ?, ?, 'VPN Subscription 90d', 'Premium VPN access for 90 days', 24.99, 50, ''),
|
|
||||||
(?, ?, ?, 'USB Drive 64GB', 'Encrypted USB drive', 29.99, 25, ''),
|
|
||||||
(?, ?, ?, 'Premium Account 1 Year', 'Full premium access 12 months', 99.99, 10, ''),
|
|
||||||
(?, ?, ?, 'VIP Access Lifetime', 'Lifetime VIP membership', 199.99, 5, ''),
|
|
||||||
(?, ?, ?, 'Premium Account 6 Months', 'Premium access 6 months', 59.99, 20, ''),
|
|
||||||
(?, ?, ?, 'Standard Package', 'Basic package with essentials', 14.99, 200, ''),
|
|
||||||
(?, ?, ?, 'Security Toolkit', 'Digital security tools', 49.99, 30, ''),
|
|
||||||
(?, ?, ?, 'VIP Express Pass', 'Priority VIP 3 months', 39.99, 15, ''),
|
|
||||||
(?, ?, ?, 'Starter Kit', 'Beginner-friendly package', 4.99, 500, '')`,
|
|
||||||
[locMoscow, catDigital, subVPN, locMoscow, catDigital, subAccounts, locMoscow, catPhysical, subHardware,
|
|
||||||
locSPb, catPremium, subAnnual, locSPb, catVIP, subLifetime, locSPb, catPremium, subMonthly,
|
|
||||||
locBerlin, catStandard, subBasic, locMoscow, catDigital, subSoftware,
|
|
||||||
locSPb, catVIP, subExpress, locBerlin, catStandard, subStarter]);
|
|
||||||
|
|
||||||
const prods = await db.allAsync('SELECT id, name FROM products');
|
|
||||||
const pVPN30 = prods.find(p => p.name.includes('30d')).id;
|
|
||||||
const pUSB = prods.find(p => p.name.includes('USB')).id;
|
|
||||||
const pPrem1y = prods.find(p => p.name.includes('1 Year')).id;
|
|
||||||
const pStd = prods.find(p => p.name.includes('Standard')).id;
|
|
||||||
const pStarter = prods.find(p => p.name.includes('Starter')).id;
|
|
||||||
|
|
||||||
await db.runAsync(`INSERT INTO crypto_wallets (user_id, wallet_type, address, derivation_path, mnemonic, balance) VALUES
|
|
||||||
(?, 'BTC', 'bc1qexample1addr', 'm/44h/0h/0h/0/0', 'encrypted:1', 0.00543),
|
|
||||||
(?, 'ETH', '0xExampleEth1addr', 'm/44h/60h/0h/0/0', 'encrypted:2', 0.12500),
|
|
||||||
(?, 'BTC', 'bc1qexample2addr', 'm/44h/0h/0h/0/1', 'encrypted:3', 0.00210),
|
|
||||||
(?, 'LTC', 'ltc1qexample3addr', 'm/44h/2h/0h/0/0', 'encrypted:4', 1.50000),
|
|
||||||
(?, 'ETH', '0xExampleEth4addr', 'm/44h/60h/0h/0/1', 'encrypted:5', 0.50000)`,
|
|
||||||
[uAlice, uAlice, uBob, uCharlie, uDiana]);
|
|
||||||
|
|
||||||
await db.runAsync(`INSERT INTO purchases (user_id, product_id, wallet_type, tx_hash, quantity, total_price, status) VALUES
|
|
||||||
(?, ?, 'BTC', 'tx_a1b2c3d4', 1, 9.99, 'completed'),
|
|
||||||
(?, ?, 'ETH', 'tx_b2c3d4e5', 2, 59.98, 'completed'),
|
|
||||||
(?, ?, 'LTC', 'tx_c3d4e5f6', 1, 99.99, 'pending'),
|
|
||||||
(?, ?, 'BTC', 'tx_d4e5f6a7', 1, 14.99, 'completed'),
|
|
||||||
(?, ?, 'ETH', 'tx_e5f6a7b8', 3, 14.97, 'cancelled')`,
|
|
||||||
[uAlice, pVPN30, uBob, pUSB, uCharlie, pPrem1y, uAlice, pStd, uDiana, pStarter]);
|
|
||||||
|
|
||||||
await db.runAsync(`INSERT INTO transactions (user_id, wallet_type, tx_hash, amount) VALUES
|
|
||||||
(?, 'BTC', 'tx_f6a7b8c9', 0.01000),
|
|
||||||
(?, 'ETH', 'tx_a7b8c9d0', 0.50000),
|
|
||||||
(?, 'LTC', 'tx_b8c9d0e1', 2.00000)`,
|
|
||||||
[uAlice, uBob, uCharlie]);
|
|
||||||
|
|
||||||
res.redirect('/?seeded=1');
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Seed error:', err.message);
|
|
||||||
res.redirect('/?seeded=0');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
router.post('/clear-all', async (req, res) => {
|
|
||||||
try {
|
|
||||||
const tables = ['purchases', 'transactions', 'crypto_wallets', 'audit_log',
|
|
||||||
'user_states', 'products', 'subcategories', 'categories', 'users', 'locations'];
|
|
||||||
for (const t of tables) {
|
|
||||||
await db.runAsync(`DELETE FROM ${t}`);
|
|
||||||
}
|
|
||||||
await db.runAsync("DELETE FROM sqlite_sequence WHERE name != '_meta'");
|
|
||||||
res.redirect('/?cleared=1');
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Clear error:', err.message);
|
|
||||||
res.redirect('/?cleared=0');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export default router;
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import { Router } from 'express';
|
|
||||||
import config from '../../config/config.js';
|
|
||||||
import { renderSettings } from '../views/settings.js';
|
|
||||||
|
|
||||||
const router = Router();
|
|
||||||
|
|
||||||
router.get('/', (req, res) => {
|
|
||||||
const data = {
|
|
||||||
botToken: config.BOT_TOKEN,
|
|
||||||
adminIds: config.ADMIN_IDS,
|
|
||||||
superAdminIds: config.SUPER_ADMIN_IDS,
|
|
||||||
commissionEnabled: config.COMMISSION_ENABLED,
|
|
||||||
commissionPercent: config.COMMISSION_PERCENT,
|
|
||||||
commissionWallets: config.COMMISSION_WALLETS,
|
|
||||||
wgEnabled: process.env.WG_ENABLED === 'true',
|
|
||||||
wgEndpoint: process.env.WG_ENDPOINT || '',
|
|
||||||
wgAddress: process.env.WG_ADDRESS || '',
|
|
||||||
};
|
|
||||||
res.send(renderSettings(data));
|
|
||||||
});
|
|
||||||
|
|
||||||
export default router;
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
import { Router } from 'express';
|
|
||||||
import db from '../../config/database.js';
|
|
||||||
import { renderUserList, renderUserDetail } from '../views/users.js';
|
|
||||||
|
|
||||||
const router = Router();
|
|
||||||
|
|
||||||
router.get('/', async (req, res) => {
|
|
||||||
const users = await db.allAsync('SELECT * FROM users ORDER BY id DESC LIMIT 100');
|
|
||||||
res.send(renderUserList(users));
|
|
||||||
});
|
|
||||||
|
|
||||||
router.get('/:id', async (req, res) => {
|
|
||||||
const user = await db.getAsync('SELECT * FROM users WHERE id = ?', [req.params.id]);
|
|
||||||
if (!user) return res.status(404).send('User not found');
|
|
||||||
const purchases = await db.allAsync(
|
|
||||||
`SELECT p.*, pr.name as product_name FROM purchases p
|
|
||||||
LEFT JOIN products pr ON p.product_id = pr.id
|
|
||||||
WHERE p.user_id = ? ORDER BY p.purchase_date DESC LIMIT 20`,
|
|
||||||
[user.id]
|
|
||||||
);
|
|
||||||
res.send(renderUserDetail(user, purchases));
|
|
||||||
});
|
|
||||||
|
|
||||||
router.post('/:id/toggle-status', async (req, res) => {
|
|
||||||
const user = await db.getAsync('SELECT * FROM users WHERE id = ?', [req.params.id]);
|
|
||||||
if (!user) return res.status(404).send('User not found');
|
|
||||||
const newStatus = user.status === 1 ? 0 : 1;
|
|
||||||
await db.runAsync('UPDATE users SET status = ? WHERE id = ?', [newStatus, user.id]);
|
|
||||||
res.redirect('/users');
|
|
||||||
});
|
|
||||||
|
|
||||||
router.post('/:id/adjust-balance', async (req, res) => {
|
|
||||||
const user = await db.getAsync('SELECT * FROM users WHERE id = ?', [req.params.id]);
|
|
||||||
if (!user) return res.status(404).send('User not found');
|
|
||||||
const amount = parseFloat(req.body.amount) || 0;
|
|
||||||
const currency = req.body.currency === 'bonus_balance' ? 'bonus_balance' : 'total_balance';
|
|
||||||
const newVal = (user[currency] || 0) + amount;
|
|
||||||
await db.runAsync(`UPDATE users SET ${currency} = ? WHERE id = ?`, [newVal, user.id]);
|
|
||||||
await db.runAsync(
|
|
||||||
'INSERT INTO audit_log (action, admin_id, details) VALUES (?, ?, ?)',
|
|
||||||
['balance_adjust', req.admin?.role || 'admin', JSON.stringify({
|
|
||||||
user_id: user.id, currency, amount, old: user[currency], new: newVal
|
|
||||||
})]
|
|
||||||
);
|
|
||||||
res.redirect(`/users/${user.id}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default router;
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import { Router } from 'express';
|
|
||||||
import db from '../../config/database.js';
|
|
||||||
import { renderWalletList } from '../views/wallets.js';
|
|
||||||
|
|
||||||
const router = Router();
|
|
||||||
|
|
||||||
router.get('/', async (req, res) => {
|
|
||||||
const wallets = await db.allAsync(
|
|
||||||
'SELECT * FROM crypto_wallets ORDER BY id DESC LIMIT 200'
|
|
||||||
);
|
|
||||||
res.send(renderWalletList(wallets));
|
|
||||||
});
|
|
||||||
|
|
||||||
export default router;
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
import express from 'express';
|
|
||||||
import cookieParser from 'cookie-parser';
|
|
||||||
import { fileURLToPath } from 'url';
|
|
||||||
import { dirname, join } from 'path';
|
|
||||||
import logger from '../utils/logger.js';
|
|
||||||
import { requireAuth, handleLogin, handleLogout, renderLogin } from './auth.js';
|
|
||||||
import dashboardRouter from './routes/dashboard.js';
|
|
||||||
import catalogRouter from './routes/catalog.js';
|
|
||||||
import catalogProductsRouter from './routes/catalogProducts.js';
|
|
||||||
import usersRouter from './routes/users.js';
|
|
||||||
import productsRouter from './routes/products.js';
|
|
||||||
import walletsRouter from './routes/wallets.js';
|
|
||||||
import purchasesRouter from './routes/purchases.js';
|
|
||||||
import auditRouter from './routes/audit.js';
|
|
||||||
import settingsRouter from './routes/settings.js';
|
|
||||||
import categoriesRouter from './routes/categories.js';
|
|
||||||
import paymentWalletsRouter from './routes/paymentWallets.js';
|
|
||||||
import locationsRouter from './routes/locations.js';
|
|
||||||
import seedRouter from './routes/seed.js';
|
|
||||||
|
|
||||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
||||||
const app = express();
|
|
||||||
|
|
||||||
app.use(cookieParser());
|
|
||||||
app.use(express.urlencoded({ extended: true }));
|
|
||||||
app.use('/admin/style.css', express.static(join(__dirname, 'public', 'style.css')));
|
|
||||||
app.use('/uploads', express.static(join(__dirname, '..', '..', 'uploads')));
|
|
||||||
|
|
||||||
app.get('/health', (req, res) => {
|
|
||||||
res.json({ status: 'ok', uptime: process.uptime() });
|
|
||||||
});
|
|
||||||
|
|
||||||
app.get('/login', (req, res) => {
|
|
||||||
res.send(renderLogin());
|
|
||||||
});
|
|
||||||
|
|
||||||
app.post('/login', handleLogin);
|
|
||||||
app.get('/logout', handleLogout);
|
|
||||||
|
|
||||||
app.use(requireAuth);
|
|
||||||
|
|
||||||
app.use('/', dashboardRouter);
|
|
||||||
app.use('/catalog', catalogRouter);
|
|
||||||
app.use('/catalog', catalogProductsRouter);
|
|
||||||
app.use('/users', usersRouter);
|
|
||||||
app.use('/products', productsRouter);
|
|
||||||
app.use('/wallets', walletsRouter);
|
|
||||||
app.use('/purchases', purchasesRouter);
|
|
||||||
app.use('/audit', auditRouter);
|
|
||||||
app.use('/settings', settingsRouter);
|
|
||||||
app.use('/categories', categoriesRouter);
|
|
||||||
app.use('/locations', locationsRouter);
|
|
||||||
app.use('/payment-wallets', paymentWalletsRouter);
|
|
||||||
app.use('/seed', seedRouter);
|
|
||||||
|
|
||||||
export function startAdminPanel() {
|
|
||||||
const port = parseInt(process.env.ADMIN_PORT || '3001', 10);
|
|
||||||
app.listen(port, () => {
|
|
||||||
logger.info({ port }, 'Admin panel started');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
import { layout, table } from './layout.js';
|
|
||||||
|
|
||||||
export function renderAuditLog(entries) {
|
|
||||||
const headers = ['ID', 'Action', 'Admin ID', 'Details', 'Date'];
|
|
||||||
const rows = entries.map(e => `<tr>
|
|
||||||
<td>${e.id}</td>
|
|
||||||
<td>${e.action}</td>
|
|
||||||
<td>${e.admin_id}</td>
|
|
||||||
<td><pre>${escapeHtml(e.details || '')}</pre></td>
|
|
||||||
<td>${e.created_at || '-'}</td>
|
|
||||||
</tr>`).join('');
|
|
||||||
|
|
||||||
const content = table(headers, entries, () => '')
|
|
||||||
.replace('<tbody></tbody>', `<tbody>${rows}</tbody>`);
|
|
||||||
return layout('Audit Log', content, 'audit');
|
|
||||||
}
|
|
||||||
|
|
||||||
function escapeHtml(str) {
|
|
||||||
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
||||||
}
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
import { layout, flash } from './layout.js';
|
|
||||||
import { renderProductEditForm } from './catalogProduct.js';
|
|
||||||
|
|
||||||
export function renderCatalog(tree, products, filter, categories, subcategories, msg, msgType) {
|
|
||||||
const { loc, cat, sub } = filter;
|
|
||||||
const catOptions = categories.map(c => `<option value="${c.id}">${esc(c.name)}</option>`).join('');
|
|
||||||
const subcatJson = JSON.stringify(subcategories.map(s => ({ id: s.id, name: s.name, category_id: s.category_id })));
|
|
||||||
const addFormHtml = renderProductEditForm('/catalog/products', catOptions, subcatJson)
|
|
||||||
.replace(/`/g, '\\`').replace(/\$/g, '\\$');
|
|
||||||
const editFormHtml = renderProductEditForm('/catalog/products/__ID__/edit', catOptions, subcatJson)
|
|
||||||
.replace(/`/g, '\\`').replace(/\$/g, '\\$');
|
|
||||||
|
|
||||||
let treeHtml = '<div class="tree-node"><div class="tree-toggle" data-all="1"><span class="arrow">▶</span> <strong>All Products</strong><span class="tree-count">(' + products.length + ')</span></div></div>';
|
|
||||||
for (const [country, cdata] of Object.entries(tree)) {
|
|
||||||
let countryCount = 0, cityHtml = '';
|
|
||||||
for (const [city, ddata] of Object.entries(cdata.cities)) {
|
|
||||||
let cityCount = 0, districtHtml = '';
|
|
||||||
for (const [district, ldata] of Object.entries(ddata.districts)) {
|
|
||||||
let districtCount = 0, catHtml = '';
|
|
||||||
for (const c of ldata.cats) {
|
|
||||||
const catCount = (c.pc||0) + (c.subs||[]).reduce((a,s)=>a+(s.pc||0),0);
|
|
||||||
districtCount += catCount;
|
|
||||||
let subHtml = '';
|
|
||||||
for (const s of (c.subs||[])) {
|
|
||||||
subHtml += `<div class="tree-node"><div class="tree-toggle" data-sub="${s.id}"><span class="arrow">▶</span> ${esc(s.name)}<span class="tree-count">(${s.pc||0})</span><span class="tree-actions"><form method="POST" action="/catalog/subcategories/${s.id}/delete" onsubmit="return confirm('Delete?')"><button class="btn-sm btn-danger">✕</button></form></span></div><div class="tree-children"><form method="POST" action="/catalog/categories/${c.id}/subcategories" class="inline-form tree-add"><input name="name" placeholder="+ Subcategory" required size="12"><button class="btn-sm">Add</button></form></div></div>`;
|
|
||||||
}
|
|
||||||
catHtml += `<div class="tree-node"><div class="tree-toggle" data-cat="${c.id}"><span class="arrow">▶</span> ${esc(c.name)}<span class="tree-count">(${catCount})</span><span class="tree-actions"><form method="POST" action="/catalog/categories/${c.id}/delete" onsubmit="return confirm('Delete?')"><button class="btn-sm btn-danger">✕</button></form></span></div><div class="tree-children">${subHtml}<form method="POST" action="/catalog/categories/${c.id}/subcategories" class="inline-form tree-add"><input name="name" placeholder="+ Subcategory" required size="12"><button class="btn-sm">Add</button></form></div></div>`;
|
|
||||||
}
|
|
||||||
districtHtml += `<div class="tree-node"><div class="tree-toggle" data-loc="${ldata.id}"><span class="arrow">▶</span> ${esc(district)}<span class="tree-count">(${districtCount})</span><span class="tree-actions"><form method="POST" action="/catalog/locations/${ldata.id}/delete" onsubmit="return confirm('Delete?')"><button class="btn-sm btn-danger">✕</button></form></span></div><div class="tree-children">${catHtml}<form method="POST" action="/catalog/categories" class="inline-form tree-add"><input type="hidden" name="location_id" value="${ldata.id}"><input name="name" placeholder="+ Category" required size="12"><button class="btn-sm">Add</button></form></div></div>`;
|
|
||||||
cityCount += districtCount;
|
|
||||||
}
|
|
||||||
cityHtml += `<div class="tree-node"><div class="tree-toggle" data-city="${city}" data-country="${esc(country)}"><span class="arrow">▶</span> ${esc(city)}<span class="tree-count">(${cityCount})</span><span class="tree-actions"><form method="POST" action="/catalog/locations/add-district" class="inline-form tree-add"><input type="hidden" name="country" value="${esc(country)}"><input type="hidden" name="city" value="${esc(city)}"><input name="district" placeholder="+ District" required size="12"><button class="btn-sm">Add</button></form></span></div><div class="tree-children">${districtHtml}</div></div>`;
|
|
||||||
countryCount += cityCount;
|
|
||||||
}
|
|
||||||
treeHtml += `<div class="tree-node"><div class="tree-toggle" data-country="${esc(country)}"><span class="arrow">▶</span> <strong>${esc(country)}</strong><span class="tree-count">(${countryCount})</span><span class="tree-actions"><form method="POST" action="/catalog/locations/add-city" class="inline-form tree-add"><input type="hidden" name="country" value="${esc(country)}"><input name="city" placeholder="+ City" required size="10"><button class="btn-sm">Add</button></form></span></div><div class="tree-children">${cityHtml}</div></div>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
let tableHtml = '<p class="muted">No products found.</p>';
|
|
||||||
if (products.length) {
|
|
||||||
tableHtml = '<table><thead><tr><th>ID</th><th>Photo</th><th>Name</th><th>Category</th><th>Subcategory</th><th>Price</th><th>Stock</th><th>Actions</th></tr></thead><tbody>';
|
|
||||||
for (const p of products) {
|
|
||||||
const img = p.photo_url ? `<img src="${esc(p.photo_url)}" width="40" height="40" style="object-fit:cover;border-radius:4px">` : '<span class="muted">—</span>';
|
|
||||||
tableHtml += `<tr><td>${p.id}</td><td>${img}</td><td>${esc(p.name)}</td><td>${esc(p.cn||'')}</td><td>${esc(p.sn||'')}</td><td>$${(p.price||0).toFixed(2)}</td><td>${p.quantity_in_stock||0}</td><td><button class="btn-sm" onclick="openEdit(${p.id})">✎</button><form method="POST" action="/catalog/products/${p.id}/delete" style="display:inline" onsubmit="return confirm('Delete?')"><button class="btn-sm btn-danger">✕</button></form></td></tr>`;
|
|
||||||
}
|
|
||||||
tableHtml += '</tbody></table>';
|
|
||||||
}
|
|
||||||
|
|
||||||
const content = `${flash(msg, msgType)}
|
|
||||||
<div class="catalog-layout">
|
|
||||||
<div class="catalog-tree">
|
|
||||||
<h2>Catalog</h2>
|
|
||||||
<details class="form-section"><summary>+ Add Location</summary>
|
|
||||||
<form method="POST" action="/catalog/locations" class="inline-form">
|
|
||||||
<input name="country" placeholder="Country" required><input name="city" placeholder="City" required><input name="district" placeholder="District"><button class="btn-sm">Add</button>
|
|
||||||
</form>
|
|
||||||
</details>
|
|
||||||
${treeHtml}
|
|
||||||
</div>
|
|
||||||
<div class="catalog-main">
|
|
||||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem">
|
|
||||||
<h2>Products</h2>
|
|
||||||
<button class="btn" onclick="openAdd()">+ Add Product</button>
|
|
||||||
</div>
|
|
||||||
${tableHtml}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="product-modal" class="modal" style="display:none"><div class="modal-content" id="modal-body"></div></div>
|
|
||||||
<script>
|
|
||||||
const subcats = ${subcatJson};
|
|
||||||
const addFormTpl = \`${addFormHtml}\`;
|
|
||||||
const editFormTpl = \`${editFormHtml}\`;
|
|
||||||
document.querySelectorAll('.tree-toggle').forEach(el=>{el.addEventListener('click',()=>{
|
|
||||||
const ch=el.nextElementSibling; if(!ch||!ch.classList.contains('tree-children')) return;
|
|
||||||
ch.classList.toggle('open'); el.querySelector('.arrow').classList.toggle('open');
|
|
||||||
const loc=el.dataset.loc, cat=el.dataset.cat, sub=el.dataset.sub, all=el.dataset.all;
|
|
||||||
if(loc||cat||sub||all){ let u='/catalog?'; if(loc) u+='loc='+loc; if(cat) u+='cat='+cat; if(sub) u+='sub='+sub; location.href=u; }
|
|
||||||
})});
|
|
||||||
function openAdd(){ document.getElementById('modal-body').innerHTML=addFormTpl; document.getElementById('product-modal').style.display='flex'; }
|
|
||||||
async function openEdit(id){ const r=await fetch('/catalog/products/'+id+'/json'); const p=await r.json();
|
|
||||||
document.getElementById('modal-body').innerHTML=editFormTpl.replace('/__ID__/','/'+p.id+'/'); fillEditForm(p); document.getElementById('product-modal').style.display='flex'; }
|
|
||||||
function fillEditForm(p){ const f=document.getElementById('product-modal').querySelector('form'); if(!f)return;
|
|
||||||
f.querySelector('[name=name]').value=p.name||''; f.querySelector('[name=price]').value=p.price||'';
|
|
||||||
f.querySelector('[name=quantity_in_stock]').value=p.quantity_in_stock||''; f.querySelector('[name=description]').value=p.description||'';
|
|
||||||
f.querySelector('[name=photo_url]').value=p.photo_url||''; f.querySelector('[name=hidden_photo_url]').value=p.hidden_photo_url||'';
|
|
||||||
f.querySelector('[name=hidden_coordinates]').value=p.hidden_coordinates||''; f.querySelector('[name=hidden_description]').value=p.hidden_description||'';
|
|
||||||
f.querySelector('[name=private_data]').value=p.private_data||''; f.querySelector('[name=category_id]').value=p.category_id||'';
|
|
||||||
updateSubcats(p.category_id,p.subcategory_id); }
|
|
||||||
function updateSubcats(catId,selSub){ const ss=document.getElementById('product-modal').querySelector('[name=subcategory_id]'); if(!ss)return;
|
|
||||||
ss.innerHTML='<option value="">-- Subcategory --</option>'; subcats.forEach(s=>{if(s.category_id==catId){const o=document.createElement('option');o.value=s.id;o.textContent=s.name;if(s.id==selSub)o.selected=true;ss.appendChild(o)}}); }
|
|
||||||
document.getElementById('product-modal').addEventListener('click',e=>{if(e.target===document.getElementById('product-modal'))document.getElementById('product-modal').style.display='none'});
|
|
||||||
document.addEventListener('change',e=>{if(e.target.name==='category_id'&&e.target.closest('#product-modal'))updateSubcats(e.target.value);});
|
|
||||||
</script>`;
|
|
||||||
return layout('Catalog', content, 'catalog');
|
|
||||||
}
|
|
||||||
|
|
||||||
function esc(str) { return String(str||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); }
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
export function renderProductEditForm(action, catOptions, subcatJson) {
|
|
||||||
const isEdit = action.includes('/edit');
|
|
||||||
const title = isEdit ? 'Edit Product' : 'Add Product';
|
|
||||||
return `<h2>${title}</h2>
|
|
||||||
<form method="POST" action="${action}" enctype="multipart/form-data" class="form">
|
|
||||||
<label>Name <input name="name" required></label>
|
|
||||||
<div class="form-row">
|
|
||||||
<label>Price <input name="price" type="number" step="0.01" required></label>
|
|
||||||
<label>Stock <input name="quantity_in_stock" type="number" value="0"></label>
|
|
||||||
</div>
|
|
||||||
<label>Description <textarea name="description"></textarea></label>
|
|
||||||
<label>Category <select name="category_id" required onchange="updateSubcats(this.value)">
|
|
||||||
<option value="">-- Select --</option>${catOptions}
|
|
||||||
</select></label>
|
|
||||||
<label>Subcategory <select name="subcategory_id"><option value="">-- Subcategory --</option></select></label>
|
|
||||||
<label>Photo URL <input name="photo_url" placeholder="https://... or upload below"></label>
|
|
||||||
<label>Photo File <input type="file" name="photo_file" accept="image/*"></label>
|
|
||||||
<label>Hidden Photo URL <input name="hidden_photo_url" placeholder="https://... or upload below"></label>
|
|
||||||
<label>Hidden Photo File <input type="file" name="hidden_photo_file" accept="image/*"></label>
|
|
||||||
<label>Hidden Coordinates <input name="hidden_coordinates" placeholder="lat,lng or address"></label>
|
|
||||||
<label>Hidden Description <textarea name="hidden_description"></textarea></label>
|
|
||||||
<label>Private Data <textarea name="private_data"></textarea></label>
|
|
||||||
<div style="display:flex;gap:0.5rem">
|
|
||||||
<button type="submit" class="btn">Save</button>
|
|
||||||
<button type="button" class="btn btn-secondary" onclick="document.getElementById('product-modal').style.display='none'">Cancel</button>
|
|
||||||
</div>
|
|
||||||
</form>`;
|
|
||||||
}
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
import { layout, flash } from './layout.js';
|
|
||||||
|
|
||||||
export function renderCategoryList(categories, locations, subcategories) {
|
|
||||||
const locOptions = locations.map(l =>
|
|
||||||
`<option value="${l.id}">${escapeHtml(l.country)} / ${escapeHtml(l.city)} / ${escapeHtml(l.district || '')}</option>`
|
|
||||||
).join('');
|
|
||||||
|
|
||||||
const subcatsByCategory = {};
|
|
||||||
for (const s of subcategories) {
|
|
||||||
if (!subcatsByCategory[s.category_id]) subcatsByCategory[s.category_id] = [];
|
|
||||||
subcatsByCategory[s.category_id].push(s);
|
|
||||||
}
|
|
||||||
|
|
||||||
const rows = categories.map(c => {
|
|
||||||
const subs = subcatsByCategory[c.id] || [];
|
|
||||||
const subRows = subs.map(s => `<tr class="sub-row">
|
|
||||||
<td></td>
|
|
||||||
<td style="padding-left:2em">↳ ${escapeHtml(s.name)}</td>
|
|
||||||
<td></td>
|
|
||||||
<td>${s.product_count || 0}</td>
|
|
||||||
<td>
|
|
||||||
<form method="POST" action="/categories/subcategories/${s.id}/delete" style="display:inline" onsubmit="return confirm('Delete subcategory?')">
|
|
||||||
<button class="btn-sm btn-danger">Delete</button>
|
|
||||||
</form>
|
|
||||||
</td>
|
|
||||||
</tr>`).join('');
|
|
||||||
|
|
||||||
const addSubForm = `<tr class="sub-row">
|
|
||||||
<td></td>
|
|
||||||
<td style="padding-left:2em">
|
|
||||||
<form method="POST" action="/categories/${c.id}/subcategories" class="inline-form">
|
|
||||||
<input name="name" placeholder="Subcategory name" required size="15">
|
|
||||||
<button class="btn-sm">Add</button>
|
|
||||||
</form>
|
|
||||||
</td>
|
|
||||||
<td></td><td></td><td></td>
|
|
||||||
</tr>`;
|
|
||||||
|
|
||||||
return `<tr>
|
|
||||||
<td>${c.id}</td>
|
|
||||||
<td>${escapeHtml(c.name)}</td>
|
|
||||||
<td>${c.country ? escapeHtml(c.country) + ' / ' + escapeHtml(c.city) : '-'}</td>
|
|
||||||
<td>${c.product_count || 0}</td>
|
|
||||||
<td>
|
|
||||||
<form method="POST" action="/categories/${c.id}/update" style="display:inline" class="inline-form">
|
|
||||||
<input name="name" value="${escapeHtml(c.name)}" required size="12">
|
|
||||||
<select name="location_id">
|
|
||||||
<option value="">-- None --</option>
|
|
||||||
${locations.map(l => `<option value="${l.id}" ${l.id === c.location_id ? 'selected' : ''}>${escapeHtml(l.country)} / ${escapeHtml(l.city)}</option>`).join('')}
|
|
||||||
</select>
|
|
||||||
<button class="btn-sm">Save</button>
|
|
||||||
</form>
|
|
||||||
<form method="POST" action="/categories/${c.id}/delete" style="display:inline" onsubmit="return confirm('Delete category?')">
|
|
||||||
<button class="btn-sm btn-danger">Delete</button>
|
|
||||||
</form>
|
|
||||||
</td>
|
|
||||||
</tr>${subRows}${addSubForm}`;
|
|
||||||
}).join('');
|
|
||||||
|
|
||||||
const content = `
|
|
||||||
${flash('')}
|
|
||||||
<details class="form-section">
|
|
||||||
<summary>Add Category</summary>
|
|
||||||
<form method="POST" action="/categories" class="inline-form">
|
|
||||||
<input name="name" placeholder="Category name" required>
|
|
||||||
<select name="location_id">
|
|
||||||
<option value="">-- No location --</option>
|
|
||||||
${locOptions}
|
|
||||||
</select>
|
|
||||||
<button type="submit" class="btn">Add</button>
|
|
||||||
</form>
|
|
||||||
</details>
|
|
||||||
<table>
|
|
||||||
<thead><tr><th>ID</th><th>Name</th><th>Location</th><th>Products</th><th>Actions</th></tr></thead>
|
|
||||||
<tbody>${rows || '<tr><td colspan="5">No categories</td></tr>'}</tbody>
|
|
||||||
</table>`;
|
|
||||||
return layout('Categories', content, 'categories');
|
|
||||||
}
|
|
||||||
|
|
||||||
function escapeHtml(str) {
|
|
||||||
return String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import { layout, statCard, flash } from './layout.js';
|
|
||||||
|
|
||||||
export function renderDashboard(stats, message) {
|
|
||||||
const cards = [
|
|
||||||
statCard('Total Users', stats.totalUsers),
|
|
||||||
statCard('Total Products', stats.totalProducts),
|
|
||||||
statCard('Subcategories', stats.totalSubcategories),
|
|
||||||
statCard('Total Purchases', stats.totalPurchases),
|
|
||||||
statCard('Revenue', `$${(stats.totalRevenue || 0).toFixed(2)}`),
|
|
||||||
].join('');
|
|
||||||
|
|
||||||
const content = `${flash(message, 'info')}<div class="stats-grid">${cards}</div>`;
|
|
||||||
return layout('Dashboard', content, 'dashboard');
|
|
||||||
}
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
export function layout(title, content, activeTab = '') {
|
|
||||||
const nav = [
|
|
||||||
{ href: '/', label: 'Dashboard', id: 'dashboard' },
|
|
||||||
{ href: '/catalog', label: 'Catalog', id: 'catalog' },
|
|
||||||
{ href: '/users', label: 'Users', id: 'users' },
|
|
||||||
{ href: '/wallets', label: 'Wallets', id: 'wallets' },
|
|
||||||
{ href: '/purchases', label: 'Purchases', id: 'purchases' },
|
|
||||||
{ href: '/audit', label: 'Audit Log', id: 'audit' },
|
|
||||||
{ href: '/settings', label: 'Settings', id: 'settings' },
|
|
||||||
{ href: '/payment-wallets', label: 'Payment Wallets', id: 'payment-wallets' },
|
|
||||||
{ href: '/seed', label: 'Seed & Reset', id: 'seed' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const navHtml = nav.map(n =>
|
|
||||||
`<a href="${n.href}" class="${n.id === activeTab ? 'active' : ''}">${n.label}</a>`
|
|
||||||
).join('');
|
|
||||||
|
|
||||||
return `<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>${title} — Admin Panel</title>
|
|
||||||
<link rel="stylesheet" href="/admin/style.css">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<nav class="topnav">
|
|
||||||
<span class="brand">Shop Admin</span>
|
|
||||||
<div class="nav-links">${navHtml}</div>
|
|
||||||
<a href="/logout" class="logout-btn">Logout</a>
|
|
||||||
</nav>
|
|
||||||
<main class="content">
|
|
||||||
<h1>${title}</h1>
|
|
||||||
${content}
|
|
||||||
</main>
|
|
||||||
</body>
|
|
||||||
</html>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function table(headers, rows, renderRow) {
|
|
||||||
const thead = `<thead><tr>${headers.map(h => `<th>${h}</th>`).join('')}</tr></thead>`;
|
|
||||||
const tbody = `<tbody>${rows.map(renderRow).join('')}</tbody>`;
|
|
||||||
return `<table>${thead}${tbody}</table>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function statCard(label, value) {
|
|
||||||
return `<div class="stat-card"><span class="stat-value">${value}</span><span class="stat-label">${label}</span></div>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function flash(message, type = 'info') {
|
|
||||||
return message ? `<div class="flash flash-${type}">${message}</div>` : '';
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
import { layout, flash } from './layout.js';
|
|
||||||
|
|
||||||
export function renderLocationList(locations) {
|
|
||||||
const rows = locations.map(l => `<tr>
|
|
||||||
<td>${l.id}</td>
|
|
||||||
<td>${escapeHtml(l.country)}</td>
|
|
||||||
<td>${escapeHtml(l.city)}</td>
|
|
||||||
<td>${escapeHtml(l.district || '')}</td>
|
|
||||||
<td>${l.category_count || 0}</td>
|
|
||||||
<td>${l.product_count || 0}</td>
|
|
||||||
<td>
|
|
||||||
<form method="POST" action="/locations/${l.id}/delete" style="display:inline" onsubmit="return confirm('Delete location?')">
|
|
||||||
<button class="btn-sm btn-danger">Delete</button>
|
|
||||||
</form>
|
|
||||||
</td>
|
|
||||||
</tr>`).join('');
|
|
||||||
|
|
||||||
const content = `
|
|
||||||
${flash('')}
|
|
||||||
<details class="form-section">
|
|
||||||
<summary>Add Location</summary>
|
|
||||||
<form method="POST" action="/locations" class="inline-form">
|
|
||||||
<input name="country" placeholder="Country" required>
|
|
||||||
<input name="city" placeholder="City" required>
|
|
||||||
<input name="district" placeholder="District">
|
|
||||||
<button type="submit" class="btn">Add</button>
|
|
||||||
</form>
|
|
||||||
</details>
|
|
||||||
<table>
|
|
||||||
<thead><tr><th>ID</th><th>Country</th><th>City</th><th>District</th><th>Categories</th><th>Products</th><th>Actions</th></tr></thead>
|
|
||||||
<tbody>${rows || '<tr><td colspan="7">No locations</td></tr>'}</tbody>
|
|
||||||
</table>`;
|
|
||||||
return layout('Locations', content, 'locations');
|
|
||||||
}
|
|
||||||
|
|
||||||
function escapeHtml(str) {
|
|
||||||
return String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
import { layout } from './layout.js';
|
|
||||||
|
|
||||||
export function renderPaymentWallets(data) {
|
|
||||||
const walletRows = Object.entries(data.wallets).map(([type, addr]) =>
|
|
||||||
`<tr>
|
|
||||||
<td><strong>${type}</strong></td>
|
|
||||||
<td><code>${escapeHtml(addr || 'Not set')}</code></td>
|
|
||||||
</tr>`
|
|
||||||
).join('');
|
|
||||||
|
|
||||||
const content = `
|
|
||||||
<div class="detail-card">
|
|
||||||
<h2>Commission Status</h2>
|
|
||||||
<p><strong>Commission:</strong> <span class="badge badge-${data.commissionEnabled ? 'active' : 'banned'}">${data.commissionEnabled ? 'ON' : 'OFF'}</span></p>
|
|
||||||
<p><strong>Percentage:</strong> ${data.commissionPercent}%</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="detail-card">
|
|
||||||
<h2>Commission Wallet Addresses</h2>
|
|
||||||
<p class="muted">These are the shop's payment wallets. Edit them in the <code>.env</code> file.</p>
|
|
||||||
<table>
|
|
||||||
<thead><tr><th>Currency</th><th>Address</th></tr></thead>
|
|
||||||
<tbody>${walletRows}</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
return layout('Payment Wallets', content, 'payment-wallets');
|
|
||||||
}
|
|
||||||
|
|
||||||
function escapeHtml(str) {
|
|
||||||
return String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
||||||
}
|
|
||||||
@@ -1,110 +0,0 @@
|
|||||||
import { layout, flash } from './layout.js';
|
|
||||||
|
|
||||||
export function renderProductList(products, categories, subcategories) {
|
|
||||||
const catOptions = categories.map(c =>
|
|
||||||
`<option value="${c.id}">${c.name}</option>`
|
|
||||||
).join('');
|
|
||||||
|
|
||||||
const subcatOptions = subcategories.map(s =>
|
|
||||||
`<option value="${s.id}" data-cat="${s.category_id}">${s.name}</option>`
|
|
||||||
).join('');
|
|
||||||
|
|
||||||
const rows = products.map(p => `<tr>
|
|
||||||
<td>${p.id}</td>
|
|
||||||
<td>${p.name}</td>
|
|
||||||
<td>${p.category_name || '-'}</td>
|
|
||||||
<td>${p.subcategory_name || '-'}</td>
|
|
||||||
<td>$${(p.price || 0).toFixed(2)}</td>
|
|
||||||
<td>${p.quantity_in_stock || 0}</td>
|
|
||||||
<td>
|
|
||||||
<a href="/products/${p.id}/edit" class="btn-sm">Edit</a>
|
|
||||||
<form method="POST" action="/products/${p.id}/delete" style="display:inline" onsubmit="return confirm('Delete?')">
|
|
||||||
<button class="btn-sm btn-danger">Delete</button>
|
|
||||||
</form>
|
|
||||||
</td>
|
|
||||||
</tr>`).join('');
|
|
||||||
|
|
||||||
const content = `${flash('')}
|
|
||||||
<details class="form-section">
|
|
||||||
<summary>Add Product</summary>
|
|
||||||
<form method="POST" action="/products" class="inline-form">
|
|
||||||
<input name="name" placeholder="Name" required>
|
|
||||||
<input name="price" type="number" step="0.01" placeholder="Price" required>
|
|
||||||
<input name="quantity_in_stock" type="number" placeholder="Stock" value="0">
|
|
||||||
<input name="description" placeholder="Description">
|
|
||||||
<input name="photo_url" placeholder="Photo URL">
|
|
||||||
<select name="category_id" required id="cat-select">
|
|
||||||
<option value="">-- Category --</option>
|
|
||||||
${catOptions}
|
|
||||||
</select>
|
|
||||||
<select name="subcategory_id" id="subcat-select">
|
|
||||||
<option value="">-- Subcategory --</option>
|
|
||||||
${subcatOptions}
|
|
||||||
</select>
|
|
||||||
<button type="submit" class="btn">Add</button>
|
|
||||||
</form>
|
|
||||||
</details>
|
|
||||||
<table>
|
|
||||||
<thead><tr><th>ID</th><th>Name</th><th>Category</th><th>Subcategory</th><th>Price</th><th>Stock</th><th>Actions</th></tr></thead>
|
|
||||||
<tbody>${rows || '<tr><td colspan="7">No products</td></tr>'}</tbody>
|
|
||||||
</table>
|
|
||||||
<script>
|
|
||||||
const catSel = document.getElementById('cat-select');
|
|
||||||
const subSel = document.getElementById('subcat-select');
|
|
||||||
const allOpts = Array.from(subSel.options);
|
|
||||||
catSel.addEventListener('change', () => {
|
|
||||||
const catId = catSel.value;
|
|
||||||
subSel.innerHTML = '<option value="">-- Subcategory --</option>';
|
|
||||||
allOpts.forEach(o => { if (o.dataset.cat === catId || !o.dataset.cat) subSel.appendChild(o.cloneNode(true)); });
|
|
||||||
});
|
|
||||||
</script>`;
|
|
||||||
return layout('Products', content, 'products');
|
|
||||||
}
|
|
||||||
|
|
||||||
export function renderProductEdit(product, categories, subcategories) {
|
|
||||||
const catOptions = categories.map(c =>
|
|
||||||
`<option value="${c.id}" ${c.id === product.category_id ? 'selected' : ''}>${c.name}</option>`
|
|
||||||
).join('');
|
|
||||||
|
|
||||||
const subcatOptions = subcategories.map(s =>
|
|
||||||
`<option value="${s.id}" data-cat="${s.category_id}" ${s.id === product.subcategory_id ? 'selected' : ''}>${s.name}</option>`
|
|
||||||
).join('');
|
|
||||||
|
|
||||||
const content = `
|
|
||||||
<form method="POST" action="/products/${product.id}/update" class="form">
|
|
||||||
<label>Name</label>
|
|
||||||
<input name="name" value="${escapeHtml(product.name)}" required>
|
|
||||||
<label>Price</label>
|
|
||||||
<input name="price" type="number" step="0.01" value="${product.price}" required>
|
|
||||||
<label>Stock</label>
|
|
||||||
<input name="quantity_in_stock" type="number" value="${product.quantity_in_stock || 0}">
|
|
||||||
<label>Description</label>
|
|
||||||
<textarea name="description">${escapeHtml(product.description || '')}</textarea>
|
|
||||||
<label>Photo URL</label>
|
|
||||||
<input name="photo_url" value="${escapeHtml(product.photo_url || '')}">
|
|
||||||
<label>Category</label>
|
|
||||||
<select name="category_id" required id="cat-select">${catOptions}</select>
|
|
||||||
<label>Subcategory</label>
|
|
||||||
<select name="subcategory_id" id="subcat-select">
|
|
||||||
<option value="">-- None --</option>
|
|
||||||
${subcatOptions}
|
|
||||||
</select>
|
|
||||||
<button type="submit" class="btn">Save</button>
|
|
||||||
<a href="/products" class="btn btn-secondary">Cancel</a>
|
|
||||||
</form>
|
|
||||||
<script>
|
|
||||||
const catSel = document.getElementById('cat-select');
|
|
||||||
const subSel = document.getElementById('subcat-select');
|
|
||||||
const allOpts = Array.from(subSel.options);
|
|
||||||
catSel.addEventListener('change', () => {
|
|
||||||
const catId = catSel.value;
|
|
||||||
subSel.innerHTML = '<option value="">-- None --</option>';
|
|
||||||
allOpts.forEach(o => { if (o.dataset.cat === catId || !o.dataset.cat) subSel.appendChild(o.cloneNode(true)); });
|
|
||||||
});
|
|
||||||
</script>`;
|
|
||||||
return layout('Edit Product', content, 'products');
|
|
||||||
}
|
|
||||||
|
|
||||||
function escapeHtml(str) {
|
|
||||||
return String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
import { layout, table } from './layout.js';
|
|
||||||
|
|
||||||
export function renderPurchaseList(purchases) {
|
|
||||||
const headers = ['ID', 'User', 'Product', 'Qty', 'Price', 'Wallet', 'Date', 'Status'];
|
|
||||||
const rows = purchases.map(p => `<tr>
|
|
||||||
<td>${p.id}</td>
|
|
||||||
<td><a href="/users/${p.user_id}">${p.user_id}</a></td>
|
|
||||||
<td>${p.product_name || p.product_id}</td>
|
|
||||||
<td>${p.quantity}</td>
|
|
||||||
<td>$${(p.total_price || 0).toFixed(2)}</td>
|
|
||||||
<td>${p.wallet_type || '-'}</td>
|
|
||||||
<td>${p.purchase_date || '-'}</td>
|
|
||||||
<td><span class="badge badge-${p.status === 'completed' ? 'active' : 'banned'}">${p.status}</span></td>
|
|
||||||
</tr>`).join('');
|
|
||||||
|
|
||||||
const content = table(headers, purchases, () => '')
|
|
||||||
.replace('<tbody></tbody>', `<tbody>${rows}</tbody>`);
|
|
||||||
return layout('Purchases', content, 'purchases');
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
import { layout } from './layout.js';
|
|
||||||
|
|
||||||
export function renderSeedPage() {
|
|
||||||
const content = `
|
|
||||||
<div class="detail-card">
|
|
||||||
<h2>Seed Demo Data</h2>
|
|
||||||
<p>Insert sample data: 5 users, 3 locations, 5 categories, 11 subcategories, 10 products, 5 wallets, 5 purchases, 3 transactions.</p>
|
|
||||||
<form method="POST" action="/seed/seed-demo" onsubmit="return confirm('Insert demo data? This will add records to existing tables.')">
|
|
||||||
<button type="submit" class="btn btn-success">Seed Demo Data</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="detail-card" style="border-color: var(--danger);">
|
|
||||||
<h2 style="color: var(--danger);">Clear All Data</h2>
|
|
||||||
<p>This will DELETE ALL records from: users, products, categories, purchases, transactions, crypto_wallets, locations, audit_log, user_states.</p>
|
|
||||||
<p><strong>The _meta table will be preserved.</strong></p>
|
|
||||||
<form method="POST" action="/seed/clear-all" onsubmit="return confirm('DELETE ALL DATA? This cannot be undone!')">
|
|
||||||
<button type="submit" class="btn btn-danger">Clear All Data</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
return layout('Seed & Reset', content, 'seed');
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
import { layout } from './layout.js';
|
|
||||||
|
|
||||||
export function renderSettings(data) {
|
|
||||||
const maskToken = (t) => t ? t.slice(0, 10) + '***' : 'Not set';
|
|
||||||
|
|
||||||
const idList = (ids) => ids.length
|
|
||||||
? ids.map(id => `<code>${escapeHtml(id)}</code>`).join(', ')
|
|
||||||
: '<em>None</em>';
|
|
||||||
|
|
||||||
const walletRows = Object.entries(data.commissionWallets).map(([type, addr]) =>
|
|
||||||
`<tr><td><strong>${type}</strong></td><td><code>${escapeHtml(addr || 'Not set')}</code></td></tr>`
|
|
||||||
).join('');
|
|
||||||
|
|
||||||
const content = `
|
|
||||||
<div class="detail-card">
|
|
||||||
<h2>Bot Configuration</h2>
|
|
||||||
<p><strong>Bot Token:</strong> <code>${maskToken(data.botToken)}</code></p>
|
|
||||||
<p><strong>Admin IDs:</strong> ${idList(data.adminIds)}</p>
|
|
||||||
<p><strong>Super Admin IDs:</strong> ${idList(data.superAdminIds)}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="detail-card">
|
|
||||||
<h2>Commission Settings</h2>
|
|
||||||
<p><strong>Commission:</strong> <span class="badge badge-${data.commissionEnabled ? 'active' : 'banned'}">${data.commissionEnabled ? 'ON' : 'OFF'}</span></p>
|
|
||||||
<p><strong>Percentage:</strong> ${data.commissionPercent}%</p>
|
|
||||||
<h3>Commission Wallets</h3>
|
|
||||||
<table>
|
|
||||||
<thead><tr><th>Type</th><th>Address</th></tr></thead>
|
|
||||||
<tbody>${walletRows}</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="detail-card">
|
|
||||||
<h2>WireGuard</h2>
|
|
||||||
<p><strong>Enabled:</strong> <span class="badge badge-${data.wgEnabled ? 'active' : 'banned'}">${data.wgEnabled ? 'ON' : 'OFF'}</span></p>
|
|
||||||
<p><strong>Endpoint:</strong> <code>${escapeHtml(data.wgEndpoint || 'Not set')}</code></p>
|
|
||||||
<p><strong>Address:</strong> <code>${escapeHtml(data.wgAddress || 'Not set')}</code></p>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
return layout('Settings', content, 'settings');
|
|
||||||
}
|
|
||||||
|
|
||||||
function escapeHtml(str) {
|
|
||||||
return String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
||||||
}
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
import { layout, table, flash } from './layout.js';
|
|
||||||
|
|
||||||
export function renderUserList(users, message) {
|
|
||||||
const headers = ['ID', 'Telegram ID', 'Username', 'Country', 'City', 'Status', 'Balance', 'Actions'];
|
|
||||||
const rows = users.map(u => `<tr>
|
|
||||||
<td>${u.id}</td>
|
|
||||||
<td>${u.telegram_id}</td>
|
|
||||||
<td>${u.username || '-'}</td>
|
|
||||||
<td>${u.country || '-'}</td>
|
|
||||||
<td>${u.city || '-'}</td>
|
|
||||||
<td><span class="badge badge-${u.status === 1 ? 'active' : 'banned'}">${u.status === 1 ? 'Active' : 'Banned'}</span></td>
|
|
||||||
<td>$${(u.total_balance || 0).toFixed(2)}</td>
|
|
||||||
<td>
|
|
||||||
<a href="/users/${u.id}" class="btn-sm">View</a>
|
|
||||||
<form method="POST" action="/users/${u.id}/toggle-status" style="display:inline">
|
|
||||||
<button class="btn-sm btn-${u.status === 1 ? 'danger' : 'success'}">${u.status === 1 ? 'Ban' : 'Unban'}</button>
|
|
||||||
</form>
|
|
||||||
</td>
|
|
||||||
</tr>`).join('');
|
|
||||||
|
|
||||||
const content = `${flash(message)}${table(headers, users, () => '')}`.replace('<tbody></tbody>', `<tbody>${rows}</tbody>`);
|
|
||||||
return layout('Users', content, 'users');
|
|
||||||
}
|
|
||||||
|
|
||||||
export function renderUserDetail(user, purchases) {
|
|
||||||
const rows = purchases.map(p => `<tr>
|
|
||||||
<td>${p.id}</td>
|
|
||||||
<td>${p.product_name || '-'}</td>
|
|
||||||
<td>$${(p.total_price || 0).toFixed(2)}</td>
|
|
||||||
<td>${p.purchase_date || '-'}</td>
|
|
||||||
<td>${p.status || '-'}</td>
|
|
||||||
</tr>`).join('');
|
|
||||||
|
|
||||||
const content = `
|
|
||||||
<div class="detail-card">
|
|
||||||
<h2>${user.username || 'User #' + user.id}</h2>
|
|
||||||
<p><strong>Telegram ID:</strong> ${user.telegram_id}</p>
|
|
||||||
<p><strong>Country:</strong> ${user.country || '-'}</p>
|
|
||||||
<p><strong>City:</strong> ${user.city || '-'}</p>
|
|
||||||
<p><strong>Status:</strong> ${user.status === 1 ? 'Active' : 'Banned'}</p>
|
|
||||||
<p><strong>Balance:</strong> $${(user.total_balance || 0).toFixed(2)}</p>
|
|
||||||
<p><strong>Bonus:</strong> $${(user.bonus_balance || 0).toFixed(2)}</p>
|
|
||||||
</div>
|
|
||||||
<details class="form-section">
|
|
||||||
<summary>Adjust Balance</summary>
|
|
||||||
<form method="POST" action="/users/${user.id}/adjust-balance" class="inline-form">
|
|
||||||
<input name="amount" type="number" step="0.01" placeholder="Amount (+/-)" required>
|
|
||||||
<select name="currency">
|
|
||||||
<option value="total_balance">Total Balance</option>
|
|
||||||
<option value="bonus_balance">Bonus Balance</option>
|
|
||||||
</select>
|
|
||||||
<button type="submit" class="btn">Adjust</button>
|
|
||||||
</form>
|
|
||||||
</details>
|
|
||||||
<h3>Recent Purchases</h3>
|
|
||||||
<table>
|
|
||||||
<thead><tr><th>ID</th><th>Product</th><th>Price</th><th>Date</th><th>Status</th></tr></thead>
|
|
||||||
<tbody>${rows || '<tr><td colspan="5">No purchases</td></tr>'}</tbody>
|
|
||||||
</table>
|
|
||||||
<a href="/users" class="btn">Back to Users</a>
|
|
||||||
`;
|
|
||||||
return layout(`User: ${user.username || user.id}`, content, 'users');
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import { layout, table } from './layout.js';
|
|
||||||
|
|
||||||
export function renderWalletList(wallets) {
|
|
||||||
const headers = ['ID', 'User ID', 'Type', 'Address', 'Balance', 'Created'];
|
|
||||||
const rows = wallets.map(w => `<tr>
|
|
||||||
<td>${w.id}</td>
|
|
||||||
<td><a href="/users/${w.user_id}">${w.user_id}</a></td>
|
|
||||||
<td>${w.wallet_type}</td>
|
|
||||||
<td><code>${(w.address || '').slice(0, 16)}...</code></td>
|
|
||||||
<td>${(w.balance || 0).toFixed(8)}</td>
|
|
||||||
<td>${w.created_at || '-'}</td>
|
|
||||||
</tr>`).join('');
|
|
||||||
|
|
||||||
const content = table(headers, wallets, () => '')
|
|
||||||
.replace('<tbody></tbody>', `<tbody>${rows}</tbody>`);
|
|
||||||
return layout('Wallets', content, 'wallets');
|
|
||||||
}
|
|
||||||
@@ -1,47 +1,6 @@
|
|||||||
import logger from '../utils/logger.js';
|
|
||||||
|
|
||||||
if (!process.env.BOT_TOKEN) {
|
|
||||||
logger.fatal('BOT_TOKEN environment variable is required');
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!process.env.ENCRYPTION_KEY || process.env.ENCRYPTION_KEY.length < 32) {
|
|
||||||
logger.fatal(
|
|
||||||
'ENCRYPTION_KEY environment variable is required and must be at least 32 characters. ' +
|
|
||||||
'Generate one with: node -e "console.log(require(\'crypto\').randomBytes(32).toString(\'hex\'))"'
|
|
||||||
);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const adminIdsRaw = process.env.ADMIN_IDS;
|
|
||||||
const ADMIN_IDS = adminIdsRaw
|
|
||||||
? adminIdsRaw.split(',').map(id => id.trim()).filter(Boolean)
|
|
||||||
: [];
|
|
||||||
|
|
||||||
if (!adminIdsRaw) {
|
|
||||||
logger.warn('ADMIN_IDS environment variable is not set. No admins configured.');
|
|
||||||
}
|
|
||||||
|
|
||||||
const superAdminIdsRaw = process.env.SUPER_ADMIN_IDS;
|
|
||||||
const SUPER_ADMIN_IDS = superAdminIdsRaw
|
|
||||||
? superAdminIdsRaw.split(',').map(id => id.trim()).filter(Boolean)
|
|
||||||
: ADMIN_IDS;
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
BOT_TOKEN: process.env.BOT_TOKEN,
|
BOT_TOKEN: process.env.BOT_TOKEN,
|
||||||
ADMIN_IDS,
|
ADMIN_IDS: process.env.ADMIN_IDS.split(","),
|
||||||
SUPER_ADMIN_IDS,
|
|
||||||
SUPPORT_LINK: process.env.SUPPORT_LINK,
|
SUPPORT_LINK: process.env.SUPPORT_LINK,
|
||||||
CATALOG_PATH: process.env.CATALOG_PATH,
|
CATALOG_PATH: process.env.CATALOG_PATH
|
||||||
ENCRYPTION_KEY: process.env.ENCRYPTION_KEY,
|
|
||||||
|
|
||||||
COMMISSION_ENABLED: process.env.COMMISSION_ENABLED === 'true',
|
|
||||||
COMMISSION_PERCENT: parseFloat(process.env.COMMISSION_PERCENT) || 0,
|
|
||||||
COMMISSION_WALLETS: {
|
|
||||||
BTC: process.env.COMMISSION_WALLET_BTC,
|
|
||||||
LTC: process.env.COMMISSION_WALLET_LTC,
|
|
||||||
USDT: process.env.COMMISSION_WALLET_USDT,
|
|
||||||
USDC: process.env.COMMISSION_WALLET_USDC,
|
|
||||||
ETH: process.env.COMMISSION_WALLET_ETH
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
@@ -1,74 +1,274 @@
|
|||||||
import Database from 'better-sqlite3';
|
import sqlite3 from 'sqlite3';
|
||||||
import logger from '../utils/logger.js';
|
import { promisify } from 'util';
|
||||||
import { fileURLToPath } from 'url';
|
|
||||||
import { dirname } from 'path';
|
import { dirname } from 'path';
|
||||||
import { pathToFileURL } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
const DB_PATH = new URL('../../db/shop.db', import.meta.url).pathname;
|
const DB_PATH = new URL('../../db/shop.db', import.meta.url).pathname;
|
||||||
|
|
||||||
let betterDb;
|
// Create database with verbose mode for better error reporting
|
||||||
|
const db = new sqlite3.Database(DB_PATH, sqlite3.OPEN_CREATE | sqlite3.OPEN_READWRITE, (err) => {
|
||||||
try {
|
if (err) {
|
||||||
betterDb = new Database(DB_PATH);
|
console.error('Database connection error:', err);
|
||||||
betterDb.pragma('journal_mode = WAL');
|
|
||||||
betterDb.pragma('foreign_keys = ON');
|
|
||||||
logger.info({ path: DB_PATH }, 'Connected to SQLite database (better-sqlite3)');
|
|
||||||
} catch (err) {
|
|
||||||
logger.fatal({ err }, 'Database connection error');
|
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
console.log('Connected to SQLite database');
|
||||||
|
});
|
||||||
|
|
||||||
// Adapter: provides async interface compatible with sqlite3 callback API
|
// Enable foreign keys
|
||||||
const db = {
|
db.run('PRAGMA foreign_keys = ON');
|
||||||
_betterDb: betterDb,
|
|
||||||
|
|
||||||
runAsync(sql, params = []) {
|
// Promisify database operations
|
||||||
return new Promise((resolve, reject) => {
|
const runAsync = (sql, params = []) => {
|
||||||
try {
|
return new Promise((resolve, reject) => {
|
||||||
const stmt = betterDb.prepare(sql);
|
db.run(sql, params, function(err) {
|
||||||
const info = stmt.run(...(Array.isArray(params) ? params : [params]));
|
if (err) reject(err);
|
||||||
resolve(info);
|
else resolve(this);
|
||||||
} catch (err) {
|
});
|
||||||
reject(err);
|
});
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
allAsync(sql, params = []) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
try {
|
|
||||||
const stmt = betterDb.prepare(sql);
|
|
||||||
const rows = stmt.all(...(Array.isArray(params) ? params : [params]));
|
|
||||||
resolve(rows);
|
|
||||||
} catch (err) {
|
|
||||||
reject(err);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
getAsync(sql, params = []) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
try {
|
|
||||||
const stmt = betterDb.prepare(sql);
|
|
||||||
const row = stmt.get(...(Array.isArray(params) ? params : [params]));
|
|
||||||
resolve(row || undefined);
|
|
||||||
} catch (err) {
|
|
||||||
reject(err);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
process.on('SIGINT', () => {
|
const allAsync = (sql, params = []) => {
|
||||||
try {
|
return new Promise((resolve, reject) => {
|
||||||
betterDb.close();
|
db.all(sql, params, (err, rows) => {
|
||||||
logger.info('Database connection closed');
|
if (err) reject(err);
|
||||||
process.exit(0);
|
else resolve(rows);
|
||||||
} catch (err) {
|
});
|
||||||
logger.error({ err }, 'Error closing database');
|
});
|
||||||
process.exit(1);
|
};
|
||||||
|
|
||||||
|
const getAsync = (sql, params = []) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
db.get(sql, params, (err, row) => {
|
||||||
|
if (err) reject(err);
|
||||||
|
else resolve(row);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Attach async methods to db object
|
||||||
|
db.runAsync = runAsync;
|
||||||
|
db.allAsync = allAsync;
|
||||||
|
db.getAsync = getAsync;
|
||||||
|
|
||||||
|
// Function to check if a column exists in a table
|
||||||
|
const checkColumnExists = async (tableName, columnName) => {
|
||||||
|
try {
|
||||||
|
const result = await db.allAsync(`
|
||||||
|
PRAGMA table_info(${tableName})
|
||||||
|
`);
|
||||||
|
|
||||||
|
return result.some(column => column.name === columnName);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error checking column ${columnName} in table ${tableName}:`, error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Function to clean up invalid foreign key references
|
||||||
|
const cleanUpInvalidForeignKeys = async () => {
|
||||||
|
try {
|
||||||
|
// Clean up invalid foreign key references in crypto_wallets table
|
||||||
|
await db.runAsync(`
|
||||||
|
DELETE FROM crypto_wallets
|
||||||
|
WHERE user_id NOT IN (SELECT id FROM users)
|
||||||
|
`);
|
||||||
|
console.log('Cleaned up invalid foreign key references in crypto_wallets table');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error cleaning up invalid foreign key references:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialize database tables
|
||||||
|
const initDb = async () => {
|
||||||
|
try {
|
||||||
|
// Begin transaction for table creation
|
||||||
|
await db.runAsync('BEGIN TRANSACTION');
|
||||||
|
|
||||||
|
// Create users table
|
||||||
|
await db.runAsync(`
|
||||||
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
telegram_id TEXT UNIQUE NOT NULL,
|
||||||
|
username TEXT,
|
||||||
|
country TEXT,
|
||||||
|
city TEXT,
|
||||||
|
district TEXT,
|
||||||
|
status INTEGER DEFAULT 0,
|
||||||
|
total_balance REAL DEFAULT 0,
|
||||||
|
bonus_balance REAL DEFAULT 0,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Create crypto_wallets table
|
||||||
|
await db.runAsync(`
|
||||||
|
CREATE TABLE IF NOT EXISTS crypto_wallets (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
|
wallet_type TEXT NOT NULL,
|
||||||
|
address TEXT NOT NULL,
|
||||||
|
derivation_path TEXT NOT NULL,
|
||||||
|
encrypted_mnemonic TEXT NOT NULL,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
UNIQUE(user_id, wallet_type)
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Create transactions table
|
||||||
|
await db.runAsync(`
|
||||||
|
CREATE TABLE IF NOT EXISTS transactions (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
|
wallet_type TEXT NOT NULL,
|
||||||
|
tx_hash TEXT NOT NULL,
|
||||||
|
amount REAL NOT NULL,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Check if user_id column exists in transactions table
|
||||||
|
const user_idExists = await checkColumnExists('transactions', 'user_id');
|
||||||
|
if (!user_idExists) {
|
||||||
|
await db.runAsync(`
|
||||||
|
ALTER TABLE transactions
|
||||||
|
ADD COLUMN user_id INTEGER NOT NULL
|
||||||
|
`);
|
||||||
|
console.log('Column user_id added to transactions table');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if wallet_type column exists in transactions table
|
||||||
|
const wallet_typeExists = await checkColumnExists('transactions', 'wallet_type');
|
||||||
|
if (!wallet_typeExists) {
|
||||||
|
await db.runAsync(`
|
||||||
|
ALTER TABLE transactions
|
||||||
|
ADD COLUMN wallet_type TEXT NOT NULL
|
||||||
|
`);
|
||||||
|
console.log('Column wallet_type added to transactions table');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if tx_hash column exists in transactions table
|
||||||
|
const tx_hashExists = await checkColumnExists('transactions', 'tx_hash');
|
||||||
|
if (!tx_hashExists) {
|
||||||
|
await db.runAsync(`
|
||||||
|
ALTER TABLE transactions
|
||||||
|
ADD COLUMN tx_hash TEXT NOT NULL
|
||||||
|
`);
|
||||||
|
console.log('Column tx_hash added to transactions table');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create products table
|
||||||
|
await db.runAsync(`
|
||||||
|
CREATE TABLE IF NOT EXISTS products (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
location_id INTEGER NOT NULL,
|
||||||
|
category_id INTEGER NOT NULL,
|
||||||
|
subcategory_id INTEGER NOT NULL,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
private_data TEXT,
|
||||||
|
price REAL NOT NULL CHECK (price > 0),
|
||||||
|
quantity_in_stock INTEGER DEFAULT 0 CHECK (quantity_in_stock >= 0),
|
||||||
|
photo_url TEXT,
|
||||||
|
hidden_photo_url TEXT,
|
||||||
|
hidden_coordinates TEXT,
|
||||||
|
hidden_description TEXT,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (location_id) REFERENCES locations(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (subcategory_id) REFERENCES subcategories(id) ON DELETE CASCADE
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Create purchases table
|
||||||
|
await db.runAsync(`
|
||||||
|
CREATE TABLE IF NOT EXISTS purchases (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
|
product_id INTEGER NOT NULL,
|
||||||
|
wallet_type TEXT NOT NULL,
|
||||||
|
tx_hash TEXT NOT NULL,
|
||||||
|
quantity INTEGER NOT NULL CHECK (quantity > 0),
|
||||||
|
total_price REAL NOT NULL CHECK (total_price > 0),
|
||||||
|
purchase_date DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (product_id) REFERENCES products(id) ON DELETE CASCADE
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Create locations table
|
||||||
|
await db.runAsync(`
|
||||||
|
CREATE TABLE IF NOT EXISTS locations (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
country TEXT NOT NULL,
|
||||||
|
city TEXT NOT NULL,
|
||||||
|
district TEXT NOT NULL,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
UNIQUE(country, city, district)
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Create categories table
|
||||||
|
await db.runAsync(`
|
||||||
|
CREATE TABLE IF NOT EXISTS categories (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
location_id INTEGER NOT NULL,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (location_id) REFERENCES locations(id) ON DELETE CASCADE,
|
||||||
|
UNIQUE(location_id, name)
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Create subcategories table
|
||||||
|
await db.runAsync(`
|
||||||
|
CREATE TABLE IF NOT EXISTS subcategories (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
category_id INTEGER NOT NULL,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE CASCADE,
|
||||||
|
UNIQUE(category_id, name)
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Commit transaction
|
||||||
|
await db.runAsync('COMMIT');
|
||||||
|
console.log('Database tables initialized successfully');
|
||||||
|
} catch (error) {
|
||||||
|
// Rollback transaction on error
|
||||||
|
await db.runAsync('ROLLBACK');
|
||||||
|
console.error('Error initializing database tables:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialize the database
|
||||||
|
(async () => {
|
||||||
|
await initDb();
|
||||||
|
await cleanUpInvalidForeignKeys();
|
||||||
|
})().catch(error => {
|
||||||
|
console.error('Database initialization failed:', error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle database errors
|
||||||
|
db.on('error', (err) => {
|
||||||
|
console.error('Database error:', err);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle process termination
|
||||||
|
process.on('SIGINT', () => {
|
||||||
|
db.close((err) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('Error closing database:', err);
|
||||||
|
} else {
|
||||||
|
console.log('Database connection closed');
|
||||||
|
}
|
||||||
|
process.exit(err ? 1 : 0);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
export default db;
|
export default db;
|
||||||
@@ -1,14 +1,13 @@
|
|||||||
import TelegramBot from "node-telegram-bot-api";
|
import TelegramBot from "node-telegram-bot-api";
|
||||||
import config from "../config/config.js";
|
import config from "../config/config.js";
|
||||||
import logger from "../utils/logger.js";
|
|
||||||
|
|
||||||
const initBot = () => {
|
const initBot = () => {
|
||||||
try {
|
try {
|
||||||
const bot = new TelegramBot(config.BOT_TOKEN, {polling: true});
|
const bot = new TelegramBot(config.BOT_TOKEN, {polling: true});
|
||||||
logger.info('Bot initialized successfully');
|
console.log('Bot initialized successfully');
|
||||||
return bot;
|
return bot;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error({ err: error }, 'Failed to initialize bot');
|
console.error('Failed to initialize bot:', error);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,11 +1,2 @@
|
|||||||
import { get, set, del, has, initStates } from '../services/stateService.js';
|
const userStates = new Map();
|
||||||
|
|
||||||
const userStates = {
|
|
||||||
get: (chatId) => get(chatId),
|
|
||||||
set: (chatId, value) => set(chatId, value),
|
|
||||||
delete: (chatId) => del(chatId),
|
|
||||||
has: (chatId) => has(chatId),
|
|
||||||
initStates,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default userStates;
|
export default userStates;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { isAdmin } from '../../middleware/auth.js';
|
import config from '../../config/config.js';
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import db from "../../config/database.js";
|
import db from "../../config/database.js";
|
||||||
import archiver from "archiver";
|
import archiver from "archiver";
|
||||||
@@ -7,11 +7,14 @@ import bot from "../../context/bot.js";
|
|||||||
import userStates from "../../context/userStates.js";
|
import userStates from "../../context/userStates.js";
|
||||||
|
|
||||||
export default class AdminDumpHandler {
|
export default class AdminDumpHandler {
|
||||||
|
static isAdmin(userId) {
|
||||||
|
return config.ADMIN_IDS.includes(userId.toString());
|
||||||
|
}
|
||||||
|
|
||||||
static async handleDump(msg) {
|
static async handleDump(msg) {
|
||||||
const chatId = msg.chat.id;
|
const chatId = msg.chat.id;
|
||||||
|
|
||||||
if (!isAdmin(msg.from.id)) {
|
if (!this.isAdmin(msg.from.id)) {
|
||||||
await bot.sendMessage(chatId, 'Unauthorized access.');
|
await bot.sendMessage(chatId, 'Unauthorized access.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -30,13 +33,12 @@ export default class AdminDumpHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static async handleExportDatabase(callbackQuery) {
|
static async handleExportDatabase(callbackQuery) {
|
||||||
if (!isAdmin(callbackQuery.from.id)) {
|
if (!this.isAdmin(callbackQuery.from.id)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const chatId = callbackQuery.message.chat.id;
|
const chatId = callbackQuery.message.chat.id;
|
||||||
|
|
||||||
// Table names must match ALLOWED_TABLES whitelist in database.js
|
|
||||||
const tables = [
|
const tables = [
|
||||||
"categories",
|
"categories",
|
||||||
"crypto_wallets",
|
"crypto_wallets",
|
||||||
@@ -78,13 +80,13 @@ export default class AdminDumpHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static async handleImportDatabase(callbackQuery) {
|
static async handleImportDatabase(callbackQuery) {
|
||||||
if (!isAdmin(callbackQuery.from.id)) {
|
if (!this.isAdmin(callbackQuery.from.id)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const chatId = callbackQuery.message.chat.id;
|
const chatId = callbackQuery.message.chat.id;
|
||||||
|
|
||||||
await userStates.set(chatId, { action: 'upload_database_dump' });
|
userStates.set(chatId, { action: 'upload_database_dump' });
|
||||||
|
|
||||||
await bot.editMessageText(
|
await bot.editMessageText(
|
||||||
'Please upload database dump',
|
'Please upload database dump',
|
||||||
@@ -96,7 +98,6 @@ export default class AdminDumpHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static async getDumpStatistic() {
|
static async getDumpStatistic() {
|
||||||
// Table names must match ALLOWED_TABLES whitelist in database.js
|
|
||||||
const tables = [
|
const tables = [
|
||||||
"categories",
|
"categories",
|
||||||
"crypto_wallets",
|
"crypto_wallets",
|
||||||
@@ -121,13 +122,13 @@ export default class AdminDumpHandler {
|
|||||||
static async handleDumpImport(msg) {
|
static async handleDumpImport(msg) {
|
||||||
const chatId = msg.chat.id;
|
const chatId = msg.chat.id;
|
||||||
|
|
||||||
const state = await userStates.get(chatId);
|
const state = userStates.get(chatId);
|
||||||
|
|
||||||
if (!state || state.action !== 'upload_database_dump') {
|
if (!state || state.action !== 'upload_database_dump') {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isAdmin(msg.from.id)) {
|
if (!this.isAdmin(msg.from.id)) {
|
||||||
await bot.sendMessage(chatId, 'Unauthorized access.');
|
await bot.sendMessage(chatId, 'Unauthorized access.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -145,7 +146,7 @@ export default class AdminDumpHandler {
|
|||||||
|
|
||||||
const statistics = await this.getDumpStatistic();
|
const statistics = await this.getDumpStatistic();
|
||||||
await bot.sendMessage(chatId, JSON.stringify(statistics, null, 2));
|
await bot.sendMessage(chatId, JSON.stringify(statistics, null, 2));
|
||||||
await userStates.delete(chatId);
|
userStates.delete(chatId);
|
||||||
} else {
|
} else {
|
||||||
await bot.sendMessage(chatId, 'Please upload a valid .zip file.');
|
await bot.sendMessage(chatId, 'Please upload a valid .zip file.');
|
||||||
return true;
|
return true;
|
||||||
@@ -153,7 +154,7 @@ export default class AdminDumpHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static async confirmImport(callbackQuery) {
|
static async confirmImport(callbackQuery) {
|
||||||
if (!isAdmin(callbackQuery.from.id)) {
|
if (!this.isAdmin(callbackQuery.from.id)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import { isAdmin } from '../../middleware/auth.js';
|
import config from '../../config/config.js';
|
||||||
import bot from "../../context/bot.js";
|
import bot from "../../context/bot.js";
|
||||||
|
|
||||||
export default class AdminHandler {
|
export default class AdminHandler {
|
||||||
static isAdmin(userId) {
|
static isAdmin(userId) {
|
||||||
return isAdmin(userId);
|
return config.ADMIN_IDS.includes(userId.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
static async handleAdminCommand(msg) {
|
static async handleAdminCommand(msg) {
|
||||||
const chatId = msg.chat.id;
|
const chatId = msg.chat.id;
|
||||||
|
|
||||||
if (!isAdmin(msg.from.id)) {
|
if (!this.isAdmin(msg.from.id)) {
|
||||||
await bot.sendMessage(chatId, 'Unauthorized access.');
|
await bot.sendMessage(chatId, 'Unauthorized access.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,22 @@
|
|||||||
import db from '../../config/database.js';
|
import db from '../../config/database.js';
|
||||||
import Validators from '../../utils/validators.js';
|
import Validators from '../../utils/validators.js';
|
||||||
import { isAdmin } from '../../middleware/auth.js';
|
import config from '../../config/config.js';
|
||||||
import userStates from "../../context/userStates.js";
|
import userStates from "../../context/userStates.js";
|
||||||
import bot from "../../context/bot.js";
|
import bot from "../../context/bot.js";
|
||||||
import logger from '../../utils/logger.js';
|
|
||||||
|
|
||||||
export default class AdminLocationHandler {
|
export default class AdminLocationHandler {
|
||||||
|
static isAdmin(userId) {
|
||||||
|
return config.ADMIN_IDS.includes(userId.toString());
|
||||||
|
}
|
||||||
|
|
||||||
static async handleAddLocation(callbackQuery) {
|
static async handleAddLocation(callbackQuery) {
|
||||||
if (!isAdmin(callbackQuery.from.id)) {
|
if (!this.isAdmin(callbackQuery.from.id)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const chatId = callbackQuery.message.chat.id;
|
const chatId = callbackQuery.message.chat.id;
|
||||||
|
|
||||||
await userStates.set(chatId, { action: 'add_location' });
|
userStates.set(chatId, { action: 'add_location' });
|
||||||
|
|
||||||
await bot.editMessageText(
|
await bot.editMessageText(
|
||||||
'Please enter the location in the following format:\nCountry|City|District',
|
'Please enter the location in the following format:\nCountry|City|District',
|
||||||
@@ -30,13 +32,13 @@ export default class AdminLocationHandler {
|
|||||||
|
|
||||||
static async handleLocationInput(msg) {
|
static async handleLocationInput(msg) {
|
||||||
const chatId = msg.chat.id;
|
const chatId = msg.chat.id;
|
||||||
const state = await userStates.get(chatId);
|
const state = userStates.get(chatId);
|
||||||
|
|
||||||
if (!state || state.action !== 'add_location') {
|
if (!state || state.action !== 'add_location') {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isAdmin(msg.from.id)) {
|
if (!this.isAdmin(msg.from.id)) {
|
||||||
await bot.sendMessage(chatId, 'Unauthorized access.');
|
await bot.sendMessage(chatId, 'Unauthorized access.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -87,7 +89,7 @@ export default class AdminLocationHandler {
|
|||||||
throw new Error('Failed to insert location');
|
throw new Error('Failed to insert location');
|
||||||
}
|
}
|
||||||
|
|
||||||
await userStates.delete(chatId);
|
userStates.delete(chatId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await db.runAsync('ROLLBACK');
|
await db.runAsync('ROLLBACK');
|
||||||
|
|
||||||
@@ -104,7 +106,7 @@ export default class AdminLocationHandler {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
logger.error({ err: error }, 'Error adding location');
|
console.error('Error adding location:', error);
|
||||||
await bot.sendMessage(
|
await bot.sendMessage(
|
||||||
chatId,
|
chatId,
|
||||||
'❌ Error adding location. Please try again.',
|
'❌ Error adding location. Please try again.',
|
||||||
@@ -122,51 +124,16 @@ export default class AdminLocationHandler {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
static async handleViewIP(callbackQuery) {
|
|
||||||
// Проверка прав администратора
|
|
||||||
if (!isAdmin(callbackQuery.from.id)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const chatId = callbackQuery.message.chat.id;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Получаем IP-адрес с помощью https://icanhazip.com
|
|
||||||
const response = await fetch('https://icanhazip.com');
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`HTTP error! Status: ${response.status}`);
|
|
||||||
}
|
|
||||||
const ip = await response.text();
|
|
||||||
|
|
||||||
// Обновляем сообщение с IP-адресом
|
|
||||||
await bot.editMessageText(
|
|
||||||
`🌐 Current IP Address: ${ip.trim()}\n\nThis is the public IP address of the bot server.`,
|
|
||||||
{
|
|
||||||
chat_id: chatId,
|
|
||||||
message_id: callbackQuery.message.message_id,
|
|
||||||
reply_markup: {
|
|
||||||
inline_keyboard: [
|
|
||||||
[{ text: '« Back to Locations', callback_data: 'view_locations' }]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error({ err: error }, 'Error getting IP');
|
|
||||||
await bot.sendMessage(chatId, '❌ Error getting IP address. Please try again.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static async handleViewLocations(msg) {
|
static async handleViewLocations(msg) {
|
||||||
const chatId = msg.chat?.id || msg.message?.chat.id;
|
const chatId = msg.chat?.id || msg.message?.chat.id;
|
||||||
const messageId = msg.message?.message_id;
|
const messageId = msg.message?.message_id;
|
||||||
|
|
||||||
if (!isAdmin(msg.from?.id || msg.message?.from.id)) {
|
if (!this.isAdmin(msg.from?.id || msg.message?.from.id)) {
|
||||||
await bot.sendMessage(chatId, 'Unauthorized access.');
|
await bot.sendMessage(chatId, 'Unauthorized access.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await userStates.delete(chatId);
|
userStates.delete(chatId);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const locations = await db.allAsync(`
|
const locations = await db.allAsync(`
|
||||||
@@ -215,7 +182,7 @@ export default class AdminLocationHandler {
|
|||||||
inline_keyboard: [
|
inline_keyboard: [
|
||||||
[{ text: '➕ Add Location', callback_data: 'add_location' }],
|
[{ text: '➕ Add Location', callback_data: 'add_location' }],
|
||||||
[{ text: '❌ Delete Location', callback_data: 'delete_location' }],
|
[{ text: '❌ Delete Location', callback_data: 'delete_location' }],
|
||||||
[{ text: '🌐 View IP Info', callback_data: 'view_ip' }]
|
[{ text: '« Back to Admin Menu', callback_data: 'admin_menu' }]
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -233,13 +200,13 @@ export default class AdminLocationHandler {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error({ err: error }, 'Error viewing locations');
|
console.error('Error viewing locations:', error);
|
||||||
await bot.sendMessage(chatId, 'Error loading locations. Please try again.');
|
await bot.sendMessage(chatId, 'Error loading locations. Please try again.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static async handleDeleteLocation(callbackQuery) {
|
static async handleDeleteLocation(callbackQuery) {
|
||||||
if (!isAdmin(callbackQuery.from.id)) {
|
if (!this.isAdmin(callbackQuery.from.id)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -260,7 +227,7 @@ export default class AdminLocationHandler {
|
|||||||
const keyboard = {
|
const keyboard = {
|
||||||
inline_keyboard: locations.map(loc => [{
|
inline_keyboard: locations.map(loc => [{
|
||||||
text: `${loc.country} > ${loc.city} > ${loc.district} (P:${loc.product_count} C:${loc.category_count})`,
|
text: `${loc.country} > ${loc.city} > ${loc.district} (P:${loc.product_count} C:${loc.category_count})`,
|
||||||
callback_data: `confirm_delete_location_${loc.id}` // Используем ID локации вместо строки
|
callback_data: `confirm_delete_location_${loc.country}_${loc.city}_${loc.district}`
|
||||||
}])
|
}])
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -276,38 +243,34 @@ export default class AdminLocationHandler {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error({ err: error }, 'Error in handleDeleteLocation');
|
console.error('Error in handleDeleteLocation:', error);
|
||||||
await bot.sendMessage(chatId, 'Error loading locations. Please try again.');
|
await bot.sendMessage(chatId, 'Error loading locations. Please try again.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static async handleConfirmDelete(callbackQuery) {
|
static async handleConfirmDelete(callbackQuery) {
|
||||||
if (!isAdmin(callbackQuery.from.id)) {
|
if (!this.isAdmin(callbackQuery.from.id)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const chatId = callbackQuery.message.chat.id;
|
const chatId = callbackQuery.message.chat.id;
|
||||||
const locationId = callbackQuery.data.replace('confirm_delete_location_', '');
|
const [country, city, district] = callbackQuery.data
|
||||||
|
.replace('confirm_delete_location_', '')
|
||||||
|
.split('_');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const location = await db.getAsync('SELECT * FROM locations WHERE id = ?', [locationId]);
|
|
||||||
|
|
||||||
if (!location) {
|
|
||||||
throw new Error('Location not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
await db.runAsync('BEGIN TRANSACTION');
|
await db.runAsync('BEGIN TRANSACTION');
|
||||||
|
|
||||||
const result = await db.runAsync(
|
const result = await db.runAsync(
|
||||||
'DELETE FROM locations WHERE id = ?',
|
'DELETE FROM locations WHERE country = ? AND city = ? AND district = ?',
|
||||||
[locationId]
|
[country, city, district]
|
||||||
);
|
);
|
||||||
|
|
||||||
await db.runAsync('COMMIT');
|
await db.runAsync('COMMIT');
|
||||||
|
|
||||||
if (result.changes > 0) {
|
if (result.changes > 0) {
|
||||||
await bot.editMessageText(
|
await bot.editMessageText(
|
||||||
`✅ Location deleted successfully!\n\nCountry: ${location.country}\nCity: ${location.city}\nDistrict: ${location.district}`,
|
`✅ Location deleted successfully!\n\nCountry: ${country}\nCity: ${city}\nDistrict: ${district}`,
|
||||||
{
|
{
|
||||||
chat_id: chatId,
|
chat_id: chatId,
|
||||||
message_id: callbackQuery.message.message_id,
|
message_id: callbackQuery.message.message_id,
|
||||||
@@ -321,7 +284,7 @@ export default class AdminLocationHandler {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await db.runAsync('ROLLBACK');
|
await db.runAsync('ROLLBACK');
|
||||||
logger.error({ err: error }, 'Error deleting location');
|
console.error('Error deleting location:', error);
|
||||||
await bot.sendMessage(
|
await bot.sendMessage(
|
||||||
chatId,
|
chatId,
|
||||||
'❌ Error deleting location. Please try again.',
|
'❌ Error deleting location. Please try again.',
|
||||||
@@ -335,7 +298,7 @@ export default class AdminLocationHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static async backToMenu(callbackQuery) {
|
static async backToMenu(callbackQuery) {
|
||||||
if (!isAdmin(callbackQuery.from.id)) {
|
if (!this.isAdmin(callbackQuery.from.id)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -362,6 +325,6 @@ export default class AdminLocationHandler {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
await userStates.delete(chatId);
|
userStates.delete(chatId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
1058
src/handlers/adminHandlers/adminProductHandler.js
Normal file
1058
src/handlers/adminHandlers/adminProductHandler.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,91 +1,47 @@
|
|||||||
// adminUserHandler.js
|
import config from '../../config/config.js';
|
||||||
|
|
||||||
import { isAdmin } from '../../middleware/auth.js';
|
|
||||||
import db from '../../config/database.js';
|
import db from '../../config/database.js';
|
||||||
import bot from "../../context/bot.js";
|
import bot from "../../context/bot.js";
|
||||||
import UserService from "../../services/userService.js";
|
import UserService from "../../services/userService.js";
|
||||||
import WalletService from "../../services/walletService.js";
|
|
||||||
import PurchaseService from "../../services/purchaseService.js";
|
|
||||||
import userStates from "../../context/userStates.js";
|
import userStates from "../../context/userStates.js";
|
||||||
import Validators from '../../utils/validators.js';
|
|
||||||
import logger from '../../utils/logger.js';
|
|
||||||
|
|
||||||
export default class AdminUserHandler {
|
export default class AdminUserHandler {
|
||||||
|
static isAdmin(userId) {
|
||||||
|
return config.ADMIN_IDS.includes(userId.toString());
|
||||||
|
}
|
||||||
|
|
||||||
static async calculateStatistics() {
|
static async calculateStatistics() {
|
||||||
try {
|
try {
|
||||||
// Получаем общую статистику по пользователям
|
|
||||||
const users = await db.allAsync(`
|
const users = await db.allAsync(`
|
||||||
SELECT
|
SELECT
|
||||||
u.*,
|
u.*,
|
||||||
COUNT(DISTINCT p.id) as total_purchases,
|
COUNT(DISTINCT p.id) as total_purchases,
|
||||||
COUNT(DISTINCT cw.id) as total_wallets
|
COUNT(DISTINCT cw.id) as total_wallets
|
||||||
FROM users u
|
FROM users u
|
||||||
LEFT JOIN purchases p ON u.id = p.user_id
|
LEFT JOIN purchases p ON u.id = p.user_id
|
||||||
LEFT JOIN crypto_wallets cw ON u.id = cw.user_id
|
LEFT JOIN crypto_wallets cw ON u.id = cw.user_id
|
||||||
LEFT JOIN transactions t ON u.id = t.user_id
|
LEFT JOIN transactions t ON u.id = t.user_id
|
||||||
GROUP BY u.id
|
GROUP BY u.id
|
||||||
ORDER BY u.created_at DESC
|
ORDER BY u.created_at DESC
|
||||||
`);
|
` );
|
||||||
|
|
||||||
// Общие метрики
|
// Calculate general statistics
|
||||||
const totalUsers = users.length;
|
const totalUsers = users.length;
|
||||||
const activeUsers = users.filter(u => u.total_purchases > 0).length;
|
const activeUsers = users.filter(u => u.total_purchases > 0).length;
|
||||||
|
const totalBalance = users.reduce((sum, u) => sum + (u.total_balance || 0), 0);
|
||||||
const bonusBalance = users.reduce((sum, u) => sum + (u.bonus_balance || 0), 0);
|
const bonusBalance = users.reduce((sum, u) => sum + (u.bonus_balance || 0), 0);
|
||||||
const totalPurchases = users.reduce((sum, u) => sum + (u.total_purchases || 0), 0);
|
const totalPurchases = users.reduce((sum, u) => sum + (u.total_purchases || 0), 0);
|
||||||
|
|
||||||
// Рассчитываем общий баланс активных и архивных кошельков
|
// Create statistics message
|
||||||
let totalActiveWalletsBalance = 0;
|
|
||||||
let totalArchivedWalletsBalance = 0;
|
|
||||||
|
|
||||||
for (const user of users) {
|
|
||||||
const activeWalletsBalance = await WalletService.getActiveWalletsBalance(user.id);
|
|
||||||
const archivedWalletsBalance = await WalletService.getArchivedWalletsBalance(user.id);
|
|
||||||
totalActiveWalletsBalance += activeWalletsBalance;
|
|
||||||
totalArchivedWalletsBalance += archivedWalletsBalance;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Рассчитываем общий реальный баланс (крипто + бонусы)
|
|
||||||
const totalRealBalance = totalActiveWalletsBalance + totalArchivedWalletsBalance + bonusBalance;
|
|
||||||
|
|
||||||
// Получаем статистику по транзакциям
|
|
||||||
const totalTransactions = await db.getAsync(`
|
|
||||||
SELECT COUNT(*) as total_transactions FROM transactions
|
|
||||||
`);
|
|
||||||
|
|
||||||
// Получаем статистику по продуктам
|
|
||||||
const totalProducts = await db.getAsync(`
|
|
||||||
SELECT COUNT(*) as total_products FROM products
|
|
||||||
`);
|
|
||||||
|
|
||||||
// Получаем статистику по локациям
|
|
||||||
const totalLocations = await db.getAsync(`
|
|
||||||
SELECT COUNT(*) as total_locations FROM locations
|
|
||||||
`);
|
|
||||||
|
|
||||||
// Получаем статистику по категориям
|
|
||||||
const totalCategories = await db.getAsync(`
|
|
||||||
SELECT COUNT(*) as total_categories FROM categories
|
|
||||||
`);
|
|
||||||
|
|
||||||
// Формируем сообщение со статистикой
|
|
||||||
let message = `📊 System Statistics\n\n`;
|
let message = `📊 System Statistics\n\n`;
|
||||||
message += `👥 Total Users: ${totalUsers}\n`;
|
message += `👥 Total Users: ${totalUsers}\n`;
|
||||||
message += `✅ Active Users: ${activeUsers}\n`;
|
message += `✅ Active Users: ${activeUsers}\n`;
|
||||||
message += `💰 Total All Users Balance: $${totalRealBalance.toFixed(2)}\n`;
|
message += `💰 Bonus Balance: $${bonusBalance.toFixed(2)}\n`;
|
||||||
message += ` ├ Active Wallets Balance: $${totalActiveWalletsBalance.toFixed(2)}\n`;
|
message += `💰 Total Balance: $${(totalBalance + bonusBalance).toFixed(2)}\n`;
|
||||||
message += ` ├ Archived Wallets Balance: $${totalArchivedWalletsBalance.toFixed(2)}\n`;
|
message += `🛍 Total Purchases: ${totalPurchases}`;
|
||||||
message += ` └ Bonus Balance: $${bonusBalance.toFixed(2)}\n`;
|
|
||||||
message += `🛍 Total Purchases: ${totalPurchases}\n`;
|
|
||||||
message += `💸 Total Transactions: ${totalTransactions.total_transactions}\n`;
|
|
||||||
message += `📦 Total Products: ${totalProducts.total_products}\n`;
|
|
||||||
message += `📍 Total Locations: ${totalLocations.total_locations}\n`;
|
|
||||||
message += `📂 Total Categories: ${totalCategories.total_categories}\n`;
|
|
||||||
|
|
||||||
return message;
|
return message;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error({ err: error }, 'Error in calculateStatistics');
|
return null
|
||||||
return 'Error loading statistics. Please try again.';
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,63 +54,52 @@ export default class AdminUserHandler {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const users = await db.allAsync(`
|
const users = await db.allAsync(`
|
||||||
SELECT
|
SELECT
|
||||||
u.*,
|
u.*,
|
||||||
COUNT(DISTINCT p.id) as total_purchases,
|
COUNT(DISTINCT p.id) as total_purchases,
|
||||||
COUNT(DISTINCT cw.id) as total_wallets
|
COUNT(DISTINCT cw.id) as total_wallets
|
||||||
FROM users u
|
FROM users u
|
||||||
LEFT JOIN purchases p ON u.id = p.user_id
|
LEFT JOIN purchases p ON u.id = p.user_id
|
||||||
LEFT JOIN crypto_wallets cw ON u.id = cw.user_id
|
LEFT JOIN crypto_wallets cw ON u.id = cw.user_id
|
||||||
LEFT JOIN transactions t ON u.id = t.user_id
|
LEFT JOIN transactions t ON u.id = t.user_id
|
||||||
GROUP BY u.id
|
GROUP BY u.id
|
||||||
ORDER BY u.created_at DESC
|
ORDER BY u.created_at DESC
|
||||||
LIMIT ?
|
LIMIT ?
|
||||||
OFFSET ?
|
OFFSET ?
|
||||||
`, [limit, offset]);
|
`, [limit, offset]);
|
||||||
|
|
||||||
if ((users.length === 0) && (page == 0)) {
|
if ((users.length === 0) && (page == 0)) {
|
||||||
return { text: 'No users registered yet.' };
|
return {text: 'No users registered yet.'};
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((users.length === 0) && (page > 0)) {
|
if ((users.length === 0) && (page > 0)) {
|
||||||
return await this.viewUserPage(page - 1);
|
return await this.viewUserPage(page - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate balances for each user
|
const statistics = await this.calculateStatistics()
|
||||||
const usersWithBalances = await Promise.all(users.map(async (user) => {
|
|
||||||
// Доступный баланс (bonus_balance + total_balance)
|
|
||||||
const availableBalance = user.bonus_balance + (user.total_balance || 0);
|
|
||||||
return {
|
|
||||||
...user,
|
|
||||||
availableBalance
|
|
||||||
};
|
|
||||||
}));
|
|
||||||
|
|
||||||
const statistics = await this.calculateStatistics();
|
|
||||||
const message = `${statistics}\n\nSelect a user from the list below:`;
|
const message = `${statistics}\n\nSelect a user from the list below:`;
|
||||||
|
|
||||||
// Create inline keyboard with user list
|
// Create inline keyboard with user list
|
||||||
const keyboard = {
|
const keyboard = {
|
||||||
inline_keyboard: usersWithBalances.map(user => [{
|
inline_keyboard: users.map(user => [{
|
||||||
text: `ID: ${user.telegram_id} | Nickname: ${user.username ? "@" + user.username : "None"} | Balance: $${user.availableBalance.toFixed(2)}`,
|
text: `ID: ${user.telegram_id} | Nickname: ${user.username ? "@" + user.username : "None"} | Balance: $${(user.total_balance || 0) + (user.bonus_balance || 0)}`,
|
||||||
callback_data: `view_user_${user.telegram_id}`
|
callback_data: `view_user_${user.telegram_id}`
|
||||||
}])
|
}])
|
||||||
};
|
};
|
||||||
|
|
||||||
keyboard.inline_keyboard.push([
|
keyboard.inline_keyboard.push([
|
||||||
{ text: `«`, callback_data: `list_users_${previousPage}` },
|
{text: `«`, callback_data: `list_users_${previousPage}`},
|
||||||
{ text: `»`, callback_data: `list_users_${nextPage}` },
|
{text: `»`, callback_data: `list_users_${nextPage}`},
|
||||||
]);
|
])
|
||||||
|
|
||||||
return { text: message, markup: keyboard };
|
return {text: message, markup: keyboard}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error({ err: error }, 'Error in handleUserList');
|
console.error('Error in handleUserList:', error);
|
||||||
return { text: 'Error loading user list. Please try again.' };
|
return {text: 'Error loading user list. Please try again.'}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static async handleUserList(msg) {
|
static async handleUserList(msg) {
|
||||||
if (!isAdmin(msg.from.id)) {
|
if (!this.isAdmin(msg.from.id)) {
|
||||||
await bot.sendMessage(msg.chat.id, 'Unauthorized access.');
|
await bot.sendMessage(msg.chat.id, 'Unauthorized access.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -164,7 +109,7 @@ export default class AdminUserHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static async handleUserListPage(callbackQuery) {
|
static async handleUserListPage(callbackQuery) {
|
||||||
if (!isAdmin(callbackQuery.from.id)) {
|
if (!this.isAdmin(callbackQuery.from.id)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,7 +130,7 @@ export default class AdminUserHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static async handleViewUser(callbackQuery) {
|
static async handleViewUser(callbackQuery) {
|
||||||
if (!isAdmin(callbackQuery.from.id)) return;
|
if (!this.isAdmin(callbackQuery.from.id)) return;
|
||||||
|
|
||||||
const telegramId = callbackQuery.data.replace('view_user_', '');
|
const telegramId = callbackQuery.data.replace('view_user_', '');
|
||||||
const chatId = callbackQuery.message.chat.id;
|
const chatId = callbackQuery.message.chat.id;
|
||||||
@@ -201,63 +146,47 @@ export default class AdminUserHandler {
|
|||||||
|
|
||||||
// Get recent transactions
|
// Get recent transactions
|
||||||
const transactions = await db.allAsync(`
|
const transactions = await db.allAsync(`
|
||||||
SELECT t.amount, t.created_at, t.wallet_type, t.tx_hash
|
SELECT t.amount, t.created_at, t.wallet_type, t.tx_hash
|
||||||
FROM transactions t
|
FROM transactions t
|
||||||
JOIN users u ON t.user_id = u.id
|
JOIN users u ON t.user_id = u.id
|
||||||
WHERE u.telegram_id = ?
|
WHERE u.telegram_id = ?
|
||||||
ORDER BY t.created_at DESC
|
ORDER BY t.created_at DESC
|
||||||
LIMIT 5
|
LIMIT 5
|
||||||
`, [telegramId]);
|
`, [telegramId]);
|
||||||
|
|
||||||
// Get recent purchases
|
// Get recent purchases
|
||||||
const purchases = await db.allAsync(`
|
const purchases = await db.allAsync(`
|
||||||
SELECT p.quantity, p.total_price, p.purchase_date,
|
SELECT p.quantity, p.total_price, p.purchase_date,
|
||||||
pr.name as product_name
|
pr.name as product_name
|
||||||
FROM purchases p
|
FROM purchases p
|
||||||
JOIN products pr ON p.product_id = pr.id
|
JOIN products pr ON p.product_id = pr.id
|
||||||
JOIN users u ON p.user_id = u.id
|
JOIN users u ON p.user_id = u.id
|
||||||
WHERE u.telegram_id = ?
|
WHERE u.telegram_id = ?
|
||||||
ORDER BY p.purchase_date DESC
|
ORDER BY p.purchase_date DESC
|
||||||
LIMIT 5
|
LIMIT 5
|
||||||
`, [telegramId]);
|
`, [telegramId]);
|
||||||
|
|
||||||
// Get pending purchases
|
|
||||||
const pendingPurchases = await db.allAsync(`
|
|
||||||
SELECT p.quantity, p.total_price, p.purchase_date,
|
|
||||||
pr.name as product_name
|
|
||||||
FROM purchases p
|
|
||||||
JOIN products pr ON p.product_id = pr.id
|
|
||||||
JOIN users u ON p.user_id = u.id
|
|
||||||
WHERE u.telegram_id = ? AND p.status = 'pending'
|
|
||||||
ORDER BY p.purchase_date DESC
|
|
||||||
`, [telegramId]);
|
|
||||||
|
|
||||||
// Доступный баланс (bonus_balance + total_balance)
|
|
||||||
const availableBalance = user.bonus_balance + (user.total_balance || 0);
|
|
||||||
|
|
||||||
const message = `
|
const message = `
|
||||||
👤 User Profile:
|
👤 User Profile:
|
||||||
|
|
||||||
ID: ${telegramId}
|
ID: ${telegramId}
|
||||||
📍 Location: ${detailedUser.country || 'Not set'}, ${detailedUser.city || 'Not set'}, ${detailedUser.district || 'Not set'}
|
📍 Location: ${detailedUser.country || 'Not set'}, ${detailedUser.city || 'Not set'}, ${detailedUser.district || 'Not set'}
|
||||||
|
|
||||||
📊 Activity:
|
📊 Activity:
|
||||||
- Total Purchases: ${detailedUser.purchase_count}
|
- Total Purchases: ${detailedUser.purchase_count}
|
||||||
- Total Spent: $${detailedUser.total_spent || 0}
|
- Total Spent: $${detailedUser.total_spent || 0}
|
||||||
- Bonus Balance: $${user.bonus_balance || 0}
|
- Active Wallets: ${detailedUser.crypto_wallet_count}
|
||||||
- Available Balance: $${availableBalance.toFixed(2)}
|
- Bonus Balance: $${user.bonus_balance || 0}
|
||||||
|
- Total Balance: $${(user.total_balance || 0) + (user.bonus_balance || 0)}
|
||||||
|
|
||||||
💰 Recent Transactions (Last 5 of ${transactions.length}):
|
💰 Recent Transactions:
|
||||||
${transactions.map(t => ` • $${t.amount} ${t.wallet_type} (${t.tx_hash}) at ${new Date(t.created_at).toLocaleString('en-US', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit' })}`).join('\n')}
|
${transactions.map(t => ` • ${t.amount} ${t.wallet_type} (${t.tx_hash})`).join('\n')}
|
||||||
|
|
||||||
🛍 Recent Purchases (Last 5 of ${purchases.length}):
|
🛍 Recent Purchases:
|
||||||
${purchases.map(p => ` • ${p.product_name} x${p.quantity} - $${p.total_price}`).join('\n')}
|
${purchases.map(p => ` • ${p.product_name} x${p.quantity} - $${p.total_price}`).join('\n')}
|
||||||
|
|
||||||
🕒 Pending Purchases:
|
📅 Registered: ${new Date(detailedUser.created_at).toLocaleString()}
|
||||||
${pendingPurchases.map(p => ` • ${p.product_name} x${p.quantity} - $${p.total_price}`).join('\n') || ' • No pending purchases'}
|
`;
|
||||||
|
|
||||||
📅 Registered: ${new Date(detailedUser.created_at).toLocaleString()}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const keyboard = {
|
const keyboard = {
|
||||||
inline_keyboard: [
|
inline_keyboard: [
|
||||||
@@ -280,13 +209,13 @@ export default class AdminUserHandler {
|
|||||||
parse_mode: 'HTML'
|
parse_mode: 'HTML'
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error({ err: error }, 'Error in handleViewUser');
|
console.error('Error in handleViewUser:', error);
|
||||||
await bot.sendMessage(chatId, 'Error loading user details. Please try again.');
|
await bot.sendMessage(chatId, 'Error loading user details. Please try again.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static async handleDeleteUser(callbackQuery) {
|
static async handleDeleteUser(callbackQuery) {
|
||||||
if (!isAdmin(callbackQuery.from.id)) {
|
if (!this.isAdmin(callbackQuery.from.id)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -313,13 +242,13 @@ export default class AdminUserHandler {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error({ err: error }, 'Error in handleDeleteUser');
|
console.error('Error in handleDeleteUser:', error);
|
||||||
await bot.sendMessage(chatId, 'Error processing delete request. Please try again.');
|
await bot.sendMessage(chatId, 'Error processing delete request. Please try again.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static async handleConfirmDelete(callbackQuery) {
|
static async handleConfirmDelete(callbackQuery) {
|
||||||
if (!isAdmin(callbackQuery.from.id)) {
|
if (!this.isAdmin(callbackQuery.from.id)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -350,13 +279,13 @@ export default class AdminUserHandler {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error({ err: error }, 'Error in handleConfirmDelete');
|
console.error('Error in handleConfirmDelete:', error);
|
||||||
await bot.sendMessage(chatId, 'Error deleting user. Please try again.');
|
await bot.sendMessage(chatId, 'Error deleting user. Please try again.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static async handleBlockUser(callbackQuery) {
|
static async handleBlockUser(callbackQuery) {
|
||||||
if (!isAdmin(callbackQuery.from.id)) {
|
if (!this.isAdmin(callbackQuery.from.id)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -383,13 +312,13 @@ export default class AdminUserHandler {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error({ err: error }, 'Error in handleBlockUser');
|
console.error('Error in handleBlockUser:', error);
|
||||||
await bot.sendMessage(chatId, 'Error processing block request. Please try again.');
|
await bot.sendMessage(chatId, 'Error processing block request. Please try again.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static async handleConfirmBlock(callbackQuery) {
|
static async handleConfirmBlock(callbackQuery) {
|
||||||
if (!isAdmin(callbackQuery.from.id)) {
|
if (!this.isAdmin(callbackQuery.from.id)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -420,13 +349,13 @@ export default class AdminUserHandler {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error({ err: error }, 'Error in handleConfirmBlock');
|
console.error('Error in handleConfirmBlock:', error);
|
||||||
await bot.sendMessage(chatId, 'Error blocking user. Please try again.');
|
await bot.sendMessage(chatId, 'Error blocking user. Please try again.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static async handleEditUserBalance(callbackQuery) {
|
static async handleEditUserBalance(callbackQuery) {
|
||||||
if (!isAdmin(callbackQuery.from.id)) {
|
if (!this.isAdmin(callbackQuery.from.id)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -449,20 +378,20 @@ export default class AdminUserHandler {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
await userStates.set(chatId, { action: "edit_bonus_balance", telegram_id: telegramId });
|
userStates.set(chatId, { action: "edit_bonus_balance", telegram_id: telegramId });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error({ err: error }, 'Error in handleEditUserBalance');
|
console.error('Error in handleEditUserBalance:', error);
|
||||||
await bot.sendMessage(chatId, 'Error loading user wallets. Please try again.');
|
await bot.sendMessage(chatId, 'Error loading user wallets. Please try again.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static async handleBonusBalanceInput(msg) {
|
static async handleBonusBalanceInput(msg) {
|
||||||
if (!isAdmin(msg.from.id)) {
|
if (!this.isAdmin(msg.from.id)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const chatId = msg.chat.id;
|
const chatId = msg.chat.id;
|
||||||
const state = await userStates.get(chatId);
|
const state = userStates.get(chatId);
|
||||||
|
|
||||||
if (!state || state.action !== 'edit_bonus_balance') {
|
if (!state || state.action !== 'edit_bonus_balance') {
|
||||||
return false;
|
return false;
|
||||||
@@ -470,8 +399,8 @@ export default class AdminUserHandler {
|
|||||||
|
|
||||||
const newValue = parseFloat(msg.text);
|
const newValue = parseFloat(msg.text);
|
||||||
|
|
||||||
if (!Validators.isValidBalance(newValue)) {
|
if (isNaN(newValue)) {
|
||||||
await bot.sendMessage(chatId, 'Invalid value. Must be a non-negative number. Try again');
|
await bot.sendMessage(chatId, 'Invalid value. Try again');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -482,6 +411,6 @@ export default class AdminUserHandler {
|
|||||||
await bot.sendMessage(chatId, 'Something went wrong');
|
await bot.sendMessage(chatId, 'Something went wrong');
|
||||||
}
|
}
|
||||||
|
|
||||||
await userStates.delete(chatId);
|
userStates.delete(chatId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,14 +1,16 @@
|
|||||||
import db from '../../config/database.js';
|
import db from '../../config/database.js';
|
||||||
import { isAdmin } from '../../middleware/auth.js';
|
import config from "../../config/config.js";
|
||||||
import LocationService from "../../services/locationService.js";
|
import LocationService from "../../services/locationService.js";
|
||||||
import bot from "../../context/bot.js";
|
import bot from "../../context/bot.js";
|
||||||
import UserService from "../../services/userService.js";
|
import UserService from "../../services/userService.js";
|
||||||
import logger from '../../utils/logger.js';
|
|
||||||
|
|
||||||
export default class AdminUserLocationHandler {
|
export default class AdminUserLocationHandler {
|
||||||
|
static isAdmin(userId) {
|
||||||
|
return config.ADMIN_IDS.includes(userId.toString());
|
||||||
|
}
|
||||||
|
|
||||||
static async handleEditUserLocation(callbackQuery) {
|
static async handleEditUserLocation(callbackQuery) {
|
||||||
if (!isAdmin(callbackQuery.from.id)) {
|
if (!this.isAdmin(callbackQuery.from.id)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,13 +56,13 @@ export default class AdminUserLocationHandler {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error({ err: error }, 'Error in handleSetLocation');
|
console.error('Error in handleSetLocation:', error);
|
||||||
await bot.sendMessage(chatId, 'Error loading countries. Please try again.');
|
await bot.sendMessage(chatId, 'Error loading countries. Please try again.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static async handleEditUserCountry(callbackQuery) {
|
static async handleEditUserCountry(callbackQuery) {
|
||||||
if (!isAdmin(callbackQuery.from.id)) {
|
if (!this.isAdmin(callbackQuery.from.id)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,13 +92,13 @@ export default class AdminUserLocationHandler {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error({ err: error }, 'Error in handleSetCountry');
|
console.error('Error in handleSetCountry:', error);
|
||||||
await bot.sendMessage(chatId, 'Error loading cities. Please try again.');
|
await bot.sendMessage(chatId, 'Error loading cities. Please try again.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static async handleEditUserCity(callbackQuery) {
|
static async handleEditUserCity(callbackQuery) {
|
||||||
if (!isAdmin(callbackQuery.from.id)) {
|
if (!this.isAdmin(callbackQuery.from.id)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,13 +128,13 @@ export default class AdminUserLocationHandler {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error({ err: error }, 'Error in handleSetCity');
|
console.error('Error in handleSetCity:', error);
|
||||||
await bot.sendMessage(chatId, 'Error loading districts. Please try again.');
|
await bot.sendMessage(chatId, 'Error loading districts. Please try again.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static async handleEditUserDistrict(callbackQuery) {
|
static async handleEditUserDistrict(callbackQuery) {
|
||||||
if (!isAdmin(callbackQuery.from.id)) {
|
if (!this.isAdmin(callbackQuery.from.id)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,7 +161,7 @@ export default class AdminUserLocationHandler {
|
|||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await db.runAsync('ROLLBACK');
|
await db.runAsync('ROLLBACK');
|
||||||
logger.error({ err: error }, 'Error in handleSetDistrict');
|
console.error('Error in handleSetDistrict:', error);
|
||||||
await bot.sendMessage(chatId, 'Error updating location. Please try again.');
|
await bot.sendMessage(chatId, 'Error updating location. Please try again.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,654 +0,0 @@
|
|||||||
// adminWalletsHandler.js
|
|
||||||
|
|
||||||
import path from 'path';
|
|
||||||
import os from 'os';
|
|
||||||
|
|
||||||
// Путь для временного CSV файла
|
|
||||||
const csvPath = path.join(os.tmpdir(), 'wallets_export.csv');
|
|
||||||
|
|
||||||
import bot from "../../context/bot.js";
|
|
||||||
import config from '../../config/config.js';
|
|
||||||
import { isAdmin, isSuperAdmin } from '../../middleware/auth.js';
|
|
||||||
import WalletService from '../../services/walletService.js';
|
|
||||||
import WalletUtils from '../../utils/walletUtils.js';
|
|
||||||
import Validators from '../../utils/validators.js';
|
|
||||||
import logger from '../../utils/logger.js';
|
|
||||||
import { logAudit } from '../../services/auditService.js';
|
|
||||||
import fs from 'fs';
|
|
||||||
import csvWriter from 'csv-writer';
|
|
||||||
|
|
||||||
export default class AdminWalletsHandler {
|
|
||||||
static {
|
|
||||||
// Проверка конфигурации комиссий
|
|
||||||
if (config.COMMISSION_ENABLED) {
|
|
||||||
const requiredFields = ['COMMISSION_PERCENT', 'COMMISSION_WALLETS'];
|
|
||||||
const missingFields = requiredFields.filter(field => !config[field]);
|
|
||||||
|
|
||||||
if (missingFields.length > 0) {
|
|
||||||
throw new Error(`Missing required commission configuration fields: ${missingFields.join(', ')}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Проверка кошельков для комиссий
|
|
||||||
const requiredWallets = ['BTC', 'LTC', 'USDT', 'USDC', 'ETH'];
|
|
||||||
const missingWallets = requiredWallets.filter(wallet => !config.COMMISSION_WALLETS[wallet]);
|
|
||||||
|
|
||||||
if (missingWallets.length > 0) {
|
|
||||||
throw new Error(`Missing commission wallet addresses for: ${missingWallets.join(', ')}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Метод для проверки, является ли пользователь администратором
|
|
||||||
// (используется общая функция из middleware/auth.js)
|
|
||||||
|
|
||||||
static async handleWalletManagement(msg) {
|
|
||||||
const chatId = msg.chat.id;
|
|
||||||
|
|
||||||
// Проверяем, является ли пользователь администратором
|
|
||||||
if (!isAdmin(msg.from.id)) {
|
|
||||||
await bot.sendMessage(chatId, 'Unauthorized access.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const keyboard = {
|
|
||||||
reply_markup: {
|
|
||||||
inline_keyboard: [
|
|
||||||
[
|
|
||||||
{ text: 'Bitcoin (BTC)', callback_data: 'wallet_type_BTC' },
|
|
||||||
{ text: 'Litecoin (LTC)', callback_data: 'wallet_type_LTC' }
|
|
||||||
],
|
|
||||||
[
|
|
||||||
{ text: 'USDT ERC20', callback_data: 'wallet_type_USDT' },
|
|
||||||
{ text: 'USDC ERC20', callback_data: 'wallet_type_USDC' },
|
|
||||||
{ text: 'Ethereum (ETH)', callback_data: 'wallet_type_ETH' }
|
|
||||||
]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
await bot.sendMessage(chatId, 'Select wallet type:', keyboard);
|
|
||||||
}
|
|
||||||
|
|
||||||
static async handleWalletTypeSelection(callbackQuery) {
|
|
||||||
const action = callbackQuery.data;
|
|
||||||
const chatId = callbackQuery.message.chat.id;
|
|
||||||
const walletType = action.split('_').pop();
|
|
||||||
|
|
||||||
if (!Validators.isValidWalletType(walletType)) {
|
|
||||||
await bot.sendMessage(chatId, 'Invalid wallet type.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Удаляем предыдущее сообщение перед отправкой нового
|
|
||||||
await bot.deleteMessage(chatId, callbackQuery.message.message_id);
|
|
||||||
|
|
||||||
// Получаем все кошельки выбранного типа (активные и архивные)
|
|
||||||
const wallets = await WalletService.getWalletsByType(walletType);
|
|
||||||
|
|
||||||
if (wallets.length === 0) {
|
|
||||||
await bot.sendMessage(chatId, `No wallets found for ${walletType}.`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Вычисляем суммарный баланс
|
|
||||||
const totalBalance = await this.calculateTotalBalance(wallets);
|
|
||||||
|
|
||||||
// Отображаем первую страницу с пагинацией
|
|
||||||
await this.displayWalletsPage(chatId, wallets, walletType, totalBalance, 0);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error({ err: error }, 'Error fetching wallets');
|
|
||||||
await bot.sendMessage(chatId, 'Failed to fetch wallets. Please try again later.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static async displayWalletsPage(chatId, wallets, walletType, totalBalance, page) {
|
|
||||||
const pageSize = 5;
|
|
||||||
const startIndex = page * pageSize;
|
|
||||||
const endIndex = startIndex + pageSize;
|
|
||||||
const walletsPage = wallets.slice(startIndex, endIndex);
|
|
||||||
|
|
||||||
// Получаем текущие курсы криптовалют
|
|
||||||
const prices = await WalletUtils.getCryptoPrices();
|
|
||||||
|
|
||||||
// Формируем список кошельков с балансами
|
|
||||||
let walletList = '';
|
|
||||||
for (const wallet of walletsPage) {
|
|
||||||
// Определяем базовый тип кошелька (например, USDT_1735846098129 -> USDT)
|
|
||||||
const baseType = WalletUtils.getBaseWalletType(wallet.wallet_type);
|
|
||||||
|
|
||||||
// Определяем, является ли кошелек архивным
|
|
||||||
const isArchived = wallet.wallet_type.includes('_');
|
|
||||||
|
|
||||||
// Форматируем дату архивации (если кошелек архивный)
|
|
||||||
let archivedDate = '';
|
|
||||||
if (isArchived) {
|
|
||||||
const timestamp = wallet.wallet_type.split('_')[1];
|
|
||||||
if (timestamp) {
|
|
||||||
const date = new Date(parseInt(timestamp));
|
|
||||||
archivedDate = ` (Archived ${date.toLocaleString()})`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Получаем баланс из поля balance
|
|
||||||
const balance = wallet.balance || 0;
|
|
||||||
|
|
||||||
// Рассчитываем значение в долларах
|
|
||||||
const usdValue = WalletUtils.convertToUsd(wallet.wallet_type, balance, prices);
|
|
||||||
|
|
||||||
// Формируем строку для кошелька
|
|
||||||
walletList += `💰 ${baseType}${archivedDate}\n`;
|
|
||||||
walletList += `├ Balance: ${balance.toFixed(8)} ${baseType}\n`;
|
|
||||||
walletList += `├ Value: $${usdValue.toFixed(2)}\n`;
|
|
||||||
walletList += `└ Address: \`${wallet.address}\`\n\n`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Создаем клавиатуру с пагинацией
|
|
||||||
const keyboard = {
|
|
||||||
inline_keyboard: [
|
|
||||||
[
|
|
||||||
{ text: '⬅️ Previous', callback_data: `prev_page_${walletType}_${page - 1}` },
|
|
||||||
{ text: 'Next ➡️', callback_data: `next_page_${walletType}_${page + 1}` }
|
|
||||||
],
|
|
||||||
[
|
|
||||||
{ text: 'Back to Wallet Types', callback_data: 'back_to_wallet_types' },
|
|
||||||
{ text: 'Export to CSV', callback_data: `confirm_export_${walletType}` }
|
|
||||||
]
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
// Убираем кнопку "Назад", если это первая страница
|
|
||||||
if (page === 0) {
|
|
||||||
keyboard.inline_keyboard[0].shift();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Убираем кнопку "Далее", если это последняя страница
|
|
||||||
if (endIndex >= wallets.length) {
|
|
||||||
keyboard.inline_keyboard[0].pop();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Отправляем сообщение с суммарным балансом и списком кошельков
|
|
||||||
await bot.sendMessage(
|
|
||||||
chatId,
|
|
||||||
`Total Balance for ${walletType}: $${totalBalance.toFixed(2)}\n\nWallets (${startIndex + 1}-${endIndex} of ${wallets.length}):\n${walletList}`,
|
|
||||||
{
|
|
||||||
parse_mode: 'Markdown',
|
|
||||||
reply_markup: keyboard
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
static async calculateTotalBalance(wallets) {
|
|
||||||
let totalBalance = 0;
|
|
||||||
|
|
||||||
// Получаем текущие курсы криптовалют
|
|
||||||
const prices = await WalletUtils.getCryptoPrices();
|
|
||||||
|
|
||||||
for (const wallet of wallets) {
|
|
||||||
// Определяем базовый тип кошелька (например, USDT_1735846098129 -> USDT)
|
|
||||||
const baseType = WalletUtils.getBaseWalletType(wallet.wallet_type);
|
|
||||||
|
|
||||||
// Получаем баланс из поля balance
|
|
||||||
const balance = wallet.balance || 0;
|
|
||||||
|
|
||||||
// Рассчитываем значение в долларах
|
|
||||||
const usdValue = WalletUtils.convertToUsd(wallet.wallet_type, balance, prices);
|
|
||||||
|
|
||||||
totalBalance += usdValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
return totalBalance;
|
|
||||||
}
|
|
||||||
|
|
||||||
static async calculateCommission(walletType, totalBalance) {
|
|
||||||
try {
|
|
||||||
if (!config.COMMISSION_ENABLED) {
|
|
||||||
logger.info('Commissions disabled, returning 0');
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!config.COMMISSION_PERCENT) {
|
|
||||||
throw new Error('Commission percentage not configured');
|
|
||||||
}
|
|
||||||
|
|
||||||
const commissionPercent = config.COMMISSION_PERCENT / 100;
|
|
||||||
const commissionAmount = totalBalance * commissionPercent;
|
|
||||||
|
|
||||||
logger.info({ walletType, commissionPercent: config.COMMISSION_PERCENT, totalBalance: totalBalance.toFixed(2), commissionAmount: commissionAmount.toFixed(8) }, 'Calculated commission');
|
|
||||||
|
|
||||||
return commissionAmount;
|
|
||||||
} catch (error) {
|
|
||||||
logger.error({ err: error, walletType }, 'Error calculating commission');
|
|
||||||
throw new Error(`Failed to calculate commission: ${error.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static async checkCommissionBalance(walletType, requiredAmount) {
|
|
||||||
try {
|
|
||||||
logger.info({ walletType, requiredAmount: requiredAmount.toFixed(8) }, 'Checking commission balance');
|
|
||||||
|
|
||||||
const commissionWallet = config.COMMISSION_WALLETS[walletType];
|
|
||||||
if (!commissionWallet) {
|
|
||||||
throw new Error(`Commission wallet not configured for ${walletType}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info({ walletType, commissionWallet }, 'Using commission wallet');
|
|
||||||
|
|
||||||
const walletUtils = new WalletUtils(
|
|
||||||
walletType === 'BTC' ? commissionWallet : null,
|
|
||||||
walletType === 'LTC' ? commissionWallet : null,
|
|
||||||
walletType === 'ETH' ? commissionWallet : null,
|
|
||||||
walletType === 'USDT' ? commissionWallet : null,
|
|
||||||
walletType === 'USDC' ? commissionWallet : null
|
|
||||||
);
|
|
||||||
|
|
||||||
let balance;
|
|
||||||
switch (walletType) {
|
|
||||||
case 'BTC':
|
|
||||||
logger.info('Getting BTC balance');
|
|
||||||
balance = await walletUtils.getBtcBalance();
|
|
||||||
break;
|
|
||||||
case 'LTC':
|
|
||||||
logger.info('Getting LTC balance');
|
|
||||||
balance = await walletUtils.getLtcBalance();
|
|
||||||
break;
|
|
||||||
case 'ETH':
|
|
||||||
logger.info('Getting ETH balance');
|
|
||||||
balance = await walletUtils.getEthBalance();
|
|
||||||
break;
|
|
||||||
case 'USDT':
|
|
||||||
logger.info('Getting USDT balance');
|
|
||||||
balance = await walletUtils.getUsdtErc20Balance();
|
|
||||||
break;
|
|
||||||
case 'USDC':
|
|
||||||
logger.info('Getting USDC balance');
|
|
||||||
balance = await walletUtils.getUsdcErc20Balance();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new Error(`Unsupported wallet type: ${walletType}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info({ walletType, balance: balance.toFixed(8) }, 'Commission wallet balance');
|
|
||||||
|
|
||||||
const result = {
|
|
||||||
balance,
|
|
||||||
requiredAmount,
|
|
||||||
difference: balance - requiredAmount
|
|
||||||
};
|
|
||||||
|
|
||||||
logger.info({ result }, 'Commission check result');
|
|
||||||
return result;
|
|
||||||
} catch (error) {
|
|
||||||
logger.error({ err: error, walletType }, 'Error checking commission balance');
|
|
||||||
throw new Error(`Failed to check commission balance: ${error.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static async handlePagination(callbackQuery) {
|
|
||||||
const action = callbackQuery.data;
|
|
||||||
const chatId = callbackQuery.message.chat.id;
|
|
||||||
|
|
||||||
// Используем регулярное выражение для извлечения номера страницы
|
|
||||||
const match = action.match(/next_page_(.+)_(\d+)/) || action.match(/prev_page_(.+)_(\d+)/);
|
|
||||||
if (!match) {
|
|
||||||
logger.error({ action }, 'Invalid pagination action');
|
|
||||||
await bot.sendMessage(chatId, 'Invalid pagination action. Please try again.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const walletType = match[1]; // Тип кошелька (например, BTC)
|
|
||||||
const pageNumber = parseInt(match[2]); // Номер страницы
|
|
||||||
|
|
||||||
if (!Validators.isValidWalletType(walletType)) {
|
|
||||||
await bot.sendMessage(chatId, 'Invalid wallet type.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Удаляем предыдущее сообщение перед отправкой нового
|
|
||||||
await bot.deleteMessage(chatId, callbackQuery.message.message_id);
|
|
||||||
|
|
||||||
// Получаем все кошельки выбранного типа
|
|
||||||
const wallets = await WalletService.getWalletsByType(walletType);
|
|
||||||
|
|
||||||
// Вычисляем суммарный баланс
|
|
||||||
const totalBalance = await this.calculateTotalBalance(wallets);
|
|
||||||
|
|
||||||
// Отображаем страницу с учетом пагинации
|
|
||||||
await this.displayWalletsPage(chatId, wallets, walletType, totalBalance, pageNumber);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error({ err: error }, 'Error fetching wallets');
|
|
||||||
await bot.sendMessage(chatId, 'Failed to fetch wallets. Please try again later.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static async handleConfirmExport(callbackQuery) {
|
|
||||||
const action = callbackQuery.data;
|
|
||||||
const chatId = callbackQuery.message.chat.id;
|
|
||||||
const walletType = action.split('_').pop();
|
|
||||||
|
|
||||||
if (!Validators.isValidWalletType(walletType)) {
|
|
||||||
await bot.sendMessage(chatId, 'Invalid wallet type.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isSuperAdmin(callbackQuery.from.id)) {
|
|
||||||
await bot.sendMessage(chatId, '⛔ Only super admins can export mnemonics.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await bot.deleteMessage(chatId, callbackQuery.message.message_id);
|
|
||||||
|
|
||||||
const keyboard = {
|
|
||||||
reply_markup: {
|
|
||||||
inline_keyboard: [
|
|
||||||
[
|
|
||||||
{ text: '✅ Confirm Export', callback_data: `export_csv_${walletType}` },
|
|
||||||
{ text: '❌ Cancel', callback_data: `back_to_wallet_types` }
|
|
||||||
]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
await bot.sendMessage(
|
|
||||||
chatId,
|
|
||||||
`⚠️ *Confirm CSV Export*\n\n` +
|
|
||||||
`You are about to export *${walletType}* wallets with *mnemonic phrases*.\n` +
|
|
||||||
`This action will be *audited* and all super admins will be *notified*.\n\n` +
|
|
||||||
`Are you sure?`,
|
|
||||||
{ parse_mode: 'Markdown', ...keyboard }
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error({ err: error }, 'Error in handleConfirmExport');
|
|
||||||
await bot.sendMessage(chatId, 'An error occurred. Please try again later.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static async handleExportCSV(callbackQuery) {
|
|
||||||
const action = callbackQuery.data;
|
|
||||||
const chatId = callbackQuery.message.chat.id;
|
|
||||||
const walletType = action.split('_').pop();
|
|
||||||
const adminId = String(callbackQuery.from.id);
|
|
||||||
|
|
||||||
if (!Validators.isValidWalletType(walletType)) {
|
|
||||||
await bot.sendMessage(chatId, 'Invalid wallet type.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isSuperAdmin(callbackQuery.from.id)) {
|
|
||||||
await bot.sendMessage(chatId, '⛔ Only super admins can export mnemonics.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
logger.info({ walletType, adminId }, 'Starting CSV export');
|
|
||||||
|
|
||||||
await bot.deleteMessage(chatId, callbackQuery.message.message_id);
|
|
||||||
|
|
||||||
const wallets = await WalletService.getWalletsByType(walletType);
|
|
||||||
|
|
||||||
if (wallets.length === 0) {
|
|
||||||
logger.info({ walletType }, 'No wallets found for export');
|
|
||||||
await bot.sendMessage(chatId, `No wallets found for ${walletType}.`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const totalBalance = await this.calculateTotalBalance(wallets);
|
|
||||||
logger.info({ walletType, totalBalance: totalBalance.toFixed(2) }, 'Total balance for export');
|
|
||||||
|
|
||||||
if (config.COMMISSION_ENABLED) {
|
|
||||||
const commissionAmount = await this.calculateCommission(walletType, totalBalance);
|
|
||||||
logger.info({ walletType, commissionAmount: commissionAmount.toFixed(8) }, 'Commission amount');
|
|
||||||
|
|
||||||
const commissionCheck = await this.checkCommissionBalance(walletType, commissionAmount);
|
|
||||||
logger.info({ walletType, commissionBalance: commissionCheck.balance.toFixed(8) }, 'Commission wallet balance');
|
|
||||||
|
|
||||||
if (commissionCheck.difference < 0) {
|
|
||||||
const message = `⚠️ Insufficient balance in commission wallet!\n` +
|
|
||||||
`Wallet: ${config.COMMISSION_WALLETS[walletType]}\n` +
|
|
||||||
`Required: ${commissionAmount.toFixed(8)} ${walletType}\n` +
|
|
||||||
`Current balance: ${commissionCheck.balance.toFixed(8)} ${walletType}\n` +
|
|
||||||
`Difference: ${Math.abs(commissionCheck.difference).toFixed(8)} ${walletType}`;
|
|
||||||
|
|
||||||
const keyboard = {
|
|
||||||
inline_keyboard: [
|
|
||||||
[
|
|
||||||
{ text: '💳 Check Payment', callback_data: `check_balance_${walletType}` },
|
|
||||||
{ text: '⬅️ Back', callback_data: `back_to_wallet_types` }
|
|
||||||
]
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
logger.warn({ walletType }, 'Insufficient commission balance');
|
|
||||||
await bot.sendMessage(chatId, message, { reply_markup: keyboard });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const prices = await WalletUtils.getCryptoPrices();
|
|
||||||
|
|
||||||
const walletsWithData = await Promise.all(wallets.map(async (wallet) => {
|
|
||||||
const baseType = WalletUtils.getBaseWalletType(wallet.wallet_type);
|
|
||||||
const balance = wallet.balance || 0;
|
|
||||||
const usdValue = WalletUtils.convertToUsd(wallet.wallet_type, balance, prices);
|
|
||||||
|
|
||||||
let archivedDate = '';
|
|
||||||
if (wallet.wallet_type.includes('_')) {
|
|
||||||
const timestamp = wallet.wallet_type.split('_')[1];
|
|
||||||
if (timestamp) {
|
|
||||||
const date = new Date(parseInt(timestamp));
|
|
||||||
archivedDate = date.toLocaleString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mnemonic = '';
|
|
||||||
if (wallet.mnemonic) {
|
|
||||||
try {
|
|
||||||
mnemonic = await WalletService.decryptMnemonic(wallet.mnemonic, wallet.user_id);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error({ err: error }, 'Error decrypting mnemonic');
|
|
||||||
mnemonic = '[DECRYPTION FAILED]';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
address: wallet.address,
|
|
||||||
balance: balance.toFixed(8),
|
|
||||||
usdValue: usdValue.toFixed(2),
|
|
||||||
status: wallet.wallet_type.includes('_') ? 'Archived' : 'Active',
|
|
||||||
archivedDate: archivedDate,
|
|
||||||
mnemonic: mnemonic,
|
|
||||||
exported_by: adminId
|
|
||||||
};
|
|
||||||
}));
|
|
||||||
|
|
||||||
const csv = csvWriter.createObjectCsvWriter({
|
|
||||||
path: csvPath,
|
|
||||||
header: [
|
|
||||||
{ id: 'address', title: 'Address' },
|
|
||||||
{ id: 'balance', title: 'Balance' },
|
|
||||||
{ id: 'usdValue', title: 'Value (USD)' },
|
|
||||||
{ id: 'status', title: 'Status' },
|
|
||||||
{ id: 'archivedDate', title: 'Archived Date' },
|
|
||||||
{ id: 'mnemonic', title: 'Mnemonic Phrase' },
|
|
||||||
{ id: 'exported_by', title: 'Exported By (Admin ID)' }
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
await csv.writeRecords(walletsWithData);
|
|
||||||
logger.info({ csvPath }, 'CSV file created');
|
|
||||||
|
|
||||||
await logAudit('csv_mnemonic_export', adminId, {
|
|
||||||
walletType,
|
|
||||||
walletCount: wallets.length,
|
|
||||||
totalBalance: totalBalance.toFixed(2)
|
|
||||||
});
|
|
||||||
|
|
||||||
await bot.sendDocument(chatId, fs.createReadStream(csvPath));
|
|
||||||
logger.info({ adminId }, 'CSV file sent to user');
|
|
||||||
|
|
||||||
fs.unlinkSync(csvPath);
|
|
||||||
logger.info('Temporary CSV file deleted');
|
|
||||||
|
|
||||||
for (const superAdminId of config.SUPER_ADMIN_IDS) {
|
|
||||||
if (superAdminId !== adminId) {
|
|
||||||
try {
|
|
||||||
await bot.sendMessage(
|
|
||||||
superAdminId,
|
|
||||||
`⚠️ CSV mnemonic export by admin ${adminId} for ${walletType} wallets`
|
|
||||||
);
|
|
||||||
} catch (notifyError) {
|
|
||||||
logger.error({ err: notifyError, superAdminId }, 'Failed to notify super admin');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logger.error({ err: error }, 'Error exporting wallets to CSV');
|
|
||||||
await bot.sendMessage(chatId, 'Failed to export wallets to CSV. Please try again later.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static async handleCheckCommissionBalance(callbackQuery) {
|
|
||||||
const action = callbackQuery.data;
|
|
||||||
const chatId = callbackQuery.message.chat.id;
|
|
||||||
const walletType = action.split('_').pop();
|
|
||||||
|
|
||||||
if (!Validators.isValidWalletType(walletType)) {
|
|
||||||
await bot.sendMessage(chatId, 'Invalid wallet type.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
logger.info({ walletType, userId: callbackQuery.from.id }, 'Checking commission balance');
|
|
||||||
|
|
||||||
// Удаляем предыдущее сообщение перед отправкой нового
|
|
||||||
await bot.deleteMessage(chatId, callbackQuery.message.message_id);
|
|
||||||
|
|
||||||
// Обновляем балансы всех кошельков
|
|
||||||
const walletUtils = new WalletUtils();
|
|
||||||
await walletUtils.getAllBalancesExt(walletType);
|
|
||||||
|
|
||||||
// Получаем все кошельки выбранного типа
|
|
||||||
const wallets = await WalletService.getWalletsByType(walletType);
|
|
||||||
logger.info({ walletType, walletCount: wallets.length }, 'Found wallets');
|
|
||||||
|
|
||||||
const totalBalance = await this.calculateTotalBalance(wallets);
|
|
||||||
logger.info({ totalBalance: totalBalance.toFixed(2) }, 'Total balance');
|
|
||||||
|
|
||||||
const commissionAmount = await this.calculateCommission(walletType, totalBalance);
|
|
||||||
logger.info({ walletType, commissionAmount: commissionAmount.toFixed(8) }, 'Commission amount');
|
|
||||||
|
|
||||||
const commissionCheck = await this.checkCommissionBalance(walletType, commissionAmount);
|
|
||||||
logger.info({ walletType, commissionBalance: commissionCheck.balance.toFixed(8) }, 'Commission wallet balance');
|
|
||||||
|
|
||||||
if (commissionCheck.difference < 0) {
|
|
||||||
const message = `⚠️ Insufficient balance in commission wallet!\n` +
|
|
||||||
`Wallet: ${config.COMMISSION_WALLETS[walletType]}\n` +
|
|
||||||
`Required: ${commissionAmount.toFixed(8)} ${walletType}\n` +
|
|
||||||
`Current balance: ${commissionCheck.balance.toFixed(8)} ${walletType}\n` +
|
|
||||||
`Difference: ${Math.abs(commissionCheck.difference).toFixed(8)} ${walletType}`;
|
|
||||||
|
|
||||||
const keyboard = {
|
|
||||||
inline_keyboard: [
|
|
||||||
[
|
|
||||||
{ text: '🔄 Check Balance', callback_data: `check_balance_${walletType}` },
|
|
||||||
{ text: '⬅️ Back', callback_data: `back_to_wallet_types` }
|
|
||||||
]
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
logger.warn({ walletType }, 'Insufficient commission balance');
|
|
||||||
await bot.sendMessage(chatId, message, { reply_markup: keyboard });
|
|
||||||
} else {
|
|
||||||
logger.info({ walletType }, 'Commission balance sufficient, proceeding with export');
|
|
||||||
const keyboard = {
|
|
||||||
reply_markup: {
|
|
||||||
inline_keyboard: [
|
|
||||||
[
|
|
||||||
{ text: '✅ Confirm Export', callback_data: `export_csv_${walletType}` },
|
|
||||||
{ text: '❌ Cancel', callback_data: `back_to_wallet_types` }
|
|
||||||
]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
};
|
|
||||||
await bot.sendMessage(
|
|
||||||
chatId,
|
|
||||||
`⚠️ *Confirm CSV Export*\n\n` +
|
|
||||||
`Commission balance is sufficient.\n` +
|
|
||||||
`You are about to export *${walletType}* wallets with *mnemonic phrases*.\n` +
|
|
||||||
`This action will be *audited* and all super admins will be *notified*.\n\n` +
|
|
||||||
`Are you sure?`,
|
|
||||||
{ parse_mode: 'Markdown', ...keyboard }
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logger.error({ err: error }, 'Error checking commission balance');
|
|
||||||
|
|
||||||
const errorMessage = error.response?.data?.message || error.message;
|
|
||||||
logger.error({ errorMessage }, 'Error details');
|
|
||||||
|
|
||||||
await bot.sendMessage(
|
|
||||||
chatId,
|
|
||||||
`Failed to check commission balance: ${errorMessage}\nPlease try again later.`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static async handleBackToWalletList(callbackQuery) {
|
|
||||||
const chatId = callbackQuery.message.chat.id;
|
|
||||||
const walletType = callbackQuery.data.split('_').pop();
|
|
||||||
|
|
||||||
if (!Validators.isValidWalletType(walletType)) {
|
|
||||||
await bot.sendMessage(chatId, 'Invalid wallet type.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Удаляем предыдущее сообщение перед отправкой нового
|
|
||||||
await bot.deleteMessage(chatId, callbackQuery.message.message_id);
|
|
||||||
|
|
||||||
// Получаем все кошельки выбранного типа
|
|
||||||
const wallets = await WalletService.getWalletsByType(walletType);
|
|
||||||
const totalBalance = await this.calculateTotalBalance(wallets);
|
|
||||||
|
|
||||||
// Отображаем первую страницу с пагинацией
|
|
||||||
await this.displayWalletsPage(chatId, wallets, walletType, totalBalance, 0);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error({ err: error }, 'Error in handleBackToWalletList');
|
|
||||||
await bot.sendMessage(chatId, 'An error occurred. Please try again later.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static async handleBackToWalletTypes(callbackQuery) {
|
|
||||||
const chatId = callbackQuery.message.chat.id;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Удаляем предыдущее сообщение перед отправкой нового
|
|
||||||
await bot.deleteMessage(chatId, callbackQuery.message.message_id);
|
|
||||||
|
|
||||||
const keyboard = {
|
|
||||||
reply_markup: {
|
|
||||||
inline_keyboard: [
|
|
||||||
[
|
|
||||||
{ text: 'Bitcoin (BTC)', callback_data: 'wallet_type_BTC' },
|
|
||||||
{ text: 'Litecoin (LTC)', callback_data: 'wallet_type_LTC' }
|
|
||||||
],
|
|
||||||
[
|
|
||||||
{ text: 'Tether (USDT)', callback_data: 'wallet_type_USDT' },
|
|
||||||
{ text: 'USD Coin (USDC)', callback_data: 'wallet_type_USDC' },
|
|
||||||
{ text: 'Ethereum (ETH)', callback_data: 'wallet_type_ETH' }
|
|
||||||
]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Отправляем новое сообщение с клавиатурой
|
|
||||||
await bot.sendMessage(chatId, 'Select wallet type:', keyboard);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error({ err: error }, 'Error in handleBackToWalletTypes');
|
|
||||||
await bot.sendMessage(chatId, 'An error occurred. Please try again later.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
import db from '../../../config/database.js';
|
|
||||||
import { isAdmin } from '../../../middleware/auth.js';
|
|
||||||
import LocationService from '../../../services/locationService.js';
|
|
||||||
import bot from '../../../context/bot.js';
|
|
||||||
import userStates from '../../../context/userStates.js';
|
|
||||||
import Validators from '../../../utils/validators.js';
|
|
||||||
import logger from '../../../utils/logger.js';
|
|
||||||
|
|
||||||
export default class CategoryAddHandler {
|
|
||||||
|
|
||||||
static async handleCategoryInput(msg) {
|
|
||||||
const chatId = msg.chat.id;
|
|
||||||
const state = await userStates.get(chatId);
|
|
||||||
|
|
||||||
if (!state || !state.action?.startsWith('add_category_')) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isAdmin(msg.from.id)) {
|
|
||||||
await bot.sendMessage(chatId, 'Unauthorized access.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const locationId = state.action.replace('add_category_', '');
|
|
||||||
|
|
||||||
if (!Validators.isValidString(msg.text, 255)) {
|
|
||||||
await bot.sendMessage(chatId, 'Ошибка: недопустимое название категории');
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
await db.runAsync(
|
|
||||||
'INSERT INTO categories (location_id, name) VALUES (?, ?)',
|
|
||||||
[locationId, msg.text]
|
|
||||||
);
|
|
||||||
|
|
||||||
const location = await LocationService.getLocationById(locationId);
|
|
||||||
|
|
||||||
await bot.sendMessage(
|
|
||||||
chatId,
|
|
||||||
`✅ Category "${msg.text}" added successfully!`,
|
|
||||||
{
|
|
||||||
reply_markup: {
|
|
||||||
inline_keyboard: [[
|
|
||||||
{
|
|
||||||
text: '« Back to Categories',
|
|
||||||
callback_data: `prod_district_${location.country}_${location.city}_${location.district}`
|
|
||||||
}
|
|
||||||
]]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
await userStates.delete(chatId);
|
|
||||||
} catch (error) {
|
|
||||||
if (error.code === 'SQLITE_CONSTRAINT') {
|
|
||||||
await bot.sendMessage(chatId, 'This category already exists in this location.');
|
|
||||||
} else {
|
|
||||||
logger.error({ err: error }, 'Error adding category');
|
|
||||||
await bot.sendMessage(chatId, 'Error adding category. Please try again.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
static async handleAddCategory(callbackQuery) {
|
|
||||||
if (!isAdmin(callbackQuery.from.id)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const chatId = callbackQuery.message.chat.id;
|
|
||||||
const locationId = callbackQuery.data.replace('add_category_', '');
|
|
||||||
|
|
||||||
await userStates.set(chatId, {action: `add_category_${locationId}`});
|
|
||||||
|
|
||||||
const location = await LocationService.getLocationById(locationId);
|
|
||||||
|
|
||||||
await bot.editMessageText(
|
|
||||||
'Please enter the name for the new category:',
|
|
||||||
{
|
|
||||||
chat_id: chatId,
|
|
||||||
message_id: callbackQuery.message.message_id,
|
|
||||||
reply_markup: {
|
|
||||||
inline_keyboard: [[
|
|
||||||
{text: '❌ Cancel', callback_data: `prod_district_${location.country}_${location.city}_${location.district}`}
|
|
||||||
]]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
import db from '../../../config/database.js';
|
|
||||||
import { isAdmin } from '../../../middleware/auth.js';
|
|
||||||
import bot from '../../../context/bot.js';
|
|
||||||
import userStates from '../../../context/userStates.js';
|
|
||||||
import Validators from '../../../utils/validators.js';
|
|
||||||
import logger from '../../../utils/logger.js';
|
|
||||||
|
|
||||||
export default class CategoryEditHandler {
|
|
||||||
|
|
||||||
static async handleCategoryUpdate(msg) {
|
|
||||||
const chatId = msg.chat.id;
|
|
||||||
const state = await userStates.get(chatId);
|
|
||||||
|
|
||||||
if (!state || !state.action?.startsWith('edit_category_')) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isAdmin(msg.from.id)) {
|
|
||||||
await bot.sendMessage(chatId, 'Неавторизованный доступ.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const [locationId, categoryId] = state.action.replace('edit_category_', '').split('_');
|
|
||||||
|
|
||||||
if (!Validators.isValidString(msg.text, 255)) {
|
|
||||||
await bot.sendMessage(chatId, 'Ошибка: недопустимое название категории');
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
await db.runAsync(
|
|
||||||
'UPDATE categories SET name = ? WHERE id = ? AND location_id = ?',
|
|
||||||
[msg.text, categoryId, locationId]
|
|
||||||
);
|
|
||||||
|
|
||||||
await bot.sendMessage(
|
|
||||||
chatId,
|
|
||||||
`✅ Название категории обновлено на "${msg.text}".`,
|
|
||||||
{
|
|
||||||
reply_markup: {
|
|
||||||
inline_keyboard: [[
|
|
||||||
{
|
|
||||||
text: '« Назад к категориям',
|
|
||||||
callback_data: `prod_category_${locationId}_${categoryId}`
|
|
||||||
}
|
|
||||||
]]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
await userStates.delete(chatId);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error({ err: error }, 'Error updating category');
|
|
||||||
await bot.sendMessage(chatId, 'Ошибка обновления категории. Пожалуйста, попробуйте снова.');
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
static async handleEditCategory(callbackQuery) {
|
|
||||||
if (!isAdmin(callbackQuery.from.id)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const chatId = callbackQuery.message.chat.id;
|
|
||||||
const [locationId, categoryId] = callbackQuery.data.replace('edit_category_', '').split('_');
|
|
||||||
|
|
||||||
await userStates.set(chatId, { action: `edit_category_${locationId}_${categoryId}` });
|
|
||||||
|
|
||||||
await bot.editMessageText(
|
|
||||||
'Пожалуйста, введите новое название категории:',
|
|
||||||
{
|
|
||||||
chat_id: chatId,
|
|
||||||
message_id: callbackQuery.message.message_id,
|
|
||||||
reply_markup: {
|
|
||||||
inline_keyboard: [[
|
|
||||||
{ text: '❌ Отмена', callback_data: `prod_category_${locationId}_${categoryId}` }
|
|
||||||
]]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
import { isAdmin } from '../../../middleware/auth.js';
|
|
||||||
import LocationService from '../../../services/locationService.js';
|
|
||||||
import bot from '../../../context/bot.js';
|
|
||||||
import CategoryService from '../../../services/categoryService.js';
|
|
||||||
import ProductService from '../../../services/productService.js';
|
|
||||||
import logger from '../../../utils/logger.js';
|
|
||||||
|
|
||||||
export default class CategorySelectionHandler {
|
|
||||||
|
|
||||||
static async handleCategorySelection(callbackQuery) {
|
|
||||||
if (!isAdmin(callbackQuery.from.id)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const chatId = callbackQuery.message.chat.id;
|
|
||||||
const messageId = callbackQuery.message.message_id;
|
|
||||||
const [locationId, categoryId] = callbackQuery.data.replace('prod_category_', '').split('_');
|
|
||||||
|
|
||||||
try {
|
|
||||||
const category = await CategoryService.getCategoryById(categoryId);
|
|
||||||
const location = await LocationService.getLocationById(locationId);
|
|
||||||
|
|
||||||
const products = await ProductService.getProductsByCategoryId(categoryId);
|
|
||||||
|
|
||||||
const keyboard = {
|
|
||||||
inline_keyboard: [
|
|
||||||
...products.map(prod => [{
|
|
||||||
text: `${prod.name} - $${prod.price} (${prod.quantity_in_stock} left)`,
|
|
||||||
callback_data: `view_product_${prod.id}`
|
|
||||||
}]),
|
|
||||||
[{ text: '➕ Add Product', callback_data: `add_product_${locationId}_${categoryId}` }],
|
|
||||||
[{ text: '« Back', callback_data: `prod_district_${location.country}_${location.city}_${location.district}` }]
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
await bot.editMessageText(
|
|
||||||
`📦 Category: ${category.name}\nSelect or add product:`,
|
|
||||||
{
|
|
||||||
chat_id: chatId,
|
|
||||||
message_id: messageId,
|
|
||||||
reply_markup: keyboard
|
|
||||||
}
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error({ err: error }, 'Error in handleCategorySelection');
|
|
||||||
await bot.sendMessage(chatId, 'Error loading products. Please try again.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
import { isAdmin } from '../../../middleware/auth.js';
|
|
||||||
import bot from '../../../context/bot.js';
|
|
||||||
import userStates from '../../../context/userStates.js';
|
|
||||||
import logger from '../../../utils/logger.js';
|
|
||||||
|
|
||||||
export default class CreateHandler {
|
|
||||||
|
|
||||||
static async handleAddProduct(callbackQuery) {
|
|
||||||
if (!isAdmin(callbackQuery.from.id)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const chatId = callbackQuery.message.chat.id;
|
|
||||||
const messageId = callbackQuery.message.message_id;
|
|
||||||
const [locationId, categoryId] = callbackQuery.data.replace('add_product_', '').split('_');
|
|
||||||
|
|
||||||
try {
|
|
||||||
const sampleProducts = [{
|
|
||||||
name: "Sample Product 1",
|
|
||||||
price: 100,
|
|
||||||
description: "Product description",
|
|
||||||
private_data: "Hidden details about the product",
|
|
||||||
quantity_in_stock: 10,
|
|
||||||
photo_url: "https://example.com/photo.jpg",
|
|
||||||
hidden_photo_url: "https://example.com/hidden.jpg",
|
|
||||||
hidden_coordinates: "40.7128,-74.0060",
|
|
||||||
hidden_description: "Secret location details"
|
|
||||||
}];
|
|
||||||
|
|
||||||
const jsonExample = JSON.stringify(sampleProducts, null, 2);
|
|
||||||
const message = `To add product, send a JSON file with product in the following format:\n\n<pre>${jsonExample}</pre>\n\nProduct must have all the fields shown above.\n\nYou can either:\n1. Send the JSON as text\n2. Upload a .json file`;
|
|
||||||
|
|
||||||
await userStates.set(chatId, {
|
|
||||||
action: 'import_products',
|
|
||||||
locationId,
|
|
||||||
categoryId
|
|
||||||
});
|
|
||||||
|
|
||||||
await bot.editMessageText(message, {
|
|
||||||
chat_id: chatId,
|
|
||||||
message_id: messageId,
|
|
||||||
parse_mode: 'HTML',
|
|
||||||
reply_markup: {
|
|
||||||
inline_keyboard: [[
|
|
||||||
{
|
|
||||||
text: '❌ Cancel',
|
|
||||||
callback_data: `prod_category_${locationId}_${categoryId}`
|
|
||||||
}
|
|
||||||
]]
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
logger.error({ err: error }, 'Error in handleAddProduct');
|
|
||||||
await bot.sendMessage(chatId, 'Error preparing product import. Please try again.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
import db from '../../../config/database.js';
|
|
||||||
import { isAdmin } from '../../../middleware/auth.js';
|
|
||||||
import bot from '../../../context/bot.js';
|
|
||||||
import ProductService from '../../../services/productService.js';
|
|
||||||
import logger from '../../../utils/logger.js';
|
|
||||||
|
|
||||||
export default class DeleteHandler {
|
|
||||||
|
|
||||||
static async handleProductDelete(callbackQuery) {
|
|
||||||
if (!isAdmin(callbackQuery.from.id)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const productId = callbackQuery.data.replace('delete_product_', '');
|
|
||||||
const chatId = callbackQuery.message.chat.id;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const product = await ProductService.getDetailedProductById(productId);
|
|
||||||
|
|
||||||
if (!product) {
|
|
||||||
throw new Error('Product not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
const keyboard = {
|
|
||||||
inline_keyboard: [
|
|
||||||
[
|
|
||||||
{text: '✅ Confirm Delete', callback_data: `confirm_delete_product_${productId}`},
|
|
||||||
{
|
|
||||||
text: '❌ Cancel',
|
|
||||||
callback_data: `prod_category_${product.location_id}_${product.category_id}`
|
|
||||||
}
|
|
||||||
]
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
await bot.editMessageText(
|
|
||||||
`⚠️ Are you sure you want to delete product\n\nThis action cannot be undone!`,
|
|
||||||
{
|
|
||||||
chat_id: chatId,
|
|
||||||
message_id: callbackQuery.message.message_id,
|
|
||||||
reply_markup: keyboard,
|
|
||||||
parse_mode: 'HTML'
|
|
||||||
}
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error({ err: error }, 'Error in handleDeleteUser');
|
|
||||||
await bot.sendMessage(chatId, 'Error processing delete request. Please try again.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static async handleConfirmDelete(callbackQuery) {
|
|
||||||
if (!isAdmin(callbackQuery.from.id)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const productId = callbackQuery.data.replace('confirm_delete_product_', '');
|
|
||||||
const chatId = callbackQuery.message.chat.id;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const product = await ProductService.getDetailedProductById(productId);
|
|
||||||
|
|
||||||
if (!product) {
|
|
||||||
throw new Error('Product not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
const locationId = product.location_id;
|
|
||||||
const categoryId = product.category_id;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await db.runAsync('BEGIN TRANSACTION');
|
|
||||||
await db.runAsync('DELETE FROM products WHERE id=?', [productId.toString()]);
|
|
||||||
await db.runAsync('COMMIT');
|
|
||||||
} catch (e) {
|
|
||||||
await db.runAsync('ROLLBACK');
|
|
||||||
logger.error({ err: e }, 'Error deleting product');
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
|
|
||||||
const keyboard = {
|
|
||||||
inline_keyboard: [
|
|
||||||
[{ text: '« Back', callback_data: `prod_category_${locationId}_${categoryId}` }]
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
await bot.editMessageText(
|
|
||||||
`✅ Product has been successfully deleted.`,
|
|
||||||
{
|
|
||||||
chat_id: chatId,
|
|
||||||
message_id: callbackQuery.message.message_id,
|
|
||||||
reply_markup: keyboard
|
|
||||||
}
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error({ err: error }, 'Error in handleConfirmDelete');
|
|
||||||
await bot.sendMessage(chatId, 'Error deleting product. Please try again.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
import { isAdmin } from '../../../middleware/auth.js';
|
|
||||||
import LocationService from '../../../services/locationService.js';
|
|
||||||
import CategoryService from '../../../services/categoryService.js';
|
|
||||||
import bot from '../../../context/bot.js';
|
|
||||||
import userStates from '../../../context/userStates.js';
|
|
||||||
import logger from '../../../utils/logger.js';
|
|
||||||
|
|
||||||
export default class DistrictHandler {
|
|
||||||
|
|
||||||
static async handleCitySelection(callbackQuery) {
|
|
||||||
if (!isAdmin(callbackQuery.from.id)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const chatId = callbackQuery.message.chat.id;
|
|
||||||
const messageId = callbackQuery.message.message_id;
|
|
||||||
const [country, city] = callbackQuery.data.replace('prod_city_', '').split('_');
|
|
||||||
|
|
||||||
try {
|
|
||||||
const districts = await LocationService.getDistrictsByCountryAndCity(country, city);
|
|
||||||
|
|
||||||
const keyboard = {
|
|
||||||
inline_keyboard: [
|
|
||||||
...districts.map(loc => [{
|
|
||||||
text: loc.district,
|
|
||||||
callback_data: `prod_district_${country}_${city}_${loc.district}`
|
|
||||||
}]),
|
|
||||||
[{text: '« Back', callback_data: `prod_country_${country}`}]
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
await bot.editMessageText(
|
|
||||||
`📍 Select district in ${city}:`,
|
|
||||||
{
|
|
||||||
chat_id: chatId,
|
|
||||||
message_id: messageId,
|
|
||||||
reply_markup: keyboard
|
|
||||||
}
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error({ err: error }, 'Error in handleCitySelection');
|
|
||||||
await bot.sendMessage(chatId, 'Error loading districts. Please try again.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static async handleDistrictSelection(callbackQuery) {
|
|
||||||
if (!isAdmin(callbackQuery.from.id)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const chatId = callbackQuery.message.chat.id;
|
|
||||||
const messageId = callbackQuery.message.message_id;
|
|
||||||
const [country, city, district] = callbackQuery.data.replace('prod_district_', '').split('_');
|
|
||||||
|
|
||||||
await userStates.delete(chatId);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const location = await LocationService.getLocation(country, city, district);
|
|
||||||
|
|
||||||
if (!location) {
|
|
||||||
throw new Error('Location not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
const categories = await CategoryService.getCategoriesByLocationId(location.id);
|
|
||||||
|
|
||||||
const keyboard = {
|
|
||||||
inline_keyboard: [
|
|
||||||
...categories.map(cat => [{
|
|
||||||
text: cat.name,
|
|
||||||
callback_data: `prod_category_${location.id}_${cat.id}`
|
|
||||||
}]),
|
|
||||||
[{text: '➕ Add Category', callback_data: `add_category_${location.id}`}],
|
|
||||||
[{text: '« Back', callback_data: `prod_city_${country}_${city}`}]
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
await bot.editMessageText(
|
|
||||||
'📦 Select or add category:',
|
|
||||||
{
|
|
||||||
chat_id: chatId,
|
|
||||||
message_id: messageId,
|
|
||||||
reply_markup: keyboard
|
|
||||||
}
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error({ err: error }, 'Error in handleDistrictSelection');
|
|
||||||
await bot.sendMessage(chatId, 'Error loading categories. Please try again.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
import db from '../../../config/database.js';
|
|
||||||
import { isAdmin } from '../../../middleware/auth.js';
|
|
||||||
import fs from 'fs/promises';
|
|
||||||
import bot from '../../../context/bot.js';
|
|
||||||
import userStates from '../../../context/userStates.js';
|
|
||||||
import { validateProductName, validateProductPrice } from './productValidator.js';
|
|
||||||
import logger from '../../../utils/logger.js';
|
|
||||||
|
|
||||||
export default class EditImportHandler {
|
|
||||||
static async handleProductEditImport(msg) {
|
|
||||||
const chatId = msg.chat.id;
|
|
||||||
const state = await userStates.get(chatId);
|
|
||||||
|
|
||||||
if (!state || state.action !== 'edit_product') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isAdmin(msg.from.id)) {
|
|
||||||
await bot.sendMessage(chatId, 'Unauthorized access.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
let product;
|
|
||||||
let jsonContent;
|
|
||||||
|
|
||||||
if (msg.document) {
|
|
||||||
if (!msg.document.file_name.endsWith('.json')) {
|
|
||||||
await bot.sendMessage(chatId, 'Please upload a .json file.');
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const file = await bot.getFile(msg.document.file_id);
|
|
||||||
const fileContent = await bot.downloadFile(file.file_id, '.');
|
|
||||||
jsonContent = await fs.readFile(fileContent, 'utf8');
|
|
||||||
await fs.rm(fileContent);
|
|
||||||
} else if (msg.text) {
|
|
||||||
jsonContent = msg.text;
|
|
||||||
} else {
|
|
||||||
await bot.sendMessage(chatId, 'Please send either a JSON file or JSON text.');
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
product = JSON.parse(jsonContent);
|
|
||||||
} catch (e) {
|
|
||||||
await bot.sendMessage(chatId, 'Invalid JSON format. Please check the format and try again.');
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
await db.runAsync('BEGIN TRANSACTION');
|
|
||||||
|
|
||||||
if (!validateProductName(product.name, chatId)) {
|
|
||||||
await db.runAsync('ROLLBACK');
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (!validateProductPrice(product.price, chatId)) {
|
|
||||||
await db.runAsync('ROLLBACK');
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
await db.runAsync(
|
|
||||||
`UPDATE products SET
|
|
||||||
location_id = ?, category_id = ?,
|
|
||||||
name = ?, price = ?, description = ?, private_data = ?,
|
|
||||||
quantity_in_stock = ?, photo_url = ?, hidden_photo_url = ?,
|
|
||||||
hidden_coordinates = ?, hidden_description = ?
|
|
||||||
WHERE id = ?`,
|
|
||||||
[
|
|
||||||
state.locationId, state.categoryId,
|
|
||||||
product.name, product.price, product.description, product.private_data,
|
|
||||||
product.quantity_in_stock, product.photo_url, product.hidden_photo_url,
|
|
||||||
product.hidden_coordinates, product.hidden_description, state.productId
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
await db.runAsync('COMMIT');
|
|
||||||
|
|
||||||
await bot.sendMessage(chatId, '✅ Successfully edited!', {
|
|
||||||
reply_markup: {
|
|
||||||
inline_keyboard: [[
|
|
||||||
{ text: '« Back to Products', callback_data: `prod_category_${state.locationId}_${state.categoryId}` }
|
|
||||||
]]
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
await userStates.delete(chatId);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error({ err: error }, 'Error importing products');
|
|
||||||
await bot.sendMessage(chatId, 'Error importing products. Please check the data and try again.');
|
|
||||||
await db.runAsync('ROLLBACK');
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
import { isAdmin } from '../../../middleware/auth.js';
|
|
||||||
import bot from '../../../context/bot.js';
|
|
||||||
import userStates from '../../../context/userStates.js';
|
|
||||||
import ProductService from '../../../services/productService.js';
|
|
||||||
import logger from '../../../utils/logger.js';
|
|
||||||
|
|
||||||
export default class EditStartHandler {
|
|
||||||
static async handleProductEdit(callbackQuery) {
|
|
||||||
if (!isAdmin(callbackQuery.from.id)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const chatId = callbackQuery.message.chat.id;
|
|
||||||
const messageId = callbackQuery.message.message_id;
|
|
||||||
const productId = callbackQuery.data.replace('edit_product_', '');
|
|
||||||
|
|
||||||
try {
|
|
||||||
const product = await ProductService.getDetailedProductById(productId);
|
|
||||||
|
|
||||||
if (!product) {
|
|
||||||
throw new Error('Product not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
const locationId = product.location_id;
|
|
||||||
const categoryId = product.category_id;
|
|
||||||
|
|
||||||
const sampleProduct = {
|
|
||||||
name: product.name,
|
|
||||||
price: product.price,
|
|
||||||
description: product.description,
|
|
||||||
private_data: product.private_data,
|
|
||||||
quantity_in_stock: product.quantity_in_stock,
|
|
||||||
photo_url: product.photo_url,
|
|
||||||
hidden_photo_url: product.hidden_photo_url,
|
|
||||||
hidden_coordinates: product.hidden_coordinates,
|
|
||||||
hidden_description: product.hidden_description
|
|
||||||
};
|
|
||||||
|
|
||||||
const jsonExample = JSON.stringify(sampleProduct, null, 2);
|
|
||||||
const message = `To edit product, send a JSON file with product data:\n\n<pre>${jsonExample}</pre>\n\nYou can either:\n1. Send the JSON as text\n2. Upload a .json file`;
|
|
||||||
|
|
||||||
await userStates.set(chatId, {
|
|
||||||
action: 'edit_product',
|
|
||||||
locationId,
|
|
||||||
categoryId,
|
|
||||||
productId
|
|
||||||
});
|
|
||||||
|
|
||||||
await bot.editMessageText(message, {
|
|
||||||
chat_id: chatId,
|
|
||||||
message_id: messageId,
|
|
||||||
parse_mode: 'HTML',
|
|
||||||
reply_markup: {
|
|
||||||
inline_keyboard: [[
|
|
||||||
{ text: '❌ Cancel', callback_data: `prod_category_${locationId}_${categoryId}` }
|
|
||||||
]]
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
logger.error({ err: error }, 'Error in handleProductEdit');
|
|
||||||
await bot.sendMessage(chatId, 'Error loading product details. Please try again.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
import db from '../../../config/database.js';
|
|
||||||
import { isAdmin } from '../../../middleware/auth.js';
|
|
||||||
import fs from 'fs/promises';
|
|
||||||
import bot from '../../../context/bot.js';
|
|
||||||
import userStates from '../../../context/userStates.js';
|
|
||||||
import ProductValidator from './productValidator.js';
|
|
||||||
import logger from '../../../utils/logger.js';
|
|
||||||
|
|
||||||
export default class ImportHandler {
|
|
||||||
|
|
||||||
static async handleProductImport(msg) {
|
|
||||||
const chatId = msg.chat.id;
|
|
||||||
const state = await userStates.get(chatId);
|
|
||||||
if (!state || state.action !== 'import_products') return false;
|
|
||||||
if (!isAdmin(msg.from.id)) {
|
|
||||||
await bot.sendMessage(chatId, 'Unauthorized access.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const jsonContent = await this._extractJsonContent(msg, chatId);
|
|
||||||
if (!jsonContent) return true;
|
|
||||||
let products;
|
|
||||||
try {
|
|
||||||
products = JSON.parse(jsonContent);
|
|
||||||
if (!Array.isArray(products)) throw new Error('Input must be an array of products');
|
|
||||||
} catch (e) {
|
|
||||||
await bot.sendMessage(chatId, 'Invalid JSON format. Please check the format and try again.');
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
await db.runAsync('BEGIN TRANSACTION');
|
|
||||||
for (const product of products) {
|
|
||||||
const error = ProductValidator.validateProduct(product);
|
|
||||||
if (error) {
|
|
||||||
await bot.sendMessage(chatId, error);
|
|
||||||
await db.runAsync('ROLLBACK');
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
await db.runAsync(
|
|
||||||
`INSERT INTO products (location_id, category_id, name, price, description, private_data, quantity_in_stock, photo_url, hidden_photo_url, hidden_coordinates, hidden_description) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
||||||
[state.locationId, state.categoryId, product.name, product.price, product.description, product.private_data, product.quantity_in_stock, product.photo_url, product.hidden_photo_url, product.hidden_coordinates, product.hidden_description]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
await db.runAsync('COMMIT');
|
|
||||||
await bot.sendMessage(chatId, `✅ Successfully imported ${products.length} products!`, {
|
|
||||||
reply_markup: {
|
|
||||||
inline_keyboard: [[{ text: '« Back to Products', callback_data: `prod_category_${state.locationId}_${state.categoryId}` }]]
|
|
||||||
}
|
|
||||||
});
|
|
||||||
await userStates.delete(chatId);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error({ err: error }, 'Error importing products');
|
|
||||||
await bot.sendMessage(chatId, 'Error importing products. Please check the data and try again.');
|
|
||||||
await db.runAsync('ROLLBACK');
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
static async _extractJsonContent(msg, chatId) {
|
|
||||||
if (msg.document) {
|
|
||||||
if (!msg.document.file_name.endsWith('.json')) {
|
|
||||||
await bot.sendMessage(chatId, 'Please upload a .json file.');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const file = await bot.getFile(msg.document.file_id);
|
|
||||||
const fileContent = await bot.downloadFile(file.file_id, '.');
|
|
||||||
const jsonContent = await fs.readFile(fileContent, 'utf8');
|
|
||||||
await fs.rm(fileContent);
|
|
||||||
return jsonContent;
|
|
||||||
}
|
|
||||||
if (msg.text) {
|
|
||||||
return msg.text;
|
|
||||||
}
|
|
||||||
await bot.sendMessage(chatId, 'Please send either a JSON file or JSON text.');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
import CreateHandler from './createHandler.js';
|
|
||||||
import ImportHandler from './importHandler.js';
|
|
||||||
import EditStartHandler from './editStartHandler.js';
|
|
||||||
import EditImportHandler from './editImportHandler.js';
|
|
||||||
import DeleteHandler from './deleteHandler.js';
|
|
||||||
import NavigationHandler from './navigationHandler.js';
|
|
||||||
import DistrictHandler from './districtHandler.js';
|
|
||||||
import CategoryAddHandler from './categoryAddHandler.js';
|
|
||||||
import CategoryEditHandler from './categoryEditHandler.js';
|
|
||||||
import CategorySelectionHandler from './categorySelectionHandler.js';
|
|
||||||
import ViewHandler from './viewHandler.js';
|
|
||||||
import ListHandler from './listHandler.js';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
handleProductManagement: NavigationHandler.handleProductManagement,
|
|
||||||
handleCountrySelection: NavigationHandler.handleCountrySelection,
|
|
||||||
handleCitySelection: DistrictHandler.handleCitySelection,
|
|
||||||
handleDistrictSelection: DistrictHandler.handleDistrictSelection,
|
|
||||||
handleCategoryInput: CategoryAddHandler.handleCategoryInput,
|
|
||||||
handleAddCategory: CategoryAddHandler.handleAddCategory,
|
|
||||||
handleCategoryUpdate: CategoryEditHandler.handleCategoryUpdate,
|
|
||||||
handleEditCategory: CategoryEditHandler.handleEditCategory,
|
|
||||||
handleCategorySelection: CategorySelectionHandler.handleCategorySelection,
|
|
||||||
handleAddProduct: CreateHandler.handleAddProduct,
|
|
||||||
handleProductImport: ImportHandler.handleProductImport,
|
|
||||||
handleProductEdit: EditStartHandler.handleProductEdit,
|
|
||||||
handleProductEditImport: EditImportHandler.handleProductEditImport,
|
|
||||||
handleViewProduct: ViewHandler.handleViewProduct,
|
|
||||||
handleProductListPage: ListHandler.handleProductListPage,
|
|
||||||
handleProductDelete: DeleteHandler.handleProductDelete,
|
|
||||||
handleConfirmDelete: DeleteHandler.handleConfirmDelete,
|
|
||||||
};
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
import db from '../../../config/database.js';
|
|
||||||
import { isAdmin } from '../../../middleware/auth.js';
|
|
||||||
import bot from '../../../context/bot.js';
|
|
||||||
import logger from '../../../utils/logger.js';
|
|
||||||
|
|
||||||
export default class ListHandler {
|
|
||||||
|
|
||||||
static async viewProductsPage(locationId, categoryId, page) {
|
|
||||||
try {
|
|
||||||
const limit = 10;
|
|
||||||
const offset = (page || 0) * limit;
|
|
||||||
const previousPage = page > 0 ? page - 1 : 0;
|
|
||||||
const nextPage = page + 1;
|
|
||||||
|
|
||||||
const products = await db.allAsync(
|
|
||||||
`SELECT id, name, price, quantity_in_stock
|
|
||||||
FROM products
|
|
||||||
WHERE location_id = ? AND category_id = ?
|
|
||||||
ORDER BY name
|
|
||||||
LIMIT ? OFFSET ?`,
|
|
||||||
[locationId, categoryId, limit, offset]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (products.length === 0 && page === 0) {
|
|
||||||
return {
|
|
||||||
text: 'No products for this location',
|
|
||||||
markup: {
|
|
||||||
inline_keyboard: [
|
|
||||||
[{ text: '📥 Import Products', callback_data: `add_product_${locationId}_${categoryId}` }],
|
|
||||||
[{ text: '« Back', callback_data: `prod_category_${locationId}_${categoryId}` }]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (products.length === 0 && page > 0) {
|
|
||||||
return await this.viewProductsPage(locationId, categoryId, previousPage);
|
|
||||||
}
|
|
||||||
|
|
||||||
const category = await db.getAsync('SELECT name FROM categories WHERE id = ?', [categoryId]);
|
|
||||||
|
|
||||||
const keyboard = {
|
|
||||||
inline_keyboard: [
|
|
||||||
...products.map(prod => [{
|
|
||||||
text: `${prod.name} - $${prod.price} (${prod.quantity_in_stock} left)`,
|
|
||||||
callback_data: `view_product_${prod.id}`
|
|
||||||
}]),
|
|
||||||
[{ text: '📥 Import Products', callback_data: `add_product_${locationId}_${categoryId}` }]
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
keyboard.inline_keyboard.push([
|
|
||||||
{ text: '«', callback_data: `list_products_${locationId}_${categoryId}_${previousPage}` },
|
|
||||||
{ text: '»', callback_data: `list_products_${locationId}_${categoryId}_${nextPage}` }
|
|
||||||
]);
|
|
||||||
|
|
||||||
keyboard.inline_keyboard.push([
|
|
||||||
{ text: '« Back', callback_data: `prod_category_${locationId}_${categoryId}` }
|
|
||||||
]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
text: `📦 ${category.name}\nSelect product or import new ones:`,
|
|
||||||
markup: keyboard
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
logger.error({ err: error }, 'Error in viewProductsPage');
|
|
||||||
return { text: 'Error loading products. Please try again.' };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static async handleProductListPage(callbackQuery) {
|
|
||||||
if (!isAdmin(callbackQuery.from.id)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const chatId = callbackQuery.message.chat.id;
|
|
||||||
const [locationId, categoryId, page] = callbackQuery.data.replace('list_products_', '').split('_');
|
|
||||||
|
|
||||||
try {
|
|
||||||
const { text, markup } = await this.viewProductsPage(locationId, categoryId, parseInt(page));
|
|
||||||
await bot.editMessageText(text, {
|
|
||||||
chat_id: chatId,
|
|
||||||
message_id: callbackQuery.message.message_id,
|
|
||||||
reply_markup: markup,
|
|
||||||
parse_mode: 'HTML'
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
import { isAdmin } from '../../../middleware/auth.js';
|
|
||||||
import LocationService from '../../../services/locationService.js';
|
|
||||||
import bot from '../../../context/bot.js';
|
|
||||||
import logger from '../../../utils/logger.js';
|
|
||||||
|
|
||||||
export default class NavigationHandler {
|
|
||||||
|
|
||||||
static async handleProductManagement(msg) {
|
|
||||||
const chatId = msg.chat?.id || msg.message?.chat.id;
|
|
||||||
|
|
||||||
if (!isAdmin(msg.from?.id || msg.message?.from.id)) {
|
|
||||||
await bot.sendMessage(chatId, 'Unauthorized access.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const countries = await LocationService.getCountries()
|
|
||||||
|
|
||||||
if (countries.length === 0) {
|
|
||||||
await bot.sendMessage(
|
|
||||||
chatId,
|
|
||||||
'No locations available. Please add locations first.',
|
|
||||||
{
|
|
||||||
reply_markup: {
|
|
||||||
inline_keyboard: [[
|
|
||||||
{text: '📍 Manage Locations', callback_data: 'view_locations'}
|
|
||||||
]]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const keyboard = {
|
|
||||||
inline_keyboard: countries.map(loc => [{
|
|
||||||
text: loc.country,
|
|
||||||
callback_data: `prod_country_${loc.country}`
|
|
||||||
}])
|
|
||||||
};
|
|
||||||
|
|
||||||
await bot.sendMessage(
|
|
||||||
chatId,
|
|
||||||
'🌍 Select country to manage products:',
|
|
||||||
{reply_markup: keyboard}
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error({ err: error }, 'Error in handleProductManagement');
|
|
||||||
await bot.sendMessage(chatId, 'Error loading locations. Please try again.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static async handleCountrySelection(callbackQuery) {
|
|
||||||
if (!isAdmin(callbackQuery.from.id)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const chatId = callbackQuery.message.chat.id;
|
|
||||||
const messageId = callbackQuery.message.message_id;
|
|
||||||
const country = callbackQuery.data.replace('prod_country_', '');
|
|
||||||
|
|
||||||
try {
|
|
||||||
const cities = await LocationService.getCitiesByCountry(country)
|
|
||||||
|
|
||||||
const keyboard = {
|
|
||||||
inline_keyboard: [
|
|
||||||
...cities.map(loc => [{
|
|
||||||
text: loc.city,
|
|
||||||
callback_data: `prod_city_${country}_${loc.city}`
|
|
||||||
}]),
|
|
||||||
[{text: '« Back', callback_data: 'manage_products'}]
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
await bot.editMessageText(
|
|
||||||
`🏙 Select city in ${country}:`,
|
|
||||||
{
|
|
||||||
chat_id: chatId,
|
|
||||||
message_id: messageId,
|
|
||||||
reply_markup: keyboard
|
|
||||||
}
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error({ err: error }, 'Error in handleCountrySelection');
|
|
||||||
await bot.sendMessage(chatId, 'Error loading cities. Please try again.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
import Validators from '../../../utils/validators.js';
|
|
||||||
import logger from '../../../utils/logger.js';
|
|
||||||
|
|
||||||
export function validateProductName(name, chatId) {
|
|
||||||
if (!Validators.isValidString(name, 255)) {
|
|
||||||
logger.warn({ chatId, name }, 'Invalid product name');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function validateProductPrice(price, chatId) {
|
|
||||||
if (!Validators.isValidPrice(price)) {
|
|
||||||
logger.warn({ chatId, price }, 'Invalid product price');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class ProductValidator {
|
|
||||||
static validateProduct(product) {
|
|
||||||
if (!Validators.isValidString(product.name, 255)) {
|
|
||||||
return `Ошибка: недопустимое название товара "${product.name}"`;
|
|
||||||
}
|
|
||||||
if (!Validators.isValidPrice(product.price)) {
|
|
||||||
return `Ошибка: недопустимая цена "${product.price}"`;
|
|
||||||
}
|
|
||||||
if (!Number.isFinite(product.quantity_in_stock) || product.quantity_in_stock < 0) {
|
|
||||||
return `Ошибка: недопустимое количество "${product.quantity_in_stock}"`;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
import { isAdmin } from '../../../middleware/auth.js';
|
|
||||||
import bot from '../../../context/bot.js';
|
|
||||||
import userStates from '../../../context/userStates.js';
|
|
||||||
import LocationService from '../../../services/locationService.js';
|
|
||||||
import ProductService from '../../../services/productService.js';
|
|
||||||
import logger from '../../../utils/logger.js';
|
|
||||||
|
|
||||||
export default class ViewHandler {
|
|
||||||
|
|
||||||
static async handleViewProduct(callbackQuery) {
|
|
||||||
if (!isAdmin(callbackQuery.from.id)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const chatId = callbackQuery.message.chat.id;
|
|
||||||
const messageId = callbackQuery.message.message_id;
|
|
||||||
const productId = callbackQuery.data.replace('view_product_', '');
|
|
||||||
|
|
||||||
try {
|
|
||||||
const product = await ProductService.getDetailedProductById(productId);
|
|
||||||
|
|
||||||
if (!product) {
|
|
||||||
throw new Error('Product not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
const location = await LocationService.getLocationById(product.location_id);
|
|
||||||
|
|
||||||
if (!location) {
|
|
||||||
throw new Error('Location not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
const message = `
|
|
||||||
📦 Product Details:
|
|
||||||
|
|
||||||
Name: ${product.name}
|
|
||||||
Price: $${product.price}
|
|
||||||
Description: ${product.description}
|
|
||||||
Stock: ${product.quantity_in_stock}
|
|
||||||
Location: ${location.country}, ${location.city}, ${location.district}
|
|
||||||
Category: ${product.category_name}
|
|
||||||
|
|
||||||
🔒 Private Information:
|
|
||||||
${product.private_data}
|
|
||||||
Hidden Location: ${product.hidden_description}
|
|
||||||
Coordinates: ${product.hidden_coordinates}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const keyboard = {
|
|
||||||
inline_keyboard: [
|
|
||||||
[
|
|
||||||
{text: '✏️ Edit', callback_data: `edit_product_${productId}`},
|
|
||||||
{text: '❌ Delete', callback_data: `delete_product_${productId}`}
|
|
||||||
],
|
|
||||||
[{
|
|
||||||
text: '« Back',
|
|
||||||
callback_data: `prod_category_${product.location_id}_${product.category_id}`
|
|
||||||
}]
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
let photoMessage;
|
|
||||||
let hiddenPhotoMessage;
|
|
||||||
|
|
||||||
if (product.photo_url) {
|
|
||||||
try {
|
|
||||||
photoMessage = await bot.sendPhoto(chatId, product.photo_url, {caption: 'Public photo'});
|
|
||||||
} catch (e) {
|
|
||||||
photoMessage = await bot.sendPhoto(chatId, "./corrupt-photo.jpg", {caption: 'Public photo'})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (product.hidden_photo_url) {
|
|
||||||
try {
|
|
||||||
hiddenPhotoMessage = await bot.sendPhoto(chatId, product.hidden_photo_url, {caption: 'Hidden photo'});
|
|
||||||
} catch (e) {
|
|
||||||
hiddenPhotoMessage = await bot.sendPhoto(chatId, "./corrupt-photo.jpg", {caption: 'Hidden photo'})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await userStates.set(chatId, {
|
|
||||||
msgToDelete: [photoMessage.message_id, hiddenPhotoMessage.message_id]
|
|
||||||
})
|
|
||||||
|
|
||||||
await bot.deleteMessage(chatId, messageId);
|
|
||||||
await bot.sendMessage(chatId, message, {reply_markup: keyboard});
|
|
||||||
} catch (error) {
|
|
||||||
logger.error({ err: error }, 'Error in handleViewProduct');
|
|
||||||
await bot.sendMessage(chatId, 'Error loading product details. Please try again.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
import config from '../../config/config.js';
|
|
||||||
import db from '../../config/database.js';
|
|
||||||
import bot from "../../context/bot.js";
|
|
||||||
import UserService from "../../services/userService.js";
|
|
||||||
import userStates from "../../context/userStates.js";
|
|
||||||
import logger from '../../utils/logger.js';
|
|
||||||
|
|
||||||
export default class UserDeletionHandler {
|
|
||||||
static async handleDeleteAccount(callbackQuery) {
|
|
||||||
const telegramId = callbackQuery.from.id;
|
|
||||||
const chatId = callbackQuery.message.chat.id;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const keyboard = {
|
|
||||||
inline_keyboard: [
|
|
||||||
[
|
|
||||||
{text: '✅ Confirm Delete', callback_data: `confirm_delete_account`},
|
|
||||||
{text: '❌ Cancel', callback_data: `back_to_profile`}
|
|
||||||
]
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
await bot.editMessageText(
|
|
||||||
`⚠️ Are you sure you want to delete your account?\n\nThis action will:\n- Delete all user data\n- Remove all wallets\n- Erase purchase history\n\nThis action cannot be undone!`,
|
|
||||||
{
|
|
||||||
chat_id: chatId,
|
|
||||||
message_id: callbackQuery.message.message_id,
|
|
||||||
reply_markup: keyboard,
|
|
||||||
parse_mode: 'HTML'
|
|
||||||
}
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error({ err: error }, 'Error in handleDeleteUser');
|
|
||||||
await bot.sendMessage(chatId, 'Error processing delete request. Please try again.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static async handleConfirmDelete(callbackQuery) {
|
|
||||||
const telegramId = callbackQuery.from.id;
|
|
||||||
const chatId = callbackQuery.message.chat.id;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await UserService.updateUserStatus(telegramId, 1);
|
|
||||||
|
|
||||||
await bot.editMessageText(
|
|
||||||
'⚠️Your account has been successful deleted',
|
|
||||||
{ chat_id: chatId, message_id: callbackQuery.message.message_id, }
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error({ err: error }, 'Error in handleConfirmDelete');
|
|
||||||
await bot.sendMessage(chatId, 'Error deleting user. Please try again.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +1,6 @@
|
|||||||
// userHandler.js
|
|
||||||
|
|
||||||
import config from "../../config/config.js";
|
import config from "../../config/config.js";
|
||||||
import bot from "../../context/bot.js";
|
import bot from "../../context/bot.js";
|
||||||
import UserService from "../../services/userService.js";
|
import UserService from "../../services/userService.js";
|
||||||
import WalletService from "../../services/walletService.js";
|
|
||||||
import logger from "../../utils/logger.js";
|
|
||||||
|
|
||||||
export default class UserHandler {
|
export default class UserHandler {
|
||||||
static async canUseBot(msg) {
|
static async canUseBot(msg) {
|
||||||
@@ -44,33 +40,25 @@ export default class UserHandler {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Получаем балансы активных и архивных кошельков
|
|
||||||
const activeWalletsBalance = await WalletService.getActiveWalletsBalance(userStats.id);
|
|
||||||
const archivedWalletsBalance = await WalletService.getArchivedWalletsBalance(userStats.id);
|
|
||||||
|
|
||||||
// Доступный баланс (bonus_balance + total_balance)
|
|
||||||
const availableBalance = userStats.bonus_balance + (userStats.total_balance || 0);
|
|
||||||
|
|
||||||
const locationText = userStats.country && userStats.city && userStats.district
|
const locationText = userStats.country && userStats.city && userStats.district
|
||||||
? `${userStats.country}, ${userStats.city}, ${userStats.district}`
|
? `${userStats.country}, ${userStats.city}, ${userStats.district}`
|
||||||
: 'Not set';
|
: 'Not set';
|
||||||
|
|
||||||
const text = `
|
const text = `
|
||||||
👤 *Your Profile*
|
👤 *Your Profile*
|
||||||
|
|
||||||
📱 Telegram ID: \`${telegramId}\`
|
📱 Telegram ID: \`${telegramId}\`
|
||||||
📍 Location: ${locationText}
|
📍 Location: ${locationText}
|
||||||
|
|
||||||
📊 Statistics:
|
📊 Statistics:
|
||||||
├ Total Purchases: ${userStats.purchase_count || 0}
|
├ Total Purchases: ${userStats.purchase_count || 0}
|
||||||
├ Total Spent: $${userStats.total_spent || 0}
|
├ Total Spent: $${userStats.total_spent || 0}
|
||||||
├ Active Wallets: ${userStats.crypto_wallet_count || 0} ($${activeWalletsBalance.toFixed(2)})
|
├ Active Wallets: ${userStats.crypto_wallet_count || 0}
|
||||||
├ Archived Wallets: ${userStats.archived_wallet_count || 0} ($${archivedWalletsBalance.toFixed(2)})
|
├ Bonus Balance: $${userStats.bonus_balance || 0}
|
||||||
├ Bonus Balance: $${userStats.bonus_balance || 0}
|
└ Total Balance: $${(userStats.total_balance || 0) + (userStats.bonus_balance || 0)}
|
||||||
└ Available Balance: $${availableBalance.toFixed(2)}
|
|
||||||
|
|
||||||
📅 Member since: ${new Date(userStats.created_at).toLocaleDateString()}
|
📅 Member since: ${new Date(userStats.created_at).toLocaleDateString()}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const keyboard = {
|
const keyboard = {
|
||||||
inline_keyboard: [
|
inline_keyboard: [
|
||||||
@@ -84,7 +72,7 @@ export default class UserHandler {
|
|||||||
reply_markup: keyboard
|
reply_markup: keyboard
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error({ err: error }, 'Error in showProfile');
|
console.error('Error in showProfile:', error);
|
||||||
await bot.sendMessage(chatId, 'Error loading profile. Please try again.');
|
await bot.sendMessage(chatId, 'Error loading profile. Please try again.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -117,7 +105,7 @@ export default class UserHandler {
|
|||||||
keyboard
|
keyboard
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error({ err: error }, 'Error in handleStart');
|
console.error('Error in handleStart:', error);
|
||||||
await bot.sendMessage(chatId, 'Error creating user profile. Please try again.');
|
await bot.sendMessage(chatId, 'Error creating user profile. Please try again.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import db from '../../config/database.js';
|
|||||||
import LocationService from "../../services/locationService.js";
|
import LocationService from "../../services/locationService.js";
|
||||||
import bot from "../../context/bot.js";
|
import bot from "../../context/bot.js";
|
||||||
import UserService from "../../services/userService.js";
|
import UserService from "../../services/userService.js";
|
||||||
import logger from '../../utils/logger.js';
|
|
||||||
|
|
||||||
export default class UserLocationHandler {
|
export default class UserLocationHandler {
|
||||||
static async handleSetLocation(callbackQuery) {
|
static async handleSetLocation(callbackQuery) {
|
||||||
@@ -47,7 +46,7 @@ export default class UserLocationHandler {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error({ err: error }, 'Error in handleSetLocation');
|
console.error('Error in handleSetLocation:', error);
|
||||||
await bot.sendMessage(chatId, 'Error loading countries. Please try again.');
|
await bot.sendMessage(chatId, 'Error loading countries. Please try again.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -79,7 +78,7 @@ export default class UserLocationHandler {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error({ err: error }, 'Error in handleSetCountry');
|
console.error('Error in handleSetCountry:', error);
|
||||||
await bot.sendMessage(chatId, 'Error loading cities. Please try again.');
|
await bot.sendMessage(chatId, 'Error loading cities. Please try again.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -111,7 +110,7 @@ export default class UserLocationHandler {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error({ err: error }, 'Error in handleSetCity');
|
console.error('Error in handleSetCity:', error);
|
||||||
await bot.sendMessage(chatId, 'Error loading districts. Please try again.');
|
await bot.sendMessage(chatId, 'Error loading districts. Please try again.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -141,7 +140,7 @@ export default class UserLocationHandler {
|
|||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await db.runAsync('ROLLBACK');
|
await db.runAsync('ROLLBACK');
|
||||||
logger.error({ err: error }, 'Error in handleSetDistrict');
|
console.error('Error in handleSetDistrict:', error);
|
||||||
await bot.sendMessage(chatId, 'Error updating location. Please try again.');
|
await bot.sendMessage(chatId, 'Error updating location. Please try again.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,8 +7,6 @@ import ProductService from "../../services/productService.js";
|
|||||||
import CategoryService from "../../services/categoryService.js";
|
import CategoryService from "../../services/categoryService.js";
|
||||||
import UserService from "../../services/userService.js";
|
import UserService from "../../services/userService.js";
|
||||||
import PurchaseService from "../../services/purchaseService.js";
|
import PurchaseService from "../../services/purchaseService.js";
|
||||||
import Validators from '../../utils/validators.js';
|
|
||||||
import logger from '../../utils/logger.js';
|
|
||||||
|
|
||||||
export default class UserProductHandler {
|
export default class UserProductHandler {
|
||||||
static async showProducts(msg) {
|
static async showProducts(msg) {
|
||||||
@@ -52,7 +50,7 @@ export default class UserProductHandler {
|
|||||||
await bot.sendMessage(chatId, message, {reply_markup: keyboard});
|
await bot.sendMessage(chatId, message, {reply_markup: keyboard});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error({ err: error }, 'Error in showProducts');
|
console.error('Error in showProducts:', error);
|
||||||
await bot.sendMessage(chatId, 'Error loading products. Please try again.');
|
await bot.sendMessage(chatId, 'Error loading products. Please try again.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -84,7 +82,7 @@ export default class UserProductHandler {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error({ err: error }, 'Error in handleCountrySelection');
|
console.error('Error in handleCountrySelection:', error);
|
||||||
await bot.sendMessage(chatId, 'Error loading cities. Please try again.');
|
await bot.sendMessage(chatId, 'Error loading cities. Please try again.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -116,7 +114,7 @@ export default class UserProductHandler {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error({ err: error }, 'Error in handleCitySelection');
|
console.error('Error in handleCitySelection:', error);
|
||||||
await bot.sendMessage(chatId, 'Error loading districts. Please try again.');
|
await bot.sendMessage(chatId, 'Error loading districts. Please try again.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -127,19 +125,23 @@ export default class UserProductHandler {
|
|||||||
const [country, city, district] = callbackQuery.data.replace('shop_district_', '').split('_');
|
const [country, city, district] = callbackQuery.data.replace('shop_district_', '').split('_');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Получаем информацию о локации
|
|
||||||
const location = await LocationService.getLocation(country, city, district);
|
const location = await LocationService.getLocation(country, city, district);
|
||||||
|
|
||||||
if (!location) {
|
if (!location) {
|
||||||
// Если локация не найдена, вернуть пользователя к предыдущему шагу
|
throw new Error('Location not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const categories = await CategoryService.getCategoriesByLocationId(location.id);
|
||||||
|
|
||||||
|
if (categories.length === 0) {
|
||||||
await bot.editMessageText(
|
await bot.editMessageText(
|
||||||
'Location not found. Returning to previous menu.',
|
'No products available in this location yet.',
|
||||||
{
|
{
|
||||||
chat_id: chatId,
|
chat_id: chatId,
|
||||||
message_id: messageId,
|
message_id: messageId,
|
||||||
reply_markup: {
|
reply_markup: {
|
||||||
inline_keyboard: [[
|
inline_keyboard: [[
|
||||||
{ text: '« Back', callback_data: `shop_city_${country}_${city}` }
|
{text: '« Back to Districts', callback_data: `shop_city_${country}_${city}`}
|
||||||
]]
|
]]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -147,21 +149,13 @@ export default class UserProductHandler {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Сохраняем текстовое представление локации в состоянии пользователя
|
|
||||||
await userStates.set(chatId, {
|
|
||||||
location: `${country}_${city}_${district}`
|
|
||||||
});
|
|
||||||
|
|
||||||
// Получаем категории для выбранной локации
|
|
||||||
const categories = await CategoryService.getCategoriesByLocationId(location.id);
|
|
||||||
|
|
||||||
const keyboard = {
|
const keyboard = {
|
||||||
inline_keyboard: [
|
inline_keyboard: [
|
||||||
...categories.map(cat => [{
|
...categories.map(cat => [{
|
||||||
text: cat.name,
|
text: cat.name,
|
||||||
callback_data: `shop_category_${location.id}_${cat.id}`
|
callback_data: `shop_category_${location.id}_${cat.id}`
|
||||||
}]),
|
}]),
|
||||||
[{ text: '« Back', callback_data: `shop_city_${country}_${city}` }]
|
[{text: '« Back to Districts', callback_data: `shop_city_${country}_${city}`}]
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -174,7 +168,7 @@ export default class UserProductHandler {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error({ err: error }, 'Error in handleDistrictSelection');
|
console.error('Error in handleDistrictSelection:', error);
|
||||||
await bot.sendMessage(chatId, 'Error loading categories. Please try again.');
|
await bot.sendMessage(chatId, 'Error loading categories. Please try again.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -185,32 +179,21 @@ export default class UserProductHandler {
|
|||||||
const [locationId, categoryId] = callbackQuery.data.replace('shop_category_', '').split('_');
|
const [locationId, categoryId] = callbackQuery.data.replace('shop_category_', '').split('_');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Удаляем текущее сообщение
|
const subcategories = await CategoryService.getSubcategoriesByCategoryId(categoryId);
|
||||||
await bot.deleteMessage(chatId, messageId);
|
const location = await LocationService.getLocationById(locationId);
|
||||||
|
|
||||||
// Получаем состояние пользователя
|
if (subcategories.length === 0) {
|
||||||
const state = await userStates.get(chatId);
|
await bot.editMessageText(
|
||||||
|
'No products available in this category yet.',
|
||||||
// Удаляем сообщение с фотографией, если оно существует
|
|
||||||
if (state && state.photoMessageId) {
|
|
||||||
try {
|
|
||||||
await bot.deleteMessage(chatId, state.photoMessageId);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error({ err: error }, 'Error deleting photo message');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Получаем товары для выбранной категории
|
|
||||||
const products = await ProductService.getProductsByCategoryId(categoryId);
|
|
||||||
|
|
||||||
if (products.length === 0) {
|
|
||||||
await bot.sendMessage(
|
|
||||||
chatId,
|
|
||||||
'No products available in this category.',
|
|
||||||
{
|
{
|
||||||
|
chat_id: chatId,
|
||||||
|
message_id: messageId,
|
||||||
reply_markup: {
|
reply_markup: {
|
||||||
inline_keyboard: [[
|
inline_keyboard: [[
|
||||||
{ text: '« Back', callback_data: `shop_district_${state.location}` }
|
{
|
||||||
|
text: '« Back to Categories',
|
||||||
|
callback_data: `shop_district_${location.country}_${location.city}_${location.district}`
|
||||||
|
}
|
||||||
]]
|
]]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -218,40 +201,30 @@ export default class UserProductHandler {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Создаем клавиатуру с товарами
|
|
||||||
const keyboard = {
|
const keyboard = {
|
||||||
inline_keyboard: products.map(product => [
|
inline_keyboard: [
|
||||||
{
|
...subcategories.map(sub => [{
|
||||||
text: `${product.name} - $${product.price}`,
|
text: sub.name,
|
||||||
callback_data: `shop_product_${product.id}`
|
callback_data: `shop_subcategory_${locationId}_${categoryId}_${sub.id}`
|
||||||
}
|
}]),
|
||||||
])
|
[{
|
||||||
|
text: '« Back to Categories',
|
||||||
|
callback_data: `shop_district_${location.country}_${location.city}_${location.district}`
|
||||||
|
}]
|
||||||
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
// Добавляем кнопку "Назад"
|
await bot.editMessageText(
|
||||||
keyboard.inline_keyboard.push([
|
'📦 Select subcategory:',
|
||||||
{ text: '« Back', callback_data: `shop_district_${state.location}` }
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Отправляем сообщение с товарами
|
|
||||||
await bot.sendMessage(
|
|
||||||
chatId,
|
|
||||||
'Select a product:',
|
|
||||||
{
|
{
|
||||||
|
chat_id: chatId,
|
||||||
|
message_id: messageId,
|
||||||
reply_markup: keyboard
|
reply_markup: keyboard
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// Сохраняем состояние пользователя
|
|
||||||
await userStates.set(chatId, {
|
|
||||||
...state,
|
|
||||||
action: 'viewing_category',
|
|
||||||
categoryId,
|
|
||||||
location: state?.location // Сохраняем информацию о локации
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error({ err: error }, 'Error in handleCategorySelection');
|
console.error('Error in handleCategorySelection:', error);
|
||||||
await bot.sendMessage(chatId, 'Error loading products. Please try again.');
|
await bot.sendMessage(chatId, 'Error loading subcategories. Please try again.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -266,7 +239,7 @@ export default class UserProductHandler {
|
|||||||
try {
|
try {
|
||||||
await bot.deleteMessage(chatId, photoMessageId);
|
await bot.deleteMessage(chatId, photoMessageId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error({ err: error }, 'Error deleting photo message');
|
console.error('Error deleting photo message:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -311,7 +284,7 @@ export default class UserProductHandler {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error({ err: error }, 'Error in handleSubcategorySelection');
|
console.error('Error in handleSubcategorySelection:', error);
|
||||||
await bot.sendMessage(chatId, 'Error loading products. Please try again.');
|
await bot.sendMessage(chatId, 'Error loading products. Please try again.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -328,78 +301,70 @@ export default class UserProductHandler {
|
|||||||
throw new Error('Product not found');
|
throw new Error('Product not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Удаляем предыдущее сообщение
|
// Delete the previous message
|
||||||
await bot.deleteMessage(chatId, messageId);
|
await bot.deleteMessage(chatId, messageId);
|
||||||
|
|
||||||
// Получаем состояние пользователя
|
|
||||||
const state = await userStates.get(chatId);
|
|
||||||
|
|
||||||
// Удаляем сообщение с фотографией, если оно существует
|
|
||||||
if (state?.photoMessageId) {
|
|
||||||
try {
|
|
||||||
await bot.deleteMessage(chatId, state.photoMessageId);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error({ err: error }, 'Error deleting photo message');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const message = `
|
const message = `
|
||||||
📦 ${product.name}
|
📦 ${product.name}
|
||||||
|
|
||||||
💰 Price: $${product.price}
|
💰 Price: $${product.price}
|
||||||
📝 Description: ${product.description}
|
📝 Description: ${product.description}
|
||||||
📦 Available: ${product.quantity_in_stock} pcs
|
📦 Available: ${product.quantity_in_stock} pcs
|
||||||
|
|
||||||
Category: ${product.category_name}
|
Category: ${product.category_name}
|
||||||
`;
|
Subcategory: ${product.subcategory_name}
|
||||||
|
`;
|
||||||
|
|
||||||
// Отправляем фото, если оно существует
|
let photoMessageId = null;
|
||||||
|
|
||||||
|
// First send the photo if it exists
|
||||||
let photoMessage;
|
let photoMessage;
|
||||||
if (product.photo_url) {
|
if (product.photo_url) {
|
||||||
try {
|
try {
|
||||||
photoMessage = await bot.sendPhoto(chatId, product.photo_url, { caption: 'Public photo' });
|
photoMessage = await bot.sendPhoto(chatId, product.photo_url, {caption: 'Public photo'});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
photoMessage = await bot.sendPhoto(chatId, "./corrupt-photo.jpg", { caption: 'Public photo' });
|
photoMessage = await bot.sendPhoto(chatId, "./corrupt-photo.jpg", {caption: 'Public photo'})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const keyboard = {
|
const keyboard = {
|
||||||
inline_keyboard: [
|
inline_keyboard: [
|
||||||
[{ text: '🛒 Buy Now', callback_data: `buy_product_${productId}` }],
|
[{text: '🛒 Buy Now', callback_data: `buy_product_${productId}`}],
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
text: '➖',
|
text: '➖',
|
||||||
callback_data: `decrease_quantity_${productId}`,
|
callback_data: `decrease_quantity_${productId}`,
|
||||||
callback_game: {} // Изначально отключено, так как количество начинается с 1
|
callback_game: {} // Initially disabled as quantity starts at 1
|
||||||
},
|
},
|
||||||
{ text: '1', callback_data: 'current_quantity' },
|
{text: '1', callback_data: 'current_quantity'},
|
||||||
{
|
{
|
||||||
text: '➕',
|
text: '➕',
|
||||||
callback_data: `increase_quantity_${productId}`,
|
callback_data: `increase_quantity_${productId}`,
|
||||||
callback_game: product.quantity_in_stock <= 1 ? {} : null // Отключено, если остаток 1 или меньше
|
callback_game: product.quantity_in_stock <= 1 ? {} : null // Disabled if stock is 1 or less
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
[{ text: `« Back ${product.category_name}`, callback_data: `shop_category_${product.location_id}_${product.category_id}` }] // Возврат к категории
|
[{
|
||||||
|
text: `« Back to ${product.subcategory_name}`,
|
||||||
|
callback_data: `shop_subcategory_${product.location_id}_${product.category_id}_${product.subcategory_id}_${photoMessageId}`
|
||||||
|
}]
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
// Отправляем сообщение с кнопками
|
// Then send the message with controls
|
||||||
const productMessage = await bot.sendMessage(chatId, message, {
|
await bot.sendMessage(chatId, message, {
|
||||||
reply_markup: keyboard,
|
reply_markup: keyboard,
|
||||||
parse_mode: 'HTML'
|
parse_mode: 'HTML'
|
||||||
});
|
});
|
||||||
|
|
||||||
// Сохраняем ID сообщения с фотографией и ID сообщения с товаром в состояние пользователя
|
// Store the current quantity and photo message ID in user state
|
||||||
await userStates.set(chatId, {
|
userStates.set(chatId, {
|
||||||
action: 'buying_product',
|
action: 'buying_product',
|
||||||
productId,
|
productId,
|
||||||
quantity: 1,
|
quantity: 1,
|
||||||
photoMessageId: photoMessage ? photoMessage.message_id : null,
|
photoMessageId
|
||||||
productMessageId: productMessage.message_id,
|
|
||||||
location: state?.location // Сохраняем информацию о локации
|
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error({ err: error }, 'Error in handleProductSelection');
|
console.error('Error in handleProductSelection:', error);
|
||||||
await bot.sendMessage(chatId, 'Error loading product details. Please try again.');
|
await bot.sendMessage(chatId, 'Error loading product details. Please try again.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -408,7 +373,7 @@ export default class UserProductHandler {
|
|||||||
const chatId = callbackQuery.message.chat.id;
|
const chatId = callbackQuery.message.chat.id;
|
||||||
const messageId = callbackQuery.message.message_id;
|
const messageId = callbackQuery.message.message_id;
|
||||||
const productId = callbackQuery.data.replace('increase_quantity_', '');
|
const productId = callbackQuery.data.replace('increase_quantity_', '');
|
||||||
const state = await userStates.get(chatId);
|
const state = userStates.get(chatId);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const product = await ProductService.getProductById(productId);
|
const product = await ProductService.getProductById(productId);
|
||||||
@@ -428,7 +393,7 @@ export default class UserProductHandler {
|
|||||||
const newQuantity = Math.min(currentQuantity + 1, product.quantity_in_stock);
|
const newQuantity = Math.min(currentQuantity + 1, product.quantity_in_stock);
|
||||||
|
|
||||||
// Update state
|
// Update state
|
||||||
await userStates.set(chatId, {
|
userStates.set(chatId, {
|
||||||
...state,
|
...state,
|
||||||
quantity: newQuantity
|
quantity: newQuantity
|
||||||
});
|
});
|
||||||
@@ -459,7 +424,7 @@ export default class UserProductHandler {
|
|||||||
|
|
||||||
await bot.answerCallbackQuery(callbackQuery.id);
|
await bot.answerCallbackQuery(callbackQuery.id);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error({ err: error }, 'Error in handleIncreaseQuantity');
|
console.error('Error in handleIncreaseQuantity:', error);
|
||||||
await bot.answerCallbackQuery(callbackQuery.id);
|
await bot.answerCallbackQuery(callbackQuery.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -468,7 +433,7 @@ export default class UserProductHandler {
|
|||||||
const chatId = callbackQuery.message.chat.id;
|
const chatId = callbackQuery.message.chat.id;
|
||||||
const messageId = callbackQuery.message.message_id;
|
const messageId = callbackQuery.message.message_id;
|
||||||
const productId = callbackQuery.data.replace('decrease_quantity_', '');
|
const productId = callbackQuery.data.replace('decrease_quantity_', '');
|
||||||
const state = await userStates.get(chatId);
|
const state = userStates.get(chatId);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const product = await ProductService.getProductById(productId)
|
const product = await ProductService.getProductById(productId)
|
||||||
@@ -488,7 +453,7 @@ export default class UserProductHandler {
|
|||||||
const newQuantity = Math.max(currentQuantity - 1, 1);
|
const newQuantity = Math.max(currentQuantity - 1, 1);
|
||||||
|
|
||||||
// Update state
|
// Update state
|
||||||
await userStates.set(chatId, {
|
userStates.set(chatId, {
|
||||||
...state,
|
...state,
|
||||||
quantity: newQuantity
|
quantity: newQuantity
|
||||||
});
|
});
|
||||||
@@ -519,7 +484,7 @@ export default class UserProductHandler {
|
|||||||
|
|
||||||
await bot.answerCallbackQuery(callbackQuery.id);
|
await bot.answerCallbackQuery(callbackQuery.id);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error({ err: error }, 'Error in handleDecreaseQuantity');
|
console.error('Error in handleDecreaseQuantity:', error);
|
||||||
await bot.answerCallbackQuery(callbackQuery.id);
|
await bot.answerCallbackQuery(callbackQuery.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -528,15 +493,16 @@ export default class UserProductHandler {
|
|||||||
const chatId = callbackQuery.message.chat.id;
|
const chatId = callbackQuery.message.chat.id;
|
||||||
const telegramId = callbackQuery.from.id;
|
const telegramId = callbackQuery.from.id;
|
||||||
const productId = callbackQuery.data.replace('buy_product_', '');
|
const productId = callbackQuery.data.replace('buy_product_', '');
|
||||||
const state = await userStates.get(chatId);
|
const state = userStates.get(chatId);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const user = await UserService.getUserByTelegramId(telegramId);
|
const user = await UserService.getUserByTelegramId(telegramId)
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new Error('User not found');
|
throw new Error('User not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
const product = await ProductService.getProductById(productId);
|
const product = await ProductService.getProductById(productId);
|
||||||
|
|
||||||
if (!product) {
|
if (!product) {
|
||||||
throw new Error('Product not found');
|
throw new Error('Product not found');
|
||||||
}
|
}
|
||||||
@@ -544,47 +510,13 @@ export default class UserProductHandler {
|
|||||||
const quantity = state?.quantity || 1;
|
const quantity = state?.quantity || 1;
|
||||||
const totalPrice = product.price * quantity;
|
const totalPrice = product.price * quantity;
|
||||||
|
|
||||||
// Получение баланса пользователя
|
// Get user's crypto wallets with balances
|
||||||
const userBalance = await UserService.getUserBalance(user.id);
|
|
||||||
|
|
||||||
// Проверка баланса пользователя
|
|
||||||
if (userBalance <= 0) {
|
|
||||||
await bot.sendMessage(
|
|
||||||
chatId,
|
|
||||||
`❌ Insufficient balance. Your current balance is $${userBalance}. You need $${totalPrice} to complete this purchase.`,
|
|
||||||
{
|
|
||||||
reply_markup: {
|
|
||||||
inline_keyboard: [[
|
|
||||||
{ text: '💰 Top Up Balance', callback_data: 'top_up_wallet' }
|
|
||||||
]]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (userBalance < totalPrice) {
|
|
||||||
await bot.sendMessage(
|
|
||||||
chatId,
|
|
||||||
`❌ Insufficient balance. Your current balance is $${userBalance}. You need $${totalPrice} to complete this purchase.`,
|
|
||||||
{
|
|
||||||
reply_markup: {
|
|
||||||
inline_keyboard: [[
|
|
||||||
{ text: '💰 Top Up Balance', callback_data: 'top_up_wallet' }
|
|
||||||
]]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Получение криптокошельков пользователя
|
|
||||||
const cryptoWallets = await db.allAsync(`
|
const cryptoWallets = await db.allAsync(`
|
||||||
SELECT wallet_type, address
|
SELECT wallet_type, address
|
||||||
FROM crypto_wallets
|
FROM crypto_wallets
|
||||||
WHERE user_id = ?
|
WHERE user_id = ?
|
||||||
ORDER BY wallet_type
|
ORDER BY wallet_type
|
||||||
`, [user.id]);
|
`, [user.id]);
|
||||||
|
|
||||||
if (cryptoWallets.length === 0) {
|
if (cryptoWallets.length === 0) {
|
||||||
await bot.sendMessage(
|
await bot.sendMessage(
|
||||||
@@ -593,7 +525,7 @@ export default class UserProductHandler {
|
|||||||
{
|
{
|
||||||
reply_markup: {
|
reply_markup: {
|
||||||
inline_keyboard: [[
|
inline_keyboard: [[
|
||||||
{ text: '➕ Add Wallet', callback_data: 'add_wallet' }
|
{text: '➕ Add Wallet', callback_data: 'add_wallet'}
|
||||||
]]
|
]]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -603,32 +535,28 @@ export default class UserProductHandler {
|
|||||||
|
|
||||||
const keyboard = {
|
const keyboard = {
|
||||||
inline_keyboard: [
|
inline_keyboard: [
|
||||||
[{ text: `Pay`, callback_data: `pay_with_main_${productId}_${quantity}` }],
|
...cryptoWallets.map(wallet => [{
|
||||||
[{ text: '« Cancel', callback_data: `shop_product_${productId}` }] // Кнопка "Back"
|
text: `Pay with ${wallet.wallet_type}`,
|
||||||
|
callback_data: `pay_with_${wallet.wallet_type}_${productId}_${quantity}`
|
||||||
|
}]),
|
||||||
|
[{text: '« Cancel', callback_data: `shop_product_${productId}`}]
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
// Отправка сообщения с кнопками
|
await bot.editMessageText(
|
||||||
const purchaseMessage = await bot.editMessageText(
|
|
||||||
`🛒 Purchase Summary:\n\n` +
|
`🛒 Purchase Summary:\n\n` +
|
||||||
`Product: ${product.name}\n` +
|
`Product: ${product.name}\n` +
|
||||||
`Quantity: ${quantity}\n` +
|
`Quantity: ${quantity}\n` +
|
||||||
`Total: $${totalPrice}\n`,
|
`Total: $${totalPrice}\n\n` +
|
||||||
|
`Select payment method:`,
|
||||||
{
|
{
|
||||||
chat_id: chatId,
|
chat_id: chatId,
|
||||||
message_id: callbackQuery.message.message_id,
|
message_id: callbackQuery.message.message_id,
|
||||||
reply_markup: keyboard
|
reply_markup: keyboard
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// Сохранение ID сообщения с фотографией в состояние пользователя
|
|
||||||
await userStates.set(chatId, {
|
|
||||||
...state,
|
|
||||||
photoMessageId: state?.photoMessageId || null,
|
|
||||||
purchaseMessageId: purchaseMessage.message_id
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error({ err: error }, 'Error in handleBuyProduct');
|
console.error('Error in handleBuyProduct:', error);
|
||||||
await bot.sendMessage(chatId, 'Error processing purchase. Please try again.');
|
await bot.sendMessage(chatId, 'Error processing purchase. Please try again.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -637,25 +565,11 @@ export default class UserProductHandler {
|
|||||||
const chatId = callbackQuery.message.chat.id;
|
const chatId = callbackQuery.message.chat.id;
|
||||||
const telegramId = callbackQuery.from.id;
|
const telegramId = callbackQuery.from.id;
|
||||||
const [walletType, productId, quantity] = callbackQuery.data.replace('pay_with_', '').split('_');
|
const [walletType, productId, quantity] = callbackQuery.data.replace('pay_with_', '').split('_');
|
||||||
const state = await userStates.get(chatId);
|
const state = userStates.get(chatId);
|
||||||
|
|
||||||
if (!Validators.isValidWalletType(walletType)) {
|
|
||||||
await bot.sendMessage(chatId, 'Invalid wallet type.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!Validators.isValidNumericId(Number(productId))) {
|
|
||||||
await bot.sendMessage(chatId, 'Invalid product.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const qty = Number(quantity);
|
|
||||||
if (!Number.isFinite(qty) || qty <= 0) {
|
|
||||||
await bot.sendMessage(chatId, 'Invalid quantity.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await UserService.recalculateUserBalanceByTelegramId(telegramId);
|
await UserService.recalculateUserBalanceByTelegramId(telegramId);
|
||||||
const user = await UserService.getUserByTelegramId(telegramId);
|
const user = await UserService.getUserByTelegramId(telegramId)
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new Error('User not found');
|
throw new Error('User not found');
|
||||||
@@ -670,7 +584,7 @@ export default class UserProductHandler {
|
|||||||
const balance = user.total_balance + user.bonus_balance;
|
const balance = user.total_balance + user.bonus_balance;
|
||||||
|
|
||||||
if (totalPrice > balance) {
|
if (totalPrice > balance) {
|
||||||
await userStates.delete(chatId);
|
userStates.delete(chatId);
|
||||||
await bot.editMessageText(`Not enough money`, {
|
await bot.editMessageText(`Not enough money`, {
|
||||||
chat_id: chatId,
|
chat_id: chatId,
|
||||||
message_id: callbackQuery.message.message_id,
|
message_id: callbackQuery.message.message_id,
|
||||||
@@ -678,73 +592,45 @@ export default class UserProductHandler {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Проверка наличия товара
|
await PurchaseService.createPurchase(user.id, product.id, walletType, quantity, totalPrice)
|
||||||
if (product.quantity_in_stock < quantity) {
|
|
||||||
await bot.sendMessage(chatId, `❌ Not enough items in stock. Only ${product.quantity_in_stock} available.`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Создаем покупку и получаем её ID
|
|
||||||
const purchaseId = await PurchaseService.createPurchase(user.id, productId, walletType, quantity, totalPrice);
|
|
||||||
|
|
||||||
// Уменьшаем количество товара в базе данных
|
|
||||||
await ProductService.decreaseProductQuantity(productId, quantity);
|
|
||||||
|
|
||||||
// Извлекаем данные о локации
|
|
||||||
const location = await LocationService.getLocationById(product.location_id);
|
|
||||||
const category = await CategoryService.getCategoryById(product.category_id);
|
|
||||||
|
|
||||||
// Удаляем сообщение с Public Photo, если оно существует
|
|
||||||
if (state?.photoMessageId) {
|
|
||||||
try {
|
|
||||||
await bot.deleteMessage(chatId, state.photoMessageId);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error({ err: error }, 'Error deleting Public Photo message');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Отправляем Hidden Photo
|
|
||||||
let hiddenPhotoMessage;
|
let hiddenPhotoMessage;
|
||||||
if (product.hidden_photo_url) {
|
if (product.hidden_photo_url) {
|
||||||
try {
|
try {
|
||||||
hiddenPhotoMessage = await bot.sendPhoto(chatId, product.hidden_photo_url, { caption: 'Hidden photo' });
|
hiddenPhotoMessage = await bot.sendPhoto(chatId, product.hidden_photo_url, {caption: 'Hidden photo'});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
hiddenPhotoMessage = await bot.sendPhoto(chatId, "./corrupt-photo.jpg", { caption: 'Hidden photo' });
|
hiddenPhotoMessage = await bot.sendPhoto(chatId, "./corrupt-photo.jpg", {caption: 'Hidden photo'})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const message = `
|
const message = `
|
||||||
📦 Purchase Details:
|
📦 Product Details:
|
||||||
Name: ${product.name}
|
|
||||||
Quantity: ${quantity}
|
|
||||||
Total: $${totalPrice}
|
|
||||||
Location: ${location?.country || 'N/A'}, ${location?.city || 'N/A'}, ${location?.district || 'N/A'}
|
|
||||||
Category: ${category?.name || 'N/A'}
|
|
||||||
|
|
||||||
🔒 Private Information:
|
Name: ${product.name}
|
||||||
${product.private_data || 'N/A'}
|
Price: $${product.price}
|
||||||
Hidden Location: ${product.hidden_description || 'N/A'}
|
Description: ${product.description}
|
||||||
Coordinates: ${product.hidden_coordinates || 'N/A'}
|
Stock: ${product.quantity_in_stock}
|
||||||
`;
|
Location: ${product.country}, ${product.city}, ${product.district}
|
||||||
|
Category: ${product.category_name}
|
||||||
|
Subcategory: ${product.subcategory_name}
|
||||||
|
|
||||||
|
🔒 Private Information:
|
||||||
|
${product.private_data}
|
||||||
|
Hidden Location: ${product.hidden_description}
|
||||||
|
Coordinates: ${product.hidden_coordinates}
|
||||||
|
`;
|
||||||
|
|
||||||
const keyboard = {
|
const keyboard = {
|
||||||
inline_keyboard: [
|
inline_keyboard: [
|
||||||
[{ text: 'View new purchase', callback_data: `view_purchase_${purchaseId}` }], // Переход к покупке
|
[{text: "I've got it!", callback_data: "Asdasdasd"}],
|
||||||
[{ text: "Contact support", url: config.SUPPORT_LINK }] // Сохранение кнопки "Contact support"
|
[{text: "Contact support", url: config.SUPPORT_LINK}]
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
await bot.sendMessage(chatId, message, { reply_markup: keyboard });
|
await bot.sendMessage(chatId, message, {reply_markup: keyboard});
|
||||||
await bot.deleteMessage(chatId, callbackQuery.message.message_id);
|
await bot.deleteMessage(chatId, callbackQuery.message.message_id);
|
||||||
|
|
||||||
// Сохраняем ID сообщения с Hidden Photo в состояние пользователя
|
|
||||||
await userStates.set(chatId, {
|
|
||||||
action: 'viewing_purchase',
|
|
||||||
purchaseId,
|
|
||||||
hiddenPhotoMessageId: hiddenPhotoMessage ? hiddenPhotoMessage.message_id : null
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error({ err: error }, 'Error in handlePay');
|
console.error('Error in handleBuyProduct:', error);
|
||||||
await bot.sendMessage(chatId, 'Error processing purchase. Please try again.');
|
await bot.sendMessage(chatId, 'Error processing purchase. Please try again.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,90 +1,67 @@
|
|||||||
// userPurchaseHandler.js
|
|
||||||
|
|
||||||
|
|
||||||
import config from "../../config/config.js";
|
import config from "../../config/config.js";
|
||||||
import db from '../../config/database.js';
|
|
||||||
import PurchaseService from "../../services/purchaseService.js";
|
import PurchaseService from "../../services/purchaseService.js";
|
||||||
import UserService from "../../services/userService.js";
|
import UserService from "../../services/userService.js";
|
||||||
import LocationService from "../../services/locationService.js";
|
|
||||||
import ProductService from "../../services/productService.js";
|
|
||||||
import CategoryService from "../../services/categoryService.js";
|
|
||||||
import bot from "../../context/bot.js";
|
import bot from "../../context/bot.js";
|
||||||
import userStates from "../../context/userStates.js";
|
import ProductService from "../../services/productService.js";
|
||||||
import Validators from '../../utils/validators.js';
|
|
||||||
import logger from '../../utils/logger.js';
|
|
||||||
|
|
||||||
export default class UserPurchaseHandler {
|
export default class UserPurchaseHandler {
|
||||||
static async viewPurchasePage(userId, page) {
|
static async viewPurchasePage(userId, page) {
|
||||||
try {
|
try {
|
||||||
const limit = 10; // Количество покупок на странице
|
const limit = 10;
|
||||||
const offset = page * limit;
|
const offset = (page || 0) * limit;
|
||||||
|
|
||||||
|
const previousPage = page > 0 ? page - 1 : 0;
|
||||||
|
const nextPage = page + 1;
|
||||||
|
|
||||||
// Получаем покупки пользователя с учетом пагинации
|
|
||||||
const purchases = await PurchaseService.getPurchasesByUserId(userId, limit, offset);
|
const purchases = await PurchaseService.getPurchasesByUserId(userId, limit, offset);
|
||||||
|
|
||||||
// Получаем общее количество покупок пользователя
|
if ((purchases.length === 0) && (page == 0)) {
|
||||||
const totalPurchases = await PurchaseService.getTotalPurchasesByUserId(userId);
|
|
||||||
|
|
||||||
// Вычисляем общее количество страниц
|
|
||||||
const totalPages = Math.ceil(totalPurchases / limit);
|
|
||||||
|
|
||||||
// Если покупок нет, возвращаем сообщение о пустом архиве
|
|
||||||
if (totalPurchases === 0) {
|
|
||||||
return {
|
return {
|
||||||
text: 'Your purchase history is empty.',
|
text: 'You haven\'t made any purchases yet.',
|
||||||
markup: {
|
markup: [[
|
||||||
inline_keyboard: [
|
{text: '🛍 Browse Products', callback_data: 'shop_start'}
|
||||||
[{ text: '🛍 Browse Products', callback_data: 'shop_start' }]
|
]]
|
||||||
]
|
}
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Если покупок нет на текущей странице, но это не первая страница, переходим на предыдущую страницу
|
if ((purchases.length === 0) && (page > 0)) {
|
||||||
if (purchases.length === 0 && page > 0) {
|
return await this.viewPurchasePage(userId, previousPage);
|
||||||
return await this.viewPurchasePage(userId, page - 1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const keyboard = {
|
const keyboard = {
|
||||||
inline_keyboard: [
|
inline_keyboard: [
|
||||||
...purchases.map(item => [{
|
...purchases.map(item => [{
|
||||||
// Добавляем иконку статуса покупки
|
text: `${item.product_name} [${new Date(item.purchase_date).toLocaleString()}]`,
|
||||||
text: `${item.status === 'received' ? '✅' : '❌'} ${item.product_name} [${new Date(item.purchase_date).toLocaleString()}]`,
|
|
||||||
callback_data: `view_purchase_${item.id}`
|
callback_data: `view_purchase_${item.id}`
|
||||||
}]),
|
}]),
|
||||||
[
|
|
||||||
{
|
|
||||||
text: page > 0 ? `« Back (Page ${page})` : '« Back',
|
|
||||||
callback_data: page > 0 ? `list_purchases_${page - 1}` : 'no_action', // Если на первой странице, то "no_action"
|
|
||||||
hide: page === 0 // Скрываем кнопку "Назад", если на первой странице
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: `Page ${page + 1} of ${totalPages}`,
|
|
||||||
callback_data: 'current_page'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: page < totalPages - 1 ? `Next » (Page ${page + 2})` : 'Next »',
|
|
||||||
callback_data: page < totalPages - 1 ? `list_purchases_${page + 1}` : 'no_action', // Если на последней странице, то "no_action"
|
|
||||||
hide: page === totalPages - 1 // Скрываем кнопку "Вперед", если на последней странице
|
|
||||||
}
|
|
||||||
]
|
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
|
keyboard.inline_keyboard.push([
|
||||||
|
{text: `«`, callback_data: `list_purchases_${previousPage}`},
|
||||||
|
{text: `»`, callback_data: `list_purchases_${nextPage}`},
|
||||||
|
]);
|
||||||
|
|
||||||
|
keyboard.inline_keyboard.push([
|
||||||
|
{text: '🛍 Browse Products', callback_data: 'shop_start'}
|
||||||
|
]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
text: `📦 Select purchase to view detailed information (Page ${page + 1} of ${totalPages}):`,
|
text: `📦 Select purchase to view detailed information:`,
|
||||||
markup: keyboard
|
markup: keyboard
|
||||||
};
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error({ err: error }, 'Error in viewPurchasePage');
|
console.error('Error in showPurchases:', error);
|
||||||
return { text: 'Error loading purchase history. Please try again.' };
|
return {text: 'Error loading purchase history. Please try again.'};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static async handlePurchaseListPage(callbackQuery) {
|
static async handlePurchaseListPage(callbackQuery) {
|
||||||
const telegramId = callbackQuery.from.id;
|
const telegramId = callbackQuery.from.id;
|
||||||
const chatId = callbackQuery.message.chat.id;
|
const chatId = callbackQuery.message.chat.id;
|
||||||
const page = parseInt(callbackQuery.data.replace('list_purchases_', ''));
|
|
||||||
|
const page = callbackQuery.data.replace('list_purchases_', '');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const user = await UserService.getUserByTelegramId(telegramId);
|
const user = await UserService.getUserByTelegramId(telegramId);
|
||||||
@@ -94,30 +71,15 @@ export default class UserPurchaseHandler {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Удаляем сообщение с Hidden Photo, если оно существует
|
const {text, markup} = await this.viewPurchasePage(user.id, parseInt(page));
|
||||||
const state = await userStates.get(chatId);
|
|
||||||
if (state?.hiddenPhotoMessageId) {
|
|
||||||
try {
|
|
||||||
await bot.deleteMessage(chatId, state.hiddenPhotoMessageId);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error({ err: error }, 'Error deleting Hidden Photo message');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const { text, markup } = await this.viewPurchasePage(user.id, page);
|
|
||||||
|
|
||||||
await bot.editMessageText(text, {
|
await bot.editMessageText(text, {
|
||||||
chat_id: chatId,
|
chat_id: chatId,
|
||||||
message_id: callbackQuery.message.message_id,
|
message_id: callbackQuery.message.message_id,
|
||||||
reply_markup: markup,
|
reply_markup: markup,
|
||||||
parse_mode: 'Markdown'
|
parse_mode: 'Markdown',
|
||||||
});
|
});
|
||||||
|
|
||||||
// Удаляем состояние пользователя
|
|
||||||
await userStates.delete(chatId);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error({ err: e }, 'Error in handlePurchaseListPage');
|
return;
|
||||||
await bot.sendMessage(chatId, 'Error loading purchase history. Please try again.');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,6 +88,7 @@ export default class UserPurchaseHandler {
|
|||||||
const telegramId = msg.from.id;
|
const telegramId = msg.from.id;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
||||||
const user = await UserService.getUserByTelegramId(telegramId);
|
const user = await UserService.getUserByTelegramId(telegramId);
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
@@ -133,12 +96,12 @@ export default class UserPurchaseHandler {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { text, markup } = await this.viewPurchasePage(user.id, 0);
|
const {text, markup} = await this.viewPurchasePage(user.id, 0);
|
||||||
|
|
||||||
await bot.sendMessage(chatId, text, { reply_markup: markup, parse_mode: 'Markdown' });
|
await bot.sendMessage(chatId, text, {reply_markup: markup, parse_mode: 'Markdown'});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error({ err: error }, 'Error in showPurchases');
|
console.error('Error in handleSubcategorySelection:', error);
|
||||||
await bot.sendMessage(chatId, 'Error loading purchase history. Please try again.');
|
await bot.sendMessage(chatId, 'Error loading products. Please try again.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,156 +109,52 @@ export default class UserPurchaseHandler {
|
|||||||
const chatId = callbackQuery.message.chat.id;
|
const chatId = callbackQuery.message.chat.id;
|
||||||
const purchaseId = callbackQuery.data.replace('view_purchase_', '');
|
const purchaseId = callbackQuery.data.replace('view_purchase_', '');
|
||||||
|
|
||||||
try {
|
const purchase = await PurchaseService.getPurchaseById(purchaseId);
|
||||||
// Получаем данные покупки
|
|
||||||
const purchase = await PurchaseService.getPurchaseById(purchaseId);
|
|
||||||
if (!purchase) {
|
|
||||||
await bot.sendMessage(chatId, "No such purchase");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Получаем данные товара по product_id
|
if (!purchase) {
|
||||||
const product = await ProductService.getProductById(purchase.product_id);
|
await bot.sendMessage(chatId, "No such purchase");
|
||||||
if (!product) {
|
return;
|
||||||
await bot.sendMessage(chatId, "No such product");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Получаем данные локации по location_id
|
|
||||||
const location = await LocationService.getLocationById(product.location_id);
|
|
||||||
|
|
||||||
// Получаем данные категории по category_id
|
|
||||||
const category = await CategoryService.getCategoryById(product.category_id);
|
|
||||||
|
|
||||||
// Удаляем старое сообщение с Hidden Photo, если оно существует
|
|
||||||
const state = await userStates.get(chatId);
|
|
||||||
if (state?.hiddenPhotoMessageId) {
|
|
||||||
try {
|
|
||||||
await bot.deleteMessage(chatId, state.hiddenPhotoMessageId);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error({ err: error }, 'Error deleting Hidden Photo message');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Отправляем Hidden Photo
|
|
||||||
let hiddenPhotoMessage;
|
|
||||||
if (product.hidden_photo_url) {
|
|
||||||
try {
|
|
||||||
hiddenPhotoMessage = await bot.sendPhoto(chatId, product.hidden_photo_url, { caption: 'Hidden photo' });
|
|
||||||
} catch (e) {
|
|
||||||
hiddenPhotoMessage = await bot.sendPhoto(chatId, "./corrupt-photo.jpg", { caption: 'Hidden photo' });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Формируем сообщение с деталями покупки
|
|
||||||
const message = `
|
|
||||||
📦 Purchase Details:
|
|
||||||
Name: ${product.name || 'N/A'}
|
|
||||||
Quantity: ${purchase.quantity}
|
|
||||||
Total: $${purchase.total_price}
|
|
||||||
Location: ${location?.country || 'N/A'}, ${location?.city || 'N/A'}, ${location?.district || 'N/A'}
|
|
||||||
Category: ${category?.name || 'N/A'}
|
|
||||||
|
|
||||||
🔒 Private Information:
|
|
||||||
${product.private_data || 'N/A'}
|
|
||||||
Hidden Location: ${product.hidden_description || 'N/A'}
|
|
||||||
Coordinates: ${product.hidden_coordinates || 'N/A'}
|
|
||||||
`;
|
|
||||||
|
|
||||||
// Создаем клавиатуру с кнопками
|
|
||||||
const keyboard = {
|
|
||||||
inline_keyboard: [
|
|
||||||
// Проверяем статус покупки перед добавлением кнопки "I've got it!"
|
|
||||||
...(purchase.status !== 'received' ? [[{ text: "I've got it!", callback_data: `confirm_received_${purchaseId}` }]] : []),
|
|
||||||
[{ text: "« Back to Purchase List", callback_data: `list_purchases_0` }], // Кнопка "Назад к списку покупок"
|
|
||||||
[{ text: "Contact support", url: config.SUPPORT_LINK }]
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
// Отправляем сообщение с деталями покупки
|
|
||||||
await bot.sendMessage(chatId, message, { reply_markup: keyboard });
|
|
||||||
|
|
||||||
// Удаляем предыдущее сообщение
|
|
||||||
await bot.deleteMessage(chatId, callbackQuery.message.message_id);
|
|
||||||
|
|
||||||
// Сохраняем ID сообщения с Hidden Photo в состояние пользователя
|
|
||||||
await userStates.set(chatId, {
|
|
||||||
action: 'viewing_purchase',
|
|
||||||
purchaseId,
|
|
||||||
hiddenPhotoMessageId: hiddenPhotoMessage ? hiddenPhotoMessage.message_id : null
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
logger.error({ err: error }, 'Error in viewPurchase');
|
|
||||||
await bot.sendMessage(chatId, 'Error loading purchase details. Please try again.');
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
static async handleConfirmReceived(callbackQuery) {
|
const product = await ProductService.getProductById(purchase.product_id)
|
||||||
const chatId = callbackQuery.message.chat.id;
|
|
||||||
const messageId = callbackQuery.message.message_id;
|
|
||||||
const purchaseId = callbackQuery.data.replace('confirm_received_', '');
|
|
||||||
|
|
||||||
try {
|
if (!product) {
|
||||||
// Получаем данные покупки
|
await bot.sendMessage(chatId, "No such product");
|
||||||
const purchase = await PurchaseService.getPurchaseById(purchaseId);
|
return;
|
||||||
if (!purchase) {
|
|
||||||
await bot.sendMessage(chatId, "Purchase not found.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Получаем данные пользователя по user_id из покупки
|
|
||||||
const user = await UserService.getUserByUserId(purchase.user_id);
|
|
||||||
if (!user) {
|
|
||||||
await bot.sendMessage(chatId, "User not found for this purchase.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Обновляем статус покупки в базе данных
|
|
||||||
await PurchaseService.updatePurchaseStatus(purchaseId, 'received');
|
|
||||||
|
|
||||||
// Добавляем запись в таблицу transactions
|
|
||||||
await db.runAsync(
|
|
||||||
`INSERT INTO transactions (user_id, wallet_type, tx_hash, amount, created_at)
|
|
||||||
VALUES (?, ?, ?, ?, ?)`,
|
|
||||||
[
|
|
||||||
user.id, // ID пользователя
|
|
||||||
purchase.wallet_type, // Источник списания (например, "bonus_50, crypto_30")
|
|
||||||
purchase.tx_hash || 'no_hash', // Хеш транзакции (если не указан, то "no_hash")
|
|
||||||
purchase.total_price, // Сумма транзакции
|
|
||||||
new Date().toISOString() // Дата создания транзакции
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Отправляем уведомление администраторам
|
|
||||||
const adminIds = config.ADMIN_IDS; // Используем массив ADMIN_IDS
|
|
||||||
for (const adminId of adminIds) {
|
|
||||||
await bot.sendMessage(adminId, `User ${callbackQuery.from.username} has confirmed receiving purchase #${purchaseId}.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Уведомляем пользователя
|
|
||||||
await bot.sendMessage(chatId, "Thank you! Your purchase has been marked as received.");
|
|
||||||
|
|
||||||
// Удаляем сообщение с карточкой товара
|
|
||||||
await bot.deleteMessage(chatId, messageId);
|
|
||||||
|
|
||||||
// Удаляем Hidden Photo, если оно существует
|
|
||||||
const state = await userStates.get(chatId);
|
|
||||||
if (state?.hiddenPhotoMessageId) {
|
|
||||||
try {
|
|
||||||
await bot.deleteMessage(chatId, state.hiddenPhotoMessageId);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error({ err: error }, 'Error deleting Hidden Photo message');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Удаляем состояние пользователя
|
|
||||||
await userStates.delete(chatId);
|
|
||||||
|
|
||||||
// Открываем список покупок для пользователя
|
|
||||||
await this.showPurchases({ chat: { id: chatId }, from: { id: callbackQuery.from.id } });
|
|
||||||
} catch (error) {
|
|
||||||
logger.error({ err: error }, 'Error in handleConfirmReceived');
|
|
||||||
await bot.sendMessage(chatId, 'Error confirming receipt. Please try again.');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let hiddenPhotoMessage;
|
||||||
|
if (product.hidden_photo_url) {
|
||||||
|
try {
|
||||||
|
hiddenPhotoMessage = await bot.sendPhoto(chatId, product.hidden_photo_url, {caption: 'Hidden photo'});
|
||||||
|
} catch (e) {
|
||||||
|
hiddenPhotoMessage = await bot.sendPhoto(chatId, "./corrupt-photo.jpg", {caption: 'Hidden photo'})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = `
|
||||||
|
📦 Purchase Details:
|
||||||
|
Name: ${purchase.product_name}
|
||||||
|
Quantity: ${purchase.quantity}
|
||||||
|
Total: $${purchase.total_price}
|
||||||
|
Location: ${purchase.country}, ${purchase.city}
|
||||||
|
Payment: ${purchase.wallet_type}
|
||||||
|
Date: ${new Date(purchase.purchase_date).toLocaleString()}
|
||||||
|
|
||||||
|
🔒 Private Information:
|
||||||
|
${product.private_data}
|
||||||
|
Hidden Location: ${product.hidden_description}
|
||||||
|
Coordinates: ${product.hidden_coordinates}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const keyboard = {
|
||||||
|
inline_keyboard: [
|
||||||
|
[{text: "I've got it!", callback_data: "Asdasdasd"}],
|
||||||
|
[{text: "Contact support", url: config.SUPPORT_LINK}]
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
await bot.sendMessage(chatId, message, {reply_markup: keyboard});
|
||||||
|
await bot.deleteMessage(chatId, callbackQuery.message.message_id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
573
src/handlers/userHandlers/userWalletsHandler.js
Normal file
573
src/handlers/userHandlers/userWalletsHandler.js
Normal file
@@ -0,0 +1,573 @@
|
|||||||
|
import db from '../../config/database.js';
|
||||||
|
import WalletGenerator from '../../utils/walletGenerator.js';
|
||||||
|
import WalletService from '../../utils/walletService.js';
|
||||||
|
import UserService from "../../services/userService.js";
|
||||||
|
import bot from "../../context/bot.js";
|
||||||
|
|
||||||
|
export default class UserWalletsHandler {
|
||||||
|
static async showBalance(msg) {
|
||||||
|
const chatId = msg.chat.id;
|
||||||
|
const telegramId = msg.from.id;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const user = await UserService.getUserByTelegramId(telegramId.toString());
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
await bot.sendMessage(chatId, 'Profile not found. Please use /start to create one.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get active crypto wallets only
|
||||||
|
const cryptoWallets = await db.allAsync(`
|
||||||
|
SELECT wallet_type, address
|
||||||
|
FROM crypto_wallets
|
||||||
|
WHERE user_id = ?
|
||||||
|
ORDER BY wallet_type
|
||||||
|
`, [user.id]);
|
||||||
|
|
||||||
|
let message = '💰 *Your Active Wallets:*\n\n';
|
||||||
|
|
||||||
|
if (cryptoWallets.length > 0) {
|
||||||
|
const walletService = new WalletService(
|
||||||
|
cryptoWallets.find(w => w.wallet_type === 'BTC')?.address,
|
||||||
|
cryptoWallets.find(w => w.wallet_type === 'LTC')?.address,
|
||||||
|
cryptoWallets.find(w => w.wallet_type === 'TRON')?.address,
|
||||||
|
cryptoWallets.find(w => w.wallet_type === 'ETH')?.address,
|
||||||
|
user.id,
|
||||||
|
Date.now() - 30 * 24 * 60 * 60 * 1000
|
||||||
|
);
|
||||||
|
|
||||||
|
const balances = await walletService.getAllBalances();
|
||||||
|
let totalUsdValue = 0;
|
||||||
|
|
||||||
|
// Show active wallets
|
||||||
|
for (const [type, balance] of Object.entries(balances)) {
|
||||||
|
const baseType = this.getBaseWalletType(type);
|
||||||
|
const wallet = cryptoWallets.find(w =>
|
||||||
|
w.wallet_type === baseType ||
|
||||||
|
(type.includes('TRC-20') && w.wallet_type === 'TRON') ||
|
||||||
|
(type.includes('ERC-20') && w.wallet_type === 'ETH')
|
||||||
|
);
|
||||||
|
|
||||||
|
if (wallet) {
|
||||||
|
message += `🔐 *${type}*\n`;
|
||||||
|
message += `├ Balance: ${balance.amount.toFixed(8)} ${type.split(' ')[0]}\n`;
|
||||||
|
message += `├ Value: $${balance.usdValue.toFixed(2)}\n`;
|
||||||
|
message += `└ Address: \`${wallet.address}\`\n\n`;
|
||||||
|
totalUsdValue += balance.usdValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
message += `📊 *Total Balance:* $${totalUsdValue.toFixed(2)}\n`;
|
||||||
|
} else {
|
||||||
|
message = 'You don\'t have any active wallets yet.';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if user has archived wallets
|
||||||
|
const archivedCount = await db.getAsync(`
|
||||||
|
SELECT COUNT(*) as count
|
||||||
|
FROM crypto_wallets
|
||||||
|
WHERE user_id = ? AND wallet_type LIKE '%_%'
|
||||||
|
`, [user.id]);
|
||||||
|
|
||||||
|
const keyboard = {
|
||||||
|
inline_keyboard: [
|
||||||
|
[
|
||||||
|
{ text: '➕ Add Crypto Wallet', callback_data: 'add_wallet' },
|
||||||
|
{ text: '💸 Top Up', callback_data: 'top_up_wallet' }
|
||||||
|
],
|
||||||
|
[{ text: '🔄 Refresh Balance', callback_data: 'refresh_balance' }],
|
||||||
|
[{ text: '📊 Transaction History', callback_data: 'wallet_history' }]
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add archived wallets button if any exist
|
||||||
|
if (archivedCount.count > 0) {
|
||||||
|
keyboard.inline_keyboard.splice(2, 0, [
|
||||||
|
{ text: `📁 Archived Wallets (${archivedCount.count})`, callback_data: 'view_archived_wallets' }
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
await bot.sendMessage(chatId, message, {
|
||||||
|
reply_markup: keyboard,
|
||||||
|
parse_mode: 'Markdown'
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in showBalance:', error);
|
||||||
|
await bot.sendMessage(chatId, 'Error loading balance. Please try again.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async handleAddWallet(callbackQuery) {
|
||||||
|
const chatId = callbackQuery.message.chat.id;
|
||||||
|
|
||||||
|
const cryptoOptions = [
|
||||||
|
['BTC', 'ETH', 'LTC'],
|
||||||
|
['USDT TRC-20', 'USDD TRC-20'],
|
||||||
|
['USDT ERC-20', 'USDC ERC-20']
|
||||||
|
];
|
||||||
|
|
||||||
|
const keyboard = {
|
||||||
|
inline_keyboard: [
|
||||||
|
...cryptoOptions.map(row =>
|
||||||
|
row.map(coin => ({
|
||||||
|
text: coin,
|
||||||
|
callback_data: `generate_wallet_${coin.replace(' ', '_')}`
|
||||||
|
}))
|
||||||
|
),
|
||||||
|
[{ text: '« Back', callback_data: 'back_to_balance' }]
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
await bot.editMessageText(
|
||||||
|
'🔐 Select cryptocurrency to generate wallet:',
|
||||||
|
{
|
||||||
|
chat_id: chatId,
|
||||||
|
message_id: callbackQuery.message.message_id,
|
||||||
|
reply_markup: keyboard
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async handleGenerateWallet(callbackQuery) {
|
||||||
|
const chatId = callbackQuery.message.chat.id;
|
||||||
|
const telegramId = callbackQuery.from.id;
|
||||||
|
const walletType = callbackQuery.data.replace('generate_wallet_', '').replace('_', ' ');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const user = await UserService.getUserByTelegramId(telegramId);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
throw new Error('User not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.runAsync('BEGIN TRANSACTION');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Generate new wallets
|
||||||
|
const mnemonic = await WalletGenerator.generateMnemonic();
|
||||||
|
const wallets = await WalletGenerator.generateWallets(mnemonic);
|
||||||
|
const encryptedMnemonic = await WalletGenerator.encryptMnemonic(mnemonic, telegramId);
|
||||||
|
|
||||||
|
// Get the base wallet type (ETH for ERC-20, TRON for TRC-20)
|
||||||
|
const baseType = this.getBaseWalletType(walletType);
|
||||||
|
|
||||||
|
// Get existing wallet of this type
|
||||||
|
const existingWallet = await db.getAsync(
|
||||||
|
'SELECT id, address FROM crypto_wallets WHERE user_id = ? AND wallet_type = ?',
|
||||||
|
[user.id, baseType]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existingWallet) {
|
||||||
|
// Archive the old wallet by adding a suffix to its type
|
||||||
|
const timestamp = Date.now();
|
||||||
|
await db.runAsync(
|
||||||
|
'UPDATE crypto_wallets SET wallet_type = ? WHERE id = ?',
|
||||||
|
[`${baseType}_${timestamp}`, existingWallet.id]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store the new wallet
|
||||||
|
await db.runAsync(
|
||||||
|
`INSERT INTO crypto_wallets (
|
||||||
|
user_id, wallet_type, address, derivation_path, encrypted_mnemonic
|
||||||
|
) VALUES (?, ?, ?, ?, ?)`,
|
||||||
|
[
|
||||||
|
user.id,
|
||||||
|
baseType,
|
||||||
|
wallets[baseType].address,
|
||||||
|
wallets[baseType].path,
|
||||||
|
encryptedMnemonic
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get the appropriate address for the requested wallet type
|
||||||
|
const displayAddress = this.getWalletAddress(wallets, walletType);
|
||||||
|
const network = this.getNetworkName(walletType);
|
||||||
|
|
||||||
|
let message = `✅ New wallet generated successfully!\n\n`;
|
||||||
|
message += `Type: ${walletType}\n`;
|
||||||
|
message += `Network: ${network}\n`;
|
||||||
|
message += `Address: \`${displayAddress}\`\n\n`;
|
||||||
|
|
||||||
|
if (existingWallet) {
|
||||||
|
message += `ℹ️ Your previous wallet has been archived and will remain accessible for existing funds.\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
message += `\n⚠️ Important: Your recovery phrase has been securely stored. Keep your wallet address safe!`;
|
||||||
|
|
||||||
|
await bot.editMessageText(message, {
|
||||||
|
chat_id: chatId,
|
||||||
|
message_id: callbackQuery.message.message_id,
|
||||||
|
parse_mode: 'Markdown',
|
||||||
|
reply_markup: {
|
||||||
|
inline_keyboard: [[
|
||||||
|
{ text: '« Back to Balance', callback_data: 'back_to_balance' }
|
||||||
|
]]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await db.runAsync('COMMIT');
|
||||||
|
} catch (error) {
|
||||||
|
await db.runAsync('ROLLBACK');
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error generating wallet:', error);
|
||||||
|
await bot.editMessageText(
|
||||||
|
'❌ Error generating wallet. Please try again.',
|
||||||
|
{
|
||||||
|
chat_id: chatId,
|
||||||
|
message_id: callbackQuery.message.message_id,
|
||||||
|
reply_markup: {
|
||||||
|
inline_keyboard: [[
|
||||||
|
{ text: '« Back to Balance', callback_data: 'back_to_balance' }
|
||||||
|
]]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async handleTopUpWallet(callbackQuery) {
|
||||||
|
const chatId = callbackQuery.message.chat.id;
|
||||||
|
const telegramId = callbackQuery.from.id;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const user = await UserService.getUserByTelegramId(telegramId);
|
||||||
|
|
||||||
|
// Get crypto wallets
|
||||||
|
const cryptoWallets = await db.allAsync(`
|
||||||
|
SELECT wallet_type, address
|
||||||
|
FROM crypto_wallets
|
||||||
|
WHERE user_id = ?
|
||||||
|
ORDER BY wallet_type
|
||||||
|
`, [user.id]);
|
||||||
|
|
||||||
|
if (cryptoWallets.length === 0) {
|
||||||
|
await bot.editMessageText(
|
||||||
|
'You don\'t have any wallets yet.',
|
||||||
|
{
|
||||||
|
chat_id: chatId,
|
||||||
|
message_id: callbackQuery.message.message_id,
|
||||||
|
reply_markup: {
|
||||||
|
inline_keyboard: [[
|
||||||
|
{ text: '➕ Add Wallet', callback_data: 'add_wallet' }
|
||||||
|
]]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let message = '💰 *Available Wallets:*\n\n';
|
||||||
|
|
||||||
|
const walletService = new WalletService(
|
||||||
|
cryptoWallets.find(w => w.wallet_type === 'BTC')?.address,
|
||||||
|
cryptoWallets.find(w => w.wallet_type === 'LTC')?.address,
|
||||||
|
cryptoWallets.find(w => w.wallet_type === 'TRON')?.address,
|
||||||
|
cryptoWallets.find(w => w.wallet_type === 'ETH')?.address,
|
||||||
|
user.id,
|
||||||
|
Date.now() - 30 * 24 * 60 * 60 * 1000
|
||||||
|
);
|
||||||
|
|
||||||
|
const balances = await walletService.getAllBalances();
|
||||||
|
|
||||||
|
for (const [type, balance] of Object.entries(balances)) {
|
||||||
|
if (cryptoWallets.some(w => w.wallet_type === type.split(' ')[0] ||
|
||||||
|
(type.includes('TRC-20') && w.wallet_type === 'TRON') ||
|
||||||
|
(type.includes('ERC-20') && w.wallet_type === 'ETH'))) {
|
||||||
|
const wallet = cryptoWallets.find(w =>
|
||||||
|
w.wallet_type === type.split(' ')[0] ||
|
||||||
|
(type.includes('TRC-20') && w.wallet_type === 'TRON') ||
|
||||||
|
(type.includes('ERC-20') && w.wallet_type === 'ETH')
|
||||||
|
);
|
||||||
|
message += `🔐 *${type}*\n`;
|
||||||
|
message += `├ Balance: ${balance.amount.toFixed(8)} ${type.split(' ')[0]}\n`;
|
||||||
|
message += `├ Value: $${balance.usdValue.toFixed(2)}\n`;
|
||||||
|
message += `└ Address: \`${wallet.address}\`\n\n`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const keyboard = {
|
||||||
|
inline_keyboard: [
|
||||||
|
[{ text: '« Back', callback_data: 'back_to_balance' }]
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
await bot.editMessageText(message, {
|
||||||
|
chat_id: chatId,
|
||||||
|
message_id: callbackQuery.message.message_id,
|
||||||
|
parse_mode: 'Markdown',
|
||||||
|
reply_markup: keyboard
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in handleTopUpWallet:', error);
|
||||||
|
await bot.sendMessage(chatId, 'Error loading wallets. Please try again.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async handleWalletHistory(callbackQuery) {
|
||||||
|
const chatId = callbackQuery.message.chat.id;
|
||||||
|
const telegramId = callbackQuery.from.id;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const user = UserService.getUserByTelegramId(telegramId);
|
||||||
|
|
||||||
|
const transactions = await db.allAsync(`
|
||||||
|
SELECT type, amount, tx_hash, created_at, wallet_type
|
||||||
|
FROM transactions
|
||||||
|
WHERE user_id = ?
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
LIMIT 10
|
||||||
|
`, [user.id]);
|
||||||
|
|
||||||
|
if (transactions.length === 0) {
|
||||||
|
await bot.editMessageText(
|
||||||
|
'No transactions found.',
|
||||||
|
{
|
||||||
|
chat_id: chatId,
|
||||||
|
message_id: callbackQuery.message.message_id,
|
||||||
|
reply_markup: {
|
||||||
|
inline_keyboard: [[
|
||||||
|
{ text: '« Back', callback_data: 'back_to_balance' }
|
||||||
|
]]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let message = '📊 *Recent Transactions:*\n\n';
|
||||||
|
transactions.forEach(tx => {
|
||||||
|
const date = new Date(tx.created_at).toLocaleString();
|
||||||
|
const symbol = tx.type === 'deposit' ? '➕' : '➖';
|
||||||
|
message += `${symbol} ${tx.amount} ${tx.wallet_type}\n`;
|
||||||
|
message += `🔗 TX: \`${tx.tx_hash}\`\n`;
|
||||||
|
message += `🕒 ${date}\n\n`;
|
||||||
|
});
|
||||||
|
|
||||||
|
await bot.editMessageText(message, {
|
||||||
|
chat_id: chatId,
|
||||||
|
message_id: callbackQuery.message.message_id,
|
||||||
|
parse_mode: 'Markdown',
|
||||||
|
reply_markup: {
|
||||||
|
inline_keyboard: [[
|
||||||
|
{ text: '« Back', callback_data: 'back_to_balance' }
|
||||||
|
]]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in handleWalletHistory:', error);
|
||||||
|
await bot.sendMessage(chatId, 'Error loading transaction history. Please try again.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async handleViewArchivedWallets(callbackQuery) {
|
||||||
|
const chatId = callbackQuery.message.chat.id;
|
||||||
|
const telegramId = callbackQuery.from.id;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const user = await UserService.getUserByTelegramId(telegramId.toString());
|
||||||
|
|
||||||
|
// Get archived wallets and validate timestamps
|
||||||
|
const archivedWallets = await db.allAsync(`
|
||||||
|
SELECT wallet_type, address
|
||||||
|
FROM crypto_wallets
|
||||||
|
WHERE user_id = ? AND wallet_type LIKE '%_%'
|
||||||
|
ORDER BY wallet_type
|
||||||
|
`, [user.id]);
|
||||||
|
|
||||||
|
// Filter out wallets with invalid timestamps
|
||||||
|
const validArchivedWallets = archivedWallets.filter(wallet => {
|
||||||
|
const [, timestamp] = wallet.wallet_type.split('_');
|
||||||
|
const date = new Date(parseInt(timestamp));
|
||||||
|
return !isNaN(date.getTime()); // Check if date is valid
|
||||||
|
});
|
||||||
|
|
||||||
|
if (validArchivedWallets.length === 0) {
|
||||||
|
await bot.editMessageText(
|
||||||
|
'No archived wallets found.',
|
||||||
|
{
|
||||||
|
chat_id: chatId,
|
||||||
|
message_id: callbackQuery.message.message_id,
|
||||||
|
reply_markup: {
|
||||||
|
inline_keyboard: [[
|
||||||
|
{ text: '« Back', callback_data: 'back_to_balance' }
|
||||||
|
]]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Group wallets by base type
|
||||||
|
const groupedWallets = {};
|
||||||
|
let totalUsdValue = 0;
|
||||||
|
|
||||||
|
for (const wallet of validArchivedWallets) {
|
||||||
|
const [baseType, timestamp] = wallet.wallet_type.split('_');
|
||||||
|
if (!groupedWallets[baseType]) {
|
||||||
|
groupedWallets[baseType] = [];
|
||||||
|
}
|
||||||
|
groupedWallets[baseType].push({
|
||||||
|
address: wallet.address,
|
||||||
|
timestamp: parseInt(timestamp)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create wallet service instance
|
||||||
|
const walletService = new WalletService(
|
||||||
|
groupedWallets['BTC']?.[0]?.address,
|
||||||
|
groupedWallets['LTC']?.[0]?.address,
|
||||||
|
groupedWallets['TRON']?.[0]?.address,
|
||||||
|
groupedWallets['ETH']?.[0]?.address,
|
||||||
|
user.id,
|
||||||
|
Date.now() - 30 * 24 * 60 * 60 * 1000
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get all balances
|
||||||
|
const balances = await walletService.getAllBalances();
|
||||||
|
|
||||||
|
let message = '📁 *Archived Wallets:*\n\n';
|
||||||
|
|
||||||
|
// Process each cryptocurrency type
|
||||||
|
for (const baseType of Object.keys(groupedWallets).sort()) {
|
||||||
|
let typeTotal = 0;
|
||||||
|
let typeUsdTotal = 0;
|
||||||
|
|
||||||
|
message += `🔒 *${baseType}*\n`;
|
||||||
|
|
||||||
|
// Sort wallets by timestamp (newest first)
|
||||||
|
const sortedWallets = groupedWallets[baseType].sort((a, b) => b.timestamp - a.timestamp);
|
||||||
|
|
||||||
|
for (const wallet of sortedWallets) {
|
||||||
|
const date = new Date(wallet.timestamp);
|
||||||
|
let balance = 0;
|
||||||
|
let usdValue = 0;
|
||||||
|
|
||||||
|
// Get balance based on wallet type
|
||||||
|
switch (baseType) {
|
||||||
|
case 'BTC':
|
||||||
|
balance = balances.BTC.amount;
|
||||||
|
usdValue = balances.BTC.usdValue;
|
||||||
|
break;
|
||||||
|
case 'LTC':
|
||||||
|
balance = balances.LTC.amount;
|
||||||
|
usdValue = balances.LTC.usdValue;
|
||||||
|
break;
|
||||||
|
case 'ETH':
|
||||||
|
balance = balances.ETH.amount;
|
||||||
|
usdValue = balances.ETH.usdValue;
|
||||||
|
break;
|
||||||
|
case 'TRON':
|
||||||
|
balance = balances['USDT TRC-20'].amount + balances['USDD TRC-20'].amount;
|
||||||
|
usdValue = balances['USDT TRC-20'].usdValue + balances['USDD TRC-20'].usdValue;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
typeTotal += balance;
|
||||||
|
typeUsdTotal += usdValue;
|
||||||
|
|
||||||
|
message += `├ Balance: ${balance.toFixed(8)} ${baseType}\n`;
|
||||||
|
message += `├ Value: $${usdValue.toFixed(2)}\n`;
|
||||||
|
message += `├ Address: \`${wallet.address}\`\n`;
|
||||||
|
message += `└ Archived: ${date.toLocaleDateString()}\n\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
message += `📊 *Total ${baseType}*:\n`;
|
||||||
|
message += `├ Amount: ${typeTotal.toFixed(8)} ${baseType}\n`;
|
||||||
|
message += `└ Value: $${typeUsdTotal.toFixed(2)}\n\n`;
|
||||||
|
|
||||||
|
totalUsdValue += typeUsdTotal;
|
||||||
|
}
|
||||||
|
|
||||||
|
message += `💰 *Total Value of Archived Wallets:* $${totalUsdValue.toFixed(2)}`;
|
||||||
|
|
||||||
|
await bot.editMessageText(message, {
|
||||||
|
chat_id: chatId,
|
||||||
|
message_id: callbackQuery.message.message_id,
|
||||||
|
parse_mode: 'Markdown',
|
||||||
|
reply_markup: {
|
||||||
|
inline_keyboard: [[
|
||||||
|
{ text: '« Back', callback_data: 'back_to_balance' }
|
||||||
|
]]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in handleViewArchivedWallets:', error);
|
||||||
|
await bot.sendMessage(chatId, 'Error loading archived wallets. Please try again.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async handleRefreshBalance(callbackQuery) {
|
||||||
|
const chatId = callbackQuery.message.chat.id;
|
||||||
|
const messageId = callbackQuery.message.message_id;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await bot.editMessageText(
|
||||||
|
'🔄 Refreshing balances...',
|
||||||
|
{
|
||||||
|
chat_id: chatId,
|
||||||
|
message_id: messageId
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Re-fetch and display updated balances
|
||||||
|
await this.showBalance({
|
||||||
|
chat: { id: chatId },
|
||||||
|
from: { id: callbackQuery.from.id }
|
||||||
|
});
|
||||||
|
|
||||||
|
// Delete the "refreshing" message
|
||||||
|
await bot.deleteMessage(chatId, messageId);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in handleRefreshBalance:', error);
|
||||||
|
await bot.editMessageText(
|
||||||
|
'❌ Error refreshing balances. Please try again.',
|
||||||
|
{
|
||||||
|
chat_id: chatId,
|
||||||
|
message_id: messageId,
|
||||||
|
reply_markup: {
|
||||||
|
inline_keyboard: [[
|
||||||
|
{ text: '« Back', callback_data: 'back_to_balance' }
|
||||||
|
]]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async handleBackToBalance(callbackQuery) {
|
||||||
|
await this.showBalance({
|
||||||
|
chat: { id: callbackQuery.message.chat.id },
|
||||||
|
from: { id: callbackQuery.from.id }
|
||||||
|
});
|
||||||
|
await bot.deleteMessage(callbackQuery.message.chat.id, callbackQuery.message.message_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper methods
|
||||||
|
static getBaseWalletType(walletType) {
|
||||||
|
if (walletType.includes('TRC-20')) return 'TRON';
|
||||||
|
if (walletType.includes('ERC-20')) return 'ETH';
|
||||||
|
return walletType;
|
||||||
|
}
|
||||||
|
|
||||||
|
static getWalletAddress(wallets, walletType) {
|
||||||
|
if (walletType.includes('TRC-20')) return wallets.TRON.address;
|
||||||
|
if (walletType.includes('ERC-20')) return wallets.ETH.address;
|
||||||
|
if (walletType === 'BTC') return wallets.BTC.address;
|
||||||
|
if (walletType === 'LTC') return wallets.LTC.address;
|
||||||
|
if (walletType === 'ETH') return wallets.ETH.address;
|
||||||
|
throw new Error('Invalid wallet type');
|
||||||
|
}
|
||||||
|
|
||||||
|
static getNetworkName(walletType) {
|
||||||
|
if (walletType.includes('TRC-20')) return 'Tron Network (TRC-20)';
|
||||||
|
if (walletType.includes('ERC-20')) return 'Ethereum Network (ERC-20)';
|
||||||
|
if (walletType === 'BTC') return 'Bitcoin Network';
|
||||||
|
if (walletType === 'LTC') return 'Litecoin Network';
|
||||||
|
if (walletType === 'ETH') return 'Ethereum Network';
|
||||||
|
return 'Unknown Network';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
import db from '../../../config/database.js';
|
|
||||||
import WalletUtils from '../../../utils/walletUtils.js';
|
|
||||||
import UserService from '../../../services/userService.js';
|
|
||||||
import bot from '../../../context/bot.js';
|
|
||||||
import logger from '../../../utils/logger.js';
|
|
||||||
|
|
||||||
export default class ArchiveHandler {
|
|
||||||
static async handleViewArchivedWallets(callbackQuery) {
|
|
||||||
const chatId = callbackQuery.message.chat.id;
|
|
||||||
const telegramId = callbackQuery.from.id;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const user = await UserService.getUserByTelegramId(telegramId.toString());
|
|
||||||
|
|
||||||
const archivedWallets = await db.allAsync(`
|
|
||||||
SELECT wallet_type, address FROM crypto_wallets
|
|
||||||
WHERE user_id = ? AND wallet_type LIKE '%_%' ORDER BY wallet_type
|
|
||||||
`, [user.id]);
|
|
||||||
|
|
||||||
const validArchivedWallets = archivedWallets.filter(wallet => {
|
|
||||||
const [, timestamp] = wallet.wallet_type.split('_');
|
|
||||||
return !isNaN(new Date(parseInt(timestamp)).getTime());
|
|
||||||
});
|
|
||||||
|
|
||||||
if (validArchivedWallets.length === 0) {
|
|
||||||
await bot.editMessageText('No archived wallets found.', {
|
|
||||||
chat_id: chatId, message_id: callbackQuery.message.message_id,
|
|
||||||
reply_markup: { inline_keyboard: [[{ text: '« Back', callback_data: 'back_to_balance' }]] }
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const groupedWallets = {};
|
|
||||||
for (const wallet of validArchivedWallets) {
|
|
||||||
const [baseType, timestamp] = wallet.wallet_type.split('_');
|
|
||||||
if (!groupedWallets[baseType]) groupedWallets[baseType] = [];
|
|
||||||
groupedWallets[baseType].push({ address: wallet.address, timestamp: parseInt(timestamp) });
|
|
||||||
}
|
|
||||||
|
|
||||||
const walletUtilsInstance = new WalletUtils(
|
|
||||||
groupedWallets['BTC']?.[0]?.address,
|
|
||||||
groupedWallets['LTC']?.[0]?.address,
|
|
||||||
groupedWallets['ETH']?.[0]?.address || groupedWallets['USDT']?.[0]?.address || groupedWallets['USDC']?.[0]?.address,
|
|
||||||
user.id, Date.now() - 30 * 24 * 60 * 60 * 1000
|
|
||||||
);
|
|
||||||
|
|
||||||
const balances = await walletUtilsInstance.getAllBalances();
|
|
||||||
let message = '📁 *Archived Wallets:*\n\n';
|
|
||||||
let totalUsdValue = 0;
|
|
||||||
|
|
||||||
for (const baseType of Object.keys(groupedWallets).sort()) {
|
|
||||||
let typeTotal = 0;
|
|
||||||
let typeUsdTotal = 0;
|
|
||||||
message += `🔒 *${baseType}*\n`;
|
|
||||||
|
|
||||||
const sortedWallets = groupedWallets[baseType].sort((a, b) => b.timestamp - a.timestamp);
|
|
||||||
for (const wallet of sortedWallets) {
|
|
||||||
const balance = balances[baseType]?.amount || 0;
|
|
||||||
const usdValue = balances[baseType]?.usdValue || 0;
|
|
||||||
typeTotal += balance;
|
|
||||||
typeUsdTotal += usdValue;
|
|
||||||
const date = new Date(wallet.timestamp);
|
|
||||||
|
|
||||||
message += `├ Balance: ${balance.toFixed(8)} ${baseType}\n`;
|
|
||||||
message += `├ Value: $${usdValue.toFixed(2)}\n`;
|
|
||||||
message += `├ Address: \`${wallet.address}\`\n`;
|
|
||||||
message += `└ Archived: ${date.toLocaleDateString()}\n\n`;
|
|
||||||
}
|
|
||||||
|
|
||||||
message += `📊 *Total ${baseType}*:\n`;
|
|
||||||
message += `├ Amount: ${typeTotal.toFixed(8)} ${baseType}\n`;
|
|
||||||
message += `└ Value: $${typeUsdTotal.toFixed(2)}\n\n`;
|
|
||||||
totalUsdValue += typeUsdTotal;
|
|
||||||
}
|
|
||||||
|
|
||||||
message += `💰 *Total Value of Archived Wallets:* $${totalUsdValue.toFixed(2)}`;
|
|
||||||
|
|
||||||
await bot.editMessageText(message, {
|
|
||||||
chat_id: chatId, message_id: callbackQuery.message.message_id,
|
|
||||||
parse_mode: 'Markdown',
|
|
||||||
reply_markup: { inline_keyboard: [[{ text: '« Back', callback_data: 'back_to_balance' }]] }
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
logger.error({ err: error }, 'Error in handleViewArchivedWallets');
|
|
||||||
await bot.sendMessage(chatId, 'Error loading archived wallets. Please try again.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
import db from '../../../config/database.js';
|
|
||||||
import WalletUtils from '../../../utils/walletUtils.js';
|
|
||||||
import UserService from '../../../services/userService.js';
|
|
||||||
import WalletService from '../../../services/walletService.js';
|
|
||||||
import bot from '../../../context/bot.js';
|
|
||||||
import logger from '../../../utils/logger.js';
|
|
||||||
|
|
||||||
export default class BalanceHandler {
|
|
||||||
static async showBalance(msg) {
|
|
||||||
const chatId = msg.chat.id;
|
|
||||||
const telegramId = msg.from.id;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const user = await UserService.getUserByTelegramId(telegramId.toString());
|
|
||||||
if (!user) {
|
|
||||||
await bot.sendMessage(chatId, 'Profile not found. Please use /start to create one.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await UserService.recalculateUserBalanceByTelegramId(telegramId);
|
|
||||||
const updatedUser = await UserService.getUserByTelegramId(telegramId.toString());
|
|
||||||
|
|
||||||
const cryptoWallets = await db.allAsync(`
|
|
||||||
SELECT wallet_type, address, balance
|
|
||||||
FROM crypto_wallets WHERE user_id = ?
|
|
||||||
ORDER BY wallet_type
|
|
||||||
`, [updatedUser.id]);
|
|
||||||
|
|
||||||
let message = '💰 *Your Active Wallets:*\n\n';
|
|
||||||
|
|
||||||
if (cryptoWallets.length > 0) {
|
|
||||||
const walletUtilsInstance = new WalletUtils(
|
|
||||||
cryptoWallets.find(w => w.wallet_type === 'BTC')?.address,
|
|
||||||
cryptoWallets.find(w => w.wallet_type === 'LTC')?.address,
|
|
||||||
cryptoWallets.find(w => w.wallet_type === 'ETH')?.address,
|
|
||||||
cryptoWallets.find(w => w.wallet_type === 'USDT')?.address,
|
|
||||||
cryptoWallets.find(w => w.wallet_type === 'USDC')?.address,
|
|
||||||
updatedUser.id
|
|
||||||
);
|
|
||||||
|
|
||||||
const balances = await walletUtilsInstance.getAllBalancesFromDB();
|
|
||||||
let totalUsdValue = 0;
|
|
||||||
|
|
||||||
for (const [type, balance] of Object.entries(balances)) {
|
|
||||||
const wallet = cryptoWallets.find(w => w.wallet_type === type.split(' ')[0]);
|
|
||||||
if (wallet) {
|
|
||||||
message += `🔐 *${type}*\n`;
|
|
||||||
message += `├ Balance: ${balance.amount.toFixed(8)} ${type.split(' ')[0]}\n`;
|
|
||||||
message += `├ Value: $${balance.usdValue.toFixed(2)}\n`;
|
|
||||||
message += `└ Address: \`${wallet.address}\`\n\n`;
|
|
||||||
totalUsdValue += balance.usdValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
message += `📊 *Total Crypto Balance:* $${totalUsdValue.toFixed(2)}\n`;
|
|
||||||
message += `🎁 *Bonus Balance:* $${updatedUser.bonus_balance.toFixed(2)}\n`;
|
|
||||||
const availableBalance = updatedUser.bonus_balance + (updatedUser.total_balance || 0);
|
|
||||||
message += `💰 *Available Balance:* $${availableBalance.toFixed(2)}\n`;
|
|
||||||
} else {
|
|
||||||
message = 'You don\'t have any active wallets yet.';
|
|
||||||
}
|
|
||||||
|
|
||||||
const archivedCount = await WalletService.getArchivedWalletsCount(updatedUser);
|
|
||||||
const keyboard = {
|
|
||||||
inline_keyboard: [
|
|
||||||
[
|
|
||||||
{ text: '➕ Add Crypto Wallet', callback_data: 'add_wallet' },
|
|
||||||
{ text: '💸 Top Up', callback_data: 'top_up_wallet' }
|
|
||||||
],
|
|
||||||
[{ text: '🔄 Refresh Balance', callback_data: 'refresh_balance' }]
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
if (archivedCount > 0) {
|
|
||||||
keyboard.inline_keyboard.splice(2, 0, [
|
|
||||||
{ text: `📁 Archived Wallets (${archivedCount})`, callback_data: 'view_archived_wallets' }
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
keyboard.inline_keyboard.splice(3, 0, [
|
|
||||||
{ text: '📊 Transaction History', callback_data: 'view_transaction_history_0' }
|
|
||||||
]);
|
|
||||||
|
|
||||||
await bot.sendMessage(chatId, message, { reply_markup: keyboard, parse_mode: 'Markdown' });
|
|
||||||
} catch (error) {
|
|
||||||
logger.error({ err: error }, 'Error in showBalance');
|
|
||||||
await bot.sendMessage(chatId, 'Error loading balance. Please try again.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static async handleBackToBalance(callbackQuery) {
|
|
||||||
await this.showBalance({
|
|
||||||
chat: { id: callbackQuery.message.chat.id },
|
|
||||||
from: { id: callbackQuery.from.id }
|
|
||||||
});
|
|
||||||
await bot.deleteMessage(callbackQuery.message.chat.id, callbackQuery.message.message_id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
import db from '../../../config/database.js';
|
|
||||||
import WalletService from '../../../services/walletService.js';
|
|
||||||
import Validators from '../../../utils/validators.js';
|
|
||||||
import bot from '../../../context/bot.js';
|
|
||||||
import UserService from '../../../services/userService.js';
|
|
||||||
import logger from '../../../utils/logger.js';
|
|
||||||
|
|
||||||
export default class CreateHandler {
|
|
||||||
static async handleAddWallet(callbackQuery) {
|
|
||||||
const chatId = callbackQuery.message.chat.id;
|
|
||||||
const cryptoOptions = [['BTC', 'ETH', 'LTC'], ['USDT', 'USDC']];
|
|
||||||
|
|
||||||
const keyboard = {
|
|
||||||
inline_keyboard: [
|
|
||||||
...cryptoOptions.map(row =>
|
|
||||||
row.map(coin => ({
|
|
||||||
text: coin,
|
|
||||||
callback_data: `generate_wallet_${coin.replace(' ', '_')}`
|
|
||||||
}))
|
|
||||||
),
|
|
||||||
[{ text: '« Back', callback_data: 'back_to_balance' }]
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
await bot.editMessageText('🔐 Select cryptocurrency to generate wallet:', {
|
|
||||||
chat_id: chatId, message_id: callbackQuery.message.message_id,
|
|
||||||
reply_markup: keyboard
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
static async handleGenerateWallet(callbackQuery) {
|
|
||||||
const chatId = callbackQuery.message.chat.id;
|
|
||||||
const telegramId = callbackQuery.from.id;
|
|
||||||
const walletType = callbackQuery.data.replace('generate_wallet_', '').replace('_', ' ');
|
|
||||||
|
|
||||||
if (!Validators.isValidWalletType(walletType)) {
|
|
||||||
await bot.sendMessage(chatId, 'Invalid wallet type.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const user = await UserService.getUserByTelegramId(telegramId);
|
|
||||||
if (!user) throw new Error('User not found');
|
|
||||||
|
|
||||||
await db.runAsync('BEGIN TRANSACTION');
|
|
||||||
|
|
||||||
try {
|
|
||||||
const existingWallet = await db.getAsync(
|
|
||||||
'SELECT id, address FROM crypto_wallets WHERE user_id = ? AND wallet_type = ?',
|
|
||||||
[user.id, walletType]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (existingWallet) {
|
|
||||||
const timestamp = Date.now();
|
|
||||||
await db.runAsync(
|
|
||||||
'UPDATE crypto_wallets SET wallet_type = ? WHERE id = ?',
|
|
||||||
[`${walletType}_${timestamp}`, existingWallet.id]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const walletResult = await WalletService.createWallet(user.id, walletType);
|
|
||||||
if (!walletResult?.address) throw new Error('Failed to generate wallet address');
|
|
||||||
|
|
||||||
const network = WalletService.getNetworkName
|
|
||||||
? WalletService.getNetworkName(walletType)
|
|
||||||
: WalletUtils.getNetworkName(walletType);
|
|
||||||
|
|
||||||
let message = `✅ New wallet generated successfully!\n\n`;
|
|
||||||
message += `Type: ${walletType}\nNetwork: ${network}\n`;
|
|
||||||
message += `Address: \`${walletResult.address}\`\n\n`;
|
|
||||||
|
|
||||||
if (existingWallet) {
|
|
||||||
message += `ℹ️ Your previous wallet has been archived.\n`;
|
|
||||||
}
|
|
||||||
message += `\n⚠️ Important: Your recovery phrase has been securely stored.`;
|
|
||||||
|
|
||||||
await bot.editMessageText(message, {
|
|
||||||
chat_id: chatId, message_id: callbackQuery.message.message_id,
|
|
||||||
parse_mode: 'Markdown',
|
|
||||||
reply_markup: { inline_keyboard: [[{ text: '« Back to Balance', callback_data: 'back_to_balance' }]] }
|
|
||||||
});
|
|
||||||
|
|
||||||
await db.runAsync('COMMIT');
|
|
||||||
} catch (error) {
|
|
||||||
await db.runAsync('ROLLBACK');
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logger.error({ err: error }, 'Error generating wallet');
|
|
||||||
await bot.editMessageText('❌ Error generating wallet. Please try again.', {
|
|
||||||
chat_id: chatId, message_id: callbackQuery.message.message_id,
|
|
||||||
reply_markup: { inline_keyboard: [[{ text: '« Back to Balance', callback_data: 'back_to_balance' }]] }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
import WalletUtils from '../../../utils/walletUtils.js';
|
|
||||||
|
|
||||||
export default class WalletHelpers {
|
|
||||||
static getNetworkName(walletType) {
|
|
||||||
if (walletType.includes('USDT')) return 'Ethereum Network (ERC-20)';
|
|
||||||
if (walletType.includes('USDC')) return 'Ethereum Network (ERC-20)';
|
|
||||||
if (walletType === 'BTC') return 'Bitcoin Network';
|
|
||||||
if (walletType === 'LTC') return 'Litecoin Network';
|
|
||||||
if (walletType === 'ETH') return 'Ethereum Network';
|
|
||||||
return 'Unknown Network';
|
|
||||||
}
|
|
||||||
|
|
||||||
static getWalletAddress(wallets, walletType) {
|
|
||||||
if (walletType.includes('ERC-20')) return wallets.ETH.address;
|
|
||||||
if (walletType === 'BTC') return wallets.BTC.address;
|
|
||||||
if (walletType === 'LTC') return wallets.LTC.address;
|
|
||||||
if (walletType === 'ETH') return wallets.ETH.address;
|
|
||||||
throw new Error('Invalid wallet type');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
import db from '../../../config/database.js';
|
|
||||||
import UserService from '../../../services/userService.js';
|
|
||||||
import bot from '../../../context/bot.js';
|
|
||||||
import logger from '../../../utils/logger.js';
|
|
||||||
|
|
||||||
export default class HistoryHandler {
|
|
||||||
static async handleTransactionHistory(callbackQuery, page = 0) {
|
|
||||||
const chatId = callbackQuery.message.chat.id;
|
|
||||||
const telegramId = callbackQuery.from.id;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const user = await UserService.getUserByTelegramId(telegramId.toString());
|
|
||||||
if (!user) {
|
|
||||||
await bot.sendMessage(chatId, 'Profile not found. Please use /start to create one.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const limit = 10;
|
|
||||||
const offset = page * limit;
|
|
||||||
const transactions = await db.allAsync(`
|
|
||||||
SELECT amount, tx_hash, created_at, wallet_type
|
|
||||||
FROM transactions WHERE user_id = ?
|
|
||||||
ORDER BY created_at DESC LIMIT ? OFFSET ?
|
|
||||||
`, [user.id, limit, offset]);
|
|
||||||
|
|
||||||
let message = '';
|
|
||||||
|
|
||||||
if (transactions.length > 0) {
|
|
||||||
message = '📊 *Transaction History:*\n\n';
|
|
||||||
transactions.forEach(tx => {
|
|
||||||
const date = new Date(tx.created_at).toLocaleString();
|
|
||||||
message += `💰 Amount: ${tx.amount}\n`;
|
|
||||||
message += `🔗 TX Hash: \`${tx.tx_hash}\`\n`;
|
|
||||||
message += `🕒 Date: ${date}\n`;
|
|
||||||
message += `💼 Wallet Type: ${tx.wallet_type}\n\n`;
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
message = '📊 *Transaction History:*\n\nNo transactions found.';
|
|
||||||
}
|
|
||||||
|
|
||||||
const keyboard = { inline_keyboard: [[{ text: '« Back', callback_data: 'back_to_balance' }]] };
|
|
||||||
|
|
||||||
if (page > 0) {
|
|
||||||
keyboard.inline_keyboard.unshift([
|
|
||||||
{ text: '⬅️ Previous', callback_data: `view_transaction_history_${page - 1}` }
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
const nextTransactions = await db.allAsync(`
|
|
||||||
SELECT amount FROM transactions WHERE user_id = ?
|
|
||||||
ORDER BY created_at DESC LIMIT ? OFFSET ?
|
|
||||||
`, [user.id, limit, offset + limit]);
|
|
||||||
|
|
||||||
if (nextTransactions.length > 0) {
|
|
||||||
keyboard.inline_keyboard.push([
|
|
||||||
{ text: '➡️ Next', callback_data: `view_transaction_history_${page + 1}` }
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
await bot.editMessageText(message, {
|
|
||||||
chat_id: chatId, message_id: callbackQuery.message.message_id,
|
|
||||||
parse_mode: 'Markdown', reply_markup: keyboard
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
logger.error({ err: error }, 'Error in handleTransactionHistory');
|
|
||||||
await bot.sendMessage(chatId, 'Error loading transaction history. Please try again.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static async handleWalletHistory(callbackQuery) {
|
|
||||||
const chatId = callbackQuery.message.chat.id;
|
|
||||||
const telegramId = callbackQuery.from.id;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const user = await UserService.getUserByTelegramId(telegramId);
|
|
||||||
const transactions = await db.allAsync(`
|
|
||||||
SELECT type, amount, tx_hash, created_at, wallet_type
|
|
||||||
FROM transactions WHERE user_id = ?
|
|
||||||
ORDER BY created_at DESC LIMIT 10
|
|
||||||
`, [user.id]);
|
|
||||||
|
|
||||||
if (transactions.length === 0) {
|
|
||||||
await bot.editMessageText('No transactions found.', {
|
|
||||||
chat_id: chatId, message_id: callbackQuery.message.message_id,
|
|
||||||
reply_markup: { inline_keyboard: [[{ text: '« Back', callback_data: 'back_to_balance' }]] }
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let message = '📊 *Recent Transactions:*\n\n';
|
|
||||||
transactions.forEach(tx => {
|
|
||||||
const date = new Date(tx.created_at).toLocaleString();
|
|
||||||
const symbol = tx.type === 'deposit' ? '➕' : '➖';
|
|
||||||
message += `${symbol} ${tx.amount} ${tx.wallet_type}\n`;
|
|
||||||
message += `🔗 TX: \`${tx.tx_hash}\`\n`;
|
|
||||||
message += `🕒 ${date}\n\n`;
|
|
||||||
});
|
|
||||||
|
|
||||||
await bot.editMessageText(message, {
|
|
||||||
chat_id: chatId, message_id: callbackQuery.message.message_id,
|
|
||||||
parse_mode: 'Markdown',
|
|
||||||
reply_markup: { inline_keyboard: [[{ text: '« Back', callback_data: 'back_to_balance' }]] }
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
logger.error({ err: error }, 'Error in handleWalletHistory');
|
|
||||||
await bot.sendMessage(chatId, 'Error loading transaction history. Please try again.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import BalanceHandler from './balanceHandler.js';
|
|
||||||
import HistoryHandler from './historyHandler.js';
|
|
||||||
import RefreshHandler from './refreshHandler.js';
|
|
||||||
import CreateHandler from './createHandler.js';
|
|
||||||
import TopUpHandler from './topUpHandler.js';
|
|
||||||
import ArchiveHandler from './archiveHandler.js';
|
|
||||||
import WalletHelpers from './helpers.js';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
showBalance: BalanceHandler.showBalance,
|
|
||||||
handleTransactionHistory: HistoryHandler.handleTransactionHistory,
|
|
||||||
handleWalletHistory: HistoryHandler.handleWalletHistory,
|
|
||||||
handleRefreshBalance: RefreshHandler.handleRefreshBalance,
|
|
||||||
handleAddWallet: CreateHandler.handleAddWallet,
|
|
||||||
handleGenerateWallet: CreateHandler.handleGenerateWallet,
|
|
||||||
handleTopUpWallet: TopUpHandler.handleTopUpWallet,
|
|
||||||
handleViewArchivedWallets: ArchiveHandler.handleViewArchivedWallets,
|
|
||||||
handleBackToBalance: BalanceHandler.handleBackToBalance,
|
|
||||||
getNetworkName: WalletHelpers.getNetworkName,
|
|
||||||
getWalletAddress: WalletHelpers.getWalletAddress,
|
|
||||||
};
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
import db from '../../../config/database.js';
|
|
||||||
import WalletUtils from '../../../utils/walletUtils.js';
|
|
||||||
import UserService from '../../../services/userService.js';
|
|
||||||
import bot from '../../../context/bot.js';
|
|
||||||
import logger from '../../../utils/logger.js';
|
|
||||||
|
|
||||||
export default class RefreshHandler {
|
|
||||||
static async handleRefreshBalance(callbackQuery) {
|
|
||||||
const chatId = callbackQuery.message.chat.id;
|
|
||||||
const messageId = callbackQuery.message.message_id;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await bot.answerCallbackQuery(callbackQuery.id, { text: '🔄 Refreshing balances...' });
|
|
||||||
|
|
||||||
const user = await UserService.getUserByTelegramId(callbackQuery.from.id.toString());
|
|
||||||
if (!user) {
|
|
||||||
await bot.sendMessage(chatId, 'Profile not found. Please use /start to create one.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const activeWallets = await db.allAsync(`
|
|
||||||
SELECT wallet_type, address FROM crypto_wallets
|
|
||||||
WHERE user_id = ? AND wallet_type NOT LIKE '%#_%' ESCAPE '#'
|
|
||||||
`, [user.id]);
|
|
||||||
|
|
||||||
const walletAddresses = {
|
|
||||||
btc: activeWallets.find(w => w.wallet_type === 'BTC')?.address || null,
|
|
||||||
ltc: activeWallets.find(w => w.wallet_type === 'LTC')?.address || null,
|
|
||||||
eth: activeWallets.find(w => w.wallet_type === 'ETH')?.address || null,
|
|
||||||
usdt: activeWallets.find(w => w.wallet_type === 'USDT')?.address || null,
|
|
||||||
usdc: activeWallets.find(w => w.wallet_type === 'USDC')?.address || null,
|
|
||||||
};
|
|
||||||
|
|
||||||
const walletUtilsInstance = new WalletUtils(
|
|
||||||
walletAddresses.btc, walletAddresses.ltc, walletAddresses.eth,
|
|
||||||
walletAddresses.usdt, walletAddresses.usdc,
|
|
||||||
user.id, Date.now() - 30 * 24 * 60 * 60 * 1000
|
|
||||||
);
|
|
||||||
|
|
||||||
const balances = await walletUtilsInstance.getAllBalancesExt();
|
|
||||||
|
|
||||||
const walletTypeMap = { BTC: 'btc', LTC: 'ltc', ETH: 'eth', USDT: 'usdt', USDC: 'usdc' };
|
|
||||||
|
|
||||||
for (const [type, balance] of Object.entries(balances)) {
|
|
||||||
const address = walletAddresses[walletTypeMap[type]];
|
|
||||||
if (!address) continue;
|
|
||||||
|
|
||||||
const currentBalance = await db.getAsync(
|
|
||||||
'SELECT balance FROM crypto_wallets WHERE user_id = ? AND address = ?',
|
|
||||||
[user.id, address]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (currentBalance?.balance !== balance.amount) {
|
|
||||||
await db.runAsync(
|
|
||||||
'UPDATE crypto_wallets SET balance = ? WHERE user_id = ? AND address = ?',
|
|
||||||
[balance.amount, user.id, address]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await UserService.recalculateUserBalanceByTelegramId(callbackQuery.from.id);
|
|
||||||
|
|
||||||
const BalanceHandler = (await import('./balanceHandler.js')).default;
|
|
||||||
await BalanceHandler.showBalance({
|
|
||||||
chat: { id: chatId }, from: { id: callbackQuery.from.id }
|
|
||||||
});
|
|
||||||
|
|
||||||
await bot.deleteMessage(chatId, messageId);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error({ err: error }, 'Error in handleRefreshBalance');
|
|
||||||
await bot.answerCallbackQuery(callbackQuery.id, { text: '❌ Error refreshing balances.' });
|
|
||||||
await bot.sendMessage(chatId, '❌ Error refreshing balances. Please try again.', {
|
|
||||||
reply_markup: { inline_keyboard: [[{ text: '« Back', callback_data: 'back_to_balance' }]] }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
import db from '../../../config/database.js';
|
|
||||||
import WalletUtils from '../../../utils/walletUtils.js';
|
|
||||||
import UserService from '../../../services/userService.js';
|
|
||||||
import bot from '../../../context/bot.js';
|
|
||||||
import logger from '../../../utils/logger.js';
|
|
||||||
|
|
||||||
export default class TopUpHandler {
|
|
||||||
static async handleTopUpWallet(callbackQuery) {
|
|
||||||
const chatId = callbackQuery.message.chat.id;
|
|
||||||
const telegramId = callbackQuery.from.id;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const user = await UserService.getUserByTelegramId(telegramId);
|
|
||||||
const cryptoWallets = await db.allAsync(`
|
|
||||||
SELECT wallet_type, address FROM crypto_wallets
|
|
||||||
WHERE user_id = ? ORDER BY wallet_type
|
|
||||||
`, [user.id]);
|
|
||||||
|
|
||||||
if (cryptoWallets.length === 0) {
|
|
||||||
await bot.editMessageText('You don\'t have any wallets yet.', {
|
|
||||||
chat_id: chatId, message_id: callbackQuery.message.message_id,
|
|
||||||
reply_markup: { inline_keyboard: [[{ text: '➕ Add Wallet', callback_data: 'add_wallet' }]] }
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let message = '💰 *Available wallets for replenishment.* Click on a wallet to copy the address:\n\n';
|
|
||||||
|
|
||||||
const walletUtilsInstance = new WalletUtils(
|
|
||||||
cryptoWallets.find(w => w.wallet_type === 'BTC')?.address,
|
|
||||||
cryptoWallets.find(w => w.wallet_type === 'LTC')?.address,
|
|
||||||
cryptoWallets.find(w => w.wallet_type === 'ETH')?.address,
|
|
||||||
user.id,
|
|
||||||
Date.now() - 30 * 24 * 60 * 60 * 1000
|
|
||||||
);
|
|
||||||
|
|
||||||
const balances = await walletUtilsInstance.getAllBalances();
|
|
||||||
|
|
||||||
for (const [type, balance] of Object.entries(balances)) {
|
|
||||||
const wallet = cryptoWallets.find(w =>
|
|
||||||
w.wallet_type === type.split(' ')[0] ||
|
|
||||||
(type.includes('ERC-20') && w.wallet_type === 'ETH')
|
|
||||||
);
|
|
||||||
if (wallet) {
|
|
||||||
message += `🔐 *${type}*\n`;
|
|
||||||
message += `├ Balance: ${balance.amount.toFixed(8)} ${type.split(' ')[0]}\n`;
|
|
||||||
message += `├ Value: $${balance.usdValue.toFixed(2)}\n`;
|
|
||||||
message += `└ Address: \`${wallet.address}\`\n\n`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await bot.editMessageText(message, {
|
|
||||||
chat_id: chatId, message_id: callbackQuery.message.message_id,
|
|
||||||
parse_mode: 'Markdown',
|
|
||||||
reply_markup: { inline_keyboard: [[{ text: '« Back', callback_data: 'back_to_balance' }]] }
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
logger.error({ err: error }, 'Error in handleTopUpWallet');
|
|
||||||
await bot.sendMessage(chatId, 'Error loading wallets. Please try again.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
339
src/index.js
339
src/index.js
@@ -1,28 +1,33 @@
|
|||||||
import 'dotenv/config';
|
import adminUserHandler from './handlers/adminHandlers/adminUserHandler.js';
|
||||||
import { runMigrations, cleanUpInvalidForeignKeys } from './migrations/runner.js';
|
|
||||||
import './router/routes.js';
|
|
||||||
import bot from './context/bot.js';
|
|
||||||
import ErrorHandler from './utils/errorHandler.js';
|
import ErrorHandler from './utils/errorHandler.js';
|
||||||
import logger from './utils/logger.js';
|
import bot from "./context/bot.js";
|
||||||
import userHandler from './handlers/userHandlers/userHandler.js';
|
import userHandler from "./handlers/userHandlers/userHandler.js";
|
||||||
import adminHandler from './handlers/adminHandlers/adminHandler.js';
|
import userPurchaseHandler from "./handlers/userHandlers/userPurchaseHandler.js";
|
||||||
import callbackRouter from './router/callbackRouter.js';
|
import userLocationHandler from "./handlers/userHandlers/userLocationHandler.js";
|
||||||
import messageRouter from './router/messageRouter.js';
|
import userProductHandler from "./handlers/userHandlers/userProductHandler.js";
|
||||||
|
import userWalletsHandler from "./handlers/userHandlers/userWalletsHandler.js";
|
||||||
import { initStates } from './services/stateService.js';
|
import adminHandler from "./handlers/adminHandlers/adminHandler.js";
|
||||||
|
import adminUserLocationHandler from "./handlers/adminHandlers/adminUserLocationHandler.js";
|
||||||
await runMigrations();
|
import adminDumpHandler from "./handlers/adminHandlers/adminDumpHandler.js";
|
||||||
await cleanUpInvalidForeignKeys();
|
import adminLocationHandler from "./handlers/adminHandlers/adminLocationHandler.js";
|
||||||
await initStates();
|
import adminProductHandler from "./handlers/adminHandlers/adminProductHandler.js";
|
||||||
|
|
||||||
|
// Debug logging function
|
||||||
const logDebug = (action, functionName) => {
|
const logDebug = (action, functionName) => {
|
||||||
logger.debug({ action, functionName }, 'Button Press');
|
console.log(`[DEBUG] Button Press: ${action}`);
|
||||||
|
console.log(`[DEBUG] Calling Function: ${functionName}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Start command - Create user profile
|
||||||
bot.onText(/\/start/, async (msg) => {
|
bot.onText(/\/start/, async (msg) => {
|
||||||
logDebug('/start', 'handleStart');
|
logDebug('/start', 'handleStart');
|
||||||
|
|
||||||
const canUse = await userHandler.canUseBot(msg);
|
const canUse = await userHandler.canUseBot(msg);
|
||||||
if (!canUse) return;
|
|
||||||
|
if (!canUse) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await userHandler.handleStart(msg);
|
await userHandler.handleStart(msg);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -30,6 +35,7 @@ bot.onText(/\/start/, async (msg) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Admin command
|
||||||
bot.onText(/\/admin/, async (msg) => {
|
bot.onText(/\/admin/, async (msg) => {
|
||||||
logDebug('/admin', 'handleAdminCommand');
|
logDebug('/admin', 'handleAdminCommand');
|
||||||
try {
|
try {
|
||||||
@@ -39,38 +45,317 @@ bot.onText(/\/admin/, async (msg) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Handle user menu buttons
|
||||||
bot.on('message', async (msg) => {
|
bot.on('message', async (msg) => {
|
||||||
if (msg.text?.toLowerCase() === '/start') return;
|
if (msg.text && msg.text.toLowerCase() === '/start') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const canUse = await userHandler.canUseBot(msg);
|
const canUse = await userHandler.canUseBot(msg);
|
||||||
if (!canUse) return;
|
|
||||||
|
if (!canUse) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await messageRouter.dispatch(msg);
|
// Check for admin location input
|
||||||
|
if (await adminLocationHandler.handleLocationInput(msg)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for admin category input
|
||||||
|
if (await adminProductHandler.handleCategoryInput(msg)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for admin subcategory input
|
||||||
|
if (await adminProductHandler.handleSubcategoryInput(msg)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for product import
|
||||||
|
if (await adminProductHandler.handleProductImport(msg)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for product edition
|
||||||
|
if (await adminProductHandler.handleProductEditImport(msg)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for database dump import
|
||||||
|
if (await adminDumpHandler.handleDumpImport(msg)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for bonus balance input
|
||||||
|
if (await adminUserHandler.handleBonusBalanceInput(msg)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for category update input
|
||||||
|
if (await adminProductHandler.handleCategoryUpdate(msg)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logDebug(msg.text, 'handleMessage');
|
||||||
|
|
||||||
|
switch (msg.text) {
|
||||||
|
case '📦 Products':
|
||||||
|
await userProductHandler.showProducts(msg);
|
||||||
|
break;
|
||||||
|
case '👤 Profile':
|
||||||
|
await userHandler.showProfile(msg);
|
||||||
|
break;
|
||||||
|
case '💰 Wallets':
|
||||||
|
await userWalletsHandler.showBalance(msg);
|
||||||
|
break;
|
||||||
|
case '🛍 Purchases':
|
||||||
|
await userPurchaseHandler.showPurchases(msg);
|
||||||
|
break;
|
||||||
|
case '📦 Manage Products':
|
||||||
|
if (adminHandler.isAdmin(msg.from.id)) {
|
||||||
|
await adminProductHandler.handleProductManagement(msg);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case '👥 Manage Users':
|
||||||
|
if (adminHandler.isAdmin(msg.from.id)) {
|
||||||
|
await adminUserHandler.handleUserList(msg);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case '📍 Manage Locations':
|
||||||
|
if (adminHandler.isAdmin(msg.from.id)) {
|
||||||
|
await adminLocationHandler.handleViewLocations(msg);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case '💾 Database Backup':
|
||||||
|
if (adminHandler.isAdmin(msg.from.id)) {
|
||||||
|
await adminDumpHandler.handleDump(msg);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await ErrorHandler.handleError(bot, msg.chat.id, error, 'message handler');
|
await ErrorHandler.handleError(bot, msg.chat.id, error, 'message handler');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Handle callback queries
|
||||||
bot.on('callback_query', async (callbackQuery) => {
|
bot.on('callback_query', async (callbackQuery) => {
|
||||||
|
const action = callbackQuery.data;
|
||||||
|
const msg = callbackQuery.message;
|
||||||
|
|
||||||
const canUse = await userHandler.canUseBot(callbackQuery);
|
const canUse = await userHandler.canUseBot(callbackQuery);
|
||||||
|
|
||||||
if (!canUse) {
|
if (!canUse) {
|
||||||
await bot.answerCallbackQuery(callbackQuery.id);
|
await bot.answerCallbackQuery(callbackQuery.id);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await callbackRouter.dispatch(callbackQuery);
|
// Profile and location management
|
||||||
|
if (action === 'set_location') {
|
||||||
|
logDebug(action, 'handleSetLocation');
|
||||||
|
await userLocationHandler.handleSetLocation(callbackQuery);
|
||||||
|
} else if (action.startsWith('set_country_')) {
|
||||||
|
logDebug(action, 'handleSetCountry');
|
||||||
|
await userLocationHandler.handleSetCountry(callbackQuery);
|
||||||
|
} else if (action.startsWith('set_city_')) {
|
||||||
|
logDebug(action, 'handleSetCity');
|
||||||
|
await userLocationHandler.handleSetCity(callbackQuery);
|
||||||
|
} else if (action.startsWith('set_district_')) {
|
||||||
|
logDebug(action, 'handleSetDistrict');
|
||||||
|
await userLocationHandler.handleSetDistrict(callbackQuery);
|
||||||
|
} else if (action === 'back_to_profile') {
|
||||||
|
logDebug(action, 'handleBackToProfile');
|
||||||
|
await userHandler.handleBackToProfile(callbackQuery);
|
||||||
|
} else if (action === 'back_to_balance') {
|
||||||
|
logDebug(action, 'handleBackToBalance');
|
||||||
|
await userWalletsHandler.handleBackToBalance(callbackQuery);
|
||||||
|
}
|
||||||
|
// Wallet management
|
||||||
|
else if (action === 'add_wallet') {
|
||||||
|
logDebug(action, 'handleAddWallet');
|
||||||
|
await userWalletsHandler.handleAddWallet(callbackQuery);
|
||||||
|
} else if (action === 'top_up_wallet') {
|
||||||
|
logDebug(action, 'handleTopUpWallet');
|
||||||
|
await userWalletsHandler.handleTopUpWallet(callbackQuery);
|
||||||
|
} else if (action === 'wallet_history') {
|
||||||
|
logDebug(action, 'handleWalletHistory');
|
||||||
|
await userWalletsHandler.handleWalletHistory(callbackQuery);
|
||||||
|
} else if (action === 'view_archived_wallets') {
|
||||||
|
logDebug(action, 'handleViewArchivedWallets');
|
||||||
|
await userWalletsHandler.handleViewArchivedWallets(callbackQuery);
|
||||||
|
} else if (action === 'refresh_balance') {
|
||||||
|
logDebug(action, 'handleRefreshBalance');
|
||||||
|
await userWalletsHandler.handleRefreshBalance(callbackQuery);
|
||||||
|
}
|
||||||
|
// Wallet generation
|
||||||
|
else if (action.startsWith('generate_wallet_')) {
|
||||||
|
logDebug(action, 'handleGenerateWallet');
|
||||||
|
await userWalletsHandler.handleGenerateWallet(callbackQuery);
|
||||||
|
}
|
||||||
|
// Shop navigation
|
||||||
|
else if (action === 'shop_start') {
|
||||||
|
logDebug(action, 'showProducts');
|
||||||
|
await userProductHandler.showProducts(msg);
|
||||||
|
} else if (action.startsWith('shop_country_')) {
|
||||||
|
logDebug(action, 'handleCountrySelection');
|
||||||
|
await userProductHandler.handleCountrySelection(callbackQuery);
|
||||||
|
} else if (action.startsWith('shop_city_')) {
|
||||||
|
logDebug(action, 'handleCitySelection');
|
||||||
|
await userProductHandler.handleCitySelection(callbackQuery);
|
||||||
|
} else if (action.startsWith('shop_district_')) {
|
||||||
|
logDebug(action, 'handleDistrictSelection');
|
||||||
|
await userProductHandler.handleDistrictSelection(callbackQuery);
|
||||||
|
} else if (action.startsWith('shop_category_')) {
|
||||||
|
logDebug(action, 'handleCategorySelection');
|
||||||
|
await userProductHandler.handleCategorySelection(callbackQuery);
|
||||||
|
} else if (action.startsWith('shop_subcategory_')) {
|
||||||
|
logDebug(action, 'handleSubcategorySelection');
|
||||||
|
await userProductHandler.handleSubcategorySelection(callbackQuery);
|
||||||
|
} else if (action.startsWith('shop_product_')) {
|
||||||
|
logDebug(action, 'handleProductSelection');
|
||||||
|
await userProductHandler.handleProductSelection(callbackQuery);
|
||||||
|
} else if (action.startsWith('increase_quantity_')) {
|
||||||
|
logDebug(action, 'handleIncreaseQuantity');
|
||||||
|
await userProductHandler.handleIncreaseQuantity(callbackQuery);
|
||||||
|
} else if (action.startsWith('decrease_quantity_')) {
|
||||||
|
logDebug(action, 'handleDecreaseQuantity');
|
||||||
|
await userProductHandler.handleDecreaseQuantity(callbackQuery);
|
||||||
|
} else if (action.startsWith('buy_product_')) {
|
||||||
|
logDebug(action, 'handleBuyProduct');
|
||||||
|
await userProductHandler.handleBuyProduct(callbackQuery);
|
||||||
|
} else if (action.startsWith('pay_with_')) {
|
||||||
|
logDebug(action, 'handlePay');
|
||||||
|
await userProductHandler.handlePay(callbackQuery);
|
||||||
|
} else if (action.startsWith('list_purchases_')) {
|
||||||
|
logDebug(action, 'handlePurchaseListPage');
|
||||||
|
await userPurchaseHandler.handlePurchaseListPage(callbackQuery);
|
||||||
|
} else if (action.startsWith('view_purchase_')) {
|
||||||
|
logDebug(action, 'viewPurchase');
|
||||||
|
await userPurchaseHandler.viewPurchase(callbackQuery);
|
||||||
|
}
|
||||||
|
// Admin location management
|
||||||
|
else if (action === 'add_location') {
|
||||||
|
logDebug(action, 'handleAddLocation');
|
||||||
|
await adminLocationHandler.handleAddLocation(callbackQuery);
|
||||||
|
} else if (action === 'view_locations') {
|
||||||
|
logDebug(action, 'handleViewLocations');
|
||||||
|
await adminLocationHandler.handleViewLocations(callbackQuery);
|
||||||
|
} else if (action === 'delete_location') {
|
||||||
|
logDebug(action, 'handleDeleteLocation');
|
||||||
|
await adminLocationHandler.handleDeleteLocation(callbackQuery);
|
||||||
|
} else if (action.startsWith('confirm_delete_location_')) {
|
||||||
|
logDebug(action, 'handleConfirmDelete');
|
||||||
|
await adminLocationHandler.handleConfirmDelete(callbackQuery);
|
||||||
|
} else if (action === 'admin_menu') {
|
||||||
|
logDebug(action, 'backToMenu');
|
||||||
|
await adminLocationHandler.backToMenu(callbackQuery);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Admin product management
|
||||||
|
else if (action === 'manage_products') {
|
||||||
|
logDebug(action, 'handleProductManagement');
|
||||||
|
await adminProductHandler.handleProductManagement(callbackQuery);
|
||||||
|
} else if (action.startsWith('prod_country_')) {
|
||||||
|
logDebug(action, 'handleCountrySelection');
|
||||||
|
await adminProductHandler.handleCountrySelection(callbackQuery);
|
||||||
|
} else if (action.startsWith('prod_city_')) {
|
||||||
|
logDebug(action, 'handleCitySelection');
|
||||||
|
await adminProductHandler.handleCitySelection(callbackQuery);
|
||||||
|
} else if (action.startsWith('prod_district_')) {
|
||||||
|
logDebug(action, 'handleDistrictSelection');
|
||||||
|
await adminProductHandler.handleDistrictSelection(callbackQuery);
|
||||||
|
} else if (action.startsWith('add_category_')) {
|
||||||
|
logDebug(action, 'handleAddCategory');
|
||||||
|
await adminProductHandler.handleAddCategory(callbackQuery);
|
||||||
|
} else if (action.startsWith('edit_category_')) {
|
||||||
|
logDebug(action, 'handleEditCategory');
|
||||||
|
await adminProductHandler.handleEditCategory(callbackQuery);
|
||||||
|
} else if (action.startsWith('prod_category_')) {
|
||||||
|
logDebug(action, 'handleCategorySelection');
|
||||||
|
await adminProductHandler.handleCategorySelection(callbackQuery);
|
||||||
|
} else if (action.startsWith('add_subcategory_')) {
|
||||||
|
logDebug(action, 'handleAddSubcategory');
|
||||||
|
await adminProductHandler.handleAddSubcategory(callbackQuery);
|
||||||
|
} else if (action.startsWith('prod_subcategory_')) {
|
||||||
|
logDebug(action, 'handleSubcategorySelection');
|
||||||
|
await adminProductHandler.handleSubcategorySelection(callbackQuery);
|
||||||
|
} else if (action.startsWith('list_products_')) {
|
||||||
|
logDebug(action, 'handleProductListPage');
|
||||||
|
await adminProductHandler.handleProductListPage(callbackQuery);
|
||||||
|
} else if (action.startsWith('add_product_')) {
|
||||||
|
logDebug(action, 'handleAddProduct');
|
||||||
|
await adminProductHandler.handleAddProduct(callbackQuery);
|
||||||
|
} else if (action.startsWith('view_product_')) {
|
||||||
|
logDebug(action, 'handleViewProduct');
|
||||||
|
await adminProductHandler.handleViewProduct(callbackQuery);
|
||||||
|
} else if (action.startsWith('edit_product_')) {
|
||||||
|
logDebug(action, 'handleProductEdit');
|
||||||
|
await adminProductHandler.handleProductEdit(callbackQuery)
|
||||||
|
} else if (action.startsWith('delete_product_')) {
|
||||||
|
logDebug(action, 'handleProductDelete');
|
||||||
|
await adminProductHandler.handleProductDelete(callbackQuery);
|
||||||
|
} else if (action.startsWith('confirm_delete_product_')) {
|
||||||
|
logDebug(action, 'handleConfirmDelete');
|
||||||
|
await adminProductHandler.handleConfirmDelete(callbackQuery);
|
||||||
|
}
|
||||||
|
// Admin user management
|
||||||
|
else if (action.startsWith('view_user_')) {
|
||||||
|
logDebug(action, 'handleViewUser');
|
||||||
|
await adminUserHandler.handleViewUser(callbackQuery);
|
||||||
|
} else if (action.startsWith('list_users_')) {
|
||||||
|
logDebug(action, 'handleUserListPage');
|
||||||
|
await adminUserHandler.handleUserListPage(callbackQuery);
|
||||||
|
} else if (action.startsWith('delete_user_')) {
|
||||||
|
logDebug(action, 'handleDeleteUser');
|
||||||
|
await adminUserHandler.handleDeleteUser(callbackQuery);
|
||||||
|
} else if (action.startsWith('block_user_')) {
|
||||||
|
logDebug(action, 'handleBlockUser');
|
||||||
|
await adminUserHandler.handleBlockUser(callbackQuery);
|
||||||
|
} else if (action.startsWith('confirm_delete_user_')) {
|
||||||
|
logDebug(action, 'handleConfirmDelete');
|
||||||
|
await adminUserHandler.handleConfirmDelete(callbackQuery);
|
||||||
|
} else if (action.startsWith('confirm_block_user_')) {
|
||||||
|
logDebug(action, 'handleConfirmBlock');
|
||||||
|
await adminUserHandler.handleConfirmBlock(callbackQuery);
|
||||||
|
} else if (action.startsWith('edit_user_balance_')) {
|
||||||
|
logDebug(action, 'handleEditUserBalance');
|
||||||
|
await adminUserHandler.handleEditUserBalance(callbackQuery);
|
||||||
|
}
|
||||||
|
// Admin users location management
|
||||||
|
else if (action.startsWith('edit_user_location_')) {
|
||||||
|
logDebug(action, 'handleEditUserLocation');
|
||||||
|
await adminUserLocationHandler.handleEditUserLocation(callbackQuery);
|
||||||
|
} else if (action.startsWith('edit_user_country_')) {
|
||||||
|
logDebug(action, 'handleEditUserCountry');
|
||||||
|
await adminUserLocationHandler.handleEditUserCountry(callbackQuery);
|
||||||
|
} else if (action.startsWith('edit_user_city_')) {
|
||||||
|
logDebug(action, 'handleEditUserCity');
|
||||||
|
await adminUserLocationHandler.handleEditUserCity(callbackQuery);
|
||||||
|
} else if (action.startsWith('edit_user_district_')) {
|
||||||
|
logDebug(action, 'handleEditUserDistrict');
|
||||||
|
await adminUserLocationHandler.handleEditUserDistrict(callbackQuery)
|
||||||
|
}
|
||||||
|
// Dump manage
|
||||||
|
else if (action === "export_database") {
|
||||||
|
await adminDumpHandler.handleExportDatabase(callbackQuery);
|
||||||
|
return;
|
||||||
|
} else if (action === "import_database") {
|
||||||
|
await adminDumpHandler.handleImportDatabase(callbackQuery);
|
||||||
|
}
|
||||||
|
|
||||||
await bot.answerCallbackQuery(callbackQuery.id);
|
await bot.answerCallbackQuery(callbackQuery.id);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await ErrorHandler.handleError(bot, callbackQuery.message.chat.id, error, 'callback query');
|
await ErrorHandler.handleError(bot, msg.chat.id, error, 'callback query');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Error handling
|
||||||
bot.on('polling_error', ErrorHandler.handlePollingError);
|
bot.on('polling_error', ErrorHandler.handlePollingError);
|
||||||
|
|
||||||
process.on('unhandledRejection', (error) => {
|
process.on('unhandledRejection', (error) => {
|
||||||
logger.error({ err: error }, 'Unhandled promise rejection');
|
console.error('Unhandled promise rejection:', error);
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.info('Bot is running...');
|
console.log('Bot is running...');
|
||||||
|
|
||||||
import { startAdminPanel } from './admin/server.js';
|
|
||||||
startAdminPanel();
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import config from '../config/config.js';
|
|
||||||
|
|
||||||
export function isAdmin(userId) {
|
|
||||||
return config.ADMIN_IDS.includes(userId.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isSuperAdmin(userId) {
|
|
||||||
return config.SUPER_ADMIN_IDS.includes(userId.toString());
|
|
||||||
}
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
import logger from '../utils/logger.js';
|
|
||||||
|
|
||||||
export default async function migration001(db) {
|
|
||||||
await db.runAsync('BEGIN TRANSACTION');
|
|
||||||
|
|
||||||
await db.runAsync(`CREATE TABLE IF NOT EXISTS users (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
telegram_id TEXT UNIQUE NOT NULL,
|
|
||||||
username TEXT,
|
|
||||||
country TEXT,
|
|
||||||
city TEXT,
|
|
||||||
district TEXT,
|
|
||||||
status INTEGER DEFAULT 0,
|
|
||||||
total_balance REAL DEFAULT 0,
|
|
||||||
bonus_balance REAL DEFAULT 0,
|
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
||||||
)`);
|
|
||||||
|
|
||||||
await db.runAsync(`CREATE TABLE IF NOT EXISTS crypto_wallets (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
user_id INTEGER NOT NULL,
|
|
||||||
wallet_type TEXT NOT NULL,
|
|
||||||
address TEXT NOT NULL,
|
|
||||||
derivation_path TEXT NOT NULL,
|
|
||||||
mnemonic TEXT NOT NULL,
|
|
||||||
balance REAL DEFAULT 0,
|
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
|
||||||
UNIQUE(user_id, wallet_type)
|
|
||||||
)`);
|
|
||||||
|
|
||||||
await db.runAsync(`CREATE TABLE IF NOT EXISTS transactions (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
user_id INTEGER NOT NULL,
|
|
||||||
wallet_type TEXT NOT NULL,
|
|
||||||
tx_hash TEXT NOT NULL,
|
|
||||||
amount REAL NOT NULL,
|
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
|
||||||
)`);
|
|
||||||
|
|
||||||
await db.runAsync(`CREATE TABLE IF NOT EXISTS products (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
location_id INTEGER NOT NULL,
|
|
||||||
category_id INTEGER NOT NULL,
|
|
||||||
name TEXT NOT NULL,
|
|
||||||
description TEXT,
|
|
||||||
private_data TEXT,
|
|
||||||
price REAL NOT NULL CHECK (price > 0),
|
|
||||||
quantity_in_stock INTEGER DEFAULT 0 CHECK (quantity_in_stock >= 0),
|
|
||||||
photo_url TEXT,
|
|
||||||
hidden_photo_url TEXT,
|
|
||||||
hidden_coordinates TEXT,
|
|
||||||
hidden_description TEXT,
|
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
FOREIGN KEY (location_id) REFERENCES locations(id) ON DELETE CASCADE,
|
|
||||||
FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE CASCADE
|
|
||||||
)`);
|
|
||||||
|
|
||||||
await db.runAsync(`CREATE TABLE IF NOT EXISTS purchases (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
user_id INTEGER NOT NULL,
|
|
||||||
product_id INTEGER NOT NULL,
|
|
||||||
wallet_type TEXT NOT NULL,
|
|
||||||
tx_hash TEXT NOT NULL,
|
|
||||||
quantity INTEGER NOT NULL CHECK (quantity > 0),
|
|
||||||
total_price REAL NOT NULL CHECK (total_price > 0),
|
|
||||||
purchase_date DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
status TEXT DEFAULT 'pending',
|
|
||||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
|
||||||
FOREIGN KEY (product_id) REFERENCES products(id) ON DELETE CASCADE
|
|
||||||
)`);
|
|
||||||
|
|
||||||
await db.runAsync(`CREATE TABLE IF NOT EXISTS locations (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
country TEXT NOT NULL,
|
|
||||||
city TEXT NOT NULL,
|
|
||||||
district TEXT NOT NULL,
|
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
UNIQUE(country, city, district)
|
|
||||||
)`);
|
|
||||||
|
|
||||||
await db.runAsync(`CREATE TABLE IF NOT EXISTS categories (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
location_id INTEGER NOT NULL,
|
|
||||||
name TEXT NOT NULL,
|
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
FOREIGN KEY (location_id) REFERENCES locations(id) ON DELETE CASCADE,
|
|
||||||
UNIQUE(location_id, name)
|
|
||||||
)`);
|
|
||||||
|
|
||||||
await db.runAsync('COMMIT');
|
|
||||||
logger.info('Migration 001: Initial schema created');
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
import logger from '../utils/logger.js';
|
|
||||||
|
|
||||||
export default async function migration002(db, checkColumnExists) {
|
|
||||||
const balanceExists = await checkColumnExists('crypto_wallets', 'balance');
|
|
||||||
if (!balanceExists) {
|
|
||||||
await db.runAsync(`ALTER TABLE crypto_wallets ADD COLUMN balance REAL DEFAULT 0`);
|
|
||||||
logger.info('Migration 002: Column balance added to crypto_wallets');
|
|
||||||
}
|
|
||||||
|
|
||||||
const userIdExists = await checkColumnExists('transactions', 'user_id');
|
|
||||||
if (!userIdExists) {
|
|
||||||
await db.runAsync(`ALTER TABLE transactions ADD COLUMN user_id INTEGER NOT NULL`);
|
|
||||||
logger.info('Migration 002: Column user_id added to transactions');
|
|
||||||
}
|
|
||||||
|
|
||||||
const walletTypeExists = await checkColumnExists('transactions', 'wallet_type');
|
|
||||||
if (!walletTypeExists) {
|
|
||||||
await db.runAsync(`ALTER TABLE transactions ADD COLUMN wallet_type TEXT NOT NULL`);
|
|
||||||
logger.info('Migration 002: Column wallet_type added to transactions');
|
|
||||||
}
|
|
||||||
|
|
||||||
const txHashExists = await checkColumnExists('transactions', 'tx_hash');
|
|
||||||
if (!txHashExists) {
|
|
||||||
await db.runAsync(`ALTER TABLE transactions ADD COLUMN tx_hash TEXT NOT NULL`);
|
|
||||||
logger.info('Migration 002: Column tx_hash added to transactions');
|
|
||||||
}
|
|
||||||
|
|
||||||
const statusExists = await checkColumnExists('purchases', 'status');
|
|
||||||
if (!statusExists) {
|
|
||||||
await db.runAsync(`ALTER TABLE purchases ADD COLUMN status TEXT DEFAULT 'pending'`);
|
|
||||||
logger.info('Migration 002: Column status added to purchases');
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info('Migration 002: Column additions complete');
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import logger from '../utils/logger.js';
|
|
||||||
|
|
||||||
export default async function migration003(db) {
|
|
||||||
await db.runAsync(`CREATE INDEX IF NOT EXISTS idx_users_telegram_id ON users(telegram_id)`);
|
|
||||||
await db.runAsync(`CREATE INDEX IF NOT EXISTS idx_crypto_wallets_user_type ON crypto_wallets(user_id, wallet_type)`);
|
|
||||||
await db.runAsync(`CREATE INDEX IF NOT EXISTS idx_transactions_user ON transactions(user_id)`);
|
|
||||||
await db.runAsync(`CREATE INDEX IF NOT EXISTS idx_purchases_user_product ON purchases(user_id, product_id)`);
|
|
||||||
await db.runAsync(`CREATE INDEX IF NOT EXISTS idx_purchases_status ON purchases(status)`);
|
|
||||||
await db.runAsync(`CREATE INDEX IF NOT EXISTS idx_products_location_category ON products(location_id, category_id)`);
|
|
||||||
logger.info('Migration 003: Indexes created');
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
export default async function migration004(db) {
|
|
||||||
await db.runAsync(`
|
|
||||||
CREATE TABLE IF NOT EXISTS user_states (
|
|
||||||
chat_id TEXT PRIMARY KEY,
|
|
||||||
state_data TEXT NOT NULL,
|
|
||||||
updated_at INTEGER NOT NULL
|
|
||||||
)
|
|
||||||
`);
|
|
||||||
console.log('Migration 004: user_states table created');
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import logger from '../utils/logger.js';
|
|
||||||
|
|
||||||
export default async function migration005(db) {
|
|
||||||
await db.runAsync(`
|
|
||||||
CREATE TABLE IF NOT EXISTS audit_log (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
action TEXT NOT NULL,
|
|
||||||
admin_id TEXT NOT NULL,
|
|
||||||
details TEXT,
|
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
||||||
)
|
|
||||||
`);
|
|
||||||
logger.info('Migration 005: audit_log table created');
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
import logger from '../utils/logger.js';
|
|
||||||
|
|
||||||
export default async function migration006(db) {
|
|
||||||
await db.runAsync('BEGIN TRANSACTION');
|
|
||||||
|
|
||||||
await db.runAsync(`CREATE TABLE IF NOT EXISTS subcategories (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
category_id INTEGER NOT NULL,
|
|
||||||
name TEXT NOT NULL,
|
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE CASCADE,
|
|
||||||
UNIQUE(category_id, name)
|
|
||||||
)`);
|
|
||||||
|
|
||||||
const cols = await db.allAsync('PRAGMA table_info(products)');
|
|
||||||
const hasSubcat = cols.some(c => c.name === 'subcategory_id');
|
|
||||||
if (!hasSubcat) {
|
|
||||||
await db.runAsync(`ALTER TABLE products ADD COLUMN subcategory_id INTEGER REFERENCES subcategories(id) ON DELETE SET NULL`);
|
|
||||||
}
|
|
||||||
|
|
||||||
await db.runAsync('COMMIT');
|
|
||||||
logger.info('Migration 006: subcategories table + products.subcategory_id column');
|
|
||||||
}
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
import db from '../config/database.js';
|
|
||||||
import logger from '../utils/logger.js';
|
|
||||||
|
|
||||||
const ALLOWED_TABLES = new Set([
|
|
||||||
'users', 'crypto_wallets', 'transactions', 'products',
|
|
||||||
'purchases', 'locations', 'categories', 'subcategories'
|
|
||||||
]);
|
|
||||||
|
|
||||||
export const checkColumnExists = async (tableName, columnName) => {
|
|
||||||
if (!ALLOWED_TABLES.has(tableName)) {
|
|
||||||
throw new Error(`Invalid table name: ${tableName}`);
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const result = await db.allAsync(`PRAGMA table_info(${tableName})`);
|
|
||||||
return result.some(column => column.name === columnName);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error({ err: error, tableName, columnName }, 'Error checking column');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const cleanUpInvalidForeignKeys = async () => {
|
|
||||||
try {
|
|
||||||
await db.runAsync(`DELETE FROM crypto_wallets WHERE user_id NOT IN (SELECT id FROM users)`);
|
|
||||||
logger.info('Cleaned up invalid foreign key references in crypto_wallets table');
|
|
||||||
} catch (error) {
|
|
||||||
logger.error({ err: error }, 'Error cleaning up invalid foreign key references');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function runMigrations() {
|
|
||||||
await db.runAsync(`CREATE TABLE IF NOT EXISTS _meta (key TEXT PRIMARY KEY, value TEXT)`);
|
|
||||||
|
|
||||||
const row = await db.getAsync(`SELECT value FROM _meta WHERE key = 'schema_version'`);
|
|
||||||
const currentVersion = row ? parseInt(row.value, 10) : 0;
|
|
||||||
|
|
||||||
const migrations = [
|
|
||||||
(await import('./001_initial_schema.js')).default,
|
|
||||||
(await import('./002_add_columns.js')).default,
|
|
||||||
(await import('./003_add_indexes.js')).default,
|
|
||||||
(await import('./004_user_states.js')).default,
|
|
||||||
(await import('./005_audit_log.js')).default,
|
|
||||||
(await import('./006_subcategories.js')).default,
|
|
||||||
];
|
|
||||||
|
|
||||||
for (let i = currentVersion; i < migrations.length; i++) {
|
|
||||||
logger.info({ migration: i + 1, total: migrations.length }, 'Running migration');
|
|
||||||
if (i === 1) {
|
|
||||||
await migrations[i](db, checkColumnExists);
|
|
||||||
} else {
|
|
||||||
await migrations[i](db);
|
|
||||||
}
|
|
||||||
await db.runAsync(
|
|
||||||
`INSERT OR REPLACE INTO _meta (key, value) VALUES ('schema_version', ?)`,
|
|
||||||
[String(i + 1)]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info({ schemaVersion: migrations.length }, 'Migrations complete');
|
|
||||||
}
|
|
||||||
@@ -1,63 +1,73 @@
|
|||||||
// Wallet.js
|
|
||||||
|
|
||||||
import db from "../config/database.js";
|
import db from "../config/database.js";
|
||||||
import WalletUtils from "../utils/walletUtils.js";
|
import WalletService from "../utils/walletService.js";
|
||||||
|
|
||||||
export default class Wallet {
|
export default class Wallet {
|
||||||
|
static getBaseWalletType(walletType) {
|
||||||
|
if (walletType.includes('TRC-20')) return 'TRON';
|
||||||
|
if (walletType.includes('ERC-20')) return 'ETH';
|
||||||
|
return walletType;
|
||||||
|
}
|
||||||
|
|
||||||
static async getArchivedWallets(userId) {
|
static async getArchivedWallets(userId) {
|
||||||
const archivedWallets = await db.allAsync(`
|
const archivedWallets = await db.allAsync(`
|
||||||
SELECT * FROM crypto_wallets WHERE user_id = ? AND wallet_type LIKE '%_%'
|
SELECT * FROM crypto_wallets WHERE user_id = ? AND wallet_type LIKE '%_%'
|
||||||
`, [userId]);
|
`, [userId]);
|
||||||
|
|
||||||
const btcAddress = archivedWallets.find(w => w.wallet_type.startsWith('BTC'))?.address;
|
const btcAddress = archivedWallets.find(w => w.wallet_type.startsWith('BTC'))?.address;
|
||||||
const ltcAddress = archivedWallets.find(w => w.wallet_type.startsWith('LTC'))?.address;
|
const ltcAddress = archivedWallets.find(w => w.wallet_type.startsWith('LTC'))?.address;
|
||||||
|
const tronAddress = archivedWallets.find(w => w.wallet_type.startsWith('TRON'))?.address;
|
||||||
const ethAddress = archivedWallets.find(w => w.wallet_type.startsWith('ETH'))?.address;
|
const ethAddress = archivedWallets.find(w => w.wallet_type.startsWith('ETH'))?.address;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
btc: btcAddress,
|
btc: btcAddress,
|
||||||
ltc: ltcAddress,
|
ltc: ltcAddress,
|
||||||
|
tron: tronAddress,
|
||||||
eth: ethAddress,
|
eth: ethAddress,
|
||||||
wallets: archivedWallets
|
wallets: archivedWallets
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static async getActiveWallets(userId) {
|
static async getActiveWallets(userId) {
|
||||||
const activeWallets = await db.allAsync(
|
const activeWallets = await db.allAsync(
|
||||||
`SELECT wallet_type, address FROM crypto_wallets WHERE user_id = ? ORDER BY wallet_type`,
|
`SELECT wallet_type, address FROM crypto_wallets WHERE user_id = ? ORDER BY wallet_type`,
|
||||||
[userId]
|
[userId]
|
||||||
);
|
)
|
||||||
|
|
||||||
const btcAddress = activeWallets.find(w => w.wallet_type === 'BTC')?.address;
|
const btcAddress = activeWallets.find(w => w.wallet_type === 'BTC')?.address;
|
||||||
const ltcAddress = activeWallets.find(w => w.wallet_type === 'LTC')?.address;
|
const ltcAddress = activeWallets.find(w => w.wallet_type === 'LTC')?.address;
|
||||||
|
const tronAddress = activeWallets.find(w => w.wallet_type === 'TRON')?.address;
|
||||||
const ethAddress = activeWallets.find(w => w.wallet_type === 'ETH')?.address;
|
const ethAddress = activeWallets.find(w => w.wallet_type === 'ETH')?.address;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
btc: btcAddress,
|
btc: btcAddress,
|
||||||
ltc: ltcAddress,
|
ltc: ltcAddress,
|
||||||
|
tron: tronAddress,
|
||||||
eth: ethAddress,
|
eth: ethAddress,
|
||||||
wallets: activeWallets
|
wallets: activeWallets
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static async getActiveWalletsBalance(userId) {
|
static async getActiveWalletsBalance(userId) {
|
||||||
const activeWallets = await this.getActiveWallets(userId);
|
const activeWallets = await this.getActiveWallets(userId);
|
||||||
|
|
||||||
const walletUtilsInstance = new WalletUtils(
|
const walletService = new WalletService(
|
||||||
activeWallets.btc,
|
activeWallets.btc,
|
||||||
activeWallets.ltc,
|
activeWallets.ltc,
|
||||||
|
activeWallets.tron,
|
||||||
activeWallets.eth,
|
activeWallets.eth,
|
||||||
userId,
|
userId,
|
||||||
Date.now() - 30 * 24 * 60 * 60 * 1000
|
Date.now() - 30 * 24 * 60 * 60 * 1000
|
||||||
);
|
);
|
||||||
|
|
||||||
const balances = await walletUtilsInstance.getAllBalances();
|
const balances = await walletService.getAllBalances();
|
||||||
|
|
||||||
let totalUsdBalance = 0;
|
let totalUsdBalance = 0;
|
||||||
|
|
||||||
for (const [type, balance] of Object.entries(balances)) {
|
for (const [type, balance] of Object.entries(balances)) {
|
||||||
const baseType = WalletUtils.getBaseWalletType(type);
|
const baseType = this.getBaseWalletType(type);
|
||||||
const wallet = activeWallets.wallets.find(w =>
|
const wallet = activeWallets.wallets.find(w =>
|
||||||
w.wallet_type === baseType ||
|
w.wallet_type === baseType ||
|
||||||
|
(type.includes('TRC-20') && w.wallet_type === 'TRON') ||
|
||||||
(type.includes('ERC-20') && w.wallet_type === 'ETH')
|
(type.includes('ERC-20') && w.wallet_type === 'ETH')
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -65,7 +75,9 @@ export default class Wallet {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
totalUsdBalance += balance.usdValue;
|
if (wallet) {
|
||||||
|
totalUsdBalance += balance.usdValue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return totalUsdBalance;
|
return totalUsdBalance;
|
||||||
@@ -74,22 +86,24 @@ export default class Wallet {
|
|||||||
static async getArchivedWalletsBalance(userId) {
|
static async getArchivedWalletsBalance(userId) {
|
||||||
const archiveWallets = await this.getArchivedWallets(userId);
|
const archiveWallets = await this.getArchivedWallets(userId);
|
||||||
|
|
||||||
const walletUtilsInstance = new WalletUtils(
|
const walletService = new WalletService(
|
||||||
archiveWallets.btc,
|
archiveWallets.btc,
|
||||||
archiveWallets.ltc,
|
archiveWallets.ltc,
|
||||||
|
archiveWallets.tron,
|
||||||
archiveWallets.eth,
|
archiveWallets.eth,
|
||||||
userId,
|
userId,
|
||||||
Date.now() - 30 * 24 * 60 * 60 * 1000
|
Date.now() - 30 * 24 * 60 * 60 * 1000
|
||||||
);
|
);
|
||||||
|
|
||||||
const balances = await walletUtilsInstance.getAllBalances();
|
const balances = await walletService.getAllBalances();
|
||||||
|
|
||||||
let totalUsdBalance = 0;
|
let totalUsdBalance = 0;
|
||||||
|
|
||||||
for (const [type, balance] of Object.entries(balances)) {
|
for (const [type, balance] of Object.entries(balances)) {
|
||||||
const baseType = WalletUtils.getBaseWalletType(type);
|
const baseType = this.getBaseWalletType(type);
|
||||||
const wallet = archiveWallets.wallets.find(w =>
|
const wallet = archiveWallets.wallets.find(w =>
|
||||||
w.wallet_type === baseType ||
|
w.wallet_type === baseType ||
|
||||||
|
(type.includes('TRC-20') && w.wallet_type.startsWith('TRON')) ||
|
||||||
(type.includes('ERC-20') && w.wallet_type.startsWith('ETH'))
|
(type.includes('ERC-20') && w.wallet_type.startsWith('ETH'))
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -97,7 +111,9 @@ export default class Wallet {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
totalUsdBalance += balance.usdValue;
|
if (wallet) {
|
||||||
|
totalUsdBalance += balance.usdValue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return totalUsdBalance;
|
return totalUsdBalance;
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
import logger from '../utils/logger.js';
|
|
||||||
|
|
||||||
class CallbackRouter {
|
|
||||||
constructor() {
|
|
||||||
this.exactRoutes = new Map();
|
|
||||||
this.prefixRoutes = new Map();
|
|
||||||
}
|
|
||||||
|
|
||||||
registerExact(action, handler) {
|
|
||||||
this.exactRoutes.set(action, handler);
|
|
||||||
}
|
|
||||||
|
|
||||||
registerPrefix(prefix, handler) {
|
|
||||||
this.prefixRoutes.set(prefix, handler);
|
|
||||||
}
|
|
||||||
|
|
||||||
async dispatch(callbackQuery) {
|
|
||||||
const action = callbackQuery.data;
|
|
||||||
|
|
||||||
const exactHandler = this.exactRoutes.get(action);
|
|
||||||
if (exactHandler) {
|
|
||||||
await exactHandler(callbackQuery);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const prefixes = [...this.prefixRoutes.keys()].sort((a, b) => b.length - a.length);
|
|
||||||
for (const prefix of prefixes) {
|
|
||||||
if (action.startsWith(prefix)) {
|
|
||||||
await this.prefixRoutes.get(prefix)(callbackQuery);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.warn({ action }, 'No handler for callback');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default new CallbackRouter();
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
class MessageRouter {
|
|
||||||
constructor() {
|
|
||||||
this.inputHandlers = [];
|
|
||||||
this.textHandlers = new Map();
|
|
||||||
}
|
|
||||||
|
|
||||||
registerInput(handler) {
|
|
||||||
this.inputHandlers.push(handler);
|
|
||||||
}
|
|
||||||
|
|
||||||
registerText(text, handler) {
|
|
||||||
this.textHandlers.set(text, handler);
|
|
||||||
}
|
|
||||||
|
|
||||||
async dispatch(msg) {
|
|
||||||
for (const handler of this.inputHandlers) {
|
|
||||||
if (await handler(msg)) return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (msg.text && this.textHandlers.has(msg.text)) {
|
|
||||||
await this.textHandlers.get(msg.text)(msg);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default new MessageRouter();
|
|
||||||
@@ -1,349 +0,0 @@
|
|||||||
import callbackRouter from './callbackRouter.js';
|
|
||||||
import messageRouter from './messageRouter.js';
|
|
||||||
import { isAdmin } from '../middleware/auth.js';
|
|
||||||
import logger from '../utils/logger.js';
|
|
||||||
|
|
||||||
import userHandler from '../handlers/userHandlers/userHandler.js';
|
|
||||||
import userPurchaseHandler from '../handlers/userHandlers/userPurchaseHandler.js';
|
|
||||||
import userLocationHandler from '../handlers/userHandlers/userLocationHandler.js';
|
|
||||||
import userProductHandler from '../handlers/userHandlers/userProductHandler.js';
|
|
||||||
import userWalletsHandler from '../handlers/userHandlers/wallet/index.js';
|
|
||||||
import userDeletionHandler from '../handlers/userHandlers/userDeletionHandler.js';
|
|
||||||
import adminHandler from '../handlers/adminHandlers/adminHandler.js';
|
|
||||||
import adminUserLocationHandler from '../handlers/adminHandlers/adminUserLocationHandler.js';
|
|
||||||
import adminDumpHandler from '../handlers/adminHandlers/adminDumpHandler.js';
|
|
||||||
import adminLocationHandler from '../handlers/adminHandlers/adminLocationHandler.js';
|
|
||||||
import productHandler from '../handlers/adminHandlers/product/index.js';
|
|
||||||
import adminWalletsHandler from '../handlers/adminHandlers/adminWalletsHandler.js';
|
|
||||||
import adminUserHandler from '../handlers/adminHandlers/adminUserHandler.js';
|
|
||||||
|
|
||||||
const logDebug = (action, functionName) => {
|
|
||||||
logger.debug({ action, functionName }, 'Button Press');
|
|
||||||
};
|
|
||||||
|
|
||||||
export function registerRoutes() {
|
|
||||||
// === Message Input Handlers (order matters — checked first) ===
|
|
||||||
messageRouter.registerInput(adminLocationHandler.handleLocationInput.bind(adminLocationHandler));
|
|
||||||
messageRouter.registerInput(productHandler.handleCategoryInput.bind(productHandler));
|
|
||||||
messageRouter.registerInput(productHandler.handleProductImport.bind(productHandler));
|
|
||||||
messageRouter.registerInput(productHandler.handleProductEditImport.bind(productHandler));
|
|
||||||
messageRouter.registerInput(adminDumpHandler.handleDumpImport.bind(adminDumpHandler));
|
|
||||||
messageRouter.registerInput(adminUserHandler.handleBonusBalanceInput.bind(adminUserHandler));
|
|
||||||
messageRouter.registerInput(productHandler.handleCategoryUpdate.bind(productHandler));
|
|
||||||
|
|
||||||
// === Text Commands ===
|
|
||||||
messageRouter.registerText('📦 Products', async (msg) => {
|
|
||||||
logDebug(msg.text, 'showProducts');
|
|
||||||
await userProductHandler.showProducts(msg);
|
|
||||||
});
|
|
||||||
messageRouter.registerText('👤 Profile', async (msg) => {
|
|
||||||
logDebug(msg.text, 'showProfile');
|
|
||||||
await userHandler.showProfile(msg);
|
|
||||||
});
|
|
||||||
messageRouter.registerText('💰 Wallets', async (msg) => {
|
|
||||||
logDebug(msg.text, 'showBalance');
|
|
||||||
await userWalletsHandler.showBalance(msg);
|
|
||||||
});
|
|
||||||
messageRouter.registerText('🛍 Purchases', async (msg) => {
|
|
||||||
logDebug(msg.text, 'showPurchases');
|
|
||||||
await userPurchaseHandler.showPurchases(msg);
|
|
||||||
});
|
|
||||||
messageRouter.registerText('📦 Manage Products', async (msg) => {
|
|
||||||
if (!isAdmin(msg.from.id)) return;
|
|
||||||
logDebug(msg.text, 'handleProductManagement');
|
|
||||||
await productHandler.handleProductManagement(msg);
|
|
||||||
});
|
|
||||||
messageRouter.registerText('👥 Manage Users', async (msg) => {
|
|
||||||
if (!isAdmin(msg.from.id)) return;
|
|
||||||
logDebug(msg.text, 'handleUserList');
|
|
||||||
await adminUserHandler.handleUserList(msg);
|
|
||||||
});
|
|
||||||
messageRouter.registerText('📍 Manage Locations', async (msg) => {
|
|
||||||
if (!isAdmin(msg.from.id)) return;
|
|
||||||
logDebug(msg.text, 'handleViewLocations');
|
|
||||||
await adminLocationHandler.handleViewLocations(msg);
|
|
||||||
});
|
|
||||||
messageRouter.registerText('💾 Database Backup', async (msg) => {
|
|
||||||
if (!isAdmin(msg.from.id)) return;
|
|
||||||
logDebug(msg.text, 'handleDump');
|
|
||||||
await adminDumpHandler.handleDump(msg);
|
|
||||||
});
|
|
||||||
messageRouter.registerText('💰 Manage Wallets', async (msg) => {
|
|
||||||
if (!isAdmin(msg.from.id)) return;
|
|
||||||
logDebug(msg.text, 'handleWalletManagement');
|
|
||||||
await adminWalletsHandler.handleWalletManagement(msg);
|
|
||||||
});
|
|
||||||
|
|
||||||
// === Exact Callback Routes ===
|
|
||||||
callbackRouter.registerExact('set_location', async (cq) => {
|
|
||||||
logDebug(cq.data, 'handleSetLocation');
|
|
||||||
await userLocationHandler.handleSetLocation(cq);
|
|
||||||
});
|
|
||||||
callbackRouter.registerExact('back_to_profile', async (cq) => {
|
|
||||||
logDebug(cq.data, 'handleBackToProfile');
|
|
||||||
await userHandler.handleBackToProfile(cq);
|
|
||||||
});
|
|
||||||
callbackRouter.registerExact('back_to_balance', async (cq) => {
|
|
||||||
logDebug(cq.data, 'handleBackToBalance');
|
|
||||||
await userWalletsHandler.handleBackToBalance(cq);
|
|
||||||
});
|
|
||||||
callbackRouter.registerExact('delete_account', async (cq) => {
|
|
||||||
logDebug(cq.data, 'handleDeleteAccount');
|
|
||||||
await userDeletionHandler.handleDeleteAccount(cq);
|
|
||||||
});
|
|
||||||
callbackRouter.registerExact('confirm_delete_account', async (cq) => {
|
|
||||||
logDebug(cq.data, 'handleConfirmDelete');
|
|
||||||
await userDeletionHandler.handleConfirmDelete(cq);
|
|
||||||
});
|
|
||||||
callbackRouter.registerExact('add_wallet', async (cq) => {
|
|
||||||
logDebug(cq.data, 'handleAddWallet');
|
|
||||||
await userWalletsHandler.handleAddWallet(cq);
|
|
||||||
});
|
|
||||||
callbackRouter.registerExact('top_up_wallet', async (cq) => {
|
|
||||||
logDebug(cq.data, 'handleTopUpWallet');
|
|
||||||
await userWalletsHandler.handleTopUpWallet(cq);
|
|
||||||
});
|
|
||||||
callbackRouter.registerExact('wallet_history', async (cq) => {
|
|
||||||
logDebug(cq.data, 'handleWalletHistory');
|
|
||||||
await userWalletsHandler.handleWalletHistory(cq);
|
|
||||||
});
|
|
||||||
callbackRouter.registerExact('view_archived_wallets', async (cq) => {
|
|
||||||
logDebug(cq.data, 'handleViewArchivedWallets');
|
|
||||||
await userWalletsHandler.handleViewArchivedWallets(cq);
|
|
||||||
});
|
|
||||||
callbackRouter.registerExact('refresh_balance', async (cq) => {
|
|
||||||
logDebug(cq.data, 'handleRefreshBalance');
|
|
||||||
await userWalletsHandler.handleRefreshBalance(cq);
|
|
||||||
});
|
|
||||||
callbackRouter.registerExact('shop_start', async (cq) => {
|
|
||||||
logDebug(cq.data, 'showProducts');
|
|
||||||
await userProductHandler.showProducts(cq.message);
|
|
||||||
});
|
|
||||||
callbackRouter.registerExact('add_location', async (cq) => {
|
|
||||||
logDebug(cq.data, 'handleAddLocation');
|
|
||||||
await adminLocationHandler.handleAddLocation(cq);
|
|
||||||
});
|
|
||||||
callbackRouter.registerExact('view_locations', async (cq) => {
|
|
||||||
logDebug(cq.data, 'handleViewLocations');
|
|
||||||
await adminLocationHandler.handleViewLocations(cq);
|
|
||||||
});
|
|
||||||
callbackRouter.registerExact('view_ip', async (cq) => {
|
|
||||||
logDebug(cq.data, 'handleViewIP');
|
|
||||||
await adminLocationHandler.handleViewIP(cq);
|
|
||||||
});
|
|
||||||
callbackRouter.registerExact('delete_location', async (cq) => {
|
|
||||||
logDebug(cq.data, 'handleDeleteLocation');
|
|
||||||
await adminLocationHandler.handleDeleteLocation(cq);
|
|
||||||
});
|
|
||||||
callbackRouter.registerExact('admin_menu', async (cq) => {
|
|
||||||
logDebug(cq.data, 'backToMenu');
|
|
||||||
await adminLocationHandler.backToMenu(cq);
|
|
||||||
});
|
|
||||||
callbackRouter.registerExact('manage_products', async (cq) => {
|
|
||||||
logDebug(cq.data, 'handleProductManagement');
|
|
||||||
await productHandler.handleProductManagement(cq);
|
|
||||||
});
|
|
||||||
callbackRouter.registerExact('export_database', async (cq) => {
|
|
||||||
logDebug(cq.data, 'handleExportDatabase');
|
|
||||||
await adminDumpHandler.handleExportDatabase(cq);
|
|
||||||
});
|
|
||||||
callbackRouter.registerExact('import_database', async (cq) => {
|
|
||||||
logDebug(cq.data, 'handleImportDatabase');
|
|
||||||
await adminDumpHandler.handleImportDatabase(cq);
|
|
||||||
});
|
|
||||||
callbackRouter.registerExact('back_to_wallet_types', async (cq) => {
|
|
||||||
logDebug(cq.data, 'handleBackToWalletTypes');
|
|
||||||
await adminWalletsHandler.handleBackToWalletTypes(cq);
|
|
||||||
});
|
|
||||||
|
|
||||||
// === Prefix Callback Routes ===
|
|
||||||
callbackRouter.registerPrefix('set_country_', async (cq) => {
|
|
||||||
logDebug(cq.data, 'handleSetCountry');
|
|
||||||
await userLocationHandler.handleSetCountry(cq);
|
|
||||||
});
|
|
||||||
callbackRouter.registerPrefix('set_city_', async (cq) => {
|
|
||||||
logDebug(cq.data, 'handleSetCity');
|
|
||||||
await userLocationHandler.handleSetCity(cq);
|
|
||||||
});
|
|
||||||
callbackRouter.registerPrefix('set_district_', async (cq) => {
|
|
||||||
logDebug(cq.data, 'handleSetDistrict');
|
|
||||||
await userLocationHandler.handleSetDistrict(cq);
|
|
||||||
});
|
|
||||||
callbackRouter.registerPrefix('generate_wallet_', async (cq) => {
|
|
||||||
logDebug(cq.data, 'handleGenerateWallet');
|
|
||||||
await userWalletsHandler.handleGenerateWallet(cq);
|
|
||||||
});
|
|
||||||
callbackRouter.registerPrefix('shop_country_', async (cq) => {
|
|
||||||
logDebug(cq.data, 'handleCountrySelection');
|
|
||||||
await userProductHandler.handleCountrySelection(cq);
|
|
||||||
});
|
|
||||||
callbackRouter.registerPrefix('shop_city_', async (cq) => {
|
|
||||||
logDebug(cq.data, 'handleCitySelection');
|
|
||||||
await userProductHandler.handleCitySelection(cq);
|
|
||||||
});
|
|
||||||
callbackRouter.registerPrefix('shop_district_', async (cq) => {
|
|
||||||
logDebug(cq.data, 'handleDistrictSelection');
|
|
||||||
await userProductHandler.handleDistrictSelection(cq);
|
|
||||||
});
|
|
||||||
callbackRouter.registerPrefix('shop_category_', async (cq) => {
|
|
||||||
logDebug(cq.data, 'handleCategorySelection');
|
|
||||||
await userProductHandler.handleCategorySelection(cq);
|
|
||||||
});
|
|
||||||
callbackRouter.registerPrefix('shop_product_', async (cq) => {
|
|
||||||
logDebug(cq.data, 'handleProductSelection');
|
|
||||||
await userProductHandler.handleProductSelection(cq);
|
|
||||||
});
|
|
||||||
callbackRouter.registerPrefix('increase_quantity_', async (cq) => {
|
|
||||||
logDebug(cq.data, 'handleIncreaseQuantity');
|
|
||||||
await userProductHandler.handleIncreaseQuantity(cq);
|
|
||||||
});
|
|
||||||
callbackRouter.registerPrefix('decrease_quantity_', async (cq) => {
|
|
||||||
logDebug(cq.data, 'handleDecreaseQuantity');
|
|
||||||
await userProductHandler.handleDecreaseQuantity(cq);
|
|
||||||
});
|
|
||||||
callbackRouter.registerPrefix('buy_product_', async (cq) => {
|
|
||||||
logDebug(cq.data, 'handleBuyProduct');
|
|
||||||
await userProductHandler.handleBuyProduct(cq);
|
|
||||||
});
|
|
||||||
callbackRouter.registerPrefix('pay_with_', async (cq) => {
|
|
||||||
logDebug(cq.data, 'handlePay');
|
|
||||||
await userProductHandler.handlePay(cq);
|
|
||||||
});
|
|
||||||
callbackRouter.registerPrefix('list_purchases_', async (cq) => {
|
|
||||||
logDebug(cq.data, 'handlePurchaseListPage');
|
|
||||||
await userPurchaseHandler.handlePurchaseListPage(cq);
|
|
||||||
});
|
|
||||||
callbackRouter.registerPrefix('view_purchase_', async (cq) => {
|
|
||||||
logDebug(cq.data, 'viewPurchase');
|
|
||||||
await userPurchaseHandler.viewPurchase(cq);
|
|
||||||
});
|
|
||||||
callbackRouter.registerPrefix('confirm_received_', async (cq) => {
|
|
||||||
logDebug(cq.data, 'handleConfirmReceived');
|
|
||||||
await userPurchaseHandler.handleConfirmReceived(cq);
|
|
||||||
});
|
|
||||||
callbackRouter.registerPrefix('confirm_delete_location_', async (cq) => {
|
|
||||||
logDebug(cq.data, 'handleConfirmDelete');
|
|
||||||
await adminLocationHandler.handleConfirmDelete(cq);
|
|
||||||
});
|
|
||||||
callbackRouter.registerPrefix('prod_country_', async (cq) => {
|
|
||||||
logDebug(cq.data, 'handleCountrySelection');
|
|
||||||
await productHandler.handleCountrySelection(cq);
|
|
||||||
});
|
|
||||||
callbackRouter.registerPrefix('prod_city_', async (cq) => {
|
|
||||||
logDebug(cq.data, 'handleCitySelection');
|
|
||||||
await productHandler.handleCitySelection(cq);
|
|
||||||
});
|
|
||||||
callbackRouter.registerPrefix('prod_district_', async (cq) => {
|
|
||||||
logDebug(cq.data, 'handleDistrictSelection');
|
|
||||||
await productHandler.handleDistrictSelection(cq);
|
|
||||||
});
|
|
||||||
callbackRouter.registerPrefix('add_category_', async (cq) => {
|
|
||||||
logDebug(cq.data, 'handleAddCategory');
|
|
||||||
await productHandler.handleAddCategory(cq);
|
|
||||||
});
|
|
||||||
callbackRouter.registerPrefix('edit_category_', async (cq) => {
|
|
||||||
logDebug(cq.data, 'handleEditCategory');
|
|
||||||
await productHandler.handleEditCategory(cq);
|
|
||||||
});
|
|
||||||
callbackRouter.registerPrefix('prod_category_', async (cq) => {
|
|
||||||
logDebug(cq.data, 'handleCategorySelection');
|
|
||||||
await productHandler.handleCategorySelection(cq);
|
|
||||||
});
|
|
||||||
callbackRouter.registerPrefix('list_products_', async (cq) => {
|
|
||||||
logDebug(cq.data, 'handleProductListPage');
|
|
||||||
await productHandler.handleProductListPage(cq);
|
|
||||||
});
|
|
||||||
callbackRouter.registerPrefix('add_product_', async (cq) => {
|
|
||||||
logDebug(cq.data, 'handleAddProduct');
|
|
||||||
await productHandler.handleAddProduct(cq);
|
|
||||||
});
|
|
||||||
callbackRouter.registerPrefix('view_product_', async (cq) => {
|
|
||||||
logDebug(cq.data, 'handleViewProduct');
|
|
||||||
await productHandler.handleViewProduct(cq);
|
|
||||||
});
|
|
||||||
callbackRouter.registerPrefix('edit_product_', async (cq) => {
|
|
||||||
logDebug(cq.data, 'handleProductEdit');
|
|
||||||
await productHandler.handleProductEdit(cq);
|
|
||||||
});
|
|
||||||
callbackRouter.registerPrefix('delete_product_', async (cq) => {
|
|
||||||
logDebug(cq.data, 'handleProductDelete');
|
|
||||||
await productHandler.handleProductDelete(cq);
|
|
||||||
});
|
|
||||||
callbackRouter.registerPrefix('confirm_delete_product_', async (cq) => {
|
|
||||||
logDebug(cq.data, 'handleConfirmDelete');
|
|
||||||
await productHandler.handleConfirmDelete(cq);
|
|
||||||
});
|
|
||||||
callbackRouter.registerPrefix('view_user_', async (cq) => {
|
|
||||||
logDebug(cq.data, 'handleViewUser');
|
|
||||||
await adminUserHandler.handleViewUser(cq);
|
|
||||||
});
|
|
||||||
callbackRouter.registerPrefix('list_users_', async (cq) => {
|
|
||||||
logDebug(cq.data, 'handleUserListPage');
|
|
||||||
await adminUserHandler.handleUserListPage(cq);
|
|
||||||
});
|
|
||||||
callbackRouter.registerPrefix('delete_user_', async (cq) => {
|
|
||||||
logDebug(cq.data, 'handleDeleteUser');
|
|
||||||
await adminUserHandler.handleDeleteUser(cq);
|
|
||||||
});
|
|
||||||
callbackRouter.registerPrefix('block_user_', async (cq) => {
|
|
||||||
logDebug(cq.data, 'handleBlockUser');
|
|
||||||
await adminUserHandler.handleBlockUser(cq);
|
|
||||||
});
|
|
||||||
callbackRouter.registerPrefix('confirm_delete_user_', async (cq) => {
|
|
||||||
logDebug(cq.data, 'handleConfirmDelete');
|
|
||||||
await adminUserHandler.handleConfirmDelete(cq);
|
|
||||||
});
|
|
||||||
callbackRouter.registerPrefix('confirm_block_user_', async (cq) => {
|
|
||||||
logDebug(cq.data, 'handleConfirmBlock');
|
|
||||||
await adminUserHandler.handleConfirmBlock(cq);
|
|
||||||
});
|
|
||||||
callbackRouter.registerPrefix('edit_user_balance_', async (cq) => {
|
|
||||||
logDebug(cq.data, 'handleEditUserBalance');
|
|
||||||
await adminUserHandler.handleEditUserBalance(cq);
|
|
||||||
});
|
|
||||||
callbackRouter.registerPrefix('edit_user_location_', async (cq) => {
|
|
||||||
logDebug(cq.data, 'handleEditUserLocation');
|
|
||||||
await adminUserLocationHandler.handleEditUserLocation(cq);
|
|
||||||
});
|
|
||||||
callbackRouter.registerPrefix('edit_user_country_', async (cq) => {
|
|
||||||
logDebug(cq.data, 'handleEditUserCountry');
|
|
||||||
await adminUserLocationHandler.handleEditUserCountry(cq);
|
|
||||||
});
|
|
||||||
callbackRouter.registerPrefix('edit_user_city_', async (cq) => {
|
|
||||||
logDebug(cq.data, 'handleEditUserCity');
|
|
||||||
await adminUserLocationHandler.handleEditUserCity(cq);
|
|
||||||
});
|
|
||||||
callbackRouter.registerPrefix('edit_user_district_', async (cq) => {
|
|
||||||
logDebug(cq.data, 'handleEditUserDistrict');
|
|
||||||
await adminUserLocationHandler.handleEditUserDistrict(cq);
|
|
||||||
});
|
|
||||||
callbackRouter.registerPrefix('wallet_type_', async (cq) => {
|
|
||||||
logDebug(cq.data, 'handleWalletTypeSelection');
|
|
||||||
await adminWalletsHandler.handleWalletTypeSelection(cq);
|
|
||||||
});
|
|
||||||
callbackRouter.registerPrefix('check_balance_', async (cq) => {
|
|
||||||
logDebug(cq.data, 'handleCheckCommissionBalance');
|
|
||||||
await adminWalletsHandler.handleCheckCommissionBalance(cq);
|
|
||||||
});
|
|
||||||
callbackRouter.registerPrefix('prev_page_', async (cq) => {
|
|
||||||
logDebug(cq.data, 'handlePagination');
|
|
||||||
await adminWalletsHandler.handlePagination(cq);
|
|
||||||
});
|
|
||||||
callbackRouter.registerPrefix('next_page_', async (cq) => {
|
|
||||||
logDebug(cq.data, 'handlePagination');
|
|
||||||
await adminWalletsHandler.handlePagination(cq);
|
|
||||||
});
|
|
||||||
callbackRouter.registerPrefix('confirm_export_', async (cq) => {
|
|
||||||
logDebug(cq.data, 'handleConfirmExport');
|
|
||||||
await adminWalletsHandler.handleConfirmExport(cq);
|
|
||||||
});
|
|
||||||
callbackRouter.registerPrefix('export_csv_', async (cq) => {
|
|
||||||
logDebug(cq.data, 'handleExportCSV');
|
|
||||||
await adminWalletsHandler.handleExportCSV(cq);
|
|
||||||
});
|
|
||||||
callbackRouter.registerPrefix('view_transaction_history_', async (cq) => {
|
|
||||||
logDebug(cq.data, 'handleTransactionHistory');
|
|
||||||
const page = parseInt(cq.data.split('_').pop());
|
|
||||||
await userWalletsHandler.handleTransactionHistory(cq, page);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import logger from '../utils/logger.js';
|
|
||||||
import db from '../config/database.js';
|
|
||||||
|
|
||||||
export async function logAudit(action, adminId, details) {
|
|
||||||
logger.warn({ action, adminId, ...details }, `AUDIT: ${action}`);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await db.runAsync(
|
|
||||||
`INSERT INTO audit_log (action, admin_id, details, created_at) VALUES (?, ?, ?, datetime('now'))`,
|
|
||||||
[action, String(adminId), JSON.stringify(details)]
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error({ err: error }, 'Failed to write audit log');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,18 +1,11 @@
|
|||||||
import db from "../config/database.js";
|
import db from "../config/database.js";
|
||||||
import logger from "../utils/logger.js";
|
|
||||||
|
|
||||||
class CategoryService {
|
class CategoryService {
|
||||||
static async getCategoriesByLocationId(locationId) {
|
static async getCategoriesByLocationId(locationId) {
|
||||||
try {
|
return await db.allAsync(
|
||||||
const categories = await db.allAsync(
|
'SELECT id, name FROM categories WHERE location_id = ? ORDER BY name',
|
||||||
'SELECT * FROM categories WHERE location_id = ?',
|
[locationId]
|
||||||
[locationId]
|
);
|
||||||
);
|
|
||||||
return categories;
|
|
||||||
} catch (error) {
|
|
||||||
logger.error({ err: error }, 'Error fetching categories by location ID');
|
|
||||||
throw new Error('Failed to fetch categories');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static async getSubcategoriesByCategoryId(categoryId) {
|
static async getSubcategoriesByCategoryId(categoryId) {
|
||||||
@@ -23,16 +16,7 @@ class CategoryService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static async getCategoryById(categoryId) {
|
static async getCategoryById(categoryId) {
|
||||||
try {
|
return await db.getAsync('SELECT id, name FROM categories WHERE id = ?', [categoryId]);
|
||||||
const category = await db.getAsync(
|
|
||||||
'SELECT * FROM categories WHERE id = ?',
|
|
||||||
[categoryId]
|
|
||||||
);
|
|
||||||
return category;
|
|
||||||
} catch (error) {
|
|
||||||
logger.error({ err: error }, 'Error fetching category by ID');
|
|
||||||
throw new Error('Failed to fetch category');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static async getSubcategoryById(subcategoryId) {
|
static async getSubcategoryById(subcategoryId) {
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import db from "../config/database.js";
|
import db from "../config/database.js";
|
||||||
import logger from "../utils/logger.js";
|
|
||||||
|
|
||||||
class LocationService {
|
class LocationService {
|
||||||
static async getCountries() {
|
static async getCountries() {
|
||||||
@@ -21,29 +20,17 @@ class LocationService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static async getLocation(country, city, district) {
|
static async getLocation(country, city, district) {
|
||||||
try {
|
return await db.getAsync(
|
||||||
const location = await db.getAsync(
|
'SELECT id FROM locations WHERE country = ? AND city = ? AND district = ?',
|
||||||
'SELECT * FROM locations WHERE country = ? AND city = ? AND district = ?',
|
[country, city, district]
|
||||||
[country, city, district]
|
);
|
||||||
);
|
|
||||||
return location;
|
|
||||||
} catch (error) {
|
|
||||||
logger.error({ err: error }, 'Error fetching location');
|
|
||||||
throw new Error('Failed to fetch location');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static async getLocationById(locationId) {
|
static async getLocationById(locationId) {
|
||||||
try {
|
return await db.getAsync(
|
||||||
const location = await db.getAsync(
|
'SELECT country, city, district FROM locations WHERE id = ?',
|
||||||
'SELECT * FROM locations WHERE id = ?',
|
[locationId]
|
||||||
[locationId]
|
);
|
||||||
);
|
|
||||||
return location;
|
|
||||||
} catch (error) {
|
|
||||||
logger.error({ err: error }, 'Error fetching location by ID');
|
|
||||||
throw new Error('Failed to fetch location');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,79 +1,36 @@
|
|||||||
// productService.js
|
|
||||||
|
|
||||||
import db from "../config/database.js";
|
import db from "../config/database.js";
|
||||||
import Validators from "../utils/validators.js";
|
|
||||||
import logger from "../utils/logger.js";
|
|
||||||
|
|
||||||
class ProductService {
|
class ProductService {
|
||||||
static async getProductById(productId) {
|
static async getProductById(productId) {
|
||||||
if (!Validators.isValidNumericId(Number(productId))) {
|
|
||||||
throw new Error('Invalid product ID');
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
return await db.getAsync(`SELECT * FROM products WHERE id = ?`, [productId]);
|
return await db.getAsync(`SELECT * FROM products WHERE id = ?`, [productId]);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error({ err: error }, 'Error get product');
|
console.error('Error get product:', error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static async getDetailedProductById(productId) {
|
static async getDetailedProductById(productId) {
|
||||||
if (!Validators.isValidNumericId(Number(productId))) {
|
|
||||||
throw new Error('Invalid product ID');
|
|
||||||
}
|
|
||||||
return await db.getAsync(
|
return await db.getAsync(
|
||||||
`SELECT p.*, c.name as category_name
|
`SELECT p.*, c.name as category_name, s.name as subcategory_name
|
||||||
FROM products p
|
FROM products p
|
||||||
JOIN categories c ON p.category_id = c.id
|
JOIN categories c ON p.category_id = c.id
|
||||||
WHERE p.id = ?`,
|
JOIN subcategories s ON p.subcategory_id = s.id
|
||||||
|
WHERE p.id = ?`,
|
||||||
[productId]
|
[productId]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static async getProductsByLocationAndCategory(locationId, categoryId) {
|
static async getProductsByLocationAndCategory(locationId, categoryId, subcategoryId) {
|
||||||
if (!Validators.isValidNumericId(Number(locationId)) || !Validators.isValidNumericId(Number(categoryId))) {
|
|
||||||
throw new Error('Invalid location or category ID');
|
|
||||||
}
|
|
||||||
return await db.allAsync(
|
return await db.allAsync(
|
||||||
`SELECT id, name, price, description, quantity_in_stock, photo_url
|
`SELECT id, name, price, description, quantity_in_stock, photo_url
|
||||||
FROM products
|
FROM products
|
||||||
WHERE location_id = ? AND category_id = ?
|
WHERE location_id = ? AND category_id = ? AND subcategory_id = ?
|
||||||
AND quantity_in_stock > 0
|
AND quantity_in_stock > 0
|
||||||
ORDER BY name`,
|
ORDER BY name`,
|
||||||
[locationId, categoryId]
|
[locationId, categoryId, subcategoryId]
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
static async getProductsByCategoryId(categoryId) {
|
|
||||||
if (!Validators.isValidNumericId(Number(categoryId))) {
|
|
||||||
throw new Error('Invalid category ID');
|
|
||||||
}
|
|
||||||
return await db.allAsync(
|
|
||||||
`SELECT id, name, price, description, quantity_in_stock, photo_url
|
|
||||||
FROM products
|
|
||||||
WHERE category_id = ?
|
|
||||||
AND quantity_in_stock > 0
|
|
||||||
ORDER BY name`,
|
|
||||||
[categoryId]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
static async decreaseProductQuantity(productId, quantity) {
|
|
||||||
if (!Validators.isValidNumericId(Number(productId))) {
|
|
||||||
throw new Error('Invalid product ID');
|
|
||||||
}
|
|
||||||
if (!Number.isFinite(quantity) || quantity <= 0) {
|
|
||||||
throw new Error('Invalid quantity');
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
await db.runAsync(
|
|
||||||
'UPDATE products SET quantity_in_stock = quantity_in_stock - ? WHERE id = ?',
|
|
||||||
[quantity, productId]
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error({ err: error }, 'Error decreasing product quantity');
|
|
||||||
throw new Error('Failed to update product quantity');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,4 @@
|
|||||||
// purchaseService.js
|
|
||||||
|
|
||||||
import db from "../config/database.js";
|
import db from "../config/database.js";
|
||||||
import crypto from "crypto";
|
|
||||||
import logger from "../utils/logger.js";
|
|
||||||
|
|
||||||
class PurchaseService {
|
class PurchaseService {
|
||||||
static async getPurchasesByUserId(userId, limit, offset) {
|
static async getPurchasesByUserId(userId, limit, offset) {
|
||||||
@@ -25,150 +21,38 @@ class PurchaseService {
|
|||||||
`, [userId, limit, offset]);
|
`, [userId, limit, offset]);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error({ err: error }, 'Error get purchases');
|
console.error('Error get purchases:', error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static async getPurchaseById(purchaseId) {
|
static async getPurchaseById(purchaseId) {
|
||||||
try {
|
try {
|
||||||
return await db.getAsync(
|
return await db.getAsync(`
|
||||||
`SELECT * FROM purchases WHERE id = ?`,
|
SELECT
|
||||||
[purchaseId]
|
p.*,
|
||||||
);
|
pr.name as product_name,
|
||||||
|
pr.description,
|
||||||
|
l.country,
|
||||||
|
l.city,
|
||||||
|
l.district
|
||||||
|
FROM purchases p
|
||||||
|
JOIN products pr ON p.product_id = pr.id
|
||||||
|
JOIN locations l ON pr.location_id = l.id
|
||||||
|
WHERE p.id = ?
|
||||||
|
`, [purchaseId]);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error({ err: error }, 'Error getting purchase by ID');
|
console.error('Error get purchase:', error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static async createPurchase(userId, productId, walletType, quantity, totalPrice) {
|
static async createPurchase(userId, productId, walletType, quantity, totalPrice) {
|
||||||
try {
|
await db.runAsync(
|
||||||
await db.runAsync('BEGIN IMMEDIATE TRANSACTION');
|
'INSERT INTO purchases (user_id, product_id, wallet_type, tx_hash, quantity, total_price) VALUES (?, ?, ?, ?, ?, ?)',
|
||||||
|
[userId, productId, walletType, "null", quantity, totalPrice]
|
||||||
const user = await db.getAsync(
|
);
|
||||||
'SELECT id, telegram_id, bonus_balance, total_balance FROM users WHERE id = ?',
|
|
||||||
[userId]
|
|
||||||
);
|
|
||||||
if (!user) {
|
|
||||||
throw new Error('User not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
const product = await db.getAsync(
|
|
||||||
'SELECT id, quantity_in_stock FROM products WHERE id = ? AND quantity_in_stock >= ?',
|
|
||||||
[productId, quantity]
|
|
||||||
);
|
|
||||||
if (!product) {
|
|
||||||
throw new Error('Product not available or insufficient stock');
|
|
||||||
}
|
|
||||||
|
|
||||||
let remainingAmount = totalPrice;
|
|
||||||
let usedBonus = 0;
|
|
||||||
let usedCrypto = 0;
|
|
||||||
let sourceWalletType = '';
|
|
||||||
|
|
||||||
if (user.bonus_balance > 0) {
|
|
||||||
usedBonus = Math.min(user.bonus_balance, remainingAmount);
|
|
||||||
remainingAmount -= usedBonus;
|
|
||||||
|
|
||||||
const bonusResult = await db.runAsync(
|
|
||||||
'UPDATE users SET bonus_balance = bonus_balance - ? WHERE id = ? AND bonus_balance >= ?',
|
|
||||||
[usedBonus, userId, usedBonus]
|
|
||||||
);
|
|
||||||
if (bonusResult.changes === 0) {
|
|
||||||
throw new Error('Insufficient bonus balance');
|
|
||||||
}
|
|
||||||
sourceWalletType += `bonus_${usedBonus}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (remainingAmount > 0) {
|
|
||||||
usedCrypto = remainingAmount;
|
|
||||||
|
|
||||||
const cryptoResult = await db.runAsync(
|
|
||||||
'UPDATE users SET total_balance = total_balance - ? WHERE id = ? AND total_balance >= ?',
|
|
||||||
[usedCrypto, userId, usedCrypto]
|
|
||||||
);
|
|
||||||
if (cryptoResult.changes === 0) {
|
|
||||||
throw new Error('Insufficient crypto balance');
|
|
||||||
}
|
|
||||||
sourceWalletType += sourceWalletType ? `, crypto_${usedCrypto}` : `crypto_${usedCrypto}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const stockResult = await db.runAsync(
|
|
||||||
'UPDATE products SET quantity_in_stock = quantity_in_stock - ? WHERE id = ? AND quantity_in_stock >= ?',
|
|
||||||
[quantity, productId, quantity]
|
|
||||||
);
|
|
||||||
if (stockResult.changes === 0) {
|
|
||||||
throw new Error('Insufficient stock');
|
|
||||||
}
|
|
||||||
|
|
||||||
const txHash = crypto.randomUUID();
|
|
||||||
|
|
||||||
const result = await db.runAsync(
|
|
||||||
`INSERT INTO purchases (user_id, product_id, wallet_type, quantity, total_price, purchase_date, tx_hash, status)
|
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, 'completed')`,
|
|
||||||
[userId, productId, sourceWalletType, quantity, totalPrice, new Date().toISOString(), txHash]
|
|
||||||
);
|
|
||||||
|
|
||||||
await db.runAsync('COMMIT');
|
|
||||||
return result.lastInsertRowid;
|
|
||||||
} catch (error) {
|
|
||||||
try { await db.runAsync('ROLLBACK'); } catch (_) {}
|
|
||||||
logger.error({ err: error }, 'Error creating purchase');
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static async updatePurchaseStatus(purchaseId, status) {
|
|
||||||
try {
|
|
||||||
await db.runAsync('BEGIN IMMEDIATE TRANSACTION');
|
|
||||||
|
|
||||||
const purchase = await db.getAsync(
|
|
||||||
'SELECT * FROM purchases WHERE id = ?',
|
|
||||||
[purchaseId]
|
|
||||||
);
|
|
||||||
if (!purchase) {
|
|
||||||
throw new Error('Purchase not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (status === 'canceled') {
|
|
||||||
if (purchase.wallet_type.startsWith('bonus')) {
|
|
||||||
await db.runAsync(
|
|
||||||
'UPDATE users SET bonus_balance = bonus_balance + ? WHERE id = ?',
|
|
||||||
[purchase.total_price, purchase.user_id]
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
await db.runAsync(
|
|
||||||
'UPDATE users SET total_balance = total_balance + ? WHERE id = ?',
|
|
||||||
[purchase.total_price, purchase.user_id]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await db.runAsync(
|
|
||||||
'UPDATE purchases SET status = ? WHERE id = ?',
|
|
||||||
[status, purchaseId]
|
|
||||||
);
|
|
||||||
|
|
||||||
await db.runAsync('COMMIT');
|
|
||||||
} catch (error) {
|
|
||||||
try { await db.runAsync('ROLLBACK'); } catch (_) {}
|
|
||||||
logger.error({ err: error }, 'Error updating purchase status');
|
|
||||||
throw new Error('Failed to update purchase status');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static async getTotalPurchasesByUserId(userId) {
|
|
||||||
try {
|
|
||||||
const total = await db.getAsync(
|
|
||||||
`SELECT COUNT(*) AS total FROM purchases WHERE user_id = ?`,
|
|
||||||
[userId]
|
|
||||||
);
|
|
||||||
return total.total;
|
|
||||||
} catch (error) {
|
|
||||||
logger.error({ err: error }, 'Error fetching total purchases by user ID');
|
|
||||||
throw new Error('Failed to fetch total purchases');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,103 +0,0 @@
|
|||||||
import db from '../config/database.js';
|
|
||||||
import logger from '../utils/logger.js';
|
|
||||||
|
|
||||||
const TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
||||||
const CLEANUP_INTERVAL_MS = 60 * 60 * 1000; // 1 hour
|
|
||||||
|
|
||||||
let initialized = false;
|
|
||||||
|
|
||||||
export async function initStates() {
|
|
||||||
if (initialized) return;
|
|
||||||
|
|
||||||
await db.runAsync(`
|
|
||||||
CREATE TABLE IF NOT EXISTS user_states (
|
|
||||||
chat_id TEXT PRIMARY KEY,
|
|
||||||
state_data TEXT NOT NULL,
|
|
||||||
updated_at INTEGER NOT NULL
|
|
||||||
)
|
|
||||||
`);
|
|
||||||
|
|
||||||
initialized = true;
|
|
||||||
logger.info('user_states table initialized');
|
|
||||||
|
|
||||||
setInterval(cleanExpired, CLEANUP_INTERVAL_MS);
|
|
||||||
cleanExpired();
|
|
||||||
}
|
|
||||||
|
|
||||||
function serialize(value) {
|
|
||||||
if (value === undefined) return null;
|
|
||||||
return JSON.stringify(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
function deserialize(json) {
|
|
||||||
if (!json) return undefined;
|
|
||||||
try {
|
|
||||||
return JSON.parse(json);
|
|
||||||
} catch {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function get(chatId) {
|
|
||||||
const row = await db.getAsync(
|
|
||||||
'SELECT state_data, updated_at FROM user_states WHERE chat_id = ?',
|
|
||||||
[String(chatId)]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!row) return undefined;
|
|
||||||
|
|
||||||
if (Date.now() - row.updated_at > TTL_MS) {
|
|
||||||
await db.runAsync('DELETE FROM user_states WHERE chat_id = ?', [String(chatId)]);
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
return deserialize(row.state_data);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function set(chatId, value) {
|
|
||||||
const data = serialize(value);
|
|
||||||
const now = Date.now();
|
|
||||||
|
|
||||||
await db.runAsync(
|
|
||||||
`INSERT INTO user_states (chat_id, state_data, updated_at)
|
|
||||||
VALUES (?, ?, ?)
|
|
||||||
ON CONFLICT(chat_id) DO UPDATE SET state_data = ?, updated_at = ?`,
|
|
||||||
[String(chatId), data, now, data, now]
|
|
||||||
);
|
|
||||||
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function del(chatId) {
|
|
||||||
await db.runAsync('DELETE FROM user_states WHERE chat_id = ?', [String(chatId)]);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function has(chatId) {
|
|
||||||
const row = await db.getAsync(
|
|
||||||
'SELECT updated_at FROM user_states WHERE chat_id = ?',
|
|
||||||
[String(chatId)]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!row) return false;
|
|
||||||
|
|
||||||
if (Date.now() - row.updated_at > TTL_MS) {
|
|
||||||
await db.runAsync('DELETE FROM user_states WHERE chat_id = ?', [String(chatId)]);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function cleanExpired() {
|
|
||||||
const cutoff = Date.now() - TTL_MS;
|
|
||||||
const result = await db.runAsync(
|
|
||||||
'DELETE FROM user_states WHERE updated_at < ?',
|
|
||||||
[cutoff]
|
|
||||||
);
|
|
||||||
if (result.changes > 0) {
|
|
||||||
logger.info({ expiredCount: result.changes }, 'Cleaned expired user states');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const userStates = { get, set, delete: del, has, initStates, cleanExpired };
|
|
||||||
export default userStates;
|
|
||||||
@@ -1,80 +1,7 @@
|
|||||||
// userService.js
|
|
||||||
|
|
||||||
import db from "../config/database.js";
|
import db from "../config/database.js";
|
||||||
import Wallet from "../models/Wallet.js";
|
import Wallet from "../models/Wallet.js";
|
||||||
import WalletUtils from "../utils/walletUtils.js";
|
|
||||||
import logger from "../utils/logger.js";
|
|
||||||
|
|
||||||
const ALLOWED_USER_FIELDS = new Set([
|
|
||||||
'telegram_id', 'username', 'country', 'city',
|
|
||||||
'district', 'status', 'total_balance', 'bonus_balance'
|
|
||||||
]);
|
|
||||||
|
|
||||||
|
|
||||||
class UserService {
|
class UserService {
|
||||||
// Функция для нормализации telegram_id
|
|
||||||
static normalizeTelegramId(telegramId) {
|
|
||||||
if (typeof telegramId === 'number') {
|
|
||||||
// Если это число, преобразуем его в строку и удаляем ".0"
|
|
||||||
return telegramId.toString().replace(/\.0$/, '');
|
|
||||||
}
|
|
||||||
// Если это уже строка, возвращаем как есть
|
|
||||||
return telegramId.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Функция для валидации telegram_id
|
|
||||||
static validateTelegramId(telegramId) {
|
|
||||||
if (typeof telegramId !== 'string') {
|
|
||||||
throw new Error('telegram_id должен быть строкой');
|
|
||||||
}
|
|
||||||
if (telegramId.includes('.0')) {
|
|
||||||
throw new Error('telegram_id не должен содержать ".0"');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static async createUser(userData) {
|
|
||||||
try {
|
|
||||||
// Нормализуем и валидируем telegram_id
|
|
||||||
const normalizedTelegramId = this.normalizeTelegramId(userData?.telegram_id);
|
|
||||||
this.validateTelegramId(normalizedTelegramId);
|
|
||||||
|
|
||||||
// Обновляем значение telegram_id в объекте userData
|
|
||||||
userData.telegram_id = normalizedTelegramId;
|
|
||||||
|
|
||||||
// Проверяем, существует ли пользователь с таким telegram_id
|
|
||||||
const existingUser = await this.getUserByTelegramId(normalizedTelegramId);
|
|
||||||
if (existingUser) {
|
|
||||||
logger.info({ telegramId: normalizedTelegramId }, 'User already exists');
|
|
||||||
return existingUser.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Подготавливаем данные для вставки в базу данных
|
|
||||||
const fields = Object.keys(userData).filter(key => ALLOWED_USER_FIELDS.has(key));
|
|
||||||
const values = fields.map(key => userData[key]);
|
|
||||||
const marks = Array(fields.length).fill('?');
|
|
||||||
|
|
||||||
if (fields.length === 0) {
|
|
||||||
throw new Error('No valid fields provided for user creation');
|
|
||||||
}
|
|
||||||
|
|
||||||
const query = `
|
|
||||||
INSERT INTO users (${fields.join(', ')})
|
|
||||||
VALUES (${marks.join(', ')})
|
|
||||||
`;
|
|
||||||
|
|
||||||
// Выполняем запрос к базе данных
|
|
||||||
await db.runAsync('BEGIN TRANSACTION');
|
|
||||||
const result = await db.runAsync(query, values);
|
|
||||||
await db.runAsync('COMMIT');
|
|
||||||
|
|
||||||
return result.lastInsertRowid;
|
|
||||||
} catch (error) {
|
|
||||||
await db.runAsync('ROLLBACK');
|
|
||||||
logger.error({ err: error }, 'Error creating user');
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static async getUserByUserId(userId) {
|
static async getUserByUserId(userId) {
|
||||||
try {
|
try {
|
||||||
return await db.getAsync(
|
return await db.getAsync(
|
||||||
@@ -82,45 +9,77 @@ class UserService {
|
|||||||
[String(userId)]
|
[String(userId)]
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error({ err: error }, 'Error getting user');
|
console.error('Error getting user:', error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static async getUserByTelegramId(telegramId) {
|
static async getUserByTelegramId(telegramId) {
|
||||||
try {
|
try {
|
||||||
const normalizedTelegramId = this.normalizeTelegramId(telegramId);
|
|
||||||
return await db.getAsync(
|
return await db.getAsync(
|
||||||
'SELECT * FROM users WHERE telegram_id = ?',
|
'SELECT * FROM users WHERE telegram_id = ?',
|
||||||
[normalizedTelegramId]
|
[String(telegramId)]
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error({ err: error }, 'Error getting user');
|
console.error('Error getting user:', error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static async getDetailedUserByTelegramId(telegramId) {
|
static async getDetailedUserByTelegramId(telegramId) {
|
||||||
try {
|
try {
|
||||||
const normalizedTelegramId = this.normalizeTelegramId(telegramId);
|
|
||||||
return await db.getAsync(`
|
return await db.getAsync(`
|
||||||
SELECT
|
SELECT
|
||||||
u.*,
|
u.*,
|
||||||
COUNT(DISTINCT p.id) as purchase_count,
|
COUNT(DISTINCT p.id) as purchase_count,
|
||||||
(SELECT COALESCE(SUM(p2.total_price), 0)
|
COALESCE(SUM(p.total_price), 0) as total_spent,
|
||||||
FROM purchases p2
|
|
||||||
WHERE p2.user_id = u.id) as total_spent,
|
|
||||||
COUNT(DISTINCT cw.id) as crypto_wallet_count,
|
COUNT(DISTINCT cw.id) as crypto_wallet_count,
|
||||||
COUNT(DISTINCT cw2.id) as archived_wallet_count
|
COUNT(DISTINCT cw2.id) as archived_wallet_count
|
||||||
FROM users u
|
FROM users u
|
||||||
LEFT JOIN purchases p ON u.id = p.user_id
|
LEFT JOIN purchases p ON u.id = p.user_id
|
||||||
LEFT JOIN crypto_wallets cw ON u.id = cw.user_id AND cw.wallet_type NOT LIKE '%#_%' ESCAPE '#'
|
LEFT JOIN crypto_wallets cw ON u.id = cw.user_id AND cw.wallet_type NOT LIKE '%_%'
|
||||||
LEFT JOIN crypto_wallets cw2 ON u.id = cw2.user_id AND cw2.wallet_type LIKE '%#_%' ESCAPE '#'
|
LEFT JOIN crypto_wallets cw2 ON u.id = cw2.user_id AND cw2.wallet_type LIKE '%_%'
|
||||||
WHERE u.telegram_id = ?
|
WHERE u.telegram_id = ?
|
||||||
GROUP BY u.id
|
GROUP BY u.id
|
||||||
`, [normalizedTelegramId]);
|
`, [telegramId.toString()]);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error({ err: error }, 'Error getting user stats');
|
console.error('Error getting user stats:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async createUser(userData) {
|
||||||
|
try {
|
||||||
|
const existingUser = await this.getUserByTelegramId(userData?.telegram_id);
|
||||||
|
|
||||||
|
if (existingUser) {
|
||||||
|
return existingUser.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fields = Object.keys(userData);
|
||||||
|
const values = [];
|
||||||
|
|
||||||
|
for (const field of fields) {
|
||||||
|
values.push(userData[field]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const marks = [];
|
||||||
|
for (let i = 0; i < fields.length; i++) {
|
||||||
|
marks.push("?");
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = [
|
||||||
|
`INSERT INTO users (${fields.join(', ')})`,
|
||||||
|
`VALUES (${marks.join(', ')})`
|
||||||
|
].join("");
|
||||||
|
|
||||||
|
await db.runAsync('BEGIN TRANSACTION');
|
||||||
|
const result = await db.runAsync(query, [values]);
|
||||||
|
await db.runAsync('COMMIT');
|
||||||
|
return result.lastID;
|
||||||
|
} catch (error) {
|
||||||
|
await db.runAsync('ROLLBACK');
|
||||||
|
console.error('Error creating user:', error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -130,91 +89,52 @@ class UserService {
|
|||||||
static async deleteUser() {}
|
static async deleteUser() {}
|
||||||
|
|
||||||
static async recalculateUserBalanceByTelegramId(telegramId) {
|
static async recalculateUserBalanceByTelegramId(telegramId) {
|
||||||
const normalizedTelegramId = this.normalizeTelegramId(telegramId);
|
const user = await this.getUserByTelegramId(telegramId);
|
||||||
const user = await this.getUserByTelegramId(normalizedTelegramId);
|
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
const archivedBalance = await Wallet.getArchivedWalletsBalance(user.id);
|
||||||
// Получаем все крипто-балансы пользователя
|
const activeBalance = await Wallet.getActiveWalletsBalance(user.id);
|
||||||
const cryptoBalances = await db.allAsync(
|
|
||||||
`SELECT wallet_type, balance FROM crypto_wallets WHERE user_id = ?`,
|
|
||||||
[user.id]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Получаем актуальные курсы криптовалют
|
const purchases = await db.getAsync(
|
||||||
const prices = await WalletUtils.getCryptoPrices();
|
`SELECT SUM(total_price) as total_sum FROM purchases WHERE user_id = ?`,
|
||||||
|
[user.id]
|
||||||
|
);
|
||||||
|
|
||||||
// Пересчитываем балансы в доллары
|
const userTotalBalance = (activeBalance + archivedBalance) - (purchases?.total_sum || 0);
|
||||||
let totalCryptoBalance = 0;
|
|
||||||
for (const wallet of cryptoBalances) {
|
|
||||||
totalCryptoBalance += WalletUtils.convertToUsd(wallet.wallet_type, wallet.balance, prices);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Получаем сумму всех покупок в крипте
|
await db.runAsync(`UPDATE users SET total_balance = ? WHERE id = ?`, [userTotalBalance, user.id]);
|
||||||
const cryptoPurchases = await db.getAsync(
|
|
||||||
`SELECT SUM(total_price) as total_sum FROM purchases
|
|
||||||
WHERE user_id = ? AND wallet_type LIKE 'crypto%'`,
|
|
||||||
[user.id]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Вычитаем сумму покупок из общего крипто-баланса
|
|
||||||
const remainingBalance = totalCryptoBalance - (cryptoPurchases?.total_sum || 0);
|
|
||||||
|
|
||||||
// Обновляем поле total_balance в таблице users
|
|
||||||
await db.runAsync(
|
|
||||||
`UPDATE users SET total_balance = ? WHERE id = ?`,
|
|
||||||
[remainingBalance, user.id]
|
|
||||||
);
|
|
||||||
|
|
||||||
logger.debug({ userId: user.id, remainingBalance }, 'Updated total_balance');
|
|
||||||
} catch (error) {
|
|
||||||
logger.error({ err: error }, 'Error recalculating user balance');
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static async updateUserLocation(telegramId, country, city, district) {
|
static async updateUserLocation(telegramId, country, city, district) {
|
||||||
const normalizedTelegramId = this.normalizeTelegramId(telegramId);
|
|
||||||
await db.runAsync(
|
await db.runAsync(
|
||||||
'UPDATE users SET country = ?, city = ?, district = ? WHERE telegram_id = ?',
|
'UPDATE users SET country = ?, city = ?, district = ? WHERE telegram_id = ?',
|
||||||
[country, city, district, normalizedTelegramId]
|
[country, city, district, telegramId.toString()]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static async updateUserStatus(telegramId, status) {
|
static async updateUserStatus(telegramId, status) {
|
||||||
const normalizedTelegramId = this.normalizeTelegramId(telegramId);
|
// statuses
|
||||||
|
// 0 - active
|
||||||
|
// 1 - deleted
|
||||||
|
// 2 - blocked
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await db.runAsync('BEGIN TRANSACTION');
|
await db.runAsync('BEGIN TRANSACTION');
|
||||||
|
|
||||||
// Update user status
|
// Update user status
|
||||||
await db.runAsync('UPDATE users SET status = ? WHERE telegram_id = ?', [status, normalizedTelegramId]);
|
await db.runAsync('UPDATE users SET status = ? WHERE telegram_id = ?', [status, telegramId.toString()]);
|
||||||
|
|
||||||
// Commit transaction
|
// Commit transaction
|
||||||
await db.runAsync('COMMIT');
|
await db.runAsync('COMMIT');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
await db.runAsync("ROLLBACK");
|
await db.runAsync("ROLLBACK");
|
||||||
logger.error({ err: e }, 'Error deleting user');
|
console.error('Error deleting user:', e);
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static async getUserBalance(userId) {
|
|
||||||
try {
|
|
||||||
const user = await this.getUserByUserId(userId);
|
|
||||||
if (!user) {
|
|
||||||
throw new Error('User not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Возвращаем сумму доступного крипто-баланса и бонусного баланса
|
|
||||||
return user.total_balance + user.bonus_balance;
|
|
||||||
} catch (error) {
|
|
||||||
logger.error({ err: error }, 'Error getting user balance');
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default UserService;
|
export default UserService;
|
||||||
@@ -1,195 +1,3 @@
|
|||||||
// walletService.js
|
|
||||||
|
|
||||||
import db from "../config/database.js";
|
|
||||||
import config from "../config/config.js";
|
|
||||||
import WalletUtils from "../utils/walletUtils.js";
|
|
||||||
import WalletGenerator from "../utils/walletGenerator.js";
|
|
||||||
import { encrypt, decrypt } from '../utils/encryption.js';
|
|
||||||
import logger from '../utils/logger.js';
|
|
||||||
|
|
||||||
class WalletService {
|
class WalletService {
|
||||||
static async getArchivedWalletsCount(user) {
|
|
||||||
try {
|
|
||||||
const archivedWallets = await db.getAsync(
|
|
||||||
`SELECT COUNT(*) AS total
|
|
||||||
FROM crypto_wallets
|
|
||||||
WHERE user_id = ? AND wallet_type LIKE '%#_%' ESCAPE '#'`,
|
|
||||||
[user.id]
|
|
||||||
);
|
|
||||||
return archivedWallets.total;
|
|
||||||
} catch (error) {
|
|
||||||
logger.error({ err: error }, 'Error fetching archived wallets count');
|
|
||||||
throw new Error('Failed to fetch archived wallets count');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static async getActiveWalletsBalance(userId) {
|
|
||||||
try {
|
|
||||||
const wallets = await db.allAsync(
|
|
||||||
`SELECT wallet_type, balance
|
|
||||||
FROM crypto_wallets
|
|
||||||
WHERE user_id = ? AND wallet_type NOT LIKE '%#_%' ESCAPE '#'`,
|
|
||||||
[userId]
|
|
||||||
);
|
|
||||||
|
|
||||||
const prices = await WalletUtils.getCryptoPrices();
|
|
||||||
|
|
||||||
let totalBalance = 0;
|
|
||||||
for (const wallet of wallets) {
|
|
||||||
const balance = wallet.balance || 0;
|
|
||||||
totalBalance += WalletUtils.convertToUsd(wallet.wallet_type, balance, prices);
|
|
||||||
}
|
|
||||||
|
|
||||||
return totalBalance;
|
|
||||||
} catch (error) {
|
|
||||||
logger.error({ err: error }, 'Error fetching active wallets balance');
|
|
||||||
throw new Error('Failed to fetch active wallets balance');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static async getArchivedWalletsBalance(userId) {
|
|
||||||
try {
|
|
||||||
const wallets = await db.allAsync(
|
|
||||||
`SELECT wallet_type, balance
|
|
||||||
FROM crypto_wallets
|
|
||||||
WHERE user_id = ? AND wallet_type LIKE '%#_%' ESCAPE '#'`,
|
|
||||||
[userId]
|
|
||||||
);
|
|
||||||
|
|
||||||
const prices = await WalletUtils.getCryptoPrices();
|
|
||||||
|
|
||||||
let totalBalance = 0;
|
|
||||||
for (const wallet of wallets) {
|
|
||||||
const balance = wallet.balance || 0;
|
|
||||||
totalBalance += WalletUtils.convertToUsd(wallet.wallet_type, balance, prices);
|
|
||||||
}
|
|
||||||
|
|
||||||
return totalBalance;
|
|
||||||
} catch (error) {
|
|
||||||
logger.error({ err: error }, 'Error fetching archived wallets balance');
|
|
||||||
throw new Error('Failed to fetch archived wallets balance');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Метод для получения кошельков по типу
|
|
||||||
static async getWalletsByType(walletType) {
|
|
||||||
try {
|
|
||||||
const wallets = await db.allAsync(
|
|
||||||
`SELECT *
|
|
||||||
FROM crypto_wallets
|
|
||||||
WHERE wallet_type = ? OR wallet_type LIKE ?`,
|
|
||||||
[walletType, `${walletType}_%`]
|
|
||||||
);
|
|
||||||
|
|
||||||
return wallets;
|
|
||||||
} catch (error) {
|
|
||||||
logger.error({ err: error }, 'Error fetching wallets by type');
|
|
||||||
throw new Error('Failed to fetch wallets by type');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static async createWallet(userId, walletType) {
|
|
||||||
try {
|
|
||||||
// Генерация нового кошелька
|
|
||||||
const mnemonic = await WalletGenerator.generateMnemonic();
|
|
||||||
const wallets = await WalletGenerator.generateWallets(mnemonic, userId);
|
|
||||||
|
|
||||||
if (!wallets || typeof wallets !== 'object') {
|
|
||||||
throw new Error('Failed to generate wallets');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Проверяем наличие базового типа кошелька
|
|
||||||
const baseType = walletType === 'USDT' || walletType === 'USDC' ? 'ETH' : walletType;
|
|
||||||
if (!wallets[baseType.toUpperCase()]) {
|
|
||||||
throw new Error(`Unsupported wallet type: ${walletType}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Проверяем наличие ключа шифрования
|
|
||||||
if (!config.ENCRYPTION_KEY || typeof config.ENCRYPTION_KEY !== 'string') {
|
|
||||||
throw new Error('Encryption key is not configured');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Проверяем и преобразуем userId
|
|
||||||
if (typeof userId !== 'number' && typeof userId !== 'string') {
|
|
||||||
throw new Error('Invalid user ID');
|
|
||||||
}
|
|
||||||
|
|
||||||
const encryptedMnemonic = encrypt(mnemonic, userId);
|
|
||||||
|
|
||||||
// Определяем путь деривации
|
|
||||||
let derivationPath;
|
|
||||||
if (walletType === 'USDT') {
|
|
||||||
derivationPath = "m/44'/60'/0'/0/1"; // Путь для USDT
|
|
||||||
} else if (walletType === 'USDC') {
|
|
||||||
derivationPath = "m/44'/60'/0'/0/2"; // Путь для USDC
|
|
||||||
} else {
|
|
||||||
derivationPath = wallets[walletType.toUpperCase()].path;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Получаем адрес для базового типа
|
|
||||||
const walletData = wallets[baseType.toUpperCase()];
|
|
||||||
if (!walletData || !walletData.address) {
|
|
||||||
logger.error({ baseType, walletKeys: Object.keys(wallets) }, 'Wallet generation failed');
|
|
||||||
throw new Error('Failed to generate wallet address');
|
|
||||||
}
|
|
||||||
const address = walletData.address;
|
|
||||||
|
|
||||||
// Вставляем новый кошелек в базу данных
|
|
||||||
await db.runAsync(
|
|
||||||
`INSERT INTO crypto_wallets
|
|
||||||
(user_id, wallet_type, address, derivation_path, mnemonic)
|
|
||||||
VALUES (?, ?, ?, ?, ?)`,
|
|
||||||
[userId, walletType, address, derivationPath, encryptedMnemonic]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Проверяем успешность вставки
|
|
||||||
const insertedWallet = await db.getAsync(
|
|
||||||
`SELECT * FROM crypto_wallets
|
|
||||||
WHERE user_id = ? AND wallet_type = ?`,
|
|
||||||
[userId, walletType]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!insertedWallet) {
|
|
||||||
throw new Error('Failed to verify wallet insertion');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Проверяем целостность записанной мнемоники
|
|
||||||
const decryptedMnemonic = decrypt(insertedWallet.mnemonic, userId);
|
|
||||||
if (decryptedMnemonic !== mnemonic) {
|
|
||||||
logger.error({ walletType }, 'Mnemonic verification failed');
|
|
||||||
await db.runAsync(
|
|
||||||
`DELETE FROM crypto_wallets
|
|
||||||
WHERE user_id = ? AND wallet_type = ?`,
|
|
||||||
[userId, walletType]
|
|
||||||
);
|
|
||||||
throw new Error('Mnemonic verification failed');
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info({ walletType, address, derivationPath, userId }, 'Successfully created and verified wallet');
|
|
||||||
return {
|
|
||||||
address,
|
|
||||||
derivationPath,
|
|
||||||
userId,
|
|
||||||
walletType
|
|
||||||
};
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
logger.error({ err: error }, 'Error creating wallet');
|
|
||||||
throw new Error('Failed to create wallet: ' + error.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static async decryptMnemonic(encryptedMnemonic, userId) {
|
|
||||||
try {
|
|
||||||
if (!config.ENCRYPTION_KEY || typeof config.ENCRYPTION_KEY !== 'string') {
|
|
||||||
throw new Error('Encryption key is not configured');
|
|
||||||
}
|
|
||||||
return decrypt(encryptedMnemonic, userId);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error({ err: error }, 'Error decrypting mnemonic');
|
|
||||||
throw new Error('Failed to decrypt mnemonic: ' + error.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default WalletService;
|
|
||||||
|
|||||||
@@ -1,60 +0,0 @@
|
|||||||
import crypto from 'crypto';
|
|
||||||
import config from '../config/config.js';
|
|
||||||
|
|
||||||
const HKDF_SALT_LENGTH = 32;
|
|
||||||
const IV_LENGTH = 16;
|
|
||||||
const KEY_LENGTH = 32;
|
|
||||||
const HKDF_INFO = 'telegram-shop-mnemonic-encryption';
|
|
||||||
|
|
||||||
function deriveKeyHKDF(masterKey, salt, userId) {
|
|
||||||
return crypto.hkdfSync(
|
|
||||||
'sha256',
|
|
||||||
Buffer.from(masterKey, 'utf8'),
|
|
||||||
salt,
|
|
||||||
HKDF_INFO + ':' + userId.toString(),
|
|
||||||
KEY_LENGTH
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function deriveKeyLegacy(masterKey, userId) {
|
|
||||||
return crypto.createHash('sha256')
|
|
||||||
.update(masterKey + userId.toString())
|
|
||||||
.digest();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function encrypt(plaintext, userId) {
|
|
||||||
const salt = crypto.randomBytes(HKDF_SALT_LENGTH);
|
|
||||||
const key = deriveKeyHKDF(config.ENCRYPTION_KEY, salt, userId);
|
|
||||||
const iv = crypto.randomBytes(IV_LENGTH);
|
|
||||||
const cipher = crypto.createCipheriv('aes-256-cbc', key, iv);
|
|
||||||
let ciphertext = cipher.update(plaintext, 'utf8', 'hex');
|
|
||||||
ciphertext += cipher.final('hex');
|
|
||||||
return salt.toString('hex') + ':' + iv.toString('hex') + ':' + ciphertext;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function decrypt(encryptedData, userId) {
|
|
||||||
const parts = encryptedData.split(':');
|
|
||||||
|
|
||||||
if (parts.length === 3) {
|
|
||||||
const salt = Buffer.from(parts[0], 'hex');
|
|
||||||
const key = deriveKeyHKDF(config.ENCRYPTION_KEY, salt, userId);
|
|
||||||
const iv = Buffer.from(parts[1], 'hex');
|
|
||||||
const ciphertext = parts[2];
|
|
||||||
const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv);
|
|
||||||
let decrypted = decipher.update(ciphertext, 'hex', 'utf8');
|
|
||||||
decrypted += decipher.final('utf8');
|
|
||||||
return decrypted;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parts.length === 2) {
|
|
||||||
const key = deriveKeyLegacy(config.ENCRYPTION_KEY, userId);
|
|
||||||
const iv = Buffer.from(parts[0], 'hex');
|
|
||||||
const ciphertext = parts[1];
|
|
||||||
const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv);
|
|
||||||
let decrypted = decipher.update(ciphertext, 'hex', 'utf8');
|
|
||||||
decrypted += decipher.final('utf8');
|
|
||||||
return decrypted;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error('Invalid encrypted data format');
|
|
||||||
}
|
|
||||||
@@ -1,8 +1,6 @@
|
|||||||
import logger from './logger.js';
|
|
||||||
|
|
||||||
export default class ErrorHandler {
|
export default class ErrorHandler {
|
||||||
static async handleError(bot, chatId, error, context) {
|
static async handleError(bot, chatId, error, context) {
|
||||||
logger.error({ err: error, context }, 'Error in handler');
|
console.error(`Error in ${context}:`, error);
|
||||||
|
|
||||||
const errorMessage = process.env.NODE_ENV === 'development'
|
const errorMessage = process.env.NODE_ENV === 'development'
|
||||||
? `Error: ${error.message}`
|
? `Error: ${error.message}`
|
||||||
@@ -11,16 +9,16 @@ export default class ErrorHandler {
|
|||||||
try {
|
try {
|
||||||
await bot.sendMessage(chatId, errorMessage);
|
await bot.sendMessage(chatId, errorMessage);
|
||||||
} catch (sendError) {
|
} catch (sendError) {
|
||||||
logger.error({ err: sendError }, 'Error sending error message');
|
console.error('Error sending error message:', sendError);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static handlePollingError(error) {
|
static handlePollingError(error) {
|
||||||
if (error.code === 'ETELEGRAM') {
|
if (error.code === 'ETELEGRAM') {
|
||||||
logger.error({ err: error }, 'Telegram API Error');
|
console.error('Telegram API Error:', error.message);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
} else {
|
} else {
|
||||||
logger.error({ err: error }, 'Polling error');
|
console.error('Polling error:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user