439 lines
14 KiB
JavaScript
439 lines
14 KiB
JavaScript
'use strict';
|
|
'require view';
|
|
'require secubox-theme/bootstrap as Theme';
|
|
'require poll';
|
|
'require dom';
|
|
'require ui';
|
|
'require wireguard-dashboard.api as API';
|
|
|
|
return view.extend({
|
|
title: _('WireGuard Peers'),
|
|
|
|
load: function() {
|
|
return Promise.all([
|
|
API.getPeers(),
|
|
API.getInterfaces()
|
|
]);
|
|
},
|
|
|
|
render: function(data) {
|
|
var self = this;
|
|
var peers = (data[0] || {}).peers || [];
|
|
var interfaces = (data[1] || {}).interfaces || [];
|
|
var activePeers = peers.filter(function(p) { return p.status === 'active'; }).length;
|
|
|
|
var view = E('div', { 'class': 'cbi-map' }, [
|
|
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', { '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(newPeers) {
|
|
// Update table dynamically
|
|
}, 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...'))
|
|
]);
|
|
|
|
API.addPeer(iface, name, allowed_ips, pubkey, psk, endpoint, keepalive).then(function(result) {
|
|
ui.hideModal();
|
|
if (result.success) {
|
|
ui.addNotification(null, E('p', result.message || _('Peer added successfully')), 'info');
|
|
window.location.reload();
|
|
} 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');
|
|
});
|
|
},
|
|
|
|
handleShowQR: function(peer, interfaces, ev) {
|
|
var self = this;
|
|
|
|
ui.showModal(_('Loading QR Code'), [
|
|
E('p', { 'class': 'spinning' }, _('Generating QR code...'))
|
|
]);
|
|
|
|
// Prompt for server endpoint
|
|
ui.hideModal();
|
|
ui.showModal(_('Server Endpoint Required'), [
|
|
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': 'server-endpoint',
|
|
'class': 'cbi-input-text',
|
|
'placeholder': 'vpn.example.com or 203.0.113.1'
|
|
})
|
|
])
|
|
]),
|
|
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('server-endpoint').value;
|
|
if (!endpoint) {
|
|
ui.addNotification(null, E('p', _('Please enter server endpoint')), 'error');
|
|
return;
|
|
}
|
|
|
|
ui.hideModal();
|
|
ui.showModal(_('Generating QR Code'), [
|
|
E('p', { 'class': 'spinning' }, _('Please wait...'))
|
|
]);
|
|
|
|
// Need to get private key from somewhere - this is tricky
|
|
// In real implementation, you'd need to store it or ask user
|
|
ui.addNotification(null, E('p', _('QR code generation requires the peer private key. Please use the config download option and scan manually.')), 'info');
|
|
ui.hideModal();
|
|
}
|
|
}, _('Generate QR'))
|
|
])
|
|
]);
|
|
},
|
|
|
|
handleDownloadConfig: function(peer, interfaces, ev) {
|
|
ui.showModal(_('Server Endpoint Required'), [
|
|
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': 'server-endpoint-cfg',
|
|
'class': 'cbi-input-text',
|
|
'placeholder': 'vpn.example.com or 203.0.113.1'
|
|
})
|
|
])
|
|
]),
|
|
E('div', { 'style': 'margin-top: 1em; padding: 0.75em; background: #fff3cd; border-radius: 4px;' }, [
|
|
E('strong', {}, _('Note:')),
|
|
' ',
|
|
_('Configuration file requires the peer private key. This was generated when the peer was created.')
|
|
]),
|
|
E('div', { 'class': 'right', 'style': 'margin-top: 1em;' }, [
|
|
E('button', {
|
|
'class': 'btn',
|
|
'click': ui.hideModal
|
|
}, _('Cancel'))
|
|
])
|
|
]);
|
|
},
|
|
|
|
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
|
|
});
|