Refactor authentication logic and API client
- Update AuthProvider with improved error handling and token management - Enhance API client with better request/response handling - Add comprehensive test coverage for auth flows - Update documentation with current tasks Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -1,13 +1,15 @@
|
||||
import { describe, expect, it, beforeEach, vi } from "vitest";
|
||||
import { ApiError, get, isAuthError, SESSION_EXPIRED_EVENT } from "../client";
|
||||
import { ApiError, get, isAuthError, setUnauthorizedHandler } from "../client";
|
||||
|
||||
describe("api/client", () => {
|
||||
beforeEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
setUnauthorizedHandler(null);
|
||||
});
|
||||
|
||||
it("dispatches session-expired for 401 responses", async () => {
|
||||
const dispatchSpy = vi.spyOn(window, "dispatchEvent");
|
||||
it("invokes unauthorized handler for 401 responses", async () => {
|
||||
const handler = vi.fn();
|
||||
setUnauthorizedHandler(handler);
|
||||
|
||||
global.fetch = vi.fn().mockResolvedValue({
|
||||
ok: false,
|
||||
@@ -16,15 +18,12 @@ describe("api/client", () => {
|
||||
});
|
||||
|
||||
await expect(get("/test")).rejects.toBeInstanceOf(ApiError);
|
||||
expect(dispatchSpy).toHaveBeenCalledTimes(1);
|
||||
const call = dispatchSpy.mock.calls[0];
|
||||
expect(call).toBeDefined();
|
||||
const event = call?.[0] as Event;
|
||||
expect(event.type).toBe(SESSION_EXPIRED_EVENT);
|
||||
expect(handler).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("dispatches session-expired for 403 responses", async () => {
|
||||
const dispatchSpy = vi.spyOn(window, "dispatchEvent");
|
||||
it("invokes unauthorized handler for 403 responses", async () => {
|
||||
const handler = vi.fn();
|
||||
setUnauthorizedHandler(handler);
|
||||
|
||||
global.fetch = vi.fn().mockResolvedValue({
|
||||
ok: false,
|
||||
@@ -33,11 +32,21 @@ describe("api/client", () => {
|
||||
});
|
||||
|
||||
await expect(get("/test")).rejects.toBeInstanceOf(ApiError);
|
||||
expect(dispatchSpy).toHaveBeenCalledTimes(1);
|
||||
const call = dispatchSpy.mock.calls[0];
|
||||
expect(call).toBeDefined();
|
||||
const event = call?.[0] as Event;
|
||||
expect(event.type).toBe(SESSION_EXPIRED_EVENT);
|
||||
expect(handler).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("does not invoke handler when handler is null", async () => {
|
||||
const handler = vi.fn();
|
||||
setUnauthorizedHandler(null);
|
||||
|
||||
global.fetch = vi.fn().mockResolvedValue({
|
||||
ok: false,
|
||||
status: 401,
|
||||
text: vi.fn().mockResolvedValue("Unauthorized"),
|
||||
});
|
||||
|
||||
await expect(get("/test")).rejects.toBeInstanceOf(ApiError);
|
||||
expect(handler).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not treat non-auth errors as auth errors", () => {
|
||||
|
||||
@@ -39,9 +39,6 @@ export class ApiError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
/** Custom event name emitted when the backend signals an expired or invalid session. */
|
||||
export const SESSION_EXPIRED_EVENT = "bangui:session-expired";
|
||||
|
||||
/**
|
||||
* Returns `true` when the error represents an expired or unauthorized session.
|
||||
*
|
||||
@@ -51,6 +48,26 @@ export function isAuthError(err: unknown): err is ApiError {
|
||||
return err instanceof ApiError && (err.status === 401 || err.status === 403);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Unauthorized handler callback
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Module-level callback invoked when the API client receives a 401/403 response.
|
||||
* Set via `setUnauthorizedHandler()` and called directly (no DOM events).
|
||||
*/
|
||||
let unauthorizedHandler: (() => void) | null = null;
|
||||
|
||||
/**
|
||||
* Register a callback to be invoked when the API receives a 401/403 response.
|
||||
* Typically called by the AuthProvider on mount.
|
||||
*
|
||||
* @param handler - Callback to invoke on unauthorized response. Pass `null` to clear.
|
||||
*/
|
||||
export function setUnauthorizedHandler(handler: (() => void) | null): void {
|
||||
unauthorizedHandler = handler;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Internal helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -77,9 +94,7 @@ async function request<T>(url: string, options: RequestInit = {}): Promise<T> {
|
||||
const body: string = await response.text();
|
||||
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
if (typeof window !== "undefined") {
|
||||
window.dispatchEvent(new Event(SESSION_EXPIRED_EVENT));
|
||||
}
|
||||
unauthorizedHandler?.();
|
||||
}
|
||||
|
||||
throw new ApiError(response.status, body);
|
||||
|
||||
Reference in New Issue
Block a user