3a4623a60a
Безопасность: - tests/🆔 скрыть is_correct и explanation для студентов (P0) - SQL injection: limit/offset через placeholder вместо template literal - Stored XSS: stripTags для lesson comments, flashcards, redBook sightings - profile.html: escape e.message в showMsg (XSS через server error) - attachment_url: валидация только /uploads/* путей - requestId: генерировать UUID сервером, не доверять клиенту - register: скрыть token_version из ответа Надёжность: - register: обработка UNIQUE constraint race condition - pet buyBg: re-check баланса внутри транзакции - DB errors: скрыть e.message в testController/questionController/courseController - preferences: лимит 50KB на размер JSON UX: - board.html: debounce 250ms на search input Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
78 lines
2.6 KiB
JavaScript
78 lines
2.6 KiB
JavaScript
'use strict';
|
|
|
|
/* ── Error handling middleware ─────────────────────────────────────────────
|
|
requestId — attach X-Request-Id to every request (use early in middleware chain)
|
|
errorHandler — 4-arg Express error handler (use as last app.use)
|
|
──────────────────────────────────────────────────────────────────────── */
|
|
|
|
const crypto = require('crypto');
|
|
const logger = require('../utils/logger');
|
|
let _errorLogStmt = null;
|
|
function getErrorLogStmt() {
|
|
if (!_errorLogStmt) {
|
|
try {
|
|
const db = require('../db/db');
|
|
_errorLogStmt = db.prepare(
|
|
'INSERT INTO error_log (level, message, stack, route, method, user_id) VALUES (?, ?, ?, ?, ?, ?)'
|
|
);
|
|
} catch {}
|
|
}
|
|
return _errorLogStmt;
|
|
}
|
|
|
|
/**
|
|
* Attaches a unique request ID to req.requestId and sets X-Request-Id header.
|
|
* Honour an incoming X-Request-Id from trusted proxies/gateways when present.
|
|
*/
|
|
function requestId(req, res, next) {
|
|
const id = crypto.randomUUID();
|
|
req.requestId = id;
|
|
res.setHeader('X-Request-Id', id);
|
|
next();
|
|
}
|
|
|
|
/**
|
|
* Global error handler — must be registered AFTER all routes.
|
|
*
|
|
* Classifies errors:
|
|
* operational (4xx) — expected client errors → warn level, message returned as-is
|
|
* programmer (5xx) — unexpected bugs → error level, stack logged, message hidden in prod
|
|
*/
|
|
function errorHandler(err, req, res, _next) {
|
|
const status = err.status || err.statusCode || 500;
|
|
const isOperational = status >= 400 && status < 500;
|
|
const isProd = process.env.NODE_ENV === 'production';
|
|
|
|
const meta = {
|
|
requestId: req.requestId,
|
|
method: req.method,
|
|
path: req.path,
|
|
status,
|
|
userId: req.user?.id,
|
|
role: req.user?.role,
|
|
};
|
|
|
|
if (isOperational) {
|
|
logger.warn(err.message || 'Client error', meta);
|
|
} else {
|
|
logger.error(err.message || 'Unhandled error', {
|
|
...meta,
|
|
stack: !isProd ? err.stack : undefined,
|
|
});
|
|
// Persist to error_log table for admin dashboard
|
|
try {
|
|
const s = getErrorLogStmt();
|
|
if (s) s.run('error', (err.message || 'Unknown').slice(0, 1000), (err.stack || '').slice(0, 4000), req.path, req.method, req.user?.id || null);
|
|
} catch {}
|
|
}
|
|
|
|
const message = isProd && !isOperational
|
|
? 'Internal server error'
|
|
: (err.message || 'Server error');
|
|
|
|
if (res.headersSent) return;
|
|
res.status(status).json({ error: message, requestId: req.requestId });
|
|
}
|
|
|
|
module.exports = { requestId, errorHandler };
|