Ревью: безопасность и корректность в сервисах бэкенда
Находки прохода /code-review high по backend/app: - achievement_service: slug из URL шёл в путь без проверки, из-за чего DELETE /api/admin/achievements/%2E%2E удалял rmtree'ом родительскую папку каталога ачивок (в проде это /data — БД, uploads, ачивки целиком). - match_service/attachment_service: версия партии = updated_at, но onupdate срабатывает лишь при реальном UPDATE строки matches. Правка одних участников и работа с вложениями его не вызывали, и оптимистичная блокировка молча пропускала конкурентную запись — бампаем updated_at явно. - admin_service: удаление группы с партиями упиралось в RESTRICT и уходило наружу голым 500; теперь понятная ошибка. Админское удаление партии не чистило файлы вложений с тома — они оставались навсегда. - user_service: при повторной загрузке аватара с тем же расширением avatar_path не менялся, updated_at не двигался, и кэш-бастер оставлял старую картинку до часа. Плюс версия считалась из наивного времени как из локального и разъезжалась с лидербордом, где то же поле считает SQL. - membership_service: единственный владелец мог разжаловать сам себя и группа оставалась без владельца навсегда. - notification_service: mark_read не слал SSE-сигнал, и бейдж непрочитанных на других устройствах висел до перезагрузки. - routers/admin: created_at после правки пользователя отдавался без смещения, и дата «создан» прыгала на часовой пояс до следующего обновления списка. #8 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0186Fk74jkkszahEHSjBzTjD
This commit is contained in:
@@ -6,6 +6,7 @@ from typing import Any
|
||||
from sqlmodel import Session, select
|
||||
|
||||
from app.core.errors import (
|
||||
ConflictError,
|
||||
InvalidCredentialsError,
|
||||
NicknameTakenError,
|
||||
NotFoundError,
|
||||
@@ -75,6 +76,10 @@ def delete_group(session: Session, group_id: int) -> None:
|
||||
group = session.get(Group, group_id)
|
||||
if group is None:
|
||||
raise NotFoundError("Группа не найдена.")
|
||||
# matches.group_id — ON DELETE RESTRICT, поэтому группу с партиями БД не отдаст.
|
||||
# Проверяем сами, иначе IntegrityError уходит наружу голым 500 без AppError-конверта.
|
||||
if session.exec(select(Match.id).where(Match.group_id == group_id)).first() is not None:
|
||||
raise ConflictError("Нельзя удалить группу, в которой есть партии. Сначала удалите их.")
|
||||
session.delete(group)
|
||||
session.commit()
|
||||
|
||||
@@ -134,11 +139,16 @@ def rename_faction(session: Session, faction_id: int, name_ru: str) -> Faction:
|
||||
|
||||
|
||||
def delete_match(session: Session, match_id: int) -> None:
|
||||
from app.services import attachment_service # избегаем цикла импорта
|
||||
|
||||
match = session.get(Match, match_id)
|
||||
if match is None:
|
||||
raise NotFoundError("Партия не найдена.")
|
||||
session.delete(match)
|
||||
session.commit()
|
||||
# Как и в игроцком пути (match_service.delete_match): строки вложений уходят
|
||||
# каскадом, а файлы с тома нужно убрать руками, иначе они остаются навсегда.
|
||||
attachment_service.delete_match_files(match_id)
|
||||
|
||||
|
||||
# ─── Журнал аудита ───────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user