-- Fix Security Definer Views Issue -- Add explicit security_invoker = true to views that are missing it -- This ensures views execute with the querying user's permissions, not the creator's -- Fix moderation_queue_with_entities view DROP VIEW IF EXISTS public.moderation_queue_with_entities CASCADE; CREATE VIEW public.moderation_queue_with_entities WITH (security_invoker = true) AS SELECT cs.id, cs.submission_type, cs.status, -- Temporal fields (with backward compatibility alias) cs.submitted_at AS created_at, cs.submitted_at, cs.reviewed_at, cs.resolved_at, cs.assigned_at, cs.escalated_at, cs.last_modified_at, cs.first_reviewed_at, -- User IDs cs.user_id AS submitted_by, cs.reviewer_id AS reviewed_by, cs.assigned_to, cs.escalated_by, cs.last_modified_by, -- State flags cs.escalated, cs.escalation_reason, cs.reviewer_notes AS review_notes, cs.approval_mode, cs.review_count, cs.locked_until, cs.is_test_data, -- Submitter profile (aliased for backward compatibility) sp.id AS submitter_profile_id, sp.username AS submitter_username, sp.display_name AS submitter_display_name, sp.avatar_url AS submitter_avatar_url, sp.reputation_score AS submitter_reputation, -- Reviewer profile rp.id AS reviewer_profile_id, rp.username AS reviewer_username, rp.display_name AS reviewer_display_name, rp.avatar_url AS reviewer_avatar_url, -- Assignee profile ap.id AS assignee_profile_id, ap.username AS assignee_username, ap.display_name AS assignee_display_name, ap.avatar_url AS assignee_avatar_url, -- Submission items (aggregated with new typed FK columns) ( SELECT json_agg(si.*) FROM submission_items si WHERE si.submission_id = cs.id ) as submission_items FROM content_submissions cs LEFT JOIN profiles sp ON sp.user_id = cs.user_id LEFT JOIN profiles rp ON rp.user_id = cs.reviewer_id LEFT JOIN profiles ap ON ap.user_id = cs.assigned_to; COMMENT ON VIEW public.moderation_queue_with_entities IS 'Optimized view for moderation queue with pre-joined entity data. Uses security_invoker to respect querying user permissions and RLS policies.'; -- Fix filtered_profiles view DROP VIEW IF EXISTS public.filtered_profiles CASCADE; CREATE VIEW public.filtered_profiles WITH (security_invoker = true) AS SELECT p.id, p.user_id, p.username, p.display_name, p.privacy_level, p.created_at, p.updated_at, -- Field-level privacy using security definer functions CASE WHEN can_view_profile_field(COALESCE(auth.uid(), '00000000-0000-0000-0000-000000000000'::uuid), p.user_id, 'bio') THEN p.bio ELSE NULL END AS bio, CASE WHEN can_view_profile_field(COALESCE(auth.uid(), '00000000-0000-0000-0000-000000000000'::uuid), p.user_id, 'avatar_url') THEN p.avatar_url ELSE NULL END AS avatar_url, CASE WHEN can_view_profile_field(COALESCE(auth.uid(), '00000000-0000-0000-0000-000000000000'::uuid), p.user_id, 'avatar_image_id') THEN p.avatar_image_id ELSE NULL END AS avatar_image_id, CASE WHEN can_view_profile_field(COALESCE(auth.uid(), '00000000-0000-0000-0000-000000000000'::uuid), p.user_id, 'preferred_pronouns') THEN p.preferred_pronouns ELSE NULL END AS preferred_pronouns, CASE WHEN can_view_profile_field(COALESCE(auth.uid(), '00000000-0000-0000-0000-000000000000'::uuid), p.user_id, 'preferred_pronouns') THEN p.show_pronouns ELSE false END AS show_pronouns, CASE WHEN can_view_profile_field(COALESCE(auth.uid(), '00000000-0000-0000-0000-000000000000'::uuid), p.user_id, 'personal_location') THEN p.personal_location ELSE NULL END AS personal_location, CASE WHEN can_view_profile_field(COALESCE(auth.uid(), '00000000-0000-0000-0000-000000000000'::uuid), p.user_id, 'location_id') THEN p.location_id ELSE NULL END AS location_id, CASE WHEN can_view_profile_field(COALESCE(auth.uid(), '00000000-0000-0000-0000-000000000000'::uuid), p.user_id, 'home_park_id') THEN p.home_park_id ELSE NULL END AS home_park_id, CASE WHEN can_view_profile_field(COALESCE(auth.uid(), '00000000-0000-0000-0000-000000000000'::uuid), p.user_id, 'date_of_birth') THEN p.date_of_birth ELSE NULL END AS date_of_birth, p.timezone, p.preferred_language, p.theme_preference, CASE WHEN can_view_profile_field(COALESCE(auth.uid(), '00000000-0000-0000-0000-000000000000'::uuid), p.user_id, 'ride_count') THEN p.ride_count ELSE 0 END AS ride_count, CASE WHEN can_view_profile_field(COALESCE(auth.uid(), '00000000-0000-0000-0000-000000000000'::uuid), p.user_id, 'ride_count') THEN p.coaster_count ELSE 0 END AS coaster_count, CASE WHEN can_view_profile_field(COALESCE(auth.uid(), '00000000-0000-0000-0000-000000000000'::uuid), p.user_id, 'ride_count') THEN p.park_count ELSE 0 END AS park_count, CASE WHEN can_view_profile_field(COALESCE(auth.uid(), '00000000-0000-0000-0000-000000000000'::uuid), p.user_id, 'ride_count') THEN p.review_count ELSE 0 END AS review_count, CASE WHEN can_view_profile_field(COALESCE(auth.uid(), '00000000-0000-0000-0000-000000000000'::uuid), p.user_id, 'ride_count') THEN p.reputation_score ELSE 0 END AS reputation_score, CASE WHEN auth.uid() = p.user_id OR is_moderator(COALESCE(auth.uid(), '00000000-0000-0000-0000-000000000000'::uuid)) THEN p.banned ELSE false END AS banned FROM public.profiles p WHERE (NOT p.banned OR auth.uid() = p.user_id OR is_moderator(COALESCE(auth.uid(), '00000000-0000-0000-0000-000000000000'::uuid))) AND (p.privacy_level = 'public' OR auth.uid() = p.user_id OR is_moderator(COALESCE(auth.uid(), '00000000-0000-0000-0000-000000000000'::uuid))); COMMENT ON VIEW public.filtered_profiles IS 'Profile view with field-level privacy controls. Uses security_invoker to respect querying user context and RLS policies. Privacy checks use security definer functions for consistent permission enforcement.';