# ADR 0001. Фундамент групп, курсов и доступа **Статус:** принято. **Дата:** 2026-10-23. **Контекст:** реализация [ТЗ 07](../07-groups-courses.md) и [плана 08](../08-groups-courses-plan.md). ## Контекст Сейчас в LMS нет сущности курса-дисциплины, ролей преподавателя, статусов студентов и истории членства. Лабораторные привязаны к семестру, материалы — к глобальному дереву глав. Доступ различает только `ADMIN` и «всё остальное»; проверки в основном находятся в web-BFF. Нужно перейти к модели: - курс-дисциплина с преподавателями и группами; - группы-когорты с потоком, курсом обучения, номером, степенью и годом набора; - студенты со статусами и историей; - лабораторные и материалы, принадлежащие курсу; - проверки доступа на сервисах; - профиль студента курса для преподавателя. ## Решения ### 1. Курс C и `course_no` - Текущий единственный курс — «Промышленное программирование на языке C». - UUID остаётся первичным ключом курса. - Для URL добавляется `courses.course_no`: `BIGINT GENERATED ALWAYS AS IDENTITY UNIQUE NOT NULL`. - Курсовые материалы открываются по `/learn/course/{course_no}/{chapter}/{section}/{article}`. - `course_no` не является секретом; доступ проверяется отдельно. - Старые ссылки на текущие материалы не поддерживаются: они не публиковались. ### 2. Все лабораторные и материалы принадлежат курсу - «Общих» лабораторных нет. - Все текущие лабы, главы, разделы, статьи и активы переносятся в курс C. - `labs.course_id` вводится как nullable на время миграции, затем становится `NOT NULL`. - `chapters.course_id` остаётся nullable, потому что глобальные материалы сохраняются как отдельный сценарий. `audience` применяется только к глобальным главам (`course_id IS NULL`). - `sections` и `articles` наследуют видимость от главы. - Активы наследуют видимость от статьи → раздела → главы. ### 3. Двухфазная миграция и bootstrap Обычный Flyway-старт применяет миграции до бизнес-логики, а курс C должен принадлежать реальному админу. Поэтому: 1. Миграции добавляют nullable-поля и новые таблицы. 2. После первого входа админа bootstrap-сервис: - создаёт курс C с `owner_account_id` админа; - привязывает к нему все текущие активные группы; - проставляет `labs.course_id` и `chapters.course_id`; - проверяет дубли `(number, course_id)` и сообщает о конфликтах. 3. После успешного bootstrap отдельным шагом применяется `ALTER TABLE labs ALTER COLUMN course_id SET NOT NULL`. `SET NOT NULL` не входит в обычную цепочку Flyway: иначе миграция выполнится раньше bootstrap и упадёт на существующих лабах. Точный механизм шага (CLI, admin endpoint или отдельная Flyway-локация) фиксируется на этапе 2. ### 4. Проверки доступа на сервисах - JWT содержит только `sub`, `role`, `login`, `name`; списки курсов и групп в токен не кладутся. - Каждый сервис проверяет доступ по своим данным и данным источника истины: - identity — аккаунт, студент, группа, статус; - academic — курс, преподаватель, группа курса, лабораторная, материал; - progress — оценки, сдачи, защиты, публикации. - Для межсервисных проверок используются внутренние эндпоинты `/api/v1/internal/**`: - identity: контекст студента (группа, статус, год набора); - academic: доступ к курсу (активный преподаватель, привязанная группа, статус курса), список лаб курса. - Внутренние эндпоинты не маршрутизируются gateway и защищены отдельным сервисным токеном (`X-Internal-Token`), а не только пользовательским JWT. - Кросс-схемные SQL-запросы и внешние ключи между `identity`, `academic` и `progress` не добавляются. - Отзыв доступа к курсу/группе/публикации действует сразу, потому что проверяется на каждом запросе по текущим данным. ### 5. Статусы и вход - `accounts.status`: `ACTIVE | ARCHIVED`. - `students.status`: `ACTIVE | ACADEMIC_LEAVE | EXPELLED | GRADUATED | ARCHIVED`. - Вход запрещён для `EXPELLED`, `GRADUATED`, `ARCHIVED` и архивных аккаунтов. - `ACADEMIC_LEAVE` — вход в режиме только чтения. - Web перепроверяет статус с кэшем 30–60 секунд; сервисы проверяют статус студента и группы при доступе к курсу. - ~~JWT остаётся 30-дневным~~ — решение пересмотрено: access 15 минут, refresh 7 дней с ротацией и абсолютным потолком 30 дней (`V11`–`V12`). Для браузера токен не доступен. ### 6. Повышение курса - `promotion_runs` — шапка запуска. - `promotion_run_items` — по-элементный план/результат для групп и студентов. - `membership_history` — история студента; ссылается на запуск. - 31 августа в 00:00 МСК автоматически создаётся `DRY_RUN`. - Применение — только вручную админом после проверки плана. - Один успешный `APPLY` на год; откат — по элементам журнала с проверкой конфликтов. ### 7. Профиль студента курса - Преподаватель активного курса открывает профиль студента по клику на ФИО или отдельной кнопке. - Профиль агрегирует: - статистику по лабам курса; - комментарии преподавателей (общий и отдельные) с автором и датой; - все попытки сдачи и скачивание файлов любой попытки. - Доступ проверяется на сервисах: преподаватель — только свой курс и студенты его групп; админ — любой; студент — только свой профиль (опционально). - Контракт API: [09-teacher-student-profile.md](../09-teacher-student-profile.md). ### 8. Резервное копирование - Backup обязателен перед каждым деплоем/миграцией; при ошибке backup деплой прерывается. - Backup включает БД (`pg_dump --format=custom`), volume решений и volume материалов, `SHA256SUMS` и manifest. - Ежедневный backup по systemd timer; retention по умолчанию 14 дней. - Восстановление — только с подтверждением `RESTORE`; после restore проверяется health web. - Backup на том же сервере не считается достаточным: нужна внешняя копия и периодическая проверка restore на staging. - Инструкция: [deploy/BACKUP.md](../../deploy/BACKUP.md). ## Последствия - Схема становится шире, но миграции аддитивные и не ломают старый код. - Появляется отдельный bootstrap-шаг; его нельзя пропускать при развёртывании. - Нужны внутренние эндпоинты и сервисный токен. - Нужно обновить security-конфиги, gateway и web-интерфейс. - Старые ссылки на материалы перестанут работать; это осознанное решение. - До включения `NOT NULL` в `labs.course_id` возможны лабы без курса; preflight и bootstrap-отчёт обязательны. - Появляется обязательный backup-шаг; без него деплой не выполняется. ## Открытые вопросы - Механизм post-bootstrap `SET NOT NULL`: CLI, admin endpoint или отдельная Flyway-локация. Решение — этап 2. - Формат аудита скачиваний профиля: общий `audit_events` или отдельная таблица. - Поведение профиля для студентов, отвязанных от курса/архивных: история или `404`. Решение — этап 3.