78 lines
2.8 KiB
JavaScript
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()
|
|
};
|