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
This commit is contained in:
2026-09-09 18:48:29 +03:00
co-authored by Claude Opus 5
parent 8ac2757cd6
commit 94e2c09952
10 changed files with 71 additions and 63 deletions
+18 -22
View File
@@ -7,12 +7,14 @@ from sqlmodel import Session
from app.auth.deps import get_current_admin
from app.core import security
from app.core.errors import NotFoundError, ValidationError
from app.core.security import client_ip
from app.core.errors import NotFoundError
from app.core.timeutil import iso_utc
from app.db.session import get_session
from app.models import User
from app.routers.matches import attachment_read, build_match_read
from app.schemas import api as s
from app.services.match_service import ParticipantInput
from app.services import (
achievement_service,
admin_service,
@@ -25,8 +27,6 @@ from app.services import (
)
_ACHIEVEMENT_ICON_MAX_BYTES = 2 * 1024 * 1024 # 2 МБ
_ATTACHMENT_MAX_BYTES = 10 * 1024 * 1024 # 10 МБ
from app.services.match_service import ParticipantInput
router = APIRouter(prefix="/admin", tags=["admin"])
@@ -48,7 +48,7 @@ def admin_login(
action="login",
entity_type="admin",
entity_id=admin.id,
ip=request.client.host if request.client else None,
ip=client_ip(request),
user_agent=request.headers.get("user-agent"),
)
session.commit()
@@ -104,7 +104,7 @@ def update_user(
entity_type="user",
entity_id=user_id,
payload=body.model_dump(exclude_none=True),
ip=request.client.host if request.client else None,
ip=client_ip(request),
)
session.commit()
return s.AdminUserRead(
@@ -148,7 +148,7 @@ def delete_group(
admin_service.delete_group(session, group_id)
audit_service.record(
session, actor_id=admin.id, action="delete", entity_type="group", entity_id=group_id,
ip=request.client.host if request.client else None,
ip=client_ip(request),
)
session.commit()
return s.OkResponse()
@@ -239,7 +239,7 @@ def update_match(
action="update",
entity_type="match",
entity_id=match.id,
ip=request.client.host if request.client else None,
ip=client_ip(request),
)
session.commit()
notify.match_changed(session, match)
@@ -277,7 +277,7 @@ def rename_faction(
entity_type="faction",
entity_id=faction_id,
payload={"name_ru": f.name_ru},
ip=request.client.host if request.client else None,
ip=client_ip(request),
)
session.commit()
return s.FactionRead(
@@ -296,7 +296,7 @@ def delete_match(
admin_service.delete_match(session, match_id)
audit_service.record(
session, actor_id=admin.id, action="delete", entity_type="match", entity_id=match_id,
ip=request.client.host if request.client else None,
ip=client_ip(request),
)
session.commit()
notify.match_removed(session, match_id, group_id)
@@ -326,12 +326,9 @@ def admin_add_attachment(
admin: User = Depends(get_current_admin),
) -> s.AttachmentRead:
match = match_service.get_match(session, match_id)
content = file.file.read(_ATTACHMENT_MAX_BYTES + 1)
if len(content) > _ATTACHMENT_MAX_BYTES:
raise ValidationError("Файл слишком большой (макс. 10 МБ).")
ext = user_service.sniff_image_ext(content)
if ext is None:
raise ValidationError("Поддерживаются только изображения PNG, JPEG или WebP.")
content, ext = user_service.read_capped_image(
file, attachment_service.MAX_ATTACHMENT_BYTES, "Файл слишком большой (макс. 10 МБ)."
)
att = attachment_service.add_photo(
session, match, admin, content, ext, user_service.avatar_media_type(ext)
)
@@ -389,7 +386,7 @@ def create_achievement(
audit_service.record(
session, actor_id=admin.id, action="create", entity_type="achievement",
payload={"slug": ach["slug"], "name": ach["name"]},
ip=request.client.host if request.client else None,
ip=client_ip(request),
)
session.commit()
return ach
@@ -408,7 +405,7 @@ def update_achievement(
)
audit_service.record(
session, actor_id=admin.id, action="update", entity_type="achievement",
payload={"slug": slug}, ip=request.client.host if request.client else None,
payload={"slug": slug}, ip=client_ip(request),
)
session.commit()
return ach
@@ -420,10 +417,9 @@ def upload_achievement_icon(
file: UploadFile = File(...),
_admin: User = Depends(get_current_admin),
) -> dict:
content = file.file.read(_ACHIEVEMENT_ICON_MAX_BYTES + 1)
if len(content) > _ACHIEVEMENT_ICON_MAX_BYTES:
raise ValidationError("Файл слишком большой (макс. 2 МБ).")
ext = achievement_service.validate_icon(content)
content, ext = user_service.read_capped_image(
file, _ACHIEVEMENT_ICON_MAX_BYTES, "Файл слишком большой (макс. 2 МБ)."
)
return achievement_service.set_icon(slug, content, ext)
@@ -437,7 +433,7 @@ def delete_achievement(
achievement_service.delete(slug)
audit_service.record(
session, actor_id=admin.id, action="delete", entity_type="achievement",
payload={"slug": slug}, ip=request.client.host if request.client else None,
payload={"slug": slug}, ip=client_ip(request),
)
session.commit()
return s.OkResponse()
+2 -1
View File
@@ -18,6 +18,7 @@ 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
@@ -63,7 +64,7 @@ def delete_user_hard(
entity_type="user",
entity_id=user_id,
payload={"hard": True, "nickname": nickname},
ip=request.client.host if request.client else None,
ip=client_ip(request),
)
session.commit()
return s.OkResponse()
+3 -2
View File
@@ -5,6 +5,7 @@ from fastapi import APIRouter, Depends, Query, Request
from sqlmodel import Session
from app.auth.deps import get_current_user
from app.core.security import client_ip
from app.core.timeutil import iso_utc
from app.db.session import get_session
from app.models import User
@@ -61,7 +62,7 @@ def create_group(
entity_type="group",
entity_id=group.id,
payload={"name": group.name},
ip=request.client.host if request.client else None,
ip=client_ip(request),
user_agent=request.headers.get("user-agent"),
)
session.commit()
@@ -157,7 +158,7 @@ def invite_member(
entity_type="group_invitation",
entity_id=group_id,
payload={"invited_user_id": invited.id, "nickname": invited.nickname},
ip=request.client.host if request.client else None,
ip=client_ip(request),
user_agent=request.headers.get("user-agent"),
)
session.commit()
+9 -12
View File
@@ -6,7 +6,8 @@ from fastapi.responses import FileResponse
from sqlmodel import Session
from app.auth.deps import get_current_user
from app.core.errors import ConflictError, NoGroupError, NotFoundError, ValidationError
from app.core.security import client_ip
from app.core.errors import ConflictError, NoGroupError, NotFoundError
from app.core.timeutil import iso_utc
from app.db.session import get_session
from app.models import Match, MatchAttachment, User
@@ -24,7 +25,6 @@ from app.services.match_service import FinishInput, ParticipantInput, RosterInpu
router = APIRouter(prefix="/matches", tags=["matches"])
_ATTACHMENT_MAX_BYTES = 10 * 1024 * 1024 # 10 МБ
def attachment_read(att: MatchAttachment, base: str) -> s.AttachmentRead:
@@ -119,7 +119,7 @@ def start_match(
entity_type="match",
entity_id=match.id,
payload={"group_id": match.group_id, "player_count": match.player_count, "status": "in_progress"},
ip=request.client.host if request.client else None,
ip=client_ip(request),
user_agent=request.headers.get("user-agent"),
)
session.commit()
@@ -165,7 +165,7 @@ def finish_match(
entity_type="match",
entity_id=match.id,
payload={"event": "finish", "win_reason": match.win_reason, "duration_minutes": match.duration_minutes},
ip=request.client.host if request.client else None,
ip=client_ip(request),
user_agent=request.headers.get("user-agent"),
)
session.commit()
@@ -225,7 +225,7 @@ def update_match(
action="update",
entity_type="match",
entity_id=match.id,
ip=request.client.host if request.client else None,
ip=client_ip(request),
user_agent=request.headers.get("user-agent"),
)
session.commit()
@@ -250,12 +250,9 @@ def add_attachment(
) -> s.AttachmentRead:
match = match_service.get_match(session, match_id)
_assert_can_attach(session, match, user)
content = file.file.read(_ATTACHMENT_MAX_BYTES + 1)
if len(content) > _ATTACHMENT_MAX_BYTES:
raise ValidationError("Файл слишком большой (макс. 10 МБ).")
ext = user_service.sniff_image_ext(content)
if ext is None:
raise ValidationError("Поддерживаются только изображения PNG, JPEG или WebP.")
content, ext = user_service.read_capped_image(
file, attachment_service.MAX_ATTACHMENT_BYTES, "Файл слишком большой (макс. 10 МБ)."
)
att = attachment_service.add_photo(
session, match, user, content, ext, user_service.avatar_media_type(ext)
)
@@ -315,7 +312,7 @@ def delete_match(
entity_type="match",
entity_id=match_id_val,
payload={"group_id": group_id_val},
ip=request.client.host if request.client else None,
ip=client_ip(request),
user_agent=request.headers.get("user-agent"),
)
session.commit()
+7 -9
View File
@@ -6,7 +6,8 @@ from fastapi.responses import FileResponse
from sqlmodel import Session
from app.auth.deps import get_current_user
from app.core.errors import NotFoundError, ValidationError
from app.core.security import client_ip
from app.core.errors import NotFoundError
from app.db.session import get_session
from app.models import User
from app.schemas import api as s
@@ -61,7 +62,7 @@ def update_me(
entity_type="user",
entity_id=user.id,
payload={"nickname": user.nickname},
ip=request.client.host if request.client else None,
ip=client_ip(request),
user_agent=request.headers.get("user-agent"),
)
session.commit()
@@ -108,7 +109,7 @@ def update_my_profile(
entity_type="user",
entity_id=user.id,
payload={key: True for key in changed},
ip=request.client.host if request.client else None,
ip=client_ip(request),
)
session.commit()
return build_me(session, user)
@@ -120,12 +121,9 @@ def upload_my_avatar(
session: Session = Depends(get_session),
user: User = Depends(get_current_user),
) -> s.MeRead:
content = file.file.read(_AVATAR_MAX_BYTES + 1)
if len(content) > _AVATAR_MAX_BYTES:
raise ValidationError("Файл слишком большой (макс. 2 МБ).")
ext = user_service.sniff_image_ext(content)
if ext is None:
raise ValidationError("Поддерживаются только изображения PNG, JPEG или WebP.")
content, ext = user_service.read_capped_image(
file, _AVATAR_MAX_BYTES, "Файл слишком большой (макс. 2 МБ)."
)
user_service.set_avatar(session, user, content, ext)
return build_me(session, user)