|
|
@@ -377,6 +377,7 @@ function navigate(tabName) {
|
|
|
settings: { title: 'Router Settings & DNS', sub: 'System DNS forwarder, upstream resolvers, static host mappings, and appliance identity' },
|
|
|
users: { title: 'Local User Manager', sub: 'Manage local system administrator credentials and access permissions' },
|
|
|
certificates: { title: 'Certificates & SSL/TLS', sub: 'Root Certificate Authorities (CA), Server/Client certificates and automated Let\'s Encrypt' },
|
|
|
+ reverse_proxy: { title: 'Reverse Proxy & Gateway', sub: 'HTTP/HTTPS Path Routing, Domain Forwarding & SSL Termination' },
|
|
|
services: { title: 'Management Services', sub: 'Enable or disable Web, SSH, and Telnet management listeners and listening ports' },
|
|
|
updates: { title: 'Git Updates', sub: 'Check, install and restore appliance software' },
|
|
|
logs: { title: 'System Activity Logs', sub: 'Real-time circular buffer log viewer and topic-based event monitoring' }
|
|
|
@@ -431,6 +432,7 @@ function navigate(tabName) {
|
|
|
if (tabName === 'settings') loadSettingsData();
|
|
|
if (tabName === 'users') loadUsers();
|
|
|
if (tabName === 'certificates') loadCertificates();
|
|
|
+ if (tabName === 'reverse_proxy') loadReverseProxy();
|
|
|
if (tabName === 'services') loadServices();
|
|
|
if (tabName === 'updates') loadUpdateStatus();
|
|
|
if (tabName === 'logs') loadLogs();
|
|
|
@@ -544,6 +546,7 @@ async function refreshCoreData(isManual = false) {
|
|
|
if (state.activeTab === 'settings') await loadSettingsData(false);
|
|
|
if (state.activeTab === 'users') await loadUsers(false);
|
|
|
if (state.activeTab === 'certificates') await loadCertificates(false);
|
|
|
+ if (state.activeTab === 'reverse_proxy') await loadReverseProxy(false);
|
|
|
if (state.activeTab === 'services') await loadServices(false);
|
|
|
if (state.activeTab === 'updates') await loadUpdateStatus(false);
|
|
|
if (state.activeTab === 'bwtest') await fetchBwTestServerStatus();
|
|
|
@@ -17798,3 +17801,416 @@ function setFirewallActionSelection(kind, value) {
|
|
|
}
|
|
|
select.value=value;
|
|
|
}
|
|
|
+
|
|
|
+// ==========================================
|
|
|
+// REVERSE PROXY & APPLICATION GATEWAY
|
|
|
+// ==========================================
|
|
|
+state.proxyRules = [];
|
|
|
+state.proxyConfig = null;
|
|
|
+
|
|
|
+async function loadReverseProxy(manual = false) {
|
|
|
+ try {
|
|
|
+ const [cfgRes, rulesRes, certsRes] = await Promise.all([
|
|
|
+ fetch(`${API_BASE}/proxy/config`),
|
|
|
+ fetch(`${API_BASE}/proxy/rules`),
|
|
|
+ fetch(`${API_BASE}/certificates`).catch(() => null)
|
|
|
+ ]);
|
|
|
+
|
|
|
+ if (!cfgRes.ok) throw new Error('Failed to load proxy config');
|
|
|
+ if (!rulesRes.ok) throw new Error('Failed to load proxy rules');
|
|
|
+
|
|
|
+ state.proxyConfig = await cfgRes.json();
|
|
|
+ state.proxyRules = await rulesRes.json() || [];
|
|
|
+
|
|
|
+ if (certsRes && certsRes.ok) {
|
|
|
+ state.certificates = await certsRes.json() || [];
|
|
|
+ }
|
|
|
+
|
|
|
+ renderProxyRules(state.proxyRules);
|
|
|
+ updateProxyMetrics();
|
|
|
+
|
|
|
+ if (manual) showToast('Reverse Proxy', 'Proxy configuration and rules reloaded', 'success');
|
|
|
+ } catch (err) {
|
|
|
+ console.error('loadReverseProxy error:', err);
|
|
|
+ if (manual) showToast('Error', err.message, 'error');
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function updateProxyMetrics() {
|
|
|
+ const badge = document.getElementById('badge-proxy-count');
|
|
|
+ if (badge) badge.textContent = (state.proxyRules || []).length;
|
|
|
+
|
|
|
+ const metricRules = document.getElementById('proxy-metric-rules');
|
|
|
+ if (metricRules) metricRules.textContent = (state.proxyRules || []).length;
|
|
|
+
|
|
|
+ const cfg = state.proxyConfig;
|
|
|
+ const statusEl = document.getElementById('proxy-metric-status');
|
|
|
+ const portsEl = document.getElementById('proxy-metric-ports');
|
|
|
+
|
|
|
+ if (cfg) {
|
|
|
+ if (statusEl) {
|
|
|
+ if (cfg.enabled) {
|
|
|
+ statusEl.innerHTML = '<span class="w-2.5 h-2.5 rounded-full bg-emerald-500 animate-pulse"></span><span class="text-white">Active</span>';
|
|
|
+ } else {
|
|
|
+ statusEl.innerHTML = '<span class="w-2.5 h-2.5 rounded-full bg-rose-500"></span><span class="text-rose-400">Disabled</span>';
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (portsEl) {
|
|
|
+ portsEl.textContent = `HTTP: ${cfg.http_port || 80} | HTTPS: ${cfg.https_port || 443}`;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ let totalRequests = 0;
|
|
|
+ const certSet = new Set();
|
|
|
+ (state.proxyRules || []).forEach(r => {
|
|
|
+ totalRequests += (r.stats && r.stats.requests) || 0;
|
|
|
+ if (r.enabled && r.certificate) certSet.add(r.certificate.toLowerCase());
|
|
|
+ });
|
|
|
+
|
|
|
+ const metricRequests = document.getElementById('proxy-metric-requests');
|
|
|
+ if (metricRequests) metricRequests.textContent = totalRequests.toLocaleString();
|
|
|
+
|
|
|
+ const metricCerts = document.getElementById('proxy-metric-certs');
|
|
|
+ if (metricCerts) metricCerts.textContent = certSet.size;
|
|
|
+}
|
|
|
+
|
|
|
+function renderProxyRules(rules) {
|
|
|
+ const tbody = document.getElementById('proxy-rules-table-body');
|
|
|
+ if (!tbody) return;
|
|
|
+
|
|
|
+ if (!rules || rules.length === 0) {
|
|
|
+ tbody.innerHTML = '<tr><td colspan="9" class="py-8 text-center text-slate-500 text-xs font-sans">No reverse proxy rules configured yet. Click "Add Proxy Rule" to begin.</td></tr>';
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ tbody.innerHTML = rules.map(r => {
|
|
|
+ const isEnabled = r.enabled !== false;
|
|
|
+ const domainsStr = (r.domains && r.domains.length > 0) ? r.domains.map(d => `<span class="px-1.5 py-0.5 rounded bg-slate-800 text-slate-200 border border-slate-700 font-mono text-[11px]">${escapeHtml(d)}</span>`).join(' ') : '<span class="px-1.5 py-0.5 rounded bg-violet-950/60 text-violet-300 border border-violet-800/40 font-mono text-[11px]">* (any)</span>';
|
|
|
+
|
|
|
+ const pathBadge = `<span class="px-2 py-0.5 rounded bg-cyan-950/60 text-cyan-300 border border-cyan-800/40 font-mono text-[11px] font-bold">${escapeHtml(r.path || '/')}</span>`;
|
|
|
+
|
|
|
+ let protoBadge = '<span class="px-1.5 py-0.5 rounded bg-slate-800 text-slate-300 text-[10px] font-mono">HTTP/S</span>';
|
|
|
+ if (r.protocol === 'https') protoBadge = '<span class="px-1.5 py-0.5 rounded bg-emerald-950 text-emerald-400 border border-emerald-800/40 text-[10px] font-mono font-bold">HTTPS</span>';
|
|
|
+ else if (r.protocol === 'http') protoBadge = '<span class="px-1.5 py-0.5 rounded bg-sky-950 text-sky-400 border border-sky-800/40 text-[10px] font-mono">HTTP</span>';
|
|
|
+
|
|
|
+ const certBadge = r.certificate ? `<span class="ml-1 text-[10px] bg-amber-950 text-amber-300 border border-amber-800/40 px-1 py-0.5 rounded font-mono truncate max-w-[120px] inline-block align-bottom" title="${escapeHtml(r.certificate)}">🔒 ${escapeHtml(r.certificate)}</span>` : '';
|
|
|
+
|
|
|
+ const stripBadge = r.strip_path ? '<span class="text-amber-400 text-[11px]">Yes</span>' : '<span class="text-slate-500 text-[11px]">No</span>';
|
|
|
+
|
|
|
+ const requests = (r.stats && r.stats.requests) ? r.stats.requests.toLocaleString() : '0';
|
|
|
+
|
|
|
+ return `
|
|
|
+ <tr class="hover:bg-slate-800/40 transition">
|
|
|
+ <td class="py-3 px-4">
|
|
|
+ <button onclick="toggleProxyRuleState('${escapeHtml(r.id)}', ${isEnabled})" class="relative inline-flex h-5 w-9 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none ${isEnabled ? 'bg-emerald-500' : 'bg-slate-700'}" title="${isEnabled ? 'Click to disable' : 'Click to enable'}">
|
|
|
+ <span class="inline-block h-4 w-4 transform rounded-full bg-white shadow transition duration-200 ease-in-out ${isEnabled ? 'translate-x-4' : 'translate-x-0'}"></span>
|
|
|
+ </button>
|
|
|
+ </td>
|
|
|
+ <td class="py-3 px-4">
|
|
|
+ <div class="font-bold text-white text-xs">${escapeHtml(r.name || 'Unnamed Rule')}</div>
|
|
|
+ ${r.comment ? `<div class="text-[10px] text-slate-400 font-sans truncate max-w-[200px]">${escapeHtml(r.comment)}</div>` : ''}
|
|
|
+ </td>
|
|
|
+ <td class="py-3 px-4 flex flex-wrap gap-1 items-center max-w-xs">
|
|
|
+ ${domainsStr}
|
|
|
+ </td>
|
|
|
+ <td class="py-3 px-4">
|
|
|
+ ${pathBadge}
|
|
|
+ </td>
|
|
|
+ <td class="py-3 px-4 font-mono text-slate-300 max-w-xs truncate" title="${escapeHtml(r.target_url || '')}">
|
|
|
+ <span class="text-emerald-400">→</span> ${escapeHtml(r.target_url || '')}
|
|
|
+ </td>
|
|
|
+ <td class="py-3 px-4 whitespace-nowrap">
|
|
|
+ ${protoBadge}${certBadge}
|
|
|
+ </td>
|
|
|
+ <td class="py-3 px-4 font-mono">
|
|
|
+ ${stripBadge}
|
|
|
+ </td>
|
|
|
+ <td class="py-3 px-4 font-mono text-slate-300">
|
|
|
+ ${requests}
|
|
|
+ </td>
|
|
|
+ <td class="py-3 px-4 text-right whitespace-nowrap">
|
|
|
+ <button onclick="openEditProxyRuleModal('${escapeHtml(r.id)}')" class="px-2.5 py-1 rounded-lg bg-slate-800 hover:bg-slate-700 text-slate-300 text-xs mr-1 transition font-sans">Edit</button>
|
|
|
+ <button onclick="deleteProxyRule('${escapeHtml(r.id)}')" class="px-2.5 py-1 rounded-lg bg-rose-950/40 hover:bg-rose-900/60 text-rose-300 border border-rose-800/40 text-xs transition font-sans">Delete</button>
|
|
|
+ </td>
|
|
|
+ </tr>
|
|
|
+ `;
|
|
|
+ }).join('');
|
|
|
+}
|
|
|
+
|
|
|
+function populateProxyCertDropdown(selectedVal = '') {
|
|
|
+ const select = document.getElementById('proxy-rule-cert');
|
|
|
+ if (!select) return;
|
|
|
+
|
|
|
+ const certs = state.certificates || [];
|
|
|
+ select.innerHTML = '<option value="">(None / Use Default Cert)</option>';
|
|
|
+
|
|
|
+ certs.filter(c => !c.is_ca).forEach(c => {
|
|
|
+ const opt = document.createElement('option');
|
|
|
+ opt.value = c.name;
|
|
|
+ const cn = c.common_name ? ` (${c.common_name})` : '';
|
|
|
+ opt.textContent = `${c.name}${cn}`;
|
|
|
+ if (selectedVal && selectedVal.toLowerCase() === c.name.toLowerCase()) {
|
|
|
+ opt.selected = true;
|
|
|
+ }
|
|
|
+ select.appendChild(opt);
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+function populateDefaultProxyCertDropdown(selectedVal = '') {
|
|
|
+ const select = document.getElementById('proxy-cfg-default-cert');
|
|
|
+ if (!select) return;
|
|
|
+
|
|
|
+ const certs = state.certificates || [];
|
|
|
+ select.innerHTML = '<option value="">(None / First Available Cert)</option>';
|
|
|
+
|
|
|
+ certs.filter(c => !c.is_ca).forEach(c => {
|
|
|
+ const opt = document.createElement('option');
|
|
|
+ opt.value = c.name;
|
|
|
+ const cn = c.common_name ? ` (${c.common_name})` : '';
|
|
|
+ opt.textContent = `${c.name}${cn}`;
|
|
|
+ if (selectedVal && selectedVal.toLowerCase() === c.name.toLowerCase()) {
|
|
|
+ opt.selected = true;
|
|
|
+ }
|
|
|
+ select.appendChild(opt);
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+function onProxyProtoChange() {
|
|
|
+ const proto = document.getElementById('proxy-rule-proto')?.value;
|
|
|
+ const certSelect = document.getElementById('proxy-rule-cert');
|
|
|
+ const redirectCheck = document.getElementById('proxy-rule-http-to-https');
|
|
|
+
|
|
|
+ if (proto === 'http') {
|
|
|
+ if (certSelect) certSelect.disabled = true;
|
|
|
+ if (redirectCheck) redirectCheck.disabled = true;
|
|
|
+ } else {
|
|
|
+ if (certSelect) certSelect.disabled = false;
|
|
|
+ if (redirectCheck) redirectCheck.disabled = false;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function openAddProxyRuleModal() {
|
|
|
+ const modal = document.getElementById('modal-proxy-rule');
|
|
|
+ if (!modal) return;
|
|
|
+
|
|
|
+ document.getElementById('proxy-rule-modal-title').innerHTML = `
|
|
|
+ <svg class="w-4 h-4 text-violet-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4" /></svg>
|
|
|
+ <span>Add Reverse Proxy Rule</span>
|
|
|
+ `;
|
|
|
+ document.getElementById('proxy-rule-id').value = '';
|
|
|
+ document.getElementById('proxy-rule-name').value = '';
|
|
|
+ document.getElementById('proxy-rule-domains').value = '';
|
|
|
+ document.getElementById('proxy-rule-path').value = '/';
|
|
|
+ document.getElementById('proxy-rule-target').value = '';
|
|
|
+ document.getElementById('proxy-rule-proto').value = 'both';
|
|
|
+ document.getElementById('proxy-rule-strip-path').checked = false;
|
|
|
+ document.getElementById('proxy-rule-http-to-https').checked = false;
|
|
|
+ document.getElementById('proxy-rule-preserve-host').checked = false;
|
|
|
+ document.getElementById('proxy-rule-insecure-tls').checked = false;
|
|
|
+ document.getElementById('proxy-rule-enabled').checked = true;
|
|
|
+ document.getElementById('proxy-rule-comment').value = '';
|
|
|
+
|
|
|
+ populateProxyCertDropdown('');
|
|
|
+ onProxyProtoChange();
|
|
|
+
|
|
|
+ modal.classList.remove('hidden');
|
|
|
+}
|
|
|
+
|
|
|
+function openEditProxyRuleModal(id) {
|
|
|
+ const modal = document.getElementById('modal-proxy-rule');
|
|
|
+ if (!modal) return;
|
|
|
+
|
|
|
+ const rule = (state.proxyRules || []).find(r => r.id === id);
|
|
|
+ if (!rule) {
|
|
|
+ showToast('Error', 'Rule not found', 'error');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ document.getElementById('proxy-rule-modal-title').innerHTML = `
|
|
|
+ <svg class="w-4 h-4 text-violet-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" /></svg>
|
|
|
+ <span>Edit Reverse Proxy Rule</span>
|
|
|
+ `;
|
|
|
+ document.getElementById('proxy-rule-id').value = rule.id || '';
|
|
|
+ document.getElementById('proxy-rule-name').value = rule.name || '';
|
|
|
+ document.getElementById('proxy-rule-domains').value = (rule.domains || []).join(', ');
|
|
|
+ document.getElementById('proxy-rule-path').value = rule.path || '/';
|
|
|
+ document.getElementById('proxy-rule-target').value = rule.target_url || '';
|
|
|
+ document.getElementById('proxy-rule-proto').value = rule.protocol || 'both';
|
|
|
+ document.getElementById('proxy-rule-strip-path').checked = !!rule.strip_path;
|
|
|
+ document.getElementById('proxy-rule-http-to-https').checked = !!rule.http_to_https;
|
|
|
+ document.getElementById('proxy-rule-preserve-host').checked = !!rule.preserve_host;
|
|
|
+ document.getElementById('proxy-rule-insecure-tls').checked = !!rule.insecure_tls;
|
|
|
+ document.getElementById('proxy-rule-enabled').checked = rule.enabled !== false;
|
|
|
+ document.getElementById('proxy-rule-comment').value = rule.comment || '';
|
|
|
+
|
|
|
+ populateProxyCertDropdown(rule.certificate || '');
|
|
|
+ onProxyProtoChange();
|
|
|
+
|
|
|
+ modal.classList.remove('hidden');
|
|
|
+}
|
|
|
+
|
|
|
+function closeProxyRuleModal() {
|
|
|
+ const modal = document.getElementById('modal-proxy-rule');
|
|
|
+ if (modal) modal.classList.add('hidden');
|
|
|
+}
|
|
|
+
|
|
|
+async function saveProxyRule(event) {
|
|
|
+ event.preventDefault();
|
|
|
+
|
|
|
+ const id = document.getElementById('proxy-rule-id').value;
|
|
|
+ const name = document.getElementById('proxy-rule-name').value.trim();
|
|
|
+ const rawDomains = document.getElementById('proxy-rule-domains').value.trim();
|
|
|
+ const pathVal = document.getElementById('proxy-rule-path').value.trim() || '/';
|
|
|
+ const target = document.getElementById('proxy-rule-target').value.trim();
|
|
|
+ const proto = document.getElementById('proxy-rule-proto').value;
|
|
|
+ const cert = document.getElementById('proxy-rule-cert').value;
|
|
|
+ const stripPath = document.getElementById('proxy-rule-strip-path').checked;
|
|
|
+ const httpToHttps = document.getElementById('proxy-rule-http-to-https').checked;
|
|
|
+ const preserveHost = document.getElementById('proxy-rule-preserve-host').checked;
|
|
|
+ const insecureTLS = document.getElementById('proxy-rule-insecure-tls').checked;
|
|
|
+ const enabled = document.getElementById('proxy-rule-enabled').checked;
|
|
|
+ const comment = document.getElementById('proxy-rule-comment').value.trim();
|
|
|
+
|
|
|
+ let domains = [];
|
|
|
+ if (rawDomains && rawDomains !== '*') {
|
|
|
+ domains = rawDomains.split(',').map(d => d.trim()).filter(d => d.length > 0);
|
|
|
+ }
|
|
|
+
|
|
|
+ const payload = {
|
|
|
+ id: id || undefined,
|
|
|
+ name,
|
|
|
+ domains,
|
|
|
+ path: pathVal,
|
|
|
+ target_url: target,
|
|
|
+ protocol: proto,
|
|
|
+ certificate: cert,
|
|
|
+ strip_path: stripPath,
|
|
|
+ http_to_https: httpToHttps,
|
|
|
+ preserve_host: preserveHost,
|
|
|
+ insecure_tls: insecureTLS,
|
|
|
+ enabled,
|
|
|
+ comment
|
|
|
+ };
|
|
|
+
|
|
|
+ try {
|
|
|
+ const url = id ? `${API_BASE}/proxy/rules/${encodeURIComponent(id)}` : `${API_BASE}/proxy/rules`;
|
|
|
+ const method = id ? 'PUT' : 'POST';
|
|
|
+
|
|
|
+ const res = await fetch(url, {
|
|
|
+ method,
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
+ body: JSON.stringify(payload)
|
|
|
+ });
|
|
|
+
|
|
|
+ if (!res.ok) {
|
|
|
+ const errData = await res.json().catch(() => ({}));
|
|
|
+ throw new Error(errData.message || 'Failed to save proxy rule');
|
|
|
+ }
|
|
|
+
|
|
|
+ closeProxyRuleModal();
|
|
|
+ showToast('Success', `Proxy rule "${name}" saved successfully`, 'success');
|
|
|
+ await loadReverseProxy(false);
|
|
|
+ } catch (err) {
|
|
|
+ showToast('Error', err.message, 'error');
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function deleteProxyRule(id) {
|
|
|
+ const rule = (state.proxyRules || []).find(r => r.id === id);
|
|
|
+ const name = rule ? rule.name : id;
|
|
|
+
|
|
|
+ if (!confirm(`Are you sure you want to delete proxy rule "${name}"?`)) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ const res = await fetch(`${API_BASE}/proxy/rules/${encodeURIComponent(id)}`, {
|
|
|
+ method: 'DELETE'
|
|
|
+ });
|
|
|
+
|
|
|
+ if (!res.ok) {
|
|
|
+ const errData = await res.json().catch(() => ({}));
|
|
|
+ throw new Error(errData.message || 'Failed to delete rule');
|
|
|
+ }
|
|
|
+
|
|
|
+ showToast('Rule Deleted', `Proxy rule "${name}" removed`, 'success');
|
|
|
+ await loadReverseProxy(false);
|
|
|
+ } catch (err) {
|
|
|
+ showToast('Error', err.message, 'error');
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function toggleProxyRuleState(id, currentState) {
|
|
|
+ try {
|
|
|
+ const res = await fetch(`${API_BASE}/proxy/rules/${encodeURIComponent(id)}/state`, {
|
|
|
+ method: 'POST',
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
+ body: JSON.stringify({ enabled: !currentState })
|
|
|
+ });
|
|
|
+
|
|
|
+ if (!res.ok) {
|
|
|
+ const errData = await res.json().catch(() => ({}));
|
|
|
+ throw new Error(errData.message || 'Failed to toggle rule state');
|
|
|
+ }
|
|
|
+
|
|
|
+ await loadReverseProxy(false);
|
|
|
+ } catch (err) {
|
|
|
+ showToast('Error', err.message, 'error');
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function openProxyConfigModal() {
|
|
|
+ const modal = document.getElementById('modal-proxy-config');
|
|
|
+ if (!modal) return;
|
|
|
+
|
|
|
+ const cfg = state.proxyConfig || { enabled: true, http_port: 80, https_port: 443, default_cert: '' };
|
|
|
+
|
|
|
+ document.getElementById('proxy-cfg-enabled').checked = cfg.enabled !== false;
|
|
|
+ document.getElementById('proxy-cfg-http-port').value = cfg.http_port || 80;
|
|
|
+ document.getElementById('proxy-cfg-https-port').value = cfg.https_port || 443;
|
|
|
+
|
|
|
+ populateDefaultProxyCertDropdown(cfg.default_cert || '');
|
|
|
+
|
|
|
+ modal.classList.remove('hidden');
|
|
|
+}
|
|
|
+
|
|
|
+function closeProxyConfigModal() {
|
|
|
+ const modal = document.getElementById('modal-proxy-config');
|
|
|
+ if (modal) modal.classList.add('hidden');
|
|
|
+}
|
|
|
+
|
|
|
+async function saveProxyConfig(event) {
|
|
|
+ event.preventDefault();
|
|
|
+
|
|
|
+ const enabled = document.getElementById('proxy-cfg-enabled').checked;
|
|
|
+ const httpPort = parseInt(document.getElementById('proxy-cfg-http-port').value, 10) || 80;
|
|
|
+ const httpsPort = parseInt(document.getElementById('proxy-cfg-https-port').value, 10) || 443;
|
|
|
+ const defaultCert = document.getElementById('proxy-cfg-default-cert').value;
|
|
|
+
|
|
|
+ const payload = {
|
|
|
+ enabled,
|
|
|
+ http_port: httpPort,
|
|
|
+ https_port: httpsPort,
|
|
|
+ default_cert: defaultCert
|
|
|
+ };
|
|
|
+
|
|
|
+ try {
|
|
|
+ const res = await fetch(`${API_BASE}/proxy/config`, {
|
|
|
+ method: 'POST',
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
+ body: JSON.stringify(payload)
|
|
|
+ });
|
|
|
+
|
|
|
+ if (!res.ok) {
|
|
|
+ const errData = await res.json().catch(() => ({}));
|
|
|
+ throw new Error(errData.message || 'Failed to update proxy settings');
|
|
|
+ }
|
|
|
+
|
|
|
+ closeProxyConfigModal();
|
|
|
+ showToast('Success', 'Reverse proxy settings updated', 'success');
|
|
|
+ await loadReverseProxy(false);
|
|
|
+ } catch (err) {
|
|
|
+ showToast('Error', err.message, 'error');
|
|
|
+ }
|
|
|
+}
|
|
|
+
|