# 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.
