mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-22 05:51:12 -05:00
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.
This commit is contained in:
@@ -16,6 +16,7 @@ import { supabase } from '@/lib/supabaseClient';
|
|||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
import { queryKeys } from '@/lib/queryKeys';
|
import { queryKeys } from '@/lib/queryKeys';
|
||||||
|
import { logAdminAction } from '@/lib/adminActionAuditHelpers';
|
||||||
|
|
||||||
const SEVERITY_CONFIG = {
|
const SEVERITY_CONFIG = {
|
||||||
critical: { color: 'destructive', icon: XCircle },
|
critical: { color: 'destructive', icon: XCircle },
|
||||||
@@ -58,6 +59,9 @@ export function PipelineHealthAlerts() {
|
|||||||
setResolvingAlertId(alertId);
|
setResolvingAlertId(alertId);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Fetch alert details before resolving
|
||||||
|
const alertToResolve = allAlerts.find(a => a.id === alertId);
|
||||||
|
|
||||||
const { error } = await supabase
|
const { error } = await supabase
|
||||||
.from('system_alerts')
|
.from('system_alerts')
|
||||||
.update({ resolved_at: new Date().toISOString() })
|
.update({ resolved_at: new Date().toISOString() })
|
||||||
@@ -72,6 +76,17 @@ export function PipelineHealthAlerts() {
|
|||||||
console.log('✅ Alert resolved successfully');
|
console.log('✅ Alert resolved successfully');
|
||||||
toast.success('Alert resolved');
|
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.)
|
// Invalidate all system-alerts queries (critical, high, medium, etc.)
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
queryClient.invalidateQueries({ queryKey: ['system-alerts'] }),
|
queryClient.invalidateQueries({ queryKey: ['system-alerts'] }),
|
||||||
|
|||||||
@@ -262,7 +262,23 @@ export const ModerationQueue = forwardRef<ModerationQueueRef, ModerationQueuePro
|
|||||||
|
|
||||||
// Superuser force release lock
|
// Superuser force release lock
|
||||||
const handleSuperuserReleaseLock = useCallback(async (submissionId: string) => {
|
const handleSuperuserReleaseLock = useCallback(async (submissionId: string) => {
|
||||||
|
// 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);
|
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
|
// Refresh locks count and queue
|
||||||
setActiveLocksCount(prev => Math.max(0, prev - 1));
|
setActiveLocksCount(prev => Math.max(0, prev - 1));
|
||||||
queueManager.refresh();
|
queueManager.refresh();
|
||||||
|
|||||||
@@ -189,6 +189,15 @@ export function UserRoleManager() {
|
|||||||
|
|
||||||
if (error) throw error;
|
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`);
|
handleSuccess('Role Granted', `User has been granted ${getRoleLabel(role)} role`);
|
||||||
setNewUserSearch('');
|
setNewUserSearch('');
|
||||||
setNewRole('');
|
setNewRole('');
|
||||||
@@ -208,10 +217,23 @@ export function UserRoleManager() {
|
|||||||
if (!isAdmin()) return;
|
if (!isAdmin()) return;
|
||||||
setActionLoading(roleId);
|
setActionLoading(roleId);
|
||||||
try {
|
try {
|
||||||
|
// Fetch role details before revoking
|
||||||
|
const roleToRevoke = userRoles.find(r => r.id === roleId);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
error
|
error
|
||||||
} = await supabase.from('user_roles').delete().eq('id', roleId);
|
} = await supabase.from('user_roles').delete().eq('id', roleId);
|
||||||
if (error) throw error;
|
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');
|
handleSuccess('Role Revoked', 'User role has been revoked');
|
||||||
fetchUserRoles();
|
fetchUserRoles();
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
|
|||||||
@@ -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 };
|
return { count: alertIds.length, updatedAlerts: data };
|
||||||
},
|
},
|
||||||
onMutate: async ({ alertIds }) => {
|
onMutate: async ({ alertIds }) => {
|
||||||
|
|||||||
@@ -90,6 +90,17 @@ export function useCreateIncident() {
|
|||||||
.insert(incidentAlerts);
|
.insert(incidentAlerts);
|
||||||
|
|
||||||
if (linkError) throw linkError;
|
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;
|
return incident as Incident;
|
||||||
},
|
},
|
||||||
@@ -122,6 +133,16 @@ export function useAcknowledgeIncident() {
|
|||||||
.single();
|
.single();
|
||||||
|
|
||||||
if (error) throw error;
|
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;
|
return data as Incident;
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
@@ -149,6 +170,13 @@ export function useResolveIncident() {
|
|||||||
resolveAlerts?: boolean;
|
resolveAlerts?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const userId = (await supabase.auth.getUser()).data.user?.id;
|
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
|
// Update incident
|
||||||
const { error: incidentError } = await supabase
|
const { error: incidentError } = await supabase
|
||||||
@@ -162,6 +190,17 @@ export function useResolveIncident() {
|
|||||||
.eq('id', incidentId);
|
.eq('id', incidentId);
|
||||||
|
|
||||||
if (incidentError) throw incidentError;
|
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
|
// Optionally resolve all linked alerts
|
||||||
if (resolveAlerts) {
|
if (resolveAlerts) {
|
||||||
|
|||||||
@@ -151,6 +151,16 @@ export function useResolveAlert() {
|
|||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: async (id: string) => {
|
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
|
const { data, error } = await supabase
|
||||||
.from('rate_limit_alerts')
|
.from('rate_limit_alerts')
|
||||||
.update({ resolved_at: new Date().toISOString() })
|
.update({ resolved_at: new Date().toISOString() })
|
||||||
@@ -159,6 +169,18 @@ export function useResolveAlert() {
|
|||||||
.single();
|
.single();
|
||||||
|
|
||||||
if (error) throw error;
|
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;
|
return data;
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
|
|||||||
45
src/lib/adminActionAuditHelpers.ts
Normal file
45
src/lib/adminActionAuditHelpers.ts
Normal file
@@ -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<string, any>,
|
||||||
|
targetUserId?: string
|
||||||
|
): Promise<void> {
|
||||||
|
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 }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user