This commit is contained in:
Arseniy Romenskiy 2025-12-27 05:27:39 +03:00
parent 676172adc5
commit 96fedf1d3b

View file

@ -9,21 +9,46 @@ function urlJoin(base, path) {
async function parseResponse(res) { async function parseResponse(res) {
const ct = (res.headers.get("content-type") || "").toLowerCase(); const ct = (res.headers.get("content-type") || "").toLowerCase();
if (ct.includes("application/json")) return await res.json(); if (ct.includes("application/json")) return await res.json();
const text = await res.text(); const text = await res.text();
// иногда сервер шлёт json как text/plain
try { return JSON.parse(text); } catch { return text; } try { return JSON.parse(text); } catch { return text; }
} }
function applyAuthHeaders(reqHeaders, session) {
const t = String(session.short_token);
reqHeaders["short_token"] = t; // контракт твоего API
reqHeaders["Authorization"] = `Bearer ${t}`; // дубль (не мешает, пригодится)
}
function filenameFromDisposition(cd) {
if (!cd) return null;
let m = cd.match(/filename\*\s*=\s*UTF-8''([^;]+)/i);
if (m?.[1]) return decodeURIComponent(m[1].replace(/"/g, "").trim());
m = cd.match(/filename\s*=\s*"?([^";]+)"?/i);
if (m?.[1]) return m[1].trim();
return null;
}
export async function renewTokens() { export async function renewTokens() {
const s = loadSession(); const s = loadSession();
if (!s) throw new Error("NO_SESSION"); if (!s?.user_id || !s?.live_token) {
clearSession();
throw new Error("NO_SESSION");
}
// /renew/ ждёт user_id и live_token в headers :contentReference[oaicite:1]{index=1}
const res = await fetch(urlJoin(CONFIG.API_BASE_URL, CONFIG.ENDPOINTS.renew), { const res = await fetch(urlJoin(CONFIG.API_BASE_URL, CONFIG.ENDPOINTS.renew), {
method: "GET", method: "GET",
headers: { headers: {
"user_id": String(s.user_id), "user_id": String(s.user_id),
"live_token": String(s.live_token) "live_token": String(s.live_token),
// дубли (на случай фильтрации) — сервер может игнорировать
"user-id": String(s.user_id),
"live-token": String(s.live_token),
"X-User-Id": String(s.user_id),
"X-Live-Token": String(s.live_token),
} }
}); });
@ -34,12 +59,17 @@ export async function renewTokens() {
if (!res.ok) { if (!res.ok) {
const body = await parseResponse(res); const body = await parseResponse(res);
throw new Error(typeof body === "string" ? body : "RENEW_FAILED"); const err = new Error("RENEW_FAILED");
err.status = res.status;
err.payload = body;
throw err;
} }
const data = await parseResponse(res); const data = await parseResponse(res);
if (!data?.short_token || !data?.live_token) { if (!data?.short_token || !data?.live_token) {
throw new Error("BAD_RENEW_RESPONSE"); const err = new Error("BAD_RENEW_RESPONSE");
err.payload = data;
throw err;
} }
const next = { ...s, short_token: data.short_token, live_token: data.live_token }; const next = { ...s, short_token: data.short_token, live_token: data.live_token };
@ -54,34 +84,32 @@ export async function apiFetch(path, {
auth = true auth = true
} = {}) { } = {}) {
const s = loadSession(); const s = loadSession();
const reqHeaders = { ...headers }; const reqHeaders = { ...headers };
if (auth) { if (auth) {
if (!s?.short_token) throw new Error("NO_AUTH"); if (!s?.short_token) throw new Error("NO_AUTH");
// Все защищённые ручки проверяют short_token в headers :contentReference[oaicite:2]{index=2} applyAuthHeaders(reqHeaders, s);
reqHeaders["short_token"] = String(s.short_token);
} }
const doRequest = async () => { const doRequest = async () => {
const res = await fetch(urlJoin(CONFIG.API_BASE_URL, path), { const finalHeaders = {
...reqHeaders,
...(body ? { "content-type": "application/json" } : {})
};
return await fetch(urlJoin(CONFIG.API_BASE_URL, path), {
method, method,
headers: { headers: finalHeaders,
...reqHeaders,
...(body ? { "content-type": "application/json" } : {})
},
body: body ? JSON.stringify(body) : null body: body ? JSON.stringify(body) : null
}); });
return res;
}; };
let res = await doRequest(); let res = await doRequest();
// Авто-обновление токенов при 426 :contentReference[oaicite:3]{index=3}
if (auth && res.status === CONFIG.AUTH_ERROR_STATUS) { if (auth && res.status === CONFIG.AUTH_ERROR_STATUS) {
await renewTokens(); await renewTokens();
// обновим заголовок short_token и повторим один раз
const s2 = loadSession(); const s2 = loadSession();
reqHeaders["short_token"] = String(s2.short_token); applyAuthHeaders(reqHeaders, s2);
res = await doRequest(); res = await doRequest();
} }
@ -95,3 +123,45 @@ export async function apiFetch(path, {
return payload; return payload;
} }
export async function apiFetchBlob(path, {
method = "GET",
headers = {},
auth = true
} = {}) {
const s = loadSession();
const reqHeaders = { ...headers };
if (auth) {
if (!s?.short_token) throw new Error("NO_AUTH");
applyAuthHeaders(reqHeaders, s);
}
const doRequest = async () => {
return await fetch(urlJoin(CONFIG.API_BASE_URL, path), {
method,
headers: reqHeaders
});
};
let res = await doRequest();
if (auth && res.status === CONFIG.AUTH_ERROR_STATUS) {
await renewTokens();
const s2 = loadSession();
applyAuthHeaders(reqHeaders, s2);
res = await doRequest();
}
if (!res.ok) {
const text = await res.text().catch(() => "");
const err = new Error("API_BLOB_ERROR");
err.status = res.status;
err.payload = text;
throw err;
}
const blob = await res.blob();
const filename = filenameFromDisposition(res.headers.get("content-disposition"));
return { blob, filename };
}