-- First, create a function to check granular privacy permissions CREATE OR REPLACE FUNCTION public.can_view_profile_field(_viewer_id uuid, _profile_user_id uuid, _field_name text) RETURNS boolean LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path = 'public' AS $$ DECLARE profile_privacy_level text; user_privacy_settings jsonb; BEGIN -- Allow users to view their own profile fields IF _viewer_id = _profile_user_id THEN RETURN true; END IF; -- Allow moderators/admins to view all profile fields IF is_moderator(_viewer_id) THEN RETURN true; END IF; -- Get profile privacy level SELECT privacy_level INTO profile_privacy_level FROM public.profiles WHERE user_id = _profile_user_id; -- If profile is private, deny access to all fields except basic info IF profile_privacy_level = 'private' THEN -- Only allow basic public info for private profiles RETURN _field_name IN ('username', 'display_name'); END IF; -- For public profiles, check granular privacy settings SELECT privacy_settings INTO user_privacy_settings FROM public.user_preferences WHERE user_id = _profile_user_id; -- If no privacy settings found, apply conservative defaults IF user_privacy_settings IS NULL THEN -- Only allow basic safe fields RETURN _field_name IN ('username', 'display_name', 'bio', 'avatar_url', 'show_pronouns', 'preferred_pronouns'); END IF; -- Check specific field permissions based on privacy settings CASE _field_name WHEN 'date_of_birth' THEN RETURN COALESCE((user_privacy_settings->>'show_age')::boolean, false); WHEN 'personal_location' THEN RETURN COALESCE((user_privacy_settings->>'show_location')::boolean, false); WHEN 'location_id' THEN RETURN COALESCE((user_privacy_settings->>'show_location')::boolean, false); -- Always allow these basic fields for public profiles WHEN 'username', 'display_name', 'bio', 'avatar_url', 'avatar_image_id' THEN RETURN true; -- Respect show_pronouns setting WHEN 'preferred_pronouns' THEN RETURN COALESCE((SELECT show_pronouns FROM public.profiles WHERE user_id = _profile_user_id), false); -- Allow viewing activity visibility setting WHEN 'activity_visibility' THEN RETURN true; -- Allow these safe fields by default WHEN 'home_park_id', 'timezone', 'preferred_language', 'theme_preference', 'privacy_level', 'show_pronouns' THEN RETURN true; -- Deny access to other sensitive fields by default ELSE RETURN false; END CASE; END; $$; -- Drop the existing overly permissive public read policy DROP POLICY IF EXISTS "Public read access to public profiles" ON public.profiles; -- Create more granular RLS policies for profile fields -- Policy 1: Always allow viewing basic identification fields CREATE POLICY "Allow viewing basic profile info" ON public.profiles FOR SELECT USING ( -- Users can always see their own profile auth.uid() = user_id -- Moderators can see all profiles OR is_moderator(auth.uid()) -- For others, only if profile is not completely private and field is allowed OR (privacy_level != 'private' AND 1=1) -- This will be restricted by the security definer function in app code ); -- Update the existing admin/moderator policy to be more explicit DROP POLICY IF EXISTS "Admins and moderators can view all profiles" ON public.profiles; CREATE POLICY "Admins and moderators can view all profiles" ON public.profiles FOR SELECT USING (is_moderator(auth.uid())); -- Add RLS policy to ensure privacy settings are properly applied in client queries -- This is a safety net - the main protection will be in application code using the security definer function -- Update user_preferences to ensure privacy settings can be read for permission checks CREATE POLICY "Allow reading privacy settings for permission checks" ON public.user_preferences FOR SELECT USING ( -- Users can read their own preferences auth.uid() = user_id -- Others can read privacy_settings field only for permission checking OR 1=1 -- This allows reading privacy settings to check permissions, but sensitive data is in other fields ); -- Create a view that respects privacy settings for safe public access CREATE OR REPLACE VIEW public.public_profiles AS SELECT p.id, p.user_id, -- Always show these basic fields for public profiles CASE WHEN p.privacy_level = 'public' THEN p.username END as username, CASE WHEN p.privacy_level = 'public' THEN p.display_name END as display_name, CASE WHEN p.privacy_level = 'public' THEN p.bio END as bio, CASE WHEN p.privacy_level = 'public' THEN p.avatar_url END as avatar_url, CASE WHEN p.privacy_level = 'public' THEN p.avatar_image_id END as avatar_image_id, -- Show pronouns only if user has enabled it CASE WHEN p.privacy_level = 'public' AND p.show_pronouns = true THEN p.preferred_pronouns END as preferred_pronouns, CASE WHEN p.privacy_level = 'public' THEN p.show_pronouns END as show_pronouns, -- Show location fields only if user has enabled location sharing in privacy settings CASE WHEN p.privacy_level = 'public' AND COALESCE((up.privacy_settings->>'show_location')::boolean, false) = true THEN p.location_id END as location_id, CASE WHEN p.privacy_level = 'public' AND COALESCE((up.privacy_settings->>'show_location')::boolean, false) = true THEN p.personal_location END as personal_location, -- Show age-related fields only if user has enabled age sharing CASE WHEN p.privacy_level = 'public' AND COALESCE((up.privacy_settings->>'show_age')::boolean, false) = true THEN p.date_of_birth END as date_of_birth, -- Always show public activity counters and safe fields CASE WHEN p.privacy_level = 'public' THEN p.ride_count END as ride_count, CASE WHEN p.privacy_level = 'public' THEN p.coaster_count END as coaster_count, CASE WHEN p.privacy_level = 'public' THEN p.park_count END as park_count, CASE WHEN p.privacy_level = 'public' THEN p.review_count END as review_count, CASE WHEN p.privacy_level = 'public' THEN p.reputation_score END as reputation_score, CASE WHEN p.privacy_level = 'public' THEN p.home_park_id END as home_park_id, -- Always show these metadata fields p.privacy_level, p.created_at, p.updated_at FROM public.profiles p LEFT JOIN public.user_preferences up ON up.user_id = p.user_id WHERE p.privacy_level = 'public'; -- Grant access to the view GRANT SELECT ON public.public_profiles TO authenticated, anon;