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

152 lines
10 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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.