Add 3 roles, settings for marathons

This commit is contained in:
2025-12-14 20:21:56 +07:00
parent bb9e9a6e1d
commit d0b8eca600
28 changed files with 1679 additions and 290 deletions

260
backend/app/api/v1/admin.py Normal file
View File

@@ -0,0 +1,260 @@
from fastapi import APIRouter, HTTPException, Query
from sqlalchemy import select, func
from sqlalchemy.orm import selectinload
from pydantic import BaseModel, Field
from app.api.deps import DbSession, CurrentUser, require_admin
from app.models import User, UserRole, Marathon, Participant, Game
from app.schemas import UserPublic, MarathonListItem, MessageResponse
router = APIRouter(prefix="/admin", tags=["admin"])
class SetUserRole(BaseModel):
role: str = Field(..., pattern="^(user|admin)$")
class AdminUserResponse(BaseModel):
id: int
login: str
nickname: str
role: str
avatar_url: str | None = None
telegram_id: int | None = None
telegram_username: str | None = None
marathons_count: int = 0
created_at: str
class Config:
from_attributes = True
class AdminMarathonResponse(BaseModel):
id: int
title: str
status: str
creator: UserPublic
participants_count: int
games_count: int
start_date: str | None
end_date: str | None
created_at: str
class Config:
from_attributes = True
@router.get("/users", response_model=list[AdminUserResponse])
async def list_users(
current_user: CurrentUser,
db: DbSession,
skip: int = Query(0, ge=0),
limit: int = Query(50, ge=1, le=100),
search: str | None = None,
):
"""List all users. Admin only."""
require_admin(current_user)
query = select(User).order_by(User.created_at.desc())
if search:
query = query.where(
(User.login.ilike(f"%{search}%")) |
(User.nickname.ilike(f"%{search}%"))
)
query = query.offset(skip).limit(limit)
result = await db.execute(query)
users = result.scalars().all()
response = []
for user in users:
# Count marathons user participates in
marathons_count = await db.scalar(
select(func.count()).select_from(Participant).where(Participant.user_id == user.id)
)
response.append(AdminUserResponse(
id=user.id,
login=user.login,
nickname=user.nickname,
role=user.role,
avatar_url=user.avatar_url,
telegram_id=user.telegram_id,
telegram_username=user.telegram_username,
marathons_count=marathons_count,
created_at=user.created_at.isoformat(),
))
return response
@router.get("/users/{user_id}", response_model=AdminUserResponse)
async def get_user(user_id: int, current_user: CurrentUser, db: DbSession):
"""Get user details. Admin only."""
require_admin(current_user)
result = await db.execute(select(User).where(User.id == user_id))
user = result.scalar_one_or_none()
if not user:
raise HTTPException(status_code=404, detail="User not found")
marathons_count = await db.scalar(
select(func.count()).select_from(Participant).where(Participant.user_id == user.id)
)
return AdminUserResponse(
id=user.id,
login=user.login,
nickname=user.nickname,
role=user.role,
avatar_url=user.avatar_url,
telegram_id=user.telegram_id,
telegram_username=user.telegram_username,
marathons_count=marathons_count,
created_at=user.created_at.isoformat(),
)
@router.patch("/users/{user_id}/role", response_model=AdminUserResponse)
async def set_user_role(
user_id: int,
data: SetUserRole,
current_user: CurrentUser,
db: DbSession,
):
"""Set user's global role. Admin only."""
require_admin(current_user)
# Cannot change own role
if user_id == current_user.id:
raise HTTPException(status_code=400, detail="Cannot change your own role")
result = await db.execute(select(User).where(User.id == user_id))
user = result.scalar_one_or_none()
if not user:
raise HTTPException(status_code=404, detail="User not found")
user.role = data.role
await db.commit()
await db.refresh(user)
marathons_count = await db.scalar(
select(func.count()).select_from(Participant).where(Participant.user_id == user.id)
)
return AdminUserResponse(
id=user.id,
login=user.login,
nickname=user.nickname,
role=user.role,
avatar_url=user.avatar_url,
telegram_id=user.telegram_id,
telegram_username=user.telegram_username,
marathons_count=marathons_count,
created_at=user.created_at.isoformat(),
)
@router.delete("/users/{user_id}", response_model=MessageResponse)
async def delete_user(user_id: int, current_user: CurrentUser, db: DbSession):
"""Delete a user. Admin only."""
require_admin(current_user)
# Cannot delete yourself
if user_id == current_user.id:
raise HTTPException(status_code=400, detail="Cannot delete yourself")
result = await db.execute(select(User).where(User.id == user_id))
user = result.scalar_one_or_none()
if not user:
raise HTTPException(status_code=404, detail="User not found")
# Cannot delete another admin
if user.role == UserRole.ADMIN.value:
raise HTTPException(status_code=400, detail="Cannot delete another admin")
await db.delete(user)
await db.commit()
return MessageResponse(message="User deleted")
@router.get("/marathons", response_model=list[AdminMarathonResponse])
async def list_marathons(
current_user: CurrentUser,
db: DbSession,
skip: int = Query(0, ge=0),
limit: int = Query(50, ge=1, le=100),
search: str | None = None,
):
"""List all marathons. Admin only."""
require_admin(current_user)
query = (
select(Marathon)
.options(selectinload(Marathon.creator))
.order_by(Marathon.created_at.desc())
)
if search:
query = query.where(Marathon.title.ilike(f"%{search}%"))
query = query.offset(skip).limit(limit)
result = await db.execute(query)
marathons = result.scalars().all()
response = []
for marathon in marathons:
participants_count = await db.scalar(
select(func.count()).select_from(Participant).where(Participant.marathon_id == marathon.id)
)
games_count = await db.scalar(
select(func.count()).select_from(Game).where(Game.marathon_id == marathon.id)
)
response.append(AdminMarathonResponse(
id=marathon.id,
title=marathon.title,
status=marathon.status,
creator=UserPublic.model_validate(marathon.creator),
participants_count=participants_count,
games_count=games_count,
start_date=marathon.start_date.isoformat() if marathon.start_date else None,
end_date=marathon.end_date.isoformat() if marathon.end_date else None,
created_at=marathon.created_at.isoformat(),
))
return response
@router.delete("/marathons/{marathon_id}", response_model=MessageResponse)
async def delete_marathon(marathon_id: int, current_user: CurrentUser, db: DbSession):
"""Delete a marathon. Admin only."""
require_admin(current_user)
result = await db.execute(select(Marathon).where(Marathon.id == marathon_id))
marathon = result.scalar_one_or_none()
if not marathon:
raise HTTPException(status_code=404, detail="Marathon not found")
await db.delete(marathon)
await db.commit()
return MessageResponse(message="Marathon deleted")
@router.get("/stats")
async def get_stats(current_user: CurrentUser, db: DbSession):
"""Get platform statistics. Admin only."""
require_admin(current_user)
users_count = await db.scalar(select(func.count()).select_from(User))
marathons_count = await db.scalar(select(func.count()).select_from(Marathon))
games_count = await db.scalar(select(func.count()).select_from(Game))
participants_count = await db.scalar(select(func.count()).select_from(Participant))
return {
"users_count": users_count,
"marathons_count": marathons_count,
"games_count": games_count,
"total_participations": participants_count,
}