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:
Maxim Dolgolyov
2026-06-03 14:27:58 +03:00
parent 0b0c113181
commit b95b639e75
6 changed files with 118 additions and 3 deletions
+28
View File
@@ -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);
});
});