Implements a comprehensive virtual host management system for OpenWrt with
nginx reverse proxy and Let's Encrypt SSL certificate integration.
Features:
- Virtual host management with nginx reverse proxy configuration
- Backend connectivity testing before deployment
- SSL/TLS certificate provisioning via acme.sh and Let's Encrypt
- Certificate expiry monitoring with color-coded warnings
- HTTP Basic Authentication support
- WebSocket protocol support with upgrade headers
- Real-time nginx access log viewer per domain
- Automatic nginx configuration generation and reload
Components:
- RPCD backend (luci.vhost-manager): 11 ubus methods for vhost and cert management
* status, list_vhosts, get_vhost, add_vhost, update_vhost, delete_vhost
* test_backend, request_cert, list_certs, reload_nginx, get_access_logs
- 4 JavaScript views: overview, vhosts, certificates, logs
- ACL with read/write permissions for all ubus methods
- UCI config with global settings and vhost sections
- Comprehensive README with API docs, examples, and troubleshooting
Configuration:
- Nginx vhost configs generated in /etc/nginx/conf.d/vhosts/
- SSL certificates managed via ACME in /etc/acme/{domain}/
- Access logs per domain: /var/log/nginx/{domain}.access.log
- HTTP Basic Auth htpasswd files in /etc/nginx/htpasswd/
Architecture follows SecuBox standards:
- RPCD naming convention (luci. prefix)
- Menu paths match view file structure
- All JavaScript in strict mode
- Backend connectivity validation
- Comprehensive error handling
Dependencies: nginx-ssl, acme, curl
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
72 lines
1.8 KiB
JavaScript
72 lines
1.8 KiB
JavaScript
'use strict';
|
|
'require view';
|
|
'require form';
|
|
'require vhost-manager/api as API';
|
|
|
|
return L.view.extend({
|
|
load: function() {
|
|
return Promise.all([
|
|
API.listVHosts()
|
|
]);
|
|
},
|
|
|
|
render: function(data) {
|
|
var vhosts = data[0] || [];
|
|
|
|
var m = new form.Map('vhost_manager', _('Access Logs'),
|
|
_('View nginx access logs for virtual hosts'));
|
|
|
|
var s = m.section(form.NamedSection, '__logs', 'logs');
|
|
s.anonymous = true;
|
|
s.addremove = false;
|
|
|
|
var o;
|
|
|
|
o = s.option(form.ListValue, 'domain', _('Select Domain'));
|
|
o.rmempty = false;
|
|
|
|
vhosts.forEach(function(vhost) {
|
|
o.value(vhost.domain, vhost.domain);
|
|
});
|
|
|
|
if (vhosts.length === 0) {
|
|
o.value('', _('No virtual hosts configured'));
|
|
}
|
|
|
|
o = s.option(form.ListValue, 'lines', _('Number of Lines'));
|
|
o.value('50', '50');
|
|
o.value('100', '100');
|
|
o.value('200', '200');
|
|
o.value('500', '500');
|
|
o.default = '50';
|
|
|
|
s.render = L.bind(function(view, section_id) {
|
|
var domain = this.section.formvalue(section_id, 'domain');
|
|
var lines = parseInt(this.section.formvalue(section_id, 'lines')) || 50;
|
|
|
|
if (!domain || vhosts.length === 0) {
|
|
return E('div', { 'class': 'cbi-section' }, [
|
|
E('p', { 'style': 'font-style: italic' }, _('No virtual hosts to display logs for'))
|
|
]);
|
|
}
|
|
|
|
return API.getAccessLogs(domain, lines).then(L.bind(function(data) {
|
|
var logs = data.logs || [];
|
|
|
|
return E('div', { 'class': 'cbi-section' }, [
|
|
E('h3', {}, _('Access Logs for: ') + domain),
|
|
E('pre', {
|
|
'style': 'background: #000; color: #0f0; padding: 10px; overflow: auto; max-height: 500px; font-size: 11px; font-family: monospace'
|
|
}, logs.length > 0 ? logs.join('\n') : _('No logs available'))
|
|
]);
|
|
}, this));
|
|
}, this, this);
|
|
|
|
return m.render();
|
|
},
|
|
|
|
handleSaveApply: null,
|
|
handleSave: null,
|
|
handleReset: null
|
|
});
|