diff --git a/frontend/js/app.js b/frontend/js/app.js index 274c0d6..ccf4d45 100755 --- a/frontend/js/app.js +++ b/frontend/js/app.js @@ -1,7 +1,7 @@ import { requireAuthOrRedirect, logout } from "./auth.js"; import { apiFetch } from "./api.js"; -import { loadSession } from "./storage.js"; import { CONFIG } from "./config.js"; +import { loadSession, saveSession, clearSession } from "./storage.js"; if (!requireAuthOrRedirect()) { // редирект уже сделан @@ -63,32 +63,34 @@ function renderTable(filter = "") { for (const r of filtered) { const tr = document.createElement("tr"); - const commentPreview = r.comments.length > 180 ? (r.comments.slice(0, 180) + "…") : r.comments; - tr.innerHTML = ` - ${r.id} - ${escapeHtml(r.name)} + ${escapeHtml(r.id)} + ${escapeHtml(r.name)} ${escapeHtml(r.status)} - ${escapeHtml(r.ownerName)} (#${r.ownerId}) - ${escapeHtml(r.targetName)} (#${r.targetId}) -
${escapeHtml(commentPreview)}
- ${escapeHtml(r.inventoryId)} + ${escapeHtml(r.ipLocal)} + ${escapeHtml(r.ipServer)} `; - const actionTd = tr.lastElementChild; - - if (isWireguardLike(r.comments)) { - const btn = document.createElement("button"); - btn.className = "btn btn-ghost btn-sm"; - btn.textContent = "Копировать WG"; - btn.addEventListener("click", async () => { - await copyToClipboard(r.comments); - setStatus("Скопировано."); - setTimeout(() => setStatus(""), 1200); - }); - actionTd.appendChild(btn); - } + const tdBtn = tr.lastElementChild; + const btn = document.createElement("button"); + btn.className = "btn btn-ghost btn-sm"; + btn.textContent = "Скачать"; + btn.addEventListener("click", async () => { + clearError(); + try { + await triggerDownloadById(r.id); + } catch (err) { + if (err?.status === CONFIG.AUTH_ERROR_STATUS) { + await logout(); + location.replace("./login.html"); + return; + } + showError("Не удалось скачать конфиг."); + setStatus(""); + } + }); + tdBtn.appendChild(btn); tbody.appendChild(tr); } @@ -108,28 +110,15 @@ async function loadData() { setStatus("Загрузка…"); try { - // /ls/ возвращает {"ls": bd_module.ls_item(uid)} :contentReference[oaicite:7]{index=7} - // Формат строк из БД: [item_id,item_name,status,user_id_1,user_name1,user_id_2,user_name2,icon,comments,inventory_id] :contentReference[oaicite:8]{index=8} const data = await apiFetch(CONFIG.ENDPOINTS.list, { method: "GET", auth: true }); - - const list = Array.isArray(data?.ls) ? data.ls : []; - rows = list.map((a) => { - const id = normalizeCell(a?.[0]); - const name = normalizeCell(a?.[1]); - const status = normalizeCell(a?.[2]); - const ownerId = normalizeCell(a?.[3]); - const ownerName = normalizeCell(a?.[4]); - const targetId = normalizeCell(a?.[5]); - const targetName = normalizeCell(a?.[6]); - const comments = normalizeCell(a?.[8]); - const inventoryId = normalizeCell(a?.[9]); - - const searchBlob = [ - id, name, status, ownerId, ownerName, targetId, targetName, comments, inventoryId - ].join(" ").toLowerCase(); - - return { id, name, status, ownerId, ownerName, targetId, targetName, comments, inventoryId, searchBlob }; - }); + const list = Array.isArray(data?.ls) ? data.ls : Array.isArray(data) ? data : []; + rows = list + .map(mapLsItem) + .filter(Boolean) + .map(r => ({ + ...r, + searchBlob: [r.id, r.name, r.status, r.ipLocal, r.ipServer].join(" ").toLowerCase() + })); renderTable(searchEl.value); setStatus(`Готово. Строк: ${rows.length}`); @@ -160,3 +149,55 @@ async function loadData() { loadData(); })(); +function filenameFromDisposition(cd) { + if (!cd) return null; + + // filename*=UTF-8''... + let m = cd.match(/filename\*\s*=\s*UTF-8''([^;]+)/i); + if (m?.[1]) return decodeURIComponent(m[1].replace(/"/g, "").trim()); + + // filename="..." + m = cd.match(/filename\s*=\s*"?([^";]+)"?/i); + if (m?.[1]) return m[1].trim(); + + return null; +} + +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"); + reqHeaders["short_token"] = String(s.short_token); + } + + const doRequest = async () => { + return await fetch(path.startsWith("http") ? path : (CONFIG.API_BASE_URL ? CONFIG.API_BASE_URL.replace(/\/+$/, "") + path : path), { + method, + headers: reqHeaders + }); + }; + + let res = await doRequest(); + + if (auth && res.status === CONFIG.AUTH_ERROR_STATUS) { + // renew + retry один раз + await (await import("./api.js")).renewTokens?.(); // на случай циклического импорта (если будет) — безопасно + const s2 = loadSession(); + reqHeaders["short_token"] = String(s2.short_token); + 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 }; +}