Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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