Phase 7: deployment, data import, UI hardening and security fixes

Docker/prod deployment:
- multi-stage Dockerfile (JDK21 build, JRE21 run), docker-compose.prod.yml,
  nginx config for lms.our-place.ru, deploy.sh; DEPLOY/IMPORT guides (UI flow)
- Fix Docker 401: LMS_JWT_JWK_SET_URI for academic/progress; forward-headers

Variant distribution (lab page only):
- round-robin by student level, shared random queue across selected groups,
  least-used variants, manual pins always preserved; occupied/dup labels
- setup page: single form + one «Сохранить всё», batch level/variant for
  selected students (checkboxes)

Grades, defense, materials:
- publication gate + comment lists; defense CSV import (2/3 cols, quotes)
- additional task after defense ("начать заново"); enroll/unenroll students
  from group page; temporary-password lock (no navigation until changed) and
  persistent "Смена пароля"; forced change once
- chapter page /learn/{c}; header: LMS home, Войти, user -> dashboard,
  compact theme toggle; responsive mobile header
- markdown: single newlines are soft breaks; highlight.js with light/dark themes
- lab/content imports ZIP-only (lab.yaml+README+variants/assets,
  chapters tree+assets); duplicate lab numbers rejected; variant ordering
  and date defaults honored

Fixes:
- 500 on student create (@MapsId set before persist); broken dialogs/JS refs in group page
This commit is contained in:
2026-09-14 22:01:46 +03:00
parent 5e60cc3819
commit cdf4fb0ee0
58 changed files with 3967 additions and 884 deletions

145
deploy/DEPLOY.md Normal file
View File

@@ -0,0 +1,145 @@
# Деплой LMS на сервер `lms.our-place.ru`
Стек целиком запускается в Docker. **Java на сервере не устанавливается** — приложение
собирается и работает внутри контейнеров (multi-stage `Dockerfile`: сборка на JDK 21,
запуск на JRE 21).
Компоненты:
| Сервис | Контейнер | Наружу |
|---|---|---|
| web (Thymeleaf UI) | `lms-web` | `127.0.0.1:8090` (проксирует nginx) |
| gateway | `lms-gateway` | нет |
| identity / academic / progress | `lms-*` | нет |
| PostgreSQL 18 | `lms-postgres` | нет |
Данные лежат в именованных volume: `lms_pgdata` (БД), `lms_submissions` (решения),
`lms_assets` (картинки материалов).
---
## 0. Что нужно
- Сервер с Ubuntu 22.04/24.04 (или другим Linux с Docker), sudo/root.
- Домен `lms.our-place.ru`, A-запись которого указывает на IP сервера.
- Открытые порты 80 и 443 (443 — если настраиваете HTTPS).
## 1. Установка Docker
```bash
curl -fsSL https://get.docker.com | sudo sh
sudo usermod -aG docker "$USER" # перелогиньтесь, чтобы работать без sudo
docker compose version # нужна версия v2
```
## 2. Получить код
```bash
sudo mkdir -p /opt/lms && sudo chown "$USER" /opt/lms
git clone <URL-репозитория> /opt/lms
cd /opt/lms
```
## 3. Настроить окружение
```bash
cp .env.prod.example .env.prod
nano .env.prod
```
Обязательно задайте надёжный `LMS_DB_PASSWORD`. Остальные значения по умолчанию подходят.
## 4. Собрать и запустить
```bash
./deploy/deploy.sh up
```
Скрипт соберёт образы (первый раз ~5–15 минут: скачиваются Gradle и зависимости)
и поднимет контейнеры. Проверка:
```bash
docker compose -f docker-compose.prod.yml --env-file .env.prod ps
curl -fsS http://127.0.0.1:8090/actuator/health # {"status":"UP",...}
```
## 5. Nginx + HTTPS
```bash
sudo apt-get update && sudo apt-get install -y nginx certbot python3-certbot-nginx
sudo mkdir -p /var/www/certbot
sudo cp deploy/nginx/lms.our-place.ru.conf /etc/nginx/sites-available/lms.our-place.ru.conf
sudo ln -sf /etc/nginx/sites-available/lms.our-place.ru.conf /etc/nginx/sites-enabled/lms.our-place.ru.conf
sudo rm -f /etc/nginx/sites-enabled/default
sudo nginx -t && sudo systemctl reload nginx
sudo certbot --nginx -d lms.our-place.ru --redirect -m admin@our-place.ru --agree-tos
```
`certbot --nginx` сам добавит TLS-блок и редирект с HTTP на HTTPS. Автопродление
уже настроено пакетом (`systemctl status certbot.timer`).
## 6. Первый запуск приложения
Откройте `https://lms.our-place.ru` → откроется `/setup` → создайте администратора.
## 6a. Наполнение данными
Дальше всё через интерфейс: импорт групп (`.xls`), импорт лабораторной ZIP'ом,
раздача вариантов, оценки и защита, материалы. Подробный порядок с проверками —
в [`IMPORT.md`](IMPORT.md). Файлы со списками студентов и выгрузками паролей —
персональные данные, держите их вне репозитория.
## 7. Обновление версии
```bash
cd /opt/lms
git pull
./deploy/deploy.sh up # пересоберёт изменённые образы и перезапустит
```
Миграции БД (Flyway) применяются автоматически при старте сервисов.
## 8. Резервное копирование
```bash
# БД
docker compose -f docker-compose.prod.yml --env-file .env.prod exec -T postgres \
pg_dump -U "$LMS_DB_USER" "$LMS_DB_NAME" | gzip > lms-$(date +%F).sql.gz
# Файлы (решения и материалы)
docker run --rm -v lms_lms_submissions:/sub -v lms_lms_assets:/assets -v "$PWD":/backup alpine \
tar czf /backup/lms-files-$(date +%F).tar.gz -C / sub assets
```
Восстановление БД: `gunzip -c backup.sql.gz | docker compose ... exec -T postgres psql -U lms lms`.
## 9. Диагностика
```bash
./deploy/deploy.sh ps
./deploy/deploy.sh logs web
./deploy/deploy.sh logs gateway
./deploy/deploy.sh logs identity
```
Типичные проблемы:
- **web отдаёт 502 через nginx** — контейнер `web` не поднялся: смотрите `logs web`.
Часто причина — занят порт 8090 или не задан `LMS_DB_PASSWORD` (compose не стартует).
- **Страница входа открывается, но запросы падают** — проверьте `logs gateway` и
`logs identity`; gateway должен получить JWKS от identity.
- **401 на страницах после входа (Docker)** — у backend-сервисов не задан адрес
JWKS: должно быть `LMS_JWT_JWK_SET_URI=http://identity:8081/api/v1/auth/jwks.json`
(уже прописано в `docker-compose.prod.yml` для academic/progress/gateway).
Внутри контейнера `localhost:8081` недоступен, поэтому токен не проверяется.
- **Не проходят загрузки больших ZIP** — увеличьте `client_max_body_size` в nginx
(сейчас 120 МБ) и `spring.servlet.multipart.max-request-size`.
## Замечания по безопасности
- PostgreSQL наружу не публикуется; связь между контейнерами — по внутренней сети `lms`.
- Публикуется только `127.0.0.1:8090`, доступ снаружи — исключительно через nginx/TLS.
- `.env.prod` содержит пароль БД и не коммитится (в `.gitignore`).
- Пароль администратора задаётся вами на шаге 6; хранение — BCrypt.

