secubox-openwrt/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/bouncers.js
CyberMind-FR 31a87c5d7a feat(structure): reorganize luci-app packages into package/secubox/ + appstore migration
Major structural reorganization and feature additions:

## Folder Reorganization
- Move 17 luci-app-* packages to package/secubox/ (except luci-app-secubox core hub)
- Update all tooling to support new structure:
  - secubox-tools/quick-deploy.sh: search both locations
  - secubox-tools/validate-modules.sh: validate both directories
  - secubox-tools/fix-permissions.sh: fix permissions in both locations
  - .github/workflows/test-validate.yml: build from both paths
- Update README.md links to new package/secubox/ paths

## AppStore Migration (Complete)
- Add catalog entries for all remaining luci-app packages:
  - network-tweaks.json: Network optimization tools
  - secubox-bonus.json: Documentation & demos hub
- Total: 24 apps in AppStore catalog (22 existing + 2 new)
- New category: 'documentation' for docs/demos/tutorials

## VHost Manager v2.0 Enhancements
- Add profile activation system for Internal Services and Redirects
- Implement createVHost() API wrapper for template-based deployment
- Fix Virtual Hosts view rendering with proper LuCI patterns
- Fix RPCD backend shell script errors (remove invalid local declarations)
- Extend backend validation for nginx return directives (redirect support)
- Add section_id parameter for named VHost profiles
- Add Remove button to Redirects page for feature parity
- Update README to v2.0 with comprehensive feature documentation

## Network Tweaks Dashboard
- Close button added to component details modal

Files changed: 340+ (336 renames with preserved git history)
Packages affected: 19 luci-app, 2 secubox-app, 1 theme, 4 tools

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-01 14:59:38 +01:00

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
});