Compare commits

...

9 Commits

Author SHA1 Message Date
coder-bot cbd5e367e0 Add missing asyncpg dependency for async postgres driver
config.py's DATABASE_URL uses postgresql+asyncpg but requirements.txt
only listed the sync psycopg2-binary driver, causing ModuleNotFoundError
on app startup.
2026-08-17 20:59:17 +00:00
coder-bot 79765f09c2 WIP checkpoint: Scaffold FastAPI backend project structure 2026-08-17 20:42:41 +00:00
coder-bot dda4a250fa WIP checkpoint: Scaffold FastAPI backend project structure 2026-08-17 20:26:26 +00:00
coder-bot d1b91abf05 WIP checkpoint: Scaffold FastAPI backend project structure 2026-08-17 20:25:43 +00:00
coder-bot 77ea745c75 WIP checkpoint: Scaffold FastAPI backend project structure 2026-08-17 20:25:28 +00:00
coder-bot a3ce38ad1d WIP checkpoint: Scaffold FastAPI backend project structure 2026-08-17 20:25:07 +00:00
coder-bot 19b90f6f3f WIP checkpoint: Scaffold FastAPI backend project structure 2026-08-17 20:22:35 +00:00
coder-bot 923c89ecc9 WIP checkpoint: Scaffold FastAPI backend project structure 2026-08-17 20:18:32 +00:00
coder-bot c88f15a052 WIP checkpoint: Scaffold FastAPI backend project structure 2026-08-17 19:58:57 +00:00
13 changed files with 265 additions and 61 deletions
+8 -2
View File
@@ -1,5 +1,5 @@
# Database # Database (async driver)
DATABASE_URL=postgresql://agp_user:agp_password@localhost:5432/agp_db DATABASE_URL=postgresql+asyncpg://agp_user:agp_password@localhost:5432/agp_db
# Redis # Redis
REDIS_URL=redis://localhost:6379/0 REDIS_URL=redis://localhost:6379/0
@@ -10,5 +10,11 @@ ALGORITHM=HS256
ACCESS_TOKEN_EXPIRE_MINUTES=30 ACCESS_TOKEN_EXPIRE_MINUTES=30
# App # App
APP_NAME=AGP Backend
APP_VERSION=0.1.0
DEBUG=true DEBUG=true
CORS_ORIGINS=http://localhost:8080,http://localhost:3000 CORS_ORIGINS=http://localhost:8080,http://localhost:3000
# File uploads
UPLOAD_DIR=uploads
MAX_UPLOAD_SIZE_MB=100
+126
View File
@@ -0,0 +1,126 @@
"""initial schema
Revision ID: 0001_initial
Revises:
Create Date: 2024-01-01 00:00:00.000000
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision: str = "0001_initial"
down_revision: Union[str, None] = None
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade database schema."""
# Create ENUM types
world_visibility = postgresql.ENUM(
"public", "private", "friends_only", name="world_visibility"
)
world_visibility.create(op.get_bind(), checkfirst=True)
asset_type = postgresql.ENUM(
"image", "audio", "video", "document", "model_3d", "other", name="asset_type"
)
asset_type.create(op.get_bind(), checkfirst=True)
# Create users table
op.create_table(
"users",
sa.Column("id", sa.Integer(), primary_key=True),
sa.Column("username", sa.String(length=50), nullable=False),
sa.Column("email", sa.String(length=120), nullable=False),
sa.Column("hashed_password", sa.String(length=255), nullable=False),
sa.Column("is_active", sa.Boolean(), nullable=True),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=True),
)
op.create_index(op.f("ix_users_email"), "users", ["email"], unique=True)
op.create_index(op.f("ix_users_id"), "users", ["id"], unique=False)
op.create_index(op.f("ix_users_username"), "users", ["username"], unique=True)
# Create sessions table
op.create_table(
"sessions",
sa.Column("id", sa.String(length=36), primary_key=True),
sa.Column("user_id", sa.Integer(), nullable=False),
sa.Column("token", sa.String(), nullable=False),
sa.Column("expires_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("is_active", sa.Boolean(), nullable=True),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
sa.ForeignKeyConstraint(["user_id"], ["users.id"], ondelete="CASCADE"),
)
op.create_index(op.f("ix_sessions_token"), "sessions", ["token"], unique=True)
# Create worlds table
op.create_table(
"worlds",
sa.Column("id", sa.Integer(), primary_key=True),
sa.Column("name", sa.String(length=255), nullable=False),
sa.Column("description", sa.Text(), nullable=True),
sa.Column("owner_id", sa.Integer(), nullable=False),
sa.Column(
"visibility",
postgresql.ENUM(
"public", "private", "friends_only", name="world_visibility"
),
nullable=True,
),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=True),
sa.ForeignKeyConstraint(["owner_id"], ["users.id"]),
)
op.create_index(op.f("ix_worlds_id"), "worlds", ["id"], unique=False)
# Create assets table
op.create_table(
"assets",
sa.Column("id", sa.Integer(), primary_key=True),
sa.Column("name", sa.String(length=255), nullable=False),
sa.Column("description", sa.Text(), nullable=True),
sa.Column(
"asset_type",
postgresql.ENUM(
"image", "audio", "video", "document", "model_3d", "other",
name="asset_type",
),
nullable=False,
),
sa.Column("file_path", sa.String(length=512), nullable=False),
sa.Column("file_size", sa.Integer(), nullable=True),
sa.Column("mime_type", sa.String(length=128), nullable=True),
sa.Column("metadata_json", sa.Text(), nullable=True),
sa.Column("world_id", sa.Integer(), nullable=True),
sa.Column("owner_id", sa.Integer(), nullable=False),
sa.Column("is_public", sa.Boolean(), nullable=False),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
sa.ForeignKeyConstraint(["owner_id"], ["users.id"]),
sa.ForeignKeyConstraint(["world_id"], ["worlds.id"]),
)
op.create_index(op.f("ix_assets_id"), "assets", ["id"], unique=False)
def downgrade() -> None:
"""Downgrade database schema."""
op.drop_index(op.f("ix_assets_id"), table_name="assets")
op.drop_table("assets")
op.drop_index(op.f("ix_worlds_id"), table_name="worlds")
op.drop_table("worlds")
op.drop_index(op.f("ix_sessions_token"), table_name="sessions")
op.drop_table("sessions")
op.drop_index(op.f("ix_users_username"), table_name="users")
op.drop_index(op.f("ix_users_id"), table_name="users")
op.drop_index(op.f("ix_users_email"), table_name="users")
op.drop_table("users")
# Drop ENUM types
postgresql.ENUM(name="asset_type").drop(op.get_bind(), checkfirst=True)
postgresql.ENUM(name="world_visibility").drop(op.get_bind(), checkfirst=True)
+8 -6
View File
@@ -1,8 +1,9 @@
"""Application configuration via pydantic-settings.""" """Application configuration via pydantic-settings."""
from pydantic_settings import BaseSettings
from functools import lru_cache from functools import lru_cache
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings): class Settings(BaseSettings):
# App # App
@@ -28,15 +29,16 @@ class Settings(BaseSettings):
UPLOAD_DIR: str = "uploads" UPLOAD_DIR: str = "uploads"
MAX_UPLOAD_SIZE_MB: int = 100 MAX_UPLOAD_SIZE_MB: int = 100
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
case_sensitive=True,
)
@property @property
def cors_origins_list(self) -> list[str]: def cors_origins_list(self) -> list[str]:
return [origin.strip() for origin in self.CORS_ORIGINS.split(",")] return [origin.strip() for origin in self.CORS_ORIGINS.split(",")]
class Config:
env_file = ".env"
env_file_encoding = "utf-8"
case_sensitive = True
@lru_cache() @lru_cache()
def get_settings() -> Settings: def get_settings() -> Settings:
+1 -1
View File
@@ -10,7 +10,7 @@ from sqlalchemy.orm import DeclarativeBase
from app.config import settings from app.config import settings
engine = create_async_engine( engine = create_async_engine(
settings.database_url, settings.DATABASE_URL,
echo=settings.DEBUG, echo=settings.DEBUG,
pool_size=10, pool_size=10,
max_overflow=20, max_overflow=20,
+50
View File
@@ -0,0 +1,50 @@
"""Shared FastAPI dependencies, including JWT-based auth."""
from typing import Annotated
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from jose import JWTError, jwt
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.config import settings
from app.database import get_db
from app.models.user import User
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/login")
async def get_current_user(
token: Annotated[str, Depends(oauth2_scheme)],
db: Annotated[AsyncSession, Depends(get_db)],
) -> User:
"""Resolve the JWT bearer token to a User, or raise 401."""
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(
token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM]
)
user_id: str | None = payload.get("sub")
if user_id is None:
raise credentials_exception
except JWTError:
raise credentials_exception
result = await db.execute(select(User).where(User.id == int(user_id)))
user = result.scalar_one_or_none()
if user is None:
raise credentials_exception
if not user.is_active:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="User account is inactive",
)
return user
CurrentUser = Annotated[User, Depends(get_current_user)]
+13 -3
View File
@@ -3,6 +3,7 @@
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from app.config import settings from app.config import settings
from app.routers import auth, users, worlds, assets from app.routers import auth, users, worlds, assets
@@ -17,12 +18,21 @@ async def lifespan(app: FastAPI):
app = FastAPI( app = FastAPI(
title=settings.app_name, title=settings.APP_NAME,
version=settings.app_version, version=settings.APP_VERSION,
description="Agentic Game Platform - Backend API", description="Agentic Game Platform - Backend API",
lifespan=lifespan, lifespan=lifespan,
) )
# CORS
app.add_middleware(
CORSMiddleware,
allow_origins=settings.cors_origins_list,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Include routers # Include routers
app.include_router(auth.router, prefix="/api/auth", tags=["auth"]) app.include_router(auth.router, prefix="/api/auth", tags=["auth"])
app.include_router(users.router, prefix="/api/users", tags=["users"]) app.include_router(users.router, prefix="/api/users", tags=["users"])
@@ -33,4 +43,4 @@ app.include_router(assets.router, prefix="/api/assets", tags=["assets"])
@app.get("/api/health") @app.get("/api/health")
async def health_check(): async def health_check():
"""Simple health check endpoint.""" """Simple health check endpoint."""
return {"status": "ok", "app": settings.app_name} return {"status": "ok", "app": settings.APP_NAME}
+7 -3
View File
@@ -1,4 +1,5 @@
from sqlalchemy import Column, Integer, String, ForeignKey, Text, DateTime, Enum as SAEnum from sqlalchemy import Column, Integer, String, ForeignKey, Text, DateTime, Boolean
from sqlalchemy.dialects.postgresql import ENUM
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from sqlalchemy.sql import func from sqlalchemy.sql import func
import enum import enum
@@ -21,14 +22,17 @@ class Asset(Base):
id = Column(Integer, primary_key=True, index=True) id = Column(Integer, primary_key=True, index=True)
name = Column(String(255), nullable=False) name = Column(String(255), nullable=False)
description = Column(Text, nullable=True) description = Column(Text, nullable=True)
asset_type = Column(SAEnum(AssetType), nullable=False) asset_type = Column(
ENUM(AssetType, name="asset_type", create_type=False),
nullable=False,
)
file_path = Column(String(512), nullable=False) file_path = Column(String(512), nullable=False)
file_size = Column(Integer, nullable=True) file_size = Column(Integer, nullable=True)
mime_type = Column(String(128), nullable=True) mime_type = Column(String(128), nullable=True)
metadata_json = Column(Text, nullable=True) metadata_json = Column(Text, nullable=True)
world_id = Column(Integer, ForeignKey("worlds.id"), nullable=True) world_id = Column(Integer, ForeignKey("worlds.id"), nullable=True)
owner_id = Column(Integer, ForeignKey("users.id"), nullable=False) owner_id = Column(Integer, ForeignKey("users.id"), nullable=False)
is_public = Column(Integer, default=0, nullable=False) is_public = Column(Boolean, default=False, nullable=False)
created_at = Column(DateTime(timezone=True), server_default=func.now()) created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
+5 -3
View File
@@ -1,4 +1,6 @@
from sqlalchemy import Column, String, DateTime, ForeignKey, Boolean import uuid
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, Boolean
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from sqlalchemy.sql import func from sqlalchemy.sql import func
from app.database import Base from app.database import Base
@@ -7,8 +9,8 @@ from app.database import Base
class Session(Base): class Session(Base):
__tablename__ = "sessions" __tablename__ = "sessions"
id = Column(String, primary_key=True) id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
user_id = Column(String, ForeignKey("users.id", ondelete="CASCADE"), nullable=False) user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
token = Column(String, unique=True, index=True, nullable=False) token = Column(String, unique=True, index=True, nullable=False)
expires_at = Column(DateTime(timezone=True), nullable=False) expires_at = Column(DateTime(timezone=True), nullable=False)
is_active = Column(Boolean, default=True) is_active = Column(Boolean, default=True)
+6 -2
View File
@@ -1,4 +1,5 @@
from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey, Enum from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey
from sqlalchemy.dialects.postgresql import ENUM
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from sqlalchemy.sql import func from sqlalchemy.sql import func
import enum import enum
@@ -19,7 +20,10 @@ class World(Base):
name = Column(String(255), nullable=False) name = Column(String(255), nullable=False)
description = Column(Text, nullable=True) description = Column(Text, nullable=True)
owner_id = Column(Integer, ForeignKey("users.id"), nullable=False) owner_id = Column(Integer, ForeignKey("users.id"), nullable=False)
visibility = Column(Enum(WorldVisibility), default=WorldVisibility.PUBLIC) visibility = Column(
ENUM(WorldVisibility, name="world_visibility", create_type=False),
default=WorldVisibility.PUBLIC,
)
created_at = Column(DateTime(timezone=True), server_default=func.now()) created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now()) updated_at = Column(DateTime(timezone=True), onupdate=func.now())
+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"}
+1
View File
@@ -3,6 +3,7 @@ uvicorn[standard]==0.34.0
sqlalchemy==2.0.36 sqlalchemy==2.0.36
alembic==1.14.0 alembic==1.14.0
psycopg2-binary==2.9.10 psycopg2-binary==2.9.10
asyncpg==0.31.0
redis==5.2.1 redis==5.2.1
pydantic==2.10.3 pydantic==2.10.3
pydantic-settings==2.7.0 pydantic-settings==2.7.0