diff --git a/backend/src/controllers/permissionsController.js b/backend/src/controllers/permissionsController.js index e7c581e..d932449 100644 --- a/backend/src/controllers/permissionsController.js +++ b/backend/src/controllers/permissionsController.js @@ -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 }; diff --git a/backend/src/routes/permissions.js b/backend/src/routes/permissions.js index e6f4e76..cb6c946 100644 --- a/backend/src/routes/permissions.js +++ b/backend/src/routes/permissions.js @@ -1,6 +1,6 @@ const router = require('express').Router(); const { authMiddleware, requireRole } = require('../middleware/auth'); -const { getPermissions, setPermission, getMyPermissions, getUserPermissions, setUserPermission, resetUserPermissions } = require('../controllers/permissionsController'); +const { getPermissions, setPermission, getMyPermissions, getUserPermissions, setUserPermission, resetUserPermissions, getPermissionLog } = require('../controllers/permissionsController'); router.use(authMiddleware); @@ -10,6 +10,7 @@ router.get('/me', getMyPermissions); router.use(requireRole('admin')); router.get('/', getPermissions); +router.get('/log', getPermissionLog); router.post('/', setPermission); /* ── Per-user overrides ── */ diff --git a/backend/tests/permissions.test.js b/backend/tests/permissions.test.js index fb0c40d..5442c18 100644 --- a/backend/tests/permissions.test.js +++ b/backend/tests/permissions.test.js @@ -191,4 +191,17 @@ describe('Permissions', () => { await inject('POST', '/api/permissions', { role: 'student', permission: 'simulations.access', enabled: true }, adminToken); }); + + // ── 11. A3: история изменений прав ───────────────────────────────────────── + it('GET /api/permissions/log — история (admin видит записи; не-админу 403)', async () => { + await inject('POST', '/api/permissions', + { role: 'teacher', permission: 'shop.manage', enabled: true }, adminToken); + const log = await inject('GET', '/api/permissions/log', null, adminToken); + assert.equal(log.status, 200); + assert.ok(Array.isArray(log.body) && log.body.length >= 1, 'есть записи истории'); + assert.ok('text' in log.body[0] && 'actor' in log.body[0], 'формат записи'); + const fresh = await getToken('student'); + const denied = await inject('GET', '/api/permissions/log', null, fresh.token); + assert.equal(denied.status, 403, 'не-админу недоступно'); + }); }); diff --git a/frontend/js/admin/sections/permissions.js b/frontend/js/admin/sections/permissions.js index 47fec9b..a7a68ec 100644 --- a/frontend/js/admin/sections/permissions.js +++ b/frontend/js/admin/sections/permissions.js @@ -102,8 +102,23 @@ }); } + async function loadPermLog() { + const box = document.getElementById('perm-log'); + if (!box) return; + box.innerHTML = '
Загрузка…
'; + try { + const log = await LS.permissionsLog(); + box.innerHTML = log.length + ? log.map(e => `Изменений пока нет.
'; + } catch (e) { + box.innerHTML = `Ошибка: ${esc(e.message)}
`; + } + } + window.togglePermission = togglePermission; window.filterPermissions = filterPermissions; + window.loadPermLog = loadPermLog; window.AdminSections = window.AdminSections || {}; window.AdminSections.permissions = { diff --git a/js/api.js b/js/api.js index 75e02bf..942c51d 100644 --- a/js/api.js +++ b/js/api.js @@ -1033,7 +1033,7 @@ window.LS = { getFolders, createFolder, renameFolder, deleteFolder, moveFile, getFolderAccess, clearFolderAccess, assignFolder, unassignFolder, getStudentsList, submitWork, resubmitWork, getMySubmissions, getClassSubmissions, reviewSubmission, deleteSubmission, submissionDownloadUrl, - getPermissions, setPermission, getUserPermissions, setUserPermission, resetUserPermissions, + getPermissions, permissionsLog, setPermission, getUserPermissions, setUserPermission, resetUserPermissions, accessCatalog, accessTargets, accessSummary, accessClassOpen, accessMatrix, accessLog, accessRules, accessSetRule, getCourseTemplates, saveCourseTemplate, createFromCourseTemplate, deleteCourseTemplate, getLessonTemplates, saveLessonTemplate, createFromLessonTemplate, deleteLessonTemplate, @@ -1295,6 +1295,7 @@ function submissionDownloadUrl(id) { return `${API}/submissions/${id}/d /* ── permissions (admin only) ────────────────────────────────────────────── */ async function getPermissions() { return req('GET', '/permissions'); } +async function permissionsLog(userId) { return req('GET', userId ? `/permissions/log?user_id=${userId}` : '/permissions/log'); } async function setPermission(role, permission, enabled) { return req('POST', '/permissions', { role, permission, enabled }); } async function getUserPermissions(uid) { return req('GET', `/permissions/users/${uid}`); } async function setUserPermission(uid, permission, enabled) { return req('POST', `/permissions/users/${uid}`, { permission, enabled }); }