WIP checkpoint: Scaffold FastAPI backend project structure

This commit is contained in:
2026-08-17 20:26:26 +00:00
parent d1b91abf05
commit dda4a250fa
3 changed files with 40 additions and 41 deletions
+7 -9
View File
@@ -5,13 +5,11 @@ from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Query from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Query
from sqlalchemy import select from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from app.database import get_db from app.database import get_db
from app.dependencies import CurrentUser
from app.models.asset import Asset, AssetType from app.models.asset import Asset, AssetType
from app.models.user import User
from app.models.world import World from app.models.world import World
from app.routers.worlds import get_current_user
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
router = APIRouter() router = APIRouter()
@@ -58,8 +56,8 @@ class AssetUpdate(BaseModel):
@router.post("/", response_model=AssetResponse, status_code=201) @router.post("/", response_model=AssetResponse, status_code=201)
async def create_asset( async def create_asset(
payload: AssetCreate, payload: AssetCreate,
current_user: CurrentUser,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
): ):
"""Create a new asset reference.""" """Create a new asset reference."""
# Verify world ownership if world_id is provided # Verify world ownership if world_id is provided
@@ -87,12 +85,12 @@ async def create_asset(
@router.get("/", response_model=list[AssetResponse]) @router.get("/", response_model=list[AssetResponse])
async def list_assets( async def list_assets(
current_user: CurrentUser,
world_id: Optional[int] = Query(None, description="Filter by world"), world_id: Optional[int] = Query(None, description="Filter by world"),
asset_type: Optional[AssetType] = Query(None), asset_type: Optional[AssetType] = Query(None),
limit: int = Query(50, ge=1, le=100), limit: int = Query(50, ge=1, le=100),
offset: int = Query(0, ge=0), offset: int = Query(0, ge=0),
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
): ):
"""List assets with optional filters.""" """List assets with optional filters."""
stmt = select(Asset).where(Asset.owner_id == current_user.id) 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) @router.get("/{asset_id}", response_model=AssetResponse)
async def get_asset( async def get_asset(
asset_id: int, asset_id: int,
current_user: CurrentUser,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
): ):
"""Get a single asset by ID.""" """Get a single asset by ID."""
asset = await db.get(Asset, asset_id) asset = await db.get(Asset, asset_id)
@@ -125,8 +123,8 @@ async def get_asset(
async def update_asset( async def update_asset(
asset_id: int, asset_id: int,
payload: AssetUpdate, payload: AssetUpdate,
current_user: CurrentUser,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
): ):
"""Update an asset.""" """Update an asset."""
asset = await db.get(Asset, asset_id) asset = await db.get(Asset, asset_id)
@@ -145,8 +143,8 @@ async def update_asset(
@router.delete("/{asset_id}", status_code=204) @router.delete("/{asset_id}", status_code=204)
async def delete_asset( async def delete_asset(
asset_id: int, asset_id: int,
current_user: CurrentUser,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
): ):
"""Delete an asset.""" """Delete an asset."""
asset = await db.get(Asset, asset_id) asset = await db.get(Asset, asset_id)
@@ -161,8 +159,8 @@ async def delete_asset(
async def upload_asset_file( async def upload_asset_file(
asset_id: int, asset_id: int,
file: UploadFile = File(...), file: UploadFile = File(...),
current_user: CurrentUser = None,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
): ):
"""Upload a file for an existing asset.""" """Upload a file for an existing asset."""
asset = await db.get(Asset, asset_id) asset = await db.get(Asset, asset_id)
+16 -18
View File
@@ -5,11 +5,12 @@ from datetime import datetime, timedelta, timezone
from fastapi import APIRouter, Depends, HTTPException, status from fastapi import APIRouter, Depends, HTTPException, status
from pydantic import BaseModel, EmailStr from pydantic import BaseModel, EmailStr
from jose import JWTError, jwt from jose import jwt
from passlib.context import CryptContext from passlib.context import CryptContext
from app.config import settings from app.config import settings
from app.database import get_db from app.database import get_db
from app.dependencies import CurrentUser
from app.models.user import User from app.models.user import User
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select from sqlalchemy import select
@@ -37,6 +38,16 @@ class LoginRequest(BaseModel):
password: str 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 --- # --- Helpers ---
def _create_access_token(data: dict) -> str: 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) return TokenResponse(access_token=access_token)
@router.get("/me") @router.get("/me", response_model=UserResponse)
async def get_current_user( async def get_current_user(current_user: CurrentUser):
user_id: int, """Get current user profile from the JWT token."""
db: AsyncSession = Depends(get_db), return current_user
):
"""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,
}
+17 -14
View File
@@ -8,23 +8,20 @@ from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload from sqlalchemy.orm import selectinload
from app.database import get_db from app.database import get_db
from app.dependencies import CurrentUser
from app.models.world import World, WorldVisibility from app.models.world import World, WorldVisibility
router = APIRouter() router = APIRouter()
DbDep = Annotated[AsyncSession, Depends(get_db)]
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"}
@router.get("/") @router.get("/")
async def list_worlds( async def list_worlds(
db: DbDep,
skip: int = Query(0, ge=0), skip: int = Query(0, ge=0),
limit: int = Query(50, ge=1, le=100), limit: int = Query(50, ge=1, le=100),
visibility: WorldVisibility | None = None, visibility: WorldVisibility | None = None,
db: Annotated[AsyncSession, Depends(get_db)] = Depends(),
): ):
"""List worlds with optional visibility filter.""" """List worlds with optional visibility filter."""
query = select(World).options(selectinload(World.owner)) query = select(World).options(selectinload(World.owner))
@@ -49,7 +46,7 @@ async def list_worlds(
@router.get("/{world_id}") @router.get("/{world_id}")
async def get_world( async def get_world(
world_id: int, world_id: int,
db: Annotated[AsyncSession, Depends(get_db)] = Depends(), db: DbDep,
): ):
"""Get a single world by ID.""" """Get a single world by ID."""
result = await db.execute( result = await db.execute(
@@ -71,8 +68,8 @@ async def get_world(
@router.post("/") @router.post("/")
async def create_world( async def create_world(
world_data: dict, world_data: dict,
current_user=Depends(get_current_user), current_user: CurrentUser,
db: Annotated[AsyncSession, Depends(get_db)] = Depends(), db: DbDep,
): ):
"""Create a new world.""" """Create a new world."""
name = world_data.get("name") name = world_data.get("name")
@@ -82,7 +79,7 @@ async def create_world(
world = World( world = World(
name=name, name=name,
description=world_data.get("description"), description=world_data.get("description"),
owner_id=current_user["id"], owner_id=current_user.id,
visibility=WorldVisibility( visibility=WorldVisibility(
world_data.get("visibility", "public") world_data.get("visibility", "public")
), ),
@@ -105,8 +102,8 @@ async def create_world(
async def update_world( async def update_world(
world_id: int, world_id: int,
world_data: dict, world_data: dict,
current_user=Depends(get_current_user), current_user: CurrentUser,
db: Annotated[AsyncSession, Depends(get_db)] = Depends(), db: DbDep,
): ):
"""Update an existing world.""" """Update an existing world."""
result = await db.execute(select(World).where(World.id == world_id)) result = await db.execute(select(World).where(World.id == world_id))
@@ -114,6 +111,9 @@ async def update_world(
if not world: if not world:
raise HTTPException(status_code=404, detail="World not found") 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"): if world_data.get("name"):
world.name = world_data["name"] world.name = world_data["name"]
if world_data.get("description") is not None: if world_data.get("description") is not None:
@@ -137,8 +137,8 @@ async def update_world(
@router.delete("/{world_id}") @router.delete("/{world_id}")
async def delete_world( async def delete_world(
world_id: int, world_id: int,
current_user=Depends(get_current_user), current_user: CurrentUser,
db: Annotated[AsyncSession, Depends(get_db)] = Depends(), db: DbDep,
): ):
"""Delete a world.""" """Delete a world."""
result = await db.execute(select(World).where(World.id == world_id)) result = await db.execute(select(World).where(World.id == world_id))
@@ -146,6 +146,9 @@ async def delete_world(
if not world: if not world:
raise HTTPException(status_code=404, detail="World not found") 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.delete(world)
await db.commit() await db.commit()
return {"detail": "World deleted"} return {"detail": "World deleted"}