Skip to content

Commit 59ecfc1

Browse files
authored
fix(settings): add platform admin tabs to organization view (#8997)
1 parent f525ff2 commit 59ecfc1

5 files changed

Lines changed: 46 additions & 13 deletions

File tree

‎apps/sim/app/o/[organizationId]/settings/[section]/page.test.tsx‎

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
import { user } from '@sim/db/schema'
12
import { authMockFns } from '@sim/testing/mocks/auth.mock'
3+
import { queueTableRows, resetDbChainMock } from '@sim/testing/mocks/database.mock'
24
import { nextNavigationMock } from '@sim/testing/mocks/next-navigation.mock'
35
import { beforeEach, describe, expect, it, vi } from 'vitest'
46

@@ -21,8 +23,9 @@ import OrganizationSettingsSectionPage from '@/app/o/[organizationId]/settings/[
2123

2224
const mockGetSession = authMockFns.mockGetSession
2325

24-
describe('organization request settings routing', () => {
26+
describe('organization settings access', () => {
2527
beforeEach(() => {
28+
resetDbChainMock()
2629
mockGetSession.mockResolvedValue({ user: { id: 'viewer' } })
2730
mocks.authorize.mockResolvedValue(true)
2831
})
@@ -36,4 +39,18 @@ describe('organization request settings routing', () => {
3639
})
3740
).rejects.toThrow('NEXT_NOT_FOUND')
3841
})
42+
43+
it.each(['admin', 'mothership'])(
44+
'conceals %s from organization admins without platform authority',
45+
async (section) => {
46+
const props = {
47+
params: Promise.resolve({ organizationId: 'organization', section }),
48+
}
49+
queueTableRows(user, [{ role: 'admin' }])
50+
await OrganizationSettingsSectionPage(props)
51+
52+
queueTableRows(user, [{ role: 'user' }])
53+
await expect(OrganizationSettingsSectionPage(props)).rejects.toThrow('NEXT_NOT_FOUND')
54+
}
55+
)
3956
})

