From aa8cc6aff4ce8f359da557702ae58a073f6b123d Mon Sep 17 00:00:00 2001 From: liangxu Date: Mon, 27 Apr 2026 20:06:47 +0800 Subject: [PATCH] fix(publish): scope desktop publish task list and retry by creator user Now that desktop clients can be re-registered with a new id under the same user, listing/searching publish tasks by client.id missed the user's older history and the retry path forbade jobs created from a previous client. Switch the list/count queries to join desktop_publish_jobs and filter by created_by_user_id, and enforce the same ownership when retrying via desktop task. Qualify column names with the t. alias so the join doesn't break the existing ORDER BY clauses. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../tenant/app/desktop_task_service.go | 80 +++++++++++-------- .../tenant/app/publish_job_service.go | 32 +++++++- 2 files changed, 76 insertions(+), 36 deletions(-) diff --git a/server/internal/tenant/app/desktop_task_service.go b/server/internal/tenant/app/desktop_task_service.go index 3cb0e9e..c5e50ea 100644 --- a/server/internal/tenant/app/desktop_task_service.go +++ b/server/internal/tenant/app/desktop_task_service.go @@ -480,7 +480,7 @@ func (s *DesktopTaskService) ListPublishTasks(ctx context.Context, client *repos title, 0, 0, - `CASE WHEN status = 'in_progress' THEN 0 ELSE 1 END ASC, created_at ASC, desktop_id DESC`, + `CASE WHEN t.status = 'in_progress' THEN 0 ELSE 1 END ASC, t.created_at ASC, t.desktop_id DESC`, ) if err != nil { return nil, err @@ -517,7 +517,7 @@ func (s *DesktopTaskService) ListPublishTasks(ctx context.Context, client *repos title, remaining, historyOffset, - `updated_at DESC, desktop_id DESC`, + `t.updated_at DESC, t.desktop_id DESC`, ) if listErr != nil { return nil, listErr @@ -546,32 +546,38 @@ func (s *DesktopTaskService) listPublishTasksByStatuses( ) ([]DesktopTaskView, error) { query := ` SELECT - desktop_id, - job_id, - tenant_id, - workspace_id, - target_account_id, - target_client_id, - platform_id, - kind, - payload, - status, - dedup_key, - active_attempt_id, - lease_expires_at, - attempts, - result, - error, - created_at, - updated_at - FROM desktop_tasks - WHERE workspace_id = $1 - AND target_client_id = $2 - AND kind = 'publish' - AND status = ANY($3) - AND ($4 = '' OR COALESCE(payload->>'title', '') ILIKE '%' || $4 || '%') + t.desktop_id, + t.job_id, + t.tenant_id, + t.workspace_id, + t.target_account_id, + t.target_client_id, + t.platform_id, + t.kind, + t.payload, + t.status, + t.dedup_key, + t.active_attempt_id, + t.lease_expires_at, + t.attempts, + t.result, + t.error, + t.created_at, + t.updated_at + FROM desktop_tasks AS t + JOIN desktop_publish_jobs AS j + ON j.desktop_id = t.job_id + AND j.workspace_id = t.workspace_id + WHERE t.workspace_id = $1 + AND j.created_by_user_id = $2 + AND t.kind = 'publish' + AND t.status = ANY($3) + AND ( + $4 = '' + OR COALESCE(t.payload->>'title', j.title, '') ILIKE '%' || $4 || '%' + ) ` - args := []any{client.WorkspaceID, client.ID, statuses, title} + args := []any{client.WorkspaceID, client.UserID, statuses, title} if strings.TrimSpace(orderBy) != "" { query += "\nORDER BY " + orderBy @@ -599,13 +605,19 @@ func (s *DesktopTaskService) countPublishTasksByStatuses( var count int err := s.pool.QueryRow(ctx, ` SELECT COUNT(1) - FROM desktop_tasks - WHERE workspace_id = $1 - AND target_client_id = $2 - AND kind = 'publish' - AND status = ANY($3) - AND ($4 = '' OR COALESCE(payload->>'title', '') ILIKE '%' || $4 || '%') - `, client.WorkspaceID, client.ID, statuses, title).Scan(&count) + FROM desktop_tasks AS t + JOIN desktop_publish_jobs AS j + ON j.desktop_id = t.job_id + AND j.workspace_id = t.workspace_id + WHERE t.workspace_id = $1 + AND j.created_by_user_id = $2 + AND t.kind = 'publish' + AND t.status = ANY($3) + AND ( + $4 = '' + OR COALESCE(t.payload->>'title', j.title, '') ILIKE '%' || $4 || '%' + ) + `, client.WorkspaceID, client.UserID, statuses, title).Scan(&count) if err != nil { return 0, response.ErrInternal(50109, "desktop_publish_tasks_query_failed", "failed to list desktop publish tasks") } diff --git a/server/internal/tenant/app/publish_job_service.go b/server/internal/tenant/app/publish_job_service.go index 5bf4f1f..b2d1a47 100644 --- a/server/internal/tenant/app/publish_job_service.go +++ b/server/internal/tenant/app/publish_job_service.go @@ -281,8 +281,8 @@ func (s *PublishJobService) RetryByDesktopTask( if task.Kind != "publish" { return nil, response.ErrBadRequest(40095, "desktop_task_not_publish", "only publish tasks can be retried") } - if task.TargetClientID != client.ID { - return nil, response.ErrForbidden(40381, "desktop_task_not_target_client", "desktop task is assigned to another desktop client") + if err := s.authorizePublishTaskForClientUser(ctx, client, task); err != nil { + return nil, err } if task.Status == "queued" || task.Status == "in_progress" { return nil, response.ErrConflict(40994, "desktop_task_retry_conflict", "desktop task is still pending or running") @@ -314,6 +314,34 @@ func (s *PublishJobService) RetryByDesktopTask( }) } +func (s *PublishJobService) authorizePublishTaskForClientUser( + ctx context.Context, + client *repository.DesktopClient, + task *repository.DesktopTask, +) error { + if client == nil || task == nil { + return response.ErrUnauthorized(40108, "desktop_client_missing", "desktop client context is required") + } + + var ownsTask bool + err := s.pool.QueryRow(ctx, ` + SELECT EXISTS ( + SELECT 1 + FROM desktop_publish_jobs + WHERE desktop_id = $1 + AND workspace_id = $2 + AND created_by_user_id = $3 + ) + `, task.JobID, client.WorkspaceID, client.UserID).Scan(&ownsTask) + if err != nil { + return response.ErrInternal(50115, "desktop_publish_job_lookup_failed", "failed to validate desktop publish task ownership") + } + if !ownsTask { + return response.ErrForbidden(40384, "desktop_publish_task_not_owned", "desktop publish task belongs to another user") + } + return nil +} + func (s *PublishJobService) resolveRetryArticleID(ctx context.Context, tenantID int64, payload map[string]any) (int64, error) { if articleID, ok := resolveRetryArticleIDFromPayload(payload); ok { return articleID, nil