Approve tool use

This commit is contained in:
gpt-engineer-app[bot]
2025-10-20 12:58:09 +00:00
parent 4983960138
commit 6f1baef8c0
7 changed files with 263 additions and 34 deletions

View File

@@ -9,7 +9,8 @@ import { useQuery } from '@tanstack/react-query';
import { supabase } from '@/integrations/supabase/client'; import { supabase } from '@/integrations/supabase/client';
import { useAuth } from '@/hooks/useAuth'; import { useAuth } from '@/hooks/useAuth';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { getErrorMessage } from '@/lib/errorHandler'; import { getErrorMessage, handleError } from '@/lib/errorHandler';
import { logger } from '@/lib/logger';
import { deleteTimelineEvent } from '@/lib/entitySubmissionHelpers'; import { deleteTimelineEvent } from '@/lib/entitySubmissionHelpers';
import type { EntityType, TimelineEvent } from '@/types/timeline'; import type { EntityType, TimelineEvent } from '@/types/timeline';
@@ -114,11 +115,11 @@ export function EntityTimelineManager({
toast.success('Event deleted', { toast.success('Event deleted', {
description: 'Your timeline event has been deleted successfully.' description: 'Your timeline event has been deleted successfully.'
}); });
} catch (error) { } catch (error: unknown) {
const errorMsg = getErrorMessage(error); handleError(error, {
console.error('Delete error:', errorMsg); action: 'Delete Timeline Event',
toast.error('Failed to delete event', { userId: user?.id,
description: errorMsg metadata: { eventId }
}); });
} }
}; };

View File

