Add auth expiry interceptor and session-expired redirect

This commit is contained in:
2026-04-19 20:31:49 +02:00
parent d0991e0d40
commit cc8c71906f
8 changed files with 164 additions and 1 deletions

View File

@@ -10,10 +10,13 @@
import {
createContext,
useCallback,
useEffect,
useMemo,
useState,
} from "react";
import { useNavigate } from "react-router-dom";
import * as authApi from "../api/auth";
import { SESSION_EXPIRED_EVENT } from "../api/client";
// ---------------------------------------------------------------------------
// Types
@@ -64,6 +67,21 @@ export function AuthProvider({
token: sessionStorage.getItem(SESSION_KEY),
expiresAt: sessionStorage.getItem(SESSION_EXPIRES_KEY),
}));
const navigate = useNavigate();
const handleSessionExpired = useCallback((): void => {
sessionStorage.removeItem(SESSION_KEY);
sessionStorage.removeItem(SESSION_EXPIRES_KEY);
setAuth({ token: null, expiresAt: null });
navigate("/login", { replace: true });
}, [navigate]);
useEffect((): (() => void) => {
window.addEventListener(SESSION_EXPIRED_EVENT, handleSessionExpired);
return (): void => {
window.removeEventListener(SESSION_EXPIRED_EVENT, handleSessionExpired);
};
}, [handleSessionExpired]);
const isAuthenticated = useMemo<boolean>(() => {
if (!auth.token || !auth.expiresAt) return false;

View File

@@ -0,0 +1,44 @@
import { describe, expect, it, vi, beforeEach } from "vitest";
import { render, screen, waitFor } from "@testing-library/react";
import { type ReactElement } from "react";
import { MemoryRouter, Route, Routes, useLocation } from "react-router-dom";
import { FluentProvider, webLightTheme } from "@fluentui/react-components";
import { AuthProvider } from "../AuthProvider";
import { SESSION_EXPIRED_EVENT } from "../../api/client";
function CurrentLocation(): ReactElement {
const location = useLocation();
return <div data-testid="location">{location.pathname}</div>;
}
describe("AuthProvider", () => {
beforeEach(() => {
sessionStorage.clear();
vi.restoreAllMocks();
});
it("clears auth state and redirects to /login when session-expired fires", async () => {
sessionStorage.setItem("bangui_token", "token");
sessionStorage.setItem("bangui_expires_at", new Date(Date.now() + 10000).toISOString());
render(
<FluentProvider theme={webLightTheme}>
<MemoryRouter initialEntries={["/private"]}>
<AuthProvider>
<Routes>
<Route path="*" element={<CurrentLocation />} />
</Routes>
</AuthProvider>
</MemoryRouter>
</FluentProvider>,
);
window.dispatchEvent(new Event(SESSION_EXPIRED_EVENT));
await waitFor(() => {
expect(screen.getByTestId("location")).toHaveTextContent("/login");
});
expect(sessionStorage.getItem("bangui_token")).toBeNull();
expect(sessionStorage.getItem("bangui_expires_at")).toBeNull();
});
});