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:
parent
09694b64a1
commit
fe7f160de9
@ -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
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@ -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();
|
||||
});
|
||||
|
||||
@ -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'))
|
||||
]);
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user