diff --git a/frontend/js/app.js b/frontend/js/app.js index ccf4d45..274049a 100755 --- a/frontend/js/app.js +++ b/frontend/js/app.js @@ -1,11 +1,9 @@ import { requireAuthOrRedirect, logout } from "./auth.js"; -import { apiFetch } from "./api.js"; +import { apiFetch, apiFetchBlob } from "./api.js"; import { CONFIG } from "./config.js"; -import { loadSession, saveSession, clearSession } from "./storage.js"; +import { loadSession } from "./storage.js"; -if (!requireAuthOrRedirect()) { - // редирект уже сделан -} +if (!requireAuthOrRedirect()) {} const userNameEl = document.getElementById("userName"); const logoutBtn = document.getElementById("logoutBtn"); @@ -17,42 +15,71 @@ const searchEl = document.getElementById("search"); let rows = []; -function showError(text) { - errorBox.textContent = text; - errorBox.classList.remove("hidden"); -} - -function clearError() { - errorBox.textContent = ""; - errorBox.classList.add("hidden"); -} - function setStatus(text) { - statusEl.textContent = text; + statusEl.textContent = text || ""; } -function normalizeCell(v) { +function showError(text) { + errorBox.textContent = text || ""; + errorBox.classList.toggle("hidden", !text); +} + +function escapeHtml(s) { + return String(s ?? "") + .replaceAll("&", "&") + .replaceAll("<", "<") + .replaceAll(">", ">") + .replaceAll('"', """) + .replaceAll("'", "'"); +} + +function norm(v) { if (v === null || v === undefined) return ""; return String(v); } -function isWireguardLike(text) { - const t = String(text || ""); - return t.includes("[Interface]") || t.includes("[Peer]") || t.includes("PrivateKey") || t.includes("AllowedIPs"); +// Поддержка двух форматов: +// 1) {"ls":[ [id,name,status,ip_local,ip_server], ... ]} +// 2) {"ls":[ {id,name,status,ip_local,ip_server}, ... ]} +function mapLsItem(x) { + if (Array.isArray(x)) { + return { + id: norm(x[0]), + name: norm(x[1]), + status: norm(x[2]), + ipLocal: norm(x[3]), + ipServer: norm(x[4]), + }; + } + if (x && typeof x === "object") { + return { + id: norm(x.id), + name: norm(x.name), + status: norm(x.status), + ipLocal: norm(x.ip_local), + ipServer: norm(x.ip_server), + }; + } + return null; } -async function copyToClipboard(text) { - try { - await navigator.clipboard.writeText(text); - } catch { - // fallback - const ta = document.createElement("textarea"); - ta.value = text; - document.body.appendChild(ta); - ta.select(); - document.execCommand("copy"); - ta.remove(); - } +async function downloadConf(id) { + const path = CONFIG.ENDPOINTS.download_conf_prefix + encodeURIComponent(id); + setStatus(`Скачивание #${id}…`); + + const { blob, filename } = await apiFetchBlob(path, { method: "GET", auth: true }); + + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = filename || `wg_${id}.conf`; + document.body.appendChild(a); + a.click(); + a.remove(); + URL.revokeObjectURL(url); + + setStatus("Готово."); + setTimeout(() => setStatus(""), 900); } function renderTable(filter = "") { @@ -60,9 +87,9 @@ function renderTable(filter = "") { const filtered = !q ? rows : rows.filter(r => r.searchBlob.includes(q)); tbody.innerHTML = ""; + for (const r of filtered) { const tr = document.createElement("tr"); - tr.innerHTML = `