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

@@ -2,8 +2,11 @@ import express from 'express';
import cookieParser from 'cookie-parser';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
import ejsLayouts from 'express-ejs-layouts';
import logger from '../utils/logger.js';
import { requireAuth, handleLogin, handleLogout, renderLogin } from './auth.js';
import { requireAuth, handleLogin, handleLogout } from './auth.js';
import { csrfMiddleware, validateCsrf } from './csrf.js';
import { asyncHandler, globalErrorHandler } from './errorHandler.js';
import dashboardRouter from './routes/dashboard.js';
import catalogRouter from './routes/catalog.js';
import catalogProductsRouter from './routes/catalogProducts.js';
@@ -22,24 +25,43 @@ import localesRouter from './routes/locales.js';
const __dirname = dirname(fileURLToPath(import.meta.url));
const app = express();
// Configure EJS as view engine with layouts
app.set('view engine', 'ejs');
app.set('views', join(__dirname, 'views'));
app.use(ejsLayouts);
app.set('layout', 'layout');
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')));
// Serve static assets from public directory
app.use(express.static(join(__dirname, 'public')));
app.get('/health', (req, res) => {
res.json({ status: 'ok', uptime: process.uptime() });
});
app.get('/login', (req, res) => {
res.send(renderLogin());
res.render('auth-login', { error: null, layout: false, pageTitle: 'Login' });
});
app.post('/login', handleLogin);
app.get('/logout', handleLogout);
// CSRF token for all authenticated views
app.use(csrfMiddleware);
app.use(requireAuth);
// Apply CSRF validation to all state-changing routes
app.use(validateCsrf);
// Protect uploads behind auth with MIME hardening
app.use('/uploads', express.static(join(__dirname, '..', '..', 'uploads'), {
setHeaders: (res, path) => {
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('Content-Disposition', 'attachment');
}
}));
app.use('/', dashboardRouter);
app.use('/catalog', catalogRouter);
app.use('/catalog', catalogProductsRouter);
@@ -55,6 +77,9 @@ app.use('/payment-wallets', paymentWalletsRouter);
app.use('/seed', seedRouter);
app.use('/locales', localesRouter);
// Global error handler
app.use(globalErrorHandler);
export function startAdminPanel() {
const port = parseInt(process.env.ADMIN_PORT || '3001', 10);
app.listen(port, () => {