|
@@ -1094,9 +1094,14 @@ async function loadWireGuard(populateInputs = true) {
|
|
|
tr.className = 'hover:bg-slate-800/40 transition text-xs';
|
|
tr.className = 'hover:bg-slate-800/40 transition text-xs';
|
|
|
tr.innerHTML = `
|
|
tr.innerHTML = `
|
|
|
<td class="py-2.5 px-3 text-center">${renderMikroTikFlag(iface.running, iface.disabled)}</td>
|
|
<td class="py-2.5 px-3 text-center">${renderMikroTikFlag(iface.running, iface.disabled)}</td>
|
|
|
- <td class="py-2.5 px-3 font-bold text-white">${iface.name}</td>
|
|
|
|
|
|
|
+ <td class="py-2.5 px-3">
|
|
|
|
|
+ <div class="flex items-center gap-1.5">
|
|
|
|
|
+ <span class="font-bold text-white">${escapeHtml(iface.name)}</span>
|
|
|
|
|
+ ${iface.comment ? `<span class="text-[10px] text-slate-400 font-sans italic">(${escapeHtml(iface.comment)})</span>` : ''}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </td>
|
|
|
<td class="py-2.5 px-3 text-amber-400 font-mono">${iface.listen_port}</td>
|
|
<td class="py-2.5 px-3 text-amber-400 font-mono">${iface.listen_port}</td>
|
|
|
- <td class="py-2.5 px-3 text-emerald-400 font-mono">${iface.address || '-'}</td>
|
|
|
|
|
|
|
+ <td class="py-2.5 px-3 text-emerald-400 font-mono">${iface.address || '<span class="text-slate-600 font-sans">--</span>'}</td>
|
|
|
<td class="py-2.5 px-3 text-slate-300 font-mono">
|
|
<td class="py-2.5 px-3 text-slate-300 font-mono">
|
|
|
<span class="cursor-pointer hover:text-white" onclick="copyToClipboard('${iface.public_key}', 'Public Key')" title="Click to copy: ${iface.public_key}">
|
|
<span class="cursor-pointer hover:text-white" onclick="copyToClipboard('${iface.public_key}', 'Public Key')" title="Click to copy: ${iface.public_key}">
|
|
|
${(iface.public_key || '').substring(0, 10)}... 📋
|
|
${(iface.public_key || '').substring(0, 10)}... 📋
|
|
@@ -1106,17 +1111,17 @@ async function loadWireGuard(populateInputs = true) {
|
|
|
<td class="py-2.5 px-3 text-center">
|
|
<td class="py-2.5 px-3 text-center">
|
|
|
<span class="px-2 py-0.5 rounded text-[10px] font-sans bg-slate-800 text-slate-300">${peersOnIface} peer${peersOnIface !== 1 ? 's' : ''}</span>
|
|
<span class="px-2 py-0.5 rounded text-[10px] font-sans bg-slate-800 text-slate-300">${peersOnIface} peer${peersOnIface !== 1 ? 's' : ''}</span>
|
|
|
</td>
|
|
</td>
|
|
|
- <td class="py-2.5 px-3 text-right space-x-2 font-sans">
|
|
|
|
|
|
|
+ <td class="py-2.5 px-3 text-right space-x-2 font-sans whitespace-nowrap">
|
|
|
${iface.running ? `
|
|
${iface.running ? `
|
|
|
- <button onclick="stopWGInterface('${iface.name}')" class="text-xs text-rose-400 hover:text-rose-300">Stop</button>
|
|
|
|
|
|
|
+ <button onclick="stopWGInterface('${iface.name}')" class="text-xs text-rose-400 hover:text-rose-300 font-medium">Stop</button>
|
|
|
` : `
|
|
` : `
|
|
|
- <button onclick="startWGInterface('${iface.name}')" class="text-xs bg-rose-600/30 hover:bg-rose-600/50 text-rose-300 px-2 py-0.5 rounded border border-rose-500/30">Start</button>
|
|
|
|
|
|
|
+ <button onclick="startWGInterface('${iface.name}')" class="text-xs bg-rose-600/30 hover:bg-rose-600/50 text-rose-300 px-2 py-0.5 rounded border border-rose-500/30 font-medium">Start</button>
|
|
|
`}
|
|
`}
|
|
|
- <button onclick="toggleWGInterfaceState('${iface.name}', ${iface.disabled ? 'true' : 'false'})" class="text-xs text-slate-400 hover:text-white">
|
|
|
|
|
|
|
+ <button onclick="toggleWGInterfaceState('${iface.name}', ${iface.disabled ? 'true' : 'false'})" class="text-xs text-slate-400 hover:text-white font-medium">
|
|
|
${iface.disabled ? 'Enable' : 'Disable'}
|
|
${iface.disabled ? 'Enable' : 'Disable'}
|
|
|
</button>
|
|
</button>
|
|
|
- <button onclick="openEditWGInterfaceModal('${iface.name}')" class="text-xs text-slate-400 hover:text-white">Edit</button>
|
|
|
|
|
- <button onclick="deleteWGInterface('${iface.name}')" class="text-xs text-slate-500 hover:text-rose-400">Delete</button>
|
|
|
|
|
|
|
+ <button onclick="openEditWGInterfaceModal('${iface.name}')" class="text-xs text-amber-400 hover:text-amber-300 font-medium">Edit</button>
|
|
|
|
|
+ <button onclick="deleteWGInterface('${iface.name}')" class="text-xs text-slate-500 hover:text-rose-400 font-medium">Delete</button>
|
|
|
</td>
|
|
</td>
|
|
|
`;
|
|
`;
|
|
|
ifacesTbody.appendChild(tr);
|
|
ifacesTbody.appendChild(tr);
|
|
@@ -1165,23 +1170,27 @@ function renderWGPeers() {
|
|
|
tr.className = 'hover:bg-slate-800/40 transition text-xs';
|
|
tr.className = 'hover:bg-slate-800/40 transition text-xs';
|
|
|
tr.innerHTML = `
|
|
tr.innerHTML = `
|
|
|
<td class="py-2.5 px-3 text-center">${renderMikroTikFlag(isOnline, p.disabled)}</td>
|
|
<td class="py-2.5 px-3 text-center">${renderMikroTikFlag(isOnline, p.disabled)}</td>
|
|
|
- <td class="py-2.5 px-3 font-mono font-bold text-rose-400">${p.interface}</td>
|
|
|
|
|
- <td class="py-2.5 px-3 font-bold text-white font-sans">${p.name || 'Unnamed'}</td>
|
|
|
|
|
|
|
+ <td class="py-2.5 px-3 font-mono font-bold text-rose-400">${escapeHtml(p.interface)}</td>
|
|
|
|
|
+ <td class="py-2.5 px-3 font-bold text-white font-sans">${escapeHtml(p.name || 'Unnamed')}</td>
|
|
|
<td class="py-2.5 px-3 text-slate-300 font-mono">
|
|
<td class="py-2.5 px-3 text-slate-300 font-mono">
|
|
|
<span class="cursor-pointer hover:text-white" onclick="copyToClipboard('${p.public_key}', 'Peer Public Key')" title="${p.public_key}">
|
|
<span class="cursor-pointer hover:text-white" onclick="copyToClipboard('${p.public_key}', 'Peer Public Key')" title="${p.public_key}">
|
|
|
- ${p.public_key.substring(0, 10)}... 📋
|
|
|
|
|
|
|
+ ${(p.public_key || '').substring(0, 10)}... 📋
|
|
|
</span>
|
|
</span>
|
|
|
</td>
|
|
</td>
|
|
|
<td class="py-2.5 px-3 text-emerald-400 font-mono font-semibold">${(p.allowed_ips || []).join(', ')}</td>
|
|
<td class="py-2.5 px-3 text-emerald-400 font-mono font-semibold">${(p.allowed_ips || []).join(', ')}</td>
|
|
|
- <td class="py-2.5 px-3 text-slate-400 font-mono">${p.endpoint_address ? (p.endpoint_address + (p.endpoint_port ? ':' + p.endpoint_port : '')) : '-'}</td>
|
|
|
|
|
|
|
+ <td class="py-2.5 px-3 text-slate-400 font-mono">${p.endpoint_address ? (p.endpoint_address + (p.endpoint_port ? ':' + p.endpoint_port : '')) : '--'}</td>
|
|
|
<td class="py-2.5 px-3 text-slate-300 font-sans">${p.latest_handshake || 'Never'}</td>
|
|
<td class="py-2.5 px-3 text-slate-300 font-sans">${p.latest_handshake || 'Never'}</td>
|
|
|
<td class="py-2.5 px-3 font-mono text-[11px]">
|
|
<td class="py-2.5 px-3 font-mono text-[11px]">
|
|
|
<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 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>
|
|
|
|
|
|
|
+ <td class="py-2.5 px-3 text-right font-sans space-x-2 whitespace-nowrap">
|
|
|
|
|
+ <button onclick="toggleWGPeerState('${escapeHtml(p.interface)}', '${escapeHtml(p.public_key)}', ${p.disabled ? 'true' : 'false'})" class="text-xs ${p.disabled ? 'text-emerald-400 hover:text-emerald-300' : 'text-slate-400 hover:text-white'} font-medium">
|
|
|
|
|
+ ${p.disabled ? 'Enable' : 'Disable'}
|
|
|
|
|
+ </button>
|
|
|
|
|
+ <button onclick="openEditWGPeerModal('${escapeHtml(p.interface)}', '${escapeHtml(p.public_key)}')" class="text-xs text-amber-400 hover:text-amber-300 font-medium">Edit</button>
|
|
|
|
|
+ <button onclick="openWGClientConfModal('${escapeHtml(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('${escapeHtml(p.interface)}', '${escapeHtml(p.public_key)}')" class="text-xs text-rose-400 hover:text-rose-300 font-medium">Delete</button>
|
|
|
</td>
|
|
</td>
|
|
|
`;
|
|
`;
|
|
|
tbody.appendChild(tr);
|
|
tbody.appendChild(tr);
|
|
@@ -1192,11 +1201,13 @@ function renderWGPeers() {
|
|
|
function openAddWGInterfaceModal() {
|
|
function openAddWGInterfaceModal() {
|
|
|
document.getElementById('wg-iface-modal-title').innerText = 'Add WireGuard Interface';
|
|
document.getElementById('wg-iface-modal-title').innerText = 'Add WireGuard Interface';
|
|
|
document.getElementById('wg-iface-modal-edit-orig').value = '';
|
|
document.getElementById('wg-iface-modal-edit-orig').value = '';
|
|
|
- document.getElementById('wg-iface-modal-name').value = `wg${(state.wgInterfaces || []).length}`;
|
|
|
|
|
- document.getElementById('wg-iface-modal-name').readOnly = false;
|
|
|
|
|
|
|
+ const nameEl = document.getElementById('wg-iface-modal-name');
|
|
|
|
|
+ nameEl.value = `wg${(state.wgInterfaces || []).length}`;
|
|
|
|
|
+ nameEl.readOnly = false;
|
|
|
document.getElementById('wg-iface-modal-port').value = 51820 + (state.wgInterfaces || []).length;
|
|
document.getElementById('wg-iface-modal-port').value = 51820 + (state.wgInterfaces || []).length;
|
|
|
document.getElementById('wg-iface-modal-addr').value = `10.${10 + (state.wgInterfaces || []).length}.0.1/24`;
|
|
document.getElementById('wg-iface-modal-addr').value = `10.${10 + (state.wgInterfaces || []).length}.0.1/24`;
|
|
|
document.getElementById('wg-iface-modal-mtu').value = '1420';
|
|
document.getElementById('wg-iface-modal-mtu').value = '1420';
|
|
|
|
|
+ document.getElementById('wg-iface-modal-comment').value = '';
|
|
|
document.getElementById('wg-iface-modal-pubkey').value = '';
|
|
document.getElementById('wg-iface-modal-pubkey').value = '';
|
|
|
document.getElementById('wg-iface-modal-privkey').value = '';
|
|
document.getElementById('wg-iface-modal-privkey').value = '';
|
|
|
document.getElementById('wg-iface-modal-disabled').checked = false;
|
|
document.getElementById('wg-iface-modal-disabled').checked = false;
|
|
@@ -1211,11 +1222,13 @@ function openEditWGInterfaceModal(name) {
|
|
|
|
|
|
|
|
document.getElementById('wg-iface-modal-title').innerText = `Edit WireGuard Interface: ${name}`;
|
|
document.getElementById('wg-iface-modal-title').innerText = `Edit WireGuard Interface: ${name}`;
|
|
|
document.getElementById('wg-iface-modal-edit-orig').value = name;
|
|
document.getElementById('wg-iface-modal-edit-orig').value = name;
|
|
|
- document.getElementById('wg-iface-modal-name').value = iface.name;
|
|
|
|
|
- document.getElementById('wg-iface-modal-name').readOnly = true;
|
|
|
|
|
|
|
+ const nameEl = document.getElementById('wg-iface-modal-name');
|
|
|
|
|
+ nameEl.value = iface.name;
|
|
|
|
|
+ nameEl.readOnly = false; // ALLOW RENAMING
|
|
|
document.getElementById('wg-iface-modal-port').value = iface.listen_port || 51820;
|
|
document.getElementById('wg-iface-modal-port').value = iface.listen_port || 51820;
|
|
|
document.getElementById('wg-iface-modal-addr').value = iface.address || '';
|
|
document.getElementById('wg-iface-modal-addr').value = iface.address || '';
|
|
|
document.getElementById('wg-iface-modal-mtu').value = iface.mtu || 1420;
|
|
document.getElementById('wg-iface-modal-mtu').value = iface.mtu || 1420;
|
|
|
|
|
+ document.getElementById('wg-iface-modal-comment').value = iface.comment || '';
|
|
|
document.getElementById('wg-iface-modal-pubkey').value = iface.public_key || '';
|
|
document.getElementById('wg-iface-modal-pubkey').value = iface.public_key || '';
|
|
|
document.getElementById('wg-iface-modal-privkey').value = iface.private_key || '';
|
|
document.getElementById('wg-iface-modal-privkey').value = iface.private_key || '';
|
|
|
document.getElementById('wg-iface-modal-disabled').checked = !!iface.disabled;
|
|
document.getElementById('wg-iface-modal-disabled').checked = !!iface.disabled;
|
|
@@ -1240,12 +1253,51 @@ async function generateWGInterfaceKeys() {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+async function onWGInterfacePrivKeyInput() {
|
|
|
|
|
+ const privKey = document.getElementById('wg-iface-modal-privkey').value.trim();
|
|
|
|
|
+ if (!privKey) return;
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await fetch(`${API_BASE}/vpn/wireguard/calc-pubkey`, {
|
|
|
|
|
+ method: 'POST',
|
|
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
+ body: JSON.stringify({ private_key: privKey })
|
|
|
|
|
+ });
|
|
|
|
|
+ if (res.ok) {
|
|
|
|
|
+ const data = await res.json();
|
|
|
|
|
+ if (data.public_key) {
|
|
|
|
|
+ document.getElementById('wg-iface-modal-pubkey').value = data.public_key;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ // Ignore while typing
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function toggleWGPrivKeyVisibility() {
|
|
|
|
|
+ const el = document.getElementById('wg-iface-modal-privkey');
|
|
|
|
|
+ const btn = document.getElementById('btn-toggle-wg-privkey');
|
|
|
|
|
+ if (el.type === 'password') {
|
|
|
|
|
+ el.type = 'text';
|
|
|
|
|
+ btn.innerText = '🙈 Hide';
|
|
|
|
|
+ } else {
|
|
|
|
|
+ el.type = 'password';
|
|
|
|
|
+ btn.innerText = '👁️ Show';
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function copyWGInterfacePubKey() {
|
|
|
|
|
+ const pub = document.getElementById('wg-iface-modal-pubkey').value;
|
|
|
|
|
+ if (pub) copyToClipboard(pub, 'Interface Public Key');
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
async function submitSaveWGInterface(e) {
|
|
async function submitSaveWGInterface(e) {
|
|
|
e.preventDefault();
|
|
e.preventDefault();
|
|
|
|
|
+ const origName = document.getElementById('wg-iface-modal-edit-orig').value.trim();
|
|
|
const name = document.getElementById('wg-iface-modal-name').value.trim();
|
|
const name = document.getElementById('wg-iface-modal-name').value.trim();
|
|
|
const port = parseInt(document.getElementById('wg-iface-modal-port').value, 10) || 51820;
|
|
const port = parseInt(document.getElementById('wg-iface-modal-port').value, 10) || 51820;
|
|
|
const addr = document.getElementById('wg-iface-modal-addr').value.trim();
|
|
const addr = document.getElementById('wg-iface-modal-addr').value.trim();
|
|
|
const mtu = parseInt(document.getElementById('wg-iface-modal-mtu').value, 10) || 1420;
|
|
const mtu = parseInt(document.getElementById('wg-iface-modal-mtu').value, 10) || 1420;
|
|
|
|
|
+ const comment = document.getElementById('wg-iface-modal-comment').value.trim();
|
|
|
const pub = document.getElementById('wg-iface-modal-pubkey').value.trim();
|
|
const pub = document.getElementById('wg-iface-modal-pubkey').value.trim();
|
|
|
const priv = document.getElementById('wg-iface-modal-privkey').value.trim();
|
|
const priv = document.getElementById('wg-iface-modal-privkey').value.trim();
|
|
|
const disabled = document.getElementById('wg-iface-modal-disabled').checked;
|
|
const disabled = document.getElementById('wg-iface-modal-disabled').checked;
|
|
@@ -1255,20 +1307,25 @@ async function submitSaveWGInterface(e) {
|
|
|
method: 'POST',
|
|
method: 'POST',
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
body: JSON.stringify({
|
|
body: JSON.stringify({
|
|
|
|
|
+ orig_name: origName,
|
|
|
name,
|
|
name,
|
|
|
listen_port: port,
|
|
listen_port: port,
|
|
|
address: addr,
|
|
address: addr,
|
|
|
mtu,
|
|
mtu,
|
|
|
|
|
+ comment,
|
|
|
public_key: pub,
|
|
public_key: pub,
|
|
|
private_key: priv,
|
|
private_key: priv,
|
|
|
disabled
|
|
disabled
|
|
|
})
|
|
})
|
|
|
});
|
|
});
|
|
|
|
|
+ const data = await res.json();
|
|
|
if (res.ok) {
|
|
if (res.ok) {
|
|
|
- showToast('WireGuard Saved', `Interface ${name} configured`, 'success');
|
|
|
|
|
|
|
+ showToast('WireGuard Saved', `Interface ${name} configured successfully`, 'success');
|
|
|
closeAddWGInterfaceModal();
|
|
closeAddWGInterfaceModal();
|
|
|
loadWireGuard(false);
|
|
loadWireGuard(false);
|
|
|
fetchData(false);
|
|
fetchData(false);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ showToast('Error', data.error || 'Failed to save interface', 'error');
|
|
|
}
|
|
}
|
|
|
} catch (err) {
|
|
} catch (err) {
|
|
|
showToast('Error', err.message, 'error');
|
|
showToast('Error', err.message, 'error');
|
|
@@ -1311,6 +1368,7 @@ async function toggleWGInterfaceState(name, enable) {
|
|
|
if (res.ok) {
|
|
if (res.ok) {
|
|
|
showToast(enable ? 'Enabled' : 'Disabled', `${name} state updated`, 'info');
|
|
showToast(enable ? 'Enabled' : 'Disabled', `${name} state updated`, 'info');
|
|
|
loadWireGuard(false);
|
|
loadWireGuard(false);
|
|
|
|
|
+ fetchData(false);
|
|
|
}
|
|
}
|
|
|
} catch (err) {
|
|
} catch (err) {
|
|
|
showToast('Error', err.message, 'error');
|
|
showToast('Error', err.message, 'error');
|
|
@@ -1318,7 +1376,7 @@ async function toggleWGInterfaceState(name, enable) {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
async function deleteWGInterface(name) {
|
|
async function deleteWGInterface(name) {
|
|
|
- if (!confirm(`Are you sure you want to delete WireGuard interface ${name}?`)) return;
|
|
|
|
|
|
|
+ if (!confirm(`Are you sure you want to delete WireGuard interface ${name}? All associated peers will also be removed.`)) return;
|
|
|
try {
|
|
try {
|
|
|
const res = await fetch(`${API_BASE}/vpn/wireguard/interfaces/${encodeURIComponent(name)}`, { method: 'DELETE' });
|
|
const res = await fetch(`${API_BASE}/vpn/wireguard/interfaces/${encodeURIComponent(name)}`, { method: 'DELETE' });
|
|
|
if (res.ok) {
|
|
if (res.ok) {
|
|
@@ -1331,30 +1389,72 @@ async function deleteWGInterface(name) {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function openAddWGPeerModal() {
|
|
|
|
|
|
|
+function populateWGPeerIfaceSelect(selectedIface = '') {
|
|
|
const sel = document.getElementById('wg-peer-modal-iface');
|
|
const sel = document.getElementById('wg-peer-modal-iface');
|
|
|
|
|
+ if (!sel) return;
|
|
|
sel.innerHTML = '';
|
|
sel.innerHTML = '';
|
|
|
(state.wgInterfaces || []).forEach(i => {
|
|
(state.wgInterfaces || []).forEach(i => {
|
|
|
const opt = document.createElement('option');
|
|
const opt = document.createElement('option');
|
|
|
opt.value = i.name;
|
|
opt.value = i.name;
|
|
|
opt.innerText = `${i.name} (${i.address || 'No IP'})`;
|
|
opt.innerText = `${i.name} (${i.address || 'No IP'})`;
|
|
|
|
|
+ if (i.name === selectedIface) opt.selected = true;
|
|
|
sel.appendChild(opt);
|
|
sel.appendChild(opt);
|
|
|
});
|
|
});
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
|
|
+function openAddWGPeerModal() {
|
|
|
if ((state.wgInterfaces || []).length === 0) {
|
|
if ((state.wgInterfaces || []).length === 0) {
|
|
|
showToast('No Interfaces', 'Please add a WireGuard interface first before adding peers', 'warning');
|
|
showToast('No Interfaces', 'Please add a WireGuard interface first before adding peers', 'warning');
|
|
|
openAddWGInterfaceModal();
|
|
openAddWGInterfaceModal();
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ document.getElementById('wg-peer-modal-title').innerText = 'Add WireGuard Peer';
|
|
|
|
|
+ document.getElementById('wg-peer-modal-edit-orig-iface').value = '';
|
|
|
|
|
+ document.getElementById('wg-peer-modal-edit-orig-pubkey').value = '';
|
|
|
|
|
+
|
|
|
|
|
+ populateWGPeerIfaceSelect();
|
|
|
|
|
+
|
|
|
document.getElementById('wg-peer-modal-name').value = '';
|
|
document.getElementById('wg-peer-modal-name').value = '';
|
|
|
document.getElementById('wg-peer-modal-pubkey').value = '';
|
|
document.getElementById('wg-peer-modal-pubkey').value = '';
|
|
|
document.getElementById('wg-peer-modal-allowedips').value = '10.10.0.2/32';
|
|
document.getElementById('wg-peer-modal-allowedips').value = '10.10.0.2/32';
|
|
|
document.getElementById('wg-peer-modal-endpoint').value = '';
|
|
document.getElementById('wg-peer-modal-endpoint').value = '';
|
|
|
|
|
+ document.getElementById('wg-peer-modal-port').value = '';
|
|
|
document.getElementById('wg-peer-modal-keepalive').value = '25';
|
|
document.getElementById('wg-peer-modal-keepalive').value = '25';
|
|
|
document.getElementById('wg-peer-modal-preshared').value = '';
|
|
document.getElementById('wg-peer-modal-preshared').value = '';
|
|
|
document.getElementById('wg-peer-modal-disabled').checked = false;
|
|
document.getElementById('wg-peer-modal-disabled').checked = false;
|
|
|
|
|
|
|
|
|
|
+ document.getElementById('wg-peer-key-warning').classList.add('hidden');
|
|
|
|
|
+ document.getElementById('wg-peer-gen-key-box').classList.add('hidden');
|
|
|
|
|
+
|
|
|
|
|
+ document.getElementById('wg-peer-modal').classList.remove('hidden');
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function openEditWGPeerModal(ifaceName, pubKey) {
|
|
|
|
|
+ const peer = (state.wgPeers || []).find(p => p.interface === ifaceName && p.public_key === pubKey);
|
|
|
|
|
+ if (!peer) {
|
|
|
|
|
+ showToast('Not Found', 'Peer not found', 'warning');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ document.getElementById('wg-peer-modal-title').innerText = `Edit WireGuard Peer (${peer.name || ifaceName})`;
|
|
|
|
|
+ document.getElementById('wg-peer-modal-edit-orig-iface').value = peer.interface;
|
|
|
|
|
+ document.getElementById('wg-peer-modal-edit-orig-pubkey').value = peer.public_key;
|
|
|
|
|
+
|
|
|
|
|
+ populateWGPeerIfaceSelect(peer.interface);
|
|
|
|
|
+
|
|
|
|
|
+ document.getElementById('wg-peer-modal-name').value = peer.name || '';
|
|
|
|
|
+ document.getElementById('wg-peer-modal-pubkey').value = peer.public_key || '';
|
|
|
|
|
+ document.getElementById('wg-peer-modal-allowedips').value = (peer.allowed_ips || []).join(', ');
|
|
|
|
|
+ document.getElementById('wg-peer-modal-endpoint').value = peer.endpoint_address || '';
|
|
|
|
|
+ document.getElementById('wg-peer-modal-port').value = peer.endpoint_port || '';
|
|
|
|
|
+ document.getElementById('wg-peer-modal-keepalive').value = peer.persistent_keepalive ?? 25;
|
|
|
|
|
+ document.getElementById('wg-peer-modal-preshared').value = peer.preshared_key || '';
|
|
|
|
|
+ document.getElementById('wg-peer-modal-disabled').checked = !!peer.disabled;
|
|
|
|
|
+
|
|
|
|
|
+ document.getElementById('wg-peer-key-warning').classList.add('hidden');
|
|
|
|
|
+ document.getElementById('wg-peer-gen-key-box').classList.add('hidden');
|
|
|
|
|
+
|
|
|
document.getElementById('wg-peer-modal').classList.remove('hidden');
|
|
document.getElementById('wg-peer-modal').classList.remove('hidden');
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -1362,23 +1462,77 @@ function closeAddWGPeerModal() {
|
|
|
document.getElementById('wg-peer-modal').classList.add('hidden');
|
|
document.getElementById('wg-peer-modal').classList.add('hidden');
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+async function generatePeerKeypairHelper() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await fetch(`${API_BASE}/vpn/wireguard/genkeys`, { method: 'POST' });
|
|
|
|
|
+ const keys = await res.json();
|
|
|
|
|
+ if (res.ok) {
|
|
|
|
|
+ document.getElementById('wg-peer-modal-pubkey').value = keys.public_key;
|
|
|
|
|
+ document.getElementById('wg-peer-gen-privkey').value = keys.private_key;
|
|
|
|
|
+ document.getElementById('wg-peer-gen-key-box').classList.remove('hidden');
|
|
|
|
|
+ checkWGPeerKeyValidity();
|
|
|
|
|
+ showToast('Keypair Generated', 'Public key inserted and client private key ready to copy', 'info');
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ showToast('Error', 'Failed to generate keypair: ' + err.message, 'error');
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function copyGeneratedPeerPrivKey() {
|
|
|
|
|
+ const priv = document.getElementById('wg-peer-gen-privkey').value;
|
|
|
|
|
+ if (priv) copyToClipboard(priv, 'Client Private Key');
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function generatePeerPSK() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await fetch(`${API_BASE}/vpn/wireguard/genkeys`, { method: 'POST' });
|
|
|
|
|
+ const keys = await res.json();
|
|
|
|
|
+ if (res.ok && keys.private_key) {
|
|
|
|
|
+ document.getElementById('wg-peer-modal-preshared').value = keys.private_key;
|
|
|
|
|
+ showToast('PSK Generated', 'Generated 256-bit preshared key', 'info');
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ showToast('Error', err.message, 'error');
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function checkWGPeerKeyValidity() {
|
|
|
|
|
+ const ifaceName = document.getElementById('wg-peer-modal-iface').value;
|
|
|
|
|
+ const peerPubKey = document.getElementById('wg-peer-modal-pubkey').value.trim();
|
|
|
|
|
+ const warningEl = document.getElementById('wg-peer-key-warning');
|
|
|
|
|
+ if (!warningEl) return;
|
|
|
|
|
+
|
|
|
|
|
+ const targetIface = (state.wgInterfaces || []).find(i => i.name === ifaceName);
|
|
|
|
|
+ if (targetIface && targetIface.public_key && peerPubKey && targetIface.public_key === peerPubKey) {
|
|
|
|
|
+ warningEl.classList.remove('hidden');
|
|
|
|
|
+ } else {
|
|
|
|
|
+ warningEl.classList.add('hidden');
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
async function submitSaveWGPeer(e) {
|
|
async function submitSaveWGPeer(e) {
|
|
|
e.preventDefault();
|
|
e.preventDefault();
|
|
|
|
|
+ const origIface = document.getElementById('wg-peer-modal-edit-orig-iface').value.trim();
|
|
|
|
|
+ const origPubKey = document.getElementById('wg-peer-modal-edit-orig-pubkey').value.trim();
|
|
|
|
|
+
|
|
|
const iface = document.getElementById('wg-peer-modal-iface').value;
|
|
const iface = document.getElementById('wg-peer-modal-iface').value;
|
|
|
const name = document.getElementById('wg-peer-modal-name').value.trim();
|
|
const name = document.getElementById('wg-peer-modal-name').value.trim();
|
|
|
const pub = document.getElementById('wg-peer-modal-pubkey').value.trim();
|
|
const pub = document.getElementById('wg-peer-modal-pubkey').value.trim();
|
|
|
- const allowed = document.getElementById('wg-peer-modal-allowedips').value.split(',').map(s => s.trim());
|
|
|
|
|
- const ep = document.getElementById('wg-peer-modal-endpoint').value.trim();
|
|
|
|
|
- const keepalive = parseInt(document.getElementById('wg-peer-modal-keepalive').value, 10) || 25;
|
|
|
|
|
|
|
+ const allowed = document.getElementById('wg-peer-modal-allowedips').value
|
|
|
|
|
+ .split(',')
|
|
|
|
|
+ .map(s => s.trim())
|
|
|
|
|
+ .filter(s => s.length > 0);
|
|
|
|
|
+ const epHost = document.getElementById('wg-peer-modal-endpoint').value.trim();
|
|
|
|
|
+ const epPort = parseInt(document.getElementById('wg-peer-modal-port').value, 10) || 0;
|
|
|
|
|
+ const keepalive = parseInt(document.getElementById('wg-peer-modal-keepalive').value, 10) || 0;
|
|
|
const psk = document.getElementById('wg-peer-modal-preshared').value.trim();
|
|
const psk = document.getElementById('wg-peer-modal-preshared').value.trim();
|
|
|
const disabled = document.getElementById('wg-peer-modal-disabled').checked;
|
|
const disabled = document.getElementById('wg-peer-modal-disabled').checked;
|
|
|
|
|
|
|
|
- let epAddr = ep;
|
|
|
|
|
- let epPort = 0;
|
|
|
|
|
- if (ep.includes(':')) {
|
|
|
|
|
- const parts = ep.split(':');
|
|
|
|
|
- epAddr = parts[0];
|
|
|
|
|
- epPort = parseInt(parts[1], 10) || 0;
|
|
|
|
|
|
|
+ // Validation
|
|
|
|
|
+ const targetIface = (state.wgInterfaces || []).find(i => i.name === iface);
|
|
|
|
|
+ if (targetIface && targetIface.public_key === pub) {
|
|
|
|
|
+ showToast('Invalid Peer Key', 'Peer public key cannot be the same as the interface public key!', 'error');
|
|
|
|
|
+ return;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
try {
|
|
@@ -1386,21 +1540,45 @@ async function submitSaveWGPeer(e) {
|
|
|
method: 'POST',
|
|
method: 'POST',
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
body: JSON.stringify({
|
|
body: JSON.stringify({
|
|
|
|
|
+ orig_interface: origIface,
|
|
|
|
|
+ orig_public_key: origPubKey,
|
|
|
interface: iface,
|
|
interface: iface,
|
|
|
name,
|
|
name,
|
|
|
public_key: pub,
|
|
public_key: pub,
|
|
|
allowed_ips: allowed,
|
|
allowed_ips: allowed,
|
|
|
- endpoint_address: epAddr,
|
|
|
|
|
|
|
+ endpoint_address: epHost,
|
|
|
endpoint_port: epPort,
|
|
endpoint_port: epPort,
|
|
|
persistent_keepalive: keepalive,
|
|
persistent_keepalive: keepalive,
|
|
|
preshared_key: psk,
|
|
preshared_key: psk,
|
|
|
disabled
|
|
disabled
|
|
|
})
|
|
})
|
|
|
});
|
|
});
|
|
|
|
|
+ const data = await res.json();
|
|
|
if (res.ok) {
|
|
if (res.ok) {
|
|
|
- showToast('Peer Saved', `Peer ${name} added to ${iface}`, 'success');
|
|
|
|
|
|
|
+ showToast('Peer Saved', `Peer ${name} saved on ${iface}`, 'success');
|
|
|
closeAddWGPeerModal();
|
|
closeAddWGPeerModal();
|
|
|
loadWireGuard(false);
|
|
loadWireGuard(false);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ showToast('Error', data.error || 'Failed to save peer', 'error');
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ showToast('Error', err.message, 'error');
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function toggleWGPeerState(iface, pubkey, enable) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await fetch(`${API_BASE}/vpn/wireguard/peers/state`, {
|
|
|
|
|
+ method: 'POST',
|
|
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
+ body: JSON.stringify({ interface: iface, public_key: pubkey, enabled: enable })
|
|
|
|
|
+ });
|
|
|
|
|
+ const data = await res.json();
|
|
|
|
|
+ if (res.ok) {
|
|
|
|
|
+ showToast(enable ? 'Peer Enabled' : 'Peer Disabled', `Peer state updated on ${iface}`, 'info');
|
|
|
|
|
+ loadWireGuard(false);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ showToast('Error', data.error || 'Failed to update peer state', 'error');
|
|
|
}
|
|
}
|
|
|
} catch (err) {
|
|
} catch (err) {
|
|
|
showToast('Error', err.message, 'error');
|
|
showToast('Error', err.message, 'error');
|
|
@@ -1410,12 +1588,17 @@ async function submitSaveWGPeer(e) {
|
|
|
async function deleteWGPeer(iface, pubkey) {
|
|
async function deleteWGPeer(iface, pubkey) {
|
|
|
if (!confirm(`Are you sure you want to remove peer from ${iface}?`)) return;
|
|
if (!confirm(`Are you sure you want to remove peer from ${iface}?`)) return;
|
|
|
try {
|
|
try {
|
|
|
- const res = await fetch(`${API_BASE}/vpn/wireguard/peers/${encodeURIComponent(iface)}/${encodeURIComponent(pubkey)}`, {
|
|
|
|
|
- method: 'DELETE'
|
|
|
|
|
|
|
+ const res = await fetch(`${API_BASE}/vpn/wireguard/peers/delete`, {
|
|
|
|
|
+ method: 'POST',
|
|
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
+ body: JSON.stringify({ interface: iface, public_key: pubkey })
|
|
|
});
|
|
});
|
|
|
|
|
+ const data = await res.json();
|
|
|
if (res.ok) {
|
|
if (res.ok) {
|
|
|
showToast('Peer Deleted', 'WireGuard peer removed', 'success');
|
|
showToast('Peer Deleted', 'WireGuard peer removed', 'success');
|
|
|
loadWireGuard(false);
|
|
loadWireGuard(false);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ showToast('Error', data.error || 'Failed to remove peer', 'error');
|
|
|
}
|
|
}
|
|
|
} catch (err) {
|
|
} catch (err) {
|
|
|
showToast('Error', err.message, 'error');
|
|
showToast('Error', err.message, 'error');
|