diff --git a/src/components/admin/PipelineHealthAlerts.tsx b/src/components/admin/PipelineHealthAlerts.tsx index 4d336f39..bf54f7a1 100644 --- a/src/components/admin/PipelineHealthAlerts.tsx +++ b/src/components/admin/PipelineHealthAlerts.tsx @@ -16,6 +16,7 @@ import { supabase } from '@/lib/supabaseClient'; import { toast } from 'sonner'; import { useQueryClient } from '@tanstack/react-query'; import { queryKeys } from '@/lib/queryKeys'; +import { logAdminAction } from '@/lib/adminActionAuditHelpers'; const SEVERITY_CONFIG = { critical: { color: 'destructive', icon: XCircle }, @@ -58,6 +59,9 @@ export function PipelineHealthAlerts() { setResolvingAlertId(alertId); try { + // Fetch alert details before resolving + const alertToResolve = allAlerts.find(a => a.id === alertId); + const { error } = await supabase .from('system_alerts') .update({ resolved_at: new Date().toISOString() }) @@ -72,6 +76,17 @@ export function PipelineHealthAlerts() { console.log('✅ Alert resolved successfully'); toast.success('Alert resolved'); + // Log to audit trail + if (alertToResolve) { + await logAdminAction('system_alert_resolved', { + alert_id: alertToResolve.id, + alert_type: alertToResolve.alert_type, + severity: alertToResolve.severity, + message: alertToResolve.message, + metadata: alertToResolve.metadata, + }); + } + // Invalidate all system-alerts queries (critical, high, medium, etc.) await Promise.all([ queryClient.invalidateQueries({ queryKey: ['system-alerts'] }), diff --git a/src/components/moderation/ModerationQueue.tsx b/src/components/moderation/ModerationQueue.tsx index b5d8f156..7cfa0026 100644 --- a/src/components/moderation/ModerationQueue.tsx +++ b/src/components/moderation/ModerationQueue.tsx @@ -262,7 +262,23 @@ export const ModerationQueue = forwardRef { + // Fetch lock details before releasing + const { data: submission } = await supabase + .from('content_submissions') + .select('assigned_to, locked_until') + .eq('id', submissionId) + .single(); + await queueManager.queue.superuserReleaseLock(submissionId); + + // Log to audit trail + const { logAdminAction } = await import('@/lib/adminActionAuditHelpers'); + await logAdminAction('moderation_lock_force_released', { + submission_id: submissionId, + original_moderator_id: submission?.assigned_to, + original_locked_until: submission?.locked_until, + }); + // Refresh locks count and queue setActiveLocksCount(prev => Math.max(0, prev - 1)); queueManager.refresh(); diff --git a/src/components/moderation/UserRoleManager.tsx b/src/components/moderation/UserRoleManager.tsx index 5ba81975..bc5a6572 100644 --- a/src/components/moderation/UserRoleManager.tsx +++ b/src/components/moderation/UserRoleManager.tsx @@ -189,6 +189,15 @@ export function UserRoleManager() { if (error) throw error; + // Log to audit trail + const { logAdminAction } = await import('@/lib/adminActionAuditHelpers'); + const targetUsername = searchResults.find(p => p.user_id === userId)?.username; + await logAdminAction('role_granted', { + target_user_id: userId, + target_username: targetUsername, + role: role, + }, userId); + handleSuccess('Role Granted', `User has been granted ${getRoleLabel(role)} role`); setNewUserSearch(''); setNewRole(''); @@ -208,10 +217,23 @@ export function UserRoleManager() { if (!isAdmin()) return; setActionLoading(roleId); try { + // Fetch role details before revoking + const roleToRevoke = userRoles.find(r => r.id === roleId); + const { error } = await supabase.from('user_roles').delete().eq('id', roleId); if (error) throw error; + + // Log to audit trail + const { logAdminAction } = await import('@/lib/adminActionAuditHelpers'); + await logAdminAction('role_revoked', { + role_id: roleId, + target_user_id: roleToRevoke?.user_id, + target_username: roleToRevoke?.profiles?.username, + role: roleToRevoke?.role, + }, roleToRevoke?.user_id); + handleSuccess('Role Revoked', 'User role has been revoked'); fetchUserRoles(); } catch (error: unknown) { diff --git a/src/hooks/admin/useAlertGroupActions.ts b/src/hooks/admin/useAlertGroupActions.ts index 45b32b5d..9963a5c7 100644 --- a/src/hooks/admin/useAlertGroupActions.ts +++ b/src/hooks/admin/useAlertGroupActions.ts @@ -44,6 +44,14 @@ export function useResolveAlertGroup() { } } + // Log to audit trail + const { logAdminAction } = await import('@/lib/adminActionAuditHelpers'); + await logAdminAction('alert_group_resolved', { + alert_source: source, + alert_count: alertIds.length, + alert_ids: alertIds, + }); + return { count: alertIds.length, updatedAlerts: data }; }, onMutate: async ({ alertIds }) => { diff --git a/src/hooks/admin/useIncidents.ts b/src/hooks/admin/useIncidents.ts index 44b25c50..e705c489 100644 --- a/src/hooks/admin/useIncidents.ts +++ b/src/hooks/admin/useIncidents.ts @@ -90,6 +90,17 @@ export function useCreateIncident() { .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; }, @@ -122,6 +133,16 @@ export function useAcknowledgeIncident() { .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: () => { @@ -149,6 +170,13 @@ export function useResolveIncident() { 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 @@ -162,6 +190,17 @@ export function useResolveIncident() { .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) { diff --git a/src/hooks/useRateLimitAlerts.ts b/src/hooks/useRateLimitAlerts.ts index 8a8bed15..afdc29cd 100644 --- a/src/hooks/useRateLimitAlerts.ts +++ b/src/hooks/useRateLimitAlerts.ts @@ -151,6 +151,16 @@ export function useResolveAlert() { return useMutation({ mutationFn: async (id: string) => { + // Fetch full alert details before resolving + const { data: alert, error: fetchError } = await supabase + .from('rate_limit_alerts') + .select('*') + .eq('id', id) + .single(); + + if (fetchError) throw fetchError; + + // Resolve the alert const { data, error } = await supabase .from('rate_limit_alerts') .update({ resolved_at: new Date().toISOString() }) @@ -159,6 +169,18 @@ export function useResolveAlert() { .single(); if (error) throw error; + + // Log to audit trail + const { logAdminAction } = await import('@/lib/adminActionAuditHelpers'); + await logAdminAction('rate_limit_alert_resolved', { + alert_id: id, + metric_type: alert.metric_type, + metric_value: alert.metric_value, + threshold_value: alert.threshold_value, + function_name: alert.function_name, + time_window_ms: alert.time_window_ms, + }); + return data; }, onSuccess: () => { diff --git a/src/lib/adminActionAuditHelpers.ts b/src/lib/adminActionAuditHelpers.ts new file mode 100644 index 00000000..5ced98eb --- /dev/null +++ b/src/lib/adminActionAuditHelpers.ts @@ -0,0 +1,45 @@ +/** + * Centralized audit logging for all admin/moderator/superuser actions + * + * This ensures consistent logging across the application and provides + * a single point of maintenance for audit trail functionality. + */ + +import { supabase } from '@/lib/supabaseClient'; +import { handleNonCriticalError } from '@/lib/errorHandler'; + +/** + * Log any admin/moderator/superuser action to the audit trail + * + * @param action - The action being performed (e.g., 'system_alert_resolved', 'role_granted') + * @param details - Key-value pairs with action-specific details + * @param targetUserId - The user affected by this action (optional, defaults to admin user) + */ +export async function logAdminAction( + action: string, + details: Record, + targetUserId?: string +): Promise { + try { + const { data: { user } } = await supabase.auth.getUser(); + if (!user) { + console.warn('Cannot log admin action: No authenticated user', { action, details }); + return; + } + + await supabase.rpc('log_admin_action', { + _admin_user_id: user.id, + _target_user_id: targetUserId || user.id, + _action: action, + _details: details + }); + + console.log('✅ Admin action logged:', { action, targetUserId, hasDetails: Object.keys(details).length > 0 }); + } catch (error) { + // Log error but don't throw - audit logging shouldn't block operations + handleNonCriticalError(error, { + action: 'Log admin action', + metadata: { adminAction: action, details } + }); + } +}