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:
@@ -1,7 +1,6 @@
|
||||
"""Пользователи: создание из внешней личности, ник, активная группа, профиль."""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import re
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
@@ -12,7 +11,7 @@ from app.auth.provider import ExternalIdentity
|
||||
from app.core.config import settings
|
||||
from app.core.errors import NicknameTakenError, NotFoundError, ValidationError
|
||||
from app.core.timeutil import utcnow
|
||||
from app.models import AuthIdentity, Faction, GroupMember, User
|
||||
from app.models import AuthIdentity, Faction, User
|
||||
|
||||
_NICK_RE = re.compile(r"^[\w .\-]{2,64}$", re.UNICODE)
|
||||
_BIO_MAX = 500
|
||||
@@ -135,7 +134,7 @@ def register_from_identity(
|
||||
|
||||
def update_nickname(session: Session, user: User, new_nickname: str) -> User:
|
||||
new_nickname = (new_nickname or "").strip()
|
||||
if not _NICK_RE.match(new_nickname):
|
||||
if not nickname_format_ok(new_nickname):
|
||||
raise ValidationError("Ник: 2–64 символа, буквы/цифры/пробел/.-_")
|
||||
if not nickname_available(session, new_nickname, exclude_user_id=user.id):
|
||||
raise NicknameTakenError()
|
||||
@@ -148,12 +147,9 @@ def update_nickname(session: Session, user: User, new_nickname: str) -> User:
|
||||
|
||||
def set_active_group(session: Session, user: User, group_id: int | None) -> User:
|
||||
if group_id is not None:
|
||||
member = session.exec(
|
||||
select(GroupMember).where(
|
||||
GroupMember.group_id == group_id, GroupMember.user_id == user.id
|
||||
)
|
||||
).first()
|
||||
if member is None:
|
||||
from app.services import group_service # избегаем цикла импорта
|
||||
|
||||
if group_service.get_membership(session, group_id, user.id) is None:
|
||||
raise ValidationError("Нельзя сделать активной группу, в которой вы не состоите.")
|
||||
user.active_group_id = group_id
|
||||
session.add(user)
|
||||
@@ -225,6 +221,21 @@ def update_favorite_faction(session: Session, user: User, faction_id: int | None
|
||||
return user
|
||||
|
||||
|
||||
def read_capped_image(file, max_bytes: int, limit_message: str) -> tuple[bytes, str]:
|
||||
"""Прочитать загруженный файл с ограничением размера и убедиться, что это картинка.
|
||||
|
||||
Читаем на байт больше лимита: так превышение видно, не загружая файл целиком.
|
||||
Один хелпер на все загрузки (аватар, фото партии, иконка ачивки) — иначе
|
||||
правка лимита или списка форматов расходится по четырём роутерам."""
|
||||
content = file.file.read(max_bytes + 1)
|
||||
if len(content) > max_bytes:
|
||||
raise ValidationError(limit_message)
|
||||
ext = sniff_image_ext(content)
|
||||
if ext is None:
|
||||
raise ValidationError("Поддерживаются только изображения PNG, JPEG или WebP.")
|
||||
return content, ext
|
||||
|
||||
|
||||
def sniff_image_ext(content: bytes) -> str | None:
|
||||
"""Расширение по магическим байтам (PNG/JPEG/WebP), без Pillow. None — не картинка."""
|
||||
if content.startswith(b"\x89PNG\r\n\x1a\n"):
|
||||
|
||||
Reference in New Issue
Block a user