lct-hack/backend/scripts/users.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

78 lines
3.1 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 users: завести учётные записи для занятия.
Без входа система не работает (lct-23), а заводить первого администратора
руками в базе — плохой первый шаг для того, кто разворачивает стенд.
Скрипт создаёт по одной записи на роль и печатает пароли один раз.
Пароли случайные: зашитый в репозиторий «admin/admin» переживёт сдачу
и уедет в реальный класс.
make users создать недостающие
make users force=1 пересоздать пароли существующих
"""
import asyncio
import os
import secrets
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
from sqlalchemy import select # noqa: E402
from app.api.auth import hash_password # noqa: E402
from app.db.base import get_sessionmaker # noqa: E402
from app.db.models import Trainee, User # noqa: E402
from app.domain.roles import Role # noqa: E402
PEOPLE = [
("admin", "Администратор стенда", Role.ADMIN, None),
("teacher", "Преподаватель", Role.INSTRUCTOR, None),
("trainee", "Курсант", Role.TRAINEE, "ДДС района"),
]
async def main() -> int:
force = bool(os.environ.get("force"))
created: list[tuple[str, str]] = []
async with get_sessionmaker()() as db:
for login, full_name, role, service in PEOPLE:
user = await db.scalar(select(User).where(User.login == login))
if user is not None and not force:
print(f" {login}: уже есть")
continue
password = secrets.token_urlsafe(9)
if user is None:
user = User(login=login, full_name=full_name, role=role.value, service=service)
db.add(user)
user.password_hash = hash_password(password)
user.blocked = False
# У обучающегося должна быть карточка курсанта: по ней собираются
# профиль и история, и по ней же разбор закрывается от чужих.
if role is Role.TRAINEE and user.trainee_id is None:
trainee = Trainee(name=full_name)
db.add(trainee)
await db.flush()
user.trainee_id = trainee.id
created.append((login, password))
await db.commit()
if not created:
print("все учётные записи на месте (пересоздать: make users force=1)")
return 0
print("\nЗаписано. Пароли показываются один раз:\n")
for login, password in created:
print(f" {login:10} {password}")
print("\nСменить пароль можно только пересозданием: экран смены пароля — lct-24.")
return 0
if __name__ == "__main__":
raise SystemExit(asyncio.run(main()))