|
@@ -8,6 +8,15 @@ let state = {
|
|
|
activeTab: 'dashboard',
|
|
activeTab: 'dashboard',
|
|
|
fwSubTab: 'filter',
|
|
fwSubTab: 'filter',
|
|
|
routesSubTab: 'routes',
|
|
routesSubTab: 'routes',
|
|
|
|
|
+ pppSubTab: 'secrets',
|
|
|
|
|
+ eoipTunnels: [],
|
|
|
|
|
+ pppSecrets: [],
|
|
|
|
|
+ pppProfiles: [],
|
|
|
|
|
+ pppActiveSessions: [],
|
|
|
|
|
+ pppoeServer: null,
|
|
|
|
|
+ pptpServer: null,
|
|
|
|
|
+ l2tpServer: null,
|
|
|
|
|
+ ipsecServer: null,
|
|
|
ifaceFilter: 'all',
|
|
ifaceFilter: 'all',
|
|
|
currentModalIface: null,
|
|
currentModalIface: null,
|
|
|
isPolling: true,
|
|
isPolling: true,
|
|
@@ -65,6 +74,8 @@ function navigate(tabName) {
|
|
|
vlans: { title: 'VLAN Virtual Networks', sub: 'IEEE 802.1Q sub-interface segmentation and routing' },
|
|
vlans: { title: 'VLAN Virtual Networks', sub: 'IEEE 802.1Q sub-interface segmentation and routing' },
|
|
|
firewall: { title: 'MikroTik Firewall', sub: 'Filter rules, NAT, Mangle, Address Lists & Active Connections tracking' },
|
|
firewall: { title: 'MikroTik Firewall', sub: 'Filter rules, NAT, Mangle, Address Lists & Active Connections tracking' },
|
|
|
routes: { title: 'Routing & Policy', sub: 'Routing tables, static routes (DAS/DAC), and policy routing rules' },
|
|
routes: { title: 'Routing & Policy', sub: 'Routing tables, static routes (DAS/DAC), and policy routing rules' },
|
|
|
|
|
+ ppp: { title: 'PPP Servers & Secrets', sub: 'MikroTik RouterOS user accounts, IP pools and VPN server concentrators' },
|
|
|
|
|
+ eoip: { title: 'EoIP Tunnels', sub: 'Ethernet over IP Layer 2 tunneling (MikroTik RouterOS style)' },
|
|
|
pppoe: { title: 'PPPoE Client WAN', sub: 'Point-to-Point Protocol over Ethernet WAN access' },
|
|
pppoe: { title: 'PPPoE Client WAN', sub: 'Point-to-Point Protocol over Ethernet WAN access' },
|
|
|
wireguard: { title: 'WireGuard Kernel VPN', sub: 'Modern ultra-fast cryptographic VPN tunnel manager' },
|
|
wireguard: { title: 'WireGuard Kernel VPN', sub: 'Modern ultra-fast cryptographic VPN tunnel manager' },
|
|
|
ipsec: { title: 'IPSec (IKEv2) strongSwan', sub: 'Enterprise Site-to-Site & Roadwarrior IPSec controller' },
|
|
ipsec: { title: 'IPSec (IKEv2) strongSwan', sub: 'Enterprise Site-to-Site & Roadwarrior IPSec controller' },
|
|
@@ -90,6 +101,8 @@ function navigate(tabName) {
|
|
|
|
|
|
|
|
if (tabName === 'firewall') loadFirewall();
|
|
if (tabName === 'firewall') loadFirewall();
|
|
|
if (tabName === 'routes') loadRoutes();
|
|
if (tabName === 'routes') loadRoutes();
|
|
|
|
|
+ if (tabName === 'ppp') loadPPP();
|
|
|
|
|
+ if (tabName === 'eoip') loadEoIP();
|
|
|
if (tabName === 'persistence') loadDebianConfig();
|
|
if (tabName === 'persistence') loadDebianConfig();
|
|
|
if (tabName === 'pppoe') loadPPPoE();
|
|
if (tabName === 'pppoe') loadPPPoE();
|
|
|
if (tabName === 'wireguard') loadWireGuard();
|
|
if (tabName === 'wireguard') loadWireGuard();
|
|
@@ -132,6 +145,8 @@ async function fetchData(isManual = false) {
|
|
|
if (state.activeTab === 'vlans') renderVlansList();
|
|
if (state.activeTab === 'vlans') renderVlansList();
|
|
|
if (state.activeTab === 'firewall') loadFirewall(false);
|
|
if (state.activeTab === 'firewall') loadFirewall(false);
|
|
|
if (state.activeTab === 'routes') loadRoutes(false);
|
|
if (state.activeTab === 'routes') loadRoutes(false);
|
|
|
|
|
+ if (state.activeTab === 'ppp') loadPPP(false);
|
|
|
|
|
+ if (state.activeTab === 'eoip') loadEoIP(false);
|
|
|
if (state.activeTab === 'pppoe') loadPPPoE(false);
|
|
if (state.activeTab === 'pppoe') loadPPPoE(false);
|
|
|
if (state.activeTab === 'wireguard') loadWireGuard(false);
|
|
if (state.activeTab === 'wireguard') loadWireGuard(false);
|
|
|
if (state.activeTab === 'ipsec') loadIPSec(false);
|
|
if (state.activeTab === 'ipsec') loadIPSec(false);
|
|
@@ -223,13 +238,27 @@ function setDotBadge(id, active) {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function updateBadges() {
|
|
function updateBadges() {
|
|
|
- document.getElementById('badge-ifaces-count').innerText = (state.interfaces || []).length;
|
|
|
|
|
- document.getElementById('badge-vlans-count').innerText = (state.vlans || []).length;
|
|
|
|
|
|
|
+ const bIfaces = document.getElementById('badge-ifaces-count');
|
|
|
|
|
+ if (bIfaces) bIfaces.innerText = (state.interfaces || []).length;
|
|
|
|
|
+ const bVlans = document.getElementById('badge-vlans-count');
|
|
|
|
|
+ if (bVlans) bVlans.innerText = (state.vlans || []).length;
|
|
|
if (state.fwSummary) {
|
|
if (state.fwSummary) {
|
|
|
const totalFwRules = (state.fwSummary.filter_rules_count || 0) + (state.fwSummary.nat_rules_count || 0) + (state.fwSummary.mangle_rules_count || 0);
|
|
const totalFwRules = (state.fwSummary.filter_rules_count || 0) + (state.fwSummary.nat_rules_count || 0) + (state.fwSummary.mangle_rules_count || 0);
|
|
|
const badgeFw = document.getElementById('badge-firewall-count');
|
|
const badgeFw = document.getElementById('badge-firewall-count');
|
|
|
if (badgeFw) badgeFw.innerText = totalFwRules;
|
|
if (badgeFw) badgeFw.innerText = totalFwRules;
|
|
|
}
|
|
}
|
|
|
|
|
+ if (state.vpnStatus) {
|
|
|
|
|
+ const v = state.vpnStatus;
|
|
|
|
|
+ const bEoIP = document.getElementById('badge-eoip-count');
|
|
|
|
|
+ if (bEoIP) bEoIP.innerText = v.total_eoip || 0;
|
|
|
|
|
+ const bPPPSess = document.getElementById('badge-ppp-sessions');
|
|
|
|
|
+ if (bPPPSess) bPPPSess.innerText = v.total_ppp_sessions || 0;
|
|
|
|
|
+ setDotBadge('badge-pppoe-status', v.active_pppoe > 0 || v.pppoe_server_running);
|
|
|
|
|
+ setDotBadge('badge-wg-status', v.active_wireguard > 0);
|
|
|
|
|
+ setDotBadge('badge-ipsec-status', v.active_ipsec > 0 || v.ipsec_server_running);
|
|
|
|
|
+ setDotBadge('badge-l2tp-status', v.active_l2tp > 0 || v.l2tp_server_running);
|
|
|
|
|
+ setDotBadge('badge-pptp-status', v.active_pptp > 0 || v.pptp_server_running);
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// Render Dashboard View
|
|
// Render Dashboard View
|
|
@@ -1021,7 +1050,8 @@ function renderWGPeers() {
|
|
|
<span class="text-emerald-400">↓ ${p.transfer_rx_human || '0 B'}</span>
|
|
<span class="text-emerald-400">↓ ${p.transfer_rx_human || '0 B'}</span>
|
|
|
<span class="text-cyan-400 ml-1">↑ ${p.transfer_tx_human || '0 B'}</span>
|
|
<span class="text-cyan-400 ml-1">↑ ${p.transfer_tx_human || '0 B'}</span>
|
|
|
</td>
|
|
</td>
|
|
|
- <td class="py-2.5 px-3 text-right font-sans">
|
|
|
|
|
|
|
+ <td class="py-2.5 px-3 text-right font-sans space-x-2">
|
|
|
|
|
+ <button onclick="openWGClientConfModal('${p.interface}', '${(p.allowed_ips || [])[0] || '10.10.0.2/32'}')" class="text-xs text-cyan-400 hover:text-cyan-300 font-semibold" title="Generate WireGuard Client Profile (.conf)">Config (.conf)</button>
|
|
|
<button onclick="deleteWGPeer('${p.interface}', '${p.public_key}')" class="text-xs text-rose-400 hover:text-rose-300">Delete</button>
|
|
<button onclick="deleteWGPeer('${p.interface}', '${p.public_key}')" class="text-xs text-rose-400 hover:text-rose-300">Delete</button>
|
|
|
</td>
|
|
</td>
|
|
|
`;
|
|
`;
|
|
@@ -3414,3 +3444,838 @@ function toggleSidebar() {
|
|
|
const sb = document.getElementById('sidebar');
|
|
const sb = document.getElementById('sidebar');
|
|
|
sb.classList.toggle('-translate-x-full');
|
|
sb.classList.toggle('-translate-x-full');
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+// ======================== EoIP (Ethernet over IP) ========================
|
|
|
|
|
+async function loadEoIP(populate = true) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await fetch(`${API_BASE}/vpn/eoip`);
|
|
|
|
|
+ const data = await res.json();
|
|
|
|
|
+ state.eoipTunnels = Array.isArray(data) ? data : [];
|
|
|
|
|
+
|
|
|
|
|
+ const badge = document.getElementById('eoip-count-badge');
|
|
|
|
|
+ if (badge) badge.innerText = `${state.eoipTunnels.length} Tunnels`;
|
|
|
|
|
+ const sideBadge = document.getElementById('badge-eoip-count');
|
|
|
|
|
+ if (sideBadge) sideBadge.innerText = state.eoipTunnels.length;
|
|
|
|
|
+
|
|
|
|
|
+ renderEoIPTunnels();
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ console.error('Failed to load EoIP:', err);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function renderEoIPTunnels() {
|
|
|
|
|
+ const tbody = document.getElementById('eoip-table-body');
|
|
|
|
|
+ const empty = document.getElementById('eoip-empty');
|
|
|
|
|
+ if (!tbody) return;
|
|
|
|
|
+ tbody.innerHTML = '';
|
|
|
|
|
+
|
|
|
|
|
+ const tunnels = state.eoipTunnels || [];
|
|
|
|
|
+ if (tunnels.length === 0) {
|
|
|
|
|
+ empty?.classList.remove('hidden');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ empty?.classList.add('hidden');
|
|
|
|
|
+
|
|
|
|
|
+ tunnels.forEach(t => {
|
|
|
|
|
+ const tr = document.createElement('tr');
|
|
|
|
|
+ tr.className = `hover:bg-slate-800/40 transition text-xs ${t.disabled ? 'opacity-50' : ''}`;
|
|
|
|
|
+ tr.innerHTML = `
|
|
|
|
|
+ <td class="py-2.5 px-3 text-center">${renderMikroTikFlag(t.running, t.disabled)}</td>
|
|
|
|
|
+ <td class="py-2.5 px-3 font-bold text-white font-mono">${t.name}</td>
|
|
|
|
|
+ <td class="py-2.5 px-3 font-bold text-emerald-400 font-mono">${t.tunnel_id}</td>
|
|
|
|
|
+ <td class="py-2.5 px-3 text-cyan-300 font-mono">${t.remote_address}</td>
|
|
|
|
|
+ <td class="py-2.5 px-3 text-slate-400 font-mono">${t.local_address || '(any)'}</td>
|
|
|
|
|
+ <td class="py-2.5 px-3 text-slate-300 font-mono text-[11px]">${t.mac_address || '-'}</td>
|
|
|
|
|
+ <td class="py-2.5 px-3 text-slate-400 font-mono">${t.mtu || 1500}</td>
|
|
|
|
|
+ <td class="py-2.5 px-3 font-mono text-[11px]">
|
|
|
|
|
+ <span class="text-emerald-400">↓ ${t.rx_human || '0 B'}</span>
|
|
|
|
|
+ <span class="text-cyan-400 ml-1">↑ ${t.tx_human || '0 B'}</span>
|
|
|
|
|
+ </td>
|
|
|
|
|
+ <td class="py-2.5 px-3 text-slate-400 italic truncate max-w-[120px]" title="${t.comment || ''}">${t.comment || '-'}</td>
|
|
|
|
|
+ <td class="py-2.5 px-3 text-right space-x-1.5 font-sans whitespace-nowrap">
|
|
|
|
|
+ <button onclick="toggleEoIPState('${t.id}', ${!t.disabled})" class="text-xs ${t.disabled ? 'text-emerald-400' : 'text-slate-400'} hover:underline">${t.disabled ? 'Enable' : 'Disable'}</button>
|
|
|
|
|
+ <button onclick="openEditEoIPModal('${t.id}')" class="text-xs text-slate-400 hover:text-white">Edit</button>
|
|
|
|
|
+ <button onclick="deleteEoIP('${t.id}')" class="text-xs text-rose-400 hover:text-rose-300">Delete</button>
|
|
|
|
|
+ </td>
|
|
|
|
|
+ `;
|
|
|
|
|
+ tbody.appendChild(tr);
|
|
|
|
|
+ });
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function openAddEoIPModal() {
|
|
|
|
|
+ document.getElementById('eoip-modal-title').innerText = 'Add EoIP Tunnel';
|
|
|
|
|
+ document.getElementById('eoip-modal-id').value = '';
|
|
|
|
|
+ document.getElementById('eoip-modal-name').value = `eoip-tunnel${(state.eoipTunnels || []).length + 1}`;
|
|
|
|
|
+ document.getElementById('eoip-modal-tunnelid').value = (state.eoipTunnels || []).length + 10;
|
|
|
|
|
+ document.getElementById('eoip-modal-remote').value = '';
|
|
|
|
|
+ document.getElementById('eoip-modal-local').value = '';
|
|
|
|
|
+ document.getElementById('eoip-modal-mac').value = '';
|
|
|
|
|
+ document.getElementById('eoip-modal-mtu').value = '1500';
|
|
|
|
|
+ document.getElementById('eoip-modal-comment').value = '';
|
|
|
|
|
+ document.getElementById('eoip-modal-clampmss').checked = true;
|
|
|
|
|
+ document.getElementById('eoip-modal-disabled').checked = false;
|
|
|
|
|
+ document.getElementById('eoip-modal').classList.remove('hidden');
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function openEditEoIPModal(id) {
|
|
|
|
|
+ const t = (state.eoipTunnels || []).find(tun => tun.id === id);
|
|
|
|
|
+ if (!t) return;
|
|
|
|
|
+ document.getElementById('eoip-modal-title').innerText = `Edit EoIP Tunnel (${t.name})`;
|
|
|
|
|
+ document.getElementById('eoip-modal-id').value = t.id;
|
|
|
|
|
+ document.getElementById('eoip-modal-name').value = t.name;
|
|
|
|
|
+ document.getElementById('eoip-modal-tunnelid').value = t.tunnel_id;
|
|
|
|
|
+ document.getElementById('eoip-modal-remote').value = t.remote_address;
|
|
|
|
|
+ document.getElementById('eoip-modal-local').value = t.local_address || '';
|
|
|
|
|
+ document.getElementById('eoip-modal-mac').value = t.mac_address || '';
|
|
|
|
|
+ document.getElementById('eoip-modal-mtu').value = t.mtu || 1500;
|
|
|
|
|
+ document.getElementById('eoip-modal-comment').value = t.comment || '';
|
|
|
|
|
+ document.getElementById('eoip-modal-clampmss').checked = !!t.clamp_mss;
|
|
|
|
|
+ document.getElementById('eoip-modal-disabled').checked = !!t.disabled;
|
|
|
|
|
+ document.getElementById('eoip-modal').classList.remove('hidden');
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function closeEoIPModal() {
|
|
|
|
|
+ document.getElementById('eoip-modal').classList.add('hidden');
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function submitSaveEoIP(e) {
|
|
|
|
|
+ e.preventDefault();
|
|
|
|
|
+ const id = document.getElementById('eoip-modal-id').value;
|
|
|
|
|
+ const name = document.getElementById('eoip-modal-name').value.trim();
|
|
|
|
|
+ const tunnelId = parseInt(document.getElementById('eoip-modal-tunnelid').value);
|
|
|
|
|
+ const remote = document.getElementById('eoip-modal-remote').value.trim();
|
|
|
|
|
+ const local = document.getElementById('eoip-modal-local').value.trim();
|
|
|
|
|
+ const mac = document.getElementById('eoip-modal-mac').value.trim();
|
|
|
|
|
+ const mtu = parseInt(document.getElementById('eoip-modal-mtu').value) || 1500;
|
|
|
|
|
+ const comment = document.getElementById('eoip-modal-comment').value.trim();
|
|
|
|
|
+ const clampMss = document.getElementById('eoip-modal-clampmss').checked;
|
|
|
|
|
+ const disabled = document.getElementById('eoip-modal-disabled').checked;
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await fetch(`${API_BASE}/vpn/eoip`, {
|
|
|
|
|
+ method: 'POST',
|
|
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
+ body: JSON.stringify({
|
|
|
|
|
+ id,
|
|
|
|
|
+ name,
|
|
|
|
|
+ tunnel_id: tunnelId,
|
|
|
|
|
+ remote_address: remote,
|
|
|
|
|
+ local_address: local,
|
|
|
|
|
+ mac_address: mac,
|
|
|
|
|
+ mtu,
|
|
|
|
|
+ clamp_mss: clampMss,
|
|
|
|
|
+ comment,
|
|
|
|
|
+ disabled
|
|
|
|
|
+ })
|
|
|
|
|
+ });
|
|
|
|
|
+ if (res.ok) {
|
|
|
|
|
+ showToast('EoIP Saved', `Tunnel ${name} configured successfully`, 'success');
|
|
|
|
|
+ closeEoIPModal();
|
|
|
|
|
+ loadEoIP(false);
|
|
|
|
|
+ fetchData(false);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ const err = await res.json();
|
|
|
|
|
+ showToast('Error', err.message, 'error');
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ showToast('Error', err.message, 'error');
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function toggleEoIPState(id, disabled) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await fetch(`${API_BASE}/vpn/eoip/${encodeURIComponent(id)}/state`, {
|
|
|
|
|
+ method: 'POST',
|
|
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
+ body: JSON.stringify({ disabled })
|
|
|
|
|
+ });
|
|
|
|
|
+ if (res.ok) {
|
|
|
|
|
+ showToast(disabled ? 'Disabled' : 'Enabled', 'EoIP tunnel state updated', 'info');
|
|
|
|
|
+ loadEoIP(false);
|
|
|
|
|
+ fetchData(false);
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ showToast('Error', err.message, 'error');
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function deleteEoIP(id) {
|
|
|
|
|
+ if (!confirm('Are you sure you want to delete this EoIP tunnel?')) return;
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await fetch(`${API_BASE}/vpn/eoip/${encodeURIComponent(id)}`, { method: 'DELETE' });
|
|
|
|
|
+ if (res.ok) {
|
|
|
|
|
+ showToast('Deleted', 'EoIP tunnel removed', 'success');
|
|
|
|
|
+ loadEoIP(false);
|
|
|
|
|
+ fetchData(false);
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ showToast('Error', err.message, 'error');
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ======================== PPP Central Hub & Servers ========================
|
|
|
|
|
+function switchPPPSubTab(tab) {
|
|
|
|
|
+ state.pppSubTab = tab;
|
|
|
|
|
+ const tabs = ['secrets', 'profiles', 'pppoe', 'pptp', 'l2tp', 'active'];
|
|
|
|
|
+ tabs.forEach(t => {
|
|
|
|
|
+ const panel = document.getElementById(`ppp-subtab-${t}`);
|
|
|
|
|
+ const btn = document.getElementById(`ppp-subtab-btn-${t}`);
|
|
|
|
|
+ if (panel) {
|
|
|
|
|
+ if (t === tab) panel.classList.remove('hidden');
|
|
|
|
|
+ else panel.classList.add('hidden');
|
|
|
|
|
+ }
|
|
|
|
|
+ if (btn) {
|
|
|
|
|
+ if (t === tab) {
|
|
|
|
|
+ btn.className = 'px-4 py-2.5 text-xs font-bold border-b-2 border-violet-500 text-white transition flex items-center gap-2 whitespace-nowrap';
|
|
|
|
|
+ } else {
|
|
|
|
|
+ btn.className = 'px-4 py-2.5 text-xs font-bold border-b-2 border-transparent text-slate-400 hover:text-slate-200 transition flex items-center gap-2 whitespace-nowrap';
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ const addBtn = document.getElementById('ppp-primary-add-btn');
|
|
|
|
|
+ if (addBtn) {
|
|
|
|
|
+ if (tab === 'secrets') addBtn.innerHTML = '<span>+ Add User (Secret)</span>';
|
|
|
|
|
+ else if (tab === 'profiles') addBtn.innerHTML = '<span>+ Add Profile</span>';
|
|
|
|
|
+ else if (tab === 'active') addBtn.innerHTML = '<span>Refresh Sessions</span>';
|
|
|
|
|
+ else addBtn.innerHTML = '<span>Save Config</span>';
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (tab === 'active') loadPPPSessions();
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function openCurrentPPPModal() {
|
|
|
|
|
+ if (state.pppSubTab === 'secrets') openAddPPPSecretModal();
|
|
|
|
|
+ else if (state.pppSubTab === 'profiles') openAddPPPProfileModal();
|
|
|
|
|
+ else if (state.pppSubTab === 'active') loadPPPSessions();
|
|
|
|
|
+ else if (state.pppSubTab === 'pppoe') document.querySelector('#ppp-subtab-pppoe form')?.requestSubmit();
|
|
|
|
|
+ else if (state.pppSubTab === 'pptp') document.querySelector('#ppp-subtab-pptp form')?.requestSubmit();
|
|
|
|
|
+ else if (state.pppSubTab === 'l2tp') document.querySelector('#ppp-subtab-l2tp form')?.requestSubmit();
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function loadPPP(populateDropdowns = true) {
|
|
|
|
|
+ await Promise.all([
|
|
|
|
|
+ loadPPPSecrets(),
|
|
|
|
|
+ loadPPPProfiles(),
|
|
|
|
|
+ loadPPPServers(),
|
|
|
|
|
+ loadPPPSessions()
|
|
|
|
|
+ ]);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function loadPPPSecrets() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await fetch(`${API_BASE}/vpn/ppp/secrets`);
|
|
|
|
|
+ const data = await res.json();
|
|
|
|
|
+ state.pppSecrets = Array.isArray(data) ? data : [];
|
|
|
|
|
+ const b = document.getElementById('ppp-badge-secrets');
|
|
|
|
|
+ if (b) b.innerText = state.pppSecrets.length;
|
|
|
|
|
+ const b2 = document.getElementById('secrets-count-badge');
|
|
|
|
|
+ if (b2) b2.innerText = `${state.pppSecrets.length} Users`;
|
|
|
|
|
+ renderPPPSecrets();
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ console.error('Failed to load PPP secrets:', err);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function renderPPPSecrets() {
|
|
|
|
|
+ const tbody = document.getElementById('secrets-table-body');
|
|
|
|
|
+ const empty = document.getElementById('secrets-empty');
|
|
|
|
|
+ if (!tbody) return;
|
|
|
|
|
+ tbody.innerHTML = '';
|
|
|
|
|
+
|
|
|
|
|
+ const secrets = state.pppSecrets || [];
|
|
|
|
|
+ if (secrets.length === 0) {
|
|
|
|
|
+ empty?.classList.remove('hidden');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ empty?.classList.add('hidden');
|
|
|
|
|
+
|
|
|
|
|
+ secrets.forEach(s => {
|
|
|
|
|
+ const tr = document.createElement('tr');
|
|
|
|
|
+ tr.className = `hover:bg-slate-800/40 transition text-xs ${s.disabled ? 'opacity-50' : ''}`;
|
|
|
|
|
+ tr.innerHTML = `
|
|
|
|
|
+ <td class="py-2.5 px-3 text-center">${renderRuleFlag(s.disabled, !!s.comment)}</td>
|
|
|
|
|
+ <td class="py-2.5 px-3 font-bold text-white font-mono">${s.name}</td>
|
|
|
|
|
+ <td class="py-2.5 px-3 text-purple-300 font-mono font-semibold">${s.service}</td>
|
|
|
|
|
+ <td class="py-2.5 px-3 text-amber-300 font-mono">${s.profile}</td>
|
|
|
|
|
+ <td class="py-2.5 px-3 text-slate-400 font-mono">${s.local_address || '(profile)'}</td>
|
|
|
|
|
+ <td class="py-2.5 px-3 text-cyan-400 font-mono font-bold">${s.remote_address || '(profile)'}</td>
|
|
|
|
|
+ <td class="py-2.5 px-3 text-slate-400 italic truncate max-w-[120px]">${s.comment || '-'}</td>
|
|
|
|
|
+ <td class="py-2.5 px-3 text-right space-x-1.5 font-sans whitespace-nowrap">
|
|
|
|
|
+ <button onclick="togglePPPSecretState('${s.id}', ${!s.disabled})" class="text-xs ${s.disabled ? 'text-emerald-400' : 'text-slate-400'} hover:underline">${s.disabled ? 'Enable' : 'Disable'}</button>
|
|
|
|
|
+ <button onclick="openEditPPPSecretModal('${s.id}')" class="text-xs text-slate-400 hover:text-white">Edit</button>
|
|
|
|
|
+ <button onclick="deletePPPSecret('${s.id}')" class="text-xs text-rose-400 hover:text-rose-300">Delete</button>
|
|
|
|
|
+ </td>
|
|
|
|
|
+ `;
|
|
|
|
|
+ tbody.appendChild(tr);
|
|
|
|
|
+ });
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function loadPPPProfiles() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await fetch(`${API_BASE}/vpn/ppp/profiles`);
|
|
|
|
|
+ const data = await res.json();
|
|
|
|
|
+ state.pppProfiles = Array.isArray(data) ? data : [];
|
|
|
|
|
+ const b = document.getElementById('ppp-badge-profiles');
|
|
|
|
|
+ if (b) b.innerText = state.pppProfiles.length;
|
|
|
|
|
+ const b2 = document.getElementById('profiles-count-badge');
|
|
|
|
|
+ if (b2) b2.innerText = `${state.pppProfiles.length} Profiles`;
|
|
|
|
|
+
|
|
|
|
|
+ // Populate profile selects across modals
|
|
|
|
|
+ const selects = ['secret-modal-profile', 'pppoe-server-profile', 'pptp-server-profile', 'l2tp-server-profile'];
|
|
|
|
|
+ selects.forEach(sid => {
|
|
|
|
|
+ const el = document.getElementById(sid);
|
|
|
|
|
+ if (!el) return;
|
|
|
|
|
+ const cur = el.value;
|
|
|
|
|
+ el.innerHTML = '';
|
|
|
|
|
+ state.pppProfiles.forEach(p => {
|
|
|
|
|
+ const opt = document.createElement('option');
|
|
|
|
|
+ opt.value = p.name;
|
|
|
|
|
+ opt.innerText = `${p.name} (${p.remote_address})`;
|
|
|
|
|
+ if (p.name === cur) opt.selected = true;
|
|
|
|
|
+ el.appendChild(opt);
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ renderPPPProfiles();
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ console.error('Failed to load PPP profiles:', err);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function renderPPPProfiles() {
|
|
|
|
|
+ const tbody = document.getElementById('profiles-table-body');
|
|
|
|
|
+ if (!tbody) return;
|
|
|
|
|
+ tbody.innerHTML = '';
|
|
|
|
|
+
|
|
|
|
|
+ (state.pppProfiles || []).forEach(p => {
|
|
|
|
|
+ const isDefault = p.name.startsWith('default');
|
|
|
|
|
+ const tr = document.createElement('tr');
|
|
|
|
|
+ tr.className = 'hover:bg-slate-800/40 transition text-xs font-mono';
|
|
|
|
|
+ tr.innerHTML = `
|
|
|
|
|
+ <td class="py-2.5 px-3 font-bold text-white">${p.name}</td>
|
|
|
|
|
+ <td class="py-2.5 px-3 text-slate-300">${p.local_address}</td>
|
|
|
|
|
+ <td class="py-2.5 px-3 text-cyan-400 font-bold">${p.remote_address}</td>
|
|
|
|
|
+ <td class="py-2.5 px-3 text-slate-400">${p.dns_servers || '-'}</td>
|
|
|
|
|
+ <td class="py-2.5 px-3 text-purple-300 font-semibold">${p.use_encryption}</td>
|
|
|
|
|
+ <td class="py-2.5 px-3 text-center">${p.only_one ? '<span class="text-emerald-400">yes</span>' : '<span class="text-slate-500">no</span>'}</td>
|
|
|
|
|
+ <td class="py-2.5 px-3 text-slate-400 italic">${p.comment || '-'}</td>
|
|
|
|
|
+ <td class="py-2.5 px-3 text-right font-sans whitespace-nowrap">
|
|
|
|
|
+ ${!isDefault ? `<button onclick="deletePPPProfile('${p.name}')" class="text-xs text-rose-400 hover:text-rose-300">Delete</button>` : `<span class="text-slate-600 text-[10px]">Built-in</span>`}
|
|
|
|
|
+ </td>
|
|
|
|
|
+ `;
|
|
|
|
|
+ tbody.appendChild(tr);
|
|
|
|
|
+ });
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function loadPPPServers() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const [pppoeRes, pptpRes, l2tpRes, ipsecRes] = await Promise.all([
|
|
|
|
|
+ fetch(`${API_BASE}/vpn/servers/pppoe`),
|
|
|
|
|
+ fetch(`${API_BASE}/vpn/servers/pptp`),
|
|
|
|
|
+ fetch(`${API_BASE}/vpn/servers/l2tp`),
|
|
|
|
|
+ fetch(`${API_BASE}/vpn/servers/ipsec`)
|
|
|
|
|
+ ]);
|
|
|
|
|
+
|
|
|
|
|
+ if (pppoeRes.ok) {
|
|
|
|
|
+ state.pppoeServer = await pppoeRes.json();
|
|
|
|
|
+ const s = state.pppoeServer;
|
|
|
|
|
+ document.getElementById('pppoe-server-enabled').checked = !!s.enabled;
|
|
|
|
|
+ document.getElementById('pppoe-server-name').value = s.service_name || 'proxmox-pppoe';
|
|
|
|
|
+ populateInterfaceSelect('pppoe-server-iface', s.interface || 'eth0');
|
|
|
|
|
+ document.getElementById('pppoe-server-profile').value = s.default_profile || 'default';
|
|
|
|
|
+ document.getElementById('pppoe-server-mtu').value = s.max_mtu || 1480;
|
|
|
|
|
+ document.getElementById('pppoe-server-onesession').checked = !!s.one_session_per_host;
|
|
|
|
|
+ const pill = document.getElementById('pppoe-server-status-pill');
|
|
|
|
|
+ if (pill) {
|
|
|
|
|
+ pill.innerText = s.running ? `ACTIVE (${s.active_sessions} sess)` : 'STOPPED';
|
|
|
|
|
+ pill.className = s.running ? 'px-2 py-0.5 rounded text-[10px] font-mono font-semibold bg-blue-500/20 text-blue-300 border border-blue-500/40' : 'px-2 py-0.5 rounded text-[10px] font-mono font-semibold bg-slate-800 text-slate-400';
|
|
|
|
|
+ }
|
|
|
|
|
+ setDotBadge('ppp-badge-pppoe-state', s.running);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (pptpRes.ok) {
|
|
|
|
|
+ state.pptpServer = await pptpRes.json();
|
|
|
|
|
+ const s = state.pptpServer;
|
|
|
|
|
+ document.getElementById('pptp-server-enabled').checked = !!s.enabled;
|
|
|
|
|
+ document.getElementById('pptp-server-port').value = s.port || 1723;
|
|
|
|
|
+ document.getElementById('pptp-server-profile').value = s.default_profile || 'default-encryption';
|
|
|
|
|
+ document.getElementById('pptp-server-mppe').checked = s.require_mppe !== false;
|
|
|
|
|
+ const pill = document.getElementById('pptp-server-status-pill');
|
|
|
|
|
+ if (pill) {
|
|
|
|
|
+ pill.innerText = s.running ? `ACTIVE (${s.active_sessions} sess)` : 'STOPPED';
|
|
|
|
|
+ pill.className = s.running ? 'px-2 py-0.5 rounded text-[10px] font-mono font-semibold bg-purple-500/20 text-purple-300 border border-purple-500/40' : 'px-2 py-0.5 rounded text-[10px] font-mono font-semibold bg-slate-800 text-slate-400';
|
|
|
|
|
+ }
|
|
|
|
|
+ setDotBadge('ppp-badge-pptp-state', s.running);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (l2tpRes.ok) {
|
|
|
|
|
+ state.l2tpServer = await l2tpRes.json();
|
|
|
|
|
+ const s = state.l2tpServer;
|
|
|
|
|
+ document.getElementById('l2tp-server-enabled').checked = !!s.enabled;
|
|
|
|
|
+ document.getElementById('l2tp-server-port').value = s.port || 1701;
|
|
|
|
|
+ document.getElementById('l2tp-server-profile').value = s.default_profile || 'default';
|
|
|
|
|
+ document.getElementById('l2tp-server-useipsec').value = s.use_ipsec || 'no';
|
|
|
|
|
+ document.getElementById('l2tp-server-ipsecsecret').value = s.ipsec_secret || '';
|
|
|
|
|
+ const pill = document.getElementById('l2tp-server-status-pill');
|
|
|
|
|
+ if (pill) {
|
|
|
|
|
+ pill.innerText = s.running ? `ACTIVE (${s.active_sessions} sess)` : 'STOPPED';
|
|
|
|
|
+ pill.className = s.running ? 'px-2 py-0.5 rounded text-[10px] font-mono font-semibold bg-cyan-500/20 text-cyan-300 border border-cyan-500/40' : 'px-2 py-0.5 rounded text-[10px] font-mono font-semibold bg-slate-800 text-slate-400';
|
|
|
|
|
+ }
|
|
|
|
|
+ setDotBadge('ppp-badge-l2tp-state', s.running);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (ipsecRes.ok) {
|
|
|
|
|
+ state.ipsecServer = await ipsecRes.json();
|
|
|
|
|
+ const s = state.ipsecServer;
|
|
|
|
|
+ const modalEn = document.getElementById('modal-ipsec-server-enabled');
|
|
|
|
|
+ if (modalEn) modalEn.checked = !!s.enabled;
|
|
|
|
|
+ const modalPsk = document.getElementById('modal-ipsec-server-psk');
|
|
|
|
|
+ if (modalPsk) modalPsk.value = s.preshared_key || '';
|
|
|
|
|
+ const modalPool = document.getElementById('modal-ipsec-server-pool');
|
|
|
|
|
+ if (modalPool) modalPool.value = s.pool_address || '10.10.30.0/24';
|
|
|
|
|
+ const modalDns = document.getElementById('modal-ipsec-server-dns');
|
|
|
|
|
+ if (modalDns) modalDns.value = s.dns_servers || '1.1.1.1,8.8.8.8';
|
|
|
|
|
+ const modalSubnet = document.getElementById('modal-ipsec-server-subnet');
|
|
|
|
|
+ if (modalSubnet) modalSubnet.value = s.local_subnet || '0.0.0.0/0';
|
|
|
|
|
+ const pill = document.getElementById('modal-ipsec-server-status');
|
|
|
|
|
+ if (pill) {
|
|
|
|
|
+ pill.innerText = s.running ? 'ACTIVE RESPONDER' : 'STOPPED';
|
|
|
|
|
+ pill.className = s.running ? 'px-2 py-0.5 rounded text-[10px] font-mono font-semibold bg-amber-500/20 text-amber-300 border border-amber-500/40' : 'px-2 py-0.5 rounded text-[10px] font-mono font-semibold bg-slate-800 text-slate-400';
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ console.error('Failed to load VPN servers:', err);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function loadPPPSessions() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await fetch(`${API_BASE}/vpn/ppp/active`);
|
|
|
|
|
+ const data = await res.json();
|
|
|
|
|
+ state.pppActiveSessions = Array.isArray(data) ? data : [];
|
|
|
|
|
+ const b = document.getElementById('ppp-badge-active');
|
|
|
|
|
+ if (b) b.innerText = state.pppActiveSessions.length;
|
|
|
|
|
+ const sideB = document.getElementById('badge-ppp-sessions');
|
|
|
|
|
+ if (sideB) sideB.innerText = state.pppActiveSessions.length;
|
|
|
|
|
+ renderPPPSessions();
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ console.error('Failed to load active PPP sessions:', err);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function renderPPPSessions() {
|
|
|
|
|
+ const tbody = document.getElementById('active-ppp-table-body');
|
|
|
|
|
+ const empty = document.getElementById('active-ppp-empty');
|
|
|
|
|
+ if (!tbody) return;
|
|
|
|
|
+ tbody.innerHTML = '';
|
|
|
|
|
+
|
|
|
|
|
+ const list = state.pppActiveSessions || [];
|
|
|
|
|
+ if (list.length === 0) {
|
|
|
|
|
+ empty?.classList.remove('hidden');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ empty?.classList.add('hidden');
|
|
|
|
|
+
|
|
|
|
|
+ list.forEach(s => {
|
|
|
|
|
+ const tr = document.createElement('tr');
|
|
|
|
|
+ tr.className = 'hover:bg-slate-800/40 transition text-xs font-mono';
|
|
|
|
|
+ tr.innerHTML = `
|
|
|
|
|
+ <td class="py-2.5 px-3 font-bold text-white">${s.interface}</td>
|
|
|
|
|
+ <td class="py-2.5 px-3 font-semibold text-violet-300">${s.username}</td>
|
|
|
|
|
+ <td class="py-2.5 px-3 text-cyan-400 uppercase">${s.service}</td>
|
|
|
|
|
+ <td class="py-2.5 px-3 text-emerald-400 font-bold">${s.address || '-'}</td>
|
|
|
|
|
+ <td class="py-2.5 px-3 text-slate-400">${s.uptime}</td>
|
|
|
|
|
+ <td class="py-2.5 px-3 text-[11px]">
|
|
|
|
|
+ <span class="text-emerald-400">↓ ${s.rx_human || '0 B'}</span>
|
|
|
|
|
+ <span class="text-cyan-400 ml-1">↑ ${s.tx_human || '0 B'}</span>
|
|
|
|
|
+ </td>
|
|
|
|
|
+ <td class="py-2.5 px-3 text-right font-sans">
|
|
|
|
|
+ <button onclick="disconnectPPPSession('${s.interface}')" class="text-xs text-rose-400 hover:text-rose-300">Disconnect</button>
|
|
|
|
|
+ </td>
|
|
|
|
|
+ `;
|
|
|
|
|
+ tbody.appendChild(tr);
|
|
|
|
|
+ });
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function disconnectPPPSession(iface) {
|
|
|
|
|
+ if (!confirm(`Disconnect session ${iface}?`)) return;
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await fetch(`${API_BASE}/vpn/ppp/active/${encodeURIComponent(iface)}`, { method: 'DELETE' });
|
|
|
|
|
+ if (res.ok) {
|
|
|
|
|
+ showToast('Disconnected', `Session ${iface} terminated`, 'info');
|
|
|
|
|
+ loadPPPSessions();
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ showToast('Error', err.message, 'error');
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// PPP Secret Modal handlers
|
|
|
|
|
+function openAddPPPSecretModal() {
|
|
|
|
|
+ document.getElementById('secret-modal-title').innerText = 'Add PPP Secret';
|
|
|
|
|
+ document.getElementById('secret-modal-id').value = '';
|
|
|
|
|
+ document.getElementById('secret-modal-name').value = '';
|
|
|
|
|
+ document.getElementById('secret-modal-password').value = '';
|
|
|
|
|
+ document.getElementById('secret-modal-service').value = 'any';
|
|
|
|
|
+ document.getElementById('secret-modal-profile').value = 'default';
|
|
|
|
|
+ document.getElementById('secret-modal-local').value = '';
|
|
|
|
|
+ document.getElementById('secret-modal-remote').value = '';
|
|
|
|
|
+ document.getElementById('secret-modal-comment').value = '';
|
|
|
|
|
+ document.getElementById('secret-modal-disabled').checked = false;
|
|
|
|
|
+ document.getElementById('ppp-secret-modal').classList.remove('hidden');
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function openEditPPPSecretModal(id) {
|
|
|
|
|
+ const s = (state.pppSecrets || []).find(sec => sec.id === id);
|
|
|
|
|
+ if (!s) return;
|
|
|
|
|
+ document.getElementById('secret-modal-title').innerText = `Edit PPP Secret (${s.name})`;
|
|
|
|
|
+ document.getElementById('secret-modal-id').value = s.id;
|
|
|
|
|
+ document.getElementById('secret-modal-name').value = s.name;
|
|
|
|
|
+ document.getElementById('secret-modal-password').value = s.password;
|
|
|
|
|
+ document.getElementById('secret-modal-service').value = s.service || 'any';
|
|
|
|
|
+ document.getElementById('secret-modal-profile').value = s.profile || 'default';
|
|
|
|
|
+ document.getElementById('secret-modal-local').value = s.local_address || '';
|
|
|
|
|
+ document.getElementById('secret-modal-remote').value = s.remote_address || '';
|
|
|
|
|
+ document.getElementById('secret-modal-comment').value = s.comment || '';
|
|
|
|
|
+ document.getElementById('secret-modal-disabled').checked = !!s.disabled;
|
|
|
|
|
+ document.getElementById('ppp-secret-modal').classList.remove('hidden');
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function closePPPSecretModal() {
|
|
|
|
|
+ document.getElementById('ppp-secret-modal').classList.add('hidden');
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function submitSavePPPSecret(e) {
|
|
|
|
|
+ e.preventDefault();
|
|
|
|
|
+ const id = document.getElementById('secret-modal-id').value;
|
|
|
|
|
+ const name = document.getElementById('secret-modal-name').value.trim();
|
|
|
|
|
+ const password = document.getElementById('secret-modal-password').value.trim();
|
|
|
|
|
+ const service = document.getElementById('secret-modal-service').value;
|
|
|
|
|
+ const profile = document.getElementById('secret-modal-profile').value;
|
|
|
|
|
+ const local = document.getElementById('secret-modal-local').value.trim();
|
|
|
|
|
+ const remote = document.getElementById('secret-modal-remote').value.trim();
|
|
|
|
|
+ const comment = document.getElementById('secret-modal-comment').value.trim();
|
|
|
|
|
+ const disabled = document.getElementById('secret-modal-disabled').checked;
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await fetch(`${API_BASE}/vpn/ppp/secrets`, {
|
|
|
|
|
+ method: 'POST',
|
|
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
+ body: JSON.stringify({
|
|
|
|
|
+ id,
|
|
|
|
|
+ name,
|
|
|
|
|
+ password,
|
|
|
|
|
+ service,
|
|
|
|
|
+ profile,
|
|
|
|
|
+ local_address: local,
|
|
|
|
|
+ remote_address: remote,
|
|
|
|
|
+ comment,
|
|
|
|
|
+ disabled
|
|
|
|
|
+ })
|
|
|
|
|
+ });
|
|
|
|
|
+ if (res.ok) {
|
|
|
|
|
+ showToast('Secret Saved', `User ${name} saved to PPP database`, 'success');
|
|
|
|
|
+ closePPPSecretModal();
|
|
|
|
|
+ loadPPPSecrets();
|
|
|
|
|
+ } else {
|
|
|
|
|
+ const err = await res.json();
|
|
|
|
|
+ showToast('Error', err.message, 'error');
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ showToast('Error', err.message, 'error');
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function togglePPPSecretState(id, disabled) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await fetch(`${API_BASE}/vpn/ppp/secrets/${encodeURIComponent(id)}/state`, {
|
|
|
|
|
+ method: 'POST',
|
|
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
+ body: JSON.stringify({ disabled })
|
|
|
|
|
+ });
|
|
|
|
|
+ if (res.ok) {
|
|
|
|
|
+ showToast(disabled ? 'Disabled' : 'Enabled', 'Secret user state updated', 'info');
|
|
|
|
|
+ loadPPPSecrets();
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ showToast('Error', err.message, 'error');
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function deletePPPSecret(id) {
|
|
|
|
|
+ if (!confirm('Are you sure you want to delete this secret account?')) return;
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await fetch(`${API_BASE}/vpn/ppp/secrets/${encodeURIComponent(id)}`, { method: 'DELETE' });
|
|
|
|
|
+ if (res.ok) {
|
|
|
|
|
+ showToast('Deleted', 'Secret account removed', 'success');
|
|
|
|
|
+ loadPPPSecrets();
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ showToast('Error', err.message, 'error');
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// PPP Profile Modal handlers
|
|
|
|
|
+function openAddPPPProfileModal() {
|
|
|
|
|
+ document.getElementById('profile-modal-name').value = '';
|
|
|
|
|
+ document.getElementById('profile-modal-local').value = '10.10.10.1';
|
|
|
|
|
+ document.getElementById('profile-modal-remote').value = '10.10.10.10-10.10.10.50';
|
|
|
|
|
+ document.getElementById('profile-modal-dns').value = '1.1.1.1,8.8.8.8';
|
|
|
|
|
+ document.getElementById('profile-modal-encryption').value = 'required';
|
|
|
|
|
+ document.getElementById('profile-modal-onlyone').checked = true;
|
|
|
|
|
+ document.getElementById('profile-modal-comment').value = '';
|
|
|
|
|
+ document.getElementById('ppp-profile-modal').classList.remove('hidden');
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function closePPPProfileModal() {
|
|
|
|
|
+ document.getElementById('ppp-profile-modal').classList.add('hidden');
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function submitSavePPPProfile(e) {
|
|
|
|
|
+ e.preventDefault();
|
|
|
|
|
+ const name = document.getElementById('profile-modal-name').value.trim();
|
|
|
|
|
+ const local = document.getElementById('profile-modal-local').value.trim();
|
|
|
|
|
+ const remote = document.getElementById('profile-modal-remote').value.trim();
|
|
|
|
|
+ const dns = document.getElementById('profile-modal-dns').value.trim();
|
|
|
|
|
+ const enc = document.getElementById('profile-modal-encryption').value;
|
|
|
|
|
+ const onlyone = document.getElementById('profile-modal-onlyone').checked;
|
|
|
|
|
+ const comment = document.getElementById('profile-modal-comment').value.trim();
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await fetch(`${API_BASE}/vpn/ppp/profiles`, {
|
|
|
|
|
+ method: 'POST',
|
|
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
+ body: JSON.stringify({
|
|
|
|
|
+ id: name,
|
|
|
|
|
+ name,
|
|
|
|
|
+ local_address: local,
|
|
|
|
|
+ remote_address: remote,
|
|
|
|
|
+ dns_servers: dns,
|
|
|
|
|
+ use_encryption: enc,
|
|
|
|
|
+ only_one: onlyone,
|
|
|
|
|
+ comment
|
|
|
|
|
+ })
|
|
|
|
|
+ });
|
|
|
|
|
+ if (res.ok) {
|
|
|
|
|
+ showToast('Profile Saved', `PPP profile ${name} created`, 'success');
|
|
|
|
|
+ closePPPProfileModal();
|
|
|
|
|
+ loadPPPProfiles();
|
|
|
|
|
+ } else {
|
|
|
|
|
+ const err = await res.json();
|
|
|
|
|
+ showToast('Error', err.message, 'error');
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ showToast('Error', err.message, 'error');
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function deletePPPProfile(name) {
|
|
|
|
|
+ if (!confirm(`Delete PPP profile ${name}?`)) return;
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await fetch(`${API_BASE}/vpn/ppp/profiles/${encodeURIComponent(name)}`, { method: 'DELETE' });
|
|
|
|
|
+ if (res.ok) {
|
|
|
|
|
+ showToast('Deleted', 'Profile removed', 'success');
|
|
|
|
|
+ loadPPPProfiles();
|
|
|
|
|
+ } else {
|
|
|
|
|
+ const err = await res.json();
|
|
|
|
|
+ showToast('Error', err.message, 'error');
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ showToast('Error', err.message, 'error');
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// Server Config submissions
|
|
|
|
|
+async function savePPPoEServerConfig(e) {
|
|
|
|
|
+ e.preventDefault();
|
|
|
|
|
+ const enabled = document.getElementById('pppoe-server-enabled').checked;
|
|
|
|
|
+ const name = document.getElementById('pppoe-server-name').value.trim();
|
|
|
|
|
+ const iface = document.getElementById('pppoe-server-iface').value;
|
|
|
|
|
+ const profile = document.getElementById('pppoe-server-profile').value;
|
|
|
|
|
+ const mtu = parseInt(document.getElementById('pppoe-server-mtu').value) || 1480;
|
|
|
|
|
+ const onesession = document.getElementById('pppoe-server-onesession').checked;
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await fetch(`${API_BASE}/vpn/servers/pppoe`, {
|
|
|
|
|
+ method: 'POST',
|
|
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
+ body: JSON.stringify({
|
|
|
|
|
+ enabled,
|
|
|
|
|
+ service_name: name,
|
|
|
|
|
+ interface: iface,
|
|
|
|
|
+ default_profile: profile,
|
|
|
|
|
+ max_mtu: mtu,
|
|
|
|
|
+ max_mru: mtu,
|
|
|
|
|
+ one_session_per_host: onesession
|
|
|
|
|
+ })
|
|
|
|
|
+ });
|
|
|
|
|
+ if (res.ok) {
|
|
|
|
|
+ showToast('PPPoE Server', enabled ? 'PPPoE Server started' : 'PPPoE Server stopped', 'success');
|
|
|
|
|
+ loadPPPServers();
|
|
|
|
|
+ fetchData(false);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ const err = await res.json();
|
|
|
|
|
+ showToast('Error', err.message, 'error');
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ showToast('Error', err.message, 'error');
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function savePPTPServerConfig(e) {
|
|
|
|
|
+ e.preventDefault();
|
|
|
|
|
+ const enabled = document.getElementById('pptp-server-enabled').checked;
|
|
|
|
|
+ const port = parseInt(document.getElementById('pptp-server-port').value) || 1723;
|
|
|
|
|
+ const profile = document.getElementById('pptp-server-profile').value;
|
|
|
|
|
+ const mppe = document.getElementById('pptp-server-mppe').checked;
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await fetch(`${API_BASE}/vpn/servers/pptp`, {
|
|
|
|
|
+ method: 'POST',
|
|
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
+ body: JSON.stringify({
|
|
|
|
|
+ enabled,
|
|
|
|
|
+ port,
|
|
|
|
|
+ default_profile: profile,
|
|
|
|
|
+ require_mppe: mppe
|
|
|
|
|
+ })
|
|
|
|
|
+ });
|
|
|
|
|
+ if (res.ok) {
|
|
|
|
|
+ showToast('PPTP Server', enabled ? 'PPTP Server started' : 'PPTP Server stopped', 'success');
|
|
|
|
|
+ loadPPPServers();
|
|
|
|
|
+ fetchData(false);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ const err = await res.json();
|
|
|
|
|
+ showToast('Error', err.message, 'error');
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ showToast('Error', err.message, 'error');
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function saveL2TPServerConfig(e) {
|
|
|
|
|
+ e.preventDefault();
|
|
|
|
|
+ const enabled = document.getElementById('l2tp-server-enabled').checked;
|
|
|
|
|
+ const port = parseInt(document.getElementById('l2tp-server-port').value) || 1701;
|
|
|
|
|
+ const profile = document.getElementById('l2tp-server-profile').value;
|
|
|
|
|
+ const useipsec = document.getElementById('l2tp-server-useipsec').value;
|
|
|
|
|
+ const psk = document.getElementById('l2tp-server-ipsecsecret').value.trim();
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await fetch(`${API_BASE}/vpn/servers/l2tp`, {
|
|
|
|
|
+ method: 'POST',
|
|
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
+ body: JSON.stringify({
|
|
|
|
|
+ enabled,
|
|
|
|
|
+ port,
|
|
|
|
|
+ default_profile: profile,
|
|
|
|
|
+ use_ipsec: useipsec,
|
|
|
|
|
+ ipsec_secret: psk
|
|
|
|
|
+ })
|
|
|
|
|
+ });
|
|
|
|
|
+ if (res.ok) {
|
|
|
|
|
+ showToast('L2TP Server', enabled ? 'L2TP Server started' : 'L2TP Server stopped', 'success');
|
|
|
|
|
+ loadPPPServers();
|
|
|
|
|
+ fetchData(false);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ const err = await res.json();
|
|
|
|
|
+ showToast('Error', err.message, 'error');
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ showToast('Error', err.message, 'error');
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// IPSec Server Modal
|
|
|
|
|
+function openIPSecServerModal() {
|
|
|
|
|
+ loadPPPServers();
|
|
|
|
|
+ document.getElementById('ipsec-server-modal').classList.remove('hidden');
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function closeIPSecServerModal() {
|
|
|
|
|
+ document.getElementById('ipsec-server-modal').classList.add('hidden');
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function submitSaveIPSecServer(e) {
|
|
|
|
|
+ e.preventDefault();
|
|
|
|
|
+ const enabled = document.getElementById('modal-ipsec-server-enabled').checked;
|
|
|
|
|
+ const psk = document.getElementById('modal-ipsec-server-psk').value.trim();
|
|
|
|
|
+ const pool = document.getElementById('modal-ipsec-server-pool').value.trim();
|
|
|
|
|
+ const dns = document.getElementById('modal-ipsec-server-dns').value.trim();
|
|
|
|
|
+ const subnet = document.getElementById('modal-ipsec-server-subnet').value.trim();
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await fetch(`${API_BASE}/vpn/servers/ipsec`, {
|
|
|
|
|
+ method: 'POST',
|
|
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
+ body: JSON.stringify({
|
|
|
|
|
+ enabled,
|
|
|
|
|
+ preshared_key: psk,
|
|
|
|
|
+ pool_address: pool,
|
|
|
|
|
+ dns_servers: dns,
|
|
|
|
|
+ local_subnet: subnet
|
|
|
|
|
+ })
|
|
|
|
|
+ });
|
|
|
|
|
+ if (res.ok) {
|
|
|
|
|
+ showToast('IPSec Server', enabled ? 'IKEv2 Roadwarrior responder active' : 'IPSec Server stopped', 'success');
|
|
|
|
|
+ closeIPSecServerModal();
|
|
|
|
|
+ loadPPPServers();
|
|
|
|
|
+ fetchData(false);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ const err = await res.json();
|
|
|
|
|
+ showToast('Error', err.message, 'error');
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ showToast('Error', err.message, 'error');
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// WireGuard Client Config Viewer
|
|
|
|
|
+let currentWGClientConf = '';
|
|
|
|
|
+
|
|
|
|
|
+async function openWGClientConfModal(iface, clientIP) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await fetch(`${API_BASE}/vpn/wireguard/client-config`, {
|
|
|
|
|
+ method: 'POST',
|
|
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
+ body: JSON.stringify({
|
|
|
|
|
+ interface: iface,
|
|
|
|
|
+ address: clientIP || '10.10.0.2/32',
|
|
|
|
|
+ dns: '1.1.1.1, 8.8.8.8'
|
|
|
|
|
+ })
|
|
|
|
|
+ });
|
|
|
|
|
+ if (res.ok) {
|
|
|
|
|
+ const data = await res.json();
|
|
|
|
|
+ currentWGClientConf = data.config;
|
|
|
|
|
+ document.getElementById('wg-client-conf-text').value = currentWGClientConf;
|
|
|
|
|
+ document.getElementById('wg-client-conf-modal').classList.remove('hidden');
|
|
|
|
|
+ } else {
|
|
|
|
|
+ const err = await res.json();
|
|
|
|
|
+ showToast('Error', err.message, 'error');
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ showToast('Error', err.message, 'error');
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function closeWGClientConfModal() {
|
|
|
|
|
+ document.getElementById('wg-client-conf-modal').classList.add('hidden');
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function copyWGClientConf() {
|
|
|
|
|
+ if (!currentWGClientConf) return;
|
|
|
|
|
+ copyToClipboard(currentWGClientConf, 'WireGuard Client Profile');
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function downloadWGClientConf() {
|
|
|
|
|
+ if (!currentWGClientConf) return;
|
|
|
|
|
+ const blob = new Blob([currentWGClientConf], { type: 'text/plain;charset=utf-8' });
|
|
|
|
|
+ const a = document.createElement('a');
|
|
|
|
|
+ a.href = URL.createObjectURL(blob);
|
|
|
|
|
+ a.download = 'wireguard-client.conf';
|
|
|
|
|
+ document.body.appendChild(a);
|
|
|
|
|
+ a.click();
|
|
|
|
|
+ document.body.removeChild(a);
|
|
|
|
|
+ showToast('Downloaded', 'wireguard-client.conf saved', 'success');
|
|
|
|
|
+}
|