From 42ec224ee9b1416e065349a4f0dc541cec71333b Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Tue, 7 Apr 2026 17:30:50 -0700 Subject: [PATCH 1/3] fix(admin): delete workspaces on ban --- .../settings/components/admin/admin.tsx | 193 +++++++++--------- apps/sim/lib/auth/auth.ts | 13 ++ apps/sim/lib/workflows/lifecycle.ts | 29 +++ 3 files changed, 141 insertions(+), 94 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/admin/admin.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/admin/admin.tsx index 5105cb404f3..f316f1f1294 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/admin/admin.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/admin/admin.tsx @@ -227,123 +227,128 @@ export function Admin() {
- - {u.name || '—'} - - {u.email} - - {u.role || 'user'} - - - {u.banned ? ( - Banned - ) : ( - Active - )} - - - {u.id !== session?.user?.id && ( - <> - - - {u.banned ? ( +
+ + {u.name || '—'} + + {u.email} + + + {u.role || 'user'} + + + + {u.banned ? ( + Banned + ) : ( + Active + )} + + + {u.id !== session?.user?.id && ( + <> + - ) : banUserId === u.id ? ( -
- setBanReason(e.target.value)} - placeholder='Reason (optional)' - className='h-[28px] w-[120px] text-caption' - /> + {u.banned ? ( + ) : ( -
- ) : ( - - )} - - )} -
+ )} + + )} + +
+ {banUserId === u.id && !u.banned && ( +
+ setBanReason(e.target.value)} + placeholder='Reason (optional)' + className='h-[28px] flex-1 text-caption' + /> + +
+ )}
))} diff --git a/apps/sim/lib/auth/auth.ts b/apps/sim/lib/auth/auth.ts index 6aa989e720b..fb4f7b7f25f 100644 --- a/apps/sim/lib/auth/auth.ts +++ b/apps/sim/lib/auth/auth.ts @@ -80,6 +80,7 @@ import { quickValidateEmail } from '@/lib/messaging/email/validation' import { scheduleLifecycleEmail } from '@/lib/messaging/lifecycle' import { captureServerEvent } from '@/lib/posthog/server' import { syncAllWebhooksForCredentialSet } from '@/lib/webhooks/utils.server' +import { disableUserResources } from '@/lib/workflows/lifecycle' import { SSO_TRUSTED_PROVIDERS } from '@/ee/sso/constants' import { createAnonymousSession, ensureAnonymousUserExists } from './anonymous' @@ -241,6 +242,18 @@ export const auth = betterAuth({ } }, }, + update: { + after: async (user) => { + if (user.banned) { + disableUserResources(user.id).catch((error) => { + logger.error( + '[databaseHooks.user.update.after] Failed to disable banned user resources', + { userId: user.id, error } + ) + }) + } + }, + }, }, account: { create: { diff --git a/apps/sim/lib/workflows/lifecycle.ts b/apps/sim/lib/workflows/lifecycle.ts index c5ecbb639c7..786323bca61 100644 --- a/apps/sim/lib/workflows/lifecycle.ts +++ b/apps/sim/lib/workflows/lifecycle.ts @@ -1,10 +1,12 @@ import { db } from '@sim/db' import { a2aAgent, + apiKey, chat, form, webhook, workflow, + workspace, workflowDeploymentVersion, workflowMcpTool, workflowSchedule, @@ -12,6 +14,7 @@ import { import { createLogger } from '@sim/logger' import { and, eq, inArray, isNull } from 'drizzle-orm' import { env } from '@/lib/core/config/env' +import { generateRequestId } from '@/lib/core/utils/request' import { getRedisClient } from '@/lib/core/config/redis' import { PlatformEvents } from '@/lib/core/telemetry' import { mcpPubSub } from '@/lib/mcp/pubsub' @@ -361,3 +364,29 @@ export async function archiveWorkflowsByIdsInWorkspace( options ) } + +/** + * Disables all resources owned by a banned user by archiving every workspace + * they own (cascading to workflows, chats, forms, KBs, tables, files, etc.) + * and deleting their personal API keys. + */ +export async function disableUserResources(userId: string): Promise { + const requestId = generateRequestId() + logger.info(`[${requestId}] Disabling resources for banned user ${userId}`) + + const { archiveWorkspace } = await import('@/lib/workspaces/lifecycle') + + const ownedWorkspaces = await db + .select({ id: workspace.id }) + .from(workspace) + .where(and(eq(workspace.ownerId, userId), isNull(workspace.archivedAt))) + + await Promise.all([ + ...ownedWorkspaces.map((w) => archiveWorkspace(w.id, { requestId })), + db.delete(apiKey).where(eq(apiKey.userId, userId)), + ]) + + logger.info( + `[${requestId}] Disabled resources for user ${userId}: archived ${ownedWorkspaces.length} workspaces, deleted API keys` + ) +} From 1db614b3a1241d8c54076599df63870cbbbfe456 Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Tue, 7 Apr 2026 17:33:24 -0700 Subject: [PATCH 2/3] Fix lint --- apps/sim/lib/workflows/lifecycle.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/sim/lib/workflows/lifecycle.ts b/apps/sim/lib/workflows/lifecycle.ts index 786323bca61..df1638dce48 100644 --- a/apps/sim/lib/workflows/lifecycle.ts +++ b/apps/sim/lib/workflows/lifecycle.ts @@ -6,17 +6,17 @@ import { form, webhook, workflow, - workspace, workflowDeploymentVersion, workflowMcpTool, workflowSchedule, + workspace, } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { and, eq, inArray, isNull } from 'drizzle-orm' import { env } from '@/lib/core/config/env' -import { generateRequestId } from '@/lib/core/utils/request' import { getRedisClient } from '@/lib/core/config/redis' import { PlatformEvents } from '@/lib/core/telemetry' +import { generateRequestId } from '@/lib/core/utils/request' import { mcpPubSub } from '@/lib/mcp/pubsub' import { getWorkflowById } from '@/lib/workflows/utils' From eb96a7371aa39347649ff89de731a2c5d4c16b25 Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Tue, 7 Apr 2026 19:38:01 -0700 Subject: [PATCH 3/3] Wait until workspace deletion to return ban success --- apps/sim/lib/auth/auth.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/apps/sim/lib/auth/auth.ts b/apps/sim/lib/auth/auth.ts index fb4f7b7f25f..d11f77eb183 100644 --- a/apps/sim/lib/auth/auth.ts +++ b/apps/sim/lib/auth/auth.ts @@ -245,12 +245,7 @@ export const auth = betterAuth({ update: { after: async (user) => { if (user.banned) { - disableUserResources(user.id).catch((error) => { - logger.error( - '[databaseHooks.user.update.after] Failed to disable banned user resources', - { userId: user.id, error } - ) - }) + await disableUserResources(user.id) } }, },