secubox-openwrt/package/secubox/luci-app-wireguard-dashboard/htdocs/luci-static/resources/view/wireguard-dashboard/qrcodes.js
CyberMind-FR db3a41928e fix(luci): Fix require syntax in all LuCI views - use slashes instead of dots
All 'require module.submodule' directives changed to 'require module/submodule'
to match LuCI's module loading convention.

Affected packages:
- luci-app-auth-guardian
- luci-app-glances
- luci-app-localai
- luci-app-magicmirror2
- luci-app-mitmproxy
- luci-app-mmpm
- luci-app-mqtt-bridge
- luci-app-ndpid
- luci-app-network-modes
- luci-app-secubox-admin
- luci-app-secubox-portal
- luci-app-wireguard-dashboard

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-21 17:15:21 +01:00

433 lines
12 KiB
JavaScript

'use strict';
'require view';
'require secubox-theme/theme as Theme';
'require dom';
'require ui';
'require wireguard-dashboard/api as api';
'require wireguard-dashboard/qrcode as qrcode';
return view.extend({
title: _('QR Code Generator'),
load: function() {
return Promise.all([
api.getConfig(),
api.getInterfaces(),
api.getPeers()
]).then(function(results) {
return {
config: results[0] || {},
interfaces: (results[1] || {}).interfaces || [],
peers: (results[2] || {}).peers || []
};
});
},
getStoredPrivateKey: function(publicKey) {
try {
var stored = sessionStorage.getItem('wg_peer_keys');
if (stored) {
var keys = JSON.parse(stored);
return keys[publicKey] || null;
}
} catch (e) {}
return null;
},
generateQRForPeer: function(iface, peer, serverEndpoint) {
var self = this;
var privateKey = this.getStoredPrivateKey(peer.public_key);
if (!privateKey) {
ui.showModal(_('Private Key Required'), [
E('p', {}, _('To generate a QR code, you need the peer\'s private key.')),
E('p', {}, _('Private keys are only available immediately after peer creation for security reasons.')),
E('div', { 'class': 'wg-form-group' }, [
E('label', {}, _('Enter Private Key:')),
E('input', {
'type': 'text',
'id': 'wg-private-key-input',
'class': 'cbi-input-text',
'placeholder': 'Base64 private key...',
'style': 'width: 100%; font-family: monospace;'
})
]),
E('div', { 'class': 'right' }, [
E('button', {
'class': 'btn',
'click': ui.hideModal
}, _('Cancel')),
' ',
E('button', {
'class': 'btn cbi-button-action',
'click': function() {
var input = document.getElementById('wg-private-key-input');
var key = input ? input.value.trim() : '';
if (key && key.length === 44) {
ui.hideModal();
self.showQRCode(iface, peer, key, serverEndpoint);
} else {
ui.addNotification(null, E('p', {}, _('Please enter a valid private key (44 characters, base64)')), 'error');
}
}
}, _('Generate QR'))
])
]);
return;
}
this.showQRCode(iface, peer, privateKey, serverEndpoint);
},
showQRCode: function(iface, peer, privateKey, serverEndpoint) {
var self = this;
// First try backend (uses qrencode if available)
api.generateQR(iface.name, peer.public_key, privateKey, serverEndpoint).then(function(result) {
if (result && result.qrcode && !result.error) {
// Backend generated QR successfully
self.displayQRModal(iface, peer, result.qrcode, result.config);
} else {
// Fall back to JavaScript QR generation
self.generateJSQR(iface, peer, privateKey, serverEndpoint);
}
}).catch(function(err) {
// Fall back to JavaScript QR generation
self.generateJSQR(iface, peer, privateKey, serverEndpoint);
});
},
generateJSQR: function(iface, peer, privateKey, serverEndpoint) {
// Build WireGuard config
var config = '[Interface]\n' +
'PrivateKey = ' + privateKey + '\n' +
'Address = ' + (peer.allowed_ips || '10.0.0.2/32') + '\n' +
'DNS = 1.1.1.1, 1.0.0.1\n\n' +
'[Peer]\n' +
'PublicKey = ' + iface.public_key + '\n' +
'Endpoint = ' + serverEndpoint + ':' + (iface.listen_port || 51820) + '\n' +
'AllowedIPs = 0.0.0.0/0, ::/0\n' +
'PersistentKeepalive = 25';
var svg = qrcode.generateSVG(config, 250);
if (svg) {
this.displayQRModal(iface, peer, svg, config, true);
} else {
ui.addNotification(null, E('p', {}, _('Failed to generate QR code. Config may be too long.')), 'error');
}
},
displayQRModal: function(iface, peer, qrData, config, isSVG) {
var qrElement;
if (isSVG) {
qrElement = E('div', { 'class': 'wg-qr-image' });
qrElement.innerHTML = qrData;
} else {
qrElement = E('img', {
'src': qrData,
'alt': 'WireGuard QR Code',
'class': 'wg-qr-image'
});
}
ui.showModal(_('WireGuard Configuration'), [
E('div', { 'class': 'wg-qr-modal' }, [
E('div', { 'class': 'wg-qr-header' }, [
E('h4', {}, iface.name + ' - Peer ' + (peer.short_key || peer.public_key.substring(0, 8)))
]),
E('div', { 'class': 'wg-qr-container' }, [qrElement]),
E('p', { 'class': 'wg-qr-hint' }, _('Scan with WireGuard app on your mobile device')),
E('div', { 'class': 'wg-qr-actions' }, [
E('button', {
'class': 'btn',
'click': function() {
navigator.clipboard.writeText(config).then(function() {
ui.addNotification(null, E('p', {}, _('Configuration copied to clipboard')), 'info');
});
}
}, _('Copy Config')),
E('button', {
'class': 'btn cbi-button-action',
'click': function() {
var blob = new Blob([config], { type: 'text/plain' });
var url = URL.createObjectURL(blob);
var a = document.createElement('a');
a.href = url;
a.download = iface.name + '-peer.conf';
a.click();
URL.revokeObjectURL(url);
}
}, _('Download .conf'))
]),
E('details', { 'class': 'wg-config-details' }, [
E('summary', {}, _('Show configuration')),
E('pre', {}, config)
])
]),
E('div', { 'class': 'right' }, [
E('button', {
'class': 'btn',
'click': ui.hideModal
}, _('Close'))
])
]);
},
render: function(data) {
var self = this;
var interfaces = data.interfaces || [];
var configData = (data.config || {}).interfaces || [];
var peers = data.peers || [];
// Merge interface data with config data
interfaces = interfaces.map(function(iface) {
var cfg = configData.find(function(c) { return c.name === iface.name; }) || {};
return Object.assign({}, iface, {
peers: cfg.peers || [],
public_key: cfg.public_key || iface.public_key
});
});
var view = E('div', { 'class': 'wireguard-dashboard' }, [
E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }),
// Header
E('div', { 'class': 'wg-header' }, [
E('div', { 'class': 'wg-logo' }, [
E('div', { 'class': 'wg-logo-icon' }, '📱'),
E('div', { 'class': 'wg-logo-text' }, ['QR ', E('span', {}, 'Generator')])
])
]),
// Server endpoint input
E('div', { 'class': 'wg-card' }, [
E('div', { 'class': 'wg-card-header' }, [
E('div', { 'class': 'wg-card-title' }, [
E('span', { 'class': 'wg-card-title-icon' }, '🌐'),
_('Server Endpoint')
])
]),
E('div', { 'class': 'wg-card-body' }, [
E('p', { 'style': 'margin-bottom: 12px; color: var(--wg-text-secondary);' },
_('Enter the public IP or hostname of this WireGuard server:')),
E('div', { 'class': 'wg-form-row' }, [
E('input', {
'type': 'text',
'id': 'wg-server-endpoint',
'class': 'cbi-input-text',
'placeholder': 'e.g., vpn.example.com or 203.0.113.1',
'style': 'flex: 1;'
}),
E('button', {
'class': 'wg-btn wg-btn-primary',
'click': function() {
var input = document.getElementById('wg-server-endpoint');
if (input && input.value.trim()) {
sessionStorage.setItem('wg_server_endpoint', input.value.trim());
ui.addNotification(null, E('p', {}, _('Server endpoint saved')), 'info');
}
}
}, _('Save'))
])
])
]),
// Interface cards
interfaces.length > 0 ?
E('div', { 'class': 'wg-interface-list' },
interfaces.map(function(iface) {
var ifacePeers = peers.filter(function(p) { return p.interface === iface.name; });
return E('div', { 'class': 'wg-card' }, [
E('div', { 'class': 'wg-card-header' }, [
E('div', { 'class': 'wg-card-title' }, [
E('span', { 'class': 'wg-card-title-icon' }, '🔐'),
iface.name
]),
E('div', { 'class': 'wg-card-badge' }, ifacePeers.length + ' peers')
]),
E('div', { 'class': 'wg-card-body' }, [
E('div', { 'class': 'wg-interface-info' }, [
E('div', { 'class': 'wg-info-item' }, [
E('span', { 'class': 'wg-info-label' }, _('Public Key:')),
E('code', {}, (iface.public_key || 'N/A').substring(0, 20) + '...')
]),
E('div', { 'class': 'wg-info-item' }, [
E('span', { 'class': 'wg-info-label' }, _('Listen Port:')),
E('span', {}, iface.listen_port || 51820)
])
]),
ifacePeers.length > 0 ?
E('div', { 'class': 'wg-peer-list' },
ifacePeers.map(function(peer) {
return E('div', { 'class': 'wg-peer-item' }, [
E('div', { 'class': 'wg-peer-info' }, [
E('span', { 'class': 'wg-peer-icon' }, '👤'),
E('div', {}, [
E('strong', {}, peer.short_key || peer.public_key.substring(0, 8)),
E('div', { 'class': 'wg-peer-ips' }, peer.allowed_ips || 'No IPs')
])
]),
E('button', {
'class': 'wg-btn wg-btn-primary',
'click': function() {
var endpoint = sessionStorage.getItem('wg_server_endpoint');
if (!endpoint) {
var input = document.getElementById('wg-server-endpoint');
endpoint = input ? input.value.trim() : '';
}
if (!endpoint) {
ui.addNotification(null, E('p', {}, _('Please enter the server endpoint first')), 'warning');
return;
}
self.generateQRForPeer(iface, peer, endpoint);
}
}, '📱 ' + _('QR Code'))
]);
})
) :
E('div', { 'class': 'wg-empty-peers' }, _('No peers configured for this interface'))
])
]);
})
) :
E('div', { 'class': 'wg-empty' }, [
E('div', { 'class': 'wg-empty-icon' }, '📱'),
E('div', { 'class': 'wg-empty-text' }, _('No WireGuard interfaces configured')),
E('p', {}, _('Create a WireGuard interface to generate QR codes'))
])
]);
// Restore saved endpoint
setTimeout(function() {
var saved = sessionStorage.getItem('wg_server_endpoint');
if (saved) {
var input = document.getElementById('wg-server-endpoint');
if (input) input.value = saved;
}
}, 100);
// Add CSS
var css = `
.wg-form-row {
display: flex;
gap: 10px;
align-items: center;
}
.wg-interface-info {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 12px;
padding: 12px;
background: var(--wg-bg-tertiary);
border-radius: 8px;
margin-bottom: 16px;
}
.wg-info-item {
display: flex;
flex-direction: column;
gap: 4px;
}
.wg-info-label {
font-size: 12px;
color: var(--wg-text-muted);
}
.wg-peer-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.wg-peer-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px;
background: var(--wg-bg-tertiary);
border: 1px solid var(--wg-border);
border-radius: 8px;
}
.wg-peer-info {
display: flex;
align-items: center;
gap: 12px;
}
.wg-peer-icon {
font-size: 24px;
}
.wg-peer-ips {
font-size: 12px;
color: var(--wg-text-muted);
font-family: monospace;
}
.wg-empty-peers {
text-align: center;
padding: 20px;
color: var(--wg-text-muted);
}
.wg-qr-modal {
text-align: center;
}
.wg-qr-container {
background: white;
padding: 20px;
border-radius: 12px;
display: inline-block;
margin: 20px 0;
}
.wg-qr-image {
max-width: 250px;
max-height: 250px;
}
.wg-qr-hint {
color: var(--wg-text-secondary);
font-size: 14px;
}
.wg-qr-actions {
display: flex;
justify-content: center;
gap: 10px;
margin: 16px 0;
}
.wg-config-details {
text-align: left;
margin-top: 16px;
}
.wg-config-details summary {
cursor: pointer;
color: var(--wg-accent-cyan);
margin-bottom: 8px;
}
.wg-config-details pre {
background: var(--wg-bg-tertiary);
padding: 12px;
border-radius: 8px;
font-size: 11px;
overflow-x: auto;
white-space: pre-wrap;
}
.wg-form-group {
margin: 16px 0;
text-align: left;
}
.wg-form-group label {
display: block;
margin-bottom: 8px;
font-weight: 500;
}
`;
var style = E('style', {}, css);
document.head.appendChild(style);
var cssLink = E('link', { 'rel': 'stylesheet', 'href': L.resource('wireguard-dashboard/dashboard.css') });
document.head.appendChild(cssLink);
return view;
},
handleSaveApply: null,
handleSave: null,
handleReset: null
});