133 lines
4.4 KiB
JavaScript
133 lines
4.4 KiB
JavaScript
// ── API client ────────────────────────────────────────────────────────────────
|
|
|
|
const API_BASE = '/api';
|
|
|
|
function getLang() { return localStorage.getItem('lang') || 'en'; }
|
|
function setLang(lang) { localStorage.setItem('lang', lang); }
|
|
|
|
function getAccess() { return localStorage.getItem('access'); }
|
|
function getRefresh() { return localStorage.getItem('refresh'); }
|
|
|
|
function setTokens(access, refresh) {
|
|
localStorage.setItem('access', access);
|
|
if (refresh) localStorage.setItem('refresh', refresh);
|
|
}
|
|
|
|
function clearTokens() {
|
|
localStorage.removeItem('access');
|
|
localStorage.removeItem('refresh');
|
|
}
|
|
|
|
async function refreshAccess() {
|
|
const refresh = getRefresh();
|
|
if (!refresh) throw new Error('no refresh token');
|
|
const res = await fetch(`${API_BASE}/auth/token/refresh/`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ refresh }),
|
|
});
|
|
if (!res.ok) throw new Error('refresh failed');
|
|
const data = await res.json();
|
|
setTokens(data.access, data.refresh || refresh);
|
|
return data.access;
|
|
}
|
|
|
|
async function apiFetch(path, opts = {}) {
|
|
const headers = { ...opts.headers };
|
|
if (!(opts.body instanceof FormData)) {
|
|
headers['Content-Type'] = headers['Content-Type'] || 'application/json';
|
|
}
|
|
headers['Accept-Language'] = getLang();
|
|
const access = getAccess();
|
|
if (access) headers['Authorization'] = `Bearer ${access}`;
|
|
|
|
let res = await fetch(`${API_BASE}${path}`, { ...opts, headers });
|
|
|
|
if (res.status === 401) {
|
|
try {
|
|
const newAccess = await refreshAccess();
|
|
headers['Authorization'] = `Bearer ${newAccess}`;
|
|
res = await fetch(`${API_BASE}${path}`, { ...opts, headers });
|
|
} catch {
|
|
clearTokens();
|
|
window.location.href = '/login.html';
|
|
throw new Error('session expired');
|
|
}
|
|
}
|
|
return res;
|
|
}
|
|
|
|
// Convenience helpers ─────────────────────────────────────────────────────────
|
|
|
|
async function apiGet(path) {
|
|
const res = await apiFetch(path);
|
|
if (!res.ok) throw new Error(`GET ${path} → ${res.status}`);
|
|
return res.json();
|
|
}
|
|
|
|
async function apiPost(path, data) {
|
|
const body = data instanceof FormData ? data : JSON.stringify(data);
|
|
const res = await apiFetch(path, { method: 'POST', body });
|
|
if (!res.ok) {
|
|
const err = await res.json().catch(() => ({}));
|
|
const e = new Error(`POST ${path} → ${res.status}`);
|
|
e.data = err; e.status = res.status;
|
|
throw e;
|
|
}
|
|
return res.status === 204 ? null : res.json();
|
|
}
|
|
|
|
async function apiPatch(path, data) {
|
|
const body = data instanceof FormData ? data : JSON.stringify(data);
|
|
const res = await apiFetch(path, { method: 'PATCH', body });
|
|
if (!res.ok) {
|
|
const err = await res.json().catch(() => ({}));
|
|
const e = new Error(`PATCH ${path} → ${res.status}`);
|
|
e.data = err; e.status = res.status;
|
|
throw e;
|
|
}
|
|
return res.json();
|
|
}
|
|
|
|
async function apiDelete(path) {
|
|
const res = await apiFetch(path, { method: 'DELETE' });
|
|
if (!res.ok) throw new Error(`DELETE ${path} → ${res.status}`);
|
|
}
|
|
|
|
// Fetch ALL pages of a paginated endpoint, returning a flat array.
|
|
// path must be relative to API_BASE (e.g. '/calibers/?status=VERIFIED').
|
|
async function apiGetAll(path) {
|
|
const results = [];
|
|
let url = path;
|
|
while (url) {
|
|
const res = await apiFetch(url);
|
|
if (!res.ok) throw new Error(`GET ${url} → ${res.status}`);
|
|
const data = await res.json();
|
|
if (Array.isArray(data)) { results.push(...data); break; }
|
|
results.push(...(data.results || []));
|
|
if (data.next) {
|
|
// DRF returns absolute next URLs; strip origin + API_BASE to get a bare path
|
|
try {
|
|
const u = new URL(data.next);
|
|
url = u.pathname.slice(API_BASE.length) + u.search;
|
|
} catch { url = null; }
|
|
} else {
|
|
url = null;
|
|
}
|
|
}
|
|
return results;
|
|
}
|
|
|
|
// Unwrap a DRF response that may be a plain array or a paginated object
|
|
function asList(data) {
|
|
return Array.isArray(data) ? data : (data.results || []);
|
|
}
|
|
|
|
// Format API validation errors into a readable string
|
|
function formatErrors(errData) {
|
|
if (!errData || typeof errData !== 'object') return 'An error occurred.';
|
|
return Object.entries(errData)
|
|
.map(([k, v]) => `${k}: ${Array.isArray(v) ? v.join(', ') : v}`)
|
|
.join('\n');
|
|
}
|