secubox-openwrt/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/bouncers.js
CyberMind-FR ba64563b3f feat: Firewall Bouncer Management UI in Bouncers Page
Enhanced CrowdSec Dashboard bouncers page with comprehensive firewall
bouncer management capabilities.

New Features:
- Dedicated Firewall Bouncer management card with 3 status panels:
  * Service Status: Running/stopped, boot start enabled/disabled, configured status
  * Blocked IPs: Real-time IPv4/IPv6 blocked IP counts with View Details modal
  * nftables Status: IPv4/IPv6 table active status

- Service Control Buttons:
  * Start/Stop service (contextual based on current state)
  * Restart service
  * Enable/Disable boot start (contextual)
  * Configuration viewer

- Real-time Updates:
  * Auto-refresh every 10 seconds via polling
  * Manual refresh button
  * Live status badge updates

- nftables Details Modal:
  * Lists all blocked IPv4 addresses (scrollable)
  * Lists all blocked IPv6 addresses (scrollable)
  * Shows IPv4/IPv6 rules count
  * Formatted with monospace font

- Configuration Viewer Modal:
  * Displays all UCI configuration settings
  * Shows enabled/disabled status
  * Shows IPv4/IPv6 support
  * Shows API URL, update frequency, deny action
  * Shows deny logging and log prefix
  * Shows configured network interfaces
  * Handles unconfigured state with installation prompt

UI Enhancements:
- Responsive grid layout for status cards
- Color-coded status indicators (green=active, red=stopped, gray=disabled, yellow=warning)
- Material design badges for all status indicators
- Visual feedback for all operations with notifications
- Loading spinners for async operations
- Professional styling consistent with SecuBox theme

Integration:
- Utilizes new API methods: getFirewallBouncerStatus, controlFirewallBouncer,
  getFirewallBouncerConfig, getNftablesStats
- Error handling with user-friendly notifications
- Proper promise chaining and async/await patterns

Technical Details:
- Added renderFirewallBouncerCard() method (125 lines)
- Added handleFirewallBouncerControl() method for service actions
- Added handleFirewallBouncerRefresh() for manual/auto refresh
- Added showNftablesDetails() modal for blocked IPs
- Added showFirewallBouncerConfig() modal for UCI settings
- Enhanced load() to fetch firewall bouncer data
- Updated polling to refresh firewall bouncer status

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-06 20:25:15 +01:00

783 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';
return view.extend({
load: function() {
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': 'cbi-map' }, [
E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }),
E('h2', {}, _('CrowdSec Bouncers')),
E('div', { 'class': 'cbi-map-descr' },
_('Bouncers are components that enforce CrowdSec decisions by blocking malicious IPs at various points (firewall, web server, etc.).')),
// 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-crowdsec-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
});