feat(admin): SmartAdmin template redesign + security hardening

- Migrated all admin views from inline JS string templates to EJS
- Integrated SmartAdmin template with dark sidebar, fixed header, CSS grid
- Added express-ejs-layouts for master layout wrapper
- Security:
  - CSRF protection (double-submit cookie)
  - Rate limiting on /login (5/15min)
  - Token revocation via jti + globalLogoutTimestamp
  - Re-auth (reauth_token) for destructive endpoints
  - Settings whitelist (ALLOWED_KEYS) + removed process.exit
  - Seed phrases no longer rendered in HTML (CSV export only)
  - Multer fileFilter for image uploads + safe filename generation
  - SQL injection fix (currency column allowlist)
  - Global error handler + asyncHandler wrapper
- New files: csrf.js, errorHandler.js, error.ejs, all EJS templates
- SmartAdmin assets: CSS, icons, webfonts, plugins, scripts
This commit is contained in:
NW
2026-07-06 17:42:54 +01:00
parent 8d85776135
commit d4c476002c
1010 changed files with 256079 additions and 1625 deletions

View File

@@ -4,7 +4,6 @@ import config from '../../config/config.js';
import WalletUtils from '../../utils/walletUtils.js';
import { decrypt } from '../../utils/encryption.js';
import logger from '../../utils/logger.js';
import { renderWalletLayout } from '../views/wallets.js';
const router = Router();
@@ -81,31 +80,8 @@ router.get('/', async (req, res) => {
const seedsPaid = lastPaidAmount >= currentCommission && currentCommission > 0;
const seedsUnlocked = seedsRequested && seedsPaid;
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]',
});
}
}
}
// Seed phrases are NEVER rendered inline in HTML (C2 fix).
// They are only available via the authenticated CSV export endpoint.
const stats = {
...walletStats,
commissionRate: config.COMMISSION_PERCENT,
@@ -119,7 +95,15 @@ router.get('/', async (req, res) => {
seedsPaid,
};
res.send(renderWalletLayout(users, selectedUser, wallets, stats, seedPhrases, seedsUnlocked));
res.render('wallets', {
title: 'Wallets',
users,
selectedUser,
wallets,
stats,
seedsUnlocked,
hasStats: true,
});
} catch (error) {
logger.error({ err: error }, 'Error loading wallets page');
res.status(500).send('Error loading wallets page');