diff --git a/admin/assets/app.js b/admin/assets/app.js index 88020b4..eae2a53 100644 --- a/admin/assets/app.js +++ b/admin/assets/app.js @@ -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 = '
No TLS forwarders configured.
'; + 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 = `${escapeHTML(fw.listen || "")} + ${escapeHTML(fw.cert_file ? fw.cert_file.split("/").pop() : "no cert")}`; + 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() { diff --git a/admin/index.html b/admin/index.html index 1a74e64..55495e1 100644 --- a/admin/index.html +++ b/admin/index.html @@ -620,7 +620,7 @@ @@ -955,6 +1098,6 @@ - + diff --git a/main.go b/main.go index c531969..97e02a9 100644 --- a/main.go +++ b/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))) diff --git a/managed_servers.go b/managed_servers.go index 15291f6..7a54706 100644 --- a/managed_servers.go +++ b/managed_servers.go @@ -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)