mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-24 12:51:14 -05:00
Fix: Correct RLS policies and function security
This commit is contained in:
95
src/lib/entityFormValidation.ts
Normal file
95
src/lib/entityFormValidation.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
/**
|
||||
* ⚠️ CRITICAL SECURITY PATTERN ⚠️
|
||||
*
|
||||
* These wrappers enforce the submission flow for all entity edits/creations.
|
||||
* DO NOT bypass these - they ensure moderation queue → versioning → live display.
|
||||
*
|
||||
* Flow: User Submit → Moderation Queue → Approval → Versioning → Live
|
||||
*
|
||||
* @see docs/SUBMISSION_FLOW.md
|
||||
*/
|
||||
|
||||
import {
|
||||
submitParkCreation,
|
||||
submitParkUpdate,
|
||||
submitRideCreation,
|
||||
submitRideUpdate,
|
||||
ParkFormData,
|
||||
RideFormData
|
||||
} from './entitySubmissionHelpers';
|
||||
|
||||
export type EntitySubmissionHandler<T> = (
|
||||
data: T,
|
||||
userId: string
|
||||
) => Promise<{ submitted: boolean; submissionId: string }>;
|
||||
|
||||
/**
|
||||
* Creates a type-safe submission handler for parks.
|
||||
* Automatically routes to creation or update based on isEditing flag.
|
||||
*
|
||||
* @example
|
||||
* const handleSubmit = enforceParkSubmissionFlow(true, park.id);
|
||||
* await handleSubmit(formData, user.id);
|
||||
*/
|
||||
export function enforceParkSubmissionFlow(
|
||||
isEditing: boolean,
|
||||
existingId?: string
|
||||
): EntitySubmissionHandler<ParkFormData> {
|
||||
if (isEditing && !existingId) {
|
||||
throw new Error('existingId is required when isEditing is true');
|
||||
}
|
||||
|
||||
return async (data: ParkFormData, userId: string) => {
|
||||
if (isEditing && existingId) {
|
||||
return await submitParkUpdate(existingId, data, userId);
|
||||
} else {
|
||||
return await submitParkCreation(data, userId);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a type-safe submission handler for rides.
|
||||
* Automatically routes to creation or update based on isEditing flag.
|
||||
*
|
||||
* @example
|
||||
* const handleSubmit = enforceRideSubmissionFlow(true, ride.id);
|
||||
* await handleSubmit(formData, user.id);
|
||||
*/
|
||||
export function enforceRideSubmissionFlow(
|
||||
isEditing: boolean,
|
||||
existingId?: string
|
||||
): EntitySubmissionHandler<RideFormData> {
|
||||
if (isEditing && !existingId) {
|
||||
throw new Error('existingId is required when isEditing is true');
|
||||
}
|
||||
|
||||
return async (data: RideFormData, userId: string) => {
|
||||
if (isEditing && existingId) {
|
||||
return await submitRideUpdate(existingId, data, userId);
|
||||
} else {
|
||||
return await submitRideCreation(data, userId);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Development-mode validation helper.
|
||||
* Warns if a form's onSubmit handler doesn't use submission helpers.
|
||||
*/
|
||||
export function validateSubmissionHandler(
|
||||
onSubmit: Function,
|
||||
entityType: 'park' | 'ride'
|
||||
): void {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
const funcString = onSubmit.toString();
|
||||
const expectedPattern = entityType === 'park' ? 'submitPark' : 'submitRide';
|
||||
|
||||
if (!funcString.includes(expectedPattern)) {
|
||||
console.warn(
|
||||
`⚠️ ${entityType}Form: onSubmit should use ${expectedPattern}Creation/${expectedPattern}Update from entitySubmissionHelpers.\n` +
|
||||
`Direct database writes bypass the moderation queue and versioning system.`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -116,10 +116,29 @@ export interface RideFormData {
|
||||
card_image_id?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* ⚠️ CRITICAL SECURITY PATTERN ⚠️
|
||||
*
|
||||
* Submits a new park for creation through the moderation queue.
|
||||
* This is the ONLY correct way to create parks.
|
||||
*
|
||||
* DO NOT use direct database inserts:
|
||||
* ❌ await supabase.from('parks').insert(data) // BYPASSES MODERATION!
|
||||
* ✅ await submitParkCreation(data, userId) // CORRECT
|
||||
*
|
||||
* Flow: User Submit → Moderation Queue → Approval → Versioning → Live
|
||||
*
|
||||
* Even moderators/admins must use this function to ensure proper versioning and audit trails.
|
||||
*
|
||||
* @see docs/SUBMISSION_FLOW.md for complete documentation
|
||||
* @param data - The park form data to submit
|
||||
* @param userId - The ID of the user submitting the park
|
||||
* @returns Object containing submitted boolean and submissionId
|
||||
*/
|
||||
export async function submitParkCreation(
|
||||
data: ParkFormData,
|
||||
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) {
|
||||
@@ -165,11 +184,31 @@ export async function submitParkCreation(
|
||||
return { submitted: true, submissionId: submissionData.id };
|
||||
}
|
||||
|
||||
/**
|
||||
* ⚠️ CRITICAL SECURITY PATTERN ⚠️
|
||||
*
|
||||
* Submits an update to an existing park through the moderation queue.
|
||||
* This is the ONLY correct way to update parks.
|
||||
*
|
||||
* DO NOT use direct database updates:
|
||||
* ❌ await supabase.from('parks').update(data) // BYPASSES MODERATION!
|
||||
* ✅ await submitParkUpdate(parkId, data, userId) // CORRECT
|
||||
*
|
||||
* Flow: User Submit → Moderation Queue → Approval → Versioning → Live
|
||||
*
|
||||
* Even moderators/admins must use this function to ensure proper versioning and audit trails.
|
||||
*
|
||||
* @see docs/SUBMISSION_FLOW.md for complete documentation
|
||||
* @param parkId - The ID of the park to update
|
||||
* @param data - The updated park form data
|
||||
* @param userId - The ID of the user submitting the update
|
||||
* @returns Object containing submitted boolean and submissionId
|
||||
*/
|
||||
export async function submitParkUpdate(
|
||||
parkId: string,
|
||||
data: ParkFormData,
|
||||
userId: string
|
||||
) {
|
||||
): Promise<{ submitted: boolean; submissionId: string }> {
|
||||
// Fetch existing park data first
|
||||
const { data: existingPark, error: fetchError } = await supabase
|
||||
.from('parks')
|
||||
@@ -228,10 +267,29 @@ export async function submitParkUpdate(
|
||||
return { submitted: true, submissionId: submissionData.id };
|
||||
}
|
||||
|
||||
/**
|
||||
* ⚠️ CRITICAL SECURITY PATTERN ⚠️
|
||||
*
|
||||
* Submits a new ride for creation through the moderation queue.
|
||||
* This is the ONLY correct way to create rides.
|
||||
*
|
||||
* DO NOT use direct database inserts:
|
||||
* ❌ await supabase.from('rides').insert(data) // BYPASSES MODERATION!
|
||||
* ✅ await submitRideCreation(data, userId) // CORRECT
|
||||
*
|
||||
* Flow: User Submit → Moderation Queue → Approval → Versioning → Live
|
||||
*
|
||||
* Even moderators/admins must use this function to ensure proper versioning and audit trails.
|
||||
*
|
||||
* @see docs/SUBMISSION_FLOW.md for complete documentation
|
||||
* @param data - The ride form data to submit
|
||||
* @param userId - The ID of the user submitting the ride
|
||||
* @returns Object containing submitted boolean and submissionId
|
||||
*/
|
||||
export async function submitRideCreation(
|
||||
data: RideFormData,
|
||||
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) {
|
||||
@@ -277,11 +335,31 @@ export async function submitRideCreation(
|
||||
return { submitted: true, submissionId: submissionData.id };
|
||||
}
|
||||
|
||||
/**
|
||||
* ⚠️ CRITICAL SECURITY PATTERN ⚠️
|
||||
*
|
||||
* Submits an update to an existing ride through the moderation queue.
|
||||
* This is the ONLY correct way to update rides.
|
||||
*
|
||||
* DO NOT use direct database updates:
|
||||
* ❌ await supabase.from('rides').update(data) // BYPASSES MODERATION!
|
||||
* ✅ await submitRideUpdate(rideId, data, userId) // CORRECT
|
||||
*
|
||||
* Flow: User Submit → Moderation Queue → Approval → Versioning → Live
|
||||
*
|
||||
* Even moderators/admins must use this function to ensure proper versioning and audit trails.
|
||||
*
|
||||
* @see docs/SUBMISSION_FLOW.md for complete documentation
|
||||
* @param rideId - The ID of the ride to update
|
||||
* @param data - The updated ride form data
|
||||
* @param userId - The ID of the user submitting the update
|
||||
* @returns Object containing submitted boolean and submissionId
|
||||
*/
|
||||
export async function submitRideUpdate(
|
||||
rideId: string,
|
||||
data: RideFormData,
|
||||
userId: string
|
||||
) {
|
||||
): Promise<{ submitted: boolean; submissionId: string }> {
|
||||
// Fetch existing ride data first
|
||||
const { data: existingRide, error: fetchError } = await supabase
|
||||
.from('rides')
|
||||
|
||||
Reference in New Issue
Block a user