615 lines
46 KiB
Markdown
615 lines
46 KiB
Markdown
# Отчёт по ошибкам и рискам проекта Magistr
|
||
|
||
Дата проверки: 2026-07-10
|
||
|
||
## Объём и способ проверки
|
||
|
||
Проверены:
|
||
|
||
- 166 Java-файлов backend, JPA-модели, репозитории, контроллеры, сервисы и конфигурация мультитенантности;
|
||
- обе Flyway-миграции без изменения существующих файлов;
|
||
- 23 JavaScript-файла, встроенный JavaScript страниц преподавателя/студента, HTML/CSS и SPA-маршрутизация;
|
||
- `compose.yaml`, Dockerfile, Gitea Actions;
|
||
- production-манифесты `../k8s/`, которые AGENTS.md относит к внешним зависимостям проекта;
|
||
- проектная документация и существующий граф связей `graphify-out/graph.json`.
|
||
|
||
Выполненные проверки:
|
||
|
||
- `mvn test` в Maven-контейнере: **30 тестов, 0 failures, 0 errors, BUILD SUCCESS**;
|
||
- `node --check` для всех внешних JS-файлов и извлечённых inline-скриптов: ошибок синтаксиса нет;
|
||
- `docker compose config --quiet`: Compose синтаксически корректен;
|
||
- `kubectl kustomize ../k8s`: Kustomize-манифесты собираются;
|
||
- сопоставление JPA-таблиц с Flyway DDL: отсутствующих таблиц для сущностей не найдено;
|
||
- `git diff --check`: проблем с пробелами и маркерами конфликтов нет.
|
||
|
||
Ограничения проверки: не выполнялись E2E-тесты в браузере, нагрузочное тестирование, тестирование с реальным PostgreSQL и одновременной работой двух backend-pod. Конкурентные дефекты ниже подтверждены по коду и воспроизводимому сценарию, но не запускались против production-кластера.
|
||
|
||
## Сводка
|
||
|
||
| Приоритет | Количество |
|
||
|---|---:|
|
||
| Критический | 2 |
|
||
| Высокий | 10 |
|
||
| Средний | 19 |
|
||
| Низкий | 3 |
|
||
| **Всего** | **34** |
|
||
|
||
## Критические проблемы
|
||
|
||
### 1. Production-манифесты содержат известные JWT/DB-секреты и хранят пароли в ConfigMap
|
||
|
||
**Файлы:**
|
||
|
||
- `backend/src/main/resources/application.properties:18-22`
|
||
- `backend/src/main/java/com/magistr/app/config/auth/JwtProperties.java:13-17`
|
||
- `../k8s/config.yaml:15-26`
|
||
- `../k8s/backend.yaml:1-23`
|
||
- `../k8s/otel-collector.yaml:1-25`
|
||
|
||
**Проблема:** приложение имеет известный JWT-секрет по умолчанию и небезопасный дефолт `refresh-cookie-secure=false`. Production-манифест `config.yaml` передаёт другой, но также заранее известный placeholder-секрет, который удовлетворяет проверке длины. Пароли PostgreSQL записаны открытым текстом в `Secret.stringData`, tenant ConfigMap и ConfigMap коллектора OpenTelemetry. Kubernetes Secret в YAML не шифрует значение в исходном файле, а ConfigMap вообще не предназначен для секретов.
|
||
|
||
**Риск:** подделка access JWT, компрометация всех tenant-БД и метрик, повторное использование опубликованных credentials. Если эти значения когда-либо применялись, их нужно считать скомпрометированными.
|
||
|
||
**Как исправить:**
|
||
|
||
- Немедленно ротировать JWT- и DB-секреты во всех окружениях.
|
||
- Удалить реальные/фиксированные значения из файлов и истории репозиториев.
|
||
- Использовать External Secrets, SOPS/Sealed Secrets или секреты CI/CD; tenant credentials и credentials OTel хранить в Secret, а не ConfigMap.
|
||
- На production-профиле завершать запуск при пустом, дефолтном или известном placeholder `JWT_SECRET` и при `JWT_REFRESH_COOKIE_SECURE != true`.
|
||
|
||
---
|
||
|
||
### 2. Flyway создаёт во всех новых tenant-БД предсказуемые учётные записи и демонстрационные данные
|
||
|
||
**Файлы:**
|
||
|
||
- `backend/src/main/resources/db/migration/V1__init.sql:21-24,38-42,85-92,221-265`
|
||
- `docs/README.md:45-54`
|
||
|
||
**Проблема:** V1 создаёт администратора с публично документированным коротким паролем, а также несколько тестовых пользователей с одинаковым известным паролем. Эта же миграция запускается для tenant-БД, добавленных в production, и одновременно добавляет демонстрационные кафедры, группы и другие данные.
|
||
|
||
**Риск:** немедленный административный доступ к свежему tenant, если пароль не сменили вручную; загрязнение production-БД тестовыми сущностями.
|
||
|
||
**Как исправить:**
|
||
|
||
- Не менять V1. Добавить новую миграцию `V3__disable_demo_accounts.sql`, которая архивирует/удаляет демонстрационные аккаунты и данные там, где они не были явно сохранены.
|
||
- Создавать первого администратора отдельной bootstrap-процедурой с одноразовым случайным секретом.
|
||
- Разнести demo/dev seed и обязательную production-схему.
|
||
- Ротировать пароли уже созданных tenant-БД.
|
||
|
||
## Высокий приоритет
|
||
|
||
### 3. Race condition при ротации refresh-токена
|
||
|
||
**Файлы:**
|
||
|
||
- `backend/src/main/java/com/magistr/app/config/auth/RefreshTokenService.java:50-91`
|
||
- `backend/src/main/java/com/magistr/app/repository/AuthRefreshTokenRepository.java:10`
|
||
|
||
**Проблема:** `rotate()` читает токен, проверяет его активность, а затем отзывает старый и создаёт новый без блокировки строки или условного атомарного UPDATE. Два параллельных запроса с одним cookie могут оба пройти проверку и создать две новые сессии.
|
||
|
||
**Риск:** повторное использование refresh-токена, раздвоение цепочки ротации и обход ожидаемой семантики single-use.
|
||
|
||
**Как исправить:** использовать `PESSIMISTIC_WRITE` либо атомарный `UPDATE ... WHERE revoked_at IS NULL AND expires_at > now()` и создавать следующий токен только при обновлении одной строки. Добавить конкурентный интеграционный тест с PostgreSQL.
|
||
|
||
---
|
||
|
||
### 4. `AuthContext` остаётся в servlet-потоке после отказа по роли
|
||
|
||
**Файл:** `backend/src/main/java/com/magistr/app/config/auth/AuthorizationInterceptor.java:45-49,65-68`
|
||
|
||
**Проблема:** пользователь записывается в ThreadLocal до проверки `@RequireRoles`. Если interceptor возвращает `false`, его собственный `afterCompletion()` не вызывается, поэтому контекст не очищается.
|
||
|
||
**Риск:** следующий запрос на том же потоке может увидеть данные предыдущего пользователя, особенно если он проходит через публичный auth endpoint или останавливается в более раннем interceptor.
|
||
|
||
**Как исправить:** очищать контекст в начале `preHandle()` и перед каждым `return false`; ещё лучше — устанавливать пользователя только после успешной проверки роли. Добавить MockMvc-тест последовательных запросов на одном executor-потоке.
|
||
|
||
---
|
||
|
||
### 5. Точечные изменения расписания создаются без проверки конфликтов и факта существования пары
|
||
|
||
**Файлы:**
|
||
|
||
- `backend/src/main/java/com/magistr/app/controller/ScheduleOverrideController.java:71-126`
|
||
- `backend/src/main/java/com/magistr/app/service/ScheduleQueryService.java:67-141`
|
||
|
||
**Проблема:** для `MOVE`/`REPLACE` проверяется только существование сущностей. Не проверяется занятость преподавателя, аудитории и групп в новой паре. Также можно сохранить override на дату, когда базовый слот вообще не генерируется, либо сохранить фактически пустой `REPLACE`.
|
||
|
||
**Риск:** двойное назначение ресурсов; накопление неработающих изменений, которые API считает успешными.
|
||
|
||
**Как исправить:** перед сохранением построить исходную пару на указанную дату, проверить action-specific поля и пересечения уже применённого расписания. При конфликте возвращать `409 Conflict`.
|
||
|
||
---
|
||
|
||
### 6. Расписание преподавателя не показывает пары, назначенные ему через override
|
||
|
||
**Файлы:**
|
||
|
||
- `backend/src/main/java/com/magistr/app/service/ScheduleQueryService.java:47-63`
|
||
- `backend/src/main/java/com/magistr/app/repository/ScheduleRuleRepository.java:36-58`
|
||
- `backend/src/main/java/com/magistr/app/repository/ScheduleOverrideRepository.java:14-26`
|
||
|
||
**Проблема:** запрос только по `teacherId` сначала выбирает базовые правила этого преподавателя. Если override заменяет преподавателя A на B, правило A не попадает в исходную выборку B, поэтому последующее `applyOverrides()` уже не может добавить пару.
|
||
|
||
**Риск:** преподаватель B не видит назначенную ему замену.
|
||
|
||
**Как исправить:** учитывать overrides с `newTeacher.id = teacherId` за период и достраивать соответствующие базовые пары до финальной фильтрации.
|
||
|
||
---
|
||
|
||
### 7. Валидатор правил расписания пропускает несколько нарушений инвариантов
|
||
|
||
**Файлы:**
|
||
|
||
- `backend/src/main/java/com/magistr/app/controller/ScheduleRuleAdminController.java:220-228,440-481,524-565`
|
||
- `backend/src/main/java/com/magistr/app/service/ScheduleGeneratorService.java:252-293`
|
||
- `backend/src/main/resources/db/migration/V1__init.sql:707-726`
|
||
|
||
**Проблемы:**
|
||
|
||
- конфликт ищется только с уже сохранёнными правилами; два конфликтующих/дублирующих слота внутри одного нового правила не сравниваются между собой;
|
||
- пользователь слота проверяется на архивность, но не на роль `TEACHER`;
|
||
- `lessonFormat` проверяется только на непустоту, хотя DB допускает лишь «Очно»/«Онлайн»;
|
||
- нечётное число академических часов принимается, хотя генератор списывает по два часа и может выдать больше часов, чем задано.
|
||
|
||
**Риск:** дубли в один момент времени, назначение студента/администратора преподавателем, HTTP 500 на DB CHECK и неверный расход часов.
|
||
|
||
**Как исправить:** валидировать пары новых слотов между собой, роль пользователя, enum формата и кратность часов двум; продублировать ключевые ограничения новой Flyway-миграцией там, где это возможно.
|
||
|
||
---
|
||
|
||
### 8. Ошибка в строке календарной сетки возвращает 400, но удаляет старую сетку и коммитит часть новой
|
||
|
||
**Файл:** `backend/src/main/java/com/magistr/app/controller/AcademicCalendarController.java:125-146`
|
||
|
||
**Проблема:** `saveGrid()` работает в транзакции, сначала удаляет всю старую сетку, затем валидирует и сохраняет строки по одной. `IllegalArgumentException` ловится внутри transactional-метода и не выходит за границу proxy, поэтому транзакция считается успешной.
|
||
|
||
**Воспроизведение:** передать список, где первая строка корректна, а вторая содержит неверный день/дату. API вернёт 400, но старая сетка будет удалена, а первая новая строка сохранится.
|
||
|
||
**Риск:** тихая потеря календарного учебного графика при ошибке пользователя.
|
||
|
||
**Как исправить:** сначала провалидировать весь список и уникальность ключей, затем одним шагом заменять данные; либо не перехватывать исключение внутри транзакции/помечать транзакцию rollback-only.
|
||
|
||
---
|
||
|
||
### 9. Обновление tenant может удалить рабочее подключение и успешно сохранить нерабочее
|
||
|
||
**Файлы:**
|
||
|
||
- `backend/src/main/java/com/magistr/app/controller/DatabaseController.java:104-124,175-182`
|
||
- `backend/src/main/java/com/magistr/app/config/tenant/TenantRoutingDataSource.java:135-148`
|
||
- `backend/src/main/java/com/magistr/app/config/tenant/TenantConfigWatcher.java:133-160`
|
||
|
||
**Проблема:** существующий DataSource сначала удаляется и закрывается. Новый Hikari pool создаётся с `initializationFailTimeout=-1`. Ошибка Flyway проглатывается, а результат записи ConfigMap игнорируется. API способен вернуть успех после неудачной миграции или неудачной персистенции.
|
||
|
||
**Риск:** одна опечатка в URL/пароле отключает рабочий tenant; разные pod получают разную конфигурацию.
|
||
|
||
**Как исправить:** создать временный pool, проверить соединение и миграции, затем атомарно заменить старый. Ошибки миграции и ConfigMap должны прерывать операцию; нужен compensating rollback.
|
||
|
||
---
|
||
|
||
### 10. Два backend-pod могут потерять изменения tenant-конфигурации, а probes не видят отказ БД
|
||
|
||
**Файлы:**
|
||
|
||
- `../k8s/backend.yaml:31,86-97`
|
||
- `backend/src/main/java/com/magistr/app/controller/DatabaseController.java:177-182`
|
||
- `backend/src/main/java/com/magistr/app/config/tenant/ConfigMapUpdater.java:62-92`
|
||
|
||
**Проблема:** deployment имеет две реплики. Каждая PATCH-операция перезаписывает весь `tenants.json` из локальной in-memory карты без resourceVersion/compare-and-swap. Параллельное добавление A на pod-1 и B на pod-2 приводит к last-write-wins и потере A или B. TCP readiness/liveness считает pod здоровым, даже если все tenant-БД недоступны и Flyway завершился ошибкой.
|
||
|
||
**Риск:** потерянные конфигурационные изменения и маршрутизация трафика на фактически неработающий pod.
|
||
|
||
**Как исправить:** вынести операции в единый coordinator/CRD/БД либо применять optimistic locking к ConfigMap с повтором; добавить health endpoint, проверяющий миграции и доступность обязательных tenant.
|
||
|
||
---
|
||
|
||
### 11. Дашборд может показать зелёное «конфликтов нет», когда проверка фактически не выполнена
|
||
|
||
**Файл:** `frontend/admin/js/views/dashboard.js:148-168,274-287`
|
||
|
||
**Проблемы:**
|
||
|
||
- диапазон недели вычисляется двойным мутированием одного `Date`; в первые дни месяца конец диапазона попадает в начало предыдущего месяца и оказывается раньше начала;
|
||
- каждый запрос кафедры имеет `catch(() => [])`, поэтому сетевые/серверные ошибки превращаются в пустое расписание;
|
||
- после этого пустой результат отображается как «Конфликты расписания не обнаружены».
|
||
|
||
**Проверенный пример:** для 2026-07-02 код строит диапазон примерно 2026-06-29 … 2026-06-05 вместо 2026-06-29 … 2026-07-05.
|
||
|
||
**Риск:** ложное подтверждение безопасности расписания именно в панели контроля.
|
||
|
||
**Как исправить:** вычислять воскресенье как `monday + 6 дней` на отдельном объекте, форматировать локальную дату без UTC и различать состояния «нет конфликтов»/«проверка частично или полностью не выполнена».
|
||
|
||
---
|
||
|
||
### 12. Access JWT и удалённый JavaScript выполняются в одном origin
|
||
|
||
**Файлы:**
|
||
|
||
- `frontend/admin/js/api.js:5-7,155-163`
|
||
- `frontend/script.js:5-28`
|
||
- `frontend/admin/js/otel.js:1-8`
|
||
|
||
**Проблема:** JWT хранится в `localStorage`. Страница входа динамически исполняет неприкреплённые по версии модули с `esm.sh`, а admin — удалённые модули без SRI. Такой код имеет доступ к DOM, введённому паролю и `localStorage`.
|
||
|
||
**Риск:** компрометация CDN/пакета или XSS сразу даёт учётные данные и Bearer-токен.
|
||
|
||
**Как исправить:** собирать и раздавать telemetry dependencies локально с lockfile; держать access token в памяти или перейти на защищённую cookie-сессию; добавить строгий CSP и убрать inline-скрипты.
|
||
|
||
## Средний приоритет
|
||
|
||
### 13. Watcher не применяет изменения существующего tenant и не повторяет неудачную синхронизацию
|
||
|
||
**Файл:** `backend/src/main/java/com/magistr/app/config/tenant/TenantConfigWatcher.java:60-71,102-125`
|
||
|
||
**Проблема:** сравниваются только множества доменов — смена URL, логина или пароля существующего домена игнорируется. Кроме того, новый hash записывается до JSON parsing/sync; если синхронизация падает, следующий poll считает файл уже обработанным и не повторяет операцию.
|
||
|
||
**Риск:** pod продолжает использовать старые credentials либо навсегда остаётся в частично синхронизированном состоянии до следующего изменения файла/рестарта.
|
||
|
||
**Как исправить:** сравнивать весь нормализованный `TenantConfig`, записывать hash только после полного успеха и сохранять retry/backoff-состояние.
|
||
|
||
---
|
||
|
||
### 14. Полностью отключена проверка TLS Kubernetes API
|
||
|
||
**Файл:** `backend/src/main/java/com/magistr/app/config/tenant/ConfigMapUpdater.java:74-76,104-119`
|
||
|
||
**Проблема:** trust manager принимает любой сертификат. Kubernetes CA уже доступен в serviceaccount volume.
|
||
|
||
**Риск:** MITM к API server, утечка serviceaccount token и подмена tenant ConfigMap.
|
||
|
||
**Как исправить:** загружать `/var/run/secrets/kubernetes.io/serviceaccount/ca.crt`, использовать стандартную проверку цепочки и hostname.
|
||
|
||
---
|
||
|
||
### 15. Редактор временных слотов позволяет создать пересекающиеся интервалы и сломать базовые правила
|
||
|
||
**Файлы:**
|
||
|
||
- `backend/src/main/java/com/magistr/app/controller/TimeSlotAdminController.java:152-224`
|
||
- `backend/src/main/java/com/magistr/app/service/ScheduleGeneratorService.java:418-435`
|
||
|
||
**Проблемы:**
|
||
|
||
- проверяется только уникальность номера пары, но не пересечение временных интервалов внутри сетки;
|
||
- update разрешает перенести существующий базовый слот в MANUAL scope, хотя правила обязаны ссылаться на DEFAULT;
|
||
- `durationMinutes` может не соответствовать разнице `startTime/endTime`.
|
||
|
||
**Риск:** два занятия реально идут одновременно, но конфликтный анализ считает их разными слотами; существующие правила исчезают из ожидаемой базовой сетки.
|
||
|
||
**Как исправить:** запретить пересечение интервалов, фиксировать scope используемого базового слота и вычислять duration на backend.
|
||
|
||
---
|
||
|
||
### 16. Учебные годы и семестры допускают пересечения и выход семестра за границы года
|
||
|
||
**Файлы:**
|
||
|
||
- `backend/src/main/java/com/magistr/app/controller/AcademicCalendarAdminController.java:46-129,188-237`
|
||
- `backend/src/main/java/com/magistr/app/service/AcademicDateService.java:32-34`
|
||
- `backend/src/main/java/com/magistr/app/repository/SemesterRepository.java:13-17`
|
||
|
||
**Проблема:** проверяется только порядок двух дат. Можно пересечь учебные годы, пересечь осенний и весенний семестры, вынести семестр за границы года. При update также не проверяется дубликат title/type до DB constraint.
|
||
|
||
**Риск:** `findFirst...` выбирает произвольный семестр для даты; номер недели и чётность становятся недетерминированными, а часть update-запросов заканчивается HTTP 500.
|
||
|
||
**Как исправить:** централизовать календарную валидацию, запретить пересечения и добавить DB-level exclusion/range constraints новой миграцией.
|
||
|
||
---
|
||
|
||
### 17. Изменение календаря или группы нарушает уже сохранённые назначения
|
||
|
||
**Файлы:**
|
||
|
||
- `backend/src/main/java/com/magistr/app/controller/AcademicCalendarController.java:85-98,178-214`
|
||
- `backend/src/main/java/com/magistr/app/controller/GroupController.java:173-212,322-359`
|
||
|
||
**Проблема:** после назначения графика группе можно изменить у графика учебный год, специальность, профиль, форму или количество курсов; можно также изменить эти поля у группы. Существующее `student_group_calendar_assignments` не перепроверяется и не удаляется. При уменьшении courseCount остаются строки сетки старших курсов.
|
||
|
||
**Риск:** расписание строится по графику, который больше не соответствует группе/году, хотя API проверял соответствие при первоначальном назначении.
|
||
|
||
**Как исправить:** запрещать изменение ключевых измерений у используемого графика либо транзакционно перевалидировать назначения, grid и subjects.
|
||
|
||
---
|
||
|
||
### 18. Историческая модель кафедр преподавателя используется непоследовательно
|
||
|
||
**Файлы:**
|
||
|
||
- `backend/src/main/java/com/magistr/app/controller/UserController.java:218-259,310-324`
|
||
- `backend/src/main/java/com/magistr/app/controller/WorkloadController.java:51-60,85-100`
|
||
- `backend/src/main/java/com/magistr/app/controller/TeacherSubjectController.java:102-115`
|
||
|
||
**Проблемы:**
|
||
|
||
- будущий перевод немедленно меняет `users.department_id`, и преподаватель до `validFrom` появляется одновременно в старой и новой кафедре;
|
||
- исторический список фильтрует `User::isActiveRecord`, поэтому архивный преподаватель исчезает даже для даты, когда он работал;
|
||
- workload прошлых периодов относится к текущему `users.department_id`;
|
||
- дополнительная активная кафедральная связь учитывается в списке преподавателей, но `TeacherSubjectController` разрешает связь только по legacy `users.department_id`.
|
||
|
||
**Риск:** неверные исторические отчёты и неработоспособность дополнительного назначения преподавателя.
|
||
|
||
**Как исправить:** считать кафедру через `teacher_department_assignments` на дату; legacy-поле обновлять только в дату вступления перевода в силу либо не использовать для бизнес-решений.
|
||
|
||
---
|
||
|
||
### 19. Роль `DEPARTMENT` может запрашивать workload чужой кафедры
|
||
|
||
**Файл:** `backend/src/main/java/com/magistr/app/controller/WorkloadController.java:43-135`
|
||
|
||
**Проблема:** endpoints принимают произвольный `departmentId` или отсутствие фильтра и не сопоставляют его с `AuthContext.departmentId`.
|
||
|
||
**Риск:** межкафедральное раскрытие отчётов и обход ограничений, которые уже применяются в `DepartmentWorkspaceController` и части `UserController`.
|
||
|
||
**Как исправить:** для `DEPARTMENT` принудительно использовать кафедру из токена; явно документировать endpoints, где глобальный просмотр действительно разрешён.
|
||
|
||
---
|
||
|
||
### 20. Глобальные workload/free-classrooms ломаются при количестве активных групп больше 50
|
||
|
||
**Файлы:**
|
||
|
||
- `backend/src/main/java/com/magistr/app/controller/WorkloadController.java:43-135`
|
||
- `backend/src/main/java/com/magistr/app/service/ScheduleQueryService.java:160-180`
|
||
|
||
**Проблема:** агрегирующие endpoints переиспользуют общий поиск с ограничением 50 групп. `free-classrooms` вообще не принимает scope, поэтому в крупном tenant неизбежно получает `IllegalArgumentException` вместо списка свободных аудиторий.
|
||
|
||
**Риск:** штатная функция перестаёт работать при росте университета.
|
||
|
||
**Как исправить:** сделать специализированные агрегирующие запросы/батч-генерацию, а не использовать ограничение интерактивного широкого поиска.
|
||
|
||
---
|
||
|
||
### 21. Импорт кафедры может переназначить дисциплину другой кафедры
|
||
|
||
**Файл:** `backend/src/main/java/com/magistr/app/controller/DepartmentWorkspaceController.java:80-90`
|
||
|
||
**Проблема:** дисциплина ищется по глобальному имени, после чего `departmentId` безусловно меняется на текущую кафедру.
|
||
|
||
**Риск:** потеря принадлежности и изменение расписаний/календарей другой кафедры.
|
||
|
||
**Как исправить:** не менять чужую запись; решить бизнес-правило уникальности и при необходимости перейти на `(department_id, lower(name))` новой миграцией.
|
||
|
||
---
|
||
|
||
### 22. Права frontend и backend расходятся для учебного отдела
|
||
|
||
**Файлы:**
|
||
|
||
- `backend/src/main/java/com/magistr/app/controller/SpecialityController.java:23-24,38-58,196-213`
|
||
- `backend/src/main/java/com/magistr/app/controller/EducationFormController.java:17-18,33-59`
|
||
- `frontend/admin/js/views/academic-calendar.js:106-118,207-218`
|
||
- `frontend/admin/settings/js/main.js:47-52`
|
||
|
||
**Проблемы:**
|
||
|
||
- `EDUCATION_OFFICE` видит вкладку календарных графиков, но её обязательные GET `/api/specialties` и profiles разрешены только `ADMIN`; инициализация попадает в 403;
|
||
- settings показывает учебному отделу CRUD форм обучения, но POST/DELETE backend разрешены только `ADMIN`.
|
||
|
||
**Риск:** видимые штатные экраны частично или полностью не работают.
|
||
|
||
**Как исправить:** согласовать role matrix в одном источнике; либо расширить read/write права, либо скрыть недоступные действия.
|
||
|
||
---
|
||
|
||
### 23. Генерация расписания имеет выраженный N+1 по дням, правилам и группам
|
||
|
||
**Файлы:**
|
||
|
||
- `backend/src/main/java/com/magistr/app/service/ScheduleGeneratorService.java:64-90,107-124,187-229,297-308`
|
||
- `backend/src/main/java/com/magistr/app/service/AcademicDateService.java:58-87`
|
||
|
||
**Проблема:** семестр повторно ищется внутри каждого правила, а `isTheoryDay()` для каждой группы и даты выполняет отдельные запросы assignment + calendar day. В teacher mode это повторяется по всем группам правила.
|
||
|
||
**Риск:** диапазон 120 дней и десятки групп порождают тысячи/десятки тысяч SQL-запросов, таймауты и нагрузку на каждую tenant-БД.
|
||
|
||
**Как исправить:** заранее батчем загружать semesters, assignments и calendar days за диапазон; передавать уже найденный semester в `processRuleForDate()`. Добавить метрики query count и нагрузочный тест.
|
||
|
||
---
|
||
|
||
### 24. Истёкшие и отозванные refresh-токены никогда не удаляются
|
||
|
||
**Файлы:**
|
||
|
||
- `backend/src/main/java/com/magistr/app/config/auth/RefreshTokenService.java:34-107`
|
||
- `backend/src/main/java/com/magistr/app/repository/AuthRefreshTokenRepository.java:8-11`
|
||
|
||
**Проблема:** каждый login/refresh добавляет строку, ротация лишь ставит `revoked_at`. Cleanup job/repository delete отсутствует.
|
||
|
||
**Риск:** неограниченный рост `auth_refresh_tokens` и индексов.
|
||
|
||
**Как исправить:** периодически удалять давно истёкшие/отозванные строки с разумным audit retention; покрыть job тестом.
|
||
|
||
---
|
||
|
||
### 25. DB constraint violations превращаются в 500 и иногда раскрывают текст драйвера
|
||
|
||
**Файлы:**
|
||
|
||
- `backend/src/main/java/com/magistr/app/controller/GlobalExceptionHandler.java:23-55`
|
||
- `backend/src/main/java/com/magistr/app/controller/GroupController.java:166-169,216-219`
|
||
- `backend/src/main/java/com/magistr/app/controller/SubjectController.java:122-125`
|
||
- `backend/src/main/java/com/magistr/app/controller/DatabaseController.java:121-124,166-171`
|
||
|
||
**Проблема:** нет общего обработчика `DataIntegrityViolationException`/constraint name. Часть контроллеров ловит общий `Exception` и возвращает клиенту `e.getMessage()`.
|
||
|
||
**Риск:** неправильный HTTP status, английские/технические сообщения в UI и раскрытие деталей схемы/JDBC.
|
||
|
||
**Как исправить:** добавить единый перевод constraint → 409/400 с русским сообщением; не отдавать внутренний exception text.
|
||
|
||
---
|
||
|
||
### 26. Чистый локальный запуск по документации не публикует приложение и имеет противоречивую конфигурацию БД
|
||
|
||
**Файлы:**
|
||
|
||
- `compose.yaml:1-43`
|
||
- `backend/src/main/resources/application.properties:3-6,18-22`
|
||
- `docs/README.md:33-43`
|
||
- `docs/INFRASTRUCTURE.md:3-37`
|
||
|
||
**Проблемы:**
|
||
|
||
- frontend/backend не имеют `ports`, а Caddy не входит в Compose, поэтому на чистой машине `localhost:80` после указанной команды недоступен;
|
||
- `POSTGRES_DB` может изменить создаваемую DB, но backend URL жёстко указывает `app_db`;
|
||
- JWT-переменные из `.env` не передаются контейнеру backend;
|
||
- для PostgreSQL не объявлен именованный volume, поэтому после down/recreate данные могут остаться в потерянном anonymous volume.
|
||
|
||
**Как исправить:** добавить локальный reverse proxy/ports, передавать согласованный JDBC URL и JWT env, объявить named volume и обновить quick start.
|
||
|
||
---
|
||
|
||
### 27. CI/CD разворачивает код без запуска тестов, а tag pipeline перезапускает `:main`
|
||
|
||
**Файлы:**
|
||
|
||
- `.gitea/workflows/docker-build.yaml:3-8,35-40,63-68,73-96`
|
||
- `backend/Dockerfile:1-6`
|
||
- `../k8s/backend.yaml:45-46`
|
||
- `../k8s/frontend.yaml:20-21`
|
||
|
||
**Проблема:** workflow не имеет test job, а Dockerfile выполняет `mvn package -DskipTests`. На событии tag metadata публикует tag-образ, но deployment продолжает ссылаться на mutable `:main` и просто перезапускается.
|
||
|
||
**Риск:** production получает непроверенный commit; release-tag может перезапустить старый main-образ вместо релиза.
|
||
|
||
**Как исправить:** обязательные test/compile/static-check jobs до push/deploy; деплой image digest или конкретного release tag; concurrency lock и rollback при failed rollout.
|
||
|
||
---
|
||
|
||
### 28. Нет ограничения попыток входа
|
||
|
||
**Файлы:**
|
||
|
||
- `backend/src/main/java/com/magistr/app/controller/AuthController.java:57-86`
|
||
- `../k8s/ingress.yaml:1-45`
|
||
|
||
**Проблема:** login не имеет rate limit, задержки, lockout или CAPTCHA; в Ingress также нет соответствующего middleware/аннотации.
|
||
|
||
**Риск:** online brute force и password spraying, особенно опасные вместе с известными seed-аккаунтами.
|
||
|
||
**Как исправить:** rate limit по tenant+IP+username, прогрессивная задержка и аудит неудачных входов без логирования пароля.
|
||
|
||
---
|
||
|
||
### 29. Часовой пояс backend не зафиксирован, а часть frontend дат строится через UTC
|
||
|
||
**Файлы:**
|
||
|
||
- `backend/src/main/java/com/magistr/app/model/LifecycleEntity.java:94-98`
|
||
- `backend/src/main/java/com/magistr/app/controller/UserController.java:85,164,235,271`
|
||
- `backend/src/main/java/com/magistr/app/service/ScheduleGeneratorService.java:236`
|
||
- `frontend/admin/js/views/dashboard.js:48,159-160`
|
||
- `frontend/admin/js/views/department-workspace.js:494-504`
|
||
- `compose.yaml` и `../k8s/backend.yaml`
|
||
|
||
**Проблема:** бизнес-даты используют `LocalDate.now()`/`LocalDateTime.now()` с timezone JVM, но контейнеры не получают `TZ`/`user.timezone`. Frontend использует `toISOString()`, поэтому в Москве до 03:00 получает предыдущую календарную дату.
|
||
|
||
**Риск:** архивирование, перевод, доступность ресурсов и «сегодняшнее расписание» сдвигаются на день/несколько часов в зависимости от окружения.
|
||
|
||
**Как исправить:** внедрить `Clock` и явный `ZoneId` для бизнес-даты, хранить абсолютные timestamps как UTC/`Instant`, форматировать date-only локальными компонентами.
|
||
|
||
---
|
||
|
||
### 30. Сборки и deployment используют mutable/непроверяемые артефакты
|
||
|
||
**Файлы:**
|
||
|
||
- `backend/Dockerfile:7`
|
||
- `../k8s/otel-collector.yaml:59`
|
||
- `../k8s/backend.yaml:45-46`
|
||
- `../k8s/frontend.yaml:20-21`
|
||
- `.gitea/workflows/docker-build.yaml:83-87`
|
||
|
||
**Проблема:** javaagent и kubectl скачиваются как latest/stable без checksum, collector использует `latest`, приложения — mutable `main`.
|
||
|
||
**Риск:** невоспроизводимые сборки, неожиданные несовместимости и supply-chain подмена.
|
||
|
||
**Как исправить:** pin версии и image digest, проверять SHA256/signature, генерировать SBOM и сканировать образы.
|
||
|
||
---
|
||
|
||
### 31. Группы допускают некорректную численность и ломают существующее деление на подгруппы
|
||
|
||
**Файлы:**
|
||
|
||
- `backend/src/main/java/com/magistr/app/controller/GroupController.java:173-212,250-272`
|
||
- `backend/src/main/java/com/magistr/app/controller/SubgroupController.java:151-175`
|
||
- `backend/src/main/resources/db/migration/V1__init.sql:203-219`
|
||
|
||
**Проблема:** `groupSize` проверяется только на null, `yearStartStudy` — только на ноль; DB не имеет CHECK для положительной численности. При update можно уменьшить группу ниже суммы активных подгрупп, хотя SubgroupController такой результат запрещает при редактировании подгрупп.
|
||
|
||
**Риск:** отрицательная/нулевая численность, неверный расчёт вместимости и внутренне противоречивые подгруппы.
|
||
|
||
**Как исправить:** единый валидатор create/update, проверка суммы подгрупп и новая миграция с CHECK constraints.
|
||
|
||
## Низкий приоритет
|
||
|
||
### 32. Лимит «120 дней» фактически разрешает 121 дату
|
||
|
||
**Файлы:**
|
||
|
||
- `backend/src/main/java/com/magistr/app/service/ScheduleQueryService.java:148-157`
|
||
- `backend/src/main/java/com/magistr/app/service/ScheduleGeneratorService.java:135-144`
|
||
|
||
**Проблема:** проверяется `DAYS.between(start, end) > 120`, но обе границы включены. Разница 120 означает 121 календарную дату.
|
||
|
||
**Как исправить:** проверять включительную длину (`between + 1`) либо изменить текст/документацию.
|
||
|
||
---
|
||
|
||
### 33. Остались XSS-точки в сообщениях ошибок и видимые поля пароля
|
||
|
||
**Файлы:**
|
||
|
||
- `frontend/admin/settings/js/views/database.js:39,49`
|
||
- `frontend/admin/js/main.js:260-262`
|
||
- `frontend/admin/settings/js/main.js:130-133`
|
||
- `frontend/admin/views/users.html:11-12`
|
||
- `frontend/admin/js/views/teacher-requests.js:47-51`
|
||
|
||
**Проблема:** несколько `e.message` вставляются через `innerHTML` без экранирования. Поля пароля при создании пользователя/одобрении заявки имеют `type="text"`.
|
||
|
||
**Риск:** потенциальный DOM XSS при попадании управляемого текста ошибки; пароль виден на экране и может сохраняться как обычный текст формы.
|
||
|
||
**Как исправить:** использовать `textContent`/`escapeHtml`, а для паролей — `type="password"` и корректные `autocomplete`.
|
||
|
||
---
|
||
|
||
### 34. Проектный языковой регламент нарушен в UI и логах
|
||
|
||
**Файлы:**
|
||
|
||
- `backend/src/main/java/com/magistr/app/config/tenant/TenantRoutingDataSource.java:65,78,95`
|
||
- `backend/src/main/java/com/magistr/app/config/tenant/TenantDataSourceConfig.java:60,123`
|
||
- `frontend/admin/settings/js/views/database.js:14-16,60-62`
|
||
- `frontend/admin/js/otel.js:47`
|
||
|
||
**Проблема:** присутствуют английские production-логи и UI-метки `Online/Offline`; JDBC exception также может попасть в UI на английском.
|
||
|
||
**Как исправить:** привести пользовательские сообщения и проектные логи к русскому языку; технические идентификаторы оставлять в structured fields.
|
||
|
||
## Что покрыть тестами в первую очередь
|
||
|
||
1. PostgreSQL/Testcontainers + Flyway: чистая БД, upgrade существующей БД, отсутствие активных demo-аккаунтов.
|
||
2. Два параллельных refresh-запроса с одним токеном.
|
||
3. Цепочка interceptor: 403, затем публичный/защищённый запрос на том же потоке.
|
||
4. Override с конфликтом и поиск преподавателя, назначенного через `newTeacher`.
|
||
5. Внутренние конфликты одного нового правила, роль преподавателя, нечётные часы и недопустимый формат.
|
||
6. `saveGrid` с корректной и ошибочной строкой: после 400 старая сетка должна остаться целой.
|
||
7. Изменение tenant URL/credentials, неудачная миграция, параллельные PATCH от двух pod.
|
||
8. Более 50 активных групп для workload/free-classrooms.
|
||
9. Future-dated перевод преподавателя и исторические отчёты.
|
||
10. E2E для `EDUCATION_OFFICE`: календарные графики и настройки форм обучения.
|
||
11. Frontend-тест дат в первые дни месяца и в 00:00–03:00 Europe/Moscow.
|
||
|
||
## Рекомендуемый порядок исправления
|
||
|
||
1. Ротация/удаление секретов и отключение seed-аккаунтов.
|
||
2. Data-loss и security races: calendar grid, refresh rotation, AuthContext.
|
||
3. Атомарность tenant-конфигурации и реальные health checks.
|
||
4. Конфликты overrides/rules и корректность расписания преподавателя.
|
||
5. Согласование ролей, календарных инвариантов и исторической модели кафедр.
|
||
6. Производительность генератора, Compose/CI/CD и frontend hardening.
|
||
|
||
Существующие Flyway-миграции не изменять. Все исправления схемы оформлять новой миграцией, начиная с `V3__...sql`.
|