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:
Maxim Dolgolyov
2026-06-01 15:28:21 +03:00
parent 30626e0928
commit fe122b7681
12 changed files with 262 additions and 2 deletions
+6 -1
View File
@@ -1,6 +1,7 @@
const jwt = require('jsonwebtoken');
const db = require('../db/db');
const registry = require('../permissions/registry');
const { logDenied } = require('../utils/securityLog');
/* ── Default values for role_permissions — sourced from central registry ── */
const PERM_DEFAULTS = registry.buildDefaultsMap();
@@ -34,6 +35,7 @@ function authMiddleware(req, res, next) {
function requireRole(...roles) {
return (req, res, next) => {
if (!roles.includes(req.user?.role)) {
if (req.user) logDenied(req, 'forbidden', `роль ${req.user.role}; требуется ${roles.join('/')}`);
return res.status(403).json({ error: 'Forbidden' });
}
next();
@@ -53,7 +55,9 @@ function requirePermission(key) {
'SELECT enabled FROM user_permissions WHERE user_id = ? AND permission = ?'
).get(uid, key);
if (userRow !== undefined) {
return userRow.enabled === 1 ? next() : res.status(403).json({ error: 'Permission denied' });
if (userRow.enabled === 1) return next();
logDenied(req, 'perm_denied', key);
return res.status(403).json({ error: 'Permission denied' });
}
// 2. Role-level
@@ -62,6 +66,7 @@ function requirePermission(key) {
).get(role, key);
const enabled = roleRow !== undefined ? roleRow.enabled === 1 : (PERM_DEFAULTS[role]?.[key] ?? false);
if (enabled) return next();
logDenied(req, 'perm_denied', key);
return res.status(403).json({ error: 'Permission denied' });
};
}
+3
View File
@@ -1,4 +1,5 @@
/* Simple in-memory rate limiter — no external dependency needed */
const { logRateLimit } = require('../utils/securityLog');
// Clean stale entries every 5 minutes across all stores
const _allStores = new Set();
@@ -35,6 +36,8 @@ module.exports = function rateLimit({ windowMs = 60_000, max = 10, message = 'To
store.set(key, entry);
if (entry.count > max) {
// Log once per window (on first crossing) to avoid spamming the security log.
if (entry.count === max + 1) logRateLimit(req, `лимит ${max}/${Math.round(windowMs / 1000)}с`);
const retryAfter = Math.ceil((entry.resetAt - now) / 1000);
res.set('Retry-After', retryAfter);
return res.status(429).json({ error: message });