diff --git a/backend/src/permissions/registry.js b/backend/src/permissions/registry.js index f34403e..0b10acd 100644 --- a/backend/src/permissions/registry.js +++ b/backend/src/permissions/registry.js @@ -162,6 +162,24 @@ const PERMISSIONS = { }, }; +/* Группы для секций в админ-UI (один источник; byRole проставляет group). */ +const GROUP = { + // teacher + 'questions.manage': 'Вопросы', 'questions.delete': 'Вопросы', + 'students.invite': 'Класс и ученики', 'sessions.reset': 'Класс и ученики', + 'results.export': 'Класс и ученики', 'classes.manage': 'Класс и ученики', + 'schedule.manage': 'Класс и ученики', 'announcements.send': 'Класс и ученики', + 'library.upload': 'Библиотека', 'library.folders': 'Библиотека', + 'templates.manage': 'Курсы и шаблоны', 'templates.public': 'Курсы и шаблоны', + 'courses.manage': 'Курсы и шаблоны', 'courses.interactive': 'Курсы и шаблоны', + 'shop.manage': 'Геймификация', 'gamification.manage': 'Геймификация', + // student + 'tests.free': 'Тесты и активность', 'board.post': 'Тесты и активность', + 'profile.edit': 'Профиль', + 'shop.purchase': 'Геймификация', 'gamification.challenges': 'Геймификация', + 'theory.access': 'Контент', 'simulations.access': 'Контент', 'simulations.quiz': 'Контент', +}; + /** * Check whether a given permission key exists in the registry. * Used by perm() helper in auth.js to fail early on typos. @@ -182,7 +200,7 @@ function listKeys() { function byRole(role) { return Object.entries(PERMISSIONS) .filter(([, v]) => v.role === role) - .map(([key, v]) => ({ key, role: v.role, default: v.default, label: v.label, desc: v.desc, requireConfirmOff: !!v.requireConfirmOff, requires: v.requires || [] })); + .map(([key, v]) => ({ key, role: v.role, default: v.default, label: v.label, desc: v.desc, requireConfirmOff: !!v.requireConfirmOff, requires: v.requires || [], group: GROUP[key] || 'Прочее' })); } /** diff --git a/backend/tests/permissions.test.js b/backend/tests/permissions.test.js index 6fc2160..1bf2d42 100644 --- a/backend/tests/permissions.test.js +++ b/backend/tests/permissions.test.js @@ -197,6 +197,15 @@ describe('Permissions', () => { { role: 'student', permission: 'simulations.access', enabled: true }, adminToken); }); + // ── B5: группы прав в определениях ───────────────────────────────────────── + it('B5: GET /api/permissions — у каждого определения есть group', async () => { + const res = await inject('GET', '/api/permissions', null, adminToken); + assert.equal(res.status, 200); + assert.ok(Array.isArray(res.body.definitions) && res.body.definitions.length > 0); + assert.ok(res.body.definitions.every(d => typeof d.group === 'string' && d.group.length > 0), + 'у каждого определения есть непустой group'); + }); + // ── 11. A3: история изменений прав ───────────────────────────────────────── it('GET /api/permissions/log — история (admin видит записи; не-админу 403)', async () => { await inject('POST', '/api/permissions', diff --git a/frontend/js/admin/sections/permissions.js b/frontend/js/admin/sections/permissions.js index a7a68ec..f63a786 100644 --- a/frontend/js/admin/sections/permissions.js +++ b/frontend/js/admin/sections/permissions.js @@ -15,6 +15,35 @@ } } + function permCard(role, def, en, labelOf) { + const enabled = en[def.key]; + const reqs = def.requires || []; + const unmet = reqs.filter(r => !en[r]); + const blocked = unmet.length > 0; // зависимость не выполнена → право неактивно + const effective = enabled && !blocked; + const isModified = (enabled ? 1 : 0) !== def.default; + const modDot = isModified + ? `` + : ''; + const reqNote = reqs.length + ? `
${blocked ? 'Требует: ' + unmet.map(r => esc(labelOf[r] || r)).join(', ') : 'Зависит от: ' + reqs.map(r => esc(labelOf[r] || r)).join(', ')}
` + : ''; + return ` +
+
+
${esc(def.label)}${modDot}
+
${esc(def.desc)}
+ ${reqNote} +
+ +
`; + } + function renderPermissions() { if (!_permData) return; const { permissions, definitions } = _permData; @@ -23,37 +52,42 @@ const defs = definitions.filter(d => d.role === role); const en = {}, labelOf = {}; defs.forEach(d => { en[d.key] = permissions[role]?.[d.key] ?? d.default; labelOf[d.key] = d.label; }); - container.innerHTML = defs.map(def => { - const enabled = en[def.key]; - const reqs = def.requires || []; - const unmet = reqs.filter(r => !en[r]); - const blocked = unmet.length > 0; // зависимость не выполнена → право неактивно - const effective = enabled && !blocked; - const isModified = (enabled ? 1 : 0) !== def.default; - const modDot = isModified - ? `` - : ''; - const reqNote = reqs.length - ? `
${blocked ? 'Требует: ' + unmet.map(r => esc(labelOf[r] || r)).join(', ') : 'Зависит от: ' + reqs.map(r => esc(labelOf[r] || r)).join(', ')}
` - : ''; - return ` -
-
-
${esc(def.label)}${modDot}
-
${esc(def.desc)}
- ${reqNote} -
- -
`; - }).join(''); + // группировка по def.group (порядок групп — по первому появлению) + const order = [], byGroup = {}; + defs.forEach(d => { const g = d.group || 'Прочее'; if (!byGroup[g]) { byGroup[g] = []; order.push(g); } byGroup[g].push(d); }); + const grpBtn = (g, on) => ``; + container.style.display = 'block'; // секции групп блоками (контейнер сам — .perm-grid) + container.innerHTML = order.map(g => ` +
+
+
${esc(g)}
+
${grpBtn(g, true)}${grpBtn(g, false)}
+
+
${byGroup[g].map(def => permCard(role, def, en, labelOf)).join('')}
+
`).join(''); }); } + async function togglePermGroup(role, group, enable) { + const defs = (_permData.definitions || []).filter(d => d.role === role && (d.group || 'Прочее') === group); + if (!defs.length) return; + if (!enable && defs.some(d => d.requireConfirmOff)) { + const roleLabel = role === 'teacher' ? 'Учитель' : 'Ученик'; + const ok = await LS.confirm( + `Выключить все права группы «${group}» для роли «${roleLabel}»? Затронет всех пользователей роли.`, + { title: 'Подтвердите выключение группы', confirmText: 'Выключить все' } + ); + if (!ok) return; + } + try { + await Promise.all(defs.map(d => LS.setPermission(role, d.key, enable))); + if (!_permData.permissions[role]) _permData.permissions[role] = {}; + defs.forEach(d => { _permData.permissions[role][d.key] = enable; }); + renderPermissions(); + LS.toast(enable ? `Группа «${group}» включена` : `Группа «${group}» выключена`, 'success'); + } catch (e) { LS.toast('Ошибка: ' + e.message, 'error'); load(); } + } + async function togglePermission(role, key, enabled, checkbox) { if (!enabled) { const def = (_permData.definitions || []).find(d => d.role === role && d.key === key); @@ -117,6 +151,7 @@ } window.togglePermission = togglePermission; + window.togglePermGroup = togglePermGroup; window.filterPermissions = filterPermissions; window.loadPermLog = loadPermLog;