87 lines
3.1 KiB
JavaScript
87 lines
3.1 KiB
JavaScript
// public/assets/api.js
|
|
/* English comments: centralized API wrapper */
|
|
|
|
(function () {
|
|
function normalizeDateString(value) {
|
|
if (typeof value !== 'string') return value;
|
|
const s = value.trim();
|
|
if (s === '') return value;
|
|
const isoDate = /^\d{4}-\d{2}-\d{2}$/;
|
|
const isoDateTime = /^\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2}/;
|
|
const dotDate = /^\d{2}\.\d{2}\.\d{4}$/;
|
|
const dotDateTime = /^\d{2}\.\d{2}\.\d{4}\s+\d{2}:\d{2}:\d{2}$/;
|
|
let v = s;
|
|
if (isoDate.test(v)) v = `${v} 00:00:00`;
|
|
if (dotDate.test(v)) {
|
|
const parts = v.split('.');
|
|
v = `${parts[2]}-${parts[1]}-${parts[0]} 00:00:00`;
|
|
} else if (dotDateTime.test(v)) {
|
|
const [datePart, timePart] = v.split(/\s+/, 2);
|
|
const parts = datePart.split('.');
|
|
v = `${parts[2]}-${parts[1]}-${parts[0]} ${timePart}`;
|
|
}
|
|
if (isoDateTime.test(v)) {
|
|
const d = new Date(v.replace(' ', 'T') + 'Z');
|
|
if (!Number.isNaN(d.getTime())) {
|
|
return d.toISOString().slice(0, 19).replace('T', ' ');
|
|
}
|
|
}
|
|
return v;
|
|
}
|
|
|
|
function normalizeDates(payload) {
|
|
if (payload === null || payload === undefined) return payload;
|
|
if (Array.isArray(payload)) return payload.map(normalizeDates);
|
|
if (typeof payload !== 'object') return payload;
|
|
const out = {};
|
|
Object.keys(payload).forEach((key) => {
|
|
const value = payload[key];
|
|
if (value && typeof value === 'object') {
|
|
out[key] = normalizeDates(value);
|
|
} else if (typeof value === 'string' && (key === 'ts' || key.endsWith('_at') || key.endsWith('_ts'))) {
|
|
out[key] = normalizeDateString(value);
|
|
} else {
|
|
out[key] = value;
|
|
}
|
|
});
|
|
return out;
|
|
}
|
|
|
|
async function request(path, opts = {}) {
|
|
const headers = { ...(opts.headers || {}) };
|
|
const hasBodyObject = opts.body && typeof opts.body === 'object' && !(opts.body instanceof FormData);
|
|
const body = hasBodyObject ? JSON.stringify(normalizeDates(opts.body)) : opts.body;
|
|
if (hasBodyObject && !headers['Content-Type']) {
|
|
headers['Content-Type'] = 'application/json';
|
|
}
|
|
if (window.Auth?.isAccessExpired?.() && window.Auth?.refreshTokens) {
|
|
await window.Auth.refreshTokens();
|
|
}
|
|
const token = window.Auth?.getAccessToken?.();
|
|
if (token) {
|
|
headers['Authorization'] = `Bearer ${token}`;
|
|
}
|
|
let res = await fetch(path, { credentials: 'same-origin', ...opts, headers, body });
|
|
if (res.status === 401 && window.Auth?.refreshTokens) {
|
|
const refreshed = await window.Auth.refreshTokens();
|
|
if (refreshed) {
|
|
const newToken = window.Auth.getAccessToken();
|
|
if (newToken) headers['Authorization'] = `Bearer ${newToken}`;
|
|
res = await fetch(path, { credentials: 'same-origin', ...opts, headers, body });
|
|
} else if (window.Auth?.redirectToLogin) {
|
|
window.Auth.redirectToLogin();
|
|
}
|
|
}
|
|
const json = await res.json().catch(() => null);
|
|
if (!json || json.ok !== true) {
|
|
const msg = json?.error?.message || `Request failed: ${res.status}`;
|
|
throw new Error(msg);
|
|
}
|
|
return json;
|
|
}
|
|
|
|
window.Api = {
|
|
request,
|
|
};
|
|
})();
|