v0.1 - макет интерфейса, аутентификация через логин, аккаунт админа, создание партии в 2 этапа, базовые настройки профиля и группы, переключение между группами, статистика
This commit is contained in:
@@ -0,0 +1,311 @@
|
||||
"""Админ-роутер: отдельный вход (логин+пароль) и управление сущностями."""
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter, Depends, Query, Request, Response
|
||||
from sqlmodel import Session
|
||||
|
||||
from app.auth.deps import get_current_admin
|
||||
from app.core import security
|
||||
from app.core.timeutil import iso_utc
|
||||
from app.db.session import get_session
|
||||
from app.models import User
|
||||
from app.routers.matches import build_match_read
|
||||
from app.schemas import api as s
|
||||
from app.services import admin_service, audit_service, faction_service, match_service
|
||||
from app.services.match_service import ParticipantInput
|
||||
|
||||
router = APIRouter(prefix="/admin", tags=["admin"])
|
||||
|
||||
|
||||
# ─── Аутентификация админа ───────────────────────────────────────────────────
|
||||
|
||||
@router.post("/auth/login", response_model=s.AdminMe)
|
||||
def admin_login(
|
||||
body: s.AdminLogin,
|
||||
request: Request,
|
||||
response: Response,
|
||||
session: Session = Depends(get_session),
|
||||
) -> s.AdminMe:
|
||||
admin = admin_service.authenticate_admin(session, body.username, body.password)
|
||||
security.set_admin_session(response, admin.id) # type: ignore[arg-type]
|
||||
audit_service.record(
|
||||
session,
|
||||
actor_id=admin.id,
|
||||
action="login",
|
||||
entity_type="admin",
|
||||
entity_id=admin.id,
|
||||
ip=request.client.host if request.client else None,
|
||||
user_agent=request.headers.get("user-agent"),
|
||||
)
|
||||
session.commit()
|
||||
return s.AdminMe(id=admin.id, nickname=admin.nickname, role=admin.role) # type: ignore[arg-type]
|
||||
|
||||
|
||||
@router.post("/auth/logout", response_model=s.OkResponse)
|
||||
def admin_logout(response: Response) -> s.OkResponse:
|
||||
security.clear_admin_session(response)
|
||||
return s.OkResponse()
|
||||
|
||||
|
||||
@router.get("/me", response_model=s.AdminMe)
|
||||
def admin_me(admin: User = Depends(get_current_admin)) -> s.AdminMe:
|
||||
return s.AdminMe(id=admin.id, nickname=admin.nickname, role=admin.role) # type: ignore[arg-type]
|
||||
|
||||
|
||||
# ─── Пользователи ────────────────────────────────────────────────────────────
|
||||
|
||||
@router.get("/users", response_model=list[s.AdminUserRead])
|
||||
def list_users(
|
||||
query: str | None = Query(None),
|
||||
session: Session = Depends(get_session),
|
||||
_admin: User = Depends(get_current_admin),
|
||||
) -> list[s.AdminUserRead]:
|
||||
return [
|
||||
s.AdminUserRead(
|
||||
id=u.id, # type: ignore[arg-type]
|
||||
nickname=u.nickname,
|
||||
role=u.role,
|
||||
is_active=u.is_active,
|
||||
auth_provider=u.auth_provider,
|
||||
telegram_id=u.telegram_id,
|
||||
created_at=iso_utc(u.created_at),
|
||||
)
|
||||
for u in admin_service.list_users(session, query)
|
||||
]
|
||||
|
||||
|
||||
@router.patch("/users/{user_id}", response_model=s.AdminUserRead)
|
||||
def update_user(
|
||||
user_id: int,
|
||||
body: s.AdminUserUpdate,
|
||||
request: Request,
|
||||
session: Session = Depends(get_session),
|
||||
admin: User = Depends(get_current_admin),
|
||||
) -> s.AdminUserRead:
|
||||
u = admin_service.update_user(session, user_id, nickname=body.nickname, is_active=body.is_active)
|
||||
audit_service.record(
|
||||
session,
|
||||
actor_id=admin.id,
|
||||
action="update",
|
||||
entity_type="user",
|
||||
entity_id=user_id,
|
||||
payload=body.model_dump(exclude_none=True),
|
||||
ip=request.client.host if request.client else None,
|
||||
)
|
||||
session.commit()
|
||||
return s.AdminUserRead(
|
||||
id=u.id, # type: ignore[arg-type]
|
||||
nickname=u.nickname,
|
||||
role=u.role,
|
||||
is_active=u.is_active,
|
||||
auth_provider=u.auth_provider,
|
||||
telegram_id=u.telegram_id,
|
||||
created_at=u.created_at.isoformat(),
|
||||
)
|
||||
|
||||
|
||||
@router.delete("/users/{user_id}", response_model=s.OkResponse)
|
||||
def delete_user(
|
||||
user_id: int,
|
||||
request: Request,
|
||||
session: Session = Depends(get_session),
|
||||
admin: User = Depends(get_current_admin),
|
||||
) -> s.OkResponse:
|
||||
admin_service.delete_user(session, user_id)
|
||||
audit_service.record(
|
||||
session, actor_id=admin.id, action="delete", entity_type="user", entity_id=user_id,
|
||||
ip=request.client.host if request.client else None,
|
||||
)
|
||||
session.commit()
|
||||
return s.OkResponse()
|
||||
|
||||
|
||||
# ─── Группы ──────────────────────────────────────────────────────────────────
|
||||
|
||||
@router.get("/groups", response_model=list[s.AdminGroupRead])
|
||||
def list_groups(
|
||||
session: Session = Depends(get_session),
|
||||
_admin: User = Depends(get_current_admin),
|
||||
) -> list[s.AdminGroupRead]:
|
||||
return [
|
||||
s.AdminGroupRead(
|
||||
id=g.id, name=g.name, owner_id=g.owner_id, created_at=iso_utc(g.created_at) # type: ignore[arg-type]
|
||||
)
|
||||
for g in admin_service.list_groups(session)
|
||||
]
|
||||
|
||||
|
||||
@router.delete("/groups/{group_id}", response_model=s.OkResponse)
|
||||
def delete_group(
|
||||
group_id: int,
|
||||
request: Request,
|
||||
session: Session = Depends(get_session),
|
||||
admin: User = Depends(get_current_admin),
|
||||
) -> s.OkResponse:
|
||||
admin_service.delete_group(session, group_id)
|
||||
audit_service.record(
|
||||
session, actor_id=admin.id, action="delete", entity_type="group", entity_id=group_id,
|
||||
ip=request.client.host if request.client else None,
|
||||
)
|
||||
session.commit()
|
||||
return s.OkResponse()
|
||||
|
||||
|
||||
# ─── Партии ──────────────────────────────────────────────────────────────────
|
||||
|
||||
@router.get("/matches", response_model=list[s.AdminMatchRead])
|
||||
def list_matches(
|
||||
group_id: int | None = Query(None),
|
||||
user_id: int | None = Query(None),
|
||||
faction_id: int | None = Query(None),
|
||||
limit: int = Query(200, ge=1, le=500),
|
||||
offset: int = Query(0, ge=0),
|
||||
session: Session = Depends(get_session),
|
||||
_admin: User = Depends(get_current_admin),
|
||||
) -> list[s.AdminMatchRead]:
|
||||
return [
|
||||
s.AdminMatchRead(
|
||||
id=m.id, # type: ignore[arg-type]
|
||||
group_id=m.group_id,
|
||||
group_name=gname,
|
||||
status=m.status,
|
||||
played_at=str(m.played_at),
|
||||
duration_minutes=m.duration_minutes,
|
||||
win_reason=m.win_reason, # type: ignore[arg-type]
|
||||
player_count=m.player_count,
|
||||
created_by=m.created_by,
|
||||
created_at=iso_utc(m.created_at),
|
||||
)
|
||||
for m, gname in admin_service.list_matches(
|
||||
session,
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
group_id=group_id,
|
||||
user_id=user_id,
|
||||
faction_id=faction_id,
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
@router.get("/matches/{match_id}", response_model=s.MatchRead)
|
||||
def get_match(
|
||||
match_id: int,
|
||||
session: Session = Depends(get_session),
|
||||
_admin: User = Depends(get_current_admin),
|
||||
) -> s.MatchRead:
|
||||
match = match_service.get_match(session, match_id)
|
||||
return build_match_read(session, match, can_modify=True)
|
||||
|
||||
|
||||
@router.patch("/matches/{match_id}", response_model=s.MatchRead)
|
||||
def update_match(
|
||||
match_id: int,
|
||||
body: s.MatchUpdate,
|
||||
request: Request,
|
||||
session: Session = Depends(get_session),
|
||||
admin: User = Depends(get_current_admin),
|
||||
) -> s.MatchRead:
|
||||
match = match_service.get_match(session, match_id)
|
||||
participants = None
|
||||
if body.participants is not None:
|
||||
participants = [
|
||||
ParticipantInput(
|
||||
user_id=p.user_id,
|
||||
faction_id=p.faction_id,
|
||||
place=p.place,
|
||||
was_random=p.was_random,
|
||||
comment=p.comment,
|
||||
)
|
||||
for p in body.participants
|
||||
]
|
||||
match = match_service.update_match(
|
||||
session,
|
||||
match,
|
||||
played_at=body.played_at,
|
||||
overall_comment=body.overall_comment,
|
||||
overall_comment_set=("overall_comment" in body.model_fields_set),
|
||||
win_reason=body.win_reason,
|
||||
win_reason_set=("win_reason" in body.model_fields_set),
|
||||
participants=participants,
|
||||
)
|
||||
audit_service.record(
|
||||
session,
|
||||
actor_id=admin.id,
|
||||
action="update",
|
||||
entity_type="match",
|
||||
entity_id=match.id,
|
||||
ip=request.client.host if request.client else None,
|
||||
)
|
||||
session.commit()
|
||||
return build_match_read(session, match, can_modify=True)
|
||||
|
||||
|
||||
# ─── Фракции (переименование во всей системе) ─────────────────────────────────
|
||||
|
||||
@router.get("/factions", response_model=list[s.FactionRead])
|
||||
def list_factions(
|
||||
session: Session = Depends(get_session),
|
||||
_admin: User = Depends(get_current_admin),
|
||||
) -> list[s.FactionRead]:
|
||||
return [
|
||||
s.FactionRead(
|
||||
id=f.id, code=f.code, name_ru=f.name_ru, expansion_id=f.expansion_id # type: ignore[arg-type]
|
||||
)
|
||||
for f in faction_service.list_factions(session)
|
||||
]
|
||||
|
||||
|
||||
@router.patch("/factions/{faction_id}", response_model=s.FactionRead)
|
||||
def rename_faction(
|
||||
faction_id: int,
|
||||
body: s.FactionRename,
|
||||
request: Request,
|
||||
session: Session = Depends(get_session),
|
||||
admin: User = Depends(get_current_admin),
|
||||
) -> s.FactionRead:
|
||||
f = admin_service.rename_faction(session, faction_id, body.name_ru)
|
||||
audit_service.record(
|
||||
session,
|
||||
actor_id=admin.id,
|
||||
action="update",
|
||||
entity_type="faction",
|
||||
entity_id=faction_id,
|
||||
payload={"name_ru": f.name_ru},
|
||||
ip=request.client.host if request.client else None,
|
||||
)
|
||||
session.commit()
|
||||
return s.FactionRead(
|
||||
id=f.id, code=f.code, name_ru=f.name_ru, expansion_id=f.expansion_id # type: ignore[arg-type]
|
||||
)
|
||||
|
||||
|
||||
@router.delete("/matches/{match_id}", response_model=s.OkResponse)
|
||||
def delete_match(
|
||||
match_id: int,
|
||||
request: Request,
|
||||
session: Session = Depends(get_session),
|
||||
admin: User = Depends(get_current_admin),
|
||||
) -> s.OkResponse:
|
||||
admin_service.delete_match(session, match_id)
|
||||
audit_service.record(
|
||||
session, actor_id=admin.id, action="delete", entity_type="match", entity_id=match_id,
|
||||
ip=request.client.host if request.client else None,
|
||||
)
|
||||
session.commit()
|
||||
return s.OkResponse()
|
||||
|
||||
|
||||
# ─── Журнал аудита ───────────────────────────────────────────────────────────
|
||||
|
||||
@router.get("/audit-logs", response_model=s.AuditLogList)
|
||||
def audit_logs(
|
||||
action: str | None = Query(None),
|
||||
entity_type: str | None = Query(None),
|
||||
limit: int = Query(100, ge=1, le=500),
|
||||
offset: int = Query(0, ge=0),
|
||||
session: Session = Depends(get_session),
|
||||
_admin: User = Depends(get_current_admin),
|
||||
) -> dict:
|
||||
return admin_service.list_audit_logs(
|
||||
session, action=action, entity_type=entity_type, limit=limit, offset=offset
|
||||
)
|
||||
Reference in New Issue
Block a user