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
10 changes: 8 additions & 2 deletions apps/sim/app/home/page.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,16 @@ describe('AppEntryPage', () => {
vi.clearAllMocks()
})

it('sends a signed-out visitor to login without resolving an entry', async () => {
/**
* The proxy sends cookie-less requests to /login before this route renders, so a
* null session here is always a stale cookie. Redirecting to /login would be
* bounced back by the proxy's presence-only cookie check, looping forever.
*/
it('sends a stale-cookie viewer to the recovery surface, never back to login', async () => {
mockGetSession.mockResolvedValue(null)

await expect(AppEntryPage()).rejects.toThrow('NEXT_REDIRECT:/login')
await expect(AppEntryPage()).rejects.toThrow('NEXT_REDIRECT:/workspace')
expect(mockRedirect).not.toHaveBeenCalledWith('/login')
expect(mockResolveAppEntryPath).not.toHaveBeenCalled()
})

Expand Down
15 changes: 14 additions & 1 deletion apps/sim/app/home/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { redirect } from 'next/navigation'
import { getSession } from '@/lib/auth'
import { WORKSPACES_PATH } from '@/lib/navigation/paths'
import { resolveAppEntryPath } from '@/lib/navigation/resolve-app-entry'

/**
Expand All @@ -10,8 +11,20 @@ import { resolveAppEntryPath } from '@/lib/navigation/resolve-app-entry'
*/
export default async function AppEntryPage() {
const session = await getSession()

/**
* A missing session here is never a signed-out visitor: the proxy treats `/home`
* as an app surface and sends cookie-less requests to `/login` before this
* renders, and auth-disabled deployments always resolve an anonymous session. So
* this branch means the cookie is present but its session is gone — and
* redirecting to `/login` would be bounced straight back by the proxy, which
* reads cookie presence rather than validity, looping until the browser gives up.
* Hand off to the workspace loader instead: it is the app's one identity-recovery
* surface, and it clears the stale cookies through `recoverFromStaleSession`
* before navigating to `/login`.
*/
if (!session?.user) {
redirect('/login')
redirect(WORKSPACES_PATH)
}

redirect(await resolveAppEntryPath(session))
Expand Down
2 changes: 1 addition & 1 deletion apps/sim/lib/navigation/organization-rollout.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ describe('organization rollout during impersonation', () => {
session: { impersonatedBy: 'platform-admin', activeOrganizationId: 'customer-org' },
}
await expect(resolveAppEntryPath(impersonatedSession)).resolves.toBe(
knowledge && groups ? '/o/customer-org/home' : '/workspace?redirect=settings'
knowledge && groups ? '/o/customer-org/home' : '/workspace'
)
expect(mocks.landing).toHaveBeenLastCalledWith('customer-member', 'customer-org')
expect(mocks.platformAdmin).not.toHaveBeenCalled()
Expand Down
6 changes: 2 additions & 4 deletions apps/sim/lib/navigation/resolve-app-entry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,10 @@ describe('resolveAppEntryPath', () => {
expect(mockSearchAvailable).toHaveBeenCalledWith({ organizationId: 'org-2' })
})

it('opens full workspace settings when Search is disabled', async () => {
it('lands an organization member on the workspace picker when Search is disabled', async () => {
mockResolveOrganizationLanding.mockResolvedValue('org-2')
mockSearchAvailable.mockResolvedValue(false)
await expect(resolveAppEntryPath({ user: { id: 'viewer' } })).resolves.toBe(
'/workspace?redirect=settings'
)
await expect(resolveAppEntryPath({ user: { id: 'viewer' } })).resolves.toBe('/workspace')
})

it('lands a viewer with no organization on the workspace picker', async () => {
Expand Down
19 changes: 9 additions & 10 deletions apps/sim/lib/navigation/resolve-app-entry.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,27 @@
import { getActiveOrganizationId } from '@/lib/auth/session-response'
import { isKnowledgeMemberAccessAvailable } from '@/lib/knowledge/access/availability'
import {
organizationRoutes,
WORKSPACE_SETTINGS_PATH,
WORKSPACES_PATH,
} from '@/lib/navigation/paths'
import { organizationRoutes, WORKSPACES_PATH } from '@/lib/navigation/paths'
import { resolveOrganizationLanding } from '@/lib/organizations/surface'

interface EntrySession {
user: { id: string }
}

/**
* Routes organization members to Home when Search is enabled and workspace settings otherwise.
* Viewers without an organization land on the workspace picker.
* Routes organization members to Home when the organization surface is enabled for
* them. Everyone else — viewers without an organization, and members whose
* organization has not been rolled out — lands on the workspace picker, which is
* where the signed-in app's front door pointed before the organization surface
* existed. The default landing never opens settings: a viewer who did not ask for
* settings must not be dropped into them.
*/
export async function resolveAppEntryPath(session: EntrySession): Promise<string> {
const organizationId = await resolveOrganizationLanding(
session.user.id,
getActiveOrganizationId(session)
)
if (!organizationId) return WORKSPACES_PATH
const routes = organizationRoutes(organizationId)
return (await isKnowledgeMemberAccessAvailable({ organizationId }))
? routes.home
: WORKSPACE_SETTINGS_PATH
? organizationRoutes(organizationId).home
: WORKSPACES_PATH
}
Loading