From 2ca98149d08f1b3c4f96e1c20a54378768d20244 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Wed, 15 Jul 2026 09:57:25 -0700 Subject: [PATCH 1/3] fix(workspace): recover from stale sessions instead of blank loader after impersonation --- .../impersonation-banner.tsx | 4 +- .../settings/components/admin/admin.tsx | 4 +- apps/sim/app/workspace/page.tsx | 102 ++++++++++++++---- 3 files changed, 89 insertions(+), 21 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/components/impersonation-banner/impersonation-banner.tsx b/apps/sim/app/workspace/[workspaceId]/components/impersonation-banner/impersonation-banner.tsx index 8a995752b33..6416d73a255 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/impersonation-banner/impersonation-banner.tsx +++ b/apps/sim/app/workspace/[workspaceId]/components/impersonation-banner/impersonation-banner.tsx @@ -4,6 +4,7 @@ import { useState } from 'react' import { Banner } from '@sim/emcn' import { useSession } from '@/lib/auth/auth-client' import { useStopImpersonating } from '@/hooks/queries/admin-users' +import { clearUserData } from '@/stores' function getImpersonationBannerText(userLabel: string, userEmail?: string) { return `Impersonating ${userLabel}${userEmail ? ` (${userEmail})` : ''}. Changes will apply to this account until you switch back.` @@ -35,8 +36,9 @@ export function ImpersonationBanner() { onError: () => { setIsRedirecting(false) }, - onSuccess: () => { + onSuccess: async () => { setIsRedirecting(true) + await clearUserData() window.location.assign('/workspace') }, }) diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/admin/admin.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/admin/admin.tsx index a46263c1ebb..9d7094ab17f 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/admin/admin.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/admin/admin.tsx @@ -21,6 +21,7 @@ import { } from '@/hooks/queries/admin-users' import { useGeneralSettings, useUpdateGeneralSetting } from '@/hooks/queries/general-settings' import { useImportWorkflow } from '@/hooks/queries/workflows' +import { clearUserData } from '@/stores' const PAGE_SIZE = 20 as const @@ -109,7 +110,8 @@ export function Admin() { onError: () => { setImpersonatingUserId(null) }, - onSuccess: () => { + onSuccess: async () => { + await clearUserData() window.location.assign('/workspace') }, } diff --git a/apps/sim/app/workspace/page.tsx b/apps/sim/app/workspace/page.tsx index b5812e838a7..419ba3d4c8f 100644 --- a/apps/sim/app/workspace/page.tsx +++ b/apps/sim/app/workspace/page.tsx @@ -1,35 +1,73 @@ 'use client' import { useEffect, useRef } from 'react' +import { Chip } from '@sim/emcn' +import { CircleAlert } from '@sim/emcn/icons' import { createLogger } from '@sim/logger' import { useRouter } from 'next/navigation' +import { isApiClientError } from '@/lib/api/client/errors' import { requestJson } from '@/lib/api/client/request' import { getWorkflowStateContract } from '@/lib/api/contracts/workflows' import { createWorkspaceContract } from '@/lib/api/contracts/workspaces' -import { useSession } from '@/lib/auth/auth-client' +import { signOut, useSession } from '@/lib/auth/auth-client' import { WorkspaceRecencyStorage } from '@/lib/core/utils/browser-storage' import { useWorkspacesWithMetadata, type WorkspaceCreationPolicy } from '@/hooks/queries/workspace' +import { clearUserData } from '@/stores' const logger = createLogger('WorkspacePage') +/** + * A 401 while the session claims we're authenticated means the auth cookies + * are stale or inconsistent (e.g. after an impersonation session expired or + * was switched). The only reliable recovery is a full sign-out, which clears + * every auth cookie server-side — matching what "clear browser cache" did + * manually — followed by a clean login. + */ +function isStaleSessionError(error: unknown): boolean { + return isApiClientError(error) && error.status === 401 +} + +async function recoverFromStaleSession(): Promise { + try { + await Promise.all([signOut(), clearUserData()]) + } catch (error) { + logger.error('Failed to sign out while recovering from a stale session:', error) + } + window.location.assign('/login') +} + export default function WorkspacePage() { const router = useRouter() - const { data: session, isPending: isSessionPending } = useSession() + const { data: session, isPending: isSessionPending, error: sessionError } = useSession() const isAuthenticated = !isSessionPending && !!session?.user const hasRedirectedRef = useRef(false) + const isRecoveringRef = useRef(false) + + const { + data, + isLoading: isWorkspacesLoading, + error: workspacesError, + } = useWorkspacesWithMetadata(isAuthenticated) - const { data, isLoading: isWorkspacesLoading } = useWorkspacesWithMetadata(isAuthenticated) + useEffect(() => { + if (!isStaleSessionError(workspacesError) || isRecoveringRef.current) return + isRecoveringRef.current = true + logger.warn('Session cookies are stale (authenticated session but 401 API); signing out') + void recoverFromStaleSession() + }, [workspacesError]) useEffect(() => { if (isSessionPending || hasRedirectedRef.current) return + if (sessionError) return + if (!session?.user) { logger.info('User not authenticated, redirecting to login') router.replace('/login') return } - if (isWorkspacesLoading || !data) return + if (isWorkspacesLoading || workspacesError || !data) return hasRedirectedRef.current = true @@ -57,26 +95,52 @@ export default function WorkspacePage() { logger.info(`Redirecting to workspace: ${targetWorkspace.id}`) router.replace(`/workspace/${targetWorkspace.id}/home`) - }, [session, isSessionPending, isWorkspacesLoading, data, router]) + }, [session, isSessionPending, sessionError, isWorkspacesLoading, workspacesError, data, router]) + + const failedToLoad = + Boolean(sessionError) || (Boolean(workspacesError) && !isStaleSessionError(workspacesError)) - if (isSessionPending || isWorkspacesLoading) { + if (failedToLoad) { return ( -
-
-
+
+
+
+ +
+
+

+ Could not load your workspaces +

+

+ Something went wrong while loading your account. Try again, or sign out and log back + in. +

+
+
+ window.location.reload()}> + Try again + + void recoverFromStaleSession()}>Sign out +
+
+
) } - return null + return ( +
+
+
+ ) } async function handleWorkflowRedirect( From 35ccd0b021d13ca6f19a6ac8ffcdf6ebb7f7f9c0 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Wed, 15 Jul 2026 10:07:45 -0700 Subject: [PATCH 2/3] fix(workspace): gate stale-session recovery on auth state and surface failed sign-out --- apps/sim/app/workspace/page.tsx | 30 +++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/apps/sim/app/workspace/page.tsx b/apps/sim/app/workspace/page.tsx index 419ba3d4c8f..f6db8d33569 100644 --- a/apps/sim/app/workspace/page.tsx +++ b/apps/sim/app/workspace/page.tsx @@ -1,6 +1,6 @@ 'use client' -import { useEffect, useRef } from 'react' +import { useEffect, useRef, useState } from 'react' import { Chip } from '@sim/emcn' import { CircleAlert } from '@sim/emcn/icons' import { createLogger } from '@sim/logger' @@ -27,13 +27,22 @@ function isStaleSessionError(error: unknown): boolean { return isApiClientError(error) && error.status === 401 } -async function recoverFromStaleSession(): Promise { +/** + * Signs out (clearing every auth cookie server-side), wipes per-user client + * state, and navigates to login. Returns false without navigating when the + * sign-out request fails — the cookies are still set, so going to /login + * would only get bounced back to /workspace by the middleware. + */ +async function recoverFromStaleSession(): Promise { try { - await Promise.all([signOut(), clearUserData()]) + await signOut() } catch (error) { logger.error('Failed to sign out while recovering from a stale session:', error) + return false } + await clearUserData() window.location.assign('/login') + return true } export default function WorkspacePage() { @@ -42,6 +51,7 @@ export default function WorkspacePage() { const isAuthenticated = !isSessionPending && !!session?.user const hasRedirectedRef = useRef(false) const isRecoveringRef = useRef(false) + const [recoveryFailed, setRecoveryFailed] = useState(false) const { data, @@ -50,11 +60,15 @@ export default function WorkspacePage() { } = useWorkspacesWithMetadata(isAuthenticated) useEffect(() => { - if (!isStaleSessionError(workspacesError) || isRecoveringRef.current) return + if (!isAuthenticated || !isStaleSessionError(workspacesError) || isRecoveringRef.current) return isRecoveringRef.current = true logger.warn('Session cookies are stale (authenticated session but 401 API); signing out') - void recoverFromStaleSession() - }, [workspacesError]) + void recoverFromStaleSession().then((recovered) => { + if (recovered) return + isRecoveringRef.current = false + setRecoveryFailed(true) + }) + }, [isAuthenticated, workspacesError]) useEffect(() => { if (isSessionPending || hasRedirectedRef.current) return @@ -98,7 +112,9 @@ export default function WorkspacePage() { }, [session, isSessionPending, sessionError, isWorkspacesLoading, workspacesError, data, router]) const failedToLoad = - Boolean(sessionError) || (Boolean(workspacesError) && !isStaleSessionError(workspacesError)) + recoveryFailed || + Boolean(sessionError) || + (isAuthenticated && Boolean(workspacesError) && !isStaleSessionError(workspacesError)) if (failedToLoad) { return ( From 81e74aecdb6fb76b57350ff40c6e467f649fc948 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Wed, 15 Jul 2026 10:15:33 -0700 Subject: [PATCH 3/3] fix(workspace): proceed with redirect when session query errors but cached identity exists --- apps/sim/app/workspace/page.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/sim/app/workspace/page.tsx b/apps/sim/app/workspace/page.tsx index f6db8d33569..5e277d699e3 100644 --- a/apps/sim/app/workspace/page.tsx +++ b/apps/sim/app/workspace/page.tsx @@ -73,9 +73,10 @@ export default function WorkspacePage() { useEffect(() => { if (isSessionPending || hasRedirectedRef.current) return - if (sessionError) return - if (!session?.user) { + // Indeterminate auth (errored session query, no cached identity): show + // the error card — /login would bounce back while a session cookie exists. + if (sessionError) return logger.info('User not authenticated, redirecting to login') router.replace('/login') return @@ -113,7 +114,7 @@ export default function WorkspacePage() { const failedToLoad = recoveryFailed || - Boolean(sessionError) || + (Boolean(sessionError) && !session?.user) || (isAuthenticated && Boolean(workspacesError) && !isStaleSessionError(workspacesError)) if (failedToLoad) {