"""Worlds router - CRUD for game worlds.""" from typing import Annotated from fastapi import APIRouter, Depends, HTTPException, Query from pydantic import BaseModel, Field from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import selectinload from app.database import get_db from app.dependencies import CurrentUser from app.models.world import World, WorldVisibility router = APIRouter() DbDep = Annotated[AsyncSession, Depends(get_db)] # --- Pydantic schemas --- class WorldCreate(BaseModel): """Schema for creating a world.""" name: str = Field(..., min_length=1, max_length=255) description: str | None = None visibility: WorldVisibility = WorldVisibility.PUBLIC class WorldUpdate(BaseModel): """Schema for updating a world.""" name: str | None = Field(None, min_length=1, max_length=255) description: str | None = None visibility: WorldVisibility | None = None class WorldResponse(BaseModel): """Schema for world responses.""" id: int name: str description: str | None visibility: WorldVisibility owner_id: int created_at: str | None = None model_config = {"from_attributes": True} # --- Endpoints --- @router.get("/", response_model=list[WorldResponse]) async def list_worlds( current_user: CurrentUser, db: DbDep, skip: int = Query(0, ge=0), limit: int = Query(50, ge=1, le=100), visibility: WorldVisibility | None = None, ): """List worlds visible to the current user. PUBLIC worlds are visible to everyone. PRIVATE/FRIENDS_ONLY worlds are visible to their owner. """ query = select(World).options(selectinload(World.owner)) # Filter by visibility if specified if visibility: query = query.where(World.visibility == visibility) if visibility != WorldVisibility.PUBLIC: # Requesting a non-public visibility explicitly must not bypass # ownership — otherwise any authenticated user could list every # PRIVATE/FRIENDS_ONLY world in the system just by passing the # filter, defeating the point of the check below. query = query.where(World.owner_id == current_user.id) else: # Show PUBLIC worlds + owner's own worlds query = query.where( (World.visibility == WorldVisibility.PUBLIC) | (World.owner_id == current_user.id) ) query = query.offset(skip).limit(limit).order_by(World.created_at.desc()) result = await db.execute(query) worlds = result.scalars().all() return worlds @router.get("/{world_id}", response_model=WorldResponse) async def get_world( world_id: int, current_user: CurrentUser, db: DbDep, ): """Get a single world by ID, respecting visibility.""" result = await db.execute( select(World).where(World.id == world_id).options(selectinload(World.owner)) ) world = result.scalar_one_or_none() if not world: raise HTTPException(status_code=404, detail="World not found") # Allow access if world is PUBLIC or owned by current user if world.visibility != WorldVisibility.PUBLIC and world.owner_id != current_user.id: raise HTTPException(status_code=404, detail="World not found") return world @router.post("/", response_model=WorldResponse, status_code=201) async def create_world( world_data: WorldCreate, current_user: CurrentUser, db: DbDep, ): """Create a new world.""" world = World( name=world_data.name, description=world_data.description, owner_id=current_user.id, visibility=world_data.visibility, ) db.add(world) await db.commit() await db.refresh(world) return world @router.put("/{world_id}", response_model=WorldResponse) async def update_world( world_id: int, world_data: WorldUpdate, current_user: CurrentUser, db: DbDep, ): """Update an existing world.""" result = await db.execute(select(World).where(World.id == world_id)) world = result.scalar_one_or_none() if not world: raise HTTPException(status_code=404, detail="World not found") if world.owner_id != current_user.id: raise HTTPException(status_code=403, detail="Not owner of this world") update_data = world_data.model_dump(exclude_unset=True) for field, value in update_data.items(): setattr(world, field, value) await db.commit() await db.refresh(world) return world @router.delete("/{world_id}", status_code=204) async def delete_world( world_id: int, current_user: CurrentUser, db: DbDep, ): """Delete a world.""" result = await db.execute(select(World).where(World.id == world_id)) world = result.scalar_one_or_none() if not world: raise HTTPException(status_code=404, detail="World not found") if world.owner_id != current_user.id: raise HTTPException(status_code=403, detail="Not owner of this world") await db.delete(world) await db.commit()