Release v0.1.2

This commit is contained in:
2026-06-25 19:58:20 +02:00
parent 15794e920e
commit 02564047e9
73 changed files with 4073 additions and 478 deletions

View File

@@ -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();