# Этап 5. Материалы курса **Статус:** реализовано в academic-service и web (без изменения identity/progress). ## Что изменилось - Публичные `/api/v1/content/tree` и `/api/v1/content/{chapter}/{section}/{article}` отдают только **глобальные** главы (`course_id IS NULL`) с аудиторией `ALL`. Курсовые главы через публичный контракт больше не доступны. - Материалы курса открываются по `/api/v1/courses/{courseId}/content/...` и требуют аутентификации. Доступ проверяется через `CourseAccessService`: админ — любой курс, преподаватель — только назначенный, студент — только курс активной группы. - Администратор и преподаватель курса видят скрытые главы/разделы/статьи; студент — только опубликованные. - В браузере материалы курса открываются по `/learn/course/{course_no}` и `/learn/course/{course_no}/{chapter}/{section}/{article}`. Анонимный пользователь перенаправляется на `/login`, чужой курс даёт `403`. - Ссылки на материалы курса добавлены в кабинеты студента (карточки лабораторных), преподавателя и администратора. - Админский просмотр и авторство глобальных материалов (`treeAll`, `/api/v1/content/all/...`, `/learn` для админа) сохранены. - Новые глобальные главы создаются и импортируются с `audience = "ALL"`. Миграция `V11__global_audience_all.sql` backfill-ом проставляет `ALL` старым глобальным главам; курсовые главы остаются с `audience = NULL`. ## Видимость активов (закрыто после этапа 5) Разрыв этапа 5 закрыт: активы привязаны к статьям и наследуют их видимость. - Добавлены entity/repository `article_assets`; импорт контента сохраняет связи для файлов, на которые ссылаются статьи. - Миграция `V12__asset_links_backfill.sql` backfill-ом восстанавливает связи для уже загруженных статей: ищет в `articles.body_md` ссылки вида `/api/v1/content/assets/{name}`, создаёт строки `article_assets` с MIME по расширению и `size = 0`. Повторный запуск не создаёт дубликатов. - Публичный API `/api/v1/content/assets/{name}` проверяет доступ по всем статьям, к которым привязан файл (достаточно одной видимой): - актив без связей в `article_assets` — secure-by-default: `404` для анонима, студента и преподавателя; только `ADMIN` может скачать файл для инвентаризации/очистки; - глобальная опубликованная статья `ALL` — публична, в том числе анонимно; - глобальные `STUDENTS`/`TEACHERS` — только с JWT и соответствующей ролью; - курсовой актив — только с JWT и доступом к курсу: админ видит любой курс, преподаватель — свой (включая скрытые материалы), студент — только опубликованные материалы своего курса. Скрытый актив для студента отдаёт `404` (не раскрывает существование), чужой курс — `403`. - Web-прокси `/api/v1/content/assets/{name}` передаёт Bearer-токен из web-сессии, поэтому защищённые картинки на страницах `/learn/...` открываются, а на публичных страницах прокси остаётся анонимным. ## Legacy-активы без привязки (закрыто) `V12` закрывает файлы, на которые есть ссылки в markdown. Если файл был загружен, но ни одна статья на него не ссылается, он больше **не публичен**: аноним, студент и преподаватель получают `404`, чтобы не раскрывать существование файла. Админ скачивает такой файл и получает его имя на странице `/admin/legacy-assets`, через `GET /api/v1/content/legacy-assets` или `scripts/legacy-assets.sh`, после чего привязывает файл к статье или удаляет. Старые внешние ссылки на несвязанные файлы у обычных пользователей перестают работать — это осознанное поведение secure-by-default, а не регрессия. Эндпоинт и страница инвентаризации защищены `ROLE_ADMIN`. ## Авторство материалов преподавателем (добавлено на этапе 6) Преподаватель своего курса редактирует главы, разделы и статьи на `/teacher/courses/{courseNo}/content`: создание/переименование/удаление, markdown- редактор с предпросмотром, публикация/снятие с публикации. API — `/api/v1/courses/{courseId}/content/**`; любая изменяемая сущность проверяется на принадлежность курсу. Глобальные материалы (`course_id IS NULL`, аудитории `ALL/STUDENTS/TEACHERS`) по-прежнему создаёт и публикует только админ.