fix(auth): redirect expired routes into login flow

The global route guard previously rendered redirect copy without navigating and also blocked the login route itself. Allow the login page through, redirect protected unauthenticated routes, and avoid a /login callback loop.
This commit is contained in:
2026-11-04 15:20:46 +08:00
parent a191d81c29
commit d98fa359ae
4 changed files with 74 additions and 6 deletions
+36 -1
View File
@@ -1,4 +1,5 @@
import { render, screen, waitFor } from "@testing-library/react";
import { signIn } from "next-auth/react";
import type { ReactNode } from "react";
import { useAccessStore } from "@/store/accessStore";
@@ -25,6 +26,8 @@ let mockSessionState: {
};
let mockPathname = "/network-simulation";
let mockProjectProviderBlocks = false;
let capturedLogin: (() => Promise<unknown>) | null = null;
const replaceMock = jest.fn();
jest.mock("next-auth/react", () => ({
SessionProvider: ({ children }: { children: ReactNode }) => children,
@@ -34,10 +37,20 @@ jest.mock("next-auth/react", () => ({
jest.mock("next/navigation", () => ({
usePathname: () => mockPathname,
useRouter: () => ({ replace: replaceMock }),
}));
jest.mock("@refinedev/core", () => ({
Refine: ({ children }: { children: ReactNode }) => children,
Refine: ({
children,
authProvider,
}: {
children: ReactNode;
authProvider: { login: (params: Record<string, never>) => Promise<unknown> };
}) => {
capturedLogin = () => authProvider.login({});
return children;
},
}));
jest.mock("@refinedev/kbar", () => ({
@@ -78,6 +91,9 @@ describe("RefineContext access authentication", () => {
clearIntentionalLogout();
mockPathname = "/network-simulation";
mockProjectProviderBlocks = false;
capturedLogin = null;
replaceMock.mockReset();
jest.mocked(signIn).mockClear();
mockSessionState = {
data: {
accessToken: "expired-access-token",
@@ -217,6 +233,25 @@ describe("RefineContext access authentication", () => {
});
});
it("starts login with a safe post-authentication callback from the login route", async () => {
mockPathname = "/login";
mockSessionState = { data: null, status: "unauthenticated" };
render(
<App>
<div>登录页面</div>
</App>,
);
expect(capturedLogin).not.toBeNull();
await capturedLogin!();
expect(signIn).toHaveBeenCalledWith("keycloak", {
callbackUrl: "/",
redirect: true,
});
});
it("does not treat returning to a protected route after logout as session expiry", async () => {
markIntentionalLogout();
mockSessionState = { data: null, status: "unauthenticated" };
+1 -1
View File
@@ -214,7 +214,7 @@ export const App = (props: React.PropsWithChildren<AppProps>) => {
const authProvider: AuthProvider = {
login: async () => {
signIn("keycloak", {
callbackUrl: to ? to.toString() : "/",
callbackUrl: to && to !== "/login" ? to.toString() : "/",
redirect: true,
});
return { success: true };