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:
@@ -606,6 +606,44 @@ function clearErrorLog(req, res) {
|
||||
res.json({ ok: true });
|
||||
}
|
||||
|
||||
/* ── GET /api/admin/security-log?limit&category&q ──────────────────────
|
||||
Журнал событий безопасности/входа (security_events, миграция 047).
|
||||
category ∈ auth | access_denied | rate_limit; q — поиск по email/ip/route/detail/event. */
|
||||
function getSecurityLog(req, res) {
|
||||
const limit = Math.min(1000, Math.max(1, Number(req.query.limit) || 200));
|
||||
const category = req.query.category;
|
||||
const q = (req.query.q || '').trim();
|
||||
const where = [], args = [];
|
||||
if (['auth', 'access_denied', 'rate_limit'].includes(category)) {
|
||||
where.push('se.category = ?'); args.push(category);
|
||||
}
|
||||
if (q) {
|
||||
where.push('(se.email LIKE ? OR se.ip LIKE ? OR se.route LIKE ? OR se.detail LIKE ? OR se.event LIKE ?)');
|
||||
const like = `%${q}%`;
|
||||
args.push(like, like, like, like, like);
|
||||
}
|
||||
const rows = db.prepare(`
|
||||
SELECT se.*, u.name AS user_name, u.email AS user_email
|
||||
FROM security_events se
|
||||
LEFT JOIN users u ON u.id = se.user_id
|
||||
${where.length ? 'WHERE ' + where.join(' AND ') : ''}
|
||||
ORDER BY se.created_at DESC LIMIT ?
|
||||
`).all(...args, limit);
|
||||
res.json(rows);
|
||||
}
|
||||
|
||||
/* ── DELETE /api/admin/security-log[?days=N] ───────────────────────────
|
||||
Без days — очистить весь журнал; с days=N — прунинг записей старше N дней. */
|
||||
function clearSecurityLog(req, res) {
|
||||
const days = Number(req.query.days);
|
||||
if (Number.isInteger(days) && days > 0) {
|
||||
const r = db.prepare(`DELETE FROM security_events WHERE created_at < datetime('now', ?)`).run(`-${days} days`);
|
||||
return res.json({ ok: true, deleted: r.changes });
|
||||
}
|
||||
const r = db.prepare('DELETE FROM security_events').run();
|
||||
res.json({ ok: true, deleted: r.changes });
|
||||
}
|
||||
|
||||
/* ── GET /api/admin/health ─────────────────────────────────────────── */
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
@@ -832,6 +870,7 @@ module.exports = {
|
||||
clearUserSessions, deleteSession, updateUser, banUser, deleteUser,
|
||||
getFeatures, updateFeatures, getFreeStudentFeatures, updateFreeStudentFeatures,
|
||||
getAuditLog, clearAuditLog, getErrorLog, clearErrorLog, getHealth, getMetrics,
|
||||
getSecurityLog, clearSecurityLog,
|
||||
getTopics, createTopic, updateTopic, deleteTopic,
|
||||
broadcast,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user