diff --git a/frontend/src/lib/hooks.tsx b/frontend/src/lib/hooks.tsx index a38f175cee..9375b9b085 100644 --- a/frontend/src/lib/hooks.tsx +++ b/frontend/src/lib/hooks.tsx @@ -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" @@ -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 { @@ -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 { @@ -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 { @@ -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, diff --git a/frontend/src/lib/last-workspace.ts b/frontend/src/lib/last-workspace.ts new file mode 100644 index 0000000000..c884408787 --- /dev/null +++ b/frontend/src/lib/last-workspace.ts @@ -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) { + 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)) +} diff --git a/frontend/tests/last-workspace.test.ts b/frontend/tests/last-workspace.test.ts new file mode 100644 index 0000000000..e94d176a8f --- /dev/null +++ b/frontend/tests/last-workspace.test.ts @@ -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") + }) +})