Files
Redflag/aggregator-web/src/lib/utils.ts
Fimeg 03fee29760 v0.1.16: Security overhaul and systematic deployment preparation
Breaking changes for clean alpha releases:
- JWT authentication with user-provided secrets (no more development defaults)
- Registration token system for secure agent enrollment
- Rate limiting with user-adjustable settings
- Enhanced agent configuration with proxy support
- Interactive server setup wizard (--setup flag)
- Heartbeat architecture separation for better UX
- Package status synchronization fixes
- Accurate timestamp tracking for RMM features

Setup process for new installations:
1. docker-compose up -d postgres
2. ./redflag-server --setup
3. ./redflag-server --migrate
4. ./redflag-server
5. Generate tokens via admin UI
6. Deploy agents with registration tokens
2025-10-29 10:38:18 -04:00

292 lines
7.3 KiB
TypeScript

import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';
// Utility function for combining class names
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
// Date formatting utilities
export const formatDate = (dateString: string): string => {
const date = new Date(dateString);
return date.toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
});
};
export const formatDateTime = (dateString: string | null): string => {
if (!dateString) return 'Never';
const date = new Date(dateString);
return date.toLocaleString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
});
};
export const formatRelativeTime = (dateString: string): string => {
if (!dateString) return 'Never';
let date: Date;
try {
// Handle various timestamp formats
if (dateString.includes('T') && dateString.includes('Z')) {
// ISO 8601 format
date = new Date(dateString);
} else if (dateString.includes(' ')) {
// Database format like "2025-01-15 10:30:00"
date = new Date(dateString.replace(' ', 'T') + 'Z');
} else {
// Try direct parsing
date = new Date(dateString);
}
// Check if date is invalid
if (isNaN(date.getTime())) {
console.warn('Invalid date string:', dateString);
return 'Invalid Date';
}
} catch (error) {
console.warn('Error parsing date:', dateString, error);
return 'Invalid Date';
}
const now = new Date();
const diffMs = now.getTime() - date.getTime();
const diffMins = Math.floor(diffMs / 60000);
const diffHours = Math.floor(diffMins / 60);
const diffDays = Math.floor(diffHours / 24);
if (diffMins < 1) {
return 'Just now';
} else if (diffMins < 60) {
return `${diffMins} minute${diffMins !== 1 ? 's' : ''} ago`;
} else if (diffHours < 24) {
return `${diffHours} hour${diffHours !== 1 ? 's' : ''} ago`;
} else if (diffDays < 7) {
return `${diffDays} day${diffDays !== 1 ? 's' : ''} ago`;
} else {
return formatDate(date.toISOString());
}
};
export const isOnline = (lastCheckin: string): boolean => {
const lastCheck = new Date(lastCheckin);
const now = new Date();
const diffMs = now.getTime() - lastCheck.getTime();
const diffMins = Math.floor(diffMs / 60000);
return diffMins < 15; // Consider online if checked in within 15 minutes (allows for 5min check-in + buffer)
};
// Size formatting utilities
export const formatBytes = (bytes: number): string => {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
};
// Version comparison utilities
export const versionCompare = (v1: string, v2: string): number => {
const parts1 = v1.split('.').map(Number);
const parts2 = v2.split('.').map(Number);
const maxLength = Math.max(parts1.length, parts2.length);
for (let i = 0; i < maxLength; i++) {
const part1 = parts1[i] || 0;
const part2 = parts2[i] || 0;
if (part1 > part2) return 1;
if (part1 < part2) return -1;
}
return 0;
};
// Status and severity utilities
export const getStatusColor = (status: string): string => {
switch (status) {
case 'online':
return 'text-success-600 bg-success-100';
case 'offline':
return 'text-danger-600 bg-danger-100';
case 'pending':
return 'text-warning-600 bg-warning-100';
case 'checking_dependencies':
return 'text-blue-500 bg-blue-100';
case 'pending_dependencies':
return 'text-orange-600 bg-orange-100';
case 'approved':
case 'scheduled':
return 'text-blue-600 bg-blue-100';
case 'installing':
return 'text-indigo-600 bg-indigo-100';
case 'installed':
return 'text-success-600 bg-success-100';
case 'failed':
return 'text-danger-600 bg-danger-100';
default:
return 'text-gray-600 bg-gray-100';
}
};
export const getSeverityColor = (severity: string): string => {
switch (severity) {
case 'critical':
return 'text-danger-600 bg-danger-100';
case 'important':
case 'high':
return 'text-warning-600 bg-warning-100';
case 'moderate':
case 'medium':
return 'text-blue-600 bg-blue-100';
case 'low':
case 'none':
return 'text-gray-600 bg-gray-100';
default:
return 'text-gray-600 bg-gray-100';
}
};
export const getPackageTypeIcon = (type: string): string => {
switch (type) {
case 'apt':
return '📦';
case 'docker':
return '🐳';
case 'yum':
case 'dnf':
return '🐧';
case 'windows':
return '🪟';
case 'winget':
return '📱';
default:
return '📋';
}
};
// Filter and search utilities
export const filterUpdates = (
updates: any[],
filters: {
status: string[];
severity: string[];
type: string[];
search: string;
}
): any[] => {
return updates.filter(update => {
// Status filter
if (filters.status.length > 0 && !filters.status.includes(update.status)) {
return false;
}
// Severity filter
if (filters.severity.length > 0 && !filters.severity.includes(update.severity)) {
return false;
}
// Type filter
if (filters.type.length > 0 && !filters.type.includes(update.package_type)) {
return false;
}
// Search filter
if (filters.search) {
const searchLower = filters.search.toLowerCase();
return (
update.package_name.toLowerCase().includes(searchLower) ||
update.current_version.toLowerCase().includes(searchLower) ||
update.available_version.toLowerCase().includes(searchLower)
);
}
return true;
});
};
// Error handling utilities
export const getErrorMessage = (error: any): string => {
if (typeof error === 'string') {
return error;
}
if (error?.message) {
return error.message;
}
if (error?.response?.data?.message) {
return error.response.data.message;
}
return 'An unexpected error occurred';
};
// Debounce utility
export const debounce = <T extends (...args: any[]) => any>(
func: T,
wait: number
): ((...args: Parameters<T>) => void) => {
let timeout: ReturnType<typeof setTimeout>;
return (...args: Parameters<T>) => {
clearTimeout(timeout);
timeout = setTimeout(() => func(...args), wait);
};
};
// Local storage utilities
export const storage = {
get: (key: string): string | null => {
try {
return localStorage.getItem(key);
} catch {
return null;
}
},
set: (key: string, value: string): void => {
try {
localStorage.setItem(key, value);
} catch {
// Silent fail for storage issues
}
},
remove: (key: string): void => {
try {
localStorage.removeItem(key);
} catch {
// Silent fail for storage issues
}
},
getJSON: <T = any>(key: string): T | null => {
try {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : null;
} catch {
return null;
}
},
setJSON: (key: string, value: any): void => {
try {
localStorage.setItem(key, JSON.stringify(value));
} catch {
// Silent fail for storage issues
}
},
};