168 lines
4.8 KiB
JavaScript
Executable file
168 lines
4.8 KiB
JavaScript
Executable file
import { requireAuthOrRedirect, logout } from "./auth.js";
|
||
import { apiFetch, apiFetchBlob } from "./api.js";
|
||
import { CONFIG } from "./config.js";
|
||
import { loadSession } from "./storage.js";
|
||
|
||
if (!requireAuthOrRedirect()) {}
|
||
|
||
const userNameEl = document.getElementById("userName");
|
||
const logoutBtn = document.getElementById("logoutBtn");
|
||
const reloadBtn = document.getElementById("reloadBtn");
|
||
const statusEl = document.getElementById("status");
|
||
const tbody = document.getElementById("tbody");
|
||
const errorBox = document.getElementById("errorBox");
|
||
const searchEl = document.getElementById("search");
|
||
|
||
let rows = [];
|
||
|
||
function setStatus(text) {
|
||
statusEl.textContent = text || "";
|
||
}
|
||
|
||
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);
|
||
}
|
||
|
||
// Поддержка двух форматов:
|
||
// 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 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 = "") {
|
||
const q = filter.trim().toLowerCase();
|
||
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>
|
||
<td>${escapeHtml(r.status)}</td>
|
||
<td>${escapeHtml(r.ipLocal)}</td>
|
||
<td>${escapeHtml(r.ipServer)}</td>
|
||
<td></td>
|
||
`;
|
||
|
||
const tdBtn = tr.lastElementChild;
|
||
const btn = document.createElement("button");
|
||
btn.className = "btn btn-ghost btn-sm";
|
||
btn.textContent = "Скачать";
|
||
btn.addEventListener("click", async () => {
|
||
showError("");
|
||
try {
|
||
await downloadConf(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);
|
||
}
|
||
}
|
||
|
||
async function loadData() {
|
||
showError("");
|
||
setStatus("Загрузка…");
|
||
|
||
try {
|
||
const data = await apiFetch(CONFIG.ENDPOINTS.list, { method: "GET", auth: true });
|
||
const list = Array.isArray(data?.ls) ? data.ls : [];
|
||
|
||
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}`);
|
||
} catch (err) {
|
||
if (err?.status === CONFIG.AUTH_ERROR_STATUS) {
|
||
await logout();
|
||
location.replace("./login.html");
|
||
return;
|
||
}
|
||
showError("Ошибка загрузки данных.");
|
||
setStatus("");
|
||
}
|
||
}
|
||
|
||
(function init() {
|
||
const s = loadSession();
|
||
userNameEl.textContent = s?.user_name || `#${s?.user_id || "—"}`;
|
||
|
||
logoutBtn.addEventListener("click", async () => {
|
||
await logout();
|
||
location.replace("./index.html");
|
||
});
|
||
|
||
reloadBtn.addEventListener("click", loadData);
|
||
searchEl.addEventListener("input", () => renderTable(searchEl.value));
|
||
|
||
loadData();
|
||
})();
|