-- Revert the previous policy and implement proper security model -- The filtered_profiles view should be the ONLY way to view other users' profiles -- Remove the policy that allows direct profile viewing DROP POLICY IF EXISTS "Authenticated users can view public profiles" ON public.profiles; -- Change filtered_profiles view to use SECURITY DEFINER -- This allows it to bypass RLS and implement its own access control DROP VIEW IF EXISTS public.filtered_profiles; CREATE VIEW public.filtered_profiles WITH (security_invoker = off, security_barrier = true) AS SELECT p.id, p.user_id, p.username, p.display_name, p.privacy_level, p.created_at, p.updated_at, -- Conditionally show bio based on privacy 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, -- Conditionally show avatar based on privacy 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, -- Conditionally show pronouns based on privacy 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, -- Conditionally show location based on privacy 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, -- Conditionally show home park based on privacy 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, -- Conditionally show date of birth based on privacy 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, -- Always show safe fields p.timezone, p.preferred_language, p.theme_preference, -- Conditionally show activity stats based on privacy 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, -- Only show banned status to owner and moderators 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 -- Hide banned profiles unless viewer is owner or moderator ((NOT p.banned) OR (auth.uid() = p.user_id) OR is_moderator(COALESCE(auth.uid(), '00000000-0000-0000-0000-000000000000'::uuid))) AND -- Only show public profiles OR owner's own profile OR moderator viewing ((p.privacy_level = 'public') OR (auth.uid() = p.user_id) OR is_moderator(COALESCE(auth.uid(), '00000000-0000-0000-0000-000000000000'::uuid))); -- Grant access to the filtered_profiles view GRANT SELECT ON public.filtered_profiles TO authenticated, anon; -- Add explanatory comment COMMENT ON VIEW public.filtered_profiles IS 'Privacy-safe profile view that enforces field-level access control. Uses security_invoker=off (SECURITY DEFINER) to bypass RLS and implement custom filtering. Sensitive fields are conditionally shown based on user privacy settings. Users can only see public non-banned profiles, their own profile, or all profiles if they are moderators. All client code should query this view instead of the profiles table directly.';