feat(admin): журнал событий безопасности (Tier 1-2) + аудит чувствительных действий (Tier 3)
- security_events (миграция 047) + utils/securityLog.js (defensive, lazy stmt) - Tier 1: login.success/fail, register, password.change в authController - Tier 2: 403 (роль/разрешение) в middleware/auth, rate_limited в rateLimit - Tier 3: audit() на выдачу доступа (access), начисление/сброс XP (gam), модерацию аватаров - API GET/DELETE /api/admin/security-log (фильтр по категории + поиск, прунинг по дням) - Frontend: вкладка «Безопасность» в admin.html + loadSecurityLog, расширены ACTION_LABELS Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
'use strict';
|
||||
/* Security / auth event logger → security_events (миграция 047).
|
||||
*
|
||||
* Парная утилита к utils/audit.js:
|
||||
* • audit() — осознанные привилегированные действия (Tier 3) в admin_audit_log
|
||||
* • logSecurity() — события безопасности/входа (Tier 1+2): аноним, большой объём, свой ретеншн
|
||||
*
|
||||
* Логирование НИКОГДА не должно ронять запрос — всё в try/catch.
|
||||
* Statement готовится лениво: server.js запускается отдельно от миграций,
|
||||
* поэтому таблицы может ещё не быть на момент require().
|
||||
*/
|
||||
const db = require('../db/db');
|
||||
|
||||
let _stmt = null;
|
||||
function stmt() {
|
||||
if (!_stmt) {
|
||||
_stmt = db.prepare(
|
||||
`INSERT INTO security_events
|
||||
(category, event, user_id, email, ip, user_agent, method, route, detail)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
||||
);
|
||||
}
|
||||
return _stmt;
|
||||
}
|
||||
|
||||
function _ip(req) { return (req && (req.ip || req.socket?.remoteAddress)) || ''; }
|
||||
function _ua(req) { const ua = req?.headers?.['user-agent']; return ua ? String(ua).slice(0, 300) : ''; }
|
||||
function _route(req) { return String(req?.originalUrl || req?.url || '').split('?')[0]; }
|
||||
|
||||
/**
|
||||
* Записать событие безопасности.
|
||||
* @param {object} opts
|
||||
* @param {object} [opts.req] Express request (для IP/UA/route/user)
|
||||
* @param {string} opts.category 'auth' | 'access_denied' | 'rate_limit'
|
||||
* @param {string} opts.event 'login.fail' | 'forbidden' | 'rate_limited' | ...
|
||||
* @param {number} [opts.userId] явный id (иначе берётся req.user.id)
|
||||
* @param {string} [opts.email] email из попытки/связанный
|
||||
* @param {string} [opts.detail] человекочитаемая деталь
|
||||
*/
|
||||
function logSecurity({ req, category, event, userId, email, detail } = {}) {
|
||||
try {
|
||||
const uid = userId != null ? userId : (req?.user?.id ?? null);
|
||||
stmt().run(
|
||||
category, event, uid, email || null,
|
||||
_ip(req), _ua(req), req?.method || null, _route(req), detail || null
|
||||
);
|
||||
} catch (e) { console.error('[securityLog]', e.message); }
|
||||
}
|
||||
|
||||
/* Эргономичные хелперы */
|
||||
const logAuth = (req, event, extra = {}) => logSecurity({ req, category: 'auth', event, ...extra });
|
||||
const logDenied = (req, event, detail) => logSecurity({ req, category: 'access_denied', event, detail });
|
||||
const logRateLimit = (req, detail) => logSecurity({ req, category: 'rate_limit', event: 'rate_limited', detail });
|
||||
|
||||
module.exports = { logSecurity, logAuth, logDenied, logRateLimit };
|
||||
Reference in New Issue
Block a user