Skip to content

Commit 4d0973f

Browse files
authored
fix(workspace): recover from stale sessions instead of blank loader after impersonation (#5688)
* fix(workspace): recover from stale sessions instead of blank loader after impersonation * fix(workspace): gate stale-session recovery on auth state and surface failed sign-out * fix(workspace): proceed with redirect when session query errors but cached identity exists
1 parent 64280c9 commit 4d0973f

3 files changed

Lines changed: 107 additions & 22 deletions

File tree

apps/sim/app/workspace/[workspaceId]/components/impersonation-banner/impersonation-banner.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { useState } from 'react'
44
import { Banner } from '@sim/emcn'
55
import { useSession } from '@/lib/auth/auth-client'
66
import { useStopImpersonating } from '@/hooks/queries/admin-users'
7+
import { clearUserData } from '@/stores'
78

89
function getImpersonationBannerText(userLabel: string, userEmail?: string) {
910
return `Impersonating ${userLabel}${userEmail ? ` (${userEmail})` : ''}. Changes will apply to this account until you switch back.`
@@ -35,8 +36,9 @@ export function ImpersonationBanner() {
3536
onError: () => {
3637
setIsRedirecting(false)
3738
},
38-
onSuccess: () => {
39+
onSuccess: async () => {
3940
setIsRedirecting(true)
41+
await clearUserData()
4042
window.location.assign('/workspace')
4143
},
4244
})

apps/sim/app/workspace/[workspaceId]/settings/components/admin/admin.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
} from '@/hooks/queries/admin-users'
2222
import { useGeneralSettings, useUpdateGeneralSetting } from '@/hooks/queries/general-settings'
2323
import { useImportWorkflow } from '@/hooks/queries/workflows'
24+
import { clearUserData } from '@/stores'
2425

2526
const PAGE_SIZE = 20 as const
2627

@@ -109,7 +110,8 @@ export function Admin() {
109110
onError: () => {
110111
setImpersonatingUserId(null)
111112
},
112-
onSuccess: () => {
113+
onSuccess: async () => {
114+
await clearUserData()
113115
window.location.assign('/workspace')
114116
},
115117
}

apps/sim/app/workspace/page.tsx

Lines changed: 101 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,88 @@
11
'use client'
22

3-
import { useEffect, useRef } from 'react'
3+
import { useEffect, useRef, useState } from 'react'
4+
import { Chip } from '@sim/emcn'
5+
import { CircleAlert } from '@sim/emcn/icons'
46
import { createLogger } from '@sim/logger'
57
import { useRouter } from 'next/navigation'
8+
import { isApiClientError } from '@/lib/api/client/errors'
69
import { requestJson } from '@/lib/api/client/request'
710
import { getWorkflowStateContract } from '@/lib/api/contracts/workflows'
811
import { createWorkspaceContract } from '@/lib/api/contracts/workspaces'
9-
import { useSession } from '@/lib/auth/auth-client'
12+
import { signOut, useSession } from '@/lib/auth/auth-client'
1013
import { WorkspaceRecencyStorage } from '@/lib/core/utils/browser-storage'
1114
import { useWorkspacesWithMetadata, type WorkspaceCreationPolicy } from '@/hooks/queries/workspace'
15+
import { clearUserData } from '@/stores'
1216

1317
const logger = createLogger('WorkspacePage')
1418

19+
/**
20+
* A 401 while the session claims we're authenticated means the auth cookies
21+
* are stale or inconsistent (e.g. after an impersonation session expired or
22+
* was switched). The only reliable recovery is a full sign-out, which clears
23+
* every auth cookie server-side — matching what "clear browser cache" did
24+
* manually — followed by a clean login.
25+
*/
26+
function isStaleSessionError(error: unknown): boolean {
27+
return isApiClientError(error) && error.status === 401
28+
}
29+
30+
/**
31+
* Signs out (clearing every auth cookie server-side), wipes per-user client
32+
* state, and navigates to login. Returns false without navigating when the
33+
* sign-out request fails — the cookies are still set, so going to /login
34+
* would only get bounced back to /workspace by the middleware.
35+
*/
36+
async function recoverFromStaleSession(): Promise<boolean> {
37+
try {
38+
await signOut()
39+
} catch (error) {
40+
logger.error('Failed to sign out while recovering from a stale session:', error)
41+
return false
42+
}
43+
await clearUserData()
44+
window.location.assign('/login')
45+
return true
46+
}
47+
1548
export default function WorkspacePage() {
1649
const router = useRouter()
17-
const { data: session, isPending: isSessionPending } = useSession()
50+
const { data: session, isPending: isSessionPending, error: sessionError } = useSession()
1851
const isAuthenticated = !isSessionPending && !!session?.user
1952
const hasRedirectedRef = useRef(false)
53+
const isRecoveringRef = useRef(false)
54+
const [recoveryFailed, setRecoveryFailed] = useState(false)
2055

21-
const { data, isLoading: isWorkspacesLoading } = useWorkspacesWithMetadata(isAuthenticated)
56+
const {
57+
data,
58+
isLoading: isWorkspacesLoading,
59+
error: workspacesError,
60+
} = useWorkspacesWithMetadata(isAuthenticated)
61+
62+
useEffect(() => {
63+
if (!isAuthenticated || !isStaleSessionError(workspacesError) || isRecoveringRef.current) return
64+
isRecoveringRef.current = true
65+
logger.warn('Session cookies are stale (authenticated session but 401 API); signing out')
66+
void recoverFromStaleSession().then((recovered) => {
67+
if (recovered) return
68+
isRecoveringRef.current = false
69+
setRecoveryFailed(true)
70+
})
71+
}, [isAuthenticated, workspacesError])
2272

2373
useEffect(() => {
2474
if (isSessionPending || hasRedirectedRef.current) return
2575

2676
if (!session?.user) {
77+
// Indeterminate auth (errored session query, no cached identity): show
78+
// the error card — /login would bounce back while a session cookie exists.
79+
if (sessionError) return
2780
logger.info('User not authenticated, redirecting to login')
2881
router.replace('/login')
2982
return
3083
}
3184

32-
if (isWorkspacesLoading || !data) return
85+
if (isWorkspacesLoading || workspacesError || !data) return
3386

3487
hasRedirectedRef.current = true
3588

@@ -57,26 +110,54 @@ export default function WorkspacePage() {
57110

58111
logger.info(`Redirecting to workspace: ${targetWorkspace.id}`)
59112
router.replace(`/workspace/${targetWorkspace.id}/home`)
60-
}, [session, isSessionPending, isWorkspacesLoading, data, router])
113+
}, [session, isSessionPending, sessionError, isWorkspacesLoading, workspacesError, data, router])
114+
115+
const failedToLoad =
116+
recoveryFailed ||
117+
(Boolean(sessionError) && !session?.user) ||
118+
(isAuthenticated && Boolean(workspacesError) && !isStaleSessionError(workspacesError))
61119

62-
if (isSessionPending || isWorkspacesLoading) {
120+
if (failedToLoad) {
63121
return (
64-
<div className='flex h-screen w-full items-center justify-center'>
65-
<div
66-
className='size-[18px] animate-spin rounded-full'
67-
style={{
68-
background:
69-
'conic-gradient(from 0deg, hsl(var(--muted-foreground)) 0deg 120deg, transparent 120deg 180deg, hsl(var(--muted-foreground)) 180deg 300deg, transparent 300deg 360deg)',
70-
mask: 'radial-gradient(farthest-side, transparent calc(100% - 1.5px), black calc(100% - 1.5px))',
71-
WebkitMask:
72-
'radial-gradient(farthest-side, transparent calc(100% - 1.5px), black calc(100% - 1.5px))',
73-
}}
74-
/>
75-
</div>
122+
<main className='flex h-screen w-full items-center justify-center bg-[var(--surface-1)] p-6'>
123+
<div className='flex max-w-md flex-col items-center gap-3 text-center'>
124+
<div className='flex size-10 items-center justify-center rounded-full bg-[var(--surface-3)]'>
125+
<CircleAlert className='size-[18px] text-[var(--text-icon)]' aria-hidden />
126+
</div>
127+
<div className='space-y-1'>
128+
<h1 className='font-medium text-[var(--text-primary)] text-lg'>
129+
Could not load your workspaces
130+
</h1>
131+
<p className='text-[var(--text-muted)] text-sm'>
132+
Something went wrong while loading your account. Try again, or sign out and log back
133+
in.
134+
</p>
135+
</div>
136+
<div className='flex items-center gap-2'>
137+
<Chip variant='primary' onClick={() => window.location.reload()}>
138+
Try again
139+
</Chip>
140+
<Chip onClick={() => void recoverFromStaleSession()}>Sign out</Chip>
141+
</div>
142+
</div>
143+
</main>
76144
)
77145
}
78146

79-
return null
147+
return (
148+
<div className='flex h-screen w-full items-center justify-center'>
149+
<div
150+
className='size-[18px] animate-spin rounded-full'
151+
style={{
152+
background:
153+
'conic-gradient(from 0deg, hsl(var(--muted-foreground)) 0deg 120deg, transparent 120deg 180deg, hsl(var(--muted-foreground)) 180deg 300deg, transparent 300deg 360deg)',
154+
mask: 'radial-gradient(farthest-side, transparent calc(100% - 1.5px), black calc(100% - 1.5px))',
155+
WebkitMask:
156+
'radial-gradient(farthest-side, transparent calc(100% - 1.5px), black calc(100% - 1.5px))',
157+
}}
158+
/>
159+
</div>
160+
)
80161
}
81162

82163
async function handleWorkflowRedirect(

0 commit comments

Comments
 (0)