secubox-openwrt/package/secubox/luci-app-haproxy/htdocs/luci-static/resources/view/haproxy/certificates.js
CyberMind-FR f3fd676ad1 feat(haproxy): Add HAProxy load balancer packages for OpenWrt
- Add secubox-app-haproxy: LXC-containerized HAProxy service
  - Alpine Linux container with HAProxy
  - Multi-certificate SSL/TLS termination with SNI routing
  - ACME/Let's Encrypt auto-renewal
  - Virtual hosts management
  - Backend health checks and load balancing

- Add luci-app-haproxy: Full LuCI web interface
  - Overview dashboard with service status
  - Virtual hosts management with SSL options
  - Backends and servers configuration
  - SSL certificate management (ACME + import)
  - ACLs and URL-based routing rules
  - Statistics dashboard and logs
  - Settings for ports, timeouts, ACME

- Update luci-app-secubox-portal:
  - Add Services category with HAProxy, HexoJS, PicoBrew,
    Tor Shield, Jellyfin, Home Assistant, AdGuard Home, Nextcloud
  - Make portal dynamic - only shows installed apps
  - Add empty state UI for sections with no apps
  - Remove 404 errors for uninstalled apps

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-23 20:09:32 +01:00

209 lines
6.3 KiB
JavaScript

'use strict';
'require view';
'require dom';
'require ui';
'require haproxy.api as api';
return view.extend({
load: function() {
return api.listCertificates();
},
render: function(certificates) {
var self = this;
certificates = certificates || [];
var view = E('div', { 'class': 'cbi-map' }, [
E('h2', {}, 'SSL Certificates'),
E('p', {}, 'Manage SSL/TLS certificates for your domains. Request free certificates via ACME or import your own.'),
// Request certificate section
E('div', { 'class': 'haproxy-form-section' }, [
E('h3', {}, 'Request Certificate (ACME/Let\'s Encrypt)'),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, 'Domain'),
E('div', { 'class': 'cbi-value-field' }, [
E('input', {
'type': 'text',
'id': 'acme-domain',
'class': 'cbi-input-text',
'placeholder': 'example.com'
}),
E('p', { 'class': 'cbi-value-description' },
'Domain must point to this server. ACME challenge will run on port 80.')
])
]),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, ''),
E('div', { 'class': 'cbi-value-field' }, [
E('button', {
'class': 'cbi-button cbi-button-apply',
'click': function() { self.handleRequestCert(); }
}, 'Request Certificate')
])
])
]),
// Import certificate section
E('div', { 'class': 'haproxy-form-section' }, [
E('h3', {}, 'Import Certificate'),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, 'Domain'),
E('div', { 'class': 'cbi-value-field' }, [
E('input', {
'type': 'text',
'id': 'import-domain',
'class': 'cbi-input-text',
'placeholder': 'example.com'
})
])
]),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, 'Certificate (PEM)'),
E('div', { 'class': 'cbi-value-field' }, [
E('textarea', {
'id': 'import-cert',
'class': 'cbi-input-textarea',
'rows': '6',
'placeholder': '-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----'
})
])
]),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, 'Private Key (PEM)'),
E('div', { 'class': 'cbi-value-field' }, [
E('textarea', {
'id': 'import-key',
'class': 'cbi-input-textarea',
'rows': '6',
'placeholder': '-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----'
})
])
]),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, ''),
E('div', { 'class': 'cbi-value-field' }, [
E('button', {
'class': 'cbi-button cbi-button-add',
'click': function() { self.handleImportCert(); }
}, 'Import Certificate')
])
])
]),
// Certificate list
E('div', { 'class': 'haproxy-form-section' }, [
E('h3', {}, 'Installed Certificates (' + certificates.length + ')'),
E('div', { 'class': 'haproxy-cert-list' },
certificates.length === 0
? E('p', { 'style': 'color: var(--text-color-medium, #666)' }, 'No certificates installed.')
: certificates.map(function(cert) {
return E('div', { 'class': 'haproxy-cert-item', 'data-id': cert.id }, [
E('div', {}, [
E('div', { 'class': 'haproxy-cert-domain' }, cert.domain),
E('div', { 'class': 'haproxy-cert-type' },
'Type: ' + (cert.type === 'acme' ? 'ACME (auto-renew)' : 'Manual'))
]),
E('div', {}, [
E('span', {
'class': 'haproxy-badge ' + (cert.enabled ? 'enabled' : 'disabled'),
'style': 'margin-right: 8px'
}, cert.enabled ? 'Enabled' : 'Disabled'),
E('button', {
'class': 'cbi-button cbi-button-remove',
'click': function() { self.handleDeleteCert(cert); }
}, 'Delete')
])
]);
})
)
])
]);
// Add CSS
var style = E('style', {}, `
@import url('/luci-static/resources/haproxy/dashboard.css');
.cbi-input-textarea {
width: 100%;
font-family: monospace;
}
`);
view.insertBefore(style, view.firstChild);
return view;
},
handleRequestCert: function() {
var domain = document.getElementById('acme-domain').value.trim();
if (!domain) {
ui.addNotification(null, E('p', {}, 'Domain is required'), 'error');
return;
}
ui.showModal('Requesting Certificate', [
E('p', { 'class': 'spinning' }, 'Requesting certificate for ' + domain + '...')
]);
return api.requestCertificate(domain).then(function(res) {
ui.hideModal();
if (res.success) {
ui.addNotification(null, E('p', {}, res.message || 'Certificate requested'));
window.location.reload();
} else {
ui.addNotification(null, E('p', {}, 'Failed: ' + (res.error || 'Unknown error')), 'error');
}
});
},
handleImportCert: function() {
var domain = document.getElementById('import-domain').value.trim();
var cert = document.getElementById('import-cert').value.trim();
var key = document.getElementById('import-key').value.trim();
if (!domain || !cert || !key) {
ui.addNotification(null, E('p', {}, 'Domain, certificate and key are all required'), 'error');
return;
}
return api.importCertificate(domain, cert, key).then(function(res) {
if (res.success) {
ui.addNotification(null, E('p', {}, res.message || 'Certificate imported'));
window.location.reload();
} else {
ui.addNotification(null, E('p', {}, 'Failed: ' + (res.error || 'Unknown error')), 'error');
}
});
},
handleDeleteCert: function(cert) {
ui.showModal('Delete Certificate', [
E('p', {}, 'Are you sure you want to delete the certificate for "' + cert.domain + '"?'),
E('div', { 'class': 'right' }, [
E('button', {
'class': 'cbi-button',
'click': ui.hideModal
}, 'Cancel'),
E('button', {
'class': 'cbi-button cbi-button-negative',
'click': function() {
ui.hideModal();
api.deleteCertificate(cert.id).then(function(res) {
if (res.success) {
ui.addNotification(null, E('p', {}, 'Certificate deleted'));
window.location.reload();
} else {
ui.addNotification(null, E('p', {}, 'Failed: ' + (res.error || 'Unknown error')), 'error');
}
});
}
}, 'Delete')
])
]);
},
handleSaveApply: null,
handleSave: null,
handleReset: null
});