feat(workspace): restore core routes on sqlalchemy adapters
This commit is contained in:
@@ -9,6 +9,7 @@ from app.api.routes.ingest_jobs import router as ingest_jobs_router
|
||||
from app.api.routes.project_management import router as project_management_router
|
||||
from app.api.routes.projects import router as projects_router
|
||||
from app.api.routes.upload import router as upload_router
|
||||
from app.api.routes.workspaces import router as workspaces_router
|
||||
from fastapi import APIRouter
|
||||
|
||||
api_router = APIRouter(prefix="/api/v1")
|
||||
@@ -19,6 +20,10 @@ api_router.include_router(
|
||||
auth_router,
|
||||
tags=["认证"],
|
||||
)
|
||||
api_router.include_router(
|
||||
workspaces_router,
|
||||
tags=["工作空间"],
|
||||
)
|
||||
api_router.include_router(
|
||||
projects_router,
|
||||
prefix="/projects",
|
||||
|
||||
@@ -1,36 +1,30 @@
|
||||
"""
|
||||
Legacy workspace API route skeleton.
|
||||
Workspace API routes wired through the canonical API composition root.
|
||||
|
||||
This module depends on the removed DI container and is intentionally disabled until
|
||||
workspace use cases are wired through the canonical API composition root.
|
||||
Only the core create/list/detail endpoints are enabled here. Member invitation
|
||||
and subscription endpoints remain in application use cases but are mounted only
|
||||
after their persistence repositories are wired and smoke-tested.
|
||||
"""
|
||||
|
||||
raise RuntimeError(
|
||||
"apps.api.app.api.routes.workspaces is disabled: rebuild DI container before mounting workspace routes"
|
||||
)
|
||||
|
||||
from datetime import datetime
|
||||
from typing import List
|
||||
|
||||
from app.auth import AuthenticatedUser, get_current_user
|
||||
from app.dependencies import get_user_repository, get_workspace_member_repository, get_workspace_repository
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from pydantic import BaseModel, EmailStr
|
||||
from pydantic import BaseModel
|
||||
|
||||
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,
|
||||
from packages.application.workspace.create_workspace_use_case import CreateWorkspaceRequest, CreateWorkspaceUseCase
|
||||
from packages.application.workspace.list_workspaces_use_case import (
|
||||
GetWorkspaceDetailRequest,
|
||||
GetWorkspaceDetailUseCase,
|
||||
ListWorkspacesRequest,
|
||||
ListWorkspacesUseCase,
|
||||
)
|
||||
from packages.application.workspace import *
|
||||
from packages.domain.entities import User
|
||||
from packages.ports.user_repository import UserRepository
|
||||
from packages.ports.workspace_member_repository import WorkspaceMemberRepository
|
||||
from packages.ports.workspace_repository import WorkspaceRepository
|
||||
|
||||
router = APIRouter(prefix="/workspaces", tags=["Workspaces"])
|
||||
|
||||
|
||||
# ==================== Request/Response Models ====================
|
||||
|
||||
|
||||
class CreateWorkspaceRequestModel(BaseModel):
|
||||
name: str
|
||||
subscription_plan: str = "free"
|
||||
@@ -44,41 +38,55 @@ class WorkspaceResponseModel(BaseModel):
|
||||
max_storage_gb: int
|
||||
|
||||
|
||||
class InviteMemberRequestModel(BaseModel):
|
||||
email: EmailStr
|
||||
role: str # admin, member, viewer
|
||||
class WorkspaceSummaryModel(BaseModel):
|
||||
workspace_id: str
|
||||
name: str
|
||||
subscription_plan: str
|
||||
max_projects: int
|
||||
max_storage_gb: int
|
||||
member_count: int
|
||||
user_role: str
|
||||
|
||||
|
||||
class UpdateMemberRoleRequestModel(BaseModel):
|
||||
role: str
|
||||
class WorkspaceListResponseModel(BaseModel):
|
||||
workspaces: list[WorkspaceSummaryModel]
|
||||
|
||||
|
||||
class UpgradeSubscriptionRequestModel(BaseModel):
|
||||
new_plan: str # pro, enterprise
|
||||
|
||||
|
||||
# ==================== Workspace CRUD ====================
|
||||
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
|
||||
|
||||
|
||||
@router.post("", response_model=WorkspaceResponseModel, status_code=status.HTTP_201_CREATED)
|
||||
async def create_workspace(
|
||||
request: CreateWorkspaceRequestModel,
|
||||
current_user: User = Depends(get_current_user),
|
||||
authenticated_user: AuthenticatedUser = Depends(get_current_user),
|
||||
workspace_repository: WorkspaceRepository = Depends(get_workspace_repository),
|
||||
workspace_member_repository: WorkspaceMemberRepository = Depends(get_workspace_member_repository),
|
||||
user_repository: UserRepository = Depends(get_user_repository),
|
||||
):
|
||||
"""创建工作空间"""
|
||||
container = get_container()
|
||||
use_case = container.get_create_workspace_use_case()
|
||||
|
||||
req = CreateWorkspaceRequest(
|
||||
name=request.name,
|
||||
owner_user_id=current_user.id,
|
||||
subscription_plan=request.subscription_plan,
|
||||
use_case = CreateWorkspaceUseCase(
|
||||
workspace_repository=workspace_repository,
|
||||
workspace_member_repository=workspace_member_repository,
|
||||
user_repository=user_repository,
|
||||
)
|
||||
|
||||
response, error = use_case.execute(req)
|
||||
|
||||
if error:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=error)
|
||||
response, error = use_case.execute(
|
||||
CreateWorkspaceRequest(
|
||||
name=request.name,
|
||||
owner_user_id=authenticated_user.user.id,
|
||||
subscription_plan=request.subscription_plan,
|
||||
)
|
||||
)
|
||||
if error or response is None:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=error or "Failed to create workspace")
|
||||
|
||||
return WorkspaceResponseModel(
|
||||
workspace_id=response.workspace_id,
|
||||
@@ -89,306 +97,62 @@ async def create_workspace(
|
||||
)
|
||||
|
||||
|
||||
@router.get("")
|
||||
async def list_workspaces(current_user: User = Depends(get_current_user)):
|
||||
"""获取用户的所有工作空间"""
|
||||
container = get_container()
|
||||
use_case = container.get_list_workspaces_use_case()
|
||||
@router.get("", response_model=WorkspaceListResponseModel)
|
||||
async def list_workspaces(
|
||||
authenticated_user: AuthenticatedUser = Depends(get_current_user),
|
||||
workspace_repository: WorkspaceRepository = Depends(get_workspace_repository),
|
||||
workspace_member_repository: WorkspaceMemberRepository = Depends(get_workspace_member_repository),
|
||||
):
|
||||
use_case = ListWorkspacesUseCase(
|
||||
workspace_repository=workspace_repository,
|
||||
workspace_member_repository=workspace_member_repository,
|
||||
)
|
||||
response, error = use_case.execute(ListWorkspacesRequest(user_id=authenticated_user.user.id))
|
||||
if error or response is None:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=error or "Failed to list workspaces")
|
||||
|
||||
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
|
||||
return WorkspaceListResponseModel(
|
||||
workspaces=[
|
||||
WorkspaceSummaryModel(
|
||||
workspace_id=workspace.workspace_id,
|
||||
name=workspace.name,
|
||||
subscription_plan=workspace.subscription_plan,
|
||||
max_projects=workspace.max_projects,
|
||||
max_storage_gb=workspace.max_storage_gb,
|
||||
member_count=workspace.member_count,
|
||||
user_role=workspace.user_role,
|
||||
)
|
||||
for workspace in response.workspaces
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@router.get("/{workspace_id}")
|
||||
@router.get("/{workspace_id}", response_model=WorkspaceDetailResponseModel)
|
||||
async def get_workspace_detail(
|
||||
workspace_id: str,
|
||||
current_user: User = Depends(get_current_user),
|
||||
authenticated_user: AuthenticatedUser = Depends(get_current_user),
|
||||
workspace_repository: WorkspaceRepository = Depends(get_workspace_repository),
|
||||
workspace_member_repository: WorkspaceMemberRepository = Depends(get_workspace_member_repository),
|
||||
):
|
||||
"""获取工作空间详情"""
|
||||
container = get_container()
|
||||
use_case = container.get_get_workspace_detail_use_case()
|
||||
|
||||
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,
|
||||
request: InviteMemberRequestModel,
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""邀请成员"""
|
||||
container = get_container()
|
||||
use_case = container.get_invite_member_use_case()
|
||||
|
||||
req = InviteMemberRequest(
|
||||
workspace_id=workspace_id,
|
||||
inviter_user_id=current_user.id,
|
||||
invitee_email=request.email,
|
||||
role=request.role,
|
||||
use_case = GetWorkspaceDetailUseCase(
|
||||
workspace_repository=workspace_repository,
|
||||
workspace_member_repository=workspace_member_repository,
|
||||
)
|
||||
|
||||
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),
|
||||
):
|
||||
"""获取成员列表"""
|
||||
container = get_container()
|
||||
use_case = container.get_list_members_use_case()
|
||||
|
||||
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),
|
||||
):
|
||||
"""移除成员"""
|
||||
container = get_container()
|
||||
use_case = container.get_remove_member_use_case()
|
||||
|
||||
req = RemoveMemberRequest(
|
||||
workspace_id=workspace_id,
|
||||
requester_user_id=current_user.id,
|
||||
target_user_id=user_id,
|
||||
detail, error = use_case.execute(
|
||||
GetWorkspaceDetailRequest(workspace_id=workspace_id, user_id=authenticated_user.user.id)
|
||||
)
|
||||
if error or detail is None:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=error or "Workspace access denied")
|
||||
|
||||
success, error = use_case.execute(req)
|
||||
|
||||
if not success:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=error)
|
||||
|
||||
|
||||
@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,
|
||||
request: UpdateMemberRoleRequestModel,
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""修改成员角色"""
|
||||
container = get_container()
|
||||
use_case = container.get_update_member_role_use_case()
|
||||
|
||||
req = UpdateMemberRoleRequest(
|
||||
workspace_id=workspace_id,
|
||||
requester_user_id=current_user.id,
|
||||
target_user_id=user_id,
|
||||
new_role=request.role,
|
||||
return WorkspaceDetailResponseModel(
|
||||
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,
|
||||
)
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
|
||||
# ==================== Subscription Management ====================
|
||||
|
||||
|
||||
@router.post("/{workspace_id}/subscription/upgrade")
|
||||
async def upgrade_subscription(
|
||||
workspace_id: str,
|
||||
request: UpgradeSubscriptionRequestModel,
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""升级订阅"""
|
||||
container = get_container()
|
||||
use_case = container.get_upgrade_subscription_use_case()
|
||||
|
||||
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")
|
||||
async def cancel_subscription(
|
||||
workspace_id: str,
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""取消订阅"""
|
||||
container = get_container()
|
||||
use_case = container.get_cancel_subscription_use_case()
|
||||
|
||||
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),
|
||||
):
|
||||
"""获取配额状态"""
|
||||
container = get_container()
|
||||
quota_checker = container.quota_checker
|
||||
|
||||
# 检查权限
|
||||
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"}
|
||||
|
||||
Reference in New Issue
Block a user