mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-23 18:11:12 -05:00
Refactor validation to edge function
Centralize all business logic validation within the edge function for the submission pipeline. Remove validation logic from React hooks, retaining only basic UX validation (e.g., checking for empty fields). This ensures a single source of truth for validation, preventing inconsistencies between the frontend and backend.
This commit is contained in:
@@ -4,7 +4,7 @@ import { supabase } from '@/lib/supabaseClient';
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
import { logger } from '@/lib/logger';
|
||||
import { getErrorMessage, handleError, isSupabaseConnectionError } from '@/lib/errorHandler';
|
||||
import { validateMultipleItems } from '@/lib/entityValidationSchemas';
|
||||
// Validation removed from client - edge function is single source of truth
|
||||
import { invokeWithTracking } from '@/lib/edgeFunctionTracking';
|
||||
import type { User } from '@supabase/supabase-js';
|
||||
import type { ModerationItem } from '@/types/moderation';
|
||||
@@ -133,206 +133,10 @@ export function useModerationActions(config: ModerationActionsConfig): Moderatio
|
||||
|
||||
if (submissionItems && submissionItems.length > 0) {
|
||||
if (action === 'approved') {
|
||||
// Fetch full item data for validation with relational joins
|
||||
const { data: fullItems, error: itemError } = await supabase
|
||||
.from('submission_items')
|
||||
.select(`
|
||||
id,
|
||||
item_type,
|
||||
park_submission:park_submissions!submission_items_park_submission_id_fkey(*),
|
||||
ride_submission:ride_submissions!submission_items_ride_submission_id_fkey(*),
|
||||
company_submission:company_submissions!submission_items_company_submission_id_fkey(*),
|
||||
ride_model_submission:ride_model_submissions!submission_items_ride_model_submission_id_fkey(*),
|
||||
timeline_event_submission:timeline_event_submissions!submission_items_timeline_event_submission_id_fkey(*),
|
||||
photo_submission:photo_submissions!submission_items_photo_submission_id_fkey(*)
|
||||
`)
|
||||
.eq('submission_id', item.id)
|
||||
.in('status', ['pending', 'rejected']);
|
||||
|
||||
if (itemError) {
|
||||
throw new Error(`Failed to fetch submission items: ${itemError.message}`);
|
||||
}
|
||||
|
||||
if (fullItems && fullItems.length > 0) {
|
||||
console.info('[Submission Flow] Preparing items for validation', {
|
||||
submissionId: item.id,
|
||||
itemCount: fullItems.length,
|
||||
itemTypes: fullItems.map(i => i.item_type),
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
// Transform to include item_data
|
||||
const itemsWithData = await Promise.all(fullItems.map(async item => {
|
||||
let itemData = {};
|
||||
switch (item.item_type) {
|
||||
case 'park': {
|
||||
const parkSub = (item.park_submission as any) || {};
|
||||
let locationData: any = null;
|
||||
if (parkSub?.id) {
|
||||
const { data } = await supabase
|
||||
.from('park_submission_locations')
|
||||
.select('*')
|
||||
.eq('park_submission_id', parkSub.id)
|
||||
.maybeSingle();
|
||||
locationData = data;
|
||||
}
|
||||
itemData = {
|
||||
...parkSub,
|
||||
location: locationData || undefined
|
||||
};
|
||||
break;
|
||||
}
|
||||
case 'ride':
|
||||
itemData = item.ride_submission || {};
|
||||
break;
|
||||
case 'operator':
|
||||
case 'manufacturer':
|
||||
case 'designer':
|
||||
case 'property_owner':
|
||||
itemData = {
|
||||
...(item.company_submission || {}),
|
||||
company_id: item.company_submission?.id // Use company_submission ID for validation
|
||||
};
|
||||
break;
|
||||
case 'ride_model':
|
||||
itemData = {
|
||||
...(item.ride_model_submission || {}),
|
||||
ride_model_id: item.ride_model_submission?.id // Use ride_model_submission ID for validation
|
||||
};
|
||||
break;
|
||||
case 'milestone':
|
||||
case 'timeline_event':
|
||||
itemData = item.timeline_event_submission || {};
|
||||
break;
|
||||
case 'photo':
|
||||
case 'photo_edit':
|
||||
case 'photo_delete':
|
||||
itemData = item.photo_submission || {};
|
||||
break;
|
||||
default:
|
||||
logger.warn(`Unknown item_type in validation: ${item.item_type}`);
|
||||
itemData = {};
|
||||
}
|
||||
return {
|
||||
id: item.id,
|
||||
item_type: item.item_type,
|
||||
item_data: itemData
|
||||
};
|
||||
}));
|
||||
|
||||
// Run validation on all items
|
||||
try {
|
||||
console.info('[Submission Flow] Starting validation', {
|
||||
submissionId: item.id,
|
||||
itemCount: itemsWithData.length,
|
||||
itemTypes: itemsWithData.map(i => i.item_type),
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
const validationResults = await validateMultipleItems(itemsWithData);
|
||||
|
||||
console.info('[Submission Flow] Validation completed', {
|
||||
submissionId: item.id,
|
||||
resultsCount: validationResults.size,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
// Check for blocking errors
|
||||
const itemsWithBlockingErrors = itemsWithData.filter(item => {
|
||||
const result = validationResults.get(item.id);
|
||||
return result && result.blockingErrors.length > 0;
|
||||
});
|
||||
|
||||
// CRITICAL: Block approval if any item has blocking errors
|
||||
if (itemsWithBlockingErrors.length > 0) {
|
||||
console.warn('[Submission Flow] Validation found blocking errors', {
|
||||
submissionId: item.id,
|
||||
itemsWithErrors: itemsWithBlockingErrors.map(i => ({
|
||||
itemId: i.id,
|
||||
itemType: i.item_type,
|
||||
errors: validationResults.get(i.id)?.blockingErrors
|
||||
})),
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
// Log detailed blocking errors
|
||||
itemsWithBlockingErrors.forEach(item => {
|
||||
const result = validationResults.get(item.id);
|
||||
logger.error('Validation blocking approval', {
|
||||
submissionId: item.id,
|
||||
itemId: item.id,
|
||||
itemType: item.item_type,
|
||||
blockingErrors: result?.blockingErrors
|
||||
});
|
||||
});
|
||||
|
||||
const errorDetails = itemsWithBlockingErrors.map(item => {
|
||||
const result = validationResults.get(item.id);
|
||||
const itemName = (item.item_data as any)?.name || item.item_type;
|
||||
const errors = result?.blockingErrors.map(e => `${e.field}: ${e.message}`).join(', ');
|
||||
return `${itemName} - ${errors}`;
|
||||
}).join('; ');
|
||||
|
||||
throw new Error(`Validation failed: ${errorDetails}`);
|
||||
}
|
||||
|
||||
// Check for warnings (optional - can proceed but inform user)
|
||||
const itemsWithWarnings = itemsWithData.filter(item => {
|
||||
const result = validationResults.get(item.id);
|
||||
return result && result.warnings.length > 0;
|
||||
});
|
||||
|
||||
if (itemsWithWarnings.length > 0) {
|
||||
logger.info('Approval proceeding with warnings', {
|
||||
submissionId: item.id,
|
||||
warningCount: itemsWithWarnings.length
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
// Check if this is a validation error or system error
|
||||
if (getErrorMessage(error).includes('Validation failed:')) {
|
||||
// This is expected - validation rules preventing approval
|
||||
handleError(error, {
|
||||
action: 'Validation Blocked Approval',
|
||||
userId: user?.id,
|
||||
metadata: {
|
||||
submissionId: item.id,
|
||||
submissionType: item.submission_type,
|
||||
selectedItemCount: itemsWithData.length
|
||||
}
|
||||
});
|
||||
|
||||
toast({
|
||||
title: 'Cannot Approve - Validation Errors',
|
||||
description: getErrorMessage(error),
|
||||
variant: 'destructive',
|
||||
});
|
||||
|
||||
// Return early - do NOT proceed with approval
|
||||
return;
|
||||
} else {
|
||||
// Unexpected validation system error
|
||||
const errorId = handleError(error, {
|
||||
action: 'Validation System Failure',
|
||||
userId: user?.id,
|
||||
metadata: {
|
||||
submissionId: item.id,
|
||||
submissionType: item.submission_type,
|
||||
phase: 'validation'
|
||||
}
|
||||
});
|
||||
|
||||
toast({
|
||||
title: 'Validation System Error',
|
||||
description: `Unable to validate submission (ref: ${errorId.slice(0, 8)})`,
|
||||
variant: 'destructive',
|
||||
});
|
||||
|
||||
// Return early - do NOT proceed with approval
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
// ⚠️ VALIDATION CENTRALIZED IN EDGE FUNCTION
|
||||
// All business logic validation happens in process-selective-approval edge function.
|
||||
// Client-side only performs basic UX validation (non-empty, format) in forms.
|
||||
// If server-side validation fails, the edge function returns detailed 400/500 errors.
|
||||
|
||||
const { data, error, requestId, attempts } = await invokeWithTracking(
|
||||
'process-selective-approval',
|
||||
@@ -465,8 +269,14 @@ export function useModerationActions(config: ModerationActionsConfig): Moderatio
|
||||
const isNetworkError = isSupabaseConnectionError(error);
|
||||
const errorMessage = getErrorMessage(error) || `Failed to ${variables.action} content`;
|
||||
|
||||
// Check if this is a validation error from edge function
|
||||
const isValidationError = errorMessage.includes('Validation failed') ||
|
||||
errorMessage.includes('blocking errors') ||
|
||||
errorMessage.includes('blockingErrors');
|
||||
|
||||
toast({
|
||||
title: isNetworkError ? 'Connection Error' : 'Action Failed',
|
||||
title: isNetworkError ? 'Connection Error' :
|
||||
isValidationError ? 'Validation Failed' : 'Action Failed',
|
||||
description: errorMessage,
|
||||
variant: 'destructive',
|
||||
});
|
||||
@@ -477,6 +287,7 @@ export function useModerationActions(config: ModerationActionsConfig): Moderatio
|
||||
error: errorMessage,
|
||||
errorId: error.errorId,
|
||||
isNetworkError,
|
||||
isValidationError,
|
||||
});
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
|
||||
Reference in New Issue
Block a user