fix
This commit is contained in:
parent
22d9f275f2
commit
676172adc5
1 changed files with 65 additions and 100 deletions
|
|
@ -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 = `
|
||||
<td>${escapeHtml(r.id)}</td>
|
||||
<td>${escapeHtml(r.name)}</td>
|
||||
|
|
@ -77,9 +104,9 @@ function renderTable(filter = "") {
|
|||
btn.className = "btn btn-ghost btn-sm";
|
||||
btn.textContent = "Скачать";
|
||||
btn.addEventListener("click", async () => {
|
||||
clearError();
|
||||
showError("");
|
||||
try {
|
||||
await triggerDownloadById(r.id);
|
||||
await downloadConf(r.id);
|
||||
} catch (err) {
|
||||
if (err?.status === CONFIG.AUTH_ERROR_STATUS) {
|
||||
await logout();
|
||||
|
|
@ -96,22 +123,14 @@ function renderTable(filter = "") {
|
|||
}
|
||||
}
|
||||
|
||||
function escapeHtml(s) {
|
||||
return String(s)
|
||||
.replaceAll("&", "&")
|
||||
.replaceAll("<", "<")
|
||||
.replaceAll(">", ">")
|
||||
.replaceAll('"', """)
|
||||
.replaceAll("'", "'");
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
clearError();
|
||||
showError("");
|
||||
setStatus("Загрузка…");
|
||||
|
||||
try {
|
||||
const data = await apiFetch(CONFIG.ENDPOINTS.list, { method: "GET", auth: true });
|
||||
const list = Array.isArray(data?.ls) ? data.ls : Array.isArray(data) ? data : [];
|
||||
const list = Array.isArray(data?.ls) ? data.ls : [];
|
||||
|
||||
rows = list
|
||||
.map(mapLsItem)
|
||||
.filter(Boolean)
|
||||
|
|
@ -124,7 +143,6 @@ async function loadData() {
|
|||
setStatus(`Готово. Строк: ${rows.length}`);
|
||||
} catch (err) {
|
||||
if (err?.status === CONFIG.AUTH_ERROR_STATUS) {
|
||||
// если даже renew не помог — сессия умерла
|
||||
await logout();
|
||||
location.replace("./login.html");
|
||||
return;
|
||||
|
|
@ -144,60 +162,7 @@ async function loadData() {
|
|||
});
|
||||
|
||||
reloadBtn.addEventListener("click", loadData);
|
||||
|
||||
searchEl.addEventListener("input", () => renderTable(searchEl.value));
|
||||
|
||||
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 };
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue