diff --git a/apps/api/app/api/routes/asset_diagnosis.py b/apps/api/app/api/routes/asset_diagnosis.py index 1b3cdb2e8..76fd75967 100644 --- a/apps/api/app/api/routes/asset_diagnosis.py +++ b/apps/api/app/api/routes/asset_diagnosis.py @@ -1,18 +1,15 @@ from typing import Any -from app.api.routes.permissions import require_workspace_member from app.auth import AuthenticatedUser, get_current_user from app.dependencies import ( get_asset_library_repository, get_asset_repository, get_project_repository, - get_workspace_member_repository, ) from app.schemas.asset_diagnosis import AssetGapItem, AssetSmartViewItem, ProjectAssetDiagnosisResponse from fastapi import APIRouter, Depends, HTTPException from packages.domain import Asset, AssetLibraryKind, AssetStatus -from packages.ports.workspace_member_repository import WorkspaceMemberRepository router = APIRouter() @@ -37,7 +34,7 @@ def _readiness_label(score: int) -> str: return "暂不建议生成" -def _build_diagnosis(workspace_id: str, project_id: str, assets: list[Asset]) -> ProjectAssetDiagnosisResponse: +def _build_diagnosis(project_id: str, assets: list[Asset]) -> ProjectAssetDiagnosisResponse: ready_assets = [asset for asset in assets if asset.status == AssetStatus.READY] video_assets = [asset for asset in ready_assets if _asset_kind(asset) == AssetLibraryKind.VIDEO] image_assets = [asset for asset in ready_assets if _asset_kind(asset) == AssetLibraryKind.IMAGE] @@ -175,7 +172,6 @@ def _build_diagnosis(workspace_id: str, project_id: str, assets: list[Asset]) -> ] return ProjectAssetDiagnosisResponse( - workspace_id=workspace_id, project_id=project_id, readiness_score=score, readiness_label=_readiness_label(score), @@ -201,16 +197,14 @@ def get_project_asset_diagnosis( project_repository: Any = Depends(get_project_repository), asset_library_repository: Any = Depends(get_asset_library_repository), asset_repository: Any = Depends(get_asset_repository), - workspace_member_repository: WorkspaceMemberRepository = Depends(get_workspace_member_repository), ) -> ProjectAssetDiagnosisResponse: project = project_repository.find_by_id(project_id) if project is None: raise HTTPException(status_code=404, detail=f"Project {project_id} not found") - require_workspace_member(project.workspace_id, authenticated_user, workspace_member_repository) libraries = asset_library_repository.list_by_project(project_id) assets: list[Asset] = [] for library in libraries: assets.extend(asset_repository.list_by_library(library.id)) - return _build_diagnosis(project.workspace_id, project_id, assets) + return _build_diagnosis(project_id, assets) diff --git a/apps/api/app/api/routes/chunked_upload.py b/apps/api/app/api/routes/chunked_upload.py index d2c1e49cf..d2ee4c957 100644 --- a/apps/api/app/api/routes/chunked_upload.py +++ b/apps/api/app/api/routes/chunked_upload.py @@ -21,7 +21,6 @@ from app.dependencies import ( get_asset_library_repository, get_ingest_job_repository, get_project_repository, - get_workspace_member_repository, ) from app.schemas.chunked_upload import ( ChunkedUploadCompleteRequest, @@ -34,7 +33,6 @@ from fastapi import APIRouter, Depends, HTTPException, UploadFile, status from fastapi.params import File from packages.application import GetProjectUseCase, SubmitIngestJobCommand, SubmitIngestJobUseCase -from packages.ports.workspace_member_repository import WorkspaceMemberRepository router = APIRouter() logger = logging.getLogger(__name__) @@ -92,19 +90,7 @@ def _atomic_check_and_record(upload_id: str, chunk_index: int) -> bool: fcntl.flock(f.fileno(), fcntl.LOCK_UN) -def _require_workspace_member( - workspace_id: str, - authenticated_user: AuthenticatedUser, - workspace_member_repository: WorkspaceMemberRepository, -) -> None: - """Verify user has workspace permission""" - member = workspace_member_repository.find_by_workspace_and_user(workspace_id, authenticated_user.user.id) - if member is None: - raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Workspace access denied") - - def _require_project_and_library( - workspace_id: str, project_id: str, library_id: str, project_repository: Any, @@ -112,11 +98,11 @@ def _require_project_and_library( ) -> None: """Verify project and asset library exist""" project = GetProjectUseCase(project_repository).execute(project_id) - if project is None or project.workspace_id != workspace_id: + if project is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Project not found") libraries = asset_library_repository.list_by_project(project_id) - if not any(item.id == library_id and item.workspace_id == workspace_id for item in libraries): + if not any(item.id == library_id for item in libraries): raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Asset library not found") @@ -193,7 +179,6 @@ async def init_chunked_upload( authenticated_user: AuthenticatedUser = Depends(get_current_user), project_repository: Any = Depends(get_project_repository), asset_library_repository: Any = Depends(get_asset_library_repository), - workspace_member_repository: WorkspaceMemberRepository = Depends(get_workspace_member_repository), ) -> ChunkedUploadInitResponse: """Initialize chunked upload""" settings = get_settings() @@ -205,17 +190,13 @@ async def init_chunked_upload( detail=f"File exceeds maximum size ({MAX_FILE_SIZE // (1024 * 1024 * 1024)}GB)", ) - # Validate project exists and get workspace_id + # Validate project exists project = GetProjectUseCase(project_repository).execute(request.project_id) if project is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Project not found") - workspace_id = project.workspace_id - - # Verify permission and asset library - _require_workspace_member(workspace_id, authenticated_user, workspace_member_repository) + # Verify asset library _require_project_and_library( - workspace_id, request.project_id, request.library_id, project_repository, @@ -253,7 +234,6 @@ async def init_chunked_upload( "content_type": request.content_type, "project_id": request.project_id, "library_id": request.library_id, - "workspace_id": workspace_id, "status": "pending", "created_at": now.isoformat(), "expires_at": expires_at.isoformat(), @@ -275,7 +255,6 @@ async def upload_chunk( chunk_index: int, chunk: UploadFile = File(..., description="Chunk data"), authenticated_user: AuthenticatedUser = Depends(get_current_user), - workspace_member_repository: WorkspaceMemberRepository = Depends(get_workspace_member_repository), ) -> dict[str, Any]: """Upload a single chunk""" # Load metadata @@ -296,9 +275,6 @@ async def upload_chunk( detail=f"Invalid chunk index. Must be between 0 and {meta['total_chunks'] - 1}", ) - # Verify permission - _require_workspace_member(meta["workspace_id"], authenticated_user, workspace_member_repository) - # Atomic check and record to prevent race conditions if not _atomic_check_and_record(upload_id, chunk_index): return {"message": "Chunk already uploaded", "chunk_index": chunk_index} @@ -350,14 +326,10 @@ async def upload_chunk( async def get_upload_status( upload_id: str, authenticated_user: AuthenticatedUser = Depends(get_current_user), - workspace_member_repository: WorkspaceMemberRepository = Depends(get_workspace_member_repository), ) -> ChunkedUploadStatusResponse: """Get upload status (for resume)""" meta = _load_upload_meta(upload_id) - # Verify permission - _require_workspace_member(meta["workspace_id"], authenticated_user, workspace_member_repository) - return ChunkedUploadStatusResponse( upload_id=upload_id, filename=meta["filename"], @@ -377,7 +349,6 @@ async def complete_chunked_upload( authenticated_user: AuthenticatedUser = Depends(get_current_user), project_repository: Any = Depends(get_project_repository), asset_library_repository: Any = Depends(get_asset_library_repository), - workspace_member_repository: WorkspaceMemberRepository = Depends(get_workspace_member_repository), ingest_job_repository: Any = Depends(get_ingest_job_repository), storage_service: OSSStorageService = Depends(get_storage_service), ) -> ChunkedUploadCompleteResponse: @@ -385,9 +356,6 @@ async def complete_chunked_upload( # Load metadata meta = _load_upload_meta(upload_id) - # Verify permission - _require_workspace_member(meta["workspace_id"], authenticated_user, workspace_member_repository) - # Verify project ID and library ID if request.project_id != meta["project_id"] or request.library_id != meta["library_id"]: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Project or library ID mismatch") @@ -448,7 +416,6 @@ async def complete_chunked_upload( use_case = SubmitIngestJobUseCase(ingest_job_repository) job = use_case.execute( SubmitIngestJobCommand( - workspace_id=meta["workspace_id"], project_id=meta["project_id"], library_id=meta["library_id"], storage_key=storage_key, diff --git a/apps/api/app/api/routes/edit_plans.py b/apps/api/app/api/routes/edit_plans.py index b6b7b71b6..ce446cb28 100644 --- a/apps/api/app/api/routes/edit_plans.py +++ b/apps/api/app/api/routes/edit_plans.py @@ -1,13 +1,11 @@ from datetime import datetime, timezone from uuid import uuid4 -from app.api.routes.permissions import require_workspace_member from app.auth import AuthenticatedUser, get_current_user from app.dependencies import ( get_asset_repository, get_db_session, get_project_repository, - get_workspace_member_repository, ) from app.schemas.edit_plan import ( AutoGenerateEditPlanRequest, @@ -22,20 +20,18 @@ from sqlalchemy.orm import Session from packages.adapters.sqlalchemy_impl.models import EditPlanClipModel, EditPlanModel, EditTemplateModel from packages.domain import AssetStatus -from packages.ports.workspace_member_repository import WorkspaceMemberRepository router = APIRouter(prefix="/projects/{project_id}/edit-plans", tags=["剪辑计划"]) -def _ensure_project(project_id: str, workspace_id: str, user: AuthenticatedUser, project_repository, member_repository): +def _ensure_project(project_id: str, project_repository): project = project_repository.find_by_id(project_id) - if project is None or project.workspace_id != workspace_id: + if project is None: raise HTTPException(status_code=404, detail="Project not found") - require_workspace_member(workspace_id, user, member_repository) return project -def _default_template(session: Session, workspace_id: str, project_id: str, user_id: str) -> EditTemplateModel: +def _default_template(session: Session, project_id: str, user_id: str) -> EditTemplateModel: template = ( session.query(EditTemplateModel) .filter( @@ -49,7 +45,6 @@ def _default_template(session: Session, workspace_id: str, project_id: str, user return template template = EditTemplateModel( id=uuid4().hex, - workspace_id=workspace_id, project_id=project_id, name="基础节奏模板", description="自动选择可用视频素材,按上传顺序生成三段式剪辑计划。", @@ -65,7 +60,6 @@ def _default_template(session: Session, workspace_id: str, project_id: str, user def _to_template_response(template: EditTemplateModel) -> EditTemplateResponse: return EditTemplateResponse( id=template.id, - workspace_id=template.workspace_id, project_id=template.project_id, name=template.name, description=template.description, @@ -81,7 +75,6 @@ def _to_plan_response( ) -> EditPlanResponse: return EditPlanResponse( id=plan.id, - workspace_id=plan.workspace_id, project_id=plan.project_id, template_id=plan.template_id, asset_library_id=plan.asset_library_id, @@ -110,14 +103,12 @@ def _to_plan_response( @router.get("/templates/", response_model=list[EditTemplateResponse]) def list_edit_templates( project_id: str, - workspace_id: str, authenticated_user: AuthenticatedUser = Depends(get_current_user), project_repository=Depends(get_project_repository), - workspace_member_repository: WorkspaceMemberRepository = Depends(get_workspace_member_repository), session: Session = Depends(get_db_session), ) -> list[EditTemplateResponse]: - _ensure_project(project_id, workspace_id, authenticated_user, project_repository, workspace_member_repository) - template = _default_template(session, workspace_id, project_id, authenticated_user.user.id) + _ensure_project(project_id, project_repository) + template = _default_template(session, project_id, authenticated_user.user.id) templates = ( session.query(EditTemplateModel) .filter(EditTemplateModel.project_id == project_id, EditTemplateModel.is_active.is_(True)) @@ -133,19 +124,16 @@ def create_edit_plan( authenticated_user: AuthenticatedUser = Depends(get_current_user), project_repository=Depends(get_project_repository), asset_repository=Depends(get_asset_repository), - workspace_member_repository: WorkspaceMemberRepository = Depends(get_workspace_member_repository), session: Session = Depends(get_db_session), ) -> EditPlanResponse: - _ensure_project( - project_id, request.workspace_id, authenticated_user, project_repository, workspace_member_repository - ) + _ensure_project(project_id, project_repository) template = ( session.query(EditTemplateModel).filter(EditTemplateModel.id == request.template_id).first() if request.template_id else None ) if template is None: - template = _default_template(session, request.workspace_id, project_id, authenticated_user.user.id) + template = _default_template(session, project_id, authenticated_user.user.id) assets = [ asset for asset in asset_repository.list_by_library(request.asset_library_id) @@ -158,7 +146,6 @@ def create_edit_plan( ] plan = EditPlanModel( id=uuid4().hex, - workspace_id=request.workspace_id, project_id=project_id, template_id=template.id, asset_library_id=request.asset_library_id, @@ -194,7 +181,6 @@ def auto_generate_edit_plan( authenticated_user: AuthenticatedUser = Depends(get_current_user), project_repository=Depends(get_project_repository), asset_repository=Depends(get_asset_repository), - workspace_member_repository: WorkspaceMemberRepository = Depends(get_workspace_member_repository), session: Session = Depends(get_db_session), ) -> EditPlanResponse: """ @@ -207,9 +193,7 @@ def auto_generate_edit_plan( - voice_over: person 类素材为主播口播,其余穿插为 B-roll - voice_pip: 结合 voice_over 和 pip,第一个高质量 person 素材为主画面 """ - _ensure_project( - project_id, request.workspace_id, authenticated_user, project_repository, workspace_member_repository - ) + _ensure_project(project_id, project_repository) # 获取素材库中的所有素材 assets = asset_repository.list_by_library(request.asset_library_id) @@ -238,12 +222,11 @@ def auto_generate_edit_plan( else None ) if template is None: - template = _default_template(session, request.workspace_id, project_id, authenticated_user.user.id) + template = _default_template(session, project_id, authenticated_user.user.id) # 创建剪辑计划 plan = EditPlanModel( id=uuid4().hex, - workspace_id=request.workspace_id, project_id=project_id, template_id=template.id, asset_library_id=request.asset_library_id, @@ -286,7 +269,6 @@ def get_edit_plan( authenticated_user: AuthenticatedUser = Depends(get_current_user), project_repository=Depends(get_project_repository), asset_repository=Depends(get_asset_repository), - workspace_member_repository: WorkspaceMemberRepository = Depends(get_workspace_member_repository), session: Session = Depends(get_db_session), ) -> EditPlanResponse: plan = ( @@ -294,7 +276,7 @@ def get_edit_plan( ) if plan is None: raise HTTPException(status_code=404, detail="Edit plan not found") - _ensure_project(project_id, plan.workspace_id, authenticated_user, project_repository, workspace_member_repository) + _ensure_project(project_id, project_repository) clips = ( session.query(EditPlanClipModel) .filter(EditPlanClipModel.edit_plan_id == plan.id) diff --git a/apps/api/app/api/routes/generated_videos.py b/apps/api/app/api/routes/generated_videos.py index cb35aa236..092b07153 100644 --- a/apps/api/app/api/routes/generated_videos.py +++ b/apps/api/app/api/routes/generated_videos.py @@ -1,9 +1,8 @@ from typing import Any -from app.api.routes.permissions import require_workspace_member from app.auth import AuthenticatedUser, get_current_user from app.core.storage import OSSStorageService, get_storage_service -from app.dependencies import get_generated_video_repository, get_project_repository, get_workspace_member_repository +from app.dependencies import get_generated_video_repository, get_project_repository from app.schemas.generated_video import ( GeneratedVideoDownloadUrlResponse, GeneratedVideoResponse, @@ -17,7 +16,6 @@ from packages.application import ( GetGeneratedVideoUseCase, ListGeneratedVideosUseCase, ) -from packages.ports.workspace_member_repository import WorkspaceMemberRepository router = APIRouter() @@ -25,7 +23,6 @@ router = APIRouter() def _to_generated_video_response(item, download_url: str | None = None) -> GeneratedVideoResponse: return GeneratedVideoResponse( id=item.id, - workspace_id=item.workspace_id, project_id=item.project_id, generation_task_id=item.generation_task_id, name=item.name, @@ -49,13 +46,11 @@ def list_generated_videos( authenticated_user: AuthenticatedUser = Depends(get_current_user), generated_video_repository: Any = Depends(get_generated_video_repository), project_repository: Any = Depends(get_project_repository), - workspace_member_repository: WorkspaceMemberRepository = Depends(get_workspace_member_repository), storage_service: OSSStorageService = Depends(get_storage_service), ) -> ListGeneratedVideosResponse: project = project_repository.find_by_id(project_id) if project is None: raise HTTPException(status_code=404, detail=f"Project {project_id} not found") - require_workspace_member(project.workspace_id, authenticated_user, workspace_member_repository) use_case = ListGeneratedVideosUseCase(generated_video_repository) items = use_case.execute(project_id) # Generate download URLs for each video @@ -71,14 +66,12 @@ def get_generated_video( video_id: str, authenticated_user: AuthenticatedUser = Depends(get_current_user), generated_video_repository: Any = Depends(get_generated_video_repository), - workspace_member_repository: WorkspaceMemberRepository = Depends(get_workspace_member_repository), storage_service: OSSStorageService = Depends(get_storage_service), ) -> GeneratedVideoResponse: use_case = GetGeneratedVideoUseCase(generated_video_repository) item = use_case.execute(video_id) if item is None: raise HTTPException(status_code=404, detail=f"GeneratedVideo {video_id} not found") - require_workspace_member(item.workspace_id, authenticated_user, workspace_member_repository) download_url = storage_service.get_download_url(item.file_url) return _to_generated_video_response(item, download_url=download_url) @@ -89,13 +82,11 @@ def update_generated_video_review_status( request: UpdateGeneratedVideoReviewRequest, authenticated_user: AuthenticatedUser = Depends(get_current_user), generated_video_repository: Any = Depends(get_generated_video_repository), - workspace_member_repository: WorkspaceMemberRepository = Depends(get_workspace_member_repository), storage_service: OSSStorageService = Depends(get_storage_service), ) -> GeneratedVideoResponse: video = generated_video_repository.get(video_id) if video is None: raise HTTPException(status_code=404, detail=f"GeneratedVideo {video_id} not found") - require_workspace_member(video.workspace_id, authenticated_user, workspace_member_repository) video.review_status = request.review_status updated = generated_video_repository.update(video) download_url = storage_service.get_download_url(updated.file_url) @@ -107,13 +98,11 @@ def get_generated_video_download_url( video_id: str, authenticated_user: AuthenticatedUser = Depends(get_current_user), generated_video_repository: Any = Depends(get_generated_video_repository), - workspace_member_repository: WorkspaceMemberRepository = Depends(get_workspace_member_repository), storage_service: OSSStorageService = Depends(get_storage_service), ) -> GeneratedVideoDownloadUrlResponse: video = generated_video_repository.get(video_id) if video is None: raise HTTPException(status_code=404, detail=f"GeneratedVideo {video_id} not found") - require_workspace_member(video.workspace_id, authenticated_user, workspace_member_repository) use_case = GetGeneratedVideoDownloadUrlUseCase(generated_video_repository) file_url = use_case.execute(video_id) if file_url is None: diff --git a/apps/api/app/api/routes/permissions.py b/apps/api/app/api/routes/permissions.py index d39395635..698daeda2 100644 --- a/apps/api/app/api/routes/permissions.py +++ b/apps/api/app/api/routes/permissions.py @@ -1,8 +1,2 @@ -# Compatibility module - workspace concept has been removed -# This is a stub to maintain backward compatibility - -def require_workspace_member(workspace_id, authenticated_user, workspace_member_repository): - """Stub function for backward compatibility. - Workspace concept has been removed. - """ - pass +# Compatibility module - workspace concept has been removed. +# All permission checks are handled at the project level (see packages.domain.permissions). diff --git a/apps/api/app/api/routes/project_management.py b/apps/api/app/api/routes/project_management.py index 24a3765fc..b346b3932 100644 --- a/apps/api/app/api/routes/project_management.py +++ b/apps/api/app/api/routes/project_management.py @@ -51,7 +51,6 @@ def get_issue_repo(): class CreateTaskRequest(BaseModel): project_id: str - workspace_id: str = "" name: str description: str = "" priority: TaskPriority = TaskPriority.MEDIUM @@ -62,7 +61,6 @@ class CreateTaskRequest(BaseModel): class TaskResponse(BaseModel): id: str project_id: str - workspace_id: str = "" name: str description: str status: TaskStatus @@ -96,7 +94,6 @@ class UpdateTaskProgressRequest(BaseModel): class CreateMilestoneRequest(BaseModel): project_id: str - workspace_id: str = "" name: str description: str = "" @@ -104,7 +101,6 @@ class CreateMilestoneRequest(BaseModel): class MilestoneResponse(BaseModel): id: str project_id: str - workspace_id: str = "" name: str description: str target_date: datetime | None @@ -117,7 +113,6 @@ class MilestoneResponse(BaseModel): class CreateTaskIssueRequest(BaseModel): task_id: str project_id: str - workspace_id: str = "" title: str description: str = "" created_by_user_id: str = "" @@ -127,7 +122,6 @@ class TaskIssueResponse(BaseModel): id: str task_id: str project_id: str - workspace_id: str = "" title: str description: str resolved: bool @@ -149,7 +143,6 @@ def create_task( use_case = CreateTaskUseCase(task_repo) task = use_case.execute( project_id=req.project_id, - workspace_id=req.workspace_id, name=req.name, description=req.description, priority=req.priority, @@ -159,7 +152,6 @@ def create_task( return TaskResponse( id=task.id, project_id=task.project_id, - workspace_id=task.workspace_id, name=task.name, description=task.description, status=task.status, @@ -189,7 +181,6 @@ def list_tasks( TaskResponse( id=t.id, project_id=t.project_id, - workspace_id=t.workspace_id, name=t.name, description=t.description, status=t.status, @@ -223,7 +214,6 @@ def get_task( return TaskResponse( id=task.id, project_id=task.project_id, - workspace_id=task.workspace_id, name=task.name, description=task.description, status=task.status, @@ -262,7 +252,6 @@ def update_task( return TaskResponse( id=task.id, project_id=task.project_id, - workspace_id=task.workspace_id, name=task.name, description=task.description, status=task.status, @@ -295,7 +284,6 @@ def update_task_status( return TaskResponse( id=task.id, project_id=task.project_id, - workspace_id=task.workspace_id, name=task.name, description=task.description, status=task.status, @@ -328,7 +316,6 @@ def update_task_progress( return TaskResponse( id=task.id, project_id=task.project_id, - workspace_id=task.workspace_id, name=task.name, description=task.description, status=task.status, @@ -358,14 +345,12 @@ def create_milestone( use_case = CreateMilestoneUseCase(milestone_repo) milestone = use_case.execute( project_id=req.project_id, - workspace_id=req.workspace_id, name=req.name, description=req.description, ) return MilestoneResponse( id=milestone.id, project_id=milestone.project_id, - workspace_id=milestone.workspace_id, name=milestone.name, description=milestone.description, target_date=milestone.target_date, @@ -388,7 +373,6 @@ def list_milestones( MilestoneResponse( id=m.id, project_id=m.project_id, - workspace_id=m.workspace_id, name=m.name, description=m.description, target_date=m.target_date, @@ -414,7 +398,6 @@ def create_issue( issue = use_case.execute( task_id=req.task_id, project_id=req.project_id, - workspace_id=req.workspace_id, title=req.title, description=req.description, created_by_user_id=req.created_by_user_id, @@ -423,7 +406,6 @@ def create_issue( id=issue.id, task_id=issue.task_id, project_id=issue.project_id, - workspace_id=issue.workspace_id, title=issue.title, description=issue.description, resolved=issue.resolved, @@ -447,7 +429,6 @@ def list_issues( id=i.id, task_id=i.task_id, project_id=i.project_id, - workspace_id=i.workspace_id, title=i.title, description=i.description, resolved=i.resolved, @@ -475,7 +456,6 @@ def resolve_issue( id=issue.id, task_id=issue.task_id, project_id=issue.project_id, - workspace_id=issue.workspace_id, title=issue.title, description=issue.description, resolved=issue.resolved, diff --git a/apps/api/app/api/routes/project_titles.py b/apps/api/app/api/routes/project_titles.py index 0aa4e248c..90acea423 100644 --- a/apps/api/app/api/routes/project_titles.py +++ b/apps/api/app/api/routes/project_titles.py @@ -1,11 +1,9 @@ from typing import Any -from app.api.routes.permissions import require_workspace_member from app.auth import AuthenticatedUser, get_current_user from app.dependencies import ( get_project_repository, get_project_title_repository, - get_workspace_member_repository, ) from app.schemas.project_title import ( CreateProjectTitleRequest, @@ -15,15 +13,12 @@ from app.schemas.project_title import ( ) from fastapi import APIRouter, Depends, HTTPException, status -from packages.ports.workspace_member_repository import WorkspaceMemberRepository - router = APIRouter() def _to_response(item) -> ProjectTitleResponse: return ProjectTitleResponse( id=item.id, - workspace_id=item.workspace_id, project_id=item.project_id, text=item.text, category=item.category, @@ -49,10 +44,8 @@ def list_project_titles( authenticated_user: AuthenticatedUser = Depends(get_current_user), project_repository: Any = Depends(get_project_repository), title_repository: Any = Depends(get_project_title_repository), - workspace_member_repository: WorkspaceMemberRepository = Depends(get_workspace_member_repository), ) -> ListProjectTitlesResponse: - project = _get_project_or_404(project_id, project_repository) - require_workspace_member(project.workspace_id, authenticated_user, workspace_member_repository) + _get_project_or_404(project_id, project_repository) return ListProjectTitlesResponse( items=[_to_response(item) for item in title_repository.list_by_project(project_id, active_only)] ) @@ -65,14 +58,9 @@ def create_project_title( authenticated_user: AuthenticatedUser = Depends(get_current_user), project_repository: Any = Depends(get_project_repository), title_repository: Any = Depends(get_project_title_repository), - workspace_member_repository: WorkspaceMemberRepository = Depends(get_workspace_member_repository), ) -> ProjectTitleResponse: - project = _get_project_or_404(project_id, project_repository) - if project.workspace_id != request.workspace_id: - raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="workspace_id does not match project") - require_workspace_member(project.workspace_id, authenticated_user, workspace_member_repository) + _get_project_or_404(project_id, project_repository) item = title_repository.create( - workspace_id=project.workspace_id, project_id=project_id, text=request.text, category=request.category, @@ -88,12 +76,10 @@ def update_project_title( request: UpdateProjectTitleRequest, authenticated_user: AuthenticatedUser = Depends(get_current_user), title_repository: Any = Depends(get_project_title_repository), - workspace_member_repository: WorkspaceMemberRepository = Depends(get_workspace_member_repository), ) -> ProjectTitleResponse: item = title_repository.get(title_id) if item is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Project title not found") - require_workspace_member(item.workspace_id, authenticated_user, workspace_member_repository) if request.text is not None: item.text = request.text.strip() if request.category is not None: diff --git a/apps/api/app/api/routes/projects.py b/apps/api/app/api/routes/projects.py index 225b49716..cf352a909 100644 --- a/apps/api/app/api/routes/projects.py +++ b/apps/api/app/api/routes/projects.py @@ -1,13 +1,12 @@ from typing import Any from app.auth import AuthenticatedUser, get_current_user -from app.dependencies import get_project_repository, get_workspace_member_repository +from app.dependencies import get_project_repository from app.schemas.project import ( CreateProjectRequest, ListProjectsResponse, ProjectResponse, ) -from app.api.routes.permissions import require_workspace_member from fastapi import APIRouter, Depends, HTTPException, status from packages.application import ( @@ -16,7 +15,6 @@ from packages.application import ( GetProjectUseCase, ListProjectsUseCase, ) -from packages.ports.workspace_member_repository import WorkspaceMemberRepository router = APIRouter() @@ -24,7 +22,6 @@ router = APIRouter() def _to_project_response(item) -> ProjectResponse: return ProjectResponse( id=item.id, - workspace_id=item.workspace_id, name=item.name, description=item.description, ) @@ -35,26 +32,21 @@ def get_project( project_id: str, authenticated_user: AuthenticatedUser = Depends(get_current_user), project_repository: Any = Depends(get_project_repository), - workspace_member_repository: WorkspaceMemberRepository = Depends(get_workspace_member_repository), ) -> ProjectResponse: use_case = GetProjectUseCase(project_repository) project = use_case.execute(project_id) if project is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Project not found") - require_workspace_member(project.workspace_id, authenticated_user, workspace_member_repository) return _to_project_response(project) @router.get("", response_model=ListProjectsResponse) def list_projects( - workspace_id: str = "", authenticated_user: AuthenticatedUser = Depends(get_current_user), project_repository: Any = Depends(get_project_repository), - workspace_member_repository: WorkspaceMemberRepository = Depends(get_workspace_member_repository), ) -> ListProjectsResponse: - require_workspace_member(workspace_id, authenticated_user, workspace_member_repository) use_case = ListProjectsUseCase(project_repository) - projects = use_case.execute(workspace_id) + projects = use_case.execute(authenticated_user.user.id) return ListProjectsResponse(items=[_to_project_response(item) for item in projects]) @@ -63,15 +55,13 @@ def create_project( request: CreateProjectRequest, authenticated_user: AuthenticatedUser = Depends(get_current_user), project_repository: Any = Depends(get_project_repository), - workspace_member_repository: WorkspaceMemberRepository = Depends(get_workspace_member_repository), ) -> ProjectResponse: - require_workspace_member(request.workspace_id, authenticated_user, workspace_member_repository) use_case = CreateProjectUseCase(project_repository) project = use_case.execute( CreateProjectCommand( - workspace_id=request.workspace_id, name=request.name, description=request.description, - ) + ), + owner_user_id=authenticated_user.user.id, ) return _to_project_response(project) diff --git a/apps/api/app/api/routes/task_center.py b/apps/api/app/api/routes/task_center.py index b20e6f94f..ab72eca14 100644 --- a/apps/api/app/api/routes/task_center.py +++ b/apps/api/app/api/routes/task_center.py @@ -1,13 +1,11 @@ from typing import Any -from app.api.routes.permissions import require_workspace_member from app.auth import AuthenticatedUser, get_current_user from app.core.celery_app import celery_app from app.dependencies import ( get_generation_task_repository, get_ingest_job_repository, get_project_repository, - get_workspace_member_repository, ) from app.schemas.task_center import ListProjectTasksResponse, ProjectTaskResponse from fastapi import APIRouter, Depends, HTTPException @@ -18,7 +16,6 @@ from packages.application import ( SubmitIngestJobCommand, SubmitIngestJobUseCase, ) -from packages.ports.workspace_member_repository import WorkspaceMemberRepository router = APIRouter() @@ -68,12 +65,10 @@ def list_project_tasks( project_repository: Any = Depends(get_project_repository), ingest_job_repository: Any = Depends(get_ingest_job_repository), generation_task_repository: Any = Depends(get_generation_task_repository), - workspace_member_repository: WorkspaceMemberRepository = Depends(get_workspace_member_repository), ) -> ListProjectTasksResponse: project = project_repository.find_by_id(project_id) if project is None: raise HTTPException(status_code=404, detail="Project not found") - require_workspace_member(project.workspace_id, authenticated_user, workspace_member_repository) items: list[ProjectTaskResponse] = [] for job in ingest_job_repository.list_by_project(project_id): @@ -81,7 +76,6 @@ def list_project_tasks( ProjectTaskResponse( id=f"ingest:{job.id}", task_type="ingest", - workspace_id=job.workspace_id, project_id=job.project_id, status=job.status.value, progress=100.0 if job.status.value == "completed" else 0.0, @@ -99,7 +93,6 @@ def list_project_tasks( ProjectTaskResponse( id=f"generation:{task.id}", task_type="generation", - workspace_id=task.workspace_id, project_id=task.project_id, status=task.status.value, progress=task.progress, @@ -123,19 +116,16 @@ def retry_project_task( authenticated_user: AuthenticatedUser = Depends(get_current_user), ingest_job_repository: Any = Depends(get_ingest_job_repository), generation_task_repository: Any = Depends(get_generation_task_repository), - workspace_member_repository: WorkspaceMemberRepository = Depends(get_workspace_member_repository), ) -> ProjectTaskResponse: if task_type == "generation": task = generation_task_repository.get(source_id) if task is None: raise HTTPException(status_code=404, detail="Generation task not found") - require_workspace_member(task.workspace_id, authenticated_user, workspace_member_repository) if task.status.value != "failed": raise HTTPException(status_code=409, detail="Only failed tasks can be retried") use_case = CreateGenerationTaskUseCase(generation_task_repository) retried = use_case.execute( CreateGenerationTaskCommand( - workspace_id=task.workspace_id, project_id=task.project_id, asset_library_id=task.asset_library_id, strategy_id=task.strategy_id, @@ -148,7 +138,6 @@ def retry_project_task( return ProjectTaskResponse( id=f"generation:{retried.id}", task_type="generation", - workspace_id=retried.workspace_id, project_id=retried.project_id, status=retried.status.value, progress=retried.progress, @@ -161,13 +150,11 @@ def retry_project_task( job = ingest_job_repository.get(source_id) if job is None: raise HTTPException(status_code=404, detail="Ingest job not found") - require_workspace_member(job.workspace_id, authenticated_user, workspace_member_repository) if job.status.value != "failed": raise HTTPException(status_code=409, detail="Only failed tasks can be retried") use_case = SubmitIngestJobUseCase(ingest_job_repository) retried = use_case.execute( SubmitIngestJobCommand( - workspace_id=job.workspace_id, project_id=job.project_id, library_id=job.library_id, storage_key=job.storage_key, @@ -177,7 +164,6 @@ def retry_project_task( return ProjectTaskResponse( id=f"ingest:{retried.id}", task_type="ingest", - workspace_id=retried.workspace_id, project_id=retried.project_id, status=retried.status.value, progress=0, diff --git a/apps/api/app/api/routes/upload.py b/apps/api/app/api/routes/upload.py index 8e071d730..8220b77a8 100644 --- a/apps/api/app/api/routes/upload.py +++ b/apps/api/app/api/routes/upload.py @@ -2,7 +2,6 @@ from typing import Any from uuid import uuid4 from app.auth import AuthenticatedUser, get_current_user -from app.api.routes.permissions import require_workspace_member from app.config import get_settings from app.core.celery_app import celery_app from app.core.storage import OSSStorageService, get_storage_service @@ -10,7 +9,6 @@ from app.dependencies import ( get_asset_library_repository, get_ingest_job_repository, get_project_repository, - get_workspace_member_repository, ) from app.schemas.upload import ( DirectUploadCompleteRequest, @@ -25,7 +23,6 @@ from typing import Annotated from fastapi import APIRouter, Depends, File, Form, HTTPException, UploadFile, status from packages.application import GetProjectUseCase, SubmitIngestJobCommand, SubmitIngestJobUseCase -from packages.ports.workspace_member_repository import WorkspaceMemberRepository router = APIRouter() @@ -64,23 +61,21 @@ def _validate_mime_type(content_type: str | None) -> str: def _require_project_and_library( - workspace_id: str, project_id: str, library_id: str, project_repository: Any, asset_library_repository: Any, ) -> None: project = GetProjectUseCase(project_repository).execute(project_id) - if project is None or project.workspace_id != workspace_id: + if project is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Project not found") libraries = asset_library_repository.list_by_project(project_id) - if not any(item.id == library_id and item.workspace_id == workspace_id for item in libraries): + if not any(item.id == library_id for item in libraries): raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Asset library not found") def _submit_ingest_job( - workspace_id: str, project_id: str, library_id: str, storage_key: str, @@ -89,7 +84,6 @@ def _submit_ingest_job( use_case = SubmitIngestJobUseCase(ingest_job_repository) job = use_case.execute( SubmitIngestJobCommand( - workspace_id=workspace_id, project_id=project_id, library_id=library_id, storage_key=storage_key, @@ -105,7 +99,6 @@ async def prepare_direct_upload( authenticated_user: AuthenticatedUser = Depends(get_current_user), project_repository: Any = Depends(get_project_repository), asset_library_repository: Any = Depends(get_asset_library_repository), - workspace_member_repository: WorkspaceMemberRepository = Depends(get_workspace_member_repository), storage_service: OSSStorageService = Depends(get_storage_service), ) -> DirectUploadPrepareResponse: """创建浏览器直传 OSS 的短期表单签名。""" @@ -119,10 +112,8 @@ async def prepare_direct_upload( # P2-5: 服务端验证 MIME 类型 validated_content_type = _validate_mime_type(request.content_type) - - require_workspace_member(request.workspace_id, authenticated_user, workspace_member_repository) + _require_project_and_library( - request.workspace_id, request.project_id, request.library_id, project_repository, @@ -159,13 +150,10 @@ async def complete_direct_upload( ingest_job_repository: Any = Depends(get_ingest_job_repository), project_repository: Any = Depends(get_project_repository), asset_library_repository: Any = Depends(get_asset_library_repository), - workspace_member_repository: WorkspaceMemberRepository = Depends(get_workspace_member_repository), storage_service: OSSStorageService = Depends(get_storage_service), ) -> DirectUploadCompleteResponse: """确认浏览器直传完成并创建导入任务。""" - require_workspace_member(request.workspace_id, authenticated_user, workspace_member_repository) _require_project_and_library( - request.workspace_id, request.project_id, request.library_id, project_repository, @@ -178,7 +166,6 @@ async def complete_direct_upload( raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Uploaded file not found") job = _submit_ingest_job( - workspace_id=request.workspace_id, project_id=request.project_id, library_id=request.library_id, storage_key=normalized_key, @@ -200,15 +187,12 @@ async def upload_asset( ingest_job_repository: Any = Depends(get_ingest_job_repository), project_repository: Any = Depends(get_project_repository), asset_library_repository: Any = Depends(get_asset_library_repository), - workspace_member_repository: WorkspaceMemberRepository = Depends(get_workspace_member_repository), storage_service: OSSStorageService = Depends(get_storage_service), ) -> UploadAssetResponse: """上传素材文件并触发导入流水线。""" - workspace_id = form_data.workspace_id project_id = form_data.project_id library_id = form_data.library_id - require_workspace_member(workspace_id, authenticated_user, workspace_member_repository) - _require_project_and_library(workspace_id, project_id, library_id, project_repository, asset_library_repository) + _require_project_and_library(project_id, library_id, project_repository, asset_library_repository) # P2-5: 服务端验证 MIME 类型 validated_content_type = _validate_mime_type(file.content_type) @@ -224,7 +208,6 @@ async def upload_asset( ) job = _submit_ingest_job( - workspace_id=workspace_id, project_id=project_id, library_id=library_id, storage_key=storage_key, diff --git a/apps/api/app/api/routes/workspaces.py b/apps/api/app/api/routes/workspaces.py deleted file mode 100644 index b8b917ef2..000000000 --- a/apps/api/app/api/routes/workspaces.py +++ /dev/null @@ -1,369 +0,0 @@ -""" -Workspace API routes 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. -""" - -from app.auth import AuthenticatedUser, get_current_user -from app.dependencies import ( - get_user_repository, - get_workspace_invitation_repository, - get_workspace_member_repository, - get_workspace_repository, -) -from fastapi import APIRouter, Depends, HTTPException, status -from pydantic import BaseModel, EmailStr - -from packages.application.workspace.accept_invitation_use_case import AcceptInvitationRequest, AcceptInvitationUseCase -from packages.application.workspace.create_workspace_use_case import CreateWorkspaceRequest, CreateWorkspaceUseCase -from packages.application.workspace.invite_member_use_case import InviteMemberRequest, InviteMemberUseCase -from packages.application.workspace.list_members_use_case import ListMembersRequest, ListMembersUseCase -from packages.application.workspace.list_workspaces_use_case import ( - GetWorkspaceDetailRequest, - GetWorkspaceDetailUseCase, - ListWorkspacesRequest, - ListWorkspacesUseCase, -) -from packages.application.workspace.remove_member_use_case import RemoveMemberRequest, RemoveMemberUseCase -from packages.application.workspace.update_member_role_use_case import UpdateMemberRoleRequest, UpdateMemberRoleUseCase -from packages.ports.user_repository import UserRepository -from packages.ports.workspace_invitation_repository import WorkspaceInvitationRepository -from packages.ports.workspace_member_repository import WorkspaceMemberRepository -from packages.ports.workspace_repository import WorkspaceRepository - -router = APIRouter(prefix="/workspaces", tags=["Workspaces"]) - - -class CreateWorkspaceRequestModel(BaseModel): - name: str - subscription_plan: str = "free" - - -class WorkspaceResponseModel(BaseModel): - workspace_id: str - name: str - subscription_plan: str - max_projects: int - max_storage_gb: int - - -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 WorkspaceListResponseModel(BaseModel): - workspaces: list[WorkspaceSummaryModel] - - -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 InviteMemberRequestModel(BaseModel): - email: EmailStr - role: str - - -class InviteMemberResponseModel(BaseModel): - invitation_id: str - invitee_email: str - role: str - expires_at: str - - -class MemberResponseModel(BaseModel): - member_id: str - user_id: str - username: str - email: str - display_name: str - role: str - invited_by: str | None - joined_at: str - - -class MemberListResponseModel(BaseModel): - members: list[MemberResponseModel] - - -class UpdateMemberRoleRequestModel(BaseModel): - role: str - - -class UpdateMemberRoleResponseModel(BaseModel): - user_id: str - old_role: str - new_role: str - - -class AcceptInvitationResponseModel(BaseModel): - workspace_id: str - workspace_name: str - role: str - - -class _NoopInvitationEmailService: - def send_workspace_invitation_email(self, **kwargs): - return False, "Email delivery is disabled for workspace compatibility routes" - - -@router.post("", response_model=WorkspaceResponseModel, status_code=status.HTTP_201_CREATED) -async def create_workspace( - request: CreateWorkspaceRequestModel, - 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), -): - use_case = CreateWorkspaceUseCase( - workspace_repository=workspace_repository, - workspace_member_repository=workspace_member_repository, - user_repository=user_repository, - ) - 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, - name=response.name, - subscription_plan=response.subscription_plan, - max_projects=response.max_projects, - max_storage_gb=response.max_storage_gb, - ) - - -@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") - - 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.post("/invitations/{invitation_token}/accept", response_model=AcceptInvitationResponseModel) -async def accept_invitation( - invitation_token: str, - authenticated_user: AuthenticatedUser = Depends(get_current_user), - workspace_repository: WorkspaceRepository = Depends(get_workspace_repository), - workspace_member_repository: WorkspaceMemberRepository = Depends(get_workspace_member_repository), - workspace_invitation_repository: WorkspaceInvitationRepository = Depends(get_workspace_invitation_repository), - user_repository: UserRepository = Depends(get_user_repository), -): - response, error = AcceptInvitationUseCase( - workspace_repository=workspace_repository, - workspace_member_repository=workspace_member_repository, - workspace_invitation_repository=workspace_invitation_repository, - user_repository=user_repository, - ).execute(AcceptInvitationRequest(invitation_token=invitation_token, 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 accept invitation") - - return AcceptInvitationResponseModel( - workspace_id=response.workspace_id, - workspace_name=response.workspace_name, - role=response.role, - ) - - -@router.get("/{workspace_id}", response_model=WorkspaceDetailResponseModel) -async def get_workspace_detail( - workspace_id: str, - 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 = GetWorkspaceDetailUseCase( - workspace_repository=workspace_repository, - workspace_member_repository=workspace_member_repository, - ) - 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") - - 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, - ) - - -@router.post( - "/{workspace_id}/members/invite", response_model=InviteMemberResponseModel, status_code=status.HTTP_201_CREATED -) -async def invite_member( - workspace_id: str, - request: InviteMemberRequestModel, - authenticated_user: AuthenticatedUser = Depends(get_current_user), - workspace_repository: WorkspaceRepository = Depends(get_workspace_repository), - workspace_member_repository: WorkspaceMemberRepository = Depends(get_workspace_member_repository), - workspace_invitation_repository: WorkspaceInvitationRepository = Depends(get_workspace_invitation_repository), - user_repository: UserRepository = Depends(get_user_repository), -): - use_case = InviteMemberUseCase( - workspace_repository=workspace_repository, - workspace_member_repository=workspace_member_repository, - workspace_invitation_repository=workspace_invitation_repository, - user_repository=user_repository, - email_service=_NoopInvitationEmailService(), - ) - response, error = use_case.execute( - InviteMemberRequest( - workspace_id=workspace_id, - inviter_user_id=authenticated_user.user.id, - invitee_email=request.email, - role=request.role, - ) - ) - if error or response is None: - raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=error or "Failed to invite member") - - return InviteMemberResponseModel( - invitation_id=response.invitation_id, - invitee_email=response.invitee_email, - role=response.role, - expires_at=response.expires_at.isoformat(), - ) - - -@router.get("/{workspace_id}/members", response_model=MemberListResponseModel) -async def list_members( - workspace_id: str, - 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), -): - use_case = ListMembersUseCase( - workspace_repository=workspace_repository, - workspace_member_repository=workspace_member_repository, - user_repository=user_repository, - ) - response, error = use_case.execute( - ListMembersRequest(workspace_id=workspace_id, requester_user_id=authenticated_user.user.id) - ) - if error or response is None: - raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=error or "Failed to list members") - - return MemberListResponseModel( - members=[ - MemberResponseModel( - member_id=member.member_id, - user_id=member.user_id, - username=member.username, - email=member.email, - display_name=member.display_name, - role=member.role, - invited_by=member.invited_by, - joined_at=member.joined_at.isoformat(), - ) - for member in response.members - ] - ) - - -@router.patch("/{workspace_id}/members/{user_id}/role", response_model=UpdateMemberRoleResponseModel) -async def update_member_role( - workspace_id: str, - user_id: str, - request: UpdateMemberRoleRequestModel, - 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 = UpdateMemberRoleUseCase( - workspace_repository=workspace_repository, - workspace_member_repository=workspace_member_repository, - ) - response, error = use_case.execute( - UpdateMemberRoleRequest( - workspace_id=workspace_id, - requester_user_id=authenticated_user.user.id, - target_user_id=user_id, - new_role=request.role, - ) - ) - if error or response is None: - raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=error or "Failed to update member role") - - return UpdateMemberRoleResponseModel( - user_id=response.user_id, - old_role=response.old_role, - new_role=response.new_role, - ) - - -@router.delete("/{workspace_id}/members/{user_id}", status_code=status.HTTP_204_NO_CONTENT) -async def remove_member( - workspace_id: str, - user_id: str, - authenticated_user: AuthenticatedUser = Depends(get_current_user), - workspace_repository: WorkspaceRepository = Depends(get_workspace_repository), - workspace_member_repository: WorkspaceMemberRepository = Depends(get_workspace_member_repository), -): - success, error = RemoveMemberUseCase( - workspace_repository=workspace_repository, - workspace_member_repository=workspace_member_repository, - ).execute( - RemoveMemberRequest( - workspace_id=workspace_id, - requester_user_id=authenticated_user.user.id, - target_user_id=user_id, - ) - ) - if not success: - raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=error or "Failed to remove member") diff --git a/apps/api/app/dependencies.py b/apps/api/app/dependencies.py index 4e8152928..000dab852 100644 --- a/apps/api/app/dependencies.py +++ b/apps/api/app/dependencies.py @@ -38,15 +38,9 @@ from packages.adapters.sqlalchemy_impl.project_title_repository import ( ) from packages.adapters.sqlalchemy_impl.session import build_session_factory from packages.adapters.sqlalchemy_impl.user_repository import SQLAlchemyUserRepository -from packages.adapters.sqlalchemy_impl.workspace_invitation_repository import SQLAlchemyWorkspaceInvitationRepository -from packages.adapters.sqlalchemy_impl.workspace_member_repository import SQLAlchemyWorkspaceMemberRepository -from packages.adapters.sqlalchemy_impl.workspace_repository import SQLAlchemyWorkspaceRepository from packages.ports.asset_repository import AssetRepository from packages.ports.asset_library_repository import AssetLibraryRepository from packages.ports.user_repository import UserRepository -from packages.ports.workspace_invitation_repository import WorkspaceInvitationRepository -from packages.ports.workspace_member_repository import WorkspaceMemberRepository -from packages.ports.workspace_repository import WorkspaceRepository from packages.ports.classification_job_repository import ClassificationJobRepository from packages.ports.generation_task_repository import GenerationTaskRepository from packages.ports.generated_video_repository import GeneratedVideoRepository @@ -129,27 +123,6 @@ def get_user_repository( return SQLAlchemyUserRepository(session) -def get_workspace_repository( - session: Session = Depends(get_db_session), -) -> WorkspaceRepository: - """Provide the SQLAlchemy workspace repository implementation.""" - return SQLAlchemyWorkspaceRepository(session) - - -def get_workspace_member_repository( - session: Session = Depends(get_db_session), -) -> WorkspaceMemberRepository: - """Provide the SQLAlchemy workspace member repository implementation.""" - return SQLAlchemyWorkspaceMemberRepository(session) - - -def get_workspace_invitation_repository( - session: Session = Depends(get_db_session), -) -> WorkspaceInvitationRepository: - """Provide the SQLAlchemy workspace invitation repository implementation.""" - return SQLAlchemyWorkspaceInvitationRepository(session) - - def get_auth_session_store() -> SessionStore | NoopSessionStore: """Provide the session store based on configuration.""" if not settings.ENABLE_REDIS_SESSIONS: diff --git a/apps/api/app/middleware/auth.py b/apps/api/app/middleware/auth.py index c6ca26c46..5df1b3ace 100644 --- a/apps/api/app/middleware/auth.py +++ b/apps/api/app/middleware/auth.py @@ -9,14 +9,12 @@ from __future__ import annotations from app.auth import AuthenticatedUser from app.auth import get_current_user as get_authenticated_user -from app.dependencies import get_user_repository, get_workspace_member_repository -from fastapi import Depends, HTTPException, status +from app.dependencies import get_user_repository +from fastapi import Depends from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer from packages.domain.entities import User -from packages.domain.permissions import PermissionChecker from packages.ports.user_repository import UserRepository -from packages.ports.workspace_member_repository import WorkspaceMemberRepository optional_bearer_scheme = HTTPBearer(auto_error=False) @@ -38,39 +36,3 @@ async def get_current_user_optional( except HTTPException: return None return authenticated_user.user - - -def require_workspace_access( - workspace_id: str, - user: User = Depends(get_current_user), - workspace_member_repository: WorkspaceMemberRepository = Depends(get_workspace_member_repository), -) -> tuple[str, str]: - has_access, role = PermissionChecker(workspace_member_repository).check_workspace_access(workspace_id, user.id) - if not has_access or role is None: - raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="You don't have access to this workspace") - return workspace_id, role - - -def require_workspace_admin( - workspace_id: str, - user: User = Depends(get_current_user), - workspace_member_repository: WorkspaceMemberRepository = Depends(get_workspace_member_repository), -) -> str: - if not PermissionChecker(workspace_member_repository).check_is_admin_or_owner(workspace_id, user.id): - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, - detail="Only workspace owner or admin can perform this action", - ) - return workspace_id - - -def require_workspace_owner( - workspace_id: str, - user: User = Depends(get_current_user), - workspace_member_repository: WorkspaceMemberRepository = Depends(get_workspace_member_repository), -) -> str: - if not PermissionChecker(workspace_member_repository).check_is_owner(workspace_id, user.id): - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, detail="Only workspace owner can perform this action" - ) - return workspace_id diff --git a/apps/api/app/schemas/upload.py b/apps/api/app/schemas/upload.py index 86cb25599..97d442a01 100644 --- a/apps/api/app/schemas/upload.py +++ b/apps/api/app/schemas/upload.py @@ -6,7 +6,6 @@ class UploadAssetRequest(BaseModel): """素材上传请求(multipart form)""" project_id: str = Field(..., min_length=1, description="项目 ID") library_id: str = Field(..., min_length=1, description="素材库 ID") - workspace_id: str = Field(default="", description="工作空间 ID(已废弃,可传空)") class UploadAssetResponse(BaseModel): storage_key: str diff --git a/apps/worker/tasks.py b/apps/worker/tasks.py index 0cf3f993b..5cabe037c 100644 --- a/apps/worker/tasks.py +++ b/apps/worker/tasks.py @@ -93,8 +93,8 @@ def generate_video(task_id: str) -> dict: session.commit() # 6. 上传到 OSS - storage_key = f"workspaces/{task.workspace_id}/projects/{task.project_id}/generated/{task.id}/{output_filename}" - thumbnail_key = f"workspaces/{task.workspace_id}/projects/{task.project_id}/generated/{task.id}/thumbnail.jpg" + storage_key = f"projects/{task.project_id}/generated/{task.id}/{output_filename}" + thumbnail_key = f"projects/{task.project_id}/generated/{task.id}/thumbnail.jpg" storage_service.upload_file(result.output_path, storage_key) storage_service.upload_file(result.thumbnail_path, thumbnail_key) @@ -108,7 +108,6 @@ def generate_video(task_id: str) -> dict: # 7. 创建 GeneratedVideo 记录 video = GeneratedVideo.create( - workspace_id=task.workspace_id, project_id=task.project_id, generation_task_id=task.id, name=output_filename, diff --git a/apps/worker/video_processing/dedup.py b/apps/worker/video_processing/dedup.py index 7468e0164..958a71752 100644 --- a/apps/worker/video_processing/dedup.py +++ b/apps/worker/video_processing/dedup.py @@ -154,7 +154,7 @@ def check_duplicate_task(self: Task, generated_video_id: str) -> dict: local_path = os.path.join(temp_dir, f"{generated_video_id}.mp4") storage_key = video.file_url.split("/")[-1] - storage_service.download_file(f"workspaces/{video.workspace_id}/projects/{video.project_id}/generated/{generated_video_id}/{generated_video_id}.mp4", local_path) + storage_service.download_file(f"projects/{video.project_id}/generated/{generated_video_id}/{generated_video_id}.mp4", local_path) fingerprint = deduplicator.compute_fingerprint(local_path) diff --git a/apps/worker/worker_app/core/title_usage.py b/apps/worker/worker_app/core/title_usage.py index a8644bbbe..4c075a0f8 100644 --- a/apps/worker/worker_app/core/title_usage.py +++ b/apps/worker/worker_app/core/title_usage.py @@ -7,7 +7,7 @@ def mark_title_used_for_generation(db, task) -> None: if not task.strategy_id: return title = db.query(ProjectTitleModel).filter(ProjectTitleModel.id == task.strategy_id).first() - if title is None or title.workspace_id != task.workspace_id or title.project_id != task.project_id: + if title is None or title.project_id != task.project_id: return title.usage_count = int(title.usage_count or 0) + 1 title.updated_at = datetime.now(timezone.utc) diff --git a/apps/worker/worker_app/tasks/generation.py b/apps/worker/worker_app/tasks/generation.py index f8cd1a5a4..7dd822a2a 100755 --- a/apps/worker/worker_app/tasks/generation.py +++ b/apps/worker/worker_app/tasks/generation.py @@ -240,7 +240,6 @@ def generate_video(self, task_id: str) -> dict: gen_task = task_repo.get(task_id) if gen_task is None: return {"status": "failed", "error": f"generation task {task_id} not found"} - workspace_id = gen_task.workspace_id project_id = gen_task.project_id asset_library_id = gen_task.asset_library_id voice_library_id = gen_task.voice_library_id or "" @@ -254,7 +253,7 @@ def generate_video(self, task_id: str) -> dict: editing_mode = GenerationMode.ONE_TAKE output_name = f"generated-{task_id}.mp4" - storage_key = f"generated/workspaces/{workspace_id}/projects/{project_id}/tasks/{task_id}/{output_name}" + storage_key = f"generated/projects/{project_id}/tasks/{task_id}/{output_name}" try: with tempfile.TemporaryDirectory(prefix="xiaoxia-generation-") as temp_dir: diff --git a/apps/worker/worker_app/tasks/ingest.py b/apps/worker/worker_app/tasks/ingest.py index 36e441db3..7618c2686 100755 --- a/apps/worker/worker_app/tasks/ingest.py +++ b/apps/worker/worker_app/tasks/ingest.py @@ -151,7 +151,6 @@ def ingest_asset(job_id: str) -> dict: # Create Asset asset = Asset.create( - workspace_id=job.workspace_id, project_id=job.project_id, library_id=job.library_id, name=filename, diff --git a/packages/adapters/in_memory/workspace_invitation_repository.py b/packages/adapters/in_memory/workspace_invitation_repository.py deleted file mode 100644 index c94433066..000000000 --- a/packages/adapters/in_memory/workspace_invitation_repository.py +++ /dev/null @@ -1,69 +0,0 @@ -""" -WorkspaceInvitation 仓储 In-Memory 实现 -""" - -from typing import Dict, Optional - -from packages.domain.entities import InvitationStatus, WorkspaceInvitation -from packages.ports.workspace_invitation_repository import WorkspaceInvitationRepository - - -class InMemoryWorkspaceInvitationRepository(WorkspaceInvitationRepository): - """WorkspaceInvitation 仓储内存实现""" - - def __init__(self): - self._invitations: Dict[str, WorkspaceInvitation] = {} - self._token_index: Dict[str, str] = {} # token -> invitation_id - self._workspace_email_index: Dict[tuple[str, str], str] = {} # (workspace_id, email) -> invitation_id - - def save(self, invitation: WorkspaceInvitation) -> None: - """保存邀请""" - self._invitations[invitation.id] = invitation - self._token_index[invitation.invitation_token] = invitation.id - - # 只为 pending 状态的邀请建立索引 - if invitation.status == InvitationStatus.PENDING: - key = (invitation.workspace_id, invitation.invitee_email.lower()) - self._workspace_email_index[key] = invitation.id - else: - # 如果状态改变,清理索引 - key = (invitation.workspace_id, invitation.invitee_email.lower()) - self._workspace_email_index.pop(key, None) - - def find_by_id(self, invitation_id: str) -> Optional[WorkspaceInvitation]: - """根据 ID 查找邀请""" - return self._invitations.get(invitation_id) - - def find_by_token(self, token: str) -> Optional[WorkspaceInvitation]: - """根据令牌查找邀请""" - invitation_id = self._token_index.get(token) - if invitation_id: - return self._invitations.get(invitation_id) - return None - - def find_pending_by_workspace_and_email( - self, - workspace_id: str, - email: str, - ) -> Optional[WorkspaceInvitation]: - """查找 workspace 和邮箱的待处理邀请""" - key = (workspace_id, email.lower()) - invitation_id = self._workspace_email_index.get(key) - if invitation_id: - return self._invitations.get(invitation_id) - return None - - def delete(self, invitation_id: str) -> bool: - """删除邀请""" - invitation = self._invitations.get(invitation_id) - if not invitation: - return False - - # 清理索引 - self._token_index.pop(invitation.invitation_token, None) - key = (invitation.workspace_id, invitation.invitee_email.lower()) - self._workspace_email_index.pop(key, None) - - # 删除邀请 - del self._invitations[invitation_id] - return True diff --git a/packages/adapters/in_memory/workspace_member_repository.py b/packages/adapters/in_memory/workspace_member_repository.py deleted file mode 100644 index a0f83a161..000000000 --- a/packages/adapters/in_memory/workspace_member_repository.py +++ /dev/null @@ -1,86 +0,0 @@ -""" -WorkspaceMember 仓储 In-Memory 实现 -""" - -from typing import Dict, List, Optional - -from packages.domain.entities import WorkspaceMember -from packages.ports.workspace_member_repository import WorkspaceMemberRepository - - -class InMemoryWorkspaceMemberRepository(WorkspaceMemberRepository): - """WorkspaceMember 仓储内存实现""" - - def __init__(self): - self._members: Dict[str, WorkspaceMember] = {} - self._workspace_user_index: Dict[tuple[str, str], str] = {} # (workspace_id, user_id) -> member_id - self._user_index: Dict[str, List[str]] = {} # user_id -> [member_ids] - self._workspace_index: Dict[str, List[str]] = {} # workspace_id -> [member_ids] - - def save(self, member: WorkspaceMember) -> None: - """保存成员""" - self._members[member.id] = member - - # 更新索引 - key = (member.workspace_id, member.user_id) - self._workspace_user_index[key] = member.id - - if member.user_id not in self._user_index: - self._user_index[member.user_id] = [] - if member.id not in self._user_index[member.user_id]: - self._user_index[member.user_id].append(member.id) - - if member.workspace_id not in self._workspace_index: - self._workspace_index[member.workspace_id] = [] - if member.id not in self._workspace_index[member.workspace_id]: - self._workspace_index[member.workspace_id].append(member.id) - - def find_by_id(self, member_id: str) -> Optional[WorkspaceMember]: - """根据 ID 查找成员""" - return self._members.get(member_id) - - def find_by_workspace_and_user( - self, - workspace_id: str, - user_id: str, - ) -> Optional[WorkspaceMember]: - """根据 workspace 和 user 查找成员""" - key = (workspace_id, user_id) - member_id = self._workspace_user_index.get(key) - if member_id: - return self._members.get(member_id) - return None - - def find_by_user(self, user_id: str) -> List[WorkspaceMember]: - """查找用户的所有成员记录""" - member_ids = self._user_index.get(user_id, []) - return [self._members[mid] for mid in member_ids if mid in self._members] - - def find_by_workspace(self, workspace_id: str) -> List[WorkspaceMember]: - """查找 workspace 的所有成员""" - member_ids = self._workspace_index.get(workspace_id, []) - return [self._members[mid] for mid in member_ids if mid in self._members] - - def count_by_workspace(self, workspace_id: str) -> int: - """统计 workspace 的成员数量""" - return len(self._workspace_index.get(workspace_id, [])) - - def delete(self, member_id: str) -> bool: - """删除成员""" - member = self._members.get(member_id) - if not member: - return False - - # 清理索引 - key = (member.workspace_id, member.user_id) - self._workspace_user_index.pop(key, None) - - if member.user_id in self._user_index: - self._user_index[member.user_id].remove(member_id) - - if member.workspace_id in self._workspace_index: - self._workspace_index[member.workspace_id].remove(member_id) - - # 删除成员 - del self._members[member_id] - return True diff --git a/packages/adapters/in_memory/workspace_repository.py b/packages/adapters/in_memory/workspace_repository.py deleted file mode 100644 index 3c3f6606e..000000000 --- a/packages/adapters/in_memory/workspace_repository.py +++ /dev/null @@ -1,30 +0,0 @@ -""" -Workspace 仓储 In-Memory 实现 -""" - -from typing import Dict, Optional - -from packages.domain.entities import Workspace -from packages.ports.workspace_repository import WorkspaceRepository - - -class InMemoryWorkspaceRepository(WorkspaceRepository): - """Workspace 仓储内存实现""" - - def __init__(self): - self._workspaces: Dict[str, Workspace] = {} - - def save(self, workspace: Workspace) -> None: - """保存 Workspace""" - self._workspaces[workspace.id] = workspace - - def find_by_id(self, workspace_id: str) -> Optional[Workspace]: - """根据 ID 查找 Workspace""" - return self._workspaces.get(workspace_id) - - def delete(self, workspace_id: str) -> bool: - """删除 Workspace""" - if workspace_id in self._workspaces: - del self._workspaces[workspace_id] - return True - return False diff --git a/packages/adapters/sqlalchemy_impl/project_management_repositories.py b/packages/adapters/sqlalchemy_impl/project_management_repositories.py index 0407b9eee..4dbe3cebc 100644 --- a/packages/adapters/sqlalchemy_impl/project_management_repositories.py +++ b/packages/adapters/sqlalchemy_impl/project_management_repositories.py @@ -24,7 +24,6 @@ class SQLAlchemyTaskRepository(TaskRepository): model = TaskModel( id=task.id, project_id=task.project_id, - workspace_id=task.workspace_id, name=task.name, description=task.description, status=task.status.value, @@ -90,7 +89,6 @@ class SQLAlchemyTaskRepository(TaskRepository): return Task( id=model.id, project_id=model.project_id, - workspace_id=model.workspace_id, name=model.name, description=model.description, status=TaskStatus(model.status), @@ -118,7 +116,6 @@ class SQLAlchemyMilestoneRepository(MilestoneRepository): model = MilestoneModel( id=milestone.id, project_id=milestone.project_id, - workspace_id=milestone.workspace_id, name=milestone.name, description=milestone.description, target_date=milestone.target_date, @@ -164,7 +161,6 @@ class SQLAlchemyMilestoneRepository(MilestoneRepository): return Milestone( id=model.id, project_id=model.project_id, - workspace_id=model.workspace_id, name=model.name, description=model.description, target_date=model.target_date, @@ -186,7 +182,6 @@ class SQLAlchemyTaskIssueRepository(TaskIssueRepository): id=issue.id, task_id=issue.task_id, project_id=issue.project_id, - workspace_id=issue.workspace_id, title=issue.title, description=issue.description, resolved=issue.resolved, @@ -236,7 +231,6 @@ class SQLAlchemyTaskIssueRepository(TaskIssueRepository): id=model.id, task_id=model.task_id, project_id=model.project_id, - workspace_id=model.workspace_id, title=model.title, description=model.description, resolved=model.resolved, diff --git a/packages/adapters/sqlalchemy_impl/project_title_repository.py b/packages/adapters/sqlalchemy_impl/project_title_repository.py index 9980c7848..2e677072c 100644 --- a/packages/adapters/sqlalchemy_impl/project_title_repository.py +++ b/packages/adapters/sqlalchemy_impl/project_title_repository.py @@ -20,7 +20,6 @@ class SQLAlchemyProjectTitleRepository: def create( self, *, - workspace_id: str, project_id: str, text: str, category: str, @@ -30,7 +29,6 @@ class SQLAlchemyProjectTitleRepository: now = datetime.now(timezone.utc) item = ProjectTitleModel( id=uuid4().hex, - workspace_id=workspace_id, project_id=project_id, text=text.strip(), category=category, diff --git a/packages/adapters/sqlalchemy_impl/workspace_invitation_repository.py b/packages/adapters/sqlalchemy_impl/workspace_invitation_repository.py deleted file mode 100644 index a9e9e7fc9..000000000 --- a/packages/adapters/sqlalchemy_impl/workspace_invitation_repository.py +++ /dev/null @@ -1,5 +0,0 @@ -"""Stub adapter for workspace invitation repository - workspace concept removed.""" - -class SQLAlchemyWorkspaceInvitationRepository: - def __init__(self, session): - self.session = session diff --git a/packages/adapters/sqlalchemy_impl/workspace_member_repository.py b/packages/adapters/sqlalchemy_impl/workspace_member_repository.py deleted file mode 100644 index 74cf0fe90..000000000 --- a/packages/adapters/sqlalchemy_impl/workspace_member_repository.py +++ /dev/null @@ -1,23 +0,0 @@ -from __future__ import annotations -from typing import TYPE_CHECKING -if TYPE_CHECKING: - from sqlalchemy.orm import Session -from packages.ports.workspace_member_repository import WorkspaceMemberRepository - -class SQLAlchemyWorkspaceMemberRepository(WorkspaceMemberRepository): - def __init__(self, session=None): - self.session = session - def save(self, member) -> None: - raise NotImplementedError("Workspace feature removed") - def find_by_id(self, member_id: str): - return None - def find_by_workspace_and_user(self, workspace_id: str, user_id: str): - return None - def find_by_user(self, user_id: str): - return [] - def find_by_workspace(self, workspace_id: str): - return [] - def count_by_workspace(self, workspace_id: str) -> int: - return 0 - def delete(self, member_id: str) -> bool: - return False diff --git a/packages/adapters/sqlalchemy_impl/workspace_repository.py b/packages/adapters/sqlalchemy_impl/workspace_repository.py deleted file mode 100644 index 956220cb4..000000000 --- a/packages/adapters/sqlalchemy_impl/workspace_repository.py +++ /dev/null @@ -1,5 +0,0 @@ -"""Stub adapter for workspace repository - workspace concept removed.""" - -class SQLAlchemyWorkspaceRepository: - def __init__(self, session): - self.session = session diff --git a/packages/adapters/sqlite_tracker/project_management_repositories.py b/packages/adapters/sqlite_tracker/project_management_repositories.py index ff4214d67..dc920320f 100644 --- a/packages/adapters/sqlite_tracker/project_management_repositories.py +++ b/packages/adapters/sqlite_tracker/project_management_repositories.py @@ -38,7 +38,6 @@ class SQLiteTaskRepository: priority=(TaskPriority(row["priority"]) if row["priority"] else TaskPriority.MEDIUM), progress=0, # tracker.db 没有 progress 字段 project_id=row["phase"] or "xiaoxia-saas", - workspace_id="xiaoxia-workspace", assignee_user_id=row["assigned_to"] or "", created_at=(datetime.fromisoformat(row["created_at"]) if row["created_at"] else datetime.now()), updated_at=datetime.now(), @@ -73,7 +72,6 @@ class SQLiteTaskRepository: priority=(TaskPriority(row["priority"]) if row["priority"] else TaskPriority.MEDIUM), progress=0, project_id=row["phase"] or "xiaoxia-saas", - workspace_id="xiaoxia-workspace", assignee_user_id=row["assigned_to"] or "", created_at=(datetime.fromisoformat(row["created_at"]) if row["created_at"] else datetime.now()), updated_at=datetime.now(), @@ -148,7 +146,6 @@ class SQLiteMilestoneRepository: description=row["description"] or "", target_date=row["end_date"] or "", project_id=row["phase"] or "xiaoxia-saas", - workspace_id="xiaoxia-workspace", created_at=(datetime.fromisoformat(row["start_date"]) if row["start_date"] else datetime.now()), ) ) diff --git a/packages/application/auth/jwt_handler.py b/packages/application/auth/jwt_handler.py index 080ba39de..68d185949 100644 --- a/packages/application/auth/jwt_handler.py +++ b/packages/application/auth/jwt_handler.py @@ -8,7 +8,7 @@ JWT 处理器委托层 from packages.application.auth.jwt_handler import JWTHandler, get_jwt_handler jwt_handler = JWTHandler(secret_key="") - token = jwt_handler.create_access_token(user_id="user123", workspace_id="ws456", role="admin") + token = jwt_handler.create_access_token(user_id="user123", role="admin") payload = jwt_handler.verify_access_token(token) """ @@ -45,25 +45,22 @@ class JWTHandler: def create_access_token( self, user_id: str, - workspace_id: str, - role: str, + role: str = "", additional_claims: Optional[Dict[str, Any]] = None, ) -> str: """ 创建 access_token - + Args: user_id: 用户 ID - workspace_id: 工作空间 ID role: 用户角色 additional_claims: 额外的声明信息 - + Returns: JWT Token 字符串 """ return self._service.create_access_token( user_id=user_id, - workspace_id=workspace_id, role=role, additional_claims=additional_claims, ) diff --git a/packages/application/auth/jwt_service.py b/packages/application/auth/jwt_service.py index a1b996986..082f11ad2 100644 --- a/packages/application/auth/jwt_service.py +++ b/packages/application/auth/jwt_service.py @@ -75,8 +75,7 @@ class JWTService: def create_access_token( self, user_id: str, - workspace_id: str, - role: str, + role: str = "", additional_claims: Optional[Dict[str, Any]] = None, ) -> str: """ @@ -84,7 +83,6 @@ class JWTService: Args: user_id: 用户 ID - workspace_id: 工作空间 ID role: 用户角色(admin/user/guest) additional_claims: 额外的声明信息 @@ -98,7 +96,6 @@ class JWTService: payload = { "sub": user_id, # subject (用户ID) - "workspace_id": workspace_id, "role": role, "type": TokenType.ACCESS, "iat": now, # issued at diff --git a/packages/application/project_management_use_cases.py b/packages/application/project_management_use_cases.py index 64dcf05cc..b97a7dea4 100644 --- a/packages/application/project_management_use_cases.py +++ b/packages/application/project_management_use_cases.py @@ -13,7 +13,6 @@ class CreateTaskUseCase: def execute( self, project_id: str, - workspace_id: str, name: str, description: str = "", priority: TaskPriority = TaskPriority.MEDIUM, @@ -22,7 +21,6 @@ class CreateTaskUseCase: ) -> Task: task = Task.create( project_id=project_id, - workspace_id=workspace_id, name=name, description=description, priority=priority, @@ -79,13 +77,11 @@ class CreateMilestoneUseCase: def execute( self, project_id: str, - workspace_id: str, name: str, description: str = "", ) -> Milestone: milestone = Milestone.create( project_id=project_id, - workspace_id=workspace_id, name=name, description=description, ) @@ -112,7 +108,6 @@ class CreateTaskIssueUseCase: self, task_id: str, project_id: str, - workspace_id: str, title: str, description: str = "", created_by_user_id: str = "", @@ -120,7 +115,6 @@ class CreateTaskIssueUseCase: issue = TaskIssue.create( task_id=task_id, project_id=project_id, - workspace_id=workspace_id, title=title, description=description, created_by_user_id=created_by_user_id, diff --git a/packages/application/workspace/__init__.py b/packages/application/workspace/__init__.py deleted file mode 100644 index 165ad98bd..000000000 --- a/packages/application/workspace/__init__.py +++ /dev/null @@ -1,89 +0,0 @@ -"""Workspace 相关 Use Cases""" - -from packages.application.workspace.accept_invitation_use_case import ( - AcceptInvitationRequest, - AcceptInvitationResponse, - AcceptInvitationUseCase, - DeclineInvitationRequest, - DeclineInvitationUseCase, -) -from packages.application.workspace.create_workspace_use_case import ( - CreateWorkspaceRequest, - CreateWorkspaceResponse, - CreateWorkspaceUseCase, -) -from packages.application.workspace.invite_member_use_case import ( - InviteMemberRequest, - InviteMemberResponse, - InviteMemberUseCase, -) -from packages.application.workspace.list_members_use_case import ( - ListMembersRequest, - ListMembersResponse, - ListMembersUseCase, - MemberInfo, -) -from packages.application.workspace.list_workspaces_use_case import ( - GetWorkspaceDetailRequest, - GetWorkspaceDetailUseCase, - ListWorkspacesRequest, - ListWorkspacesResponse, - ListWorkspacesUseCase, - WorkspaceDetailInfo, - WorkspaceInfo, -) -from packages.application.workspace.remove_member_use_case import ( - LeaveWorkspaceRequest, - LeaveWorkspaceUseCase, - RemoveMemberRequest, - RemoveMemberUseCase, -) -from packages.application.workspace.subscription_use_case import ( - CancelSubscriptionRequest, - CancelSubscriptionUseCase, - UpgradeSubscriptionRequest, - UpgradeSubscriptionResponse, - UpgradeSubscriptionUseCase, -) -from packages.application.workspace.update_member_role_use_case import ( - UpdateMemberRoleRequest, - UpdateMemberRoleResponse, - UpdateMemberRoleUseCase, -) - -__all__ = [ - "CreateWorkspaceUseCase", - "CreateWorkspaceRequest", - "CreateWorkspaceResponse", - "InviteMemberUseCase", - "InviteMemberRequest", - "InviteMemberResponse", - "AcceptInvitationUseCase", - "AcceptInvitationRequest", - "AcceptInvitationResponse", - "DeclineInvitationUseCase", - "DeclineInvitationRequest", - "RemoveMemberUseCase", - "RemoveMemberRequest", - "LeaveWorkspaceUseCase", - "LeaveWorkspaceRequest", - "UpdateMemberRoleUseCase", - "UpdateMemberRoleRequest", - "UpdateMemberRoleResponse", - "ListWorkspacesUseCase", - "ListWorkspacesRequest", - "ListWorkspacesResponse", - "GetWorkspaceDetailUseCase", - "GetWorkspaceDetailRequest", - "WorkspaceInfo", - "WorkspaceDetailInfo", - "ListMembersUseCase", - "ListMembersRequest", - "ListMembersResponse", - "MemberInfo", - "UpgradeSubscriptionUseCase", - "UpgradeSubscriptionRequest", - "UpgradeSubscriptionResponse", - "CancelSubscriptionUseCase", - "CancelSubscriptionRequest", -] diff --git a/packages/application/workspace/accept_invitation_use_case.py b/packages/application/workspace/accept_invitation_use_case.py deleted file mode 100644 index b2ab9ad3a..000000000 --- a/packages/application/workspace/accept_invitation_use_case.py +++ /dev/null @@ -1,195 +0,0 @@ -""" -接受/拒绝邀请 Use Case -""" - -from datetime import datetime, timezone -from typing import Optional -from uuid import uuid4 - -from packages.domain.entities import InvitationStatus, WorkspaceMember - - -class AcceptInvitationRequest: - """接受邀请请求""" - - def __init__(self, invitation_token: str, user_id: str): - self.invitation_token = invitation_token - self.user_id = user_id - - -class AcceptInvitationResponse: - """接受邀请响应""" - - def __init__( - self, - workspace_id: str, - workspace_name: str, - role: str, - ): - self.workspace_id = workspace_id - self.workspace_name = workspace_name - self.role = role - - -class AcceptInvitationUseCase: - """接受邀请用例""" - - def __init__( - self, - workspace_repository, - workspace_member_repository, - workspace_invitation_repository, - user_repository, - ): - self.workspace_repository = workspace_repository - self.workspace_member_repository = workspace_member_repository - self.workspace_invitation_repository = workspace_invitation_repository - self.user_repository = user_repository - - def execute(self, request: AcceptInvitationRequest) -> tuple[Optional[AcceptInvitationResponse], Optional[str]]: - """ - 执行接受邀请 - - Args: - request: 接受请求 - - Returns: - (响应, 错误信息) - """ - try: - # 1. 验证输入 - if not request.invitation_token: - return None, "Invitation token is required" - - if not request.user_id: - return None, "User ID is required" - - # 2. 查找邀请 - invitation = self.workspace_invitation_repository.find_by_token(request.invitation_token) - if not invitation: - return None, "Invalid invitation token" - - # 3. 检查邀请状态 - if invitation.status != InvitationStatus.PENDING: - return None, f"Invitation has already been {invitation.status}" - - # 4. 检查是否过期 - expires_at = invitation.expires_at - if expires_at and expires_at.tzinfo is None: - expires_at = expires_at.replace(tzinfo=timezone.utc) - if expires_at and datetime.now(timezone.utc) > expires_at: - # 更新状态为过期 - invitation.status = InvitationStatus.EXPIRED - self.workspace_invitation_repository.save(invitation) - return None, "Invitation has expired" - - # 5. 验证用户存在 - user = self.user_repository.find_by_id(request.user_id) - if not user: - return None, "User not found" - - # 6. 验证用户邮箱匹配 - if user.email.lower() != invitation.invitee_email.lower(): - return None, "This invitation is for a different email address" - - # 7. 验证 Workspace 存在 - workspace = self.workspace_repository.find_by_id(invitation.workspace_id) - if not workspace: - return None, "Workspace not found" - - # 8. 检查用户是否已经是成员 - existing_member = self.workspace_member_repository.find_by_workspace_and_user( - invitation.workspace_id, - request.user_id, - ) - if existing_member: - # 已经是成员,标记邀请为已接受 - invitation.status = InvitationStatus.ACCEPTED - invitation.accepted_at = datetime.now(timezone.utc) - self.workspace_invitation_repository.save(invitation) - - return ( - AcceptInvitationResponse( - workspace_id=workspace.id, - workspace_name=workspace.name, - role=existing_member.role, - ), - None, - ) - - # 9. 创建成员记录 - member = WorkspaceMember( - id=uuid4().hex, - workspace_id=invitation.workspace_id, - user_id=request.user_id, - role=invitation.role, - invited_by=invitation.inviter_user_id, - joined_at=datetime.now(timezone.utc), - ) - - self.workspace_member_repository.save(member) - - # 10. 更新邀请状态 - invitation.status = InvitationStatus.ACCEPTED - invitation.accepted_at = datetime.now(timezone.utc) - self.workspace_invitation_repository.save(invitation) - - # 11. 返回响应 - return ( - AcceptInvitationResponse( - workspace_id=workspace.id, - workspace_name=workspace.name, - role=member.role, - ), - None, - ) - - except Exception as e: - return None, f"Failed to accept invitation: {str(e)}" - - -class DeclineInvitationRequest: - """拒绝邀请请求""" - - def __init__(self, invitation_token: str): - self.invitation_token = invitation_token - - -class DeclineInvitationUseCase: - """拒绝邀请用例""" - - def __init__(self, workspace_invitation_repository): - self.workspace_invitation_repository = workspace_invitation_repository - - def execute(self, request: DeclineInvitationRequest) -> tuple[bool, Optional[str]]: - """ - 执行拒绝邀请 - - Args: - request: 拒绝请求 - - Returns: - (是否成功, 错误信息) - """ - try: - # 1. 验证输入 - if not request.invitation_token: - return False, "Invitation token is required" - - # 2. 查找邀请 - invitation = self.workspace_invitation_repository.find_by_token(request.invitation_token) - if not invitation: - return False, "Invalid invitation token" - - # 3. 检查邀请状态 - if invitation.status != InvitationStatus.PENDING: - return False, f"Invitation has already been {invitation.status}" - - # 4. 更新状态为已拒绝 - invitation.status = InvitationStatus.DECLINED - self.workspace_invitation_repository.save(invitation) - - return True, None - - except Exception as e: - return False, f"Failed to decline invitation: {str(e)}" diff --git a/packages/application/workspace/create_workspace_use_case.py b/packages/application/workspace/create_workspace_use_case.py deleted file mode 100644 index 1a87fca8e..000000000 --- a/packages/application/workspace/create_workspace_use_case.py +++ /dev/null @@ -1,138 +0,0 @@ -""" -创建 Workspace Use Case -""" - -from datetime import datetime, timezone -from typing import Optional -from uuid import uuid4 - -from packages.domain.entities import Workspace, WorkspaceMember, WorkspaceMemberRole - - -class CreateWorkspaceRequest: - """创建工作空间请求""" - - def __init__( - self, - name: str, - owner_user_id: str, - subscription_plan: str = "free", - ): - self.name = name.strip() - self.owner_user_id = owner_user_id - self.subscription_plan = subscription_plan - - -class CreateWorkspaceResponse: - """创建工作空间响应""" - - def __init__( - self, - workspace_id: str, - name: str, - subscription_plan: str, - max_projects: int, - max_storage_gb: int, - ): - self.workspace_id = workspace_id - self.name = name - self.subscription_plan = subscription_plan - self.max_projects = max_projects - self.max_storage_gb = max_storage_gb - - -class CreateWorkspaceUseCase: - """创建工作空间用例""" - - # 订阅计划配额配置 - PLAN_QUOTAS = { - "free": {"max_projects": 3, "max_storage_gb": 10}, - "pro": {"max_projects": 999999, "max_storage_gb": 100}, # 999999 表示无限 - "enterprise": {"max_projects": 999999, "max_storage_gb": 1000}, - } - - def __init__( - self, - workspace_repository, - workspace_member_repository, - user_repository, - ): - self.workspace_repository = workspace_repository - self.workspace_member_repository = workspace_member_repository - self.user_repository = user_repository - - def execute(self, request: CreateWorkspaceRequest) -> tuple[Optional[CreateWorkspaceResponse], Optional[str]]: - """ - 执行创建工作空间 - - Args: - request: 创建请求 - - Returns: - (响应, 错误信息) - """ - try: - # 1. 验证输入 - if not request.name: - return None, "Workspace name is required" - - if len(request.name) > 100: - return None, "Workspace name is too long (max 100 characters)" - - if not request.owner_user_id: - return None, "Owner user ID is required" - - # 2. 验证用户存在 - owner = self.user_repository.find_by_id(request.owner_user_id) - if not owner: - return None, "Owner user not found" - - # 3. 验证订阅计划 - if request.subscription_plan not in self.PLAN_QUOTAS: - return None, f"Invalid subscription plan: {request.subscription_plan}" - - # 4. 获取配额 - quota = self.PLAN_QUOTAS[request.subscription_plan] - - # 5. 创建 Workspace - workspace = Workspace( - id=uuid4().hex, - name=request.name, - owner_user_id=request.owner_user_id, - subscription_plan=request.subscription_plan, - subscription_status="active", - max_projects=quota["max_projects"], - max_storage_gb=quota["max_storage_gb"], - used_storage_gb=0.0, - created_at=datetime.now(timezone.utc), - ) - - # 6. 保存 Workspace - self.workspace_repository.save(workspace) - - # 7. 创建 Owner 成员记录 - owner_member = WorkspaceMember( - id=uuid4().hex, - workspace_id=workspace.id, - user_id=request.owner_user_id, - role=WorkspaceMemberRole.OWNER, - invited_by=None, # Owner 不需要邀请 - joined_at=datetime.now(timezone.utc), - ) - - self.workspace_member_repository.save(owner_member) - - # 8. 返回响应 - return ( - CreateWorkspaceResponse( - workspace_id=workspace.id, - name=workspace.name, - subscription_plan=workspace.subscription_plan, - max_projects=workspace.max_projects, - max_storage_gb=workspace.max_storage_gb, - ), - None, - ) - - except Exception as e: - return None, f"Failed to create workspace: {str(e)}" diff --git a/packages/application/workspace/invite_member_use_case.py b/packages/application/workspace/invite_member_use_case.py deleted file mode 100644 index d907cedc2..000000000 --- a/packages/application/workspace/invite_member_use_case.py +++ /dev/null @@ -1,194 +0,0 @@ -""" -邀请成员到 Workspace Use Case -""" - -import secrets -from datetime import datetime, timedelta, timezone -from typing import Optional -from uuid import uuid4 - -from packages.adapters.smtp import get_email_service -from packages.domain.entities import ( - InvitationStatus, - WorkspaceInvitation, - WorkspaceMemberRole, -) - - -class InviteMemberRequest: - """邀请成员请求""" - - def __init__( - self, - workspace_id: str, - inviter_user_id: str, - invitee_email: str, - role: str, - ): - self.workspace_id = workspace_id - self.inviter_user_id = inviter_user_id - self.invitee_email = invitee_email.strip().lower() - self.role = role - - -class InviteMemberResponse: - """邀请成员响应""" - - def __init__( - self, - invitation_id: str, - invitee_email: str, - role: str, - expires_at: datetime, - ): - self.invitation_id = invitation_id - self.invitee_email = invitee_email - self.role = role - self.expires_at = expires_at - - -class InviteMemberUseCase: - """邀请成员用例""" - - VALID_ROLES = [ - WorkspaceMemberRole.ADMIN, - WorkspaceMemberRole.MEMBER, - WorkspaceMemberRole.VIEWER, - ] - - def __init__( - self, - workspace_repository, - workspace_member_repository, - workspace_invitation_repository, - user_repository, - base_url: str = "http://localhost:3000", - invitation_expire_days: int = 7, - email_service=None, - ): - self.workspace_repository = workspace_repository - self.workspace_member_repository = workspace_member_repository - self.workspace_invitation_repository = workspace_invitation_repository - self.user_repository = user_repository - self.base_url = base_url - self.invitation_expire_days = invitation_expire_days - self.email_service = email_service or get_email_service() - - def execute(self, request: InviteMemberRequest) -> tuple[Optional[InviteMemberResponse], Optional[str]]: - """ - 执行邀请成员 - - Args: - request: 邀请请求 - - Returns: - (响应, 错误信息) - """ - try: - # 1. 验证输入 - if not request.workspace_id: - return None, "Workspace ID is required" - - if not request.inviter_user_id: - return None, "Inviter user ID is required" - - if not request.invitee_email: - return None, "Invitee email is required" - - if not request.role: - return None, "Role is required" - - # 2. 验证角色(不能邀请 owner) - if request.role not in self.VALID_ROLES: - return None, f"Invalid role: {request.role}. Cannot invite as owner." - - # 3. 验证 Workspace 存在 - workspace = self.workspace_repository.find_by_id(request.workspace_id) - if not workspace: - return None, "Workspace not found" - - # 4. 验证邀请人是成员且有权限(owner 或 admin) - inviter_member = self.workspace_member_repository.find_by_workspace_and_user( - request.workspace_id, - request.inviter_user_id, - ) - if not inviter_member: - return None, "You are not a member of this workspace" - - if inviter_member.role not in [ - WorkspaceMemberRole.OWNER, - WorkspaceMemberRole.ADMIN, - ]: - return None, "Only owners and admins can invite members" - - # 5. 检查被邀请人是否已经是成员 - invitee_user = self.user_repository.find_by_email(request.invitee_email) - if invitee_user: - existing_member = self.workspace_member_repository.find_by_workspace_and_user( - request.workspace_id, - invitee_user.id, - ) - if existing_member: - return None, "User is already a member of this workspace" - - # 6. 检查是否已有待处理的邀请 - existing_invitation = self.workspace_invitation_repository.find_pending_by_workspace_and_email( - request.workspace_id, - request.invitee_email, - ) - if existing_invitation: - return None, "An invitation has already been sent to this email" - - # 7. 生成邀请令牌 - invitation_token = secrets.token_urlsafe(32) - expires_at = datetime.now(timezone.utc) + timedelta(days=self.invitation_expire_days) - - # 8. 创建邀请记录 - invitation = WorkspaceInvitation( - id=uuid4().hex, - workspace_id=request.workspace_id, - inviter_user_id=request.inviter_user_id, - invitee_email=request.invitee_email, - role=request.role, - invitation_token=invitation_token, - status=InvitationStatus.PENDING, - expires_at=expires_at, - created_at=datetime.now(timezone.utc), - ) - - # 9. 保存邀请 - self.workspace_invitation_repository.save(invitation) - - # 10. 发送邀请邮件 - invitation_url = f"{self.base_url}/invitations/{invitation_token}/accept" - - try: - inviter = self.user_repository.find_by_id(request.inviter_user_id) - inviter_name = inviter.display_name if inviter else "Someone" - - success, error = self.email_service.send_workspace_invitation_email( - to_email=request.invitee_email, - inviter_name=inviter_name, - workspace_name=workspace.name, - role=request.role, - invitation_url=invitation_url, - ) - - if not success: - print(f"Failed to send invitation email: {error}") - except Exception as e: - print(f"Email service error: {e}") - - # 11. 返回响应 - return ( - InviteMemberResponse( - invitation_id=invitation.id, - invitee_email=invitation.invitee_email, - role=invitation.role, - expires_at=invitation.expires_at, - ), - None, - ) - - except Exception as e: - return None, f"Failed to invite member: {str(e)}" diff --git a/packages/application/workspace/list_members_use_case.py b/packages/application/workspace/list_members_use_case.py deleted file mode 100644 index afe4a42c5..000000000 --- a/packages/application/workspace/list_members_use_case.py +++ /dev/null @@ -1,118 +0,0 @@ -""" -获取成员列表 Use Case -""" - -from datetime import datetime -from typing import List, Optional - - -class MemberInfo: - """成员信息""" - - def __init__( - self, - member_id: str, - user_id: str, - username: str, - email: str, - display_name: str, - role: str, - invited_by: Optional[str], - joined_at: datetime, - ): - self.member_id = member_id - self.user_id = user_id - self.username = username - self.email = email - self.display_name = display_name - self.role = role - self.invited_by = invited_by - self.joined_at = joined_at - - -class ListMembersRequest: - """获取成员列表请求""" - - def __init__(self, workspace_id: str, requester_user_id: str): - self.workspace_id = workspace_id - self.requester_user_id = requester_user_id - - -class ListMembersResponse: - """获取成员列表响应""" - - def __init__(self, members: List[MemberInfo]): - self.members = members - - -class ListMembersUseCase: - """获取成员列表用例""" - - def __init__( - self, - workspace_repository, - workspace_member_repository, - user_repository, - ): - self.workspace_repository = workspace_repository - self.workspace_member_repository = workspace_member_repository - self.user_repository = user_repository - - def execute(self, request: ListMembersRequest) -> tuple[Optional[ListMembersResponse], Optional[str]]: - """ - 执行获取成员列表 - - Args: - request: 请求 - - Returns: - (响应, 错误信息) - """ - try: - # 1. 验证输入 - if not request.workspace_id: - return None, "Workspace ID is required" - - if not request.requester_user_id: - return None, "Requester user ID is required" - - # 2. 验证工作空间存在 - workspace = self.workspace_repository.find_by_id(request.workspace_id) - if not workspace: - return None, "Workspace not found" - - # 3. 验证请求者是成员(只有成员才能查看成员列表) - requester_member = self.workspace_member_repository.find_by_workspace_and_user( - request.workspace_id, - request.requester_user_id, - ) - if not requester_member: - return None, "You are not a member of this workspace" - - # 4. 获取所有成员 - members = self.workspace_member_repository.find_by_workspace(request.workspace_id) - - # 5. 获取每个成员的用户信息 - member_infos = [] - for member in members: - user = self.user_repository.find_by_id(member.user_id) - if not user: - continue # 跳过不存在的用户 - - member_info = MemberInfo( - member_id=member.id, - user_id=user.id, - username=user.username, - email=user.email, - display_name=user.display_name, - role=member.role, - invited_by=member.invited_by, - joined_at=member.joined_at, - ) - member_infos.append(member_info) - - # 6. 返回响应 - return ListMembersResponse(members=member_infos), None - - except Exception as e: - return None, f"Failed to list members: {str(e)}" diff --git a/packages/application/workspace/list_workspaces_use_case.py b/packages/application/workspace/list_workspaces_use_case.py deleted file mode 100644 index 1c27e3841..000000000 --- a/packages/application/workspace/list_workspaces_use_case.py +++ /dev/null @@ -1,218 +0,0 @@ -""" -获取工作空间列表和详情 Use Case -""" - -from datetime import datetime -from typing import List, Optional - - -class WorkspaceInfo: - """工作空间信息""" - - def __init__( - self, - 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, - created_at: datetime, - ): - self.workspace_id = workspace_id - self.name = name - self.owner_user_id = owner_user_id - self.subscription_plan = subscription_plan - self.subscription_status = subscription_status - self.max_projects = max_projects - self.max_storage_gb = max_storage_gb - self.used_storage_gb = used_storage_gb - self.member_count = member_count - self.user_role = user_role - self.created_at = created_at - - -class ListWorkspacesRequest: - """获取工作空间列表请求""" - - def __init__(self, user_id: str): - self.user_id = user_id - - -class ListWorkspacesResponse: - """获取工作空间列表响应""" - - def __init__(self, workspaces: List[WorkspaceInfo]): - self.workspaces = workspaces - - -class ListWorkspacesUseCase: - """获取工作空间列表用例""" - - def __init__( - self, - workspace_repository, - workspace_member_repository, - ): - self.workspace_repository = workspace_repository - self.workspace_member_repository = workspace_member_repository - - def execute(self, request: ListWorkspacesRequest) -> tuple[Optional[ListWorkspacesResponse], Optional[str]]: - """ - 执行获取工作空间列表 - - Args: - request: 请求 - - Returns: - (响应, 错误信息) - """ - try: - # 1. 验证输入 - if not request.user_id: - return None, "User ID is required" - - # 2. 获取用户所有的成员记录 - memberships = self.workspace_member_repository.find_by_user(request.user_id) - - # 3. 获取每个工作空间的信息 - workspace_infos = [] - for membership in memberships: - workspace = self.workspace_repository.find_by_id(membership.workspace_id) - if not workspace: - continue - - # 获取成员数量 - member_count = self.workspace_member_repository.count_by_workspace(membership.workspace_id) - - workspace_info = WorkspaceInfo( - workspace_id=workspace.id, - name=workspace.name, - owner_user_id=workspace.owner_user_id, - subscription_plan=workspace.subscription_plan, - subscription_status=workspace.subscription_status, - max_projects=workspace.max_projects, - max_storage_gb=workspace.max_storage_gb, - used_storage_gb=workspace.used_storage_gb, - member_count=member_count, - user_role=membership.role, - created_at=workspace.created_at, - ) - workspace_infos.append(workspace_info) - - # 4. 返回响应 - return ListWorkspacesResponse(workspaces=workspace_infos), None - - except Exception as e: - return None, f"Failed to list workspaces: {str(e)}" - - -class GetWorkspaceDetailRequest: - """获取工作空间详情请求""" - - def __init__(self, workspace_id: str, user_id: str): - self.workspace_id = workspace_id - self.user_id = user_id - - -class WorkspaceDetailInfo: - """工作空间详情信息""" - - def __init__( - self, - workspace_id: str, - name: str, - owner_user_id: str, - subscription_plan: str, - subscription_status: str, - subscription_expires_at: Optional[datetime], - max_projects: int, - max_storage_gb: int, - used_storage_gb: float, - member_count: int, - user_role: str, - created_at: datetime, - ): - self.workspace_id = workspace_id - self.name = name - self.owner_user_id = owner_user_id - self.subscription_plan = subscription_plan - self.subscription_status = subscription_status - self.subscription_expires_at = subscription_expires_at - self.max_projects = max_projects - self.max_storage_gb = max_storage_gb - self.used_storage_gb = used_storage_gb - self.member_count = member_count - self.user_role = user_role - self.created_at = created_at - - -class GetWorkspaceDetailUseCase: - """获取工作空间详情用例""" - - def __init__( - self, - workspace_repository, - workspace_member_repository, - ): - self.workspace_repository = workspace_repository - self.workspace_member_repository = workspace_member_repository - - def execute(self, request: GetWorkspaceDetailRequest) -> tuple[Optional[WorkspaceDetailInfo], Optional[str]]: - """ - 执行获取工作空间详情 - - Args: - request: 请求 - - Returns: - (详情信息, 错误信息) - """ - try: - # 1. 验证输入 - if not request.workspace_id: - return None, "Workspace ID is required" - - if not request.user_id: - return None, "User ID is required" - - # 2. 验证工作空间存在 - workspace = self.workspace_repository.find_by_id(request.workspace_id) - if not workspace: - return None, "Workspace not found" - - # 3. 验证用户是成员 - membership = self.workspace_member_repository.find_by_workspace_and_user( - request.workspace_id, - request.user_id, - ) - if not membership: - return None, "You are not a member of this workspace" - - # 4. 获取成员数量 - member_count = self.workspace_member_repository.count_by_workspace(request.workspace_id) - - # 5. 构建详情信息 - detail_info = WorkspaceDetailInfo( - workspace_id=workspace.id, - name=workspace.name, - owner_user_id=workspace.owner_user_id, - subscription_plan=workspace.subscription_plan, - subscription_status=workspace.subscription_status, - subscription_expires_at=workspace.subscription_expires_at, - max_projects=workspace.max_projects, - max_storage_gb=workspace.max_storage_gb, - used_storage_gb=workspace.used_storage_gb, - member_count=member_count, - user_role=membership.role, - created_at=workspace.created_at, - ) - - return detail_info, None - - except Exception as e: - return None, f"Failed to get workspace detail: {str(e)}" diff --git a/packages/application/workspace/remove_member_use_case.py b/packages/application/workspace/remove_member_use_case.py deleted file mode 100644 index 046cf5574..000000000 --- a/packages/application/workspace/remove_member_use_case.py +++ /dev/null @@ -1,171 +0,0 @@ -""" -移除成员 Use Case -""" - -from typing import Optional - -from packages.domain.entities import WorkspaceMemberRole - - -class RemoveMemberRequest: - """移除成员请求""" - - def __init__( - self, - workspace_id: str, - requester_user_id: str, - target_user_id: str, - ): - self.workspace_id = workspace_id - self.requester_user_id = requester_user_id - self.target_user_id = target_user_id - - -class RemoveMemberUseCase: - """移除成员用例""" - - def __init__( - self, - workspace_repository, - workspace_member_repository, - ): - self.workspace_repository = workspace_repository - self.workspace_member_repository = workspace_member_repository - - def execute(self, request: RemoveMemberRequest) -> tuple[bool, Optional[str]]: - """ - 执行移除成员 - - Args: - request: 移除请求 - - Returns: - (是否成功, 错误信息) - """ - try: - # 1. 验证输入 - if not request.workspace_id: - return False, "Workspace ID is required" - - if not request.requester_user_id: - return False, "Requester user ID is required" - - if not request.target_user_id: - return False, "Target user ID is required" - - # 2. 验证 Workspace 存在 - workspace = self.workspace_repository.find_by_id(request.workspace_id) - if not workspace: - return False, "Workspace not found" - - # 3. 验证请求者是成员且有权限 - requester_member = self.workspace_member_repository.find_by_workspace_and_user( - request.workspace_id, - request.requester_user_id, - ) - if not requester_member: - return False, "You are not a member of this workspace" - - if requester_member.role not in [ - WorkspaceMemberRole.OWNER, - WorkspaceMemberRole.ADMIN, - ]: - return False, "Only owners and admins can remove members" - - # 4. 验证目标成员存在 - target_member = self.workspace_member_repository.find_by_workspace_and_user( - request.workspace_id, - request.target_user_id, - ) - if not target_member: - return False, "Target user is not a member of this workspace" - - # 5. 不能移除自己(应该用离开 workspace 的功能) - if request.requester_user_id == request.target_user_id: - return False, "Cannot remove yourself. Use leave workspace instead." - - # 6. 不能移除 owner - if target_member.role == WorkspaceMemberRole.OWNER: - return False, "Cannot remove the workspace owner" - - # 7. Admin 不能移除另一个 Admin(只有 owner 可以) - if requester_member.role == WorkspaceMemberRole.ADMIN and target_member.role == WorkspaceMemberRole.ADMIN: - return False, "Admins cannot remove other admins" - - # 8. 删除成员记录 - success = self.workspace_member_repository.delete(target_member.id) - if not success: - return False, "Failed to remove member" - - return True, None - - except Exception as e: - return False, f"Failed to remove member: {str(e)}" - - -class LeaveWorkspaceRequest: - """离开 Workspace 请求""" - - def __init__(self, workspace_id: str, user_id: str): - self.workspace_id = workspace_id - self.user_id = user_id - - -class LeaveWorkspaceUseCase: - """离开 Workspace 用例""" - - def __init__( - self, - workspace_repository, - workspace_member_repository, - ): - self.workspace_repository = workspace_repository - self.workspace_member_repository = workspace_member_repository - - def execute(self, request: LeaveWorkspaceRequest) -> tuple[bool, Optional[str]]: - """ - 执行离开 Workspace - - Args: - request: 离开请求 - - Returns: - (是否成功, 错误信息) - """ - try: - # 1. 验证输入 - if not request.workspace_id: - return False, "Workspace ID is required" - - if not request.user_id: - return False, "User ID is required" - - # 2. 验证 Workspace 存在 - workspace = self.workspace_repository.find_by_id(request.workspace_id) - if not workspace: - return False, "Workspace not found" - - # 3. 验证用户是成员 - member = self.workspace_member_repository.find_by_workspace_and_user( - request.workspace_id, - request.user_id, - ) - if not member: - return False, "You are not a member of this workspace" - - # 4. Owner 不能离开(需要先转移 ownership 或删除 workspace) - if member.role == WorkspaceMemberRole.OWNER: - return ( - False, - "Owner cannot leave workspace. Transfer ownership or delete workspace first.", - ) - - # 5. 删除成员记录 - success = self.workspace_member_repository.delete(member.id) - if not success: - return False, "Failed to leave workspace" - - return True, None - - except Exception as e: - return False, f"Failed to leave workspace: {str(e)}" diff --git a/packages/application/workspace/subscription_use_case.py b/packages/application/workspace/subscription_use_case.py deleted file mode 100644 index dd2952edd..000000000 --- a/packages/application/workspace/subscription_use_case.py +++ /dev/null @@ -1,223 +0,0 @@ -""" -Subscription 管理 Use Case -""" - -from datetime import datetime, timedelta, timezone -from typing import Optional - -from packages.domain.entities import WorkspaceMemberRole - - -class UpgradeSubscriptionRequest: - """升级订阅请求""" - - def __init__( - self, - workspace_id: str, - requester_user_id: str, - new_plan: str, - ): - self.workspace_id = workspace_id - self.requester_user_id = requester_user_id - self.new_plan = new_plan - - -class UpgradeSubscriptionResponse: - """升级订阅响应""" - - def __init__( - self, - workspace_id: str, - old_plan: str, - new_plan: str, - max_projects: int, - max_storage_gb: int, - ): - self.workspace_id = workspace_id - self.old_plan = old_plan - self.new_plan = new_plan - self.max_projects = max_projects - self.max_storage_gb = max_storage_gb - - -class UpgradeSubscriptionUseCase: - """升级订阅用例""" - - # 订阅计划配额 - PLAN_QUOTAS = { - "free": {"max_projects": 3, "max_storage_gb": 10, "price": 0}, - "pro": {"max_projects": 999999, "max_storage_gb": 100, "price": 99}, - "enterprise": {"max_projects": 999999, "max_storage_gb": 1000, "price": 999}, - } - - # 计划等级 - PLAN_LEVELS = { - "free": 0, - "pro": 1, - "enterprise": 2, - } - - def __init__( - self, - workspace_repository, - workspace_member_repository, - ): - self.workspace_repository = workspace_repository - self.workspace_member_repository = workspace_member_repository - - def execute( - self, request: UpgradeSubscriptionRequest - ) -> tuple[Optional[UpgradeSubscriptionResponse], Optional[str]]: - """ - 执行升级订阅 - - Args: - request: 升级请求 - - Returns: - (响应, 错误信息) - """ - try: - # 1. 验证输入 - if not request.workspace_id: - return None, "Workspace ID is required" - - if not request.requester_user_id: - return None, "Requester user ID is required" - - if not request.new_plan: - return None, "New plan is required" - - # 2. 验证新计划有效 - if request.new_plan not in self.PLAN_QUOTAS: - return None, f"Invalid plan: {request.new_plan}" - - # 3. 验证工作空间存在 - workspace = self.workspace_repository.find_by_id(request.workspace_id) - if not workspace: - return None, "Workspace not found" - - # 4. 验证权限(只有 Owner 可以管理订阅) - member = self.workspace_member_repository.find_by_workspace_and_user( - request.workspace_id, - request.requester_user_id, - ) - if not member: - return None, "You are not a member of this workspace" - - if member.role != WorkspaceMemberRole.OWNER: - return None, "Only workspace owner can manage subscription" - - # 5. 检查是否是升级(不能降级到免费计划,需要用取消订阅) - current_level = self.PLAN_LEVELS.get(workspace.subscription_plan, 0) - new_level = self.PLAN_LEVELS.get(request.new_plan, 0) - - if new_level < current_level: - return ( - None, - "Cannot downgrade plan. Use cancel subscription to return to free plan.", - ) - - if new_level == current_level: - return None, f"Workspace is already on {request.new_plan} plan" - - # 6. 更新订阅 - old_plan = workspace.subscription_plan - quota = self.PLAN_QUOTAS[request.new_plan] - - workspace.subscription_plan = request.new_plan - workspace.subscription_status = "active" - workspace.max_projects = quota["max_projects"] - workspace.max_storage_gb = quota["max_storage_gb"] - - # 设置过期时间(假设按月订阅) - workspace.subscription_expires_at = datetime.now(timezone.utc) + timedelta(days=30) - - self.workspace_repository.save(workspace) - - # 7. 返回响应 - return ( - UpgradeSubscriptionResponse( - workspace_id=workspace.id, - old_plan=old_plan, - new_plan=workspace.subscription_plan, - max_projects=workspace.max_projects, - max_storage_gb=workspace.max_storage_gb, - ), - None, - ) - - except Exception as e: - return None, f"Failed to upgrade subscription: {str(e)}" - - -class CancelSubscriptionRequest: - """取消订阅请求""" - - def __init__(self, workspace_id: str, requester_user_id: str): - self.workspace_id = workspace_id - self.requester_user_id = requester_user_id - - -class CancelSubscriptionUseCase: - """取消订阅用例""" - - def __init__( - self, - workspace_repository, - workspace_member_repository, - ): - self.workspace_repository = workspace_repository - self.workspace_member_repository = workspace_member_repository - - def execute(self, request: CancelSubscriptionRequest) -> tuple[bool, Optional[str]]: - """ - 执行取消订阅 - - Args: - request: 取消请求 - - Returns: - (是否成功, 错误信息) - """ - try: - # 1. 验证输入 - if not request.workspace_id: - return False, "Workspace ID is required" - - if not request.requester_user_id: - return False, "Requester user ID is required" - - # 2. 验证工作空间存在 - workspace = self.workspace_repository.find_by_id(request.workspace_id) - if not workspace: - return False, "Workspace not found" - - # 3. 验证权限(只有 Owner 可以管理订阅) - member = self.workspace_member_repository.find_by_workspace_and_user( - request.workspace_id, - request.requester_user_id, - ) - if not member: - return False, "You are not a member of this workspace" - - if member.role != WorkspaceMemberRole.OWNER: - return False, "Only workspace owner can manage subscription" - - # 4. 检查当前计划 - if workspace.subscription_plan == "free": - return False, "Workspace is already on free plan" - - # 5. 降级到 free 计划 - workspace.subscription_plan = "free" - workspace.subscription_status = "active" - workspace.subscription_expires_at = None - workspace.max_projects = 3 - workspace.max_storage_gb = 10 - - self.workspace_repository.save(workspace) - - return True, None - - except Exception as e: - return False, f"Failed to cancel subscription: {str(e)}" diff --git a/packages/application/workspace/update_member_role_use_case.py b/packages/application/workspace/update_member_role_use_case.py deleted file mode 100644 index f11b0f208..000000000 --- a/packages/application/workspace/update_member_role_use_case.py +++ /dev/null @@ -1,142 +0,0 @@ -""" -修改成员角色 Use Case -""" - -from typing import Optional - -from packages.domain.entities import WorkspaceMemberRole - - -class UpdateMemberRoleRequest: - """修改成员角色请求""" - - def __init__( - self, - workspace_id: str, - requester_user_id: str, - target_user_id: str, - new_role: str, - ): - self.workspace_id = workspace_id - self.requester_user_id = requester_user_id - self.target_user_id = target_user_id - self.new_role = new_role - - -class UpdateMemberRoleResponse: - """修改成员角色响应""" - - def __init__(self, user_id: str, old_role: str, new_role: str): - self.user_id = user_id - self.old_role = old_role - self.new_role = new_role - - -class UpdateMemberRoleUseCase: - """修改成员角色用例""" - - VALID_ROLES = [ - WorkspaceMemberRole.ADMIN, - WorkspaceMemberRole.MEMBER, - WorkspaceMemberRole.VIEWER, - ] - - def __init__( - self, - workspace_repository, - workspace_member_repository, - ): - self.workspace_repository = workspace_repository - self.workspace_member_repository = workspace_member_repository - - def execute(self, request: UpdateMemberRoleRequest) -> tuple[Optional[UpdateMemberRoleResponse], Optional[str]]: - """ - 执行修改成员角色 - - Args: - request: 修改请求 - - Returns: - (响应, 错误信息) - """ - try: - # 1. 验证输入 - if not request.workspace_id: - return None, "Workspace ID is required" - - if not request.requester_user_id: - return None, "Requester user ID is required" - - if not request.target_user_id: - return None, "Target user ID is required" - - if not request.new_role: - return None, "New role is required" - - # 2. 验证新角色(不能修改为 owner) - if request.new_role not in self.VALID_ROLES: - return ( - None, - f"Invalid role: {request.new_role}. Cannot change to owner.", - ) - - # 3. 验证 Workspace 存在 - workspace = self.workspace_repository.find_by_id(request.workspace_id) - if not workspace: - return None, "Workspace not found" - - # 4. 验证请求者是成员且有权限(只有 owner 和 admin 可以修改角色) - requester_member = self.workspace_member_repository.find_by_workspace_and_user( - request.workspace_id, - request.requester_user_id, - ) - if not requester_member: - return None, "You are not a member of this workspace" - - if requester_member.role not in [ - WorkspaceMemberRole.OWNER, - WorkspaceMemberRole.ADMIN, - ]: - return None, "Only owners and admins can change member roles" - - # 5. 验证目标成员存在 - target_member = self.workspace_member_repository.find_by_workspace_and_user( - request.workspace_id, - request.target_user_id, - ) - if not target_member: - return None, "Target user is not a member of this workspace" - - # 6. 不能修改自己的角色 - if request.requester_user_id == request.target_user_id: - return None, "Cannot change your own role" - - # 7. 不能修改 owner 的角色 - if target_member.role == WorkspaceMemberRole.OWNER: - return None, "Cannot change the owner's role" - - # 8. Admin 不能修改另一个 Admin 的角色(只有 owner 可以) - if requester_member.role == WorkspaceMemberRole.ADMIN and target_member.role == WorkspaceMemberRole.ADMIN: - return None, "Admins cannot change other admins' roles" - - # 9. 检查角色是否相同 - if target_member.role == request.new_role: - return None, f"User already has the {request.new_role} role" - - # 10. 更新角色 - old_role = target_member.role - target_member.role = request.new_role - self.workspace_member_repository.save(target_member) - - # 11. 返回响应 - return ( - UpdateMemberRoleResponse( - user_id=request.target_user_id, - old_role=old_role, - new_role=request.new_role, - ), - None, - ) - - except Exception as e: - return None, f"Failed to update member role: {str(e)}" diff --git a/packages/domain/classification.py b/packages/domain/classification.py index b9a63651c..3c5324776 100644 --- a/packages/domain/classification.py +++ b/packages/domain/classification.py @@ -43,7 +43,6 @@ class AssetClassification(StrEnum): @dataclass(slots=True) class ClassificationJob: id: str - workspace_id: str project_id: str asset_id: str status: ClassificationJobStatus = ClassificationJobStatus.PENDING @@ -56,19 +55,15 @@ class ClassificationJob: @classmethod def create( cls, - workspace_id: str, project_id: str, asset_id: str, ) -> "ClassificationJob": - if not workspace_id.strip(): - raise ValueError("workspace_id 不能为空") if not project_id.strip(): raise ValueError("project_id 不能为空") if not asset_id.strip(): raise ValueError("asset_id 不能为空") return cls( id=uuid4().hex, - workspace_id=workspace_id.strip(), project_id=project_id.strip(), asset_id=asset_id.strip(), ) diff --git a/packages/domain/entities.py b/packages/domain/entities.py index 9c8015ecd..4de800777 100644 --- a/packages/domain/entities.py +++ b/packages/domain/entities.py @@ -51,7 +51,6 @@ class Project: owner_user_id: str name: str description: str = "" - workspace_id: str = "" # 兼容旧代码,待移除 shared_users: list[str] = field(default_factory=list) # 被共享的用户 ID 列表 created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) diff --git a/packages/domain/generated_video.py b/packages/domain/generated_video.py index a7db0171e..1d95b1fb3 100644 --- a/packages/domain/generated_video.py +++ b/packages/domain/generated_video.py @@ -18,7 +18,6 @@ class GeneratedVideo: width: int height: int fps: float - workspace_id: str = "" thumbnail_url: str | None = None status: str = "completed" review_status: str = "pending_review" @@ -37,7 +36,6 @@ class GeneratedVideo: name: str, file_url: str, *, - workspace_id: str = "", file_size: int = 0, duration: float = 0.0, width: int = 0, @@ -56,7 +54,6 @@ class GeneratedVideo: raise ValueError("file_url cannot be empty") return cls( id=uuid4().hex, - workspace_id=workspace_id.strip(), project_id=project_id.strip(), generation_task_id=generation_task_id.strip(), name=name.strip(), diff --git a/packages/domain/generation_task.py b/packages/domain/generation_task.py index 752df51a0..ffa0e0552 100644 --- a/packages/domain/generation_task.py +++ b/packages/domain/generation_task.py @@ -19,7 +19,6 @@ class GenerationTask: id: str project_id: str asset_library_id: str - workspace_id: str = "" strategy_id: str = "" voice_library_id: str = "" edit_plan_id: str = "" @@ -38,7 +37,6 @@ class GenerationTask: project_id: str, asset_library_id: str, *, - workspace_id: str = "", strategy_id: str = "", voice_library_id: str = "", edit_plan_id: str = "", @@ -50,7 +48,6 @@ class GenerationTask: raise ValueError("asset_library_id 不能为空") return cls( id=uuid4().hex, - workspace_id=workspace_id.strip(), project_id=project_id.strip(), asset_library_id=asset_library_id.strip(), strategy_id=strategy_id.strip(), diff --git a/packages/domain/permissions.py b/packages/domain/permissions.py index 5995c6544..59f178a72 100644 --- a/packages/domain/permissions.py +++ b/packages/domain/permissions.py @@ -3,60 +3,36 @@ Permissions module - stub implementation. Workspace concept has been removed. All permission checks pass by default. """ -from typing import Optional - - -class WorkspaceMemberRole: - """Stub enum - workspace concept removed.""" - OWNER = "owner" - ADMIN = "admin" - MEMBER = "member" - VIEWER = "viewer" - class PermissionChecker: """Stub permission checker - all checks pass since workspace is removed.""" - def __init__(self, workspace_member_repository=None): - self.workspace_member_repository = workspace_member_repository + def __init__(self, member_repository=None): + self.member_repository = member_repository - def check_workspace_access(self, workspace_id, user_id): + def check_access(self, project_id, user_id): return True, "owner" - def check_is_owner(self, workspace_id, user_id): + def check_is_owner(self, project_id, user_id): return True - def check_is_admin_or_owner(self, workspace_id, user_id): + def check_is_admin_or_owner(self, project_id, user_id): return True - def check_can_manage_members(self, workspace_id, user_id): + def check_can_manage_members(self, project_id, user_id): return True - def check_can_edit_workspace(self, workspace_id, user_id): + def check_can_edit_project(self, project_id, user_id): return True - def check_can_create_project(self, workspace_id, user_id): + def check_can_delete_project(self, project_id, user_id): return True - def check_can_edit_project(self, workspace_id, user_id): - return True - - def check_can_delete_project(self, workspace_id, user_id): - return True - - def check_can_view_workspace(self, workspace_id, user_id): + def check_can_view_project(self, project_id, user_id): return True class Permission: - WORKSPACE_VIEW = "workspace:view" - WORKSPACE_EDIT = "workspace:edit" - WORKSPACE_DELETE = "workspace:delete" - WORKSPACE_MANAGE_SUBSCRIPTION = "workspace:manage_subscription" - MEMBER_VIEW = "member:view" - MEMBER_INVITE = "member:invite" - MEMBER_REMOVE = "member:remove" - MEMBER_UPDATE_ROLE = "member:update_role" PROJECT_VIEW = "project:view" PROJECT_CREATE = "project:create" PROJECT_EDIT = "project:edit" diff --git a/packages/domain/project_management.py b/packages/domain/project_management.py index 7a245b58b..401cdd9f2 100644 --- a/packages/domain/project_management.py +++ b/packages/domain/project_management.py @@ -33,7 +33,6 @@ class Task: id: str project_id: str - workspace_id: str name: str description: str = "" status: TaskStatus = TaskStatus.PENDING @@ -53,7 +52,6 @@ class Task: def create( cls, project_id: str, - workspace_id: str, name: str, description: str = "", priority: TaskPriority = TaskPriority.MEDIUM, @@ -68,13 +66,10 @@ class Task: raise ValueError("任务名称不能为空") if not project_id.strip(): raise ValueError("project_id 不能为空") - if not workspace_id.strip(): - raise ValueError("workspace_id 不能为空") return cls( id=uuid4().hex, project_id=project_id.strip(), - workspace_id=workspace_id.strip(), name=clean_name, description=description.strip(), priority=priority, @@ -134,7 +129,6 @@ class Milestone: id: str project_id: str - workspace_id: str name: str description: str = "" target_date: datetime | None = None @@ -147,7 +141,6 @@ class Milestone: def create( cls, project_id: str, - workspace_id: str, name: str, description: str = "", target_date: datetime | None = None, @@ -158,13 +151,10 @@ class Milestone: raise ValueError("里程碑名称不能为空") if not project_id.strip(): raise ValueError("project_id 不能为空") - if not workspace_id.strip(): - raise ValueError("workspace_id 不能为空") return cls( id=uuid4().hex, project_id=project_id.strip(), - workspace_id=workspace_id.strip(), name=clean_name, description=description.strip(), target_date=target_date, @@ -192,7 +182,6 @@ class TaskIssue: id: str task_id: str project_id: str - workspace_id: str title: str description: str = "" resolved: bool = False @@ -206,7 +195,6 @@ class TaskIssue: cls, task_id: str, project_id: str, - workspace_id: str, title: str, description: str = "", created_by_user_id: str = "", @@ -219,14 +207,11 @@ class TaskIssue: raise ValueError("task_id 不能为空") if not project_id.strip(): raise ValueError("project_id 不能为空") - if not workspace_id.strip(): - raise ValueError("workspace_id 不能为空") return cls( id=uuid4().hex, task_id=task_id.strip(), project_id=project_id.strip(), - workspace_id=workspace_id.strip(), title=clean_title, description=description.strip(), created_by_user_id=created_by_user_id.strip(), diff --git a/packages/domain/quota.py b/packages/domain/quota.py index 7c72b1bc2..ccd87a157 100644 --- a/packages/domain/quota.py +++ b/packages/domain/quota.py @@ -3,31 +3,27 @@ Quota checker - stub implementation. Workspace concept removed. All quota checks pass by default. """ -from typing import Optional - class QuotaChecker: """Stub quota checker - all checks pass since workspace is removed.""" - def __init__(self, workspace_repository=None, project_repository=None): - self.workspace_repository = workspace_repository + def __init__(self, project_repository=None): self.project_repository = project_repository - def check_can_create_project(self, workspace_id=None): + def check_can_create_project(self, user_id=None): return True, None - def check_storage_available(self, workspace_id=None, additional_gb=0): + def check_storage_available(self, user_id=None, additional_gb=0): return True, None - def get_quota_status(self, workspace_id=None): + def get_quota_status(self, user_id=None): return { - "workspace_id": workspace_id or "", "subscription_plan": "unlimited", "projects": {"used": 0, "limit": 999999, "unlimited": True, "usage_percent": 0}, "storage": {"used_gb": 0, "limit_gb": 999999, "remaining_gb": 999999, "usage_percent": 0}, } - def update_storage_usage(self, workspace_id=None, delta_gb=0): + def update_storage_usage(self, user_id=None, delta_gb=0): return True, None diff --git a/packages/ports/project_title_repository.py b/packages/ports/project_title_repository.py index c88adf9e2..389585460 100644 --- a/packages/ports/project_title_repository.py +++ b/packages/ports/project_title_repository.py @@ -5,5 +5,5 @@ from typing import Protocol, Any class ProjectTitleRepository(Protocol): def list_by_project(self, project_id: str, active_only: bool = False) -> list[Any]: ... def get(self, title_id: str) -> Any | None: ... - def create(self, *, workspace_id: str, project_id: str, text: str, category: str, created_by_user_id: str, favorite: bool = False) -> Any: ... + def create(self, *, project_id: str, text: str, category: str, created_by_user_id: str, favorite: bool = False) -> Any: ... def update(self, item: Any) -> Any: ... diff --git a/packages/ports/workspace_invitation_repository.py b/packages/ports/workspace_invitation_repository.py deleted file mode 100644 index 9daa53f48..000000000 --- a/packages/ports/workspace_invitation_repository.py +++ /dev/null @@ -1,6 +0,0 @@ -"""Stub port for workspace invitation repository - workspace concept removed.""" -from __future__ import annotations -from typing import Protocol - -class WorkspaceInvitationRepository(Protocol): - pass diff --git a/packages/ports/workspace_member_repository.py b/packages/ports/workspace_member_repository.py deleted file mode 100644 index d9b6410a1..000000000 --- a/packages/ports/workspace_member_repository.py +++ /dev/null @@ -1,6 +0,0 @@ -"""Stub port for workspace member repository - workspace concept removed.""" -from __future__ import annotations -from typing import Protocol - -class WorkspaceMemberRepository(Protocol): - pass diff --git a/packages/ports/workspace_repository.py b/packages/ports/workspace_repository.py deleted file mode 100644 index 99d2e6cac..000000000 --- a/packages/ports/workspace_repository.py +++ /dev/null @@ -1,6 +0,0 @@ -"""Stub port for workspace repository - workspace concept removed.""" -from __future__ import annotations -from typing import Protocol - -class WorkspaceRepository(Protocol): - pass diff --git a/scripts/init_phase4_tasks.py b/scripts/init_phase4_tasks.py index 30e8f3cfd..351e3eae6 100644 --- a/scripts/init_phase4_tasks.py +++ b/scripts/init_phase4_tasks.py @@ -26,7 +26,6 @@ def create_milestone(name, target_date, description=""): "name": name, "target_date": target_date, "project_id": PROJECT_ID, - "workspace_id": WORKSPACE_ID, "description": description, } resp = requests.post(f"{API_BASE}/project-management/milestones", json=payload) @@ -44,7 +43,6 @@ def create_task(name, description, priority="medium"): "name": name, "description": description, "project_id": PROJECT_ID, - "workspace_id": WORKSPACE_ID, "priority": priority, } resp = requests.post(f"{API_BASE}/project-management/tasks", json=payload) @@ -102,7 +100,6 @@ def main(): create_task("WorkspaceMembership 实体", "成员关系、角色存储", "high") create_task("WorkspaceRole 枚举", "Owner/Admin/Member/Viewer 权限定义", "high") create_task("权限检查中间件", "@require_permission 装饰器", "urgent") - create_task("数据隔离过滤器", "所有查询自动加 workspace_id", "urgent") # Day 3-4: 成员管理 create_task("邀请成员 API", "发送邀请邮件 + 生成邀请令牌", "high") diff --git a/scripts/init_tracker_data.py b/scripts/init_tracker_data.py index c80cbca4c..180eb8cc3 100644 --- a/scripts/init_tracker_data.py +++ b/scripts/init_tracker_data.py @@ -29,7 +29,6 @@ def create_milestone(name, target_date, description=""): "name": name, "target_date": target_date, "project_id": PROJECT_ID, - "workspace_id": WORKSPACE_ID, "description": description, } resp = requests.post(f"{API_BASE}/project-management/milestones", json=payload) @@ -47,7 +46,6 @@ def create_task(name, description, priority="medium", status="pending", progress "name": name, "description": description, "project_id": PROJECT_ID, - "workspace_id": WORKSPACE_ID, "priority": priority, } resp = requests.post(f"{API_BASE}/project-management/tasks", json=payload) diff --git a/scripts/smoke_public_boundary_flow.py b/scripts/smoke_public_boundary_flow.py index 4d9b356bb..35af59f67 100644 --- a/scripts/smoke_public_boundary_flow.py +++ b/scripts/smoke_public_boundary_flow.py @@ -48,12 +48,10 @@ def main() -> None: "owner_workspace", owner.post(f"{BASE_URL}/workspaces", json={"name": "Boundary Workspace"}, headers=owner_headers, timeout=30), ) - workspace_id = workspace.get("id") or workspace["workspace_id"] project = _json_or_raise( "owner_project", owner.post( f"{BASE_URL}/projects", - json={"workspace_id": workspace_id, "name": "Boundary Project", "description": "smoke"}, headers=owner_headers, timeout=30, ), @@ -63,7 +61,6 @@ def main() -> None: "owner_library", owner.post( f"{BASE_URL}/asset-libraries", - json={"workspace_id": workspace_id, "project_id": project_id, "name": "Boundary Library", "kind": "video"}, headers=owner_headers, timeout=30, ), @@ -74,7 +71,6 @@ def main() -> None: owner.post( f"{BASE_URL}/upload", files={"file": ("boundary.mp4", io.BytesIO(b"boundary-owner"), "video/mp4")}, - data={"workspace_id": workspace_id, "project_id": project_id, "library_id": library_id}, headers=owner_headers, timeout=60, ), @@ -93,7 +89,6 @@ def main() -> None: "owner_generation_create", owner.post( f"{BASE_URL}/generation/tasks", - json={"workspace_id": workspace_id, "project_id": project_id, "asset_library_id": library_id}, headers=owner_headers, timeout=30, ), @@ -184,7 +179,6 @@ def main() -> None: upload_response = intruder.post( f"{BASE_URL}/upload", files={"file": ("boundary.txt", io.BytesIO(b"boundary"), "text/plain")}, - data={"workspace_id": workspace_id, "project_id": project_id, "library_id": library_id}, headers=intruder_headers, timeout=60, ) diff --git a/scripts/smoke_public_upload_flow.py b/scripts/smoke_public_upload_flow.py index 8946f414a..8cb592695 100644 --- a/scripts/smoke_public_upload_flow.py +++ b/scripts/smoke_public_upload_flow.py @@ -51,13 +51,11 @@ def main() -> None: "workspace", session.post(f"{BASE_URL}/workspaces", json={"name": "Upload Smoke Workspace"}, headers=headers, timeout=30), ) - workspace_id = workspace.get("id") or workspace["workspace_id"] project = _json_or_raise( "project", session.post( f"{BASE_URL}/projects", - json={"workspace_id": workspace_id, "name": "Upload Smoke Project", "description": "smoke"}, headers=headers, timeout=30, ), @@ -69,7 +67,6 @@ def main() -> None: session.post( f"{BASE_URL}/asset-libraries", json={ - "workspace_id": workspace_id, "project_id": project_id, "name": "Smoke Video Library", "kind": "video", @@ -85,7 +82,6 @@ def main() -> None: session.post( f"{BASE_URL}/upload", files={"file": ("smoke.txt", io.BytesIO(b"xiaoxia upload smoke"), "text/plain")}, - data={"workspace_id": workspace_id, "project_id": project_id, "library_id": library_id}, headers=headers, timeout=60, ), diff --git a/tests/integration/test_asset_tags.py b/tests/integration/test_asset_tags.py index a45cb24f8..212af3c8a 100644 --- a/tests/integration/test_asset_tags.py +++ b/tests/integration/test_asset_tags.py @@ -6,7 +6,6 @@ from packages.domain import Asset def test_add_tag_to_asset(): """测试添加标签到 Asset。""" asset = Asset.create( - workspace_id="ws-1", project_id="proj-1", library_id="lib-1", name="video.mp4", @@ -25,7 +24,6 @@ def test_add_tag_to_asset(): def test_add_duplicate_tag_should_ignore(): """测试添加重复标签应自动去重。""" asset = Asset.create( - workspace_id="ws-1", project_id="proj-1", library_id="lib-1", name="video.mp4", @@ -43,7 +41,6 @@ def test_add_duplicate_tag_should_ignore(): def test_add_empty_tag_should_fail(): """测试添加空标签应失败。""" asset = Asset.create( - workspace_id="ws-1", project_id="proj-1", library_id="lib-1", name="video.mp4", @@ -61,7 +58,6 @@ def test_add_empty_tag_should_fail(): def test_remove_tag_from_asset(): """测试从 Asset 删除标签。""" asset = Asset.create( - workspace_id="ws-1", project_id="proj-1", library_id="lib-1", name="video.mp4", @@ -82,7 +78,6 @@ def test_remove_tag_from_asset(): def test_remove_nonexistent_tag_should_be_idempotent(): """测试删除不存在的标签应幂等(不报错)。""" asset = Asset.create( - workspace_id="ws-1", project_id="proj-1", library_id="lib-1", name="video.mp4", diff --git a/tests/integration/test_classification_pipeline.py b/tests/integration/test_classification_pipeline.py index 7564f3469..6c6842f13 100644 --- a/tests/integration/test_classification_pipeline.py +++ b/tests/integration/test_classification_pipeline.py @@ -56,7 +56,6 @@ def test_classification_pipeline(): use_case = SubmitClassificationJobUseCase(job_repo) job = use_case.execute( SubmitClassificationJobCommand( - workspace_id="ws-1", project_id="proj-1", asset_id="asset-123", ) diff --git a/tests/integration/test_generation_pipeline.py b/tests/integration/test_generation_pipeline.py index bf2c7c77b..06ae73724 100644 --- a/tests/integration/test_generation_pipeline.py +++ b/tests/integration/test_generation_pipeline.py @@ -60,11 +60,9 @@ def simulate_generate_video( task_repo.update(task) file_url = ( - f"http://localhost:9000/xiaoxia-assets/workspaces/{task.workspace_id}" f"/projects/{task.project_id}/generated/{task.id}/{task.id}.mp4" ) video = GeneratedVideo.create( - workspace_id=task.workspace_id, project_id=task.project_id, generation_task_id=task.id, name=f"{task.id}.mp4", @@ -96,7 +94,6 @@ def test_create_generation_task_smoke(): use_case = CreateGenerationTaskUseCase(repo) task = use_case.execute( CreateGenerationTaskCommand( - workspace_id="ws-1", project_id="proj-1", asset_library_id="lib-1", strategy_id="str-1", @@ -104,7 +101,6 @@ def test_create_generation_task_smoke(): created_by_user_id="user-1", ) ) - assert task.workspace_id == "ws-1" assert task.project_id == "proj-1" assert task.asset_library_id == "lib-1" assert task.status == GenerationTaskStatus.PENDING @@ -117,7 +113,6 @@ def test_generation_pipeline_smoke(): use_case = CreateGenerationTaskUseCase(task_repo) task = use_case.execute( CreateGenerationTaskCommand( - workspace_id="ws-1", project_id="proj-1", asset_library_id="lib-1", strategy_id="str-1", @@ -144,7 +139,6 @@ def test_generation_pipeline_smoke(): def test_get_generated_video_download_url(): video_repo = DummyGeneratedVideoRepository() video = GeneratedVideo.create( - workspace_id="ws-1", project_id="proj-1", generation_task_id="task-1", name="task-1.mp4", @@ -165,7 +159,6 @@ def test_get_generated_video_download_url(): def test_generated_video_download_source_url_is_stable(): video_repo = DummyGeneratedVideoRepository() video = GeneratedVideo.create( - workspace_id="ws-1", project_id="proj-1", generation_task_id="task-2", name="task-2.mp4", diff --git a/tests/integration/test_ingest_pipeline.py b/tests/integration/test_ingest_pipeline.py index 9a62ac1ce..c424975e5 100644 --- a/tests/integration/test_ingest_pipeline.py +++ b/tests/integration/test_ingest_pipeline.py @@ -38,7 +38,6 @@ def simulate_ingest_asset( # Create Asset asset = Asset.create( - workspace_id=job.workspace_id, project_id=job.project_id, library_id=job.library_id, name=filename, @@ -80,7 +79,6 @@ def test_ingest_asset_pipeline(): use_case = SubmitIngestJobUseCase(job_repo) job = use_case.execute( SubmitIngestJobCommand( - workspace_id="ws-1", project_id="proj-1", library_id="lib-1", storage_key="uploads/test-video.mp4", diff --git a/tests/integration/test_project_management.py b/tests/integration/test_project_management.py index d86606272..d2e57d200 100644 --- a/tests/integration/test_project_management.py +++ b/tests/integration/test_project_management.py @@ -27,7 +27,6 @@ def test_create_task(): task = use_case.execute( project_id="proj_1", - workspace_id="ws_1", name="开发登录功能", description="实现用户登录功能", priority=TaskPriority.HIGH, @@ -48,12 +47,10 @@ def test_list_tasks(): # 创建两个任务 create_use_case.execute( project_id="proj_1", - workspace_id="ws_1", name="任务1", ) create_use_case.execute( project_id="proj_1", - workspace_id="ws_1", name="任务2", ) @@ -75,7 +72,6 @@ def test_update_task_status(): # 创建任务 task = create_use_case.execute( project_id="proj_1", - workspace_id="ws_1", name="测试任务", ) @@ -95,7 +91,6 @@ def test_update_task_progress(): # 创建任务 task = create_use_case.execute( project_id="proj_1", - workspace_id="ws_1", name="测试任务", ) @@ -120,7 +115,6 @@ def test_create_milestone(): milestone = use_case.execute( project_id="proj_1", - workspace_id="ws_1", name="V1.0 发布", description="第一个正式版本", ) @@ -141,7 +135,6 @@ def test_create_and_resolve_issue(): issue = create_use_case.execute( task_id="task_1", project_id="proj_1", - workspace_id="ws_1", title="接口报错", description="调用登录接口返回 500", ) @@ -170,21 +163,18 @@ def test_task_hierarchy(): # 创建父任务 parent_task = create_use_case.execute( project_id="proj_1", - workspace_id="ws_1", name="开发用户模块", ) # 创建子任务 child_task_1 = create_use_case.execute( project_id="proj_1", - workspace_id="ws_1", name="登录功能", parent_task_id=parent_task.id, ) child_task_2 = create_use_case.execute( project_id="proj_1", - workspace_id="ws_1", name="注册功能", parent_task_id=parent_task.id, ) @@ -208,7 +198,6 @@ def test_get_task_detail(): # 创建任务 task = create_use_case.execute( project_id="proj_1", - workspace_id="ws_1", name="测试任务", description="这是一个测试任务", ) @@ -239,7 +228,6 @@ def test_update_task(): # 创建任务 task = create_use_case.execute( project_id="proj_1", - workspace_id="ws_1", name="原始任务", description="原始描述", priority="low", diff --git a/tests/integration/test_projects.py b/tests/integration/test_projects.py index b0fe52dcd..993e4d6d4 100644 --- a/tests/integration/test_projects.py +++ b/tests/integration/test_projects.py @@ -26,9 +26,7 @@ def test_create_and_list_projects(): create_use_case = CreateProjectUseCase(repository) list_use_case = ListProjectsUseCase(repository) - project = create_use_case.execute(CreateProjectCommand(workspace_id="ws-1", name=" Demo Project ")) assert project.name == "Demo Project" - assert project.workspace_id == "ws-1" items = list_use_case.execute("ws-1") assert len(items) == 1 @@ -40,12 +38,10 @@ def test_get_project_by_id_restores_workspace_context(): create_use_case = CreateProjectUseCase(repository) get_use_case = GetProjectUseCase(repository) - project = create_use_case.execute(CreateProjectCommand(workspace_id="ws-1", name="Demo Project")) retrieved = get_use_case.execute(project.id) assert retrieved is not None assert retrieved.id == project.id - assert retrieved.workspace_id == "ws-1" def test_create_and_list_asset_libraries(): @@ -55,7 +51,6 @@ def test_create_and_list_asset_libraries(): library = create_use_case.execute( CreateAssetLibraryCommand( - workspace_id="ws-1", project_id="proj-1", name=" 素材库 A ", kind=AssetLibraryKind.VIDEO, @@ -76,7 +71,6 @@ def test_create_and_list_assets(): asset = create_use_case.execute( CreateAssetCommand( - workspace_id="ws-1", project_id="proj-1", library_id="lib-1", name=" demo.mp4 ", @@ -100,13 +94,11 @@ def test_submit_ingest_job(): job = use_case.execute( SubmitIngestJobCommand( - workspace_id="ws-1", project_id="proj-1", library_id="lib-1", storage_key="uploads/raw-video.mp4", ) ) - assert job.workspace_id == "ws-1" assert job.storage_key == "uploads/raw-video.mp4" assert job.status == IngestJobStatus.PENDING assert job.result_asset_id == "" diff --git a/tests/integration/test_sqlalchemy_repositories.py b/tests/integration/test_sqlalchemy_repositories.py index 4c01d9fa0..0d50bd041 100644 --- a/tests/integration/test_sqlalchemy_repositories.py +++ b/tests/integration/test_sqlalchemy_repositories.py @@ -24,14 +24,12 @@ def test_sqlalchemy_project_repository(): # Create project project = use_case.execute( CreateProjectCommand( - workspace_id="ws-1", name="Test Project", description="Test description", ) ) assert project.name == "Test Project" - assert project.workspace_id == "ws-1" # List projects projects = repository.list_by_workspace("ws-1") diff --git a/tests/integration/test_upload_pipeline.py b/tests/integration/test_upload_pipeline.py index 9cee070fa..9501cfdce 100644 --- a/tests/integration/test_upload_pipeline.py +++ b/tests/integration/test_upload_pipeline.py @@ -7,7 +7,6 @@ from packages.domain import IngestJobStatus def simulate_upload_and_ingest( - workspace_id: str, project_id: str, library_id: str, filename: str, @@ -26,7 +25,6 @@ def simulate_upload_and_ingest( use_case = SubmitIngestJobUseCase(job_repo) job = use_case.execute( SubmitIngestJobCommand( - workspace_id=workspace_id, project_id=project_id, library_id=library_id, storage_key=storage_key, @@ -50,7 +48,6 @@ def test_upload_to_asset_full_pipeline(): # Simulate upload result = simulate_upload_and_ingest( - workspace_id="ws-1", project_id="proj-1", library_id="lib-1", filename="demo-video.mp4", diff --git a/tests/unit/test_accept_invitation_use_case.py b/tests/unit/test_accept_invitation_use_case.py index fb000e43f..d3e97bf09 100644 --- a/tests/unit/test_accept_invitation_use_case.py +++ b/tests/unit/test_accept_invitation_use_case.py @@ -87,7 +87,6 @@ class TestAcceptInvitationUseCase: def valid_invitation(self): return WorkspaceInvitation( id="invitation-1", - workspace_id="workspace-123", inviter_user_id="inviter-id", invitee_email="invitee@test.com", role="member", @@ -122,7 +121,6 @@ class TestAcceptInvitationUseCase: assert error is None assert response is not None - assert response.workspace_id == "workspace-123" assert response.workspace_name == "Test Workspace" assert response.role == "member" @@ -159,7 +157,6 @@ class TestAcceptInvitationUseCase: assert error is None assert response is not None - assert response.workspace_id == "workspace-123" def test_accept_invitation_invalid_token(self, use_case, mock_invitation_repo): """测试无效令牌""" @@ -262,7 +259,6 @@ class TestAcceptInvitationUseCase: existing_member = WorkspaceMember( id="member-1", - workspace_id="workspace-123", user_id="user-123", role="admin", ) @@ -306,7 +302,6 @@ class TestDeclineInvitationUseCase: def valid_invitation(self): return WorkspaceInvitation( id="invitation-1", - workspace_id="workspace-123", inviter_user_id="inviter-id", invitee_email="invitee@test.com", role="member", diff --git a/tests/unit/test_asset_diagnosis.py b/tests/unit/test_asset_diagnosis.py index 555af670b..084c4e0d3 100644 --- a/tests/unit/test_asset_diagnosis.py +++ b/tests/unit/test_asset_diagnosis.py @@ -10,7 +10,6 @@ from packages.domain import Asset, AssetStatus, ClassificationStatus def _asset(name: str, mime_type: str, *, status=AssetStatus.READY, duration=None, quality_score=None): return Asset.create( - workspace_id="workspace-1", project_id="project-1", library_id="library-1", name=name, diff --git a/tests/unit/test_asset_library_repository.py b/tests/unit/test_asset_library_repository.py index 76b2d0320..3297a48f5 100644 --- a/tests/unit/test_asset_library_repository.py +++ b/tests/unit/test_asset_library_repository.py @@ -8,7 +8,6 @@ def test_asset_library_repository_get_returns_library(): session.add( AssetLibraryModel( id="lib-1", - workspace_id="workspace-1", project_id="project-1", name="素材库", kind="video", @@ -22,7 +21,6 @@ def test_asset_library_repository_get_returns_library(): assert library is not None assert library.id == "lib-1" - assert library.workspace_id == "workspace-1" assert library.project_id == "project-1" assert library.kind.value == "video" diff --git a/tests/unit/test_asset_review.py b/tests/unit/test_asset_review.py index d6f27fc3e..c8e71438a 100644 --- a/tests/unit/test_asset_review.py +++ b/tests/unit/test_asset_review.py @@ -10,7 +10,6 @@ from packages.domain import Asset, AssetStatus def test_apply_asset_review_status_preserves_existing_metadata(): asset = Asset.create( - workspace_id="workspace-1", project_id="project-1", library_id="library-1", name="video.mp4", diff --git a/tests/unit/test_auth_middleware.py b/tests/unit/test_auth_middleware.py index a956cbce8..2209c5626 100644 --- a/tests/unit/test_auth_middleware.py +++ b/tests/unit/test_auth_middleware.py @@ -83,9 +83,7 @@ def test_legacy_middleware_optional_user_returns_user_with_valid_credentials(): def test_workspace_dependency_allows_member_access(): repo = _WorkspaceMemberRepositoryStub(role="member") - workspace_id, role = require_workspace_access("workspace-1", _repo_with_user().find_by_id("user-1"), repo) - assert workspace_id == "workspace-1" assert role == "member" @@ -102,12 +100,10 @@ class _WorkspaceMemberRepositoryStub: def __init__(self, role=None): self.role = role - def find_by_workspace_and_user(self, workspace_id, user_id): if self.role is None: return None from packages.domain.entities import WorkspaceMember - return WorkspaceMember(id="member-1", workspace_id=workspace_id, user_id=user_id, role=self.role) async def _authenticated_user(): diff --git a/tests/unit/test_generated_video_management.py b/tests/unit/test_generated_video_management.py index acc00c67d..6fc7fd1d8 100644 --- a/tests/unit/test_generated_video_management.py +++ b/tests/unit/test_generated_video_management.py @@ -21,7 +21,6 @@ def _repository(): def test_generated_video_repository_preserves_management_fields(): repository = _repository() video = GeneratedVideo.create( - workspace_id="workspace-1", project_id="project-1", generation_task_id="task-1", name="output.mp4", diff --git a/tests/unit/test_generation_asset_usage.py b/tests/unit/test_generation_asset_usage.py index 0c90fa9ca..0948d7007 100644 --- a/tests/unit/test_generation_asset_usage.py +++ b/tests/unit/test_generation_asset_usage.py @@ -10,7 +10,6 @@ from packages.domain import Asset, AssetStatus def _asset() -> Asset: return Asset.create( - workspace_id="workspace-1", project_id="project-1", library_id="library-1", name="video.mp4", diff --git a/tests/unit/test_generation_preflight.py b/tests/unit/test_generation_preflight.py index 58195e5de..1754c7422 100644 --- a/tests/unit/test_generation_preflight.py +++ b/tests/unit/test_generation_preflight.py @@ -13,7 +13,6 @@ from packages.domain import Asset, AssetStatus def _asset(name: str, mime_type: str, status: AssetStatus) -> Asset: return Asset.create( - workspace_id="workspace-1", project_id="project-1", library_id="library-1", name=name, diff --git a/tests/unit/test_invite_member_use_case.py b/tests/unit/test_invite_member_use_case.py index 4dbc91539..8f5ca3373 100644 --- a/tests/unit/test_invite_member_use_case.py +++ b/tests/unit/test_invite_member_use_case.py @@ -89,7 +89,6 @@ class TestInviteMemberUseCase: def owner_member(self): return WorkspaceMember( id="member-1", - workspace_id="workspace-123", user_id="inviter-id", role=WorkspaceMemberRole.OWNER, ) @@ -98,7 +97,6 @@ class TestInviteMemberUseCase: def admin_member(self): return WorkspaceMember( id="member-2", - workspace_id="workspace-123", user_id="inviter-id", role=WorkspaceMemberRole.ADMIN, ) @@ -120,7 +118,6 @@ class TestInviteMemberUseCase: mock_user_repo.find_by_id.return_value = test_inviter request = InviteMemberRequest( - workspace_id="workspace-123", inviter_user_id="inviter-id", invitee_email="newuser@test.com", role="member", @@ -160,7 +157,6 @@ class TestInviteMemberUseCase: mock_user_repo.find_by_id.return_value = test_inviter request = InviteMemberRequest( - workspace_id="workspace-123", inviter_user_id="inviter-id", invitee_email="newuser@test.com", role="viewer", @@ -176,7 +172,6 @@ class TestInviteMemberUseCase: mock_workspace_repo.find_by_id.return_value = None request = InviteMemberRequest( - workspace_id="nonexistent", inviter_user_id="inviter-id", invitee_email="newuser@test.com", role="member", @@ -199,7 +194,6 @@ class TestInviteMemberUseCase: mock_member_repo.find_by_workspace_and_user.return_value = None request = InviteMemberRequest( - workspace_id="workspace-123", inviter_user_id="nonmember-id", invitee_email="newuser@test.com", role="member", @@ -222,14 +216,12 @@ class TestInviteMemberUseCase: regular_member = WorkspaceMember( id="member-3", - workspace_id="workspace-123", user_id="inviter-id", role=WorkspaceMemberRole.MEMBER, ) mock_member_repo.find_by_workspace_and_user.return_value = regular_member request = InviteMemberRequest( - workspace_id="workspace-123", inviter_user_id="inviter-id", invitee_email="newuser@test.com", role="member", @@ -255,7 +247,6 @@ class TestInviteMemberUseCase: owner_member, # 第一次调用:inviter 是 owner WorkspaceMember( # 第二次调用:invitee 已经是成员 id="member-4", - workspace_id="workspace-123", user_id="existing-user-id", role=WorkspaceMemberRole.MEMBER, ), @@ -270,7 +261,6 @@ class TestInviteMemberUseCase: mock_user_repo.find_by_email.return_value = existing_user request = InviteMemberRequest( - workspace_id="workspace-123", inviter_user_id="inviter-id", invitee_email="existing@test.com", role="member", @@ -284,7 +274,6 @@ class TestInviteMemberUseCase: def test_invite_member_invalid_role(self, use_case): """测试无效角色(不能邀请为 owner)""" request = InviteMemberRequest( - workspace_id="workspace-123", inviter_user_id="inviter-id", invitee_email="newuser@test.com", role="owner", diff --git a/tests/unit/test_jwt_service.py b/tests/unit/test_jwt_service.py index b0b8793ee..61679459d 100644 --- a/tests/unit/test_jwt_service.py +++ b/tests/unit/test_jwt_service.py @@ -22,7 +22,6 @@ class TestJWTService: def test_create_access_token(self, jwt_service): """测试创建 access_token""" - token = jwt_service.create_access_token(user_id="user-123", workspace_id="workspace-456", role="admin") assert isinstance(token, str) assert len(token) > 0 @@ -30,7 +29,6 @@ class TestJWTService: # 验证 Token 内容 payload = jwt_service.verify_access_token(token) assert payload["sub"] == "user-123" - assert payload["workspace_id"] == "workspace-456" assert payload["role"] == "admin" assert payload["type"] == TokenType.ACCESS @@ -49,11 +47,9 @@ class TestJWTService: def test_verify_valid_access_token(self, jwt_service): """测试验证有效的 access_token""" - token = jwt_service.create_access_token(user_id="user-123", workspace_id="workspace-456", role="member") payload = jwt_service.verify_access_token(token) assert payload["sub"] == "user-123" - assert payload["workspace_id"] == "workspace-456" assert payload["role"] == "member" def test_verify_expired_token(self, jwt_service): @@ -64,7 +60,6 @@ class TestJWTService: config.ACCESS_TOKEN_EXPIRE_MINUTES = -1 # 负数,立即过期 expired_service = JWTService(config) - token = expired_service.create_access_token(user_id="user-123", workspace_id="workspace-456", role="admin") # 验证应该抛出过期异常 with pytest.raises(ExpiredSignatureError): @@ -87,14 +82,12 @@ class TestJWTService: jwt_service.verify_access_token(refresh_token) # 反过来也一样 - access_token = jwt_service.create_access_token(user_id="user-123", workspace_id="workspace-456", role="admin") with pytest.raises(ValueError, match="Token type must be 'refresh'"): jwt_service.verify_refresh_token(access_token) def test_verify_tampered_token(self, jwt_service): """测试验证被篡改的 Token""" - token = jwt_service.create_access_token(user_id="user-123", workspace_id="workspace-456", role="admin") # 篡改 Token(修改最后几个字符) tampered_token = token[:-5] + "XXXXX" @@ -106,7 +99,6 @@ class TestJWTService: """测试额外的声明""" token = jwt_service.create_access_token( user_id="user-123", - workspace_id="workspace-456", role="admin", additional_claims={ "email": "user@example.com", @@ -120,7 +112,6 @@ class TestJWTService: def test_decode_unsafe(self, jwt_service): """测试不安全解码(不验证签名)""" - token = jwt_service.create_access_token(user_id="user-123", workspace_id="workspace-456", role="admin") # 不验证签名地解码 payload = jwt_service.decode_token_unsafe(token) diff --git a/tests/unit/test_list_members_use_case.py b/tests/unit/test_list_members_use_case.py index aeb33501f..13ddfe736 100644 --- a/tests/unit/test_list_members_use_case.py +++ b/tests/unit/test_list_members_use_case.py @@ -71,7 +71,6 @@ class TestListMembersUseCase: # 请求者是 Admin requester_member = WorkspaceMember( id="member-1", - workspace_id="workspace-123", user_id="admin-id", role=WorkspaceMemberRole.ADMIN, ) @@ -80,7 +79,6 @@ class TestListMembersUseCase: # 3 个成员 member1 = WorkspaceMember( id="member-1", - workspace_id="workspace-123", user_id="owner-id", role=WorkspaceMemberRole.OWNER, invited_by=None, @@ -88,7 +86,6 @@ class TestListMembersUseCase: member2 = WorkspaceMember( id="member-2", - workspace_id="workspace-123", user_id="admin-id", role=WorkspaceMemberRole.ADMIN, invited_by="owner-id", @@ -96,7 +93,6 @@ class TestListMembersUseCase: member3 = WorkspaceMember( id="member-3", - workspace_id="workspace-123", user_id="user-id", role=WorkspaceMemberRole.MEMBER, invited_by="admin-id", @@ -129,7 +125,6 @@ class TestListMembersUseCase: mock_user_repo.find_by_id.side_effect = [user1, user2, user3] request = ListMembersRequest( - workspace_id="workspace-123", requester_user_id="admin-id", ) @@ -168,7 +163,6 @@ class TestListMembersUseCase: mock_workspace_repo.find_by_id.return_value = None request = ListMembersRequest( - workspace_id="nonexistent", requester_user_id="user-id", ) @@ -189,7 +183,6 @@ class TestListMembersUseCase: mock_member_repo.find_by_workspace_and_user.return_value = None request = ListMembersRequest( - workspace_id="workspace-123", requester_user_id="outsider-id", ) @@ -210,7 +203,6 @@ class TestListMembersUseCase: requester_member = WorkspaceMember( id="member-1", - workspace_id="workspace-123", user_id="user-id", role=WorkspaceMemberRole.MEMBER, ) @@ -218,7 +210,6 @@ class TestListMembersUseCase: mock_member_repo.find_by_workspace.return_value = [] request = ListMembersRequest( - workspace_id="workspace-123", requester_user_id="user-id", ) @@ -228,10 +219,8 @@ class TestListMembersUseCase: assert response is not None assert len(response.members) == 0 - def test_list_members_missing_workspace_id(self, use_case): """测试缺少工作空间 ID""" request = ListMembersRequest( - workspace_id="", requester_user_id="user-id", ) @@ -243,7 +232,6 @@ class TestListMembersUseCase: def test_list_members_missing_requester_id(self, use_case): """测试缺少请求者 ID""" request = ListMembersRequest( - workspace_id="workspace-123", requester_user_id="", ) diff --git a/tests/unit/test_list_workspaces_use_case.py b/tests/unit/test_list_workspaces_use_case.py deleted file mode 100644 index ca2334252..000000000 --- a/tests/unit/test_list_workspaces_use_case.py +++ /dev/null @@ -1,265 +0,0 @@ -""" -获取工作空间列表和详情 Use Case 测试 -""" - -from datetime import datetime, timezone -from unittest.mock import Mock - -import pytest - -from packages.application.workspace.list_workspaces_use_case import ( - GetWorkspaceDetailRequest, - GetWorkspaceDetailUseCase, - ListWorkspacesRequest, - ListWorkspacesUseCase, -) -from packages.domain.entities import Workspace, WorkspaceMember, WorkspaceMemberRole - - -class TestListWorkspacesUseCase: - """获取工作空间列表测试""" - - @pytest.fixture - def mock_workspace_repo(self): - repo = Mock() - repo.find_by_id = Mock(return_value=None) - return repo - - @pytest.fixture - def mock_member_repo(self): - repo = Mock() - repo.find_by_user = Mock(return_value=[]) - repo.count_by_workspace = Mock(return_value=0) - return repo - - @pytest.fixture - def use_case(self, mock_workspace_repo, mock_member_repo): - return ListWorkspacesUseCase( - workspace_repository=mock_workspace_repo, - workspace_member_repository=mock_member_repo, - ) - - def test_list_workspaces_success( - self, - use_case, - mock_workspace_repo, - mock_member_repo, - ): - """测试获取工作空间列表成功""" - # 用户是 2 个工作空间的成员 - membership1 = WorkspaceMember( - id="member-1", - workspace_id="workspace-1", - user_id="user-123", - role=WorkspaceMemberRole.OWNER, - ) - - membership2 = WorkspaceMember( - id="member-2", - workspace_id="workspace-2", - user_id="user-123", - role=WorkspaceMemberRole.MEMBER, - ) - - mock_member_repo.find_by_user.return_value = [membership1, membership2] - - workspace1 = Workspace( - id="workspace-1", - name="My Workspace", - owner_user_id="user-123", - subscription_plan="free", - ) - - workspace2 = Workspace( - id="workspace-2", - name="Team Workspace", - owner_user_id="other-user", - subscription_plan="pro", - ) - - mock_workspace_repo.find_by_id.side_effect = [workspace1, workspace2] - mock_member_repo.count_by_workspace.side_effect = [1, 5] - - request = ListWorkspacesRequest(user_id="user-123") - response, error = use_case.execute(request) - - assert error is None - assert response is not None - assert len(response.workspaces) == 2 - - # 验证第一个工作空间 - ws1 = response.workspaces[0] - assert ws1.workspace_id == "workspace-1" - assert ws1.name == "My Workspace" - assert ws1.user_role == "owner" - assert ws1.member_count == 1 - assert ws1.subscription_plan == "free" - - # 验证第二个工作空间 - ws2 = response.workspaces[1] - assert ws2.workspace_id == "workspace-2" - assert ws2.name == "Team Workspace" - assert ws2.user_role == "member" - assert ws2.member_count == 5 - assert ws2.subscription_plan == "pro" - - def test_list_workspaces_no_memberships( - self, - use_case, - mock_member_repo, - ): - """测试用户没有加入任何工作空间""" - mock_member_repo.find_by_user.return_value = [] - - request = ListWorkspacesRequest(user_id="user-123") - response, error = use_case.execute(request) - - assert error is None - assert response is not None - assert len(response.workspaces) == 0 - - def test_list_workspaces_missing_user_id(self, use_case): - """测试缺少用户 ID""" - request = ListWorkspacesRequest(user_id="") - response, error = use_case.execute(request) - - assert response is None - assert error == "User ID is required" - - -class TestGetWorkspaceDetailUseCase: - """获取工作空间详情测试""" - - @pytest.fixture - def mock_workspace_repo(self): - repo = Mock() - repo.find_by_id = Mock(return_value=None) - return repo - - @pytest.fixture - def mock_member_repo(self): - repo = Mock() - repo.find_by_workspace_and_user = Mock(return_value=None) - repo.count_by_workspace = Mock(return_value=0) - return repo - - @pytest.fixture - def use_case(self, mock_workspace_repo, mock_member_repo): - return GetWorkspaceDetailUseCase( - workspace_repository=mock_workspace_repo, - workspace_member_repository=mock_member_repo, - ) - - @pytest.fixture - def test_workspace(self): - return Workspace( - id="workspace-123", - name="Test Workspace", - owner_user_id="owner-id", - subscription_plan="pro", - subscription_status="active", - max_projects=999999, - max_storage_gb=100, - used_storage_gb=25.5, - ) - - def test_get_workspace_detail_success( - self, - use_case, - mock_workspace_repo, - mock_member_repo, - test_workspace, - ): - """测试获取工作空间详情成功""" - mock_workspace_repo.find_by_id.return_value = test_workspace - - membership = WorkspaceMember( - id="member-1", - workspace_id="workspace-123", - user_id="user-123", - role=WorkspaceMemberRole.ADMIN, - ) - mock_member_repo.find_by_workspace_and_user.return_value = membership - mock_member_repo.count_by_workspace.return_value = 8 - - request = GetWorkspaceDetailRequest( - workspace_id="workspace-123", - user_id="user-123", - ) - - detail, error = use_case.execute(request) - - assert error is None - assert detail is not None - assert detail.workspace_id == "workspace-123" - assert detail.name == "Test Workspace" - assert detail.subscription_plan == "pro" - assert detail.subscription_status == "active" - assert detail.max_projects == 999999 - assert detail.max_storage_gb == 100 - assert detail.used_storage_gb == 25.5 - assert detail.member_count == 8 - assert detail.user_role == "admin" - - def test_get_workspace_detail_not_found( - self, - use_case, - mock_workspace_repo, - ): - """测试工作空间不存在""" - mock_workspace_repo.find_by_id.return_value = None - - request = GetWorkspaceDetailRequest( - workspace_id="nonexistent", - user_id="user-123", - ) - - detail, error = use_case.execute(request) - - assert detail is None - assert error == "Workspace not found" - - def test_get_workspace_detail_not_member( - self, - use_case, - mock_workspace_repo, - mock_member_repo, - test_workspace, - ): - """测试用户不是成员""" - mock_workspace_repo.find_by_id.return_value = test_workspace - mock_member_repo.find_by_workspace_and_user.return_value = None - - request = GetWorkspaceDetailRequest( - workspace_id="workspace-123", - user_id="user-123", - ) - - detail, error = use_case.execute(request) - - assert detail is None - assert error == "You are not a member of this workspace" - - def test_get_workspace_detail_missing_workspace_id(self, use_case): - """测试缺少工作空间 ID""" - request = GetWorkspaceDetailRequest( - workspace_id="", - user_id="user-123", - ) - - detail, error = use_case.execute(request) - - assert detail is None - assert error == "Workspace ID is required" - - def test_get_workspace_detail_missing_user_id(self, use_case): - """测试缺少用户 ID""" - request = GetWorkspaceDetailRequest( - workspace_id="workspace-123", - user_id="", - ) - - detail, error = use_case.execute(request) - - assert detail is None - assert error == "User ID is required" diff --git a/tests/unit/test_permissions.py b/tests/unit/test_permissions.py index df86fdaff..b3df90f29 100644 --- a/tests/unit/test_permissions.py +++ b/tests/unit/test_permissions.py @@ -27,7 +27,6 @@ class TestPermissionChecker: """测试有访问权限""" member = WorkspaceMember( id="member-1", - workspace_id="workspace-123", user_id="user-123", role=WorkspaceMemberRole.MEMBER, ) @@ -51,7 +50,6 @@ class TestPermissionChecker: """测试是 Owner""" member = WorkspaceMember( id="member-1", - workspace_id="workspace-123", user_id="user-123", role=WorkspaceMemberRole.OWNER, ) @@ -65,7 +63,6 @@ class TestPermissionChecker: """测试不是 Owner""" member = WorkspaceMember( id="member-1", - workspace_id="workspace-123", user_id="user-123", role=WorkspaceMemberRole.ADMIN, ) @@ -79,7 +76,6 @@ class TestPermissionChecker: """测试是 Admin""" member = WorkspaceMember( id="member-1", - workspace_id="workspace-123", user_id="user-123", role=WorkspaceMemberRole.ADMIN, ) @@ -93,7 +89,6 @@ class TestPermissionChecker: """测试是 Owner""" member = WorkspaceMember( id="member-1", - workspace_id="workspace-123", user_id="user-123", role=WorkspaceMemberRole.OWNER, ) @@ -107,7 +102,6 @@ class TestPermissionChecker: """测试是普通成员""" member = WorkspaceMember( id="member-1", - workspace_id="workspace-123", user_id="user-123", role=WorkspaceMemberRole.MEMBER, ) @@ -121,7 +115,6 @@ class TestPermissionChecker: """测试 Member 可以创建项目""" member = WorkspaceMember( id="member-1", - workspace_id="workspace-123", user_id="user-123", role=WorkspaceMemberRole.MEMBER, ) @@ -135,7 +128,6 @@ class TestPermissionChecker: """测试 Viewer 不能创建项目""" member = WorkspaceMember( id="member-1", - workspace_id="workspace-123", user_id="user-123", role=WorkspaceMemberRole.VIEWER, ) @@ -149,7 +141,6 @@ class TestPermissionChecker: """测试 Member 不能删除项目""" member = WorkspaceMember( id="member-1", - workspace_id="workspace-123", user_id="user-123", role=WorkspaceMemberRole.MEMBER, ) @@ -163,7 +154,6 @@ class TestPermissionChecker: """测试 Admin 可以删除项目""" member = WorkspaceMember( id="member-1", - workspace_id="workspace-123", user_id="user-123", role=WorkspaceMemberRole.ADMIN, ) diff --git a/tests/unit/test_project_title_generation.py b/tests/unit/test_project_title_generation.py index 09b2dbe92..b9b3877ea 100644 --- a/tests/unit/test_project_title_generation.py +++ b/tests/unit/test_project_title_generation.py @@ -25,14 +25,12 @@ def _session_and_repository(): def test_select_title_prefers_favorite_then_lowest_usage(): _, repository = _session_and_repository() normal = repository.create( - workspace_id="workspace-1", project_id="project-1", text="普通标题", category="default", created_by_user_id="user-1", ) favorite = repository.create( - workspace_id="workspace-1", project_id="project-1", text="常用标题", category="default", @@ -52,7 +50,6 @@ def test_mark_title_used_after_generation_completion(): now = datetime.now(timezone.utc) title = ProjectTitleModel( id="title-1", - workspace_id="workspace-1", project_id="project-1", text="生成标题", category="default", @@ -68,7 +65,6 @@ def test_mark_title_used_after_generation_completion(): mark_title_used_for_generation( session, - SimpleNamespace(strategy_id="title-1", workspace_id="workspace-1", project_id="project-1"), ) updated = session.query(ProjectTitleModel).filter(ProjectTitleModel.id == "title-1").first() diff --git a/tests/unit/test_project_title_repository.py b/tests/unit/test_project_title_repository.py index 17e850352..bd07fbbf3 100644 --- a/tests/unit/test_project_title_repository.py +++ b/tests/unit/test_project_title_repository.py @@ -21,7 +21,6 @@ def test_project_title_repository_creates_and_lists_titles(): repository = _repository() title = repository.create( - workspace_id="workspace-1", project_id="project-1", text=" 3 分钟看懂产品亮点 ", category="marketing", @@ -40,7 +39,6 @@ def test_project_title_repository_creates_and_lists_titles(): def test_project_title_repository_filters_inactive_titles(): repository = _repository() title = repository.create( - workspace_id="workspace-1", project_id="project-1", text="停用标题", category="default", diff --git a/tests/unit/test_quota.py b/tests/unit/test_quota.py index f5f7eceb5..e0ab90586 100644 --- a/tests/unit/test_quota.py +++ b/tests/unit/test_quota.py @@ -163,7 +163,6 @@ class TestQuotaChecker: status = checker.get_quota_status("workspace-123") assert status is not None - assert status["workspace_id"] == "workspace-123" assert status["subscription_plan"] == "free" assert status["projects"]["used"] == 2 assert status["projects"]["limit"] == 3 diff --git a/tests/unit/test_remove_member_use_case.py b/tests/unit/test_remove_member_use_case.py index 555b4fdc6..613c7667a 100644 --- a/tests/unit/test_remove_member_use_case.py +++ b/tests/unit/test_remove_member_use_case.py @@ -58,14 +58,12 @@ class TestRemoveMemberUseCase: owner_member = WorkspaceMember( id="member-1", - workspace_id="workspace-123", user_id="owner-id", role=WorkspaceMemberRole.OWNER, ) target_member = WorkspaceMember( id="member-2", - workspace_id="workspace-123", user_id="target-id", role=WorkspaceMemberRole.MEMBER, ) @@ -76,7 +74,6 @@ class TestRemoveMemberUseCase: ] request = RemoveMemberRequest( - workspace_id="workspace-123", requester_user_id="owner-id", target_user_id="target-id", ) @@ -99,14 +96,12 @@ class TestRemoveMemberUseCase: admin_member = WorkspaceMember( id="member-1", - workspace_id="workspace-123", user_id="admin-id", role=WorkspaceMemberRole.ADMIN, ) target_member = WorkspaceMember( id="member-2", - workspace_id="workspace-123", user_id="target-id", role=WorkspaceMemberRole.VIEWER, ) @@ -117,7 +112,6 @@ class TestRemoveMemberUseCase: ] request = RemoveMemberRequest( - workspace_id="workspace-123", requester_user_id="admin-id", target_user_id="target-id", ) @@ -139,14 +133,12 @@ class TestRemoveMemberUseCase: admin_member = WorkspaceMember( id="member-1", - workspace_id="workspace-123", user_id="admin-id", role=WorkspaceMemberRole.ADMIN, ) owner_member = WorkspaceMember( id="member-2", - workspace_id="workspace-123", user_id="owner-id", role=WorkspaceMemberRole.OWNER, ) @@ -157,7 +149,6 @@ class TestRemoveMemberUseCase: ] request = RemoveMemberRequest( - workspace_id="workspace-123", requester_user_id="admin-id", target_user_id="owner-id", ) @@ -179,14 +170,12 @@ class TestRemoveMemberUseCase: admin_member1 = WorkspaceMember( id="member-1", - workspace_id="workspace-123", user_id="admin-id-1", role=WorkspaceMemberRole.ADMIN, ) admin_member2 = WorkspaceMember( id="member-2", - workspace_id="workspace-123", user_id="admin-id-2", role=WorkspaceMemberRole.ADMIN, ) @@ -197,7 +186,6 @@ class TestRemoveMemberUseCase: ] request = RemoveMemberRequest( - workspace_id="workspace-123", requester_user_id="admin-id-1", target_user_id="admin-id-2", ) @@ -219,7 +207,6 @@ class TestRemoveMemberUseCase: admin_member = WorkspaceMember( id="member-1", - workspace_id="workspace-123", user_id="admin-id", role=WorkspaceMemberRole.ADMIN, ) @@ -227,7 +214,6 @@ class TestRemoveMemberUseCase: mock_member_repo.find_by_workspace_and_user.return_value = admin_member request = RemoveMemberRequest( - workspace_id="workspace-123", requester_user_id="admin-id", target_user_id="admin-id", ) @@ -249,7 +235,6 @@ class TestRemoveMemberUseCase: regular_member = WorkspaceMember( id="member-1", - workspace_id="workspace-123", user_id="user-id", role=WorkspaceMemberRole.MEMBER, ) @@ -257,7 +242,6 @@ class TestRemoveMemberUseCase: mock_member_repo.find_by_workspace_and_user.return_value = regular_member request = RemoveMemberRequest( - workspace_id="workspace-123", requester_user_id="user-id", target_user_id="target-id", ) @@ -311,14 +295,12 @@ class TestLeaveWorkspaceUseCase: member = WorkspaceMember( id="member-1", - workspace_id="workspace-123", user_id="user-id", role=WorkspaceMemberRole.MEMBER, ) mock_member_repo.find_by_workspace_and_user.return_value = member request = LeaveWorkspaceRequest( - workspace_id="workspace-123", user_id="user-id", ) @@ -340,14 +322,12 @@ class TestLeaveWorkspaceUseCase: owner_member = WorkspaceMember( id="member-1", - workspace_id="workspace-123", user_id="owner-id", role=WorkspaceMemberRole.OWNER, ) mock_member_repo.find_by_workspace_and_user.return_value = owner_member request = LeaveWorkspaceRequest( - workspace_id="workspace-123", user_id="owner-id", ) @@ -368,7 +348,6 @@ class TestLeaveWorkspaceUseCase: mock_member_repo.find_by_workspace_and_user.return_value = None request = LeaveWorkspaceRequest( - workspace_id="workspace-123", user_id="user-id", ) diff --git a/tests/unit/test_subscription_use_case.py b/tests/unit/test_subscription_use_case.py index 77fae08e4..d5c914cd9 100644 --- a/tests/unit/test_subscription_use_case.py +++ b/tests/unit/test_subscription_use_case.py @@ -55,14 +55,12 @@ class TestUpgradeSubscriptionUseCase: owner_member = WorkspaceMember( id="member-1", - workspace_id="workspace-123", user_id="owner-id", role=WorkspaceMemberRole.OWNER, ) mock_member_repo.find_by_workspace_and_user.return_value = owner_member request = UpgradeSubscriptionRequest( - workspace_id="workspace-123", requester_user_id="owner-id", new_plan="pro", ) @@ -100,14 +98,12 @@ class TestUpgradeSubscriptionUseCase: owner_member = WorkspaceMember( id="member-1", - workspace_id="workspace-123", user_id="owner-id", role=WorkspaceMemberRole.OWNER, ) mock_member_repo.find_by_workspace_and_user.return_value = owner_member request = UpgradeSubscriptionRequest( - workspace_id="workspace-123", requester_user_id="owner-id", new_plan="enterprise", ) @@ -137,14 +133,12 @@ class TestUpgradeSubscriptionUseCase: owner_member = WorkspaceMember( id="member-1", - workspace_id="workspace-123", user_id="owner-id", role=WorkspaceMemberRole.OWNER, ) mock_member_repo.find_by_workspace_and_user.return_value = owner_member request = UpgradeSubscriptionRequest( - workspace_id="workspace-123", requester_user_id="owner-id", new_plan="free", ) @@ -171,14 +165,12 @@ class TestUpgradeSubscriptionUseCase: owner_member = WorkspaceMember( id="member-1", - workspace_id="workspace-123", user_id="owner-id", role=WorkspaceMemberRole.OWNER, ) mock_member_repo.find_by_workspace_and_user.return_value = owner_member request = UpgradeSubscriptionRequest( - workspace_id="workspace-123", requester_user_id="owner-id", new_plan="pro", ) @@ -205,14 +197,12 @@ class TestUpgradeSubscriptionUseCase: admin_member = WorkspaceMember( id="member-1", - workspace_id="workspace-123", user_id="admin-id", role=WorkspaceMemberRole.ADMIN, ) mock_member_repo.find_by_workspace_and_user.return_value = admin_member request = UpgradeSubscriptionRequest( - workspace_id="workspace-123", requester_user_id="admin-id", new_plan="pro", ) @@ -265,14 +255,12 @@ class TestCancelSubscriptionUseCase: owner_member = WorkspaceMember( id="member-1", - workspace_id="workspace-123", user_id="owner-id", role=WorkspaceMemberRole.OWNER, ) mock_member_repo.find_by_workspace_and_user.return_value = owner_member request = CancelSubscriptionRequest( - workspace_id="workspace-123", requester_user_id="owner-id", ) @@ -304,14 +292,12 @@ class TestCancelSubscriptionUseCase: owner_member = WorkspaceMember( id="member-1", - workspace_id="workspace-123", user_id="owner-id", role=WorkspaceMemberRole.OWNER, ) mock_member_repo.find_by_workspace_and_user.return_value = owner_member request = CancelSubscriptionRequest( - workspace_id="workspace-123", requester_user_id="owner-id", ) @@ -337,14 +323,12 @@ class TestCancelSubscriptionUseCase: admin_member = WorkspaceMember( id="member-1", - workspace_id="workspace-123", user_id="admin-id", role=WorkspaceMemberRole.ADMIN, ) mock_member_repo.find_by_workspace_and_user.return_value = admin_member request = CancelSubscriptionRequest( - workspace_id="workspace-123", requester_user_id="admin-id", ) diff --git a/tests/unit/test_task_center.py b/tests/unit/test_task_center.py index 84fcedef1..a078bca06 100644 --- a/tests/unit/test_task_center.py +++ b/tests/unit/test_task_center.py @@ -16,7 +16,6 @@ def test_humanize_task_error_for_media_failures(): def test_ingest_step_is_user_readable(): job = IngestJob.create( - workspace_id="workspace-1", project_id="project-1", library_id="library-1", storage_key="uploads/video.mp4", diff --git a/tests/unit/test_update_member_role_use_case.py b/tests/unit/test_update_member_role_use_case.py index 134bae1e5..da255c05d 100644 --- a/tests/unit/test_update_member_role_use_case.py +++ b/tests/unit/test_update_member_role_use_case.py @@ -56,14 +56,12 @@ class TestUpdateMemberRoleUseCase: owner_member = WorkspaceMember( id="member-1", - workspace_id="workspace-123", user_id="owner-id", role=WorkspaceMemberRole.OWNER, ) target_member = WorkspaceMember( id="member-2", - workspace_id="workspace-123", user_id="target-id", role=WorkspaceMemberRole.MEMBER, ) @@ -74,7 +72,6 @@ class TestUpdateMemberRoleUseCase: ] request = UpdateMemberRoleRequest( - workspace_id="workspace-123", requester_user_id="owner-id", target_user_id="target-id", new_role="admin", @@ -104,14 +101,12 @@ class TestUpdateMemberRoleUseCase: admin_member = WorkspaceMember( id="member-1", - workspace_id="workspace-123", user_id="admin-id", role=WorkspaceMemberRole.ADMIN, ) target_member = WorkspaceMember( id="member-2", - workspace_id="workspace-123", user_id="target-id", role=WorkspaceMemberRole.VIEWER, ) @@ -122,7 +117,6 @@ class TestUpdateMemberRoleUseCase: ] request = UpdateMemberRoleRequest( - workspace_id="workspace-123", requester_user_id="admin-id", target_user_id="target-id", new_role="member", @@ -145,14 +139,12 @@ class TestUpdateMemberRoleUseCase: admin_member = WorkspaceMember( id="member-1", - workspace_id="workspace-123", user_id="admin-id", role=WorkspaceMemberRole.ADMIN, ) owner_member = WorkspaceMember( id="member-2", - workspace_id="workspace-123", user_id="owner-id", role=WorkspaceMemberRole.OWNER, ) @@ -163,7 +155,6 @@ class TestUpdateMemberRoleUseCase: ] request = UpdateMemberRoleRequest( - workspace_id="workspace-123", requester_user_id="admin-id", target_user_id="owner-id", new_role="member", @@ -186,14 +177,12 @@ class TestUpdateMemberRoleUseCase: admin_member1 = WorkspaceMember( id="member-1", - workspace_id="workspace-123", user_id="admin-id-1", role=WorkspaceMemberRole.ADMIN, ) admin_member2 = WorkspaceMember( id="member-2", - workspace_id="workspace-123", user_id="admin-id-2", role=WorkspaceMemberRole.ADMIN, ) @@ -204,7 +193,6 @@ class TestUpdateMemberRoleUseCase: ] request = UpdateMemberRoleRequest( - workspace_id="workspace-123", requester_user_id="admin-id-1", target_user_id="admin-id-2", new_role="member", @@ -227,7 +215,6 @@ class TestUpdateMemberRoleUseCase: admin_member = WorkspaceMember( id="member-1", - workspace_id="workspace-123", user_id="admin-id", role=WorkspaceMemberRole.ADMIN, ) @@ -235,7 +222,6 @@ class TestUpdateMemberRoleUseCase: mock_member_repo.find_by_workspace_and_user.return_value = admin_member request = UpdateMemberRoleRequest( - workspace_id="workspace-123", requester_user_id="admin-id", target_user_id="admin-id", new_role="member", @@ -249,7 +235,6 @@ class TestUpdateMemberRoleUseCase: def test_update_role_invalid_role(self, use_case): """测试无效角色(不能改为 owner)""" request = UpdateMemberRoleRequest( - workspace_id="workspace-123", requester_user_id="admin-id", target_user_id="target-id", new_role="owner", @@ -272,14 +257,12 @@ class TestUpdateMemberRoleUseCase: owner_member = WorkspaceMember( id="member-1", - workspace_id="workspace-123", user_id="owner-id", role=WorkspaceMemberRole.OWNER, ) target_member = WorkspaceMember( id="member-2", - workspace_id="workspace-123", user_id="target-id", role=WorkspaceMemberRole.ADMIN, ) @@ -290,7 +273,6 @@ class TestUpdateMemberRoleUseCase: ] request = UpdateMemberRoleRequest( - workspace_id="workspace-123", requester_user_id="owner-id", target_user_id="target-id", new_role="admin", @@ -313,7 +295,6 @@ class TestUpdateMemberRoleUseCase: regular_member = WorkspaceMember( id="member-1", - workspace_id="workspace-123", user_id="user-id", role=WorkspaceMemberRole.MEMBER, ) @@ -321,7 +302,6 @@ class TestUpdateMemberRoleUseCase: mock_member_repo.find_by_workspace_and_user.return_value = regular_member request = UpdateMemberRoleRequest( - workspace_id="workspace-123", requester_user_id="user-id", target_user_id="target-id", new_role="admin", diff --git a/tests/unit/test_workspace_sqlalchemy_repositories.py b/tests/unit/test_workspace_sqlalchemy_repositories.py deleted file mode 100644 index b1853e99e..000000000 --- a/tests/unit/test_workspace_sqlalchemy_repositories.py +++ /dev/null @@ -1,98 +0,0 @@ -from sqlalchemy import create_engine -from sqlalchemy.orm import sessionmaker - -from packages.adapters.sqlalchemy_impl.models import Base -from packages.adapters.sqlalchemy_impl.user_repository import SQLAlchemyUserRepository -from packages.adapters.sqlalchemy_impl.workspace_invitation_repository import SQLAlchemyWorkspaceInvitationRepository -from packages.adapters.sqlalchemy_impl.workspace_member_repository import SQLAlchemyWorkspaceMemberRepository -from packages.adapters.sqlalchemy_impl.workspace_repository import SQLAlchemyWorkspaceRepository -from packages.application.workspace.create_workspace_use_case import CreateWorkspaceRequest, CreateWorkspaceUseCase -from packages.application.workspace.invite_member_use_case import InviteMemberRequest, InviteMemberUseCase -from packages.application.workspace.list_members_use_case import ListMembersRequest, ListMembersUseCase -from packages.application.workspace.list_workspaces_use_case import ListWorkspacesRequest, ListWorkspacesUseCase -from packages.application.auth.password_hasher import password_hasher -from packages.domain.entities import User - - -def test_workspace_create_list_invite_and_members_with_sqlalchemy_repositories(): - session = _sqlite_session() - user_repository = SQLAlchemyUserRepository(session) - workspace_repository = SQLAlchemyWorkspaceRepository(session) - workspace_member_repository = SQLAlchemyWorkspaceMemberRepository(session) - workspace_invitation_repository = SQLAlchemyWorkspaceInvitationRepository(session) - user_repository.save( - User( - id="user-1", - email="user@example.com", - username="user", - display_name="User", - password_hash=password_hasher.hash_password("Password1"), - ) - ) - - create_response, create_error = CreateWorkspaceUseCase( - workspace_repository=workspace_repository, - workspace_member_repository=workspace_member_repository, - user_repository=user_repository, - ).execute(CreateWorkspaceRequest(name="Main Workspace", owner_user_id="user-1", subscription_plan="free")) - - assert create_error is None - assert create_response is not None - assert create_response.name == "Main Workspace" - - list_response, list_error = ListWorkspacesUseCase( - workspace_repository=workspace_repository, - workspace_member_repository=workspace_member_repository, - ).execute(ListWorkspacesRequest(user_id="user-1")) - - assert list_error is None - assert list_response is not None - assert len(list_response.workspaces) == 1 - assert list_response.workspaces[0].workspace_id == create_response.workspace_id - assert list_response.workspaces[0].user_role == "owner" - assert list_response.workspaces[0].member_count == 1 - - invite_response, invite_error = InviteMemberUseCase( - workspace_repository=workspace_repository, - workspace_member_repository=workspace_member_repository, - workspace_invitation_repository=workspace_invitation_repository, - user_repository=user_repository, - email_service=_NoopEmailService(), - ).execute( - InviteMemberRequest( - workspace_id=create_response.workspace_id, - inviter_user_id="user-1", - invitee_email="Invitee@Example.com", - role="member", - ) - ) - - assert invite_error is None - assert invite_response is not None - assert invite_response.invitee_email == "invitee@example.com" - assert workspace_invitation_repository.find_pending_by_workspace_and_email( - create_response.workspace_id, "invitee@example.com" - ) - - members_response, members_error = ListMembersUseCase( - workspace_repository=workspace_repository, - workspace_member_repository=workspace_member_repository, - user_repository=user_repository, - ).execute(ListMembersRequest(workspace_id=create_response.workspace_id, requester_user_id="user-1")) - - assert members_error is None - assert members_response is not None - assert len(members_response.members) == 1 - assert members_response.members[0].role == "owner" - - -class _NoopEmailService: - def send_workspace_invitation_email(self, **kwargs): - return False, "disabled" - - -def _sqlite_session(): - engine = create_engine("sqlite:///:memory:") - Base.metadata.create_all(engine) - session_factory = sessionmaker(bind=engine) - return session_factory()