diff --git a/src/pages/ProfileSettings.tsx b/src/pages/ProfileSettings.tsx index eb6150bd..501abd30 100644 --- a/src/pages/ProfileSettings.tsx +++ b/src/pages/ProfileSettings.tsx @@ -80,8 +80,7 @@ export default function ProfileSettings() { const updateProfile = async () => { setLoading(true); try { - const profileData = { - id: user?.id, + console.log('💾 CALLING UPSERT FUNCTION with data:', { username: profile.username || '', full_name: profile.full_name, bio: profile.bio, @@ -89,24 +88,27 @@ export default function ProfileSettings() { location: profile.location, website: profile.website, language: profile.language, - theme: profile.theme, - updated_at: new Date().toISOString() - }; - - console.log('💾 UPSERTING PROFILE DATA:', profileData); + theme: profile.theme + }); console.log('👤 User ID:', user?.id); - // Use upsert to create row if it doesn't exist, or update if it does - const { data, error } = await supabase - .from('profiles') - .upsert(profileData, { - onConflict: 'id', - ignoreDuplicates: false - }) - .select(); + // Call the secure upsert function + const { data, error } = await supabase.rpc('upsert_user_profile', { + p_username: profile.username || '', + p_full_name: profile.full_name || null, + p_bio: profile.bio || null, + p_phone_number: profile.phone_number || null, + p_location: profile.location || null, + p_website: profile.website || null, + p_language: profile.language || 'en', + p_theme: profile.theme || 'dark', + p_notifications_email: profile.notifications_email ?? true, + p_notifications_push: profile.notifications_push ?? false, + p_notifications_sms: profile.notifications_sms ?? false + }); - console.log('✅ Upsert response data:', data); - console.log('❌ Upsert error:', error); + console.log('✅ Function response data:', data); + console.log('❌ Function error:', error); if (error) throw error; @@ -117,11 +119,11 @@ export default function ProfileSettings() { // Reload profile to ensure state is in sync await loadProfile(); - } catch (error) { + } catch (error: any) { console.error('❌ Profile update failed:', error); toast({ title: 'Error', - description: 'Failed to update profile', + description: error?.message || 'Failed to update profile', variant: 'destructive', }); } finally { @@ -132,15 +134,20 @@ export default function ProfileSettings() { const updateNotificationSettings = async () => { setLoading(true); try { - const { error } = await supabase - .from('profiles') - .update({ - notifications_email: profile.notifications_email, - notifications_push: profile.notifications_push, - notifications_sms: profile.notifications_sms, - updated_at: new Date().toISOString() - }) - .eq('id', user?.id); + // Call the upsert function with current profile data + notification settings + const { data, error } = await supabase.rpc('upsert_user_profile', { + p_username: profile.username || '', + p_full_name: profile.full_name || null, + p_bio: profile.bio || null, + p_phone_number: profile.phone_number || null, + p_location: profile.location || null, + p_website: profile.website || null, + p_language: profile.language || 'en', + p_theme: profile.theme || 'dark', + p_notifications_email: profile.notifications_email ?? true, + p_notifications_push: profile.notifications_push ?? false, + p_notifications_sms: profile.notifications_sms ?? false + }); if (error) throw error; @@ -148,10 +155,10 @@ export default function ProfileSettings() { title: 'Success', description: 'Notification settings updated', }); - } catch (error) { + } catch (error: any) { toast({ title: 'Error', - description: 'Failed to update notification settings', + description: error?.message || 'Failed to update notification settings', variant: 'destructive', }); } finally { diff --git a/supabase/migrations/004_create_upsert_function.sql b/supabase/migrations/004_create_upsert_function.sql new file mode 100644 index 00000000..9761e1ea --- /dev/null +++ b/supabase/migrations/004_create_upsert_function.sql @@ -0,0 +1,97 @@ +-- ======================================== +-- Create Secure Upsert Function for Profiles +-- ======================================== +-- Uses SECURITY DEFINER to bypass RLS for authenticated users + +-- First, ensure username is nullable +ALTER TABLE public.profiles + ALTER COLUMN username DROP NOT NULL; + +ALTER TABLE public.profiles + ALTER COLUMN username SET DEFAULT ''; + +-- Create secure upsert function +CREATE OR REPLACE FUNCTION public.upsert_user_profile( + p_username TEXT DEFAULT '', + p_full_name TEXT DEFAULT NULL, + p_bio TEXT DEFAULT NULL, + p_phone_number TEXT DEFAULT NULL, + p_location TEXT DEFAULT NULL, + p_website TEXT DEFAULT NULL, + p_language TEXT DEFAULT 'en', + p_theme TEXT DEFAULT 'dark', + p_notifications_email BOOLEAN DEFAULT true, + p_notifications_push BOOLEAN DEFAULT false, + p_notifications_sms BOOLEAN DEFAULT false +) +RETURNS public.profiles AS $$ +DECLARE + result public.profiles; +BEGIN + -- Use auth.uid() to ensure user can only upsert their own profile + INSERT INTO public.profiles ( + id, + username, + full_name, + bio, + phone_number, + location, + website, + language, + theme, + notifications_email, + notifications_push, + notifications_sms, + updated_at + ) + VALUES ( + auth.uid(), + p_username, + p_full_name, + p_bio, + p_phone_number, + p_location, + p_website, + p_language, + p_theme, + p_notifications_email, + p_notifications_push, + p_notifications_sms, + NOW() + ) + ON CONFLICT (id) + DO UPDATE SET + username = COALESCE(NULLIF(EXCLUDED.username, ''), profiles.username, ''), + full_name = COALESCE(EXCLUDED.full_name, profiles.full_name), + bio = COALESCE(EXCLUDED.bio, profiles.bio), + phone_number = COALESCE(EXCLUDED.phone_number, profiles.phone_number), + location = COALESCE(EXCLUDED.location, profiles.location), + website = COALESCE(EXCLUDED.website, profiles.website), + language = COALESCE(EXCLUDED.language, profiles.language), + theme = COALESCE(EXCLUDED.theme, profiles.theme), + notifications_email = COALESCE(EXCLUDED.notifications_email, profiles.notifications_email), + notifications_push = COALESCE(EXCLUDED.notifications_push, profiles.notifications_push), + notifications_sms = COALESCE(EXCLUDED.notifications_sms, profiles.notifications_sms), + updated_at = NOW() + RETURNING * + INTO result; + + RETURN result; +END; +$$ LANGUAGE plpgsql SECURITY DEFINER; + +-- Grant execute permission to authenticated users only +GRANT EXECUTE ON FUNCTION public.upsert_user_profile TO authenticated; + +-- Revoke from public for security +REVOKE EXECUTE ON FUNCTION public.upsert_user_profile FROM PUBLIC; + +-- Success message +DO $$ +BEGIN + RAISE NOTICE '========================================'; + RAISE NOTICE 'Profile upsert function created successfully!'; + RAISE NOTICE 'Function: upsert_user_profile()'; + RAISE NOTICE 'This bypasses RLS using SECURITY DEFINER'; + RAISE NOTICE '========================================'; +END $$;