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