Release v0.1.2
This commit is contained in:
@@ -6,6 +6,14 @@ const CSRF_COOKIE_NAME = import.meta.env.VITE_CSRF_COOKIE_NAME ?? "msm_csrf";
|
||||
const RECENT_SAFE_REQUEST_TTL_MS = 750;
|
||||
const MAX_RECENT_SAFE_REQUESTS = 100;
|
||||
|
||||
export const AUTH_REQUIRED_EVENT = "govoplan:auth-required";
|
||||
|
||||
export type AuthRequiredEventDetail = {
|
||||
path: string;
|
||||
status: number;
|
||||
message: string;
|
||||
};
|
||||
|
||||
type RecentSafeRequest = {
|
||||
value: unknown;
|
||||
expiresAt: number;
|
||||
@@ -169,6 +177,25 @@ export function authHeaders(settings: ApiSettings): Headers {
|
||||
return headers;
|
||||
}
|
||||
|
||||
function shouldNotifyAuthRequired(path: string): boolean {
|
||||
const normalized = path.split("?", 1)[0].replace(/\/+$/, "");
|
||||
return !normalized.endsWith("/auth/login");
|
||||
}
|
||||
|
||||
function notifyAuthRequired(path: string): void {
|
||||
if (typeof window === "undefined" || !shouldNotifyAuthRequired(path)) return;
|
||||
const detail: AuthRequiredEventDetail = {
|
||||
path,
|
||||
status: 401,
|
||||
message: "Your session has expired. Sign in again to continue."
|
||||
};
|
||||
window.dispatchEvent(new CustomEvent<AuthRequiredEventDetail>(AUTH_REQUIRED_EVENT, { detail }));
|
||||
}
|
||||
|
||||
function authExpiredError(statusText: string): ApiError {
|
||||
return new ApiError(401, statusText || "Unauthorized", "Your session has expired. Sign in again to continue.");
|
||||
}
|
||||
|
||||
export async function apiFetch<T>(settings: ApiSettings, path: string, init?: RequestInit): Promise<T> {
|
||||
const headers = new Headers(init?.headers || {});
|
||||
const method = (init?.method || "GET").toUpperCase();
|
||||
@@ -200,6 +227,10 @@ export async function apiFetch<T>(settings: ApiSettings, path: string, init?: Re
|
||||
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
if (response.status === 401 && shouldNotifyAuthRequired(path)) {
|
||||
notifyAuthRequired(path);
|
||||
throw authExpiredError(response.statusText);
|
||||
}
|
||||
throw new ApiError(response.status, response.statusText, text);
|
||||
}
|
||||
|
||||
@@ -252,6 +283,10 @@ export async function apiDownload(settings: ApiSettings, path: string, filename:
|
||||
const response = await fetch(apiUrl(settings, path), { headers: authHeaders(settings), credentials: "include" });
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
if (response.status === 401 && shouldNotifyAuthRequired(path)) {
|
||||
notifyAuthRequired(path);
|
||||
throw authExpiredError(response.statusText);
|
||||
}
|
||||
throw new ApiError(response.status, response.statusText, text);
|
||||
}
|
||||
const blob = await response.blob();
|
||||
|
||||
Reference in New Issue
Block a user