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
+7 -1
View File
@@ -3,6 +3,7 @@ const jwt = require('jsonwebtoken');
const db = require('../db/db');
const { BCRYPT_ROUNDS } = require('../config');
const { stripTags } = require('../utils/sanitize');
const { logAuth } = require('../utils/securityLog');
function signToken(user) {
return jwt.sign(
@@ -38,6 +39,7 @@ async function register(req, res, next) {
}
const user = db.prepare('SELECT id, email, name, role FROM users WHERE id = ?').get(lastInsertRowid);
logAuth(req, 'register', { userId: user.id, email });
const token = signToken(user);
res.status(201).json({ token, user });
} catch (err) { next(err); }
@@ -54,10 +56,13 @@ async function login(req, res, next) {
'SELECT id, email, name, role, password_hash, token_version, avatar_url FROM users WHERE email = ?'
).get(email);
if (!user || !(await bcrypt.compare(password, user.password_hash)))
if (!user || !(await bcrypt.compare(password, user.password_hash))) {
logAuth(req, 'login.fail', { userId: user?.id, email });
return res.status(401).json({ error: 'Invalid credentials' });
}
db.prepare("UPDATE users SET last_login = datetime('now') WHERE id = ?").run(user.id);
logAuth(req, 'login.success', { userId: user.id, email: user.email });
const token = signToken(user);
res.json({ token, user: { id: user.id, email: user.email, name: user.name, role: user.role, avatar_url: user.avatar_url } });
@@ -87,6 +92,7 @@ async function updateProfile(req, res, next) {
if (newPassword.length < 8) return res.status(400).json({ error: 'Пароль минимум 8 символов' });
const hash = await bcrypt.hash(newPassword, BCRYPT_ROUNDS);
db.prepare('UPDATE users SET password_hash = ?, token_version = token_version + 1 WHERE id = ?').run(hash, user.id);
logAuth(req, 'password.change', { userId: user.id, email: user.email });
}
const updated = db.prepare('SELECT id, email, name, role, created_at, token_version FROM users WHERE id = ?').get(user.id);