diff --git a/package/secubox/luci-app-wireguard-dashboard/Makefile b/package/secubox/luci-app-wireguard-dashboard/Makefile index 1d4dcb1f..58bc6950 100644 --- a/package/secubox/luci-app-wireguard-dashboard/Makefile +++ b/package/secubox/luci-app-wireguard-dashboard/Makefile @@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-wireguard-dashboard -PKG_VERSION:=0.5.0 +PKG_VERSION:=0.7.0 PKG_RELEASE:=1 PKG_ARCH:=all diff --git a/package/secubox/luci-app-wireguard-dashboard/htdocs/luci-static/resources/view/wireguard-dashboard/overview.js b/package/secubox/luci-app-wireguard-dashboard/htdocs/luci-static/resources/view/wireguard-dashboard/overview.js index acd93467..51b7a4c1 100644 --- a/package/secubox/luci-app-wireguard-dashboard/htdocs/luci-static/resources/view/wireguard-dashboard/overview.js +++ b/package/secubox/luci-app-wireguard-dashboard/htdocs/luci-static/resources/view/wireguard-dashboard/overview.js @@ -11,11 +11,69 @@ return view.extend({ pollInterval: 5, pollActive: true, selectedInterface: 'all', + peerDescriptions: {}, + bandwidthRates: {}, load: function() { return api.getAllData(); }, + // Interface control actions + handleInterfaceAction: function(iface, action) { + var self = this; + ui.showModal(_('Interface Control'), [ + E('p', { 'class': 'spinning' }, _('Executing %s on %s...').format(action, iface)) + ]); + + api.interfaceControl(iface, action).then(function(result) { + ui.hideModal(); + if (result.success) { + ui.addNotification(null, E('p', result.message || _('Action completed')), 'info'); + } else { + ui.addNotification(null, E('p', result.error || _('Action failed')), 'error'); + } + }).catch(function(err) { + ui.hideModal(); + ui.addNotification(null, E('p', _('Error: %s').format(err.message || err)), 'error'); + }); + }, + + // Ping peer + handlePingPeer: function(peerIp, peerName) { + var self = this; + if (!peerIp || peerIp === '(none)') { + ui.addNotification(null, E('p', _('No endpoint IP available for this peer')), 'warning'); + return; + } + + // Extract IP from endpoint (remove port) + var ip = peerIp.split(':')[0]; + + ui.showModal(_('Ping Peer'), [ + E('p', { 'class': 'spinning' }, _('Pinging %s (%s)...').format(peerName, ip)) + ]); + + api.pingPeer(ip).then(function(result) { + ui.hideModal(); + if (result.reachable) { + ui.addNotification(null, E('p', _('Peer %s is reachable (RTT: %s ms)').format(peerName, result.rtt_ms)), 'info'); + } else { + ui.addNotification(null, E('p', _('Peer %s is not reachable').format(peerName)), 'warning'); + } + }).catch(function(err) { + ui.hideModal(); + ui.addNotification(null, E('p', _('Ping failed: %s').format(err.message || err)), 'error'); + }); + }, + + // Get peer display name + getPeerName: function(peer) { + if (this.peerDescriptions && this.peerDescriptions[peer.public_key]) { + return this.peerDescriptions[peer.public_key]; + } + return 'Peer ' + peer.short_key; + }, + // Interface tab filtering setInterfaceFilter: function(ifaceName) { this.selectedInterface = ifaceName; @@ -184,7 +242,10 @@ return view.extend({ var status = data.status || {}; var interfaces = (data.interfaces || {}).interfaces || []; var peers = (data.peers || {}).peers || []; - + + // Store peer descriptions + this.peerDescriptions = data.descriptions || {}; + var activePeers = peers.filter(function(p) { return p.status === 'active'; }).length; var view = E('div', { 'class': 'wireguard-dashboard' }, [ @@ -323,6 +384,25 @@ return view.extend({ E('div', { 'class': 'wg-interface-detail-value wg-interface-traffic' }, 'β' + api.formatBytes(iface.rx_bytes) + ' / β' + api.formatBytes(iface.tx_bytes)) ]) + ]), + // Interface control buttons + E('div', { 'class': 'wg-interface-controls' }, [ + iface.state === 'up' ? + E('button', { + 'class': 'wg-btn wg-btn-sm wg-btn-warning', + 'click': L.bind(self.handleInterfaceAction, self, iface.name, 'down'), + 'title': _('Bring interface down') + }, 'βΉ Stop') : + E('button', { + 'class': 'wg-btn wg-btn-sm wg-btn-success', + 'click': L.bind(self.handleInterfaceAction, self, iface.name, 'up'), + 'title': _('Bring interface up') + }, 'βΆ Start'), + E('button', { + 'class': 'wg-btn wg-btn-sm', + 'click': L.bind(self.handleInterfaceAction, self, iface.name, 'restart'), + 'title': _('Restart interface') + }, 'π Restart') ]) ]); }) @@ -347,12 +427,13 @@ return view.extend({ E('div', { 'class': 'wg-card-body' }, [ E('div', { 'class': 'wg-peer-grid' }, peers.slice(0, 6).map(function(peer) { + var peerName = self.getPeerName(peer); return E('div', { 'class': 'wg-peer-card ' + (peer.status === 'active' ? 'active' : ''), 'data-peer': peer.public_key, 'data-interface': peer.interface || '' }, [ E('div', { 'class': 'wg-peer-header' }, [ E('div', { 'class': 'wg-peer-info' }, [ E('div', { 'class': 'wg-peer-icon' }, peer.status === 'active' ? 'β ' : 'π€'), E('div', {}, [ - E('p', { 'class': 'wg-peer-name' }, 'Peer ' + peer.short_key), + E('p', { 'class': 'wg-peer-name' }, peerName), E('p', { 'class': 'wg-peer-key' }, api.shortenKey(peer.public_key, 16)) ]) ]), @@ -383,7 +464,16 @@ return view.extend({ E('div', { 'class': 'wg-peer-traffic-value tx' }, api.formatBytes(peer.tx_bytes)), E('div', { 'class': 'wg-peer-traffic-label' }, 'Sent') ]) - ]) + ]), + // Peer action buttons + peer.endpoint && peer.endpoint !== '(none)' ? + E('div', { 'class': 'wg-peer-actions' }, [ + E('button', { + 'class': 'wg-btn wg-btn-xs', + 'click': L.bind(self.handlePingPeer, self, peer.endpoint, peerName), + 'title': _('Ping peer') + }, 'π‘ Ping') + ]) : '' ]); }) ) 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 new file mode 100644 index 00000000..ea3b65b2 --- /dev/null +++ b/package/secubox/luci-app-wireguard-dashboard/htdocs/luci-static/resources/view/wireguard-dashboard/wizard.js @@ -0,0 +1,1236 @@ +'use strict'; +'require view'; +'require ui'; +'require rpc'; +'require form'; +'require network'; +'require wireguard-dashboard.api as api'; + +// Zone presets for peer creation +var ZONE_PRESETS = { + 'home-user': { + name: 'Home User', + icon: 'π ', + color: '#22c55e', + description: 'Family members with full network access', + allowed_ips: '0.0.0.0/0, ::/0', + dns: '1.1.1.1, 1.0.0.1', + keepalive: 25, + mtu: 1420, + split_tunnel: false + }, + 'remote-worker': { + name: 'Remote Worker', + icon: 'πΌ', + color: '#3b82f6', + description: 'Work from home with access to office resources', + allowed_ips: '10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16', + dns: '${SERVER_IP}', + keepalive: 25, + mtu: 1420, + split_tunnel: true + }, + 'mobile-device': { + name: 'Mobile Device', + icon: 'π±', + color: '#8b5cf6', + description: 'Smartphones and tablets on the go', + allowed_ips: '0.0.0.0/0, ::/0', + dns: '1.1.1.1, 1.0.0.1', + keepalive: 25, + mtu: 1280, + split_tunnel: false + }, + 'iot-device': { + name: 'IoT Device', + icon: 'π', + color: '#f59e0b', + description: 'Smart home devices with limited access', + allowed_ips: '${VPN_NETWORK}', + dns: '${SERVER_IP}', + keepalive: 60, + mtu: 1420, + split_tunnel: true + }, + 'guest': { + name: 'Guest', + icon: 'π€', + color: '#06b6d4', + description: 'Temporary access for visitors', + allowed_ips: '0.0.0.0/0, ::/0', + dns: '1.1.1.1', + keepalive: 25, + mtu: 1420, + split_tunnel: false, + expires: true + }, + 'server': { + name: 'Server/Site', + icon: 'π₯οΈ', + color: '#ef4444', + description: 'Site-to-site connection to another network', + allowed_ips: '${REMOTE_NETWORK}', + dns: '', + keepalive: 25, + mtu: 1420, + split_tunnel: true + } +}; + +var TUNNEL_PRESETS = { + 'road-warrior': { + name: 'Road Warrior (Remote Access)', + icon: 'π', + description: 'Connect mobile users to your network from anywhere', + listen_port: 51820, + network: '10.10.0.0/24', + server_ip: '10.10.0.1', + peer_start_ip: 2, + recommended_zones: ['home-user', 'remote-worker', 'mobile-device', 'guest'] + }, + 'site-to-site': { + name: 'Site-to-Site VPN', + icon: 'π’', + description: 'Connect two networks securely over the internet', + listen_port: 51821, + network: '10.20.0.0/24', + server_ip: '10.20.0.1', + peer_start_ip: 2, + recommended_zones: ['server'] + }, + 'iot-tunnel': { + name: 'IoT Secure Tunnel', + icon: 'π', + description: 'Isolated tunnel for smart home devices', + listen_port: 51822, + network: '10.30.0.0/24', + server_ip: '10.30.0.1', + peer_start_ip: 2, + recommended_zones: ['iot-device'] + } +}; + +return view.extend({ + title: _('WireGuard Setup Wizard'), + currentStep: 1, + totalSteps: 4, + wizardData: {}, + + load: function() { + return Promise.all([ + api.getInterfaces(), + api.getStatus(), + this.getPublicIP() + ]); + }, + + getPublicIP: function() { + // Try to get public IP + return new Promise(function(resolve) { + fetch('https://api.ipify.org?format=json') + .then(function(r) { return r.json(); }) + .then(function(d) { resolve(d.ip); }) + .catch(function() { resolve(''); }); + }); + }, + + render: function(data) { + var self = this; + var interfaces = (data[0] || {}).interfaces || []; + var status = data[1] || {}; + var publicIP = data[2] || ''; + + this.wizardData.publicIP = publicIP; + this.wizardData.existingInterfaces = interfaces; + + var view = E('div', { 'class': 'wg-wizard' }, [ + E('link', { 'rel': 'stylesheet', 'href': L.resource('wireguard-dashboard/dashboard.css') }), + E('style', {}, this.getWizardCSS()), + + // Header + E('div', { 'class': 'wg-wizard-header' }, [ + E('div', { 'class': 'wg-wizard-logo' }, 'π'), + E('h1', {}, _('WireGuard Setup Wizard')), + E('p', {}, _('Create and configure secure VPN tunnels in minutes')) + ]), + + // Progress bar + E('div', { 'class': 'wg-wizard-progress' }, [ + this.renderProgressStep(1, _('Tunnel Type'), true), + this.renderProgressStep(2, _('Configuration'), false), + this.renderProgressStep(3, _('Add Peers'), false), + this.renderProgressStep(4, _('Complete'), false) + ]), + + // Wizard content + E('div', { 'class': 'wg-wizard-content', 'id': 'wizard-content' }, [ + this.renderStep1() + ]), + + // Navigation + E('div', { 'class': 'wg-wizard-nav' }, [ + E('button', { + 'class': 'wg-btn wg-btn-secondary', + 'id': 'btn-prev', + 'style': 'visibility: hidden;', + 'click': L.bind(this.prevStep, this) + }, _('β Back')), + E('button', { + 'class': 'wg-btn wg-btn-primary', + 'id': 'btn-next', + 'click': L.bind(this.nextStep, this) + }, _('Continue β')) + ]) + ]); + + return view; + }, + + renderProgressStep: function(num, label, active) { + return E('div', { 'class': 'wg-progress-step ' + (active ? 'active' : ''), 'data-step': num }, [ + E('div', { 'class': 'wg-progress-num' }, num), + E('div', { 'class': 'wg-progress-label' }, label) + ]); + }, + + renderStep1: function() { + var self = this; + var presets = Object.keys(TUNNEL_PRESETS).map(function(key) { + var preset = TUNNEL_PRESETS[key]; + return E('div', { + 'class': 'wg-preset-card', + 'data-preset': key, + 'click': function() { + document.querySelectorAll('.wg-preset-card').forEach(function(c) { + c.classList.remove('selected'); + }); + this.classList.add('selected'); + self.wizardData.tunnelPreset = key; + } + }, [ + E('div', { 'class': 'wg-preset-icon' }, preset.icon), + E('div', { 'class': 'wg-preset-info' }, [ + E('h3', {}, preset.name), + E('p', {}, preset.description) + ]), + E('div', { 'class': 'wg-preset-check' }, 'β') + ]); + }); + + return E('div', { 'class': 'wg-wizard-step' }, [ + E('h2', {}, _('Choose Tunnel Type')), + E('p', { 'class': 'wg-step-desc' }, _('Select the type of VPN tunnel you want to create')), + E('div', { 'class': 'wg-preset-grid' }, presets) + ]); + }, + + renderStep2: function() { + var self = this; + var preset = TUNNEL_PRESETS[this.wizardData.tunnelPreset] || TUNNEL_PRESETS['road-warrior']; + + return E('div', { 'class': 'wg-wizard-step' }, [ + E('h2', {}, _('Configure Tunnel')), + E('p', { 'class': 'wg-step-desc' }, _('Set up your %s tunnel').format(preset.name)), + + E('div', { 'class': 'wg-config-grid' }, [ + // Left column - Basic config + E('div', { 'class': 'wg-config-section' }, [ + E('h3', {}, 'π ' + _('Network Settings')), + + E('div', { 'class': 'wg-form-group' }, [ + E('label', {}, _('Interface Name')), + E('input', { + 'type': 'text', + 'id': 'cfg-iface-name', + 'class': 'wg-input', + 'value': 'wg0', + 'placeholder': 'wg0' + }), + E('small', {}, _('Name for the WireGuard interface')) + ]), + + E('div', { 'class': 'wg-form-group' }, [ + E('label', {}, _('Listen Port')), + E('input', { + 'type': 'number', + 'id': 'cfg-listen-port', + 'class': 'wg-input', + 'value': preset.listen_port, + 'min': 1024, + 'max': 65535 + }), + E('small', {}, _('UDP port for incoming connections')) + ]), + + E('div', { 'class': 'wg-form-group' }, [ + E('label', {}, _('VPN Network')), + E('input', { + 'type': 'text', + 'id': 'cfg-vpn-network', + 'class': 'wg-input', + 'value': preset.network, + 'placeholder': '10.10.0.0/24' + }), + E('small', {}, _('Internal network for VPN clients')) + ]), + + E('div', { 'class': 'wg-form-group' }, [ + E('label', {}, _('Server VPN IP')), + E('input', { + 'type': 'text', + 'id': 'cfg-server-ip', + 'class': 'wg-input', + 'value': preset.server_ip, + 'placeholder': '10.10.0.1' + }), + E('small', {}, _('IP address of this server in VPN')) + ]) + ]), + + // Right column - Endpoint + 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) + ]) : '' + ]), + + E('div', { 'class': 'wg-form-group' }, [ + E('label', {}, _('MTU')), + E('input', { + 'type': 'number', + 'id': 'cfg-mtu', + 'class': 'wg-input', + 'value': 1420, + 'min': 1280, + 'max': 1500 + }), + E('small', {}, _('Maximum transmission unit (1420 recommended)')) + ]), + + E('div', { 'class': 'wg-info-box' }, [ + E('strong', {}, 'π‘ ' + _('Firewall Note')), + E('p', {}, _('Port %d/UDP will be opened automatically').format(preset.listen_port)) + ]) + ]) + ]) + ]); + }, + + renderStep3: function() { + var self = this; + var preset = TUNNEL_PRESETS[this.wizardData.tunnelPreset] || TUNNEL_PRESETS['road-warrior']; + var recommendedZones = preset.recommended_zones || Object.keys(ZONE_PRESETS); + + var zoneCards = recommendedZones.map(function(zoneKey) { + var zone = ZONE_PRESETS[zoneKey]; + return E('div', { + 'class': 'wg-zone-card', + 'data-zone': zoneKey, + 'style': '--zone-color: ' + zone.color, + 'click': function() { + this.classList.toggle('selected'); + self.updateSelectedZones(); + } + }, [ + E('div', { 'class': 'wg-zone-header' }, [ + E('span', { 'class': 'wg-zone-icon' }, zone.icon), + E('span', { 'class': 'wg-zone-name' }, zone.name), + E('span', { 'class': 'wg-zone-check' }, 'β') + ]), + E('p', { 'class': 'wg-zone-desc' }, zone.description), + E('div', { 'class': 'wg-zone-details' }, [ + zone.split_tunnel ? + E('span', { 'class': 'wg-tag' }, _('Split Tunnel')) : + E('span', { 'class': 'wg-tag full' }, _('Full Tunnel')), + zone.expires ? + E('span', { 'class': 'wg-tag temp' }, _('Temporary')) : '' + ]) + ]); + }); + + return E('div', { 'class': 'wg-wizard-step' }, [ + E('h2', {}, _('Select Peer Zones')), + E('p', { 'class': 'wg-step-desc' }, _('Choose which types of peers will connect to this tunnel')), + + E('div', { 'class': 'wg-zone-grid' }, zoneCards), + + E('div', { 'class': 'wg-peer-preview', 'id': 'peer-preview' }, [ + E('h3', {}, _('Peers to Create')), + E('p', { 'class': 'wg-no-zones' }, _('Select zones above to add peer templates')) + ]) + ]); + }, + + updateSelectedZones: function() { + var selected = []; + document.querySelectorAll('.wg-zone-card.selected').forEach(function(card) { + selected.push(card.dataset.zone); + }); + this.wizardData.selectedZones = selected; + + var preview = document.getElementById('peer-preview'); + if (selected.length === 0) { + preview.innerHTML = '
' + _('Select zones above to add peer templates') + '
'; + return; + } + + var self = this; + var peerList = selected.map(function(zoneKey, idx) { + var zone = ZONE_PRESETS[zoneKey]; + var ipNum = (self.wizardData.peerStartIP || 2) + idx; + var baseNet = (self.wizardData.vpnNetwork || '10.10.0').split('/')[0].replace(/\.\d+$/, ''); + + return E('div', { 'class': 'wg-peer-item' }, [ + E('span', { 'class': 'wg-peer-icon', 'style': 'background: ' + zone.color }, zone.icon), + E('div', { 'class': 'wg-peer-info' }, [ + E('input', { + 'type': 'text', + 'class': 'wg-peer-name-input', + 'value': zone.name + ' #1', + 'data-zone': zoneKey + }), + E('code', {}, baseNet + '.' + ipNum + '/32') + ]) + ]); + }); + + preview.innerHTML = ''; + preview.appendChild(E('h3', {}, _('Peers to Create') + ' (' + selected.length + ')')); + peerList.forEach(function(p) { preview.appendChild(p); }); + }, + + renderStep4: function() { + var self = this; + + return E('div', { 'class': 'wg-wizard-step wg-step-complete' }, [ + E('div', { 'class': 'wg-complete-icon' }, 'β '), + E('h2', {}, _('Ready to Create Tunnel')), + E('p', { 'class': 'wg-step-desc' }, _('Review your configuration and create the tunnel')), + + E('div', { 'class': 'wg-summary' }, [ + E('div', { 'class': 'wg-summary-section' }, [ + E('h3', {}, 'π ' + _('Tunnel Configuration')), + E('table', { 'class': 'wg-summary-table' }, [ + E('tr', {}, [ + E('td', {}, _('Interface')), + E('td', { 'id': 'sum-iface' }, this.wizardData.ifaceName || 'wg0') + ]), + E('tr', {}, [ + E('td', {}, _('Listen Port')), + E('td', { 'id': 'sum-port' }, this.wizardData.listenPort || '51820') + ]), + E('tr', {}, [ + E('td', {}, _('VPN Network')), + E('td', { 'id': 'sum-network' }, this.wizardData.vpnNetwork || '10.10.0.0/24') + ]), + E('tr', {}, [ + E('td', {}, _('Endpoint')), + E('td', { 'id': 'sum-endpoint' }, this.wizardData.publicEndpoint || '-') + ]) + ]) + ]), + + E('div', { 'class': 'wg-summary-section' }, [ + E('h3', {}, 'π₯ ' + _('Peers')), + E('div', { 'class': 'wg-peer-badges', 'id': 'sum-peers' }, + (this.wizardData.selectedZones || []).map(function(zoneKey) { + var zone = ZONE_PRESETS[zoneKey]; + return E('span', { + 'class': 'wg-peer-badge', + 'style': 'background: ' + zone.color + }, zone.icon + ' ' + zone.name); + }) + ) + ]) + ]), + + E('div', { 'class': 'wg-action-buttons' }, [ + E('button', { + 'class': 'wg-btn wg-btn-lg wg-btn-primary', + 'id': 'btn-create', + 'click': L.bind(this.createTunnel, this) + }, 'π ' + _('Create Tunnel & Peers')) + ]) + ]); + }, + + nextStep: function() { + if (this.currentStep === 1) { + if (!this.wizardData.tunnelPreset) { + ui.addNotification(null, E('p', _('Please select a tunnel type')), 'warning'); + return; + } + } + + if (this.currentStep === 2) { + // Save config values + this.wizardData.ifaceName = document.getElementById('cfg-iface-name').value; + 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; + + if (!this.wizardData.ifaceName || !this.wizardData.listenPort || !this.wizardData.vpnNetwork) { + ui.addNotification(null, E('p', _('Please fill in all required fields')), 'warning'); + return; + } + } + + if (this.currentStep === 3) { + if (!this.wizardData.selectedZones || this.wizardData.selectedZones.length === 0) { + ui.addNotification(null, E('p', _('Please select at least one peer zone')), 'warning'); + return; + } + } + + if (this.currentStep < this.totalSteps) { + this.currentStep++; + this.updateWizard(); + } + }, + + prevStep: function() { + if (this.currentStep > 1) { + this.currentStep--; + this.updateWizard(); + } + }, + + updateWizard: function() { + var content = document.getElementById('wizard-content'); + var btnPrev = document.getElementById('btn-prev'); + var btnNext = document.getElementById('btn-next'); + + // Update progress + document.querySelectorAll('.wg-progress-step').forEach(function(step) { + var stepNum = parseInt(step.dataset.step); + step.classList.toggle('active', stepNum <= this.currentStep); + step.classList.toggle('current', stepNum === this.currentStep); + }.bind(this)); + + // Update content + content.innerHTML = ''; + switch (this.currentStep) { + case 1: content.appendChild(this.renderStep1()); break; + case 2: content.appendChild(this.renderStep2()); break; + case 3: content.appendChild(this.renderStep3()); break; + case 4: content.appendChild(this.renderStep4()); break; + } + + // Update navigation + btnPrev.style.visibility = this.currentStep > 1 ? 'visible' : 'hidden'; + btnNext.textContent = this.currentStep === this.totalSteps ? _('Finish') : _('Continue β'); + btnNext.style.display = this.currentStep === this.totalSteps ? 'none' : ''; + }, + + createTunnel: function() { + var self = this; + + ui.showModal(_('Creating Tunnel'), [ + E('p', { 'class': 'spinning' }, _('Generating keys and configuring tunnel...')) + ]); + + // First generate keys + api.generateKeys().then(function(keys) { + self.wizardData.privateKey = keys.private_key; + self.wizardData.publicKey = keys.public_key; + + // Create interface via UCI + return self.createInterface(); + }).then(function() { + // Create peers + return self.createPeers(); + }).then(function(results) { + ui.hideModal(); + + // Show success with QR codes + self.showCompletionModal(results); + }).catch(function(err) { + ui.hideModal(); + ui.addNotification(null, E('p', _('Error: %s').format(err.message || err)), 'error'); + }); + }, + + createInterface: function() { + var self = this; + var data = this.wizardData; + + // Call backend to create interface + return rpc.call('uci', 'add', { + config: 'network', + type: 'interface', + name: data.ifaceName, + values: { + proto: 'wireguard', + private_key: data.privateKey, + listen_port: data.listenPort, + addresses: [data.serverIP + '/' + data.vpnNetwork.split('/')[1]] + } + }).then(function() { + return rpc.call('uci', 'commit', { config: 'network' }); + }); + }, + + createPeers: function() { + var self = this; + var data = this.wizardData; + var promises = []; + var results = []; + + var baseNet = data.vpnNetwork.split('/')[0].replace(/\.\d+$/, ''); + + (data.selectedZones || []).forEach(function(zoneKey, idx) { + var zone = ZONE_PRESETS[zoneKey]; + var peerIP = baseNet + '.' + (2 + idx); + + promises.push( + api.generateKeys().then(function(keys) { + var peerData = { + zone: zoneKey, + zoneName: zone.name, + zoneIcon: zone.icon, + zoneColor: zone.color, + publicKey: keys.public_key, + privateKey: keys.private_key, + presharedKey: keys.preshared_key, + allowedIP: peerIP + '/32', + serverPublicKey: data.publicKey, + endpoint: data.publicEndpoint + ':' + data.listenPort, + dns: zone.dns.replace('${SERVER_IP}', data.serverIP), + mtu: zone.mtu || data.mtu, + clientAllowedIPs: zone.split_tunnel ? data.vpnNetwork : '0.0.0.0/0, ::/0' + }; + + results.push(peerData); + + // Add peer to interface + return api.addPeer( + data.ifaceName, + zone.name + '_' + (idx + 1), + peerData.allowedIP, + keys.public_key, + keys.preshared_key, + '', + zone.keepalive.toString() + ); + }) + ); + }); + + return Promise.all(promises).then(function() { + return results; + }); + }, + + showCompletionModal: function(peers) { + var self = this; + var data = this.wizardData; + + var peerCards = peers.map(function(peer) { + var config = self.generateClientConfig(peer); + + return E('div', { 'class': 'wg-result-peer' }, [ + E('div', { 'class': 'wg-result-header', 'style': 'border-color: ' + peer.zoneColor }, [ + E('span', { 'class': 'wg-result-icon' }, peer.zoneIcon), + E('span', { 'class': 'wg-result-name' }, peer.zoneName), + E('code', {}, peer.allowedIP) + ]), + E('div', { 'class': 'wg-result-actions' }, [ + E('button', { + 'class': 'wg-btn wg-btn-sm', + 'click': function() { + navigator.clipboard.writeText(config); + ui.addNotification(null, E('p', _('Configuration copied!')), 'info'); + } + }, 'π ' + _('Copy Config')), + E('button', { + 'class': 'wg-btn wg-btn-sm wg-btn-primary', + 'click': function() { + self.showQRModal(peer, config); + } + }, 'π± ' + _('QR Code')) + ]) + ]); + }); + + ui.showModal(_('π Tunnel Created Successfully!'), [ + E('div', { 'class': 'wg-completion' }, [ + E('p', {}, _('Your WireGuard tunnel "%s" is ready.').format(data.ifaceName)), + E('div', { 'class': 'wg-result-grid' }, peerCards), + E('div', { 'class': 'wg-completion-actions' }, [ + E('button', { + 'class': 'wg-btn wg-btn-primary', + 'click': function() { + ui.hideModal(); + window.location.href = L.url('admin/secubox/network/wireguard/overview'); + } + }, _('Go to Dashboard')) + ]) + ]) + ]); + }, + + generateClientConfig: function(peer) { + return '[Interface]\n' + + 'PrivateKey = ' + peer.privateKey + '\n' + + 'Address = ' + peer.allowedIP + '\n' + + 'DNS = ' + peer.dns + '\n' + + 'MTU = ' + peer.mtu + '\n\n' + + '[Peer]\n' + + 'PublicKey = ' + peer.serverPublicKey + '\n' + + 'PresharedKey = ' + peer.presharedKey + '\n' + + 'Endpoint = ' + peer.endpoint + '\n' + + 'AllowedIPs = ' + peer.clientAllowedIPs + '\n' + + 'PersistentKeepalive = 25'; + }, + + showQRModal: function(peer, config) { + var self = this; + + // Generate QR using JavaScript library + var qrContainer = E('div', { 'class': 'wg-qr-container', 'id': 'qr-code' }); + + ui.showModal(peer.zoneIcon + ' ' + peer.zoneName + ' - QR Code', [ + E('div', { 'style': 'text-align: center;' }, [ + qrContainer, + E('p', { 'style': 'margin-top: 1em;' }, _('Scan with WireGuard app')), + E('details', { 'style': 'margin-top: 1em; text-align: left;' }, [ + E('summary', {}, _('Show configuration')), + E('pre', { 'style': 'font-size: 11px; background: #1e293b; padding: 12px; border-radius: 8px;' }, config) + ]) + ]), + E('div', { 'class': 'right', 'style': 'margin-top: 1em;' }, [ + E('button', { 'class': 'btn', 'click': ui.hideModal }, _('Close')) + ]) + ]); + + // Load QR library and generate + if (typeof QRCode !== 'undefined') { + new QRCode(qrContainer, { + text: config, + width: 256, + height: 256 + }); + } else { + qrContainer.innerHTML = '' + _('QR library not loaded') + '
'; + } + }, + + getWizardCSS: function() { + return ` + .wg-wizard { + max-width: 900px; + margin: 0 auto; + padding: 20px; + } + + .wg-wizard-header { + text-align: center; + margin-bottom: 30px; + } + + .wg-wizard-logo { + font-size: 48px; + margin-bottom: 10px; + } + + .wg-wizard-header h1 { + font-size: 28px; + margin: 0 0 8px; + background: linear-gradient(135deg, #06b6d4, #6366f1); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + } + + .wg-wizard-header p { + color: var(--wg-text-secondary); + margin: 0; + } + + /* Progress */ + .wg-wizard-progress { + display: flex; + justify-content: space-between; + margin-bottom: 30px; + position: relative; + } + + .wg-wizard-progress::before { + content: ''; + position: absolute; + top: 18px; + left: 50px; + right: 50px; + height: 2px; + background: var(--wg-border); + } + + .wg-progress-step { + display: flex; + flex-direction: column; + align-items: center; + position: relative; + z-index: 1; + } + + .wg-progress-num { + width: 36px; + height: 36px; + border-radius: 50%; + background: var(--wg-bg-tertiary); + border: 2px solid var(--wg-border); + display: flex; + align-items: center; + justify-content: center; + font-weight: 600; + transition: all 0.3s; + } + + .wg-progress-step.active .wg-progress-num { + background: linear-gradient(135deg, #06b6d4, #6366f1); + border-color: transparent; + color: white; + } + + .wg-progress-step.current .wg-progress-num { + box-shadow: 0 0 20px rgba(6, 182, 212, 0.5); + } + + .wg-progress-label { + margin-top: 8px; + font-size: 12px; + color: var(--wg-text-muted); + } + + /* Content */ + .wg-wizard-content { + background: var(--wg-bg-secondary); + border: 1px solid var(--wg-border); + border-radius: 12px; + padding: 30px; + min-height: 400px; + } + + .wg-wizard-step h2 { + margin: 0 0 8px; + font-size: 22px; + } + + .wg-step-desc { + color: var(--wg-text-secondary); + margin: 0 0 24px; + } + + /* Preset cards */ + .wg-preset-grid { + display: flex; + flex-direction: column; + gap: 12px; + } + + .wg-preset-card { + display: flex; + align-items: center; + gap: 16px; + padding: 16px 20px; + background: var(--wg-bg-tertiary); + border: 2px solid var(--wg-border); + border-radius: 10px; + cursor: pointer; + transition: all 0.2s; + } + + .wg-preset-card:hover { + border-color: var(--wg-accent-cyan); + } + + .wg-preset-card.selected { + border-color: var(--wg-accent-cyan); + background: rgba(6, 182, 212, 0.1); + } + + .wg-preset-icon { + font-size: 32px; + } + + .wg-preset-info { + flex: 1; + } + + .wg-preset-info h3 { + margin: 0 0 4px; + font-size: 16px; + } + + .wg-preset-info p { + margin: 0; + font-size: 13px; + color: var(--wg-text-secondary); + } + + .wg-preset-check { + width: 24px; + height: 24px; + border-radius: 50%; + background: var(--wg-accent-cyan); + color: white; + display: flex; + align-items: center; + justify-content: center; + opacity: 0; + transition: opacity 0.2s; + } + + .wg-preset-card.selected .wg-preset-check { + opacity: 1; + } + + /* Zone cards */ + .wg-zone-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + gap: 12px; + margin-bottom: 24px; + } + + .wg-zone-card { + padding: 16px; + background: var(--wg-bg-tertiary); + border: 2px solid var(--wg-border); + border-radius: 10px; + cursor: pointer; + transition: all 0.2s; + } + + .wg-zone-card:hover { + border-color: var(--zone-color, var(--wg-accent-cyan)); + } + + .wg-zone-card.selected { + border-color: var(--zone-color, var(--wg-accent-cyan)); + background: color-mix(in srgb, var(--zone-color, var(--wg-accent-cyan)) 10%, transparent); + } + + .wg-zone-header { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 8px; + } + + .wg-zone-icon { + font-size: 20px; + } + + .wg-zone-name { + flex: 1; + font-weight: 600; + } + + .wg-zone-check { + color: var(--zone-color, var(--wg-accent-cyan)); + opacity: 0; + } + + .wg-zone-card.selected .wg-zone-check { + opacity: 1; + } + + .wg-zone-desc { + font-size: 12px; + color: var(--wg-text-secondary); + margin: 0 0 8px; + } + + .wg-zone-details { + display: flex; + gap: 6px; + flex-wrap: wrap; + } + + .wg-tag { + font-size: 10px; + padding: 2px 6px; + border-radius: 4px; + background: var(--wg-bg-secondary); + color: var(--wg-text-muted); + } + + .wg-tag.full { background: rgba(6, 182, 212, 0.2); color: #06b6d4; } + .wg-tag.temp { background: rgba(245, 158, 11, 0.2); color: #f59e0b; } + + /* Config grid */ + .wg-config-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 24px; + } + + @media (max-width: 768px) { + .wg-config-grid { grid-template-columns: 1fr; } + } + + .wg-config-section h3 { + font-size: 14px; + margin: 0 0 16px; + padding-bottom: 8px; + border-bottom: 1px solid var(--wg-border); + } + + .wg-form-group { + margin-bottom: 16px; + } + + .wg-form-group label { + display: block; + font-size: 13px; + font-weight: 500; + margin-bottom: 6px; + } + + .wg-input { + width: 100%; + padding: 10px 12px; + background: var(--wg-bg-primary); + border: 1px solid var(--wg-border); + border-radius: 6px; + color: var(--wg-text-primary); + font-size: 14px; + } + + .wg-input:focus { + border-color: var(--wg-accent-cyan); + outline: none; + } + + .wg-form-group small { + display: block; + margin-top: 4px; + font-size: 11px; + color: var(--wg-text-muted); + } + + .wg-detected { + margin-top: 8px; + padding: 8px 12px; + background: rgba(16, 185, 129, 0.1); + border-radius: 6px; + font-size: 12px; + color: var(--wg-accent-green); + } + + .wg-info-box { + padding: 12px; + background: rgba(6, 182, 212, 0.1); + border-radius: 8px; + font-size: 12px; + } + + .wg-info-box strong { + display: block; + margin-bottom: 4px; + } + + .wg-info-box p { + margin: 0; + color: var(--wg-text-secondary); + } + + /* Peer preview */ + .wg-peer-preview { + background: var(--wg-bg-primary); + border-radius: 8px; + padding: 16px; + } + + .wg-peer-preview h3 { + font-size: 14px; + margin: 0 0 12px; + } + + .wg-no-zones { + color: var(--wg-text-muted); + font-size: 13px; + text-align: center; + padding: 20px; + } + + .wg-peer-item { + display: flex; + align-items: center; + gap: 12px; + padding: 10px; + background: var(--wg-bg-secondary); + border-radius: 6px; + margin-bottom: 8px; + } + + .wg-peer-icon { + width: 32px; + height: 32px; + border-radius: 6px; + display: flex; + align-items: center; + justify-content: center; + font-size: 16px; + } + + .wg-peer-info { + flex: 1; + display: flex; + align-items: center; + gap: 12px; + } + + .wg-peer-name-input { + flex: 1; + background: transparent; + border: 1px solid transparent; + padding: 4px 8px; + border-radius: 4px; + color: var(--wg-text-primary); + } + + .wg-peer-name-input:focus { + border-color: var(--wg-border); + background: var(--wg-bg-tertiary); + } + + /* Complete step */ + .wg-step-complete { + text-align: center; + } + + .wg-complete-icon { + font-size: 64px; + margin-bottom: 16px; + } + + .wg-summary { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 24px; + margin: 24px 0; + text-align: left; + } + + .wg-summary-section { + background: var(--wg-bg-tertiary); + padding: 16px; + border-radius: 8px; + } + + .wg-summary-section h3 { + font-size: 14px; + margin: 0 0 12px; + } + + .wg-summary-table { + width: 100%; + font-size: 13px; + } + + .wg-summary-table td { + padding: 6px 0; + border-bottom: 1px solid var(--wg-border); + } + + .wg-summary-table td:first-child { + color: var(--wg-text-muted); + } + + .wg-peer-badges { + display: flex; + flex-wrap: wrap; + gap: 8px; + } + + .wg-peer-badge { + padding: 6px 12px; + border-radius: 20px; + font-size: 12px; + color: white; + } + + .wg-action-buttons { + margin-top: 24px; + } + + .wg-btn-lg { + padding: 14px 28px; + font-size: 16px; + } + + /* Result */ + .wg-result-grid { + display: grid; + gap: 12px; + margin: 20px 0; + } + + .wg-result-peer { + background: var(--wg-bg-tertiary); + border-radius: 8px; + padding: 12px; + } + + .wg-result-header { + display: flex; + align-items: center; + gap: 12px; + padding-bottom: 10px; + margin-bottom: 10px; + border-bottom: 2px solid; + } + + .wg-result-icon { + font-size: 24px; + } + + .wg-result-name { + flex: 1; + font-weight: 600; + } + + .wg-result-actions { + display: flex; + gap: 8px; + justify-content: center; + } + + /* Navigation */ + .wg-wizard-nav { + display: flex; + justify-content: space-between; + margin-top: 24px; + } + + .wg-btn-secondary { + background: transparent; + border: 1px solid var(--wg-border); + } + + .wg-btn-secondary:hover { + background: var(--wg-bg-tertiary); + } + `; + }, + + handleSaveApply: null, + handleSave: null, + handleReset: null +}); 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 4a7f6fd7..8b00d2d2 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 @@ -18,13 +18,13 @@ var callStatus = rpc.declare({ var callGetPeers = rpc.declare({ object: 'luci.wireguard-dashboard', - method: 'get_peers', + method: 'peers', expect: { peers: [] } }); var callGetInterfaces = rpc.declare({ object: 'luci.wireguard-dashboard', - method: 'get_interfaces', + method: 'interfaces', expect: { interfaces: [] } }); @@ -74,6 +74,32 @@ var callGetTraffic = rpc.declare({ expect: { } }); +var callInterfaceControl = rpc.declare({ + object: 'luci.wireguard-dashboard', + method: 'interface_control', + params: ['interface', 'action'], + expect: { success: false } +}); + +var callPeerDescriptions = rpc.declare({ + object: 'luci.wireguard-dashboard', + method: 'peer_descriptions', + expect: { descriptions: {} } +}); + +var callBandwidthRates = rpc.declare({ + object: 'luci.wireguard-dashboard', + method: 'bandwidth_rates', + expect: { rates: [] } +}); + +var callPingPeer = rpc.declare({ + object: 'luci.wireguard-dashboard', + method: 'ping_peer', + params: ['ip'], + expect: { reachable: false } +}); + function formatBytes(bytes) { if (bytes === 0) return '0 B'; var k = 1024; @@ -112,6 +138,15 @@ function formatHandshake(seconds) { return Math.floor(seconds / 86400) + 'd ago'; } +function formatRate(bytesPerSec) { + if (!bytesPerSec || bytesPerSec === 0) return '0 B/s'; + var k = 1024; + var sizes = ['B/s', 'KB/s', 'MB/s', 'GB/s']; + var i = Math.floor(Math.log(bytesPerSec) / Math.log(k)); + if (i >= sizes.length) i = sizes.length - 1; + return parseFloat((bytesPerSec / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]; +} + return baseclass.extend({ getStatus: callStatus, getPeers: callGetPeers, @@ -123,11 +158,16 @@ return baseclass.extend({ removePeer: callRemovePeer, generateConfig: callGenerateConfig, generateQR: callGenerateQR, + interfaceControl: callInterfaceControl, + getPeerDescriptions: callPeerDescriptions, + getBandwidthRates: callBandwidthRates, + pingPeer: callPingPeer, formatBytes: formatBytes, formatLastHandshake: formatLastHandshake, getPeerStatusClass: getPeerStatusClass, shortenKey: shortenKey, formatHandshake: formatHandshake, + formatRate: formatRate, // Aggregate function for overview page getAllData: function() { @@ -135,13 +175,32 @@ return baseclass.extend({ callStatus(), callGetPeers(), callGetInterfaces(), - callGetTraffic() + callGetTraffic(), + callPeerDescriptions() ]).then(function(results) { return { status: results[0] || {}, peers: results[1] || { peers: [] }, interfaces: results[2] || { interfaces: [] }, - traffic: results[3] || {} + traffic: results[3] || {}, + descriptions: (results[4] || {}).descriptions || {} + }; + }); + }, + + // Get data with bandwidth rates for real-time monitoring + getMonitoringData: function() { + return Promise.all([ + callStatus(), + callGetPeers(), + callBandwidthRates(), + callPeerDescriptions() + ]).then(function(results) { + return { + status: results[0] || {}, + peers: results[1] || { peers: [] }, + rates: (results[2] || {}).rates || [], + descriptions: (results[3] || {}).descriptions || {} }; }); } diff --git a/package/secubox/luci-app-wireguard-dashboard/htdocs/luci-static/resources/wireguard-dashboard/dashboard.css b/package/secubox/luci-app-wireguard-dashboard/htdocs/luci-static/resources/wireguard-dashboard/dashboard.css index 5aab2df6..d8542d04 100644 --- a/package/secubox/luci-app-wireguard-dashboard/htdocs/luci-static/resources/wireguard-dashboard/dashboard.css +++ b/package/secubox/luci-app-wireguard-dashboard/htdocs/luci-static/resources/wireguard-dashboard/dashboard.css @@ -816,6 +816,62 @@ font-size: 12px; } +.wg-btn-xs { + padding: 4px 8px; + font-size: 11px; + gap: 4px; +} + +.wg-btn-success { + background: linear-gradient(135deg, var(--wg-accent-green), #059669); + border: none; + color: white; +} + +.wg-btn-success:hover { + box-shadow: 0 0 20px rgba(16, 185, 129, 0.4); +} + +.wg-btn-warning { + background: linear-gradient(135deg, var(--wg-accent-yellow), #d97706); + border: none; + color: white; +} + +.wg-btn-warning:hover { + box-shadow: 0 0 20px rgba(245, 158, 11, 0.4); +} + +.wg-btn-danger { + background: linear-gradient(135deg, var(--wg-accent-red), #dc2626); + border: none; + color: white; +} + +.wg-btn-danger:hover { + box-shadow: 0 0 20px rgba(239, 68, 68, 0.4); +} + +/* Interface controls */ +.wg-interface-controls { + display: flex; + gap: 8px; + padding-top: 12px; + margin-top: 12px; + border-top: 1px solid var(--wg-border); + justify-content: flex-end; +} + +/* Peer actions */ +.wg-peer-actions { + display: flex; + gap: 6px; + padding-top: 10px; + margin-top: 10px; + border-top: 1px solid var(--wg-border); + justify-content: center; +} + /* Auto-refresh control */ .wg-refresh-control { display: flex; @@ -1140,3 +1196,528 @@ .wg-chart-legend-dot.tx { background: var(--wg-accent-blue); } + +/* ========== Wizard Styles ========== */ + +/* Tunnel type selection */ +.wg-tunnel-types { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: 16px; + margin: 20px 0; +} + +.wg-tunnel-type-card { + background: var(--wg-bg-tertiary); + border: 2px solid var(--wg-border); + border-radius: var(--wg-radius-lg); + padding: 24px; + cursor: pointer; + transition: all 0.3s; + position: relative; + overflow: hidden; +} + +.wg-tunnel-type-card:hover { + border-color: var(--wg-accent-cyan); + transform: translateY(-2px); +} + +.wg-tunnel-type-card.selected { + border-color: var(--wg-accent-green); + background: linear-gradient(135deg, rgba(16, 185, 129, 0.1), transparent); +} + +.wg-tunnel-type-card.selected::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 4px; + background: var(--wg-accent-green); +} + +.wg-tunnel-type-icon { + font-size: 40px; + margin-bottom: 12px; +} + +.wg-tunnel-type-name { + font-size: 18px; + font-weight: 600; + margin-bottom: 8px; +} + +.wg-tunnel-type-desc { + font-size: 13px; + color: var(--wg-text-secondary); + line-height: 1.5; +} + +.wg-tunnel-type-features { + display: flex; + flex-wrap: wrap; + gap: 6px; + margin-top: 12px; +} + +.wg-tunnel-type-feature { + font-size: 10px; + padding: 4px 8px; + background: var(--wg-bg-primary); + border-radius: 10px; + color: var(--wg-text-muted); +} + +/* Zone preset cards */ +.wg-zone-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 14px; + margin: 20px 0; +} + +.wg-zone-card { + background: var(--wg-bg-tertiary); + border: 2px solid var(--wg-border); + border-radius: var(--wg-radius-lg); + padding: 20px; + cursor: pointer; + transition: all 0.3s; + text-align: center; +} + +.wg-zone-card:hover { + border-color: var(--wg-accent-cyan); + transform: translateY(-2px); +} + +.wg-zone-card.selected { + border-color: var(--zone-color, var(--wg-accent-green)); + background: linear-gradient(135deg, rgba(var(--zone-color-rgb, 16, 185, 129), 0.1), transparent); +} + +.wg-zone-icon { + font-size: 36px; + margin-bottom: 10px; +} + +.wg-zone-name { + font-size: 14px; + font-weight: 600; + margin-bottom: 6px; +} + +.wg-zone-desc { + font-size: 11px; + color: var(--wg-text-muted); + line-height: 1.4; +} + +/* Wizard form */ +.wg-wizard-form { + max-width: 600px; + margin: 0 auto; +} + +.wg-wizard-section { + margin-bottom: 24px; +} + +.wg-wizard-section-title { + font-size: 14px; + font-weight: 600; + margin-bottom: 12px; + display: flex; + align-items: center; + gap: 8px; +} + +.wg-wizard-section-title::before { + content: ''; + width: 4px; + height: 16px; + background: var(--wg-tunnel-gradient); + border-radius: 2px; +} + +/* Input styles */ +.wg-input { + width: 100%; + padding: 12px 14px; + background: var(--wg-bg-primary); + border: 1px solid var(--wg-border); + border-radius: var(--wg-radius); + color: var(--wg-text-primary); + font-family: var(--wg-font-mono); + font-size: 13px; + transition: all 0.2s; +} + +.wg-input:focus { + outline: none; + border-color: var(--wg-accent-cyan); + box-shadow: 0 0 0 3px rgba(6, 182, 212, 0.15); +} + +.wg-input::placeholder { + color: var(--wg-text-muted); +} + +.wg-select { + width: 100%; + padding: 12px 14px; + background: var(--wg-bg-primary); + border: 1px solid var(--wg-border); + border-radius: var(--wg-radius); + color: var(--wg-text-primary); + font-size: 13px; + cursor: pointer; +} + +.wg-select:focus { + outline: none; + border-color: var(--wg-accent-cyan); +} + +.wg-checkbox-group { + display: flex; + align-items: center; + gap: 10px; + padding: 12px 14px; + background: var(--wg-bg-tertiary); + border-radius: var(--wg-radius); + cursor: pointer; +} + +.wg-checkbox-group:hover { + background: var(--wg-bg-secondary); +} + +.wg-checkbox { + width: 18px; + height: 18px; + accent-color: var(--wg-accent-cyan); +} + +/* Peer list in wizard */ +.wg-peer-list { + display: flex; + flex-direction: column; + gap: 12px; + margin: 16px 0; +} + +.wg-peer-list-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 14px 16px; + background: var(--wg-bg-tertiary); + border: 1px solid var(--wg-border); + border-radius: var(--wg-radius); +} + +.wg-peer-list-info { + display: flex; + align-items: center; + gap: 12px; +} + +.wg-peer-list-icon { + width: 40px; + height: 40px; + border-radius: 10px; + display: flex; + align-items: center; + justify-content: center; + font-size: 20px; + background: var(--wg-bg-primary); +} + +.wg-peer-list-details h4 { + font-size: 14px; + font-weight: 600; + margin: 0 0 4px 0; +} + +.wg-peer-list-details p { + font-size: 11px; + color: var(--wg-text-muted); + margin: 0; + font-family: var(--wg-font-mono); +} + +.wg-peer-list-actions { + display: flex; + gap: 8px; +} + +/* QR Code Modal */ +.wg-modal-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.8); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; + animation: fade-in 0.2s ease-out; +} + +@keyframes fade-in { + from { opacity: 0; } + to { opacity: 1; } +} + +.wg-modal { + background: var(--wg-bg-secondary); + border: 1px solid var(--wg-border); + border-radius: var(--wg-radius-lg); + max-width: 500px; + width: 90%; + max-height: 90vh; + overflow-y: auto; + animation: modal-slide 0.3s ease-out; +} + +@keyframes modal-slide { + from { + opacity: 0; + transform: translateY(-20px) scale(0.95); + } + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} + +.wg-modal-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 16px 20px; + border-bottom: 1px solid var(--wg-border); +} + +.wg-modal-title { + font-size: 16px; + font-weight: 600; + display: flex; + align-items: center; + gap: 10px; +} + +.wg-modal-close { + width: 32px; + height: 32px; + border: none; + background: transparent; + color: var(--wg-text-muted); + font-size: 24px; + cursor: pointer; + border-radius: 6px; + transition: all 0.2s; +} + +.wg-modal-close:hover { + background: var(--wg-bg-tertiary); + color: var(--wg-text-primary); +} + +.wg-modal-body { + padding: 24px; +} + +.wg-modal-footer { + display: flex; + gap: 12px; + justify-content: flex-end; + padding: 16px 20px; + border-top: 1px solid var(--wg-border); +} + +/* QR Code display */ +.wg-qr-container { + text-align: center; + margin: 20px 0; +} + +.wg-qr-code { + display: inline-block; + padding: 16px; + background: white; + border-radius: var(--wg-radius-lg); +} + +.wg-qr-code img, +.wg-qr-code svg { + display: block; + max-width: 200px; + height: auto; +} + +.wg-qr-hint { + font-size: 12px; + color: var(--wg-text-muted); + margin-top: 12px; +} + +/* Config preview */ +.wg-config-preview { + background: var(--wg-bg-primary); + border: 1px solid var(--wg-border); + border-radius: var(--wg-radius); + padding: 16px; + font-family: var(--wg-font-mono); + font-size: 12px; + line-height: 1.6; + white-space: pre-wrap; + word-break: break-all; + max-height: 300px; + overflow-y: auto; +} + +/* Wizard navigation */ +.wg-wizard-nav { + display: flex; + justify-content: space-between; + margin-top: 30px; + padding-top: 20px; + border-top: 1px solid var(--wg-border); +} + +/* Success state */ +.wg-success-state { + text-align: center; + padding: 40px 20px; +} + +.wg-success-icon { + font-size: 64px; + margin-bottom: 20px; + animation: bounce-in 0.5s ease-out; +} + +@keyframes bounce-in { + 0% { transform: scale(0); } + 50% { transform: scale(1.2); } + 100% { transform: scale(1); } +} + +.wg-success-title { + font-size: 24px; + font-weight: 700; + margin-bottom: 12px; + color: var(--wg-accent-green); +} + +.wg-success-desc { + font-size: 14px; + color: var(--wg-text-secondary); + margin-bottom: 24px; +} + +/* Loading state */ +.wg-loading { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 60px 20px; +} + +.wg-loading-spinner { + width: 48px; + height: 48px; + border: 3px solid var(--wg-border); + border-top-color: var(--wg-accent-cyan); + border-radius: 50%; + animation: spin 1s linear infinite; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +.wg-loading-text { + margin-top: 16px; + font-size: 14px; + color: var(--wg-text-secondary); +} + +/* Info box */ +.wg-info-box { + display: flex; + gap: 12px; + padding: 14px 16px; + background: rgba(6, 182, 212, 0.1); + border: 1px solid rgba(6, 182, 212, 0.3); + border-radius: var(--wg-radius); + margin: 16px 0; +} + +.wg-info-box-icon { + font-size: 20px; + flex-shrink: 0; +} + +.wg-info-box-content { + font-size: 13px; + color: var(--wg-text-secondary); + line-height: 1.5; +} + +.wg-info-box.warning { + background: rgba(245, 158, 11, 0.1); + border-color: rgba(245, 158, 11, 0.3); +} + +/* Copy button */ +.wg-copy-btn { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 6px 12px; + background: var(--wg-bg-tertiary); + border: 1px solid var(--wg-border); + border-radius: 6px; + color: var(--wg-text-secondary); + font-size: 12px; + cursor: pointer; + transition: all 0.2s; +} + +.wg-copy-btn:hover { + border-color: var(--wg-accent-cyan); + color: var(--wg-text-primary); +} + +.wg-copy-btn.copied { + border-color: var(--wg-accent-green); + color: var(--wg-accent-green); +} + +/* Endpoint detection */ +.wg-endpoint-detect { + display: flex; + gap: 10px; + align-items: center; +} + +.wg-endpoint-detect .wg-input { + flex: 1; +} + +.wg-endpoint-status { + font-size: 11px; + color: var(--wg-text-muted); + display: flex; + align-items: center; + gap: 6px; + margin-top: 6px; +} 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 baec31ee..79dc5daa 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 @@ -752,10 +752,148 @@ ping_peer() { json_dump } +# Interface up/down control +interface_control() { + read input + json_load "$input" + json_get_var iface interface + json_get_var action action + + json_init + + if [ -z "$iface" ] || [ -z "$action" ]; then + json_add_boolean "success" 0 + json_add_string "error" "Missing interface or action" + json_dump + return + fi + + # Validate action + case "$action" in + up|down|restart) + ;; + *) + json_add_boolean "success" 0 + json_add_string "error" "Invalid action. Use: up, down, restart" + json_dump + return + ;; + esac + + # Check if interface exists + if ! uci -q get network.$iface >/dev/null; then + json_add_boolean "success" 0 + json_add_string "error" "Interface not found: $iface" + json_dump + return + fi + + # Execute action + case "$action" in + up) + ifup "$iface" 2>/dev/null + ;; + down) + ifdown "$iface" 2>/dev/null + ;; + restart) + ifdown "$iface" 2>/dev/null + sleep 1 + ifup "$iface" 2>/dev/null + ;; + esac + + local rc=$? + if [ $rc -eq 0 ]; then + json_add_boolean "success" 1 + json_add_string "message" "Interface $iface $action completed" + else + json_add_boolean "success" 0 + json_add_string "error" "Failed to $action interface $iface" + fi + + json_dump +} + +# Get peer descriptions from UCI +get_peer_descriptions() { + json_init + json_add_object "descriptions" + + local interfaces=$($WG_CMD show interfaces 2>/dev/null) + + for iface in $interfaces; do + local sections=$(uci show network 2>/dev/null | grep "=wireguard_$iface$" | cut -d'.' -f2 | cut -d'=' -f1) + for section in $sections; do + local pubkey=$(uci -q get network.$section.public_key) + local desc=$(uci -q get network.$section.description) + if [ -n "$pubkey" ] && [ -n "$desc" ]; then + json_add_string "$pubkey" "$desc" + fi + done + done + + json_close_object + json_dump +} + +# Get current bandwidth rates (requires previous call to calculate delta) +get_bandwidth_rates() { + json_init + + local interfaces=$($WG_CMD show interfaces 2>/dev/null) + local now=$(date +%s) + + json_add_array "rates" + + for iface in $interfaces; do + local rx_bytes=$(cat /sys/class/net/$iface/statistics/rx_bytes 2>/dev/null || echo 0) + local tx_bytes=$(cat /sys/class/net/$iface/statistics/tx_bytes 2>/dev/null || echo 0) + + # Get previous values from temp file + local prev_file="/tmp/wg_rate_$iface" + local prev_rx=0 + local prev_tx=0 + local prev_time=0 + + if [ -f "$prev_file" ]; then + read prev_rx prev_tx prev_time < "$prev_file" + fi + + # Calculate rates + local rx_rate=0 + local tx_rate=0 + local time_diff=$((now - prev_time)) + + if [ $time_diff -gt 0 ] && [ $prev_time -gt 0 ]; then + rx_rate=$(( (rx_bytes - prev_rx) / time_diff )) + tx_rate=$(( (tx_bytes - prev_tx) / time_diff )) + # Ensure non-negative rates + [ $rx_rate -lt 0 ] && rx_rate=0 + [ $tx_rate -lt 0 ] && tx_rate=0 + fi + + # Save current values + echo "$rx_bytes $tx_bytes $now" > "$prev_file" + + json_add_object + json_add_string "interface" "$iface" + json_add_int "rx_rate" "$rx_rate" + json_add_int "tx_rate" "$tx_rate" + json_add_int "rx_total" "$rx_bytes" + json_add_int "tx_total" "$tx_bytes" + json_close_object + done + + json_close_array + json_add_int "timestamp" "$now" + json_dump +} + # Main dispatcher case "$1" in list) - echo '{"status":{},"interfaces":{},"peers":{},"traffic":{},"config":{},"generate_keys":{},"add_peer":{"interface":"str","name":"str","allowed_ips":"str","public_key":"str","preshared_key":"str","endpoint":"str","persistent_keepalive":"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"}}' + echo '{"status":{},"interfaces":{},"peers":{},"traffic":{},"config":{},"generate_keys":{},"add_peer":{"interface":"str","name":"str","allowed_ips":"str","public_key":"str","preshared_key":"str","endpoint":"str","persistent_keepalive":"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":{}}' ;; call) case "$2" in @@ -798,6 +936,15 @@ case "$1" in ping_peer) ping_peer ;; + interface_control) + interface_control + ;; + peer_descriptions) + get_peer_descriptions + ;; + bandwidth_rates) + get_bandwidth_rates + ;; *) echo '{"error": "Unknown method"}' ;; diff --git a/package/secubox/luci-app-wireguard-dashboard/root/usr/share/luci/menu.d/luci-app-wireguard-dashboard.json b/package/secubox/luci-app-wireguard-dashboard/root/usr/share/luci/menu.d/luci-app-wireguard-dashboard.json index fa47548e..e4cccede 100644 --- a/package/secubox/luci-app-wireguard-dashboard/root/usr/share/luci/menu.d/luci-app-wireguard-dashboard.json +++ b/package/secubox/luci-app-wireguard-dashboard/root/usr/share/luci/menu.d/luci-app-wireguard-dashboard.json @@ -9,6 +9,14 @@ "acl": ["luci-app-wireguard-dashboard"] } }, + "admin/secubox/network/wireguard/wizard": { + "title": "Setup Wizard", + "order": 5, + "action": { + "type": "view", + "path": "wireguard-dashboard/wizard" + } + }, "admin/secubox/network/wireguard/overview": { "title": "Overview", "order": 10, 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 0fb2595d..7128a566 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 @@ -5,10 +5,14 @@ "ubus": { "luci.wireguard-dashboard": [ "status", - "get_interfaces", - "get_peers", + "interfaces", + "peers", "config", - "traffic" + "traffic", + "peer_descriptions", + "bandwidth_rates", + "bandwidth_history", + "endpoint_info" ], "system": [ "info", "board" ], "file": [ "read", "stat", "exec" ] @@ -26,10 +30,12 @@ "add_peer", "remove_peer", "generate_config", - "generate_qr" + "generate_qr", + "interface_control", + "ping_peer" ] }, - "uci": [ "wireguard-dashboard" ] + "uci": [ "wireguard-dashboard", "network" ] } } } diff --git a/package/secubox/secubox-app-webapp/Makefile b/package/secubox/secubox-app-webapp/Makefile index 1fcd70da..58ca8d60 100644 --- a/package/secubox/secubox-app-webapp/Makefile +++ b/package/secubox/secubox-app-webapp/Makefile @@ -1,7 +1,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=secubox-app-webapp -PKG_VERSION:=1.4.1 +PKG_VERSION:=1.5.0 PKG_RELEASE:=1 PKG_LICENSE:=MIT PKG_MAINTAINER:=CyberMind.FR| Utilisateur | +IP Source | +Expiration | +Statut | +
|---|
+ ${escapeHtml(session.ip)}
+
+