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
17 changes: 16 additions & 1 deletion apps/sim/app/o/[organizationId]/home/page.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ describe('organization Search page gates', () => {
['Home', () => OrganizationHomePage({ params })],
['Search', () => OrganizationSearchPage({ params })],
['chat', () => OrganizationChatPage({ params })],
['organization entry', () => OrganizationPage({ params })],
] as const)('redirects %s to workspace settings when Search is disabled', async (_name, open) => {
mocks.context.mockResolvedValue({ searchAccess: { memberScoped: false, sourceMirrored: true } })
await expect(open()).rejects.toThrow('redirect:/workspace?redirect=settings')
Expand All @@ -58,6 +57,7 @@ describe('organization Search page gates', () => {
['Home', () => OrganizationHomePage({ params })],
['Search', () => OrganizationSearchPage({ params })],
['chat', () => OrganizationChatPage({ params })],
['organization entry', () => OrganizationPage({ params })],
] as const)('denies %s to nonmembers before loading content', async (_name, open) => {
mocks.context.mockResolvedValue(null)
await expect(open()).rejects.toThrow('not-found')
Expand Down Expand Up @@ -90,6 +90,21 @@ describe('organization Search page gates', () => {
await expect(OrganizationPage({ params })).rejects.toThrow('redirect:/o/org-1/home')
})

it('preserves the organization entry through sign-in', async () => {
authMockFns.mockGetSession.mockResolvedValue(null)

await expect(OrganizationPage({ params })).rejects.toThrow(
'redirect:/login?callbackUrl=%2Fo%2Forg-1'
)
expect(mocks.context).not.toHaveBeenCalled()
})

it('keeps the organization entry in organization settings when Search is disabled', async () => {
mocks.context.mockResolvedValue({ searchAccess: { memberScoped: false } })
await expect(OrganizationPage({ params })).rejects.toThrow('redirect:/o/org-1/settings/members')
expect(mocks.context).toHaveBeenCalledWith('org-1', 'viewer')
})

it('propagates availability failures instead of rendering the Assistant', async () => {
mocks.context.mockRejectedValue(new Error('Availability unavailable'))
await expect(OrganizationHomePage({ params })).rejects.toThrow('Availability unavailable')
Expand Down
16 changes: 15 additions & 1 deletion apps/sim/app/o/[organizationId]/layout.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ vi.mock('next/headers', () => ({
}))

vi.mock('next/navigation', () => ({
redirect: vi.fn(),
redirect: (path: string) => {
throw new Error(`redirect:${path}`)
},
}))

vi.mock('@/lib/organizations/surface', () => ({
Expand Down Expand Up @@ -66,6 +68,18 @@ describe('OrganizationLayout', () => {
mockGetSession.mockResolvedValue({ user: { id: 'viewer-1' } })
})

it('returns signed-out visitors to the organization entry after sign-in', async () => {
mockGetSession.mockResolvedValue(null)

await expect(
OrganizationLayout({
children: null,
params: Promise.resolve({ organizationId: 'org-1' }),
})
).rejects.toThrow('redirect:/login?callbackUrl=%2Fo%2Forg-1')
expect(mockGetOrganizationSurfaceContext).not.toHaveBeenCalled()
})

it('renders the surface for a member and seeds the chrome from the collapse cookie', async () => {
mockGetOrganizationSurfaceContext.mockResolvedValue(SURFACE_CONTEXT)

Expand Down
2 changes: 1 addition & 1 deletion apps/sim/app/o/[organizationId]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export default async function OrganizationLayout({
if (!session?.user) {
redirect(
buildAuthCrossLink('/login', {
callbackUrl: organizationRoutes(organizationId).home,
callbackUrl: organizationRoutes(organizationId).root,
isInviteFlow: false,
})
)
Expand Down
11 changes: 7 additions & 4 deletions apps/sim/app/o/[organizationId]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
import { notFound, redirect } from 'next/navigation'
import { getSession } from '@/lib/auth'
import { organizationRoutes, WORKSPACE_SETTINGS_PATH } from '@/lib/navigation/paths'
import { organizationRoutes } from '@/lib/navigation/paths'
import { getOrganizationSurfaceContext } from '@/lib/organizations/surface'
import { buildAuthCrossLink } from '@/app/(auth)/auth-redirect'

export default async function OrganizationPage({
params,
}: {
params: Promise<{ organizationId: string }>
}) {
const { organizationId } = await params
const routes = organizationRoutes(organizationId)
const session = await getSession()
if (!session?.user?.id) notFound()
if (!session?.user?.id) {
redirect(buildAuthCrossLink('/login', { callbackUrl: routes.root, isInviteFlow: false }))
}
const context = await getOrganizationSurfaceContext(organizationId, session.user.id)
if (!context) notFound()
const routes = organizationRoutes(organizationId)
redirect(context.searchAccess.memberScoped ? routes.home : WORKSPACE_SETTINGS_PATH)
redirect(context.searchAccess.memberScoped ? routes.home : routes.settingsSection('members'))
}
39 changes: 39 additions & 0 deletions apps/sim/app/o/[organizationId]/settings/[section]/layout.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/**
* @vitest-environment node
*/
import { describe, expect, it, vi } from 'vitest'

vi.mock('next/navigation', () => ({
redirect: (path: string) => {
throw new Error(`redirect:${path}`)
},
notFound: () => {
throw new Error('not-found')
},
}))
vi.mock('@/components/settings/settings-header', () => ({
SettingsHeaderProvider: () => null,
SettingsHeaderShell: () => null,
}))

import OrganizationSettingsSectionLayout from '@/app/o/[organizationId]/settings/[section]/layout'

describe('organization settings section routing', () => {
it('redirects legacy authorized-app links before the section loading boundary', async () => {
await expect(
OrganizationSettingsSectionLayout({
children: null,
params: Promise.resolve({ organizationId: 'target-org', section: 'authorized-apps' }),
})
).rejects.toThrow('redirect:/o/target-org/settings/general?view=authorized-apps')
})

it('rejects unknown sections', async () => {
await expect(
OrganizationSettingsSectionLayout({
children: null,
params: Promise.resolve({ organizationId: 'target-org', section: 'unknown' }),
})
).rejects.toThrow('not-found')
})
})
12 changes: 9 additions & 3 deletions apps/sim/app/o/[organizationId]/settings/[section]/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,29 @@
import type { ReactNode } from 'react'
import { notFound } from 'next/navigation'
import { notFound, redirect } from 'next/navigation'
import {
getSettingsSectionMeta,
ORGANIZATION_SETTINGS_ITEMS,
toSettingsHeaderMeta,
} from '@/components/settings/navigation'
import { SettingsHeaderProvider, SettingsHeaderShell } from '@/components/settings/settings-header'
import { organizationRoutes } from '@/lib/navigation/paths'
import { resolveOrganizationSurfaceSection } from '@/app/o/[organizationId]/settings/navigation'

interface OrganizationSettingsSectionLayoutProps {
children: ReactNode
params: Promise<{ section: string }>
params: Promise<{ organizationId: string; section: string }>
}

export default async function OrganizationSettingsSectionLayout({
children,
params,
}: OrganizationSettingsSectionLayoutProps) {
const { section } = await params
const { organizationId, section } = await params
if (section === 'authorized-apps') {
redirect(
`${organizationRoutes(organizationId).settingsSection('general')}?view=authorized-apps`
)
}
const resolved = resolveOrganizationSurfaceSection(section)
const meta =
resolved?.plane === 'organization'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/** Lets the section layout show its heading while authorization and content finish loading. */
export default function OrganizationSettingsSectionLoading() {
return null
}
3 changes: 0 additions & 3 deletions apps/sim/app/o/[organizationId]/settings/[section]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,6 @@ export default async function OrganizationSettingsSectionPage({
}: OrganizationSettingsSectionPageProps) {
const { organizationId, section } = await params
const routes = organizationRoutes(organizationId)
if (section === 'authorized-apps') {
redirect(`${routes.settingsSection('general')}?view=authorized-apps`)
}
const resolved = resolveOrganizationSurfaceSection(section)
if (!resolved) notFound()
const session = await getSession()
Expand Down
19 changes: 16 additions & 3 deletions apps/sim/app/o/[organizationId]/settings/[section]/settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,22 @@ import {
} from '@/components/settings/navigation'
import { SettingsSectionProvider } from '@/components/settings/settings-panel'
import { useOrganizationContext } from '@/app/o/[organizationId]/providers/organization-provider'
import { OrganizationIntegrationsSettings } from '@/app/o/[organizationId]/settings/components/integrations/organization-integrations-settings'
import { OrganizationSearchMcp } from '@/app/o/[organizationId]/settings/components/organization-search-mcp'
import { OrganizationConnectedAccounts } from '@/ee/credential-groups/components/organization-connected-accounts'

const OrganizationIntegrationsSettings = dynamic(() =>
import(
'@/app/o/[organizationId]/settings/components/integrations/organization-integrations-settings'
).then((m) => m.OrganizationIntegrationsSettings)
)
const OrganizationSearchMcp = dynamic(() =>
import('@/app/o/[organizationId]/settings/components/organization-search-mcp').then(
(m) => m.OrganizationSearchMcp
)
)
const OrganizationConnectedAccounts = dynamic(() =>
import('@/ee/credential-groups/components/organization-connected-accounts').then(
(m) => m.OrganizationConnectedAccounts
)
)

const TeamManagement = dynamic(() =>
import('@/app/workspace/[workspaceId]/settings/components/team-management/team-management').then(
Expand Down
Loading
Loading