feat(api): add dependency injection container and auth middleware
- Implement DependencyContainer for all repositories and use cases - Create singleton pattern for repository instances - Factory methods for all use cases (auth + workspace) - Auth middleware: get_current_user with JWT verification - Permission middleware: require_workspace_access/admin/owner - Support optional authentication (get_current_user_optional) - Integrate with PermissionChecker and QuotaChecker Phase 4 Task 25/68 completed
This commit is contained in:
+220
-22
@@ -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
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user