secubox-openwrt/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/wizard.js
CyberMind-FR ca562f69cd fix(crowdsec): Restart CrowdSec after console enrollment
- Add automatic restart after successful console enrollment
- Update wizard UI to inform user about validation on app.crowdsec.net
- Service must restart after enrollment is validated on CrowdSec Console

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-13 13:49:59 +01:00

787 lines
27 KiB
JavaScript

'use strict';
'require view';
'require secubox-theme/theme as Theme';
'require ui';
'require form';
'require rpc';
'require uci';
'require crowdsec-dashboard/api as API';
'require crowdsec-dashboard/nav as CsNav';
'require secubox-portal/header as SbHeader';
// Hide LuCI tabs immediately
(function() {
if (typeof document === 'undefined') return;
var style = document.getElementById('cs-hide-tabs') || document.createElement('style');
style.id = 'cs-hide-tabs';
style.textContent = 'ul.tabs, .cbi-tabmenu { display: none !important; }';
if (!style.parentNode) (document.head || document.documentElement).appendChild(style);
})();
return view.extend({
// Health check results
health: {
crowdsecRunning: false,
lapiAvailable: false,
capiConnected: false,
capiEnrolled: false,
bouncerRegistered: false,
bouncerRunning: false,
nftablesActive: false,
hubUpToDate: false,
acquisitionConfigured: false,
collectionsInstalled: 0,
checking: true,
error: null
},
// Configuration options (user selections)
config: {
// What needs to be fixed (auto-detected)
needsLapiRepair: false,
needsCapiRegister: false,
needsHubUpdate: false,
needsAcquisition: false,
needsCollections: false,
needsBouncer: false,
needsServices: false,
// User inputs
enrollmentKey: '',
machineName: '',
// Acquisition options
syslogEnabled: true,
firewallEnabled: true,
sshEnabled: true,
httpEnabled: false,
// Collections to install
selectedCollections: [],
selectedParsers: [],
// Bouncer options
ipv4Enabled: true,
ipv6Enabled: true,
updateFrequency: '10s',
// Apply state
applying: false,
applyStep: '',
applyProgress: 0,
applyComplete: false,
applyErrors: []
},
load: function() {
return this.runHealthCheck();
},
runHealthCheck: function() {
var self = this;
this.health.checking = true;
this.health.error = null;
return Promise.all([
API.getStatus(),
API.getConsoleStatus(),
API.getBouncers(),
API.getFirewallBouncerStatus(),
API.getCollections()
]).then(function(results) {
var status = results[0] || {};
var consoleStatus = results[1] || {};
var bouncers = results[2] || {};
var bouncerStatus = results[3] || {};
var collections = results[4] || [];
// Update health status
self.health.crowdsecRunning = status.crowdsec === 'running';
self.health.lapiAvailable = status.lapi_status === 'available';
self.health.capiConnected = status.capi_status === 'connected' || status.capi_status === 'ok';
self.health.capiEnrolled = consoleStatus.enrolled === true;
// Check bouncer registration
var bouncerList = bouncers.bouncers || bouncers || [];
var firewallBouncer = bouncerList.find(function(b) {
return b.name === 'crowdsec-firewall-bouncer' || b.name === 'firewall-bouncer';
});
self.health.bouncerRegistered = !!firewallBouncer;
self.health.bouncerRunning = bouncerStatus.running === true;
self.health.nftablesActive = bouncerStatus.nftables_ipv4 || bouncerStatus.nftables_ipv6;
// Count installed collections
self.health.collectionsInstalled = Array.isArray(collections) ?
collections.filter(function(c) { return c.installed; }).length : 0;
self.health.checking = false;
// Determine what needs to be fixed
self.config.needsLapiRepair = self.health.crowdsecRunning && !self.health.lapiAvailable;
self.config.needsCapiRegister = !self.health.capiConnected && !self.health.capiEnrolled;
self.config.needsHubUpdate = self.health.capiConnected; // Only if CAPI works
self.config.needsCollections = self.health.collectionsInstalled < 3;
self.config.needsBouncer = !self.health.bouncerRegistered;
self.config.needsServices = !self.health.bouncerRunning || !self.health.nftablesActive;
return self.health;
}).catch(function(err) {
console.error('[Wizard] Health check error:', err);
self.health.checking = false;
self.health.error = err.message;
return self.health;
});
},
render: function(data) {
// Initialize theme
Theme.init();
// Load CSS
var head = document.head || document.getElementsByTagName('head')[0];
var cssLink = E('link', {
'rel': 'stylesheet',
'type': 'text/css',
'href': L.resource('crowdsec-dashboard/wizard.css')
});
head.appendChild(cssLink);
var themeLink = E('link', {
'rel': 'stylesheet',
'type': 'text/css',
'href': L.resource('secubox-theme/secubox-theme.css')
});
head.appendChild(themeLink);
// Main wrapper
var wrapper = E('div', { 'class': 'secubox-page-wrapper' });
wrapper.appendChild(SbHeader.render());
var container = E('div', { 'class': 'crowdsec-dashboard wizard-container' });
container.appendChild(CsNav.renderTabs('wizard'));
// Render content based on state
if (this.config.applyComplete) {
container.appendChild(this.renderComplete());
} else if (this.config.applying) {
container.appendChild(this.renderApplying());
} else {
container.appendChild(this.renderHealthAndConfig());
}
wrapper.appendChild(container);
return wrapper;
},
renderHealthAndConfig: function() {
var self = this;
return E('div', { 'class': 'wizard-step' }, [
// Header
E('h2', {}, _('CrowdSec Setup Wizard')),
E('p', {}, _('Health check and configuration in one step.')),
// Health Check Section
this.renderHealthCheck(),
// Separator
E('hr', { 'style': 'margin: 24px 0; border-color: rgba(255,255,255,0.1);' }),
// Configuration Section (only if health check passed basic requirements)
this.health.crowdsecRunning ? this.renderConfigOptions() : E([]),
// Apply Button
E('div', { 'class': 'wizard-nav', 'style': 'margin-top: 32px;' }, [
E('button', {
'class': 'cbi-button',
'click': function() {
window.location.href = L.url('admin', 'secubox', 'security', 'crowdsec', 'overview');
}
}, _('Cancel')),
E('button', {
'class': 'cbi-button cbi-button-positive',
'style': 'font-size: 1.1em; padding: 12px 32px;',
'disabled': !this.health.crowdsecRunning || this.health.checking ? true : null,
'click': L.bind(this.handleApplyAll, this)
}, _('Apply Configuration'))
])
]);
},
renderHealthCheck: function() {
var self = this;
var checks = [
{
id: 'crowdsec',
label: _('CrowdSec Service'),
ok: this.health.crowdsecRunning,
status: this.health.crowdsecRunning ? _('Running') : _('Stopped'),
critical: true
},
{
id: 'lapi',
label: _('Local API (LAPI)'),
ok: this.health.lapiAvailable,
status: this.health.lapiAvailable ? _('Available') : _('Unavailable'),
action: !this.health.lapiAvailable && this.health.crowdsecRunning ? _('Will repair') : null
},
{
id: 'capi',
label: _('Central API (CAPI)'),
ok: this.health.capiConnected,
status: this.health.capiConnected ? _('Connected') : (this.health.capiEnrolled ? _('Enrolled but not connected') : _('Not registered')),
action: !this.health.capiConnected ? _('Enrollment required') : null
},
{
id: 'bouncer',
label: _('Firewall Bouncer'),
ok: this.health.bouncerRegistered && this.health.bouncerRunning,
status: this.health.bouncerRegistered ?
(this.health.bouncerRunning ? _('Running') : _('Registered but stopped')) :
_('Not registered'),
action: !this.health.bouncerRegistered ? _('Will register') : (!this.health.bouncerRunning ? _('Will start') : null)
},
{
id: 'nftables',
label: _('nftables Rules'),
ok: this.health.nftablesActive,
status: this.health.nftablesActive ? _('Active') : _('Not loaded'),
action: !this.health.nftablesActive ? _('Will configure') : null
},
{
id: 'collections',
label: _('Security Collections'),
ok: this.health.collectionsInstalled >= 3,
status: _('%d installed').format(this.health.collectionsInstalled),
action: this.health.collectionsInstalled < 3 ? _('Will install') : null
}
];
return E('div', { 'class': 'health-check-section' }, [
E('h3', { 'style': 'margin-bottom: 16px; display: flex; align-items: center;' }, [
E('span', { 'style': 'margin-right: 8px;' }, '🔍'),
_('Health Check'),
this.health.checking ? E('span', { 'class': 'spinning', 'style': 'margin-left: 12px; font-size: 14px;' }, '') : E([])
]),
E('div', { 'class': 'status-checks', 'style': 'display: grid; gap: 8px;' },
checks.map(function(check) {
return E('div', {
'class': 'check-item',
'style': 'display: flex; align-items: center; padding: 12px; background: rgba(15, 23, 42, 0.5); border-radius: 8px;'
}, [
E('span', {
'class': 'check-icon',
'style': 'font-size: 20px; margin-right: 12px; color: ' + (check.ok ? '#22c55e' : (check.critical ? '#ef4444' : '#eab308')) + ';'
}, check.ok ? '✓' : (check.critical ? '✗' : '⚠')),
E('div', { 'style': 'flex: 1;' }, [
E('strong', {}, check.label),
E('div', { 'style': 'font-size: 0.85em; color: ' + (check.ok ? '#22c55e' : '#94a3b8') + ';' }, check.status)
]),
check.action ? E('span', {
'class': 'badge',
'style': 'background: rgba(102, 126, 234, 0.2); color: #818cf8; padding: 4px 8px; border-radius: 4px; font-size: 0.8em;'
}, check.action) : E([])
]);
})
),
// Error message if any
this.health.error ? E('div', {
'style': 'margin-top: 16px; padding: 12px; background: rgba(239, 68, 68, 0.1); border: 1px solid rgba(239, 68, 68, 0.3); border-radius: 8px; color: #ef4444;'
}, this.health.error) : E([]),
// Refresh button
E('div', { 'style': 'margin-top: 12px;' }, [
E('button', {
'class': 'cbi-button',
'click': L.bind(function() {
this.runHealthCheck().then(L.bind(function() {
this.refreshView();
}, this));
}, this)
}, _('Refresh'))
])
]);
},
renderConfigOptions: function() {
var self = this;
return E('div', { 'class': 'config-section' }, [
E('h3', { 'style': 'margin-bottom: 16px; display: flex; align-items: center;' }, [
E('span', { 'style': 'margin-right: 8px;' }, '⚙️'),
_('Configuration Options')
]),
// CAPI Enrollment (if needed)
!this.health.capiConnected ? E('div', {
'class': 'config-group',
'style': 'margin-bottom: 20px; padding: 16px; background: rgba(15, 23, 42, 0.5); border-radius: 8px;'
}, [
E('h4', { 'style': 'margin: 0 0 12px 0; color: #818cf8;' }, _('Console Enrollment (Optional)')),
E('p', { 'style': 'margin: 0 0 12px 0; font-size: 0.9em; color: #94a3b8;' },
_('Enroll to receive community blocklists. Leave empty to skip.')),
E('input', {
'type': 'text',
'id': 'enrollment-key',
'class': 'cbi-input-text',
'placeholder': _('Enrollment key from app.crowdsec.net'),
'style': 'width: 100%; padding: 10px; margin-bottom: 8px;',
'value': this.config.enrollmentKey,
'input': function(ev) { self.config.enrollmentKey = ev.target.value; }
}),
E('input', {
'type': 'text',
'id': 'machine-name',
'class': 'cbi-input-text',
'placeholder': _('Machine name (optional)'),
'style': 'width: 100%; padding: 10px;',
'value': this.config.machineName,
'input': function(ev) { self.config.machineName = ev.target.value; }
}),
E('p', { 'style': 'margin: 8px 0 0 0; font-size: 0.85em; color: #64748b;' }, [
_('After enrollment, validate on '),
E('a', { 'href': 'https://app.crowdsec.net', 'target': '_blank', 'style': 'color: #818cf8;' }, 'app.crowdsec.net'),
_('. Service will restart automatically.')
])
]) : E([]),
// Log Acquisition
E('div', {
'class': 'config-group',
'style': 'margin-bottom: 20px; padding: 16px; background: rgba(15, 23, 42, 0.5); border-radius: 8px;'
}, [
E('h4', { 'style': 'margin: 0 0 12px 0; color: #818cf8;' }, _('Log Sources to Monitor')),
this.renderToggle('syslog', _('System Syslog'), _('/var/log/messages'), this.config.syslogEnabled),
this.renderToggle('firewall', _('Firewall Logs'), _('Port scan detection'), this.config.firewallEnabled),
this.renderToggle('ssh', _('SSH/Dropbear'), _('Brute force detection'), this.config.sshEnabled),
this.renderToggle('http', _('HTTP Server'), _('Web attacks (if running)'), this.config.httpEnabled)
]),
// Collections
E('div', {
'class': 'config-group',
'style': 'margin-bottom: 20px; padding: 16px; background: rgba(15, 23, 42, 0.5); border-radius: 8px;'
}, [
E('h4', { 'style': 'margin: 0 0 12px 0; color: #818cf8;' }, _('Security Collections to Install')),
this.renderCollectionToggle('crowdsecurity/linux', _('Base Linux scenarios'), true),
this.renderCollectionToggle('crowdsecurity/sshd', _('SSH protection'), true),
this.renderCollectionToggle('crowdsecurity/iptables', _('Firewall log parser'), this.config.firewallEnabled),
this.renderCollectionToggle('crowdsecurity/http-cve', _('Web CVE protection'), this.config.httpEnabled),
E('div', { 'style': 'margin-top: 12px;' }, [
E('strong', { 'style': 'font-size: 0.9em; color: #94a3b8;' }, _('OpenWrt Parsers:')),
]),
this.renderCollectionToggle('crowdsecurity/syslog-logs', _('Syslog parser'), true, 'parser'),
this.renderCollectionToggle('crowdsecurity/dropbear-logs', _('Dropbear SSH parser'), this.config.sshEnabled, 'parser')
]),
// Bouncer Options
E('div', {
'class': 'config-group',
'style': 'padding: 16px; background: rgba(15, 23, 42, 0.5); border-radius: 8px;'
}, [
E('h4', { 'style': 'margin: 0 0 12px 0; color: #818cf8;' }, _('Firewall Bouncer Options')),
this.renderToggle('ipv4', _('Block IPv4'), _('Enable IPv4 blocking'), this.config.ipv4Enabled),
this.renderToggle('ipv6', _('Block IPv6'), _('Enable IPv6 blocking'), this.config.ipv6Enabled),
E('div', { 'style': 'margin-top: 12px;' }, [
E('label', { 'style': 'display: block; margin-bottom: 4px; font-size: 0.9em;' }, _('Update Frequency:')),
E('select', {
'id': 'update-frequency',
'class': 'cbi-input-select',
'style': 'width: 100%; padding: 8px;',
'change': function(ev) { self.config.updateFrequency = ev.target.value; }
}, [
E('option', { 'value': '10s', 'selected': this.config.updateFrequency === '10s' }, _('10 seconds (recommended)')),
E('option', { 'value': '30s', 'selected': this.config.updateFrequency === '30s' }, _('30 seconds')),
E('option', { 'value': '1m', 'selected': this.config.updateFrequency === '1m' }, _('1 minute'))
])
])
])
]);
},
renderToggle: function(id, label, description, checked) {
var self = this;
return E('div', {
'class': 'toggle-item',
'data-id': id,
'data-checked': checked ? '1' : '0',
'style': 'display: flex; align-items: center; padding: 8px 0; cursor: pointer;',
'click': function(ev) {
var el = ev.currentTarget;
var current = el.getAttribute('data-checked') === '1';
var newState = !current;
el.setAttribute('data-checked', newState ? '1' : '0');
var checkbox = el.querySelector('.toggle-check');
if (checkbox) {
checkbox.textContent = newState ? '☑' : '☐';
checkbox.style.color = newState ? '#22c55e' : '#64748b';
}
// Update config
var configId = el.getAttribute('data-id');
if (configId === 'syslog') self.config.syslogEnabled = newState;
else if (configId === 'firewall') self.config.firewallEnabled = newState;
else if (configId === 'ssh') self.config.sshEnabled = newState;
else if (configId === 'http') self.config.httpEnabled = newState;
else if (configId === 'ipv4') self.config.ipv4Enabled = newState;
else if (configId === 'ipv6') self.config.ipv6Enabled = newState;
}
}, [
E('span', {
'class': 'toggle-check',
'style': 'font-size: 22px; margin-right: 12px; color: ' + (checked ? '#22c55e' : '#64748b') + ';'
}, checked ? '☑' : '☐'),
E('div', { 'style': 'flex: 1;' }, [
E('strong', { 'style': 'display: block;' }, label),
E('span', { 'style': 'font-size: 0.85em; color: #64748b;' }, description)
])
]);
},
renderCollectionToggle: function(name, description, checked, type) {
var self = this;
type = type || 'collection';
return E('div', {
'class': 'collection-toggle',
'data-name': name,
'data-type': type,
'data-checked': checked ? '1' : '0',
'style': 'display: flex; align-items: center; padding: 6px 0; cursor: pointer;',
'click': function(ev) {
var el = ev.currentTarget;
var current = el.getAttribute('data-checked') === '1';
var newState = !current;
el.setAttribute('data-checked', newState ? '1' : '0');
var checkbox = el.querySelector('.toggle-check');
if (checkbox) {
checkbox.textContent = newState ? '☑' : '☐';
checkbox.style.color = newState ? '#22c55e' : '#64748b';
}
}
}, [
E('span', {
'class': 'toggle-check',
'style': 'font-size: 20px; margin-right: 10px; color: ' + (checked ? '#22c55e' : '#64748b') + ';'
}, checked ? '☑' : '☐'),
E('div', { 'style': 'flex: 1;' }, [
E('code', { 'style': 'font-size: 0.9em;' }, name),
E('span', { 'style': 'margin-left: 8px; font-size: 0.85em; color: #64748b;' }, '— ' + description)
])
]);
},
renderApplying: function() {
var progressPercent = Math.round(this.config.applyProgress);
return E('div', { 'class': 'wizard-step', 'style': 'text-align: center; padding: 48px 24px;' }, [
E('div', { 'class': 'spinning', 'style': 'font-size: 48px; margin-bottom: 24px;' }, '⚙️'),
E('h2', {}, _('Applying Configuration...')),
E('p', { 'style': 'color: #94a3b8; margin-bottom: 24px;' }, this.config.applyStep),
// Progress bar
E('div', { 'style': 'max-width: 400px; margin: 0 auto 24px;' }, [
E('div', {
'style': 'height: 8px; background: rgba(255,255,255,0.1); border-radius: 4px; overflow: hidden;'
}, [
E('div', {
'style': 'height: 100%; width: ' + progressPercent + '%; background: linear-gradient(90deg, #667eea, #764ba2); transition: width 0.3s;'
})
]),
E('div', { 'style': 'margin-top: 8px; font-size: 0.9em; color: #64748b;' }, progressPercent + '%')
]),
// Errors if any
this.config.applyErrors.length > 0 ? E('div', {
'style': 'max-width: 500px; margin: 0 auto; text-align: left; padding: 12px; background: rgba(239, 68, 68, 0.1); border-radius: 8px;'
}, [
E('strong', { 'style': 'color: #ef4444;' }, _('Warnings:')),
E('ul', { 'style': 'margin: 8px 0 0 0; padding-left: 20px; color: #f87171;' },
this.config.applyErrors.map(function(err) {
return E('li', {}, err);
})
)
]) : E([])
]);
},
renderComplete: function() {
return E('div', { 'class': 'wizard-step wizard-complete', 'style': 'text-align: center;' }, [
E('div', { 'class': 'success-hero', 'style': 'margin-bottom: 32px;' }, [
E('div', { 'style': 'font-size: 64px; margin-bottom: 16px;' }, '🎉'),
E('h2', {}, _('Setup Complete!'))
]),
E('p', { 'style': 'color: #94a3b8; margin-bottom: 32px;' },
_('CrowdSec is now protecting your network.')),
// Summary of what was done
E('div', { 'style': 'max-width: 400px; margin: 0 auto 32px; text-align: left;' }, [
this.health.lapiAvailable ? this.renderCompletedItem(_('LAPI available')) : E([]),
this.health.capiConnected ? this.renderCompletedItem(_('CAPI connected')) : E([]),
this.health.bouncerRegistered ? this.renderCompletedItem(_('Bouncer registered')) : E([]),
this.health.nftablesActive ? this.renderCompletedItem(_('nftables rules active')) : E([]),
this.health.collectionsInstalled > 0 ?
this.renderCompletedItem(_('%d collections installed').format(this.health.collectionsInstalled)) : E([])
]),
// Errors/warnings if any
this.config.applyErrors.length > 0 ? E('div', {
'style': 'max-width: 500px; margin: 0 auto 24px; padding: 16px; background: rgba(234, 179, 8, 0.1); border: 1px solid rgba(234, 179, 8, 0.3); border-radius: 8px; text-align: left;'
}, [
E('strong', { 'style': 'color: #eab308;' }, _('Some steps had issues:')),
E('ul', { 'style': 'margin: 8px 0 0 0; padding-left: 20px; color: #fbbf24;' },
this.config.applyErrors.map(function(err) {
return E('li', {}, err);
})
)
]) : E([]),
// Navigation
E('div', { 'class': 'wizard-nav' }, [
E('button', {
'class': 'cbi-button cbi-button-positive',
'style': 'font-size: 1.1em; padding: 12px 32px;',
'click': function() {
window.location.href = L.url('admin', 'secubox', 'security', 'crowdsec', 'overview');
}
}, _('Go to Dashboard'))
])
]);
},
renderCompletedItem: function(text) {
return E('div', { 'style': 'display: flex; align-items: center; padding: 8px 0;' }, [
E('span', { 'style': 'color: #22c55e; margin-right: 12px; font-size: 18px;' }, '✓'),
E('span', {}, text)
]);
},
handleApplyAll: function() {
var self = this;
this.config.applying = true;
this.config.applyProgress = 0;
this.config.applyErrors = [];
this.refreshView();
// Gather selected collections and parsers from DOM
var collectionToggles = document.querySelectorAll('.collection-toggle[data-type="collection"][data-checked="1"]');
var parserToggles = document.querySelectorAll('.collection-toggle[data-type="parser"][data-checked="1"]');
this.config.selectedCollections = Array.from(collectionToggles).map(function(el) {
return el.getAttribute('data-name');
});
this.config.selectedParsers = Array.from(parserToggles).map(function(el) {
return el.getAttribute('data-name');
});
// Get enrollment key
var keyInput = document.getElementById('enrollment-key');
var nameInput = document.getElementById('machine-name');
this.config.enrollmentKey = keyInput ? keyInput.value.trim() : '';
this.config.machineName = nameInput ? nameInput.value.trim() : '';
// Define steps
var steps = [];
var stepWeight = 0;
// Step 1: Repair LAPI if needed
if (this.config.needsLapiRepair) {
steps.push({ name: 'lapi', label: _('Repairing LAPI...'), weight: 15 });
stepWeight += 15;
}
// Step 2: Enroll CAPI if key provided
if (this.config.enrollmentKey) {
steps.push({ name: 'capi', label: _('Enrolling in CrowdSec Console...'), weight: 10 });
stepWeight += 10;
}
// Step 3: Update hub if CAPI available
if (this.health.capiConnected || this.config.enrollmentKey) {
steps.push({ name: 'hub', label: _('Updating hub...'), weight: 10 });
stepWeight += 10;
}
// Step 4: Configure acquisition
steps.push({ name: 'acquisition', label: _('Configuring log acquisition...'), weight: 15 });
stepWeight += 15;
// Step 5: Install collections
if (this.config.selectedCollections.length > 0 || this.config.selectedParsers.length > 0) {
steps.push({ name: 'collections', label: _('Installing collections...'), weight: 20 });
stepWeight += 20;
}
// Step 6: Configure bouncer
if (this.config.needsBouncer) {
steps.push({ name: 'bouncer', label: _('Registering bouncer...'), weight: 15 });
stepWeight += 15;
}
// Step 7: Start services
steps.push({ name: 'services', label: _('Starting services...'), weight: 15 });
stepWeight += 15;
// Normalize weights
var totalWeight = stepWeight;
steps.forEach(function(s) {
s.weight = (s.weight / totalWeight) * 100;
});
// Execute steps
var currentProgress = 0;
return steps.reduce(function(promise, step) {
return promise.then(function() {
self.config.applyStep = step.label;
self.refreshView();
return self.executeStep(step.name).then(function(result) {
currentProgress += step.weight;
self.config.applyProgress = currentProgress;
if (result && result.error) {
self.config.applyErrors.push(step.label + ': ' + result.error);
}
}).catch(function(err) {
currentProgress += step.weight;
self.config.applyProgress = currentProgress;
self.config.applyErrors.push(step.label + ': ' + err.message);
});
});
}, Promise.resolve()).then(function() {
// Final health check
return self.runHealthCheck();
}).then(function() {
self.config.applying = false;
self.config.applyComplete = true;
self.refreshView();
});
},
executeStep: function(stepName) {
var self = this;
switch (stepName) {
case 'lapi':
return API.repairLapi();
case 'capi':
return API.consoleEnroll(this.config.enrollmentKey, this.config.machineName);
case 'hub':
return API.updateHub().catch(function(err) {
// Hub update failure is not critical
return { success: false, error: err.message };
});
case 'acquisition':
return API.configureAcquisition(
this.config.syslogEnabled ? '1' : '0',
this.config.firewallEnabled ? '1' : '0',
this.config.sshEnabled ? '1' : '0',
this.config.httpEnabled ? '1' : '0',
'/var/log/messages'
).catch(function(err) {
// XHR abort during restart is OK
if (err.message && err.message.indexOf('abort') !== -1) {
return { success: true };
}
throw err;
});
case 'collections':
// Install collections sequentially
var items = this.config.selectedCollections.map(function(c) {
return { type: 'collection', name: c };
}).concat(this.config.selectedParsers.map(function(p) {
return { type: 'parser', name: p };
}));
return items.reduce(function(promise, item) {
return promise.then(function() {
if (item.type === 'collection') {
return API.installCollection(item.name).catch(function() { return {}; });
} else {
return API.installHubItem('parser', item.name).catch(function() { return {}; });
}
});
}, Promise.resolve()).then(function() {
return { success: true };
});
case 'bouncer':
return API.registerBouncer('crowdsec-firewall-bouncer').then(function(result) {
if (!result.success) {
return result;
}
// Configure bouncer settings
return Promise.all([
API.updateFirewallBouncerConfig('enabled', '1'),
API.updateFirewallBouncerConfig('ipv4', self.config.ipv4Enabled ? '1' : '0'),
API.updateFirewallBouncerConfig('ipv6', self.config.ipv6Enabled ? '1' : '0'),
API.updateFirewallBouncerConfig('update_frequency', self.config.updateFrequency),
API.updateFirewallBouncerConfig('api_key', result.api_key)
]).then(function() {
return { success: true };
});
}).catch(function(err) {
// XHR abort during service restart is OK
if (err.message && err.message.indexOf('abort') !== -1) {
return { success: true };
}
throw err;
});
case 'services':
return API.controlFirewallBouncer('enable').then(function() {
return API.controlFirewallBouncer('start');
}).then(function() {
// Wait for service to start
return new Promise(function(resolve) { setTimeout(resolve, 2000); });
}).then(function() {
return { success: true };
}).catch(function(err) {
// XHR abort during service restart is OK
if (err.message && err.message.indexOf('abort') !== -1) {
return { success: true };
}
throw err;
});
default:
return Promise.resolve({ success: true });
}
},
refreshView: function() {
var container = document.querySelector('.wizard-container');
if (!container) return;
// Remove old content (keep nav tabs)
var tabs = container.querySelector('.cs-nav-tabs');
while (container.lastChild && container.lastChild !== tabs) {
container.removeChild(container.lastChild);
}
// Render new content
if (this.config.applyComplete) {
container.appendChild(this.renderComplete());
} else if (this.config.applying) {
container.appendChild(this.renderApplying());
} else {
container.appendChild(this.renderHealthAndConfig());
}
},
goToStep: function() {},
handleSaveAndApply: null,
handleSave: null,
handleReset: null
});