Files
Learn_System/plans/access-redesign/PLAN.md
T
2026-06-03 13:56:50 +03:00

167 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.