secubox-openwrt/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/bouncers.js
CyberMind-FR 4e5d5275f9 refactor: Merge secubox-app-crowdsec-bouncer into cs-firewall-bouncer
- Move UCI defaults script for auto-registration to cs-firewall-bouncer
- Remove redundant secubox-app-crowdsec-bouncer wrapper package
- Update luci-app-crowdsec-dashboard reference to new package name
- Increment PKG_RELEASE to 3

The defaults script handles:
- Automatic bouncer registration with CrowdSec LAPI
- Interface detection for LAN/WAN
- API key generation and UCI config update

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-17 06:46:09 +01:00

786 lines
29 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use strict';
'require view';
'require secubox-theme/theme as Theme';
'require dom';
'require poll';
'require ui';
'require crowdsec-dashboard/api as API';
'require crowdsec-dashboard/nav as CsNav';
return view.extend({
load: function() {
var cssLink = document.createElement('link');
cssLink.rel = 'stylesheet';
cssLink.href = L.resource('crowdsec-dashboard/dashboard.css');
document.head.appendChild(cssLink);
return Promise.all([
API.getBouncers(),
API.getStatus(),
API.getFirewallBouncerStatus(),
API.getNftablesStats()
]);
},
render: function(data) {
var bouncers = data[0] || [];
var status = data[1] || {};
var fwStatus = data[2] || {};
var nftStats = data[3] || {};
var view = E('div', { 'class': 'crowdsec-dashboard' }, [
CsNav.renderTabs('bouncers'),
// Status Card
E('div', { 'class': 'cbi-section', 'style': 'background: ' + (status.crowdsec === 'running' ? '#d4edda' : '#f8d7da') + '; border-left: 4px solid ' + (status.crowdsec === 'running' ? '#28a745' : '#dc3545') + '; padding: 1em; margin-bottom: 1.5em;' }, [
E('div', { 'style': 'display: flex; justify-content: space-between; align-items: center;' }, [
E('div', {}, [
E('strong', {}, _('CrowdSec Status:')),
' ',
E('span', { 'class': 'badge', 'style': 'background: ' + (status.crowdsec === 'running' ? '#28a745' : '#dc3545') + '; color: white; padding: 0.25em 0.6em; border-radius: 3px; margin-left: 0.5em;' },
status.crowdsec === 'running' ? _('RUNNING') : _('STOPPED'))
]),
E('div', {}, [
E('strong', {}, _('Active Bouncers:')),
' ',
E('span', { 'style': 'font-size: 1.3em; color: #0088cc; font-weight: bold; margin-left: 0.5em;' },
bouncers.length.toString())
])
])
]),
// Firewall Bouncer Management Card
this.renderFirewallBouncerCard(fwStatus, nftStats),
// Bouncers Table
E('div', { 'class': 'cbi-section' }, [
E('div', { 'style': 'display: flex; justify-content: space-between; align-items: center; margin-bottom: 1em;' }, [
E('h3', { 'style': 'margin: 0;' }, _('Registered Bouncers')),
E('div', { 'style': 'display: flex; gap: 0.5em;' }, [
E('button', {
'class': 'cbi-button cbi-button-positive',
'click': L.bind(this.openRegisterWizard, this)
}, _(' Register Bouncer')),
E('button', {
'class': 'cbi-button cbi-button-action',
'click': L.bind(this.handleRefresh, this)
}, _('Refresh'))
])
]),
E('div', { 'class': 'table-wrapper' }, [
E('table', { 'class': 'table', 'id': 'bouncers-table' }, [
E('thead', {}, [
E('tr', {}, [
E('th', {}, _('Name')),
E('th', {}, _('IP Address')),
E('th', {}, _('Type')),
E('th', {}, _('Version')),
E('th', {}, _('Last Pull')),
E('th', {}, _('Status')),
E('th', {}, _('Authentication')),
E('th', {}, _('Actions'))
])
]),
E('tbody', { 'id': 'bouncers-tbody' },
this.renderBouncerRows(bouncers)
)
])
])
]),
// Help Section
E('div', { 'class': 'cbi-section', 'style': 'background: #e8f4f8; padding: 1em; margin-top: 2em;' }, [
E('h3', {}, _('About Bouncers')),
E('p', {}, _('Bouncers are remediation components that connect to the CrowdSec Local API to fetch decisions and apply them on your infrastructure.')),
E('div', { 'style': 'margin-top: 1em;' }, [
E('strong', {}, _('Common Bouncer Types:')),
E('ul', { 'style': 'margin-top: 0.5em;' }, [
E('li', {}, [
E('strong', {}, 'cs-firewall-bouncer:'),
' ',
_('Manages iptables/nftables rules to block IPs at the firewall level')
]),
E('li', {}, [
E('strong', {}, 'cs-nginx-bouncer:'),
' ',
_('Blocks IPs directly in Nginx web server')
]),
E('li', {}, [
E('strong', {}, 'cs-haproxy-bouncer:'),
' ',
_('Integrates with HAProxy load balancer')
]),
E('li', {}, [
E('strong', {}, 'cs-cloudflare-bouncer:'),
' ',
_('Pushes decisions to Cloudflare firewall')
])
])
]),
E('p', { 'style': 'margin-top: 1em; padding: 0.75em; background: #fff3cd; border-radius: 4px;' }, [
E('strong', {}, _('Note:')),
' ',
_('To register a new bouncer, use the command: '),
E('code', {}, 'cscli bouncers add <bouncer-name>')
])
])
]);
// Setup auto-refresh
poll.add(L.bind(function() {
return Promise.all([
API.getBouncers(),
API.getFirewallBouncerStatus(),
API.getNftablesStats()
]).then(L.bind(function(refreshData) {
// Update bouncer table
var tbody = document.getElementById('bouncers-tbody');
if (tbody) {
dom.content(tbody, this.renderBouncerRows(refreshData[0] || []));
}
// Update firewall bouncer status
var fwStatus = refreshData[1] || {};
var nftStats = refreshData[2] || {};
var statusBadge = document.getElementById('fw-bouncer-status');
if (statusBadge) {
var running = fwStatus.running || false;
statusBadge.textContent = running ? _('ACTIVE') : _('STOPPED');
statusBadge.style.background = running ? '#28a745' : '#dc3545';
}
var enabledBadge = document.getElementById('fw-bouncer-enabled');
if (enabledBadge) {
var enabled = fwStatus.enabled || false;
enabledBadge.textContent = enabled ? _('ENABLED') : _('DISABLED');
enabledBadge.style.background = enabled ? '#17a2b8' : '#6c757d';
}
var ipv4Count = document.getElementById('fw-bouncer-ipv4-count');
if (ipv4Count) {
ipv4Count.textContent = (fwStatus.blocked_ipv4 || 0).toString();
}
var ipv6Count = document.getElementById('fw-bouncer-ipv6-count');
if (ipv6Count) {
ipv6Count.textContent = (fwStatus.blocked_ipv6 || 0).toString();
}
}, this));
}, this), 10);
return view;
},
renderBouncerRows: function(bouncers) {
if (!bouncers || bouncers.length === 0) {
return E('tr', {}, [
E('td', { 'colspan': 8, 'style': 'text-align: center; padding: 2em; color: #999;' },
_('No bouncers registered. Click "Register Bouncer" to add one.'))
]);
}
return bouncers.map(L.bind(function(bouncer) {
var lastPull = bouncer.last_pull || bouncer.lastPull || 'Never';
var isRecent = this.isRecentPull(lastPull);
var bouncerName = bouncer.name || 'Unknown';
return E('tr', {
'style': isRecent ? '' : 'opacity: 0.6;'
}, [
E('td', {}, [
E('strong', {}, bouncerName)
]),
E('td', {}, [
E('code', { 'style': 'font-size: 0.9em;' }, bouncer.ip_address || bouncer.ipAddress || 'N/A')
]),
E('td', {}, bouncer.type || 'Unknown'),
E('td', {}, bouncer.version || 'N/A'),
E('td', {}, this.formatLastPull(lastPull)),
E('td', {}, [
E('span', {
'class': 'badge',
'style': 'background: ' + (isRecent ? '#28a745' : '#6c757d') + '; color: white; padding: 0.25em 0.6em; border-radius: 3px;'
}, isRecent ? _('Active') : _('Inactive'))
]),
E('td', {}, [
E('span', {
'class': 'badge',
'style': 'background: ' + (bouncer.revoked ? '#dc3545' : '#28a745') + '; color: white; padding: 0.25em 0.6em; border-radius: 3px;'
}, bouncer.revoked ? _('Revoked') : _('Valid'))
]),
E('td', {}, [
E('button', {
'class': 'cbi-button cbi-button-remove',
'click': L.bind(this.handleDeleteBouncer, this, bouncerName)
}, _('Delete'))
])
]);
}, this));
},
formatLastPull: function(lastPull) {
if (!lastPull || lastPull === 'Never' || lastPull === 'never') {
return E('span', { 'style': 'color: #999;' }, _('Never'));
}
try {
var pullDate = new Date(lastPull);
var now = new Date();
var diffMinutes = Math.floor((now - pullDate) / 60000);
if (diffMinutes < 1) return _('Just now');
if (diffMinutes < 60) return diffMinutes + 'm ago';
if (diffMinutes < 1440) return Math.floor(diffMinutes / 60) + 'h ago';
return Math.floor(diffMinutes / 1440) + 'd ago';
} catch(e) {
return lastPull;
}
},
isRecentPull: function(lastPull) {
if (!lastPull || lastPull === 'Never' || lastPull === 'never') {
return false;
}
try {
var pullDate = new Date(lastPull);
var now = new Date();
var diffMinutes = Math.floor((now - pullDate) / 60000);
// Consider active if pulled within last 5 minutes
return diffMinutes < 5;
} catch(e) {
return false;
}
},
handleRefresh: function() {
poll.start();
return Promise.all([
API.getBouncers(),
API.getStatus()
]).then(L.bind(function(data) {
var tbody = document.getElementById('bouncers-tbody');
if (tbody) {
var bouncers = data[0] || [];
dom.content(tbody, this.renderBouncerRows(bouncers));
}
ui.addNotification(null, E('p', _('Bouncer list refreshed')), 'info');
}, this)).catch(function(err) {
ui.addNotification(null, E('p', _('Failed to refresh: %s').format(err.message || err)), 'error');
});
},
openRegisterWizard: function() {
var self = this;
var nameInput;
ui.showModal(_('Register New Bouncer'), [
E('div', { 'class': 'cbi-section' }, [
E('div', { 'class': 'cbi-section-descr' },
_('Register a new bouncer to enforce CrowdSec decisions. The bouncer will receive an API key to connect to the Local API.')),
E('div', { 'class': 'cbi-value', 'style': 'margin-top: 1em;' }, [
E('label', { 'class': 'cbi-value-title', 'for': 'bouncer-name-input' },
_('Bouncer Name')),
E('div', { 'class': 'cbi-value-field' }, [
nameInput = E('input', {
'type': 'text',
'id': 'bouncer-name-input',
'class': 'cbi-input-text',
'placeholder': _('e.g., firewall-bouncer-1'),
'style': 'width: 100%;'
}),
E('div', { 'class': 'cbi-value-description' },
_('Choose a descriptive name (lowercase, hyphens allowed)'))
])
]),
E('div', { 'class': 'cbi-section', 'style': 'background: #e8f4f8; padding: 1em; margin-top: 1em; border-radius: 4px;' }, [
E('strong', {}, _('What happens next?')),
E('ol', { 'style': 'margin: 0.5em 0 0 1.5em; padding: 0;' }, [
E('li', {}, _('CrowdSec will generate a unique API key for this bouncer')),
E('li', {}, _('Copy the API key and configure your bouncer with it')),
E('li', {}, _('The bouncer will start pulling and applying decisions'))
])
])
]),
E('div', { 'class': 'right', 'style': 'margin-top: 1em;' }, [
E('button', {
'class': 'btn',
'click': ui.hideModal
}, _('Cancel')),
' ',
E('button', {
'class': 'btn cbi-button-positive',
'click': function() {
var bouncerName = nameInput.value.trim();
if (!bouncerName) {
ui.addNotification(null, E('p', _('Please enter a bouncer name')), 'error');
return;
}
// Validate name (alphanumeric, hyphens, underscores)
if (!/^[a-z0-9_-]+$/i.test(bouncerName)) {
ui.addNotification(null, E('p', _('Bouncer name can only contain letters, numbers, hyphens and underscores')), 'error');
return;
}
ui.hideModal();
ui.showModal(_('Registering Bouncer...'), [
E('p', {}, _('Creating bouncer: %s').format(bouncerName)),
E('div', { 'class': 'spinning' })
]);
API.registerBouncer(bouncerName).then(function(result) {
ui.hideModal();
if (result && result.success && result.api_key) {
// Show API key in a modal
ui.showModal(_('Bouncer Registered Successfully'), [
E('div', { 'class': 'cbi-section' }, [
E('p', { 'style': 'color: #28a745; font-weight: bold;' },
_('✓ Bouncer "%s" has been registered!').format(bouncerName)),
E('div', { 'class': 'cbi-value', 'style': 'margin-top: 1em;' }, [
E('label', { 'class': 'cbi-value-title' }, _('API Key')),
E('div', { 'class': 'cbi-value-field' }, [
E('code', {
'id': 'api-key-display',
'style': 'display: block; padding: 0.75em; background: #f5f5f5; border: 1px solid #ddd; border-radius: 4px; word-break: break-all; font-size: 0.9em;'
}, result.api_key),
E('button', {
'class': 'cbi-button cbi-button-action',
'style': 'margin-top: 0.5em;',
'click': function() {
navigator.clipboard.writeText(result.api_key).then(function() {
ui.addNotification(null, E('p', _('API key copied to clipboard')), 'info');
}).catch(function() {
ui.addNotification(null, E('p', _('Failed to copy. Please select and copy manually.')), 'error');
});
}
}, _('📋 Copy to Clipboard'))
])
]),
E('div', { 'class': 'cbi-section', 'style': 'background: #fff3cd; padding: 1em; margin-top: 1em; border-radius: 4px;' }, [
E('strong', { 'style': 'color: #856404;' }, _('⚠️ Important:')),
E('p', { 'style': 'margin: 0.5em 0 0 0; color: #856404;' },
_('Save this API key now! It will not be shown again. Use it to configure your bouncer.'))
])
]),
E('div', { 'class': 'right', 'style': 'margin-top: 1em;' }, [
E('button', {
'class': 'btn',
'click': function() {
ui.hideModal();
self.handleRefresh();
}
}, _('Close'))
])
]);
} else {
ui.addNotification(null, E('p', result.error || _('Failed to register bouncer')), 'error');
}
}).catch(function(err) {
ui.hideModal();
ui.addNotification(null, E('p', err.message || err), 'error');
});
}
}, _('Register'))
])
]);
// Focus the input field
setTimeout(function() {
if (nameInput) nameInput.focus();
}, 100);
},
handleDeleteBouncer: function(bouncerName) {
var self = this;
ui.showModal(_('Delete Bouncer'), [
E('p', {}, _('Are you sure you want to delete bouncer "%s"?').format(bouncerName)),
E('p', { 'style': 'color: #dc3545; font-weight: bold;' },
_('⚠️ This action cannot be undone. The bouncer will no longer be able to connect to the Local API.')),
E('div', { 'class': 'right', 'style': 'margin-top: 1em;' }, [
E('button', {
'class': 'btn',
'click': ui.hideModal
}, _('Cancel')),
' ',
E('button', {
'class': 'btn cbi-button-negative',
'click': function() {
ui.hideModal();
ui.showModal(_('Deleting Bouncer...'), [
E('p', {}, _('Removing bouncer: %s').format(bouncerName)),
E('div', { 'class': 'spinning' })
]);
API.deleteBouncer(bouncerName).then(function(result) {
ui.hideModal();
if (result && result.success) {
ui.addNotification(null, E('p', _('Bouncer "%s" deleted successfully').format(bouncerName)), 'info');
self.handleRefresh();
} else {
ui.addNotification(null, E('p', result.error || _('Failed to delete bouncer')), 'error');
}
}).catch(function(err) {
ui.hideModal();
ui.addNotification(null, E('p', err.message || err), 'error');
});
}
}, _('Delete'))
])
]);
},
renderFirewallBouncerCard: function(fwStatus, nftStats) {
var running = fwStatus.running || false;
var enabled = fwStatus.enabled || false;
var configured = fwStatus.configured || false;
var blockedIPv4 = fwStatus.blocked_ipv4 || 0;
var blockedIPv6 = fwStatus.blocked_ipv6 || 0;
var nftIPv4 = fwStatus.nftables_ipv4 || false;
var nftIPv6 = fwStatus.nftables_ipv6 || false;
return E('div', { 'class': 'cbi-section', 'id': 'firewall-bouncer-card' }, [
E('div', { 'style': 'display: flex; justify-content: space-between; align-items: center; margin-bottom: 1em;' }, [
E('h3', { 'style': 'margin: 0;' }, _('Firewall Bouncer')),
E('div', { 'style': 'display: flex; gap: 0.5em;' }, [
E('button', {
'class': 'cbi-button cbi-button-action',
'click': L.bind(this.handleFirewallBouncerRefresh, this)
}, _('Refresh'))
])
]),
E('div', { 'style': 'display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 1em; margin-bottom: 1em;' }, [
// Status Card
E('div', { 'style': 'background: ' + (running ? '#d4edda' : '#f8d7da') + '; border-left: 4px solid ' + (running ? '#28a745' : '#dc3545') + '; padding: 1em; border-radius: 4px;' }, [
E('div', { 'style': 'font-weight: bold; margin-bottom: 0.5em; color: #333;' }, _('Service Status')),
E('div', { 'style': 'display: flex; justify-content: space-between; align-items: center;' }, [
E('span', {}, _('Running:')),
E('span', {
'class': 'badge',
'id': 'fw-bouncer-status',
'style': 'background: ' + (running ? '#28a745' : '#dc3545') + '; color: white; padding: 0.25em 0.6em; border-radius: 3px;'
}, running ? _('ACTIVE') : _('STOPPED'))
]),
E('div', { 'style': 'display: flex; justify-content: space-between; align-items: center; margin-top: 0.5em;' }, [
E('span', {}, _('Boot Start:')),
E('span', {
'class': 'badge',
'id': 'fw-bouncer-enabled',
'style': 'background: ' + (enabled ? '#17a2b8' : '#6c757d') + '; color: white; padding: 0.25em 0.6em; border-radius: 3px;'
}, enabled ? _('ENABLED') : _('DISABLED'))
]),
E('div', { 'style': 'display: flex; justify-content: space-between; align-items: center; margin-top: 0.5em;' }, [
E('span', {}, _('Configured:')),
E('span', {
'class': 'badge',
'style': 'background: ' + (configured ? '#28a745' : '#ffc107') + '; color: ' + (configured ? 'white' : '#333') + '; padding: 0.25em 0.6em; border-radius: 3px;'
}, configured ? _('YES') : _('NO'))
])
]),
// Blocked IPs Card
E('div', { 'style': 'background: #e8f4f8; border-left: 4px solid #0088cc; padding: 1em; border-radius: 4px;' }, [
E('div', { 'style': 'font-weight: bold; margin-bottom: 0.5em; color: #333;' }, _('Blocked IPs')),
E('div', { 'style': 'display: flex; justify-content: space-between; align-items: center;' }, [
E('span', {}, _('IPv4:')),
E('span', {
'id': 'fw-bouncer-ipv4-count',
'style': 'font-size: 1.5em; color: #dc3545; font-weight: bold;'
}, blockedIPv4.toString())
]),
E('div', { 'style': 'display: flex; justify-content: space-between; align-items: center; margin-top: 0.5em;' }, [
E('span', {}, _('IPv6:')),
E('span', {
'id': 'fw-bouncer-ipv6-count',
'style': 'font-size: 1.5em; color: #dc3545; font-weight: bold;'
}, blockedIPv6.toString())
]),
E('div', { 'style': 'margin-top: 0.75em; padding-top: 0.75em; border-top: 1px solid #d1e7f0;' }, [
E('button', {
'class': 'cbi-button cbi-button-action',
'style': 'width: 100%; font-size: 0.9em;',
'click': L.bind(this.showNftablesDetails, this, nftStats)
}, _('View Details'))
])
]),
// nftables Status Card
E('div', { 'style': 'background: #fff3cd; border-left: 4px solid #ffc107; padding: 1em; border-radius: 4px;' }, [
E('div', { 'style': 'font-weight: bold; margin-bottom: 0.5em; color: #333;' }, _('nftables Status')),
E('div', { 'style': 'display: flex; justify-content: space-between; align-items: center;' }, [
E('span', {}, _('IPv4 Table:')),
E('span', {
'class': 'badge',
'style': 'background: ' + (nftIPv4 ? '#28a745' : '#6c757d') + '; color: white; padding: 0.25em 0.6em; border-radius: 3px;'
}, nftIPv4 ? _('ACTIVE') : _('INACTIVE'))
]),
E('div', { 'style': 'display: flex; justify-content: space-between; align-items: center; margin-top: 0.5em;' }, [
E('span', {}, _('IPv6 Table:')),
E('span', {
'class': 'badge',
'style': 'background: ' + (nftIPv6 ? '#28a745' : '#6c757d') + '; color: white; padding: 0.25em 0.6em; border-radius: 3px;'
}, nftIPv6 ? _('ACTIVE') : _('INACTIVE'))
])
])
]),
// Control Buttons
E('div', { 'style': 'display: flex; gap: 0.5em; flex-wrap: wrap;' }, [
running ?
E('button', {
'class': 'cbi-button cbi-button-negative',
'click': L.bind(this.handleFirewallBouncerControl, this, 'stop')
}, _('Stop Service')) :
E('button', {
'class': 'cbi-button cbi-button-positive',
'click': L.bind(this.handleFirewallBouncerControl, this, 'start')
}, _('Start Service')),
E('button', {
'class': 'cbi-button cbi-button-action',
'click': L.bind(this.handleFirewallBouncerControl, this, 'restart')
}, _('Restart')),
enabled ?
E('button', {
'class': 'cbi-button',
'click': L.bind(this.handleFirewallBouncerControl, this, 'disable')
}, _('Disable Boot Start')) :
E('button', {
'class': 'cbi-button cbi-button-apply',
'click': L.bind(this.handleFirewallBouncerControl, this, 'enable')
}, _('Enable Boot Start')),
E('button', {
'class': 'cbi-button',
'click': L.bind(this.showFirewallBouncerConfig, this)
}, _('Configuration'))
])
]);
},
handleFirewallBouncerControl: function(action) {
var actionLabels = {
'start': _('Starting'),
'stop': _('Stopping'),
'restart': _('Restarting'),
'enable': _('Enabling'),
'disable': _('Disabling')
};
ui.showModal(_('Firewall Bouncer Control'), [
E('p', {}, _('%s firewall bouncer...').format(actionLabels[action] || action)),
E('div', { 'class': 'spinning' })
]);
return API.controlFirewallBouncer(action).then(L.bind(function(result) {
ui.hideModal();
if (result && result.success) {
ui.addNotification(null, E('p', result.message || _('Operation completed successfully')), 'info');
this.handleFirewallBouncerRefresh();
} else {
ui.addNotification(null, E('p', result.error || _('Operation failed')), 'error');
}
}, this)).catch(function(err) {
ui.hideModal();
ui.addNotification(null, E('p', err.message || err), 'error');
});
},
handleFirewallBouncerRefresh: function() {
return Promise.all([
API.getFirewallBouncerStatus(),
API.getNftablesStats()
]).then(L.bind(function(data) {
var fwStatus = data[0] || {};
var nftStats = data[1] || {};
// Update status badges
var statusBadge = document.getElementById('fw-bouncer-status');
if (statusBadge) {
var running = fwStatus.running || false;
statusBadge.textContent = running ? _('ACTIVE') : _('STOPPED');
statusBadge.style.background = running ? '#28a745' : '#dc3545';
}
var enabledBadge = document.getElementById('fw-bouncer-enabled');
if (enabledBadge) {
var enabled = fwStatus.enabled || false;
enabledBadge.textContent = enabled ? _('ENABLED') : _('DISABLED');
enabledBadge.style.background = enabled ? '#17a2b8' : '#6c757d';
}
// Update blocked IP counts
var ipv4Count = document.getElementById('fw-bouncer-ipv4-count');
if (ipv4Count) {
ipv4Count.textContent = (fwStatus.blocked_ipv4 || 0).toString();
}
var ipv6Count = document.getElementById('fw-bouncer-ipv6-count');
if (ipv6Count) {
ipv6Count.textContent = (fwStatus.blocked_ipv6 || 0).toString();
}
// Re-render the entire card to update buttons
var card = document.getElementById('firewall-bouncer-card');
if (card) {
dom.content(card, this.renderFirewallBouncerCard(fwStatus, nftStats).childNodes);
}
ui.addNotification(null, E('p', _('Firewall bouncer status refreshed')), 'info');
}, this)).catch(function(err) {
ui.addNotification(null, E('p', _('Failed to refresh: %s').format(err.message || err)), 'error');
});
},
showNftablesDetails: function(nftStats) {
var ipv4Blocked = nftStats.ipv4_blocked || [];
var ipv6Blocked = nftStats.ipv6_blocked || [];
var ipv4Rules = nftStats.ipv4_rules || 0;
var ipv6Rules = nftStats.ipv6_rules || 0;
ui.showModal(_('nftables Blocked IPs'), [
E('div', { 'class': 'cbi-section' }, [
E('h4', {}, _('IPv4 Blocked Addresses (%d)').format(ipv4Blocked.length)),
ipv4Blocked.length > 0 ?
E('div', { 'style': 'max-height: 200px; overflow-y: auto; background: #f5f5f5; padding: 0.5em; border-radius: 4px; margin-bottom: 1em;' },
ipv4Blocked.map(function(ip) {
return E('div', { 'style': 'font-family: monospace; padding: 0.25em 0;' }, ip);
})
) :
E('p', { 'style': 'color: #999; margin-bottom: 1em;' }, _('No IPv4 addresses blocked')),
E('h4', {}, _('IPv6 Blocked Addresses (%d)').format(ipv6Blocked.length)),
ipv6Blocked.length > 0 ?
E('div', { 'style': 'max-height: 200px; overflow-y: auto; background: #f5f5f5; padding: 0.5em; border-radius: 4px; margin-bottom: 1em;' },
ipv6Blocked.map(function(ip) {
return E('div', { 'style': 'font-family: monospace; padding: 0.25em 0;' }, ip);
})
) :
E('p', { 'style': 'color: #999; margin-bottom: 1em;' }, _('No IPv6 addresses blocked')),
E('div', { 'style': 'background: #e8f4f8; padding: 1em; border-radius: 4px;' }, [
E('div', { 'style': 'display: flex; justify-content: space-between; margin-bottom: 0.5em;' }, [
E('strong', {}, _('IPv4 Rules:')),
E('span', {}, ipv4Rules.toString())
]),
E('div', { 'style': 'display: flex; justify-content: space-between;' }, [
E('strong', {}, _('IPv6 Rules:')),
E('span', {}, ipv6Rules.toString())
])
])
]),
E('div', { 'class': 'right', 'style': 'margin-top: 1em;' }, [
E('button', {
'class': 'btn',
'click': ui.hideModal
}, _('Close'))
])
]);
},
showFirewallBouncerConfig: function() {
ui.showModal(_('Loading Configuration...'), [
E('div', { 'class': 'spinning' })
]);
return API.getFirewallBouncerConfig().then(function(config) {
if (!config.configured) {
ui.hideModal();
ui.showModal(_('Firewall Bouncer Configuration'), [
E('div', { 'class': 'cbi-section' }, [
E('p', { 'style': 'color: #ffc107; font-weight: bold;' },
_('⚠️ Firewall bouncer is not configured yet.')),
E('p', {},
_('Please install the secubox-app-cs-firewall-bouncer package to configure the firewall bouncer.'))
]),
E('div', { 'class': 'right', 'style': 'margin-top: 1em;' }, [
E('button', {
'class': 'btn',
'click': ui.hideModal
}, _('Close'))
])
]);
return;
}
ui.hideModal();
ui.showModal(_('Firewall Bouncer Configuration'), [
E('div', { 'class': 'cbi-section' }, [
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, _('Enabled')),
E('div', { 'class': 'cbi-value-field' }, [
E('span', {
'class': 'badge',
'style': 'background: ' + (config.enabled === '1' ? '#28a745' : '#dc3545') + '; color: white; padding: 0.25em 0.6em;'
}, config.enabled === '1' ? _('YES') : _('NO'))
])
]),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, _('IPv4 Support')),
E('div', { 'class': 'cbi-value-field' }, config.ipv4 === '1' ? _('Enabled') : _('Disabled'))
]),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, _('IPv6 Support')),
E('div', { 'class': 'cbi-value-field' }, config.ipv6 === '1' ? _('Enabled') : _('Disabled'))
]),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, _('API URL')),
E('div', { 'class': 'cbi-value-field' }, E('code', {}, config.api_url || 'N/A'))
]),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, _('Update Frequency')),
E('div', { 'class': 'cbi-value-field' }, config.update_frequency || 'N/A')
]),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, _('Deny Action')),
E('div', { 'class': 'cbi-value-field' }, config.deny_action || 'drop')
]),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, _('Deny Logging')),
E('div', { 'class': 'cbi-value-field' }, config.deny_log === '1' ? _('Enabled') : _('Disabled'))
]),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, _('Log Prefix')),
E('div', { 'class': 'cbi-value-field' }, E('code', {}, config.log_prefix || 'N/A'))
]),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, _('Interfaces')),
E('div', { 'class': 'cbi-value-field' },
config.interfaces && config.interfaces.length > 0 ?
config.interfaces.join(', ') :
_('None configured')
)
]),
E('div', { 'class': 'cbi-section', 'style': 'background: #e8f4f8; padding: 1em; margin-top: 1em; border-radius: 4px;' }, [
E('p', { 'style': 'margin: 0;' }, [
E('strong', {}, _('Note:')),
' ',
_('To modify these settings, edit /etc/config/crowdsec using UCI commands or the configuration file.')
])
])
]),
E('div', { 'class': 'right', 'style': 'margin-top: 1em;' }, [
E('button', {
'class': 'btn',
'click': ui.hideModal
}, _('Close'))
])
]);
}).catch(function(err) {
ui.hideModal();
ui.addNotification(null, E('p', _('Failed to load configuration: %s').format(err.message || err)), 'error');
});
},
handleSaveApply: null,
handleSave: null,
handleReset: null
});