From 96e3914a60be11e4b4705dd31c5136892063a3d5 Mon Sep 17 00:00:00 2001 From: waleed Date: Sat, 11 Apr 2026 19:16:09 -0700 Subject: [PATCH 01/13] feat(ee): enterprise feature flags, permission group platform controls, audit logs ui, delete account --- .../docs/content/docs/en/enterprise/index.mdx | 3 + .../app/api/permission-groups/[id]/route.ts | 4 + apps/sim/app/api/permission-groups/route.ts | 4 + .../api/settings/allowed-providers/route.ts | 14 + apps/sim/app/api/users/me/route.ts | 47 ++ .../[workspaceId]/settings/[section]/page.tsx | 3 +- .../settings/[section]/prefetch.ts | 23 + .../settings/[section]/settings.tsx | 3 +- .../settings/components/general/general.tsx | 98 +++ .../[workspaceId]/settings/layout.tsx | 2 +- .../[workspaceId]/settings/navigation.ts | 5 +- .../settings-sidebar/settings-sidebar.tsx | 218 ++++--- .../emcn/components/modal/modal.tsx | 4 +- apps/sim/components/icons.tsx | 2 +- .../components/access-control.tsx | 576 +++++++++++------- .../utils/permission-check.test.ts | 4 + .../ee/audit-logs/components/audit-logs.tsx | 292 ++++++--- apps/sim/ee/sso/hooks/sso.ts | 9 +- .../components/whitelabeling-settings.tsx | 21 +- apps/sim/hooks/queries/user-profile.ts | 18 + apps/sim/lib/core/config/env.ts | 8 + apps/sim/lib/core/config/feature-flags.ts | 23 + apps/sim/lib/permission-groups/types.ts | 14 + apps/sim/lib/posthog/events.ts | 2 + helm/sim/values.yaml | 8 + 25 files changed, 952 insertions(+), 453 deletions(-) create mode 100644 apps/sim/app/api/settings/allowed-providers/route.ts create mode 100644 apps/sim/app/api/users/me/route.ts diff --git a/apps/docs/content/docs/en/enterprise/index.mdx b/apps/docs/content/docs/en/enterprise/index.mdx index 69fc92e8a12..0cd2aa9dbae 100644 --- a/apps/docs/content/docs/en/enterprise/index.mdx +++ b/apps/docs/content/docs/en/enterprise/index.mdx @@ -69,6 +69,9 @@ For self-hosted deployments, enterprise features can be enabled via environment | `ACCESS_CONTROL_ENABLED`, `NEXT_PUBLIC_ACCESS_CONTROL_ENABLED` | Permission groups for access restrictions | | `SSO_ENABLED`, `NEXT_PUBLIC_SSO_ENABLED` | Single Sign-On with SAML/OIDC | | `CREDENTIAL_SETS_ENABLED`, `NEXT_PUBLIC_CREDENTIAL_SETS_ENABLED` | Polling Groups for email triggers | +| `INBOX_ENABLED`, `NEXT_PUBLIC_INBOX_ENABLED` | Sim Mailer inbox for outbound email | +| `WHITELABELING_ENABLED`, `NEXT_PUBLIC_WHITELABELING_ENABLED` | Custom branding and white-labeling | +| `AUDIT_LOGS_ENABLED`, `NEXT_PUBLIC_AUDIT_LOGS_ENABLED` | Audit logging for compliance and monitoring | | `DISABLE_INVITATIONS`, `NEXT_PUBLIC_DISABLE_INVITATIONS` | Globally disable workspace/organization invitations | ### Organization Management diff --git a/apps/sim/app/api/permission-groups/[id]/route.ts b/apps/sim/app/api/permission-groups/[id]/route.ts index 51cbe1222b6..591b70b83a4 100644 --- a/apps/sim/app/api/permission-groups/[id]/route.ts +++ b/apps/sim/app/api/permission-groups/[id]/route.ts @@ -21,7 +21,10 @@ const configSchema = z.object({ hideKnowledgeBaseTab: z.boolean().optional(), hideTablesTab: z.boolean().optional(), hideCopilot: z.boolean().optional(), + hideIntegrationsTab: z.boolean().optional(), + hideSecretsTab: z.boolean().optional(), hideApiKeysTab: z.boolean().optional(), + hideInboxTab: z.boolean().optional(), hideEnvironmentTab: z.boolean().optional(), hideFilesTab: z.boolean().optional(), disableMcpTools: z.boolean().optional(), @@ -29,6 +32,7 @@ const configSchema = z.object({ disableSkills: z.boolean().optional(), hideTemplates: z.boolean().optional(), disableInvitations: z.boolean().optional(), + disablePublicApi: z.boolean().optional(), hideDeployApi: z.boolean().optional(), hideDeployMcp: z.boolean().optional(), hideDeployA2a: z.boolean().optional(), diff --git a/apps/sim/app/api/permission-groups/route.ts b/apps/sim/app/api/permission-groups/route.ts index 9b88d482617..14197237bb6 100644 --- a/apps/sim/app/api/permission-groups/route.ts +++ b/apps/sim/app/api/permission-groups/route.ts @@ -23,7 +23,10 @@ const configSchema = z.object({ hideKnowledgeBaseTab: z.boolean().optional(), hideTablesTab: z.boolean().optional(), hideCopilot: z.boolean().optional(), + hideIntegrationsTab: z.boolean().optional(), + hideSecretsTab: z.boolean().optional(), hideApiKeysTab: z.boolean().optional(), + hideInboxTab: z.boolean().optional(), hideEnvironmentTab: z.boolean().optional(), hideFilesTab: z.boolean().optional(), disableMcpTools: z.boolean().optional(), @@ -31,6 +34,7 @@ const configSchema = z.object({ disableSkills: z.boolean().optional(), hideTemplates: z.boolean().optional(), disableInvitations: z.boolean().optional(), + disablePublicApi: z.boolean().optional(), hideDeployApi: z.boolean().optional(), hideDeployMcp: z.boolean().optional(), hideDeployA2a: z.boolean().optional(), diff --git a/apps/sim/app/api/settings/allowed-providers/route.ts b/apps/sim/app/api/settings/allowed-providers/route.ts new file mode 100644 index 00000000000..2880c9eca08 --- /dev/null +++ b/apps/sim/app/api/settings/allowed-providers/route.ts @@ -0,0 +1,14 @@ +import { NextResponse } from 'next/server' +import { getSession } from '@/lib/auth' +import { getBlacklistedProvidersFromEnv } from '@/lib/core/config/feature-flags' + +export async function GET() { + const session = await getSession() + if (!session?.user?.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + return NextResponse.json({ + blacklistedProviders: getBlacklistedProvidersFromEnv(), + }) +} diff --git a/apps/sim/app/api/users/me/route.ts b/apps/sim/app/api/users/me/route.ts new file mode 100644 index 00000000000..85c6c370e15 --- /dev/null +++ b/apps/sim/app/api/users/me/route.ts @@ -0,0 +1,47 @@ +import { db } from '@sim/db' +import { user, workspace } from '@sim/db/schema' +import { createLogger } from '@sim/logger' +import { and, eq, ne, sql } from 'drizzle-orm' +import { NextResponse } from 'next/server' +import { getSession } from '@/lib/auth' +import { generateRequestId } from '@/lib/core/utils/request' +import { captureServerEvent } from '@/lib/posthog/server' + +const logger = createLogger('DeleteUserAPI') + +export const dynamic = 'force-dynamic' + +export async function DELETE() { + const requestId = generateRequestId() + + try { + const session = await getSession() + + if (!session?.user?.id) { + logger.warn(`[${requestId}] Unauthorized account deletion attempt`) + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const userId = session.user.id + + captureServerEvent(userId, 'user_deleted', {}) + + await db.transaction(async (tx) => { + await tx + .update(workspace) + .set({ billedAccountUserId: sql`owner_id` }) + .where(and(eq(workspace.billedAccountUserId, userId), ne(workspace.ownerId, userId))) + + await tx.delete(workspace).where(eq(workspace.ownerId, userId)) + + await tx.delete(user).where(eq(user.id, userId)) + }) + + logger.info(`[${requestId}] User account deleted`, { userId }) + + return NextResponse.json({ success: true }) + } catch (error) { + logger.error(`[${requestId}] Account deletion error`, error) + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) + } +} diff --git a/apps/sim/app/workspace/[workspaceId]/settings/[section]/page.tsx b/apps/sim/app/workspace/[workspaceId]/settings/[section]/page.tsx index ca48abef01b..69da9112fcd 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/[section]/page.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/[section]/page.tsx @@ -2,7 +2,7 @@ import { dehydrate, HydrationBoundary } from '@tanstack/react-query' import type { Metadata } from 'next' import { getQueryClient } from '@/app/_shell/providers/get-query-client' import type { SettingsSection } from '@/app/workspace/[workspaceId]/settings/navigation' -import { prefetchGeneralSettings, prefetchUserProfile } from './prefetch' +import { prefetchGeneralSettings, prefetchSubscriptionData, prefetchUserProfile } from './prefetch' import { SettingsPage } from './settings' const SECTION_TITLES: Record = { @@ -46,6 +46,7 @@ export default async function SettingsSectionPage({ void prefetchGeneralSettings(queryClient) void prefetchUserProfile(queryClient) + void prefetchSubscriptionData(queryClient) return ( diff --git a/apps/sim/app/workspace/[workspaceId]/settings/[section]/prefetch.ts b/apps/sim/app/workspace/[workspaceId]/settings/[section]/prefetch.ts index 7ac50861e5b..d04d9481d1a 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/[section]/prefetch.ts +++ b/apps/sim/app/workspace/[workspaceId]/settings/[section]/prefetch.ts @@ -2,6 +2,7 @@ import type { QueryClient } from '@tanstack/react-query' import { headers } from 'next/headers' import { getInternalApiBaseUrl } from '@/lib/core/utils/urls' import { generalSettingsKeys, mapGeneralSettingsResponse } from '@/hooks/queries/general-settings' +import { subscriptionKeys } from '@/hooks/queries/subscription' import { mapUserProfileResponse, userProfileKeys } from '@/hooks/queries/user-profile' /** @@ -35,6 +36,28 @@ export function prefetchGeneralSettings(queryClient: QueryClient) { }) } +/** + * Prefetch subscription data server-side via internal API fetch. + * Uses the same query key as the client `useSubscriptionData` hook (with includeOrg=false) + * so data is shared via HydrationBoundary — ensuring the settings sidebar renders + * with the correct Team/Enterprise tabs on the first paint, with no flash. + */ +export function prefetchSubscriptionData(queryClient: QueryClient) { + return queryClient.prefetchQuery({ + queryKey: subscriptionKeys.user(false), + queryFn: async () => { + const fwdHeaders = await getForwardedHeaders() + const baseUrl = getInternalApiBaseUrl() + const response = await fetch(`${baseUrl}/api/billing?context=user`, { + headers: fwdHeaders, + }) + if (!response.ok) throw new Error(`Subscription prefetch failed: ${response.status}`) + return response.json() + }, + staleTime: 5 * 60 * 1000, + }) +} + /** * Prefetch user profile server-side via internal API fetch. * Uses the same query keys as the client `useUserProfile` hook diff --git a/apps/sim/app/workspace/[workspaceId]/settings/[section]/settings.tsx b/apps/sim/app/workspace/[workspaceId]/settings/[section]/settings.tsx index b6f439635b9..9610babe879 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/[section]/settings.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/[section]/settings.tsx @@ -6,6 +6,7 @@ import { useSearchParams } from 'next/navigation' import { usePostHog } from 'posthog-js/react' import { Skeleton } from '@/components/emcn' import { useSession } from '@/lib/auth/auth-client' +import { cn } from '@/lib/core/utils/cn' import { captureEvent } from '@/lib/posthog/client' import { AdminSkeleton } from '@/app/workspace/[workspaceId]/settings/components/admin/admin-skeleton' import { ApiKeysSkeleton } from '@/app/workspace/[workspaceId]/settings/components/api-keys/api-key-skeleton' @@ -198,7 +199,7 @@ export function SettingsPage({ section }: SettingsPageProps) { }, [effectiveSection, sessionLoading, posthog]) return ( -
+

