dev1/src/auth/apiFetch.js
Josh 761f511601
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
cookie implementation
2025-08-13 19:58:24 +00:00

83 lines
2.8 KiB
JavaScript

// src/auth/apiFetch.js
//
// A tiny wrapper around window.fetch.
// - NEVER sets Authorization (shim does that).
// - Smart JSON handling (auto stringify, auto parse in helpers).
// - Optional timeout via AbortController.
// - Optional shim bypass: { bypassAuth: true } adds X-Bypass-Auth: 1.
// - Leaves error semantics up to caller or helper.
//
// Use:
// const res = await apiFetch('/api/user-profile');
// const json = await res.json();
//
// Or helpers:
// const data = await apiGetJSON('/api/user-profile');
// const out = await apiPostJSON('/api/premium/thing', { foo: 'bar' });
const DEFAULT_TIMEOUT_MS = 25000; // 25s
export async function apiFetch(url, options = {}) {
const headers = new Headers(options.headers || {});
const init = { ...options, credentials: options.credentials || 'include' };
// If body is a plain object and no Content-Type set, send JSON
if (!headers.has('Content-Type') &&
options.body &&
typeof options.body === 'object' &&
!(options.body instanceof FormData)) {
headers.set('Content-Type', 'application/json');
init.body = JSON.stringify(options.body);
}
if (headers.size) init.headers = headers;
// This must always return a Response, never null
return fetch(url, init);
}
export async function apiGetJSON(url) {
const res = await apiFetch(url);
if (!res.ok) throw new Error(`GET ${url} failed: ${res.status}`);
return res.json();
}
export async function apiPostJSON(url, payload) {
const res = await apiFetch(url, { method: 'POST', body: payload });
if (!res.ok) {
const errBody = await res.json().catch(() => ({}));
const msg = errBody?.error || `POST ${url} failed: ${res.status}`;
throw new Error(msg);
}
return res.json().catch(() => ({}));
}
/** PUT JSON → parse JSON, throw on !ok. */
export async function apiPutJSON(url, payload, opts = {}) {
const res = await apiFetch(url, { ...opts, method: 'PUT', body: payload });
const text = await res.text();
const data = safeJSON(text);
if (!res.ok) throw new Error(errorFromServer(data, text, res.status));
return data;
}
/** DELETE → parse JSON (if any), throw on !ok. */
export async function apiDeleteJSON(url, opts = {}) {
const res = await apiFetch(url, { ...opts, method: 'DELETE' });
const text = await res.text();
const data = safeJSON(text);
if (!res.ok) throw new Error(errorFromServer(data, text, res.status));
return data || { ok: true };
}
/* -------------------- utils -------------------- */
function safeJSON(text) {
if (!text) return null;
try { return JSON.parse(text); } catch { return null; }
}
function errorFromServer(json, text, status) {
if (json && typeof json === 'object' && json.error) return json.error;
if (text) return `Request failed (${status}): ${text.slice(0, 240)}`;
return `Request failed (${status})`;
}