feat(access): доступ к учебникам и экзаменам по классам/ученикам из админ-панели

Модель allowlist (закрыто по умолчанию), правило ученика важнее класса.
Управляют админ (все) и учителя (свои классы/ученики).

- миграция 040: таблица content_access + непрерывный переход
  (всем существующим классам открыт текущий контент)
- сервис contentAccess: резолвинг доступа, главы наследуют хаб
- API /api/access (catalog/targets/rules) для admin+teacher
- гейты: каталог учебников, router.param slug/examKey, фильтр tracks
- клиентские редиректы на /403 (textbook-tracker, exam-prep boot)
- раздел админки «Доступ к учебникам»: классы + ученики (tri-state)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maxim Dolgolyov
2026-05-30 12:33:05 +03:00
parent 98f955a85e
commit 471171b77c
12 changed files with 564 additions and 4 deletions
+12
View File
@@ -1025,6 +1025,7 @@ window.LS = {
getFolderAccess, clearFolderAccess, assignFolder, unassignFolder, getStudentsList,
submitWork, resubmitWork, getMySubmissions, getClassSubmissions, reviewSubmission, deleteSubmission, submissionDownloadUrl,
getPermissions, setPermission, getUserPermissions, setUserPermission, resetUserPermissions,
accessCatalog, accessTargets, accessRules, accessSetRule,
getCourseTemplates, saveCourseTemplate, createFromCourseTemplate, deleteCourseTemplate,
getLessonTemplates, saveLessonTemplate, createFromLessonTemplate, deleteLessonTemplate,
getBookmarks, addBookmark, removeBookmark, removeBookmarkByEntity, checkBookmark,
@@ -1288,6 +1289,17 @@ async function getUserPermissions(uid) { return req('GET',
async function setUserPermission(uid, permission, enabled) { return req('POST', `/permissions/users/${uid}`, { permission, enabled }); }
async function resetUserPermissions(uid, permission) { return req('DELETE', `/permissions/users/${uid}/reset`, permission ? { permission } : undefined); }
/* ── content access (учебники / экзамены: открыть-закрыть классам/ученикам) ── */
async function accessCatalog() { return req('GET', '/access/catalog'); }
async function accessTargets() { return req('GET', '/access/targets'); }
async function accessRules(content_type, content_ref) {
const p = new URLSearchParams({ content_type, content_ref });
return req('GET', `/access/rules?${p}`);
}
async function accessSetRule(content_type, content_ref, scope, target_id, allow) {
return req('POST', '/access/rules', { content_type, content_ref, scope, target_id, allow });
}
/* ── notifications ───────────────────────────────────────────────────────── */
async function getNotifications() { return req('GET', '/notifications'); }
async function markNotifRead(id) { return req('PATCH',`/notifications/${id}/read`); }