79 lines
4.1 KiB
JavaScript
79 lines
4.1 KiB
JavaScript
import { logDebug, logError, logWarn } from "@rustpad/logger";
|
|
|
|
function formatBytes(bytes) {
|
|
if (bytes >= 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(bytes % (1024 * 1024) ? 1 : 0)} MB`;
|
|
if (bytes >= 1024) return `${Math.ceil(bytes / 1024)} KB`;
|
|
return `${bytes} B`;
|
|
}
|
|
|
|
function clearExpiredSession() {
|
|
localStorage.removeItem("rustpad:auth-token");
|
|
sessionStorage.removeItem("rustpad:auth-token");
|
|
localStorage.removeItem("rustpad:nickname");
|
|
sessionStorage.removeItem("rustpad:nickname");
|
|
document.cookie = "rustpad_nickname=; Path=/; SameSite=Lax; Max-Age=0";
|
|
window.dispatchEvent(new CustomEvent("rustpad:session-expired"));
|
|
}
|
|
|
|
async function clearSessionIfInvalid() {
|
|
const token = localStorage.getItem("rustpad:auth-token") || sessionStorage.getItem("rustpad:auth-token");
|
|
if (!token) return;
|
|
try {
|
|
const response = await fetch("/api/auth/me", {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
signal: AbortSignal.timeout(5000),
|
|
});
|
|
if (response.status === 401) clearExpiredSession();
|
|
} catch {
|
|
// A network failure does not prove that the session is invalid.
|
|
}
|
|
}
|
|
|
|
function validateUploadSize(body) {
|
|
if (!(body instanceof FormData)) return;
|
|
const maxBytes = Number(window.__RUSTPAD_CONFIG__?.uploadMaxSizeBytes || 0);
|
|
if (!Number.isFinite(maxBytes) || maxBytes <= 0) return;
|
|
for (const value of body.values()) {
|
|
if (value instanceof File && value.size > maxBytes) {
|
|
const error = new Error(`The selected file is ${formatBytes(value.size)}. The upload limit is ${formatBytes(maxBytes)}.`);
|
|
error.status = 413;
|
|
throw error;
|
|
}
|
|
}
|
|
}
|
|
|
|
export async function api(path, options = {}) {
|
|
validateUploadSize(options.body);
|
|
const controller = new AbortController();
|
|
const timeout = setTimeout(() => controller.abort(), 12000);
|
|
try {
|
|
const headers = new Headers(options.headers || {});
|
|
const authToken = localStorage.getItem("rustpad:auth-token") || sessionStorage.getItem("rustpad:auth-token");
|
|
if (authToken && !headers.has("authorization")) headers.set("authorization", `Bearer ${authToken}`);
|
|
if (!(options.body instanceof FormData) && !headers.has("content-type")) headers.set("content-type", "application/json");
|
|
const started = performance.now();
|
|
logDebug("api.request", { method: options.method || "GET", path });
|
|
const response = await fetch(path, { ...options, headers, signal: controller.signal });
|
|
const durationMs = Math.round(performance.now() - started);
|
|
logDebug("api.response", { method: options.method || "GET", path, status: response.status, durationMs });
|
|
const contentType = response.headers.get("content-type") || "";
|
|
const data = contentType.includes("application/json") ? await response.json().catch(() => ({})) : {};
|
|
if (!response.ok) {
|
|
if (response.status === 401) await clearSessionIfInvalid();
|
|
const defaults = { 400: "Invalid request.", 401: "Authentication required.", 403: "Access denied.", 404: "The requested resource was not found.", 405: "This operation is not allowed.", 409: "The requested change conflicts with existing data.", 413: "The selected file exceeds the allowed upload limit.", 429: "Too many requests. Try again later.", 500: "Server error. Try again later.", 503: "Service temporarily unavailable." };
|
|
const requestError = new Error(data.error || defaults[response.status] || `Request failed (${response.status}).`);
|
|
requestError.status = response.status;
|
|
logWarn("api.failed", { method: options.method || "GET", path, status: response.status, message: requestError.message });
|
|
throw requestError;
|
|
}
|
|
return data;
|
|
} catch (error) {
|
|
if (error.name === "AbortError") { logWarn("api.timeout", { method: options.method || "GET", path }); throw new Error("Timed out"); }
|
|
logError("api.network_error", error, { method: options.method || "GET", path });
|
|
if (options.body instanceof FormData && error instanceof TypeError) {
|
|
throw new Error("Upload failed before the server returned a response. The file may exceed the server or proxy upload limit.");
|
|
}
|
|
throw error;
|
|
} finally { clearTimeout(timeout); }
|
|
}
|