"""Раздел «Пользователи»: список с поиском, карточка и управление.""" from __future__ import annotations from fastapi import APIRouter, HTTPException, Request, Response from fastapi.responses import HTMLResponse, RedirectResponse from admin.security import check_csrf from common import __version__ from common.repositories import users_admin_repo router = APIRouter(prefix="/users", tags=["users"]) #: Сколько пользователей показывать на странице. PAGE_SIZE = 25 def _editor_id(request: Request) -> int: """Telegram-ID текущего администратора (ставит middleware).""" return int(request.state.admin["telegram_id"]) async def _card_response( request: Request, user_id: int, *, errors: list[str] | None = None, saved: str | None = None, status_code: int = 200, ) -> HTMLResponse: """Отрисовать карточку (общая точка для GET и ошибок валидации).""" user = await users_admin_repo.get_user(user_id) if user is None: raise HTTPException(status_code=404, detail="Пользователь не найден") return request.app.state.templates.TemplateResponse( request=request, name="users/card.html", context={ "version": __version__, "user": user, "money": await users_admin_repo.money_history(user_id), "titles": await users_admin_repo.list_titles(), "history": await users_admin_repo.user_history(user_id), "errors": errors or [], "saved": saved, "max_nick_length": users_admin_repo.MAX_NICK_LENGTH, "max_comment_length": users_admin_repo.MAX_COMMENT_LENGTH, }, status_code=status_code, ) @router.get("", response_class=HTMLResponse) async def list_users(request: Request, q: str = "", page: int = 1) -> HTMLResponse: """Список пользователей: поиск по нику/username/имени/ID и пагинация.""" total = await users_admin_repo.count_users(q) pages = max((total + PAGE_SIZE - 1) // PAGE_SIZE, 1) page = min(max(page, 1), pages) return request.app.state.templates.TemplateResponse( request=request, name="users/list.html", context={ "version": __version__, "users": await users_admin_repo.list_users( q, limit=PAGE_SIZE, offset=(page - 1) * PAGE_SIZE, ), "q": q, "page": page, "pages": pages, "total": total, }, ) @router.get("/{user_id}", response_class=HTMLResponse) async def user_card(request: Request, user_id: int, saved: str | None = None) -> HTMLResponse: """Карточка пользователя: сводка, управление, история и операции.""" return await _card_response(request, user_id, saved=saved) @router.post("/{user_id}/nick") async def update_nick(request: Request, user_id: int) -> Response: """Сменить ник игрока.""" form = await request.form() check_csrf(request, str(form.get("csrf_token", ""))) nick = str(form.get("nick", "")).strip() errors: list[str] = [] if not nick: errors.append("Ник не может быть пустым") elif len(nick) > users_admin_repo.MAX_NICK_LENGTH: errors.append(f"Ник длиннее {users_admin_repo.MAX_NICK_LENGTH} символов") elif any(char.isspace() for char in nick): errors.append("Ник не должен содержать пробелы") if errors: return await _card_response(request, user_id, errors=errors, status_code=400) if not await users_admin_repo.set_nick(user_id, nick, editor_id=_editor_id(request)): raise HTTPException(status_code=404, detail="Пользователь не найден") return RedirectResponse(url=f"/users/{user_id}?saved=nick", status_code=303) @router.post("/{user_id}/title") async def update_title(request: Request, user_id: int) -> Response: """Сменить звание игрока.""" form = await request.form() check_csrf(request, str(form.get("csrf_token", ""))) try: title_id = int(str(form.get("title_id", ""))) except (TypeError, ValueError): return await _card_response(request, user_id, errors=["Выберите звание"], status_code=400) titles = await users_admin_repo.list_titles() if not any(int(title["id_title"]) == title_id for title in titles): return await _card_response(request, user_id, errors=["Звание не найдено"], status_code=400) if not await users_admin_repo.set_title(user_id, title_id, editor_id=_editor_id(request)): raise HTTPException(status_code=404, detail="Пользователь не найден") return RedirectResponse(url=f"/users/{user_id}?saved=title", status_code=303) @router.post("/{user_id}/subscription") async def update_subscription(request: Request, user_id: int) -> Response: """Подписать или отписать от рассылок.""" form = await request.form() check_csrf(request, str(form.get("csrf_token", ""))) value = str(form.get("subscribed", "")) if value not in {"0", "1"}: return await _card_response(request, user_id, errors=["Некорректное значение подписки"], status_code=400) if not await users_admin_repo.set_subscription( user_id, value == "1", editor_id=_editor_id(request), ): raise HTTPException(status_code=404, detail="Пользователь не найден") return RedirectResponse(url=f"/users/{user_id}?saved=subscription", status_code=303) @router.post("/{user_id}/balance") async def update_balance(request: Request, user_id: int) -> Response: """Скорректировать баланс записью в журнал money (действие «Корректировка админом»).""" form = await request.form() check_csrf(request, str(form.get("csrf_token", ""))) raw_amount = str(form.get("amount", "")).strip() reason = str(form.get("reason", "")).strip() errors: list[str] = [] try: amount = int(raw_amount) except (TypeError, ValueError): amount = 0 errors.append("Сумма должна быть целым числом") if amount == 0 and "Сумма должна быть целым числом" not in errors: errors.append("Сумма не может быть нулевой") if not reason: errors.append("Укажите причину корректировки") elif len(reason) > users_admin_repo.MAX_COMMENT_LENGTH: errors.append(f"Причина длиннее {users_admin_repo.MAX_COMMENT_LENGTH} символов") if errors: return await _card_response(request, user_id, errors=errors, status_code=400) balance = await users_admin_repo.adjust_balance( user_id, amount, reason, editor_id=_editor_id(request), ) if balance is None: raise HTTPException(status_code=404, detail="Пользователь не найден") return RedirectResponse(url=f"/users/{user_id}?saved=balance", status_code=303)