Fix Mult server
This commit is contained in:
@@ -6,6 +6,7 @@ let currentUser = "";
|
||||
let statsTimer = null, usersTimer = null, xrayTimer = null;
|
||||
let formCollapsed = true;
|
||||
let tlsForwardersState = [];
|
||||
let managedTlsForwardersState = [];
|
||||
let editingXrayClientId = null;
|
||||
let wzInbounds = [];
|
||||
let dashboardCache = { sshUsers: [], xrayInbounds: [], me: null };
|
||||
@@ -1387,6 +1388,8 @@ document.getElementById("testServerBtn")?.addEventListener("click", testServerFo
|
||||
document.getElementById("backToServersBtn")?.addEventListener("click", () => showServerListView());
|
||||
document.getElementById("loadManagedConfigBtn")?.addEventListener("click", () => loadManagedServerConfig(configuringServerID));
|
||||
document.getElementById("saveManagedConfigBtn")?.addEventListener("click", saveManagedServerConfig);
|
||||
document.getElementById("saveManagedConfigBottomBtn")?.addEventListener("click", saveManagedServerConfig);
|
||||
document.getElementById("reloadManagedConfigBottomBtn")?.addEventListener("click", () => loadManagedServerConfig(configuringServerID));
|
||||
serverForm?.addEventListener("submit", async e => {
|
||||
e.preventDefault();
|
||||
await saveServerForm();
|
||||
@@ -1576,34 +1579,278 @@ function openManagedServerConfig(id) {
|
||||
loadManagedServerConfig(configuringServerID);
|
||||
}
|
||||
|
||||
function toggleManagedDnsttFields(on) {
|
||||
const el = document.getElementById("managedDnsttFields");
|
||||
if (!el) return;
|
||||
el.style.opacity = on ? "1" : ".4";
|
||||
el.style.pointerEvents = on ? "" : "none";
|
||||
}
|
||||
function toggleManagedUdpgwFields(on) {
|
||||
const el = document.getElementById("managedUdpgwFields");
|
||||
if (!el) return;
|
||||
el.style.opacity = on ? "1" : ".4";
|
||||
el.style.pointerEvents = on ? "" : "none";
|
||||
}
|
||||
|
||||
async function loadManagedServerConfig(id) {
|
||||
if (!id) return;
|
||||
managedConfigStatus.textContent = "Loading config…";
|
||||
const st = document.getElementById("managedConfigStatus");
|
||||
if (st) st.textContent = "Loading config…";
|
||||
try {
|
||||
const res = await api(withServerParam("/api/servers/config", id));
|
||||
if (!res.ok) throw new Error(await res.text());
|
||||
const text = await res.text();
|
||||
try { managedConfigEditor.value = JSON.stringify(JSON.parse(text), null, 2); }
|
||||
catch { managedConfigEditor.value = text; }
|
||||
managedConfigStatus.textContent = "Config loaded.";
|
||||
const c = await res.json();
|
||||
|
||||
document.getElementById("managedCfgListen").value = c.listen || "";
|
||||
document.getElementById("managedCfgExtraListen").value = (c.extra_listen || []).join("\n");
|
||||
|
||||
document.getElementById("managedCfgLimitUp").value = c.default_limit_mbps_up || 0;
|
||||
document.getElementById("managedCfgLimitDown").value = c.default_limit_mbps_down || 0;
|
||||
document.getElementById("managedCfgQuiet").checked = !!c.quiet;
|
||||
document.getElementById("managedCfgUserCount").checked = !!c.user_count;
|
||||
document.getElementById("managedCfgBanner").value = c.banner || "";
|
||||
|
||||
const hasDnstt = !!c.dnstt;
|
||||
document.getElementById("managedCfgDnsttEnabled").checked = hasDnstt;
|
||||
toggleManagedDnsttFields(hasDnstt);
|
||||
const d = c.dnstt || {};
|
||||
document.getElementById("managedCfgDnsttDomain").value = d.domain || "";
|
||||
document.getElementById("managedCfgDnsttUDP").value = d.udp_listen || "";
|
||||
document.getElementById("managedCfgDnsttKey").value = d.privkey_file || "/opt/sshpanel/dnstt.key";
|
||||
document.getElementById("managedCfgDnsttNoStats").checked = !!d.disable_stats_log;
|
||||
document.getElementById("managedCfgDnsttNoConsole").checked = !!d.disable_console_log;
|
||||
|
||||
const hasUdpgw = !!c.udpgw;
|
||||
document.getElementById("managedCfgUdpgwEnabled").checked = hasUdpgw;
|
||||
toggleManagedUdpgwFields(hasUdpgw);
|
||||
const u = c.udpgw || {};
|
||||
document.getElementById("managedCfgUdpgwListen").value = u.listen || "";
|
||||
document.getElementById("managedCfgUdpgwMaxConns").value = u.max_client_conns || 0;
|
||||
document.getElementById("managedCfgUdpgwIdle").value = u.idle_timeout || "";
|
||||
document.getElementById("managedCfgUdpgwMapTTL").value = u.map_ttl || "";
|
||||
document.getElementById("managedCfgUdpgwDebug").checked = !!u.debug;
|
||||
|
||||
managedTlsForwardersState = c.tls_forwarders || [];
|
||||
renderManagedTLSForwarders();
|
||||
|
||||
const x = c.xray || {};
|
||||
document.getElementById("managedCfgXrayEnabled").checked = !!x.enabled;
|
||||
|
||||
document.getElementById("managedDnsttPubkeyWrap")?.classList.add("hidden");
|
||||
if (st) st.textContent = "Config loaded.";
|
||||
} catch (e) {
|
||||
if (e.message === "auth") doAuthError();
|
||||
else managedConfigStatus.textContent = "Error: " + e.message;
|
||||
else if (st) st.textContent = "Error: " + e.message;
|
||||
}
|
||||
}
|
||||
|
||||
function managedConfigFromForm() {
|
||||
const extraLines = document.getElementById("managedCfgExtraListen").value
|
||||
.split("\n").map(s => s.trim()).filter(Boolean);
|
||||
return {
|
||||
listen: document.getElementById("managedCfgListen").value.trim(),
|
||||
extra_listen: extraLines,
|
||||
host_key_file: "/opt/sshpanel/ssh_host_rsa_key",
|
||||
admin_dir: "/opt/sshpanel/admin",
|
||||
default_limit_mbps_up: parseInt(document.getElementById("managedCfgLimitUp").value || "0", 10),
|
||||
default_limit_mbps_down: parseInt(document.getElementById("managedCfgLimitDown").value || "0", 10),
|
||||
quiet: document.getElementById("managedCfgQuiet").checked,
|
||||
user_count: document.getElementById("managedCfgUserCount").checked,
|
||||
banner: document.getElementById("managedCfgBanner").value,
|
||||
banner_file: "/opt/sshpanel/banner.txt",
|
||||
dnstt: document.getElementById("managedCfgDnsttEnabled").checked ? {
|
||||
domain: document.getElementById("managedCfgDnsttDomain").value.trim(),
|
||||
udp_listen: document.getElementById("managedCfgDnsttUDP").value.trim(),
|
||||
privkey_file: document.getElementById("managedCfgDnsttKey").value.trim(),
|
||||
disable_stats_log: document.getElementById("managedCfgDnsttNoStats").checked,
|
||||
disable_console_log: document.getElementById("managedCfgDnsttNoConsole").checked,
|
||||
} : null,
|
||||
udpgw: document.getElementById("managedCfgUdpgwEnabled").checked ? {
|
||||
listen: document.getElementById("managedCfgUdpgwListen").value.trim(),
|
||||
max_client_conns: parseInt(document.getElementById("managedCfgUdpgwMaxConns").value || "0", 10),
|
||||
idle_timeout: document.getElementById("managedCfgUdpgwIdle").value.trim(),
|
||||
map_ttl: document.getElementById("managedCfgUdpgwMapTTL").value.trim(),
|
||||
debug: document.getElementById("managedCfgUdpgwDebug").checked,
|
||||
} : null,
|
||||
tls_forwarders: managedTlsForwardersState,
|
||||
xray: {
|
||||
enabled: document.getElementById("managedCfgXrayEnabled").checked,
|
||||
bin_path: "/opt/sshpanel/xray",
|
||||
config_file: "/opt/sshpanel/xray_config.json",
|
||||
api_server: "127.0.0.1:10085",
|
||||
online_window_seconds: 90,
|
||||
stats_poll_seconds: 15,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function saveManagedServerConfig() {
|
||||
if (!configuringServerID) return;
|
||||
const text = managedConfigEditor.value.trim();
|
||||
try { JSON.parse(text); } catch (e) { managedConfigStatus.textContent = "Invalid JSON: " + e.message; return; }
|
||||
managedConfigStatus.textContent = "Saving config…";
|
||||
const st = document.getElementById("managedConfigStatus");
|
||||
if (st) st.textContent = "Saving config…";
|
||||
try {
|
||||
const res = await api(withServerParam("/api/servers/config", configuringServerID), { method:"POST", body: text });
|
||||
const cfg = managedConfigFromForm();
|
||||
const res = await api(withServerParam("/api/servers/config", configuringServerID), { method:"POST", body: JSON.stringify(cfg) });
|
||||
if (!res.ok) throw new Error(await res.text());
|
||||
managedConfigStatus.textContent = "Saved and applied.";
|
||||
const report = await res.json().catch(() => null);
|
||||
const warnings = report?.warnings || [];
|
||||
const bad = Object.entries(report?.services || {}).filter(([_, v]) => v?.enabled && !v?.running);
|
||||
if (warnings.length || bad.length) {
|
||||
const badText = bad.map(([name, v]) => `${name}: ${v.error || "not running"}`).join(" | ");
|
||||
if (st) st.textContent = "Saved live with warnings: " + [...warnings, badText].filter(Boolean).join(" | ");
|
||||
} else if (st) {
|
||||
st.textContent = "Saved and applied live.";
|
||||
}
|
||||
} catch (e) {
|
||||
if (e.message === "auth") doAuthError();
|
||||
else managedConfigStatus.textContent = "Error: " + e.message;
|
||||
else if (st) st.textContent = "Error: " + e.message;
|
||||
}
|
||||
}
|
||||
|
||||
function renderManagedTLSForwarders() {
|
||||
const list = document.getElementById("managedTlsForwardersList");
|
||||
const chip = document.getElementById("managedTlsCountChip");
|
||||
if (!list) return;
|
||||
if (chip) chip.textContent = managedTlsForwardersState.length;
|
||||
if (!managedTlsForwardersState.length) {
|
||||
list.innerHTML = '<div class="hint" style="padding:4px 0;">No TLS forwarders configured.</div>';
|
||||
return;
|
||||
}
|
||||
list.innerHTML = "";
|
||||
managedTlsForwardersState.forEach((fw, i) => {
|
||||
const row = document.createElement("div");
|
||||
row.style = "display:flex;align-items:center;gap:8px;padding:5px 0;border-bottom:1px solid var(--border);font-size:.73rem;";
|
||||
row.innerHTML = `<span style="flex:1;font-family:monospace;">${escapeHTML(fw.listen || "")}</span>
|
||||
<span class="hint">${escapeHTML(fw.cert_file ? fw.cert_file.split("/").pop() : "no cert")}</span>`;
|
||||
const delBtn = document.createElement("button");
|
||||
delBtn.className = "btn btn-danger btn-sm";
|
||||
delBtn.textContent = "Remove";
|
||||
delBtn.onclick = () => { managedTlsForwardersState.splice(i,1); renderManagedTLSForwarders(); };
|
||||
row.appendChild(delBtn);
|
||||
list.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
function toggleManagedAddTLSForm() {
|
||||
const panel = document.getElementById("managedAddTLSPanel");
|
||||
if (!panel) return;
|
||||
panel.classList.toggle("hidden");
|
||||
if (!panel.classList.contains("hidden")) {
|
||||
document.getElementById("managedTlsAddStatus").textContent = "";
|
||||
document.getElementById("managedTlsListenAddr").value = "";
|
||||
document.getElementById("managedTlsSSLDomain").value = "";
|
||||
document.getElementById("managedTlsCertType").value = "selfsigned";
|
||||
onManagedTLSTypeChange("selfsigned");
|
||||
}
|
||||
}
|
||||
|
||||
function onManagedTLSTypeChange(val) {
|
||||
const setVisible = (id, on, display = "") => {
|
||||
const el = document.getElementById(id);
|
||||
if (!el) return;
|
||||
el.classList.toggle("hidden", !on);
|
||||
el.style.display = on ? display : "none";
|
||||
};
|
||||
setVisible("managedTlsSSFields", val === "selfsigned", "");
|
||||
setVisible("managedTlsLEFields", val === "letsencrypt", "grid");
|
||||
setVisible("managedTlsPasteFields", val === "paste", "");
|
||||
setVisible("managedTlsCustomFields", val === "custom", "grid");
|
||||
}
|
||||
|
||||
async function addManagedTLSForwarder() {
|
||||
const st = document.getElementById("managedTlsAddStatus");
|
||||
const listen = document.getElementById("managedTlsListenAddr").value.trim();
|
||||
const certType = document.getElementById("managedTlsCertType").value;
|
||||
if (!listen) { st.textContent = "Listen address required."; return; }
|
||||
let certFile = "", keyFile = "";
|
||||
st.textContent = "Processing…";
|
||||
if (certType === "selfsigned") {
|
||||
const domain = document.getElementById("managedTlsSSLDomain").value.trim();
|
||||
if (!domain) { st.textContent = "Domain required."; return; }
|
||||
try {
|
||||
const res = await api(withServerParam("/api/tls/generate-selfsigned", configuringServerID), { method:"POST", body: JSON.stringify({ domain }) });
|
||||
if (!res.ok) throw new Error(await res.text());
|
||||
const data = await res.json();
|
||||
certFile = data.cert_file; keyFile = data.key_file;
|
||||
st.textContent = "Self-signed cert generated.";
|
||||
} catch (e) {
|
||||
if (e.message === "auth") doAuthError();
|
||||
else st.textContent = "Cert error: " + e.message;
|
||||
return;
|
||||
}
|
||||
} else if (certType === "letsencrypt") {
|
||||
const domain = document.getElementById("managedTlsLEDomain").value.trim();
|
||||
const email = document.getElementById("managedTlsLEEmail").value.trim();
|
||||
if (!domain || !email) { st.textContent = "Domain and email required."; return; }
|
||||
st.textContent = "Running certbot… (may take ~30s)";
|
||||
try {
|
||||
const res = await api(withServerParam("/api/tls/letsencrypt", configuringServerID), { method:"POST", body: JSON.stringify({ domain, email }) });
|
||||
if (!res.ok) throw new Error(await res.text());
|
||||
const data = await res.json();
|
||||
certFile = data.cert_file; keyFile = data.key_file;
|
||||
st.textContent = "Let's Encrypt cert issued.";
|
||||
} catch (e) {
|
||||
if (e.message === "auth") doAuthError();
|
||||
else st.textContent = "certbot error: " + e.message;
|
||||
return;
|
||||
}
|
||||
} else if (certType === "paste") {
|
||||
const name = document.getElementById("managedTlsPasteName").value.trim();
|
||||
const cert = document.getElementById("managedTlsPasteCert").value.trim();
|
||||
const key = document.getElementById("managedTlsPasteKey").value.trim();
|
||||
if (!name || !cert || !key) { st.textContent = "Name, cert PEM, and key PEM required."; return; }
|
||||
try {
|
||||
const res = await api(withServerParam("/api/tls/upload-pem", configuringServerID), { method:"POST", body: JSON.stringify({ name, cert, key }) });
|
||||
if (!res.ok) throw new Error(await res.text());
|
||||
const data = await res.json();
|
||||
certFile = data.cert_file; keyFile = data.key_file;
|
||||
st.textContent = "PEM saved.";
|
||||
} catch (e) {
|
||||
if (e.message === "auth") doAuthError();
|
||||
else st.textContent = "Upload error: " + e.message;
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
certFile = document.getElementById("managedTlsCustomCert").value.trim();
|
||||
keyFile = document.getElementById("managedTlsCustomKey").value.trim();
|
||||
if (!certFile || !keyFile) { st.textContent = "Cert and key paths required."; return; }
|
||||
}
|
||||
managedTlsForwardersState.push({ listen, cert_file: certFile, key_file: keyFile });
|
||||
renderManagedTLSForwarders();
|
||||
document.getElementById("managedAddTLSPanel").classList.add("hidden");
|
||||
st.textContent = "Added. Save config to apply.";
|
||||
}
|
||||
|
||||
async function generateManagedDnsttKey() {
|
||||
const st = document.getElementById("managedDnsttKeyStatus");
|
||||
if (st) st.textContent = "Generating key…";
|
||||
try {
|
||||
const res = await api(withServerParam("/api/dnstt/genkey", configuringServerID), { method:"POST" });
|
||||
if (!res.ok) throw new Error(await res.text());
|
||||
const data = await res.json();
|
||||
document.getElementById("managedCfgDnsttKey").value = data.privkey_file || "/opt/sshpanel/dnstt.key";
|
||||
if (st) st.textContent = "Key generated. Save config to apply.";
|
||||
await loadManagedDnsttPubkey();
|
||||
} catch (e) {
|
||||
if (e.message === "auth") doAuthError();
|
||||
else if (st) st.textContent = "Error: " + e.message;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadManagedDnsttPubkey() {
|
||||
const st = document.getElementById("managedDnsttKeyStatus");
|
||||
if (st) st.textContent = "Loading public key…";
|
||||
try {
|
||||
const res = await api(withServerParam("/api/dnstt/pubkey", configuringServerID));
|
||||
if (!res.ok) throw new Error(await res.text());
|
||||
const data = await res.json();
|
||||
const val = data.public_key || data.pubkey || "";
|
||||
document.getElementById("managedDnsttPubkeyVal").value = val;
|
||||
document.getElementById("managedDnsttPubkeyWrap")?.classList.remove("hidden");
|
||||
if (st) st.textContent = val ? "Public key loaded." : "No public key returned.";
|
||||
} catch (e) {
|
||||
if (e.message === "auth") doAuthError();
|
||||
else if (st) st.textContent = "Error: " + e.message;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1985,10 +2232,16 @@ function toggleAddTLSForm() {
|
||||
}
|
||||
|
||||
function onTLSTypeChange(val) {
|
||||
document.getElementById("tlsSSFields").style.display = val === "selfsigned" ? "" : "none";
|
||||
document.getElementById("tlsLEFields").style.display = val === "letsencrypt" ? "grid" : "none";
|
||||
document.getElementById("tlsPasteFields").style.display = val === "paste" ? "" : "none";
|
||||
document.getElementById("tlsCustomFields").style.display = val === "custom" ? "grid" : "none";
|
||||
const setVisible = (id, on, display = "") => {
|
||||
const el = document.getElementById(id);
|
||||
if (!el) return;
|
||||
el.classList.toggle("hidden", !on);
|
||||
el.style.display = on ? display : "none";
|
||||
};
|
||||
setVisible("tlsSSFields", val === "selfsigned", "");
|
||||
setVisible("tlsLEFields", val === "letsencrypt", "grid");
|
||||
setVisible("tlsPasteFields", val === "paste", "");
|
||||
setVisible("tlsCustomFields", val === "custom", "grid");
|
||||
}
|
||||
|
||||
async function addTLSForwarder() {
|
||||
|
||||
151
admin/index.html
151
admin/index.html
@@ -620,7 +620,7 @@
|
||||
</div>
|
||||
|
||||
<div id="serverConfigSubpage" class="hidden">
|
||||
<div class="card">
|
||||
<div class="card" style="margin-bottom:12px;">
|
||||
<div class="card-hdr">
|
||||
<div class="card-title">Configure server <span class="chip" id="cfgServerName">--</span></div>
|
||||
<div class="card-actions">
|
||||
@@ -629,8 +629,151 @@
|
||||
<button class="btn btn-sm" id="saveManagedConfigBtn" type="button">Save config</button>
|
||||
</div>
|
||||
</div>
|
||||
<textarea id="managedConfigEditor" rows="24" style="width:100%;box-sizing:border-box;background:var(--input-bg);border:1px solid var(--border);border-radius:8px;color:inherit;padding:10px;font-family:monospace;font-size:.75rem;resize:vertical;"></textarea>
|
||||
<div class="statusbar"><span id="managedConfigStatus">Select Configure on a server to edit that node config.</span></div>
|
||||
<div class="statusbar"><span id="managedConfigStatus">Select Configure on a server to edit that node config.</span><span class="hint">This visual editor writes the same config.json on the selected node.</span></div>
|
||||
</div>
|
||||
|
||||
<div class="grid2">
|
||||
<div>
|
||||
<div class="card">
|
||||
<div class="card-hdr"><div class="card-title">Network</div><span class="chip green">live</span></div>
|
||||
<div class="field">
|
||||
<label>Main Listen (SSH / HTTP)</label>
|
||||
<input type="text" id="managedCfgListen" placeholder="0.0.0.0:80"/>
|
||||
</div>
|
||||
<div class="field" style="margin-top:8px;">
|
||||
<label>Extra Listen Addresses <span class="hint">(one per line, e.g. 0.0.0.0:8080)</span></label>
|
||||
<textarea id="managedCfgExtraListen" rows="4" style="width:100%;box-sizing:border-box;background:var(--input-bg);border:1px solid var(--border);border-radius:8px;color:inherit;padding:10px;resize:vertical;"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-top:12px;">
|
||||
<div class="card-hdr"><div class="card-title">SSH & General</div></div>
|
||||
<div class="form-grid">
|
||||
<div class="field"><label>Default Upload Limit (Mbps)</label><input type="number" id="managedCfgLimitUp" min="0" placeholder="0"/></div>
|
||||
<div class="field"><label>Default Download Limit (Mbps)</label><input type="number" id="managedCfgLimitDown" min="0" placeholder="0"/></div>
|
||||
<label style="font-size:.73rem;display:flex;align-items:center;gap:5px;cursor:pointer;"><input type="checkbox" id="managedCfgQuiet"/> Quiet Logs</label>
|
||||
<label style="font-size:.73rem;display:flex;align-items:center;gap:5px;cursor:pointer;"><input type="checkbox" id="managedCfgUserCount"/> User Count Display</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-top:12px;">
|
||||
<div class="card-hdr"><div class="card-title">SSH Banner</div><span class="chip green">live</span></div>
|
||||
<div class="field">
|
||||
<label>Banner Text <span class="hint">(shown to connecting SSH clients)</span></label>
|
||||
<textarea id="managedCfgBanner" rows="5" style="width:100%;box-sizing:border-box;background:var(--input-bg);border:1px solid var(--border);border-radius:8px;color:inherit;padding:10px;resize:vertical;"></textarea>
|
||||
</div>
|
||||
<div class="hint" style="margin-top:6px;">Banner file: /opt/sshpanel/banner.txt</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="card">
|
||||
<div class="card-hdr">
|
||||
<div class="card-title">DNSTT Tunnel</div>
|
||||
<span class="chip green">live</span>
|
||||
<label style="font-size:.73rem;display:flex;align-items:center;gap:5px;cursor:pointer;margin-left:auto;">
|
||||
<input type="checkbox" id="managedCfgDnsttEnabled" onchange="toggleManagedDnsttFields(this.checked)"/> Enabled
|
||||
</label>
|
||||
</div>
|
||||
<div id="managedDnsttFields" class="form-grid" style="opacity:.4;pointer-events:none;">
|
||||
<div class="field"><label>Domain</label><input type="text" id="managedCfgDnsttDomain" placeholder="t.example.com"/></div>
|
||||
<div class="field"><label>UDP Listen</label><input type="text" id="managedCfgDnsttUDP" placeholder="[::]:5300"/></div>
|
||||
<div class="field" style="grid-column:1/-1">
|
||||
<label>Private Key <span class="hint">auto-saved to /opt/sshpanel/dnstt.key</span></label>
|
||||
<div class="field-row">
|
||||
<input type="text" id="managedCfgDnsttKey" readonly placeholder="Generate a key first…"/>
|
||||
<button class="btn btn-ghost btn-sm" type="button" onclick="generateManagedDnsttKey()">Generate</button>
|
||||
<button class="btn btn-ghost btn-sm" type="button" onclick="loadManagedDnsttPubkey()">Public Key</button>
|
||||
</div>
|
||||
<div id="managedDnsttPubkeyWrap" class="hidden" style="margin-top:6px;padding:8px;border-radius:8px;background:rgba(15,23,42,.9);border:1px solid rgba(55,65,81,.9);">
|
||||
<div style="font-size:.68rem;color:var(--muted);margin-bottom:4px;">Public Key — share with dnstt clients</div>
|
||||
<div class="field-row">
|
||||
<input type="text" id="managedDnsttPubkeyVal" readonly style="font-family:monospace;font-size:.65rem;"/>
|
||||
<button class="btn btn-ghost btn-sm" type="button" id="managedDnsttCopyBtn" onclick="navigator.clipboard.writeText(document.getElementById('managedDnsttPubkeyVal').value);document.getElementById('managedDnsttCopyBtn').textContent='Copied!';setTimeout(()=>document.getElementById('managedDnsttCopyBtn').textContent='Copy',1500)">Copy</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="managedDnsttKeyStatus" class="hint" style="margin-top:4px;"></div>
|
||||
</div>
|
||||
<label style="font-size:.73rem;display:flex;align-items:center;gap:5px;cursor:pointer;grid-column:1/-1"><input type="checkbox" id="managedCfgDnsttNoStats"/> Disable Stats Log</label>
|
||||
<label style="font-size:.73rem;display:flex;align-items:center;gap:5px;cursor:pointer;grid-column:1/-1"><input type="checkbox" id="managedCfgDnsttNoConsole"/> Disable Console Log</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-top:12px">
|
||||
<div class="card-hdr">
|
||||
<div class="card-title">UDP Gateway</div>
|
||||
<span class="chip green">live</span>
|
||||
<label style="font-size:.73rem;display:flex;align-items:center;gap:5px;cursor:pointer;margin-left:auto;">
|
||||
<input type="checkbox" id="managedCfgUdpgwEnabled" onchange="toggleManagedUdpgwFields(this.checked)"/> Enabled
|
||||
</label>
|
||||
</div>
|
||||
<div id="managedUdpgwFields" class="form-grid" style="opacity:.4;pointer-events:none;">
|
||||
<div class="field"><label>Listen</label><input type="text" id="managedCfgUdpgwListen" placeholder="0.0.0.0:7400"/></div>
|
||||
<div class="field"><label>Max UDP Sessions Per Client <span class="hint">(not total server users)</span></label><input type="number" id="managedCfgUdpgwMaxConns" min="0" placeholder="10"/></div>
|
||||
<div class="field"><label>Idle Timeout</label><input type="text" id="managedCfgUdpgwIdle" placeholder="2m"/></div>
|
||||
<div class="field"><label>Map TTL</label><input type="text" id="managedCfgUdpgwMapTTL" placeholder="90s"/></div>
|
||||
<label style="font-size:.73rem;display:flex;align-items:center;gap:5px;cursor:pointer;grid-column:1/-1"><input type="checkbox" id="managedCfgUdpgwDebug"/> Debug Logging</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-top:12px">
|
||||
<div class="card-hdr">
|
||||
<div class="card-title">TLS Forwarders <span class="chip" id="managedTlsCountChip">0</span></div>
|
||||
<span class="chip green">live</span>
|
||||
<button class="btn btn-ghost btn-sm" type="button" onclick="toggleManagedAddTLSForm()">+ Add</button>
|
||||
</div>
|
||||
<div id="managedTlsForwardersList" style="margin-bottom:4px;"></div>
|
||||
<div id="managedAddTLSPanel" class="hidden" style="border:1px solid var(--border);border-radius:8px;padding:10px;margin-top:6px;">
|
||||
<div class="form-grid">
|
||||
<div class="field" style="grid-column:1/-1"><label>Listen Address</label><input type="text" id="managedTlsListenAddr" placeholder="0.0.0.0:443"/></div>
|
||||
<div class="field" style="grid-column:1/-1">
|
||||
<label>Certificate</label>
|
||||
<select id="managedTlsCertType" onchange="onManagedTLSTypeChange(this.value)">
|
||||
<option value="selfsigned">Generate Self-Signed</option>
|
||||
<option value="letsencrypt">Let's Encrypt (certbot)</option>
|
||||
<option value="paste">Paste PEM text</option>
|
||||
<option value="custom">Custom file paths</option>
|
||||
</select>
|
||||
</div>
|
||||
<div id="managedTlsSSFields" style="grid-column:1/-1"><div class="field"><label>Domain Name <span class="hint">(CN for certificate)</span></label><input type="text" id="managedTlsSSLDomain" placeholder="example.com"/></div></div>
|
||||
<div id="managedTlsLEFields" class="hidden" style="grid-column:1/-1;display:none;grid-template-columns:1fr 1fr;gap:8px;">
|
||||
<div class="field"><label>Domain</label><input type="text" id="managedTlsLEDomain" placeholder="example.com"/></div>
|
||||
<div class="field"><label>Email</label><input type="text" id="managedTlsLEEmail" placeholder="admin@example.com"/></div>
|
||||
</div>
|
||||
<div id="managedTlsCustomFields" class="hidden" style="grid-column:1/-1;display:none;grid-template-columns:1fr 1fr;gap:8px;">
|
||||
<div class="field"><label>Cert File</label><input type="text" id="managedTlsCustomCert" placeholder="/path/to/fullchain.pem"/></div>
|
||||
<div class="field"><label>Key File</label><input type="text" id="managedTlsCustomKey" placeholder="/path/to/privkey.pem"/></div>
|
||||
</div>
|
||||
<div id="managedTlsPasteFields" class="hidden" style="grid-column:1/-1;display:none;">
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;">
|
||||
<div class="field"><label>Certificate PEM</label><textarea id="managedTlsPasteCert" rows="5" placeholder="-----BEGIN CERTIFICATE----- …" style="font-family:monospace;font-size:.7rem;width:100%;box-sizing:border-box;resize:vertical;background:var(--input-bg);border:1px solid var(--border);border-radius:4px;color:inherit;padding:4px;"></textarea></div>
|
||||
<div class="field"><label>Private Key PEM</label><textarea id="managedTlsPasteKey" rows="5" placeholder="-----BEGIN EC PRIVATE KEY----- …" style="font-family:monospace;font-size:.7rem;width:100%;box-sizing:border-box;resize:vertical;background:var(--input-bg);border:1px solid var(--border);border-radius:4px;color:inherit;padding:4px;"></textarea></div>
|
||||
</div>
|
||||
<div class="field" style="margin-top:6px;"><label>Name <span class="hint">(storage folder, e.g. my-cert)</span></label><input type="text" id="managedTlsPasteName" placeholder="my-cert"/></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-actions" style="margin-top:8px;">
|
||||
<button class="btn btn-sm" type="button" onclick="addManagedTLSForwarder()">Add Forwarder</button>
|
||||
<button class="btn btn-ghost btn-sm" type="button" onclick="toggleManagedAddTLSForm()">Cancel</button>
|
||||
</div>
|
||||
<div id="managedTlsAddStatus" class="hint" style="margin-top:4px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-top:12px">
|
||||
<div class="card-hdr"><div class="card-title">Xray Core</div><span class="chip green">live</span></div>
|
||||
<label style="font-size:.73rem;display:flex;align-items:center;gap:5px;cursor:pointer;margin-top:4px;"><input type="checkbox" id="managedCfgXrayEnabled"/> Enabled</label>
|
||||
<div class="hint" style="margin-top:6px;color:var(--muted);">Binary: /opt/sshpanel/xray · Config: /opt/sshpanel/xray_config.json · Online counters use Xray Stats API on 127.0.0.1:10085</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="save-bar">
|
||||
<div class="card-actions save-bar-actions">
|
||||
<button class="btn" id="saveManagedConfigBottomBtn" type="button">Save Config</button>
|
||||
<button class="btn btn-ghost" id="reloadManagedConfigBottomBtn" type="button">Reload</button>
|
||||
</div>
|
||||
<span class="hint">All service changes apply live on the selected node.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div><!-- /tab-servers -->
|
||||
@@ -955,6 +1098,6 @@
|
||||
</div><!-- /shell -->
|
||||
</div><!-- /app -->
|
||||
|
||||
<script defer src="assets/app.js?v=20260511servers1"></script>
|
||||
<script defer src="assets/app.js?v=20260511servers2"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
10
main.go
10
main.go
@@ -1392,13 +1392,13 @@ func startAdminAPI(store *Store, addr string, adminDir string) {
|
||||
mux.Handle("/api/xray/clients/remove", sessionMiddleware(http.HandlerFunc(handleXrayClientRemove)))
|
||||
|
||||
// Superadmin-only: TLS certificate generation
|
||||
mux.Handle("/api/tls/generate-selfsigned", saSession(http.HandlerFunc(handleTLSGenerateSelfSigned)))
|
||||
mux.Handle("/api/tls/letsencrypt", saSession(http.HandlerFunc(handleTLSLetsEncrypt)))
|
||||
mux.Handle("/api/tls/upload-pem", saSession(http.HandlerFunc(handleTLSUploadPEM)))
|
||||
mux.Handle("/api/tls/generate-selfsigned", saSession(handleManagedProxyOrLocal(store, handleTLSGenerateSelfSigned)))
|
||||
mux.Handle("/api/tls/letsencrypt", saSession(handleManagedProxyOrLocal(store, handleTLSLetsEncrypt)))
|
||||
mux.Handle("/api/tls/upload-pem", saSession(handleManagedProxyOrLocal(store, handleTLSUploadPEM)))
|
||||
|
||||
// Superadmin-only: DNSTT key management
|
||||
mux.Handle("/api/dnstt/genkey", saSession(http.HandlerFunc(handleDnsttGenKey)))
|
||||
mux.Handle("/api/dnstt/pubkey", saSession(http.HandlerFunc(handleDnsttGetPubKey)))
|
||||
mux.Handle("/api/dnstt/genkey", saSession(handleManagedProxyOrLocal(store, handleDnsttGenKey)))
|
||||
mux.Handle("/api/dnstt/pubkey", saSession(handleManagedProxyOrLocal(store, handleDnsttGetPubKey)))
|
||||
|
||||
// Superadmin-only: server config (read/write config.json + live banner apply)
|
||||
mux.Handle("/api/server/config", saSession(http.HandlerFunc(handleServerConfig)))
|
||||
|
||||
@@ -296,6 +296,15 @@ func proxyManagedServer(ctx context.Context, ms *ManagedServer, method, path str
|
||||
return resp.StatusCode, data, resp.Header.Get("Content-Type"), nil
|
||||
}
|
||||
|
||||
func handleManagedProxyOrLocal(store *Store, local http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if proxyManagedServerFromRequest(w, r, store, "", nil, "") {
|
||||
return
|
||||
}
|
||||
local(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
func writeProxyResponse(w http.ResponseWriter, status int, body []byte, contentType string) {
|
||||
if contentType != "" {
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
|
||||
Reference in New Issue
Block a user