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
@@ -0,0 +1,33 @@
-- 047_security_events.sql
-- Журнал событий безопасности и входа (Tier 1 + Tier 2 плана логирования).
--
-- Отдельная таблица, НЕ admin_audit_log, потому что:
-- • автор часто аноним (у неудачного логина ещё нет user_id);
-- • объём может быть большим (боты долбят логин / rate-limit);
-- • свой ретеншн (чистится агрессивнее, чем осознанные админ-действия).
-- admin_audit_log остаётся домом для привилегированных действий (Tier 3).
--
-- category:
-- auth — login.success | login.fail | register | password.change
-- access_denied — forbidden (роль) | perm_denied (разрешение)
-- rate_limit — rate_limited (превышение лимита запросов)
CREATE TABLE IF NOT EXISTS security_events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
category TEXT NOT NULL CHECK (category IN ('auth','access_denied','rate_limit')),
event TEXT NOT NULL,
user_id INTEGER REFERENCES users(id) ON DELETE SET NULL,
email TEXT, -- email из попытки/связанный (для анонимных)
ip TEXT,
user_agent TEXT,
method TEXT,
route TEXT,
detail TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_sec_events_date ON security_events (created_at DESC);
CREATE INDEX IF NOT EXISTS idx_sec_events_category ON security_events (category, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_sec_events_email ON security_events (email);
CREATE INDEX IF NOT EXISTS idx_sec_events_ip ON security_events (ip);
CREATE INDEX IF NOT EXISTS idx_sec_events_user ON security_events (user_id);