From 321014765405183aee806231bb1dbd1cdcbe82f6 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Mon, 6 Oct 2025 17:01:41 +0000 Subject: [PATCH] Fix: Add dashboard widget for flow violations --- src/components/admin/RideForm.tsx | 5 ++ src/pages/AdminDashboard.tsx | 37 ++++++++- ...5_191c398e-46f8-4b77-8d8d-8e87ebba25cd.sql | 77 +++++++++++++++++++ 3 files changed, 117 insertions(+), 2 deletions(-) create mode 100644 supabase/migrations/20251006170055_191c398e-46f8-4b77-8d8d-8e87ebba25cd.sql diff --git a/src/components/admin/RideForm.tsx b/src/components/admin/RideForm.tsx index f465fbb1..5b8fc437 100644 --- a/src/components/admin/RideForm.tsx +++ b/src/components/admin/RideForm.tsx @@ -155,6 +155,11 @@ export function RideForm({ onSubmit, onCancel, initialData, isEditing = false }: const { preferences } = useUnitPreferences(); const measurementSystem = preferences.measurement_system; + // Validate that onSubmit uses submission helpers (dev mode only) + useEffect(() => { + validateSubmissionHandler(onSubmit, 'ride'); + }, [onSubmit]); + // Manufacturer and model state const [selectedManufacturerId, setSelectedManufacturerId] = useState( initialData?.manufacturer_id || '' diff --git a/src/pages/AdminDashboard.tsx b/src/pages/AdminDashboard.tsx index 712798ff..9ba1bc97 100644 --- a/src/pages/AdminDashboard.tsx +++ b/src/pages/AdminDashboard.tsx @@ -1,6 +1,6 @@ import { useRef, useEffect, useCallback, useState } from 'react'; import { useNavigate } from 'react-router-dom'; -import { FileText, Flag, AlertCircle, Activity } from 'lucide-react'; +import { FileText, Flag, AlertCircle, Activity, ShieldAlert } from 'lucide-react'; import { useUserRole } from '@/hooks/useUserRole'; import { useAuth } from '@/hooks/useAuth'; import { Card, CardContent } from '@/components/ui/card'; @@ -12,6 +12,8 @@ import { ReportsQueue } from '@/components/moderation/ReportsQueue'; import { RecentActivity } from '@/components/moderation/RecentActivity'; import { useModerationStats } from '@/hooks/useModerationStats'; import { useAdminSettings } from '@/hooks/useAdminSettings'; +import { supabase } from '@/integrations/supabase/client'; +import { Alert, AlertDescription } from '@/components/ui/alert'; export default function AdminDashboard() { const { user, loading: authLoading } = useAuth(); @@ -19,6 +21,7 @@ export default function AdminDashboard() { const navigate = useNavigate(); const [isRefreshing, setIsRefreshing] = useState(false); const [activeTab, setActiveTab] = useState('moderation'); + const [suspiciousVersionsCount, setSuspiciousVersionsCount] = useState(0); const moderationQueueRef = useRef(null); const reportsQueueRef = useRef(null); @@ -38,9 +41,28 @@ export default function AdminDashboard() { pollingInterval: pollInterval, }); + // Check for suspicious versions (bypassed submission flow) + const checkSuspiciousVersions = useCallback(async () => { + if (!user || !isModerator()) return; + + const { count, error } = await supabase + .from('entity_versions') + .select('*', { count: 'exact', head: true }) + .is('changed_by', null); + + if (!error && count !== null) { + setSuspiciousVersionsCount(count); + } + }, [user, isModerator]); + + useEffect(() => { + checkSuspiciousVersions(); + }, [checkSuspiciousVersions]); + const handleRefresh = useCallback(async () => { setIsRefreshing(true); await refreshStats(); + await checkSuspiciousVersions(); // Refresh active tab's content switch (activeTab) { @@ -56,7 +78,7 @@ export default function AdminDashboard() { } setTimeout(() => setIsRefreshing(false), 500); - }, [refreshStats, activeTab]); + }, [refreshStats, checkSuspiciousVersions, activeTab]); const handleStatCardClick = (cardType: 'submissions' | 'reports' | 'flagged') => { switch (cardType) { @@ -143,6 +165,17 @@ export default function AdminDashboard() {

+ {/* Security Warning for Suspicious Versions */} + {suspiciousVersionsCount > 0 && ( + + + + Security Alert: {suspiciousVersionsCount} entity version{suspiciousVersionsCount !== 1 ? 's' : ''} detected without user attribution. + This may indicate submission flow bypass. Check admin audit logs for details. + + + )} +
{statCards.map((card) => { const Icon = card.icon; diff --git a/supabase/migrations/20251006170055_191c398e-46f8-4b77-8d8d-8e87ebba25cd.sql b/supabase/migrations/20251006170055_191c398e-46f8-4b77-8d8d-8e87ebba25cd.sql new file mode 100644 index 00000000..bf632697 --- /dev/null +++ b/supabase/migrations/20251006170055_191c398e-46f8-4b77-8d8d-8e87ebba25cd.sql @@ -0,0 +1,77 @@ +-- Update auto_create_entity_version to log suspicious versions without user attribution +CREATE OR REPLACE FUNCTION public.auto_create_entity_version() +RETURNS TRIGGER +LANGUAGE plpgsql +SECURITY DEFINER +SET search_path = public +AS $$ +DECLARE + v_entity_type TEXT; + v_change_type version_change_type; + v_user_id UUID; + v_version_data JSONB; +BEGIN + -- Determine entity type from table name + v_entity_type := CASE TG_TABLE_NAME + WHEN 'parks' THEN 'park' + WHEN 'rides' THEN 'ride' + WHEN 'companies' THEN 'company' + WHEN 'ride_models' THEN 'ride_model' + WHEN 'photos' THEN 'photo' + ELSE substring(TG_TABLE_NAME from 1 for length(TG_TABLE_NAME) - 1) + END; + + -- Determine change type + v_change_type := CASE TG_OP + WHEN 'INSERT' THEN 'created'::version_change_type + WHEN 'UPDATE' THEN 'updated'::version_change_type + ELSE 'updated'::version_change_type + END; + + -- Get user from session or auth context + BEGIN + v_user_id := current_setting('app.current_user_id', true)::UUID; + EXCEPTION WHEN OTHERS THEN + v_user_id := auth.uid(); + END; + + -- Convert NEW record to JSONB + v_version_data := to_jsonb(NEW); + + -- Create version (only if we have a user context) + IF v_user_id IS NOT NULL THEN + PERFORM public.create_entity_version( + v_entity_type, + NEW.id, + v_version_data, + v_user_id, + CASE TG_OP + WHEN 'INSERT' THEN 'Entity created' + WHEN 'UPDATE' THEN 'Entity updated' + ELSE 'Entity modified' + END, + NULL, + v_change_type + ); + ELSE + -- Log suspicious version without user (audit trail) + INSERT INTO public.admin_audit_log ( + action, + details, + created_at + ) VALUES ( + 'version_without_user', + jsonb_build_object( + 'entity_type', v_entity_type, + 'entity_id', NEW.id, + 'table', TG_TABLE_NAME, + 'operation', TG_OP, + 'timestamp', NOW() + ), + NOW() + ); + END IF; + + RETURN NEW; +END; +$$; \ No newline at end of file