From e340f1c48918090f94181358e3e141ebb27443e0 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Thu, 16 Oct 2025 12:54:47 +0000 Subject: [PATCH] Refactor: Fix type safety and auth --- src/lib/entitySubmissionHelpers.ts | 85 ++++++++++++++++++++++++++++++ src/pages/DesignerRides.tsx | 39 +++++++++----- src/pages/ManufacturerModels.tsx | 32 +++++++---- src/pages/ManufacturerRides.tsx | 21 ++++++-- src/pages/RideModelDetail.tsx | 27 +++++----- 5 files changed, 162 insertions(+), 42 deletions(-) diff --git a/src/lib/entitySubmissionHelpers.ts b/src/lib/entitySubmissionHelpers.ts index 0d3b2d4e..e73422bd 100644 --- a/src/lib/entitySubmissionHelpers.ts +++ b/src/lib/entitySubmissionHelpers.ts @@ -134,6 +134,20 @@ export interface CompanyFormData { card_image_id?: string; } +export interface RideModelFormData { + name: string; + slug: string; + manufacturer_id: string; + category: string; + ride_type?: string; + description?: string; + images?: ImageAssignments; + banner_image_url?: string; + banner_image_id?: string; + card_image_url?: string; + card_image_id?: string; +} + // Import timeline types import type { TimelineEventFormData, TimelineSubmissionData, EntityType } from '@/types/timeline'; @@ -463,6 +477,77 @@ export async function submitRideUpdate( return { submitted: true, submissionId: submissionData.id }; } +/** + * ⚠️ CRITICAL SECURITY PATTERN ⚠️ + * + * Submits a new ride model for creation through the moderation queue. + * This is the ONLY correct way to create ride models. + * + * DO NOT use direct database inserts: + * ❌ await supabase.from('ride_models').insert(data) // BYPASSES MODERATION! + * ✅ await submitRideModelCreation(data, userId) // CORRECT + * + * Flow: User Submit → Moderation Queue → Approval → Versioning → Live + * + * @param data - The ride model form data to submit + * @param userId - The ID of the user submitting the ride model + * @returns Object containing submitted boolean and submissionId + */ +export async function submitRideModelCreation( + data: RideModelFormData, + userId: string +): Promise<{ submitted: boolean; submissionId: string }> { + // Upload any pending local images first + let processedImages = data.images; + if (data.images?.uploaded && data.images.uploaded.length > 0) { + try { + const uploadedImages = await uploadPendingImages(data.images.uploaded); + processedImages = { + ...data.images, + uploaded: uploadedImages + }; + } catch (error) { + console.error('Failed to upload images for ride model creation:', error); + throw new Error('Failed to upload images. Please check your connection and try again.'); + } + } + + // Create the main submission record + const { data: submissionData, error: submissionError } = await supabase + .from('content_submissions') + .insert({ + user_id: userId, + submission_type: 'ride_model', + content: { + action: 'create' + }, + status: 'pending' + }) + .select() + .single(); + + 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: { + ...data, + images: processedImages as unknown as Json + }, + status: 'pending', + order_index: 0 + }); + + if (itemError) throw itemError; + + return { submitted: true, submissionId: submissionData.id }; +} + /** * ⚠️ CRITICAL SECURITY PATTERN ⚠️ * diff --git a/src/pages/DesignerRides.tsx b/src/pages/DesignerRides.tsx index ba07d61d..a5e76836 100644 --- a/src/pages/DesignerRides.tsx +++ b/src/pages/DesignerRides.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useCallback } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; import { Header } from '@/components/layout/Header'; import { Button } from '@/components/ui/button'; @@ -28,13 +28,7 @@ export default function DesignerRides() { const [filterStatus, setFilterStatus] = useState('all'); const [isCreateModalOpen, setIsCreateModalOpen] = useState(false); - useEffect(() => { - if (designerSlug) { - fetchData(); - } - }, [designerSlug, sortBy, filterCategory, filterStatus]); - - const fetchData = async () => { + const fetchData = useCallback(async () => { try { // Fetch designer const { data: designerData, error: designerError } = await supabase @@ -91,7 +85,13 @@ export default function DesignerRides() { } finally { setLoading(false); } - }; + }, [designerSlug, sortBy, filterCategory, filterStatus]); + + useEffect(() => { + if (designerSlug) { + fetchData(); + } + }, [designerSlug, fetchData]); const filteredRides = rides.filter(ride => ride.name.toLowerCase().includes(searchQuery.toLowerCase()) || @@ -105,7 +105,16 @@ export default function DesignerRides() { return; } - const submissionData = { + if (!designer) { + toast({ + title: "Error", + description: "Designer information is missing.", + variant: "destructive" + }); + return; + } + + const submissionData: RideSubmissionData = { ...data, designer_id: designer.id, }; @@ -200,10 +209,12 @@ export default function DesignerRides() {