{label}

{effectiveSection === 'general' && } {effectiveSection === 'integrations' && } diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/general/general.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/general/general.tsx index b55b588605e..24e790f293e 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/general/general.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/general/general.tsx @@ -28,6 +28,7 @@ import { useProfilePictureUpload } from '@/app/workspace/[workspaceId]/settings/ import { useBrandConfig } from '@/ee/whitelabeling' import { useGeneralSettings, useUpdateGeneralSetting } from '@/hooks/queries/general-settings' import { + useDeleteAccount, useResetPassword, useUpdateUserProfile, useUserProfile, @@ -79,6 +80,10 @@ export function General() { const [showResetPasswordModal, setShowResetPasswordModal] = useState(false) const resetPassword = useResetPassword() + const [showDeleteAccountModal, setShowDeleteAccountModal] = useState(false) + const [deleteConfirmText, setDeleteConfirmText] = useState('') + const deleteAccount = useDeleteAccount() + const [uploadError, setUploadError] = useState(null) const snapToGridValue = settings?.snapToGridSize ?? 0 @@ -166,6 +171,23 @@ export function General() { } } + const handleDeleteAccountConfirm = async () => { + deleteAccount.mutate(undefined, { + onSuccess: async () => { + try { + await Promise.all([signOut(), clearUserData()]) + router.push('/login') + } catch (error) { + logger.error('Error during account cleanup', { error }) + router.push('/login') + } + }, + onError: (error) => { + logger.error('Error deleting account:', error) + }, + }) + } + const handleResetPasswordConfirm = async () => { if (!profile?.email) return @@ -467,6 +489,20 @@ export function General() { time.

+ {isHosted && !isAuthDisabled && ( +
+
+ +

+ Permanently delete your account and all associated data. +

+
+ +
+ )} + {isTrainingEnabled && (
@@ -500,6 +536,68 @@ export function General() { )}
+ {/* Delete Account Confirmation Modal */} + { + setShowDeleteAccountModal(open) + if (!open) { + setDeleteConfirmText('') + deleteAccount.reset() + } + }} + > + + Delete Account + +

+ This will permanently delete your account and all associated data, including + workspaces, workflows, API keys, and execution history.{' '} + This action cannot be undone. +

