1 Commits

Author SHA1 Message Date
1323ed5
8ff01f5f68 Merge pull request 'main' (#35) from main into refactoring
Reviewed-on: #35
2024-12-04 20:10:47 +00:00
108 changed files with 4682 additions and 8460 deletions

View File

@@ -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
View File

@@ -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

View File

@@ -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
View File

@@ -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, с возможностью работы с криптовалютами и традиционными средствами. Проект ориентирован на пользователей, которые ценят удобство, безопасность и скорость совершения покупок. Для администраторов — это мощный инструмент для управления товаром, пользователями и финансовыми потоками магазина.

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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"

View File

@@ -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');
});
});

View File

@@ -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 };

View File

@@ -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; }
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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');
});
}

View File

@@ -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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}

View File

@@ -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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;'); }

View File

@@ -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>`;
}

View File

@@ -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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}

View File

@@ -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');
}

View File

@@ -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>` : '';
}

View File

@@ -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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}

View File

@@ -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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}

View File

@@ -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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}

View File

@@ -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');
}

View File

@@ -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');
}

View File

@@ -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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}

View File

@@ -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');
}

View File

@@ -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');
}

View File

@@ -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
}
}; };

View File

@@ -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;

View File

@@ -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);
} }
}; };

View File

@@ -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;

View File

@@ -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;
} }
} }

View File

@@ -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;
} }

View File

@@ -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);
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -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);
} }
} }

View File

@@ -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.');
} }
} }

View File

@@ -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.');
}
}
}

View File

@@ -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}`}
]]
}
}
);
}
}

View File

@@ -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}` }
]]
}
}
);
}
}

View File

@@ -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.');
}
}
}

View File

@@ -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.');
}
}
}

View File

@@ -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.');
}
}
}

View File

@@ -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.');
}
}
}

View File

@@ -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;
}
}

View File

@@ -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.');
}
}
}

View File

@@ -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;
}
}

View File

@@ -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,
};

View File

@@ -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;
}
}
}

View File

@@ -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.');
}
}
}

View File

@@ -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;
}
}

View File

@@ -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.');
}
}
}

View File

@@ -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.');
}
}
}

View File

@@ -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.');
} }
} }

View File

@@ -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.');
} }
} }

View File

@@ -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.');
} }
} }

View File

@@ -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);
} }
} }

View 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';
}
}

View File

@@ -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.');
}
}
}

View File

@@ -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);
}
}

View File

@@ -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' }]] }
});
}
}
}

View File

@@ -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');
}
}

View File

@@ -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.');
}
}
}

View File

@@ -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,
};

View File

@@ -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' }]] }
});
}
}
}

View File

@@ -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.');
}
}
}

View File

@@ -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();

View File

@@ -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());
}

View File

@@ -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');
}

View File

@@ -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');
}

View File

@@ -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');
}

View File

@@ -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');
}

View File

@@ -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');
}

View File

@@ -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');
}

View File

@@ -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');
}

View File

@@ -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;

View File

@@ -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();

View File

@@ -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();

View File

@@ -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);
});
}

View File

@@ -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');
}
}

View File

@@ -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) {

View File

@@ -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');
}
} }
} }

View File

@@ -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');
}
} }
} }

View File

@@ -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');
}
} }
} }

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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');
}

View File

@@ -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