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` + ) +}