mirror of
https://github.com/pezkuwichain/pwap.git
synced 2026-06-13 06:41:02 +00:00
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:
@@ -80,8 +80,7 @@ export default function ProfileSettings() {
|
|||||||
const updateProfile = async () => {
|
const updateProfile = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const profileData = {
|
console.log('💾 CALLING UPSERT FUNCTION with data:', {
|
||||||
id: user?.id,
|
|
||||||
username: profile.username || '',
|
username: profile.username || '',
|
||||||
full_name: profile.full_name,
|
full_name: profile.full_name,
|
||||||
bio: profile.bio,
|
bio: profile.bio,
|
||||||
@@ -89,24 +88,27 @@ export default function ProfileSettings() {
|
|||||||
location: profile.location,
|
location: profile.location,
|
||||||
website: profile.website,
|
website: profile.website,
|
||||||
language: profile.language,
|
language: profile.language,
|
||||||
theme: profile.theme,
|
theme: profile.theme
|
||||||
updated_at: new Date().toISOString()
|
});
|
||||||
};
|
|
||||||
|
|
||||||
console.log('💾 UPSERTING PROFILE DATA:', profileData);
|
|
||||||
console.log('👤 User ID:', user?.id);
|
console.log('👤 User ID:', user?.id);
|
||||||
|
|
||||||
// Use upsert to create row if it doesn't exist, or update if it does
|
// Call the secure upsert function
|
||||||
const { data, error } = await supabase
|
const { data, error } = await supabase.rpc('upsert_user_profile', {
|
||||||
.from('profiles')
|
p_username: profile.username || '',
|
||||||
.upsert(profileData, {
|
p_full_name: profile.full_name || null,
|
||||||
onConflict: 'id',
|
p_bio: profile.bio || null,
|
||||||
ignoreDuplicates: false
|
p_phone_number: profile.phone_number || null,
|
||||||
})
|
p_location: profile.location || null,
|
||||||
.select();
|
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('✅ Function response data:', data);
|
||||||
console.log('❌ Upsert error:', error);
|
console.log('❌ Function error:', error);
|
||||||
|
|
||||||
if (error) throw error;
|
if (error) throw error;
|
||||||
|
|
||||||
@@ -117,11 +119,11 @@ export default function ProfileSettings() {
|
|||||||
|
|
||||||
// Reload profile to ensure state is in sync
|
// Reload profile to ensure state is in sync
|
||||||
await loadProfile();
|
await loadProfile();
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error('❌ Profile update failed:', error);
|
console.error('❌ Profile update failed:', error);
|
||||||
toast({
|
toast({
|
||||||
title: 'Error',
|
title: 'Error',
|
||||||
description: 'Failed to update profile',
|
description: error?.message || 'Failed to update profile',
|
||||||
variant: 'destructive',
|
variant: 'destructive',
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
@@ -132,15 +134,20 @@ export default function ProfileSettings() {
|
|||||||
const updateNotificationSettings = async () => {
|
const updateNotificationSettings = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const { error } = await supabase
|
// Call the upsert function with current profile data + notification settings
|
||||||
.from('profiles')
|
const { data, error } = await supabase.rpc('upsert_user_profile', {
|
||||||
.update({
|
p_username: profile.username || '',
|
||||||
notifications_email: profile.notifications_email,
|
p_full_name: profile.full_name || null,
|
||||||
notifications_push: profile.notifications_push,
|
p_bio: profile.bio || null,
|
||||||
notifications_sms: profile.notifications_sms,
|
p_phone_number: profile.phone_number || null,
|
||||||
updated_at: new Date().toISOString()
|
p_location: profile.location || null,
|
||||||
})
|
p_website: profile.website || null,
|
||||||
.eq('id', user?.id);
|
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;
|
if (error) throw error;
|
||||||
|
|
||||||
@@ -148,10 +155,10 @@ export default function ProfileSettings() {
|
|||||||
title: 'Success',
|
title: 'Success',
|
||||||
description: 'Notification settings updated',
|
description: 'Notification settings updated',
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
toast({
|
toast({
|
||||||
title: 'Error',
|
title: 'Error',
|
||||||
description: 'Failed to update notification settings',
|
description: error?.message || 'Failed to update notification settings',
|
||||||
variant: 'destructive',
|
variant: 'destructive',
|
||||||
});
|
});
|
||||||
} finally {
|
} 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 $$;
|
||||||
Reference in New Issue
Block a user