Refactor: Fix type safety and auth

This commit is contained in:
gpt-engineer-app[bot]
2025-10-16 12:54:47 +00:00
parent e79eaf76ba
commit e340f1c489
5 changed files with 162 additions and 42 deletions

View File

@@ -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 ⚠️
*

View File

@@ -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() {
<FerrisWheel className="w-8 h-8 text-primary" />
<h1 className="text-4xl font-bold">Rides by {designer.name}</h1>
</div>
<Button onClick={() => setIsCreateModalOpen(true)}>
<Plus className="w-4 h-4 mr-2" />
Add Ride
</Button>
{user && (
<Button onClick={() => setIsCreateModalOpen(true)}>
<Plus className="w-4 h-4 mr-2" />
Add Ride
</Button>
)}
</div>
<p className="text-lg text-muted-foreground mb-4">
Explore all rides designed by {designer.name}

View File

@@ -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() {
<FerrisWheel className="w-8 h-8 text-primary" />
<h1 className="text-4xl font-bold">Models by {manufacturer.name}</h1>
</div>
<Button onClick={() => setIsCreateModalOpen(true)}>
<Plus className="w-4 h-4 mr-2" />
Add Ride Model
</Button>
{user && (
<Button onClick={() => setIsCreateModalOpen(true)}>
<Plus className="w-4 h-4 mr-2" />
Add Ride Model
</Button>
)}
</div>
<p className="text-lg text-muted-foreground mb-4">
Explore all ride models manufactured by {manufacturer.name}

View File

@@ -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() {
<FerrisWheel className="w-8 h-8 text-primary" />
<h1 className="text-4xl font-bold">Rides by {manufacturer.name}</h1>
</div>
<Button onClick={() => setIsCreateModalOpen(true)}>
<Plus className="w-4 h-4 mr-2" />
Add Ride
</Button>
{user && (
<Button onClick={() => setIsCreateModalOpen(true)}>
<Plus className="w-4 h-4 mr-2" />
Add Ride
</Button>
)}
</div>
<p className="text-lg text-muted-foreground mb-4">
Explore all rides manufactured by {manufacturer.name}

View File

@@ -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<string, any>;
}
export default function RideModelDetail() {
const { manufacturerSlug, modelSlug } = useParams<{ manufacturerSlug: string; modelSlug: string }>();
const navigate = useNavigate();
const [model, setModel] = useState<RideModel | null>(null);
const [model, setModel] = useState<RideModelWithImages | null>(null);
const [manufacturer, setManufacturer] = useState<Company | null>(null);
const [rides, setRides] = useState<Ride[]>([]);
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 (
<div className="min-h-screen bg-background">
<Header />
@@ -183,10 +184,10 @@ export default function RideModelDetail() {
{/* Hero Section */}
<div className="mb-8">
{(extendedModel.banner_image_url || extendedModel.banner_image_id) && (
{(model.banner_image_url || model.banner_image_id) && (
<div className="relative w-full h-64 mb-6 rounded-lg overflow-hidden">
<img
src={extendedModel.banner_image_url || getCloudflareImageUrl(extendedModel.banner_image_id, 'banner')}
src={model.banner_image_url || (model.banner_image_id ? getCloudflareImageUrl(model.banner_image_id, 'banner') : '')}
alt={model.name}
className="w-full h-full object-cover"
/>
@@ -242,12 +243,12 @@ export default function RideModelDetail() {
</Card>
)}
{(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 && (
<Card>
<CardContent className="pt-6">
<h2 className="text-2xl font-semibold mb-4">Technical Specifications</h2>
<div className="grid md:grid-cols-2 gap-4">
{Object.entries((model as any).technical_specs as Record<string, any>).map(([key, value]) => (
{Object.entries(model.technical_specs as Record<string, any>).map(([key, value]) => (
<div key={key} className="flex justify-between py-2 border-b">
<span className="font-medium capitalize">{key.replace(/_/g, ' ')}</span>
<span className="text-muted-foreground">{String(value)}</span>