Rides by {designer.name}

- + {user && ( + + )}

Explore all rides designed by {designer.name} diff --git a/src/pages/ManufacturerModels.tsx b/src/pages/ManufacturerModels.tsx index 88612b69..08a2e7f0 100644 --- a/src/pages/ManufacturerModels.tsx +++ b/src/pages/ManufacturerModels.tsx @@ -98,12 +98,22 @@ export default function ManufacturerModels() { return; } - // For now, just show a toast since ride model submission isn't implemented yet - toast({ - title: "Coming Soon", - description: "Ride model submission is not yet available.", - }); - return; + if (!manufacturer) { + toast({ + title: "Error", + description: "Manufacturer information is missing.", + variant: "destructive" + }); + return; + } + + const submissionData: RideModelSubmissionData = { + ...data, + manufacturer_id: manufacturer.id, + }; + + const { submitRideModelCreation } = await import('@/lib/entitySubmissionHelpers'); + await submitRideModelCreation(submissionData, user.id); toast({ title: "Ride Model Submitted", @@ -184,10 +194,12 @@ export default function ManufacturerModels() {

Models by {manufacturer.name}

- + {user && ( + + )}

Explore all ride models manufactured by {manufacturer.name} diff --git a/src/pages/ManufacturerRides.tsx b/src/pages/ManufacturerRides.tsx index 328503c7..a1253f50 100644 --- a/src/pages/ManufacturerRides.tsx +++ b/src/pages/ManufacturerRides.tsx @@ -105,7 +105,16 @@ export default function ManufacturerRides() { return; } - const submissionData = { + if (!manufacturer) { + toast({ + title: "Error", + description: "Manufacturer information is missing.", + variant: "destructive" + }); + return; + } + + const submissionData: RideSubmissionData = { ...data, manufacturer_id: manufacturer.id, }; @@ -200,10 +209,12 @@ export default function ManufacturerRides() {

Rides by {manufacturer.name}

- + {user && ( + + )}

Explore all rides manufactured by {manufacturer.name} diff --git a/src/pages/RideModelDetail.tsx b/src/pages/RideModelDetail.tsx index 417fe55d..915fb636 100644 --- a/src/pages/RideModelDetail.tsx +++ b/src/pages/RideModelDetail.tsx @@ -13,10 +13,18 @@ import { RideCard } from '@/components/rides/RideCard'; import { AutocompleteSearch } from '@/components/search/AutocompleteSearch'; import { getCloudflareImageUrl } from '@/lib/cloudflareImageUtils'; +interface RideModelWithImages extends RideModel { + card_image_url?: string; + card_image_id?: string; + banner_image_url?: string; + banner_image_id?: string; + technical_specs?: Record; +} + export default function RideModelDetail() { const { manufacturerSlug, modelSlug } = useParams<{ manufacturerSlug: string; modelSlug: string }>(); const navigate = useNavigate(); - const [model, setModel] = useState(null); + const [model, setModel] = useState(null); const [manufacturer, setManufacturer] = useState(null); const [rides, setRides] = useState([]); const [loading, setLoading] = useState(true); @@ -47,7 +55,7 @@ export default function RideModelDetail() { .maybeSingle(); if (modelError) throw modelError; - setModel(modelData as RideModel); + setModel(modelData as RideModelWithImages); if (modelData) { // Fetch rides using this model @@ -162,13 +170,6 @@ export default function RideModelDetail() { ); } - const extendedModel = model as RideModel & { - card_image_url?: string; - card_image_id?: string; - banner_image_url?: string; - banner_image_id?: string; - }; - return (

@@ -183,10 +184,10 @@ export default function RideModelDetail() { {/* Hero Section */}
- {(extendedModel.banner_image_url || extendedModel.banner_image_id) && ( + {(model.banner_image_url || model.banner_image_id) && (
{model.name} @@ -242,12 +243,12 @@ export default function RideModelDetail() { )} - {(model as any).technical_specs && Object.keys((model as any).technical_specs).length > 0 && ( + {model.technical_specs && typeof model.technical_specs === 'object' && Object.keys(model.technical_specs).length > 0 && (

Technical Specifications

- {Object.entries((model as any).technical_specs as Record).map(([key, value]) => ( + {Object.entries(model.technical_specs as Record).map(([key, value]) => (
{key.replace(/_/g, ' ')} {String(value)}