asset_browser/public/api/utils.js
setonc a558804026 Initial commit — asset browser web app
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-27 09:06:25 -04:00

59 lines
1.9 KiB
JavaScript
Executable file

// Request wrapper — all calls go through the Express proxy at /syncro-api/
// The Authorization header is injected server-side; never sent from the browser.
// Session cookie is attached automatically by the browser (same-origin, httpOnly).
export async function apiRequest(method, path, body = null, params = {}, { base = '/syncro-api' } = {}) {
const url = new URL(base + path, window.location.origin);
for (const [k, v] of Object.entries(params)) {
if (v !== null && v !== undefined && v !== '') {
url.searchParams.set(k, v);
}
}
const options = {
method,
credentials: 'same-origin',
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
};
if (body && (method === 'PUT' || method === 'POST')) {
options.body = JSON.stringify(body);
}
let response;
try {
response = await fetch(url.toString(), options);
} catch (err) {
throw new Error('Network error — check connection. ' + err.message);
}
// Session expired — redirect to login only if the 401 came from our own auth
// wall (code: SESSION_EXPIRED), not from an upstream Syncro API auth error.
if (response.status === 401) {
let body = {};
try { body = await response.clone().json(); } catch { /* ignore */ }
if (body.code === 'SESSION_EXPIRED') {
window.location.href = '/login.html?expired=1';
throw new Error('Session expired.');
}
throw new Error('Upstream authorization error (HTTP 401).');
}
// Syncro returns 204 No Content on successful PUT
if (response.status === 204) return {};
let data;
try {
data = await response.json();
} catch {
throw new Error(`Non-JSON response (HTTP ${response.status})`);
}
if (!response.ok) {
const msg = data?.error || data?.message || data?.errors?.join?.(', ') || `HTTP ${response.status}`;
throw new Error(msg);
}
return data;
}