From ca798703af77e5a82e00d44f509483a6cb7e6ee7 Mon Sep 17 00:00:00 2001 From: Xiaoxia AI Date: Wed, 17 Jun 2026 07:34:10 +0800 Subject: [PATCH] feat(api): implement complete workspace API routes - Workspace CRUD: create/list/get_detail - Member management: invite/list/remove/leave/update_role - Invitation flow: accept/decline - Subscription: upgrade/cancel - Quota status: get usage info - Connect all 13 endpoints to use cases - Add proper authentication and permission checks - Return structured JSON responses Phase 4 Task 27/68 completed --- apps/api/app/api/routes/workspaces.py | 412 ++++++++++++++++++-------- 1 file changed, 286 insertions(+), 126 deletions(-) diff --git a/apps/api/app/api/routes/workspaces.py b/apps/api/app/api/routes/workspaces.py index f9366e31a..17d173a1e 100644 --- a/apps/api/app/api/routes/workspaces.py +++ b/apps/api/app/api/routes/workspaces.py @@ -1,18 +1,15 @@ """ -Workspace API 路由 +Workspace API 路由(完整实现) """ from fastapi import APIRouter, HTTPException, status, Depends -from pydantic import BaseModel +from pydantic import BaseModel, EmailStr from typing import List +from datetime import datetime -from packages.application.workspace import ( - CreateWorkspaceUseCase, - CreateWorkspaceRequest, - ListWorkspacesUseCase, - ListWorkspacesRequest, - GetWorkspaceDetailUseCase, - GetWorkspaceDetailRequest, -) +from packages.application.workspace import * +from packages.domain.entities import User +from apps.api.app.dependencies import get_container +from apps.api.app.middleware.auth import get_current_user, require_workspace_access, require_workspace_admin, require_workspace_owner router = APIRouter(prefix="/workspaces", tags=["Workspaces"]) @@ -30,186 +27,349 @@ class WorkspaceResponseModel(BaseModel): subscription_plan: str max_projects: int max_storage_gb: int - member_count: int - user_role: str -class WorkspaceListResponseModel(BaseModel): - workspaces: List[WorkspaceResponseModel] +class InviteMemberRequestModel(BaseModel): + email: EmailStr + role: str # admin, member, viewer -class WorkspaceDetailResponseModel(BaseModel): - workspace_id: str - name: str - owner_user_id: str - subscription_plan: str - subscription_status: str - max_projects: int - max_storage_gb: int - used_storage_gb: float - member_count: int - user_role: str +class UpdateMemberRoleRequestModel(BaseModel): + role: str -# ==================== API Endpoints ==================== +class UpgradeSubscriptionRequestModel(BaseModel): + new_plan: str # pro, enterprise + + +# ==================== Workspace CRUD ==================== @router.post("", response_model=WorkspaceResponseModel, status_code=status.HTTP_201_CREATED) async def create_workspace( request: CreateWorkspaceRequestModel, - # current_user: User = Depends(get_current_user), - # use_case: CreateWorkspaceUseCase = Depends(...), + current_user: User = Depends(get_current_user), ): - """ - 创建工作空间 + """创建工作空间""" + container = get_container() + use_case = container.get_create_workspace_use_case() - - 自动成为工作空间 Owner - - 根据 subscription_plan 设置配额 - - Free: 3 projects, 10GB storage - - Pro: unlimited projects, 100GB storage - - Enterprise: unlimited projects, 1TB storage - """ - # TODO: 实现创建工作空间逻辑 - pass - - -@router.get("", response_model=WorkspaceListResponseModel) -async def list_workspaces( - # current_user: User = Depends(get_current_user), - # use_case: ListWorkspacesUseCase = Depends(...), -): - """ - 获取用户的所有工作空间 + req = CreateWorkspaceRequest( + name=request.name, + owner_user_id=current_user.id, + subscription_plan=request.subscription_plan, + ) - - 返回用户作为成员的所有工作空间 - - 包含用户在每个工作空间的角色 - """ - # TODO: 实现获取工作空间列表逻辑 - pass + response, error = use_case.execute(req) + + if error: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=error) + + return WorkspaceResponseModel( + workspace_id=response.workspace_id, + name=response.name, + subscription_plan=response.subscription_plan, + max_projects=response.max_projects, + max_storage_gb=response.max_storage_gb, + ) -@router.get("/{workspace_id}", response_model=WorkspaceDetailResponseModel) +@router.get("") +async def list_workspaces(current_user: User = Depends(get_current_user)): + """获取用户的所有工作空间""" + container = get_container() + use_case = container.get_list_workspaces_use_case() + + req = ListWorkspacesRequest(user_id=current_user.id) + response, error = use_case.execute(req) + + if error: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=error) + + return { + "workspaces": [ + { + "workspace_id": ws.workspace_id, + "name": ws.name, + "subscription_plan": ws.subscription_plan, + "max_projects": ws.max_projects, + "max_storage_gb": ws.max_storage_gb, + "member_count": ws.member_count, + "user_role": ws.user_role, + } + for ws in response.workspaces + ] + } + + +@router.get("/{workspace_id}") async def get_workspace_detail( workspace_id: str, - # current_user: User = Depends(get_current_user), - # use_case: GetWorkspaceDetailUseCase = Depends(...), + current_user: User = Depends(get_current_user), ): - """ - 获取工作空间详情 + """获取工作空间详情""" + container = get_container() + use_case = container.get_get_workspace_detail_use_case() - - 只有成员可以查看 - - 返回详细的配额和使用情况 - """ - # TODO: 实现获取工作空间详情逻辑 - pass + req = GetWorkspaceDetailRequest(workspace_id=workspace_id, user_id=current_user.id) + detail, error = use_case.execute(req) + + if error: + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=error) + + return { + "workspace_id": detail.workspace_id, + "name": detail.name, + "owner_user_id": detail.owner_user_id, + "subscription_plan": detail.subscription_plan, + "subscription_status": detail.subscription_status, + "max_projects": detail.max_projects, + "max_storage_gb": detail.max_storage_gb, + "used_storage_gb": detail.used_storage_gb, + "member_count": detail.member_count, + "user_role": detail.user_role, + } +# ==================== Member Management ==================== + @router.post("/{workspace_id}/members/invite", status_code=status.HTTP_201_CREATED) async def invite_member( workspace_id: str, - # current_user: User = Depends(get_current_user), + request: InviteMemberRequestModel, + current_user: User = Depends(get_current_user), ): - """ - 邀请成员到工作空间 + """邀请成员""" + container = get_container() + use_case = container.get_invite_member_use_case() - - 只有 Owner 和 Admin 可以邀请 - - 发送邀请邮件 - - 邀请有效期 7 天 - """ - # TODO: 实现邀请成员逻辑 - pass + req = InviteMemberRequest( + workspace_id=workspace_id, + inviter_user_id=current_user.id, + invitee_email=request.email, + role=request.role, + ) + + response, error = use_case.execute(req) + + if error: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=error) + + return { + "invitation_id": response.invitation_id, + "invitee_email": response.invitee_email, + "role": response.role, + "expires_at": response.expires_at.isoformat(), + } @router.get("/{workspace_id}/members") async def list_members( workspace_id: str, - # current_user: User = Depends(get_current_user), + current_user: User = Depends(get_current_user), ): - """ - 获取工作空间成员列表 + """获取成员列表""" + container = get_container() + use_case = container.get_list_members_use_case() - - 只有成员可以查看 - - 返回成员的角色和加入时间 - """ - # TODO: 实现获取成员列表逻辑 - pass + req = ListMembersRequest(workspace_id=workspace_id, requester_user_id=current_user.id) + response, error = use_case.execute(req) + + if error: + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=error) + + return { + "members": [ + { + "member_id": m.member_id, + "user_id": m.user_id, + "username": m.username, + "email": m.email, + "display_name": m.display_name, + "role": m.role, + "invited_by": m.invited_by, + "joined_at": m.joined_at.isoformat(), + } + for m in response.members + ] + } @router.delete("/{workspace_id}/members/{user_id}", status_code=status.HTTP_204_NO_CONTENT) async def remove_member( workspace_id: str, user_id: str, - # current_user: User = Depends(get_current_user), + current_user: User = Depends(get_current_user), ): - """ - 移除工作空间成员 + """移除成员""" + container = get_container() + use_case = container.get_remove_member_use_case() - - 只有 Owner 和 Admin 可以移除 - - Admin 不能移除其他 Admin - - 不能移除 Owner - """ - # TODO: 实现移除成员逻辑 - pass + req = RemoveMemberRequest( + workspace_id=workspace_id, + requester_user_id=current_user.id, + target_user_id=user_id, + ) + + success, error = use_case.execute(req) + + if not success: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=error) -@router.patch("/{workspace_id}/members/{user_id}/role", status_code=status.HTTP_200_OK) +@router.post("/{workspace_id}/leave", status_code=status.HTTP_204_NO_CONTENT) +async def leave_workspace( + workspace_id: str, + current_user: User = Depends(get_current_user), +): + """离开工作空间""" + container = get_container() + use_case = container.get_leave_workspace_use_case() + + req = LeaveWorkspaceRequest(workspace_id=workspace_id, user_id=current_user.id) + success, error = use_case.execute(req) + + if not success: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=error) + + +@router.patch("/{workspace_id}/members/{user_id}/role") async def update_member_role( workspace_id: str, user_id: str, - # current_user: User = Depends(get_current_user), + request: UpdateMemberRoleRequestModel, + current_user: User = Depends(get_current_user), ): - """ - 修改成员角色 + """修改成员角色""" + container = get_container() + use_case = container.get_update_member_role_use_case() - - 只有 Owner 和 Admin 可以修改 - - Admin 不能修改其他 Admin 的角色 - - 不能修改自己的角色 - - 不能修改 Owner 的角色 - """ - # TODO: 实现修改成员角色逻辑 - pass + req = UpdateMemberRoleRequest( + workspace_id=workspace_id, + requester_user_id=current_user.id, + target_user_id=user_id, + new_role=request.role, + ) + + response, error = use_case.execute(req) + + if error: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=error) + + return { + "user_id": response.user_id, + "old_role": response.old_role, + "new_role": response.new_role, + } -@router.post("/{workspace_id}/subscription/upgrade", status_code=status.HTTP_200_OK) +# ==================== Subscription Management ==================== + +@router.post("/{workspace_id}/subscription/upgrade") async def upgrade_subscription( workspace_id: str, - # current_user: User = Depends(get_current_user), + request: UpgradeSubscriptionRequestModel, + current_user: User = Depends(get_current_user), ): - """ - 升级订阅计划 + """升级订阅""" + container = get_container() + use_case = container.get_upgrade_subscription_use_case() - - 只有 Owner 可以升级 - - Free → Pro: ¥99/月 - - Pro → Enterprise: ¥999/月 - """ - # TODO: 实现升级订阅逻辑 - pass + req = UpgradeSubscriptionRequest( + workspace_id=workspace_id, + requester_user_id=current_user.id, + new_plan=request.new_plan, + ) + + response, error = use_case.execute(req) + + if error: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=error) + + return { + "workspace_id": response.workspace_id, + "old_plan": response.old_plan, + "new_plan": response.new_plan, + "max_projects": response.max_projects, + "max_storage_gb": response.max_storage_gb, + } -@router.post("/{workspace_id}/subscription/cancel", status_code=status.HTTP_200_OK) +@router.post("/{workspace_id}/subscription/cancel") async def cancel_subscription( workspace_id: str, - # current_user: User = Depends(get_current_user), + current_user: User = Depends(get_current_user), ): - """ - 取消订阅计划 + """取消订阅""" + container = get_container() + use_case = container.get_cancel_subscription_use_case() - - 只有 Owner 可以取消 - - 降级到 Free 计划 - """ - # TODO: 实现取消订阅逻辑 - pass + req = CancelSubscriptionRequest(workspace_id=workspace_id, requester_user_id=current_user.id) + success, error = use_case.execute(req) + + if not success: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=error) + + return {"message": "Subscription cancelled successfully"} @router.get("/{workspace_id}/quota") async def get_quota_status( workspace_id: str, - # current_user: User = Depends(get_current_user), + current_user: User = Depends(get_current_user), ): - """ - 获取配额使用状态 + """获取配额状态""" + container = get_container() + quota_checker = container.quota_checker - - 返回项目数量和存储使用情况 - - 返回使用率百分比 - """ - # TODO: 实现获取配额状态逻辑 - pass + # 检查权限 + permission_checker = container.permission_checker + has_access, _ = permission_checker.check_workspace_access(workspace_id, current_user.id) + + if not has_access: + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Access denied") + + status = quota_checker.get_quota_status(workspace_id) + + if not status: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Workspace not found") + + return status + + +# ==================== Invitation Acceptance ==================== + +@router.post("/invitations/{token}/accept") +async def accept_invitation( + token: str, + current_user: User = Depends(get_current_user), +): + """接受邀请""" + container = get_container() + use_case = container.get_accept_invitation_use_case() + + req = AcceptInvitationRequest(invitation_token=token, user_id=current_user.id) + response, error = use_case.execute(req) + + if error: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=error) + + return { + "workspace_id": response.workspace_id, + "workspace_name": response.workspace_name, + "role": response.role, + } + + +@router.post("/invitations/{token}/decline") +async def decline_invitation(token: str): + """拒绝邀请""" + container = get_container() + use_case = container.get_decline_invitation_use_case() + + req = DeclineInvitationRequest(invitation_token=token) + success, error = use_case.execute(req) + + if not success: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=error) + + return {"message": "Invitation declined"}