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
Original file line number Diff line number Diff line change
Expand Up @@ -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.`
Expand Down Expand Up @@ -35,8 +36,9 @@ export function ImpersonationBanner() {
onError: () => {
setIsRedirecting(false)
},
onSuccess: () => {
onSuccess: async () => {
setIsRedirecting(true)
await clearUserData()
window.location.assign('/workspace')
},
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -109,7 +110,8 @@ export function Admin() {
onError: () => {
setImpersonatingUserId(null)
},
onSuccess: () => {
onSuccess: async () => {
await clearUserData()
window.location.assign('/workspace')
},
}
Expand Down
121 changes: 101 additions & 20 deletions apps/sim/app/workspace/page.tsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,88 @@
'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'
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
}

/**
* 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<boolean> {
try {
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')
Comment thread
waleedlatif1 marked this conversation as resolved.
return true
}
Comment thread
waleedlatif1 marked this conversation as resolved.

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 [recoveryFailed, setRecoveryFailed] = useState(false)

const { data, isLoading: isWorkspacesLoading } = useWorkspacesWithMetadata(isAuthenticated)
const {
data,
isLoading: isWorkspacesLoading,
error: workspacesError,
} = useWorkspacesWithMetadata(isAuthenticated)

useEffect(() => {
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().then((recovered) => {
if (recovered) return
isRecoveringRef.current = false
setRecoveryFailed(true)
})
}, [isAuthenticated, workspacesError])

useEffect(() => {
if (isSessionPending || hasRedirectedRef.current) 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
}

if (isWorkspacesLoading || !data) return
if (isWorkspacesLoading || workspacesError || !data) return

hasRedirectedRef.current = true

Expand Down Expand Up @@ -57,26 +110,54 @@ 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 =
recoveryFailed ||
(Boolean(sessionError) && !session?.user) ||
(isAuthenticated && Boolean(workspacesError) && !isStaleSessionError(workspacesError))
Comment thread
waleedlatif1 marked this conversation as resolved.

if (isSessionPending || isWorkspacesLoading) {
if (failedToLoad) {
return (
<div className='flex h-screen w-full items-center justify-center'>
<div
className='size-[18px] animate-spin rounded-full'
style={{
background:
'conic-gradient(from 0deg, hsl(var(--muted-foreground)) 0deg 120deg, transparent 120deg 180deg, hsl(var(--muted-foreground)) 180deg 300deg, transparent 300deg 360deg)',
mask: 'radial-gradient(farthest-side, transparent calc(100% - 1.5px), black calc(100% - 1.5px))',
WebkitMask:
'radial-gradient(farthest-side, transparent calc(100% - 1.5px), black calc(100% - 1.5px))',
}}
/>
</div>
<main className='flex h-screen w-full items-center justify-center bg-[var(--surface-1)] p-6'>
<div className='flex max-w-md flex-col items-center gap-3 text-center'>
<div className='flex size-10 items-center justify-center rounded-full bg-[var(--surface-3)]'>
<CircleAlert className='size-[18px] text-[var(--text-icon)]' aria-hidden />
</div>
<div className='space-y-1'>
<h1 className='font-medium text-[var(--text-primary)] text-lg'>
Could not load your workspaces
</h1>
<p className='text-[var(--text-muted)] text-sm'>
Something went wrong while loading your account. Try again, or sign out and log back
in.
</p>
</div>
<div className='flex items-center gap-2'>
<Chip variant='primary' onClick={() => window.location.reload()}>
Try again
</Chip>
<Chip onClick={() => void recoverFromStaleSession()}>Sign out</Chip>
</div>
</div>
</main>
)
}

return null
return (
<div className='flex h-screen w-full items-center justify-center'>
<div
className='size-[18px] animate-spin rounded-full'
style={{
background:
'conic-gradient(from 0deg, hsl(var(--muted-foreground)) 0deg 120deg, transparent 120deg 180deg, hsl(var(--muted-foreground)) 180deg 300deg, transparent 300deg 360deg)',
mask: 'radial-gradient(farthest-side, transparent calc(100% - 1.5px), black calc(100% - 1.5px))',
WebkitMask:
'radial-gradient(farthest-side, transparent calc(100% - 1.5px), black calc(100% - 1.5px))',
}}
/>
</div>
)
}

async function handleWorkflowRedirect(
Expand Down
Loading