feat: admin panel - Settings, Categories, Payment Wallets, Seed/Clear data, User Balances
- Settings page: bot token (masked), admin IDs, commission config, WireGuard status - Categories page: CRUD with product count, delete guard - Payment Wallets page: commission wallets display, toggle, percentage - Users page: balance adjustment form (total_balance / bonus_balance) with audit log - Seed & Reset page: seed demo data (5 users, 10 products, 5 wallets, 5 purchases) and clear all data button with confirmation - Dashboard: flash messages for seed/clear success - Fixed seed.js: use dynamic IDs instead of hardcoded to avoid FK violations - Fixed seed.js: clear all tables before seeding to avoid UNIQUE constraints
This commit is contained in:
@@ -206,6 +206,30 @@ textarea { min-height: 80px; resize: vertical; }
|
||||
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); }
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.topnav { flex-direction: column; align-items: flex-start; }
|
||||
.logout-btn { margin-left: 0; }
|
||||
|
||||
39
src/admin/routes/categories.js
Normal file
39
src/admin/routes/categories.js
Normal file
@@ -0,0 +1,39 @@
|
||||
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] = 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'),
|
||||
]);
|
||||
res.send(renderCategoryList(categories, locations));
|
||||
});
|
||||
|
||||
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');
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -12,7 +12,11 @@ router.get('/', async (req, res) => {
|
||||
db.allAsync('SELECT COALESCE(SUM(total_price), 0) as totalRevenue FROM purchases WHERE status = ?', ['completed']),
|
||||
]);
|
||||
|
||||
res.send(renderDashboard({ totalUsers, totalProducts, totalPurchases, totalRevenue }));
|
||||
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 }, message));
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
||||
15
src/admin/routes/paymentWallets.js
Normal file
15
src/admin/routes/paymentWallets.js
Normal file
@@ -0,0 +1,15 @@
|
||||
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;
|
||||
117
src/admin/routes/seed.js
Normal file
117
src/admin/routes/seed.js
Normal file
@@ -0,0 +1,117 @@
|
||||
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', '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 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, 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, locMoscow, catDigital, locMoscow, catPhysical,
|
||||
locSPb, catPremium, locSPb, catVIP, locSPb, catPremium,
|
||||
locBerlin, catStandard, locMoscow, catPhysical,
|
||||
locSPb, catVIP, locBerlin, catStandard]);
|
||||
|
||||
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', '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;
|
||||
22
src/admin/routes/settings.js
Normal file
22
src/admin/routes/settings.js
Normal file
@@ -0,0 +1,22 @@
|
||||
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;
|
||||
@@ -29,4 +29,20 @@ router.post('/:id/toggle-status', async (req, res) => {
|
||||
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;
|
||||
|
||||
@@ -10,6 +10,10 @@ 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 seedRouter from './routes/seed.js';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const app = express();
|
||||
@@ -37,6 +41,10 @@ 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('/payment-wallets', paymentWalletsRouter);
|
||||
app.use('/seed', seedRouter);
|
||||
|
||||
export function startAdminPanel() {
|
||||
const port = parseInt(process.env.ADMIN_PORT || '3001', 10);
|
||||
|
||||
50
src/admin/views/categories.js
Normal file
50
src/admin/views/categories.js
Normal file
@@ -0,0 +1,50 @@
|
||||
import { layout, flash } from './layout.js';
|
||||
|
||||
export function renderCategoryList(categories, locations) {
|
||||
const locOptions = locations.map(l =>
|
||||
`<option value="${l.id}">${escapeHtml(l.country)} / ${escapeHtml(l.city)} / ${escapeHtml(l.district || '')}</option>`
|
||||
).join('');
|
||||
|
||||
const rows = categories.map(c => `<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>
|
||||
<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>`).join('');
|
||||
|
||||
const content = `
|
||||
${flash('')}
|
||||
<details class="form-section">
|
||||
<summary>Add Category</summary>
|
||||
<form method="POST" action="/categories" class="inline-form">
|
||||
<input name="name" placeholder="Category name" required>
|
||||
<select name="location_id">
|
||||
<option value="">-- No location --</option>
|
||||
${locOptions}
|
||||
</select>
|
||||
<button type="submit" class="btn">Add</button>
|
||||
</form>
|
||||
</details>
|
||||
<table>
|
||||
<thead><tr><th>ID</th><th>Name</th><th>Location</th><th>Products</th><th>Actions</th></tr></thead>
|
||||
<tbody>${rows || '<tr><td colspan="5">No categories</td></tr>'}</tbody>
|
||||
</table>`;
|
||||
return layout('Categories', content, 'categories');
|
||||
}
|
||||
|
||||
function escapeHtml(str) {
|
||||
return String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { layout, statCard } from './layout.js';
|
||||
import { layout, statCard, flash } from './layout.js';
|
||||
|
||||
export function renderDashboard(stats) {
|
||||
export function renderDashboard(stats, message) {
|
||||
const cards = [
|
||||
statCard('Total Users', stats.totalUsers),
|
||||
statCard('Total Products', stats.totalProducts),
|
||||
@@ -8,6 +8,6 @@ export function renderDashboard(stats) {
|
||||
statCard('Revenue', `$${(stats.totalRevenue || 0).toFixed(2)}`),
|
||||
].join('');
|
||||
|
||||
const content = `<div class="stats-grid">${cards}</div>`;
|
||||
const content = `${flash(message, 'info')}<div class="stats-grid">${cards}</div>`;
|
||||
return layout('Dashboard', content, 'dashboard');
|
||||
}
|
||||
|
||||
@@ -6,6 +6,10 @@ export function layout(title, content, activeTab = '') {
|
||||
{ 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: '/categories', label: 'Categories', id: 'categories' },
|
||||
{ href: '/payment-wallets', label: 'Payment Wallets', id: 'payment-wallets' },
|
||||
{ href: '/seed', label: 'Seed & Reset', id: 'seed' },
|
||||
];
|
||||
|
||||
const navHtml = nav.map(n =>
|
||||
|
||||
32
src/admin/views/paymentWallets.js
Normal file
32
src/admin/views/paymentWallets.js
Normal file
@@ -0,0 +1,32 @@
|
||||
import { layout } from './layout.js';
|
||||
|
||||
export function renderPaymentWallets(data) {
|
||||
const walletRows = Object.entries(data.wallets).map(([type, addr]) =>
|
||||
`<tr>
|
||||
<td><strong>${type}</strong></td>
|
||||
<td><code>${escapeHtml(addr || 'Not set')}</code></td>
|
||||
</tr>`
|
||||
).join('');
|
||||
|
||||
const content = `
|
||||
<div class="detail-card">
|
||||
<h2>Commission Status</h2>
|
||||
<p><strong>Commission:</strong> <span class="badge badge-${data.commissionEnabled ? 'active' : 'banned'}">${data.commissionEnabled ? 'ON' : 'OFF'}</span></p>
|
||||
<p><strong>Percentage:</strong> ${data.commissionPercent}%</p>
|
||||
</div>
|
||||
|
||||
<div class="detail-card">
|
||||
<h2>Commission Wallet Addresses</h2>
|
||||
<p class="muted">These are the shop's payment wallets. Edit them in the <code>.env</code> file.</p>
|
||||
<table>
|
||||
<thead><tr><th>Currency</th><th>Address</th></tr></thead>
|
||||
<tbody>${walletRows}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
`;
|
||||
return layout('Payment Wallets', content, 'payment-wallets');
|
||||
}
|
||||
|
||||
function escapeHtml(str) {
|
||||
return String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||||
}
|
||||
23
src/admin/views/seed.js
Normal file
23
src/admin/views/seed.js
Normal file
@@ -0,0 +1,23 @@
|
||||
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, 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');
|
||||
}
|
||||
45
src/admin/views/settings.js
Normal file
45
src/admin/views/settings.js
Normal file
@@ -0,0 +1,45 @@
|
||||
import { layout } from './layout.js';
|
||||
|
||||
export function renderSettings(data) {
|
||||
const maskToken = (t) => t ? t.slice(0, 10) + '***' : 'Not set';
|
||||
|
||||
const idList = (ids) => ids.length
|
||||
? ids.map(id => `<code>${escapeHtml(id)}</code>`).join(', ')
|
||||
: '<em>None</em>';
|
||||
|
||||
const walletRows = Object.entries(data.commissionWallets).map(([type, addr]) =>
|
||||
`<tr><td><strong>${type}</strong></td><td><code>${escapeHtml(addr || 'Not set')}</code></td></tr>`
|
||||
).join('');
|
||||
|
||||
const content = `
|
||||
<div class="detail-card">
|
||||
<h2>Bot Configuration</h2>
|
||||
<p><strong>Bot Token:</strong> <code>${maskToken(data.botToken)}</code></p>
|
||||
<p><strong>Admin IDs:</strong> ${idList(data.adminIds)}</p>
|
||||
<p><strong>Super Admin IDs:</strong> ${idList(data.superAdminIds)}</p>
|
||||
</div>
|
||||
|
||||
<div class="detail-card">
|
||||
<h2>Commission Settings</h2>
|
||||
<p><strong>Commission:</strong> <span class="badge badge-${data.commissionEnabled ? 'active' : 'banned'}">${data.commissionEnabled ? 'ON' : 'OFF'}</span></p>
|
||||
<p><strong>Percentage:</strong> ${data.commissionPercent}%</p>
|
||||
<h3>Commission Wallets</h3>
|
||||
<table>
|
||||
<thead><tr><th>Type</th><th>Address</th></tr></thead>
|
||||
<tbody>${walletRows}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="detail-card">
|
||||
<h2>WireGuard</h2>
|
||||
<p><strong>Enabled:</strong> <span class="badge badge-${data.wgEnabled ? 'active' : 'banned'}">${data.wgEnabled ? 'ON' : 'OFF'}</span></p>
|
||||
<p><strong>Endpoint:</strong> <code>${escapeHtml(data.wgEndpoint || 'Not set')}</code></p>
|
||||
<p><strong>Address:</strong> <code>${escapeHtml(data.wgAddress || 'Not set')}</code></p>
|
||||
</div>
|
||||
`;
|
||||
return layout('Settings', content, 'settings');
|
||||
}
|
||||
|
||||
function escapeHtml(str) {
|
||||
return String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||||
}
|
||||
@@ -41,6 +41,17 @@ export function renderUserDetail(user, purchases) {
|
||||
<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>
|
||||
|
||||
Reference in New Issue
Block a user