diff --git a/apps/web/src/components/app-shell/user-menu.tsx b/apps/web/src/components/app-shell/user-menu.tsx index 97cda2ea..8679444e 100644 --- a/apps/web/src/components/app-shell/user-menu.tsx +++ b/apps/web/src/components/app-shell/user-menu.tsx @@ -18,11 +18,7 @@ import { SidebarMenuButton, SidebarMenuItem, } from "@/components/ui/sidebar"; -import { - currentUserOptionalQueryOptions, - currentUserQueryOptions, - logout, -} from "@/lib/auth-api"; +import { currentUserOptionalQueryOptions, logout } from "@/lib/auth-api"; function getInitials(name: string): string { return name @@ -66,13 +62,11 @@ export function UserMenu() { setIsLoggingOut(true); try { await logout(); - await queryClient.cancelQueries({ - queryKey: currentUserQueryOptions.queryKey, - }); - queryClient.removeQueries({ - queryKey: currentUserQueryOptions.queryKey, - exact: true, - }); + // Clear ALL React Query caches so no stale data from the just-logged-out + // user (e.g. "auth"/"me-optional" used by the sidebar, permissions, + // projects, etc.) leaks into the next session. The login route will + // re-fetch everything from scratch. + queryClient.clear(); await navigate({ to: "/", replace: true }); } finally { setIsLoggingOut(false); diff --git a/apps/web/src/hooks/use-login-form.test.tsx b/apps/web/src/hooks/use-login-form.test.tsx index df7b800c..8129ee22 100644 --- a/apps/web/src/hooks/use-login-form.test.tsx +++ b/apps/web/src/hooks/use-login-form.test.tsx @@ -1,7 +1,7 @@ import { act, renderHook } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { ApiErrorCode } from "@/lib/api-error"; -import { currentUserQueryOptions, login } from "@/lib/auth-api"; +import { login } from "@/lib/auth-api"; import { useLoginForm } from "./use-login-form"; type SubmitPayload = { @@ -91,8 +91,10 @@ describe("useLoginForm", () => { }); expect(login).toHaveBeenCalledWith("alice", "password123", true); + // The login flow invalidates the entire "auth" namespace so both the + // "auth"/"me" and "auth"/"me-optional" caches are refreshed. expect(mocks.invalidateQueriesMock).toHaveBeenCalledWith({ - queryKey: currentUserQueryOptions.queryKey, + queryKey: ["auth"], }); expect(mocks.navigateMock).toHaveBeenCalledWith({ to: "/home" }); }); diff --git a/apps/web/src/hooks/use-login-form.ts b/apps/web/src/hooks/use-login-form.ts index 19d8755c..862f5e5a 100644 --- a/apps/web/src/hooks/use-login-form.ts +++ b/apps/web/src/hooks/use-login-form.ts @@ -3,7 +3,7 @@ import { useQueryClient } from "@tanstack/react-query"; import { useNavigate } from "@tanstack/react-router"; import { useState } from "react"; import { ApiErrorCode, getApiErrorCode } from "@/lib/api-error"; -import { currentUserQueryOptions, login } from "@/lib/auth-api"; +import { login } from "@/lib/auth-api"; const loginErrorMessages: Partial> = { [ApiErrorCode.InvalidCredentials]: "Invalid username or password.", @@ -25,9 +25,10 @@ export function useLoginForm() { setServerError(null); try { await login(value.username, value.password, value.rememberMe); - await queryClient.invalidateQueries({ - queryKey: currentUserQueryOptions.queryKey, - }); + // Invalidate the entire "auth" query namespace so both the required + // ("auth"/"me") and the optional ("auth"/"me-optional") caches are + // refreshed. Without this the sidebar keeps the previous user's data. + await queryClient.invalidateQueries({ queryKey: ["auth"] }); await navigate({ to: "/home" }); } catch (err: unknown) { const code = getApiErrorCode(err);