Files
LMS/README.md
Vladislav Kopnin f3d614bb33 ci: изолировать пайплайны на отдельной VM
- Woodpecker workflows требуют агент с меткой isolation=dedicated-vm
- шаги выполняются в lms-ci:1 с nested dockerd, без хост-Docker сокета
- ci/Dockerfile: прогретый immutable-образ (Java, Python, Node, Gradle-кэш)
- ci-dind.sh поднимает dockerd внутри privileged-шага; ci-build-image.sh собирает образ
- deploy/WOODPECKER.md: отдельная VM, firewall, отсутствие production-секретов,
  trusted repo, чек-лист безопасности
2026-10-29 12:01:44 +03:00

234 lines
19 KiB
Markdown
Raw 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.

# LMS
Система для проведения практических занятий на основе микросервисной архитектуры (Kotlin + Spring Boot + PostgreSQL 18).
## Документация
UML- и архитектурные диаграммы (Mermaid) — в каталоге [`docs/`](docs/README.md):
- [Архитектура](docs/01-architecture.md) — контекст, компоненты, модули, аутентификация;
- [Развёртывание](docs/02-deployment.md) — Docker, nginx, тома, обновление;
- [Модель данных и классов](docs/03-domain-model.md) — диаграммы классов по сервисам и ER-модель;
- [Диаграммы последовательностей](docs/04-sequences.md) — вход, импорт ЛР, раздача вариантов, сдача, защита, оценки, материалы;
- [Диаграммы состояний](docs/05-states.md) — защита, публикация оценок и материалов, сдача, пароль.
## Модули
- `libs/common-http` — общий формат ошибок и обработчик исключений.
- `libs/security-jwt` — выпуск/проверка JWT (RSA) и `PasswordEncoder`.
- `libs/markdown` — серверный рендер markdown со спойлерами и санитизацией.
- `libs/test-support` — общая тестовая инфраструктура (Testcontainers, `postgres:18`).
- `services/identity-service` — аккаунты, студенты, группы, зачисления (схема `identity`).
- `services/academic-service` — курсы, статьи, лабораторные, уровни, варианты (схема `academic`).
- `services/progress-service` — уровни студентов, распределение вариантов, работы, оценки (схема `progress`).
- `services/gateway` — Spring Cloud Gateway, маршрутизация и проверка JWT.
- `web` — серверный веб-интерфейс (Thymeleaf + htmx), BFF.
Ключевое в текущей версии:
- роли `ADMIN/TEACHER/STUDENT`, курсы и группы-когорты, повышение и жизненный
цикл студента, профиль студента курса;
- access JWT 15 минут + refresh 7 дней с ротацией/отзывом; web-сессия 7 дней
sliding с абсолютным потолком 30 дней и автоматическим обновлением access;
- аудит с keyset-пагинацией, пакетная выдача лабораторных и групп курса;
- материалы и активы secure-by-default: файл без статьи доступен только
админу (инвентаризация `/admin/legacy-assets`).
## Требования
- JDK 21 (runtime) + Docker (для PostgreSQL 18). Gradle сам скачает toolchain 21 для сборки.
- **Не запускать сервисы на JAVA_HOME=Java 25**: на 25 ломается logback внутри
fat-jar (NoClassDefFoundError `ThrowableProxy`), что роняет потоки Tomcat.
Скрипт `run.sh` использует JDK 21 автоматически.
## Запуск
```bash
./gradlew bootJar -x test # собрать все сервисы
./run.sh start # поднять БД + все 5 сервисов на JDK 21
./run.sh status # проверить здоровье
./run.sh logs web # посмотреть лог сервиса
./run.sh stop | restart
```
Можно и без скрипта: `docker compose up -d postgres`, затем
`/usr/lib/jvm/java-21-openjdk/bin/java -jar <jar>` для каждого сервиса,
или `./gradlew :services:identity-service:bootRun` и т.д. по одному.
Порты по умолчанию: gateway `:8080`, identity `:8081`, academic `:8082`,
progress `:8083`, web `:8090`. Интерфейс: `http://localhost:8090`.
## Качество кода и тесты
```bash
./gradlew ktlintCheck detekt test # линтеры (строгий режим) + тесты
./gradlew evidenceCheck # LDD: P0-claims должны иметь зелёные свидетельства
./gradlew ktlintFormat # автоформат Kotlin
./scripts/ldd-smoke.sh # быстрый smoke полного стека (как CI workflow smoke)
./scripts/ldd-runtime.sh # runtime-smoke полного стека + scale load
./scripts/ldd-migration-upgrade.sh # upgrade с master-схемы
./scripts/ldd-mutation.sh # PIT: mutation strength критичных P0-компонентов
./scripts/ldd-browser.sh # Playwright E2E: Chromium/Firefox/WebKit
./scripts/ldd-release.sh # полный release gate: test/upgrade/mutation/runtime/E2E
./scripts/ci-preflight.sh --release # проверка окружения runner без установки
./scripts/ci-build-image.sh # прогретый CI-образ lms-ci:1 для Woodpecker
./scripts/legacy-assets.sh # инвентаризация бесхозных файлов на стенде (admin)
```
- `ktlint` отвечает за формат, `detekt` — за качество (сложность, вложенность, ошибки).
Оба подключены во всех Kotlin-модулях конвенцией `lms.lint` и **ломают сборку** при замечаниях.
- Конфигурация: `.editorconfig`, `config/detekt/detekt.yml`.
- Gradle daemon фиксируется на JDK 21 (`gradle/gradle-daemon-jvm.properties`): detekt 1.23
не работает на JDK 25.
- Тестовая инфраструктура — `libs:test-support` (JUnit 5, MockK, Testcontainers `postgres:18`).
- **LDD** — реестр проверяемых утверждений `docs/evidence/claims.json`;
P0 без зелёного теста валит `./gradlew evidenceCheck`. Runtime, migration,
mutation, browser E2E и smoke — скрипты из `scripts/`; baseline —
`docs/evidence/baseline-2026-10-27.md`.
- CI merge gate — `.woodpecker/` (Gitea + Woodpecker 3.x):
workflow `check` (`test ktlintCheck detekt evidenceCheck`) и `smoke`
(`scripts/ldd-smoke.sh`) на каждом push/MR; полный `release` —
`scripts/ldd-release.sh` — на тег или вручную. Smoke/release идут на
отдельной CI VM: Woodpecker Docker backend, nested dind, Java 21,
Docker Compose v2, Python 3, Node и Chromium/Firefox/WebKit. Установка —
[`deploy/WOODPECKER.md`](deploy/WOODPECKER.md).
Baseline: **51 claim, 346 тестов, 18/18 browser E2E**.
## Запуск в Docker (для сервера)
На сервере Java не нужна: всё собирается и работает в контейнерах (multi-stage
`Dockerfile`, сборка на JDK 21, запуск на JRE 21).
```bash
cp .env.prod.example .env.prod # заполнить LMS_DB_PASSWORD
./deploy/deploy.sh up # backup + сборка + запуск (postgres, 5 сервисов)
./deploy/deploy.sh logs web
./deploy/deploy.sh ps
./deploy/deploy.sh down
```
Перед обновлением `deploy.sh up` обязательно делает backup БД и файлов.
Ежедневное расписание, retention и восстановление — в [`deploy/BACKUP.md`](deploy/BACKUP.md).
Наружу публикуется только web на `127.0.0.1:8090`; reverse proxy — nginx
(конфиг и полная инструкция по деплою на `lms.our-place.ru` в
[`deploy/DEPLOY.md`](deploy/DEPLOY.md) и `deploy/nginx/lms.our-place.ru.conf`).
## Наполнение данными
Порядок ввода данных (всё через админку): `/setup` → администратор; курс
(bootstrap или «+ Новый курс»); импорт групп (`.xls`, логин = студенческий номер,
временный пароль со сменой при первом входе); импорт лабораторной ZIP'ом с
выбором курса (`lab.yaml` + `README.md` + `variants/*.md`); «Распределить»
— случайная раздача по кругам в рамках уровня; оценки/защита — на странице группы;
учебные материалы — статьи или ZIP (`chapters/…`, доступны гостям).
Подробный пошаговый порядок — в [`deploy/IMPORT.md`](deploy/IMPORT.md).
## Текущий функционал
Открыть `http://localhost:8090`. При первом запуске создаётся аккаунт
администратора (`/setup`); далее — вход по логину и паролю.
- **Доступ**: JWT (RSA, ключи identity генерирует при первом старте и хранит в БД),
проверка токена на gateway, роли ADMIN/TEACHER/STUDENT, CSRF на всех POST-формах,
session fixation-защита; вход студента с обязательной сменой пароля.
Access-токен живёт 15 минут (`LMS_ACCESS_TOKEN_TTL`), refresh — 7 дней
(`LMS_REFRESH_TOKEN_TTL`) с ротацией и отзывом; прямой API использует
`POST /api/v1/auth/refresh`. Web-сессия `LMS_SESSION` — 7 дней sliding
(`LMS_SESSION_TIMEOUT`), access обновляется автоматически за 2 минуты до
истечения; logout и смена пароля отзывают refresh.
- **Группы и студенты**: группы-когорты (поток, курс, номер, степень, год набора),
импорт `.xls/.xlsx/.csv` по шаблону `поток/курс/номер/степень/год набора/ФИО/№/база`,
ручное добавление студента (генерация пароля) и «Добавить существующего»
(перевод из другой группы), «Отчислить из группы», выгрузка логинов XLSX/CSV,
сброс пароля, история членства.
- **Курсы**: админ-страницы курсов (`/admin/courses`), номер `course_no` для URL,
назначение преподавателей и привязка групп с историей; bootstrap переносит
существующие лабораторные, главы и активные группы в курс «Промышленное
программирование на языке C». Лабораторные привязаны к курсу: создание
и импорт требуют выбора курса, лабу можно скопировать в другой курс.
- **Преподаватели и доступ**: `/admin/teachers` создаёт аккаунты `TEACHER`
с одноразовым паролем; преподаватель видит только назначенные курсы
(`/teacher`), их группы, студентов и профили. Курсы и лабораторные
фильтруются по роли, внутренние вызовы identity защищены `LMS_INTERNAL_TOKEN`.
- **Профиль студента курса**: `/teacher/courses/{id}/students/{id}` и админский
маршрут показывают сводку, оценки, комментарии с авторами, защиты и все
попытки сдачи со скачиванием ZIP; студенту API отдаёт только его профиль
и опубликованные данные.
- **Работа преподавателя**: ведомость группы переиспользует
`/admin/groups/{groupId}?courseId=...` без админских контролов — оценки и
комментарии, публикация, уровни/варианты и раздача, защиты (код/reset),
скачивание попыток и ZIP группы; лабораторные/уровни/варианты своего курса,
привязка/отвязка групп на `/teacher/courses/{id}`, авторские материалы на
`/teacher/courses/{id}/content`. Чужой курс отклоняется на сервисах.
- **Аудит**: чувствительные действия (профили, скачивания, оценки, защиты,
курсы, группы, материалы, преподаватели, жизненный цикл) пишутся в
`identity.audit_events`; админ смотрит `/admin/audit` с фильтром по действию,
временем МСК и постраничным переходом «Показать ещё» (`page`/`limit`).
- **Повышение и жизненный цикл**: `/admin/promotion` — dry-run, применение
и откат повышения (автоматический dry-run 31 августа МСК); на странице
группы — академотпуск, возврат, отчисление, архив и восстановление;
архив группы и подтверждение удаления логином/ФИО. Вход закрыт для
выпущенных и архивных групп.
- **Лабораторные**: импорт ZIP (`lab.yaml` + `README.md` + `variants/*.md` +
`assets/`); создание через UI с произвольным числом уровней; единая форма
настроек с одной кнопкой «Сохранить всё»; статусы draft/published
(клик по бейджу меняет); даты в московском времени.
- **Распределение вариантов** (только на странице лабы): «Распределить» с
выбором групп; раздача случайная и равномерная — студенты выбранных групп
идут общей очередью, вариант берётся со своего уровня, пока в уровне есть
невыданные — без повторов, затем выдаются наименее занятые; ручные
закрепления сохраняются всегда. На странице лабы — список назначений по
группам (уровень/вариант каждому), массовые операции чекбоксами, индикаторы
«выдано/всего». Матрица на странице группы — просмотр (вариант, сдача) +
оценки/защита, управление раздачей оттуда убрано.
- **Индивидуальные задания (защита)**: на странице группы — назначение/изменение
задания, импорт CSV (2 или 3 колонки, с кавычками и `\n`), выдача кода,
«доп. задание: начать заново» после завершения защиты.
- **Материалы** (`/learn`, публичная читалка): главы → разделы → статьи в
markdown (код, спойлеры, картинки; санитизация OWASP); импорт ZIP
(`chapters/N-глава/N-раздел/N-статья.md` + `assets/`); создание/переименование/
удаление глав, разделов, статей; перенос статей между разделами.
Видимость картинок наследуется от статьи; файл без привязки к статье отдаёт
`404` обычным пользователям и доступен только админу, который получает
список таких файлов через `GET /api/v1/content/legacy-assets`.
- **Студент**: дашборд успеваемости (сдано, средний балл, лучшая/слабая оценка),
карточки опубликованных лаб, страница лабы: условие, свой вариант, сдача
файлов (zip/.c, переотправка до дедлайна, история, скачивание последней),
оценка и комментарии отдельным блоком, защита по одноразовому коду (30 мин).
- **Публикация оценок**: в группе по каждой лабе — кнопка «опубликовать»; студент
видит оценку и комментарии только после публикации (публикуется вся группа).
- **Комментарии к оценкам**: общий (итоговый) комментарий по лабораторной + комментарии
с тональностью (positive/negative/neutral); на странице студента — ненумерованными списками
в трёх колонках, хранятся в БД, автоподсказки при вводе ранжируются по частоте использования.
- **Архив сдач группы**: одна кнопка «Скачать все работы» — ZIP со всеми
последними решениями группы, разложенными по лабам и студентам.
- **Материалы** `/learn` доступны без входа; аккордеон глав: свёрнуты все,
открыта последняя.
- UI: светлая/тёмная тема (сохраняется в localStorage), модальные окна вместо
отдельных страниц, аккордеоны глав/разделов в `/learn`, фавиконка,
адаптивная вёрстка без inline-стилей.
## Примечания
- **JDK 21 обязателен для runtime**. В связке Spring Boot 4.0.x + fat-jar logback
не находит `ThrowableProxy`, и это падает при любом исключении в потоке ответа.
Если после деплоя сервис вдруг «висит» на страницах (а `/actuator/health` даёт 200) —
лечится перезапуском (`./run.sh restart`).
- Даты хранятся в UTC (instant), а в UI конвертируются в Europe/Moscow
(`DateUtils`); формы datetime-local отправляются в московском времени.
- Капча/rate-limit на входе отсутствует — для учебного стендома оставлено;
пароли студентов показываются один раз при выдаче.
- `lms.submissions.dir` (default ./data/submissions) и `lms.content.assets-dir`
держите на постоянном томе при деплое.
## Сборка и тесты
```bash
./gradlew build # сборка всех модулей
./gradlew check # тесты и проверки
```
Переменные окружения для БД: `LMS_DB_URL`, `LMS_DB_USER`, `LMS_DB_PASSWORD`.