From 0174f935c314452b7b9c8755cde321e62ce1ad49 Mon Sep 17 00:00:00 2001 From: Your Name <119736744+aborayan2022@users.noreply.github.com> Date: Fri, 20 Mar 2026 10:07:48 +0200 Subject: [PATCH] fix: resolve download 404 caused by file UUID / Celery task ID mismatch\n\nThe download route checked access using the file UUID from the URL,\nbut the session and usage_events only stored the Celery task ID.\nThese are different UUIDs, causing all downloads to return 404.\n\nFixes:\n- Add has_download_access() to check file_history table as fallback\n- Update assert_web/api_task_access to use file_history lookup\n- Remember file UUID in session when task status returns SUCCESS" --- backend/app/routes/tasks.py | 12 ++++++++++++ backend/app/services/account_service.py | 17 +++++++++++++++++ backend/app/services/policy_service.py | 11 ++++++++++- 3 files changed, 39 insertions(+), 1 deletion(-) diff --git a/backend/app/routes/tasks.py b/backend/app/routes/tasks.py index b2bddc0..4d28e33 100644 --- a/backend/app/routes/tasks.py +++ b/backend/app/routes/tasks.py @@ -11,6 +11,7 @@ from app.services.policy_service import ( resolve_api_actor, resolve_web_actor, ) +from app.utils.auth import remember_task_access tasks_bp = Blueprint("tasks", __name__) @@ -52,6 +53,17 @@ def get_task_status(task_id: str): task_result = result.result or {} response["result"] = task_result + # Remember the file UUID in the session so the download route can verify access. + # The download URL contains a different UUID than the Celery task ID. + download_url = task_result.get("download_url", "") + if download_url: + parts = download_url.split("/") + # URL format: /api/download// + if len(parts) >= 4: + file_uuid = parts[3] + if file_uuid != task_id: + remember_task_access(file_uuid) + elif result.state == "FAILURE": response["error"] = str(result.info) if result.info else "Task failed." diff --git a/backend/app/services/account_service.py b/backend/app/services/account_service.py index 88af715..5177439 100644 --- a/backend/app/services/account_service.py +++ b/backend/app/services/account_service.py @@ -678,6 +678,23 @@ def has_task_access(user_id: int, source: str, task_id: str) -> bool: return row is not None +def has_download_access(user_id: int, file_task_id: str) -> bool: + """Return whether one user owns a file_history entry whose download_url contains the given file task id.""" + pattern = f"/api/download/{file_task_id}/" + with _connect() as conn: + row = conn.execute( + """ + SELECT 1 + FROM file_history + WHERE user_id = ? AND download_url LIKE ? + LIMIT 1 + """, + (user_id, f"%{pattern}%"), + ).fetchone() + + return row is not None + + # --------------------------------------------------------------------------- # Password reset tokens # --------------------------------------------------------------------------- diff --git a/backend/app/services/policy_service.py b/backend/app/services/policy_service.py index 6de8406..3651c10 100644 --- a/backend/app/services/policy_service.py +++ b/backend/app/services/policy_service.py @@ -8,6 +8,7 @@ from app.services.account_service import ( get_api_key_actor, get_user_by_id, get_current_period_month, + has_download_access, has_task_access, normalize_plan, record_usage_event, @@ -227,8 +228,13 @@ def build_task_tracking_kwargs(actor: ActorContext) -> dict: def assert_api_task_access(actor: ActorContext, task_id: str): """Ensure one API actor can poll one task id.""" - if actor.user_id is None or not has_task_access(actor.user_id, "api", task_id): + if actor.user_id is None: raise PolicyError("Task not found.", 404) + if has_task_access(actor.user_id, "api", task_id): + return + if has_download_access(actor.user_id, task_id): + return + raise PolicyError("Task not found.", 404) def assert_web_task_access(actor: ActorContext, task_id: str): @@ -236,6 +242,9 @@ def assert_web_task_access(actor: ActorContext, task_id: str): if actor.user_id is not None and has_task_access(actor.user_id, "web", task_id): return + if actor.user_id is not None and has_download_access(actor.user_id, task_id): + return + if has_session_task_access(task_id): return