"""DEV-ТОЛЬКО роутер: жёсткое удаление аккаунта игрока. Этот файл ФИЗИЧЕСКИ исключён из прод-образа (.dockerignore), а роутер подключается лишь когда APP_ENV == development (см. app/main.py). На фронте кнопка удаления вырезается из прод-сборки тришейкингом (import.meta.env.DEV). Так возможность удаления не попадает в прод — там аккаунт можно только отключить (PATCH is_active). Семантика («вычёркивание из партий»): аккаунт удаляется, а партии сохраняются — строки участия игрока удаляются (player_count партий НЕ пересчитывается, поэтому их статистика может стать несогласованной — осознанный выбор). Созданные им партии и группы в его владении переназначаются на админа (FK RESTRICT не даёт удалить пользователя, а сами партии/группы мы сохраняем). """ from __future__ import annotations from fastapi import APIRouter, Depends, Request from sqlmodel import Session, select from app.auth.deps import get_current_admin from app.core.security import client_ip from app.core.errors import NotFoundError, ValidationError from app.db.session import get_session from app.models import Group, Match, MatchParticipant, User from app.schemas import api as s from app.services import audit_service router = APIRouter(prefix="/admin/dev", tags=["admin-dev"]) @router.delete("/users/{user_id}", response_model=s.OkResponse) def delete_user_hard( user_id: int, request: Request, session: Session = Depends(get_session), admin: User = Depends(get_current_admin), ) -> s.OkResponse: user = session.get(User, user_id) if user is None: raise NotFoundError("Пользователь не найден.") if user.role == "admin": raise ValidationError("Нельзя удалить администратора.") nickname = user.nickname # сохраняем до удаления (для аудита) # 1. Вычёркиваем игрока из всех партий (его строки участия). for part in session.exec( select(MatchParticipant).where(MatchParticipant.user_id == user_id) ).all(): session.delete(part) # 2. Партии и группы, где он создатель/владелец, переназначаем на админа. for match in session.exec(select(Match).where(Match.created_by == user_id)).all(): match.created_by = admin.id # type: ignore[assignment] session.add(match) for group in session.exec(select(Group).where(Group.owner_id == user_id)).all(): group.owner_id = admin.id # type: ignore[assignment] session.add(group) session.flush() # 3. group_members и auth_identity уйдут каскадом, audit_log.actor_id → NULL. session.delete(user) audit_service.record( session, actor_id=admin.id, action="delete", entity_type="user", entity_id=user_id, payload={"hard": True, "nickname": nickname}, ip=client_ip(request), ) session.commit() return s.OkResponse()