feat: deploy infrastructure + disk/drive, calendar, presentation, workspaces, onboarding, demo user

This commit is contained in:
2026-05-19 16:26:26 +03:00
parent 688d043dad
commit 3529c39b22
304 changed files with 18826 additions and 1176 deletions
+178
View File
@@ -0,0 +1,178 @@
"""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")