Skip to content

Commit a2f1ac7

Browse files
waleedlatif1claude
andauthored
fix(navigation): land on workspaces from the app entry, not settings (#7692)
The signed-in front door sent organization members whose organization has not been rolled out to workspace settings, so opening the app dropped them on the General settings form instead of their workspace. Send them to the workspace picker, which is where the entry pointed before the organization surface existed. The default landing never opens settings; the /o guards still fall back to settings for viewers who explicitly asked for the organization surface. Also stop the entry from bouncing a stale session cookie to /login. The proxy treats /home as an app surface and redirects cookie-less requests to /login before the route renders, and auth-disabled deployments always resolve an anonymous session, so a null session here always means a present-but-invalid cookie. Redirecting that to /login was bounced straight back by the proxy's presence-only cookie check, looping until the browser gave up and leaving the viewer unable to reach the login page at all. Hand off to the workspace loader instead, the one identity-recovery surface, which clears the stale cookies before navigating. Claude-Session: https://claude.ai/code/session_01CGkraEeFNiPCU4jb784FE4 Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent bfa61f3 commit a2f1ac7

5 files changed

Lines changed: 34 additions & 18 deletions

File tree

apps/sim/app/home/page.test.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,16 @@ describe('AppEntryPage', () => {
2828
vi.clearAllMocks()
2929
})
3030

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

34-
await expect(AppEntryPage()).rejects.toThrow('NEXT_REDIRECT:/login')
39+
await expect(AppEntryPage()).rejects.toThrow('NEXT_REDIRECT:/workspace')
40+
expect(mockRedirect).not.toHaveBeenCalledWith('/login')
3541
expect(mockResolveAppEntryPath).not.toHaveBeenCalled()
3642
})
3743

apps/sim/app/home/page.tsx

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { redirect } from 'next/navigation'
22
import { getSession } from '@/lib/auth'
3+
import { WORKSPACES_PATH } from '@/lib/navigation/paths'
34
import { resolveAppEntryPath } from '@/lib/navigation/resolve-app-entry'
45

56
/**
@@ -10,8 +11,20 @@ import { resolveAppEntryPath } from '@/lib/navigation/resolve-app-entry'
1011
*/
1112
export default async function AppEntryPage() {
1213
const session = await getSession()
14+
15+
/**
16+
* A missing session here is never a signed-out visitor: the proxy treats `/home`
17+
* as an app surface and sends cookie-less requests to `/login` before this
18+
* renders, and auth-disabled deployments always resolve an anonymous session. So
19+
* this branch means the cookie is present but its session is gone — and
20+
* redirecting to `/login` would be bounced straight back by the proxy, which
21+
* reads cookie presence rather than validity, looping until the browser gives up.
22+
* Hand off to the workspace loader instead: it is the app's one identity-recovery
23+
* surface, and it clears the stale cookies through `recoverFromStaleSession`
24+
* before navigating to `/login`.
25+
*/
1326
if (!session?.user) {
14-
redirect('/login')
27+
redirect(WORKSPACES_PATH)
1528
}
1629

1730
redirect(await resolveAppEntryPath(session))

apps/sim/lib/navigation/organization-rollout.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ describe('organization rollout during impersonation', () => {
6666
session: { impersonatedBy: 'platform-admin', activeOrganizationId: 'customer-org' },
6767
}
6868
await expect(resolveAppEntryPath(impersonatedSession)).resolves.toBe(
69-
knowledge && groups ? '/o/customer-org/home' : '/workspace?redirect=settings'
69+
knowledge && groups ? '/o/customer-org/home' : '/workspace'
7070
)
7171
expect(mocks.landing).toHaveBeenLastCalledWith('customer-member', 'customer-org')
7272
expect(mocks.platformAdmin).not.toHaveBeenCalled()

apps/sim/lib/navigation/resolve-app-entry.test.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,10 @@ describe('resolveAppEntryPath', () => {
3737
expect(mockSearchAvailable).toHaveBeenCalledWith({ organizationId: 'org-2' })
3838
})
3939

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

4846
it('lands a viewer with no organization on the workspace picker', async () => {
Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,27 @@
11
import { getActiveOrganizationId } from '@/lib/auth/session-response'
22
import { isKnowledgeMemberAccessAvailable } from '@/lib/knowledge/access/availability'
3-
import {
4-
organizationRoutes,
5-
WORKSPACE_SETTINGS_PATH,
6-
WORKSPACES_PATH,
7-
} from '@/lib/navigation/paths'
3+
import { organizationRoutes, WORKSPACES_PATH } from '@/lib/navigation/paths'
84
import { resolveOrganizationLanding } from '@/lib/organizations/surface'
95

106
interface EntrySession {
117
user: { id: string }
128
}
139

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

0 commit comments

Comments
 (0)