From 96fedf1d3ba16c5e2b9b81bfb6184b4a3d6efcec Mon Sep 17 00:00:00 2001 From: Arseniy Romenskiy Date: Sat, 27 Dec 2025 05:27:39 +0300 Subject: [PATCH] fix --- frontend/js/api.js | 106 +++++++++++++++++++++++++++++++++++++-------- 1 file changed, 88 insertions(+), 18 deletions(-) diff --git a/frontend/js/api.js b/frontend/js/api.js index e017ac2..34d2bc0 100755 --- a/frontend/js/api.js +++ b/frontend/js/api.js @@ -9,21 +9,46 @@ function urlJoin(base, path) { async function parseResponse(res) { const ct = (res.headers.get("content-type") || "").toLowerCase(); if (ct.includes("application/json")) return await res.json(); + const text = await res.text(); - // иногда сервер шлёт json как text/plain 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() { 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), { method: "GET", headers: { "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) { 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); 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 }; @@ -54,34 +84,32 @@ export async function apiFetch(path, { auth = true } = {}) { const s = loadSession(); - const reqHeaders = { ...headers }; + if (auth) { if (!s?.short_token) throw new Error("NO_AUTH"); - // Все защищённые ручки проверяют short_token в headers :contentReference[oaicite:2]{index=2} - reqHeaders["short_token"] = String(s.short_token); + applyAuthHeaders(reqHeaders, s); } 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, - headers: { - ...reqHeaders, - ...(body ? { "content-type": "application/json" } : {}) - }, + headers: finalHeaders, body: body ? JSON.stringify(body) : null }); - return res; }; let res = await doRequest(); - // Авто-обновление токенов при 426 :contentReference[oaicite:3]{index=3} if (auth && res.status === CONFIG.AUTH_ERROR_STATUS) { await renewTokens(); - // обновим заголовок short_token и повторим один раз const s2 = loadSession(); - reqHeaders["short_token"] = String(s2.short_token); + applyAuthHeaders(reqHeaders, s2); res = await doRequest(); } @@ -95,3 +123,45 @@ export async function apiFetch(path, { 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 }; +}