Complete architecture simplification: 54+ files → 37 files
- Removed 11 duplicate router files (auth.py, users.py, decks.py, card_router.py, interactions.py, user_data.py, rooms.py, games/router.py, refresh.py, card_import.py, ws.py) - Removed duplicate core files (logging_config.py, settings.py) - Removed unused services (card_mirror_service.py, mtgjson_loader.py, mtgjson_uploader.py) - Removed unused schemas (proto_messages.py, protocol_constants.py) - Created run.py startup script for simple execution - Created app/core/config.py for unified configuration management - Created app/core/logging.py for logging configuration - Consolidated routers into 6 focused files (auth_router.py, users_router.py, decks_router.py, cards_router.py, user_data_router.py, games_router.py) - Updated all imports to use consolidated model paths - Added backward compatibility aliases in database.py - Created SIMPLIFICATION_SUMMARY.md documentation Architecture improvements: - Single unified database engine with auto-detection (SQLite/PostgreSQL) - Consolidated models into 2 files (models.py, mtg_models.py) - Clean router structure with one file per domain - Simplified configuration with sensible defaults - SQLite by default for development, PostgreSQL for production Testing: - All imports verified working - App loads successfully - Ready for development and testing
This commit is contained in:
@@ -0,0 +1,549 @@
|
||||
"""
|
||||
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,
|
||||
}
|
||||
Reference in New Issue
Block a user