diff --git a/package/secubox/luci-app-wireguard-dashboard/Makefile b/package/secubox/luci-app-wireguard-dashboard/Makefile index 218bd0d3..05adf599 100644 --- a/package/secubox/luci-app-wireguard-dashboard/Makefile +++ b/package/secubox/luci-app-wireguard-dashboard/Makefile @@ -33,7 +33,7 @@ PKG_FILE_MODES:=/usr/libexec/rpcd/luci.wireguard-dashboard:root:root:755 include $(TOPDIR)/feeds/luci/luci.mk define Package/$(PKG_NAME)/conffiles -/etc/config/wireguard-dashboard +/etc/config/wireguard_dashboard endef # call BuildPackage - OpenWrt buildroot diff --git a/package/secubox/luci-app-wireguard-dashboard/htdocs/luci-static/resources/view/wireguard-dashboard/peers.js b/package/secubox/luci-app-wireguard-dashboard/htdocs/luci-static/resources/view/wireguard-dashboard/peers.js index 99ccec1a..62c11da2 100644 --- a/package/secubox/luci-app-wireguard-dashboard/htdocs/luci-static/resources/view/wireguard-dashboard/peers.js +++ b/package/secubox/luci-app-wireguard-dashboard/htdocs/luci-static/resources/view/wireguard-dashboard/peers.js @@ -37,7 +37,8 @@ return view.extend({ load: function() { return Promise.all([ API.getPeers(), - API.getInterfaces() + API.getInterfaces(), + API.getEndpoints() ]); }, @@ -46,10 +47,13 @@ return view.extend({ // Handle RPC expect unwrapping - results may be array or object with .peers/.interfaces var peersData = data[0] || []; var interfacesData = data[1] || []; + var endpointData = data[2] || {}; 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; + this.endpointData = endpointData; + var view = E('div', { 'class': 'cbi-map' }, [ E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }), E('h2', {}, _('WireGuard Peers')), @@ -418,20 +422,23 @@ return view.extend({ promptForEndpointAndShowQR: function(peer, ifaceObj, privateKey) { var self = this; - var savedEndpoint = sessionStorage.getItem('wg_server_endpoint') || ''; + var endpointData = this.endpointData || {}; + var endpoints = endpointData.endpoints || []; + + // If exactly one endpoint exists, skip the prompt + if (endpoints.length === 1) { + self.generateAndShowQR(peer, ifaceObj, privateKey, endpoints[0].address); + return; + } + + var selector = API.buildEndpointSelector(endpointData, 'qr-server-endpoint'); ui.showModal(_('Server Endpoint'), [ - E('p', {}, _('Enter the public IP or hostname of this WireGuard server:')), + E('p', {}, _('Select or 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 - }) + selector ]) ]), E('div', { 'class': 'right', 'style': 'margin-top: 1em;' }, [ @@ -443,12 +450,11 @@ return view.extend({ E('button', { 'class': 'btn cbi-button-action', 'click': function() { - var endpoint = document.getElementById('qr-server-endpoint').value.trim(); + var endpoint = API.getEndpointValue('qr-server-endpoint'); 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); } @@ -636,6 +642,8 @@ return view.extend({ var self = this; var privateKey = this.getStoredPrivateKey(peer.public_key); var ifaceObj = interfaces.find(function(i) { return i.name === peer.interface; }) || {}; + var endpointData = this.endpointData || {}; + var endpoints = endpointData.endpoints || []; var downloadConfig = function(config) { var blob = new Blob([config], { type: 'text/plain' }); @@ -648,21 +656,46 @@ return view.extend({ ui.addNotification(null, E('p', _('Configuration file downloaded')), 'info'); }; + var doDownload = function(privKey, endpoint) { + if (privKey) { + 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'; + downloadConfig(config); + } else { + API.generateConfig(peer.interface, peer.public_key, '', endpoint).then(function(result) { + if (result && result.config && !result.error) { + downloadConfig(result.config); + } else { + ui.addNotification(null, E('p', result.error || _('Failed to generate config')), 'error'); + } + }).catch(function(err) { + ui.addNotification(null, E('p', _('Error: %s').format(err.message || err)), 'error'); + }); + } + }; + var showConfigModal = function(privKey) { - var savedEndpoint = sessionStorage.getItem('wg_server_endpoint') || ''; + // If exactly one endpoint exists, skip the prompt + if (endpoints.length === 1) { + doDownload(privKey, endpoints[0].address); + return; + } + + var selector = API.buildEndpointSelector(endpointData, 'cfg-server-endpoint'); ui.showModal(_('Download Configuration'), [ - E('p', {}, _('Enter the server endpoint to generate the client configuration:')), + E('p', {}, _('Select or 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 - }) + selector ]) ]), E('div', { 'class': 'right', 'style': 'margin-top: 1em;' }, [ @@ -674,37 +707,13 @@ return view.extend({ E('button', { 'class': 'btn cbi-button-action', 'click': function() { - var endpoint = document.getElementById('cfg-server-endpoint').value.trim(); + var endpoint = API.getEndpointValue('cfg-server-endpoint'); if (!endpoint) { ui.addNotification(null, E('p', _('Please enter server endpoint')), 'error'); return; } - sessionStorage.setItem('wg_server_endpoint', endpoint); ui.hideModal(); - - if (privKey) { - 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'; - downloadConfig(config); - } else { - // Use backend to generate config (it has the stored key) - API.generateConfig(peer.interface, peer.public_key, '', endpoint).then(function(result) { - if (result && result.config && !result.error) { - downloadConfig(result.config); - } else { - ui.addNotification(null, E('p', result.error || _('Failed to generate config')), 'error'); - } - }).catch(function(err) { - ui.addNotification(null, E('p', _('Error: %s').format(err.message || err)), 'error'); - }); - } + doDownload(privKey, endpoint); } }, _('Download')) ]) @@ -715,10 +724,8 @@ return view.extend({ // Try backend first - it may have the stored key API.generateConfig(peer.interface, peer.public_key, '', 'test').then(function(result) { if (result && result.config && !result.error) { - // Backend has the key, show config modal with backend-generated config showConfigModal(''); } else { - // Fallback to manual prompt self.showPrivateKeyPrompt(peer, ifaceObj, function(key) { showConfigModal(key); }); diff --git a/package/secubox/luci-app-wireguard-dashboard/htdocs/luci-static/resources/view/wireguard-dashboard/qrcodes.js b/package/secubox/luci-app-wireguard-dashboard/htdocs/luci-static/resources/view/wireguard-dashboard/qrcodes.js index 5b0dbb35..78393ddf 100644 --- a/package/secubox/luci-app-wireguard-dashboard/htdocs/luci-static/resources/view/wireguard-dashboard/qrcodes.js +++ b/package/secubox/luci-app-wireguard-dashboard/htdocs/luci-static/resources/view/wireguard-dashboard/qrcodes.js @@ -13,12 +13,14 @@ return view.extend({ return Promise.all([ api.getConfig(), api.getInterfaces(), - api.getPeers() + api.getPeers(), + api.getEndpoints() ]).then(function(results) { return { config: results[0] || {}, interfaces: (results[1] || {}).interfaces || [], - peers: (results[2] || {}).peers || [] + peers: (results[2] || {}).peers || [], + endpointData: results[3] || {} }; }); }, @@ -190,11 +192,105 @@ return view.extend({ ]); }, + showManageEndpointsModal: function() { + var self = this; + + api.getEndpoints().then(function(endpointData) { + var endpoints = (endpointData || {}).endpoints || []; + var defaultId = (endpointData || {})['default'] || ''; + + var rows = endpoints.map(function(ep) { + return E('tr', {}, [ + E('td', {}, ep.name || ep.id), + E('td', {}, E('code', {}, ep.address)), + E('td', {}, ep.id === defaultId ? E('strong', {}, _('Default')) : E('button', { + 'class': 'cbi-button cbi-button-apply', + 'style': 'padding: 2px 8px; font-size: 0.85em;', + 'click': function() { + api.setDefaultEndpoint(ep.id).then(function() { + ui.hideModal(); + self.showManageEndpointsModal(); + }); + } + }, _('Set Default'))), + E('td', {}, E('button', { + 'class': 'cbi-button cbi-button-negative', + 'style': 'padding: 2px 8px; font-size: 0.85em;', + 'click': function() { + api.deleteEndpoint(ep.id).then(function() { + ui.hideModal(); + self.showManageEndpointsModal(); + }); + } + }, _('Delete'))) + ]); + }); + + ui.showModal(_('Manage Endpoints'), [ + endpoints.length > 0 ? + E('table', { 'class': 'table' }, [ + E('thead', {}, [ + E('tr', {}, [ + E('th', {}, _('Name')), + E('th', {}, _('Address')), + E('th', {}, _('Status')), + E('th', {}, _('Actions')) + ]) + ]), + E('tbody', {}, rows) + ]) : + E('p', { 'style': 'color: #666; text-align: center; padding: 1em;' }, _('No saved endpoints')), + + E('h4', { 'style': 'margin-top: 1em;' }, _('Add New Endpoint')), + E('div', { 'style': 'display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-bottom: 8px;' }, [ + E('input', { + 'type': 'text', + 'id': 'new-ep-name', + 'class': 'cbi-input-text', + 'placeholder': _('Name (e.g. Home Server)') + }), + E('input', { + 'type': 'text', + 'id': 'new-ep-address', + 'class': 'cbi-input-text', + 'placeholder': _('Address (e.g. vpn.example.com)') + }) + ]), + E('div', { 'class': 'right', 'style': 'margin-top: 1em;' }, [ + E('button', { + 'class': 'btn', + 'click': ui.hideModal + }, _('Close')), + ' ', + E('button', { + 'class': 'btn cbi-button-action', + 'click': function() { + var name = document.getElementById('new-ep-name').value.trim(); + var address = document.getElementById('new-ep-address').value.trim(); + if (!address) { + ui.addNotification(null, E('p', _('Please enter an address')), 'error'); + return; + } + var id = (name || address).toLowerCase().replace(/[^a-z0-9]/g, '_').substring(0, 20); + api.setEndpoint(id, name || address, address).then(function() { + ui.hideModal(); + self.showManageEndpointsModal(); + }); + } + }, _('Add Endpoint')) + ]) + ]); + }); + }, + render: function(data) { var self = this; var interfaces = data.interfaces || []; var configData = (data.config || {}).interfaces || []; var peers = data.peers || []; + var endpointData = data.endpointData || {}; + + this.endpointData = endpointData; // Merge interface data with config data interfaces = interfaces.map(function(iface) { @@ -205,6 +301,8 @@ return view.extend({ }); }); + var endpointSelector = api.buildEndpointSelector(endpointData, 'wg-server-endpoint'); + var view = E('div', { 'class': 'wireguard-dashboard' }, [ E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }), @@ -216,35 +314,24 @@ return view.extend({ ]) ]), - // Server endpoint input + // Server endpoint selector E('div', { 'class': 'wg-card' }, [ E('div', { 'class': 'wg-card-header' }, [ E('div', { 'class': 'wg-card-title' }, [ E('span', { 'class': 'wg-card-title-icon' }, '🌐'), _('Server Endpoint') - ]) + ]), + E('button', { + 'class': 'wg-btn', + 'style': 'font-size: 0.85em;', + 'click': L.bind(this.showManageEndpointsModal, this) + }, _('Manage')) ]), E('div', { 'class': 'wg-card-body' }, [ E('p', { 'style': 'margin-bottom: 12px; color: var(--wg-text-secondary);' }, - _('Enter the public IP or hostname of this WireGuard server:')), + _('Select or enter the public IP or hostname of this WireGuard server:')), E('div', { 'class': 'wg-form-row' }, [ - E('input', { - 'type': 'text', - 'id': 'wg-server-endpoint', - 'class': 'cbi-input-text', - 'placeholder': 'e.g., vpn.example.com or 203.0.113.1', - 'style': 'flex: 1;' - }), - E('button', { - 'class': 'wg-btn wg-btn-primary', - 'click': function() { - var input = document.getElementById('wg-server-endpoint'); - if (input && input.value.trim()) { - sessionStorage.setItem('wg_server_endpoint', input.value.trim()); - ui.addNotification(null, E('p', {}, _('Server endpoint saved')), 'info'); - } - } - }, _('Save')) + E('div', { 'style': 'flex: 1;' }, [ endpointSelector ]) ]) ]) ]), @@ -289,13 +376,9 @@ return view.extend({ E('button', { 'class': 'wg-btn wg-btn-primary', 'click': function() { - var endpoint = sessionStorage.getItem('wg_server_endpoint'); + var endpoint = api.getEndpointValue('wg-server-endpoint'); if (!endpoint) { - var input = document.getElementById('wg-server-endpoint'); - endpoint = input ? input.value.trim() : ''; - } - if (!endpoint) { - ui.addNotification(null, E('p', {}, _('Please enter the server endpoint first')), 'warning'); + ui.addNotification(null, E('p', {}, _('Please select or enter the server endpoint first')), 'warning'); return; } self.generateQRForPeer(iface, peer, endpoint); @@ -316,15 +399,6 @@ return view.extend({ ]) ]); - // Restore saved endpoint - setTimeout(function() { - var saved = sessionStorage.getItem('wg_server_endpoint'); - if (saved) { - var input = document.getElementById('wg-server-endpoint'); - if (input) input.value = saved; - } - }, 100); - // Add CSS var css = ` .wg-form-row { diff --git a/package/secubox/luci-app-wireguard-dashboard/htdocs/luci-static/resources/view/wireguard-dashboard/wizard.js b/package/secubox/luci-app-wireguard-dashboard/htdocs/luci-static/resources/view/wireguard-dashboard/wizard.js index f8b8858c..9f476832 100644 --- a/package/secubox/luci-app-wireguard-dashboard/htdocs/luci-static/resources/view/wireguard-dashboard/wizard.js +++ b/package/secubox/luci-app-wireguard-dashboard/htdocs/luci-static/resources/view/wireguard-dashboard/wizard.js @@ -121,7 +121,8 @@ return view.extend({ return Promise.all([ api.getInterfaces(), api.getStatus(), - this.getPublicIP() + this.getPublicIP(), + api.getEndpoints() ]); }, @@ -135,6 +136,82 @@ return view.extend({ }); }, + renderEndpointField: function() { + var self = this; + var endpointData = this.endpointData || {}; + var endpoints = (endpointData || {}).endpoints || []; + var defaultId = (endpointData || {})['default'] || ''; + var publicIP = this.wizardData.publicIP || ''; + + if (endpoints.length > 0) { + // Build a dropdown with saved endpoints + auto-detected IP + custom option + var options = []; + + endpoints.forEach(function(ep) { + options.push(E('option', { + 'value': ep.address, + 'selected': (ep.id === defaultId) ? '' : null + }, (ep.name || ep.id) + ' (' + ep.address + ')')); + }); + + // Add detected IP as an option if not already in the list + if (publicIP) { + var alreadyExists = endpoints.some(function(ep) { return ep.address === publicIP; }); + if (!alreadyExists) { + options.push(E('option', { 'value': publicIP }, _('Detected IP') + ' (' + publicIP + ')')); + } + } + + options.push(E('option', { 'value': '__custom__' }, _('Custom...'))); + + var container = E('div', { 'class': 'wg-form-group' }); + container.appendChild(E('label', {}, _('Public IP / Hostname'))); + + var select = E('select', { + 'id': 'cfg-public-endpoint', + 'class': 'wg-input', + 'change': function() { + var customInput = document.getElementById('cfg-public-endpoint-custom'); + if (this.value === '__custom__') { + customInput.style.display = ''; + customInput.focus(); + } else { + customInput.style.display = 'none'; + } + } + }, options); + + container.appendChild(select); + container.appendChild(E('input', { + 'type': 'text', + 'id': 'cfg-public-endpoint-custom', + 'class': 'wg-input', + 'placeholder': 'vpn.example.com', + 'style': 'display: none; margin-top: 6px;' + })); + container.appendChild(E('small', {}, _('How clients will reach this server'))); + + return container; + } + + // No saved endpoints - show plain text input with auto-detected IP + return E('div', { 'class': 'wg-form-group' }, [ + E('label', {}, _('Public IP / Hostname')), + E('input', { + 'type': 'text', + 'id': 'cfg-public-endpoint', + 'class': 'wg-input', + 'value': publicIP, + 'placeholder': 'vpn.example.com' + }), + E('small', {}, _('How clients will reach this server')), + publicIP ? E('div', { 'class': 'wg-detected' }, [ + E('span', {}, '✓ ' + _('Detected: ')), + E('code', {}, publicIP) + ]) : '' + ]); + }, + getNextInterfaceName: function(interfaces) { var existing = interfaces.map(function(i) { return i.name; }); for (var i = 0; i < 100; i++) { @@ -152,10 +229,12 @@ return view.extend({ var interfaces = Array.isArray(interfacesData) ? interfacesData : (interfacesData.interfaces || []); var status = data[1] || {}; var publicIP = data[2] || ''; + var endpointData = data[3] || {}; this.wizardData.publicIP = publicIP; this.wizardData.existingInterfaces = interfaces; this.wizardData.nextIfaceName = this.getNextInterfaceName(interfaces); + this.endpointData = endpointData; var view = E('div', { 'class': 'wg-wizard' }, [ E('link', { 'rel': 'stylesheet', 'href': L.resource('wireguard-dashboard/dashboard.css') }), @@ -305,21 +384,7 @@ return view.extend({ E('div', { 'class': 'wg-config-section' }, [ E('h3', {}, '🔗 ' + _('Public Endpoint')), - E('div', { 'class': 'wg-form-group' }, [ - E('label', {}, _('Public IP / Hostname')), - E('input', { - 'type': 'text', - 'id': 'cfg-public-endpoint', - 'class': 'wg-input', - 'value': this.wizardData.publicIP || '', - 'placeholder': 'vpn.example.com' - }), - E('small', {}, _('How clients will reach this server')), - this.wizardData.publicIP ? E('div', { 'class': 'wg-detected' }, [ - E('span', {}, '✓ ' + _('Detected: ')), - E('code', {}, this.wizardData.publicIP) - ]) : '' - ]), + this.renderEndpointField(), E('div', { 'class': 'wg-form-group' }, [ E('label', {}, _('MTU')), @@ -495,9 +560,17 @@ return view.extend({ this.wizardData.listenPort = document.getElementById('cfg-listen-port').value; this.wizardData.vpnNetwork = document.getElementById('cfg-vpn-network').value; this.wizardData.serverIP = document.getElementById('cfg-server-ip').value; - this.wizardData.publicEndpoint = document.getElementById('cfg-public-endpoint').value; this.wizardData.mtu = document.getElementById('cfg-mtu').value; + // Handle endpoint - could be a select or text input + var endpointEl = document.getElementById('cfg-public-endpoint'); + var endpointValue = endpointEl ? endpointEl.value : ''; + if (endpointValue === '__custom__') { + var customEl = document.getElementById('cfg-public-endpoint-custom'); + endpointValue = customEl ? customEl.value.trim() : ''; + } + this.wizardData.publicEndpoint = endpointValue; + if (!this.wizardData.ifaceName || !this.wizardData.listenPort || !this.wizardData.vpnNetwork) { ui.addNotification(null, E('p', _('Please fill in all required fields')), 'warning'); return; @@ -568,6 +641,17 @@ return view.extend({ }).then(function() { // Create peers return self.createPeers(); + }).then(function(results) { + // Save the public endpoint to UCI for reuse in peers/QR views + var data = self.wizardData; + if (data.publicEndpoint) { + return api.setEndpoint(data.ifaceName, data.ifaceName + ' server', data.publicEndpoint).then(function() { + return api.setDefaultEndpoint(data.ifaceName); + }).then(function() { + return results; + }); + } + return results; }).then(function(results) { ui.hideModal(); diff --git a/package/secubox/luci-app-wireguard-dashboard/htdocs/luci-static/resources/wireguard-dashboard/api.js b/package/secubox/luci-app-wireguard-dashboard/htdocs/luci-static/resources/wireguard-dashboard/api.js index 6b98085a..3a6312f1 100644 --- a/package/secubox/luci-app-wireguard-dashboard/htdocs/luci-static/resources/wireguard-dashboard/api.js +++ b/package/secubox/luci-app-wireguard-dashboard/htdocs/luci-static/resources/wireguard-dashboard/api.js @@ -107,6 +107,106 @@ var callPingPeer = rpc.declare({ expect: { reachable: false } }); +var callGetEndpoints = rpc.declare({ + object: 'luci.wireguard-dashboard', + method: 'get_endpoints', + expect: { } +}); + +var callSetEndpoint = rpc.declare({ + object: 'luci.wireguard-dashboard', + method: 'set_endpoint', + params: ['id', 'name', 'address'], + expect: { } +}); + +var callSetDefaultEndpoint = rpc.declare({ + object: 'luci.wireguard-dashboard', + method: 'set_default_endpoint', + params: ['id'], + expect: { } +}); + +var callDeleteEndpoint = rpc.declare({ + object: 'luci.wireguard-dashboard', + method: 'delete_endpoint', + params: ['id'], + expect: { } +}); + +function buildEndpointSelector(endpointData, inputId) { + var endpoints = (endpointData || {}).endpoints || []; + var defaultId = (endpointData || {})['default'] || ''; + + if (endpoints.length === 0) { + // No saved endpoints - return a plain text input + return E('input', { + 'type': 'text', + 'id': inputId, + 'class': 'cbi-input-text', + 'placeholder': 'vpn.example.com or 203.0.113.1', + 'data-mode': 'text' + }); + } + + var container = E('div', { 'style': 'display: flex; flex-direction: column; gap: 8px;' }); + + var options = endpoints.map(function(ep) { + return E('option', { + 'value': ep.address, + 'selected': (ep.id === defaultId) ? '' : null, + 'data-id': ep.id + }, (ep.name || ep.id) + ' (' + ep.address + ')'); + }); + + options.push(E('option', { 'value': '__custom__' }, _('Custom...'))); + + var select = E('select', { + 'id': inputId, + 'class': 'cbi-input-select', + 'data-mode': 'select', + 'change': function() { + var customInput = container.querySelector('.wg-custom-endpoint'); + if (this.value === '__custom__') { + customInput.style.display = ''; + customInput.focus(); + } else { + customInput.style.display = 'none'; + } + } + }, options); + + var customInput = E('input', { + 'type': 'text', + 'class': 'cbi-input-text wg-custom-endpoint', + 'placeholder': 'vpn.example.com or 203.0.113.1', + 'style': 'display: none; margin-top: 4px;' + }); + + container.appendChild(select); + container.appendChild(customInput); + + return container; +} + +function getEndpointValue(inputId) { + var el = document.getElementById(inputId); + if (!el) return ''; + + if (el.dataset.mode === 'text') { + return el.value.trim(); + } + + // select mode + if (el.value === '__custom__') { + var container = el.closest('div'); + var customInput = container ? container.querySelector('.wg-custom-endpoint') : null; + return customInput ? customInput.value.trim() : ''; + } + + return el.value; +} + function formatBytes(bytes) { if (bytes === 0) return '0 B'; var k = 1024; @@ -170,6 +270,12 @@ return baseclass.extend({ getPeerDescriptions: callPeerDescriptions, getBandwidthRates: callBandwidthRates, pingPeer: callPingPeer, + getEndpoints: callGetEndpoints, + setEndpoint: callSetEndpoint, + setDefaultEndpoint: callSetDefaultEndpoint, + deleteEndpoint: callDeleteEndpoint, + buildEndpointSelector: buildEndpointSelector, + getEndpointValue: getEndpointValue, formatBytes: formatBytes, formatLastHandshake: formatLastHandshake, getPeerStatusClass: getPeerStatusClass, diff --git a/package/secubox/luci-app-wireguard-dashboard/root/etc/config/wireguard_dashboard b/package/secubox/luci-app-wireguard-dashboard/root/etc/config/wireguard_dashboard new file mode 100644 index 00000000..821258b4 --- /dev/null +++ b/package/secubox/luci-app-wireguard-dashboard/root/etc/config/wireguard_dashboard @@ -0,0 +1,2 @@ +config globals 'globals' + option default_endpoint '' diff --git a/package/secubox/luci-app-wireguard-dashboard/root/usr/libexec/rpcd/luci.wireguard-dashboard b/package/secubox/luci-app-wireguard-dashboard/root/usr/libexec/rpcd/luci.wireguard-dashboard index 8790fccc..aaa38076 100755 --- a/package/secubox/luci-app-wireguard-dashboard/root/usr/libexec/rpcd/luci.wireguard-dashboard +++ b/package/secubox/luci-app-wireguard-dashboard/root/usr/libexec/rpcd/luci.wireguard-dashboard @@ -1051,10 +1051,124 @@ get_bandwidth_rates() { json_dump } +# Emit a single endpoint section as JSON object (callback for config_foreach) +_emit_endpoint() { + local section="$1" + local name address + config_get name "$section" name "" + config_get address "$section" address "" + json_add_object + json_add_string "id" "$section" + json_add_string "name" "$name" + json_add_string "address" "$address" + json_close_object +} + +# Get all saved server endpoints from UCI +get_endpoints() { + json_init + local default_ep + default_ep=$(uci -q get wireguard_dashboard.globals.default_endpoint) + json_add_string "default" "${default_ep:-}" + json_add_array "endpoints" + config_load wireguard_dashboard + config_foreach _emit_endpoint endpoint + json_close_array + json_dump +} + +# Create or update a server endpoint entry +set_endpoint() { + read input + json_load "$input" + json_get_var ep_id id + json_get_var ep_name name + json_get_var ep_address address + + json_init + + if [ -z "$ep_id" ] || [ -z "$ep_address" ]; then + json_add_boolean "success" 0 + json_add_string "error" "Missing required fields: id and address" + json_dump + return + fi + + # Sanitize id: only allow alphanumeric, underscore, hyphen + local safe_id=$(echo "$ep_id" | sed 's/[^a-zA-Z0-9_-]/_/g') + + uci set wireguard_dashboard.${safe_id}=endpoint + uci set wireguard_dashboard.${safe_id}.name="${ep_name:-$safe_id}" + uci set wireguard_dashboard.${safe_id}.address="$ep_address" + uci commit wireguard_dashboard + + json_add_boolean "success" 1 + json_add_string "id" "$safe_id" + json_dump +} + +# Set the default endpoint +set_default_endpoint() { + read input + json_load "$input" + json_get_var ep_id id + + json_init + + if [ -z "$ep_id" ]; then + json_add_boolean "success" 0 + json_add_string "error" "Missing required field: id" + json_dump + return + fi + + uci set wireguard_dashboard.globals.default_endpoint="$ep_id" + uci commit wireguard_dashboard + + json_add_boolean "success" 1 + json_dump +} + +# Delete a server endpoint entry +delete_endpoint() { + read input + json_load "$input" + json_get_var ep_id id + + json_init + + if [ -z "$ep_id" ]; then + json_add_boolean "success" 0 + json_add_string "error" "Missing required field: id" + json_dump + return + fi + + # Check if section exists + if ! uci -q get wireguard_dashboard.${ep_id} >/dev/null 2>&1; then + json_add_boolean "success" 0 + json_add_string "error" "Endpoint not found: $ep_id" + json_dump + return + fi + + # If deleting the default, clear the default + local default_ep=$(uci -q get wireguard_dashboard.globals.default_endpoint) + if [ "$default_ep" = "$ep_id" ]; then + uci set wireguard_dashboard.globals.default_endpoint="" + fi + + uci delete wireguard_dashboard.${ep_id} + uci commit wireguard_dashboard + + json_add_boolean "success" 1 + json_dump +} + # Main dispatcher case "$1" in list) - echo '{"status":{},"interfaces":{},"peers":{},"traffic":{},"config":{},"generate_keys":{},"create_interface":{"name":"str","private_key":"str","listen_port":"str","addresses":"str","mtu":"str"},"add_peer":{"interface":"str","name":"str","allowed_ips":"str","public_key":"str","preshared_key":"str","endpoint":"str","persistent_keepalive":"str","private_key":"str"},"remove_peer":{"interface":"str","public_key":"str"},"generate_config":{"interface":"str","peer":"str","private_key":"str","endpoint":"str"},"generate_qr":{"interface":"str","peer":"str","private_key":"str","endpoint":"str"},"bandwidth_history":{},"endpoint_info":{"endpoint":"str"},"ping_peer":{"ip":"str"},"interface_control":{"interface":"str","action":"str"},"peer_descriptions":{},"bandwidth_rates":{}}' + echo '{"status":{},"interfaces":{},"peers":{},"traffic":{},"config":{},"generate_keys":{},"create_interface":{"name":"str","private_key":"str","listen_port":"str","addresses":"str","mtu":"str"},"add_peer":{"interface":"str","name":"str","allowed_ips":"str","public_key":"str","preshared_key":"str","endpoint":"str","persistent_keepalive":"str","private_key":"str"},"remove_peer":{"interface":"str","public_key":"str"},"generate_config":{"interface":"str","peer":"str","private_key":"str","endpoint":"str"},"generate_qr":{"interface":"str","peer":"str","private_key":"str","endpoint":"str"},"bandwidth_history":{},"endpoint_info":{"endpoint":"str"},"ping_peer":{"ip":"str"},"interface_control":{"interface":"str","action":"str"},"peer_descriptions":{},"bandwidth_rates":{},"get_endpoints":{},"set_endpoint":{"id":"str","name":"str","address":"str"},"set_default_endpoint":{"id":"str"},"delete_endpoint":{"id":"str"}}' ;; call) case "$2" in @@ -1109,6 +1223,18 @@ case "$1" in bandwidth_rates) get_bandwidth_rates ;; + get_endpoints) + get_endpoints + ;; + set_endpoint) + set_endpoint + ;; + set_default_endpoint) + set_default_endpoint + ;; + delete_endpoint) + delete_endpoint + ;; *) echo '{"error": "Unknown method"}' ;; diff --git a/package/secubox/luci-app-wireguard-dashboard/root/usr/share/rpcd/acl.d/luci-app-wireguard-dashboard.json b/package/secubox/luci-app-wireguard-dashboard/root/usr/share/rpcd/acl.d/luci-app-wireguard-dashboard.json index f94018b2..6425a6ce 100644 --- a/package/secubox/luci-app-wireguard-dashboard/root/usr/share/rpcd/acl.d/luci-app-wireguard-dashboard.json +++ b/package/secubox/luci-app-wireguard-dashboard/root/usr/share/rpcd/acl.d/luci-app-wireguard-dashboard.json @@ -12,14 +12,16 @@ "peer_descriptions", "bandwidth_rates", "bandwidth_history", - "endpoint_info" + "endpoint_info", + "get_endpoints" ], "system": [ "info", "board" ], "file": [ "read", "stat", "exec" ] }, - "uci": [ "network", "wireguard-dashboard" ], + "uci": [ "network", "wireguard_dashboard" ], "file": { "/etc/config/network": [ "read" ], + "/etc/config/wireguard_dashboard": [ "read" ], "/usr/bin/wg": [ "exec" ] } }, @@ -33,10 +35,13 @@ "generate_config", "generate_qr", "interface_control", - "ping_peer" + "ping_peer", + "set_endpoint", + "set_default_endpoint", + "delete_endpoint" ] }, - "uci": [ "wireguard-dashboard", "network" ] + "uci": [ "wireguard_dashboard", "network" ] } } }