Files
voidea/app/api/v1/workspaces.py

179 lines
6.3 KiB
Python

"""Workspace API routes for VoIdea."""
from typing import Annotated
from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.dependencies import get_current_user, get_db
from app.models.user import User
from app.schemas.workspace import (
AddMemberRequest,
MemberResponse,
WorkspaceCreate,
WorkspaceResponse,
WorkspaceUpdate,
)
from app.services.workspace_service import WorkspaceError, WorkspaceLimitError, WorkspaceService
router = APIRouter()
def _ws_to_response(ws, member_count=0) -> WorkspaceResponse:
return WorkspaceResponse(
id=str(ws.id),
name=ws.name,
type=ws.type,
owner_id=str(ws.owner_id),
description=ws.description,
member_count=member_count,
created_at=ws.created_at,
updated_at=ws.updated_at,
)
@router.get("/", response_model=list[WorkspaceResponse])
async def list_workspaces(
user: Annotated[User, Depends(get_current_user)],
db: AsyncSession = Depends(get_db),
):
svc = WorkspaceService(db)
personal = await svc.create_personal(user.id)
workspaces = await svc.list_user_workspaces(user.id)
result = []
for ws in workspaces:
members = await svc.list_members(ws.id)
result.append(_ws_to_response(ws, member_count=len(members)))
return result
@router.post("/", response_model=WorkspaceResponse, status_code=status.HTTP_201_CREATED)
async def create_workspace(
body: WorkspaceCreate,
user: Annotated[User, Depends(get_current_user)],
db: AsyncSession = Depends(get_db),
):
svc = WorkspaceService(db)
try:
if body.type == "personal":
ws = await svc.create_personal(user.id, body.name)
else:
ws = await svc.create_team(body.name, user.id)
members = await svc.list_members(ws.id)
return _ws_to_response(ws, member_count=len(members))
except WorkspaceLimitError as e:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=str(e))
except WorkspaceError as e:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
@router.get("/{workspace_id}", response_model=WorkspaceResponse)
async def get_workspace(
workspace_id: str,
user: Annotated[User, Depends(get_current_user)],
db: AsyncSession = Depends(get_db),
):
svc = WorkspaceService(db)
ws = await svc.get_by_id(UUID(workspace_id))
if not ws:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Workspace not found")
membership = await svc.get_membership(ws.id, user.id)
if not membership:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not a member")
members = await svc.list_members(ws.id)
return _ws_to_response(ws, member_count=len(members))
@router.patch("/{workspace_id}", response_model=WorkspaceResponse)
async def update_workspace(
workspace_id: str,
body: WorkspaceUpdate,
user: Annotated[User, Depends(get_current_user)],
db: AsyncSession = Depends(get_db),
):
svc = WorkspaceService(db)
try:
ws = await svc.update_workspace(
UUID(workspace_id), user.id, body.model_dump(exclude_none=True)
)
if not ws:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Workspace not found")
members = await svc.list_members(ws.id)
return _ws_to_response(ws, member_count=len(members))
except WorkspaceError as e:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
@router.delete("/{workspace_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_workspace(
workspace_id: str,
user: Annotated[User, Depends(get_current_user)],
db: AsyncSession = Depends(get_db),
):
svc = WorkspaceService(db)
try:
success = await svc.delete_workspace(UUID(workspace_id), user.id)
if not success:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Workspace not found")
except WorkspaceError as e:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
# ── Members ──
@router.get("/{workspace_id}/members", response_model=list[MemberResponse])
async def list_members(
workspace_id: str,
user: Annotated[User, Depends(get_current_user)],
db: AsyncSession = Depends(get_db),
):
svc = WorkspaceService(db)
ws = await svc.get_by_id(UUID(workspace_id))
if not ws:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Workspace not found")
membership = await svc.get_membership(ws.id, user.id)
if not membership:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not a member")
members = await svc.list_members(ws.id)
return [MemberResponse(**m) for m in members]
@router.post("/{workspace_id}/members", response_model=MemberResponse, status_code=status.HTTP_201_CREATED)
async def add_member(
workspace_id: str,
body: AddMemberRequest,
user: Annotated[User, Depends(get_current_user)],
db: AsyncSession = Depends(get_db),
):
svc = WorkspaceService(db)
try:
member = await svc.add_member(UUID(workspace_id), UUID(body.user_id), body.role)
members = await svc.list_members(ws_id := UUID(workspace_id))
m = next((m for m in members if m["id"] == str(member.id)), None)
return MemberResponse(**m) if m else MemberResponse(
id=str(member.id), user_id=str(member.user_id),
workspace_id=str(member.workspace_id), role=member.role,
joined_at=member.created_at,
)
except WorkspaceLimitError as e:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=str(e))
except WorkspaceError as e:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
@router.delete("/{workspace_id}/members/{member_id}", status_code=status.HTTP_204_NO_CONTENT)
async def remove_member(
workspace_id: str,
member_id: str,
user: Annotated[User, Depends(get_current_user)],
db: AsyncSession = Depends(get_db),
):
svc = WorkspaceService(db)
success = await svc.remove_member(UUID(workspace_id), UUID(member_id))
if not success:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Member not found")