secubox-openwrt/package/secubox/luci-app-haproxy/htdocs/luci-static/resources/view/haproxy/acls.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

348 lines
11 KiB
JavaScript

'use strict';
'require view';
'require dom';
'require ui';
'require haproxy.api as api';
return view.extend({
load: function() {
return Promise.all([
api.listAcls(),
api.listRedirects(),
api.listBackends()
]);
},
render: function(data) {
var self = this;
var acls = data[0] || [];
var redirects = data[1] || [];
var backends = data[2] || [];
var view = E('div', { 'class': 'cbi-map' }, [
E('h2', {}, 'ACLs & Routing'),
E('p', {}, 'Configure URL-based routing rules and redirections.'),
// ACL Rules section
E('div', { 'class': 'haproxy-form-section' }, [
E('h3', {}, 'Add ACL Rule'),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, 'Name'),
E('div', { 'class': 'cbi-value-field' }, [
E('input', {
'type': 'text',
'id': 'acl-name',
'class': 'cbi-input-text',
'placeholder': 'is_api'
})
])
]),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, 'Match Type'),
E('div', { 'class': 'cbi-value-field' }, [
E('select', { 'id': 'acl-type', 'class': 'cbi-input-select' }, [
E('option', { 'value': 'path_beg' }, 'Path begins with'),
E('option', { 'value': 'path_end' }, 'Path ends with'),
E('option', { 'value': 'path_reg' }, 'Path regex'),
E('option', { 'value': 'hdr(host)' }, 'Host header'),
E('option', { 'value': 'hdr_beg(host)' }, 'Host begins with'),
E('option', { 'value': 'src' }, 'Source IP'),
E('option', { 'value': 'url_param' }, 'URL parameter')
])
])
]),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, 'Pattern'),
E('div', { 'class': 'cbi-value-field' }, [
E('input', {
'type': 'text',
'id': 'acl-pattern',
'class': 'cbi-input-text',
'placeholder': '/api/'
})
])
]),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, 'Route to Backend'),
E('div', { 'class': 'cbi-value-field' }, [
E('select', { 'id': 'acl-backend', 'class': 'cbi-input-select' },
[E('option', { 'value': '' }, '-- No routing (ACL only) --')].concat(
backends.map(function(b) {
return E('option', { 'value': b.id }, b.name);
})
)
)
])
]),
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.handleAddAcl(); }
}, 'Add ACL Rule')
])
])
]),
// ACL list
E('div', { 'class': 'haproxy-form-section' }, [
E('h3', {}, 'ACL Rules (' + acls.length + ')'),
this.renderAclsTable(acls, backends)
]),
// Redirects section
E('div', { 'class': 'haproxy-form-section' }, [
E('h3', {}, 'Add Redirect Rule'),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, 'Name'),
E('div', { 'class': 'cbi-value-field' }, [
E('input', {
'type': 'text',
'id': 'redirect-name',
'class': 'cbi-input-text',
'placeholder': 'www-redirect'
})
])
]),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, 'Match Host'),
E('div', { 'class': 'cbi-value-field' }, [
E('input', {
'type': 'text',
'id': 'redirect-match',
'class': 'cbi-input-text',
'placeholder': '^www\\.'
}),
E('p', { 'class': 'cbi-value-description' }, 'Regex pattern to match against host header')
])
]),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, 'Target Host'),
E('div', { 'class': 'cbi-value-field' }, [
E('input', {
'type': 'text',
'id': 'redirect-target',
'class': 'cbi-input-text',
'placeholder': 'Leave empty to strip matched portion'
})
])
]),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, 'Options'),
E('div', { 'class': 'cbi-value-field' }, [
E('label', { 'style': 'margin-right: 1rem' }, [
E('input', { 'type': 'checkbox', 'id': 'redirect-strip-www' }),
' Strip www prefix'
]),
E('select', { 'id': 'redirect-code', 'class': 'cbi-input-select', 'style': 'width: auto' }, [
E('option', { 'value': '301' }, '301 Permanent'),
E('option', { 'value': '302' }, '302 Temporary'),
E('option', { 'value': '303' }, '303 See Other'),
E('option', { 'value': '307' }, '307 Temporary Redirect'),
E('option', { 'value': '308' }, '308 Permanent Redirect')
])
])
]),
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.handleAddRedirect(); }
}, 'Add Redirect')
])
])
]),
// Redirect list
E('div', { 'class': 'haproxy-form-section' }, [
E('h3', {}, 'Redirect Rules (' + redirects.length + ')'),
this.renderRedirectsTable(redirects)
])
]);
// Add CSS
var style = E('style', {}, `
@import url('/luci-static/resources/haproxy/dashboard.css');
`);
view.insertBefore(style, view.firstChild);
return view;
},
renderAclsTable: function(acls, backends) {
var self = this;
if (acls.length === 0) {
return E('p', { 'style': 'color: var(--text-color-medium, #666)' },
'No ACL rules configured.');
}
var backendMap = {};
backends.forEach(function(b) { backendMap[b.id] = b.name; });
return E('table', { 'class': 'haproxy-vhosts-table' }, [
E('thead', {}, [
E('tr', {}, [
E('th', {}, 'Name'),
E('th', {}, 'Type'),
E('th', {}, 'Pattern'),
E('th', {}, 'Backend'),
E('th', {}, 'Status'),
E('th', { 'style': 'width: 100px' }, 'Actions')
])
]),
E('tbody', {}, acls.map(function(acl) {
return E('tr', { 'data-id': acl.id }, [
E('td', {}, E('strong', {}, acl.name)),
E('td', {}, E('code', {}, acl.type)),
E('td', {}, E('code', {}, acl.pattern)),
E('td', {}, backendMap[acl.backend] || acl.backend || '-'),
E('td', {}, E('span', {
'class': 'haproxy-badge ' + (acl.enabled ? 'enabled' : 'disabled')
}, acl.enabled ? 'Enabled' : 'Disabled')),
E('td', {}, [
E('button', {
'class': 'cbi-button cbi-button-remove',
'click': function() { self.handleDeleteAcl(acl); }
}, 'Delete')
])
]);
}))
]);
},
renderRedirectsTable: function(redirects) {
var self = this;
if (redirects.length === 0) {
return E('p', { 'style': 'color: var(--text-color-medium, #666)' },
'No redirect rules configured.');
}
return E('table', { 'class': 'haproxy-vhosts-table' }, [
E('thead', {}, [
E('tr', {}, [
E('th', {}, 'Name'),
E('th', {}, 'Match Host'),
E('th', {}, 'Target'),
E('th', {}, 'Code'),
E('th', {}, 'Status'),
E('th', { 'style': 'width: 100px' }, 'Actions')
])
]),
E('tbody', {}, redirects.map(function(r) {
return E('tr', { 'data-id': r.id }, [
E('td', {}, E('strong', {}, r.name)),
E('td', {}, E('code', {}, r.match_host)),
E('td', {}, r.strip_www ? 'Strip www' : (r.target_host || '-')),
E('td', {}, r.code),
E('td', {}, E('span', {
'class': 'haproxy-badge ' + (r.enabled ? 'enabled' : 'disabled')
}, r.enabled ? 'Enabled' : 'Disabled')),
E('td', {}, [
E('button', {
'class': 'cbi-button cbi-button-remove',
'click': function() { self.handleDeleteRedirect(r); }
}, 'Delete')
])
]);
}))
]);
},
handleAddAcl: function() {
var name = document.getElementById('acl-name').value.trim();
var type = document.getElementById('acl-type').value;
var pattern = document.getElementById('acl-pattern').value.trim();
var backend = document.getElementById('acl-backend').value;
if (!name || !type || !pattern) {
ui.addNotification(null, E('p', {}, 'Name, type and pattern are required'), 'error');
return;
}
return api.createAcl(name, type, pattern, backend, 1).then(function(res) {
if (res.success) {
ui.addNotification(null, E('p', {}, 'ACL rule created'));
window.location.reload();
} else {
ui.addNotification(null, E('p', {}, 'Failed: ' + (res.error || 'Unknown error')), 'error');
}
});
},
handleDeleteAcl: function(acl) {
ui.showModal('Delete ACL', [
E('p', {}, 'Are you sure you want to delete ACL rule "' + acl.name + '"?'),
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.deleteAcl(acl.id).then(function(res) {
if (res.success) {
ui.addNotification(null, E('p', {}, 'ACL deleted'));
window.location.reload();
} else {
ui.addNotification(null, E('p', {}, 'Failed: ' + (res.error || 'Unknown error')), 'error');
}
});
}
}, 'Delete')
])
]);
},
handleAddRedirect: function() {
var name = document.getElementById('redirect-name').value.trim();
var matchHost = document.getElementById('redirect-match').value.trim();
var targetHost = document.getElementById('redirect-target').value.trim();
var stripWww = document.getElementById('redirect-strip-www').checked ? 1 : 0;
var code = parseInt(document.getElementById('redirect-code').value) || 301;
if (!name || !matchHost) {
ui.addNotification(null, E('p', {}, 'Name and match host pattern are required'), 'error');
return;
}
return api.createRedirect(name, matchHost, targetHost, stripWww, code, 1).then(function(res) {
if (res.success) {
ui.addNotification(null, E('p', {}, 'Redirect rule created'));
window.location.reload();
} else {
ui.addNotification(null, E('p', {}, 'Failed: ' + (res.error || 'Unknown error')), 'error');
}
});
},
handleDeleteRedirect: function(r) {
ui.showModal('Delete Redirect', [
E('p', {}, 'Are you sure you want to delete redirect rule "' + r.name + '"?'),
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.deleteRedirect(r.id).then(function(res) {
if (res.success) {
ui.addNotification(null, E('p', {}, 'Redirect deleted'));
window.location.reload();
} else {
ui.addNotification(null, E('p', {}, 'Failed: ' + (res.error || 'Unknown error')), 'error');
}
});
}
}, 'Delete')
])
]);
},
handleSaveApply: null,
handleSave: null,
handleReset: null
});