‎apps/sim/app/o/[organizationId]/settings/[section]/page.tsx‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { prefetchStandaloneGeneral } from '@/components/settings/prefetch-standa
1111
import { SettingsSectionProvider } from '@/components/settings/settings-panel'
1212
import { getSession } from '@/lib/auth'
1313
import { organizationRoutes } from '@/lib/navigation/paths'
14+
import { isPlatformAdmin } from '@/lib/permissions/super-user'
1415
import { authorizeOrganizationSettingsSection } from '@/lib/settings/application/organization-section-access'
1516
import { getQueryClient } from '@/app/_shell/providers/get-query-client'
1617
import { buildAuthCrossLink } from '@/app/(auth)/auth-redirect'
@@ -39,8 +40,8 @@ export async function generateMetadata({
3940

4041
/**
4142
* One settings surface for the organization view. An organization section runs
42-
* through the organization gate; an account section needs only the sign-in the
43-
* layout already established, and renders through the account plane's renderer.
43+
* through the organization gate; account sections use the account renderer,
44+
* with platform administration restricted to platform admins.
4445
*/
4546
export default async function OrganizationSettingsSectionPage({
4647
params,
@@ -81,6 +82,10 @@ export default async function OrganizationSettingsSectionPage({
8182
return <OrganizationSettings section={resolved.section} />
8283
}
8384

85+
if (resolved.section === 'admin' || resolved.section === 'mothership') {
86+
if (!(await isPlatformAdmin(session.user.id))) notFound()
87+
}
88+
8489
/**
8590
* No Suspense boundary: a fallback mounted with the section puts React's 300ms reveal floor
8691
* under every switch (see the workspace section layout).

‎apps/sim/app/o/[organizationId]/settings/navigation.ts‎

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,11 @@ export type OrganizationSurfaceSettingsSection =
2727
* The account sections the organization surface hosts, rendered by the account
2828
* plane's own renderer. General is the one that belongs to the person alone: the
2929
* personal Subscription gives way to the organization's, which sits beside
30-
* General in the Account group, and the Desktop, Browser, and Terminal sections
31-
* are bound to the workspace they are opened from.
30+
* General in the Account group. Platform administration keeps the account
31+
* plane's access rules; Desktop, Browser, and Terminal are workspace-bound.
3232
*/
3333
export const ORGANIZATION_SURFACE_ACCOUNT_ITEMS: SettingsNavigationItem<AccountSettingsSection>[] =
34-
ACCOUNT_SETTINGS_ITEMS.filter((item) => item.id === 'general')
34+
ACCOUNT_SETTINGS_ITEMS.filter((item) => item.id === 'general' || item.group === 'platform')
3535

3636
export function resolveOrganizationSettingsSection(
3737
path: string
@@ -108,10 +108,13 @@ export function organizationSettingsNavigation(
108108
export function organizationSurfaceSettingsNavigation(
109109
isAdmin: boolean,
110110
features: OrganizationSettingsFeatures,
111-
availability: { connectedAccounts: boolean; search: boolean; knowledgeGraphs?: boolean }
111+
availability: { connectedAccounts: boolean; search: boolean; knowledgeGraphs?: boolean },
112+
isPlatformAdmin: boolean
112113
): SettingsNavigationItem<AccountSettingsSection | OrganizationSettingsSection>[] {
113114
return [
114-
...ORGANIZATION_SURFACE_ACCOUNT_ITEMS,
115+
...ORGANIZATION_SURFACE_ACCOUNT_ITEMS.filter(
116+
(item) => item.group !== 'platform' || isPlatformAdmin
117+
),
115118
...organizationSettingsNavigation(isAdmin, features, availability),
116119
]
117120
}

‎apps/sim/app/o/[organizationId]/settings/organization-settings-sidebar.tsx‎

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { usePathname } from 'next/navigation'
55
import { ORGANIZATION_SETTINGS_GROUPS } from '@/components/settings/navigation'
66
import { SettingsSidebar } from '@/components/settings/settings-sidebar'
77
import { isApiClientError } from '@/lib/api/client/errors'
8+
import { useSession } from '@/lib/auth/auth-client'
89
import { isEnterprise } from '@/lib/billing/plan-helpers'
910
import {
1011
hasPaidSubscriptionStatus,
@@ -30,6 +31,7 @@ export function OrganizationSettingsSidebar(props: OrganizationSettingsSidebarPr
3031
useOrganizationContext()
3132
const knowledgeGraphs = useFeatureFlag('mothership-memory-spaces')
3233
const pathname = usePathname()
34+
const { data: session } = useSession()
3335
const queryClient = useQueryClient()
3436
const refreshPlan = viewer.isAdmin && settingsFeatures.hosted && settingsFeatures.billingEnabled
3537
const { data: summary, error } = useOrganizationBillingSummary(organization.id, {
@@ -67,11 +69,16 @@ export function OrganizationSettingsSidebar(props: OrganizationSettingsSidebarPr
6769
plane='organization'
6870
activeSection={resolveOrganizationSurfaceSection(pathname ?? '')?.section ?? 'general'}
6971
groups={ORGANIZATION_SETTINGS_GROUPS}
70-
items={organizationSurfaceSettingsNavigation(isAdmin, features, {
71-
connectedAccounts: connectedAccountsAvailable,
72-
search: searchAccess.memberScoped,
73-
knowledgeGraphs,
74-
})}
72+
items={organizationSurfaceSettingsNavigation(
73+
isAdmin,
74+
features,
75+
{
76+
connectedAccounts: connectedAccountsAvailable,
77+
search: searchAccess.memberScoped,
78+
knowledgeGraphs,
79+
},
80+
session?.user?.role === 'admin'
81+
)}
7582
hrefForSection={(section) => routes.settingsSection(section)}
7683
onSectionIntent={(section) =>
7784
warmOrganizationSettingsSectionQuery(

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -887,6 +887,7 @@ export const ORGANIZATION_SETTINGS_GROUPS = [
887887
{ key: 'organization', title: 'Organization' },
888888
{ key: 'governance', title: 'Governance' },
889889
{ key: 'sim-search', title: 'Sim Search' },
890+
{ key: 'platform', title: 'Platform' },
890891
] as const
891892

892893
type OrganizationSettingsGroup = (typeof ORGANIZATION_SETTINGS_GROUPS)[number]['key']

0 commit comments

Comments
 (0)