feat(permissions): B6 — массовая выдача права классу (личный оверрайд всем ученикам)
POST /api/permissions/class/:id/bulk { permission, enabled } (admin, явный
requireRole) — выставляет user_permissions всем ученикам класса (1/0/null=сброс),
точечный token_version bump каждому. Валидация: только студенческие ключи.
Клиент LS.setClassPermission. В админке «Доступ · роли» — блок «Массово по
классу»: выбор класса → у каждого права «включить/выключить всем / сбросить».
Тест: оверрайд всем + сброс + отклонение teacher-ключа. Backend 221 pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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 };
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user