feat(permissions): A1 — зависимости между правами (requires) + план переработки

registry: поле requires (questions.delete→manage, templates.public→manage,
courses.interactive→manage, simulations.quiz→access), проброшено в byRole.
auth.requirePermission: вынесен isEnabled(); право = own AND все requires
(дочернее не работает без родителя). /me и /users/🆔 effective с учётом
requires + requires в ответе. UI permissions.js: каскад — дочернее с
невыполненной зависимостью неактивно (тумблер заблокирован + «Требует: …»).
Тест зависимости. План: plans/permissions-rework/PLAN.md. Backend 216 pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maxim Dolgolyov
2026-06-03 14:10:20 +03:00
parent e37432d812
commit 9ac2a612e0
6 changed files with 117 additions and 24 deletions
+17
View File
@@ -174,4 +174,21 @@ describe('Permissions', () => {
}, adminToken);
assert.equal(res.status, 400);
});
// ── 10. A1: зависимости (requires) ─────────────────────────────────────────
it('зависимость requires: simulations.quiz неэффективен при выключенном simulations.access', async () => {
const off = await inject('POST', '/api/permissions',
{ role: 'student', permission: 'simulations.access', enabled: false }, adminToken);
assert.equal(off.status, 200);
const view = await inject('GET', `/api/permissions/users/${studentUser.userId}`, null, adminToken);
assert.equal(view.status, 200);
const quiz = view.body.permissions.find(p => p.key === 'simulations.quiz');
const acc = view.body.permissions.find(p => p.key === 'simulations.access');
assert.equal(acc.effective, false, 'родитель simulations.access выключен');
assert.equal(quiz.effective, false, 'дочернее simulations.quiz неэффективно из-за requires');
assert.deepEqual(quiz.requires, ['simulations.access'], 'requires проброшен в API');
// restore
await inject('POST', '/api/permissions',
{ role: 'student', permission: 'simulations.access', enabled: true }, adminToken);
});
});