From dda4a250fa787d48b5cbc0f77a822ef6b0abe641 Mon Sep 17 00:00:00 2001 From: coder-bot Date: Mon, 17 Aug 2026 20:26:26 +0000 Subject: [PATCH] WIP checkpoint: Scaffold FastAPI backend project structure --- backend/app/routers/assets.py | 16 +++++++--------- backend/app/routers/auth.py | 34 ++++++++++++++++------------------ backend/app/routers/worlds.py | 31 +++++++++++++++++-------------- 3 files changed, 40 insertions(+), 41 deletions(-) diff --git a/backend/app/routers/assets.py b/backend/app/routers/assets.py index 985e11c..e52e751 100644 --- a/backend/app/routers/assets.py +++ b/backend/app/routers/assets.py @@ -5,13 +5,11 @@ from typing import Optional from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Query 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.asset import Asset, AssetType -from app.models.user import User from app.models.world import World -from app.routers.worlds import get_current_user from pydantic import BaseModel, Field router = APIRouter() @@ -58,8 +56,8 @@ class AssetUpdate(BaseModel): @router.post("/", response_model=AssetResponse, status_code=201) async def create_asset( payload: AssetCreate, + current_user: CurrentUser, db: AsyncSession = Depends(get_db), - current_user: User = Depends(get_current_user), ): """Create a new asset reference.""" # Verify world ownership if world_id is provided @@ -87,12 +85,12 @@ async def create_asset( @router.get("/", response_model=list[AssetResponse]) async def list_assets( + current_user: CurrentUser, world_id: Optional[int] = Query(None, description="Filter by world"), asset_type: Optional[AssetType] = Query(None), limit: int = Query(50, ge=1, le=100), offset: int = Query(0, ge=0), db: AsyncSession = Depends(get_db), - current_user: User = Depends(get_current_user), ): """List assets with optional filters.""" stmt = select(Asset).where(Asset.owner_id == current_user.id) @@ -111,8 +109,8 @@ async def list_assets( @router.get("/{asset_id}", response_model=AssetResponse) async def get_asset( asset_id: int, + current_user: CurrentUser, db: AsyncSession = Depends(get_db), - current_user: User = Depends(get_current_user), ): """Get a single asset by ID.""" asset = await db.get(Asset, asset_id) @@ -125,8 +123,8 @@ async def get_asset( async def update_asset( asset_id: int, payload: AssetUpdate, + current_user: CurrentUser, db: AsyncSession = Depends(get_db), - current_user: User = Depends(get_current_user), ): """Update an asset.""" asset = await db.get(Asset, asset_id) @@ -145,8 +143,8 @@ async def update_asset( @router.delete("/{asset_id}", status_code=204) async def delete_asset( asset_id: int, + current_user: CurrentUser, db: AsyncSession = Depends(get_db), - current_user: User = Depends(get_current_user), ): """Delete an asset.""" asset = await db.get(Asset, asset_id) @@ -161,8 +159,8 @@ async def delete_asset( async def upload_asset_file( asset_id: int, file: UploadFile = File(...), + current_user: CurrentUser = None, db: AsyncSession = Depends(get_db), - current_user: User = Depends(get_current_user), ): """Upload a file for an existing asset.""" asset = await db.get(Asset, asset_id) diff --git a/backend/app/routers/auth.py b/backend/app/routers/auth.py index 252b98d..5cf9157 100644 --- a/backend/app/routers/auth.py +++ b/backend/app/routers/auth.py @@ -5,11 +5,12 @@ from datetime import datetime, timedelta, timezone from fastapi import APIRouter, Depends, HTTPException, status from pydantic import BaseModel, EmailStr -from jose import JWTError, jwt +from jose import jwt from passlib.context import CryptContext from app.config import settings from app.database import get_db +from app.dependencies import CurrentUser from app.models.user import User from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select @@ -37,6 +38,16 @@ class LoginRequest(BaseModel): password: str +class UserResponse(BaseModel): + id: int + username: str + email: str + is_active: bool + created_at: datetime | None = None + + model_config = {"from_attributes": True} + + # --- Helpers --- def _create_access_token(data: dict) -> str: @@ -114,20 +125,7 @@ async def login(req: LoginRequest, db: AsyncSession = Depends(get_db)): return TokenResponse(access_token=access_token) -@router.get("/me") -async def get_current_user( - user_id: int, - db: AsyncSession = Depends(get_db), -): - """Get current user profile.""" - result = await db.execute(select(User).where(User.id == user_id)) - user = result.scalar_one_or_none() - if not user: - raise HTTPException(status_code=404, detail="User not found") - return { - "id": user.id, - "username": user.username, - "email": user.email, - "is_active": user.is_active, - "created_at": user.created_at, - } +@router.get("/me", response_model=UserResponse) +async def get_current_user(current_user: CurrentUser): + """Get current user profile from the JWT token.""" + return current_user diff --git a/backend/app/routers/worlds.py b/backend/app/routers/worlds.py index 43d4209..709574f 100644 --- a/backend/app/routers/worlds.py +++ b/backend/app/routers/worlds.py @@ -8,23 +8,20 @@ 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() - -async def get_current_user(): - """Placeholder dependency for current user auth.""" - # TODO: Implement JWT auth dependency - return {"id": 1, "username": "testuser", "email": "test@example.com"} +DbDep = Annotated[AsyncSession, Depends(get_db)] @router.get("/") async def list_worlds( + db: DbDep, skip: int = Query(0, ge=0), limit: int = Query(50, ge=1, le=100), visibility: WorldVisibility | None = None, - db: Annotated[AsyncSession, Depends(get_db)] = Depends(), ): """List worlds with optional visibility filter.""" query = select(World).options(selectinload(World.owner)) @@ -49,7 +46,7 @@ async def list_worlds( @router.get("/{world_id}") async def get_world( world_id: int, - db: Annotated[AsyncSession, Depends(get_db)] = Depends(), + db: DbDep, ): """Get a single world by ID.""" result = await db.execute( @@ -71,8 +68,8 @@ async def get_world( @router.post("/") async def create_world( world_data: dict, - current_user=Depends(get_current_user), - db: Annotated[AsyncSession, Depends(get_db)] = Depends(), + current_user: CurrentUser, + db: DbDep, ): """Create a new world.""" name = world_data.get("name") @@ -82,7 +79,7 @@ async def create_world( world = World( name=name, description=world_data.get("description"), - owner_id=current_user["id"], + owner_id=current_user.id, visibility=WorldVisibility( world_data.get("visibility", "public") ), @@ -105,8 +102,8 @@ async def create_world( async def update_world( world_id: int, world_data: dict, - current_user=Depends(get_current_user), - db: Annotated[AsyncSession, Depends(get_db)] = Depends(), + current_user: CurrentUser, + db: DbDep, ): """Update an existing world.""" result = await db.execute(select(World).where(World.id == world_id)) @@ -114,6 +111,9 @@ async def update_world( 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") + if world_data.get("name"): world.name = world_data["name"] if world_data.get("description") is not None: @@ -137,8 +137,8 @@ async def update_world( @router.delete("/{world_id}") async def delete_world( world_id: int, - current_user=Depends(get_current_user), - db: Annotated[AsyncSession, Depends(get_db)] = Depends(), + current_user: CurrentUser, + db: DbDep, ): """Delete a world.""" result = await db.execute(select(World).where(World.id == world_id)) @@ -146,6 +146,9 @@ async def delete_world( 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() return {"detail": "World deleted"}