fix tenant account unbind flow
Desktop Client Build / Resolve Build Metadata (push) Successful in 28s
Frontend CI / Frontend (push) Successful in 2m53s
Backend CI / Backend (push) Successful in 16m10s
Desktop Client Build / Build Desktop Client (push) Successful in 23m44s
Desktop Client Build / Publish Client Artifacts to NAS (push) Successful in 50s

This commit is contained in:
2026-06-19 09:29:04 +08:00
parent 0cddb47055
commit 97ae601cfd
20 changed files with 561 additions and 15 deletions
@@ -193,6 +193,7 @@ var routeDocs = map[string]routeDoc{
// --- Tenant:账号 / 任务统一视图(管理员视角) ---
"GET /api/tenant/accounts": {"租户媒体账号列表", "运营人员在管理后台查看本租户下所有媒体账号。"},
"POST /api/tenant/accounts/:id/request-delete": {"申请删除媒体账号", "由后台发起异步删除,桌面端确认后真正解绑。"},
"DELETE /api/tenant/accounts/:id": {"解绑媒体账号节点", "解除指定运行节点上的媒体账号绑定,并取消发往该节点该账号的排队发布任务。"},
"POST /api/tenant/tasks/:id/reconcile": {"对账发布任务", "管理后台对发布任务做对账:状态校正、结果回填。"},
"POST /api/tenant/tasks/:id/cancel": {"取消发布任务(后台)", "管理员在后台取消还未派发或还在执行的发布任务。"},
"GET /api/tenant/publish-tasks": {"发布任务列表(后台)", "管理后台分页查询本 Workspace 下的发布队列与历史发送结果。"},
@@ -79,6 +79,12 @@ type DesktopAccountView struct {
OldestQueuedPublishTaskAt *time.Time `json:"oldest_queued_publish_task_at"`
}
type DesktopAccountDeleteResult struct {
Account DesktopAccountView `json:"account"`
CancelledQueuedPublishTasks int `json:"cancelled_queued_publish_task_count"`
CompletedServerSideUnbinding bool `json:"completed_server_side_unbinding"`
}
type UpsertDesktopAccountRequest struct {
Platform string `json:"platform" binding:"required"`
PlatformUID string `json:"platform_uid" binding:"required"`
@@ -412,6 +418,46 @@ func (s *DesktopAccountService) RequestDelete(ctx context.Context, actor auth.Ac
return &view, nil
}
func (s *DesktopAccountService) DeleteForActor(ctx context.Context, actor auth.Actor, desktopID uuid.UUID) (*DesktopAccountDeleteResult, error) {
if actor.PrimaryWorkspaceID == 0 || actor.UserID == 0 {
return nil, response.ErrUnauthorized(40132, "workspace_scope_missing", "workspace context is required")
}
if s.pool == nil {
return nil, response.ErrServiceUnavailable(50342, "desktop_publish_job_store_unavailable", "desktop publish job store is unavailable")
}
tx, err := s.pool.Begin(ctx)
if err != nil {
return nil, response.ErrInternal(50086, "desktop_account_delete_begin_failed", "failed to start desktop account delete")
}
defer tx.Rollback(ctx)
accountRepo := repository.NewDesktopAccountRepository(tx)
account, err := accountRepo.TombstoneForActor(ctx, desktopID, actor.PrimaryWorkspaceID, actor.UserID)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil, response.ErrNotFound(40481, "desktop_account_not_found", "desktop account not found")
}
return nil, response.ErrInternal(50085, "desktop_account_delete_failed", "failed to delete desktop account")
}
cancelled, err := cancelQueuedPublishTasksForDesktopAccountUnbind(ctx, tx, account.TenantID, account.WorkspaceID, account.DesktopID)
if err != nil {
return nil, err
}
if err := tx.Commit(ctx); err != nil {
return nil, response.ErrInternal(50086, "desktop_account_delete_commit_failed", "failed to commit desktop account delete")
}
view := s.buildDesktopAccountView(account, nil, nil, nil, nil)
return &DesktopAccountDeleteResult{
Account: view,
CancelledQueuedPublishTasks: cancelled,
CompletedServerSideUnbinding: true,
}, nil
}
func desktopAccountHealthLatestKey(workspaceID int64, accountID uuid.UUID) string {
return fmt.Sprintf("desktop:account-health:latest:%d:%s", workspaceID, accountID.String())
}
@@ -13,9 +13,12 @@ import (
)
type desktopAccountRepoSpy struct {
markDesktopID uuid.UUID
markWorkspaceID int64
markUserID int64
markDesktopID uuid.UUID
markWorkspaceID int64
markUserID int64
tombstoneActorDesktopID uuid.UUID
tombstoneActorWorkspaceID int64
tombstoneActorUserID int64
}
func (r *desktopAccountRepoSpy) ListByClient(context.Context, int64, uuid.UUID) ([]repository.DesktopAccount, error) {
@@ -46,6 +49,24 @@ func (r *desktopAccountRepoSpy) Tombstone(context.Context, uuid.UUID, int64, uui
return nil, nil
}
func (r *desktopAccountRepoSpy) TombstoneForActor(_ context.Context, desktopID uuid.UUID, workspaceID, userID int64) (*repository.DesktopAccount, error) {
r.tombstoneActorDesktopID = desktopID
r.tombstoneActorWorkspaceID = workspaceID
r.tombstoneActorUserID = userID
now := time.Date(2026, 6, 18, 9, 45, 0, 0, time.UTC)
return &repository.DesktopAccount{
DesktopID: desktopID,
TenantID: 10,
WorkspaceID: workspaceID,
UserID: userID,
Platform: "baijiahao",
PlatformUID: "351884467",
DisplayName: "Media Account",
Health: "live",
DeletedAt: &now,
}, nil
}
func (r *desktopAccountRepoSpy) MarkDeleteRequested(_ context.Context, desktopID uuid.UUID, workspaceID, userID int64) (*repository.DesktopAccount, error) {
r.markDesktopID = desktopID
r.markWorkspaceID = workspaceID
@@ -552,6 +552,15 @@ func (s *DesktopTaskService) leaseNextQueuedPublishTask(
AND dt.kind = 'publish'
AND dt.status = 'queued'
AND dt.attempts < $4
AND EXISTS (
SELECT 1
FROM platform_accounts AS pa
WHERE pa.desktop_id = dt.target_account_id
AND pa.workspace_id = dt.workspace_id
AND pa.client_id = dt.target_client_id
AND pa.deleted_at IS NULL
AND pa.delete_requested_at IS NULL
)
AND (
NOT EXISTS (
SELECT 1
@@ -607,6 +616,18 @@ func (s *DesktopTaskService) leaseQueuedDesktopTaskByID(
AND dt.workspace_id = $2
AND dt.status = 'queued'
AND (dt.kind <> 'publish' OR dt.attempts < $9)
AND (
dt.kind <> 'publish'
OR EXISTS (
SELECT 1
FROM platform_accounts AS pa
WHERE pa.desktop_id = dt.target_account_id
AND pa.workspace_id = dt.workspace_id
AND pa.client_id = dt.target_client_id
AND pa.deleted_at IS NULL
AND pa.delete_requested_at IS NULL
)
)
AND (
(
dt.kind = 'monitor'
@@ -157,6 +157,9 @@ func (s *PublishJobService) Create(ctx context.Context, actor auth.Actor, req Cr
}
return nil, response.ErrInternal(50098, "desktop_account_lookup_failed", "failed to load selected desktop account")
}
if account.DeletedAt != nil || account.DeleteRequestedAt != nil {
return nil, response.ErrBadRequest(40093, "desktop_account_not_found", "one or more selected desktop accounts are being unbound or no longer exist")
}
accountHealth := account.Health
if report, ok := runtimeHealth[account.DesktopID]; ok && strings.TrimSpace(report.Health) != "" {
accountHealth = effectiveDesktopAccountRuntimeHealth(report)
@@ -261,6 +261,137 @@ func cancelQueuedPublishTasksForUnavailableArticle(ctx context.Context, tx pgx.T
return nil
}
func cancelQueuedPublishTasksForDesktopAccountUnbind(
ctx context.Context,
tx pgx.Tx,
tenantID int64,
workspaceID int64,
accountID uuid.UUID,
) (int, error) {
if tx == nil || tenantID <= 0 || workspaceID <= 0 || accountID == uuid.Nil {
return 0, nil
}
errorJSON, err := marshalOptionalJSON(map[string]any{
"code": "desktop_account_unbound",
"message": "desktop account was unbound before publishing",
"source": "desktop_account_unbind",
})
if err != nil {
return 0, response.ErrInternal(50120, "publish_task_cancel_payload_failed", "failed to encode publish task cancellation payload")
}
rows, err := tx.Query(ctx, `
SELECT DISTINCT pr.id, pr.publish_batch_id, pr.article_id
FROM desktop_tasks dt
JOIN desktop_publish_jobs j
ON j.desktop_id = dt.job_id
AND j.tenant_id = dt.tenant_id
AND j.workspace_id = dt.workspace_id
JOIN publish_records pr
ON pr.id::text = dt.payload->>'publish_record_id'
AND pr.tenant_id = dt.tenant_id
WHERE dt.tenant_id = $1
AND dt.workspace_id = $2
AND dt.target_account_id = $3
AND dt.kind = 'publish'
AND dt.status = 'queued'
AND pr.status IN ('queued', 'publishing', 'pending', 'running')
`, tenantID, workspaceID, accountID)
if err != nil {
return 0, response.ErrInternal(50121, "publish_task_cancel_account_unbind_failed", "failed to inspect queued publish tasks for unbound account")
}
defer rows.Close()
recordIDs := make([]int64, 0)
batchIDs := make(map[int64]struct{})
articleIDs := make(map[int64]struct{})
for rows.Next() {
var (
recordID int64
batchID int64
articleID int64
)
if scanErr := rows.Scan(&recordID, &batchID, &articleID); scanErr != nil {
return 0, response.ErrInternal(50121, "publish_task_cancel_account_unbind_failed", "failed to parse queued publish task cancellation candidates")
}
recordIDs = append(recordIDs, recordID)
if batchID > 0 {
batchIDs[batchID] = struct{}{}
}
if articleID > 0 {
articleIDs[articleID] = struct{}{}
}
}
if err := rows.Err(); err != nil {
return 0, response.ErrInternal(50121, "publish_task_cancel_account_unbind_failed", "failed to iterate queued publish task cancellation candidates")
}
if len(recordIDs) > 0 {
if _, err := tx.Exec(ctx, `
UPDATE publish_records
SET status = 'cancelled',
error_message = COALESCE(NULLIF(error_message, ''), 'desktop_account_unbound'),
updated_at = NOW()
WHERE tenant_id = $1
AND id = ANY($2::bigint[])
AND status IN ('queued', 'publishing', 'pending', 'running')
`, tenantID, recordIDs); err != nil {
return 0, response.ErrInternal(50121, "publish_task_cancel_account_unbind_failed", "failed to cancel publish records for unbound account")
}
}
tag, err := tx.Exec(ctx, `
UPDATE desktop_tasks dt
SET status = 'aborted',
error = $4,
active_attempt_id = NULL,
lease_token_hash = NULL,
lease_expires_at = NULL,
updated_at = NOW()
FROM desktop_publish_jobs j
WHERE dt.job_id = j.desktop_id
AND dt.tenant_id = j.tenant_id
AND dt.workspace_id = j.workspace_id
AND dt.tenant_id = $1
AND dt.workspace_id = $2
AND dt.target_account_id = $3
AND dt.kind = 'publish'
AND dt.status = 'queued'
`, tenantID, workspaceID, accountID, errorJSON)
if err != nil {
return 0, response.ErrInternal(50121, "publish_task_cancel_account_unbind_failed", "failed to cancel queued publish tasks for unbound account")
}
for batchID := range batchIDs {
batchStatus, recalcErr := recalculatePublishBatchStatus(ctx, tx, batchID)
if recalcErr != nil {
return 0, recalcErr
}
if _, err := tx.Exec(ctx, `
UPDATE publish_batches
SET status = $1, updated_at = NOW()
WHERE id = $2
`, batchStatus, batchID); err != nil {
return 0, response.ErrInternal(50054, "publish_batch_update_failed", "failed to update publish batch")
}
}
for articleID := range articleIDs {
articleStatus, recalcErr := recalculateArticlePublishStatus(ctx, tx, tenantID, articleID)
if recalcErr != nil {
return 0, recalcErr
}
if _, err := tx.Exec(ctx, `
UPDATE articles
SET publish_status = $1, updated_at = NOW()
WHERE id = $2 AND tenant_id = $3 AND deleted_at IS NULL
`, articleStatus, articleID, tenantID); err != nil {
return 0, response.ErrInternal(50055, "article_publish_update_failed", "failed to update article publish status")
}
}
return int(tag.RowsAffected()), nil
}
func syncDesktopPublishTaskState(
ctx context.Context,
tx pgx.Tx,
@@ -514,6 +645,54 @@ func recalculatePublishBatchStatus(ctx context.Context, tx pgx.Tx, publishBatchI
}
}
func recalculateArticlePublishStatus(ctx context.Context, tx pgx.Tx, tenantID, articleID int64) (string, error) {
rows, err := tx.Query(ctx, `
SELECT status
FROM publish_records
WHERE tenant_id = $1 AND article_id = $2
`, tenantID, articleID)
if err != nil {
return "", response.ErrInternal(50055, "article_publish_status_query_failed", "failed to inspect article publish status")
}
defer rows.Close()
statuses := make([]string, 0)
for rows.Next() {
var status string
if scanErr := rows.Scan(&status); scanErr != nil {
return "", response.ErrInternal(50055, "article_publish_status_scan_failed", "failed to parse article publish status")
}
statuses = append(statuses, normalizePublishStatus(status))
}
if err := rows.Err(); err != nil {
return "", response.ErrInternal(50055, "article_publish_status_scan_failed", "failed to iterate article publish status")
}
if len(statuses) == 0 {
return "unpublished", nil
}
counts := map[string]int{
"success": 0,
"failed": 0,
"publishing": 0,
}
for _, status := range statuses {
counts[normalizePublishStatus(status)]++
}
switch {
case counts["publishing"] > 0:
return "publishing", nil
case counts["success"] > 0 && counts["failed"] > 0:
return "partial_success", nil
case counts["success"] > 0:
return "success", nil
default:
return "failed", nil
}
}
func resolvePublishArticleID(contentRef map[string]any) (int64, error) {
if articleID, ok := extractInt64FromMap(contentRef, "article_id", "id"); ok && articleID > 0 {
return articleID, nil
@@ -1,6 +1,9 @@
package app
import "testing"
import (
"context"
"testing"
)
func TestDesktopTaskErrorMessageMapsBaijiahaoRiskControl(t *testing.T) {
tests := []struct {
@@ -171,3 +174,15 @@ func TestPublishBatchStatusToArticleStatusKeepsPartialSuccess(t *testing.T) {
t.Fatalf("publishBatchStatusToArticleStatus(partial_success) = %q, want partial_success", got)
}
}
func TestRecalculateArticlePublishStatusDefaultsToUnpublished(t *testing.T) {
tx := &capturePublishDedupLookupTx{rows: emptyPublishDedupRows{}}
got, err := recalculateArticlePublishStatus(context.Background(), tx, 1, 2)
if err != nil {
t.Fatalf("recalculateArticlePublishStatus() error = %v", err)
}
if got != "unpublished" {
t.Fatalf("recalculateArticlePublishStatus() = %q, want unpublished", got)
}
}
@@ -68,6 +68,7 @@ type DesktopAccountRepository interface {
Upsert(ctx context.Context, params UpsertDesktopAccountParams) (*DesktopAccount, error)
Patch(ctx context.Context, params PatchDesktopAccountParams) (*DesktopAccount, error)
Tombstone(ctx context.Context, desktopID uuid.UUID, workspaceID int64, clientID uuid.UUID, ifSyncVersion int64) (*DesktopAccount, error)
TombstoneForActor(ctx context.Context, desktopID uuid.UUID, workspaceID, userID int64) (*DesktopAccount, error)
MarkDeleteRequested(ctx context.Context, desktopID uuid.UUID, workspaceID, userID int64) (*DesktopAccount, error)
ClearDeleteRequested(ctx context.Context, desktopID uuid.UUID, workspaceID, userID int64) (*DesktopAccount, error)
}
@@ -222,6 +223,18 @@ func (r *desktopAccountRepository) Tombstone(ctx context.Context, desktopID uuid
return desktopAccountFromTombstoneRow(row)
}
func (r *desktopAccountRepository) TombstoneForActor(ctx context.Context, desktopID uuid.UUID, workspaceID, userID int64) (*DesktopAccount, error) {
row, err := r.q.TombstoneDesktopAccountForActor(ctx, generated.TombstoneDesktopAccountForActorParams{
DesktopID: pgUUID(desktopID),
WorkspaceID: workspaceID,
UserID: userID,
})
if err != nil {
return nil, err
}
return desktopAccountFromTombstoneForActorRow(row)
}
func (r *desktopAccountRepository) MarkDeleteRequested(ctx context.Context, desktopID uuid.UUID, workspaceID, userID int64) (*DesktopAccount, error) {
row, err := r.q.MarkDesktopAccountDeleteRequested(ctx, generated.MarkDesktopAccountDeleteRequestedParams{
DesktopID: pgUUID(desktopID),
@@ -407,6 +420,29 @@ func desktopAccountFromTombstoneRow(row generated.TombstoneDesktopAccountRow) (*
)
}
func desktopAccountFromTombstoneForActorRow(row generated.TombstoneDesktopAccountForActorRow) (*DesktopAccount, error) {
return buildDesktopAccount(
row.DesktopID,
row.TenantID,
row.WorkspaceID,
row.UserID,
row.ClientID,
row.PlatformID,
row.PlatformUid,
row.AccountFingerprint,
row.DisplayName,
row.AvatarUrl,
row.Health,
row.VerifiedAt,
row.Tags,
row.SyncVersion,
row.DeletedAt,
row.DeleteRequestedAt,
row.CreatedAt,
row.UpdatedAt,
)
}
func desktopAccountFromMarkDeleteRow(row generated.MarkDesktopAccountDeleteRequestedRow) (*DesktopAccount, error) {
return buildDesktopAccount(
row.DesktopID,
@@ -723,6 +723,90 @@ func (q *Queries) TombstoneDesktopAccount(ctx context.Context, arg TombstoneDesk
return i, err
}
const tombstoneDesktopAccountForActor = `-- name: TombstoneDesktopAccountForActor :one
UPDATE platform_accounts
SET deleted_at = now(),
delete_requested_at = COALESCE(delete_requested_at, now()),
sync_version = sync_version + 1,
updated_at = now()
WHERE desktop_id = $1
AND workspace_id = $2
AND user_id = $3
AND deleted_at IS NULL
RETURNING
desktop_id,
tenant_id,
workspace_id,
user_id,
client_id,
platform_id,
platform_uid,
account_fingerprint,
display_name,
avatar_url,
health,
verified_at,
tags,
sync_version,
deleted_at,
delete_requested_at,
created_at,
updated_at
`
type TombstoneDesktopAccountForActorParams struct {
DesktopID pgtype.UUID `json:"desktop_id"`
WorkspaceID int64 `json:"workspace_id"`
UserID int64 `json:"user_id"`
}
type TombstoneDesktopAccountForActorRow struct {
DesktopID pgtype.UUID `json:"desktop_id"`
TenantID int64 `json:"tenant_id"`
WorkspaceID int64 `json:"workspace_id"`
UserID int64 `json:"user_id"`
ClientID pgtype.UUID `json:"client_id"`
PlatformID string `json:"platform_id"`
PlatformUid string `json:"platform_uid"`
AccountFingerprint pgtype.Text `json:"account_fingerprint"`
DisplayName string `json:"display_name"`
AvatarUrl pgtype.Text `json:"avatar_url"`
Health string `json:"health"`
VerifiedAt pgtype.Timestamptz `json:"verified_at"`
Tags []byte `json:"tags"`
SyncVersion int64 `json:"sync_version"`
DeletedAt pgtype.Timestamptz `json:"deleted_at"`
DeleteRequestedAt pgtype.Timestamptz `json:"delete_requested_at"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
}
func (q *Queries) TombstoneDesktopAccountForActor(ctx context.Context, arg TombstoneDesktopAccountForActorParams) (TombstoneDesktopAccountForActorRow, error) {
row := q.db.QueryRow(ctx, tombstoneDesktopAccountForActor, arg.DesktopID, arg.WorkspaceID, arg.UserID)
var i TombstoneDesktopAccountForActorRow
err := row.Scan(
&i.DesktopID,
&i.TenantID,
&i.WorkspaceID,
&i.UserID,
&i.ClientID,
&i.PlatformID,
&i.PlatformUid,
&i.AccountFingerprint,
&i.DisplayName,
&i.AvatarUrl,
&i.Health,
&i.VerifiedAt,
&i.Tags,
&i.SyncVersion,
&i.DeletedAt,
&i.DeleteRequestedAt,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const upsertDesktopAccount = `-- name: UpsertDesktopAccount :one
INSERT INTO platform_accounts (
desktop_id,
@@ -182,6 +182,7 @@ type Querier interface {
SoftDeleteScheduleTask(ctx context.Context, arg SoftDeleteScheduleTaskParams) error
TenantCancelDesktopTask(ctx context.Context, arg TenantCancelDesktopTaskParams) (DesktopTask, error)
TombstoneDesktopAccount(ctx context.Context, arg TombstoneDesktopAccountParams) (TombstoneDesktopAccountRow, error)
TombstoneDesktopAccountForActor(ctx context.Context, arg TombstoneDesktopAccountForActorParams) (TombstoneDesktopAccountForActorRow, error)
UpdateArticleCurrentVersion(ctx context.Context, arg UpdateArticleCurrentVersionParams) error
UpdateArticleGenerateStatus(ctx context.Context, arg UpdateArticleGenerateStatusParams) error
UpdateBrand(ctx context.Context, arg UpdateBrandParams) error
@@ -253,6 +253,36 @@ RETURNING
created_at,
updated_at;
-- name: TombstoneDesktopAccountForActor :one
UPDATE platform_accounts
SET deleted_at = now(),
delete_requested_at = COALESCE(delete_requested_at, now()),
sync_version = sync_version + 1,
updated_at = now()
WHERE desktop_id = sqlc.arg(desktop_id)
AND workspace_id = sqlc.arg(workspace_id)
AND user_id = sqlc.arg(user_id)
AND deleted_at IS NULL
RETURNING
desktop_id,
tenant_id,
workspace_id,
user_id,
client_id,
platform_id,
platform_uid,
account_fingerprint,
display_name,
avatar_url,
health,
verified_at,
tags,
sync_version,
deleted_at,
delete_requested_at,
created_at,
updated_at;
-- name: MarkDesktopAccountDeleteRequested :one
UPDATE platform_accounts
SET delete_requested_at = now(),
@@ -139,3 +139,18 @@ func (h *DesktopAccountHandler) RequestDelete(c *gin.Context) {
}
response.Success(c, data)
}
func (h *DesktopAccountHandler) TenantDelete(c *gin.Context) {
desktopID, err := uuid.Parse(c.Param("id"))
if err != nil {
response.Error(c, response.ErrBadRequest(40082, "invalid_desktop_account_id", "desktop account id must be a uuid"))
return
}
data, err := h.svc.DeleteForActor(c.Request.Context(), auth.MustActor(c.Request.Context()), desktopID)
if err != nil {
response.Error(c, err)
return
}
response.Success(c, data)
}
@@ -193,6 +193,15 @@ func (h *DesktopDispatchHandler) replayQueuedPublishTasks(ctx context.Context, c
AND dt.kind = 'publish'
AND dt.status = 'queued'
AND dt.attempts < 3
AND EXISTS (
SELECT 1
FROM platform_accounts AS pa
WHERE pa.desktop_id = dt.target_account_id
AND pa.workspace_id = dt.workspace_id
AND pa.client_id = dt.target_client_id
AND pa.deleted_at IS NULL
AND pa.delete_requested_at IS NULL
)
AND NOT EXISTS (
SELECT 1
FROM desktop_publish_jobs AS j
@@ -89,6 +89,7 @@ func RegisterRoutes(app *bootstrap.App) {
))
tenantProtected.GET("/accounts", desktopAccountHandler.TenantList)
tenantProtected.POST("/accounts/:id/request-delete", desktopAccountHandler.RequestDelete)
tenantProtected.DELETE("/accounts/:id", desktopAccountHandler.TenantDelete)
tenantProtected.POST("/tasks/:id/reconcile", desktopTaskHandler.Reconcile)
tenantProtected.POST("/tasks/:id/cancel", desktopTaskHandler.TenantCancel)
tenantProtected.GET("/publish-tasks", desktopTaskHandler.TenantListPublish)
@@ -29,6 +29,7 @@ func TestProtectedRoutes_RequireAuth(t *testing.T) {
{http.MethodPost, "/api/desktop/clients/register"},
{http.MethodGet, "/api/tenant/accounts"},
{http.MethodPost, "/api/tenant/accounts/:id/request-delete"},
{http.MethodDelete, "/api/tenant/accounts/:id"},
{http.MethodPost, "/api/tenant/tasks/:id/reconcile"},
{http.MethodPost, "/api/tenant/tasks/:id/cancel"},
{http.MethodGet, "/api/tenant/publish-tasks"},