feat(wireguard-dashboard): Persist server endpoints in UCI for reuse across views
Server endpoints were stored only in browser sessionStorage, lost on tab close/refresh. Now endpoints are saved in a dedicated UCI config file (wireguard_dashboard) with RPCD methods to manage them. The wizard auto-saves the endpoint after tunnel creation, and peers/QR views use a dropdown of saved endpoints instead of requiring manual re-entry. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
bb0f24d93a
commit
02126aa7d3
@ -33,7 +33,7 @@ PKG_FILE_MODES:=/usr/libexec/rpcd/luci.wireguard-dashboard:root:root:755
|
|||||||
include $(TOPDIR)/feeds/luci/luci.mk
|
include $(TOPDIR)/feeds/luci/luci.mk
|
||||||
|
|
||||||
define Package/$(PKG_NAME)/conffiles
|
define Package/$(PKG_NAME)/conffiles
|
||||||
/etc/config/wireguard-dashboard
|
/etc/config/wireguard_dashboard
|
||||||
endef
|
endef
|
||||||
|
|
||||||
# call BuildPackage - OpenWrt buildroot
|
# call BuildPackage - OpenWrt buildroot
|
||||||
|
|||||||
@ -37,7 +37,8 @@ return view.extend({
|
|||||||
load: function() {
|
load: function() {
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
API.getPeers(),
|
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
|
// Handle RPC expect unwrapping - results may be array or object with .peers/.interfaces
|
||||||
var peersData = data[0] || [];
|
var peersData = data[0] || [];
|
||||||
var interfacesData = data[1] || [];
|
var interfacesData = data[1] || [];
|
||||||
|
var endpointData = data[2] || {};
|
||||||
var peers = Array.isArray(peersData) ? peersData : (peersData.peers || []);
|
var peers = Array.isArray(peersData) ? peersData : (peersData.peers || []);
|
||||||
var interfaces = Array.isArray(interfacesData) ? interfacesData : (interfacesData.interfaces || []);
|
var interfaces = Array.isArray(interfacesData) ? interfacesData : (interfacesData.interfaces || []);
|
||||||
var activePeers = peers.filter(function(p) { return p.status === 'active'; }).length;
|
var activePeers = peers.filter(function(p) { return p.status === 'active'; }).length;
|
||||||
|
|
||||||
|
this.endpointData = endpointData;
|
||||||
|
|
||||||
var view = E('div', { 'class': 'cbi-map' }, [
|
var view = E('div', { 'class': 'cbi-map' }, [
|
||||||
E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }),
|
E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }),
|
||||||
E('h2', {}, _('WireGuard Peers')),
|
E('h2', {}, _('WireGuard Peers')),
|
||||||
@ -418,20 +422,23 @@ return view.extend({
|
|||||||
|
|
||||||
promptForEndpointAndShowQR: function(peer, ifaceObj, privateKey) {
|
promptForEndpointAndShowQR: function(peer, ifaceObj, privateKey) {
|
||||||
var self = this;
|
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'), [
|
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('div', { 'class': 'cbi-value' }, [
|
||||||
E('label', { 'class': 'cbi-value-title' }, _('Server Endpoint')),
|
E('label', { 'class': 'cbi-value-title' }, _('Server Endpoint')),
|
||||||
E('div', { 'class': 'cbi-value-field' }, [
|
E('div', { 'class': 'cbi-value-field' }, [
|
||||||
E('input', {
|
selector
|
||||||
'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('div', { 'class': 'right', 'style': 'margin-top: 1em;' }, [
|
||||||
@ -443,12 +450,11 @@ return view.extend({
|
|||||||
E('button', {
|
E('button', {
|
||||||
'class': 'btn cbi-button-action',
|
'class': 'btn cbi-button-action',
|
||||||
'click': function() {
|
'click': function() {
|
||||||
var endpoint = document.getElementById('qr-server-endpoint').value.trim();
|
var endpoint = API.getEndpointValue('qr-server-endpoint');
|
||||||
if (!endpoint) {
|
if (!endpoint) {
|
||||||
ui.addNotification(null, E('p', _('Please enter server endpoint')), 'error');
|
ui.addNotification(null, E('p', _('Please enter server endpoint')), 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
sessionStorage.setItem('wg_server_endpoint', endpoint);
|
|
||||||
ui.hideModal();
|
ui.hideModal();
|
||||||
self.generateAndShowQR(peer, ifaceObj, privateKey, endpoint);
|
self.generateAndShowQR(peer, ifaceObj, privateKey, endpoint);
|
||||||
}
|
}
|
||||||
@ -636,6 +642,8 @@ return view.extend({
|
|||||||
var self = this;
|
var self = this;
|
||||||
var privateKey = this.getStoredPrivateKey(peer.public_key);
|
var privateKey = this.getStoredPrivateKey(peer.public_key);
|
||||||
var ifaceObj = interfaces.find(function(i) { return i.name === peer.interface; }) || {};
|
var ifaceObj = interfaces.find(function(i) { return i.name === peer.interface; }) || {};
|
||||||
|
var endpointData = this.endpointData || {};
|
||||||
|
var endpoints = endpointData.endpoints || [];
|
||||||
|
|
||||||
var downloadConfig = function(config) {
|
var downloadConfig = function(config) {
|
||||||
var blob = new Blob([config], { type: 'text/plain' });
|
var blob = new Blob([config], { type: 'text/plain' });
|
||||||
@ -648,21 +656,46 @@ return view.extend({
|
|||||||
ui.addNotification(null, E('p', _('Configuration file downloaded')), 'info');
|
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 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'), [
|
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('div', { 'class': 'cbi-value' }, [
|
||||||
E('label', { 'class': 'cbi-value-title' }, _('Server Endpoint')),
|
E('label', { 'class': 'cbi-value-title' }, _('Server Endpoint')),
|
||||||
E('div', { 'class': 'cbi-value-field' }, [
|
E('div', { 'class': 'cbi-value-field' }, [
|
||||||
E('input', {
|
selector
|
||||||
'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('div', { 'class': 'right', 'style': 'margin-top: 1em;' }, [
|
||||||
@ -674,37 +707,13 @@ return view.extend({
|
|||||||
E('button', {
|
E('button', {
|
||||||
'class': 'btn cbi-button-action',
|
'class': 'btn cbi-button-action',
|
||||||
'click': function() {
|
'click': function() {
|
||||||
var endpoint = document.getElementById('cfg-server-endpoint').value.trim();
|
var endpoint = API.getEndpointValue('cfg-server-endpoint');
|
||||||
if (!endpoint) {
|
if (!endpoint) {
|
||||||
ui.addNotification(null, E('p', _('Please enter server endpoint')), 'error');
|
ui.addNotification(null, E('p', _('Please enter server endpoint')), 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
sessionStorage.setItem('wg_server_endpoint', endpoint);
|
|
||||||
ui.hideModal();
|
ui.hideModal();
|
||||||
|
doDownload(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 {
|
|
||||||
// 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');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, _('Download'))
|
}, _('Download'))
|
||||||
])
|
])
|
||||||
@ -715,10 +724,8 @@ return view.extend({
|
|||||||
// Try backend first - it may have the stored key
|
// Try backend first - it may have the stored key
|
||||||
API.generateConfig(peer.interface, peer.public_key, '', 'test').then(function(result) {
|
API.generateConfig(peer.interface, peer.public_key, '', 'test').then(function(result) {
|
||||||
if (result && result.config && !result.error) {
|
if (result && result.config && !result.error) {
|
||||||
// Backend has the key, show config modal with backend-generated config
|
|
||||||
showConfigModal('');
|
showConfigModal('');
|
||||||
} else {
|
} else {
|
||||||
// Fallback to manual prompt
|
|
||||||
self.showPrivateKeyPrompt(peer, ifaceObj, function(key) {
|
self.showPrivateKeyPrompt(peer, ifaceObj, function(key) {
|
||||||
showConfigModal(key);
|
showConfigModal(key);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -13,12 +13,14 @@ return view.extend({
|
|||||||
return Promise.all([
|
return Promise.all([
|
||||||
api.getConfig(),
|
api.getConfig(),
|
||||||
api.getInterfaces(),
|
api.getInterfaces(),
|
||||||
api.getPeers()
|
api.getPeers(),
|
||||||
|
api.getEndpoints()
|
||||||
]).then(function(results) {
|
]).then(function(results) {
|
||||||
return {
|
return {
|
||||||
config: results[0] || {},
|
config: results[0] || {},
|
||||||
interfaces: (results[1] || {}).interfaces || [],
|
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) {
|
render: function(data) {
|
||||||
var self = this;
|
var self = this;
|
||||||
var interfaces = data.interfaces || [];
|
var interfaces = data.interfaces || [];
|
||||||
var configData = (data.config || {}).interfaces || [];
|
var configData = (data.config || {}).interfaces || [];
|
||||||
var peers = data.peers || [];
|
var peers = data.peers || [];
|
||||||
|
var endpointData = data.endpointData || {};
|
||||||
|
|
||||||
|
this.endpointData = endpointData;
|
||||||
|
|
||||||
// Merge interface data with config data
|
// Merge interface data with config data
|
||||||
interfaces = interfaces.map(function(iface) {
|
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' }, [
|
var view = E('div', { 'class': 'wireguard-dashboard' }, [
|
||||||
E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }),
|
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' }, [
|
||||||
E('div', { 'class': 'wg-card-header' }, [
|
E('div', { 'class': 'wg-card-header' }, [
|
||||||
E('div', { 'class': 'wg-card-title' }, [
|
E('div', { 'class': 'wg-card-title' }, [
|
||||||
E('span', { 'class': 'wg-card-title-icon' }, '🌐'),
|
E('span', { 'class': 'wg-card-title-icon' }, '🌐'),
|
||||||
_('Server Endpoint')
|
_('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('div', { 'class': 'wg-card-body' }, [
|
||||||
E('p', { 'style': 'margin-bottom: 12px; color: var(--wg-text-secondary);' },
|
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('div', { 'class': 'wg-form-row' }, [
|
||||||
E('input', {
|
E('div', { 'style': 'flex: 1;' }, [ endpointSelector ])
|
||||||
'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'))
|
|
||||||
])
|
])
|
||||||
])
|
])
|
||||||
]),
|
]),
|
||||||
@ -289,13 +376,9 @@ return view.extend({
|
|||||||
E('button', {
|
E('button', {
|
||||||
'class': 'wg-btn wg-btn-primary',
|
'class': 'wg-btn wg-btn-primary',
|
||||||
'click': function() {
|
'click': function() {
|
||||||
var endpoint = sessionStorage.getItem('wg_server_endpoint');
|
var endpoint = api.getEndpointValue('wg-server-endpoint');
|
||||||
if (!endpoint) {
|
if (!endpoint) {
|
||||||
var input = document.getElementById('wg-server-endpoint');
|
ui.addNotification(null, E('p', {}, _('Please select or enter the server endpoint first')), 'warning');
|
||||||
endpoint = input ? input.value.trim() : '';
|
|
||||||
}
|
|
||||||
if (!endpoint) {
|
|
||||||
ui.addNotification(null, E('p', {}, _('Please enter the server endpoint first')), 'warning');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
self.generateQRForPeer(iface, peer, endpoint);
|
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
|
// Add CSS
|
||||||
var css = `
|
var css = `
|
||||||
.wg-form-row {
|
.wg-form-row {
|
||||||
|
|||||||
@ -121,7 +121,8 @@ return view.extend({
|
|||||||
return Promise.all([
|
return Promise.all([
|
||||||
api.getInterfaces(),
|
api.getInterfaces(),
|
||||||
api.getStatus(),
|
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) {
|
getNextInterfaceName: function(interfaces) {
|
||||||
var existing = interfaces.map(function(i) { return i.name; });
|
var existing = interfaces.map(function(i) { return i.name; });
|
||||||
for (var i = 0; i < 100; i++) {
|
for (var i = 0; i < 100; i++) {
|
||||||
@ -152,10 +229,12 @@ return view.extend({
|
|||||||
var interfaces = Array.isArray(interfacesData) ? interfacesData : (interfacesData.interfaces || []);
|
var interfaces = Array.isArray(interfacesData) ? interfacesData : (interfacesData.interfaces || []);
|
||||||
var status = data[1] || {};
|
var status = data[1] || {};
|
||||||
var publicIP = data[2] || '';
|
var publicIP = data[2] || '';
|
||||||
|
var endpointData = data[3] || {};
|
||||||
|
|
||||||
this.wizardData.publicIP = publicIP;
|
this.wizardData.publicIP = publicIP;
|
||||||
this.wizardData.existingInterfaces = interfaces;
|
this.wizardData.existingInterfaces = interfaces;
|
||||||
this.wizardData.nextIfaceName = this.getNextInterfaceName(interfaces);
|
this.wizardData.nextIfaceName = this.getNextInterfaceName(interfaces);
|
||||||
|
this.endpointData = endpointData;
|
||||||
|
|
||||||
var view = E('div', { 'class': 'wg-wizard' }, [
|
var view = E('div', { 'class': 'wg-wizard' }, [
|
||||||
E('link', { 'rel': 'stylesheet', 'href': L.resource('wireguard-dashboard/dashboard.css') }),
|
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('div', { 'class': 'wg-config-section' }, [
|
||||||
E('h3', {}, '🔗 ' + _('Public Endpoint')),
|
E('h3', {}, '🔗 ' + _('Public Endpoint')),
|
||||||
|
|
||||||
E('div', { 'class': 'wg-form-group' }, [
|
this.renderEndpointField(),
|
||||||
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)
|
|
||||||
]) : ''
|
|
||||||
]),
|
|
||||||
|
|
||||||
E('div', { 'class': 'wg-form-group' }, [
|
E('div', { 'class': 'wg-form-group' }, [
|
||||||
E('label', {}, _('MTU')),
|
E('label', {}, _('MTU')),
|
||||||
@ -495,9 +560,17 @@ return view.extend({
|
|||||||
this.wizardData.listenPort = document.getElementById('cfg-listen-port').value;
|
this.wizardData.listenPort = document.getElementById('cfg-listen-port').value;
|
||||||
this.wizardData.vpnNetwork = document.getElementById('cfg-vpn-network').value;
|
this.wizardData.vpnNetwork = document.getElementById('cfg-vpn-network').value;
|
||||||
this.wizardData.serverIP = document.getElementById('cfg-server-ip').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;
|
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) {
|
if (!this.wizardData.ifaceName || !this.wizardData.listenPort || !this.wizardData.vpnNetwork) {
|
||||||
ui.addNotification(null, E('p', _('Please fill in all required fields')), 'warning');
|
ui.addNotification(null, E('p', _('Please fill in all required fields')), 'warning');
|
||||||
return;
|
return;
|
||||||
@ -568,6 +641,17 @@ return view.extend({
|
|||||||
}).then(function() {
|
}).then(function() {
|
||||||
// Create peers
|
// Create peers
|
||||||
return self.createPeers();
|
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) {
|
}).then(function(results) {
|
||||||
ui.hideModal();
|
ui.hideModal();
|
||||||
|
|
||||||
|
|||||||
@ -107,6 +107,106 @@ var callPingPeer = rpc.declare({
|
|||||||
expect: { reachable: false }
|
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) {
|
function formatBytes(bytes) {
|
||||||
if (bytes === 0) return '0 B';
|
if (bytes === 0) return '0 B';
|
||||||
var k = 1024;
|
var k = 1024;
|
||||||
@ -170,6 +270,12 @@ return baseclass.extend({
|
|||||||
getPeerDescriptions: callPeerDescriptions,
|
getPeerDescriptions: callPeerDescriptions,
|
||||||
getBandwidthRates: callBandwidthRates,
|
getBandwidthRates: callBandwidthRates,
|
||||||
pingPeer: callPingPeer,
|
pingPeer: callPingPeer,
|
||||||
|
getEndpoints: callGetEndpoints,
|
||||||
|
setEndpoint: callSetEndpoint,
|
||||||
|
setDefaultEndpoint: callSetDefaultEndpoint,
|
||||||
|
deleteEndpoint: callDeleteEndpoint,
|
||||||
|
buildEndpointSelector: buildEndpointSelector,
|
||||||
|
getEndpointValue: getEndpointValue,
|
||||||
formatBytes: formatBytes,
|
formatBytes: formatBytes,
|
||||||
formatLastHandshake: formatLastHandshake,
|
formatLastHandshake: formatLastHandshake,
|
||||||
getPeerStatusClass: getPeerStatusClass,
|
getPeerStatusClass: getPeerStatusClass,
|
||||||
|
|||||||
@ -0,0 +1,2 @@
|
|||||||
|
config globals 'globals'
|
||||||
|
option default_endpoint ''
|
||||||
@ -1051,10 +1051,124 @@ get_bandwidth_rates() {
|
|||||||
json_dump
|
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
|
# Main dispatcher
|
||||||
case "$1" in
|
case "$1" in
|
||||||
list)
|
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)
|
call)
|
||||||
case "$2" in
|
case "$2" in
|
||||||
@ -1109,6 +1223,18 @@ case "$1" in
|
|||||||
bandwidth_rates)
|
bandwidth_rates)
|
||||||
get_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"}'
|
echo '{"error": "Unknown method"}'
|
||||||
;;
|
;;
|
||||||
|
|||||||
@ -12,14 +12,16 @@
|
|||||||
"peer_descriptions",
|
"peer_descriptions",
|
||||||
"bandwidth_rates",
|
"bandwidth_rates",
|
||||||
"bandwidth_history",
|
"bandwidth_history",
|
||||||
"endpoint_info"
|
"endpoint_info",
|
||||||
|
"get_endpoints"
|
||||||
],
|
],
|
||||||
"system": [ "info", "board" ],
|
"system": [ "info", "board" ],
|
||||||
"file": [ "read", "stat", "exec" ]
|
"file": [ "read", "stat", "exec" ]
|
||||||
},
|
},
|
||||||
"uci": [ "network", "wireguard-dashboard" ],
|
"uci": [ "network", "wireguard_dashboard" ],
|
||||||
"file": {
|
"file": {
|
||||||
"/etc/config/network": [ "read" ],
|
"/etc/config/network": [ "read" ],
|
||||||
|
"/etc/config/wireguard_dashboard": [ "read" ],
|
||||||
"/usr/bin/wg": [ "exec" ]
|
"/usr/bin/wg": [ "exec" ]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -33,10 +35,13 @@
|
|||||||
"generate_config",
|
"generate_config",
|
||||||
"generate_qr",
|
"generate_qr",
|
||||||
"interface_control",
|
"interface_control",
|
||||||
"ping_peer"
|
"ping_peer",
|
||||||
|
"set_endpoint",
|
||||||
|
"set_default_endpoint",
|
||||||
|
"delete_endpoint"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"uci": [ "wireguard-dashboard", "network" ]
|
"uci": [ "wireguard_dashboard", "network" ]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user