Files
thrilltrack-explorer/src/hooks/admin/useIncidents.ts
gpt-engineer-app[bot] 8581950a6e Implement Phase 1 audit logging
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.
2025-11-11 14:22:30 +00:00

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');
},
});
}