Кастомизация профиля (о себе, аватар) и просмотр профилей других игроков + общая статистика

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-17 17:27:24 +03:00
co-authored by Claude Opus 4.8
parent b302ec769a
commit 96daddae05
22 changed files with 1106 additions and 23 deletions
+82 -1
View File
@@ -1,10 +1,12 @@
"""Роутер текущего пользователя."""
from __future__ import annotations
from fastapi import APIRouter, Depends, Query, Request
from fastapi import APIRouter, Depends, File, Query, Request, UploadFile
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.db.session import get_session
from app.models import User
from app.schemas import api as s
@@ -12,6 +14,8 @@ from app.services import audit_service, group_service, stats_service, user_servi
router = APIRouter(prefix="/users", tags=["users"])
_AVATAR_MAX_BYTES = 2 * 1024 * 1024 # 2 МБ
def build_me(session: Session, user: User) -> s.MeRead:
groups = [
@@ -25,6 +29,8 @@ def build_me(session: Session, user: User) -> s.MeRead:
auth_provider=user.auth_provider,
telegram_id=user.telegram_id,
active_group_id=user.active_group_id,
bio=user.bio,
avatar_url=user_service.avatar_url_for(user.id, user.avatar_path, user.updated_at), # type: ignore[arg-type]
groups=groups,
)
@@ -63,9 +69,84 @@ def update_me(
auth_provider=user.auth_provider,
telegram_id=user.telegram_id,
active_group_id=user.active_group_id,
bio=user.bio,
avatar_url=user_service.avatar_url_for(user.id, user.avatar_path, user.updated_at), # type: ignore[arg-type]
)
@router.patch("/me/profile", response_model=s.MeRead)
def update_my_profile(
body: s.ProfileUpdate,
request: Request,
session: Session = Depends(get_session),
user: User = Depends(get_current_user),
) -> s.MeRead:
user_service.update_bio(session, user, body.bio)
audit_service.record(
session,
actor_id=user.id,
action="update",
entity_type="user",
entity_id=user.id,
payload={"bio": True},
ip=request.client.host if request.client else None,
)
session.commit()
return build_me(session, user)
@router.put("/me/avatar", response_model=s.MeRead)
def upload_my_avatar(
file: UploadFile = File(...),
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.")
user_service.set_avatar(session, user, content, ext)
return build_me(session, user)
@router.delete("/me/avatar", response_model=s.MeRead)
def delete_my_avatar(
session: Session = Depends(get_session),
user: User = Depends(get_current_user),
) -> s.MeRead:
user_service.clear_avatar(session, user)
return build_me(session, user)
@router.get("/{user_id}/avatar")
def get_user_avatar(
user_id: int,
session: Session = Depends(get_session),
_user: User = Depends(get_current_user),
) -> FileResponse:
target = session.get(User, user_id)
path = user_service.avatar_abs_path(target.avatar_path) if target else None
if path is None or not path.exists():
raise NotFoundError("Аватар не найден.")
ext = path.suffix.lstrip(".")
return FileResponse(
path,
media_type=user_service.avatar_media_type(ext),
headers={"Cache-Control": "public, max-age=3600"},
)
@router.get("/{user_id}/profile", response_model=s.PublicProfile)
def get_user_profile(
user_id: int,
session: Session = Depends(get_session),
_user: User = Depends(get_current_user),
) -> dict:
return user_service.public_profile(session, user_id)
@router.get("/me/stats", response_model=s.ProfileStats)
def my_stats(
session: Session = Depends(get_session),