""" Users and Admin Router - Consolidated Endpoints This module consolidates user management and administrative operations into a single router. It provides endpoints for: User Management: - GET /{user_id}: Retrieve user profile by ID - PATCH /{user_id}: Update user profile (self or admin) - POST /{user_id}/ban: Ban a user (admin/judge only) - POST /{user_id}/unban: Unban a user (admin/judge only) Admin Operations: - GET /users: List all users with pagination (admin/judge only) - GET /bans: List all bans, optionally active only (admin/judge only) - POST /bans: Create a new ban record (admin/judge only) - POST /bans/{ban_id}/unban: Unban via ban ID (admin/judge only) - GET /logs: List game logs with optional room filter (admin/judge only) - POST /audit: Create an audit log entry (admin/judge only) Both per-user ban/unban and global ban listing/creation are maintained. """ from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, update from typing import Optional, List from datetime import datetime from app.core.database import get_db from app.core.security import get_current_user, hash_password from app.models.models import User, Ban, GameLog, AuditLog from app.schemas.schemas import UserUpdate, UserResponse, BanCreate, BanResponse router = APIRouter() # ============================================================================= # User Management Endpoints # ============================================================================= @router.get("/{user_id}", response_model=UserResponse) async def get_user( user_id: int, db: AsyncSession = Depends(get_db), current_user: dict = Depends(get_current_user), ) -> UserResponse: """ Get user by ID. Args: user_id: The ID of the user to retrieve db: Database session current_user: The currently authenticated user Returns: UserResponse with user profile data Raises: HTTPException 404: If user not found """ stmt = select(User).where(User.id == user_id) result = await db.execute(stmt) user = result.scalar_one_or_none() if not user: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="User not found", ) return UserResponse.model_validate(user) @router.patch("/{user_id}", response_model=UserResponse) async def update_user( user_id: int, request: UserUpdate, db: AsyncSession = Depends(get_db), current_user: dict = Depends(get_current_user), ) -> UserResponse: """ Update user profile. Users can only update their own profile unless they have admin privileges. Args: user_id: The ID of the user to update request: UserUpdate schema with fields to update db: Database session current_user: The currently authenticated user Returns: UserResponse with updated user data Raises: HTTPException 403: If trying to update another user's profile HTTPException 404: If user not found """ # Users can only update their own profile unless admin if int(current_user["user_id"]) != user_id: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Cannot update another user's profile", ) stmt = select(User).where(User.id == user_id) result = await db.execute(stmt) user = result.scalar_one_or_none() if not user: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="User not found", ) # Update fields update_data = request.model_dump(exclude_unset=True) # Hash new password if provided if "new_password" in update_data: update_data["password_hash"] = hash_password(update_data.pop("new_password")) # Update user stmt = ( update(User) .where(User.id == user_id) .values(**update_data) ) await db.execute(stmt) await db.flush() # Fetch updated user stmt = select(User).where(User.id == user_id) result = await db.execute(stmt) updated_user = result.scalar_one_or_none() return UserResponse.model_validate(updated_user) @router.post("/{user_id}/ban") async def ban_user( user_id: int, reason: str, expiration_time: Optional[str] = None, db: AsyncSession = Depends(get_db), current_user: dict = Depends(get_current_user), ) -> dict: """ Ban a user (admin/judge only). Args: user_id: The ID of the user to ban reason: Reason for the ban expiration_time: Optional ISO format datetime for ban expiration db: Database session current_user: The currently authenticated user Returns: Success message Raises: HTTPException 403: If user is not admin/judge HTTPException 404: If user not found """ # Check if current user is admin if current_user.get("privlevel") not in ["Admin", "Judge"]: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Admin privileges required", ) stmt = select(User).where(User.id == user_id) result = await db.execute(stmt) user = result.scalar_one_or_none() if not user: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="User not found", ) # Update user ban status ban_ends = None if expiration_time: ban_ends = datetime.fromisoformat(expiration_time) stmt = ( update(User) .where(User.id == user_id) .values( is_banned=True, ban_reason=reason, ban_ends=ban_ends, ) ) await db.execute(stmt) await db.flush() return {"message": f"User {user_id} has been banned"} @router.post("/{user_id}/unban") async def unban_user( user_id: int, db: AsyncSession = Depends(get_db), current_user: dict = Depends(get_current_user), ) -> dict: """ Unban a user (admin/judge only). Args: user_id: The ID of the user to unban db: Database session current_user: The currently authenticated user Returns: Success message Raises: HTTPException 403: If user is not admin/judge HTTPException 404: If user not found """ # Check if current user is admin if current_user.get("privlevel") not in ["Admin", "Judge"]: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Admin privileges required", ) stmt = select(User).where(User.id == user_id) result = await db.execute(stmt) user = result.scalar_one_or_none() if not user: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="User not found", ) # Update user ban status stmt = ( update(User) .where(User.id == user_id) .values( is_banned=False, ban_reason=None, ban_ends=None, ) ) await db.execute(stmt) await db.flush() return {"message": f"User {user_id} has been unbanned"} # ============================================================================= # Admin Operations Endpoints # ============================================================================= @router.get("/users", response_model=List[dict]) async def list_users( page: int = 1, page_size: int = 50, db: AsyncSession = Depends(get_db), current_user: dict = Depends(get_current_user), ) -> List[dict]: """ List all users with pagination (admin/judge only). Args: page: Page number (1-indexed) page_size: Number of users per page db: Database session current_user: The currently authenticated user Returns: List of user dictionaries Raises: HTTPException 403: If user is not admin/judge """ # Check if current user is admin if current_user.get("privlevel") not in ["Admin", "Judge"]: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Admin privileges required", ) stmt = ( select(User) .order_by(User.id) .offset((page - 1) * page_size) .limit(page_size) ) result = await db.execute(stmt) users = result.scalars().all() return [ { "id": user.id, "username": user.username, "email": user.email, "privlevel": user.privlevel, "is_active": user.is_active, "is_banned": user.is_banned, "vip_status": user.vip_status, "creation_date": user.creation_date, } for user in users ] @router.get("/bans", response_model=List[BanResponse]) async def list_bans( active_only: bool = True, db: AsyncSession = Depends(get_db), current_user: dict = Depends(get_current_user), ) -> List[BanResponse]: """ List all bans (admin/judge only). Args: active_only: If True, only return active bans db: Database session current_user: The currently authenticated user Returns: List of BanResponse objects Raises: HTTPException 403: If user is not admin/judge """ # Check if current user is admin if current_user.get("privlevel") not in ["Admin", "Judge"]: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Admin privileges required", ) stmt = select(Ban).order_by(Ban.id) if active_only: stmt = stmt.where(Ban.active == True) result = await db.execute(stmt) bans = result.scalars().all() return [BanResponse.model_validate(ban) for ban in bans] @router.post("/bans", response_model=BanResponse) async def create_ban( request: BanCreate, db: AsyncSession = Depends(get_db), current_user: dict = Depends(get_current_user), ) -> BanResponse: """ Create a new ban record (admin/judge only). Args: request: BanCreate schema with ban details db: Database session current_user: The currently authenticated user Returns: BanResponse with created ban data Raises: HTTPException 403: If user is not admin/judge """ # Check if current user is admin if current_user.get("privlevel") not in ["Admin", "Judge"]: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Admin privileges required", ) new_ban = Ban( user_id=request.user_id, reason=request.reason, expiration_time=request.expiration_time, moderators=current_user.get("username"), ip_address=None, # Would get from request ) db.add(new_ban) await db.flush() return BanResponse.model_validate(new_ban) @router.post("/bans/{ban_id}/unban") async def unban( ban_id: int, db: AsyncSession = Depends(get_db), current_user: dict = Depends(get_current_user), ) -> dict: """ Unban a user via ban ID (admin/judge only). This deactivates the ban record and also updates the user's ban status. Args: ban_id: The ID of the ban to unban db: Database session current_user: The currently authenticated user Returns: Success message Raises: HTTPException 403: If user is not admin/judge HTTPException 404: If ban not found """ # Check if current user is admin if current_user.get("privlevel") not in ["Admin", "Judge"]: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Admin privileges required", ) stmt = select(Ban).where(Ban.id == ban_id) result = await db.execute(stmt) ban = result.scalar_one_or_none() if not ban: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Ban not found", ) # Deactivate ban stmt = ( update(Ban) .where(Ban.id == ban_id) .values(active=False) ) await db.execute(stmt) # Also unban the user stmt = ( update(User) .where(User.id == ban.user_id) .values( is_banned=False, ban_reason=None, ban_ends=None, ) ) await db.execute(stmt) return {"message": "User unbanned successfully"} @router.get("/logs", response_model=List[dict]) async def list_logs( room_id: Optional[int] = None, limit: int = 100, db: AsyncSession = Depends(get_db), current_user: dict = Depends(get_current_user), ) -> List[dict]: """ List game logs with optional room filter (admin/judge only). Args: room_id: Optional room ID to filter logs limit: Maximum number of logs to return db: Database session current_user: The currently authenticated user Returns: List of log dictionaries Raises: HTTPException 403: If user is not admin/judge """ # Check if current user is admin if current_user.get("privlevel") not in ["Admin", "Judge"]: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Admin privileges required", ) stmt = select(GameLog).order_by(GameLog.id.desc()).limit(limit) if room_id: stmt = stmt.where(GameLog.room_id == room_id) result = await db.execute(stmt) logs = result.scalars().all() return [ { "id": log.id, "room_id": log.room_id, "player_id": log.player_id, "message": log.message, "timestamp": log.timestamp, } for log in logs ] @router.post("/audit", response_model=dict) async def log_audit( action_type: str, target_user_id: Optional[int] = None, details: Optional[str] = None, db: AsyncSession = Depends(get_db), current_user: dict = Depends(get_current_user), ) -> dict: """ Create an audit log entry (admin/judge only). Args: action_type: Type of administrative action target_user_id: Optional target user ID details: Optional details about the action db: Database session current_user: The currently authenticated user Returns: Dictionary with audit log data Raises: HTTPException 403: If user is not admin/judge """ # Check if current user is admin if current_user.get("privlevel") not in ["Admin", "Judge"]: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Admin privileges required", ) new_audit = AuditLog( admin_id=int(current_user["user_id"]), action_type=action_type, target_user_id=target_user_id, details=details, ip_address=None, # Would get from request ) db.add(new_audit) await db.flush() return { "id": new_audit.id, "admin_id": new_audit.admin_id, "action_type": new_audit.action_type, "target_user_id": new_audit.target_user_id, "details": new_audit.details, "timestamp": new_audit.timestamp, }