secubox-openwrt/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/bouncers.js

401 lines
14 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()
]);
},
render: function(data) {
var bouncers = data[0] || [];
var status = data[1] || {};
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())
])
])
]),
// 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 API.getBouncers().then(L.bind(function(refreshData) {
var tbody = document.getElementById('bouncers-tbody');
if (tbody) {
dom.content(tbody, this.renderBouncerRows(refreshData || []));
}
}, 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'))
])
]);
},
handleSaveApply: null,
handleSave: null,
handleReset: null
});