feat: export client .conf from last_config

Adds POST /api/clients/export-config, UI button when exportAvailable, CLIENT_* env for Endpoint/DNS in install.sh.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Андрей Бобырев
2026-05-14 18:27:10 +03:00
parent ecb64b8b33
commit 75a3e56354
4 changed files with 339 additions and 1 deletions

View File

@@ -548,6 +548,11 @@ function renderRows(clients) {
btn("Включить", "btn small primary", () => mutate("/api/clients/enable", c.clientId))
);
}
if (c.exportAvailable) {
actTd.appendChild(
btn("Скачать .conf", "btn small ghost", () => void downloadClientConfig(c)),
);
}
actTd.appendChild(btn("Удалить", "btn small warn", () => confirmDelete(c.name, c.clientId)));
tr.append(nameTd, ipTd, stTd, offTd, actTd);
@@ -703,6 +708,45 @@ function escapeHtml(s) {
.replace(/"/g, "&quot;");
}
async function downloadClientConfig(c) {
try {
setStatus("Готовлю конфиг…", false);
const res = await fetch("/api/clients/export-config", {
method: "POST",
credentials: "same-origin",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ clientId: c.clientId }),
});
const text = await res.text();
if (!res.ok) {
let msg = text;
try {
const j = JSON.parse(text);
msg = typeof j.error === "string" ? j.error : msg;
} catch {
/* сырой текст */
}
throw new Error(msg);
}
const blob = new Blob([text], { type: "text/plain;charset=utf-8" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
const safe = String(c.name || "client")
.replace(/[^\w\u0400-\u04FF\-]+/g, "_")
.slice(0, 60);
a.href = url;
a.download = `amnezia-${safe}.conf`;
a.rel = "noopener";
document.body.appendChild(a);
a.click();
a.remove();
URL.revokeObjectURL(url);
setStatus("Конфиг скачан.", false);
} catch (e) {
setStatus(String(e.message || e), true);
}
}
async function renameClient(c) {
const next = prompt(`Новое имя для «${c.name}»:`, c.name);
if (next === null) return;