Skip to content
Open
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
32 changes: 18 additions & 14 deletions frontend/src/lib/hooks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import {
useQuery,
useQueryClient,
} from "@tanstack/react-query"
import Cookies from "js-cookie"
import { AlertTriangleIcon, CircleCheck } from "lucide-react"
import { useRouter } from "next/navigation"
import { useCallback, useRef, useState } from "react"
Expand Down Expand Up @@ -353,8 +352,8 @@ import {
workspacesListWorkspaces,
workspacesUpdateWorkspace,
} from "@/client"

import { toast } from "@/components/ui/use-toast"
import { useAuth } from "@/hooks/use-auth"
import { type AgentSessionWithStatus, enrichAgentSession } from "@/lib/agents"
import { client as apiClient, getBaseUrl } from "@/lib/api"
import {
Expand All @@ -365,6 +364,11 @@ import { invalidateCaseActivityQueries } from "@/lib/cases/invalidation"
import type { ModelInfo } from "@/lib/chat"
import { retryHandler, type TracecatApiError } from "@/lib/errors"
import type { WorkflowExecutionReadCompact } from "@/lib/event-history"
import {
clearLastWorkspaceIdForUser,
getLastWorkspaceIdForUser,
setLastWorkspaceIdForUser,
} from "@/lib/last-workspace"
import { useWorkspaceId } from "@/providers/workspace-id"

interface AppInfo {
Expand Down Expand Up @@ -881,6 +885,8 @@ export function useWorkflowManager(
export function useWorkspaceManager() {
const queryClient = useQueryClient()
const router = useRouter()
const { user } = useAuth()
const userId = user?.id

// List workspaces
const {
Expand Down Expand Up @@ -966,19 +972,17 @@ export function useWorkspaceManager() {

// Cookies
const getLastWorkspaceId = useCallback(
() => Cookies.get("__tracecat:workspaces:last-viewed"),
[]
() => getLastWorkspaceIdForUser(userId),
[userId]
)
const setLastWorkspaceId = useCallback(
(id?: string) => setLastWorkspaceIdForUser(userId, id),
[userId]
)
const clearLastWorkspaceId = useCallback(
() => clearLastWorkspaceIdForUser(userId),
[userId]
)
const setLastWorkspaceId = useCallback((id?: string) => {
if (!id) {
Cookies.set("__tracecat:workspaces:last-viewed", "")
return
}
Cookies.set("__tracecat:workspaces:last-viewed", id)
}, [])
const clearLastWorkspaceId = useCallback(() => {
Cookies.remove("__tracecat:workspaces:last-viewed")
}, [])

return {
workspaces,
Expand Down
47 changes: 47 additions & 0 deletions frontend/src/lib/last-workspace.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import Cookies from "js-cookie"

const LAST_WORKSPACE_COOKIE_PREFIX = "__tracecat:workspaces:last-viewed"
const LEGACY_LAST_WORKSPACE_COOKIE = LAST_WORKSPACE_COOKIE_PREFIX

function userLastWorkspaceCookieName(userId: string): string {
return `${LAST_WORKSPACE_COOKIE_PREFIX}:${encodeURIComponent(userId)}`
}

/**
* Reads the client-side last viewed workspace ID for a specific user.
*
* Anonymous callers use the legacy shared cookie, but authenticated users use a
* user-scoped cookie so switching accounts in the same browser does not leak the
* previously selected workspace across users.
*/
export function getLastWorkspaceIdForUser(userId?: string): string | undefined {
if (!userId) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Using !userId to select the legacy cookie conflates anonymous and auth-loading states, so authenticated sessions can still hit the shared cookie before user data resolves.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At frontend/src/lib/last-workspace.ts, line 18:

<comment>Using `!userId` to select the legacy cookie conflates anonymous and auth-loading states, so authenticated sessions can still hit the shared cookie before user data resolves.</comment>

<file context>
@@ -0,0 +1,47 @@
+ * previously selected workspace across users.
+ */
+export function getLastWorkspaceIdForUser(userId?: string): string | undefined {
+  if (!userId) {
+    return Cookies.get(LEGACY_LAST_WORKSPACE_COOKIE)
+  }
</file context>

return Cookies.get(LEGACY_LAST_WORKSPACE_COOKIE)
}
return Cookies.get(userLastWorkspaceCookieName(userId))
}

/** Stores the client-side last viewed workspace ID for a specific user. */
export function setLastWorkspaceIdForUser(
userId: string | undefined,
workspaceId?: string
) {
const cookieName = userId
? userLastWorkspaceCookieName(userId)
: LEGACY_LAST_WORKSPACE_COOKIE

if (!workspaceId) {
Cookies.set(cookieName, "")
return
}
Cookies.set(cookieName, workspaceId)
}

/** Clears the client-side last viewed workspace ID for a specific user. */
export function clearLastWorkspaceIdForUser(userId?: string) {
if (!userId) {
Cookies.remove(LEGACY_LAST_WORKSPACE_COOKIE)
return
}
Cookies.remove(userLastWorkspaceCookieName(userId))
}
44 changes: 44 additions & 0 deletions frontend/tests/last-workspace.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/**
* @jest-environment jsdom
*/

import Cookies from "js-cookie"
import {
clearLastWorkspaceIdForUser,
getLastWorkspaceIdForUser,
setLastWorkspaceIdForUser,
} from "@/lib/last-workspace"

describe("last workspace persistence", () => {
beforeEach(() => {
Cookies.remove("__tracecat:workspaces:last-viewed")
Cookies.remove("__tracecat:workspaces:last-viewed:user-a")
Cookies.remove("__tracecat:workspaces:last-viewed:user-b")
})

it("stores the last workspace separately for each user", () => {
setLastWorkspaceIdForUser("user-a", "workspace-a")
setLastWorkspaceIdForUser("user-b", "workspace-b")

expect(getLastWorkspaceIdForUser("user-a")).toBe("workspace-a")
expect(getLastWorkspaceIdForUser("user-b")).toBe("workspace-b")
})

it("keeps anonymous storage on the legacy shared cookie", () => {
setLastWorkspaceIdForUser(undefined, "workspace-anonymous")
setLastWorkspaceIdForUser("user-a", "workspace-a")

expect(getLastWorkspaceIdForUser()).toBe("workspace-anonymous")
expect(getLastWorkspaceIdForUser("user-a")).toBe("workspace-a")
})

it("clears only the scoped user's last workspace", () => {
setLastWorkspaceIdForUser("user-a", "workspace-a")
setLastWorkspaceIdForUser("user-b", "workspace-b")

clearLastWorkspaceIdForUser("user-a")

expect(getLastWorkspaceIdForUser("user-a")).toBeUndefined()
expect(getLastWorkspaceIdForUser("user-b")).toBe("workspace-b")
})
})
Loading