Implements the from-scratch multi-server WireGuard management fork per CLAUDE.md spec: sqlite schema (servers/peers/audit_log/users), Curve25519 key generation, per-interface config rendering + wg-quick/systemd control, nftables hook scaffolding, session+CSRF-protected REST API with QR code and config download endpoints, a minimal vanilla-JS web UI, legacy wg0.conf migration, and both a native installer and a Proxmox LXC provisioning script (with auto-detected latest Debian template). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
174 lines
5.7 KiB
JavaScript
174 lines
5.7 KiB
JavaScript
function getCookie(name) {
|
|
const match = document.cookie.match(new RegExp("(?:^|; )" + name + "=([^;]*)"));
|
|
return match ? decodeURIComponent(match[1]) : "";
|
|
}
|
|
|
|
async function apiFetch(url, options) {
|
|
options = options || {};
|
|
options.headers = options.headers || {};
|
|
if (options.method && options.method !== "GET") {
|
|
options.headers["X-CSRF-Token"] = getCookie("wgm_csrf");
|
|
}
|
|
const res = await fetch(url, options);
|
|
if (res.status === 401) {
|
|
window.location.href = "/login";
|
|
throw new Error("unauthenticated");
|
|
}
|
|
return res;
|
|
}
|
|
|
|
function serverIDFromPath() {
|
|
const parts = window.location.pathname.split("/").filter(Boolean);
|
|
return parts[1];
|
|
}
|
|
|
|
const serverID = serverIDFromPath();
|
|
const errEl = document.getElementById("server-error");
|
|
|
|
async function loadServer() {
|
|
errEl.textContent = "";
|
|
const res = await apiFetch("/api/servers/" + serverID);
|
|
if (!res.ok) {
|
|
const data = await res.json().catch(() => ({}));
|
|
errEl.textContent = data.error || "Server konnte nicht geladen werden.";
|
|
return;
|
|
}
|
|
const s = await res.json();
|
|
document.getElementById("server-name").textContent = s.Name;
|
|
document.getElementById("d-interface").textContent = s.InterfaceName;
|
|
document.getElementById("d-port").textContent = s.ListenPort;
|
|
document.getElementById("d-address").textContent = s.AddressRange;
|
|
document.getElementById("d-dns").textContent = s.DNS || "-";
|
|
document.getElementById("d-mtu").textContent = s.MTU;
|
|
|
|
const statusRes = await apiFetch("/api/servers");
|
|
if (statusRes.ok) {
|
|
const servers = await statusRes.json();
|
|
const match = servers.find((x) => String(x.ID) === String(serverID));
|
|
const statusTd = document.getElementById("d-status");
|
|
statusTd.innerHTML = "";
|
|
const badge = document.createElement("span");
|
|
const status = match ? match.status : "DOWN";
|
|
badge.className = "badge " + (status === "UP" ? "up" : "down");
|
|
badge.textContent = status;
|
|
statusTd.appendChild(badge);
|
|
}
|
|
}
|
|
|
|
async function loadPeers() {
|
|
const tbody = document.querySelector("#peers tbody");
|
|
tbody.innerHTML = "";
|
|
const res = await apiFetch("/api/server/" + serverID + "/peers");
|
|
if (!res.ok) return;
|
|
const peers = await res.json();
|
|
|
|
for (const p of peers) {
|
|
const tr = document.createElement("tr");
|
|
|
|
const nameTd = document.createElement("td");
|
|
nameTd.textContent = p.name;
|
|
|
|
const emailTd = document.createElement("td");
|
|
emailTd.textContent = p.email || "-";
|
|
|
|
const allowedTd = document.createElement("td");
|
|
allowedTd.textContent = p.allowed_ips;
|
|
|
|
const enabledTd = document.createElement("td");
|
|
enabledTd.textContent = p.enabled ? "Ja" : "Nein";
|
|
|
|
const actionsTd = document.createElement("td");
|
|
|
|
const qrBtn = document.createElement("button");
|
|
qrBtn.textContent = "QR-Code";
|
|
qrBtn.className = "secondary";
|
|
qrBtn.addEventListener("click", () => showQRCode(p.id));
|
|
actionsTd.appendChild(qrBtn);
|
|
|
|
const dlLink = document.createElement("a");
|
|
dlLink.href = "/api/server/" + serverID + "/peer/" + p.id + "/config?host=" + encodeURIComponent(window.location.hostname);
|
|
dlLink.textContent = "Config";
|
|
dlLink.style.marginLeft = "0.5rem";
|
|
actionsTd.appendChild(dlLink);
|
|
|
|
const delBtn = document.createElement("button");
|
|
delBtn.textContent = "Löschen";
|
|
delBtn.className = "danger";
|
|
delBtn.addEventListener("click", () => deletePeer(p.id));
|
|
actionsTd.appendChild(delBtn);
|
|
|
|
tr.appendChild(nameTd);
|
|
tr.appendChild(emailTd);
|
|
tr.appendChild(allowedTd);
|
|
tr.appendChild(enabledTd);
|
|
tr.appendChild(actionsTd);
|
|
tbody.appendChild(tr);
|
|
}
|
|
}
|
|
|
|
function showQRCode(peerID) {
|
|
const modal = document.getElementById("qrcode-modal");
|
|
const img = document.getElementById("qrcode-img");
|
|
img.src = "/api/server/" + serverID + "/peer/" + peerID + "/qrcode?host=" + encodeURIComponent(window.location.hostname) + "&t=" + Date.now();
|
|
modal.classList.remove("hidden");
|
|
}
|
|
|
|
document.getElementById("qrcode-close").addEventListener("click", () => {
|
|
document.getElementById("qrcode-modal").classList.add("hidden");
|
|
});
|
|
|
|
async function deletePeer(peerID) {
|
|
if (!confirm("Peer wirklich löschen?")) return;
|
|
await apiFetch("/api/server/" + serverID + "/peer/" + peerID, { method: "DELETE" });
|
|
loadPeers();
|
|
}
|
|
|
|
document.getElementById("btn-start").addEventListener("click", async () => {
|
|
await apiFetch("/api/servers/" + serverID + "/start", { method: "POST" });
|
|
loadServer();
|
|
});
|
|
|
|
document.getElementById("btn-stop").addEventListener("click", async () => {
|
|
await apiFetch("/api/servers/" + serverID + "/stop", { method: "POST" });
|
|
loadServer();
|
|
});
|
|
|
|
document.getElementById("btn-reload").addEventListener("click", async () => {
|
|
await apiFetch("/api/servers/" + serverID + "/reload", { method: "POST" });
|
|
loadServer();
|
|
});
|
|
|
|
document.getElementById("btn-download").addEventListener("click", () => {
|
|
window.location.href = "/api/servers/" + serverID + "/config";
|
|
});
|
|
|
|
document.getElementById("peer-form").addEventListener("submit", async (e) => {
|
|
e.preventDefault();
|
|
const errP = document.getElementById("peer-error");
|
|
errP.textContent = "";
|
|
const form = e.target;
|
|
const body = {
|
|
name: form.name.value,
|
|
email: form.email.value,
|
|
allowed_ips: form.allowed_ips.value,
|
|
persistent_keepalive: parseInt(form.persistent_keepalive.value, 10) || 25,
|
|
use_preshared_key: form.use_preshared_key.checked,
|
|
};
|
|
const res = await apiFetch("/api/server/" + serverID + "/peer", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(body),
|
|
});
|
|
if (!res.ok) {
|
|
const data = await res.json().catch(() => ({}));
|
|
errP.textContent = data.error || "Peer konnte nicht erstellt werden.";
|
|
return;
|
|
}
|
|
form.reset();
|
|
form.persistent_keepalive.value = 25;
|
|
loadPeers();
|
|
});
|
|
|
|
loadServer();
|
|
loadPeers();
|