lct-hack/backend/scripts/start_lesson.py
Ivan Gerasimov d55c8bc8cd 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 чистый.
2026-09-20 09:01:05 +03:00

69 lines
3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""make lesson: запустить занятие и напечатать ссылки на экраны.
Нужен, пока нет пульта преподавателя (lct-17): без запущенного занятия
АРМ курсанта открыть не с чем. Занятие живёт в памяти сервера и после
выхода скрипта остаётся запущенным.
make lesson сценарий по умолчанию, тренировочный режим
make lesson s=fire-apartment-l2 m=exam
"""
import asyncio
import json
import sys
import urllib.request
import uuid
import websockets
BACKEND = "ws://localhost:8000"
FRONTEND = "http://localhost:5173"
def dev_cookie() -> str:
"""Вход для служебного скрипта.
Сокеты закрыты ролями (lct-23), а `make lesson` — наш инструмент, а не
пользователь. Точка `dev-token` живёт за флагом `DEV_AUTH_BYPASS` и на
рабочем стенде отвечает 404 — тогда скрипт честно скажет, что занятие
запускается с пульта преподавателя.
"""
request = urllib.request.Request(
f"{BACKEND.replace('ws://', 'http://')}/api/auth/dev-token", method="POST"
)
try:
with urllib.request.urlopen(request, timeout=5) as response:
return response.headers.get("set-cookie", "").split(";")[0]
except urllib.error.HTTPError as exc:
if exc.code == 404:
raise SystemExit(
"вход выключен: на стенде занятие запускается с пульта преподавателя.\n"
"Для разработки поставь DEV_AUTH_BYPASS=true в backend/.env"
) from exc
raise
async def main(scenario_id: str, mode: str) -> None:
session_id = str(uuid.uuid4())
headers = {"Cookie": dev_cookie()}
async with websockets.connect(
f"{BACKEND}/ws/control/{session_id}", additional_headers=headers
) as control:
await control.send(json.dumps({
"type": "scenario.start",
"scenario_id": scenario_id,
"trainee": "Курсант",
"mode": mode,
}))
await asyncio.sleep(0.5) # дать серверу зарегистрировать занятие до закрытия сокета
print(f"занятие {session_id} — {scenario_id}, режим {mode}")
print(f" курсант: {FRONTEND}/trainee?session={session_id}")
print(f" монитор: {FRONTEND}/wall?session={session_id}")
print(f" преподаватель: {FRONTEND}/instructor?session={session_id}")
if __name__ == "__main__":
scenario = sys.argv[1] if len(sys.argv) > 1 and sys.argv[1] else "fire-apartment-l2"
mode = sys.argv[2] if len(sys.argv) > 2 and sys.argv[2] else "training"
asyncio.run(main(scenario, mode))