Compare commits

...

2 Commits

Author SHA1 Message Date
gpt-engineer-app[bot]
a5fed1e26a Implement Phase 2 audit logging
Add audit logging for admin settings changes, rate limit config updates, anomaly detection config changes (skipped due to no UI), and version cleanup settings updates. Implement logging via central logAdminAction helper and integrate into AdminSettings, VersionCleanupSettings, and RateLimitAlerts mutations (create, update, delete).
2025-11-11 14:36:10 +00:00
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
9 changed files with 255 additions and 6 deletions

View File

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

View File

@@ -68,7 +68,15 @@ export function VersionCleanupSettings() {
const handleSaveRetention = async () => { const handleSaveRetention = async () => {
setIsSaving(true); setIsSaving(true);
const oldRetentionDays = retentionDays;
try { try {
// Get current value for audit log
const { data: currentSetting } = await supabase
.from('admin_settings')
.select('setting_value')
.eq('setting_key', 'version_retention_days')
.single();
const { error } = await supabase const { error } = await supabase
.from('admin_settings') .from('admin_settings')
.update({ setting_value: retentionDays.toString() }) .update({ setting_value: retentionDays.toString() })
@@ -76,6 +84,14 @@ export function VersionCleanupSettings() {
if (error) throw error; if (error) throw error;
// Log to audit trail
const { logAdminAction } = await import('@/lib/adminActionAuditHelpers');
await logAdminAction('version_cleanup_config_changed', {
setting_key: 'version_retention_days',
old_value: currentSetting?.setting_value,
new_value: retentionDays,
});
toast({ toast({
title: 'Settings Saved', title: 'Settings Saved',
description: 'Retention period updated successfully' description: 'Retention period updated successfully'

View File

@@ -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();

View File

@@ -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) {

View File

@@ -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 }) => {

View File

@@ -91,6 +91,17 @@ export function useCreateIncident() {
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;
}, },
onSuccess: (incident) => { onSuccess: (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: () => {
@@ -150,6 +171,13 @@ export function useResolveIncident() {
}) => { }) => {
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
.from('incidents') .from('incidents')
@@ -163,6 +191,17 @@ export function useResolveIncident() {
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) {
const { data: linkedAlerts } = await supabase const { data: linkedAlerts } = await supabase

View File

@@ -49,6 +49,10 @@ export function useAdminSettings() {
const updateSettingMutation = useMutation({ const updateSettingMutation = useMutation({
mutationFn: async ({ key, value }: { key: string; value: unknown }) => { mutationFn: async ({ key, value }: { key: string; value: unknown }) => {
// Get old value for audit log
const oldSetting = settings?.find(s => s.setting_key === key);
const oldValue = oldSetting?.setting_value;
const { error } = await supabase const { error } = await supabase
.from('admin_settings') .from('admin_settings')
.update({ .update({
@@ -59,10 +63,19 @@ export function useAdminSettings() {
.eq('setting_key', key); .eq('setting_key', key);
if (error) throw error; if (error) throw error;
return { key, value }; return { key, value, oldValue };
}, },
onSuccess: () => { onSuccess: async (data) => {
queryClient.invalidateQueries({ queryKey: ['admin-settings'] }); queryClient.invalidateQueries({ queryKey: ['admin-settings'] });
// Log to audit trail
const { logAdminAction } = await import('@/lib/adminActionAuditHelpers');
await logAdminAction('admin_setting_updated', {
setting_key: data.key,
old_value: data.oldValue,
new_value: data.value,
});
toast({ toast({
title: "Setting Updated", title: "Setting Updated",
description: "The setting has been saved successfully.", description: "The setting has been saved successfully.",

View File

@@ -80,6 +80,13 @@ export function useUpdateAlertConfig() {
return useMutation({ return useMutation({
mutationFn: async ({ id, updates }: { id: string; updates: Partial<AlertConfig> }) => { mutationFn: async ({ id, updates }: { id: string; updates: Partial<AlertConfig> }) => {
// Fetch old config for audit log
const { data: oldConfig } = await supabase
.from('rate_limit_alert_config')
.select('*')
.eq('id', id)
.single();
const { data, error } = await supabase const { data, error } = await supabase
.from('rate_limit_alert_config') .from('rate_limit_alert_config')
.update(updates) .update(updates)
@@ -88,10 +95,23 @@ export function useUpdateAlertConfig() {
.single(); .single();
if (error) throw error; if (error) throw error;
return data; return { data, oldConfig };
}, },
onSuccess: () => { onSuccess: async ({ data, oldConfig }) => {
queryClient.invalidateQueries({ queryKey: ['rateLimitAlertConfigs'] }); queryClient.invalidateQueries({ queryKey: ['rateLimitAlertConfigs'] });
// Log to audit trail
const { logAdminAction } = await import('@/lib/adminActionAuditHelpers');
await logAdminAction('rate_limit_config_updated', {
config_id: data.id,
metric_type: data.metric_type,
old_threshold: oldConfig?.threshold_value,
new_threshold: data.threshold_value,
old_enabled: oldConfig?.enabled,
new_enabled: data.enabled,
function_name: data.function_name,
});
toast.success('Alert configuration updated'); toast.success('Alert configuration updated');
}, },
onError: (error) => { onError: (error) => {
@@ -114,8 +134,20 @@ export function useCreateAlertConfig() {
if (error) throw error; if (error) throw error;
return data; return data;
}, },
onSuccess: () => { onSuccess: async (data) => {
queryClient.invalidateQueries({ queryKey: ['rateLimitAlertConfigs'] }); queryClient.invalidateQueries({ queryKey: ['rateLimitAlertConfigs'] });
// Log to audit trail
const { logAdminAction } = await import('@/lib/adminActionAuditHelpers');
await logAdminAction('rate_limit_config_created', {
config_id: data.id,
metric_type: data.metric_type,
threshold_value: data.threshold_value,
time_window_ms: data.time_window_ms,
function_name: data.function_name,
enabled: data.enabled,
});
toast.success('Alert configuration created'); toast.success('Alert configuration created');
}, },
onError: (error) => { onError: (error) => {
@@ -129,15 +161,36 @@ export function useDeleteAlertConfig() {
return useMutation({ return useMutation({
mutationFn: async (id: string) => { mutationFn: async (id: string) => {
// Fetch config details before deletion for audit log
const { data: config } = await supabase
.from('rate_limit_alert_config')
.select('*')
.eq('id', id)
.single();
const { error } = await supabase const { error } = await supabase
.from('rate_limit_alert_config') .from('rate_limit_alert_config')
.delete() .delete()
.eq('id', id); .eq('id', id);
if (error) throw error; if (error) throw error;
return config;
}, },
onSuccess: () => { onSuccess: async (config) => {
queryClient.invalidateQueries({ queryKey: ['rateLimitAlertConfigs'] }); queryClient.invalidateQueries({ queryKey: ['rateLimitAlertConfigs'] });
// Log to audit trail
if (config) {
const { logAdminAction } = await import('@/lib/adminActionAuditHelpers');
await logAdminAction('rate_limit_config_deleted', {
config_id: config.id,
metric_type: config.metric_type,
threshold_value: config.threshold_value,
time_window_ms: config.time_window_ms,
function_name: config.function_name,
});
}
toast.success('Alert configuration deleted'); toast.success('Alert configuration deleted');
}, },
onError: (error) => { onError: (error) => {
@@ -151,6 +204,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 +222,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: () => {

View 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 }
});
}
}