Добавлена функция удаления аккаунтов с каскадным удалением из всех записей для дева

This commit is contained in:
2026-06-17 16:34:29 +03:00
parent 865c967d5e
commit 3291c67b2c
9 changed files with 271 additions and 73 deletions
+9 -8
View File
@@ -111,15 +111,16 @@ def create_app() -> FastAPI:
for r in api_routers:
app.include_router(r, prefix="/api")
# DEV-вход (по нику) — только в development и только если код физически есть
# (в test/prod-образе dev_auth/dev_stub исключены, импорт просто не выполнится).
# DEV-роутеры (вход по нику, жёсткое удаление аккаунтов) — только в development
# и только если код физически есть (в test/prod-образе dev_*-файлы исключены
# .dockerignore, импорт просто не выполнится).
if settings.is_development:
try:
from app.routers import dev_auth
app.include_router(dev_auth.router, prefix="/api")
except ImportError:
pass
for mod_name in ("dev_auth", "dev_admin"):
try:
mod = __import__(f"app.routers.{mod_name}", fromlist=["router"])
app.include_router(mod.router, prefix="/api")
except ImportError:
pass
@app.get("/api/health", tags=["meta"])
def health() -> dict:
+3 -14
View File
@@ -104,20 +104,9 @@ def update_user(
)
@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()
# Удаление аккаунта — намеренно НЕ здесь: это dev-only возможность, вынесена в
# routers/dev_admin.py (исключён из прод/тест-образа). В проде аккаунт только
# отключается (PATCH is_active), удалять нельзя.
# ─── Группы ──────────────────────────────────────────────────────────────────
+69
View File
@@ -0,0 +1,69 @@
"""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.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=request.client.host if request.client else None,
)
session.commit()
return s.OkResponse()
+2 -6
View File
@@ -48,12 +48,8 @@ def update_user(session: Session, user_id: int, *, nickname: str | None = None,
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()
# Жёсткое удаление пользователя — dev-only, в services/admin_service нет намеренно:
# логика вынесена в routers/dev_admin.py (файл исключён из прод/тест-образа).
# ─── Группы ──────────────────────────────────────────────────────────────────