mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 13:51:13 -05:00
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:
@@ -1755,15 +1755,30 @@ export async function submitRideModelCreation(
|
||||
data: RideModelFormData,
|
||||
userId: 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
|
||||
assertValid(validateRideModelCreateFields(data));
|
||||
|
||||
// Check if user is banned
|
||||
const { data: profile } = await supabase
|
||||
.from('profiles')
|
||||
.select('banned')
|
||||
.eq('user_id', userId)
|
||||
.single();
|
||||
// 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) {
|
||||
throw new Error('Account suspended. Contact support for assistance.');
|
||||
@@ -1786,88 +1801,114 @@ export async function submitRideModelCreation(
|
||||
}
|
||||
}
|
||||
|
||||
// Create the main submission record
|
||||
const { data: submissionData, error: submissionError } = await supabase
|
||||
.from('content_submissions')
|
||||
.insert({
|
||||
user_id: userId,
|
||||
submission_type: 'ride_model',
|
||||
status: 'pending' as const
|
||||
})
|
||||
.select()
|
||||
.single();
|
||||
// Submit with retry logic
|
||||
breadcrumb.apiCall('content_submissions', 'INSERT');
|
||||
const result = await withRetry(
|
||||
async () => {
|
||||
// Create the main submission record
|
||||
const { data: submissionData, error: submissionError } = await supabase
|
||||
.from('content_submissions')
|
||||
.insert({
|
||||
user_id: userId,
|
||||
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
|
||||
const { error: itemError } = await supabase
|
||||
.from('submission_items')
|
||||
.insert({
|
||||
submission_id: submissionData.id,
|
||||
item_type: 'ride_model',
|
||||
action_type: 'create',
|
||||
item_data: {
|
||||
// ✅ FIXED: Don't use extractChangedFields for CREATE - include ALL data
|
||||
...(() => {
|
||||
const { images, ...dataWithoutImages } = data;
|
||||
return dataWithoutImages;
|
||||
})(),
|
||||
images: processedImages as unknown as Json
|
||||
// Create the submission item with actual ride model data
|
||||
const { error: itemError } = await supabase
|
||||
.from('submission_items')
|
||||
.insert({
|
||||
submission_id: submissionData.id,
|
||||
item_type: 'ride_model',
|
||||
action_type: 'create',
|
||||
item_data: {
|
||||
// ✅ FIXED: Don't use extractChangedFields for CREATE - include ALL data
|
||||
...(() => {
|
||||
const { images, ...dataWithoutImages } = data;
|
||||
return dataWithoutImages;
|
||||
})(),
|
||||
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,
|
||||
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;
|
||||
shouldRetry: (error) => {
|
||||
if (error instanceof Error) {
|
||||
const message = error.message.toLowerCase();
|
||||
if (message.includes('required')) return false;
|
||||
if (message.includes('banned')) return false;
|
||||
if (message.includes('slug')) return false;
|
||||
}
|
||||
return isRetryableError(error);
|
||||
}
|
||||
}
|
||||
|
||||
logger.log('✅ Ride model technical specifications inserted:', (data as any)._technical_specifications.length);
|
||||
}
|
||||
);
|
||||
|
||||
return { submitted: true, submissionId: submissionData.id };
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1881,12 +1922,27 @@ export async function submitRideModelUpdate(
|
||||
data: RideModelFormData,
|
||||
userId: string
|
||||
): Promise<{ submitted: boolean; submissionId: string }> {
|
||||
// Check if user is banned
|
||||
const { data: profile } = await supabase
|
||||
.from('profiles')
|
||||
.select('banned')
|
||||
.eq('user_id', userId)
|
||||
.single();
|
||||
// Rate limiting check
|
||||
checkRateLimitOrThrow(userId, 'ride_model_update');
|
||||
recordSubmissionAttempt(userId);
|
||||
|
||||
// Breadcrumb tracking
|
||||
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) {
|
||||
throw new Error('Account suspended. Contact support for assistance.');
|
||||
@@ -1909,86 +1965,112 @@ export async function submitRideModelUpdate(
|
||||
|
||||
let processedImages = data.images;
|
||||
|
||||
// Create the main submission record
|
||||
const { data: submissionData, error: submissionError } = await supabase
|
||||
.from('content_submissions')
|
||||
.insert({
|
||||
user_id: userId,
|
||||
submission_type: 'ride_model',
|
||||
status: 'pending' as const
|
||||
})
|
||||
.select()
|
||||
.single();
|
||||
// Submit with retry logic
|
||||
breadcrumb.apiCall('content_submissions', 'INSERT');
|
||||
const result = await withRetry(
|
||||
async () => {
|
||||
// Create the main submission record
|
||||
const { data: submissionData, error: submissionError } = await supabase
|
||||
.from('content_submissions')
|
||||
.insert({
|
||||
user_id: userId,
|
||||
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
|
||||
const { error: itemError } = await supabase
|
||||
.from('submission_items')
|
||||
.insert({
|
||||
submission_id: submissionData.id,
|
||||
item_type: 'ride_model',
|
||||
action_type: 'edit',
|
||||
item_data: {
|
||||
...extractChangedFields(data, existingModel as any),
|
||||
ride_model_id: rideModelId, // Always include for relational integrity
|
||||
images: processedImages as unknown as Json
|
||||
// Create the submission item with actual ride model data
|
||||
const { error: itemError } = await supabase
|
||||
.from('submission_items')
|
||||
.insert({
|
||||
submission_id: submissionData.id,
|
||||
item_type: 'ride_model',
|
||||
action_type: 'edit',
|
||||
item_data: {
|
||||
...extractChangedFields(data, existingModel as any),
|
||||
ride_model_id: rideModelId, // Always include for relational integrity
|
||||
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)),
|
||||
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;
|
||||
shouldRetry: (error) => {
|
||||
if (error instanceof Error) {
|
||||
const message = error.message.toLowerCase();
|
||||
if (message.includes('required')) return false;
|
||||
if (message.includes('banned')) return false;
|
||||
if (message.includes('slug')) return false;
|
||||
}
|
||||
return isRetryableError(error);
|
||||
}
|
||||
}
|
||||
|
||||
logger.log('✅ Ride model update technical specifications inserted:', (data as any)._technical_specifications.length);
|
||||
}
|
||||
);
|
||||
|
||||
return { submitted: true, submissionId: submissionData.id };
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user