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:
Xiaoxia AI
2026-06-17 07:30:09 +08:00
parent 381ffb4ee9
commit 0abfc98be0
2 changed files with 381 additions and 22 deletions
+220 -22
View File
@@ -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
+161
View File
@@ -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