mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-09 03:18:44 +00:00
fix(ui): logout no longer reloads the current page before redirecting to login
When PROXY_LOGOUT_URL is unset (the default), handleLogout navigated to an empty string, which reloads whatever page the user is on with the token already cleared. The per-route auth guard then waited for the async UI config fetch before redirecting, so users saw the old page (with 401ing data calls) for a few seconds after clicking logout. Logout now goes straight to the login page via location.replace and clears the stored return URL so a later login cannot bounce back to the pre-logout page. useAuthorized also redirects immediately when the token is missing or invalid instead of waiting on the UI config fetch, which covers expired sessions hard-loading any dashboard route.
This commit is contained in:
parent
8177230a29
commit
1b6cee9b12
4 changed files with 80 additions and 11 deletions
|
|
@ -243,6 +243,39 @@ describe("useAuthorized", () => {
|
|||
expect(result.current.token).toBeNull();
|
||||
});
|
||||
|
||||
it("should redirect immediately when token is missing, without waiting for the UI config fetch", async () => {
|
||||
getUiConfigMock.mockReturnValue(new Promise(() => {}));
|
||||
|
||||
decodeTokenMock.mockReturnValue(null);
|
||||
checkTokenValidityMock.mockReturnValue(false);
|
||||
|
||||
const { result } = renderHook(() => useAuthorized(), { wrapper });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(replaceMock).toHaveBeenCalledWith("http://proxy.example/ui/login/");
|
||||
});
|
||||
|
||||
expect(clearTokenCookiesMock).not.toHaveBeenCalled();
|
||||
expect(result.current.token).toBeNull();
|
||||
});
|
||||
|
||||
it("should clear cookies and redirect immediately when token is expired, without waiting for the UI config fetch", async () => {
|
||||
getUiConfigMock.mockReturnValue(new Promise(() => {}));
|
||||
|
||||
decodeTokenMock.mockReturnValue({ key: "api-key-123", user_id: "user-1" });
|
||||
checkTokenValidityMock.mockReturnValue(false);
|
||||
|
||||
document.cookie = "token=expired-token; path=/;";
|
||||
|
||||
renderHook(() => useAuthorized(), { wrapper });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(clearTokenCookiesMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
expect(replaceMock).toHaveBeenCalledWith("http://proxy.example/ui/login/");
|
||||
});
|
||||
|
||||
it("should clear cookies and redirect when token is expired", async () => {
|
||||
getUiConfigMock.mockResolvedValue({
|
||||
server_root_path: "/",
|
||||
|
|
|
|||
|
|
@ -28,15 +28,14 @@ const useAuthorized = () => {
|
|||
|
||||
// Single useEffect for all redirect logic
|
||||
useEffect(() => {
|
||||
if (isLoading) return;
|
||||
if (isTokenValid && isLoading) return;
|
||||
if (isAuthorized) return;
|
||||
|
||||
if (!isAuthorized) {
|
||||
if (token) {
|
||||
clearTokenCookies();
|
||||
}
|
||||
redirectToLogin();
|
||||
if (token) {
|
||||
clearTokenCookies();
|
||||
}
|
||||
}, [isLoading, isAuthorized, token, redirectToLogin]);
|
||||
redirectToLogin();
|
||||
}, [isLoading, isTokenValid, isAuthorized, token, redirectToLogin]);
|
||||
|
||||
return {
|
||||
isLoading,
|
||||
|
|
|
|||
|
|
@ -132,9 +132,17 @@ vi.mock("@/utils/cookieUtils", () => ({
|
|||
clearTokenCookies: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock window.location.href for logout testing
|
||||
vi.mock("@/utils/returnUrlUtils", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("@/utils/returnUrlUtils")>();
|
||||
return {
|
||||
...actual,
|
||||
clearStoredReturnUrl: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
// Mock window.location for logout testing
|
||||
Object.defineProperty(window, "location", {
|
||||
value: { href: "" },
|
||||
value: { href: "", replace: vi.fn() },
|
||||
writable: true,
|
||||
});
|
||||
|
||||
|
|
@ -285,6 +293,7 @@ describe("Navbar", () => {
|
|||
|
||||
it("should handle logout functionality", async () => {
|
||||
const user = userEvent.setup();
|
||||
vi.mocked(window.location.replace).mockClear();
|
||||
|
||||
renderWithProviders(<Navbar {...defaultProps} />);
|
||||
|
||||
|
|
@ -298,9 +307,36 @@ describe("Navbar", () => {
|
|||
await user.click(screen.getByText("Logout"));
|
||||
|
||||
const cookieUtils = vi.mocked(await import("@/utils/cookieUtils"));
|
||||
const returnUrlUtils = vi.mocked(await import("@/utils/returnUrlUtils"));
|
||||
expect(cookieUtils.clearTokenCookies).toHaveBeenCalled();
|
||||
expect(returnUrlUtils.clearStoredReturnUrl).toHaveBeenCalled();
|
||||
await waitFor(() => {
|
||||
expect(window.location.href).toBe("https://example.com/logout");
|
||||
expect(window.location.replace).toHaveBeenCalledWith("https://example.com/logout");
|
||||
});
|
||||
});
|
||||
|
||||
it("should redirect to the login page on logout when PROXY_LOGOUT_URL is not configured", async () => {
|
||||
const user = userEvent.setup();
|
||||
vi.mocked(window.location.replace).mockClear();
|
||||
|
||||
const proxyUtils = vi.mocked(await import("@/utils/proxyUtils"));
|
||||
proxyUtils.fetchProxySettings.mockResolvedValueOnce({
|
||||
PROXY_BASE_URL: "",
|
||||
PROXY_LOGOUT_URL: "",
|
||||
});
|
||||
|
||||
renderWithProviders(<Navbar {...defaultProps} accessToken="test-token-no-logout-url" />);
|
||||
|
||||
await user.click(screen.getByRole("button", { name: /open account menu/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("test-user")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
await user.click(screen.getByText("Logout"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(window.location.replace).toHaveBeenCalledWith("http://localhost:4000/ui/login/");
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -46,9 +46,10 @@ const Navbar: React.FC<NavbarProps> = ({
|
|||
|
||||
const handleLogout = () => {
|
||||
clearTokenCookies();
|
||||
clearStoredReturnUrl();
|
||||
localStorage.removeItem("litellm_selected_worker_id");
|
||||
localStorage.removeItem("litellm_worker_url");
|
||||
window.location.href = proxySettings.PROXY_LOGOUT_URL || "";
|
||||
window.location.replace(proxySettings.PROXY_LOGOUT_URL || getLoginUrl(baseUrl));
|
||||
};
|
||||
|
||||
const handleWorkerSwitch = (workerId: string) => {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue