fix(ui): refresh auth state after login soft navigation

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
mrinal 2026-11-10 00:58:00 +00:00
parent 6c9f9f50f3
commit 5b6ebe8918
3 changed files with 60 additions and 12 deletions

View file

@ -1,7 +1,8 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { fireEvent, renderWithProviders as render, screen, waitFor, within } from "../../../tests/test-utils";
import { act, fireEvent, renderWithProviders as render, screen, waitFor, within } from "../../../tests/test-utils";
import { usePathname } from "next/navigation";
import { AuthProvider } from "@/contexts/AuthContext";
import { AuthProvider, useAuth } from "@/contexts/AuthContext";
import { clearTokenCookies, storeLoginToken } from "@/utils/cookieUtils";
import { recordUiEvent } from "@/lib/telemetry/uiEvents";
import Layout from "./layout";
@ -241,10 +242,49 @@ describe("(dashboard) Layout", () => {
};
afterEach(() => {
clearTokenCookies();
document.cookie = "token=; Max-Age=0; Path=/";
document.cookie = "litellm_return_url=; Max-Age=0; Path=/";
});
function SessionIdentity() {
const { authLoading, userEmail, userRole } = useAuth();
if (authLoading) return <div>Loading session</div>;
return <div>{userEmail ? `${userEmail} (${userRole})` : "Signed out"}</div>;
}
it("updates the mounted session after login and when a different user signs in", async () => {
clearTokenCookies();
render(
<AuthProvider>
<SessionIdentity />
</AuthProvider>,
);
pendingUiConfig.resolve();
expect(await screen.findByText("Signed out")).toBeInTheDocument();
act(() => storeLoginToken(sessionCookie({ user_email: "admin@example.test", user_role: "proxy_admin" })));
expect(await screen.findByText("admin@example.test (Admin)")).toBeInTheDocument();
act(() => storeLoginToken(sessionCookie({ user_email: "user@example.test", user_role: "internal_user" })));
expect(await screen.findByText("user@example.test (Internal User)")).toBeInTheDocument();
});
it.each(["", " ", "invalid-token"])("does not authenticate a stored invalid token %j", async (token) => {
clearTokenCookies();
render(
<AuthProvider>
<SessionIdentity />
</AuthProvider>,
);
pendingUiConfig.resolve();
expect(await screen.findByText("Signed out")).toBeInTheDocument();
act(() => storeLoginToken(token));
expect(screen.getByText("Signed out")).toBeInTheDocument();
});
it("routes a session flagged password_reset_required to the change-password page", async () => {
const flaggedClaims = {
user_id: "flagged-user",

View file

@ -2,7 +2,7 @@
import React, { createContext, useContext, useEffect, useState } from "react";
import { jwtDecode } from "jwt-decode";
import { clearTokenCookies, getCookie } from "@/utils/cookieUtils";
import { clearTokenCookies, getCookie, LOGIN_TOKEN_CHANGED_EVENT } from "@/utils/cookieUtils";
import { isJwtExpired } from "@/utils/jwtUtils";
import { effectiveSessionRole } from "@/utils/roles";
import { getUiConfig, setGlobalLitellmHeaderName } from "@/components/networking";
@ -56,6 +56,17 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
useEffect(() => {
let cancelled = false;
const refreshToken = () => {
const raw = getCookie("token");
const valid = raw && !isJwtExpired(raw) ? raw : null;
if (raw && !valid) {
deleteCookie("token", "/");
}
setToken(valid);
};
window.addEventListener(LOGIN_TOKEN_CHANGED_EVENT, refreshToken);
(async () => {
try {
await getUiConfig();
@ -65,20 +76,13 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
if (cancelled) return;
const raw = getCookie("token");
const valid = raw && !isJwtExpired(raw) ? raw : null;
// Clear expired/invalid token so downstream code doesn't keep trying to use it.
if (raw && !valid) {
deleteCookie("token", "/");
}
setToken(valid);
refreshToken();
setAuthLoading(false);
})();
return () => {
cancelled = true;
window.removeEventListener(LOGIN_TOKEN_CHANGED_EVENT, refreshToken);
};
}, []);

View file

@ -4,6 +4,8 @@
import { clearAllMcpTokens } from "./mcpTokenStore";
export const LOGIN_TOKEN_CHANGED_EVENT = "litellm-login-token-changed";
/**
* Returns the cookie path for the UI.
* Derives the path from window.location.pathname so it works when
@ -103,6 +105,8 @@ export function storeLoginToken(token: string) {
} catch {
// sessionStorage may be unavailable (e.g. private browsing quota exceeded)
}
window.dispatchEvent(new Event(LOGIN_TOKEN_CHANGED_EVENT));
}
/**