# 13. Отчёт этапа 3: доступ и профиль студента курса **Дата:** 2026-10-27. **Статус:** этап 3 выполнен; этап 4 (повышение и жизненный цикл) — следующий. ## 1. Что сделано ### Аккаунты преподавателей - Добавлен административный API `/api/v1/teachers`: список, создание, сброс пароля. Создаётся только аккаунт роли `TEACHER`. - Логин проверяется на длину (минимум 3 символа) и уникальность; пустой пароль заменяется сгенерированным. Новый пароль помечается как временный (`mustChangePassword`), показывается один раз. - В админке появилась страница `/admin/teachers` с созданием преподавателя и сбросом пароля; одноразовый пароль выводится во всплывающем уведомлении. ### Курсовая область видимости - `CourseAccessService` в academic-service определяет доступные курсы по роли: - `ADMIN` — все курсы, включая архивные; - `TEACHER` — только активные курсы с активным назначением `course_teachers` (`removed_at IS NULL`); - `STUDENT` — только активные курсы активных групп, в которых студент состоит сейчас (группы запрашиваются у identity-service); - анонимный пользователь до сервиса не доходит — gateway требует JWT. - `GET /api/v1/courses/mine` возвращает курсы текущего пользователя. - `GET /api/v1/courses/{id}` проверяет доступ к курсу. Админские операции создания, переименования, архивирования, назначения преподавателей и привязки групп остались только для `ADMIN`. - Список лабораторных без `courseId` фильтруется по доступным курсам; с `courseId` доступ проверяется явно. Карточка лабораторной тоже проверяет доступ через её курс. Лабораторная без курса доступна только админу. - Недоступный курс/лабораторная дают `403`, несуществующая — `404`. Недоступность identity при проверке групп даёт `502`, а не `403`. ### Внутренние API identity Добавлены read-only эндпоинты для межсервисных вызовов: | Метод | Путь | Назначение | |---|---|---| | `GET` | `/api/v1/internal/students/{studentId}/context` | карточка студента: ФИО, №, статус, год набора, текущая группа | | `POST` | `/api/v1/internal/accounts/lookup` | пакетные логины авторов комментариев и оценок | | `GET` | `/api/v1/internal/groups/{groupId}` | группа и текущий состав без учётных данных | Эндпоинты защищены заголовком `X-Internal-Token`; сравнение токена — constant-time. Gateway не маршрутизирует `/api/v1/internal/**`, поэтому снаружи эти пути недоступны. ### Кабинет преподавателя - `/teacher` — список назначенных курсов с количеством групп и лабораторных. - `/teacher/courses/{id}` — карточка курса: привязанные группы и лабораторные в режиме просмотра. Редактирование курса и лабораторных остаётся у админа. - `/teacher/courses/{courseId}/groups/{groupId}` — состав группы, привязанной к курсу, со ссылками на профиль студента. - Доступ к `/teacher/**` разрешён роли `TEACHER` и админу; студент перенаправляется в свой кабинет, аноним — на вход. После входа преподаватель попадает на `/teacher`. ### Профиль студента курса - Progress-service собирает агрегированный профиль одним запросом: `GET /api/v1/courses/{courseId}/students/{studentId}/profile`. - В ответе: курс, карточка студента, сводка и полный список лабораторных курса. - Для каждой лабораторной: вариант и уровень, число попыток, дата последней сдачи, оценка и её публикация, общий комментарий, комментарии преподавателей, защита (статус, код, задание, остаток времени), все попытки с файлами и ссылкой на скачивание. - Скачивание любой попытки: `GET /api/v1/courses/{courseId}/submissions/{submissionId}/download` (ZIP). Проверяется, что сдача относится к лабораторной этого курса; чужой `submissionId` и чужой курс отклоняются. - BFF-страницы профиля: - `/teacher/courses/{courseId}/students/{studentId}`; - `/admin/courses/{courseId}/students/{studentId}`. - Авторство: миграция `V13__profile_authors.sql` добавляет `grade_comments.author_account_id` и `lab_grades.updated_by_account_id`; новые комментарии и оценки сохраняют автора. Старые записи показываются без автора. Индекс `lab_grades(lab_id)` ускоряет выборку по списку лаб. - В ведомости группы ФИО студента и кнопка «Профиль» ведут на профиль, если группа открыта из карточки курса. На странице группы преподавателя те же ссылки доступны из состава группы. ## 2. Правила доступа | Объект | ADMIN | TEACHER | STUDENT | |---|---|---|---| | Курс | любой, включая архивный | только активный назначенный | только активный курс текущей группы | | Лабораторные курса | все | все курса, включая черновики | все лабораторные доступного курса | | Состав группы курса | любая привязанная группа | группа должна быть активной привязкой курса | доступ к курсу по своей группе | | Профиль студента | любой студент любого курса | студент должен состоять в активной группе курса | только свой профиль | | Оценки и комментарии | все | все, включая неопубликованные | только опубликованные | | Скачивание попытки | любая попытка курса | только студент из активной группы курса | отдельный download-эндпоинт недоступен | | Внутренние API identity | по сервисному токену | по сервисному токену | по сервисному токену | Дополнительно: - Преподаватель чужого курса получает `403`; студент чужого профиля — `403`. - Студент видит только опубликованные оценки и комментарии. Черновики и неопубликованные комментарии скрыты. - Профиль не даёт доступа к другим курсам студента. - Автор комментария/оценки показывается логином; старые записи без автора — «—». Логины подтягиваются одним пакетным запросом. ## 3. Токен и конфигурация - Общий секрет: `LMS_INTERNAL_TOKEN`. - Где задаётся: `lms.internal.token` в `application.yml` identity, academic и progress. Локально допускается значение по умолчанию `dev-internal-token`; в production оно обязательно заменяется. - `docker-compose.prod.yml` передаёт токен в identity, academic и progress; `.env.prod.example` содержит заготовку без секрета. - Заголовок: `X-Internal-Token`. Пустой или неверный токен — `403`. - Gateway не публикует внутренние пути; вызовы идут напрямую между сервисами по внутренней сети Docker. ## 4. UI - `/admin/teachers` — создание преподавателя, список, сброс пароля, отображение временного пароля один раз. - `/teacher` — «Мои курсы». - `/teacher/courses/{id}` — группы и лабораторные курса, переход к составу группы. - `/teacher/courses/{courseId}/groups/{groupId}` — студенты группы, ФИО и кнопка «Открыть» ведут в профиль. - `/teacher/courses/{courseId}/students/{studentId}` и `/admin/courses/{courseId}/students/{studentId}` — единый шаблон профиля с разными маршрутами скачивания и возврата. - В навигации: «Преподаватели» у админа, «Мои курсы» у преподавателя. - Профиль показывает сводку (лабораторных, сдано, оценено, средний балл, защиты), каждую лабораторную, комментарии, защиты и все попытки. ## 5. Тесты Добавлены и расширены наборы: - identity: `InternalControllerIntegrationTest` (12) — контекст студента, пакетные логины, состав группы, неверный/пустой токен, неизвестные id; `TeacherIntegrationTest` (6) — создание, генерация пароля, список, сброс пароля, запрет для не-админа. - academic: `CourseAccessServiceTest` (16) — роли, чужие/архивные курсы, снятые назначения, студенты без групп, `mine`, доступ к лабораторным; `CourseRosterServiceTest` (6) — состав группы, чужая группа/преподаватель, недоступность identity. - progress: `StudentProfileIntegrationTest` (5) — неопубликованные данные для преподавателя, только опубликованное для студента, чужой курс/студент, скачивание чужой сдачи. - web: `StudentProfileTemplateTest`, `ProfileDtoJsonTest`, `TeacherGroupTemplateTest`, `TeacherInterceptorTest`, `CourseGroupRosterDtoTest` — контракты, шаблоны, роли и ссылки. По результатам последнего прогона (2026-10-27) перечисленные наборы прошли с `0 failures / 0 errors`. ## 6. Известные пробелы - **Контент курса.** URL материалов вида `/learn/course/{course_no}/...`, проверка `chapters.course_id/audience`, наследуемая видимость активов и выдача файлов по курсу ещё не реализованы. Схема подготовлена на этапе 2. - **Права преподавателя.** Кабинет преподавателя — просмотр: курс, группы, состав, лабораторные и профили. Редактирование лабораторных, оценок и раздачи вариантов из кабинета не добавлялось. - **Названия групп в кабинете.** На странице курса преподавателя группа показывается UUID: academic отдаёт `groupId`, а имя группы не входит в контракт карточки курса. Нужен batch-запрос имён или расширение DTO. - **Собственный профиль студента.** API допускает `STUDENT` только для своего профиля, но отдельной студенческой BFF-страницы профиля нет; текущие страницы профиля — преподавательская и админская. - **Аудит скачиваний.** Скачивание попыток пока не пишется в аудит; это этап 6.