mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 06:51:12 -05:00
155 lines
5.7 KiB
PL/PgSQL
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.'; |