# Этап 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` — legacy-файл, публичен как раньше; - глобальная опубликованная статья `ALL` — публична, в том числе анонимно; - глобальные `STUDENTS`/`TEACHERS` — только с JWT и соответствующей ролью; - курсовой актив — только с JWT и доступом к курсу: админ видит любой курс, преподаватель — свой (включая скрытые материалы), студент — только опубликованные материалы своего курса. Скрытый актив для студента отдаёт `404` (не раскрывает существование), чужой курс — `403`. - Web-прокси `/api/v1/content/assets/{name}` передаёт Bearer-токен из web-сессии, поэтому защищённые картинки на страницах `/learn/...` открываются, а на публичных страницах прокси остаётся анонимным. **Остаточный риск — legacy-активы без привязки.** `V12` закрывает только файлы, на которые есть ссылки в markdown. Если файл был загружен, но ни одна статья на него не ссылается (осиротевший или удалённый из текста актив), он остаётся публичным. Нужна инвентаризация таких файлов и привязка к статье либо удаление; до этого поведение сохраняется осознанно, чтобы не ломать старые публичные ссылки.