v0.1 - макет интерфейса, аутентификация через логин, аккаунт админа, создание партии в 2 этапа, базовые настройки профиля и группы, переключение между группами, статистика

This commit is contained in:
2026-06-16 17:41:06 +03:00
parent 6ab74f01aa
commit 56b5d09a4d
123 changed files with 13572 additions and 21 deletions
+168
View File
@@ -0,0 +1,168 @@
"""Админ: аутентификация (логин=ник + пароль) и управление сущностями."""
from __future__ import annotations
from typing import Any
from sqlmodel import Session, select
from app.core.errors import InvalidCredentialsError, NotFoundError, ValidationError
from app.core.security import verify_password
from app.core.timeutil import iso_utc
from app.models import AuditLog, Faction, Group, Match, MatchParticipant, User
def authenticate_admin(session: Session, username: str, password: str) -> User:
user = session.exec(
select(User).where(
User.nickname == username,
User.role == "admin",
User.auth_provider == "local",
User.is_active == True, # noqa: E712
)
).first()
if user is None or not user.password_hash or not verify_password(password, user.password_hash):
raise InvalidCredentialsError()
return user
# ─── Пользователи ────────────────────────────────────────────────────────────
def list_users(session: Session, query: str | None = None) -> list[User]:
stmt = select(User).order_by(User.created_at.desc())
if query:
stmt = stmt.where(User.nickname.contains(query))
return list(session.exec(stmt).all())
def update_user(session: Session, user_id: int, *, nickname: str | None = None, is_active: bool | None = None) -> User:
user = session.get(User, user_id)
if user is None:
raise NotFoundError("Пользователь не найден.")
if nickname is not None:
user.nickname = nickname.strip()
if is_active is not None:
user.is_active = is_active
session.add(user)
session.commit()
session.refresh(user)
return user
def delete_user(session: Session, user_id: int) -> None:
user = session.get(User, user_id)
if user is None:
raise NotFoundError("Пользователь не найден.")
session.delete(user)
session.commit()
# ─── Группы ──────────────────────────────────────────────────────────────────
def list_groups(session: Session) -> list[Group]:
return list(session.exec(select(Group).order_by(Group.created_at.desc())).all())
def delete_group(session: Session, group_id: int) -> None:
group = session.get(Group, group_id)
if group is None:
raise NotFoundError("Группа не найдена.")
session.delete(group)
session.commit()
# ─── Партии ──────────────────────────────────────────────────────────────────
def list_matches(
session: Session,
limit: int = 200,
offset: int = 0,
group_id: int | None = None,
user_id: int | None = None,
faction_id: int | None = None,
) -> list[tuple[Match, str]]:
"""Возвращает партии вместе с названием группы (для группировки в админке).
Фильтры: по группе, по игроку-участнику, по фракции участника.
"""
stmt = (
select(Match, Group.name)
.join(Group, Group.id == Match.group_id)
.order_by(Match.group_id, Match.played_at.desc(), Match.id.desc())
)
if group_id is not None:
stmt = stmt.where(Match.group_id == group_id)
if user_id is not None:
stmt = stmt.where(
Match.id.in_(
select(MatchParticipant.match_id).where(MatchParticipant.user_id == user_id)
)
)
if faction_id is not None:
stmt = stmt.where(
Match.id.in_(
select(MatchParticipant.match_id).where(
MatchParticipant.faction_id == faction_id
)
)
)
rows = session.exec(stmt.offset(offset).limit(limit)).all()
return [(mt, gname) for mt, gname in rows]
def rename_faction(session: Session, faction_id: int, name_ru: str) -> Faction:
"""Переименовывает фракцию во всей системе (имя хранится один раз)."""
name_ru = (name_ru or "").strip()
if not (1 <= len(name_ru) <= 64):
raise ValidationError("Название фракции: 1–64 символа.")
faction = session.get(Faction, faction_id)
if faction is None:
raise NotFoundError("Фракция не найдена.")
faction.name_ru = name_ru
session.add(faction)
session.commit()
session.refresh(faction)
return faction
def delete_match(session: Session, match_id: int) -> None:
match = session.get(Match, match_id)
if match is None:
raise NotFoundError("Партия не найдена.")
session.delete(match)
session.commit()
# ─── Журнал аудита ───────────────────────────────────────────────────────────
def list_audit_logs(
session: Session,
*,
action: str | None = None,
entity_type: str | None = None,
limit: int = 100,
offset: int = 0,
) -> dict[str, Any]:
stmt = select(AuditLog).order_by(AuditLog.created_at.desc())
if action:
stmt = stmt.where(AuditLog.action == action)
if entity_type:
stmt = stmt.where(AuditLog.entity_type == entity_type)
rows = session.exec(stmt.offset(offset).limit(limit)).all()
return {
"items": [
{
"id": r.id,
"actor_id": r.actor_id,
"action": r.action,
"entity_type": r.entity_type,
"entity_id": r.entity_id,
"payload": r.payload,
"ip": r.ip,
"user_agent": r.user_agent,
"created_at": iso_utc(r.created_at),
}
for r in rows
],
"limit": limit,
"offset": offset,
}