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:
parent
4c2bbfd021
commit
d55c8bc8cd
30 changed files with 984 additions and 21 deletions
|
|
@ -8,11 +8,13 @@
|
|||
from datetime import datetime
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.api.auth import require
|
||||
from app.domain.roles import Role
|
||||
from app.db.base import get_session
|
||||
from app.db.models import Group, Score, Session, Trainee
|
||||
|
||||
|
|
@ -59,7 +61,10 @@ class ProfileOut(BaseModel):
|
|||
|
||||
|
||||
@router.get("", response_model=list[TraineeOut])
|
||||
async def listing(db: AsyncSession = Depends(get_session)) -> list[TraineeOut]:
|
||||
async def listing(request: Request, db: AsyncSession = Depends(get_session)) -> list[TraineeOut]:
|
||||
"""Список курсантов — преподавателю и администратору: обучающемуся он
|
||||
не нужен, а чужие фамилии из него видны."""
|
||||
require(request, Role.INSTRUCTOR, Role.ADMIN)
|
||||
rows = await db.execute(
|
||||
select(Trainee, Group.name).join(Group, Group.id == Trainee.group_id, isouter=True)
|
||||
)
|
||||
|
|
@ -67,7 +72,12 @@ async def listing(db: AsyncSession = Depends(get_session)) -> list[TraineeOut]:
|
|||
|
||||
|
||||
@router.get("/{trainee_id}/profile", response_model=ProfileOut)
|
||||
async def profile(trainee_id: UUID, db: AsyncSession = Depends(get_session)) -> ProfileOut:
|
||||
async def profile(
|
||||
trainee_id: UUID, request: Request, db: AsyncSession = Depends(get_session)
|
||||
) -> ProfileOut:
|
||||
who = require(request)
|
||||
if who.role is Role.TRAINEE and who.trainee_id != trainee_id:
|
||||
raise HTTPException(status_code=403, detail="not_your_profile")
|
||||
trainee = await db.get(Trainee, trainee_id)
|
||||
if trainee is None:
|
||||
raise HTTPException(status_code=404, detail="trainee_not_found")
|
||||
|
|
|
|||
Loading…
Reference in a new issue