Skip to content

Commit db2fb3c

Browse files
fix(upgrade): client side env var check (#5673)
* fix(upgrade): client side env var check * fix misc self hosted visibility * add tests
1 parent 3282fd8 commit db2fb3c

7 files changed

Lines changed: 83 additions & 11 deletions

File tree

apps/sim/app/account/settings/[section]/page.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
parseSettingsPathSection,
1111
} from '@/components/settings/navigation'
1212
import { getSession } from '@/lib/auth'
13-
import { isBillingEnabled } from '@/lib/core/config/env-flags'
13+
import { isBillingEnabled, isHosted } from '@/lib/core/config/env-flags'
1414
import { isPlatformAdmin } from '@/lib/permissions/super-user'
1515

1616
interface AccountSettingsSectionPageProps {
@@ -46,6 +46,7 @@ export default async function AccountSettingsSectionPage({
4646
})
4747
if (!parsed) notFound()
4848
if (parsed === 'billing' && !isBillingEnabled) redirect(getAccountSettingsHref('general'))
49+
if (parsed === 'copilot' && !isHosted) redirect(getAccountSettingsHref('general'))
4950
if (parsed === 'admin' || parsed === 'mothership') {
5051
const isSuperUser = await isPlatformAdmin(session.user.id)
5152
if (!isSuperUser) notFound()

apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-sidebar/settings-sidebar.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
44
import { ChevronDown, ChipConfirmModal, chipVariants, cn } from '@sim/emcn'
55
import { useQueryClient } from '@tanstack/react-query'
66
import { useParams, usePathname, useRouter } from 'next/navigation'
7+
import { ORGANIZATION_PLANE_UNIFIED_SECTIONS } from '@/components/settings/navigation'
78
import { useSession } from '@/lib/auth/auth-client'
89
import { getSubscriptionAccessState } from '@/lib/billing/client'
910
import { canManageWorkspaceBilling } from '@/lib/billing/workspace-permissions'
@@ -120,6 +121,15 @@ export function SettingsSidebar({
120121
}
121122

122123
if (item.selfHostedOverride && !isHosted) {
124+
/**
125+
* Org-plane sections route through the organization gate in
126+
* `settings/[section]/page.tsx` (host organization + org-admin viewer),
127+
* which 404s other viewers — mirror it here so the item never links to
128+
* a dead page.
129+
*/
130+
if (ORGANIZATION_PLANE_UNIFIED_SECTIONS.has(item.id) && !isOrgAdminOrOwner) {
131+
return false
132+
}
123133
if (item.id === 'sso') {
124134
const hasProviders = (ssoProvidersData?.providers?.length ?? 0) > 0
125135
return !hasProviders || isSSOProviderOwner === true

apps/sim/blocks/utils.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,17 @@ function getProviderFromStore(model: string): string | null {
159159
return null
160160
}
161161

162+
/**
163+
* Whether an Ollama instance is available. `isOllamaConfigured` reads the
164+
* server-only `OLLAMA_URL` env var, which is always undefined in the browser —
165+
* there the providers store (populated from the server's model list, which is
166+
* non-empty only when Ollama is configured) is the signal.
167+
*/
168+
function isOllamaAvailable(): boolean {
169+
if (isOllamaConfigured) return true
170+
return useProvidersStore.getState().providers.ollama.models.length > 0
171+
}
172+
162173
function buildModelVisibilityCondition(model: string, shouldShow: boolean) {
163174
if (!model) {
164175
return { field: 'model', value: '__no_model_selected__' }
@@ -197,7 +208,7 @@ function shouldRequireApiKeyForModel(model: string): boolean {
197208
return false
198209
if (storeProvider) return true
199210

200-
if (isOllamaConfigured) {
211+
if (isOllamaAvailable()) {
201212
if (normalizedModel.includes('/')) return true
202213
if (normalizedModel in getBaseModelProviders()) return true
203214
return false

apps/sim/components/settings/navigation.test.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
getOrganizationSettingsHref,
1212
getWorkspaceSettingsHref,
1313
isOrganizationSettingsSectionAvailable,
14+
ORGANIZATION_PLANE_UNIFIED_SECTIONS,
1415
ORGANIZATION_SETTINGS_ITEMS,
1516
ORGANIZATION_SETTINGS_PATH_ALIASES,
1617
parseSettingsPathSection,
@@ -109,6 +110,19 @@ describe('settings navigation boundaries', () => {
109110
expect([...workspaceIds].sort()).toEqual(WORKSPACE_SETTINGS_ITEMS.map(({ id }) => id).sort())
110111
})
111112

113+
it('derives the organization-plane unified sections from the registry', () => {
114+
expect([...ORGANIZATION_PLANE_UNIFIED_SECTIONS].sort()).toEqual([
115+
'access-control',
116+
'audit-logs',
117+
'billing',
118+
'data-drains',
119+
'data-retention',
120+
'organization',
121+
'sso',
122+
'whitelabeling',
123+
])
124+
})
125+
112126
it('shares labels, icons, and docs links across projections', () => {
113127
const unifiedSso = buildUnifiedSettingsNavigation().find(({ id }) => id === 'sso')
114128
const organizationSso = ORGANIZATION_SETTINGS_ITEMS.find(({ id }) => id === 'sso')

apps/sim/components/settings/navigation.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -671,6 +671,18 @@ export const ORGANIZATION_SETTINGS_ITEMS: SettingsNavigationItem<OrganizationSet
671671
export const WORKSPACE_SETTINGS_ITEMS: SettingsNavigationItem<WorkspaceSettingsSection>[] =
672672
buildPlaneSettingsItems('workspace')
673673

674+
/**
675+
* Unified sections that resolve to organization-plane settings. The workspace
676+
* settings section page routes these through the organization gate (host
677+
* organization present + org-admin viewer), so workspace-plane navigation must
678+
* apply the same requirement before surfacing them.
679+
*/
680+
export const ORGANIZATION_PLANE_UNIFIED_SECTIONS: ReadonlySet<UnifiedSettingsSection> = new Set(
681+
SETTINGS_SECTION_REGISTRY.flatMap((entry) =>
682+
entry.planes?.organization ? [entry.unified.id] : []
683+
)
684+
)
685+
674686
export type OrganizationSectionAccess = 'unavailable' | 'view' | 'manage'
675687

676688
interface ResolveOrganizationSectionAccessOptions {

apps/sim/components/settings/standalone-settings-shell.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import { SettingsHeaderProvider, SettingsHeaderShell } from '@/components/settin
2121
import { SettingsSectionProvider } from '@/components/settings/settings-panel'
2222
import { SettingsSidebar } from '@/components/settings/settings-sidebar'
2323
import { useSettingsBeforeUnload } from '@/components/settings/use-settings-before-unload'
24-
import { isBillingEnabled } from '@/lib/core/config/env-flags'
24+
import { isBillingEnabled, isHosted } from '@/lib/core/config/env-flags'
2525

2626
interface StandaloneSettingsShellBaseProps {
2727
children: ReactNode
@@ -52,6 +52,7 @@ export function StandaloneSettingsShell(props: StandaloneSettingsShellProps) {
5252
const organizationFeatures = getOrganizationSettingsFeatures(hasEnterprisePlan)
5353
const accountItems = ACCOUNT_SETTINGS_ITEMS.filter((item) => {
5454
if (item.id === 'billing' && !isBillingEnabled) return false
55+
if (item.id === 'copilot' && !isHosted) return false
5556
if ((item.id === 'admin' || item.id === 'mothership') && !isSuperUser) return false
5657
return true
5758
})

apps/sim/lib/core/config/env-flags.ts

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,18 @@ export const isCopilotBillingAttributionV1Enabled = isTruthy(
4646
export const isCopilotBillingProtocolRequired = isTruthy(env.COPILOT_BILLING_PROTOCOL_REQUIRED)
4747

4848
/**
49-
* Is billing enforcement enabled
49+
* Is billing enforcement enabled.
50+
*
51+
* Server code reads `BILLING_ENABLED`. Server-only vars never reach browser
52+
* bundles, so client evaluation reads the `NEXT_PUBLIC_BILLING_ENABLED` twin
53+
* (via `window.__ENV`, populated by `<PublicEnvScript>`) — reading
54+
* `env.BILLING_ENABLED` in client code is always `undefined`. Deployments must
55+
* set both vars together.
5056
*/
51-
export const isBillingEnabled = isTruthy(env.BILLING_ENABLED)
57+
export const isBillingEnabled =
58+
typeof window === 'undefined'
59+
? isTruthy(env.BILLING_ENABLED)
60+
: isTruthy(getEnv('NEXT_PUBLIC_BILLING_ENABLED'))
5261

5362
/**
5463
* Block free-plan accounts from programmatic workflow execution (API key, public
@@ -154,18 +163,32 @@ export const isTriggerDevEnabled = isTruthy(env.TRIGGER_DEV_ENABLED)
154163
export const isSsoEnabled = isTruthy(env.SSO_ENABLED)
155164

156165
/**
157-
* Is access control (permission groups) enabled via env var override
158-
* This bypasses plan requirements for self-hosted deployments
166+
* Is access control (permission groups) enabled via env var override.
167+
* This bypasses plan requirements for self-hosted deployments.
168+
*
169+
* Server code reads `ACCESS_CONTROL_ENABLED`; the browser reads the
170+
* `NEXT_PUBLIC_ACCESS_CONTROL_ENABLED` twin (see {@link isBillingEnabled}).
159171
*/
160-
export const isAccessControlEnabled = isTruthy(env.ACCESS_CONTROL_ENABLED)
172+
export const isAccessControlEnabled =
173+
typeof window === 'undefined'
174+
? isTruthy(env.ACCESS_CONTROL_ENABLED)
175+
: isTruthy(getEnv('NEXT_PUBLIC_ACCESS_CONTROL_ENABLED'))
161176

162177
/**
163-
* Is organizations enabled
178+
* Is organizations enabled.
164179
* True if billing is enabled (orgs come with billing), OR explicitly enabled via env var,
165-
* OR if access control is enabled (access control requires organizations)
180+
* OR if access control is enabled (access control requires organizations).
181+
*
182+
* Each term resolves through its `NEXT_PUBLIC_*` twin in the browser (see
183+
* {@link isBillingEnabled}), so client code — e.g. the better-auth
184+
* `organizationClient` plugin registration — sees the same value as the server.
166185
*/
167186
export const isOrganizationsEnabled =
168-
isBillingEnabled || isTruthy(env.ORGANIZATIONS_ENABLED) || isAccessControlEnabled
187+
isBillingEnabled ||
188+
(typeof window === 'undefined'
189+
? isTruthy(env.ORGANIZATIONS_ENABLED)
190+
: isTruthy(getEnv('NEXT_PUBLIC_ORGANIZATIONS_ENABLED'))) ||
191+
isAccessControlEnabled
169192

170193
/**
171194
* Is inbox (Sim Mailer) enabled via env var override

0 commit comments

Comments
 (0)