Files
Void-Homelab/public/api.js
2026-06-09 08:58:18 +10:00

78 lines
2.8 KiB
JavaScript

// Thin fetch wrapper. Owner token in localStorage.void_token. On 401 we
// prompt for a token via a modal in modal-root. The wrapper is intentionally
// minimal — no retries, no auto-refresh; the owner-only auth model doesn't
// need them.
import { el, mount, clear } from './dom.js';
const TOKEN_KEY = 'void_token';
function token() { return localStorage.getItem(TOKEN_KEY) || ''; }
async function call(method, path, body) {
const headers = { 'Authorization': 'Bearer ' + token() };
// FormData bodies: let the browser set the multipart/form-data boundary
// automatically — do NOT set Content-Type or JSON.stringify.
const isFormData = body instanceof FormData;
if (body !== undefined && !isFormData) headers['Content-Type'] = 'application/json';
const res = await fetch(path, {
method,
headers,
body: body === undefined ? undefined : (isFormData ? body : JSON.stringify(body))
});
if (res.status === 401) { await promptForToken(); return call(method, path, body); }
if (res.status === 204) return null;
const ct = res.headers.get('content-type') || '';
const data = ct.includes('application/json') ? await res.json() : await res.text();
if (!res.ok) {
const err = new Error(data?.error?.message || res.statusText);
err.status = res.status;
err.body = data;
throw err;
}
return data;
}
function promptForToken() {
return new Promise((resolve) => {
const root = document.getElementById('modal-root');
const input = el('input', { type: 'password', style: { width: '100%' }, autofocus: true });
const save = () => {
const v = input.value.trim();
if (!v) return;
localStorage.setItem(TOKEN_KEY, v);
clear(root);
resolve();
};
input.addEventListener('keydown', (e) => { if (e.key === 'Enter') save(); });
mount(root,
el('div', { class: 'modal-backdrop' },
el('div', { class: 'modal' },
el('h2', {}, 'Owner Token'),
el('p', { class: 'muted' },
'Paste the OWNER_TOKEN configured for the server. It will be stored in localStorage on this device only.'
),
input,
el('div', { class: 'actions' },
el('button', { class: 'primary', onclick: save }, 'Save')
)
)
)
);
input.focus();
});
}
export const api = {
get: (p) => call('GET', p),
post: (p, body) => call('POST', p, body ?? {}),
put: (p, body) => call('PUT', p, body ?? {}),
patch: (p, body) => call('PATCH', p, body ?? {}),
del: (p) => call('DELETE', p),
// POST a FormData body (multipart/form-data). Content-Type is omitted so
// the browser appends the correct multipart boundary automatically.
postForm: (p, fd) => call('POST', p, fd),
setToken: (v) => localStorage.setItem(TOKEN_KEY, v),
hasToken: () => !!token()
};