- 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>
209 lines
6.3 KiB
JavaScript
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
|
|
});
|