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
+5 -1
View File
@@ -37,6 +37,7 @@ const PERMISSIONS = {
role: 'teacher', roles: ['teacher'], default: 0,
label: 'Удалять вопросы',
desc: 'Удалять вопросы из банка (требует "Управление вопросами")',
requires: ['questions.manage'],
},
'students.invite': {
role: 'teacher', roles: ['teacher'], default: 0,
@@ -90,6 +91,7 @@ const PERMISSIONS = {
role: 'teacher', roles: ['teacher'], default: 0,
label: 'Публикация шаблонов',
desc: 'Делать свои шаблоны публичными для всех учителей',
requires: ['templates.manage'],
},
'courses.manage': {
role: 'teacher', roles: ['teacher'], default: 1,
@@ -101,6 +103,7 @@ const PERMISSIONS = {
role: 'teacher', roles: ['teacher'], default: 1,
label: 'Интерактивные блоки',
desc: 'Добавлять интерактивные задания в уроки (сопоставление, пропуски, порядок)',
requires: ['courses.manage'],
},
'shop.manage': {
role: 'teacher', roles: ['teacher'], default: 0,
@@ -155,6 +158,7 @@ const PERMISSIONS = {
role: 'student', roles: ['student', 'free_student'], default: 1,
label: 'Задания в симуляциях',
desc: 'Использовать режим "Задания" в симуляциях (квиз-режим)',
requires: ['simulations.access'],
},
};
@@ -178,7 +182,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 }));
.map(([key, v]) => ({ key, role: v.role, default: v.default, label: v.label, desc: v.desc, requireConfirmOff: !!v.requireConfirmOff, requires: v.requires || [] }));
}
/**