Files
magistr/BUG_REPORT.md
2026-07-10 22:20:50 +03:00

46 KiB
Raw Blame History

Отчёт по ошибкам и рискам проекта 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:0003: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.