Files
ForbiddenStarsApp/backend/app/routers/dev_admin.py
T
NotBigGhostandClaude Opus 5 94e2c09952 Simplify: общие хелперы в роутерах
- IP клиента для аудита брался инлайном в 19 местах шести модулей; теперь
  security.client_ip — за привратником адрес придётся читать из
  X-Forwarded-For, и одна точка правки для этого обязательна.
- Чтение загруженной картинки (лимит размера + sniff формата) было скопировано
  в четыре обработчика; вынесено в user_service.read_capped_image.
- Лимит размера вложения жил двумя одинаковыми константами в игроцком и
  админском роутере — перенесён к самим вложениям.
- update_nickname и set_active_group переиспользуют nickname_format_ok и
  group_service.get_membership вместо собственных копий проверки.
- Убраны осиротевшие импорты и комментарий-заготовка о вложениях, которые
  давно реализованы (MatchAttachment).

#8

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BoiJK9ux8peeyjLb8TYjFf
2026-09-09 18:48:29 +03:00

71 lines
3.5 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.
"""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()