feat(materials): «Мои материалы» v2 — харднинг безопасности и доводка UX

Безопасность/целостность: allowlist схемы URL (safeUrl) против stored-XSS через javascript:-ссылку; ссылочно-подсчётная чистка файлов при delete/смене url (releaseFileForUrl, учёт share-алиасов); квота на пользователя — число материалов + байты (колонка bytes, миграция 073).

Производительность: список отдаёт превью body (1000 симв.) + body_trunc; полный текст — ленивый GET /api/materials/:id (getOne, owner-only).

Фичи/UX (my-materials.html): теги-UI (ввод + чипы-фильтр + пилюля), ссылка на исходный урок, сортировка, множественный выбор + массовые действия, цвет/порядок папок, live-KaTeX в редакторе заметки.

Тесты: backend/tests/materials.test.js (16 тестов) — ранее их не было.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maxim Dolgolyov
2026-06-13 14:21:30 +03:00
parent 222005c0ba
commit abe84b9f90
8 changed files with 578 additions and 34 deletions
+57
View File
@@ -0,0 +1,57 @@
# «Мои материалы» — v2: харднинг и доводка
> Составлен Opus 2026-06-13. Базовый план (PLAN.md, Фазы 1–6) **полностью реализован**.
> Его раздел «Сквозные риски» отложил ровно то, что закрывает этот план: учёт/лимиты/чистку
> хранилища и `materials.test.js`. Источник истины по текущему состоянию — код
> (`studentMaterialsController.js`, `materials.js`, `my-materials.html`, `board-clip.js`,
> `material-save.js`) и [[reference_student_materials]].
Готчи проекта: новый `:id`-роут → `// @public-by-design` + проверка владельца; большие HTML — только Edit;
без эмодзи (inline SVG `.ic`); коммит поимённо + push; перезапуск сервера при правке backend; ветка
`feature/sim-builder` в рабочем дереве — НЕ коммитить чужие правки, только свои файлы.
---
## Фаза 1 — Целостность и безопасность (backend, фундамент) ✅ цель этого захода
1. **Ссылочно-подсчётная чистка файлов.** `DELETE /:id` и смена `url` (аннотация) сейчас оставляют
файл в `uploads/materials/` сиротой. `share` копирует `url` дословно → несколько строк ссылаются на
ОДИН файл, поэтому `unlink` только когда на `url` не ссылается ни одна строка. Хелпер
`releaseFileForUrl(url)` вызывается ПОСЛЕ delete/update.
2. **Allowlist схемы URL.** `create`/`update` принимали любой `url``link` со схемой `javascript:`
рендерится как рабочий `<a href>` (раздача делает это вектором учитель→ученики). Хелпер `safeUrl`:
только `http(s)://` или app-relative `/…` (не `//host`); иначе 400.
3. **Квота на пользователя.** Колонка `bytes` (мигр. 073), счёт `SUM(bytes)`/`COUNT(*)`. Лимит по числу
материалов — в `create()`; лимит по байтам — в `uploadPersonalFile` (до приёма файла). Конфигурируемо
через `MATERIALS_MAX_ITEMS` / `MATERIALS_MAX_BYTES` (для тестов — низкий потолок).
4. **`backend/tests/materials.test.js`** — CRUD, владелец (403/404), коллекции, share-копия + роль/owner,
валидация URL, лимит числа, ссылочная чистка (прямой вызов хелпера на временном файле).
## Фаза 2 — Производительность ✅ (частично)
-`GET /api/materials` отдаёт **обрезанный** `body` (первые 1000 симв.) + флаг `body_trunc`; полный текст —
ленивый `GET /api/materials/:id` (`getOne`, owner-only). Клиент `ensureFullBody()` подгружает перед
просмотром/правкой/флешкартой (иначе правка сохранила бы усечённый текст).
- ⬜ Пагинация/keyset — отложено (клиент пока фильтрует в памяти; включить при росте объёмов).
- ⬜ Серверные миниатюры `board/image` — отложено (нужна обработка картинок; пока `loading=lazy`).
## Фаза 3 — Доводка заложенных фич ✅
- ✅ UI тегов: ввод в модалках создания/правки + чипы на карточке (клик → фильтр) + пилюля активного фильтра.
- ✅ Ссылка «открыть исходный урок» на карточке (`/my-lessons?session=<id>`, есть `source_session_id`).
- ✅ Цвет папки (палитра 8 пресетов, тинт иконки в рейле) + сортировка папок «Выше/Ниже» в модалке правки
(нормализует `sort_order` к индексам). `safeColor` гейтит inline-style инъекцию (только hex).
## Фаза 4 — UX ✅
- ✅ Варианты сортировки (новые/старые/имя/тип) — селект в тулбаре.
- ✅ Множественный выбор (чекбокс на карточке) + панель массовых действий (переместить/удалить, reuse per-item API).
- ✅ Живое превью KaTeX в редакторе заметки (oninput → `mmPreview``mathHtml`).
### Статус
Сделано и проверено: **Ф1 целиком** (16 backend-тестов), **Ф2/Ф3/Ф4 ✅** (headless-смоук `my-materials.html`:
синтаксис + рендер карточек с deep-link/тегами/чекбоксом + фильтр по тегу + bulk-bar + тинт папки).
Осталось ⬜ (инфра, отложено): пагинация/keyset списка, серверные миниатюры board/image.
---
## Порядок
**Ф1 (этот заход) → Ф2 → Ф3 → Ф4.** Ф1 — серверный фундамент (риск-возврат, без него фронт-фичи множат
мусор). Дальше преимущественно фронтенд `my-materials.html` + точечные ручки API.