Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions apps/docs/content/docs/en/enterprise/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
59 changes: 34 additions & 25 deletions apps/sim/app/api/permission-groups/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,18 @@ 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(),
disableCustomTools: z.boolean().optional(),
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(),
Expand Down Expand Up @@ -151,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()
Expand Down Expand Up @@ -245,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 })

Expand Down
32 changes: 18 additions & 14 deletions apps/sim/app/api/permission-groups/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,18 @@ 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(),
disableCustomTools: z.boolean().optional(),
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(),
Expand Down Expand Up @@ -167,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(),
Expand All @@ -193,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,
Expand Down
14 changes: 14 additions & 0 deletions apps/sim/app/api/settings/allowed-providers/route.ts
Original file line number Diff line number Diff line change
@@ -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(),
})
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
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, prefetchUserProfile } from './prefetch'
import { prefetchGeneralSettings, prefetchSubscriptionData, prefetchUserProfile } from './prefetch'
import { SettingsPage } from './settings'

const SECTION_TITLES: Record<string, string> = {
Expand All @@ -11,6 +12,7 @@ const SECTION_TITLES: Record<string, string> = {
secrets: 'Secrets',
'template-profile': 'Template Profile',
'access-control': 'Access Control',
'audit-logs': 'Audit Logs',
apikeys: 'Sim Keys',
byok: 'BYOK',
subscription: 'Subscription',
Expand Down Expand Up @@ -46,6 +48,7 @@ export default async function SettingsSectionPage({

void prefetchGeneralSettings(queryClient)
void prefetchUserProfile(queryClient)
if (isBillingEnabled) void prefetchSubscriptionData(queryClient)

return (
<HydrationBoundary state={dehydrate(queryClient)}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'

/**
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -198,7 +199,7 @@ export function SettingsPage({ section }: SettingsPageProps) {
}, [effectiveSection, sessionLoading, posthog])

return (
<div>
<div className={cn(effectiveSection === 'access-control' && 'flex h-full flex-col')}>
<h2 className='mb-7 font-medium text-[22px] text-[var(--text-primary)]'>{label}</h2>
{effectiveSection === 'general' && <General />}
{effectiveSection === 'integrations' && <Integrations />}
Expand Down
2 changes: 1 addition & 1 deletion apps/sim/app/workspace/[workspaceId]/settings/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
export default function SettingsLayout({ children }: { children: React.ReactNode }) {
return (
<div className='h-full overflow-y-auto [scrollbar-gutter:stable]'>
<div className='mx-auto flex min-h-full max-w-[900px] flex-col px-[26px] pt-9 pb-[52px]'>
<div className='mx-auto flex min-h-full max-w-[940px] flex-col px-[26px] pt-9 pb-[52px]'>
{children}
</div>
</div>
Expand Down
5 changes: 4 additions & 1 deletion apps/sim/app/workspace/[workspaceId]/settings/navigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down Expand Up @@ -106,6 +108,7 @@ export const allNavigationItems: NavigationItem[] = [
section: 'enterprise',
requiresHosted: true,
requiresEnterprise: true,
selfHostedOverride: isAuditLogsEnabled,
},
{
id: 'subscription',
Expand Down Expand Up @@ -181,7 +184,7 @@ export const allNavigationItems: NavigationItem[] = [
section: 'enterprise',
requiresHosted: true,
requiresEnterprise: true,
selfHostedOverride: isBillingEnabled,
selfHostedOverride: isWhitelabelingEnabled,
},
{
id: 'admin',
Expand Down
Loading
Loading