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>
145 lines
4.3 KiB
JavaScript
145 lines
4.3 KiB
JavaScript
'use strict';
|
|
'require view';
|
|
'require ui';
|
|
'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', _('Virtual Hosts'),
|
|
_('Manage nginx reverse proxy virtual hosts'));
|
|
|
|
var s = m.section(form.GridSection, 'vhost', _('Virtual Hosts'));
|
|
s.anonymous = false;
|
|
s.addremove = true;
|
|
s.sortable = true;
|
|
|
|
s.modaltitle = function(section_id) {
|
|
return _('Edit VHost: ') + section_id;
|
|
};
|
|
|
|
var o;
|
|
|
|
o = s.option(form.Value, 'domain', _('Domain'));
|
|
o.rmempty = false;
|
|
o.placeholder = 'example.com';
|
|
o.description = _('Domain name for this virtual host');
|
|
|
|
o = s.option(form.Value, 'backend', _('Backend URL'));
|
|
o.rmempty = false;
|
|
o.placeholder = 'http://192.168.1.100:8080';
|
|
o.description = _('Backend server URL to proxy to');
|
|
|
|
// Test backend button
|
|
o.renderWidget = function(section_id, option_index, cfgvalue) {
|
|
var widget = form.Value.prototype.renderWidget.apply(this, [section_id, option_index, cfgvalue]);
|
|
|
|
var testBtn = E('button', {
|
|
'class': 'cbi-button cbi-button-action',
|
|
'style': 'margin-left: 10px',
|
|
'click': function(ev) {
|
|
ev.preventDefault();
|
|
var backend = this.parentNode.querySelector('input').value;
|
|
|
|
if (!backend) {
|
|
ui.addNotification(null, E('p', _('Please enter a backend URL')), 'warning');
|
|
return;
|
|
}
|
|
|
|
ui.addNotification(null, E('p', _('Testing backend connectivity...')), 'info');
|
|
|
|
API.testBackend(backend).then(function(result) {
|
|
if (result.reachable) {
|
|
ui.addNotification(null, E('p', '✓ ' + _('Backend is reachable')), 'info');
|
|
} else {
|
|
ui.addNotification(null, E('p', '✗ ' + _('Backend is unreachable')), 'error');
|
|
}
|
|
});
|
|
}
|
|
}, _('Test'));
|
|
|
|
widget.appendChild(testBtn);
|
|
return widget;
|
|
};
|
|
|
|
o = s.option(form.Flag, 'ssl', _('Enable SSL'));
|
|
o.default = o.disabled;
|
|
o.description = _('Enable HTTPS (requires valid SSL certificate)');
|
|
|
|
o = s.option(form.Flag, 'auth', _('Enable Authentication'));
|
|
o.default = o.disabled;
|
|
o.description = _('Require HTTP basic authentication');
|
|
|
|
o = s.option(form.Value, 'auth_user', _('Auth Username'));
|
|
o.depends('auth', '1');
|
|
o.placeholder = 'admin';
|
|
|
|
o = s.option(form.Value, 'auth_pass', _('Auth Password'));
|
|
o.depends('auth', '1');
|
|
o.password = true;
|
|
|
|
o = s.option(form.Flag, 'websocket', _('WebSocket Support'));
|
|
o.default = o.disabled;
|
|
o.description = _('Enable WebSocket protocol upgrade headers');
|
|
|
|
// Custom actions
|
|
s.addModalOptions = function(s, section_id, ev) {
|
|
// Get form values
|
|
var domain = this.section.formvalue(section_id, 'domain');
|
|
var backend = this.section.formvalue(section_id, 'backend');
|
|
var ssl = this.section.formvalue(section_id, 'ssl') === '1';
|
|
var auth = this.section.formvalue(section_id, 'auth') === '1';
|
|
var websocket = this.section.formvalue(section_id, 'websocket') === '1';
|
|
|
|
if (!domain || !backend) {
|
|
ui.addNotification(null, E('p', _('Domain and backend are required')), 'error');
|
|
return;
|
|
}
|
|
|
|
// Call API to add vhost
|
|
API.addVHost(domain, backend, ssl, auth, websocket).then(function(result) {
|
|
if (result.success) {
|
|
ui.addNotification(null, E('p', _('VHost created successfully')), 'info');
|
|
|
|
if (result.reload_required) {
|
|
ui.showModal(_('Reload Nginx?'), [
|
|
E('p', {}, _('Configuration changed. Reload nginx to apply?')),
|
|
E('div', { 'class': 'right' }, [
|
|
E('button', {
|
|
'class': 'cbi-button cbi-button-neutral',
|
|
'click': ui.hideModal
|
|
}, _('Later')),
|
|
E('button', {
|
|
'class': 'cbi-button cbi-button-positive',
|
|
'click': function() {
|
|
API.reloadNginx().then(function(reload_result) {
|
|
ui.hideModal();
|
|
if (reload_result.success) {
|
|
ui.addNotification(null, E('p', '✓ ' + _('Nginx reloaded')), 'info');
|
|
} else {
|
|
ui.addNotification(null, E('p', '✗ ' + reload_result.message), 'error');
|
|
}
|
|
});
|
|
}
|
|
}, _('Reload Now'))
|
|
])
|
|
]);
|
|
}
|
|
} else {
|
|
ui.addNotification(null, E('p', '✗ ' + result.message), 'error');
|
|
}
|
|
});
|
|
};
|
|
|
|
return m.render();
|
|
}
|
|
});
|