feat: вход, роли и аудит действий (lct-23)

Самое крупное расхождение с ТЗ: входа не было вовсе, экраны открывались
ссылкой с номером занятия, и пускало знание адреса.

- Таблицы users и audit_log, миграция. Пароль argon2, сессия — подписанная
  cookie; роль на сокетах читается из той же cookie в момент рукопожатия,
  отдельного протокола авторизации в канале нет.
- Разграничение: control — преподавателю, observe — преподавателю и админу,
  call и station — обучающемуся и преподавателю. Отказ приходит событием
  error с кодом forbidden.
- Обучающийся не видит чужого: история подменяет фильтр на его собственный
  идентификатор, разбор и профиль сверяют trainee_id. ТЗ запрещает доступ
  к чужим результатам, а не только к чужим экранам.
- Администратору закрыта правка оценок — ТЗ запрещает это прямо.
- make users заводит по записи на роль и печатает случайные пароли один раз:
  зашитый в репозиторий admin/admin пережил бы сдачу.
- Экран входа и проверка роли на каждом маршруте фронта.

Наши инструменты не сломались: make lesson и тесты входят через dev-token
за флагом dev_auth_bypass, на стенде точка отвечает 404 — выключенной
функции не должно быть видно вовсе. У тестов появился conftest.py.

Role уехала в домен и в generated.ts через EventCatalog.principal: иначе
фронт переписывал бы список ролей руками.

181 тест зелёный (14 новых), make typecheck чистый.
This commit is contained in:
Ivan Gerasimov 2026-09-20 09:01:05 +03:00
commit d55c8bc8cd
30 changed files with 984 additions and 21 deletions

31
backend/tests/conftest.py Normal file
View file

@ -0,0 +1,31 @@
"""Общая настройка тестов.
Голосовой контур выключен: он грузит модели около десяти секунд, а проверяется
отдельно в `test_voice_pipeline.py`. Вход включён как на стенде, но тесты
логинятся через `dev-token` — так же, как `make lesson` (tasks/lct-23-auth-roles.md).
"""
import os
os.environ.setdefault("DEV_AUTH_BYPASS", "true")
import pytest # noqa: E402
from app.config import get_settings # noqa: E402
@pytest.fixture(autouse=True, scope="session")
def _dev_auth():
"""Флаг обхода читается один раз при создании настроек."""
get_settings.cache_clear()
settings = get_settings()
assert settings.dev_auth_bypass, "тесты должны ходить через dev-token"
yield
@pytest.fixture
def as_instructor(client):
"""Клиент, вошедший преподавателем. Cookie живёт в самом TestClient."""
response = client.post("/api/auth/dev-token")
assert response.status_code == 200, response.text
return client