78
deploy/IMPORT.md Normal file
View File

@@ -0,0 +1,78 @@
# Порядок наполнения LMS данными (через интерфейс)
Всё делается в админке, порядок: администратор → группы и студенты → лабораторная →
раздача вариантов → защита → материалы.
## Шаг 1. Деплой
См. [`DEPLOY.md`](DEPLOY.md): Docker + nginx + HTTPS. После старта сервисов
откройте домен — попадёте на `/setup`, создайте администратора (логин и пароль
храните в секрете, в репозиторий их не писать).
## Шаг 2. Группы и студенты
**Группы → Импорт** — загрузите `.xls`/`.xlsx`/`.csv` со списком группы
(столбцы: «Студенческий номер», «Фамилия Имя Отчество», «Основа обучения»).
Название группы берётся из файла или указывается при импорте. Логином студента
становится студенческий номер, пароль генерируется (временный, с обязательной
сменой при первом входе).
Дальше по каждому студенту при необходимости:
* **«Сбросить пароль»** — выдать новый временный пароль;
* **«Добавить существующего»** — перевести студента из другой группы;
* **«Отчислить из группы»** — убрать из группы (аккаунт и данные сохраняются).
Выгрузить логины: **«Логины XLSX» / «CSV»**.
## Шаг 3. Лабораторная
**Лабораторные → Импорт** — ZIP со структурой:
```text
lab.yaml # номер, название, points_max, уровни (code/title), сроки, статус
README.md # общее условие (markdown)
variants/01.md # вариант: front-matter (level, number, title) + текст задания
```
Примеры — в личном рабочем каталоге (не в репозитории). После импорта на
странице лабораторной:
* «Настроить» — срок сдачи, дата защиты, максимум баллов, статус;
* уровни/варианты можно править и добавлять прямо там же.
## Шаг 4. Раздача вариантов
На странице лабораторной — **«Распределить»**: выбрать группы, при необходимости
«пересоздать заново». Логика: случайная очередь по всем вариантам выбранного
уровня; без повторов, пока в уровне есть невыданные варианты; закрепленные
вручную не трогаются. Частично/точно — в списке назначений (уровень и вариант по
каждому студенту) или массово чекбоксами.
## Шаг 5. Оценки и защита
На странице группы (матрица):
* **«опубликовать оценки»** — показать/скрыть оценки и комментарии студентам;
* **«поставить оценку»** — балл + комментарии (позитивный/негативный/нейтральный);
* раздел «Индивидуальные задания (защита)»: «Назначить задание» или импорт CSV
(`студент;ЛР;задание` или `студент;задание` при выбранной ЛР), «выдать код»,
«изменить», «текст», после готовности — «доп. задание: начать заново».
## Шаг 6. Учебные материалы
**Лабораторные → материалы (`/learn`)** доступны гостям. Для наполнения:
создание глав/разделов/статей в админке или **импорт ZIP** со структурой:
```text
chapters/01-глава/01-раздел/01-статья.md # front-matter: title, slug
assets/ # картинки для md
```
Материалы видны всем; для редактирования — «Материалы» с правами администратора.
## Замечания
* Данные в `*.xls`/PDF/PDF-пароли содержат персональные сведения — храните их во
временном каталоге и не коммитьте в репозиторий.
* Пароли студентов выдаются автоматически; временный пароль нужно менять при
первом входе (до смены другие страницы недоступны).

