Initial commit of mtgonline project
This commit is contained in:
@@ -0,0 +1,213 @@
|
||||
"""Admin router endpoints."""
|
||||
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
|
||||
from app.models.models import User, Ban, GameLog, AuditLog
|
||||
from app.schemas.schemas import BanCreate, BanResponse
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@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 all users (admin only)."""
|
||||
# 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 all bans (admin only)."""
|
||||
# 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),
|
||||
):
|
||||
"""Create a ban (admin only)."""
|
||||
# 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),
|
||||
):
|
||||
"""Unban a user (admin only)."""
|
||||
# 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 game logs (admin only)."""
|
||||
# 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=AuditLog)
|
||||
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),
|
||||
):
|
||||
"""Create an audit log entry (admin only)."""
|
||||
# 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 AuditLog.model_validate(new_audit)
|
||||
@@ -0,0 +1,160 @@
|
||||
"""Authentication router endpoints."""
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from typing import Optional
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.core.security import (
|
||||
verify_password,
|
||||
hash_password,
|
||||
create_access_token,
|
||||
create_refresh_token,
|
||||
decode_token,
|
||||
)
|
||||
from app.models.models import User
|
||||
from app.schemas.schemas import (
|
||||
LoginRequest,
|
||||
LoginResponse,
|
||||
RefreshTokenRequest,
|
||||
TokenResponse,
|
||||
UserCreate,
|
||||
UserResponse,
|
||||
)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/login", response_model=LoginResponse)
|
||||
async def login(request: LoginRequest, db: AsyncSession = Depends(get_db)):
|
||||
"""Authenticate user and return JWT tokens."""
|
||||
# Find user by username
|
||||
stmt = select(User).where(User.username == request.username)
|
||||
result = await db.execute(stmt)
|
||||
user = result.scalar_one_or_none()
|
||||
|
||||
if not user or not verify_password(request.password, user.password_sha512):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid username or password",
|
||||
)
|
||||
|
||||
if not user.is_active:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Account is disabled",
|
||||
)
|
||||
|
||||
if user.is_banned and user.ban_ends and user.ban_ends > __import__("datetime").datetime.now():
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Account is banned",
|
||||
)
|
||||
|
||||
# Update last login
|
||||
user.last_login = __import__("datetime").datetime.now()
|
||||
await db.flush()
|
||||
|
||||
# Generate tokens
|
||||
access_token = create_access_token(str(user.id))
|
||||
refresh_token = create_refresh_token(str(user.id))
|
||||
|
||||
return LoginResponse(
|
||||
access_token=access_token,
|
||||
refresh_token=refresh_token,
|
||||
user=UserResponse.model_validate(user).model_dump(),
|
||||
)
|
||||
|
||||
|
||||
@router.post("/refresh", response_model=TokenResponse)
|
||||
async def refresh_token(request: RefreshTokenRequest, db: AsyncSession = Depends(get_db)):
|
||||
"""Refresh access token using refresh token."""
|
||||
payload = decode_token(request.refresh_token)
|
||||
|
||||
if not payload or payload.get("type") != "refresh":
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid refresh token",
|
||||
)
|
||||
|
||||
# Verify user still exists and is active
|
||||
user_id = payload["sub"]
|
||||
stmt = select(User).where(User.id == int(user_id))
|
||||
result = await db.execute(stmt)
|
||||
user = result.scalar_one_or_none()
|
||||
|
||||
if not user or not user.is_active:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="User account is invalid",
|
||||
)
|
||||
|
||||
# Generate new access token
|
||||
access_token = create_access_token(str(user.id))
|
||||
|
||||
return TokenResponse(access_token=access_token)
|
||||
|
||||
|
||||
@router.post("/register", response_model=UserResponse)
|
||||
async def register(request: UserCreate, db: AsyncSession = Depends(get_db)):
|
||||
"""Register a new user account."""
|
||||
# Check if username exists
|
||||
stmt = select(User).where(User.username == request.username)
|
||||
result = await db.execute(stmt)
|
||||
if result.scalar_one_or_none():
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail="Username already exists",
|
||||
)
|
||||
|
||||
# Check if email exists (if provided)
|
||||
if request.email:
|
||||
stmt = select(User).where(User.email == request.email)
|
||||
result = await db.execute(stmt)
|
||||
if result.scalar_one_or_none():
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail="Email already registered",
|
||||
)
|
||||
|
||||
# Create new user
|
||||
new_user = User(
|
||||
username=request.username,
|
||||
password_sha512=hash_password(request.password),
|
||||
salt="random_salt", # In production, generate random salt
|
||||
email=request.email,
|
||||
country=request.country,
|
||||
real_name=request.real_name,
|
||||
)
|
||||
db.add(new_user)
|
||||
await db.flush()
|
||||
|
||||
return UserResponse.model_validate(new_user)
|
||||
|
||||
|
||||
@router.get("/me", response_model=UserResponse)
|
||||
async def get_current_user(
|
||||
token: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""Get current authenticated user."""
|
||||
payload = decode_token(token)
|
||||
|
||||
if not payload or payload.get("type") != "access":
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid token",
|
||||
)
|
||||
|
||||
user_id = payload["sub"]
|
||||
stmt = select(User).where(User.id == int(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)
|
||||
@@ -0,0 +1,265 @@
|
||||
"""Deck management router endpoints."""
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, delete
|
||||
from typing import Optional, List
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.core.security import get_current_user
|
||||
from app.models.models import DecklistFile, DecklistFolder
|
||||
from app.schemas.schemas import DeckCreate, DeckUpdate, DeckResponse, FolderCreate, FolderResponse
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/", response_model=List[DeckResponse])
|
||||
async def list_decks(
|
||||
folder_id: Optional[int] = None,
|
||||
page: int = 1,
|
||||
page_size: int = 50,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
):
|
||||
"""List decks for current user."""
|
||||
user_id = int(current_user["user_id"])
|
||||
|
||||
if folder_id:
|
||||
stmt = (
|
||||
select(DecklistFile)
|
||||
.where(
|
||||
DecklistFile.owner_id == user_id,
|
||||
DecklistFile.folder_id == folder_id,
|
||||
)
|
||||
.offset((page - 1) * page_size)
|
||||
.limit(page_size)
|
||||
)
|
||||
else:
|
||||
stmt = (
|
||||
select(DecklistFile)
|
||||
.where(DecklistFile.owner_id == user_id)
|
||||
.offset((page - 1) * page_size)
|
||||
.limit(page_size)
|
||||
)
|
||||
|
||||
result = await db.execute(stmt)
|
||||
decks = result.scalars().all()
|
||||
|
||||
return [DeckResponse.model_validate(deck) for deck in decks]
|
||||
|
||||
|
||||
@router.post("/", response_model=DeckResponse)
|
||||
async def create_deck(
|
||||
request: DeckCreate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
):
|
||||
"""Create a new deck."""
|
||||
user_id = int(current_user["user_id"])
|
||||
|
||||
# Verify folder exists if specified
|
||||
if request.folder_id:
|
||||
stmt = select(DecklistFolder).where(
|
||||
DecklistFolder.id == request.folder_id,
|
||||
DecklistFolder.owner_id == user_id,
|
||||
)
|
||||
result = await db.execute(stmt)
|
||||
folder = result.scalar_one_or_none()
|
||||
if not folder:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Folder not found",
|
||||
)
|
||||
|
||||
new_deck = DecklistFile(
|
||||
owner_id=user_id,
|
||||
folder_id=request.folder_id,
|
||||
name=request.name,
|
||||
content=request.content,
|
||||
format=request.format,
|
||||
)
|
||||
db.add(new_deck)
|
||||
await db.flush()
|
||||
|
||||
return DeckResponse.model_validate(new_deck)
|
||||
|
||||
|
||||
@router.get("/{deck_id}", response_model=DeckResponse)
|
||||
async def get_deck(
|
||||
deck_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
):
|
||||
"""Get deck by ID."""
|
||||
user_id = int(current_user["user_id"])
|
||||
|
||||
stmt = select(DecklistFile).where(
|
||||
DecklistFile.id == deck_id,
|
||||
DecklistFile.owner_id == user_id,
|
||||
)
|
||||
result = await db.execute(stmt)
|
||||
deck = result.scalar_one_or_none()
|
||||
|
||||
if not deck:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Deck not found",
|
||||
)
|
||||
|
||||
return DeckResponse.model_validate(deck)
|
||||
|
||||
|
||||
@router.patch("/{deck_id}", response_model=DeckResponse)
|
||||
async def update_deck(
|
||||
deck_id: int,
|
||||
request: DeckUpdate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
):
|
||||
"""Update deck."""
|
||||
user_id = int(current_user["user_id"])
|
||||
|
||||
stmt = select(DecklistFile).where(
|
||||
DecklistFile.id == deck_id,
|
||||
DecklistFile.owner_id == user_id,
|
||||
)
|
||||
result = await db.execute(stmt)
|
||||
deck = result.scalar_one_or_none()
|
||||
|
||||
if not deck:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Deck not found",
|
||||
)
|
||||
|
||||
# Update fields
|
||||
update_data = request.model_dump(exclude_unset=True)
|
||||
|
||||
stmt = (
|
||||
update(DecklistFile)
|
||||
.where(DecklistFile.id == deck_id)
|
||||
.values(**update_data)
|
||||
)
|
||||
await db.execute(stmt)
|
||||
await db.flush()
|
||||
|
||||
# Fetch updated deck
|
||||
stmt = select(DecklistFile).where(DecklistFile.id == deck_id)
|
||||
result = await db.execute(stmt)
|
||||
updated_deck = result.scalar_one_or_none()
|
||||
|
||||
return DeckResponse.model_validate(updated_deck)
|
||||
|
||||
|
||||
@router.delete("/{deck_id}")
|
||||
async def delete_deck(
|
||||
deck_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
):
|
||||
"""Delete deck."""
|
||||
user_id = int(current_user["user_id"])
|
||||
|
||||
stmt = select(DecklistFile).where(
|
||||
DecklistFile.id == deck_id,
|
||||
DecklistFile.owner_id == user_id,
|
||||
)
|
||||
result = await db.execute(stmt)
|
||||
deck = result.scalar_one_or_none()
|
||||
|
||||
if not deck:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Deck not found",
|
||||
)
|
||||
|
||||
await db.execute(delete(DecklistFile).where(DecklistFile.id == deck_id))
|
||||
|
||||
return {"message": "Deck deleted successfully"}
|
||||
|
||||
|
||||
@router.get("/folders", response_model=List[FolderResponse])
|
||||
async def list_folders(
|
||||
parent_id: Optional[int] = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
):
|
||||
"""List folders for current user."""
|
||||
user_id = int(current_user["user_id"])
|
||||
|
||||
if parent_id:
|
||||
stmt = select(DecklistFolder).where(
|
||||
DecklistFolder.parent_id == parent_id,
|
||||
DecklistFolder.owner_id == user_id,
|
||||
)
|
||||
else:
|
||||
stmt = select(DecklistFolder).where(
|
||||
DecklistFolder.parent_id == None, # Top-level folders
|
||||
DecklistFolder.owner_id == user_id,
|
||||
)
|
||||
|
||||
result = await db.execute(stmt)
|
||||
folders = result.scalars().all()
|
||||
|
||||
return [FolderResponse.model_validate(folder) for folder in folders]
|
||||
|
||||
|
||||
@router.post("/folders", response_model=FolderResponse)
|
||||
async def create_folder(
|
||||
request: FolderCreate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
):
|
||||
"""Create a new folder."""
|
||||
user_id = int(current_user["user_id"])
|
||||
|
||||
# Verify parent folder exists if specified
|
||||
if request.parent_id:
|
||||
stmt = select(DecklistFolder).where(
|
||||
DecklistFolder.id == request.parent_id,
|
||||
DecklistFolder.owner_id == user_id,
|
||||
)
|
||||
result = await db.execute(stmt)
|
||||
parent_folder = result.scalar_one_or_none()
|
||||
if not parent_folder:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Parent folder not found",
|
||||
)
|
||||
|
||||
new_folder = DecklistFolder(
|
||||
owner_id=user_id,
|
||||
name=request.name,
|
||||
parent_id=request.parent_id,
|
||||
)
|
||||
db.add(new_folder)
|
||||
await db.flush()
|
||||
|
||||
return FolderResponse.model_validate(new_folder)
|
||||
|
||||
|
||||
@router.delete("/folders/{folder_id}")
|
||||
async def delete_folder(
|
||||
folder_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
):
|
||||
"""Delete folder and all its contents."""
|
||||
user_id = int(current_user["user_id"])
|
||||
|
||||
stmt = select(DecklistFolder).where(
|
||||
DecklistFolder.id == folder_id,
|
||||
DecklistFolder.owner_id == user_id,
|
||||
)
|
||||
result = await db.execute(stmt)
|
||||
folder = result.scalar_one_or_none()
|
||||
|
||||
if not folder:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Folder not found",
|
||||
)
|
||||
|
||||
# Delete folder and all decks (cascading delete)
|
||||
await db.execute(delete(DecklistFolder).where(DecklistFolder.id == folder_id))
|
||||
|
||||
return {"message": "Folder deleted successfully"}
|
||||
@@ -0,0 +1,135 @@
|
||||
"""Game management router endpoints."""
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, insert, update, delete
|
||||
from typing import Optional, List
|
||||
from datetime import datetime
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.core.security import get_current_user
|
||||
from app.models.models import User
|
||||
from app.schemas.schemas import GameCreate, GameResponse
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/", response_model=List[GameResponse])
|
||||
async def list_games(
|
||||
room_id: Optional[int] = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
):
|
||||
"""List active games."""
|
||||
# This would query a games table - simplified for now
|
||||
# In production, you'd have a CockatriceGames model
|
||||
return []
|
||||
|
||||
|
||||
@router.post("/", response_model=GameResponse)
|
||||
async def create_game(
|
||||
request: GameCreate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
):
|
||||
"""Create a new game."""
|
||||
user_id = int(current_user["user_id"])
|
||||
|
||||
# Verify user exists
|
||||
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",
|
||||
)
|
||||
|
||||
# In production, you'd create a game in the game database
|
||||
# For now, return a mock response
|
||||
game_data = {
|
||||
"id": 1, # Mock ID
|
||||
"room_id": request.room_id,
|
||||
"game_type": request.game_type,
|
||||
"description": request.description,
|
||||
"with_password": bool(request.password),
|
||||
"max_players": 4,
|
||||
"player_count": 1,
|
||||
"started": False,
|
||||
"creation_date": datetime.now(),
|
||||
}
|
||||
|
||||
return GameResponse(**game_data)
|
||||
|
||||
|
||||
@router.get("/{game_id}", response_model=GameResponse)
|
||||
async def get_game(
|
||||
game_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
):
|
||||
"""Get game by ID."""
|
||||
# In production, query the games table
|
||||
return GameResponse(
|
||||
id=game_id,
|
||||
room_id=1,
|
||||
game_type="Casual",
|
||||
description="Test game",
|
||||
with_password=False,
|
||||
max_players=4,
|
||||
player_count=0,
|
||||
started=False,
|
||||
creation_date=datetime.now(),
|
||||
)
|
||||
|
||||
|
||||
@router.post("/{game_id}/join")
|
||||
async def join_game(
|
||||
game_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
):
|
||||
"""Join a game."""
|
||||
user_id = int(current_user["user_id"])
|
||||
|
||||
# In production, add user to game players table
|
||||
return {"message": f"User {user_id} joined game {game_id}"}
|
||||
|
||||
|
||||
@router.post("/{game_id}/leave")
|
||||
async def leave_game(
|
||||
game_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
):
|
||||
"""Leave a game."""
|
||||
user_id = int(current_user["user_id"])
|
||||
|
||||
# In production, remove user from game players table
|
||||
return {"message": f"User {user_id} left game {game_id}"}
|
||||
|
||||
|
||||
@router.post("/{game_id}/start")
|
||||
async def start_game(
|
||||
game_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
):
|
||||
"""Start a game (host only)."""
|
||||
user_id = int(current_user["user_id"])
|
||||
|
||||
# In production, update game started status
|
||||
return {"message": f"Game {game_id} started"}
|
||||
|
||||
|
||||
@router.post("/{game_id}/end")
|
||||
async def end_game(
|
||||
game_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
):
|
||||
"""End a game."""
|
||||
user_id = int(current_user["user_id"])
|
||||
|
||||
# In production, update game closed status
|
||||
return {"message": f"Game {game_id} ended"}
|
||||
@@ -0,0 +1,158 @@
|
||||
"""Room management router endpoints."""
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, insert, update, delete
|
||||
from typing import Optional, List
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.core.security import get_current_user
|
||||
from app.models.models import Room
|
||||
from app.schemas.schemas import RoomResponse
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/", response_model=List[RoomResponse])
|
||||
async def list_rooms(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
):
|
||||
"""List all available rooms."""
|
||||
stmt = select(Room).order_by(Room.id)
|
||||
result = await db.execute(stmt)
|
||||
rooms = result.scalars().all()
|
||||
|
||||
return [RoomResponse.model_validate(room) for room in rooms]
|
||||
|
||||
|
||||
@router.get("/{room_id}", response_model=RoomResponse)
|
||||
async def get_room(
|
||||
room_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
):
|
||||
"""Get room by ID."""
|
||||
stmt = select(Room).where(Room.id == room_id)
|
||||
result = await db.execute(stmt)
|
||||
room = result.scalar_one_or_none()
|
||||
|
||||
if not room:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Room not found",
|
||||
)
|
||||
|
||||
return RoomResponse.model_validate(room)
|
||||
|
||||
|
||||
@router.post("/")
|
||||
async def create_room(
|
||||
name: str,
|
||||
description: Optional[str] = None,
|
||||
is_password_protected: bool = False,
|
||||
password_hash: Optional[str] = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
):
|
||||
"""Create a new room (admin only)."""
|
||||
# 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",
|
||||
)
|
||||
|
||||
# Check if room name already exists
|
||||
stmt = select(Room).where(Room.name == name)
|
||||
result = await db.execute(stmt)
|
||||
if result.scalar_one_or_none():
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail="Room name already exists",
|
||||
)
|
||||
|
||||
new_room = Room(
|
||||
name=name,
|
||||
description=description,
|
||||
is_password_protected=is_password_protected,
|
||||
password_hash=password_hash,
|
||||
)
|
||||
db.add(new_room)
|
||||
await db.flush()
|
||||
|
||||
return {"message": f"Room '{name}' created successfully", "room_id": new_room.id}
|
||||
|
||||
|
||||
@router.patch("/{room_id}")
|
||||
async def update_room(
|
||||
room_id: int,
|
||||
name: Optional[str] = None,
|
||||
description: Optional[str] = None,
|
||||
is_password_protected: Optional[bool] = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
):
|
||||
"""Update room (admin only)."""
|
||||
# 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(Room).where(Room.id == room_id)
|
||||
result = await db.execute(stmt)
|
||||
room = result.scalar_one_or_none()
|
||||
|
||||
if not room:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Room not found",
|
||||
)
|
||||
|
||||
# Update fields
|
||||
update_data = {}
|
||||
if name:
|
||||
update_data["name"] = name
|
||||
if description is not None:
|
||||
update_data["description"] = description
|
||||
if is_password_protected is not None:
|
||||
update_data["is_password_protected"] = is_password_protected
|
||||
|
||||
stmt = (
|
||||
update(Room)
|
||||
.where(Room.id == room_id)
|
||||
.values(**update_data)
|
||||
)
|
||||
await db.execute(stmt)
|
||||
|
||||
return {"message": f"Room '{name or room.name}' updated successfully"}
|
||||
|
||||
|
||||
@router.delete("/{room_id}")
|
||||
async def delete_room(
|
||||
room_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
):
|
||||
"""Delete room (admin only)."""
|
||||
# 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(Room).where(Room.id == room_id)
|
||||
result = await db.execute(stmt)
|
||||
room = result.scalar_one_or_none()
|
||||
|
||||
if not room:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Room not found",
|
||||
)
|
||||
|
||||
await db.execute(delete(Room).where(Room.id == room_id))
|
||||
|
||||
return {"message": f"Room '{room.name}' deleted successfully"}
|
||||
@@ -0,0 +1,168 @@
|
||||
"""User management router endpoints."""
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, update
|
||||
from typing import Optional
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.core.security import get_current_user, hash_password
|
||||
from app.models.models import User
|
||||
from app.schemas.schemas import UserUpdate, UserResponse
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@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),
|
||||
):
|
||||
"""Get user by ID."""
|
||||
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),
|
||||
):
|
||||
"""Update user profile."""
|
||||
# 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_sha512"] = 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),
|
||||
):
|
||||
"""Ban a user (admin only)."""
|
||||
# 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
|
||||
from datetime import datetime
|
||||
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),
|
||||
):
|
||||
"""Unban a user (admin only)."""
|
||||
# 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"}
|
||||
@@ -0,0 +1,18 @@
|
||||
"""WebSocket router for game connections."""
|
||||
from fastapi import APIRouter, WebSocket, WebSocketDisconnect, Query
|
||||
from typing import Optional
|
||||
|
||||
from app.services.game_server import game_websocket_endpoint
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.websocket("/ws/game/{game_id}")
|
||||
async def game_websocket(
|
||||
websocket: WebSocket,
|
||||
game_id: int,
|
||||
player_id: Optional[int] = None,
|
||||
):
|
||||
"""WebSocket endpoint for game connections."""
|
||||
await websocket.accept()
|
||||
await game_websocket_endpoint(websocket, game_id)
|
||||
Reference in New Issue
Block a user