From e747e1f881a860818bbccfb8c75c28eac993c53b Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Fri, 7 Nov 2025 01:02:58 +0000 Subject: [PATCH] Implement RLS and security functions Apply Row Level Security to orphaned_images and system_alerts tables. Create RLS policies for admin/moderator access. Replace system_health view with get_system_health() function. --- src/hooks/useSystemHealth.ts | 129 ++++++++++++++++++ src/integrations/supabase/types.ts | 18 +-- src/lib/imageUploadHelper.ts | 43 ++++-- src/lib/integrationTests/suites/authTests.ts | 4 +- .../suites/performanceTests.ts | 2 +- ...7_76e982f0-45cb-4201-b306-d935c61776a9.sql | 91 ++++++++++++ 6 files changed, 265 insertions(+), 22 deletions(-) create mode 100644 src/hooks/useSystemHealth.ts create mode 100644 supabase/migrations/20251107010017_76e982f0-45cb-4201-b306-d935c61776a9.sql diff --git a/src/hooks/useSystemHealth.ts b/src/hooks/useSystemHealth.ts new file mode 100644 index 00000000..9a606741 --- /dev/null +++ b/src/hooks/useSystemHealth.ts @@ -0,0 +1,129 @@ +import { useQuery } from '@tanstack/react-query'; +import { supabase } from '@/lib/supabaseClient'; +import { handleError } from '@/lib/errorHandler'; + +interface SystemHealthData { + orphaned_images_count: number; + critical_alerts_count: number; + alerts_last_24h: number; + checked_at: string; +} + +interface SystemAlert { + id: string; + alert_type: 'orphaned_images' | 'stale_submissions' | 'circular_dependency' | 'validation_error' | 'ban_attempt' | 'upload_timeout' | 'high_error_rate'; + severity: 'low' | 'medium' | 'high' | 'critical'; + message: string; + metadata: Record | null; + resolved_at: string | null; + created_at: string; +} + +/** + * Hook to fetch system health metrics + * Only accessible to moderators and admins + */ +export function useSystemHealth() { + return useQuery({ + queryKey: ['system-health'], + queryFn: async () => { + try { + const { data, error } = await supabase + .rpc('get_system_health'); + + if (error) { + handleError(error, { + action: 'Fetch System Health', + metadata: { error: error.message } + }); + throw error; + } + + return data?.[0] as SystemHealthData | null; + } catch (error) { + handleError(error, { + action: 'Fetch System Health', + metadata: { error: String(error) } + }); + throw error; + } + }, + refetchInterval: 60000, // Refetch every minute + staleTime: 30000, // Consider data stale after 30 seconds + }); +} + +/** + * Hook to fetch unresolved system alerts + * Only accessible to moderators and admins + */ +export function useSystemAlerts(severity?: 'low' | 'medium' | 'high' | 'critical') { + return useQuery({ + queryKey: ['system-alerts', severity], + queryFn: async () => { + try { + let query = supabase + .from('system_alerts') + .select('*') + .is('resolved_at', null) + .order('created_at', { ascending: false }); + + if (severity) { + query = query.eq('severity', severity); + } + + const { data, error } = await query; + + if (error) { + handleError(error, { + action: 'Fetch System Alerts', + metadata: { severity, error: error.message } + }); + throw error; + } + + return (data || []) as SystemAlert[]; + } catch (error) { + handleError(error, { + action: 'Fetch System Alerts', + metadata: { severity, error: String(error) } + }); + throw error; + } + }, + refetchInterval: 30000, // Refetch every 30 seconds + staleTime: 15000, // Consider data stale after 15 seconds + }); +} + +/** + * Hook to run system maintenance manually + * Only accessible to admins + */ +export function useRunSystemMaintenance() { + return async () => { + try { + const { data, error } = await supabase.rpc('run_system_maintenance'); + + if (error) { + handleError(error, { + action: 'Run System Maintenance', + metadata: { error: error.message } + }); + throw error; + } + + return data as Array<{ + task: string; + status: 'success' | 'error'; + details: Record; + }>; + } catch (error) { + handleError(error, { + action: 'Run System Maintenance', + metadata: { error: String(error) } + }); + throw error; + } + }; +} diff --git a/src/integrations/supabase/types.ts b/src/integrations/supabase/types.ts index cff8ca73..2e00f1bc 100644 --- a/src/integrations/supabase/types.ts +++ b/src/integrations/supabase/types.ts @@ -5989,15 +5989,6 @@ export type Database = { } Relationships: [] } - system_health: { - Row: { - alerts_last_24h: number | null - checked_at: string | null - critical_alerts_count: number | null - orphaned_images_count: number | null - } - Relationships: [] - } } Functions: { anonymize_user_submissions: { @@ -6221,6 +6212,15 @@ export type Database = { updated_at: string }[] } + get_system_health: { + Args: never + Returns: { + alerts_last_24h: number + checked_at: string + critical_alerts_count: number + orphaned_images_count: number + }[] + } get_user_management_permissions: { Args: { _user_id: string } Returns: Json diff --git a/src/lib/imageUploadHelper.ts b/src/lib/imageUploadHelper.ts index a75b1151..4b9f8a55 100644 --- a/src/lib/imageUploadHelper.ts +++ b/src/lib/imageUploadHelper.ts @@ -16,6 +16,21 @@ interface UploadedImageWithFlag extends UploadedImage { wasNewlyUploaded?: boolean; } +// Upload timeout in milliseconds (30 seconds) +const UPLOAD_TIMEOUT_MS = 30000; + +/** + * Creates a promise that rejects after a timeout + */ +function withTimeout(promise: Promise, timeoutMs: number, operation: string): Promise { + return Promise.race([ + promise, + new Promise((_, reject) => + setTimeout(() => reject(new Error(`${operation} timed out after ${timeoutMs}ms`)), timeoutMs) + ) + ]); +} + /** * Uploads pending local images to Cloudflare via Supabase Edge Function * @param images Array of UploadedImage objects (mix of local and already uploaded) @@ -27,10 +42,14 @@ export async function uploadPendingImages(images: UploadedImage[]): Promise now() - interval '24 hours')::BIGINT as alerts_last_24h, + now() as checked_at; +END; +$$; + +GRANT EXECUTE ON FUNCTION get_system_health() TO authenticated; \ No newline at end of file