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..d11f77eb183 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,13 @@ export const auth = betterAuth({
}
},
},
+ update: {
+ after: async (user) => {
+ if (user.banned) {
+ await disableUserResources(user.id)
+ }
+ },
+ },
},
account: {
create: {
diff --git a/apps/sim/lib/workflows/lifecycle.ts b/apps/sim/lib/workflows/lifecycle.ts
index c5ecbb639c7..df1638dce48 100644
--- a/apps/sim/lib/workflows/lifecycle.ts
+++ b/apps/sim/lib/workflows/lifecycle.ts
@@ -1,6 +1,7 @@
import { db } from '@sim/db'
import {
a2aAgent,
+ apiKey,
chat,
form,
webhook,
@@ -8,12 +9,14 @@ import {
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 { 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'
@@ -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`
+ )
+}