Fix ride model submissions

Implement rate limiting, ban checks, retry logic, and breadcrumb tracking for ride model creation and update functions. Wrap existing ban checks and database operations in retry logic.
This commit is contained in:
gpt-engineer-app[bot]
2025-11-07 19:59:32 +00:00
parent 2c9358e884
commit 48c1e9cdda

View File

@@ -1755,15 +1755,30 @@ export async function submitRideModelCreation(
data: RideModelFormData, data: RideModelFormData,
userId: string userId: string
): Promise<{ submitted: boolean; submissionId: string }> { ): Promise<{ submitted: boolean; submissionId: string }> {
// Rate limiting check
checkRateLimitOrThrow(userId, 'ride_model_creation');
recordSubmissionAttempt(userId);
// Breadcrumb tracking
breadcrumb.userAction('Start ride model submission', 'submitRideModelCreation', { userId });
// Validate required fields client-side // Validate required fields client-side
assertValid(validateRideModelCreateFields(data)); assertValid(validateRideModelCreateFields(data));
// Check if user is banned // Ban check with retry logic
const { data: profile } = await supabase const { withRetry } = await import('./retryHelpers');
.from('profiles') breadcrumb.apiCall('profiles', 'SELECT');
.select('banned') const profile = await withRetry(
.eq('user_id', userId) async () => {
.single(); const { data: profile } = await supabase
.from('profiles')
.select('banned')
.eq('user_id', userId)
.single();
return profile;
},
{ maxAttempts: 2 }
);
if (profile?.banned) { if (profile?.banned) {
throw new Error('Account suspended. Contact support for assistance.'); throw new Error('Account suspended. Contact support for assistance.');
@@ -1786,88 +1801,114 @@ export async function submitRideModelCreation(
} }
} }
// Create the main submission record // Submit with retry logic
const { data: submissionData, error: submissionError } = await supabase breadcrumb.apiCall('content_submissions', 'INSERT');
.from('content_submissions') const result = await withRetry(
.insert({ async () => {
user_id: userId, // Create the main submission record
submission_type: 'ride_model', const { data: submissionData, error: submissionError } = await supabase
status: 'pending' as const .from('content_submissions')
}) .insert({
.select() user_id: userId,
.single(); submission_type: 'ride_model',
status: 'pending' as const
})
.select()
.single();
if (submissionError) throw submissionError; if (submissionError) throw submissionError;
// Create the submission item with actual ride model data // Create the submission item with actual ride model data
const { error: itemError } = await supabase const { error: itemError } = await supabase
.from('submission_items') .from('submission_items')
.insert({ .insert({
submission_id: submissionData.id, submission_id: submissionData.id,
item_type: 'ride_model', item_type: 'ride_model',
action_type: 'create', action_type: 'create',
item_data: { item_data: {
// ✅ FIXED: Don't use extractChangedFields for CREATE - include ALL data // ✅ FIXED: Don't use extractChangedFields for CREATE - include ALL data
...(() => { ...(() => {
const { images, ...dataWithoutImages } = data; const { images, ...dataWithoutImages } = data;
return dataWithoutImages; return dataWithoutImages;
})(), })(),
images: processedImages as unknown as Json images: processedImages as unknown as Json
},
status: 'pending' as const,
order_index: 0
});
if (itemError) throw itemError;
// Insert into ride_model_submissions table for relational integrity
const { data: rideModelSubmissionData, error: rideModelSubmissionError } = await supabase
.from('ride_model_submissions')
.insert({
submission_id: submissionData.id,
name: data.name,
slug: data.slug,
manufacturer_id: data.manufacturer_id,
category: data.category,
ride_type: data.ride_type || data.category,
description: data.description || null,
banner_image_url: data.banner_image_url || null,
banner_image_id: data.banner_image_id || null,
card_image_url: data.card_image_url || null,
card_image_id: data.card_image_id || null
})
.select()
.single();
if (rideModelSubmissionError) {
logger.error('Failed to insert ride model submission', { error: rideModelSubmissionError });
throw rideModelSubmissionError;
}
// Insert technical specifications into submission table
if ((data as any)._technical_specifications?.length > 0) {
const { error: techSpecError } = await supabase
.from('ride_model_submission_technical_specifications')
.insert(
(data as any)._technical_specifications.map((spec: any) => ({
ride_model_submission_id: rideModelSubmissionData.id,
spec_name: spec.spec_name,
spec_value: spec.spec_value,
spec_unit: spec.spec_unit || null,
category: spec.category || null,
display_order: spec.display_order || 0
}))
);
if (techSpecError) {
logger.error('Failed to insert ride model technical specs', { error: techSpecError });
throw techSpecError;
}
logger.log('✅ Ride model technical specifications inserted:', (data as any)._technical_specifications.length);
}
return { submitted: true, submissionId: submissionData.id };
},
{
maxAttempts: 3,
onRetry: (attempt, error, delay) => {
logger.warn('Retrying ride model submission', { attempt, delay });
window.dispatchEvent(new CustomEvent('submission-retry', {
detail: { attempt, maxAttempts: 3, delay, type: 'ride_model' }
}));
}, },
status: 'pending' as const, shouldRetry: (error) => {
order_index: 0 if (error instanceof Error) {
}); const message = error.message.toLowerCase();
if (message.includes('required')) return false;
if (itemError) throw itemError; if (message.includes('banned')) return false;
if (message.includes('slug')) return false;
// Insert into ride_model_submissions table for relational integrity }
const { data: rideModelSubmissionData, error: rideModelSubmissionError } = await supabase return isRetryableError(error);
.from('ride_model_submissions') }
.insert({
submission_id: submissionData.id,
name: data.name,
slug: data.slug,
manufacturer_id: data.manufacturer_id,
category: data.category,
ride_type: data.ride_type || data.category,
description: data.description || null,
banner_image_url: data.banner_image_url || null,
banner_image_id: data.banner_image_id || null,
card_image_url: data.card_image_url || null,
card_image_id: data.card_image_id || null
})
.select()
.single();
if (rideModelSubmissionError) {
logger.error('Failed to insert ride model submission', { error: rideModelSubmissionError });
throw rideModelSubmissionError;
}
// Insert technical specifications into submission table
if ((data as any)._technical_specifications?.length > 0) {
const { error: techSpecError } = await supabase
.from('ride_model_submission_technical_specifications')
.insert(
(data as any)._technical_specifications.map((spec: any) => ({
ride_model_submission_id: rideModelSubmissionData.id,
spec_name: spec.spec_name,
spec_value: spec.spec_value,
spec_unit: spec.spec_unit || null,
category: spec.category || null,
display_order: spec.display_order || 0
}))
);
if (techSpecError) {
logger.error('Failed to insert ride model technical specs', { error: techSpecError });
throw techSpecError;
} }
);
logger.log('✅ Ride model technical specifications inserted:', (data as any)._technical_specifications.length); return result;
}
return { submitted: true, submissionId: submissionData.id };
} }
/** /**
@@ -1881,12 +1922,27 @@ export async function submitRideModelUpdate(
data: RideModelFormData, data: RideModelFormData,
userId: string userId: string
): Promise<{ submitted: boolean; submissionId: string }> { ): Promise<{ submitted: boolean; submissionId: string }> {
// Check if user is banned // Rate limiting check
const { data: profile } = await supabase checkRateLimitOrThrow(userId, 'ride_model_update');
.from('profiles') recordSubmissionAttempt(userId);
.select('banned')
.eq('user_id', userId) // Breadcrumb tracking
.single(); breadcrumb.userAction('Start ride model update', 'submitRideModelUpdate', { userId, rideModelId });
// Ban check with retry logic
const { withRetry } = await import('./retryHelpers');
breadcrumb.apiCall('profiles', 'SELECT');
const profile = await withRetry(
async () => {
const { data: profile } = await supabase
.from('profiles')
.select('banned')
.eq('user_id', userId)
.single();
return profile;
},
{ maxAttempts: 2 }
);
if (profile?.banned) { if (profile?.banned) {
throw new Error('Account suspended. Contact support for assistance.'); throw new Error('Account suspended. Contact support for assistance.');
@@ -1909,86 +1965,112 @@ export async function submitRideModelUpdate(
let processedImages = data.images; let processedImages = data.images;
// Create the main submission record // Submit with retry logic
const { data: submissionData, error: submissionError } = await supabase breadcrumb.apiCall('content_submissions', 'INSERT');
.from('content_submissions') const result = await withRetry(
.insert({ async () => {
user_id: userId, // Create the main submission record
submission_type: 'ride_model', const { data: submissionData, error: submissionError } = await supabase
status: 'pending' as const .from('content_submissions')
}) .insert({
.select() user_id: userId,
.single(); submission_type: 'ride_model',
status: 'pending' as const
})
.select()
.single();
if (submissionError) throw submissionError; if (submissionError) throw submissionError;
// Create the submission item with actual ride model data // Create the submission item with actual ride model data
const { error: itemError } = await supabase const { error: itemError } = await supabase
.from('submission_items') .from('submission_items')
.insert({ .insert({
submission_id: submissionData.id, submission_id: submissionData.id,
item_type: 'ride_model', item_type: 'ride_model',
action_type: 'edit', action_type: 'edit',
item_data: { item_data: {
...extractChangedFields(data, existingModel as any), ...extractChangedFields(data, existingModel as any),
ride_model_id: rideModelId, // Always include for relational integrity ride_model_id: rideModelId, // Always include for relational integrity
images: processedImages as unknown as Json images: processedImages as unknown as Json
},
original_data: JSON.parse(JSON.stringify(existingModel)),
status: 'pending' as const,
order_index: 0
});
if (itemError) throw itemError;
// Insert into ride_model_submissions table for relational integrity
const { data: rideModelSubmissionData, error: rideModelSubmissionError } = await supabase
.from('ride_model_submissions')
.insert({
submission_id: submissionData.id,
name: data.name,
slug: data.slug,
manufacturer_id: data.manufacturer_id,
category: data.category,
ride_type: data.ride_type || data.category,
description: data.description || null,
banner_image_url: data.banner_image_url || null,
banner_image_id: data.banner_image_id || null,
card_image_url: data.card_image_url || null,
card_image_id: data.card_image_id || null
})
.select()
.single();
if (rideModelSubmissionError) {
logger.error('Failed to insert ride model update submission', { error: rideModelSubmissionError });
throw rideModelSubmissionError;
}
// Insert technical specifications into submission table
if ((data as any)._technical_specifications?.length > 0) {
const { error: techSpecError } = await supabase
.from('ride_model_submission_technical_specifications')
.insert(
(data as any)._technical_specifications.map((spec: any) => ({
ride_model_submission_id: rideModelSubmissionData.id,
spec_name: spec.spec_name,
spec_value: spec.spec_value,
spec_unit: spec.spec_unit || null,
category: spec.category || null,
display_order: spec.display_order || 0
}))
);
if (techSpecError) {
logger.error('Failed to insert ride model update technical specs', { error: techSpecError });
throw techSpecError;
}
logger.log('✅ Ride model update technical specifications inserted:', (data as any)._technical_specifications.length);
}
return { submitted: true, submissionId: submissionData.id };
},
{
maxAttempts: 3,
onRetry: (attempt, error, delay) => {
logger.warn('Retrying ride model update', { attempt, delay });
window.dispatchEvent(new CustomEvent('submission-retry', {
detail: { attempt, maxAttempts: 3, delay, type: 'ride_model_update' }
}));
}, },
original_data: JSON.parse(JSON.stringify(existingModel)), shouldRetry: (error) => {
status: 'pending' as const, if (error instanceof Error) {
order_index: 0 const message = error.message.toLowerCase();
}); if (message.includes('required')) return false;
if (message.includes('banned')) return false;
if (itemError) throw itemError; if (message.includes('slug')) return false;
}
// Insert into ride_model_submissions table for relational integrity return isRetryableError(error);
const { data: rideModelSubmissionData, error: rideModelSubmissionError } = await supabase }
.from('ride_model_submissions')
.insert({
submission_id: submissionData.id,
name: data.name,
slug: data.slug,
manufacturer_id: data.manufacturer_id,
category: data.category,
ride_type: data.ride_type || data.category,
description: data.description || null,
banner_image_url: data.banner_image_url || null,
banner_image_id: data.banner_image_id || null,
card_image_url: data.card_image_url || null,
card_image_id: data.card_image_id || null
})
.select()
.single();
if (rideModelSubmissionError) {
logger.error('Failed to insert ride model update submission', { error: rideModelSubmissionError });
throw rideModelSubmissionError;
}
// Insert technical specifications into submission table
if ((data as any)._technical_specifications?.length > 0) {
const { error: techSpecError } = await supabase
.from('ride_model_submission_technical_specifications')
.insert(
(data as any)._technical_specifications.map((spec: any) => ({
ride_model_submission_id: rideModelSubmissionData.id,
spec_name: spec.spec_name,
spec_value: spec.spec_value,
spec_unit: spec.spec_unit || null,
category: spec.category || null,
display_order: spec.display_order || 0
}))
);
if (techSpecError) {
logger.error('Failed to insert ride model update technical specs', { error: techSpecError });
throw techSpecError;
} }
);
logger.log('✅ Ride model update technical specifications inserted:', (data as any)._technical_specifications.length); return result;
}
return { submitted: true, submissionId: submissionData.id };
} }
/** /**