Files
LMS/docs/adr/0001-groups-courses-foundation.md
Vladislav Kopnin 1e1e9ae0e4 feat: закрыть остаточный backlog
- keyset-пагинация аудита с курсором (occurred_at, id) и UI «Показать ещё»
- пакетный /api/v1/labs/details и переход progress на один запрос
- абсолютный потолок сессии 30 дней (identity V12 + web), очистка refresh
- админская инвентаризация legacy-активов (/admin/legacy-assets, скрипт)
- E2E в Chromium/Firefox/WebKit (18/18), CI preflight
- LDD: 51 claim, 346 тестов, docs и handoff обновлены
2026-10-29 12:01:44 +03:00

10 KiB
Raw Blame History

ADR 0001. Фундамент групп, курсов и доступа

Статус: принято. Дата: 2026-10-23. Контекст: реализация ТЗ 07 и плана 08.

Контекст

Сейчас в 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.

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.

Последствия

  • Схема становится шире, но миграции аддитивные и не ломают старый код.
  • Появляется отдельный 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.