From fa5662604df7c1c44bd9117b7b97240e44f5ccc3 Mon Sep 17 00:00:00 2001 From: CyberMind-FR Date: Tue, 13 Jan 2026 13:18:01 +0100 Subject: [PATCH] feat(crowdsec-wizard): Complete rewrite with single-page health check New wizard approach: - Automatic health check on load (LAPI, CAPI, Bouncer, nftables, collections) - Single configuration page with all options visible - Only repairs what's broken - No hub update without CAPI connection - Single "Apply Configuration" button at the end - Progress bar during apply - Summary of what was done at completion Co-Authored-By: Claude Opus 4.5 --- .../luci-app-crowdsec-dashboard/Makefile | 2 +- .../view/crowdsec-dashboard/wizard.js | 1875 ++++++----------- 2 files changed, 613 insertions(+), 1264 deletions(-) diff --git a/package/secubox/luci-app-crowdsec-dashboard/Makefile b/package/secubox/luci-app-crowdsec-dashboard/Makefile index 9d0181a8..2212ee97 100644 --- a/package/secubox/luci-app-crowdsec-dashboard/Makefile +++ b/package/secubox/luci-app-crowdsec-dashboard/Makefile @@ -9,7 +9,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-crowdsec-dashboard PKG_VERSION:=0.7.0 -PKG_RELEASE:=21 +PKG_RELEASE:=22 PKG_ARCH:=all PKG_LICENSE:=Apache-2.0 diff --git a/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/wizard.js b/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/wizard.js index fad790c6..b74ae2b1 100644 --- a/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/wizard.js +++ b/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/wizard.js @@ -19,124 +19,125 @@ })(); return view.extend({ - wizardData: { - currentStep: 1, - totalSteps: 8, - - // Step 1 data + // Health check results + health: { crowdsecRunning: false, lapiAvailable: false, - lapiRepairing: false, - lapiRepairAttempted: false, + capiConnected: false, + capiEnrolled: false, + bouncerRegistered: false, + bouncerRunning: false, + nftablesActive: false, + hubUpToDate: false, + acquisitionConfigured: false, + collectionsInstalled: 0, + checking: true, + error: null + }, - // Step 2 data (Console Enrollment) - consoleEnrolled: false, - enrolling: false, + // 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: '', - // Step 3 data (Hub Update) - hubUpdating: false, - hubUpdated: false, - - // Step 4 data (Log Acquisition) - acquisitionConfigured: false, - acquisitionConfiguring: false, + // Acquisition options syslogEnabled: true, firewallEnabled: true, sshEnabled: true, httpEnabled: false, - syslogPath: '/var/log/messages', - // Step 5 data (Collections) - collections: [], - installing: false, - installed: false, - installStatus: '', - installedCount: 0, + // Collections to install + selectedCollections: [], + selectedParsers: [], - // Step 6 data (Bouncer) - configuring: false, - bouncerConfigured: false, - apiKey: '', - resetting: false, + // Bouncer options + ipv4Enabled: true, + ipv6Enabled: true, + updateFrequency: '10s', - // Step 7 data (Services) - starting: false, - enabling: false, - enabled: false, - running: false, - nftablesActive: false, - lapiConnected: false, - - // Step 8 data (Complete) - blockedIPs: 0, - activeDecisions: 0 + // 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.checkWizardNeeded(), - API.getConsoleStatus() - ]).then(L.bind(function(results) { - var status = results[0]; - var wizardNeeded = results[1]; - var consoleStatus = results[2]; + 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 wizard data from status - this.wizardData.crowdsecRunning = status && status.crowdsec === 'running'; - this.wizardData.lapiAvailable = status && status.lapi_status === 'available'; - this.wizardData.consoleEnrolled = consoleStatus && consoleStatus.enrolled; + // 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; - // Auto-repair LAPI if CrowdSec is running but LAPI is not available - if (this.wizardData.crowdsecRunning && !this.wizardData.lapiAvailable && !this.wizardData.lapiRepairAttempted) { - console.log('[Wizard] LAPI unavailable, triggering auto-repair...'); - this.wizardData.lapiRepairing = true; - this.wizardData.lapiRepairAttempted = 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; - return API.repairLapi().then(L.bind(function(repairResult) { - console.log('[Wizard] LAPI repair result:', repairResult); - this.wizardData.lapiRepairing = false; + // Count installed collections + self.health.collectionsInstalled = Array.isArray(collections) ? + collections.filter(function(c) { return c.installed; }).length : 0; - if (repairResult && repairResult.success) { - ui.addNotification(null, E('p', _('LAPI auto-repaired successfully')), 'success'); - // Re-fetch status after repair - return API.getStatus().then(L.bind(function(newStatus) { - this.wizardData.crowdsecRunning = newStatus && newStatus.crowdsec === 'running'; - this.wizardData.lapiAvailable = newStatus && newStatus.lapi_status === 'available'; - return { - status: newStatus, - wizardNeeded: wizardNeeded, - consoleStatus: consoleStatus, - repaired: true - }; - }, this)); - } else { - console.log('[Wizard] LAPI repair failed:', repairResult); - return { - status: status, - wizardNeeded: wizardNeeded, - consoleStatus: consoleStatus, - repairFailed: true - }; - } - }, this)); - } + self.health.checking = false; - return { - status: status, - wizardNeeded: wizardNeeded, - consoleStatus: consoleStatus - }; - }, this)); + // 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 wizard CSS + // Load CSS var head = document.head || document.getElementsByTagName('head')[0]; var cssLink = E('link', { 'rel': 'stylesheet', @@ -145,7 +146,6 @@ return view.extend({ }); head.appendChild(cssLink); - // Load SecuBox theme CSS var themeLink = E('link', { 'rel': 'stylesheet', 'type': 'text/css', @@ -153,853 +153,398 @@ return view.extend({ }); head.appendChild(themeLink); - // Main wrapper with SecuBox header + // Main wrapper var wrapper = E('div', { 'class': 'secubox-page-wrapper' }); - - // Add SecuBox global header wrapper.appendChild(SbHeader.render()); var container = E('div', { 'class': 'crowdsec-dashboard wizard-container' }); - - // Add navigation tabs container.appendChild(CsNav.renderTabs('wizard')); - // Create stepper - container.appendChild(this.createStepper()); - - // Create step content - container.appendChild(this.renderCurrentStep(data)); + // 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; }, - createStepper: function() { - var steps = [ - { number: 1, title: _('Welcome') }, - { number: 2, title: _('Console') }, - { number: 3, title: _('Update Hub') }, - { number: 4, title: _('Log Sources') }, - { number: 5, title: _('Install Packs') }, - { number: 6, title: _('Configure Bouncer') }, - { number: 7, title: _('Enable Services') }, - { number: 8, title: _('Complete') } - ]; - - var stepper = E('div', { 'class': 'wizard-stepper' }); - - steps.forEach(L.bind(function(step) { - var classes = ['wizard-step-indicator']; - if (step.number < this.wizardData.currentStep) { - classes.push('complete'); - } else if (step.number === this.wizardData.currentStep) { - classes.push('active'); - } - - var indicator = E('div', { 'class': classes.join(' ') }, [ - E('div', { 'class': 'wizard-step-index' }, - step.number < this.wizardData.currentStep ? '✓' : step.number.toString() - ), - E('div', { 'class': 'wizard-step-title' }, step.title) - ]); - - stepper.appendChild(indicator); - }, this)); - - return stepper; - }, - - renderCurrentStep: function(data) { - switch (this.wizardData.currentStep) { - case 1: - return this.renderStep1(data); - case 2: - return this.renderStep2Console(data); - case 3: - return this.renderStep3Hub(data); - case 4: - return this.renderStep4Acquisition(data); - case 5: - return this.renderStep5Collections(data); - case 6: - return this.renderStep6Bouncer(data); - case 7: - return this.renderStep7Services(data); - case 8: - return this.renderStep8Complete(data); - default: - return E('div', {}, _('Invalid step')); - } - }, - - renderStep1: function(data) { - console.log('[Wizard] renderStep1 data:', data); - var status = data ? data.status : {}; - console.log('[Wizard] status:', status); - var crowdsecRunning = status && status.crowdsec === 'running'; - var lapiAvailable = status && status.lapi_status === 'available'; - var lapiRepairing = this.wizardData.lapiRepairing; - var repaired = data && data.repaired; - var repairFailed = data && data.repairFailed; - console.log('[Wizard] crowdsecRunning:', crowdsecRunning, 'lapiAvailable:', lapiAvailable, 'repairing:', lapiRepairing); - - // Determine LAPI status display - var lapiStatusText = lapiAvailable ? _('AVAILABLE') : (lapiRepairing ? _('REPAIRING...') : _('UNAVAILABLE')); - var lapiStatusClass = lapiAvailable ? 'success' : (lapiRepairing ? 'warning' : 'error'); - var lapiIconClass = lapiAvailable ? ' success' : (lapiRepairing ? ' warning' : ' error'); - var lapiIcon = lapiAvailable ? '✓' : (lapiRepairing ? '⟳' : '✗'); + renderHealthAndConfig: function() { + var self = this; return E('div', { 'class': 'wizard-step' }, [ - E('h2', {}, _('Welcome to CrowdSec Setup')), - E('p', {}, _('This wizard will help you set up CrowdSec security suite with firewall bouncer protection.')), + // Header + E('h2', {}, _('CrowdSec Setup Wizard')), + E('p', {}, _('Health check and configuration in one step.')), - // Status checks - E('div', { 'class': 'status-checks' }, [ - E('div', { 'class': 'check-item' }, [ - E('span', { 'class': 'check-icon' + (crowdsecRunning ? ' success' : ' error') }, - crowdsecRunning ? '✓' : '✗'), - E('span', {}, _('CrowdSec Service')), - E('span', { 'class': 'badge badge-' + (crowdsecRunning ? 'success' : 'error') }, - crowdsecRunning ? _('RUNNING') : _('STOPPED')) - ]), - E('div', { 'class': 'check-item' }, [ - E('span', { 'class': 'check-icon' + lapiIconClass + (lapiRepairing ? ' spinning' : '') }, - lapiIcon), - E('span', {}, _('Local API (LAPI)')), - E('span', { 'class': 'badge badge-' + lapiStatusClass }, - lapiStatusText) - ]) - ]), + // Health Check Section + this.renderHealthCheck(), - // Repair status message - repaired ? E('div', { 'class': 'success-message', 'style': 'margin: 16px 0; padding: 12px; background: rgba(34, 197, 94, 0.15); border-radius: 8px; color: #16a34a;' }, [ - E('span', { 'style': 'margin-right: 8px;' }, '✓'), - _('LAPI was automatically repaired!') - ]) : E([]), + // Separator + E('hr', { 'style': 'margin: 24px 0; border-color: rgba(255,255,255,0.1);' }), - // Manual repair button if auto-repair failed - (repairFailed || (!lapiAvailable && !lapiRepairing && this.wizardData.lapiRepairAttempted)) ? - E('div', { 'style': 'margin: 16px 0; padding: 16px; background: rgba(239, 68, 68, 0.1); border-radius: 8px; border: 1px solid rgba(239, 68, 68, 0.3);' }, [ - E('p', { 'style': 'margin: 0 0 12px 0; color: #dc2626;' }, - _('LAPI auto-repair failed. You can try manual repair or check the CrowdSec logs.')), - E('button', { - 'class': 'cbi-button cbi-button-action', - 'click': L.bind(this.handleManualRepair, this) - }, _('Retry Repair')) - ]) : E([]), + // Configuration Section (only if health check passed basic requirements) + this.health.crowdsecRunning ? this.renderConfigOptions() : E([]), - // Info box - E('div', { 'class': 'info-box' }, [ - E('h4', {}, _('What will be configured:')), - E('ul', {}, [ - E('li', {}, _('Enroll in CrowdSec Console for community blocklists')), - E('li', {}, _('Update CrowdSec hub with latest collections')), - E('li', {}, _('Install essential security scenarios')), - E('li', {}, _('Register and configure firewall bouncer')), - E('li', {}, _('Enable automatic IP blocking via nftables')) - ]) - ]), - - // Navigation - E('div', { 'class': 'wizard-nav' }, [ + // Apply Button + E('div', { 'class': 'wizard-nav', 'style': 'margin-top: 32px;' }, [ E('button', { 'class': 'cbi-button', - 'click': L.bind(function() { + 'click': function() { window.location.href = L.url('admin', 'secubox', 'security', 'crowdsec', 'overview'); - }, this) + } }, _('Cancel')), E('button', { 'class': 'cbi-button cbi-button-positive', - 'disabled': (!crowdsecRunning || !lapiAvailable || lapiRepairing) ? true : null, - 'click': L.bind(function(ev) { - console.log('[Wizard] Next button clicked!'); - ev.preventDefault(); - ev.stopPropagation(); - this.goToStep(2); + '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) - }, _('Next')) + }, _('Refresh')) ]) ]); }, - renderStep2Console: function(data) { - var consoleStatus = data && data.consoleStatus ? data.consoleStatus : {}; - var enrolled = consoleStatus.enrolled || this.wizardData.consoleEnrolled; - - return E('div', { 'class': 'wizard-step' }, [ - E('h2', {}, _('CrowdSec Console Enrollment')), - E('p', {}, _('Connect to CrowdSec Console to receive community blocklists and monitor your security.')), - - // Benefits - E('div', { 'class': 'info-box', 'style': 'margin-bottom: 24px;' }, [ - E('h4', {}, _('Benefits of enrolling:')), - E('ul', {}, [ - E('li', {}, _('Access to community-curated blocklists')), - E('li', {}, _('Real-time threat intelligence sharing')), - E('li', {}, _('Centralized monitoring dashboard')), - E('li', {}, _('Free tier available')) - ]) - ]), - - // Enrollment status - enrolled ? - E('div', { 'class': 'success-message', 'style': 'margin: 24px 0; padding: 16px; background: rgba(34, 197, 94, 0.15); border-radius: 8px;' }, [ - E('span', { 'class': 'check-icon success', 'style': 'font-size: 24px; margin-right: 12px;' }, '✓'), - E('div', { 'style': 'display: inline-block;' }, [ - E('strong', { 'style': 'display: block; color: #16a34a;' }, _('Already enrolled!')), - E('span', { 'style': 'color: #15803d; font-size: 0.9em;' }, - _('Your instance is connected to CrowdSec Console')) - ]) - ]) : - E('div', { 'class': 'enrollment-form', 'style': 'margin: 24px 0;' }, [ - // Enrollment key input - E('div', { 'class': 'form-group', 'style': 'margin-bottom: 16px;' }, [ - E('label', { 'style': 'display: block; margin-bottom: 8px; font-weight: 500;' }, - _('Enrollment Key')), - E('input', { - 'type': 'text', - 'id': 'console-enrollment-key', - 'class': 'cbi-input-text', - 'placeholder': _('Paste your enrollment key from console.crowdsec.net'), - 'style': 'width: 100%; padding: 12px; font-family: monospace;' - }), - E('p', { 'style': 'margin-top: 8px; color: var(--cyber-text-secondary, #666); font-size: 0.9em;' }, [ - _('Get your key from '), - E('a', { - 'href': 'https://app.crowdsec.net/security-engines', - 'target': '_blank', - 'style': 'color: var(--cyber-accent-primary, #667eea);' - }, 'app.crowdsec.net') - ]) - ]), - - // Machine name (optional) - E('div', { 'class': 'form-group', 'style': 'margin-bottom: 16px;' }, [ - E('label', { 'style': 'display: block; margin-bottom: 8px; font-weight: 500;' }, - _('Machine Name (optional)')), - E('input', { - 'type': 'text', - 'id': 'console-machine-name', - 'class': 'cbi-input-text', - 'placeholder': _('e.g., secubox-router'), - 'style': 'width: 100%; padding: 12px;' - }) - ]), - - // Enrolling status - this.wizardData.enrolling ? - E('div', { 'style': 'text-align: center; padding: 16px;' }, [ - E('div', { 'class': 'spinning' }), - E('p', {}, _('Enrolling...')) - ]) : E([]), - - // Enroll button - E('button', { - 'class': 'cbi-button cbi-button-action', - 'style': 'width: 100%; padding: 12px; font-size: 1em;', - 'disabled': this.wizardData.enrolling ? true : null, - 'click': L.bind(this.handleConsoleEnroll, this) - }, _('Enroll in CrowdSec Console')) - ]), - - // Navigation - E('div', { 'class': 'wizard-nav' }, [ - E('button', { - 'class': 'cbi-button', - 'click': L.bind(this.goToStep, this, 1) - }, _('Back')), - E('button', { - 'class': 'cbi-button', - 'click': L.bind(this.goToStep, this, 3), - 'disabled': this.wizardData.enrolling ? true : null - }, enrolled ? _('Next') : _('Skip')) - ]) - ]); - }, - - renderStep3Hub: function(data) { - return E('div', { 'class': 'wizard-step' }, [ - E('h2', {}, _('Update CrowdSec Hub')), - E('p', {}, _('Fetching the latest security collections from CrowdSec hub...')), - - E('div', { 'id': 'hub-update-status', 'class': 'status-area' }, [ - this.wizardData.hubUpdating ? - E('div', { 'class': 'spinning' }, _('Updating hub...')) : - this.wizardData.hubUpdated ? - E('div', { 'class': 'success-message' }, [ - E('span', { 'class': 'check-icon success' }, '✓'), - _('Hub updated successfully!') - ]) : - E('div', {}, _('Ready to update hub')) - ]), - - // Navigation - E('div', { 'class': 'wizard-nav' }, [ - E('button', { - 'class': 'cbi-button', - 'click': L.bind(this.goToStep, this, 2) - }, _('Back')), - this.wizardData.hubUpdated ? - E('button', { - 'class': 'cbi-button cbi-button-positive', - 'click': L.bind(this.goToStep, this, 4) - }, _('Next')) : - E('button', { - 'class': 'cbi-button cbi-button-action', - 'click': L.bind(this.handleUpdateHub, this) - }, _('Update Hub')) - ]) - ]); - }, - - renderStep4Acquisition: function(data) { + renderConfigOptions: function() { var self = this; - return E('div', { 'class': 'wizard-step' }, [ - E('h2', {}, _('Configure Log Acquisition')), - E('p', {}, _('Select which log sources CrowdSec should monitor for security threats.')), - // Info box about log acquisition - E('div', { 'class': 'info-box', 'style': 'margin-bottom: 24px;' }, [ - E('h4', {}, _('About Log Acquisition')), - E('p', { 'style': 'margin: 0; font-size: 0.9em; color: var(--cyber-text-secondary, #94a3b8);' }, - _('CrowdSec analyzes logs to detect malicious activity. Enable the log sources relevant to your setup.')) + 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') ]), - // Log source toggles - E('div', { 'class': 'config-section' }, [ - // Syslog - E('div', { - 'class': 'config-group', - 'id': 'acq-syslog', - 'data-checked': this.wizardData.syslogEnabled ? '1' : '0', - 'style': 'display: flex; align-items: center; cursor: pointer; padding: 12px; background: rgba(15, 23, 42, 0.5); border-radius: 8px; margin-bottom: 12px;', - 'click': function(ev) { - var item = ev.currentTarget; - var currentState = item.getAttribute('data-checked') === '1'; - var newState = !currentState; - item.setAttribute('data-checked', newState ? '1' : '0'); - self.wizardData.syslogEnabled = newState; - var checkbox = item.querySelector('.checkbox-indicator'); - if (checkbox) { - checkbox.textContent = newState ? '☑' : '☐'; - checkbox.style.color = newState ? '#22c55e' : '#94a3b8'; - } - } - }, [ - E('span', { - 'class': 'checkbox-indicator', - 'style': 'display: inline-block; font-size: 24px; margin-right: 12px; user-select: none; color: ' + (this.wizardData.syslogEnabled ? '#22c55e' : '#94a3b8') + '; min-width: 24px;' - }, this.wizardData.syslogEnabled ? '☑' : '☐'), - E('div', { 'style': 'flex: 1;' }, [ - E('strong', {}, _('System Syslog')), - E('div', { 'style': 'font-size: 0.85em; color: var(--cyber-text-secondary, #94a3b8);' }, - _('Monitor /var/log/messages for system events')) - ]) - ]), + // 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;' }, [ + _('Note: Hub update requires CAPI connection.') + ]) + ]) : E([]), - // Firewall logs - E('div', { - 'class': 'config-group', - 'id': 'acq-firewall', - 'data-checked': this.wizardData.firewallEnabled ? '1' : '0', - 'style': 'display: flex; align-items: center; cursor: pointer; padding: 12px; background: rgba(15, 23, 42, 0.5); border-radius: 8px; margin-bottom: 12px;', - 'click': function(ev) { - var item = ev.currentTarget; - var currentState = item.getAttribute('data-checked') === '1'; - var newState = !currentState; - item.setAttribute('data-checked', newState ? '1' : '0'); - self.wizardData.firewallEnabled = newState; - var checkbox = item.querySelector('.checkbox-indicator'); - if (checkbox) { - checkbox.textContent = newState ? '☑' : '☐'; - checkbox.style.color = newState ? '#22c55e' : '#94a3b8'; - } - } - }, [ - E('span', { - 'class': 'checkbox-indicator', - 'style': 'display: inline-block; font-size: 24px; margin-right: 12px; user-select: none; color: ' + (this.wizardData.firewallEnabled ? '#22c55e' : '#94a3b8') + '; min-width: 24px;' - }, this.wizardData.firewallEnabled ? '☑' : '☐'), - E('div', { 'style': 'flex: 1;' }, [ - E('strong', {}, _('Firewall Logs')), - E('div', { 'style': 'font-size: 0.85em; color: var(--cyber-text-secondary, #94a3b8);' }, - _('Monitor iptables/nftables for port scans (requires iptables collection)')) - ]) - ]), + // 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) + ]), - // SSH/Dropbear logs - E('div', { - 'class': 'config-group', - 'id': 'acq-ssh', - 'data-checked': this.wizardData.sshEnabled ? '1' : '0', - 'style': 'display: flex; align-items: center; cursor: pointer; padding: 12px; background: rgba(15, 23, 42, 0.5); border-radius: 8px; margin-bottom: 12px;', - 'click': function(ev) { - var item = ev.currentTarget; - var currentState = item.getAttribute('data-checked') === '1'; - var newState = !currentState; - item.setAttribute('data-checked', newState ? '1' : '0'); - self.wizardData.sshEnabled = newState; - var checkbox = item.querySelector('.checkbox-indicator'); - if (checkbox) { - checkbox.textContent = newState ? '☑' : '☐'; - checkbox.style.color = newState ? '#22c55e' : '#94a3b8'; - } - } - }, [ - E('span', { - 'class': 'checkbox-indicator', - 'style': 'display: inline-block; font-size: 24px; margin-right: 12px; user-select: none; color: ' + (this.wizardData.sshEnabled ? '#22c55e' : '#94a3b8') + '; min-width: 24px;' - }, this.wizardData.sshEnabled ? '☑' : '☐'), - E('div', { 'style': 'flex: 1;' }, [ - E('strong', {}, _('SSH/Dropbear Logs')), - E('div', { 'style': 'font-size: 0.85em; color: var(--cyber-text-secondary, #94a3b8);' }, - _('Detect SSH brute force attacks (via syslog)')) - ]) + // 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') + ]), - // HTTP logs - E('div', { - 'class': 'config-group', - 'id': 'acq-http', - 'data-checked': this.wizardData.httpEnabled ? '1' : '0', - 'style': 'display: flex; align-items: center; cursor: pointer; padding: 12px; background: rgba(15, 23, 42, 0.5); border-radius: 8px; margin-bottom: 12px;', - 'click': function(ev) { - var item = ev.currentTarget; - var currentState = item.getAttribute('data-checked') === '1'; - var newState = !currentState; - item.setAttribute('data-checked', newState ? '1' : '0'); - self.wizardData.httpEnabled = newState; - var checkbox = item.querySelector('.checkbox-indicator'); - if (checkbox) { - checkbox.textContent = newState ? '☑' : '☐'; - checkbox.style.color = newState ? '#22c55e' : '#94a3b8'; - } - } - }, [ - E('span', { - 'class': 'checkbox-indicator', - 'style': 'display: inline-block; font-size: 24px; margin-right: 12px; user-select: none; color: ' + (this.wizardData.httpEnabled ? '#22c55e' : '#94a3b8') + '; min-width: 24px;' - }, this.wizardData.httpEnabled ? '☑' : '☐'), - E('div', { 'style': 'flex: 1;' }, [ - E('strong', {}, _('HTTP Server Logs')), - E('div', { 'style': 'font-size: 0.85em; color: var(--cyber-text-secondary, #94a3b8);' }, - _('Monitor uHTTPd/nginx web server (disabled by default)')) - ]) - ]), - - // Note about OpenWrt log handling - E('div', { 'class': 'info-box', 'style': 'margin-top: 16px; padding: 12px; background: rgba(102, 126, 234, 0.1); border-radius: 8px; border: 1px solid rgba(102, 126, 234, 0.3);' }, [ - E('p', { 'style': 'margin: 0; font-size: 0.9em; color: var(--cyber-text-secondary, #94a3b8);' }, [ - E('strong', { 'style': 'color: var(--cyber-accent-primary, #667eea);' }, _('Note: ')), - _('OpenWrt uses logread command instead of log files. CrowdSec will stream logs via "logread -f". All enabled sources (syslog, SSH, firewall) share the same log stream.') + // 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')) ]) ]) - ]), - - // Configuration status - this.wizardData.acquisitionConfigured ? - E('div', { 'class': 'success-message', 'style': 'margin-top: 16px;' }, [ - E('span', { 'class': 'check-icon success' }, '✓'), - _('Log acquisition configured successfully!') - ]) : - this.wizardData.acquisitionConfiguring ? - E('div', { 'class': 'spinning', 'style': 'margin-top: 16px;' }, _('Configuring acquisition...')) : - E([]), - - // Navigation - E('div', { 'class': 'wizard-nav' }, [ - E('button', { - 'class': 'cbi-button', - 'click': L.bind(this.goToStep, this, 3), - 'disabled': this.wizardData.acquisitionConfiguring ? true : null - }, _('Back')), - this.wizardData.acquisitionConfigured ? - E('button', { - 'class': 'cbi-button cbi-button-positive', - 'click': L.bind(this.goToStep, this, 5) - }, _('Next')) : - E('button', { - 'class': 'cbi-button cbi-button-action', - 'click': L.bind(this.handleConfigureAcquisition, this), - 'disabled': this.wizardData.acquisitionConfiguring ? true : null - }, _('Apply Configuration')) ]) ]); }, - renderStep5Collections: function(data) { - var recommendedCollections = [ - { name: 'crowdsecurity/linux', description: 'Base Linux scenarios (SSH, syslog)', preselected: true }, - { name: 'crowdsecurity/iptables', description: 'Firewall log parser (port scan detection)', preselected: this.wizardData.firewallEnabled }, - { name: 'crowdsecurity/ssh-bf', description: 'SSH brute force protection', preselected: true }, - { name: 'crowdsecurity/http-cve', description: 'Web CVE protection', preselected: this.wizardData.httpEnabled }, - { name: 'crowdsecurity/whitelist-good-actors', description: 'Whitelist known good bots', preselected: false } - ]; + 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'); - // OpenWrt-specific parsers for log sources - var recommendedParsers = [ - { name: 'crowdsecurity/dropbear-logs', description: 'Dropbear SSH daemon logs (OpenWrt)', preselected: this.wizardData.sshEnabled }, - { name: 'crowdsecurity/syslog-logs', description: 'Generic syslog parser', preselected: this.wizardData.syslogEnabled } - ]; - - var renderCheckboxItem = function(item, type) { - var attrs = { - 'class': 'collection-item', - 'data-type': type, - 'data-name': item.name, - 'data-checked': item.preselected ? '1' : '0', - 'style': 'display: flex; align-items: center; cursor: pointer;', - 'click': function(ev) { - var el = ev.currentTarget; - var currentState = el.getAttribute('data-checked') === '1'; - var newState = !currentState; - el.setAttribute('data-checked', newState ? '1' : '0'); - var checkbox = el.querySelector('.checkbox-indicator'); - if (checkbox) { - checkbox.textContent = newState ? '☑' : '☐'; - checkbox.style.color = newState ? '#22c55e' : '#94a3b8'; - } + var checkbox = el.querySelector('.toggle-check'); + if (checkbox) { + checkbox.textContent = newState ? '☑' : '☐'; + checkbox.style.color = newState ? '#22c55e' : '#64748b'; } - }; - return E('div', attrs, [ - E('span', { - 'class': 'checkbox-indicator', - 'style': 'display: inline-block; font-size: 28px; margin-right: 16px; user-select: none; color: ' + (item.preselected ? '#22c55e' : '#94a3b8') + '; line-height: 1; min-width: 28px;' - }, item.preselected ? '☑' : '☐'), - E('div', { 'class': 'collection-info', 'style': 'flex: 1;' }, [ - E('strong', {}, item.name), - E('div', { 'class': 'collection-desc' }, item.description) - ]) - ]); - }; - return E('div', { 'class': 'wizard-step' }, [ - E('h2', {}, _('Install Security Collections & Parsers')), - E('p', {}, _('Select collections and parsers to install. Recommended items are pre-selected based on your log configuration.')), - - // Collections section - E('h3', { 'style': 'margin-top: 20px; margin-bottom: 12px; font-size: 1.1em; color: #94a3b8;' }, [ - E('span', { 'style': 'margin-right: 8px;' }, '📦'), - _('Collections') - ]), - E('div', { 'class': 'collections-list' }, - recommendedCollections.map(function(c) { return renderCheckboxItem(c, 'collection'); }) - ), - - // Parsers section - E('h3', { 'style': 'margin-top: 24px; margin-bottom: 12px; font-size: 1.1em; color: #94a3b8;' }, [ - E('span', { 'style': 'margin-right: 8px;' }, '📝'), - _('OpenWrt Parsers') - ]), - E('div', { 'class': 'collections-list' }, - recommendedParsers.map(function(p) { return renderCheckboxItem(p, 'parser'); }) - ), - - // Install progress - this.wizardData.installing ? - E('div', { 'class': 'install-progress' }, [ - E('div', { 'class': 'spinning' }), - E('p', {}, _('Installing...')), - E('div', { 'id': 'install-status' }, this.wizardData.installStatus || '') - ]) : E([]), - - // Navigation - E('div', { 'class': 'wizard-nav' }, [ - E('button', { - 'class': 'cbi-button', - 'click': L.bind(this.goToStep, this, 4), - 'disabled': this.wizardData.installing ? true : null - }, _('Back')), - E('button', { - 'class': 'cbi-button', - 'click': L.bind(this.goToStep, this, 6), - 'disabled': this.wizardData.installing ? true : null - }, _('Skip')), - E('button', { - 'class': 'cbi-button cbi-button-positive', - 'click': L.bind(this.handleInstallCollections, this), - 'disabled': (this.wizardData.installing || this.wizardData.installed) ? true : null - }, this.wizardData.installed ? _('Installed') : _('Install Selected')) + // 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) ]) ]); }, - renderStep6Bouncer: function(data) { + renderCollectionToggle: function(name, description, checked, type) { var self = this; - return E('div', { 'class': 'wizard-step' }, [ - E('h2', {}, _('Configure Firewall Bouncer')), - E('p', {}, _('The firewall bouncer will automatically block malicious IPs using nftables.')), + type = type || 'collection'; - // Recovery mode warning/option - E('div', { 'id': 'recovery-mode-section', 'class': 'config-section', 'style': 'margin-bottom: 20px; padding: 16px; background: rgba(234, 179, 8, 0.1); border: 1px solid rgba(234, 179, 8, 0.3); border-radius: 8px;' }, [ - E('div', { 'style': 'display: flex; align-items: center; margin-bottom: 12px;' }, [ - E('span', { 'style': 'font-size: 24px; margin-right: 12px;' }, '🔄'), - E('div', {}, [ - E('strong', { 'style': 'color: #eab308;' }, _('Recovery Mode')), - E('div', { 'style': 'font-size: 0.9em; color: var(--cyber-text-secondary, #94a3b8);' }, - _('Use this if bouncer registration fails or you want to start fresh')) - ]) - ]), - E('button', { - 'class': 'cbi-button cbi-button-negative', - 'style': 'width: 100%;', - 'disabled': this.wizardData.resetting ? true : null, - 'click': L.bind(this.handleResetWizard, this) - }, this.wizardData.resetting ? _('Resetting...') : _('Reset Bouncer Configuration')) - ]), + 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'); - // Configuration options - E('div', { 'class': 'config-section' }, [ - E('div', { - 'class': 'config-group', - 'id': 'bouncer-ipv4', - 'data-checked': '1', - 'style': 'display: flex; align-items: center; cursor: pointer; padding: 12px; background: rgba(15, 23, 42, 0.5); border-radius: 8px; margin-bottom: 12px;', - 'click': function(ev) { - var item = ev.currentTarget; - var currentState = item.getAttribute('data-checked') === '1'; - var newState = !currentState; - item.setAttribute('data-checked', newState ? '1' : '0'); - var checkbox = item.querySelector('.checkbox-indicator'); - if (checkbox) { - checkbox.textContent = newState ? '☑' : '☐'; - checkbox.style.color = newState ? '#22c55e' : '#94a3b8'; - } - } - }, [ - E('span', { - 'class': 'checkbox-indicator', - 'style': 'display: inline-block; font-size: 24px; margin-right: 12px; user-select: none; color: #22c55e; min-width: 24px;' - }, '☑'), - E('span', {}, _('Enable IPv4 blocking')) - ]), - E('div', { - 'class': 'config-group', - 'id': 'bouncer-ipv6', - 'data-checked': '1', - 'style': 'display: flex; align-items: center; cursor: pointer; padding: 12px; background: rgba(15, 23, 42, 0.5); border-radius: 8px; margin-bottom: 12px;', - 'click': function(ev) { - var item = ev.currentTarget; - var currentState = item.getAttribute('data-checked') === '1'; - var newState = !currentState; - item.setAttribute('data-checked', newState ? '1' : '0'); - var checkbox = item.querySelector('.checkbox-indicator'); - if (checkbox) { - checkbox.textContent = newState ? '☑' : '☐'; - checkbox.style.color = newState ? '#22c55e' : '#94a3b8'; - } - } - }, [ - E('span', { - 'class': 'checkbox-indicator', - 'style': 'display: inline-block; font-size: 24px; margin-right: 12px; user-select: none; color: #22c55e; min-width: 24px;' - }, '☑'), - E('span', {}, _('Enable IPv6 blocking')) - ]), - E('div', { 'class': 'config-group' }, [ - E('label', {}, _('Update Frequency:')), - E('select', { 'id': 'bouncer-frequency', 'class': 'cbi-input-select' }, [ - E('option', { 'value': '10s', 'selected': true }, _('10 seconds (recommended)')), - E('option', { 'value': '30s' }, _('30 seconds')), - E('option', { 'value': '1m' }, _('1 minute')) - ]) - ]) - ]), - - // Registration status - this.wizardData.bouncerConfigured ? - E('div', { 'class': 'success-message' }, [ - E('span', { 'class': 'check-icon success' }, '✓'), - _('Firewall bouncer configured successfully!') - ]) : - this.wizardData.configuring ? - E('div', { 'class': 'spinning' }, _('Configuring bouncer...')) : - E([]), - - // API key display (if registered) - this.wizardData.apiKey ? - E('div', { 'class': 'api-key-display' }, [ - E('strong', {}, _('API Key generated:')), - E('code', { 'id': 'bouncer-api-key' }, this.wizardData.apiKey), - E('button', { - 'class': 'cbi-button cbi-button-action', - 'click': function() { - var key = document.getElementById('bouncer-api-key').textContent; - navigator.clipboard.writeText(key); - ui.addNotification(null, E('p', _('API key copied')), 'info'); - } - }, _('Copy')) - ]) : E([]), - - // Navigation - E('div', { 'class': 'wizard-nav' }, [ - E('button', { - 'class': 'cbi-button', - 'click': L.bind(this.goToStep, this, 5), - 'disabled': this.wizardData.configuring ? true : null - }, _('Back')), - this.wizardData.bouncerConfigured ? - E('button', { - 'class': 'cbi-button cbi-button-positive', - 'click': L.bind(this.goToStep, this, 7) - }, _('Next')) : - E('button', { - 'class': 'cbi-button cbi-button-action', - 'click': L.bind(this.handleConfigureBouncer, this), - 'disabled': this.wizardData.configuring ? true : null - }, _('Configure Bouncer')) + 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) ]) ]); }, - renderStep7Services: function(data) { - return E('div', { 'class': 'wizard-step' }, [ - E('h2', {}, _('Enable & Start Services')), - E('p', {}, _('Starting the firewall bouncer service and verifying operation...')), + renderApplying: function() { + var progressPercent = Math.round(this.config.applyProgress); - // Service startup progress - E('div', { 'class': 'service-status' }, [ - E('div', { 'class': 'status-item' }, [ - E('span', { 'class': 'status-label' }, _('Enable at boot:')), - E('span', { 'class': 'status-value' + (this.wizardData.enabled ? ' success' : '') }, - this.wizardData.enabled ? _('Enabled') : this.wizardData.enabling ? _('Enabling...') : _('Not enabled')) + 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', { 'class': 'status-item' }, [ - E('span', { 'class': 'status-label' }, _('Service status:')), - E('span', { 'class': 'status-value' + (this.wizardData.running ? ' success' : '') }, - this.wizardData.running ? _('Running') : this.wizardData.starting ? _('Starting...') : _('Stopped')) - ]), - E('div', { 'class': 'status-item' }, [ - E('span', { 'class': 'status-label' }, _('nftables rules:')), - E('span', { 'class': 'status-value' + (this.wizardData.nftablesActive ? ' success' : '') }, - this.wizardData.nftablesActive ? _('Loaded') : _('Not loaded')) - ]), - E('div', { 'class': 'status-item' }, [ - E('span', { 'class': 'status-label' }, _('LAPI connection:')), - E('span', { 'class': 'status-value' + (this.wizardData.lapiConnected ? ' success' : '') }, - this.wizardData.lapiConnected ? _('Connected') : _('Not connected')) - ]) + E('div', { 'style': 'margin-top: 8px; font-size: 0.9em; color: #64748b;' }, progressPercent + '%') ]), - // Navigation - E('div', { 'class': 'wizard-nav' }, [ - E('button', { - 'class': 'cbi-button', - 'click': L.bind(this.goToStep, this, 6), - 'disabled': this.wizardData.starting ? true : null - }, _('Back')), - (this.wizardData.enabled && this.wizardData.running && this.wizardData.nftablesActive && this.wizardData.lapiConnected) ? - E('button', { - 'class': 'cbi-button cbi-button-positive', - 'click': L.bind(this.goToStep, this, 8) - }, _('Next')) : - E('button', { - 'class': 'cbi-button cbi-button-action', - 'click': L.bind(this.handleStartServices, this), - 'disabled': this.wizardData.starting ? true : null - }, _('Start Services')) - ]) + // 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([]) ]); }, - renderStep8Complete: function(data) { - return E('div', { 'class': 'wizard-step wizard-complete' }, [ - E('div', { 'class': 'success-hero' }, [ - E('div', { 'class': 'success-icon' }, '🎉'), + 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', { 'class': 'text-center' }, _('CrowdSec is now protecting your network.')), + E('p', { 'style': 'color: #94a3b8; margin-bottom: 32px;' }, + _('CrowdSec is now protecting your network.')), - // Summary - E('div', { 'class': 'summary-grid' }, [ - E('div', { 'class': 'summary-item' }, [ - E('span', { 'class': 'check-icon success' }, '✓'), - E('div', {}, [ - E('strong', {}, _('CrowdSec Service')), - E('div', { 'class': 'summary-desc' }, _('Running and monitoring')) - ]) - ]), - this.wizardData.consoleEnrolled ? - E('div', { 'class': 'summary-item' }, [ - E('span', { 'class': 'check-icon success' }, '✓'), - E('div', {}, [ - E('strong', {}, _('Console Enrolled')), - E('div', { 'class': 'summary-desc' }, _('Receiving community blocklists')) - ]) - ]) : E([]), - E('div', { 'class': 'summary-item' }, [ - E('span', { 'class': 'check-icon success' }, '✓'), - E('div', {}, [ - E('strong', {}, _('Hub Updated')), - E('div', { 'class': 'summary-desc' }, _('Latest collections available')) - ]) - ]), - E('div', { 'class': 'summary-item' }, [ - E('span', { 'class': 'check-icon success' }, '✓'), - E('div', {}, [ - E('strong', {}, _('Collections Installed')), - E('div', { 'class': 'summary-desc' }, - _('%d security packs active').format(this.wizardData.installedCount || 0)) - ]) - ]), - E('div', { 'class': 'summary-item' }, [ - E('span', { 'class': 'check-icon success' }, '✓'), - E('div', {}, [ - E('strong', {}, _('Firewall Bouncer')), - E('div', { 'class': 'summary-desc' }, _('Blocking malicious IPs')) - ]) - ]), - E('div', { 'class': 'summary-item' }, [ - E('span', { 'class': 'check-icon success' }, '✓'), - E('div', {}, [ - E('strong', {}, _('nftables Rules')), - E('div', { 'class': 'summary-desc' }, _('IPv4 and IPv6 protection active')) - ]) - ]) + // 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([]) ]), - // Current stats - E('div', { 'class': 'stats-box' }, [ - E('h4', {}, _('Current Status')), - E('div', { 'class': 'stats-row' }, [ - E('div', { 'class': 'stat' }, [ - E('div', { 'class': 'stat-value' }, (this.wizardData.blockedIPs || 0).toString()), - E('div', { 'class': 'stat-label' }, _('IPs Blocked')) - ]), - E('div', { 'class': 'stat' }, [ - E('div', { 'class': 'stat-value' }, (this.wizardData.activeDecisions || 0).toString()), - E('div', { 'class': 'stat-label' }, _('Active Decisions')) - ]), - E('div', { 'class': 'stat' }, [ - E('div', { 'class': 'stat-value' }, (this.wizardData.installedCount || 0).toString()), - E('div', { 'class': 'stat-label' }, _('Scenarios')) - ]) - ]) - ]), - - // Next steps - E('div', { 'class': 'info-box' }, [ - E('h4', {}, _('Next Steps')), - E('ul', {}, [ - E('li', {}, _('View real-time decisions in the Decisions tab')), - E('li', {}, _('Monitor alerts in the Alerts tab')), - E('li', {}, _('Check blocked IPs in the Bouncers tab')), - this.wizardData.consoleEnrolled ? - E('li', {}, [ - _('Monitor from '), - E('a', { 'href': 'https://app.crowdsec.net', 'target': '_blank' }, 'CrowdSec Console') - ]) : - E('li', {}, _('Consider enrolling in CrowdSec Console for blocklists')) - ]) - ]), + // 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: 16px; padding: 12px 24px;', + 'style': 'font-size: 1.1em; padding: 12px 32px;', 'click': function() { window.location.href = L.url('admin', 'secubox', 'security', 'crowdsec', 'overview'); } @@ -1008,415 +553,219 @@ return view.extend({ ]); }, - goToStep: function(stepNumber) { - this.wizardData.currentStep = stepNumber; + 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 }; + }); + }); + + 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 }; + }); + + default: + return Promise.resolve({ success: true }); + } }, refreshView: function() { var container = document.querySelector('.wizard-container'); - if (container) { - // Update stepper - var stepper = this.createStepper(); - container.replaceChild(stepper, container.firstChild); + if (!container) return; - // Update step content - this.load().then(L.bind(function(data) { - var stepContent = this.renderCurrentStep(data); - container.replaceChild(stepContent, container.lastChild); - }, this)); + // 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()); } }, - handleConsoleEnroll: function() { - var keyInput = document.getElementById('console-enrollment-key'); - var nameInput = document.getElementById('console-machine-name'); - var key = keyInput ? keyInput.value.trim() : ''; - var name = nameInput ? nameInput.value.trim() : ''; - - if (!key) { - ui.addNotification(null, E('p', _('Please enter an enrollment key')), 'warning'); - return; - } - - console.log('[Wizard] Enrolling with key:', key.substring(0, 10) + '...'); - this.wizardData.enrolling = true; - this.refreshView(); - - return API.consoleEnroll(key, name).then(L.bind(function(result) { - console.log('[Wizard] Enrollment result:', result); - this.wizardData.enrolling = false; - - if (result && result.success) { - this.wizardData.consoleEnrolled = true; - ui.addNotification(null, E('p', _('Successfully enrolled in CrowdSec Console!')), 'success'); - // Auto-advance after 2 seconds - setTimeout(L.bind(function() { this.goToStep(3); }, this), 2000); - } else { - ui.addNotification(null, E('p', _('Enrollment failed: ') + (result.error || result.output || 'Unknown error')), 'error'); - } - this.refreshView(); - }, this)).catch(L.bind(function(err) { - console.error('[Wizard] Enrollment error:', err); - this.wizardData.enrolling = false; - ui.addNotification(null, E('p', _('Enrollment failed: ') + err.message), 'error'); - this.refreshView(); - }, this)); - }, - - handleUpdateHub: function() { - console.log('[Wizard] handleUpdateHub called'); - this.wizardData.hubUpdating = true; - this.refreshView(); - - return API.updateHub().then(L.bind(function(result) { - console.log('[Wizard] updateHub result:', result); - this.wizardData.hubUpdating = false; - this.wizardData.hubUpdated = result.success; - - if (result.success) { - ui.addNotification(null, E('p', _('Hub updated successfully')), 'info'); - return API.getCollections(); - } else { - ui.addNotification(null, E('p', result.error || _('Hub update failed')), 'error'); - return null; - } - }, this)).then(L.bind(function(collections) { - console.log('[Wizard] getCollections result:', collections); - if (collections) { - this.wizardData.collections = collections; - } - this.refreshView(); - }, this)).catch(L.bind(function(error) { - console.error('[Wizard] Hub update error:', error); - this.wizardData.hubUpdating = false; - this.wizardData.hubUpdated = false; - ui.addNotification(null, E('p', _('Hub update failed: ') + error.message), 'error'); - this.refreshView(); - }, this)); - }, - - handleConfigureAcquisition: function() { - console.log('[Wizard] handleConfigureAcquisition called'); - this.wizardData.acquisitionConfiguring = true; - - // Update button state without full refresh (which would abort the XHR) - var btn = document.querySelector('.cbi-button-action'); - if (btn) { - btn.disabled = true; - btn.textContent = _('Configuring...'); - } - - // Get values from wizard data - var syslogEnabled = this.wizardData.syslogEnabled ? '1' : '0'; - var firewallEnabled = this.wizardData.firewallEnabled ? '1' : '0'; - var sshEnabled = this.wizardData.sshEnabled ? '1' : '0'; - var httpEnabled = this.wizardData.httpEnabled ? '1' : '0'; - var syslogPath = this.wizardData.syslogPath || '/var/log/messages'; - - console.log('[Wizard] Acquisition config:', { - syslog: syslogEnabled, - firewall: firewallEnabled, - ssh: sshEnabled, - http: httpEnabled, - path: syslogPath - }); - - return API.configureAcquisition(syslogEnabled, firewallEnabled, sshEnabled, httpEnabled, syslogPath) - .then(L.bind(function(result) { - console.log('[Wizard] configureAcquisition result:', result); - this.wizardData.acquisitionConfiguring = false; - - if (result && result.success) { - this.wizardData.acquisitionConfigured = true; - ui.addNotification(null, E('p', _('Log acquisition configured successfully')), 'info'); - this.refreshView(); - - // Auto-advance to Step 5 (Collections) after 2 seconds - console.log('[Wizard] Auto-advancing to Step 5 in 2 seconds...'); - setTimeout(L.bind(function() { this.goToStep(5); }, this), 2000); - } else { - ui.addNotification(null, E('p', _('Configuration failed: ') + (result.error || 'Unknown error')), 'error'); - this.refreshView(); - } - }, this)) - .catch(L.bind(function(err) { - console.log('[Wizard] Acquisition configuration catch:', err); - this.wizardData.acquisitionConfiguring = false; - - // XHR abort during CrowdSec restart is expected - treat as success - if (err && err.message && err.message.indexOf('abort') !== -1) { - console.log('[Wizard] XHR aborted (CrowdSec restart) - treating as success'); - this.wizardData.acquisitionConfigured = true; - ui.addNotification(null, E('p', _('Log acquisition configured (service restarted)')), 'info'); - // Auto-advance to Step 5 after brief delay - setTimeout(L.bind(function() { this.goToStep(5); }, this), 1500); - } else { - console.error('[Wizard] Acquisition configuration error:', err); - ui.addNotification(null, E('p', _('Configuration failed: ') + err.message), 'error'); - this.refreshView(); - } - }, this)); - }, - - handleInstallCollections: function() { - // Read collections from data-checked attributes - var collectionItems = document.querySelectorAll('.collection-item[data-type="collection"]'); - var selectedCollections = Array.from(collectionItems) - .filter(function(item) { return item.getAttribute('data-checked') === '1'; }) - .map(function(item) { return item.getAttribute('data-name'); }); - - // Read parsers from data-checked attributes - var parserItems = document.querySelectorAll('.collection-item[data-type="parser"]'); - var selectedParsers = Array.from(parserItems) - .filter(function(item) { return item.getAttribute('data-checked') === '1'; }) - .map(function(item) { return item.getAttribute('data-name'); }); - - console.log('[Wizard] Selected collections:', selectedCollections); - console.log('[Wizard] Selected parsers:', selectedParsers); - - var totalItems = selectedCollections.length + selectedParsers.length; - - if (totalItems === 0) { - this.goToStep(6); - return; - } - - this.wizardData.installing = true; - this.wizardData.installStatus = _('Installing 0 of %d items...').format(totalItems); - this.refreshView(); - - var currentIndex = 0; - var self = this; - - // Install collections sequentially - var installPromises = selectedCollections.reduce(function(promise, collection) { - return promise.then(function() { - currentIndex++; - self.wizardData.installStatus = _('Installing %d of %d: %s').format(currentIndex, totalItems, collection); - self.refreshView(); - return API.installCollection(collection); - }); - }, Promise.resolve()); - - // Then install parsers sequentially - installPromises = selectedParsers.reduce(function(promise, parser) { - return promise.then(function() { - currentIndex++; - self.wizardData.installStatus = _('Installing %d of %d: %s').format(currentIndex, totalItems, parser); - self.refreshView(); - return API.installHubItem('parser', parser); - }); - }, installPromises); - - return installPromises.then(L.bind(function() { - this.wizardData.installing = false; - this.wizardData.installed = true; - this.wizardData.installedCount = totalItems; - ui.addNotification(null, E('p', _('Installed %d collections and %d parsers').format(selectedCollections.length, selectedParsers.length)), 'info'); - this.refreshView(); - - // Auto-advance to Step 6 (Configure Bouncer) after 2 seconds - setTimeout(L.bind(function() { this.goToStep(6); }, this), 2000); - }, this)).catch(L.bind(function(err) { - this.wizardData.installing = false; - ui.addNotification(null, E('p', _('Installation failed: %s').format(err.message)), 'error'); - this.refreshView(); - }, this)); - }, - - handleConfigureBouncer: function() { - console.log('[Wizard] handleConfigureBouncer called'); - this.wizardData.configuring = true; - this.refreshView(); - - var ipv4Elem = document.getElementById('bouncer-ipv4'); - var ipv6Elem = document.getElementById('bouncer-ipv6'); - var ipv4 = ipv4Elem ? ipv4Elem.getAttribute('data-checked') === '1' : true; - var ipv6 = ipv6Elem ? ipv6Elem.getAttribute('data-checked') === '1' : true; - var frequency = document.getElementById('bouncer-frequency').value; - - console.log('[Wizard] Bouncer config:', { ipv4: ipv4, ipv6: ipv6, frequency: frequency }); - - // Step 1: Register bouncer - console.log('[Wizard] Registering bouncer...'); - return API.registerBouncer('crowdsec-firewall-bouncer').then(L.bind(function(result) { - console.log('[Wizard] registerBouncer result:', result); - if (!result.success) { - throw new Error(result.error || 'Bouncer registration failed'); - } - - this.wizardData.apiKey = result.api_key; - - // Step 2: Configure UCI settings - var configPromises = [ - API.updateFirewallBouncerConfig('enabled', '1'), - API.updateFirewallBouncerConfig('ipv4', ipv4 ? '1' : '0'), - API.updateFirewallBouncerConfig('ipv6', ipv6 ? '1' : '0'), - API.updateFirewallBouncerConfig('update_frequency', frequency), - API.updateFirewallBouncerConfig('api_key', result.api_key) - ]; - - return Promise.all(configPromises); - }, this)).then(L.bind(function(results) { - console.log('[Wizard] UCI config results:', results); - this.wizardData.configuring = false; - this.wizardData.bouncerConfigured = true; - ui.addNotification(null, E('p', _('Bouncer configured successfully')), 'info'); - this.refreshView(); - - // Auto-advance to Step 7 (Enable Services) after 2 seconds - console.log('[Wizard] Auto-advancing to Step 7 in 2 seconds...'); - setTimeout(L.bind(function() { this.goToStep(7); }, this), 2000); - }, this)).catch(L.bind(function(err) { - console.error('[Wizard] Configuration error:', err); - this.wizardData.configuring = false; - ui.addNotification(null, E('p', _('Configuration failed: %s').format(err.message)), 'error'); - this.refreshView(); - }, this)); - }, - - handleStartServices: function() { - console.log('[Wizard] handleStartServices called'); - this.wizardData.starting = true; - this.wizardData.enabling = true; - this.refreshView(); - - // Step 1: Enable service - console.log('[Wizard] Enabling firewall bouncer...'); - return API.controlFirewallBouncer('enable').then(L.bind(function(result) { - console.log('[Wizard] Enable result:', result); - this.wizardData.enabling = false; - this.wizardData.enabled = result.success; - this.refreshView(); - - // Step 2: Start service - console.log('[Wizard] Starting firewall bouncer...'); - return API.controlFirewallBouncer('start'); - }, this)).then(L.bind(function(result) { - console.log('[Wizard] Start result:', result); - this.wizardData.running = result.success; - this.refreshView(); - - // Step 3: Wait 3 seconds for service to initialize - console.log('[Wizard] Waiting 3 seconds for service initialization...'); - return new Promise(function(resolve) { setTimeout(resolve, 3000); }); - }, this)).then(L.bind(function() { - // Step 4: Check status - console.log('[Wizard] Checking firewall bouncer status...'); - return API.getFirewallBouncerStatus(); - }, this)).then(L.bind(function(status) { - console.log('[Wizard] Bouncer status:', status); - this.wizardData.nftablesActive = status.nftables_ipv4 || status.nftables_ipv6; - this.wizardData.starting = false; - - // Step 5: Verify LAPI connection (check if bouncer pulled decisions) - console.log('[Wizard] Checking LAPI connection...'); - return API.getBouncers(); - }, this)).then(L.bind(function(bouncers) { - console.log('[Wizard] Bouncers list:', bouncers); - var bouncerList = bouncers && bouncers.bouncers ? bouncers.bouncers : bouncers; - var bouncer = (bouncerList || []).find(function(b) { - return b.name === 'crowdsec-firewall-bouncer'; - }); - - this.wizardData.lapiConnected = bouncer && bouncer.last_pull; - console.log('[Wizard] Final status:', { - enabled: this.wizardData.enabled, - running: this.wizardData.running, - nftablesActive: this.wizardData.nftablesActive, - lapiConnected: this.wizardData.lapiConnected - }); - this.refreshView(); - - // Success if enabled, running, and nftables active - // LAPI connection may take a few seconds to establish, so it's optional - if (this.wizardData.enabled && this.wizardData.running && - this.wizardData.nftablesActive) { - console.log('[Wizard] All critical services started! Auto-advancing to Step 8 (Complete)...'); - ui.addNotification(null, E('p', _('Services started successfully!')), 'info'); - // Auto-advance to Step 8 (Complete) after 2 seconds - setTimeout(L.bind(function() { this.goToStep(8); }, this), 2000); - } else { - console.log('[Wizard] Service startup incomplete'); - ui.addNotification(null, E('p', _('Service startup incomplete. Check status and retry.')), 'warning'); - } - }, this)).catch(L.bind(function(err) { - console.error('[Wizard] Service startup error:', err); - this.wizardData.starting = false; - ui.addNotification(null, E('p', _('Service start failed: %s').format(err.message)), 'error'); - this.refreshView(); - }, this)); - }, - - handleManualRepair: function() { - console.log('[Wizard] Manual repair triggered'); - this.wizardData.lapiRepairing = true; - this.wizardData.lapiRepairAttempted = false; // Reset to allow retry - this.refreshView(); - - return API.repairLapi().then(L.bind(function(result) { - console.log('[Wizard] Manual repair result:', result); - this.wizardData.lapiRepairing = false; - this.wizardData.lapiRepairAttempted = true; - - if (result && result.success) { - ui.addNotification(null, E('p', _('LAPI repaired successfully: ') + (result.steps || '')), 'success'); - // Re-check status - return API.getStatus().then(L.bind(function(status) { - this.wizardData.crowdsecRunning = status && status.crowdsec === 'running'; - this.wizardData.lapiAvailable = status && status.lapi_status === 'available'; - this.refreshView(); - }, this)); - } else { - ui.addNotification(null, E('p', _('LAPI repair failed: ') + (result.error || result.errors || 'Unknown error')), 'error'); - this.refreshView(); - } - }, this)).catch(L.bind(function(err) { - console.error('[Wizard] Manual repair error:', err); - this.wizardData.lapiRepairing = false; - ui.addNotification(null, E('p', _('LAPI repair failed: ') + err.message), 'error'); - this.refreshView(); - }, this)); - }, - - handleResetWizard: function() { - console.log('[Wizard] Reset wizard triggered (recovery mode)'); - - if (!confirm(_('This will delete existing bouncer registration and clear all bouncer configuration. Continue?'))) { - return; - } - - this.wizardData.resetting = true; - this.wizardData.bouncerConfigured = false; - this.wizardData.apiKey = ''; - this.refreshView(); - - return API.resetWizard().then(L.bind(function(result) { - console.log('[Wizard] Reset wizard result:', result); - this.wizardData.resetting = false; - - if (result && result.success) { - ui.addNotification(null, E('p', _('Bouncer configuration reset successfully. You can now configure fresh.')), 'success'); - // Reset relevant wizard data - this.wizardData.bouncerConfigured = false; - this.wizardData.apiKey = ''; - this.wizardData.enabled = false; - this.wizardData.running = false; - this.wizardData.nftablesActive = false; - this.wizardData.lapiConnected = false; - } else { - ui.addNotification(null, E('p', _('Reset failed: ') + (result.error || 'Unknown error')), 'error'); - } - this.refreshView(); - }, this)).catch(L.bind(function(err) { - console.error('[Wizard] Reset wizard error:', err); - this.wizardData.resetting = false; - ui.addNotification(null, E('p', _('Reset failed: ') + err.message), 'error'); - this.refreshView(); - }, this)); - }, - + goToStep: function() {}, handleSaveAndApply: null, handleSave: null, handleReset: null