secubox-openwrt/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/decisions.js
CyberMind-FR ed52af6ab9 feat(theming): Add custom navigation tabs to Client Guardian and CrowdSec dashboards
- Create nav.js for Client Guardian with SecuBox themed tabs
- Create nav.js for CrowdSec dashboard with themed navigation
- Update all Client Guardian views to use CgNav.renderTabs()
- Update all CrowdSec views to use CsNav.renderTabs()
- Update Client Guardian menu.json paths from /client-guardian/ to /guardian/
- Hide default LuCI tabs via CSS injection for both dashboards

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

461 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use strict';
'require view';
'require secubox-theme/theme as Theme';
'require dom';
'require poll';
'require ui';
'require crowdsec-dashboard/api as api';
'require crowdsec-dashboard/nav as CsNav';
/**
* CrowdSec Dashboard - Decisions View
* Detailed view and management of all active decisions
* Copyright (C) 2024 CyberMind.fr - Gandalf
*/
return view.extend({
title: _('Decisions'),
csApi: null,
decisions: [],
filteredDecisions: [],
searchQuery: '',
sortField: 'value',
sortOrder: 'asc',
load: function() {
var cssLink = document.createElement('link');
cssLink.rel = 'stylesheet';
cssLink.href = L.resource('crowdsec-dashboard/dashboard.css');
document.head.appendChild(cssLink);
this.csApi = api;
return this.csApi.getDecisions();
},
filterDecisions: function() {
var self = this;
var query = this.searchQuery.toLowerCase();
this.filteredDecisions = this.decisions.filter(function(d) {
if (!query) return true;
var searchFields = [
d.value,
d.scenario,
d.country,
d.type,
d.origin
].filter(Boolean).join(' ').toLowerCase();
return searchFields.indexOf(query) !== -1;
});
// Sort
this.filteredDecisions.sort(function(a, b) {
var aVal = a[self.sortField] || '';
var bVal = b[self.sortField] || '';
if (self.sortOrder === 'asc') {
return aVal.localeCompare(bVal);
} else {
return bVal.localeCompare(aVal);
}
});
},
handleSearch: function(ev) {
this.searchQuery = ev.target.value;
this.filterDecisions();
this.updateTable();
},
handleSort: function(field, ev) {
if (this.sortField === field) {
this.sortOrder = this.sortOrder === 'asc' ? 'desc' : 'asc';
} else {
this.sortField = field;
this.sortOrder = 'asc';
}
this.filterDecisions();
this.updateTable();
},
handleUnban: function(ip, ev) {
var self = this;
if (!confirm('Remove ban for ' + ip + '?')) {
return;
}
this.csApi.unbanIP(ip).then(function(result) {
if (result.success) {
self.showToast('IP ' + ip + ' unbanned successfully', 'success');
return self.csApi.getDecisions();
} else {
self.showToast('Failed to unban: ' + (result.error || 'Unknown error'), 'error');
return null;
}
}).then(function(data) {
if (data) {
// Flatten alerts->decisions structure
self.decisions = [];
if (Array.isArray(data)) {
data.forEach(function(alert) {
if (alert.decisions && Array.isArray(alert.decisions)) {
self.decisions = self.decisions.concat(alert.decisions);
}
});
}
self.filterDecisions();
self.updateTable();
}
}).catch(function(err) {
self.showToast('Error: ' + err.message, 'error');
});
},
handleBulkUnban: function(ev) {
var self = this;
var checkboxes = document.querySelectorAll('.cs-decision-checkbox:checked');
if (checkboxes.length === 0) {
self.showToast('No decisions selected', 'error');
return;
}
if (!confirm('Remove ban for ' + checkboxes.length + ' IP(s)?')) {
return;
}
var promises = [];
checkboxes.forEach(function(cb) {
promises.push(self.csApi.unbanIP(cb.dataset.ip));
});
Promise.all(promises).then(function(results) {
var success = results.filter(function(r) { return r.success; }).length;
var failed = results.length - success;
if (success > 0) {
self.showToast(success + ' IP(s) unbanned' + (failed > 0 ? ', ' + failed + ' failed' : ''),
failed > 0 ? 'warning' : 'success');
} else {
self.showToast('Failed to unban IPs', 'error');
}
return self.csApi.getDecisions();
}).then(function(data) {
if (data) {
// Flatten alerts->decisions structure
self.decisions = [];
if (Array.isArray(data)) {
data.forEach(function(alert) {
if (alert.decisions && Array.isArray(alert.decisions)) {
self.decisions = self.decisions.concat(alert.decisions);
}
});
}
self.filterDecisions();
self.updateTable();
}
});
},
handleSelectAll: function(ev) {
var checked = ev.target.checked;
document.querySelectorAll('.cs-decision-checkbox').forEach(function(cb) {
cb.checked = checked;
});
},
showToast: function(message, type) {
var existing = document.querySelector('.cs-toast');
if (existing) existing.remove();
var toast = E('div', { 'class': 'cs-toast ' + (type || '') }, message);
document.body.appendChild(toast);
setTimeout(function() { toast.remove(); }, 4000);
},
updateTable: function() {
var container = document.getElementById('decisions-table-container');
if (container) {
dom.content(container, this.renderTable());
}
var countEl = document.getElementById('decisions-count');
if (countEl) {
countEl.textContent = this.filteredDecisions.length + ' of ' + this.decisions.length + ' decisions';
}
},
renderSortIcon: function(field) {
if (this.sortField !== field) return ' ↕';
return this.sortOrder === 'asc' ? ' ↑' : ' ↓';
},
renderTable: function() {
var self = this;
if (this.filteredDecisions.length === 0) {
return E('div', { 'class': 'cs-empty' }, [
E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }),
E('div', { 'class': 'cs-empty-icon' }, this.searchQuery ? '🔍' : '✅'),
E('p', {}, this.searchQuery ? 'No matching decisions found' : 'No active decisions')
]);
}
var rows = this.filteredDecisions.map(function(d, i) {
return E('tr', {}, [
E('td', {}, E('input', {
'type': 'checkbox',
'class': 'cs-decision-checkbox',
'data-ip': d.value
})),
E('td', {}, E('span', { 'class': 'cs-ip' }, d.value || 'N/A')),
E('td', {}, E('span', { 'class': 'cs-scenario' }, self.csApi.parseScenario(d.scenario))),
E('td', {}, E('span', { 'class': 'cs-country' }, [
E('span', { 'class': 'cs-country-flag' }, self.csApi.getCountryFlag(d.country)),
' ',
d.country || 'N/A'
])),
E('td', {}, d.origin || 'crowdsec'),
E('td', {}, E('span', { 'class': 'cs-action ' + (d.type || 'ban') }, d.type || 'ban')),
E('td', {}, E('span', { 'class': 'cs-time' }, self.csApi.formatDuration(d.duration))),
E('td', {}, E('span', { 'class': 'cs-time' }, self.csApi.formatRelativeTime(d.created_at))),
E('td', {}, E('button', {
'class': 'cs-btn cs-btn-danger cs-btn-sm',
'click': ui.createHandlerFn(self, 'handleUnban', d.value)
}, 'Unban'))
]);
});
return E('table', { 'class': 'cs-table' }, [
E('thead', {}, E('tr', {}, [
E('th', { 'style': 'width: 40px' }, E('input', {
'type': 'checkbox',
'id': 'select-all',
'change': ui.createHandlerFn(this, 'handleSelectAll')
})),
E('th', {
'click': ui.createHandlerFn(this, 'handleSort', 'value'),
'style': 'cursor: pointer'
}, 'IP Address' + this.renderSortIcon('value')),
E('th', {
'click': ui.createHandlerFn(this, 'handleSort', 'scenario'),
'style': 'cursor: pointer'
}, 'Scenario' + this.renderSortIcon('scenario')),
E('th', {
'click': ui.createHandlerFn(this, 'handleSort', 'country'),
'style': 'cursor: pointer'
}, 'Country' + this.renderSortIcon('country')),
E('th', {}, 'Origin'),
E('th', {}, 'Action'),
E('th', {}, 'Expires'),
E('th', {}, 'Created'),
E('th', {}, 'Actions')
])),
E('tbody', {}, rows)
]);
},
renderBanModal: function() {
return E('div', { 'class': 'cs-modal-overlay', 'id': 'ban-modal', 'style': 'display: none' }, [
E('div', { 'class': 'cs-modal' }, [
E('div', { 'class': 'cs-modal-header' }, [
E('div', { 'class': 'cs-modal-title' }, 'Add IP Ban'),
E('button', {
'class': 'cs-modal-close',
'click': ui.createHandlerFn(this, 'closeBanModal')
}, '×')
]),
E('div', { 'class': 'cs-modal-body' }, [
E('div', { 'class': 'cs-form-group' }, [
E('label', { 'class': 'cs-form-label' }, 'IP Address or Range'),
E('input', {
'class': 'cs-input',
'id': 'ban-ip',
'type': 'text',
'placeholder': '192.168.1.100 or 10.0.0.0/24'
})
]),
E('div', { 'class': 'cs-form-group' }, [
E('label', { 'class': 'cs-form-label' }, 'Duration'),
E('input', {
'class': 'cs-input',
'id': 'ban-duration',
'type': 'text',
'placeholder': '4h, 24h, 7d...',
'value': '4h'
})
]),
E('div', { 'class': 'cs-form-group' }, [
E('label', { 'class': 'cs-form-label' }, 'Reason'),
E('input', {
'class': 'cs-input',
'id': 'ban-reason',
'type': 'text',
'placeholder': 'Manual ban from dashboard'
})
])
]),
E('div', { 'class': 'cs-modal-footer' }, [
E('button', {
'class': 'cs-btn',
'click': ui.createHandlerFn(this, 'closeBanModal')
}, 'Cancel'),
E('button', {
'class': 'cs-btn cs-btn-primary',
'click': ui.createHandlerFn(this, 'submitBan')
}, 'Add Ban')
])
])
]);
},
openBanModal: function(ev) {
document.getElementById('ban-modal').style.display = 'flex';
},
closeBanModal: function(ev) {
document.getElementById('ban-modal').style.display = 'none';
document.getElementById('ban-ip').value = '';
document.getElementById('ban-duration').value = '4h';
document.getElementById('ban-reason').value = '';
},
submitBan: function(ev) {
var self = this;
var ip = document.getElementById('ban-ip').value.trim();
var duration = document.getElementById('ban-duration').value.trim() || '4h';
var reason = document.getElementById('ban-reason').value.trim() || 'Manual ban from dashboard';
if (!ip) {
self.showToast('Please enter an IP address', 'error');
return;
}
if (!self.csApi.isValidIP(ip)) {
self.showToast('Invalid IP address format', 'error');
return;
}
console.log('[Decisions] Banning IP:', ip, 'Duration:', duration, 'Reason:', reason);
self.csApi.banIP(ip, duration, reason).then(function(result) {
console.log('[Decisions] Ban result:', result);
if (result.success) {
self.showToast('IP ' + ip + ' banned for ' + duration, 'success');
self.closeBanModal();
// Wait 1 second for CrowdSec to process the decision
console.log('[Decisions] Waiting 1 second before refreshing...');
return new Promise(function(resolve) {
setTimeout(function() {
console.log('[Decisions] Refreshing decisions list...');
resolve(self.csApi.getDecisions());
}, 1000);
});
} else {
self.showToast('Failed to ban: ' + (result.error || 'Unknown error'), 'error');
return null;
}
}).then(function(data) {
console.log('[Decisions] Updated decisions data:', data);
if (data) {
// Flatten alerts->decisions structure
self.decisions = [];
if (Array.isArray(data)) {
data.forEach(function(alert) {
if (alert.decisions && Array.isArray(alert.decisions)) {
self.decisions = self.decisions.concat(alert.decisions);
}
});
}
self.filterDecisions();
self.updateTable();
console.log('[Decisions] Table updated with', self.decisions.length, 'decisions');
}
}).catch(function(err) {
console.error('[Decisions] Ban error:', err);
self.showToast('Error: ' + err.message, 'error');
});
},
render: function(data) {
var self = this;
// Flatten alerts->decisions structure
// data is an array of alerts, each containing a decisions array
this.decisions = [];
if (Array.isArray(data)) {
data.forEach(function(alert) {
if (alert.decisions && Array.isArray(alert.decisions)) {
self.decisions = self.decisions.concat(alert.decisions);
}
});
}
console.log('[Decisions] Flattened', this.decisions.length, 'decisions from', data ? data.length : 0, 'alerts');
this.filterDecisions();
var view = E('div', { 'class': 'crowdsec-dashboard' }, [
CsNav.renderTabs('decisions'),
E('div', { 'class': 'cs-card' }, [
E('div', { 'class': 'cs-card-header' }, [
E('div', { 'class': 'cs-card-title' }, [
'Active Decisions',
E('span', {
'id': 'decisions-count',
'style': 'font-weight: normal; margin-left: 12px; font-size: 12px; color: var(--cs-text-muted)'
}, this.filteredDecisions.length + ' of ' + this.decisions.length + ' decisions')
]),
E('div', { 'class': 'cs-actions-bar' }, [
E('div', { 'class': 'cs-search-box' }, [
E('input', {
'class': 'cs-input',
'type': 'text',
'placeholder': 'Search IP, scenario, country...',
'input': ui.createHandlerFn(this, 'handleSearch')
})
]),
E('button', {
'class': 'cs-btn cs-btn-danger',
'click': ui.createHandlerFn(this, 'handleBulkUnban')
}, 'Unban Selected'),
E('button', {
'class': 'cs-btn cs-btn-primary',
'click': ui.createHandlerFn(this, 'openBanModal')
}, '+ Add Ban')
])
]),
E('div', { 'class': 'cs-card-body no-padding', 'id': 'decisions-table-container' },
this.renderTable()
)
]),
this.renderBanModal()
]);
// Setup polling
poll.add(function() {
return self.csApi.getDecisions().then(function(newData) {
// Flatten alerts->decisions structure
self.decisions = [];
if (Array.isArray(newData)) {
newData.forEach(function(alert) {
if (alert.decisions && Array.isArray(alert.decisions)) {
self.decisions = self.decisions.concat(alert.decisions);
}
});
}
self.filterDecisions();
self.updateTable();
});
}, 30);
return view;
},
handleSaveApply: null,
handleSave: null,
handleReset: null
});