fix: Use SECURITY DEFINER function for profile upserts

Changes:
1. Created migration 004_create_upsert_function.sql:
   - Creates upsert_user_profile() function with SECURITY DEFINER
   - Bypasses RLS to allow profile creation/updates
   - Only accessible to authenticated users via auth.uid()

2. Updated ProfileSettings.tsx:
   - Changed from direct upsert to RPC function call
   - Updated updateProfile() to use supabase.rpc()
   - Updated updateNotificationSettings() to use same function

This solves the RLS policy violation by using a secure
server-side function that properly handles authentication.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-10-29 05:10:53 +03:00
parent ceec6a5569
commit 205e22e083
2 changed files with 134 additions and 30 deletions
+37 -30
View File
@@ -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 {
@@ -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 $$;