"""Avatar router - the current user's persisted avatar loadout. The loadout is a single JSON document matching the client's ``AvatarLoadout.to_json()`` shape. The server has no copy of the asset catalog, so it only validates the envelope (JSON object, non-empty ``base_body``); which assets are valid for which slot is the client's job. """ from typing import Annotated, Any from fastapi import APIRouter, Depends, HTTPException from pydantic import BaseModel, Field from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.database import get_db from app.dependencies import CurrentUser from app.models.avatar_loadout import AvatarLoadout, DEFAULT_LOADOUT router = APIRouter() DbDep = Annotated[AsyncSession, Depends(get_db)] # A loadout document is small; reject anything absurd outright. MAX_LOADOUT_BYTES = 16 * 1024 class LoadoutResponse(BaseModel): data: dict[str, Any] is_default: bool = Field( description="True when the user has never saved a loadout and this is " "the server default." ) class LoadoutUpdate(BaseModel): data: dict[str, Any] def _validate_envelope(data: dict[str, Any]) -> None: base_body = data.get("base_body") if not isinstance(base_body, str) or not base_body.strip(): raise HTTPException( status_code=422, detail="loadout.data must have a non-empty string 'base_body'", ) slots = data.get("slots", {}) if not isinstance(slots, dict): raise HTTPException(status_code=422, detail="loadout.data.slots must be an object") if len(str(data)) > MAX_LOADOUT_BYTES: raise HTTPException(status_code=413, detail="loadout document too large") @router.get("/loadout", response_model=LoadoutResponse) async def get_loadout(current_user: CurrentUser, db: DbDep): """Return the current user's saved loadout, or the server default if they have never saved one.""" row = await db.get(AvatarLoadout, current_user.id) if row is None: return LoadoutResponse(data=dict(DEFAULT_LOADOUT), is_default=True) return LoadoutResponse(data=row.data, is_default=False) @router.put("/loadout", response_model=LoadoutResponse) async def put_loadout(payload: LoadoutUpdate, current_user: CurrentUser, db: DbDep): """Replace the current user's loadout.""" _validate_envelope(payload.data) row = await db.get(AvatarLoadout, current_user.id) if row is None: row = AvatarLoadout(user_id=current_user.id, data=payload.data) db.add(row) else: row.data = payload.data await db.commit() await db.refresh(row) return LoadoutResponse(data=row.data, is_default=False)