diff --git a/supabase/migrations/20251112134512_4bcf2594-220f-436a-b2f4-96e8d213848e.sql b/supabase/migrations/20251112134512_4bcf2594-220f-436a-b2f4-96e8d213848e.sql new file mode 100644 index 00000000..45e527c0 --- /dev/null +++ b/supabase/migrations/20251112134512_4bcf2594-220f-436a-b2f4-96e8d213848e.sql @@ -0,0 +1,259 @@ +-- Fix security warning: Add search_path to process_approval_transaction +CREATE OR REPLACE FUNCTION process_approval_transaction( + p_submission_id UUID, + p_item_ids UUID[], + p_moderator_id UUID, + p_submitter_id UUID, + p_request_id TEXT DEFAULT NULL, + p_approval_mode TEXT DEFAULT 'full', + p_idempotency_key TEXT DEFAULT NULL +) RETURNS JSONB +LANGUAGE plpgsql +SECURITY DEFINER +SET search_path TO 'public' +AS $$ +DECLARE + v_item RECORD; + v_entity_id UUID; + v_entity_type TEXT; + v_action_type TEXT; + v_item_data JSONB; + v_approved_items JSONB := '[]'::JSONB; + v_failed_items JSONB := '[]'::JSONB; + v_submission_type TEXT; + v_result JSONB; + v_error_message TEXT; + v_error_detail TEXT; + v_start_time TIMESTAMP := clock_timestamp(); + v_duration_ms INTEGER; + v_rollback_triggered BOOLEAN := FALSE; + v_lock_acquired BOOLEAN := FALSE; +BEGIN + -- Validate moderator has permission + IF NOT is_moderator(p_moderator_id) THEN + RAISE EXCEPTION 'User % does not have moderator privileges', p_moderator_id + USING ERRCODE = 'insufficient_privilege'; + END IF; + + -- Get submission type + SELECT submission_type INTO v_submission_type + FROM content_submissions + WHERE id = p_submission_id; + + IF v_submission_type IS NULL THEN + RAISE EXCEPTION 'Submission % not found', p_submission_id + USING ERRCODE = 'no_data_found'; + END IF; + + -- Acquire advisory lock + IF NOT pg_try_advisory_xact_lock(hashtext(p_submission_id::TEXT)) THEN + RAISE EXCEPTION 'Could not acquire lock for submission %', p_submission_id + USING ERRCODE = '55P03'; + END IF; + v_lock_acquired := TRUE; + + -- Process each item + FOR v_item IN + SELECT si.* + FROM submission_items si + WHERE si.submission_id = p_submission_id + AND si.id = ANY(p_item_ids) + AND si.status = 'pending' + ORDER BY si.order_index + LOOP + BEGIN + v_entity_type := v_item.item_type; + v_action_type := v_item.action_type; + v_item_data := v_item.item_data; + + -- Create/update entity based on type and action + IF v_action_type = 'create' THEN + IF v_entity_type = 'park' THEN + INSERT INTO parks (name, slug, description, location_id, operator_id, property_owner_id) + SELECT + v_item_data->>'name', + v_item_data->>'slug', + v_item_data->>'description', + (v_item_data->>'location_id')::UUID, + (v_item_data->>'operator_id')::UUID, + (v_item_data->>'property_owner_id')::UUID + RETURNING id INTO v_entity_id; + + ELSIF v_entity_type = 'ride' THEN + INSERT INTO rides (name, slug, park_id, manufacturer_id, designer_id) + SELECT + v_item_data->>'name', + v_item_data->>'slug', + (v_item_data->>'park_id')::UUID, + (v_item_data->>'manufacturer_id')::UUID, + (v_item_data->>'designer_id')::UUID + RETURNING id INTO v_entity_id; + + ELSIF v_entity_type IN ('manufacturer', 'operator', 'designer', 'property_owner') THEN + INSERT INTO companies (name, slug, company_type, description) + SELECT + v_item_data->>'name', + v_item_data->>'slug', + v_entity_type, + v_item_data->>'description' + RETURNING id INTO v_entity_id; + + ELSE + RAISE EXCEPTION 'Unsupported entity type: %', v_entity_type; + END IF; + + ELSIF v_action_type = 'edit' THEN + v_entity_id := (v_item_data->>'entity_id')::UUID; + + IF v_entity_type = 'park' THEN + UPDATE parks SET + name = COALESCE(v_item_data->>'name', name), + description = COALESCE(v_item_data->>'description', description), + location_id = COALESCE((v_item_data->>'location_id')::UUID, location_id), + updated_at = now() + WHERE id = v_entity_id; + + ELSIF v_entity_type = 'ride' THEN + UPDATE rides SET + name = COALESCE(v_item_data->>'name', name), + description = COALESCE(v_item_data->>'description', description), + updated_at = now() + WHERE id = v_entity_id; + + ELSIF v_entity_type IN ('manufacturer', 'operator', 'designer', 'property_owner') THEN + UPDATE companies SET + name = COALESCE(v_item_data->>'name', name), + description = COALESCE(v_item_data->>'description', description), + updated_at = now() + WHERE id = v_entity_id; + END IF; + END IF; + + -- Update submission item with approved status and timestamp + UPDATE submission_items + SET + approved_entity_id = v_entity_id, + status = 'approved', + approved_at = now(), + updated_at = now() + WHERE id = v_item.id; + + -- Add to success list + v_approved_items := v_approved_items || jsonb_build_object( + 'item_id', v_item.id, + 'entity_id', v_entity_id, + 'entity_type', v_entity_type + ); + + EXCEPTION WHEN OTHERS THEN + GET STACKED DIAGNOSTICS + v_error_message = MESSAGE_TEXT, + v_error_detail = PG_EXCEPTION_DETAIL; + + -- Add to failed list + v_failed_items := v_failed_items || jsonb_build_object( + 'item_id', v_item.id, + 'error', v_error_message, + 'detail', v_error_detail + ); + + -- Mark item as failed + UPDATE submission_items + SET + status = 'flagged', + rejection_reason = v_error_message, + updated_at = now() + WHERE id = v_item.id; + END; + END LOOP; + + -- Update submission status based on approval mode + IF p_approval_mode = 'selective' THEN + UPDATE content_submissions + SET + status = 'partially_approved', + reviewed_at = now(), + reviewer_id = p_moderator_id, + updated_at = now() + WHERE id = p_submission_id; + ELSE + UPDATE content_submissions + SET + status = 'approved', + reviewed_at = now(), + reviewer_id = p_moderator_id, + resolved_at = now(), + updated_at = now() + WHERE id = p_submission_id; + END IF; + + -- Calculate duration + v_duration_ms := EXTRACT(EPOCH FROM (clock_timestamp() - v_start_time)) * 1000; + + -- Log metrics + INSERT INTO approval_transaction_metrics ( + submission_id, + moderator_id, + submitter_id, + items_count, + success, + duration_ms, + request_id, + rollback_triggered + ) VALUES ( + p_submission_id, + p_moderator_id, + p_submitter_id, + jsonb_array_length(v_approved_items), + jsonb_array_length(v_failed_items) = 0, + v_duration_ms, + p_request_id, + v_rollback_triggered + ); + + -- Build result + v_result := jsonb_build_object( + 'success', TRUE, + 'approved_items', v_approved_items, + 'failed_items', v_failed_items, + 'duration_ms', v_duration_ms + ); + + RETURN v_result; + +EXCEPTION WHEN OTHERS THEN + v_rollback_triggered := TRUE; + GET STACKED DIAGNOSTICS + v_error_message = MESSAGE_TEXT, + v_error_detail = PG_EXCEPTION_DETAIL; + + -- Log failed transaction + v_duration_ms := EXTRACT(EPOCH FROM (clock_timestamp() - v_start_time)) * 1000; + + INSERT INTO approval_transaction_metrics ( + submission_id, + moderator_id, + submitter_id, + items_count, + success, + duration_ms, + error_message, + error_details, + request_id, + rollback_triggered + ) VALUES ( + p_submission_id, + p_moderator_id, + p_submitter_id, + array_length(p_item_ids, 1), + FALSE, + v_duration_ms, + v_error_message, + v_error_detail, + p_request_id, + v_rollback_triggered + ); + + RAISE; +END; +$$; \ No newline at end of file