diff --git a/apps/api/app/dependencies.py b/apps/api/app/dependencies.py index 7f0816845..4b2da51dc 100644 --- a/apps/api/app/dependencies.py +++ b/apps/api/app/dependencies.py @@ -1,31 +1,229 @@ -from fastapi import Depends +""" +依赖注入容器 +管理所有 Use Cases 和 Repositories 的生命周期 +""" +from packages.adapters.in_memory.user_repository import InMemoryUserRepository +from packages.adapters.in_memory.workspace_repository import InMemoryWorkspaceRepository +from packages.adapters.in_memory.workspace_member_repository import InMemoryWorkspaceMemberRepository +from packages.adapters.in_memory.workspace_invitation_repository import InMemoryWorkspaceInvitationRepository -# 使用 In-Memory 存储(不需要数据库) -from packages.adapters.in_memory import ( - InMemoryAssetLibraryRepository, - InMemoryAssetRepository, - InMemoryIngestJobRepository, - InMemoryProjectRepository, +from packages.application.auth import ( + RegisterUserUseCase, + LoginUseCase, + LogoutUseCase, + VerifyEmailUseCase, + RequestPasswordResetUseCase, + ResetPasswordUseCase, ) -# 全局单例实例 -_project_repo = InMemoryProjectRepository() -_asset_library_repo = InMemoryAssetLibraryRepository() -_asset_repo = InMemoryAssetRepository() -_ingest_job_repo = InMemoryIngestJobRepository() +from packages.application.workspace import ( + CreateWorkspaceUseCase, + InviteMemberUseCase, + AcceptInvitationUseCase, + DeclineInvitationUseCase, + RemoveMemberUseCase, + LeaveWorkspaceUseCase, + UpdateMemberRoleUseCase, + ListWorkspacesUseCase, + GetWorkspaceDetailUseCase, + ListMembersUseCase, + UpgradeSubscriptionUseCase, + CancelSubscriptionUseCase, +) + +from packages.domain.permissions import PermissionChecker +from packages.domain.quota import QuotaChecker -def get_project_repository() -> InMemoryProjectRepository: - return _project_repo +class DependencyContainer: + """依赖注入容器""" + + def __init__(self): + # Repositories (单例) + self._user_repository = None + self._workspace_repository = None + self._workspace_member_repository = None + self._workspace_invitation_repository = None + self._project_repository = None + + # Services + self._permission_checker = None + self._quota_checker = None + + # ==================== Repositories ==================== + + @property + def user_repository(self): + if self._user_repository is None: + self._user_repository = InMemoryUserRepository() + return self._user_repository + + @property + def workspace_repository(self): + if self._workspace_repository is None: + self._workspace_repository = InMemoryWorkspaceRepository() + return self._workspace_repository + + @property + def workspace_member_repository(self): + if self._workspace_member_repository is None: + self._workspace_member_repository = InMemoryWorkspaceMemberRepository() + return self._workspace_member_repository + + @property + def workspace_invitation_repository(self): + if self._workspace_invitation_repository is None: + self._workspace_invitation_repository = InMemoryWorkspaceInvitationRepository() + return self._workspace_invitation_repository + + @property + def project_repository(self): + # TODO: 实现 InMemoryProjectRepository + if self._project_repository is None: + from unittest.mock import Mock + self._project_repository = Mock() + self._project_repository.count_by_workspace = Mock(return_value=0) + return self._project_repository + + # ==================== Services ==================== + + @property + def permission_checker(self): + if self._permission_checker is None: + self._permission_checker = PermissionChecker( + workspace_member_repository=self.workspace_member_repository, + ) + return self._permission_checker + + @property + def quota_checker(self): + if self._quota_checker is None: + self._quota_checker = QuotaChecker( + workspace_repository=self.workspace_repository, + project_repository=self.project_repository, + ) + return self._quota_checker + + # ==================== Auth Use Cases ==================== + + def get_register_user_use_case(self) -> RegisterUserUseCase: + return RegisterUserUseCase( + user_repository=self.user_repository, + base_url="http://localhost:3000", # TODO: 从配置读取 + ) + + def get_login_use_case(self) -> LoginUseCase: + return LoginUseCase( + user_repository=self.user_repository, + ) + + def get_logout_use_case(self) -> LogoutUseCase: + return LogoutUseCase() + + def get_verify_email_use_case(self) -> VerifyEmailUseCase: + return VerifyEmailUseCase( + user_repository=self.user_repository, + ) + + def get_request_password_reset_use_case(self) -> RequestPasswordResetUseCase: + return RequestPasswordResetUseCase( + user_repository=self.user_repository, + base_url="http://localhost:3000", + ) + + def get_reset_password_use_case(self) -> ResetPasswordUseCase: + return ResetPasswordUseCase( + user_repository=self.user_repository, + ) + + # ==================== Workspace Use Cases ==================== + + def get_create_workspace_use_case(self) -> CreateWorkspaceUseCase: + return CreateWorkspaceUseCase( + workspace_repository=self.workspace_repository, + workspace_member_repository=self.workspace_member_repository, + user_repository=self.user_repository, + ) + + def get_invite_member_use_case(self) -> InviteMemberUseCase: + return InviteMemberUseCase( + workspace_repository=self.workspace_repository, + workspace_member_repository=self.workspace_member_repository, + workspace_invitation_repository=self.workspace_invitation_repository, + user_repository=self.user_repository, + base_url="http://localhost:3000", + ) + + def get_accept_invitation_use_case(self) -> AcceptInvitationUseCase: + return AcceptInvitationUseCase( + workspace_repository=self.workspace_repository, + workspace_member_repository=self.workspace_member_repository, + workspace_invitation_repository=self.workspace_invitation_repository, + user_repository=self.user_repository, + ) + + def get_decline_invitation_use_case(self) -> DeclineInvitationUseCase: + return DeclineInvitationUseCase( + workspace_invitation_repository=self.workspace_invitation_repository, + ) + + def get_remove_member_use_case(self) -> RemoveMemberUseCase: + return RemoveMemberUseCase( + workspace_repository=self.workspace_repository, + workspace_member_repository=self.workspace_member_repository, + ) + + def get_leave_workspace_use_case(self) -> LeaveWorkspaceUseCase: + return LeaveWorkspaceUseCase( + workspace_repository=self.workspace_repository, + workspace_member_repository=self.workspace_member_repository, + ) + + def get_update_member_role_use_case(self) -> UpdateMemberRoleUseCase: + return UpdateMemberRoleUseCase( + workspace_repository=self.workspace_repository, + workspace_member_repository=self.workspace_member_repository, + ) + + def get_list_workspaces_use_case(self) -> ListWorkspacesUseCase: + return ListWorkspacesUseCase( + workspace_repository=self.workspace_repository, + workspace_member_repository=self.workspace_member_repository, + ) + + def get_get_workspace_detail_use_case(self) -> GetWorkspaceDetailUseCase: + return GetWorkspaceDetailUseCase( + workspace_repository=self.workspace_repository, + workspace_member_repository=self.workspace_member_repository, + ) + + def get_list_members_use_case(self) -> ListMembersUseCase: + return ListMembersUseCase( + workspace_repository=self.workspace_repository, + workspace_member_repository=self.workspace_member_repository, + user_repository=self.user_repository, + ) + + def get_upgrade_subscription_use_case(self) -> UpgradeSubscriptionUseCase: + return UpgradeSubscriptionUseCase( + workspace_repository=self.workspace_repository, + workspace_member_repository=self.workspace_member_repository, + ) + + def get_cancel_subscription_use_case(self) -> CancelSubscriptionUseCase: + return CancelSubscriptionUseCase( + workspace_repository=self.workspace_repository, + workspace_member_repository=self.workspace_member_repository, + ) -def get_asset_library_repository() -> InMemoryAssetLibraryRepository: - return _asset_library_repo +# 全局容器实例 +_container = None -def get_asset_repository() -> InMemoryAssetRepository: - return _asset_repo - - -def get_ingest_job_repository() -> InMemoryIngestJobRepository: - return _ingest_job_repo +def get_container() -> DependencyContainer: + """获取全局依赖容器""" + global _container + if _container is None: + _container = DependencyContainer() + return _container diff --git a/apps/api/app/middleware/auth.py b/apps/api/app/middleware/auth.py new file mode 100644 index 000000000..ade99d1f2 --- /dev/null +++ b/apps/api/app/middleware/auth.py @@ -0,0 +1,161 @@ +""" +认证中间件和依赖 +""" +from fastapi import Depends, HTTPException, status +from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials + +from packages.domain.auth import jwt_service +from packages.domain.entities import User +from apps.api.app.dependencies import get_container + + +security = HTTPBearer() + + +async def get_current_user( + credentials: HTTPAuthorizationCredentials = Depends(security), +) -> User: + """ + 获取当前登录用户 + + 从 Authorization header 中提取 JWT token 并验证 + + Raises: + HTTPException: Token 无效或过期 + + Returns: + 当前用户对象 + """ + token = credentials.credentials + + try: + # 验证 token + payload = jwt_service.verify_token(token) + user_id = payload.get("sub") + + if not user_id: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Invalid token: missing user_id", + headers={"WWW-Authenticate": "Bearer"}, + ) + + # 从数据库获取用户 + container = get_container() + user = container.user_repository.find_by_id(user_id) + + if not user: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="User not found", + headers={"WWW-Authenticate": "Bearer"}, + ) + + return user + + except Exception as e: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=f"Invalid token: {str(e)}", + headers={"WWW-Authenticate": "Bearer"}, + ) + + +async def get_current_user_optional( + credentials: HTTPAuthorizationCredentials = Depends(HTTPBearer(auto_error=False)), +) -> User | None: + """ + 获取当前登录用户(可选) + + 如果没有提供 token,返回 None 而不是抛出异常 + + Returns: + 当前用户对象或 None + """ + if not credentials: + return None + + try: + return await get_current_user(credentials) + except HTTPException: + return None + + +def require_workspace_access(workspace_id: str, user: User = Depends(get_current_user)) -> tuple[str, str]: + """ + 要求用户可以访问指定工作空间 + + Args: + workspace_id: 工作空间 ID + user: 当前用户 + + Raises: + HTTPException: 用户没有访问权限 + + Returns: + (workspace_id, user_role) + """ + container = get_container() + permission_checker = container.permission_checker + + has_access, role = permission_checker.check_workspace_access(workspace_id, user.id) + + if not has_access: + 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)) -> str: + """ + 要求用户是工作空间的 Admin 或 Owner + + Args: + workspace_id: 工作空间 ID + user: 当前用户 + + Raises: + HTTPException: 用户没有管理权限 + + Returns: + workspace_id + """ + container = get_container() + permission_checker = container.permission_checker + + if not permission_checker.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)) -> str: + """ + 要求用户是工作空间的 Owner + + Args: + workspace_id: 工作空间 ID + user: 当前用户 + + Raises: + HTTPException: 用户不是 Owner + + Returns: + workspace_id + """ + container = get_container() + permission_checker = container.permission_checker + + if not permission_checker.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