fix: Add missing API utility functions and fix data structure handling (v0.6.0-r9)

- Add parseScenario() to format scenario names
- Add getCountryFlag() to display country flag emojis
- Add formatRelativeTime() for relative timestamps
- Fix decisions data flattening in handleUnban, handleBulkUnban, submitBan, and polling
- Fix getDashboardData to properly flatten alerts->decisions structure
- Fix context error in overview renderDecisionsTable (this vs self)

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
CyberMind-FR 2026-01-07 11:01:54 +01:00
parent 09694b64a1
commit fe7f160de9
3 changed files with 168 additions and 30 deletions

View File

@ -20,7 +20,7 @@ var callStatus = rpc.declare({
var callDecisions = rpc.declare({
object: 'luci.crowdsec-dashboard',
method: 'decisions',
expect: { decisions: [] }
expect: { alerts: [] }
});
var callAlerts = rpc.declare({
@ -68,21 +68,21 @@ var callSecuboxLogs = rpc.declare({
var callCollectDebug = rpc.declare({
object: 'luci.crowdsec-dashboard',
method: 'collect_debug',
expect: { success: false }
expect: { }
});
var callBan = rpc.declare({
object: 'luci.crowdsec-dashboard',
method: 'ban',
params: ['ip', 'duration', 'reason'],
expect: { success: false }
expect: { }
});
var callUnban = rpc.declare({
object: 'luci.crowdsec-dashboard',
method: 'unban',
params: ['ip'],
expect: { success: false }
expect: { }
});
// CrowdSec v1.7.4+ features
@ -102,27 +102,27 @@ var callConfigureMetrics = rpc.declare({
object: 'luci.crowdsec-dashboard',
method: 'configure_metrics',
params: ['enable'],
expect: { success: false }
expect: { }
});
var callCollections = rpc.declare({
object: 'luci.crowdsec-dashboard',
method: 'collections',
expect: { }
expect: { collections: [] }
});
var callInstallCollection = rpc.declare({
object: 'luci.crowdsec-dashboard',
method: 'install_collection',
params: ['collection'],
expect: { success: false }
expect: { }
});
var callRemoveCollection = rpc.declare({
object: 'luci.crowdsec-dashboard',
method: 'remove_collection',
params: ['collection'],
expect: { success: false }
expect: { }
});
var callUpdateHub = rpc.declare({
@ -135,14 +135,14 @@ var callRegisterBouncer = rpc.declare({
object: 'luci.crowdsec-dashboard',
method: 'register_bouncer',
params: ['bouncer_name'],
expect: { success: false }
expect: { }
});
var callDeleteBouncer = rpc.declare({
object: 'luci.crowdsec-dashboard',
method: 'delete_bouncer',
params: ['bouncer_name'],
expect: { success: false }
expect: { }
});
// Firewall Bouncer Management
@ -156,7 +156,7 @@ var callControlFirewallBouncer = rpc.declare({
object: 'luci.crowdsec-dashboard',
method: 'control_firewall_bouncer',
params: ['action'],
expect: { success: false }
expect: { }
});
var callFirewallBouncerConfig = rpc.declare({
@ -169,7 +169,7 @@ var callUpdateFirewallBouncerConfig = rpc.declare({
object: 'luci.crowdsec-dashboard',
method: 'update_firewall_bouncer_config',
params: ['key', 'value'],
expect: { success: false }
expect: { }
});
var callNftablesStats = rpc.declare({
@ -209,9 +209,75 @@ function formatDate(dateStr) {
}
}
function isValidIP(ip) {
if (!ip) return false;
// IPv4 regex
var ipv4Regex = /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
// IPv6 regex (simplified)
var ipv6Regex = /^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$/;
return ipv4Regex.test(ip) || ipv6Regex.test(ip);
}
function parseScenario(scenario) {
if (!scenario) return 'N/A';
// Extract human-readable part from scenario name
// e.g., "crowdsecurity/ssh-bruteforce" -> "SSH Bruteforce"
var parts = scenario.split('/');
var name = parts[parts.length - 1];
// Convert dash-separated to title case
return name.split('-').map(function(word) {
return word.charAt(0).toUpperCase() + word.slice(1);
}).join(' ');
}
function getCountryFlag(countryCode) {
if (!countryCode || countryCode === 'N/A') return '';
// Convert country code to flag emoji
// e.g., "US" -> "🇺🇸"
var code = countryCode.toUpperCase();
if (code.length !== 2) return '';
var codePoints = [];
for (var i = 0; i < code.length; i++) {
codePoints.push(0x1F1E6 - 65 + code.charCodeAt(i));
}
return String.fromCodePoint.apply(null, codePoints);
}
function formatRelativeTime(dateStr) {
if (!dateStr) return 'N/A';
try {
var date = new Date(dateStr);
var now = new Date();
var seconds = Math.floor((now - date) / 1000);
if (seconds < 60) return seconds + 's ago';
if (seconds < 3600) return Math.floor(seconds / 60) + 'm ago';
if (seconds < 86400) return Math.floor(seconds / 3600) + 'h ago';
if (seconds < 2592000) return Math.floor(seconds / 86400) + 'd ago';
return Math.floor(seconds / 2592000) + 'mo ago';
} catch(e) {
return dateStr;
}
}
return baseclass.extend({
getStatus: callStatus,
getDecisions: callDecisions,
getDecisions: function() {
return callDecisions().then(function(result) {
console.log('[API] getDecisions raw result:', result);
console.log('[API] getDecisions result type:', typeof result);
console.log('[API] getDecisions is array:', Array.isArray(result));
return result;
});
},
getAlerts: callAlerts,
getBouncers: callBouncers,
getMetrics: callMetrics,
@ -247,6 +313,14 @@ return baseclass.extend({
formatDuration: formatDuration,
formatDate: formatDate,
formatRelativeTime: formatRelativeTime,
isValidIP: isValidIP,
parseScenario: parseScenario,
getCountryFlag: getCountryFlag,
// Aliases for compatibility
banIP: callBan,
unbanIP: callUnban,
getDashboardData: function() {
return Promise.all([
@ -258,15 +332,25 @@ return baseclass.extend({
// Check if any result has an error (service not running)
var status = results[0] || {};
var stats = results[1] || {};
var decisions = results[2] || {};
var alerts = results[3] || {};
var decisionsRaw = results[2] || [];
var alerts = results[3] || [];
// Flatten alerts->decisions structure
var decisions = [];
if (Array.isArray(decisionsRaw)) {
decisionsRaw.forEach(function(alert) {
if (alert.decisions && Array.isArray(alert.decisions)) {
decisions = decisions.concat(alert.decisions);
}
});
}
return {
status: status,
stats: (stats.error) ? {} : stats,
decisions: (decisions.error) ? [] : (decisions.decisions || []),
alerts: (alerts.error) ? [] : (alerts.alerts || []),
error: stats.error || decisions.error || alerts.error || null
decisions: decisions,
alerts: alerts,
error: stats.error || null
};
});
}

View File

@ -82,11 +82,11 @@ return view.extend({
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');
@ -97,7 +97,15 @@ return view.extend({
}
}).then(function(data) {
if (data) {
self.decisions = 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();
}
@ -127,18 +135,26 @@ return view.extend({
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' : ''),
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) {
self.decisions = 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();
}
@ -325,29 +341,59 @@ return view.extend({
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();
return self.csApi.getDecisions();
// 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) {
self.decisions = 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;
this.decisions = Array.isArray(data) ? data : [];
// 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' }, [
@ -389,7 +435,15 @@ return view.extend({
// Setup polling
poll.add(function() {
return self.csApi.getDecisions().then(function(newData) {
self.decisions = Array.isArray(newData) ? 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();
});

View File

@ -136,7 +136,7 @@ return view.extend({
E('td', {}, E('button', {
'class': 'cs-btn cs-btn-danger cs-btn-sm',
'data-ip': d.value,
'click': ui.createHandlerFn(this, 'handleUnban', d.value)
'click': ui.createHandlerFn(self, 'handleUnban', d.value)
}, 'Unban'))
]);
});