mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-25 07:51:12 -05:00
Add centralized admin action logger and integrate logging for: - Alert resolutions (system, rate limit, grouped) - Role grants/revokes in UserRoleManager - Incident creation/acknowledgement/resolution - Moderation lock overrides Includes file updates and usage across relevant components to ensure consistent audit trails.
237 lines
7.0 KiB
TypeScript
237 lines
7.0 KiB
TypeScript
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
import { supabase } from '@/lib/supabaseClient';
|
|
import { queryKeys } from '@/lib/queryKeys';
|
|
import { toast } from 'sonner';
|
|
|
|
export interface Incident {
|
|
id: string;
|
|
incident_number: string;
|
|
title: string;
|
|
description: string;
|
|
severity: 'critical' | 'high' | 'medium' | 'low';
|
|
status: 'open' | 'investigating' | 'resolved' | 'closed';
|
|
correlation_rule_id?: string;
|
|
detected_at: string;
|
|
acknowledged_at?: string;
|
|
acknowledged_by?: string;
|
|
resolved_at?: string;
|
|
resolved_by?: string;
|
|
resolution_notes?: string;
|
|
alert_count: number;
|
|
created_at: string;
|
|
updated_at: string;
|
|
}
|
|
|
|
export function useIncidents(status?: 'open' | 'investigating' | 'resolved' | 'closed') {
|
|
return useQuery({
|
|
queryKey: queryKeys.monitoring.incidents(status),
|
|
queryFn: async () => {
|
|
let query = supabase
|
|
.from('incidents')
|
|
.select('*')
|
|
.order('detected_at', { ascending: false });
|
|
|
|
if (status) {
|
|
query = query.eq('status', status);
|
|
}
|
|
|
|
const { data, error } = await query;
|
|
if (error) throw error;
|
|
return (data || []) as Incident[];
|
|
},
|
|
staleTime: 15000,
|
|
refetchInterval: 30000,
|
|
});
|
|
}
|
|
|
|
export function useCreateIncident() {
|
|
const queryClient = useQueryClient();
|
|
|
|
return useMutation({
|
|
mutationFn: async ({
|
|
ruleId,
|
|
title,
|
|
description,
|
|
severity,
|
|
alertIds,
|
|
alertSources,
|
|
}: {
|
|
ruleId?: string;
|
|
title: string;
|
|
description?: string;
|
|
severity: 'critical' | 'high' | 'medium' | 'low';
|
|
alertIds: string[];
|
|
alertSources: ('system' | 'rate_limit')[];
|
|
}) => {
|
|
// Create the incident (incident_number is auto-generated by trigger)
|
|
const { data: incident, error: incidentError } = await supabase
|
|
.from('incidents')
|
|
.insert([{
|
|
title,
|
|
description,
|
|
severity,
|
|
correlation_rule_id: ruleId,
|
|
status: 'open' as const,
|
|
} as any])
|
|
.select()
|
|
.single();
|
|
|
|
if (incidentError) throw incidentError;
|
|
|
|
// Link alerts to the incident
|
|
const incidentAlerts = alertIds.map((alertId, index) => ({
|
|
incident_id: incident.id,
|
|
alert_source: alertSources[index] || 'system',
|
|
alert_id: alertId,
|
|
}));
|
|
|
|
const { error: linkError } = await supabase
|
|
.from('incident_alerts')
|
|
.insert(incidentAlerts);
|
|
|
|
if (linkError) throw linkError;
|
|
|
|
// Log to audit trail
|
|
const { logAdminAction } = await import('@/lib/adminActionAuditHelpers');
|
|
await logAdminAction('incident_created', {
|
|
incident_id: incident.id,
|
|
incident_number: incident.incident_number,
|
|
title: title,
|
|
severity: severity,
|
|
alert_count: alertIds.length,
|
|
correlation_rule_id: ruleId,
|
|
});
|
|
|
|
return incident as Incident;
|
|
},
|
|
onSuccess: (incident) => {
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.monitoring.incidents() });
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.monitoring.correlatedAlerts() });
|
|
toast.success(`Incident ${incident.incident_number} created`);
|
|
},
|
|
onError: (error) => {
|
|
console.error('Failed to create incident:', error);
|
|
toast.error('Failed to create incident');
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useAcknowledgeIncident() {
|
|
const queryClient = useQueryClient();
|
|
|
|
return useMutation({
|
|
mutationFn: async (incidentId: string) => {
|
|
const { data, error } = await supabase
|
|
.from('incidents')
|
|
.update({
|
|
status: 'investigating',
|
|
acknowledged_at: new Date().toISOString(),
|
|
acknowledged_by: (await supabase.auth.getUser()).data.user?.id,
|
|
})
|
|
.eq('id', incidentId)
|
|
.select()
|
|
.single();
|
|
|
|
if (error) throw error;
|
|
|
|
// Log to audit trail
|
|
const { logAdminAction } = await import('@/lib/adminActionAuditHelpers');
|
|
await logAdminAction('incident_acknowledged', {
|
|
incident_id: incidentId,
|
|
incident_number: data.incident_number,
|
|
severity: data.severity,
|
|
status_change: 'open -> investigating',
|
|
});
|
|
|
|
return data as Incident;
|
|
},
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.monitoring.incidents() });
|
|
toast.success('Incident acknowledged');
|
|
},
|
|
onError: (error) => {
|
|
console.error('Failed to acknowledge incident:', error);
|
|
toast.error('Failed to acknowledge incident');
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useResolveIncident() {
|
|
const queryClient = useQueryClient();
|
|
|
|
return useMutation({
|
|
mutationFn: async ({
|
|
incidentId,
|
|
resolutionNotes,
|
|
resolveAlerts = true,
|
|
}: {
|
|
incidentId: string;
|
|
resolutionNotes?: string;
|
|
resolveAlerts?: boolean;
|
|
}) => {
|
|
const userId = (await supabase.auth.getUser()).data.user?.id;
|
|
|
|
// Fetch incident details before resolving
|
|
const { data: incident } = await supabase
|
|
.from('incidents')
|
|
.select('incident_number, severity, alert_count')
|
|
.eq('id', incidentId)
|
|
.single();
|
|
|
|
// Update incident
|
|
const { error: incidentError } = await supabase
|
|
.from('incidents')
|
|
.update({
|
|
status: 'resolved',
|
|
resolved_at: new Date().toISOString(),
|
|
resolved_by: userId,
|
|
resolution_notes: resolutionNotes,
|
|
})
|
|
.eq('id', incidentId);
|
|
|
|
if (incidentError) throw incidentError;
|
|
|
|
// Log to audit trail
|
|
const { logAdminAction } = await import('@/lib/adminActionAuditHelpers');
|
|
await logAdminAction('incident_resolved', {
|
|
incident_id: incidentId,
|
|
incident_number: incident?.incident_number,
|
|
severity: incident?.severity,
|
|
alert_count: incident?.alert_count,
|
|
resolution_notes: resolutionNotes,
|
|
resolved_linked_alerts: resolveAlerts,
|
|
});
|
|
|
|
// Optionally resolve all linked alerts
|
|
if (resolveAlerts) {
|
|
const { data: linkedAlerts } = await supabase
|
|
.from('incident_alerts')
|
|
.select('alert_source, alert_id')
|
|
.eq('incident_id', incidentId);
|
|
|
|
if (linkedAlerts) {
|
|
for (const alert of linkedAlerts) {
|
|
const table = alert.alert_source === 'system' ? 'system_alerts' : 'rate_limit_alerts';
|
|
await supabase
|
|
.from(table)
|
|
.update({ resolved_at: new Date().toISOString() })
|
|
.eq('id', alert.alert_id);
|
|
}
|
|
}
|
|
}
|
|
|
|
return { incidentId };
|
|
},
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.monitoring.incidents() });
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.monitoring.groupedAlerts() });
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.monitoring.combinedAlerts() });
|
|
toast.success('Incident resolved');
|
|
},
|
|
onError: (error) => {
|
|
console.error('Failed to resolve incident:', error);
|
|
toast.error('Failed to resolve incident');
|
|
},
|
|
});
|
|
}
|