diff --git a/backend/src/controllers/permissionsController.js b/backend/src/controllers/permissionsController.js index 5a5ef61..89914f4 100644 --- a/backend/src/controllers/permissionsController.js +++ b/backend/src/controllers/permissionsController.js @@ -157,6 +157,36 @@ function resetUserPermissions(req, res) { res.json({ ok: true }); } +/* ── POST /api/permissions/class/:id/bulk { permission, enabled } ────────── + Выставить личное правило (user_permissions) сразу всем ученикам класса. + enabled: 1 (вкл) | 0 (выкл) | null/'inherit' (снять оверрайд → наследование роли). */ +function setClassPermission(req, res) { + const cid = Number(req.params.id); + const { permission } = req.body || {}; + let { enabled } = req.body || {}; + if (!Number.isInteger(cid) || cid <= 0) return res.status(400).json({ error: 'неверный id класса' }); + if (!ALL_PERMISSIONS.find(p => p.key === permission && p.role === 'student')) + return res.status(400).json({ error: 'Unknown student permission' }); + + const members = db.prepare(` + SELECT u.id FROM class_members cm JOIN users u ON u.id = cm.user_id + WHERE cm.class_id = ? AND u.role IN ('student','free_student')`).all(cid); + const inherit = (enabled === null || enabled === undefined || enabled === 'inherit'); + const val = (enabled === 1 || enabled === true || enabled === '1') ? 1 : 0; + + const del = db.prepare('DELETE FROM user_permissions WHERE user_id = ? AND permission = ?'); + const up = db.prepare('INSERT OR REPLACE INTO user_permissions (user_id, permission, enabled) VALUES (?, ?, ?)'); + const bump = db.prepare('UPDATE users SET token_version = token_version + 1 WHERE id = ?'); + db.transaction(() => { + for (const m of members) { + if (inherit) del.run(m.id, permission); else up.run(m.id, permission, val); + bump.run(m.id); // user-level: точечно обновляем сессию каждого затронутого ученика + } + })(); + audit(req, 'permission.class_bulk', `class:${cid}/${permission}`, inherit ? 'inherit' : `enabled=${val}`); + res.json({ ok: true, affected: members.length }); +} + /* ── GET /api/permissions/log?user_id= — история изменений прав (admin) ── */ function getPermissionLog(req, res) { const uid = req.query.user_id ? Number(req.query.user_id) : null; @@ -195,4 +225,4 @@ function getPermissionLog(req, res) { res.json(out); } -module.exports = { getPermissions, setPermission, seedDefaults, ALL_PERMISSIONS, getMyPermissions, getUserPermissions, setUserPermission, resetUserPermissions, getPermissionLog }; +module.exports = { getPermissions, setPermission, seedDefaults, ALL_PERMISSIONS, getMyPermissions, getUserPermissions, setUserPermission, resetUserPermissions, getPermissionLog, setClassPermission }; diff --git a/backend/src/routes/permissions.js b/backend/src/routes/permissions.js index cb6c946..97128e6 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, getPermissionLog } = require('../controllers/permissionsController'); +const { getPermissions, setPermission, getMyPermissions, getUserPermissions, setUserPermission, resetUserPermissions, getPermissionLog, setClassPermission } = require('../controllers/permissionsController'); router.use(authMiddleware); @@ -13,6 +13,9 @@ router.get('/', getPermissions); router.get('/log', getPermissionLog); router.post('/', setPermission); +/* ── Массово по классу (личные оверрайды всем ученикам класса) ── */ +router.post('/class/:id/bulk', requireRole('admin'), setClassPermission); + /* ── Per-user overrides ── */ router.get('/users/:id', getUserPermissions); router.post('/users/:id', setUserPermission); diff --git a/backend/tests/permissions.test.js b/backend/tests/permissions.test.js index 1bf2d42..7c83048 100644 --- a/backend/tests/permissions.test.js +++ b/backend/tests/permissions.test.js @@ -218,4 +218,32 @@ describe('Permissions', () => { const denied = await inject('GET', '/api/permissions/log', null, fresh.token); assert.equal(denied.status, 403, 'не-админу недоступно'); }); + + // ── B6: массово по классу ────────────────────────────────────────────────── + it('B6: массовое право классу — личный оверрайд всем ученикам + сброс + валидация', async () => { + const cr = await inject('POST', '/api/classes', { name: 'PermBulk Class' }, adminToken); + assert.ok(cr.status < 300, JSON.stringify(cr.body)); + const cid = db.prepare('SELECT id FROM classes WHERE name = ?').get('PermBulk Class').id; + await inject('POST', `/api/classes/${cid}/members`, { user_id: studentUser.userId }, adminToken); + + const off = await inject('POST', `/api/permissions/class/${cid}/bulk`, + { permission: 'shop.purchase', enabled: false }, adminToken); + assert.equal(off.status, 200); + assert.ok(off.body.affected >= 1, 'затронут хотя бы один ученик'); + const row = db.prepare('SELECT enabled FROM user_permissions WHERE user_id = ? AND permission = ?') + .get(studentUser.userId, 'shop.purchase'); + assert.ok(row && row.enabled === 0, 'личный оверрайд выключен у ученика класса'); + + // сброс (наследование роли) + await inject('POST', `/api/permissions/class/${cid}/bulk`, + { permission: 'shop.purchase', enabled: null }, adminToken); + const gone = db.prepare('SELECT 1 FROM user_permissions WHERE user_id = ? AND permission = ?') + .get(studentUser.userId, 'shop.purchase'); + assert.ok(!gone, 'оверрайд снят'); + + // teacher-право для массовой студенческой операции отклоняется + const bad = await inject('POST', `/api/permissions/class/${cid}/bulk`, + { permission: 'questions.manage', enabled: true }, adminToken); + assert.equal(bad.status, 400); + }); }); diff --git a/frontend/admin.html b/frontend/admin.html index 4f9ba24..31af389 100644 --- a/frontend/admin.html +++ b/frontend/admin.html @@ -1319,6 +1319,8 @@
+ +Ошибка загрузки: ${esc(e.message)}
`; } } + /* ── Массово по классу ── */ + async function loadBulk() { + const root = document.getElementById('perm-bulk'); + if (!root) return; + try { _bulkTargets = await LS.accessTargets(); } + catch { _bulkTargets = { classes: [] }; } + renderBulk(); + } + function renderBulk() { + const root = document.getElementById('perm-bulk'); + if (!root || !_permData) return; + const classes = (_bulkTargets && _bulkTargets.classes) || []; + const studentDefs = (_permData.definitions || []).filter(d => d.role === 'student'); + const opts = classes.map(c => ``).join(''); + const cls = classes.find(c => c.id === _bulkClassId); + const btn = (key, val, label, bg) => ``; + const rows = (cls ? studentDefs : []).map(d => ` +Нет студенческих прав.
') : 'Выберите класс.
'}