59
deploy/deploy.sh Executable file
View File

@@ -0,0 +1,59 @@
#!/usr/bin/env bash
# Деплой LMS в Docker на сервере. Java на хосте не требуется.
#
# ./deploy/deploy.sh # сборка + запуск/обновление
# ./deploy/deploy.sh logs web # логи сервиса
# ./deploy/deploy.sh ps
# ./deploy/deploy.sh down # остановить (данные сохраняются в volume)
set -euo pipefail
cd "$(dirname "$0")/.."
COMPOSE="docker compose -f docker-compose.prod.yml --env-file .env.prod"
if ! command -v docker >/dev/null 2>&1 || ! docker compose version >/dev/null 2>&1; then
echo "Ошибка: нужен Docker с плагином compose v2." >&2
exit 1
fi
if [[ ! -f .env.prod ]]; then
if [[ -f .env.prod.example ]]; then
cp .env.prod.example .env.prod
echo "Создан .env.prod из примера. ОБЯЗАТЕЛЬНО смените LMS_DB_PASSWORD и повторите запуск." >&2
exit 1
fi
echo "Не найден .env.prod" >&2
exit 1
fi
cmd="${1:-up}"
case "$cmd" in
up)
echo ">> Сборка образов (Gradle внутри Docker, сервер Java не требует)..."
$COMPOSE build
echo ">> Запуск сервисов..."
$COMPOSE up -d --remove-orphans
echo
$COMPOSE ps
echo
echo "Готово. Проверьте: curl -fsS http://127.0.0.1:${LMS_WEB_PORT:-8090}/actuator/health"
echo "Первый запуск: откройте https://<домен>/setup и создайте администратора."
;;
logs)
$COMPOSE logs -f --tail=200 "${2:-web}"
;;
ps)
$COMPOSE ps
;;
down)
$COMPOSE down
;;
restart)
$COMPOSE restart "${2:-}"
;;
*)
echo "usage: $0 [up|ps|logs <service>|restart [service]|down]" >&2
exit 1
;;
esac

View File

@@ -0,0 +1,67 @@
# Nginx для LMS на сервере lms.our-place.ru.
#
# Схема:
# интернет -> nginx (80/443) -> 127.0.0.1:8090 (контейнер web)
#
# Установка (Debian/Ubuntu):
# sudo cp deploy/nginx/lms.our-place.ru.conf /etc/nginx/sites-available/lms.our-place.ru.conf
# sudo ln -sf /etc/nginx/sites-available/lms.our-place.ru.conf /etc/nginx/sites-enabled/lms.our-place.ru.conf
# sudo nginx -t && sudo systemctl reload nginx
# sudo certbot --nginx -d lms.our-place.ru # добавит TLS автоматически
#
# Ниже — HTTP-версия, достаточная и для первичного выпуска сертификата,
# и для работы без TLS. После `certbot --nginx` блок server 443 появится сам.
server {
listen 80;
listen [::]:80;
server_name lms.our-place.ru;
# ACME-челлендж Let's Encrypt.
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
# Большие загрузки: ZIP лаб, решения, материалы.
client_max_body_size 120m;
location / {
proxy_pass http://127.0.0.1:8090;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
# Загрузка файлов может быть долгой.
proxy_read_timeout 300s;
proxy_send_timeout 300s;
proxy_connect_timeout 30s;
}
}
# --- Если TLS настраивается вручную (без certbot --nginx) ---
# server {
# listen 443 ssl;
# listen [::]:443 ssl;
# http2 on;
# server_name lms.our-place.ru;
#
# ssl_certificate /etc/letsencrypt/live/lms.our-place.ru/fullchain.pem;
# ssl_certificate_key /etc/letsencrypt/live/lms.our-place.ru/privkey.pem;
# ssl_protocols TLSv1.2 TLSv1.3;
#
# client_max_body_size 120m;
#
# location / {
# proxy_pass http://127.0.0.1:8090;
# proxy_http_version 1.1;
# proxy_set_header Host $host;
# proxy_set_header X-Real-IP $remote_addr;
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# proxy_set_header X-Forwarded-Proto $scheme;
# proxy_read_timeout 300s;
# proxy_send_timeout 300s;
# }
# }