mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-28 08:46:57 -05:00
Compare commits
3 Commits
80d823a1b9
...
14f413daab
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
14f413daab | ||
|
|
bb6f914424 | ||
|
|
11a1ae5f65 |
@@ -83,6 +83,12 @@ export function DesignerForm({ onSubmit, onCancel, initialData }: DesignerFormPr
|
|||||||
...data,
|
...data,
|
||||||
company_type: 'designer' as const,
|
company_type: 'designer' as const,
|
||||||
founded_year: data.founded_year ? parseInt(String(data.founded_year)) : undefined,
|
founded_year: data.founded_year ? parseInt(String(data.founded_year)) : undefined,
|
||||||
|
founded_date: undefined,
|
||||||
|
founded_date_precision: undefined,
|
||||||
|
banner_image_id: undefined,
|
||||||
|
banner_image_url: undefined,
|
||||||
|
card_image_id: undefined,
|
||||||
|
card_image_url: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
await onSubmit(formData);
|
await onSubmit(formData);
|
||||||
|
|||||||
@@ -87,6 +87,10 @@ export function ManufacturerForm({ onSubmit, onCancel, initialData }: Manufactur
|
|||||||
...data,
|
...data,
|
||||||
company_type: 'manufacturer' as const,
|
company_type: 'manufacturer' as const,
|
||||||
founded_year: data.founded_year ? parseInt(String(data.founded_year)) : undefined,
|
founded_year: data.founded_year ? parseInt(String(data.founded_year)) : undefined,
|
||||||
|
banner_image_id: undefined,
|
||||||
|
banner_image_url: undefined,
|
||||||
|
card_image_id: undefined,
|
||||||
|
card_image_url: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
await onSubmit(formData);
|
await onSubmit(formData);
|
||||||
|
|||||||
@@ -83,6 +83,12 @@ export function OperatorForm({ onSubmit, onCancel, initialData }: OperatorFormPr
|
|||||||
...data,
|
...data,
|
||||||
company_type: 'operator' as const,
|
company_type: 'operator' as const,
|
||||||
founded_year: data.founded_year ? parseInt(String(data.founded_year)) : undefined,
|
founded_year: data.founded_year ? parseInt(String(data.founded_year)) : undefined,
|
||||||
|
founded_date: undefined,
|
||||||
|
founded_date_precision: undefined,
|
||||||
|
banner_image_id: undefined,
|
||||||
|
banner_image_url: undefined,
|
||||||
|
card_image_id: undefined,
|
||||||
|
card_image_url: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
await onSubmit(formData);
|
await onSubmit(formData);
|
||||||
|
|||||||
@@ -83,6 +83,12 @@ export function PropertyOwnerForm({ onSubmit, onCancel, initialData }: PropertyO
|
|||||||
...data,
|
...data,
|
||||||
company_type: 'property_owner' as const,
|
company_type: 'property_owner' as const,
|
||||||
founded_year: data.founded_year ? parseInt(String(data.founded_year)) : undefined,
|
founded_year: data.founded_year ? parseInt(String(data.founded_year)) : undefined,
|
||||||
|
founded_date: undefined,
|
||||||
|
founded_date_precision: undefined,
|
||||||
|
banner_image_id: undefined,
|
||||||
|
banner_image_url: undefined,
|
||||||
|
card_image_id: undefined,
|
||||||
|
card_image_url: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
await onSubmit(formData);
|
await onSubmit(formData);
|
||||||
|
|||||||
@@ -661,7 +661,7 @@ export function RideForm({ onSubmit, onCancel, initialData, isEditing = false }:
|
|||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>Coaster Type</Label>
|
<Label>Coaster Type</Label>
|
||||||
<Select onValueChange={(value) => setValue('coaster_type', value)} defaultValue={initialData?.coaster_type}>
|
<Select onValueChange={(value) => setValue('coaster_type', value)} defaultValue={initialData?.coaster_type ?? undefined}>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder="Select type" />
|
<SelectValue placeholder="Select type" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
@@ -677,7 +677,7 @@ export function RideForm({ onSubmit, onCancel, initialData, isEditing = false }:
|
|||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>Seating Type</Label>
|
<Label>Seating Type</Label>
|
||||||
<Select onValueChange={(value) => setValue('seating_type', value)} defaultValue={initialData?.seating_type}>
|
<Select onValueChange={(value) => setValue('seating_type', value)} defaultValue={initialData?.seating_type ?? undefined}>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder="Select seating" />
|
<SelectValue placeholder="Select seating" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
@@ -693,7 +693,7 @@ export function RideForm({ onSubmit, onCancel, initialData, isEditing = false }:
|
|||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>Intensity Level</Label>
|
<Label>Intensity Level</Label>
|
||||||
<Select onValueChange={(value) => setValue('intensity_level', value)} defaultValue={initialData?.intensity_level}>
|
<Select onValueChange={(value) => setValue('intensity_level', value)} defaultValue={initialData?.intensity_level ?? undefined}>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder="Select intensity" />
|
<SelectValue placeholder="Select intensity" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
@@ -846,7 +846,7 @@ export function RideForm({ onSubmit, onCancel, initialData, isEditing = false }:
|
|||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>Wetness Level</Label>
|
<Label>Wetness Level</Label>
|
||||||
<Select onValueChange={(value) => setValue('wetness_level', value as 'dry' | 'light' | 'moderate' | 'soaked')} defaultValue={initialData?.wetness_level}>
|
<Select onValueChange={(value) => setValue('wetness_level', value as 'dry' | 'light' | 'moderate' | 'soaked')} defaultValue={initialData?.wetness_level ?? undefined}>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder="Select wetness level" />
|
<SelectValue placeholder="Select wetness level" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
@@ -969,7 +969,7 @@ export function RideForm({ onSubmit, onCancel, initialData, isEditing = false }:
|
|||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>Rotation Type</Label>
|
<Label>Rotation Type</Label>
|
||||||
<Select onValueChange={(value) => setValue('rotation_type', value as 'horizontal' | 'vertical' | 'multi_axis' | 'pendulum' | 'none')} defaultValue={initialData?.rotation_type}>
|
<Select onValueChange={(value) => setValue('rotation_type', value as 'horizontal' | 'vertical' | 'multi_axis' | 'pendulum' | 'none')} defaultValue={initialData?.rotation_type ?? undefined}>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder="Select rotation type" />
|
<SelectValue placeholder="Select rotation type" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
@@ -1114,7 +1114,7 @@ export function RideForm({ onSubmit, onCancel, initialData, isEditing = false }:
|
|||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>Transport Type</Label>
|
<Label>Transport Type</Label>
|
||||||
<Select onValueChange={(value) => setValue('transport_type', value as 'train' | 'monorail' | 'skylift' | 'ferry' | 'peoplemover' | 'cable_car')} defaultValue={initialData?.transport_type}>
|
<Select onValueChange={(value) => setValue('transport_type', value as 'train' | 'monorail' | 'skylift' | 'ferry' | 'peoplemover' | 'cable_car')} defaultValue={initialData?.transport_type ?? undefined}>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder="Select transport type" />
|
<SelectValue placeholder="Select transport type" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
|
|||||||
@@ -25,24 +25,24 @@ const imageAssignmentSchema = z.object({
|
|||||||
export const parkValidationSchema = z.object({
|
export const parkValidationSchema = z.object({
|
||||||
name: z.string().trim().min(1, 'Park name is required').max(200, 'Name must be less than 200 characters'),
|
name: z.string().trim().min(1, 'Park name is required').max(200, 'Name must be less than 200 characters'),
|
||||||
slug: z.string().trim().min(1, 'Slug is required').regex(/^[a-z0-9-]+$/, 'Slug must contain only lowercase letters, numbers, and hyphens'),
|
slug: z.string().trim().min(1, 'Slug is required').regex(/^[a-z0-9-]+$/, 'Slug must contain only lowercase letters, numbers, and hyphens'),
|
||||||
description: z.string().trim().max(2000, 'Description must be less than 2000 characters').optional().or(z.literal('')),
|
description: z.string().trim().max(2000, 'Description must be less than 2000 characters').nullable().optional().or(z.literal('')).transform(val => val || undefined),
|
||||||
park_type: z.string().min(1, 'Park type is required'),
|
park_type: z.string().min(1, 'Park type is required'),
|
||||||
status: z.enum(['operating', 'closed_permanently', 'closed_temporarily', 'under_construction', 'planned', 'abandoned']),
|
status: z.enum(['operating', 'closed_permanently', 'closed_temporarily', 'under_construction', 'planned', 'abandoned']),
|
||||||
opening_date: z.string().optional().or(z.literal('')).refine((val) => {
|
opening_date: z.string().nullable().optional().or(z.literal('')).transform(val => val || undefined).refine((val) => {
|
||||||
if (!val) return true;
|
if (!val) return true;
|
||||||
const date = new Date(val);
|
const date = new Date(val);
|
||||||
return date <= new Date();
|
return date <= new Date();
|
||||||
}, 'Opening date cannot be in the future'),
|
}, 'Opening date cannot be in the future'),
|
||||||
opening_date_precision: z.enum(['day', 'month', 'year']).optional(),
|
opening_date_precision: z.enum(['day', 'month', 'year']).nullable().optional(),
|
||||||
closing_date: z.string().optional().or(z.literal('')),
|
closing_date: z.string().nullable().optional().or(z.literal('')).transform(val => val || undefined),
|
||||||
closing_date_precision: z.enum(['day', 'month', 'year']).optional(),
|
closing_date_precision: z.enum(['day', 'month', 'year']).nullable().optional(),
|
||||||
location_id: z.string().uuid().optional().nullable(),
|
location_id: z.string().uuid().optional().nullable(),
|
||||||
website_url: z.string().trim().optional().or(z.literal('')).refine((val) => {
|
website_url: z.string().trim().nullable().optional().or(z.literal('')).transform(val => val || undefined).refine((val) => {
|
||||||
if (!val || val === '') return true;
|
if (!val || val === '') return true;
|
||||||
return z.string().url().safeParse(val).success;
|
return z.string().url().safeParse(val).success;
|
||||||
}, 'Invalid URL format'),
|
}, 'Invalid URL format'),
|
||||||
phone: z.string().trim().max(50, 'Phone must be less than 50 characters').optional().or(z.literal('')),
|
phone: z.string().trim().max(50, 'Phone must be less than 50 characters').nullable().optional().or(z.literal('')).transform(val => val || undefined),
|
||||||
email: z.string().trim().optional().or(z.literal('')).refine((val) => {
|
email: z.string().trim().nullable().optional().or(z.literal('')).transform(val => val || undefined).refine((val) => {
|
||||||
if (!val || val === '') return true;
|
if (!val || val === '') return true;
|
||||||
return z.string().email().safeParse(val).success;
|
return z.string().email().safeParse(val).success;
|
||||||
}, 'Invalid email format'),
|
}, 'Invalid email format'),
|
||||||
@@ -64,19 +64,21 @@ export const parkValidationSchema = z.object({
|
|||||||
.nullable()
|
.nullable()
|
||||||
.or(z.literal(''))
|
.or(z.literal(''))
|
||||||
.transform(val => val || undefined),
|
.transform(val => val || undefined),
|
||||||
banner_image_id: z.string().optional(),
|
banner_image_id: z.string().nullable().optional().or(z.literal('')).transform(val => val || undefined),
|
||||||
banner_image_url: z.string().optional(),
|
banner_image_url: z.string().nullable().optional().or(z.literal('')).transform(val => val || undefined),
|
||||||
card_image_id: z.string().optional(),
|
card_image_id: z.string().nullable().optional().or(z.literal('')).transform(val => val || undefined),
|
||||||
card_image_url: z.string().optional(),
|
card_image_url: z.string().nullable().optional().or(z.literal('')).transform(val => val || undefined),
|
||||||
images: imageAssignmentSchema,
|
images: imageAssignmentSchema,
|
||||||
source_url: z.string().trim().optional().or(z.literal('')).refine((val) => {
|
source_url: z.string().trim().nullable().optional().or(z.literal('')).transform(val => val || undefined).refine((val) => {
|
||||||
if (!val || val === '') return true;
|
if (!val || val === '') return true;
|
||||||
return z.string().url().safeParse(val).success;
|
return z.string().url().safeParse(val).success;
|
||||||
}, 'Invalid URL format. Must be a valid URL starting with http:// or https://'),
|
}, 'Invalid URL format. Must be a valid URL starting with http:// or https://'),
|
||||||
submission_notes: z.string().trim()
|
submission_notes: z.string().trim()
|
||||||
.max(1000, 'Submission notes must be less than 1000 characters')
|
.max(1000, 'Submission notes must be less than 1000 characters')
|
||||||
|
.nullable()
|
||||||
.optional()
|
.optional()
|
||||||
.or(z.literal('')),
|
.or(z.literal(''))
|
||||||
|
.transform(val => val || undefined),
|
||||||
}).refine((data) => {
|
}).refine((data) => {
|
||||||
if (data.closing_date && data.opening_date) {
|
if (data.closing_date && data.opening_date) {
|
||||||
return new Date(data.closing_date) >= new Date(data.opening_date);
|
return new Date(data.closing_date) >= new Date(data.opening_date);
|
||||||
@@ -94,9 +96,9 @@ export const parkValidationSchema = z.object({
|
|||||||
export const rideValidationSchema = z.object({
|
export const rideValidationSchema = z.object({
|
||||||
name: z.string().trim().min(1, 'Ride name is required').max(200, 'Name must be less than 200 characters'),
|
name: z.string().trim().min(1, 'Ride name is required').max(200, 'Name must be less than 200 characters'),
|
||||||
slug: z.string().trim().min(1, 'Slug is required').regex(/^[a-z0-9-]+$/, 'Slug must contain only lowercase letters, numbers, and hyphens'),
|
slug: z.string().trim().min(1, 'Slug is required').regex(/^[a-z0-9-]+$/, 'Slug must contain only lowercase letters, numbers, and hyphens'),
|
||||||
description: z.string().trim().max(2000, 'Description must be less than 2000 characters').optional().or(z.literal('')),
|
description: z.string().trim().max(2000, 'Description must be less than 2000 characters').nullable().optional().or(z.literal('')).transform(val => val || undefined),
|
||||||
category: z.string().min(1, 'Category is required'),
|
category: z.string().min(1, 'Category is required'),
|
||||||
ride_sub_type: z.string().trim().max(100, 'Sub type must be less than 100 characters').optional().or(z.literal('')),
|
ride_sub_type: z.string().trim().max(100, 'Sub type must be less than 100 characters').nullable().optional().or(z.literal('')).transform(val => val || undefined),
|
||||||
status: z.enum(['operating', 'closed_permanently', 'closed_temporarily', 'under_construction', 'relocated', 'stored', 'demolished']),
|
status: z.enum(['operating', 'closed_permanently', 'closed_temporarily', 'under_construction', 'relocated', 'stored', 'demolished']),
|
||||||
park_id: z.string().uuid().optional().nullable(),
|
park_id: z.string().uuid().optional().nullable(),
|
||||||
designer_id: z.string()
|
designer_id: z.string()
|
||||||
@@ -106,10 +108,10 @@ export const rideValidationSchema = z.object({
|
|||||||
)
|
)
|
||||||
.optional()
|
.optional()
|
||||||
.nullable(),
|
.nullable(),
|
||||||
opening_date: z.string().optional().or(z.literal('')),
|
opening_date: z.string().nullable().optional().or(z.literal('')).transform(val => val || undefined),
|
||||||
opening_date_precision: z.enum(['day', 'month', 'year']).optional(),
|
opening_date_precision: z.enum(['day', 'month', 'year']).nullable().optional(),
|
||||||
closing_date: z.string().optional().or(z.literal('')),
|
closing_date: z.string().nullable().optional().or(z.literal('')).transform(val => val || undefined),
|
||||||
closing_date_precision: z.enum(['day', 'month', 'year']).optional(),
|
closing_date_precision: z.enum(['day', 'month', 'year']).nullable().optional(),
|
||||||
height_requirement: z.preprocess(
|
height_requirement: z.preprocess(
|
||||||
(val) => val === '' || val === null || val === undefined ? undefined : Number(val),
|
(val) => val === '' || val === null || val === undefined ? undefined : Number(val),
|
||||||
z.number().int().min(0, 'Height requirement must be positive').max(300, 'Height requirement must be less than 300cm').optional()
|
z.number().int().min(0, 'Height requirement must be positive').max(300, 'Height requirement must be less than 300cm').optional()
|
||||||
@@ -164,9 +166,9 @@ export const rideValidationSchema = z.object({
|
|||||||
)
|
)
|
||||||
.optional()
|
.optional()
|
||||||
.nullable(),
|
.nullable(),
|
||||||
coaster_type: z.string().optional(),
|
coaster_type: z.string().nullable().optional(),
|
||||||
seating_type: z.string().optional(),
|
seating_type: z.string().nullable().optional(),
|
||||||
intensity_level: z.string().optional(),
|
intensity_level: z.string().nullable().optional(),
|
||||||
track_material: z.array(z.string()).optional().nullable(),
|
track_material: z.array(z.string()).optional().nullable(),
|
||||||
support_material: z.array(z.string()).optional().nullable(),
|
support_material: z.array(z.string()).optional().nullable(),
|
||||||
propulsion_method: z.array(z.string()).optional().nullable(),
|
propulsion_method: z.array(z.string()).optional().nullable(),
|
||||||
@@ -179,15 +181,15 @@ export const rideValidationSchema = z.object({
|
|||||||
(val) => val === '' || val === null || val === undefined ? undefined : Number(val),
|
(val) => val === '' || val === null || val === undefined ? undefined : Number(val),
|
||||||
z.number().min(0, 'Splash height must be positive').max(100, 'Splash height must be less than 100 meters').optional()
|
z.number().min(0, 'Splash height must be positive').max(100, 'Splash height must be less than 100 meters').optional()
|
||||||
),
|
),
|
||||||
wetness_level: z.enum(['dry', 'light', 'moderate', 'soaked']).optional(),
|
wetness_level: z.enum(['dry', 'light', 'moderate', 'soaked']).nullable().optional(),
|
||||||
flume_type: z.string().trim().max(100, 'Flume type must be less than 100 characters').optional().or(z.literal('')),
|
flume_type: z.string().trim().max(100, 'Flume type must be less than 100 characters').nullable().optional().or(z.literal('')).transform(val => val || undefined),
|
||||||
boat_capacity: z.preprocess(
|
boat_capacity: z.preprocess(
|
||||||
(val) => val === '' || val === null || val === undefined ? undefined : Number(val),
|
(val) => val === '' || val === null || val === undefined ? undefined : Number(val),
|
||||||
z.number().int().min(1, 'Boat capacity must be positive').max(100, 'Boat capacity must be less than 100').optional()
|
z.number().int().min(1, 'Boat capacity must be positive').max(100, 'Boat capacity must be less than 100').optional()
|
||||||
),
|
),
|
||||||
// Dark ride specific fields
|
// Dark ride specific fields
|
||||||
theme_name: z.string().trim().max(200, 'Theme name must be less than 200 characters').optional().or(z.literal('')),
|
theme_name: z.string().trim().max(200, 'Theme name must be less than 200 characters').nullable().optional().or(z.literal('')).transform(val => val || undefined),
|
||||||
story_description: z.string().trim().max(2000, 'Story description must be less than 2000 characters').optional().or(z.literal('')),
|
story_description: z.string().trim().max(2000, 'Story description must be less than 2000 characters').nullable().optional().or(z.literal('')).transform(val => val || undefined),
|
||||||
show_duration_seconds: z.preprocess(
|
show_duration_seconds: z.preprocess(
|
||||||
(val) => val === '' || val === null || val === undefined ? undefined : Number(val),
|
(val) => val === '' || val === null || val === undefined ? undefined : Number(val),
|
||||||
z.number().int().min(0, 'Show duration must be positive').max(7200, 'Show duration must be less than 2 hours').optional()
|
z.number().int().min(0, 'Show duration must be positive').max(7200, 'Show duration must be less than 2 hours').optional()
|
||||||
@@ -196,15 +198,15 @@ export const rideValidationSchema = z.object({
|
|||||||
(val) => val === '' || val === null || val === undefined ? undefined : Number(val),
|
(val) => val === '' || val === null || val === undefined ? undefined : Number(val),
|
||||||
z.number().int().min(0, 'Animatronics count must be positive').max(1000, 'Animatronics count must be less than 1000').optional()
|
z.number().int().min(0, 'Animatronics count must be positive').max(1000, 'Animatronics count must be less than 1000').optional()
|
||||||
),
|
),
|
||||||
projection_type: z.string().trim().max(100, 'Projection type must be less than 100 characters').optional().or(z.literal('')),
|
projection_type: z.string().trim().max(100, 'Projection type must be less than 100 characters').nullable().optional().or(z.literal('')).transform(val => val || undefined),
|
||||||
ride_system: z.string().trim().max(100, 'Ride system must be less than 100 characters').optional().or(z.literal('')),
|
ride_system: z.string().trim().max(100, 'Ride system must be less than 100 characters').nullable().optional().or(z.literal('')).transform(val => val || undefined),
|
||||||
scenes_count: z.preprocess(
|
scenes_count: z.preprocess(
|
||||||
(val) => val === '' || val === null || val === undefined ? undefined : Number(val),
|
(val) => val === '' || val === null || val === undefined ? undefined : Number(val),
|
||||||
z.number().int().min(0, 'Scenes count must be positive').max(100, 'Scenes count must be less than 100').optional()
|
z.number().int().min(0, 'Scenes count must be positive').max(100, 'Scenes count must be less than 100').optional()
|
||||||
),
|
),
|
||||||
// Flat ride specific fields
|
// Flat ride specific fields
|
||||||
rotation_type: z.enum(['horizontal', 'vertical', 'multi_axis', 'pendulum', 'none']).optional(),
|
rotation_type: z.enum(['horizontal', 'vertical', 'multi_axis', 'pendulum', 'none']).nullable().optional(),
|
||||||
motion_pattern: z.string().trim().max(200, 'Motion pattern must be less than 200 characters').optional().or(z.literal('')),
|
motion_pattern: z.string().trim().max(200, 'Motion pattern must be less than 200 characters').nullable().optional().or(z.literal('')).transform(val => val || undefined),
|
||||||
platform_count: z.preprocess(
|
platform_count: z.preprocess(
|
||||||
(val) => val === '' || val === null || val === undefined ? undefined : Number(val),
|
(val) => val === '' || val === null || val === undefined ? undefined : Number(val),
|
||||||
z.number().int().min(1, 'Platform count must be positive').max(100, 'Platform count must be less than 100').optional()
|
z.number().int().min(1, 'Platform count must be positive').max(100, 'Platform count must be less than 100').optional()
|
||||||
@@ -234,10 +236,10 @@ export const rideValidationSchema = z.object({
|
|||||||
(val) => val === '' || val === null || val === undefined ? undefined : Number(val),
|
(val) => val === '' || val === null || val === undefined ? undefined : Number(val),
|
||||||
z.number().int().min(0, 'Max age must be positive').max(18, 'Max age must be less than 18').optional()
|
z.number().int().min(0, 'Max age must be positive').max(18, 'Max age must be less than 18').optional()
|
||||||
),
|
),
|
||||||
educational_theme: z.string().trim().max(200, 'Educational theme must be less than 200 characters').optional().or(z.literal('')),
|
educational_theme: z.string().trim().max(200, 'Educational theme must be less than 200 characters').nullable().optional().or(z.literal('')).transform(val => val || undefined),
|
||||||
character_theme: z.string().trim().max(200, 'Character theme must be less than 200 characters').optional().or(z.literal('')),
|
character_theme: z.string().trim().max(200, 'Character theme must be less than 200 characters').nullable().optional().or(z.literal('')).transform(val => val || undefined),
|
||||||
// Transportation ride specific fields
|
// Transportation ride specific fields
|
||||||
transport_type: z.enum(['train', 'monorail', 'skylift', 'ferry', 'peoplemover', 'cable_car']).optional(),
|
transport_type: z.enum(['train', 'monorail', 'skylift', 'ferry', 'peoplemover', 'cable_car']).nullable().optional(),
|
||||||
route_length_meters: z.preprocess(
|
route_length_meters: z.preprocess(
|
||||||
(val) => val === '' || val === null || val === undefined ? undefined : Number(val),
|
(val) => val === '' || val === null || val === undefined ? undefined : Number(val),
|
||||||
z.number().min(0, 'Route length must be positive').max(50000, 'Route length must be less than 50km').optional()
|
z.number().min(0, 'Route length must be positive').max(50000, 'Route length must be less than 50km').optional()
|
||||||
@@ -258,19 +260,21 @@ export const rideValidationSchema = z.object({
|
|||||||
(val) => val === '' || val === null || val === undefined ? undefined : Number(val),
|
(val) => val === '' || val === null || val === undefined ? undefined : Number(val),
|
||||||
z.number().int().min(0, 'Round trip duration must be positive').max(7200, 'Round trip duration must be less than 2 hours').optional()
|
z.number().int().min(0, 'Round trip duration must be positive').max(7200, 'Round trip duration must be less than 2 hours').optional()
|
||||||
),
|
),
|
||||||
banner_image_id: z.string().optional(),
|
banner_image_id: z.string().nullable().optional().or(z.literal('')).transform(val => val || undefined),
|
||||||
banner_image_url: z.string().optional(),
|
banner_image_url: z.string().nullable().optional().or(z.literal('')).transform(val => val || undefined),
|
||||||
card_image_id: z.string().optional(),
|
card_image_id: z.string().nullable().optional().or(z.literal('')).transform(val => val || undefined),
|
||||||
card_image_url: z.string().optional(),
|
card_image_url: z.string().nullable().optional().or(z.literal('')).transform(val => val || undefined),
|
||||||
images: imageAssignmentSchema,
|
images: imageAssignmentSchema,
|
||||||
source_url: z.string().trim().optional().or(z.literal('')).refine((val) => {
|
source_url: z.string().trim().nullable().optional().or(z.literal('')).transform(val => val || undefined).refine((val) => {
|
||||||
if (!val || val === '') return true;
|
if (!val || val === '') return true;
|
||||||
return z.string().url().safeParse(val).success;
|
return z.string().url().safeParse(val).success;
|
||||||
}, 'Invalid URL format. Must be a valid URL starting with http:// or https://'),
|
}, 'Invalid URL format. Must be a valid URL starting with http:// or https://'),
|
||||||
submission_notes: z.string().trim()
|
submission_notes: z.string().trim()
|
||||||
.max(1000, 'Submission notes must be less than 1000 characters')
|
.max(1000, 'Submission notes must be less than 1000 characters')
|
||||||
|
.nullable()
|
||||||
.optional()
|
.optional()
|
||||||
.or(z.literal('')),
|
.or(z.literal(''))
|
||||||
|
.transform(val => val || undefined),
|
||||||
});
|
});
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
@@ -281,32 +285,34 @@ export const companyValidationSchema = z.object({
|
|||||||
name: z.string().trim().min(1, 'Company name is required').max(200, 'Name must be less than 200 characters'),
|
name: z.string().trim().min(1, 'Company name is required').max(200, 'Name must be less than 200 characters'),
|
||||||
slug: z.string().trim().min(1, 'Slug is required').regex(/^[a-z0-9-]+$/, 'Slug must contain only lowercase letters, numbers, and hyphens'),
|
slug: z.string().trim().min(1, 'Slug is required').regex(/^[a-z0-9-]+$/, 'Slug must contain only lowercase letters, numbers, and hyphens'),
|
||||||
company_type: z.enum(['manufacturer', 'designer', 'operator', 'property_owner']),
|
company_type: z.enum(['manufacturer', 'designer', 'operator', 'property_owner']),
|
||||||
description: z.string().trim().max(2000, 'Description must be less than 2000 characters').optional().or(z.literal('')),
|
description: z.string().trim().max(2000, 'Description must be less than 2000 characters').nullable().optional().or(z.literal('')).transform(val => val || undefined),
|
||||||
person_type: z.enum(['company', 'individual', 'firm', 'organization']),
|
person_type: z.enum(['company', 'individual', 'firm', 'organization']),
|
||||||
founded_date: z.string().optional().or(z.literal('')),
|
founded_date: z.string().nullable().optional().or(z.literal('')).transform(val => val || undefined),
|
||||||
founded_date_precision: z.enum(['day', 'month', 'year']).optional(),
|
founded_date_precision: z.enum(['day', 'month', 'year']).nullable().optional(),
|
||||||
founded_year: z.preprocess(
|
founded_year: z.preprocess(
|
||||||
(val) => val === '' || val === null || val === undefined ? undefined : Number(val),
|
(val) => val === '' || val === null || val === undefined ? undefined : Number(val),
|
||||||
z.number().int().min(1800, 'Founded year must be after 1800').max(currentYear, `Founded year cannot be after ${currentYear}`).optional()
|
z.number().int().min(1800, 'Founded year must be after 1800').max(currentYear, `Founded year cannot be after ${currentYear}`).optional()
|
||||||
),
|
),
|
||||||
headquarters_location: z.string().trim().max(200, 'Location must be less than 200 characters').optional().or(z.literal('')),
|
headquarters_location: z.string().trim().max(200, 'Location must be less than 200 characters').nullable().optional().or(z.literal('')).transform(val => val || undefined),
|
||||||
website_url: z.string().trim().optional().or(z.literal('')).refine((val) => {
|
website_url: z.string().trim().nullable().optional().or(z.literal('')).transform(val => val || undefined).refine((val) => {
|
||||||
if (!val || val === '') return true;
|
if (!val || val === '') return true;
|
||||||
return z.string().url().safeParse(val).success;
|
return z.string().url().safeParse(val).success;
|
||||||
}, 'Invalid URL format'),
|
}, 'Invalid URL format'),
|
||||||
banner_image_id: z.string().optional(),
|
banner_image_id: z.string().nullable().optional().or(z.literal('')).transform(val => val || undefined),
|
||||||
banner_image_url: z.string().optional(),
|
banner_image_url: z.string().nullable().optional().or(z.literal('')).transform(val => val || undefined),
|
||||||
card_image_id: z.string().optional(),
|
card_image_id: z.string().nullable().optional().or(z.literal('')).transform(val => val || undefined),
|
||||||
card_image_url: z.string().optional(),
|
card_image_url: z.string().nullable().optional().or(z.literal('')).transform(val => val || undefined),
|
||||||
images: imageAssignmentSchema,
|
images: imageAssignmentSchema,
|
||||||
source_url: z.string().trim().optional().or(z.literal('')).refine((val) => {
|
source_url: z.string().trim().nullable().optional().or(z.literal('')).transform(val => val || undefined).refine((val) => {
|
||||||
if (!val || val === '') return true;
|
if (!val || val === '') return true;
|
||||||
return z.string().url().safeParse(val).success;
|
return z.string().url().safeParse(val).success;
|
||||||
}, 'Invalid URL format. Must be a valid URL starting with http:// or https://'),
|
}, 'Invalid URL format. Must be a valid URL starting with http:// or https://'),
|
||||||
submission_notes: z.string().trim()
|
submission_notes: z.string().trim()
|
||||||
.max(1000, 'Submission notes must be less than 1000 characters')
|
.max(1000, 'Submission notes must be less than 1000 characters')
|
||||||
|
.nullable()
|
||||||
.optional()
|
.optional()
|
||||||
.or(z.literal('')),
|
.or(z.literal(''))
|
||||||
|
.transform(val => val || undefined),
|
||||||
});
|
});
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
@@ -318,21 +324,23 @@ export const rideModelValidationSchema = z.object({
|
|||||||
slug: z.string().trim().min(1, 'Slug is required').regex(/^[a-z0-9-]+$/, 'Slug must contain only lowercase letters, numbers, and hyphens'),
|
slug: z.string().trim().min(1, 'Slug is required').regex(/^[a-z0-9-]+$/, 'Slug must contain only lowercase letters, numbers, and hyphens'),
|
||||||
category: z.string().min(1, 'Category is required'),
|
category: z.string().min(1, 'Category is required'),
|
||||||
ride_type: z.string().trim().min(1, 'Ride type is required').max(100, 'Ride type must be less than 100 characters'),
|
ride_type: z.string().trim().min(1, 'Ride type is required').max(100, 'Ride type must be less than 100 characters'),
|
||||||
description: z.string().trim().max(2000, 'Description must be less than 2000 characters').optional().or(z.literal('')),
|
description: z.string().trim().max(2000, 'Description must be less than 2000 characters').nullable().optional().or(z.literal('')).transform(val => val || undefined),
|
||||||
manufacturer_id: z.string()
|
manufacturer_id: z.string()
|
||||||
.refine(
|
.refine(
|
||||||
val => !val || val === '' || val.startsWith('temp-') || /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(val),
|
val => !val || val === '' || val.startsWith('temp-') || /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(val),
|
||||||
'Must be a valid UUID or temporary placeholder'
|
'Must be a valid UUID or temporary placeholder'
|
||||||
)
|
)
|
||||||
.optional(),
|
.optional(),
|
||||||
source_url: z.string().trim().optional().or(z.literal('')).refine((val) => {
|
source_url: z.string().trim().nullable().optional().or(z.literal('')).transform(val => val || undefined).refine((val) => {
|
||||||
if (!val || val === '') return true;
|
if (!val || val === '') return true;
|
||||||
return z.string().url().safeParse(val).success;
|
return z.string().url().safeParse(val).success;
|
||||||
}, 'Invalid URL format. Must be a valid URL starting with http:// or https://'),
|
}, 'Invalid URL format. Must be a valid URL starting with http:// or https://'),
|
||||||
submission_notes: z.string().trim()
|
submission_notes: z.string().trim()
|
||||||
.max(1000, 'Submission notes must be less than 1000 characters')
|
.max(1000, 'Submission notes must be less than 1000 characters')
|
||||||
|
.nullable()
|
||||||
.optional()
|
.optional()
|
||||||
.or(z.literal('')),
|
.or(z.literal(''))
|
||||||
|
.transform(val => val || undefined),
|
||||||
});
|
});
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|||||||
@@ -61,6 +61,9 @@ export async function fetchSubmissionItems(submissionId: string): Promise<Submis
|
|||||||
*,
|
*,
|
||||||
park_submission:park_submissions!park_submission_id(*),
|
park_submission:park_submissions!park_submission_id(*),
|
||||||
ride_submission:ride_submissions!ride_submission_id(*),
|
ride_submission:ride_submissions!ride_submission_id(*),
|
||||||
|
company_submission:company_submissions!company_submission_id(*),
|
||||||
|
ride_model_submission:ride_model_submissions!ride_model_submission_id(*),
|
||||||
|
timeline_event_submission:timeline_event_submissions!timeline_event_submission_id(*),
|
||||||
photo_submission:photo_submissions!photo_submission_id(
|
photo_submission:photo_submissions!photo_submission_id(
|
||||||
*,
|
*,
|
||||||
photo_items:photo_submission_items(*)
|
photo_items:photo_submission_items(*)
|
||||||
@@ -82,13 +85,30 @@ export async function fetchSubmissionItems(submissionId: string): Promise<Submis
|
|||||||
case 'ride':
|
case 'ride':
|
||||||
item_data = (item as any).ride_submission;
|
item_data = (item as any).ride_submission;
|
||||||
break;
|
break;
|
||||||
|
case 'operator':
|
||||||
|
case 'manufacturer':
|
||||||
|
case 'designer':
|
||||||
|
case 'property_owner':
|
||||||
|
item_data = (item as any).company_submission;
|
||||||
|
break;
|
||||||
|
case 'ride_model':
|
||||||
|
item_data = (item as any).ride_model_submission;
|
||||||
|
break;
|
||||||
|
case 'milestone':
|
||||||
|
case 'timeline_event':
|
||||||
|
item_data = (item as any).timeline_event_submission;
|
||||||
|
break;
|
||||||
case 'photo':
|
case 'photo':
|
||||||
|
case 'photo_edit':
|
||||||
|
case 'photo_delete':
|
||||||
item_data = {
|
item_data = {
|
||||||
...(item as any).photo_submission,
|
...(item as any).photo_submission,
|
||||||
photos: (item as any).photo_submission?.photo_items || []
|
photos: (item as any).photo_submission?.photo_items || []
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
// Log warning for unknown types but don't crash
|
||||||
|
console.warn(`Unknown item_type: ${item.item_type}`);
|
||||||
item_data = null;
|
item_data = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,228 @@
|
|||||||
|
-- Fix all direct auth.mfa_factors queries by replacing with has_mfa_enabled() function
|
||||||
|
-- This prevents "permission denied for table mfa_factors" errors for non-superuser roles
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- BLOG POSTS
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
DROP POLICY IF EXISTS "Admins can insert blog posts with MFA" ON public.blog_posts;
|
||||||
|
DROP POLICY IF EXISTS "Admins can create blog posts with AAL2" ON public.blog_posts;
|
||||||
|
CREATE POLICY "Admins can insert blog posts with MFA"
|
||||||
|
ON public.blog_posts
|
||||||
|
FOR INSERT
|
||||||
|
TO authenticated
|
||||||
|
WITH CHECK (
|
||||||
|
(has_role(auth.uid(), 'admin'::app_role) OR has_role(auth.uid(), 'superuser'::app_role))
|
||||||
|
AND ((NOT has_mfa_enabled((SELECT auth.uid()))) OR has_aal2())
|
||||||
|
);
|
||||||
|
|
||||||
|
DROP POLICY IF EXISTS "Admins can update blog posts with MFA" ON public.blog_posts;
|
||||||
|
DROP POLICY IF EXISTS "Admins can update blog posts with AAL2" ON public.blog_posts;
|
||||||
|
CREATE POLICY "Admins can update blog posts with MFA"
|
||||||
|
ON public.blog_posts
|
||||||
|
FOR UPDATE
|
||||||
|
TO authenticated
|
||||||
|
USING (
|
||||||
|
(has_role(auth.uid(), 'admin'::app_role) OR has_role(auth.uid(), 'superuser'::app_role))
|
||||||
|
AND ((NOT has_mfa_enabled((SELECT auth.uid()))) OR has_aal2())
|
||||||
|
);
|
||||||
|
|
||||||
|
DROP POLICY IF EXISTS "Admins can delete blog posts with MFA" ON public.blog_posts;
|
||||||
|
DROP POLICY IF EXISTS "Admins can delete blog posts with AAL2" ON public.blog_posts;
|
||||||
|
CREATE POLICY "Admins can delete blog posts with MFA"
|
||||||
|
ON public.blog_posts
|
||||||
|
FOR DELETE
|
||||||
|
TO authenticated
|
||||||
|
USING (
|
||||||
|
(has_role(auth.uid(), 'admin'::app_role) OR has_role(auth.uid(), 'superuser'::app_role))
|
||||||
|
AND ((NOT has_mfa_enabled((SELECT auth.uid()))) OR has_aal2())
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- ADMIN AUDIT LOG - Only fix the SELECT policy
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
-- Note: INSERT policy already uses has_aal2() correctly, no need to change
|
||||||
|
-- We're only fixing policies that query auth.mfa_factors directly
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- COMPANY SUBMISSIONS
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
DROP POLICY IF EXISTS "Moderators can view all company submissions" ON public.company_submissions;
|
||||||
|
CREATE POLICY "Moderators can view all company submissions"
|
||||||
|
ON public.company_submissions
|
||||||
|
FOR SELECT
|
||||||
|
TO authenticated
|
||||||
|
USING (
|
||||||
|
is_moderator((SELECT auth.uid()))
|
||||||
|
AND ((NOT has_mfa_enabled((SELECT auth.uid()))) OR has_aal2())
|
||||||
|
);
|
||||||
|
|
||||||
|
DROP POLICY IF EXISTS "Moderators can update company submissions" ON public.company_submissions;
|
||||||
|
CREATE POLICY "Moderators can update company submissions"
|
||||||
|
ON public.company_submissions
|
||||||
|
FOR UPDATE
|
||||||
|
TO authenticated
|
||||||
|
USING (
|
||||||
|
is_moderator((SELECT auth.uid()))
|
||||||
|
AND ((NOT has_mfa_enabled((SELECT auth.uid()))) OR has_aal2())
|
||||||
|
);
|
||||||
|
|
||||||
|
DROP POLICY IF EXISTS "Moderators can delete company submissions" ON public.company_submissions;
|
||||||
|
CREATE POLICY "Moderators can delete company submissions"
|
||||||
|
ON public.company_submissions
|
||||||
|
FOR DELETE
|
||||||
|
TO authenticated
|
||||||
|
USING (
|
||||||
|
is_moderator((SELECT auth.uid()))
|
||||||
|
AND ((NOT has_mfa_enabled((SELECT auth.uid()))) OR has_aal2())
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- PARK SUBMISSIONS
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
DROP POLICY IF EXISTS "Moderators can view all park submissions" ON public.park_submissions;
|
||||||
|
CREATE POLICY "Moderators can view all park submissions"
|
||||||
|
ON public.park_submissions
|
||||||
|
FOR SELECT
|
||||||
|
TO authenticated
|
||||||
|
USING (
|
||||||
|
is_moderator((SELECT auth.uid()))
|
||||||
|
AND ((NOT has_mfa_enabled((SELECT auth.uid()))) OR has_aal2())
|
||||||
|
);
|
||||||
|
|
||||||
|
DROP POLICY IF EXISTS "Moderators can update park submissions" ON public.park_submissions;
|
||||||
|
CREATE POLICY "Moderators can update park submissions"
|
||||||
|
ON public.park_submissions
|
||||||
|
FOR UPDATE
|
||||||
|
TO authenticated
|
||||||
|
USING (
|
||||||
|
is_moderator((SELECT auth.uid()))
|
||||||
|
AND ((NOT has_mfa_enabled((SELECT auth.uid()))) OR has_aal2())
|
||||||
|
);
|
||||||
|
|
||||||
|
DROP POLICY IF EXISTS "Moderators can delete park submissions" ON public.park_submissions;
|
||||||
|
CREATE POLICY "Moderators can delete park submissions"
|
||||||
|
ON public.park_submissions
|
||||||
|
FOR DELETE
|
||||||
|
TO authenticated
|
||||||
|
USING (
|
||||||
|
is_moderator((SELECT auth.uid()))
|
||||||
|
AND ((NOT has_mfa_enabled((SELECT auth.uid()))) OR has_aal2())
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- PHOTO SUBMISSION ITEMS
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
DROP POLICY IF EXISTS "Moderators can manage photo submission items" ON public.photo_submission_items;
|
||||||
|
CREATE POLICY "Moderators can manage photo submission items"
|
||||||
|
ON public.photo_submission_items
|
||||||
|
FOR ALL
|
||||||
|
TO authenticated
|
||||||
|
USING (
|
||||||
|
is_moderator((SELECT auth.uid()))
|
||||||
|
AND ((NOT has_mfa_enabled((SELECT auth.uid()))) OR has_aal2())
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- PHOTO SUBMISSIONS
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
DROP POLICY IF EXISTS "Moderators can manage photo submissions" ON public.photo_submissions;
|
||||||
|
CREATE POLICY "Moderators can manage photo submissions"
|
||||||
|
ON public.photo_submissions
|
||||||
|
FOR ALL
|
||||||
|
TO authenticated
|
||||||
|
USING (
|
||||||
|
is_moderator((SELECT auth.uid()))
|
||||||
|
AND ((NOT has_mfa_enabled((SELECT auth.uid()))) OR has_aal2())
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- REPORTS
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
DROP POLICY IF EXISTS "Moderators can manage reports" ON public.reports;
|
||||||
|
CREATE POLICY "Moderators can manage reports"
|
||||||
|
ON public.reports
|
||||||
|
FOR ALL
|
||||||
|
TO authenticated
|
||||||
|
USING (
|
||||||
|
is_moderator((SELECT auth.uid()))
|
||||||
|
AND ((NOT has_mfa_enabled((SELECT auth.uid()))) OR has_aal2())
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- RIDE MODEL SUBMISSIONS
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
DROP POLICY IF EXISTS "Moderators can manage ride model submissions" ON public.ride_model_submissions;
|
||||||
|
CREATE POLICY "Moderators can manage ride model submissions"
|
||||||
|
ON public.ride_model_submissions
|
||||||
|
FOR ALL
|
||||||
|
TO authenticated
|
||||||
|
USING (
|
||||||
|
is_moderator((SELECT auth.uid()))
|
||||||
|
AND ((NOT has_mfa_enabled((SELECT auth.uid()))) OR has_aal2())
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- RIDE SUBMISSIONS
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
DROP POLICY IF EXISTS "Moderators can manage ride submissions" ON public.ride_submissions;
|
||||||
|
CREATE POLICY "Moderators can manage ride submissions"
|
||||||
|
ON public.ride_submissions
|
||||||
|
FOR ALL
|
||||||
|
TO authenticated
|
||||||
|
USING (
|
||||||
|
is_moderator((SELECT auth.uid()))
|
||||||
|
AND ((NOT has_mfa_enabled((SELECT auth.uid()))) OR has_aal2())
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- SUBMISSION ITEMS
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
DROP POLICY IF EXISTS "Moderators can view all submission items" ON public.submission_items;
|
||||||
|
CREATE POLICY "Moderators can view all submission items"
|
||||||
|
ON public.submission_items
|
||||||
|
FOR SELECT
|
||||||
|
TO authenticated
|
||||||
|
USING (
|
||||||
|
is_moderator((SELECT auth.uid()))
|
||||||
|
AND ((NOT has_mfa_enabled((SELECT auth.uid()))) OR has_aal2())
|
||||||
|
);
|
||||||
|
|
||||||
|
DROP POLICY IF EXISTS "Moderators can update submission items" ON public.submission_items;
|
||||||
|
CREATE POLICY "Moderators can update submission items"
|
||||||
|
ON public.submission_items
|
||||||
|
FOR UPDATE
|
||||||
|
TO authenticated
|
||||||
|
USING (
|
||||||
|
is_moderator((SELECT auth.uid()))
|
||||||
|
AND ((NOT has_mfa_enabled((SELECT auth.uid()))) OR has_aal2())
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- TIMELINE EVENT SUBMISSIONS
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
DROP POLICY IF EXISTS "Moderators can manage timeline event submissions" ON public.timeline_event_submissions;
|
||||||
|
CREATE POLICY "Moderators can manage timeline event submissions"
|
||||||
|
ON public.timeline_event_submissions
|
||||||
|
FOR ALL
|
||||||
|
TO authenticated
|
||||||
|
USING (
|
||||||
|
is_moderator((SELECT auth.uid()))
|
||||||
|
AND ((NOT has_mfa_enabled((SELECT auth.uid()))) OR has_aal2())
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- VERIFICATION
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
COMMENT ON FUNCTION public.has_mfa_enabled IS 'Security definer function to safely check MFA enrollment without granting direct access to auth.mfa_factors table';
|
||||||
Reference in New Issue
Block a user