+
+ + setDeleteConfirmText(e.target.value)} + className='w-full rounded-md border border-[var(--border)] bg-transparent px-3 py-2 text-[var(--text-primary)] text-sm placeholder:text-[var(--text-tertiary)] focus:border-[var(--border-1)] focus:outline-none' + placeholder='delete my account' + disabled={deleteAccount.isPending} + /> +
+ {deleteAccount.error && ( +

+ {deleteAccount.error.message} +

+ )} +
+ + + + +
+
+ {/* Password Reset Confirmation Modal */} diff --git a/apps/sim/app/workspace/[workspaceId]/settings/layout.tsx b/apps/sim/app/workspace/[workspaceId]/settings/layout.tsx index 0fab587de01..06cebe3773b 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/layout.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/layout.tsx @@ -1,7 +1,7 @@ export default function SettingsLayout({ children }: { children: React.ReactNode }) { return (
-
+
{children}
diff --git a/apps/sim/app/workspace/[workspaceId]/settings/navigation.ts b/apps/sim/app/workspace/[workspaceId]/settings/navigation.ts index ff25389fc0c..222bb1962aa 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/navigation.ts +++ b/apps/sim/app/workspace/[workspaceId]/settings/navigation.ts @@ -74,6 +74,8 @@ const isSSOEnabled = isTruthy(getEnv('NEXT_PUBLIC_SSO_ENABLED')) const isCredentialSetsEnabled = isTruthy(getEnv('NEXT_PUBLIC_CREDENTIAL_SETS_ENABLED')) const isAccessControlEnabled = isTruthy(getEnv('NEXT_PUBLIC_ACCESS_CONTROL_ENABLED')) const isInboxEnabled = isTruthy(getEnv('NEXT_PUBLIC_INBOX_ENABLED')) +const isWhitelabelingEnabled = isTruthy(getEnv('NEXT_PUBLIC_WHITELABELING_ENABLED')) +const isAuditLogsEnabled = isTruthy(getEnv('NEXT_PUBLIC_AUDIT_LOGS_ENABLED')) export const isBillingEnabled = isTruthy(getEnv('NEXT_PUBLIC_BILLING_ENABLED')) export { isCredentialSetsEnabled } @@ -106,6 +108,7 @@ export const allNavigationItems: NavigationItem[] = [ section: 'enterprise', requiresHosted: true, requiresEnterprise: true, + selfHostedOverride: isAuditLogsEnabled, }, { id: 'subscription', @@ -181,7 +184,7 @@ export const allNavigationItems: NavigationItem[] = [ section: 'enterprise', requiresHosted: true, requiresEnterprise: true, - selfHostedOverride: isBillingEnabled, + selfHostedOverride: isWhitelabelingEnabled, }, { id: 'admin', diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-sidebar/settings-sidebar.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-sidebar/settings-sidebar.tsx index ccb7cba760b..9c1bc3fafcb 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-sidebar/settings-sidebar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-sidebar/settings-sidebar.tsx @@ -34,7 +34,23 @@ import { usePermissionConfig } from '@/hooks/use-permission-config' import { useSettingsNavigation } from '@/hooks/use-settings-navigation' import { useSettingsDirtyStore } from '@/stores/settings/dirty/store' -const SKELETON_SECTIONS = [3, 2, 2] as const +const SKELETON_SECTIONS = sectionConfig + .map(({ key }) => + Math.min( + allNavigationItems.filter( + (item) => + item.section === key && + !(item.hideWhenBillingDisabled && !isBillingEnabled) && + !item.requiresTeam && + !item.requiresEnterprise && + !item.requiresSuperUser && + !item.requiresAdminRole && + item.id !== 'template-profile' + ).length, + 3 + ) + ) + .filter((count) => count > 0) interface SettingsSidebarProps { isCollapsed?: boolean @@ -61,14 +77,16 @@ export function SettingsSidebar({ const { data: session, isPending: sessionLoading } = useSession() const { data: organizationsData, isLoading: orgsLoading } = useOrganizations() const { data: generalSettings } = useGeneralSettings() - const { data: subscriptionData } = useSubscriptionData({ + const { data: subscriptionData, isLoading: subscriptionLoading } = useSubscriptionData({ enabled: isBillingEnabled, staleTime: 5 * 60 * 1000, }) - const { data: ssoProvidersData, isLoading: isLoadingSSO } = useSSOProviders() + const { data: ssoProvidersData, isLoading: isLoadingSSO } = useSSOProviders({ + enabled: !isHosted, + }) const activeOrganization = organizationsData?.activeOrganization - const { config: permissionConfig } = usePermissionConfig() + const { config: permissionConfig, isLoading: permissionLoading } = usePermissionConfig() const userEmail = session?.user?.email const userId = session?.user?.id @@ -100,9 +118,18 @@ export function SettingsSidebar({ if (item.id === 'template-profile') { return false } + if (item.id === 'integrations' && permissionConfig.hideIntegrationsTab) { + return false + } + if (item.id === 'secrets' && permissionConfig.hideSecretsTab) { + return false + } if (item.id === 'apikeys' && permissionConfig.hideApiKeysTab) { return false } + if (item.id === 'inbox' && permissionConfig.hideInboxTab) { + return false + } if (item.id === 'mcp' && permissionConfig.disableMcpTools) { return false } @@ -244,113 +271,102 @@ export function SettingsSidebar({ !isCollapsed && 'overflow-y-auto overflow-x-hidden' )} > - {sessionLoading || orgsLoading ? ( - isCollapsed ? ( - <> - {SKELETON_SECTIONS.map((count, sectionIdx) => ( -
- {Array.from({ length: count }, (_, i) => ( -
- -
- ))} -
- ))} - - ) : ( - Array.from({ length: 3 }, (_, i) => ( -
-
+ {sessionLoading || + orgsLoading || + (isBillingEnabled && subscriptionLoading) || + permissionLoading || + (!isHosted && isLoadingSSO) + ? SKELETON_SECTIONS.map((count, i) => ( +
+
- {Array.from({ length: i === 0 ? 3 : 2 }, (_, j) => ( -
- + {Array.from({ length: count }, (_, j) => ( +
+ +
))}
)) - ) - ) : ( - sectionConfig.map(({ key, title }) => { - const sectionItems = navigationItems.filter((item) => item.section === key) - if (sectionItems.length === 0) return null - - return ( -
-
-
{title}
-
-
- {sectionItems.map((item) => { - const Icon = item.icon - const active = activeSection === item.id - const isLocked = item.requiresMax && !subscriptionAccess.hasUsableMaxAccess - const itemClassName = cn( - 'group mx-0.5 flex h-[30px] items-center gap-2 rounded-[8px] px-2 text-[14px]', - !active && 'hover-hover:bg-[var(--surface-hover)]', - active && 'bg-[var(--surface-active)]' - ) - const content = ( - <> - - - {item.label} - - {isLocked && ( - - Max + : sectionConfig.map(({ key, title }) => { + const sectionItems = navigationItems.filter((item) => item.section === key) + if (sectionItems.length === 0) return null + + return ( +
+
+
{title}
+
+
+ {sectionItems.map((item) => { + const Icon = item.icon + const active = activeSection === item.id + const isLocked = item.requiresMax && !subscriptionAccess.hasUsableMaxAccess + const itemClassName = cn( + 'group mx-0.5 flex h-[30px] items-center gap-2 rounded-[8px] px-2 text-[14px]', + !active && 'hover-hover:bg-[var(--surface-hover)]', + active && 'bg-[var(--surface-active)]' + ) + const content = ( + <> + + + {item.label} - )} - - ) - - const element = item.externalUrl ? ( - - {content} - - ) : ( - - ) - - return ( - - {element} - - ) - })} + {isLocked && ( + + Max + + )} + + ) + + const element = item.externalUrl ? ( + + {content} + + ) : ( + + ) + + return ( + + {element} + + ) + })} +
-
- ) - }) - )} + ) + })}
!open && handleCancelDiscard()}> diff --git a/apps/sim/components/emcn/components/modal/modal.tsx b/apps/sim/components/emcn/components/modal/modal.tsx index 3e2341ce0cf..91fc76ebd93 100644 --- a/apps/sim/components/emcn/components/modal/modal.tsx +++ b/apps/sim/components/emcn/components/modal/modal.tsx @@ -102,7 +102,7 @@ ModalOverlay.displayName = 'ModalOverlay' * Each size uses viewport units with sensible min/max constraints. */ const MODAL_SIZES = { - sm: 'w-[90vw] max-w-[400px]', + sm: 'w-[90vw] max-w-[440px]', md: 'w-[90vw] max-w-[500px]', lg: 'w-[90vw] max-w-[600px]', xl: 'w-[90vw] max-w-[800px]', @@ -120,7 +120,7 @@ export interface ModalContentProps showClose?: boolean /** * Modal size variant with responsive viewport-based sizing. - * - sm: max 400px (dialogs, confirmations) + * - sm: max 440px (dialogs, confirmations) * - md: max 500px (default, forms) * - lg: max 600px (content-heavy modals) * - xl: max 800px (complex editors) diff --git a/apps/sim/components/icons.tsx b/apps/sim/components/icons.tsx index 2806cd4d314..0e4de0bf5e5 100644 --- a/apps/sim/components/icons.tsx +++ b/apps/sim/components/icons.tsx @@ -3554,7 +3554,7 @@ export function FireworksIcon(props: SVGProps) { > ) diff --git a/apps/sim/ee/access-control/components/access-control.tsx b/apps/sim/ee/access-control/components/access-control.tsx index cc636fb9df8..9f751f91740 100644 --- a/apps/sim/ee/access-control/components/access-control.tsx +++ b/apps/sim/ee/access-control/components/access-control.tsx @@ -2,6 +2,7 @@ import { useCallback, useMemo, useState } from 'react' import { createLogger } from '@sim/logger' +import { useQuery } from '@tanstack/react-query' import { Plus, Search } from 'lucide-react' import { Avatar, @@ -140,7 +141,7 @@ function AddMembersModal({ className='h-auto flex-1 border-0 bg-transparent p-0 font-base text-sm leading-none placeholder:text-[var(--text-tertiary)] focus-visible:ring-0 focus-visible:ring-offset-0' />
-
@@ -314,16 +315,22 @@ export function AccessControl() { configKey: 'hideCopilot' as const, }, { - id: 'hide-api-keys', - label: 'API Keys', + id: 'hide-integrations', + label: 'Integrations', category: 'Settings Tabs', - configKey: 'hideApiKeysTab' as const, + configKey: 'hideIntegrationsTab' as const, }, { - id: 'hide-environment', - label: 'Environment', + id: 'hide-secrets', + label: 'Secrets', category: 'Settings Tabs', - configKey: 'hideEnvironmentTab' as const, + configKey: 'hideSecretsTab' as const, + }, + { + id: 'hide-api-keys', + label: 'API Keys', + category: 'Settings Tabs', + configKey: 'hideApiKeysTab' as const, }, { id: 'hide-files', @@ -391,6 +398,12 @@ export function AccessControl() { category: 'Collaboration', configKey: 'disableInvitations' as const, }, + { + id: 'hide-inbox', + label: 'Sim Mailer', + category: 'Features', + configKey: 'hideInboxTab' as const, + }, { id: 'disable-public-api', label: 'Public API', @@ -420,6 +433,23 @@ export function AccessControl() { return categories }, [filteredPlatformFeatures]) + const platformCategoryColumns = useMemo(() => { + const categoryGroups = [ + ['Sidebar', 'Deploy Tabs', 'Collaboration'], + ['Workflow Panel', 'Tools', 'Features'], + ['Settings Tabs', 'Logs'], + ] + + return categoryGroups.map((column) => + column + .map((category) => ({ + category, + features: platformCategories[category] ?? [], + })) + .filter((section) => section.features.length > 0) + ) + }, [platformCategories]) + const hasConfigChanges = useMemo(() => { if (!viewingGroup || !editingConfig) return false const original = viewingGroup.config @@ -436,7 +466,23 @@ export function AccessControl() { return a.name.localeCompare(b.name) }) }, []) - const allProviderIds = useMemo(() => getAllProviderIds(), []) + const { data: blacklistedProvidersData } = useQuery({ + queryKey: ['blacklistedProviders'], + queryFn: async ({ signal }) => { + const res = await fetch('/api/settings/allowed-providers', { signal }) + if (!res.ok) return { blacklistedProviders: [] as string[] } + return res.json() as Promise<{ blacklistedProviders: string[] }> + }, + staleTime: 5 * 60 * 1000, + enabled: showConfigModal, + }) + + const allProviderIds = useMemo(() => { + const allIds = getAllProviderIds() + const blacklist = blacklistedProvidersData?.blacklistedProviders ?? [] + if (blacklist.length === 0) return allIds + return allIds.filter((id) => !blacklist.includes(id.toLowerCase())) + }, [blacklistedProvidersData]) const filteredProviders = useMemo(() => { if (!providerSearchTerm.trim()) return allProviderIds @@ -450,6 +496,16 @@ export function AccessControl() { return allBlocks.filter((b) => b.name.toLowerCase().includes(query)) }, [allBlocks, integrationSearchTerm]) + const filteredCoreBlocks = useMemo(() => { + return filteredBlocks.filter((block) => block.category === 'blocks') + }, [filteredBlocks]) + + const filteredToolBlocks = useMemo(() => { + return filteredBlocks + .filter((block) => block.category === 'tools') + .sort((a, b) => a.name.localeCompare(b.name)) + }, [filteredBlocks]) + const orgMembers = useMemo(() => { return organization?.members || [] }, [organization]) @@ -841,249 +897,307 @@ export function AccessControl() { } }} > - + Configure Permissions - + Model Providers Blocks Platform - - -
-
-
- - setProviderSearchTerm(e.target.value)} - className='h-auto flex-1 border-0 bg-transparent p-0 font-base text-sm leading-none placeholder:text-[var(--text-tertiary)] focus-visible:ring-0 focus-visible:ring-offset-0' - /> -
- + + +
+
+ + setProviderSearchTerm(e.target.value)} + className='h-auto flex-1 border-0 bg-transparent p-0 font-base text-sm leading-none placeholder:text-[var(--text-tertiary)] focus-visible:ring-0 focus-visible:ring-offset-0' + />
-
- {filteredProviders.map((providerId) => { - const ProviderIcon = PROVIDER_DEFINITIONS[providerId]?.icon - const providerName = - PROVIDER_DEFINITIONS[providerId]?.name || - providerId.replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase()) - return ( -
- toggleProvider(providerId)} - /> -
- {ProviderIcon && } -
- {providerName} -
+
+ }} + > + {editingConfig?.allowedModelProviders === null || + editingConfig?.allowedModelProviders?.length === allProviderIds.length + ? 'Deselect All' + : 'Select All'} +
-
- - - - -
-
-
- - setIntegrationSearchTerm(e.target.value)} - className='h-auto flex-1 border-0 bg-transparent p-0 font-base text-sm leading-none placeholder:text-[var(--text-tertiary)] focus-visible:ring-0 focus-visible:ring-offset-0' - /> -
- -
-
- {filteredBlocks.map((block) => { - const BlockIcon = block.icon - return ( -
- toggleIntegration(block.type)} - /> -
- {BlockIcon && ( - - )} -
- {block.name} +
+ {filteredProviders.map((providerId) => { + const ProviderIcon = PROVIDER_DEFINITIONS[providerId]?.icon + const providerName = + PROVIDER_DEFINITIONS[providerId]?.name || + providerId.replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase()) + const checkboxId = `provider-${providerId}` + return ( + + ) + })} +
+ + + +
+
+ + setIntegrationSearchTerm(e.target.value)} + className='h-auto flex-1 border-0 bg-transparent p-0 font-base text-sm leading-none placeholder:text-[var(--text-tertiary)] focus-visible:ring-0 focus-visible:ring-offset-0' + />
+
- -
- - - -
-
-
- - setPlatformSearchTerm(e.target.value)} - className='h-auto flex-1 border-0 bg-transparent p-0 font-base text-sm leading-none placeholder:text-[var(--text-tertiary)] focus-visible:ring-0 focus-visible:ring-offset-0' - /> +
+ {filteredCoreBlocks.length > 0 && ( +
+ + Core Blocks + +
+ {filteredCoreBlocks.map((block) => { + const BlockIcon = block.icon + const checkboxId = `block-${block.type}` + return ( + + ) + })} +
- -
-
- {Object.entries(platformCategories).map(([category, features]) => ( -
- - {category} - -
- {features.map((feature) => ( -
+ )} + {filteredToolBlocks.length > 0 && ( +
+ + Tools + +
+ {filteredToolBlocks.map((block) => { + const BlockIcon = block.icon + const checkboxId = `block-${block.type}` + return ( +
- ))} -
+ {BlockIcon && ( + + )} +
+ {block.name} + + ) + })}
- ))} +
+ )} +
+ + + +
+
+ + setPlatformSearchTerm(e.target.value)} + className='h-auto flex-1 border-0 bg-transparent p-0 font-base text-sm leading-none placeholder:text-[var(--text-tertiary)] focus-visible:ring-0 focus-visible:ring-offset-0' + />
+ +
+
+ {platformCategoryColumns.map((column, columnIndex) => ( +
+ {column.map(({ category, features }) => ( +
+ + {category} + +
+ {features.map((feature) => ( + + ))} +
+
+ ))} +
+ ))}
- -
+ + {expanded && ( -
-
- Resource - - {formatResourceType(entry.resourceType)} - {entry.resourceId && ( - ({entry.resourceId}) - )} - -
- {entry.resourceName && ( +
+
- Name - {entry.resourceName} + Resource + + {formatResourceType(entry.resourceType)} + {entry.resourceId && ( + ({entry.resourceId}) + )} +
- )} -
- Actor - - {entry.actorName || 'Unknown'} - {entry.actorEmail && ( - ({entry.actorEmail}) - )} - -
- {entry.description && ( -
- Description - {entry.description} -
- )} - {entry.metadata != null && - Object.keys(entry.metadata as Record).length > 0 ? ( + {entry.resourceName && ( +
+ Name + {entry.resourceName} +
+ )}
- Details -
-                {JSON.stringify(entry.metadata, null, 2)}
-              
+ Actor + + {entry.actorName || 'Unknown'} + {entry.actorEmail && ( + ({entry.actorEmail}) + )} +
- ) : null} + {entry.description && ( +
+ + Description + + {entry.description} +
+ )} + {metadataEntries.map(([key, value]) => ( +
+ + {formatMetadataLabel(key)} + +
{renderMetadataValue(value)}
+
+ ))} +
)}
@@ -178,7 +290,7 @@ export function AuditLogs() { return (
-
+
@@ -205,7 +317,7 @@ export function AuditLogs() { value={dateRange} onChange={setDateRange} placeholder='Date range' - size='md' + size='sm' />
-
- - Timestamp - - - Event - - - Description - - - Actor - -
+
+
+ Timestamp + Event + Description + Actor +
-
- {isLoading ? ( -
- {Array.from({ length: 8 }).map((_, i) => ( -
- - - - -
- ))} -
- ) : allEntries.length === 0 ? ( -
- {debouncedSearch ? `No results for "${debouncedSearch}"` : 'No audit logs found'} -
- ) : ( -
- {allEntries.map((entry) => ( - - ))} - {hasNextPage && ( -
- -
- )} -
- )} +
+ {isLoading ? ( +
+ {Array.from({ length: 8 }).map((_, i) => ( +
+
+ + + + +
+
+ ))} +
+ ) : allEntries.length === 0 ? ( +
+ {debouncedSearch ? `No results for "${debouncedSearch}"` : 'No audit logs found'} +
+ ) : ( +
+ {allEntries.map((entry) => ( + + ))} + {hasNextPage && ( +
+ +
+ )} +
+ )} +
) diff --git a/apps/sim/ee/sso/hooks/sso.ts b/apps/sim/ee/sso/hooks/sso.ts index 2dfa1592ea4..9adcd365907 100644 --- a/apps/sim/ee/sso/hooks/sso.ts +++ b/apps/sim/ee/sso/hooks/sso.ts @@ -25,12 +25,17 @@ async function fetchSSOProviders() { /** * Hook to fetch SSO providers */ -export function useSSOProviders() { +interface UseSSOProvidersOptions { + enabled?: boolean +} + +export function useSSOProviders({ enabled = true }: UseSSOProvidersOptions = {}) { return useQuery({ queryKey: ssoKeys.providers(), queryFn: fetchSSOProviders, - staleTime: 5 * 60 * 1000, // 5 minutes + staleTime: 5 * 60 * 1000, placeholderData: keepPreviousData, + enabled, }) } diff --git a/apps/sim/ee/whitelabeling/components/whitelabeling-settings.tsx b/apps/sim/ee/whitelabeling/components/whitelabeling-settings.tsx index a6946772c9a..5de0b6e6c32 100644 --- a/apps/sim/ee/whitelabeling/components/whitelabeling-settings.tsx +++ b/apps/sim/ee/whitelabeling/components/whitelabeling-settings.tsx @@ -4,7 +4,7 @@ import { useCallback, useState } from 'react' import { createLogger } from '@sim/logger' import { Loader2, X } from 'lucide-react' import Image from 'next/image' -import { Button, Input, Label, Switch } from '@/components/emcn' +import { Button, Input, Label } from '@/components/emcn' import { useSession } from '@/lib/auth/auth-client' import { getSubscriptionAccessState } from '@/lib/billing/client/utils' import { HEX_COLOR_REGEX } from '@/lib/branding' @@ -154,7 +154,6 @@ export function WhitelabelingSettings() { const [documentationUrl, setDocumentationUrl] = useState('') const [termsUrl, setTermsUrl] = useState('') const [privacyUrl, setPrivacyUrl] = useState('') - const [hidePoweredBySim, setHidePoweredBySim] = useState(false) const [logoUrl, setLogoUrl] = useState(null) const [wordmarkUrl, setWordmarkUrl] = useState(null) const [formInitialized, setFormInitialized] = useState(false) @@ -172,7 +171,6 @@ export function WhitelabelingSettings() { setDocumentationUrl(savedSettings.documentationUrl ?? '') setTermsUrl(savedSettings.termsUrl ?? '') setPrivacyUrl(savedSettings.privacyUrl ?? '') - setHidePoweredBySim(savedSettings.hidePoweredBySim ?? false) setLogoUrl(savedSettings.logoUrl ?? null) setWordmarkUrl(savedSettings.wordmarkUrl ?? null) setFormInitialized(true) @@ -222,7 +220,6 @@ export function WhitelabelingSettings() { documentationUrl: documentationUrl || null, termsUrl: termsUrl || null, privacyUrl: privacyUrl || null, - hidePoweredBySim, } try { @@ -246,7 +243,6 @@ export function WhitelabelingSettings() { documentationUrl, termsUrl, privacyUrl, - hidePoweredBySim, ]) if (isBillingEnabled) { @@ -496,21 +492,6 @@ export function WhitelabelingSettings() {
-
- Advanced -
-
- - Hide "Powered by Sim" branding - - - Removes the Sim logo from deployed chats and forms. - -
- -
-
-
-
- )} - {isTrainingEnabled && (
@@ -536,68 +500,6 @@ export function General() { )}
- {/* Delete Account Confirmation Modal */} - { - setShowDeleteAccountModal(open) - if (!open) { - setDeleteConfirmText('') - deleteAccount.reset() - } - }} - > - - Delete Account - -

- This will permanently delete your account and all associated data, including - workspaces, workflows, API keys, and execution history.{' '} - This action cannot be undone. -

-
- - setDeleteConfirmText(e.target.value)} - className='w-full rounded-md border border-[var(--border)] bg-transparent px-3 py-2 text-[var(--text-primary)] text-sm placeholder:text-[var(--text-tertiary)] focus:border-[var(--border-1)] focus:outline-none' - placeholder='delete my account' - disabled={deleteAccount.isPending} - /> -
- {deleteAccount.error && ( -

- {deleteAccount.error.message} -

- )} -
- - - - -
-
- {/* Password Reset Confirmation Modal */} diff --git a/apps/sim/hooks/queries/user-profile.ts b/apps/sim/hooks/queries/user-profile.ts index dddadc93c8f..593175544b0 100644 --- a/apps/sim/hooks/queries/user-profile.ts +++ b/apps/sim/hooks/queries/user-profile.ts @@ -142,21 +142,3 @@ export function useResetPassword() { }, }) } - -/** - * Delete account mutation — permanently removes the user's account and all associated data. - */ -export function useDeleteAccount() { - return useMutation({ - mutationFn: async () => { - const response = await fetch('/api/users/me', { method: 'DELETE' }) - - if (!response.ok) { - const error = await response.json() - throw new Error(error.error || 'Failed to delete account') - } - - return response.json() - }, - }) -} diff --git a/apps/sim/lib/posthog/events.ts b/apps/sim/lib/posthog/events.ts index c2b1ee7566b..faf9895bf62 100644 --- a/apps/sim/lib/posthog/events.ts +++ b/apps/sim/lib/posthog/events.ts @@ -12,8 +12,6 @@ export interface PostHogEventMap { provider?: string } - user_deleted: Record - landing_page_viewed: Record landing_cta_clicked: { From a54549efe8114073ac53692fd970a27509520e16 Mon Sep 17 00:00:00 2001 From: waleed Date: Sat, 11 Apr 2026 19:35:54 -0700 Subject: [PATCH 04/13] =?UTF-8?q?fix(settings):=20address=20pr=20review=20?= =?UTF-8?q?=E2=80=94=20atomic=20autoAddNewMembers,=20extract=20query=20hoo?= =?UTF-8?q?k,=20fix=20types=20and=20signal=20forwarding?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/api/permission-groups/[id]/route.ts | 49 ++++++++++--------- apps/sim/app/api/permission-groups/route.ts | 28 +++++------ .../[workspaceId]/settings/[section]/page.tsx | 4 +- .../components/access-control.tsx | 36 +++++++------- apps/sim/ee/sso/hooks/sso.ts | 6 +-- apps/sim/hooks/queries/allowed-providers.ts | 35 +++++++++++++ apps/sim/providers/utils.ts | 10 +--- 7 files changed, 101 insertions(+), 67 deletions(-) create mode 100644 apps/sim/hooks/queries/allowed-providers.ts diff --git a/apps/sim/app/api/permission-groups/[id]/route.ts b/apps/sim/app/api/permission-groups/[id]/route.ts index 591b70b83a4..5c1c8af131f 100644 --- a/apps/sim/app/api/permission-groups/[id]/route.ts +++ b/apps/sim/app/api/permission-groups/[id]/route.ts @@ -155,31 +155,34 @@ export async function PUT(req: NextRequest, { params }: { params: Promise<{ id: ? { ...currentConfig, ...updates.config } : currentConfig - // If setting autoAddNewMembers to true, unset it on other groups in the org first - if (updates.autoAddNewMembers === true) { - await db - .update(permissionGroup) - .set({ autoAddNewMembers: false, updatedAt: new Date() }) - .where( - and( - eq(permissionGroup.organizationId, result.group.organizationId), - eq(permissionGroup.autoAddNewMembers, true) + const now = new Date() + + await db.transaction(async (tx) => { + if (updates.autoAddNewMembers === true) { + await tx + .update(permissionGroup) + .set({ autoAddNewMembers: false, updatedAt: now }) + .where( + and( + eq(permissionGroup.organizationId, result.group.organizationId), + eq(permissionGroup.autoAddNewMembers, true) + ) ) - ) - } + } - await db - .update(permissionGroup) - .set({ - ...(updates.name !== undefined && { name: updates.name }), - ...(updates.description !== undefined && { description: updates.description }), - ...(updates.autoAddNewMembers !== undefined && { - autoAddNewMembers: updates.autoAddNewMembers, - }), - config: newConfig, - updatedAt: new Date(), - }) - .where(eq(permissionGroup.id, id)) + await tx + .update(permissionGroup) + .set({ + ...(updates.name !== undefined && { name: updates.name }), + ...(updates.description !== undefined && { description: updates.description }), + ...(updates.autoAddNewMembers !== undefined && { + autoAddNewMembers: updates.autoAddNewMembers, + }), + config: newConfig, + updatedAt: now, + }) + .where(eq(permissionGroup.id, id)) + }) const [updated] = await db .select() diff --git a/apps/sim/app/api/permission-groups/route.ts b/apps/sim/app/api/permission-groups/route.ts index 14197237bb6..dd5c09e5453 100644 --- a/apps/sim/app/api/permission-groups/route.ts +++ b/apps/sim/app/api/permission-groups/route.ts @@ -171,19 +171,6 @@ export async function POST(req: Request) { ...config, } - // If autoAddNewMembers is true, unset it on any existing groups first - if (autoAddNewMembers) { - await db - .update(permissionGroup) - .set({ autoAddNewMembers: false, updatedAt: new Date() }) - .where( - and( - eq(permissionGroup.organizationId, organizationId), - eq(permissionGroup.autoAddNewMembers, true) - ) - ) - } - const now = new Date() const newGroup = { id: generateId(), @@ -197,7 +184,20 @@ export async function POST(req: Request) { autoAddNewMembers: autoAddNewMembers || false, } - await db.insert(permissionGroup).values(newGroup) + await db.transaction(async (tx) => { + if (autoAddNewMembers) { + await tx + .update(permissionGroup) + .set({ autoAddNewMembers: false, updatedAt: now }) + .where( + and( + eq(permissionGroup.organizationId, organizationId), + eq(permissionGroup.autoAddNewMembers, true) + ) + ) + } + await tx.insert(permissionGroup).values(newGroup) + }) logger.info('Created permission group', { permissionGroupId: newGroup.id, diff --git a/apps/sim/app/workspace/[workspaceId]/settings/[section]/page.tsx b/apps/sim/app/workspace/[workspaceId]/settings/[section]/page.tsx index 69da9112fcd..2c0db1d6e1d 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/[section]/page.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/[section]/page.tsx @@ -1,5 +1,6 @@ import { dehydrate, HydrationBoundary } from '@tanstack/react-query' import type { Metadata } from 'next' +import { isBillingEnabled } from '@/lib/core/config/feature-flags' import { getQueryClient } from '@/app/_shell/providers/get-query-client' import type { SettingsSection } from '@/app/workspace/[workspaceId]/settings/navigation' import { prefetchGeneralSettings, prefetchSubscriptionData, prefetchUserProfile } from './prefetch' @@ -11,6 +12,7 @@ const SECTION_TITLES: Record = { secrets: 'Secrets', 'template-profile': 'Template Profile', 'access-control': 'Access Control', + 'audit-logs': 'Audit Logs', apikeys: 'Sim Keys', byok: 'BYOK', subscription: 'Subscription', @@ -46,7 +48,7 @@ export default async function SettingsSectionPage({ void prefetchGeneralSettings(queryClient) void prefetchUserProfile(queryClient) - void prefetchSubscriptionData(queryClient) + if (isBillingEnabled) void prefetchSubscriptionData(queryClient) return ( diff --git a/apps/sim/ee/access-control/components/access-control.tsx b/apps/sim/ee/access-control/components/access-control.tsx index 9f751f91740..6cb2c44504b 100644 --- a/apps/sim/ee/access-control/components/access-control.tsx +++ b/apps/sim/ee/access-control/components/access-control.tsx @@ -2,7 +2,6 @@ import { useCallback, useMemo, useState } from 'react' import { createLogger } from '@sim/logger' -import { useQuery } from '@tanstack/react-query' import { Plus, Search } from 'lucide-react' import { Avatar, @@ -42,6 +41,7 @@ import { useRemovePermissionGroupMember, useUpdatePermissionGroup, } from '@/ee/access-control/hooks/permission-groups' +import { useBlacklistedProviders } from '@/hooks/queries/allowed-providers' import { useOrganization, useOrganizations } from '@/hooks/queries/organization' import { useSubscriptionData } from '@/hooks/queries/subscription' import { PROVIDER_DEFINITIONS } from '@/providers/models' @@ -49,10 +49,19 @@ import { getAllProviderIds } from '@/providers/utils' const logger = createLogger('AccessControl') +interface OrgMember { + userId: string + user: { + name: string | null + email: string + image?: string | null + } +} + interface AddMembersModalProps { open: boolean onOpenChange: (open: boolean) => void - availableMembers: any[] + availableMembers: OrgMember[] selectedMemberIds: Set setSelectedMemberIds: React.Dispatch>> onAddMembers: () => void @@ -73,7 +82,7 @@ function AddMembersModal({ const filteredMembers = useMemo(() => { if (!searchTerm.trim()) return availableMembers const query = searchTerm.toLowerCase() - return availableMembers.filter((m: any) => { + return availableMembers.filter((m) => { const name = m.user?.name || '' const email = m.user?.email || '' return name.toLowerCase().includes(query) || email.toLowerCase().includes(query) @@ -82,12 +91,12 @@ function AddMembersModal({ const allFilteredSelected = useMemo(() => { if (filteredMembers.length === 0) return false - return filteredMembers.every((m: any) => selectedMemberIds.has(m.userId)) + return filteredMembers.every((m) => selectedMemberIds.has(m.userId)) }, [filteredMembers, selectedMemberIds]) const handleToggleAll = () => { if (allFilteredSelected) { - const filteredIds = new Set(filteredMembers.map((m: any) => m.userId)) + const filteredIds = new Set(filteredMembers.map((m) => m.userId)) setSelectedMemberIds((prev) => { const next = new Set(prev) filteredIds.forEach((id) => next.delete(id)) @@ -96,7 +105,7 @@ function AddMembersModal({ } else { setSelectedMemberIds((prev) => { const next = new Set(prev) - filteredMembers.forEach((m: any) => next.add(m.userId)) + filteredMembers.forEach((m) => next.add(m.userId)) return next }) } @@ -153,7 +162,7 @@ function AddMembersModal({

) : (
- {filteredMembers.map((member: any) => { + {filteredMembers.map((member) => { const name = member.user?.name || 'Unknown' const email = member.user?.email || '' const avatarInitial = name.charAt(0).toUpperCase() @@ -466,16 +475,7 @@ export function AccessControl() { return a.name.localeCompare(b.name) }) }, []) - const { data: blacklistedProvidersData } = useQuery({ - queryKey: ['blacklistedProviders'], - queryFn: async ({ signal }) => { - const res = await fetch('/api/settings/allowed-providers', { signal }) - if (!res.ok) return { blacklistedProviders: [] as string[] } - return res.json() as Promise<{ blacklistedProviders: string[] }> - }, - staleTime: 5 * 60 * 1000, - enabled: showConfigModal, - }) + const { data: blacklistedProvidersData } = useBlacklistedProviders({ enabled: showConfigModal }) const allProviderIds = useMemo(() => { const allIds = getAllProviderIds() @@ -733,7 +733,7 @@ export function AccessControl() { const availableMembersToAdd = useMemo(() => { const existingMemberUserIds = new Set(members.map((m) => m.userId)) - return orgMembers.filter((m: any) => !existingMemberUserIds.has(m.userId)) + return orgMembers.filter((m) => !existingMemberUserIds.has(m.userId)) }, [orgMembers, members]) if (isLoading) { diff --git a/apps/sim/ee/sso/hooks/sso.ts b/apps/sim/ee/sso/hooks/sso.ts index 9adcd365907..37c20ffb7fb 100644 --- a/apps/sim/ee/sso/hooks/sso.ts +++ b/apps/sim/ee/sso/hooks/sso.ts @@ -14,8 +14,8 @@ export const ssoKeys = { /** * Fetch SSO providers */ -async function fetchSSOProviders() { - const response = await fetch('/api/auth/sso/providers') +async function fetchSSOProviders(signal: AbortSignal) { + const response = await fetch('/api/auth/sso/providers', { signal }) if (!response.ok) { throw new Error('Failed to fetch SSO providers') } @@ -32,7 +32,7 @@ interface UseSSOProvidersOptions { export function useSSOProviders({ enabled = true }: UseSSOProvidersOptions = {}) { return useQuery({ queryKey: ssoKeys.providers(), - queryFn: fetchSSOProviders, + queryFn: ({ signal }) => fetchSSOProviders(signal), staleTime: 5 * 60 * 1000, placeholderData: keepPreviousData, enabled, diff --git a/apps/sim/hooks/queries/allowed-providers.ts b/apps/sim/hooks/queries/allowed-providers.ts new file mode 100644 index 00000000000..ed251567b4b --- /dev/null +++ b/apps/sim/hooks/queries/allowed-providers.ts @@ -0,0 +1,35 @@ +'use client' + +import { useQuery } from '@tanstack/react-query' + +/** + * Query key factory for allowed providers queries + */ +export const allowedProvidersKeys = { + all: ['allowedProviders'] as const, + blacklisted: () => [...allowedProvidersKeys.all, 'blacklisted'] as const, +} + +interface BlacklistedProvidersResponse { + blacklistedProviders: string[] +} + +async function fetchBlacklistedProviders( + signal: AbortSignal +): Promise { + const res = await fetch('/api/settings/allowed-providers', { signal }) + if (!res.ok) return { blacklistedProviders: [] } + return res.json() +} + +/** + * Hook to fetch the list of blacklisted provider IDs from the server. + */ +export function useBlacklistedProviders({ enabled = true }: { enabled?: boolean } = {}) { + return useQuery({ + queryKey: allowedProvidersKeys.blacklisted(), + queryFn: ({ signal }) => fetchBlacklistedProviders(signal), + staleTime: 5 * 60 * 1000, + enabled, + }) +} diff --git a/apps/sim/providers/utils.ts b/apps/sim/providers/utils.ts index 40d4a0ebdf7..f4f5d4c9f04 100644 --- a/apps/sim/providers/utils.ts +++ b/apps/sim/providers/utils.ts @@ -4,7 +4,7 @@ import type { ChatCompletionChunk } from 'openai/resources/chat/completions' import type { CompletionUsage } from 'openai/resources/completions' import { dollarsToCredits } from '@/lib/billing/credits/conversion' import { env } from '@/lib/core/config/env' -import { isHosted } from '@/lib/core/config/feature-flags' +import { getBlacklistedProvidersFromEnv, isHosted } from '@/lib/core/config/feature-flags' import { buildCanonicalIndex, type CanonicalGroup, @@ -281,14 +281,8 @@ export function getProviderModels(providerId: ProviderId): string[] { return getProviderModelsFromDefinitions(providerId) } -function getBlacklistedProviders(): string[] { - if (!env.BLACKLISTED_PROVIDERS) return [] - return env.BLACKLISTED_PROVIDERS.split(',').map((p) => p.trim().toLowerCase()) -} - export function isProviderBlacklisted(providerId: string): boolean { - const blacklist = getBlacklistedProviders() - return blacklist.includes(providerId.toLowerCase()) + return getBlacklistedProvidersFromEnv().includes(providerId.toLowerCase()) } /** From ed7de175c45d3755c855d00df85a0d9399498a68 Mon Sep 17 00:00:00 2001 From: waleed Date: Sat, 11 Apr 2026 19:41:34 -0700 Subject: [PATCH 05/13] chore(helm): add CREDENTIAL_SETS_ENABLED to values.yaml --- helm/sim/values.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/helm/sim/values.yaml b/helm/sim/values.yaml index 516b1f8815b..7913e730f4a 100644 --- a/helm/sim/values.yaml +++ b/helm/sim/values.yaml @@ -238,6 +238,8 @@ app: NEXT_PUBLIC_SSO_ENABLED: "" # Show SSO login button in UI ("true" to enable) # Enterprise Feature Overrides (self-hosted) + CREDENTIAL_SETS_ENABLED: "" # Enable credential sets (email polling) on self-hosted ("true" to enable) + NEXT_PUBLIC_CREDENTIAL_SETS_ENABLED: "" # Show credential sets settings page ("true" to enable) INBOX_ENABLED: "" # Enable Sim Mailer on self-hosted ("true" to enable) NEXT_PUBLIC_INBOX_ENABLED: "" # Show Sim Mailer settings page ("true" to enable) WHITELABELING_ENABLED: "" # Enable whitelabeling on self-hosted ("true" to enable) From c557c0744157c9764fb4c4778422267789ef4936 Mon Sep 17 00:00:00 2001 From: waleed Date: Sat, 11 Apr 2026 19:47:07 -0700 Subject: [PATCH 06/13] fix(access-control): dynamic platform category columns, atomic permission group delete --- apps/sim/app/api/permission-groups/[id]/route.ts | 6 ++++-- apps/sim/ee/access-control/components/access-control.tsx | 6 +++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/apps/sim/app/api/permission-groups/[id]/route.ts b/apps/sim/app/api/permission-groups/[id]/route.ts index 5c1c8af131f..7cab684f043 100644 --- a/apps/sim/app/api/permission-groups/[id]/route.ts +++ b/apps/sim/app/api/permission-groups/[id]/route.ts @@ -252,8 +252,10 @@ export async function DELETE(req: NextRequest, { params }: { params: Promise<{ i return NextResponse.json({ error: 'Admin or owner permissions required' }, { status: 403 }) } - await db.delete(permissionGroupMember).where(eq(permissionGroupMember.permissionGroupId, id)) - await db.delete(permissionGroup).where(eq(permissionGroup.id, id)) + await db.transaction(async (tx) => { + await tx.delete(permissionGroupMember).where(eq(permissionGroupMember.permissionGroupId, id)) + await tx.delete(permissionGroup).where(eq(permissionGroup.id, id)) + }) logger.info('Deleted permission group', { permissionGroupId: id, userId: session.user.id }) diff --git a/apps/sim/ee/access-control/components/access-control.tsx b/apps/sim/ee/access-control/components/access-control.tsx index 6cb2c44504b..030b0f6dbe8 100644 --- a/apps/sim/ee/access-control/components/access-control.tsx +++ b/apps/sim/ee/access-control/components/access-control.tsx @@ -449,7 +449,11 @@ export function AccessControl() { ['Settings Tabs', 'Logs'], ] - return categoryGroups.map((column) => + const assignedCategories = new Set(categoryGroups.flat()) + const unassigned = Object.keys(platformCategories).filter((c) => !assignedCategories.has(c)) + const groups = unassigned.length > 0 ? [...categoryGroups, unassigned] : categoryGroups + + return groups.map((column) => column .map((category) => ({ category, From d04e7f45d64dc44d4daccff5bfd191611b99148d Mon Sep 17 00:00:00 2001 From: waleed Date: Sat, 11 Apr 2026 19:59:01 -0700 Subject: [PATCH 07/13] fix(access-control): restore triggers section in blocks tab --- .../components/access-control.tsx | 43 ++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/apps/sim/ee/access-control/components/access-control.tsx b/apps/sim/ee/access-control/components/access-control.tsx index 030b0f6dbe8..d0619571886 100644 --- a/apps/sim/ee/access-control/components/access-control.tsx +++ b/apps/sim/ee/access-control/components/access-control.tsx @@ -500,6 +500,12 @@ export function AccessControl() { return allBlocks.filter((b) => b.name.toLowerCase().includes(query)) }, [allBlocks, integrationSearchTerm]) + const filteredTriggerBlocks = useMemo(() => { + return filteredBlocks + .filter((block) => block.category === 'triggers') + .sort((a, b) => a.name.localeCompare(b.name)) + }, [filteredBlocks]) + const filteredCoreBlocks = useMemo(() => { return filteredBlocks.filter((block) => block.category === 'blocks') }, [filteredBlocks]) @@ -1003,8 +1009,43 @@ export function AccessControl() {
- {filteredCoreBlocks.length > 0 && ( + {filteredTriggerBlocks.length > 0 && (
+ + Triggers + +
+ {filteredTriggerBlocks.map((block) => { + const BlockIcon = block.icon + const checkboxId = `block-${block.type}` + return ( + + ) + })} +
+
+ )} + {filteredCoreBlocks.length > 0 && ( +
Core Blocks From 78ed32c55eaa8ae32f87a53715ca0a488218ece7 Mon Sep 17 00:00:00 2001 From: waleed Date: Sat, 11 Apr 2026 20:00:12 -0700 Subject: [PATCH 08/13] fix(access-control): merge triggers into tools section in blocks tab --- .../components/access-control.tsx | 45 +------------------ 1 file changed, 2 insertions(+), 43 deletions(-) diff --git a/apps/sim/ee/access-control/components/access-control.tsx b/apps/sim/ee/access-control/components/access-control.tsx index d0619571886..cce8fd5a3ce 100644 --- a/apps/sim/ee/access-control/components/access-control.tsx +++ b/apps/sim/ee/access-control/components/access-control.tsx @@ -500,19 +500,13 @@ export function AccessControl() { return allBlocks.filter((b) => b.name.toLowerCase().includes(query)) }, [allBlocks, integrationSearchTerm]) - const filteredTriggerBlocks = useMemo(() => { - return filteredBlocks - .filter((block) => block.category === 'triggers') - .sort((a, b) => a.name.localeCompare(b.name)) - }, [filteredBlocks]) - const filteredCoreBlocks = useMemo(() => { return filteredBlocks.filter((block) => block.category === 'blocks') }, [filteredBlocks]) const filteredToolBlocks = useMemo(() => { return filteredBlocks - .filter((block) => block.category === 'tools') + .filter((block) => block.category === 'tools' || block.category === 'triggers') .sort((a, b) => a.name.localeCompare(b.name)) }, [filteredBlocks]) @@ -1009,43 +1003,8 @@ export function AccessControl() {
- {filteredTriggerBlocks.length > 0 && ( -
- - Triggers - -
- {filteredTriggerBlocks.map((block) => { - const BlockIcon = block.icon - const checkboxId = `block-${block.type}` - return ( - - ) - })} -
-
- )} {filteredCoreBlocks.length > 0 && ( -
+
Core Blocks From 34a238c11f03bcb8e565cdd0793b2804df9e0851 Mon Sep 17 00:00:00 2001 From: waleed Date: Sat, 11 Apr 2026 20:03:27 -0700 Subject: [PATCH 09/13] upgrade tubro --- bun.lock | 16 ++++++++-------- package.json | 2 +- turbo.json | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/bun.lock b/bun.lock index e05bc532f5e..b071c71851b 100644 --- a/bun.lock +++ b/bun.lock @@ -13,7 +13,7 @@ "glob": "13.0.0", "husky": "9.1.7", "lint-staged": "16.0.0", - "turbo": "2.9.3", + "turbo": "2.9.5", }, }, "apps/docs": { @@ -1498,17 +1498,17 @@ "@trigger.dev/sdk": ["@trigger.dev/sdk@4.4.3", "", { "dependencies": { "@opentelemetry/api": "1.9.0", "@opentelemetry/semantic-conventions": "1.36.0", "@trigger.dev/core": "4.4.3", "chalk": "^5.2.0", "cronstrue": "^2.21.0", "debug": "^4.3.4", "evt": "^2.4.13", "slug": "^6.0.0", "ulid": "^2.3.0", "uncrypto": "^0.1.3", "uuid": "^9.0.0", "ws": "^8.11.0" }, "peerDependencies": { "ai": "^4.2.0 || ^5.0.0 || ^6.0.0", "zod": "^3.0.0 || ^4.0.0" }, "optionalPeers": ["ai"] }, "sha512-ghJkak+PTBJJ9HiHMcnahJmzjsgCzYiIHu5Qj5R7I9q5LS6i7mkx169rB/tOE9HLadd4HSu3yYA5DrH4wXhZuw=="], - "@turbo/darwin-64": ["@turbo/darwin-64@2.9.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-P8foouaP+y/p+hhEGBoZpzMbpVvUMwPjDpcy6wN7EYfvvyISD1USuV27qWkczecihwuPJzQ1lDBuL8ERcavTyg=="], + "@turbo/darwin-64": ["@turbo/darwin-64@2.9.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-qPxhKsLMQP+9+dsmPgAGidi5uNifD4AoAOnEnljab3Qgn0QZRR31Hp+/CgW3Ia5AanWj6JuLLTBYvuQj4mqTWg=="], - "@turbo/darwin-arm64": ["@turbo/darwin-arm64@2.9.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-SIzEkvtNdzdI50FJDaIQ6kQGqgSSdFPcdn0wqmmONN6iGKjy6hsT+EH99GP65FsfV7DLZTh2NmtTIRl2kdoz5Q=="], + "@turbo/darwin-arm64": ["@turbo/darwin-arm64@2.9.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-vkF/9F/l3aWd4bHxTui5Hh0F5xrTZ4e3rbBsc57zA6O8gNbmHN3B6eZ5psAIP2CnJRZ8ZxRjV3WZHeNXMXkPBw=="], - "@turbo/linux-64": ["@turbo/linux-64@2.9.3", "", { "os": "linux", "cpu": "x64" }, "sha512-pLRwFmcHHNBvsCySLS6OFabr/07kDT2pxEt/k6eBf/3asiVQZKJ7Rk88AafQx2aYA641qek4RsXvYO3JYpiBug=="], + "@turbo/linux-64": ["@turbo/linux-64@2.9.5", "", { "os": "linux", "cpu": "x64" }, "sha512-z/Get5NUaUxm5HSGFqVMICDRjFNsCUhSc4wnFa/PP1QD0NXCjr7bu9a2EM6md/KMCBW0Qe393Ac+UM7/ryDDTw=="], - "@turbo/linux-arm64": ["@turbo/linux-arm64@2.9.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-gy6ApUroC2Nzv+qjGtE/uPNkhHAFU4c8God+zd5Aiv9L9uBgHlxVJpHT3XWl5xwlJZ2KWuMrlHTaS5kmNB+q1Q=="], + "@turbo/linux-arm64": ["@turbo/linux-arm64@2.9.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-jyBifaNoI5/NheyswomiZXJvjdAdvT7hDRYzQ4meP0DKGvpXUjnqsD+4/J2YSDQ34OHxFkL30FnSCUIVOh2PHw=="], - "@turbo/windows-64": ["@turbo/windows-64@2.9.3", "", { "os": "win32", "cpu": "x64" }, "sha512-d0YelTX6hAsB7kIEtGB3PzIzSfAg3yDoUlHwuwJc3adBXUsyUIs0YLG+1NNtuhcDOUGnWQeKUoJ2pGWvbpRj7w=="], + "@turbo/windows-64": ["@turbo/windows-64@2.9.5", "", { "os": "win32", "cpu": "x64" }, "sha512-ph24K5uPtvo7UfuyDXnBiB/8XvrO+RQWbbw5zkA/bVNoy9HDiNoIJJj3s62MxT9tjEb6DnPje5PXSz1UR7QAyg=="], - "@turbo/windows-arm64": ["@turbo/windows-arm64@2.9.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-/08CwpKJl3oRY8nOlh2YgilZVJDHsr60XTNxRhuDeuFXONpUZ5X+Nv65izbG/xBew9qxcJFbDX9/sAmAX+ITcQ=="], + "@turbo/windows-arm64": ["@turbo/windows-arm64@2.9.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-6c5RccT/+iR39SdT1G5HyZaD2n57W77o+l0TTfxG/cVlhV94Acyg2gTQW7zUOhW1BeQpBjHzu9x8yVBZwrHh7g=="], "@tweenjs/tween.js": ["@tweenjs/tween.js@23.1.3", "", {}, "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA=="], @@ -3644,7 +3644,7 @@ "tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="], - "turbo": ["turbo@2.9.3", "", { "optionalDependencies": { "@turbo/darwin-64": "2.9.3", "@turbo/darwin-arm64": "2.9.3", "@turbo/linux-64": "2.9.3", "@turbo/linux-arm64": "2.9.3", "@turbo/windows-64": "2.9.3", "@turbo/windows-arm64": "2.9.3" }, "bin": { "turbo": "bin/turbo" } }, "sha512-J/VUvsGRykPb9R8Kh8dHVBOqioDexLk9BhLCU/ZybRR+HN9UR3cURdazFvNgMDt9zPP8TF6K73Z+tplfmi0PqQ=="], + "turbo": ["turbo@2.9.5", "", { "optionalDependencies": { "@turbo/darwin-64": "2.9.5", "@turbo/darwin-arm64": "2.9.5", "@turbo/linux-64": "2.9.5", "@turbo/linux-arm64": "2.9.5", "@turbo/windows-64": "2.9.5", "@turbo/windows-arm64": "2.9.5" }, "bin": { "turbo": "bin/turbo" } }, "sha512-JXNkRe6H6MjSlk5UQRTjyoKX5YN2zlc2632xcSlSFBao5yvbMWTpv9SNolOZlZmUlcDOHuszPLItbKrvcXnnZA=="], "tweetnacl": ["tweetnacl@0.14.5", "", {}, "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA=="], diff --git a/package.json b/package.json index c7ff404e8c2..69e33c58900 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "glob": "13.0.0", "husky": "9.1.7", "lint-staged": "16.0.0", - "turbo": "2.9.3" + "turbo": "2.9.5" }, "lint-staged": { "*.{js,jsx,ts,tsx,json,css,scss}": [ diff --git a/turbo.json b/turbo.json index 4469e31e0ae..ebca501d47d 100644 --- a/turbo.json +++ b/turbo.json @@ -1,5 +1,5 @@ { - "$schema": "https://v2-9-5.turborepo.dev/schema.json", + "$schema": "https://v2-9-6.turborepo.dev/schema.json", "envMode": "loose", "tasks": { "transit": { From f706006b009568310091b343b6140b6a34fddc17 Mon Sep 17 00:00:00 2001 From: waleed Date: Sat, 11 Apr 2026 20:12:39 -0700 Subject: [PATCH 10/13] fix(access-control): fix Select All state when config has stale blacklisted provider IDs --- apps/sim/ee/access-control/components/access-control.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/apps/sim/ee/access-control/components/access-control.tsx b/apps/sim/ee/access-control/components/access-control.tsx index cce8fd5a3ce..62edf794ecd 100644 --- a/apps/sim/ee/access-control/components/access-control.tsx +++ b/apps/sim/ee/access-control/components/access-control.tsx @@ -928,14 +928,18 @@ export function AccessControl() { onClick={() => { const allAllowed = editingConfig?.allowedModelProviders === null || - editingConfig?.allowedModelProviders?.length === allProviderIds.length + allProviderIds.every((id) => + editingConfig?.allowedModelProviders?.includes(id) + ) setEditingConfig((prev) => prev ? { ...prev, allowedModelProviders: allAllowed ? [] : null } : prev ) }} > {editingConfig?.allowedModelProviders === null || - editingConfig?.allowedModelProviders?.length === allProviderIds.length + allProviderIds.every((id) => + editingConfig?.allowedModelProviders?.includes(id) + ) ? 'Deselect All' : 'Select All'} From 8682c72cb6b49f9335cfecf2924d2be483ea05a4 Mon Sep 17 00:00:00 2001 From: waleed Date: Sat, 11 Apr 2026 20:18:31 -0700 Subject: [PATCH 11/13] fix(access-control): derive platform Select All from features list; revert turbo schema version --- .../components/access-control.tsx | 68 ++----------------- turbo.json | 2 +- 2 files changed, 8 insertions(+), 62 deletions(-) diff --git a/apps/sim/ee/access-control/components/access-control.tsx b/apps/sim/ee/access-control/components/access-control.tsx index 62edf794ecd..0bc7f489fee 100644 --- a/apps/sim/ee/access-control/components/access-control.tsx +++ b/apps/sim/ee/access-control/components/access-control.tsx @@ -1095,76 +1095,22 @@ export function AccessControl() { variant='default' className='h-8' onClick={() => { - const allVisible = - !editingConfig?.hideKnowledgeBaseTab && - !editingConfig?.hideTablesTab && - !editingConfig?.hideTemplates && - !editingConfig?.hideCopilot && - !editingConfig?.hideIntegrationsTab && - !editingConfig?.hideSecretsTab && - !editingConfig?.hideApiKeysTab && - !editingConfig?.hideInboxTab && - !editingConfig?.hideFilesTab && - !editingConfig?.disableMcpTools && - !editingConfig?.disableCustomTools && - !editingConfig?.disableSkills && - !editingConfig?.hideTraceSpans && - !editingConfig?.disableInvitations && - !editingConfig?.disablePublicApi && - !editingConfig?.hideDeployApi && - !editingConfig?.hideDeployMcp && - !editingConfig?.hideDeployA2a && - !editingConfig?.hideDeployChatbot && - !editingConfig?.hideDeployTemplate + const allVisible = platformFeatures.every( + (f) => !editingConfig?.[f.configKey] + ) setEditingConfig((prev) => prev ? { ...prev, - hideKnowledgeBaseTab: allVisible, - hideTablesTab: allVisible, - hideTemplates: allVisible, - hideCopilot: allVisible, - hideIntegrationsTab: allVisible, - hideSecretsTab: allVisible, - hideApiKeysTab: allVisible, - hideInboxTab: allVisible, - hideFilesTab: allVisible, - disableMcpTools: allVisible, - disableCustomTools: allVisible, - disableSkills: allVisible, - hideTraceSpans: allVisible, - disableInvitations: allVisible, - disablePublicApi: allVisible, - hideDeployApi: allVisible, - hideDeployMcp: allVisible, - hideDeployA2a: allVisible, - hideDeployChatbot: allVisible, - hideDeployTemplate: allVisible, + ...Object.fromEntries( + platformFeatures.map((f) => [f.configKey, allVisible]) + ), } : prev ) }} > - {!editingConfig?.hideKnowledgeBaseTab && - !editingConfig?.hideTablesTab && - !editingConfig?.hideTemplates && - !editingConfig?.hideCopilot && - !editingConfig?.hideIntegrationsTab && - !editingConfig?.hideSecretsTab && - !editingConfig?.hideApiKeysTab && - !editingConfig?.hideInboxTab && - !editingConfig?.hideFilesTab && - !editingConfig?.disableMcpTools && - !editingConfig?.disableCustomTools && - !editingConfig?.disableSkills && - !editingConfig?.hideTraceSpans && - !editingConfig?.disableInvitations && - !editingConfig?.disablePublicApi && - !editingConfig?.hideDeployApi && - !editingConfig?.hideDeployMcp && - !editingConfig?.hideDeployA2a && - !editingConfig?.hideDeployChatbot && - !editingConfig?.hideDeployTemplate + {platformFeatures.every((f) => !editingConfig?.[f.configKey]) ? 'Deselect All' : 'Select All'} diff --git a/turbo.json b/turbo.json index ebca501d47d..4469e31e0ae 100644 --- a/turbo.json +++ b/turbo.json @@ -1,5 +1,5 @@ { - "$schema": "https://v2-9-6.turborepo.dev/schema.json", + "$schema": "https://v2-9-5.turborepo.dev/schema.json", "envMode": "loose", "tasks": { "transit": { From 2774363d44a1eed01bc673cce082416bd4a768a5 Mon Sep 17 00:00:00 2001 From: waleed Date: Sat, 11 Apr 2026 20:30:13 -0700 Subject: [PATCH 12/13] fix(access-control): fix blocks Select All check, filter empty platform columns --- .../components/access-control.tsx | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/apps/sim/ee/access-control/components/access-control.tsx b/apps/sim/ee/access-control/components/access-control.tsx index 0bc7f489fee..65cc642e13d 100644 --- a/apps/sim/ee/access-control/components/access-control.tsx +++ b/apps/sim/ee/access-control/components/access-control.tsx @@ -453,14 +453,16 @@ export function AccessControl() { const unassigned = Object.keys(platformCategories).filter((c) => !assignedCategories.has(c)) const groups = unassigned.length > 0 ? [...categoryGroups, unassigned] : categoryGroups - return groups.map((column) => - column - .map((category) => ({ - category, - features: platformCategories[category] ?? [], - })) - .filter((section) => section.features.length > 0) - ) + return groups + .map((column) => + column + .map((category) => ({ + category, + features: platformCategories[category] ?? [], + })) + .filter((section) => section.features.length > 0) + ) + .filter((column) => column.length > 0) }, [platformCategories]) const hasConfigChanges = useMemo(() => { @@ -989,7 +991,9 @@ export function AccessControl() { onClick={() => { const allAllowed = editingConfig?.allowedIntegrations === null || - editingConfig?.allowedIntegrations?.length === allBlocks.length + allBlocks.every((b) => + editingConfig?.allowedIntegrations?.includes(b.type) + ) setEditingConfig((prev) => prev ? { @@ -1001,7 +1005,7 @@ export function AccessControl() { }} > {editingConfig?.allowedIntegrations === null || - editingConfig?.allowedIntegrations?.length === allBlocks.length + allBlocks.every((b) => editingConfig?.allowedIntegrations?.includes(b.type)) ? 'Deselect All' : 'Select All'} From be879f40414caf2461a218d5fbb14f6f57280f3d Mon Sep 17 00:00:00 2001 From: waleed Date: Sat, 11 Apr 2026 20:40:01 -0700 Subject: [PATCH 13/13] revert(settings): restore original skeleton icon and text sizes --- .../sidebar/components/settings-sidebar/settings-sidebar.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-sidebar/settings-sidebar.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-sidebar/settings-sidebar.tsx index fd95ddb98f5..9c1bc3fafcb 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-sidebar/settings-sidebar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-sidebar/settings-sidebar.tsx @@ -284,8 +284,8 @@ export function SettingsSidebar({
{Array.from({ length: count }, (_, j) => (
- - + +
))}