# PLAN — Переработка системы выдачи прав (Access / Permissions Redesign) > Составлен 2026-06-03 (Opus) по итогам ревью. Цель — свести «кто что видит» к единой > связной модели, закрыть дыры целостности и безопасности, сделать админку удобной при росте > числа учебников/классов/учеников. Источники-факты (проверено по коду): > - `backend/src/services/contentAccess.js` — резолвер allowlist (учебники/экзамены). > - `backend/src/routes/access.js` — API `/api/access` (catalog/targets/summary/rules/class). > - `backend/src/db/migrations/040_content_access.sql` — таблица `content_access`. > - `backend/src/permissions/registry.js` (23 ключа) + `role_permissions`/`user_permissions`, > middleware `requirePermission` (`backend/src/middleware/auth.js`). > - Админ-UI: `frontend/js/admin/sections/access.js` (контент по классам) и `permissions.js` (роли). > - Гейты: `textbooks.js` / `exam-prep.js` (`router.param`), отдача HTML `server.js:~436/~460`, > клиентский редирект `frontend/js/textbook-tracker.js`, `frontend/js/exam-prep/common.js`. --- ## 1. Текущее состояние — 2,5 параллельные системы | Система | Регулирует | Гранулярность | Хранилище | UI | |---|---|---|---|---| | **A. content_access** | учебники, экзамены | класс + ученик (allowlist, ученик > класс) | `content_access` (040) | вкладка «Доступ к учебникам» | | **B. role/user permissions** | способности (вести вопросы/классы/курсы), **симуляции**, испытания, магазин | глобально по роли + override на пользователя | `role_permissions`+`user_permissions` (registry.js) | вкладка «Права доступа» | | **C. курсы/теория** | видимость курсов | `is_published` + привязка к классу | таблицы курсов | — (нет единого места) | Проблема: на один вопрос «кому показывать контент» отвечают три механизма и две похоже названные вкладки. --- ## 2. Принципы целевой модели 1. **Два чётко разведённых понятия:** - **Способности (capabilities)** — что роль/пользователь *умеет делать* (вести классы, модерация, покупки в магазине, управление курсами). Остаётся в `role_permissions`/`user_permissions`. - **Видимость контента (content visibility)** — что классу/ученику *показывать* (учебники, экзамены, **курсы/теория**, **симуляции**). Переезжает в единый `content_access`. 2. **Allowlist по умолчанию** сохраняем (безопасно). Правило **ученик > класс** сохраняем. 3. **Один резолвер** `canAccess(user, type, ref)` для всех типов контента. 4. **Целостность БД** — ни одного пути, оставляющего «осиротевшие» правила. 5. **Сервер — источник правды**: блокировка не должна держаться только на клиенте. Что НЕ трогаем: ролевые «способности» (manage-права учителя, магазин) остаются по ролям — их некорректно вешать на классы. --- ## 3. Находки ревью (приоритеты) - **P0 фрагментация:** теорию/симуляции нельзя открыть *одному классу* (только глобально по роли); путаница «Права доступа» vs «Доступ к учебникам». - **P1 HTML не гейтится на сервере:** `/textbook/:slug`, `/exam-prep/:examKey` отдают HTML всем; блок — только клиентский редирект `/403` (причина: JWT в `localStorage`, не cookie). - **P1 масштаб UI:** плоские списки, нет поиска/группировки; «отдельные ученики» `LIMIT 500`. - **P2 осиротевшие per-student правила:** `kickMember` (classController.js:405) не чистит правила ученика. - **P2 нет FK** на `content_access` → чистота на ручных `DELETE`. - **P2 нет обзора** «класс × контент» и операций «открыть весь предмет/параллель». --- ## 4. Целевая модель данных `content_access` расширяется (обратносовместимо): ```sql -- было: content_type IN ('textbook','exam') -- станет: content_type IN ('textbook','exam','course','sim') content_ref -- textbook: slug хаба; exam: exam_key; course: course slug/id; sim: sim id/slug scope IN ('class','student') -- + опционально групповые правила (Фаза 2): -- scope IN ('class','student','tag'), где target_ref = 'math:5' (subject:grade) allow IN (0,1) ``` Резолвер `contentAccess.js` обобщается: - `canAccess(user, type, ref)` уже generic — добавить `canAccessCourse`, `canAccessSim` и расширить `allowedRefs`/`filterX` на новые типы. - Порядок разрешения: **личное правило ученика → правило класса (любой `allow=1`) → (Фаза 2) групповое правило по тегу → default deny**. - Для `sim`/`course` на время миграции — мост: если правил нет, читать старое ролевое `simulations.access`/публикацию, чтобы переход не отнял доступ (см. Фаза 1, миграция данных). --- ## Прогресс (2026-06-03) - ✅ **Фаза 0** — `purgeAccessFor` + рефактор удалений + confirm bulk-close + тест content-access (commit 1bbddc0). - ✅ **Фаза 2a** — режим «Матрица» класс×контент + `GET /api/access/matrix` + поиск (commit 67a70c6). - ✅ **Фаза 2b** — поиск/группировка по предмету в левой колонке + бейдж «эффективный доступ» у ученика (commit 596e8d8). - ✅ **Фаза 1a+1b (симуляции)** — гейт + мост + админ-UI (commits 9a145e5, 4549b4e). Курсы (1c) отложены. - ⏳ **Фаза 1 (исходная заметка)** — РЕШЕНО (пользователь): модель **ДОБАВОЧНАЯ** — ролевой `simulations.access` остаётся «модуль включён для роли», а видимость конкретных sim/курсов — дополнительно по классам через content_access. Эффективно: `roleHasModule AND classAllowsItem`. Миграция-мост открывает все sim/курсы всем классам → текущее поведение не меняется. Начинать с чтения подсистем lab/courses (где список симуляций/курсов отдаётся — туда вешать фильтр; refs: sim id/slug, course slug). ## 5. Фазы ### Фаза 0 — Целостность и быстрые победы (низкий риск, без смены модели) - [ ] `purgeAccessFor({scope, id})` в `services/contentAccess.js` — единая чистка правил; вызвать из `deleteClass` (уже чистит — заменить на общий вызов), `_deleteUserTx`, и **добавить в `kickMember`** (решить: чистить ли личные правила при исключении — по умолчанию чистить только если у ученика нет других классов с этим контентом; минимально — убрать «висячие» `student`-deny). Зафиксировать поведение тестом. - [ ] Подтверждение для массового «Закрыть у всех / Закрыть весь» в `access.js` (сейчас мгновенно). - [ ] Тех-долг БД: добавить чистящие триггеры ИЛИ оставить ручную чистку, но покрыть тестом «после удаления класса/ученика — нет строк content_access с этим target». - [ ] Тесты: резолвер (ученик>класс, default deny), чистка при удалениях. ### Фаза 1 — Единая модель видимости (расширение content_access на course/sim) - [ ] Миграция `05X_content_access_types.sql`: расширить CHECK `content_type` до `('textbook','exam','course','sim')`. **Перенос данных**: для каждого включённого курса/симуляции, видимых сейчас (по роли/публикации), создать `allow=1` всем существующим классам (как делала 040 для учебников) — чтобы переход не отнял доступ. - [ ] `contentAccess.js`: `canAccessCourse`, `canAccessSim`, расширить `allowedRefs`/фильтры. - [ ] Гейты: подключить резолвер на роутах курсов (`GET /api/courses` — сейчас гейтится только `is_published`+класс) и симуляций (`/api/gamification/lab-activity` и список симуляций). Ролевой `simulations.access` оставить как «способность видеть лабу вообще» ИЛИ перенести полностью в content_access (решить в начале фазы — по умолчанию: видимость по классам, `simulations.access` депрекейтим до «модуль доступен роли»). - [ ] `/api/access/catalog` + `targets` + `summary` + `rules`: добавить секции «Курсы», «Симуляции». - [ ] Тесты резолвера для новых типов + миграционный тест (после переноса все классы видят то же, что и раньше). ### Фаза 2 — UX админки (удобство при масштабе) - [ ] **Третий режим «Матрица»**: таблица класс × контент с чекбоксами, фильтр по предмету/типу, поиск. Обзор и правка «кто что видит» одним экраном. - [ ] **Поиск + группировка по предмету** в левой колонке (вместо плоских списков). - [ ] **Групповые правила (теги `subject:grade`)**: кнопка «Открыть весь предмет / всю параллель» одной записью; резолвер учитывает теги. (Требует `scope='tag'` + UI.) - [ ] **«Эффективный доступ»**: для выбранного ученика/класса показать, что реально видит и *почему* (унаследовано от класса / личное правило / групповое) — снимает путаницу tri-state. - [ ] **Пресеты**: «стартовый набор для N класса» в один клик. - [ ] **История правила** из `audit` (кто/когда открыл) в карточке. - [ ] **Объединить вкладки** под раздел «Доступ» → под-вкладки «Контент» (видимость) и «Способности» (роли). Прекращает путаницу двух похожих названий. - [ ] Пагинация/виртуализация для «отдельных учеников» (убрать `LIMIT 500` как потолок). ### Фаза 3 — Серверный гейт HTML (безопасность, крупная) - [ ] Перейти на аутентификацию через **httpOnly-cookie сессию** (или дублировать JWT в cookie), чтобы `/textbook/:slug` и `/exam-prep/:examKey` могли проверять доступ ПЕРЕД `res.sendFile`. - [ ] Middleware-гейт на отдачу HTML: нет доступа → редирект/`/403` на сервере. - [ ] Сохранить клиентский редирект как запасной слой. - [ ] Прогон полного backend-набора + ручная проверка логина/cookie во всех ролях. - ⚠️ Затрагивает аутентификацию целиком — делать отдельной веткой, с откатом. --- ## 6. Тестирование - Юнит резолвера `contentAccess` (все типы, ученик>класс, теги, default deny). - Миграционные тесты: после 040-аналога для course/sim — прежняя видимость сохранена. - Тесты целостности: удаление класса/ученика/исключение из класса → нет осиротевших правил. - E2E-смоук админки по ролям (admin видит всё; teacher — только свои классы/учеников). - pre-commit гоняет полный backend-набор (baseline 3 Auth-фейла — не трогать). ## 7. Риски и откат - Фаза 1 меняет поведение симуляций/курсов → обязательный перенос данных «открыть всем классам» + флаг отката (если что — вернуть ролевую проверку). Согласовать с пользователем ДО миграции (в памяти объединение было «отложено» — теперь решено делать: [[project_content_access]]). - Фаза 3 трогает аутентификацию — отдельная ветка, не смешивать с контентными фазами. ## 8. Порядок исполнения (рекомендация) Фаза 0 → Фаза 2 (часть: матрица/поиск/эффективный доступ — дают пользу сразу на текущей модели) → Фаза 1 (объединение типов) → Фаза 2 (групповые правила/теги, требуют новой модели) → Фаза 3. ## Прогресс 2 (2026-06-03, продолжение) - Фаза 2c ГОТОВА: массовые операции матрицы, «открыть весь предмет классу», история правил (GET /api/access/log, admin-only), пресет «копировать доступ из класса», объединение вкладок по смыслу («Доступ · контент» + «Доступ · роли»). - Фаза 3 (серверный гейт HTML через httpOnly-cookie) — ОТЛОЖЕНА ОСОЗНАННО (низкий ROI: данные через API уже гейтятся, видно лишь пустой каркас; стоит полной переделки auth). Делать только под требование приватности/комплаенса. Ветка feature/html-access-gate.