WIP checkpoint: Scaffold FastAPI backend project structure

This commit is contained in:
2026-08-17 20:26:26 +00:00
parent 2fe6d5dbf0
commit d7d77474f9
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 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)
+16 -18
View File
@@ -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
+17 -14
View File
@@ -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"}