- 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 обновлены
152 lines
10 KiB
Markdown
152 lines
10 KiB
Markdown
# 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.
|