feat(permissions): A3 — история изменений прав (endpoint + UI)

GET /api/permissions/log (admin-only) — последние изменения ролевых прав (или
?user_id= для личных оверрайдов) из admin_audit_log; читаемый текст («включил
«X» для роли «учитель»») с резолвом меток через registry. Клиент LS.permissionsLog.
Вкладка «Доступ · роли»: блок «История изменений прав ролей» с кнопкой «Показать».
Тест: admin видит записи, не-админу 403. permissions 13/13.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maxim Dolgolyov
2026-06-03 14:14:56 +03:00
parent 1b78f675f8
commit 7d474b40c0
5 changed files with 71 additions and 3 deletions
@@ -159,4 +159,42 @@ function resetUserPermissions(req, res) {
res.json({ ok: true });
}
module.exports = { getPermissions, setPermission, seedDefaults, ALL_PERMISSIONS, getMyPermissions, getUserPermissions, setUserPermission, resetUserPermissions };
/* ── GET /api/permissions/log?user_id= — история изменений прав (admin) ── */
function getPermissionLog(req, res) {
const uid = req.query.user_id ? Number(req.query.user_id) : null;
const rows = uid
? db.prepare(`
SELECT a.action, a.target, a.detail, a.created_at, u.name AS actor
FROM admin_audit_log a LEFT JOIN users u ON u.id = a.admin_id
WHERE a.action LIKE 'permission.user%' AND (a.target = ? OR a.target LIKE ?)
ORDER BY a.id DESC LIMIT 50`).all('user:' + uid, 'user:' + uid + '/%')
: db.prepare(`
SELECT a.action, a.target, a.detail, a.created_at, u.name AS actor
FROM admin_audit_log a LEFT JOIN users u ON u.id = a.admin_id
WHERE a.action = 'permission.set'
ORDER BY a.id DESC LIMIT 50`).all();
const labelOf = {};
for (const k of registry.listKeys()) labelOf[k] = registry.PERMISSIONS[k].label;
const roleName = (r) => (r === 'teacher' ? 'учитель' : r === 'student' ? 'ученик' : r);
const onoff = (d) => (/enabled=1/.test(d || '') ? 'включил' : /enabled=0/.test(d || '') ? 'выключил' : 'изменил');
const out = rows.map(r => {
let text;
if (r.action === 'permission.set') {
const m = /^role:([^/]+)\/(.+)$/.exec(r.target || '');
const key = m ? m[2] : '';
text = `${onoff(r.detail)} «${labelOf[key] || key}» для роли «${roleName(m ? m[1] : '')}»`;
} else if (r.action === 'permission.user_set') {
const m = /^user:\d+\/(.+)$/.exec(r.target || '');
const key = m ? m[1] : '';
text = `${onoff(r.detail)} личное «${labelOf[key] || key}»`;
} else { // permission.user_reset
text = r.detail ? `сбросил личное «${labelOf[r.detail] || r.detail}»` : 'сбросил все личные правила';
}
return { actor: r.actor || '—', text, at: r.created_at };
});
res.json(out);
}
module.exports = { getPermissions, setPermission, seedDefaults, ALL_PERMISSIONS, getMyPermissions, getUserPermissions, setUserPermission, resetUserPermissions, getPermissionLog };