cleanup: remove 2,369 lines of dead code
Removed backup files and unused legacy scanner function. All code verified as unreferenced.
This commit is contained in:
200
aggregator-web/src/components/AgentUpdate.tsx
Normal file
200
aggregator-web/src/components/AgentUpdate.tsx
Normal file
@@ -0,0 +1,200 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Upload, CheckCircle, XCircle, RotateCw, Download } from 'lucide-react';
|
||||
import { useAgentUpdate } from '@/hooks/useAgentUpdate';
|
||||
import { Agent } from '@/types';
|
||||
import { cn } from '@/lib/utils';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
interface AgentUpdateProps {
|
||||
agent: Agent;
|
||||
onUpdateComplete?: () => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function AgentUpdate({ agent, onUpdateComplete, className }: AgentUpdateProps) {
|
||||
const {
|
||||
checkForUpdate,
|
||||
triggerAgentUpdate,
|
||||
updateStatus,
|
||||
checkingUpdate,
|
||||
updatingAgent,
|
||||
hasUpdate,
|
||||
availableVersion,
|
||||
currentVersion
|
||||
} = useAgentUpdate();
|
||||
|
||||
const [isChecking, setIsChecking] = useState(false);
|
||||
const [showConfirmDialog, setShowConfirmDialog] = useState(false);
|
||||
const [hasChecked, setHasChecked] = useState(false);
|
||||
|
||||
const handleCheckUpdate = async (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
setIsChecking(true);
|
||||
|
||||
try {
|
||||
await checkForUpdate(agent.id);
|
||||
setHasChecked(true);
|
||||
|
||||
if (hasUpdate && availableVersion) {
|
||||
setShowConfirmDialog(true);
|
||||
} else if (!hasUpdate && hasChecked) {
|
||||
toast.info('Agent is already at latest version');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[UI] Failed to check for updates:', error);
|
||||
toast.error('Failed to check for available updates');
|
||||
} finally {
|
||||
setIsChecking(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleConfirmUpdate = async () => {
|
||||
if (!hasUpdate || !availableVersion) {
|
||||
toast.error('No update available');
|
||||
return;
|
||||
}
|
||||
|
||||
setShowConfirmDialog(false);
|
||||
|
||||
try {
|
||||
await triggerAgentUpdate(agent, availableVersion);
|
||||
|
||||
if (onUpdateComplete) {
|
||||
onUpdateComplete();
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('[UI] Update failed:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const buttonContent = () => {
|
||||
if (updatingAgent) {
|
||||
return (
|
||||
<>
|
||||
<RotateCw className="h-4 w-4 animate-spin" />
|
||||
<span>
|
||||
{updateStatus.status === 'downloading' && 'Downloading...'}
|
||||
{updateStatus.status === 'installing' && 'Installing...'}
|
||||
{updateStatus.status === 'pending' && 'Starting update...'}
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (agent.is_updating) {
|
||||
return (
|
||||
<>
|
||||
<RotateCw className="h-4 w-4 animate-pulse" />
|
||||
<span>Updating...</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (isChecking) {
|
||||
return (
|
||||
<>
|
||||
<RotateCw className="h-4 w-4" />
|
||||
<span>Checking...</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (hasChecked && hasUpdate) {
|
||||
return (
|
||||
<>
|
||||
<Download className="h-4 w-4" />
|
||||
<span>Update to {availableVersion}</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Upload className="h-4 w-4" />
|
||||
<span>Check for Update</span>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="inline-flex items-center">
|
||||
<button
|
||||
onClick={handleCheckUpdate}
|
||||
disabled={updatingAgent || agent.is_updating || isChecking}
|
||||
className={cn(
|
||||
"text-sm px-3 py-1 rounded border flex items-center space-x-2 transition-colors",
|
||||
{
|
||||
"bg-green-50 border-green-300 text-green-700 hover:bg-green-100": hasChecked && hasUpdate,
|
||||
"bg-amber-50 border-amber-300 text-amber-700": updatingAgent || agent.is_updating,
|
||||
"text-gray-600 hover:text-primary-600 border-gray-300 bg-white hover:bg-primary-50":
|
||||
!updatingAgent && !agent.is_updating && !hasUpdate
|
||||
},
|
||||
className
|
||||
)}
|
||||
title={updatingAgent ? "Updating agent..." : agent.is_updating ? "Agent is updating..." : "Check for available updates"}
|
||||
>
|
||||
{buttonContent()}
|
||||
</button>
|
||||
|
||||
{/* Progress indicator */}
|
||||
{updatingAgent && updateStatus.progress && (
|
||||
<div className="ml-2 w-16 h-2 bg-gray-200 rounded">
|
||||
<div
|
||||
className="h-2 bg-green-500 rounded"
|
||||
style={{ width: `${updateStatus.progress}%` }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Status icon */}
|
||||
{hasChecked && !updatingAgent && (
|
||||
<div className="ml-2">
|
||||
{hasUpdate ? (
|
||||
<CheckCircle className="h-4 w-4 text-green-600" />
|
||||
) : (
|
||||
<XCircle className="h-4 w-4 text-gray-400" />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Version info popup */}
|
||||
{hasChecked && (
|
||||
<div className="ml-2 text-xs text-gray-500">
|
||||
{currentVersion} → {hasUpdate ? availableVersion : 'Latest'}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Confirmation Dialog */}
|
||||
{showConfirmDialog && (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div className="bg-white p-6 rounded-lg max-w-md mx-4">
|
||||
<h3 className="text-lg font-semibold mb-4 text-gray-900">
|
||||
Update Agent: {agent.hostname}
|
||||
</h3>
|
||||
<p className="mb-4 text-gray-600">
|
||||
Update agent from <strong>{currentVersion}</strong> to <strong>{availableVersion}</strong>?
|
||||
</p>
|
||||
<p className="mb-4 text-sm text-gray-500">
|
||||
This will temporarily take the agent offline during the update process.
|
||||
</p>
|
||||
<div className="flex justify-end space-x-3">
|
||||
<button
|
||||
onClick={() => setShowConfirmDialog(false)}
|
||||
className="px-4 py-2 text-gray-600 border border-gray-300 rounded hover:bg-gray-50"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={handleConfirmUpdate}
|
||||
className="px-4 py-2 bg-primary-600 text-white rounded hover:bg-primary-700"
|
||||
>
|
||||
Update Agent
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
208
aggregator-web/src/components/RelayList.tsx
Normal file
208
aggregator-web/src/components/RelayList.tsx
Normal file
@@ -0,0 +1,208 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Upload, RefreshCw } from 'lucide-react';
|
||||
import { agentApi } from '@/lib/api';
|
||||
import { Agent } from '@/types';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
interface BulkAgentUpdateProps {
|
||||
agents: Agent[];
|
||||
onBulkUpdateComplete?: () => void;
|
||||
}
|
||||
|
||||
export function BulkAgentUpdate({ agents, onBulkUpdateComplete }: BulkAgentUpdateProps) {
|
||||
const [updatingAgents, setUpdatingAgents] = useState<Set<string>>(new Set());
|
||||
const [checkingUpdates, setCheckingUpdates] = useState<Set<string>>(new Set());
|
||||
|
||||
const handleBulkUpdate = async () => {
|
||||
if (agents.length === 0) {
|
||||
toast.error('No agents selected');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check each agent for available updates first
|
||||
let agentsNeedingUpdate: Agent[] = [];
|
||||
let availableVersion: string | undefined;
|
||||
|
||||
// This will populate the checking state
|
||||
agents.forEach(agent => setCheckingUpdates(prev => new Set(prev).add(agent.id)));
|
||||
|
||||
try {
|
||||
const checkPromises = agents.map(async (agent) => {
|
||||
try {
|
||||
const result = await agentApi.checkForUpdateAvailable(agent.id);
|
||||
|
||||
if (result.hasUpdate && result.latestVersion) {
|
||||
agentsNeedingUpdate.push(agent);
|
||||
if (!availableVersion) {
|
||||
availableVersion = result.latestVersion;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to check updates for agent ${agent.id}:`, error);
|
||||
} finally {
|
||||
setCheckingUpdates(prev => {
|
||||
const newSet = new Set(prev);
|
||||
newSet.delete(agent.id);
|
||||
return newSet;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.all(checkPromises);
|
||||
|
||||
if (agentsNeedingUpdate.length === 0) {
|
||||
toast.info('Selected agents are already up to date');
|
||||
return;
|
||||
}
|
||||
|
||||
// Generate nonces for each agent that needs updating
|
||||
const noncePromises = agentsNeedingUpdate.map(async (agent) => {
|
||||
if (availableVersion) {
|
||||
try {
|
||||
const nonceData = await agentApi.generateUpdateNonce(agent.id, availableVersion);
|
||||
|
||||
// Store nonce for use in update request
|
||||
return {
|
||||
agentId: agent.id,
|
||||
hostname: agent.hostname,
|
||||
nonce: nonceData.update_nonce,
|
||||
targetVersion: availableVersion
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(`Failed to generate nonce for ${agent.hostname}:`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
const nonceResults = await Promise.all(noncePromises);
|
||||
const validUpdates = nonceResults.filter(item => item !== null);
|
||||
|
||||
if (validUpdates.length === 0) {
|
||||
toast.error('Failed to generate update nonces for any agents');
|
||||
return;
|
||||
}
|
||||
|
||||
// Perform bulk updates
|
||||
const updateData = {
|
||||
agent_ids: validUpdates.map(item => item.agentId),
|
||||
version: availableVersion,
|
||||
platform: 'linux-amd64', // This should match the platform
|
||||
nonces: validUpdates.map(item => item.nonce)
|
||||
};
|
||||
|
||||
// Mark agents as updating
|
||||
validUpdates.forEach(item => {
|
||||
setUpdatingAgents(prev => new Set(prev).add(item.agentId));
|
||||
});
|
||||
|
||||
const result = await agentApi.updateMultipleAgents(updateData);
|
||||
|
||||
toast.success(`Initiated updates for ${result.updated.length} of ${agents.length} agents`);
|
||||
|
||||
if (result.failed.length > 0) {
|
||||
toast.error(`Failed to update ${result.failed.length} agents`);
|
||||
}
|
||||
|
||||
// Start polling for completion
|
||||
startBulkUpdatePolling(validUpdates);
|
||||
|
||||
if (onBulkUpdateComplete) {
|
||||
onBulkUpdateComplete();
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Bulk update failed:', error);
|
||||
toast.error(`Bulk update failed: ${error.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
const startBulkUpdatePolling = (agents: Array<{agentId: string, hostname: string}>) => {
|
||||
let attempts = 0;
|
||||
const maxAttempts = 60; // 5 minutes max
|
||||
|
||||
const pollInterval = setInterval(async () => {
|
||||
attempts++;
|
||||
|
||||
if (attempts >= maxAttempts || updatingAgents.size === 0) {
|
||||
clearInterval(pollInterval);
|
||||
setUpdatingAgents(new Set());
|
||||
return;
|
||||
}
|
||||
|
||||
const statusPromises = agents.map(async (item) => {
|
||||
try {
|
||||
const status = await agentApi.getUpdateStatus(item.agentId);
|
||||
|
||||
if (status.status === 'complete' || status.status === 'failed') {
|
||||
// Remove from updating set
|
||||
setUpdatingAgents(prev => {
|
||||
const newSet = new Set(prev);
|
||||
newSet.delete(item.agentId);
|
||||
return newSet;
|
||||
});
|
||||
|
||||
if (status.status === 'complete') {
|
||||
toast.success(`${item.hostname} updated successfully`);
|
||||
} else {
|
||||
toast.error(`${item.hostname} update failed: ${status.error || 'Unknown error'}`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to poll ${item.hostname}:`, error);
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.allSettled(statusPromises);
|
||||
|
||||
}, 5000); // Check every 5 seconds
|
||||
|
||||
return () => clearInterval(pollInterval);
|
||||
};
|
||||
|
||||
const isAnyAgentUpdating = (): boolean => {
|
||||
return agents.some(agent => updatingAgents.has(agent.id));
|
||||
};
|
||||
|
||||
const isAnyAgentChecking = (): boolean => {
|
||||
return agents.some(agent => checkingUpdates.has(agent.id));
|
||||
};
|
||||
|
||||
const getButtonContent = () => {
|
||||
if (isAnyAgentUpdating() || isAnyAgentChecking()) {
|
||||
return (
|
||||
<>
|
||||
<RefreshCw className="h-4 w-4 animate-spin" />
|
||||
<span>{isAnyAgentChecking() ? "Checking..." : "Updating..."}</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (agents.length === 1) {
|
||||
return (
|
||||
<>
|
||||
<Upload className="h-4 w-4" />
|
||||
<span>Update 1 Agent</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Upload className="h-4 w-4" />
|
||||
<span>Update {agents.length} Agents</span>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={handleBulkUpdate}
|
||||
disabled={isAnyAgentUpdating() || isAnyAgentChecking()}
|
||||
className="btn btn-secondary"
|
||||
>
|
||||
{getButtonContent()}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
159
aggregator-web/src/hooks/useAgentUpdate.ts
Normal file
159
aggregator-web/src/hooks/useAgentUpdate.ts
Normal file
@@ -0,0 +1,159 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { agentApi } from '@/lib/api';
|
||||
import { Agent } from '@/types';
|
||||
|
||||
interface UseAgentUpdateReturn {
|
||||
checkForUpdate: (agentId: string) => Promise<void>;
|
||||
triggerAgentUpdate: (agent: Agent, targetVersion: string) => Promise<void>;
|
||||
updateStatus: UpdateStatus;
|
||||
checkingUpdate: boolean;
|
||||
updatingAgent: boolean;
|
||||
hasUpdate: boolean;
|
||||
availableVersion?: string;
|
||||
currentVersion?: string;
|
||||
}
|
||||
|
||||
interface UpdateStatus {
|
||||
status: 'idle' | 'checking' | 'pending' | 'downloading' | 'installing' | 'complete' | 'failed';
|
||||
progress?: number;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export function useAgentUpdate(): UseAgentUpdateReturn {
|
||||
const queryClient = useQueryClient();
|
||||
const [updateStatus, setUpdateStatus] = useState<UpdateStatus>({ status: 'idle' });
|
||||
const [hasUpdate, setHasUpdate] = useState(false);
|
||||
const [availableVersion, setAvailableVersion] = useState<string>();
|
||||
const [currentVersion, setCurrentVersion] = useState<string>();
|
||||
|
||||
// Check if update available for agent
|
||||
const checkMutation = useMutation({
|
||||
mutationFn: agentApi.checkForUpdateAvailable,
|
||||
onSuccess: (data) => {
|
||||
setHasUpdate(data.hasUpdate);
|
||||
setAvailableVersion(data.latestVersion);
|
||||
setCurrentVersion(data.currentVersion);
|
||||
|
||||
if (!data.hasUpdate) {
|
||||
toast.info('Agent is already at latest version');
|
||||
}
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('Failed to check for updates:', error);
|
||||
toast.error(`Failed to check for updates: ${error.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Check for update available
|
||||
const checkForUpdate = async (agentId: string) => {
|
||||
try {
|
||||
await checkMutation.mutateAsync(agentId);
|
||||
} catch (error) {
|
||||
console.error('Error checking for update:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Trigger agent update with nonce generation
|
||||
const triggerAgentUpdate = async (agent: Agent, targetVersion: string) => {
|
||||
try {
|
||||
// Step 1: Check for update availability (already done by checkmutation)
|
||||
if (!hasUpdate) {
|
||||
await checkForUpdate(agent.id);
|
||||
if (!hasUpdate) {
|
||||
toast.info('No updates available');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: Generate nonce for authorized update
|
||||
const nonceData = await agentApi.generateUpdateNonce(agent.id, targetVersion);
|
||||
|
||||
console.log('[UI] Update nonce generated:', nonceData);
|
||||
|
||||
// Step 3: Trigger the actual update
|
||||
const updateResponse = await agentApi.updateAgent(agent.id, {
|
||||
version: targetVersion,
|
||||
platform: `${agent.os_type}-${agent.os_architecture}`,
|
||||
// Include nonce in request for security
|
||||
nonce: nonceData.update_nonce
|
||||
});
|
||||
|
||||
setUpdateStatus({ status: 'pending', progress: 0 });
|
||||
|
||||
// Step 4: Start polling for progress
|
||||
startUpdatePolling(agent.id);
|
||||
|
||||
// Step 5: Refresh agent data in cache
|
||||
queryClient.invalidateQueries({ queryKey: ['agents'] });
|
||||
|
||||
console.log('[UI] Update initiated successfully:', updateResponse);
|
||||
|
||||
} catch (error) {
|
||||
console.error('[UI] Update failed:', error);
|
||||
toast.error(`Update failed: ${error.message}`);
|
||||
setUpdateStatus({ status: 'failed', error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
// Poll for update progress
|
||||
const startUpdatePolling = (agentId: string) => {
|
||||
let attempts = 0;
|
||||
const maxAttempts = 60; // 5 minutes with 5 second intervals
|
||||
|
||||
const pollInterval = setInterval(async () => {
|
||||
attempts++;
|
||||
|
||||
if (attempts >= maxAttempts) {
|
||||
clearInterval(pollInterval);
|
||||
setUpdateStatus({ status: 'failed', error: 'Update timeout' });
|
||||
toast.error('Update timed out after 5 minutes');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const status = await agentApi.getUpdateStatus(agentId);
|
||||
|
||||
switch (status.status) {
|
||||
case 'complete':
|
||||
clearInterval(pollInterval);
|
||||
setUpdateStatus({ status: 'complete' });
|
||||
toast.success('Agent updated successfully!');
|
||||
setHasUpdate(false);
|
||||
setAvailableVersion(undefined);
|
||||
break;
|
||||
case 'failed':
|
||||
clearInterval(pollInterval);
|
||||
setUpdateStatus({ status: 'failed', error: status.error || 'Update failed' });
|
||||
toast.error(`Update failed: ${status.error || 'Unknown error'}`);
|
||||
break;
|
||||
case 'downloading':
|
||||
setUpdateStatus({ status: 'downloading', progress: status.progress });
|
||||
break;
|
||||
case 'installing':
|
||||
setUpdateStatus({ status: 'installing', progress: status.progress });
|
||||
break;
|
||||
default:
|
||||
setUpdateStatus({ status: 'idle' });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[UI] Failed to get update status:', error);
|
||||
clearInterval(pollInterval);
|
||||
setUpdateStatus({ status: 'failed', error: 'Failed to get update status' });
|
||||
}
|
||||
}, 5000); // Poll every 5 seconds
|
||||
|
||||
return () => clearInterval(pollInterval);
|
||||
};
|
||||
|
||||
return {
|
||||
checkForUpdate,
|
||||
triggerAgentUpdate,
|
||||
updateStatus,
|
||||
updatingAgent: updateStatus.status === 'downloading' || updateStatus.status === 'installing' || updateStatus.status === 'pending',
|
||||
hasUpdate,
|
||||
availableVersion,
|
||||
currentVersion
|
||||
};
|
||||
}
|
||||
25
aggregator-web/src/hooks/useSecurity.ts
Normal file
25
aggregator-web/src/hooks/useSecurity.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import api from '@/lib/api';
|
||||
|
||||
export interface ServerKeySecurityStatus {
|
||||
has_private_key: boolean;
|
||||
public_key_fingerprint?: string;
|
||||
algorithm?: string;
|
||||
}
|
||||
|
||||
export const useServerKeySecurity = () => {
|
||||
return useQuery<ServerKeySecurityStatus, Error>({
|
||||
queryKey: ['serverKeySecurity'],
|
||||
queryFn: async () => {
|
||||
const response = await api.get('/security/overview');
|
||||
const overview = response.data;
|
||||
const signingStatus = overview.subsystems.ed25519_signing;
|
||||
|
||||
return {
|
||||
has_private_key: signingStatus.status === 'healthy',
|
||||
public_key_fingerprint: signingStatus.checks?.public_key_fingerprint,
|
||||
algorithm: signingStatus.checks?.algorithm,
|
||||
};
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -28,11 +28,13 @@ import {
|
||||
Upload,
|
||||
} from 'lucide-react';
|
||||
import { useAgents, useAgent, useScanAgent, useScanMultipleAgents, useUnregisterAgent } from '@/hooks/useAgents';
|
||||
import { useAgentUpdate } from '@/hooks/useAgentUpdate';
|
||||
import { useActiveCommands, useCancelCommand } from '@/hooks/useCommands';
|
||||
import { useHeartbeatStatus, useInvalidateHeartbeat, useHeartbeatAgentSync } from '@/hooks/useHeartbeat';
|
||||
import { agentApi } from '@/lib/api';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { getStatusColor, formatRelativeTime, isOnline, formatBytes } from '@/lib/utils';
|
||||
import { AgentUpdate } from '@/components/AgentUpdate';
|
||||
import { cn } from '@/lib/utils';
|
||||
import toast from 'react-hot-toast';
|
||||
import { AgentSystemUpdates } from '@/components/AgentUpdates';
|
||||
@@ -40,6 +42,7 @@ import { AgentStorage } from '@/components/AgentStorage';
|
||||
import { AgentUpdatesEnhanced } from '@/components/AgentUpdatesEnhanced';
|
||||
import { AgentScanners } from '@/components/AgentScanners';
|
||||
import { AgentUpdatesModal } from '@/components/AgentUpdatesModal';
|
||||
import { BulkAgentUpdate } from '@/components/RelayList';
|
||||
import ChatTimeline from '@/components/ChatTimeline';
|
||||
|
||||
const Agents: React.FC = () => {
|
||||
@@ -1167,13 +1170,12 @@ const Agents: React.FC = () => {
|
||||
)}
|
||||
Scan Selected ({selectedAgents.length})
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowUpdateModal(true)}
|
||||
className="btn btn-secondary"
|
||||
>
|
||||
<Upload className="h-4 w-4 mr-2" />
|
||||
Update Selected ({selectedAgents.length})
|
||||
</button>
|
||||
<BulkAgentUpdate
|
||||
agents={agents.filter(agent => selectedAgents.includes(agent.id))}
|
||||
onBulkUpdateComplete={() => {
|
||||
queryClient.invalidateQueries({ queryKey: ['agents'] });
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
@@ -1393,6 +1395,13 @@ const Agents: React.FC = () => {
|
||||
>
|
||||
<Upload className="h-4 w-4" />
|
||||
</button>
|
||||
{/* Agent Update with nonce security */}
|
||||
<AgentUpdate
|
||||
agent={agent}
|
||||
onUpdateComplete={() => {
|
||||
queryClient.invalidateQueries({ queryKey: ['agents'] });
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
onClick={() => handleRemoveAgent(agent.id, agent.hostname)}
|
||||
disabled={unregisterAgentMutation.isPending}
|
||||
|
||||
@@ -11,8 +11,11 @@ import {
|
||||
} from 'lucide-react';
|
||||
import { useDashboardStats } from '@/hooks/useStats';
|
||||
|
||||
import { useServerKeySecurity } from '@/hooks/useSecurity';
|
||||
|
||||
const Dashboard: React.FC = () => {
|
||||
const { data: stats, isPending, error } = useDashboardStats();
|
||||
const { data: serverKeySecurity } = useServerKeySecurity();
|
||||
|
||||
if (isPending) {
|
||||
return (
|
||||
@@ -95,6 +98,19 @@ const Dashboard: React.FC = () => {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Important Messages / Security Alert */}
|
||||
{serverKeySecurity && !serverKeySecurity.has_private_key && (
|
||||
<div className="bg-yellow-50 border border-yellow-200 text-yellow-800 px-4 py-3 rounded-lg relative mb-8" role="alert">
|
||||
<div className="flex items-center">
|
||||
<AlertTriangle className="h-5 w-5 mr-3" />
|
||||
<div>
|
||||
<strong className="font-bold">Security Upgrade Required:</strong>
|
||||
<span className="block sm:inline"> Your server is missing a private key for secure agent updates. Please go to <Link to="/settings/agents" className="font-bold underline hover:text-yellow-900">Agent Management</Link> to generate one.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Stats cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
||||
{statCards.map((stat) => {
|
||||
|
||||
@@ -18,11 +18,15 @@ import {
|
||||
import { useRegistrationTokens } from '@/hooks/useRegistrationTokens';
|
||||
import { toast } from 'react-hot-toast';
|
||||
|
||||
import { useServerKeySecurity } from '@/hooks/useSecurity';
|
||||
|
||||
const AgentManagement: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const [copiedCommand, setCopiedCommand] = useState<string | null>(null);
|
||||
const [selectedPlatform, setSelectedPlatform] = useState<string>('linux');
|
||||
const { data: tokens, isLoading: tokensLoading } = useRegistrationTokens({ is_active: true });
|
||||
const { data: serverKeySecurity, isLoading: isLoadingServerKeySecurity, refetch: refetchServerKeySecurity } = useServerKeySecurity();
|
||||
const [generatingKeys, setGeneratingKeys] = useState(false);
|
||||
|
||||
const platforms = [
|
||||
{
|
||||
@@ -303,6 +307,88 @@ const AgentManagement: React.FC = () => {
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
{/* Server Signing Key */}
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900 mb-3">🔑 Server Signing Key</h4>
|
||||
{isLoadingServerKeySecurity ? (
|
||||
<div className="text-center py-4">
|
||||
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-600 mx-auto"></div>
|
||||
<p className="text-sm text-gray-500 mt-2">Loading key status...</p>
|
||||
</div>
|
||||
) : serverKeySecurity?.has_private_key ? (
|
||||
<div className="space-y-3">
|
||||
<div className="bg-green-50 border border-green-200 rounded-md p-3">
|
||||
<p className="text-sm text-green-800">
|
||||
✓ Server has a private key for signing agent updates.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Public Key Fingerprint
|
||||
</label>
|
||||
<input
|
||||
readOnly
|
||||
value={serverKeySecurity.public_key_fingerprint}
|
||||
className="block w-full px-3 py-2 bg-gray-100 border border-gray-300 rounded-md font-mono text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Algorithm
|
||||
</label>
|
||||
<input
|
||||
readOnly
|
||||
value={serverKeySecurity.algorithm?.toUpperCase()}
|
||||
className="block w-full px-3 py-2 bg-gray-100 border border-gray-300 rounded-md text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
<div className="bg-yellow-50 border border-yellow-200 rounded-md p-3">
|
||||
<p className="text-sm text-yellow-800">
|
||||
Your server is missing a private key. Generate one to enable secure agent updates.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={async () => {
|
||||
setGeneratingKeys(true);
|
||||
try {
|
||||
const response = await fetch('/api/setup/generate-keys', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to generate keys');
|
||||
}
|
||||
toast.success('Signing keys generated successfully! Please restart your server.');
|
||||
refetchServerKeySecurity(); // Refresh status
|
||||
} catch (error: any) {
|
||||
toast.error(error.message || 'Failed to generate keys');
|
||||
} finally {
|
||||
setGeneratingKeys(false);
|
||||
}
|
||||
}}
|
||||
disabled={generatingKeys}
|
||||
className="w-full py-2 px-4 border border-transparent rounded-md text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center"
|
||||
>
|
||||
{generatingKeys ? (
|
||||
<>
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
|
||||
Generating Keys...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Key className="h-4 w-4 mr-2" />
|
||||
Generate Signing Keys
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900 mb-3">🛡️ Security Model</h4>
|
||||
<p className="text-sm text-gray-600 mb-4">
|
||||
|
||||
Reference in New Issue
Block a user