Add 3 roles, settings for marathons
This commit is contained in:
260
backend/app/api/v1/admin.py
Normal file
260
backend/app/api/v1/admin.py
Normal 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,
|
||||
}
|
||||
Reference in New Issue
Block a user