fix(api): enforce media result permission boundaries

This commit is contained in:
Xiaoxia AI
2026-06-23 09:30:09 +08:00
parent a02036f806
commit 0a0c73bbd7
4 changed files with 153 additions and 85 deletions
+30 -3
View File
@@ -1,8 +1,15 @@
from typing import Any
from app.dependencies import get_asset_repository
from app.auth import AuthenticatedUser, get_current_user
from app.api.routes.permissions import require_workspace_member
from app.dependencies import (
get_asset_library_repository,
get_asset_repository,
get_project_repository,
get_workspace_member_repository,
)
from app.schemas.asset import AssetResponse, CreateAssetRequest, ListAssetsResponse
from fastapi import APIRouter, Depends
from fastapi import APIRouter, Depends, HTTPException
from packages.application import (
CreateAssetCommand,
@@ -10,6 +17,7 @@ from packages.application import (
ListAssetsUseCase,
)
from packages.domain import AssetStatus, ClassificationStatus
from packages.ports.workspace_member_repository import WorkspaceMemberRepository
router = APIRouter()
@@ -41,8 +49,15 @@ def _to_asset_response(item) -> AssetResponse:
@router.get("", response_model=ListAssetsResponse)
def list_assets(
library_id: str,
authenticated_user: AuthenticatedUser = Depends(get_current_user),
asset_repository: Any = Depends(get_asset_repository),
asset_library_repository: Any = Depends(get_asset_library_repository),
workspace_member_repository: WorkspaceMemberRepository = Depends(get_workspace_member_repository),
) -> ListAssetsResponse:
library = asset_library_repository.get(library_id)
if library is None:
raise HTTPException(status_code=404, detail=f"AssetLibrary {library_id} not found")
require_workspace_member(library.workspace_id, authenticated_user, workspace_member_repository)
use_case = ListAssetsUseCase(asset_repository)
items = use_case.execute(library_id)
return ListAssetsResponse(items=[_to_asset_response(item) for item in items])
@@ -51,8 +66,20 @@ def list_assets(
@router.post("", response_model=AssetResponse)
def create_asset(
request: CreateAssetRequest,
authenticated_user: AuthenticatedUser = Depends(get_current_user),
asset_repository: Any = Depends(get_asset_repository),
asset_library_repository: Any = Depends(get_asset_library_repository),
project_repository: Any = Depends(get_project_repository),
workspace_member_repository: WorkspaceMemberRepository = Depends(get_workspace_member_repository),
) -> AssetResponse:
require_workspace_member(request.workspace_id, authenticated_user, workspace_member_repository)
project = project_repository.find_by_id(request.project_id)
if project is None or project.workspace_id != request.workspace_id:
raise HTTPException(status_code=404, detail=f"Project {request.project_id} not found")
library = asset_library_repository.get(request.library_id)
if library is None or library.workspace_id != request.workspace_id or library.project_id != request.project_id:
raise HTTPException(status_code=404, detail=f"AssetLibrary {request.library_id} not found")
use_case = CreateAssetUseCase(asset_repository)
item = use_case.execute(
CreateAssetCommand(
@@ -73,7 +100,7 @@ def create_asset(
status=AssetStatus(request.status),
classification_status=ClassificationStatus(request.classification_status),
quality_score=request.quality_score,
uploaded_by_user_id=request.uploaded_by_user_id,
uploaded_by_user_id=authenticated_user.user.id,
)
)
return _to_asset_response(item)
+43 -38
View File
@@ -1,7 +1,9 @@
from typing import Any
from app.auth import AuthenticatedUser, get_current_user
from app.api.routes.permissions import require_workspace_member
from app.core.storage import OSSStorageService, get_storage_service
from app.dependencies import get_generated_video_repository
from app.dependencies import get_generated_video_repository, get_project_repository, get_workspace_member_repository
from app.schemas.generated_video import (
GeneratedVideoDownloadUrlResponse,
GeneratedVideoResponse,
@@ -14,47 +16,12 @@ from packages.application import (
GetGeneratedVideoUseCase,
ListGeneratedVideosUseCase,
)
from packages.ports.workspace_member_repository import WorkspaceMemberRepository
router = APIRouter()
@router.get("", response_model=ListGeneratedVideosResponse)
def list_generated_videos(
project_id: str,
generated_video_repository: Any = Depends(get_generated_video_repository),
) -> ListGeneratedVideosResponse:
use_case = ListGeneratedVideosUseCase(generated_video_repository)
items = use_case.execute(project_id)
return ListGeneratedVideosResponse(
items=[
GeneratedVideoResponse(
id=item.id,
workspace_id=item.workspace_id,
project_id=item.project_id,
generation_task_id=item.generation_task_id,
name=item.name,
file_url=item.file_url,
file_size=item.file_size,
duration=item.duration,
thumbnail_url=item.thumbnail_url,
width=item.width,
height=item.height,
fps=item.fps,
)
for item in items
]
)
@router.get("/{video_id}", response_model=GeneratedVideoResponse)
def get_generated_video(
video_id: str,
generated_video_repository: Any = Depends(get_generated_video_repository),
) -> 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")
def _to_generated_video_response(item) -> GeneratedVideoResponse:
return GeneratedVideoResponse(
id=item.id,
workspace_id=item.workspace_id,
@@ -71,12 +38,50 @@ def get_generated_video(
)
@router.get("", response_model=ListGeneratedVideosResponse)
def list_generated_videos(
project_id: str,
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),
) -> 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)
return ListGeneratedVideosResponse(items=[_to_generated_video_response(item) for item in items])
@router.get("/{video_id}", response_model=GeneratedVideoResponse)
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),
) -> 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)
return _to_generated_video_response(item)
@router.get("/{video_id}/download-url", response_model=GeneratedVideoDownloadUrlResponse)
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:
+64 -44
View File
@@ -1,9 +1,14 @@
from typing import Any
from app.auth import AuthenticatedUser, get_current_user
from app.api.routes.permissions import require_workspace_member
from app.core.celery_app import celery_app
from app.dependencies import (
get_asset_library_repository,
get_generated_video_repository,
get_generation_task_repository,
get_project_repository,
get_workspace_member_repository,
)
from app.schemas.generated_video import (
GeneratedVideoResponse,
@@ -21,15 +26,60 @@ from packages.application import (
GetGenerationTaskUseCase,
ListGeneratedVideosByTaskUseCase,
)
from packages.ports.workspace_member_repository import WorkspaceMemberRepository
router = APIRouter()
def _to_generation_task_response(task) -> GenerationTaskResponse:
return GenerationTaskResponse(
id=task.id,
workspace_id=task.workspace_id,
project_id=task.project_id,
asset_library_id=task.asset_library_id,
strategy_id=task.strategy_id,
voice_library_id=task.voice_library_id,
status=task.status.value,
progress=task.progress,
result_count=task.result_count,
error_message=task.error_message,
)
def _to_generated_video_response(item) -> 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,
file_url=item.file_url,
file_size=item.file_size,
duration=item.duration,
thumbnail_url=item.thumbnail_url,
width=item.width,
height=item.height,
fps=item.fps,
)
@router.post("/tasks", response_model=GenerationTaskResponse)
def create_generation_task(
request: CreateGenerationTaskRequest,
authenticated_user: AuthenticatedUser = Depends(get_current_user),
generation_task_repository: Any = Depends(get_generation_task_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),
) -> GenerationTaskResponse:
require_workspace_member(request.workspace_id, authenticated_user, workspace_member_repository)
project = project_repository.find_by_id(request.project_id)
if project is None or project.workspace_id != request.workspace_id:
raise HTTPException(status_code=404, detail=f"Project {request.project_id} not found")
library = asset_library_repository.get(request.asset_library_id)
if library is None or library.workspace_id != request.workspace_id or library.project_id != request.project_id:
raise HTTPException(status_code=404, detail=f"AssetLibrary {request.asset_library_id} not found")
use_case = CreateGenerationTaskUseCase(generation_task_repository)
task = use_case.execute(
CreateGenerationTaskCommand(
@@ -38,70 +88,40 @@ def create_generation_task(
asset_library_id=request.asset_library_id,
strategy_id=request.strategy_id,
voice_library_id=request.voice_library_id,
created_by_user_id=request.created_by_user_id,
created_by_user_id=authenticated_user.user.id,
)
)
celery_app.send_task("worker.generate_video", args=[task.id])
return GenerationTaskResponse(
id=task.id,
workspace_id=task.workspace_id,
project_id=task.project_id,
asset_library_id=task.asset_library_id,
strategy_id=task.strategy_id,
voice_library_id=task.voice_library_id,
status=task.status.value,
progress=task.progress,
result_count=task.result_count,
error_message=task.error_message,
)
return _to_generation_task_response(task)
@router.get("/tasks/{task_id}", response_model=GenerationTaskResponse)
def get_generation_task(
task_id: str,
authenticated_user: AuthenticatedUser = Depends(get_current_user),
generation_task_repository: Any = Depends(get_generation_task_repository),
workspace_member_repository: WorkspaceMemberRepository = Depends(get_workspace_member_repository),
) -> GenerationTaskResponse:
use_case = GetGenerationTaskUseCase(generation_task_repository)
task = use_case.execute(task_id)
if task is None:
raise HTTPException(status_code=404, detail=f"GenerationTask {task_id} not found")
return GenerationTaskResponse(
id=task.id,
workspace_id=task.workspace_id,
project_id=task.project_id,
asset_library_id=task.asset_library_id,
strategy_id=task.strategy_id,
voice_library_id=task.voice_library_id,
status=task.status.value,
progress=task.progress,
result_count=task.result_count,
error_message=task.error_message,
)
require_workspace_member(task.workspace_id, authenticated_user, workspace_member_repository)
return _to_generation_task_response(task)
@router.get("/tasks/{task_id}/results", response_model=ListGeneratedVideosResponse)
def list_generation_results(
task_id: str,
authenticated_user: AuthenticatedUser = Depends(get_current_user),
generation_task_repository: Any = Depends(get_generation_task_repository),
generated_video_repository: Any = Depends(get_generated_video_repository),
workspace_member_repository: WorkspaceMemberRepository = Depends(get_workspace_member_repository),
) -> ListGeneratedVideosResponse:
task = generation_task_repository.get(task_id)
if task is None:
raise HTTPException(status_code=404, detail=f"GenerationTask {task_id} not found")
require_workspace_member(task.workspace_id, authenticated_user, workspace_member_repository)
use_case = ListGeneratedVideosByTaskUseCase(generated_video_repository)
items = use_case.execute(task_id)
return ListGeneratedVideosResponse(
items=[
GeneratedVideoResponse(
id=item.id,
workspace_id=item.workspace_id,
project_id=item.project_id,
generation_task_id=item.generation_task_id,
name=item.name,
file_url=item.file_url,
file_size=item.file_size,
duration=item.duration,
thumbnail_url=item.thumbnail_url,
width=item.width,
height=item.height,
fps=item.fps,
)
for item in items
]
)
return ListGeneratedVideosResponse(items=[_to_generated_video_response(item) for item in items])
+16
View File
@@ -0,0 +1,16 @@
from __future__ import annotations
from app.auth import AuthenticatedUser
from fastapi import HTTPException
from packages.ports.workspace_member_repository import WorkspaceMemberRepository
def require_workspace_member(
workspace_id: str,
authenticated_user: AuthenticatedUser,
workspace_member_repository: WorkspaceMemberRepository,
) -> None:
member = workspace_member_repository.find_by_workspace_and_user(workspace_id, authenticated_user.user.id)
if member is None:
raise HTTPException(status_code=403, detail="Workspace access denied")