@@ -46,7 +46,7 @@ import { Loader2, Trash } from 'lucide-react';
import { useAuth } from '@/hooks/useAuth'; import { useAuth } from '@/hooks/useAuth';
import { useToast } from '@/hooks/use-toast'; import { useToast } from '@/hooks/use-toast';
import { submitTimelineEvent, submitTimelineEventUpdate, deleteTimelineEvent } from '@/lib/entitySubmissionHelpers'; import { submitTimelineEvent, submitTimelineEventUpdate, deleteTimelineEvent } from '@/lib/entitySubmissionHelpers';
import { getErrorMessage } from '@/lib/errorHandler'; import { handleError } from '@/lib/errorHandler';
import type { import type {
EntityType, EntityType,
TimelineEventFormData, TimelineEventFormData,
@@ -170,12 +170,11 @@ export function TimelineEventEditorDialog({
form.reset(); form.reset();
onOpenChange(false); onOpenChange(false);
onSuccess?.(); onSuccess?.();
} catch (error) { } catch (error: unknown) {
console.error('Failed to submit timeline event:', error); handleError(error, {
toast({ action: isEditing ? 'Update Timeline Event' : 'Submit Timeline Event',
title: 'Submission failed', userId: user?.id,
description: error instanceof Error ? error.message : 'Failed to submit timeline event', metadata: { entityType, entityId }
variant: 'destructive',
}); });
} finally { } finally {
setIsSubmitting(false); setIsSubmitting(false);
@@ -196,13 +195,11 @@ export function TimelineEventEditorDialog({
onOpenChange(false); onOpenChange(false);
setShowDeleteConfirm(false); setShowDeleteConfirm(false);
form.reset(); form.reset();
} catch (error) { } catch (error: unknown) {
const errorMsg = getErrorMessage(error); handleError(error, {
console.error('Delete error:', errorMsg); action: 'Delete Timeline Event',
toast({ userId: user?.id,
title: 'Failed to delete event', metadata: { eventId: existingEvent?.id }
description: errorMsg,
variant: 'destructive',
}); });
} finally { } finally {
setIsDeleting(false); setIsDeleting(false);

View File

@@ -3235,6 +3235,94 @@ export type Database = {
}, },
] ]
} }
timeline_event_submissions: {
Row: {
created_at: string
description: string | null
display_order: number | null
entity_id: string
entity_type: string
event_date: string
event_date_precision: string
event_type: string
from_entity_id: string | null
from_location_id: string | null
from_value: string | null
id: string
is_public: boolean | null
submission_id: string
title: string
to_entity_id: string | null
to_location_id: string | null
to_value: string | null
updated_at: string
}
Insert: {
created_at?: string
description?: string | null
display_order?: number | null
entity_id: string
entity_type: string
event_date: string
event_date_precision: string
event_type: string
from_entity_id?: string | null
from_location_id?: string | null
from_value?: string | null
id?: string
is_public?: boolean | null
submission_id: string
title: string
to_entity_id?: string | null
to_location_id?: string | null
to_value?: string | null
updated_at?: string
}
Update: {
created_at?: string
description?: string | null
display_order?: number | null
entity_id?: string
entity_type?: string
event_date?: string
event_date_precision?: string
event_type?: string
from_entity_id?: string | null
from_location_id?: string | null
from_value?: string | null
id?: string
is_public?: boolean | null
submission_id?: string
title?: string
to_entity_id?: string | null
to_location_id?: string | null
to_value?: string | null
updated_at?: string
}
Relationships: [
{
foreignKeyName: "timeline_event_submissions_from_location_id_fkey"
columns: ["from_location_id"]
isOneToOne: false
referencedRelation: "locations"
referencedColumns: ["id"]
},
{
foreignKeyName: "timeline_event_submissions_submission_id_fkey"
columns: ["submission_id"]
isOneToOne: false
referencedRelation: "content_submissions"
referencedColumns: ["id"]
},
{
foreignKeyName: "timeline_event_submissions_to_location_id_fkey"
columns: ["to_location_id"]
isOneToOne: false
referencedRelation: "locations"
referencedColumns: ["id"]
},
]
}
user_blocks: { user_blocks: {
Row: { Row: {
blocked_id: string blocked_id: string

View File

@@ -7,6 +7,7 @@ import { extractChangedFields } from './submissionChangeDetection';
import type { CompanyDatabaseRecord, TimelineEventDatabaseRecord } from '@/types/company-data'; import type { CompanyDatabaseRecord, TimelineEventDatabaseRecord } from '@/types/company-data';
import { logger } from './logger'; import { logger } from './logger';
import { getErrorMessage } from './errorHandler'; import { getErrorMessage } from './errorHandler';
import type { TimelineEventFormData, EntityType } from '@/types/timeline';
/** /**
* ═══════════════════════════════════════════════════════════════════ * ═══════════════════════════════════════════════════════════════════
@@ -153,9 +154,6 @@ export interface RideModelFormData {
card_image_id?: string; card_image_id?: string;
} }
// Import timeline types
import type { TimelineEventFormData, TimelineSubmissionData, EntityType } from '@/types/timeline';
/** /**
* ⚠️ CRITICAL SECURITY PATTERN ⚠️ * ⚠️ CRITICAL SECURITY PATTERN ⚠️
* *
@@ -1183,7 +1181,7 @@ export async function submitTimelineEvent(
}; };
const items = [{ const items = [{
item_type: 'milestone', item_type: 'timeline_event',
action_type: 'create', action_type: 'create',
item_data: itemData, item_data: itemData,
order_index: 0, order_index: 0,
@@ -1192,7 +1190,7 @@ export async function submitTimelineEvent(
const { data: submissionId, error } = await supabase const { data: submissionId, error } = await supabase
.rpc('create_submission_with_items', { .rpc('create_submission_with_items', {
p_user_id: userId, p_user_id: userId,
p_submission_type: 'milestone', p_submission_type: 'timeline_event',
p_content: content, p_content: content,
p_items: items as unknown as Json[], p_items: items as unknown as Json[],
}); });
@@ -1255,7 +1253,7 @@ export async function submitTimelineEventUpdate(
'create_submission_with_items', 'create_submission_with_items',
{ {
p_user_id: userId, p_user_id: userId,
p_submission_type: 'milestone', p_submission_type: 'timeline_event',
p_content: { p_content: {
action: 'edit', action: 'edit',
event_id: eventId, event_id: eventId,
@@ -1263,7 +1261,7 @@ export async function submitTimelineEventUpdate(
} as unknown as Json, } as unknown as Json,
p_items: [ p_items: [
{ {
item_type: 'milestone', item_type: 'timeline_event',
action_type: 'edit', action_type: 'edit',
item_data: itemData, item_data: itemData,
original_data: originalEvent, original_data: originalEvent,
@@ -1290,12 +1288,7 @@ export async function submitTimelineEventUpdate(
}; };
} }
/**
* Delete a timeline event (only pending/own events)
* @param eventId - Timeline event ID
* @param userId - Current user ID
* @throws Error if event not found or cannot be deleted
*/
export async function deleteTimelineEvent( export async function deleteTimelineEvent(
eventId: string, eventId: string,
userId: string userId: string

View File

@@ -90,3 +90,21 @@ export interface RideModelSubmissionData {
card_image_url?: string | null; card_image_url?: string | null;
card_image_id?: string | null; card_image_id?: string | null;
} }
export interface TimelineEventItemData {
entity_id: string;
entity_type: 'park' | 'ride' | 'company' | 'ride_model';
event_type: string;
event_date: string; // ISO date
event_date_precision: 'day' | 'month' | 'year';
title: string;
description?: string | null;
from_value?: string | null;
to_value?: string | null;
from_entity_id?: string | null;
to_entity_id?: string | null;
from_location_id?: string | null;
to_location_id?: string | null;
display_order?: number;
is_public?: boolean;
}

View File

@@ -359,7 +359,7 @@ serve(async (req) => {
entityId = resolvedData.photo_id; entityId = resolvedData.photo_id;
break; break;
case 'milestone': case 'milestone':
case 'timeline_event': // Keep for backward compatibility case 'timeline_event': // Unified timeline event handling
entityId = await createTimelineEvent(supabase, resolvedData, submitterId, authenticatedUserId, submissionId); entityId = await createTimelineEvent(supabase, resolvedData, submitterId, authenticatedUserId, submissionId);
break; break;
default: default:

View File

@@ -0,0 +1,132 @@
-- Phase 1: Timeline Event Submissions Infrastructure
-- Create timeline_event_submissions table (NO JSONB - fully relational)
CREATE TABLE timeline_event_submissions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
submission_id UUID NOT NULL REFERENCES content_submissions(id) ON DELETE CASCADE,
-- Entity reference
entity_id UUID NOT NULL,
entity_type TEXT NOT NULL CHECK (entity_type IN ('park', 'ride', 'company', 'ride_model')),
-- Event core data
event_type TEXT NOT NULL CHECK (event_type IN (
'opening', 'closing', 'name_change', 'relocation', 'renovation',
'ownership_change', 'operator_change', 'status_change', 'milestone', 'other'
)),
event_date DATE NOT NULL,
event_date_precision TEXT NOT NULL CHECK (event_date_precision IN ('day', 'month', 'year')),
title TEXT NOT NULL CHECK (char_length(title) >= 1 AND char_length(title) <= 200),
description TEXT CHECK (description IS NULL OR char_length(description) <= 2000),
-- Relational fields for specific event types (NO JSONB!)
from_value TEXT,
to_value TEXT,
from_entity_id UUID,
to_entity_id UUID,
from_location_id UUID REFERENCES locations(id),
to_location_id UUID REFERENCES locations(id),
-- Display settings
display_order INTEGER DEFAULT 0,
is_public BOOLEAN DEFAULT TRUE,
-- Timestamps
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Indexes for performance
CREATE INDEX idx_timeline_event_submissions_submission ON timeline_event_submissions(submission_id);
CREATE INDEX idx_timeline_event_submissions_entity ON timeline_event_submissions(entity_type, entity_id);
CREATE INDEX idx_timeline_event_submissions_date ON timeline_event_submissions(event_date);
-- Enable RLS
ALTER TABLE timeline_event_submissions ENABLE ROW LEVEL SECURITY;
-- RLS Policy: Users can insert their own timeline submissions
CREATE POLICY "Users can insert own timeline submissions"
ON timeline_event_submissions FOR INSERT
WITH CHECK (
EXISTS (
SELECT 1 FROM content_submissions cs
WHERE cs.id = timeline_event_submissions.submission_id
AND cs.user_id = auth.uid()
)
);
-- RLS Policy: Moderators can view all timeline submissions (with MFA)
CREATE POLICY "Moderators can view all timeline submissions"
ON timeline_event_submissions FOR SELECT
USING (is_moderator(auth.uid()) AND has_aal2());
-- RLS Policy: Users can view their own timeline submissions
CREATE POLICY "Users can view own timeline submissions"
ON timeline_event_submissions FOR SELECT
USING (
EXISTS (
SELECT 1 FROM content_submissions cs
WHERE cs.id = timeline_event_submissions.submission_id
AND cs.user_id = auth.uid()
)
);
-- RLS Policy: Moderators can update timeline submissions (for review)
CREATE POLICY "Moderators can update timeline submissions"
ON timeline_event_submissions FOR UPDATE
USING (is_moderator(auth.uid()) AND has_aal2())
WITH CHECK (is_moderator(auth.uid()) AND has_aal2());
-- RLS Policy: Moderators can delete timeline submissions
CREATE POLICY "Moderators can delete timeline submissions"
ON timeline_event_submissions FOR DELETE
USING (is_moderator(auth.uid()) AND has_aal2());
-- Update trigger for timestamps
CREATE TRIGGER update_timeline_event_submissions_updated_at
BEFORE UPDATE ON timeline_event_submissions
FOR EACH ROW
EXECUTE FUNCTION update_content_submissions_updated_at();
-- =====================================================
-- Fix entity_timeline_events RLS (DENY direct writes)
-- =====================================================
-- Drop existing problematic policies if they exist
DROP POLICY IF EXISTS "Users can submit timeline events" ON entity_timeline_events;
DROP POLICY IF EXISTS "Users can delete own pending timeline events" ON entity_timeline_events;
DROP POLICY IF EXISTS "Users can update own pending timeline events" ON entity_timeline_events;
DROP POLICY IF EXISTS "Users can view own pending timeline events" ON entity_timeline_events;
-- CRITICAL: Deny direct inserts from users (only service role via edge function)
CREATE POLICY "Deny direct inserts to timeline events"
ON entity_timeline_events FOR INSERT
WITH CHECK (FALSE);
-- CRITICAL: Deny direct updates from users
CREATE POLICY "Deny direct updates to timeline events"
ON entity_timeline_events FOR UPDATE
USING (FALSE);
-- Allow public to view approved events
DROP POLICY IF EXISTS "Public can view approved timeline events" ON entity_timeline_events;
CREATE POLICY "Public can view approved timeline events"
ON entity_timeline_events FOR SELECT
USING (is_public = TRUE AND approved_by IS NOT NULL);
-- Moderators can view all events
DROP POLICY IF EXISTS "Moderators can view all timeline events" ON entity_timeline_events;
CREATE POLICY "Moderators can view all timeline events"
ON entity_timeline_events FOR SELECT
USING (is_moderator(auth.uid()) AND has_aal2());
-- Only moderators can delete (for cleanup/corrections)
DROP POLICY IF EXISTS "Moderators can delete timeline events" ON entity_timeline_events;
CREATE POLICY "Moderators can delete timeline events"
ON entity_timeline_events FOR DELETE
USING (is_moderator(auth.uid()) AND has_aal2());
-- Comment for clarity
COMMENT ON TABLE timeline_event_submissions IS 'Timeline event submissions go through moderation queue before being approved into entity_timeline_events';
COMMENT ON POLICY "Deny direct inserts to timeline events" ON entity_timeline_events IS 'Only edge functions (service role) can insert approved timeline events';
COMMENT ON POLICY "Deny direct updates to timeline events" ON entity_timeline_events IS 'Timeline events are immutable after approval - delete and recreate if needed';