diff --git a/src/components/moderation/ReportsQueue.tsx b/src/components/moderation/ReportsQueue.tsx index 0cfb58d8..f9f9b013 100644 --- a/src/components/moderation/ReportsQueue.tsx +++ b/src/components/moderation/ReportsQueue.tsx @@ -333,8 +333,12 @@ export const ReportsQueue = forwardRef((props, ref) => { const handleReportAction = async (reportId: string, action: 'reviewed' | 'dismissed') => { setActionLoading(reportId); try { - // Fetch report details for audit log - const report = reports.find(r => r.id === reportId); + // Fetch full report details including reporter_id for audit log + const { data: reportData } = await supabase + .from('reports') + .select('reporter_id, reported_entity_type, reported_entity_id, reason') + .eq('id', reportId) + .single(); const { error } = await supabase .from('reports') @@ -348,17 +352,17 @@ export const ReportsQueue = forwardRef((props, ref) => { if (error) throw error; // Log audit trail for report resolution - if (user && report) { + if (user && reportData) { try { await supabase.rpc('log_admin_action', { _admin_user_id: user.id, - _target_user_id: report.reported_by, + _target_user_id: reportData.reporter_id, _action: action === 'reviewed' ? 'report_resolved' : 'report_dismissed', _details: { report_id: reportId, - reported_entity_type: report.reported_entity_type, - reported_entity_id: report.reported_entity_id, - report_reason: report.reason, + reported_entity_type: reportData.reported_entity_type, + reported_entity_id: reportData.reported_entity_id, + report_reason: reportData.reason, action: action } }); diff --git a/src/hooks/moderation/useModerationActions.ts b/src/hooks/moderation/useModerationActions.ts index 66ee8bc0..016b7a9f 100644 --- a/src/hooks/moderation/useModerationActions.ts +++ b/src/hooks/moderation/useModerationActions.ts @@ -238,14 +238,18 @@ export function useModerationActions(config: ModerationActionsConfig): Moderatio // Log audit trail for review moderation if (table === 'reviews' && user) { try { + // Extract entity information from item content + const entityType = item.content?.ride_id ? 'ride' : item.content?.park_id ? 'park' : 'unknown'; + const entityId = item.content?.ride_id || item.content?.park_id || null; + await supabase.rpc('log_admin_action', { _admin_user_id: user.id, _target_user_id: item.user_id, _action: `review_${action}`, _details: { review_id: item.id, - entity_type: item.entity_type, - entity_id: item.entity_id, + entity_type: entityType, + entity_id: entityId, moderator_notes: moderatorNotes } }); diff --git a/src/integrations/supabase/types.ts b/src/integrations/supabase/types.ts index 21bada8a..9cf528b4 100644 --- a/src/integrations/supabase/types.ts +++ b/src/integrations/supabase/types.ts @@ -3741,10 +3741,7 @@ export type Database = { Args: { target_user_id: string } Returns: undefined } - backfill_sort_orders: { - Args: Record - Returns: undefined - } + backfill_sort_orders: { Args: never; Returns: undefined } can_approve_submission_item: { Args: { item_id: string } Returns: boolean @@ -3765,10 +3762,7 @@ export type Database = { Args: { _profile_user_id: string; _viewer_id: string } Returns: boolean } - cancel_user_email_change: { - Args: { _user_id: string } - Returns: boolean - } + cancel_user_email_change: { Args: { _user_id: string }; Returns: boolean } check_rate_limit: { Args: { p_action: string @@ -3777,10 +3771,7 @@ export type Database = { } Returns: Json } - check_realtime_access: { - Args: Record - Returns: boolean - } + check_realtime_access: { Args: never; Returns: boolean } claim_next_submission: { Args: { lock_duration?: unknown; moderator_id: string } Returns: { @@ -3789,30 +3780,15 @@ export type Database = { waiting_time: unknown }[] } - cleanup_expired_sessions: { - Args: Record - Returns: undefined - } - cleanup_old_page_views: { - Args: Record - Returns: undefined - } - cleanup_old_request_metadata: { - Args: Record - Returns: undefined - } + cleanup_expired_sessions: { Args: never; Returns: undefined } + cleanup_old_page_views: { Args: never; Returns: undefined } + cleanup_old_request_metadata: { Args: never; Returns: undefined } cleanup_old_versions: { Args: { entity_type: string; keep_versions?: number } Returns: number } - cleanup_orphaned_submissions: { - Args: Record - Returns: number - } - cleanup_rate_limits: { - Args: Record - Returns: undefined - } + cleanup_orphaned_submissions: { Args: never; Returns: number } + cleanup_rate_limits: { Args: never; Returns: undefined } create_submission_with_items: { Args: { p_content: Json @@ -3830,24 +3806,15 @@ export type Database = { } Returns: string } - extract_cf_image_id: { - Args: { url: string } - Returns: string - } - generate_deletion_confirmation_code: { - Args: Record - Returns: string - } - get_email_change_status: { - Args: Record - Returns: Json - } + extract_cf_image_id: { Args: { url: string }; Returns: string } + generate_deletion_confirmation_code: { Args: never; Returns: string } + get_email_change_status: { Args: never; Returns: Json } get_filtered_profile: { Args: { _profile_user_id: string; _viewer_id?: string } Returns: Json } get_my_sessions: { - Args: Record + Args: never Returns: { aal: string created_at: string @@ -3879,18 +3846,9 @@ export type Database = { } Returns: Json } - has_aal2: { - Args: Record - Returns: boolean - } - has_mfa_enabled: { - Args: { _user_id: string } - Returns: boolean - } - has_pending_dependents: { - Args: { item_id: string } - Returns: boolean - } + has_aal2: { Args: never; Returns: boolean } + has_mfa_enabled: { Args: { _user_id: string }; Returns: boolean } + has_pending_dependents: { Args: { item_id: string }; Returns: boolean } has_role: { Args: { _role: Database["public"]["Enums"]["app_role"] @@ -3898,26 +3856,14 @@ export type Database = { } Returns: boolean } - hash_ip_address: { - Args: { ip_text: string } - Returns: string - } - hash_session_ip: { - Args: { session_ip: unknown } - Returns: string - } + hash_ip_address: { Args: { ip_text: string }; Returns: string } + hash_session_ip: { Args: { session_ip: unknown }; Returns: string } increment_blog_view_count: { Args: { post_slug: string } Returns: undefined } - is_moderator: { - Args: { _user_id: string } - Returns: boolean - } - is_superuser: { - Args: { _user_id: string } - Returns: boolean - } + is_moderator: { Args: { _user_id: string }; Returns: boolean } + is_superuser: { Args: { _user_id: string }; Returns: boolean } log_admin_action: { Args: { _action: string @@ -3927,10 +3873,7 @@ export type Database = { } Returns: undefined } - log_cleanup_results: { - Args: Record - Returns: undefined - } + log_cleanup_results: { Args: never; Returns: undefined } log_request_metadata: { Args: { p_client_version?: string @@ -3948,18 +3891,9 @@ export type Database = { } Returns: undefined } - migrate_ride_technical_data: { - Args: Record - Returns: undefined - } - migrate_user_list_items: { - Args: Record - Returns: undefined - } - release_expired_locks: { - Args: Record - Returns: number - } + migrate_ride_technical_data: { Args: never; Returns: undefined } + migrate_user_list_items: { Args: never; Returns: undefined } + release_expired_locks: { Args: never; Returns: number } release_submission_lock: { Args: { moderator_id: string; submission_id: string } Returns: boolean @@ -3968,10 +3902,7 @@ export type Database = { Args: { p_credit_id: string; p_new_position: number } Returns: undefined } - revoke_my_session: { - Args: { session_id: string } - Returns: undefined - } + revoke_my_session: { Args: { session_id: string }; Returns: undefined } revoke_session_with_mfa: { Args: { target_session_id: string; target_user_id: string } Returns: Json @@ -3998,10 +3929,7 @@ export type Database = { Args: { target_company_id: string } Returns: undefined } - update_entity_view_counts: { - Args: Record - Returns: undefined - } + update_entity_view_counts: { Args: never; Returns: undefined } update_park_ratings: { Args: { target_park_id: string } Returns: undefined diff --git a/supabase/migrations/20251027171535_1b39d879-8de0-41c1-8a39-8b893de9bd1e.sql b/supabase/migrations/20251027171535_1b39d879-8de0-41c1-8a39-8b893de9bd1e.sql new file mode 100644 index 00000000..a0dc40bc --- /dev/null +++ b/supabase/migrations/20251027171535_1b39d879-8de0-41c1-8a39-8b893de9bd1e.sql @@ -0,0 +1,153 @@ +-- Add audit logging to submission lock RPC functions + +CREATE OR REPLACE FUNCTION public.claim_next_submission( + moderator_id UUID, + lock_duration INTERVAL DEFAULT '15 minutes' +) +RETURNS TABLE ( + submission_id UUID, + submission_type TEXT, + waiting_time INTERVAL +) +LANGUAGE plpgsql +SECURITY DEFINER +SET search_path = public +AS $$ +DECLARE + claimed_submission RECORD; +BEGIN + UPDATE content_submissions + SET + assigned_to = moderator_id, + assigned_at = NOW(), + locked_until = NOW() + lock_duration, + first_reviewed_at = COALESCE(first_reviewed_at, NOW()) + WHERE id = ( + SELECT cs.id FROM content_submissions cs + WHERE cs.status IN ('pending', 'partially_approved') + AND (cs.assigned_to IS NULL OR cs.locked_until < NOW()) + ORDER BY + cs.escalated DESC, + cs.submitted_at ASC + LIMIT 1 + FOR UPDATE SKIP LOCKED + ) + RETURNING + content_submissions.id, + content_submissions.submission_type, + content_submissions.user_id, + NOW() - content_submissions.submitted_at + INTO claimed_submission; + + IF FOUND THEN + BEGIN + PERFORM log_admin_action( + moderator_id, + claimed_submission.user_id, + 'submission_claimed', + jsonb_build_object( + 'submission_id', claimed_submission.id, + 'submission_type', claimed_submission.submission_type + ) + ); + EXCEPTION WHEN OTHERS THEN + RAISE WARNING 'Failed to log submission claim audit: %', SQLERRM; + END; + END IF; + + RETURN QUERY + SELECT + claimed_submission.id, + claimed_submission.submission_type, + claimed_submission.waiting_time; +END; +$$; + +CREATE OR REPLACE FUNCTION release_submission_lock( + submission_id UUID, + moderator_id UUID +) RETURNS BOOLEAN +LANGUAGE plpgsql +SECURITY DEFINER +SET search_path = public +AS $$ +DECLARE + submission_details RECORD; +BEGIN + SELECT user_id, submission_type INTO submission_details + FROM content_submissions + WHERE id = submission_id + AND assigned_to = moderator_id; + + UPDATE content_submissions + SET + assigned_to = NULL, + assigned_at = NULL, + locked_until = NULL + WHERE id = submission_id + AND assigned_to = moderator_id; + + IF FOUND THEN + BEGIN + PERFORM log_admin_action( + moderator_id, + submission_details.user_id, + 'submission_released', + jsonb_build_object( + 'submission_id', submission_id, + 'submission_type', submission_details.submission_type + ) + ); + EXCEPTION WHEN OTHERS THEN + RAISE WARNING 'Failed to log submission release audit: %', SQLERRM; + END; + END IF; + + RETURN FOUND; +END; +$$; + +CREATE OR REPLACE FUNCTION extend_submission_lock( + submission_id UUID, + moderator_id UUID, + extension_duration INTERVAL DEFAULT '15 minutes' +) RETURNS TIMESTAMP WITH TIME ZONE +LANGUAGE plpgsql +SECURITY DEFINER +SET search_path = public +AS $$ +DECLARE + new_lock_time TIMESTAMP WITH TIME ZONE; + submission_details RECORD; +BEGIN + SELECT user_id, submission_type INTO submission_details + FROM content_submissions + WHERE id = submission_id + AND assigned_to = moderator_id; + + UPDATE content_submissions + SET locked_until = NOW() + extension_duration + WHERE id = submission_id + AND assigned_to = moderator_id + RETURNING locked_until INTO new_lock_time; + + IF FOUND THEN + BEGIN + PERFORM log_admin_action( + moderator_id, + submission_details.user_id, + 'submission_lock_extended', + jsonb_build_object( + 'submission_id', submission_id, + 'submission_type', submission_details.submission_type, + 'new_lock_until', new_lock_time + ) + ); + EXCEPTION WHEN OTHERS THEN + RAISE WARNING 'Failed to log submission lock extension audit: %', SQLERRM; + END; + END IF; + + RETURN new_lock_time; +END; +$$; \ No newline at end of file