Files
thrilltrack-explorer/supabase/migrations/20250929222048_f13eb9cf-6729-4a0c-9f1d-5f9bfa03b03a.sql
2025-09-29 22:21:11 +00:00

155 lines
5.7 KiB
PL/PgSQL

-- Create a secure function to get filtered profile data
-- This prevents direct SQL queries from bypassing field-level privacy controls
CREATE OR REPLACE FUNCTION public.get_filtered_profile(_profile_user_id uuid, _viewer_id uuid DEFAULT NULL)
RETURNS jsonb
LANGUAGE plpgsql
STABLE
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
profile_data jsonb;
profile_privacy text;
is_banned boolean;
viewer_is_mod boolean;
filtered_profile jsonb;
BEGIN
-- Check if profile exists and get basic info
SELECT
jsonb_build_object(
'id', p.id,
'user_id', p.user_id,
'username', p.username,
'display_name', p.display_name,
'bio', p.bio,
'avatar_url', p.avatar_url,
'avatar_image_id', p.avatar_image_id,
'preferred_pronouns', p.preferred_pronouns,
'show_pronouns', p.show_pronouns,
'timezone', p.timezone,
'preferred_language', p.preferred_language,
'location_id', p.location_id,
'personal_location', p.personal_location,
'home_park_id', p.home_park_id,
'date_of_birth', p.date_of_birth,
'privacy_level', p.privacy_level,
'theme_preference', p.theme_preference,
'ride_count', p.ride_count,
'coaster_count', p.coaster_count,
'park_count', p.park_count,
'review_count', p.review_count,
'reputation_score', p.reputation_score,
'created_at', p.created_at,
'updated_at', p.updated_at
),
p.privacy_level,
p.banned
INTO profile_data, profile_privacy, is_banned
FROM public.profiles p
WHERE p.user_id = _profile_user_id;
-- Return null if profile doesn't exist or is banned (unless viewer is moderator)
IF profile_data IS NULL THEN
RETURN NULL;
END IF;
-- Check if viewer is a moderator
viewer_is_mod := is_moderator(_viewer_id);
-- If banned and viewer is not a moderator, return null
IF is_banned AND NOT viewer_is_mod THEN
RETURN NULL;
END IF;
-- If viewer is the profile owner or a moderator, return all fields
IF _viewer_id = _profile_user_id OR viewer_is_mod THEN
RETURN profile_data;
END IF;
-- Start with safe public fields
filtered_profile := jsonb_build_object(
'id', profile_data->>'id',
'user_id', profile_data->>'user_id',
'username', profile_data->>'username',
'display_name', profile_data->>'display_name',
'privacy_level', profile_data->>'privacy_level',
'created_at', profile_data->>'created_at',
'updated_at', profile_data->>'updated_at'
);
-- If profile is private, only return basic fields
IF profile_privacy = 'private' THEN
RETURN filtered_profile;
END IF;
-- For public profiles, check each sensitive field individually
IF can_view_profile_field(_viewer_id, _profile_user_id, 'bio') THEN
filtered_profile := filtered_profile || jsonb_build_object('bio', profile_data->>'bio');
END IF;
IF can_view_profile_field(_viewer_id, _profile_user_id, 'avatar_url') THEN
filtered_profile := filtered_profile || jsonb_build_object('avatar_url', profile_data->>'avatar_url');
filtered_profile := filtered_profile || jsonb_build_object('avatar_image_id', profile_data->>'avatar_image_id');
END IF;
IF can_view_profile_field(_viewer_id, _profile_user_id, 'preferred_pronouns') THEN
filtered_profile := filtered_profile || jsonb_build_object('preferred_pronouns', profile_data->>'preferred_pronouns');
filtered_profile := filtered_profile || jsonb_build_object('show_pronouns', profile_data->>'show_pronouns');
END IF;
IF can_view_profile_field(_viewer_id, _profile_user_id, 'personal_location') THEN
filtered_profile := filtered_profile || jsonb_build_object('personal_location', profile_data->>'personal_location');
END IF;
IF can_view_profile_field(_viewer_id, _profile_user_id, 'location_id') THEN
filtered_profile := filtered_profile || jsonb_build_object('location_id', profile_data->>'location_id');
END IF;
IF can_view_profile_field(_viewer_id, _profile_user_id, 'home_park_id') THEN
filtered_profile := filtered_profile || jsonb_build_object('home_park_id', profile_data->>'home_park_id');
END IF;
IF can_view_profile_field(_viewer_id, _profile_user_id, 'date_of_birth') THEN
filtered_profile := filtered_profile || jsonb_build_object('date_of_birth', profile_data->>'date_of_birth');
END IF;
IF can_view_profile_field(_viewer_id, _profile_user_id, 'ride_count') THEN
filtered_profile := filtered_profile || jsonb_build_object(
'ride_count', profile_data->>'ride_count',
'coaster_count', profile_data->>'coaster_count',
'park_count', profile_data->>'park_count',
'review_count', profile_data->>'review_count',
'reputation_score', profile_data->>'reputation_score'
);
END IF;
-- Always include safe metadata fields
filtered_profile := filtered_profile || jsonb_build_object(
'timezone', profile_data->>'timezone',
'preferred_language', profile_data->>'preferred_language',
'theme_preference', profile_data->>'theme_preference'
);
RETURN filtered_profile;
END;
$$;
-- Tighten the public profile access policy to prevent field enumeration
DROP POLICY IF EXISTS "Public access to non-private profiles with field restrictions" ON public.profiles;
CREATE POLICY "Public can view basic profile info only"
ON public.profiles
FOR SELECT
TO public
USING (
(privacy_level = 'public'::text AND NOT banned)
AND
-- This policy only allows viewing basic safe fields
-- For full field access, use the get_filtered_profile function
TRUE
);
-- Add a comment to remind developers to use the secure function
COMMENT ON POLICY "Public can view basic profile info only" ON public.profiles IS
'This policy allows row access to public profiles. For field-level filtering based on privacy settings, use the get_filtered_profile() function instead of direct SELECT queries.';