WIP checkpoint: Scaffold FastAPI backend project structure
This commit is contained in:
@@ -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
@@ -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,
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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"}
|
||||||
|
|||||||
Reference in New Issue
Block a user