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:
46
src/admin/csrf.js
Normal file
46
src/admin/csrf.js
Normal file
@@ -0,0 +1,46 @@
|
||||
import crypto from 'crypto';
|
||||
import logger from '../utils/logger.js';
|
||||
|
||||
const CSRF_COOKIE = '_csrf';
|
||||
const CSRF_HEADER = 'x-csrf-token';
|
||||
|
||||
function generateToken() {
|
||||
return crypto.randomBytes(32).toString('hex');
|
||||
}
|
||||
|
||||
export function csrfMiddleware(req, res, next) {
|
||||
let token = req.cookies?.[CSRF_COOKIE];
|
||||
if (!token) {
|
||||
token = generateToken();
|
||||
res.cookie(CSRF_COOKIE, token, {
|
||||
httpOnly: false,
|
||||
sameSite: 'strict',
|
||||
path: '/',
|
||||
secure: process.env.NODE_ENV === 'production'
|
||||
});
|
||||
}
|
||||
res.locals.csrfToken = token;
|
||||
next();
|
||||
}
|
||||
|
||||
export function validateCsrf(req, res, next) {
|
||||
// Only validate state-changing methods
|
||||
if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) return next();
|
||||
|
||||
const token = req.body?._csrf || req.headers[CSRF_HEADER];
|
||||
const cookieToken = req.cookies?.[CSRF_COOKIE];
|
||||
|
||||
if (!token || !cookieToken) {
|
||||
logger.warn({ ip: req.ip, url: req.originalUrl, method: req.method }, 'CSRF token missing');
|
||||
return res.status(403).send('CSRF token missing. Please reload the page.');
|
||||
}
|
||||
|
||||
const provided = Buffer.from(token, 'utf8');
|
||||
const expected = Buffer.from(cookieToken, 'utf8');
|
||||
if (provided.length !== expected.length || !crypto.timingSafeEqual(provided, expected)) {
|
||||
logger.warn({ ip: req.ip, url: req.originalUrl, method: req.method }, 'CSRF token mismatch');
|
||||
return res.status(403).send('CSRF token invalid. Please reload the page.');
|
||||
}
|
||||
|
||||
next();
|
||||
}
|
||||
Reference in New Issue
Block a user