- Add RPCD methods to mitmproxy: settings, save_settings, set_mode, setup_firewall, clear_firewall - Add apply_now parameter to tor-shield save_settings to restart service and apply iptables rules immediately - Update ACL files with new permissions - Add Save & Apply button to tor-shield settings page - Update api.js files to use correct RPCD method signatures Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
761 lines
26 KiB
JavaScript
761 lines
26 KiB
JavaScript
'use strict';
|
|
'require view';
|
|
'require secubox-theme/theme as Theme';
|
|
'require poll';
|
|
'require dom';
|
|
'require ui';
|
|
'require wireguard-dashboard/api as API';
|
|
'require wireguard-dashboard/qrcode as qrcode';
|
|
|
|
return view.extend({
|
|
title: _('WireGuard Peers'),
|
|
|
|
// Store private key in session storage for QR generation
|
|
storePrivateKey: function(publicKey, privateKey) {
|
|
try {
|
|
var stored = sessionStorage.getItem('wg_peer_keys');
|
|
var keys = stored ? JSON.parse(stored) : {};
|
|
keys[publicKey] = privateKey;
|
|
sessionStorage.setItem('wg_peer_keys', JSON.stringify(keys));
|
|
} catch (e) {
|
|
console.error('Failed to store private key:', e);
|
|
}
|
|
},
|
|
|
|
// Retrieve stored private key
|
|
getStoredPrivateKey: function(publicKey) {
|
|
try {
|
|
var stored = sessionStorage.getItem('wg_peer_keys');
|
|
if (stored) {
|
|
var keys = JSON.parse(stored);
|
|
return keys[publicKey] || null;
|
|
}
|
|
} catch (e) {}
|
|
return null;
|
|
},
|
|
|
|
load: function() {
|
|
return Promise.all([
|
|
API.getPeers(),
|
|
API.getInterfaces()
|
|
]);
|
|
},
|
|
|
|
render: function(data) {
|
|
var self = this;
|
|
// Handle RPC expect unwrapping - results may be array or object with .peers/.interfaces
|
|
var peersData = data[0] || [];
|
|
var interfacesData = data[1] || [];
|
|
var peers = Array.isArray(peersData) ? peersData : (peersData.peers || []);
|
|
var interfaces = Array.isArray(interfacesData) ? interfacesData : (interfacesData.interfaces || []);
|
|
var activePeers = peers.filter(function(p) { return p.status === 'active'; }).length;
|
|
|
|
var view = E('div', { 'class': 'cbi-map' }, [
|
|
E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }),
|
|
E('h2', {}, _('WireGuard Peers')),
|
|
E('div', { 'class': 'cbi-map-descr' },
|
|
_('Manage WireGuard VPN peers - add, configure, and monitor connected devices.')),
|
|
|
|
// Action Buttons
|
|
E('div', { 'class': 'cbi-section', 'style': 'margin-bottom: 1em;' }, [
|
|
E('div', { 'style': 'display: flex; gap: 1em; align-items: center;' }, [
|
|
E('button', {
|
|
'class': 'cbi-button cbi-button-action',
|
|
'click': L.bind(this.handleAddPeer, this, interfaces)
|
|
}, '+ ' + _('Add New Peer')),
|
|
E('span', { 'class': 'peers-active-count', 'style': 'margin-left: auto; font-weight: bold;' },
|
|
_('Active: %d / %d').format(activePeers, peers.length))
|
|
])
|
|
]),
|
|
|
|
// Peers Table
|
|
E('div', { 'class': 'cbi-section' }, [
|
|
E('h3', {}, _('Configured Peers')),
|
|
peers.length > 0 ?
|
|
E('div', { 'class': 'table-wrapper' }, [
|
|
E('table', { 'class': 'table', 'id': 'peers-table' }, [
|
|
E('thead', {}, [
|
|
E('tr', {}, [
|
|
E('th', {}, _('Interface')),
|
|
E('th', {}, _('Public Key')),
|
|
E('th', {}, _('Endpoint')),
|
|
E('th', {}, _('Allowed IPs')),
|
|
E('th', {}, _('Status')),
|
|
E('th', {}, _('Last Handshake')),
|
|
E('th', {}, _('RX / TX')),
|
|
E('th', {}, _('Actions'))
|
|
])
|
|
]),
|
|
E('tbody', {},
|
|
peers.map(function(peer) {
|
|
var statusColor = peer.status === 'active' ? '#28a745' :
|
|
peer.status === 'idle' ? '#ffc107' : '#6c757d';
|
|
var statusIcon = peer.status === 'active' ? '✓' :
|
|
peer.status === 'idle' ? '~' : '✗';
|
|
|
|
return E('tr', {}, [
|
|
E('td', {}, [
|
|
E('strong', {}, peer.interface)
|
|
]),
|
|
E('td', {}, [
|
|
E('code', { 'style': 'font-size: 0.85em;' },
|
|
peer.short_key + '...')
|
|
]),
|
|
E('td', {}, peer.endpoint !== '(none)' ? peer.endpoint : E('em', {}, 'roaming')),
|
|
E('td', {}, [
|
|
E('code', { 'style': 'font-size: 0.85em;' }, peer.allowed_ips || 'N/A')
|
|
]),
|
|
E('td', {}, [
|
|
E('span', {
|
|
'class': 'badge',
|
|
'style': 'background: ' + statusColor + '; color: white; padding: 0.25em 0.6em; border-radius: 3px;'
|
|
}, statusIcon + ' ' + peer.status)
|
|
]),
|
|
E('td', {}, API.formatLastHandshake(peer.handshake_ago)),
|
|
E('td', {}, [
|
|
E('div', { 'style': 'font-size: 0.9em;' }, [
|
|
E('div', {}, '↓ ' + API.formatBytes(peer.rx_bytes)),
|
|
E('div', {}, '↑ ' + API.formatBytes(peer.tx_bytes))
|
|
])
|
|
]),
|
|
E('td', {}, [
|
|
E('button', {
|
|
'class': 'cbi-button cbi-button-action',
|
|
'style': 'margin: 2px;',
|
|
'click': L.bind(self.handleShowQR, self, peer, interfaces)
|
|
}, _('QR Code')),
|
|
E('button', {
|
|
'class': 'cbi-button cbi-button-apply',
|
|
'style': 'margin: 2px;',
|
|
'click': L.bind(self.handleDownloadConfig, self, peer, interfaces)
|
|
}, _('Config')),
|
|
E('button', {
|
|
'class': 'cbi-button cbi-button-negative',
|
|
'style': 'margin: 2px;',
|
|
'click': L.bind(self.handleDeletePeer, self, peer)
|
|
}, _('Delete'))
|
|
])
|
|
]);
|
|
})
|
|
)
|
|
])
|
|
]) :
|
|
E('div', { 'style': 'text-align: center; padding: 3em; background: #f8f9fa; border-radius: 4px;' }, [
|
|
E('div', { 'style': 'font-size: 3em; margin-bottom: 0.5em;' }, '👥'),
|
|
E('h3', {}, _('No Peers Configured')),
|
|
E('p', { 'style': 'color: #666;' },
|
|
_('Add peers to allow devices to connect to your WireGuard VPN.')),
|
|
E('button', {
|
|
'class': 'cbi-button cbi-button-action',
|
|
'style': 'margin-top: 1em;',
|
|
'click': L.bind(this.handleAddPeer, this, interfaces)
|
|
}, '+ ' + _('Add First Peer'))
|
|
])
|
|
])
|
|
]);
|
|
|
|
// Setup auto-refresh every 5 seconds
|
|
poll.add(L.bind(function() {
|
|
return API.getPeers().then(L.bind(function(data) {
|
|
var newPeers = (data || {}).peers || [];
|
|
var table = document.getElementById('peers-table');
|
|
if (!table) return;
|
|
|
|
var tbody = table.querySelector('tbody');
|
|
if (!tbody) return;
|
|
|
|
// Update existing rows
|
|
newPeers.forEach(function(peer, idx) {
|
|
var row = tbody.children[idx];
|
|
if (!row) return;
|
|
|
|
var cells = row.querySelectorAll('td');
|
|
if (cells.length < 7) return;
|
|
|
|
// Update status (cell 4)
|
|
var statusColor = peer.status === 'active' ? '#28a745' :
|
|
peer.status === 'idle' ? '#ffc107' : '#6c757d';
|
|
var statusIcon = peer.status === 'active' ? '✓' :
|
|
peer.status === 'idle' ? '~' : '✗';
|
|
var statusSpan = cells[4].querySelector('.badge');
|
|
if (statusSpan) {
|
|
statusSpan.style.background = statusColor;
|
|
statusSpan.textContent = statusIcon + ' ' + peer.status;
|
|
}
|
|
|
|
// Update last handshake (cell 5)
|
|
cells[5].textContent = API.formatLastHandshake(peer.handshake_ago);
|
|
|
|
// Update RX/TX (cell 6)
|
|
var trafficDiv = cells[6].querySelector('div');
|
|
if (trafficDiv) {
|
|
trafficDiv.innerHTML = '<div>↓ ' + API.formatBytes(peer.rx_bytes) + '</div>' +
|
|
'<div>↑ ' + API.formatBytes(peer.tx_bytes) + '</div>';
|
|
}
|
|
});
|
|
|
|
// Update active count
|
|
var activePeers = newPeers.filter(function(p) { return p.status === 'active'; }).length;
|
|
var countSpan = document.querySelector('.peers-active-count');
|
|
if (countSpan) {
|
|
countSpan.textContent = _('Active: %d / %d').format(activePeers, newPeers.length);
|
|
}
|
|
}, this));
|
|
}, this), 5);
|
|
|
|
return view;
|
|
},
|
|
|
|
handleAddPeer: function(interfaces, ev) {
|
|
var self = this;
|
|
|
|
if (interfaces.length === 0) {
|
|
ui.addNotification(null, E('p', _('No WireGuard interfaces found. Please create an interface first.')), 'error');
|
|
return;
|
|
}
|
|
|
|
// Generate keys
|
|
API.generateKeys().then(function(keys) {
|
|
var selectedIface = interfaces[0].name;
|
|
var generatedPrivKey = keys.private_key;
|
|
var generatedPubKey = keys.public_key;
|
|
var generatedPSK = keys.preshared_key;
|
|
|
|
var formElements = [
|
|
E('p', {}, _('Configure a new peer for your WireGuard VPN.')),
|
|
|
|
E('div', { 'class': 'cbi-value' }, [
|
|
E('label', { 'class': 'cbi-value-title' }, _('Interface')),
|
|
E('div', { 'class': 'cbi-value-field' }, [
|
|
E('select', { 'id': 'peer-interface', 'class': 'cbi-input-select' },
|
|
interfaces.map(function(iface) {
|
|
return E('option', { 'value': iface.name }, iface.name);
|
|
})
|
|
)
|
|
])
|
|
]),
|
|
|
|
E('div', { 'class': 'cbi-value' }, [
|
|
E('label', { 'class': 'cbi-value-title' }, _('Peer Name')),
|
|
E('div', { 'class': 'cbi-value-field' }, [
|
|
E('input', {
|
|
'type': 'text',
|
|
'id': 'peer-name',
|
|
'class': 'cbi-input-text',
|
|
'placeholder': 'my-phone',
|
|
'value': 'peer_' + generatedPubKey.substring(0, 8)
|
|
})
|
|
])
|
|
]),
|
|
|
|
E('div', { 'class': 'cbi-value' }, [
|
|
E('label', { 'class': 'cbi-value-title' }, _('Public Key (generated)')),
|
|
E('div', { 'class': 'cbi-value-field' }, [
|
|
E('input', {
|
|
'type': 'text',
|
|
'id': 'peer-pubkey',
|
|
'class': 'cbi-input-text',
|
|
'value': generatedPubKey,
|
|
'readonly': true
|
|
})
|
|
])
|
|
]),
|
|
|
|
E('div', { 'class': 'cbi-value' }, [
|
|
E('label', { 'class': 'cbi-value-title' }, _('Allowed IPs')),
|
|
E('div', { 'class': 'cbi-value-field' }, [
|
|
E('input', {
|
|
'type': 'text',
|
|
'id': 'peer-allowed-ips',
|
|
'class': 'cbi-input-text',
|
|
'placeholder': '10.0.0.2/32',
|
|
'value': '10.0.0.2/32'
|
|
}),
|
|
E('div', { 'class': 'cbi-value-description' },
|
|
_('IP address(es) allowed for this peer (comma-separated)'))
|
|
])
|
|
]),
|
|
|
|
E('div', { 'class': 'cbi-value' }, [
|
|
E('label', { 'class': 'cbi-value-title' }, _('Endpoint (optional)')),
|
|
E('div', { 'class': 'cbi-value-field' }, [
|
|
E('input', {
|
|
'type': 'text',
|
|
'id': 'peer-endpoint',
|
|
'class': 'cbi-input-text',
|
|
'placeholder': 'peer.example.com:51820'
|
|
}),
|
|
E('div', { 'class': 'cbi-value-description' },
|
|
_('Endpoint for outbound connections (leave empty for road warrior)'))
|
|
])
|
|
]),
|
|
|
|
E('div', { 'class': 'cbi-value' }, [
|
|
E('label', { 'class': 'cbi-value-title' }, _('Persistent Keepalive')),
|
|
E('div', { 'class': 'cbi-value-field' }, [
|
|
E('input', {
|
|
'type': 'number',
|
|
'id': 'peer-keepalive',
|
|
'class': 'cbi-input-text',
|
|
'value': '25',
|
|
'min': '0',
|
|
'max': '300'
|
|
}),
|
|
E('div', { 'class': 'cbi-value-description' },
|
|
_('Seconds between keepalive packets (0 = disabled, 25 recommended for NAT)'))
|
|
])
|
|
]),
|
|
|
|
E('div', { 'class': 'cbi-value' }, [
|
|
E('label', { 'class': 'cbi-value-title' }, [
|
|
E('input', {
|
|
'type': 'checkbox',
|
|
'id': 'peer-use-psk',
|
|
'checked': false
|
|
}),
|
|
' ' + _('Use Preshared Key (PSK)')
|
|
]),
|
|
E('div', { 'class': 'cbi-value-description' },
|
|
_('Optional post-quantum security (recommended)'))
|
|
]),
|
|
|
|
E('input', { 'type': 'hidden', 'id': 'peer-privkey', 'value': generatedPrivKey }),
|
|
E('input', { 'type': 'hidden', 'id': 'peer-psk', 'value': generatedPSK }),
|
|
|
|
E('div', { 'style': 'margin-top: 1em; padding: 0.75em; background: #fff3cd; border-radius: 4px;' }, [
|
|
E('strong', {}, _('Note:')),
|
|
' ',
|
|
_('After creating the peer, you can generate a QR code or download the client configuration.')
|
|
]),
|
|
|
|
E('div', { 'class': 'right', 'style': 'margin-top: 1em;' }, [
|
|
E('button', {
|
|
'class': 'btn',
|
|
'click': ui.hideModal
|
|
}, _('Cancel')),
|
|
' ',
|
|
E('button', {
|
|
'class': 'btn cbi-button-action',
|
|
'click': function() {
|
|
var iface = document.getElementById('peer-interface').value;
|
|
var name = document.getElementById('peer-name').value;
|
|
var pubkey = document.getElementById('peer-pubkey').value;
|
|
var allowed_ips = document.getElementById('peer-allowed-ips').value;
|
|
var endpoint = document.getElementById('peer-endpoint').value || '';
|
|
var keepalive = document.getElementById('peer-keepalive').value;
|
|
var use_psk = document.getElementById('peer-use-psk').checked;
|
|
var psk = use_psk ? document.getElementById('peer-psk').value : '';
|
|
|
|
if (!name || !pubkey || !allowed_ips) {
|
|
ui.addNotification(null, E('p', _('Please fill in all required fields')), 'error');
|
|
return;
|
|
}
|
|
|
|
ui.hideModal();
|
|
ui.showModal(_('Adding Peer'), [
|
|
E('p', { 'class': 'spinning' }, _('Adding peer configuration...'))
|
|
]);
|
|
|
|
var privkey = document.getElementById('peer-privkey').value;
|
|
|
|
API.addPeer(iface, name, allowed_ips, pubkey, psk, endpoint, keepalive).then(function(result) {
|
|
ui.hideModal();
|
|
if (result.success) {
|
|
// Store private key for QR generation
|
|
self.storePrivateKey(pubkey, privkey);
|
|
ui.addNotification(null, E('p', result.message || _('Peer added successfully')), 'info');
|
|
|
|
// Offer to generate QR code immediately
|
|
ui.showModal(_('Peer Created Successfully'), [
|
|
E('p', {}, _('The peer has been added. Would you like to generate a QR code for mobile setup?')),
|
|
E('div', { 'style': 'background: #d4edda; padding: 1em; border-radius: 4px; margin: 1em 0;' }, [
|
|
E('strong', {}, _('Private Key Stored')),
|
|
E('p', { 'style': 'margin: 0.5em 0 0 0; font-size: 0.9em;' },
|
|
_('The private key has been temporarily stored in your browser session for QR generation.'))
|
|
]),
|
|
E('div', { 'class': 'right', 'style': 'margin-top: 1em;' }, [
|
|
E('button', {
|
|
'class': 'btn',
|
|
'click': function() {
|
|
ui.hideModal();
|
|
window.location.reload();
|
|
}
|
|
}, _('Skip')),
|
|
' ',
|
|
E('button', {
|
|
'class': 'btn cbi-button-action',
|
|
'click': function() {
|
|
ui.hideModal();
|
|
// Find the interface for QR generation
|
|
var ifaceObj = interfaces.find(function(i) { return i.name === iface; });
|
|
self.promptForEndpointAndShowQR({
|
|
public_key: pubkey,
|
|
short_key: pubkey.substring(0, 8),
|
|
allowed_ips: allowed_ips,
|
|
interface: iface
|
|
}, ifaceObj, privkey);
|
|
}
|
|
}, _('Generate QR Code'))
|
|
])
|
|
]);
|
|
} else {
|
|
ui.addNotification(null, E('p', result.error || _('Failed to add peer')), 'error');
|
|
}
|
|
}).catch(function(err) {
|
|
ui.hideModal();
|
|
ui.addNotification(null, E('p', _('Error: %s').format(err.message || err)), 'error');
|
|
});
|
|
}
|
|
}, _('Add Peer'))
|
|
])
|
|
];
|
|
|
|
ui.showModal(_('Add New Peer'), formElements, 'cbi-modal');
|
|
}).catch(function(err) {
|
|
ui.addNotification(null, E('p', _('Error generating keys: %s').format(err.message || err)), 'error');
|
|
});
|
|
},
|
|
|
|
promptForEndpointAndShowQR: function(peer, ifaceObj, privateKey) {
|
|
var self = this;
|
|
var savedEndpoint = sessionStorage.getItem('wg_server_endpoint') || '';
|
|
|
|
ui.showModal(_('Server Endpoint'), [
|
|
E('p', {}, _('Enter the public IP or hostname of this WireGuard server:')),
|
|
E('div', { 'class': 'cbi-value' }, [
|
|
E('label', { 'class': 'cbi-value-title' }, _('Server Endpoint')),
|
|
E('div', { 'class': 'cbi-value-field' }, [
|
|
E('input', {
|
|
'type': 'text',
|
|
'id': 'qr-server-endpoint',
|
|
'class': 'cbi-input-text',
|
|
'placeholder': 'vpn.example.com or 203.0.113.1',
|
|
'value': savedEndpoint
|
|
})
|
|
])
|
|
]),
|
|
E('div', { 'class': 'right', 'style': 'margin-top: 1em;' }, [
|
|
E('button', {
|
|
'class': 'btn',
|
|
'click': ui.hideModal
|
|
}, _('Cancel')),
|
|
' ',
|
|
E('button', {
|
|
'class': 'btn cbi-button-action',
|
|
'click': function() {
|
|
var endpoint = document.getElementById('qr-server-endpoint').value.trim();
|
|
if (!endpoint) {
|
|
ui.addNotification(null, E('p', _('Please enter server endpoint')), 'error');
|
|
return;
|
|
}
|
|
sessionStorage.setItem('wg_server_endpoint', endpoint);
|
|
ui.hideModal();
|
|
self.generateAndShowQR(peer, ifaceObj, privateKey, endpoint);
|
|
}
|
|
}, _('Generate QR'))
|
|
])
|
|
]);
|
|
},
|
|
|
|
generateAndShowQR: function(peer, ifaceObj, privateKey, serverEndpoint) {
|
|
var self = this;
|
|
|
|
// Build WireGuard client config
|
|
var config = '[Interface]\n' +
|
|
'PrivateKey = ' + privateKey + '\n' +
|
|
'Address = ' + (peer.allowed_ips || '10.0.0.2/32') + '\n' +
|
|
'DNS = 1.1.1.1, 1.0.0.1\n\n' +
|
|
'[Peer]\n' +
|
|
'PublicKey = ' + (ifaceObj.public_key || '') + '\n' +
|
|
'Endpoint = ' + serverEndpoint + ':' + (ifaceObj.listen_port || 51820) + '\n' +
|
|
'AllowedIPs = 0.0.0.0/0, ::/0\n' +
|
|
'PersistentKeepalive = 25';
|
|
|
|
// First try backend QR generation
|
|
API.generateQR(peer.interface, peer.public_key, privateKey, serverEndpoint).then(function(result) {
|
|
if (result && result.qrcode && !result.error) {
|
|
self.displayQRModal(peer, result.qrcode, config, false);
|
|
} else {
|
|
// Fall back to JavaScript QR generation
|
|
var svg = qrcode.generateSVG(config, 250);
|
|
if (svg) {
|
|
self.displayQRModal(peer, svg, config, true);
|
|
} else {
|
|
ui.addNotification(null, E('p', _('Failed to generate QR code')), 'error');
|
|
}
|
|
}
|
|
}).catch(function(err) {
|
|
// Fall back to JavaScript QR generation
|
|
var svg = qrcode.generateSVG(config, 250);
|
|
if (svg) {
|
|
self.displayQRModal(peer, svg, config, true);
|
|
} else {
|
|
ui.addNotification(null, E('p', _('Failed to generate QR code')), 'error');
|
|
}
|
|
});
|
|
},
|
|
|
|
displayQRModal: function(peer, qrData, config, isSVG) {
|
|
var qrElement;
|
|
|
|
if (isSVG) {
|
|
qrElement = E('div', { 'style': 'display: inline-block;' });
|
|
qrElement.innerHTML = qrData;
|
|
} else {
|
|
qrElement = E('img', {
|
|
'src': qrData,
|
|
'alt': 'WireGuard QR Code',
|
|
'style': 'max-width: 250px; max-height: 250px;'
|
|
});
|
|
}
|
|
|
|
ui.showModal(_('WireGuard QR Code'), [
|
|
E('div', { 'style': 'text-align: center;' }, [
|
|
E('h4', {}, peer.interface + ' - ' + (peer.short_key || peer.public_key.substring(0, 8))),
|
|
E('div', { 'style': 'background: white; padding: 20px; border-radius: 12px; display: inline-block; margin: 20px 0;' }, [
|
|
qrElement
|
|
]),
|
|
E('p', { 'style': 'color: #666;' }, _('Scan with WireGuard app on your mobile device')),
|
|
E('div', { 'style': 'display: flex; gap: 10px; justify-content: center; margin: 1em 0;' }, [
|
|
E('button', {
|
|
'class': 'btn',
|
|
'click': function() {
|
|
navigator.clipboard.writeText(config).then(function() {
|
|
ui.addNotification(null, E('p', _('Configuration copied to clipboard')), 'info');
|
|
});
|
|
}
|
|
}, _('Copy Config')),
|
|
E('button', {
|
|
'class': 'btn cbi-button-action',
|
|
'click': function() {
|
|
var blob = new Blob([config], { type: 'text/plain' });
|
|
var url = URL.createObjectURL(blob);
|
|
var a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = peer.interface + '-peer.conf';
|
|
a.click();
|
|
URL.revokeObjectURL(url);
|
|
}
|
|
}, _('Download .conf'))
|
|
]),
|
|
E('details', { 'style': 'text-align: left; margin-top: 1em;' }, [
|
|
E('summary', { 'style': 'cursor: pointer; color: #06b6d4;' }, _('Show configuration')),
|
|
E('pre', { 'style': 'background: #f8f9fa; padding: 12px; border-radius: 8px; font-size: 11px; margin-top: 10px;' }, config)
|
|
])
|
|
]),
|
|
E('div', { 'class': 'right' }, [
|
|
E('button', {
|
|
'class': 'btn',
|
|
'click': ui.hideModal
|
|
}, _('Close'))
|
|
])
|
|
]);
|
|
},
|
|
|
|
handleShowQR: function(peer, interfaces, ev) {
|
|
var self = this;
|
|
var privateKey = this.getStoredPrivateKey(peer.public_key);
|
|
var ifaceObj = interfaces.find(function(i) { return i.name === peer.interface; }) || {};
|
|
|
|
if (!privateKey) {
|
|
// Private key not stored - ask user to input it
|
|
ui.showModal(_('Private Key Required'), [
|
|
E('p', {}, _('To generate a QR code, the peer\'s private key is needed.')),
|
|
E('p', { 'style': 'color: #666; font-size: 0.9em;' },
|
|
_('Private keys are only stored in your browser session immediately after peer creation. If you closed or refreshed the page, you\'ll need to enter it manually.')),
|
|
E('div', { 'class': 'cbi-value' }, [
|
|
E('label', { 'class': 'cbi-value-title' }, _('Private Key')),
|
|
E('div', { 'class': 'cbi-value-field' }, [
|
|
E('input', {
|
|
'type': 'text',
|
|
'id': 'manual-private-key',
|
|
'class': 'cbi-input-text',
|
|
'placeholder': 'Base64 private key (44 characters)',
|
|
'style': 'font-family: monospace;'
|
|
})
|
|
])
|
|
]),
|
|
E('div', { 'class': 'right', 'style': 'margin-top: 1em;' }, [
|
|
E('button', {
|
|
'class': 'btn',
|
|
'click': ui.hideModal
|
|
}, _('Cancel')),
|
|
' ',
|
|
E('button', {
|
|
'class': 'btn cbi-button-action',
|
|
'click': function() {
|
|
var key = document.getElementById('manual-private-key').value.trim();
|
|
if (!key || key.length !== 44) {
|
|
ui.addNotification(null, E('p', _('Please enter a valid private key (44 characters)')), 'error');
|
|
return;
|
|
}
|
|
// Store for future use
|
|
self.storePrivateKey(peer.public_key, key);
|
|
ui.hideModal();
|
|
self.promptForEndpointAndShowQR(peer, ifaceObj, key);
|
|
}
|
|
}, _('Continue'))
|
|
])
|
|
]);
|
|
return;
|
|
}
|
|
|
|
this.promptForEndpointAndShowQR(peer, ifaceObj, privateKey);
|
|
},
|
|
|
|
handleDownloadConfig: function(peer, interfaces, ev) {
|
|
var self = this;
|
|
var privateKey = this.getStoredPrivateKey(peer.public_key);
|
|
var ifaceObj = interfaces.find(function(i) { return i.name === peer.interface; }) || {};
|
|
|
|
var showConfigModal = function(privKey) {
|
|
var savedEndpoint = sessionStorage.getItem('wg_server_endpoint') || '';
|
|
|
|
ui.showModal(_('Download Configuration'), [
|
|
E('p', {}, _('Enter the server endpoint to generate the client configuration:')),
|
|
E('div', { 'class': 'cbi-value' }, [
|
|
E('label', { 'class': 'cbi-value-title' }, _('Server Endpoint')),
|
|
E('div', { 'class': 'cbi-value-field' }, [
|
|
E('input', {
|
|
'type': 'text',
|
|
'id': 'cfg-server-endpoint',
|
|
'class': 'cbi-input-text',
|
|
'placeholder': 'vpn.example.com or 203.0.113.1',
|
|
'value': savedEndpoint
|
|
})
|
|
])
|
|
]),
|
|
E('div', { 'class': 'right', 'style': 'margin-top: 1em;' }, [
|
|
E('button', {
|
|
'class': 'btn',
|
|
'click': ui.hideModal
|
|
}, _('Cancel')),
|
|
' ',
|
|
E('button', {
|
|
'class': 'btn cbi-button-action',
|
|
'click': function() {
|
|
var endpoint = document.getElementById('cfg-server-endpoint').value.trim();
|
|
if (!endpoint) {
|
|
ui.addNotification(null, E('p', _('Please enter server endpoint')), 'error');
|
|
return;
|
|
}
|
|
sessionStorage.setItem('wg_server_endpoint', endpoint);
|
|
|
|
var config = '[Interface]\n' +
|
|
'PrivateKey = ' + privKey + '\n' +
|
|
'Address = ' + (peer.allowed_ips || '10.0.0.2/32') + '\n' +
|
|
'DNS = 1.1.1.1, 1.0.0.1\n\n' +
|
|
'[Peer]\n' +
|
|
'PublicKey = ' + (ifaceObj.public_key || '') + '\n' +
|
|
'Endpoint = ' + endpoint + ':' + (ifaceObj.listen_port || 51820) + '\n' +
|
|
'AllowedIPs = 0.0.0.0/0, ::/0\n' +
|
|
'PersistentKeepalive = 25';
|
|
|
|
var blob = new Blob([config], { type: 'text/plain' });
|
|
var url = URL.createObjectURL(blob);
|
|
var a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = peer.interface + '-' + (peer.short_key || 'peer') + '.conf';
|
|
a.click();
|
|
URL.revokeObjectURL(url);
|
|
|
|
ui.hideModal();
|
|
ui.addNotification(null, E('p', _('Configuration file downloaded')), 'info');
|
|
}
|
|
}, _('Download'))
|
|
])
|
|
]);
|
|
};
|
|
|
|
if (!privateKey) {
|
|
// Private key not stored - ask user to input it
|
|
ui.showModal(_('Private Key Required'), [
|
|
E('p', {}, _('Enter the peer\'s private key to generate the configuration file:')),
|
|
E('div', { 'class': 'cbi-value' }, [
|
|
E('label', { 'class': 'cbi-value-title' }, _('Private Key')),
|
|
E('div', { 'class': 'cbi-value-field' }, [
|
|
E('input', {
|
|
'type': 'text',
|
|
'id': 'cfg-private-key',
|
|
'class': 'cbi-input-text',
|
|
'placeholder': 'Base64 private key (44 characters)',
|
|
'style': 'font-family: monospace;'
|
|
})
|
|
])
|
|
]),
|
|
E('div', { 'class': 'right', 'style': 'margin-top: 1em;' }, [
|
|
E('button', {
|
|
'class': 'btn',
|
|
'click': ui.hideModal
|
|
}, _('Cancel')),
|
|
' ',
|
|
E('button', {
|
|
'class': 'btn cbi-button-action',
|
|
'click': function() {
|
|
var key = document.getElementById('cfg-private-key').value.trim();
|
|
if (!key || key.length !== 44) {
|
|
ui.addNotification(null, E('p', _('Please enter a valid private key (44 characters)')), 'error');
|
|
return;
|
|
}
|
|
self.storePrivateKey(peer.public_key, key);
|
|
ui.hideModal();
|
|
showConfigModal(key);
|
|
}
|
|
}, _('Continue'))
|
|
])
|
|
]);
|
|
return;
|
|
}
|
|
|
|
showConfigModal(privateKey);
|
|
},
|
|
|
|
handleDeletePeer: function(peer, ev) {
|
|
var self = this;
|
|
|
|
ui.showModal(_('Delete Peer'), [
|
|
E('p', {}, _('Are you sure you want to delete this peer?')),
|
|
E('div', { 'style': 'background: #f8f9fa; padding: 1em; border-radius: 4px; margin: 1em 0;' }, [
|
|
E('strong', {}, _('Public Key:')),
|
|
E('br'),
|
|
E('code', {}, peer.public_key)
|
|
]),
|
|
E('p', { 'style': 'color: #dc3545;' },
|
|
_('This action cannot be undone.')),
|
|
E('div', { 'class': 'right' }, [
|
|
E('button', {
|
|
'class': 'btn',
|
|
'click': ui.hideModal
|
|
}, _('Cancel')),
|
|
' ',
|
|
E('button', {
|
|
'class': 'btn cbi-button-negative',
|
|
'click': function() {
|
|
ui.hideModal();
|
|
ui.showModal(_('Deleting Peer'), [
|
|
E('p', { 'class': 'spinning' }, _('Removing peer...'))
|
|
]);
|
|
|
|
API.removePeer(peer.interface, peer.public_key).then(function(result) {
|
|
ui.hideModal();
|
|
if (result.success) {
|
|
ui.addNotification(null, E('p', result.message || _('Peer deleted successfully')), 'info');
|
|
window.location.reload();
|
|
} else {
|
|
ui.addNotification(null, E('p', result.error || _('Failed to delete peer')), 'error');
|
|
}
|
|
}).catch(function(err) {
|
|
ui.hideModal();
|
|
ui.addNotification(null, E('p', _('Error: %s').format(err.message || err)), 'error');
|
|
});
|
|
}
|
|
}, _('Delete Peer'))
|
|
])
|
|
]);
|
|
},
|
|
|
|
handleSaveApply: null,
|
|
handleSave: null,
|
|
handleReset: null
|
|
});
|