fe122b7681
- 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>
56 lines
2.7 KiB
JavaScript
56 lines
2.7 KiB
JavaScript
'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 };
|