mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-21 11:51:14 -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,
|
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 };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user