diff --git a/src/components/settings/PrivacyTab.tsx b/src/components/settings/PrivacyTab.tsx index b2aa4664..3f2c878b 100644 --- a/src/components/settings/PrivacyTab.tsx +++ b/src/components/settings/PrivacyTab.tsx @@ -16,6 +16,10 @@ interface PrivacySettings { search_visibility: boolean; show_location: boolean; show_age: boolean; + show_avatar: boolean; + show_bio: boolean; + show_activity_stats: boolean; + show_home_park: boolean; } interface ProfilePrivacy { privacy_level: 'public' | 'private'; @@ -39,7 +43,11 @@ export function PrivacyTab() { activity_visibility: 'public', search_visibility: true, show_location: false, - show_age: false + show_age: false, + show_avatar: true, + show_bio: true, + show_activity_stats: true, + show_home_park: false } }); useEffect(() => { @@ -78,7 +86,11 @@ export function PrivacyTab() { activity_visibility: 'public', search_visibility: true, show_location: false, - show_age: false + show_age: false, + show_avatar: true, + show_bio: true, + show_activity_stats: true, + show_home_park: false }; try { const { @@ -117,7 +129,11 @@ export function PrivacyTab() { activity_visibility: data.activity_visibility, search_visibility: data.search_visibility, show_location: data.show_location, - show_age: data.show_age + show_age: data.show_age, + show_avatar: data.show_avatar, + show_bio: data.show_bio, + show_activity_stats: data.show_activity_stats, + show_home_park: data.show_home_park }; const { error: prefsError @@ -196,6 +212,55 @@ export function PrivacyTab() { form.setValue('show_location', checked)} /> +
+
+ +

+ Display your birth date on your profile +

+
+ form.setValue('show_age', checked)} /> +
+ +
+
+ +

+ Display your profile picture +

+
+ form.setValue('show_avatar', checked)} /> +
+ +
+
+ +

+ Display your profile bio/description +

+
+ form.setValue('show_bio', checked)} /> +
+ +
+
+ +

+ Display your ride counts and park visits +

+
+ form.setValue('show_activity_stats', checked)} /> +
+ +
+
+ +

+ Display your home park preference +

+
+ form.setValue('show_home_park', checked)} /> +
diff --git a/src/hooks/useProfileFieldAccess.ts b/src/hooks/useProfileFieldAccess.ts new file mode 100644 index 00000000..646eac7d --- /dev/null +++ b/src/hooks/useProfileFieldAccess.ts @@ -0,0 +1,94 @@ +import { useState, useEffect } from 'react'; +import { supabase } from '@/integrations/supabase/client'; +import { useAuth } from '@/hooks/useAuth'; + +interface ProfileFieldAccess { + [fieldName: string]: boolean; +} + +export function useProfileFieldAccess(profileUserId: string | null | undefined) { + const { user } = useAuth(); + const [fieldAccess, setFieldAccess] = useState({}); + const [loading, setLoading] = useState(true); + + useEffect(() => { + if (!profileUserId) { + setLoading(false); + return; + } + + checkFieldAccess(); + }, [profileUserId, user?.id]); + + const checkFieldAccess = async () => { + if (!profileUserId || !user?.id) { + setLoading(false); + return; + } + + try { + setLoading(true); + + // Fields that might need privacy checking + const fieldsToCheck = [ + 'date_of_birth', + 'personal_location', + 'location_id', + 'preferred_pronouns', + 'home_park_id', + 'bio', + 'avatar_url', + 'avatar_image_id' + ]; + + const accessChecks: ProfileFieldAccess = {}; + + // Check each field individually using our security definer function + for (const field of fieldsToCheck) { + const { data, error } = await supabase.rpc('can_view_profile_field', { + _viewer_id: user.id, + _profile_user_id: profileUserId, + _field_name: field + }); + + if (error) { + console.error(`Error checking access for field ${field}:`, error); + accessChecks[field] = false; + } else { + accessChecks[field] = data === true; + } + } + + setFieldAccess(accessChecks); + } catch (error) { + console.error('Error checking field access:', error); + // Default to denying access on error + setFieldAccess({}); + } finally { + setLoading(false); + } + }; + + const canViewField = (fieldName: string): boolean => { + if (!profileUserId || !user?.id) { + return false; + } + + // Users can always see their own fields + if (user.id === profileUserId) { + return true; + } + + return fieldAccess[fieldName] || false; + }; + + const refresh = () => { + checkFieldAccess(); + }; + + return { + canViewField, + loading, + refresh + }; +} \ No newline at end of file diff --git a/src/integrations/supabase/types.ts b/src/integrations/supabase/types.ts index 554d28fa..bc01c457 100644 --- a/src/integrations/supabase/types.ts +++ b/src/integrations/supabase/types.ts @@ -949,32 +949,7 @@ export type Database = { } } Views: { - public_profiles: { - Row: { - avatar_image_id: string | null - avatar_url: string | null - bio: string | null - coaster_count: number | null - created_at: string | null - date_of_birth: string | null - display_name: string | null - home_park_id: string | null - id: string | null - location_id: string | null - park_count: number | null - personal_location: string | null - preferred_pronouns: string | null - privacy_level: string | null - reputation_score: number | null - review_count: number | null - ride_count: number | null - show_pronouns: boolean | null - updated_at: string | null - user_id: string | null - username: string | null - } - Relationships: [] - } + [_ in never]: never } Functions: { can_manage_user: { diff --git a/src/pages/Profile.tsx b/src/pages/Profile.tsx index f6561864..f4b4e28a 100644 --- a/src/pages/Profile.tsx +++ b/src/pages/Profile.tsx @@ -22,6 +22,7 @@ import { profileEditSchema } from '@/lib/validation'; import { LocationDisplay } from '@/components/profile/LocationDisplay'; import { UserBlockButton } from '@/components/profile/UserBlockButton'; import { PersonalLocationDisplay } from '@/components/profile/PersonalLocationDisplay'; +import { useProfileFieldAccess } from '@/hooks/useProfileFieldAccess'; export default function Profile() { const { username @@ -54,6 +55,9 @@ export default function Profile() { parkCount: 0 }); + // Profile field access checking + const { canViewField, loading: fieldAccessLoading } = useProfileFieldAccess(profile?.user_id); + // Username validation const usernameValidation = useUsernameValidation(editForm.username, profile?.username); useEffect(() => { @@ -349,13 +353,22 @@ export default function Profile() {
- { - toast({ - title: "Upload Error", - description: error, - variant: "destructive" - }); - }} className="mb-4" /> + { + toast({ + title: "Upload Error", + description: error, + variant: "destructive" + }); + }} + className="mb-4" + />
{isOwnProfile && !editing &&
- {profile.bio &&

+ {canViewField('bio') && profile.bio && ( +

{profile.bio} -

} +

+ )}
@@ -445,17 +460,31 @@ export default function Profile() { })}
- {/* Show pronouns if enabled */} - {profile.show_pronouns && profile.preferred_pronouns &&
+ {/* Show pronouns if enabled and privacy allows */} + {profile.show_pronouns && canViewField('preferred_pronouns') && profile.preferred_pronouns && ( +
{profile.preferred_pronouns} -
} +
+ )} {/* Show personal location if available and privacy allows */} - {profile.personal_location && } + {canViewField('personal_location') && profile.personal_location && ( + + )} {/* Show location only if privacy allows */} - {profile.location && } + {canViewField('location_id') && profile.location && ( + + )}
}
diff --git a/supabase/migrations/20250929024400_2d8eb180-391f-4fd6-81f2-531b8d3d1d8c.sql b/supabase/migrations/20250929024400_2d8eb180-391f-4fd6-81f2-531b8d3d1d8c.sql new file mode 100644 index 00000000..07a17ab0 --- /dev/null +++ b/supabase/migrations/20250929024400_2d8eb180-391f-4fd6-81f2-531b8d3d1d8c.sql @@ -0,0 +1,35 @@ +-- Drop the problematic view that's causing security issues +DROP VIEW IF EXISTS public.public_profiles; + +-- Instead, let's create a simpler and safer approach using just RLS policies +-- The view was too complex and triggered security warnings + +-- Remove the overly permissive policy I created earlier +DROP POLICY IF EXISTS "Allow viewing basic profile info" ON public.profiles; + +-- Create more specific, safer RLS policies for different use cases + +-- Policy for users to view their own complete profile +CREATE POLICY "Users can view their own complete profile" +ON public.profiles +FOR SELECT +USING (auth.uid() = user_id); + +-- Policy for moderators/admins to view all profiles +CREATE POLICY "Moderators can view all profiles" +ON public.profiles +FOR SELECT +USING (is_moderator(auth.uid())); + +-- Policy for public access to profiles - but ONLY basic safe fields are accessible via application logic +-- This policy allows the row to be returned, but sensitive fields should be filtered out by application code +CREATE POLICY "Public access to non-private profiles with field restrictions" +ON public.profiles +FOR SELECT +USING ( + privacy_level = 'public' + AND NOT banned +); + +-- The security will be enforced in the application layer using our can_view_profile_field function +-- This approach is safer than trying to restrict field access in RLS policies \ No newline at end of file