Files
Learn_System/backend/src/utils/securityLog.js
T
Maxim Dolgolyov fe122b7681 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>
2026-06-01 15:28:21 +03:00

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 };