From 09b55f90444d7d4c147c8aad71aadb40fcb61be4 Mon Sep 17 00:00:00 2001 From: zramsay Date: Wed, 12 Mar 2025 10:01:34 -0400 Subject: [PATCH] fixes --- src/app/api/analyze/route.ts | 28 +++-- src/app/api/debug/merge-users/route.ts | 62 ++++++++++ src/services/userPointsService.ts | 160 ++++++++++++++++++++----- supabase-schema-updates.sql | 64 ++++++++++ supabase-schema.sql | 25 +++- 5 files changed, 296 insertions(+), 43 deletions(-) create mode 100644 src/app/api/debug/merge-users/route.ts diff --git a/src/app/api/analyze/route.ts b/src/app/api/analyze/route.ts index fd81b56..4ff829a 100644 --- a/src/app/api/analyze/route.ts +++ b/src/app/api/analyze/route.ts @@ -252,15 +252,25 @@ export async function POST(req: NextRequest): Promise { if (pointsUserId && userEmail) { console.log('Awarding points to user:', { pointsUserId, userEmail }); try { - const pointsResult = await awardPointsForImage( - pointsUserId, - userEmail, - buffer, - ipfsUrl, - visionResult.description, - visionResult.mainObject - ); - console.log('Points award result:', pointsResult); + // First ensure the user exists and get the correct user ID + // This prevents duplicate users if we're using different IDs between sessions + // and ensures we always use the same ID for the same user (by email) + const ensuredUserId = await ensureUserExists(pointsUserId, userEmail); + console.log('Using ensured user ID for points:', ensuredUserId); + + if (!ensuredUserId) { + console.error('Failed to ensure user exists, cannot award points'); + } else { + const pointsResult = await awardPointsForImage( + ensuredUserId, // Use the ensured ID, which might be different from pointsUserId + userEmail, + buffer, + ipfsUrl, + visionResult.description, + visionResult.mainObject + ); + console.log('Points award result:', pointsResult); + } } catch (err) { console.error('Failed to award points for image:', err); console.error('Points error details:', { diff --git a/src/app/api/debug/merge-users/route.ts b/src/app/api/debug/merge-users/route.ts new file mode 100644 index 0000000..4daeec7 --- /dev/null +++ b/src/app/api/debug/merge-users/route.ts @@ -0,0 +1,62 @@ +// src/app/api/debug/merge-users/route.ts +import { NextRequest, NextResponse } from 'next/server'; +import { mergeDuplicateUsersByEmail } from '../../../../services/userPointsService'; + +// WARNING: This is a debug endpoint and should be protected in production +export async function POST(req: NextRequest): Promise { + try { + // For security in production, this should check for admin credentials + const isProduction = process.env.NODE_ENV === 'production'; + + // Require admin authorization in production + if (isProduction) { + const adminKey = req.headers.get('x-admin-key'); + const validKey = process.env.ADMIN_DEBUG_KEY; + + if (!adminKey || adminKey !== validKey) { + console.warn('Unauthorized access attempt to merge-users API'); + return NextResponse.json( + { error: 'Unauthorized. Admin access required.' }, + { status: 401 } + ); + } + } + + // Parse request body + const body = await req.json(); + const { email } = body; + + if (!email) { + return NextResponse.json( + { error: 'Email is required' }, + { status: 400 } + ); + } + + console.log(`Attempting to merge duplicate users for email: ${email}`); + + // Execute the merge + const result = await mergeDuplicateUsersByEmail(email); + + console.log('Merge result:', result); + + if (!result.success) { + return NextResponse.json( + { error: result.message }, + { status: 400 } + ); + } + + return NextResponse.json(result); + + } catch (error) { + console.error('Error in merge-users API:', error); + return NextResponse.json( + { error: 'Failed to merge users' }, + { status: 500 } + ); + } +} + +// Ensure dynamic routing +export const dynamic = 'force-dynamic'; \ No newline at end of file diff --git a/src/services/userPointsService.ts b/src/services/userPointsService.ts index 5a71bbd..5f59608 100644 --- a/src/services/userPointsService.ts +++ b/src/services/userPointsService.ts @@ -13,6 +13,8 @@ export const POINTS = { /** * Ensure a user exists in the database + * If a user with the provided email already exists, their ID will be returned + * This prevents duplicate users with the same email from being created */ export async function ensureUserExists(userId: string, email: string, name?: string) { try { @@ -21,6 +23,11 @@ export async function ensureUserExists(userId: string, email: string, name?: str return null; } + if (!email) { + console.warn('Cannot ensure user exists with undefined email'); + return null; + } + console.log('Ensuring user exists:', { userId, email }); // IMPORTANT: Do NOT generate a new UUID, only validate format @@ -29,7 +36,7 @@ export async function ensureUserExists(userId: string, email: string, name?: str return null; } - // Call the Supabase function to ensure user exists + // Call the Supabase function to ensure user exists (updated version checks for email first) const { data, error } = await supabaseAdmin.rpc('ensure_user_exists', { user_id_param: userId, email_param: email, @@ -39,52 +46,82 @@ export async function ensureUserExists(userId: string, email: string, name?: str if (error) { console.error('Error ensuring user exists:', error); - // Fallback to direct insert + // Fallback to direct query approach try { - // First check if the user already exists - const { data: existingUser, error: checkError } = await supabaseAdmin + // First check if a user with this email already exists + const { data: existingUserByEmail, error: emailCheckError } = await supabaseAdmin + .from('users') + .select('id') + .eq('email', email) + .maybeSingle(); + + if (!emailCheckError && existingUserByEmail) { + // User exists by email, update last_login + const { error: updateError } = await supabaseAdmin + .from('users') + .update({ + last_login: new Date().toISOString(), + name: name || undefined // Only update if provided + }) + .eq('id', existingUserByEmail.id); + + if (updateError) { + console.warn('Error updating existing user by email:', updateError); + } + + console.log('Found existing user by email, using their ID:', existingUserByEmail.id); + return existingUserByEmail.id; + } + + // If no user found by email, check if one exists with the provided userId + const { data: existingUserById, error: idCheckError } = await supabaseAdmin .from('users') .select('id') .eq('id', userId) .maybeSingle(); - if (!checkError && existingUser) { - // User exists, update last_login + if (!idCheckError && existingUserById) { + // User exists by ID, update info const { error: updateError } = await supabaseAdmin .from('users') - .update({ last_login: new Date().toISOString() }) + .update({ + last_login: new Date().toISOString(), + email: email, + name: name || undefined // Only update if provided + }) .eq('id', userId); if (updateError) { - console.warn('Error updating user last_login:', updateError); + console.warn('Error updating existing user by ID:', updateError); } return userId; - } else { - // User doesn't exist, insert new user - const { data: insertData, error: insertError } = await supabaseAdmin - .from('users') - .insert({ - id: userId, - email: email, - name: name || null, - first_login: new Date().toISOString(), - last_login: new Date().toISOString() - }); - - if (insertError) { - console.error('Error inserting user:', insertError); - return null; - } + } + + // If we get here, no user exists with this email or ID, so create one + const { data: insertData, error: insertError } = await supabaseAdmin + .from('users') + .insert({ + id: userId, + email: email, + name: name || null, + first_login: new Date().toISOString(), + last_login: new Date().toISOString() + }); - return userId; + if (insertError) { + console.error('Error inserting new user:', insertError); + return null; } + + return userId; } catch (err) { console.error('Error in fallback user creation:', err); return null; } } + console.log('User ensured with ID:', data); return data; } catch (error) { console.error('Unexpected error in ensureUserExists:', error); @@ -135,7 +172,8 @@ export async function awardPointsForImage( console.log('Award points starting values:', { userId, email }); - // Ensure user exists in database + // Ensure user exists in database and get the correct UUID + // This ensures we're always using the same UUID for a given email const userUuid = await ensureUserExists(userId, email); if (!userUuid) { console.error('Failed to ensure user exists'); @@ -144,14 +182,19 @@ export async function awardPointsForImage( message: 'Failed to register user' }; } + + // If the ensured UUID is different from the original userId, log it + if (userUuid !== userId) { + console.log(`Using different UUID for user: ${userUuid} instead of ${userId}`); + } // Generate hash for the image const imageHash = generateImageHash(imageBuffer); - console.log(`Processing points for image ${imageHash.substring(0, 8)}... for user ${userId}`); + console.log(`Processing points for image ${imageHash.substring(0, 8)}... for user ${userUuid}`); // Check if this image has already been awarded points const { data: existingImage, error: checkError } = await supabaseAdmin.rpc('check_duplicate_upload', { - user_id_param: userId, + user_id_param: userUuid, image_hash_param: imageHash }); @@ -163,7 +206,7 @@ export async function awardPointsForImage( .from('point_transactions') .select('*') .eq('image_hash', imageHash) - .eq('user_id', userId) + .eq('user_id', userUuid) .maybeSingle(); if (!directError && existingTransaction) { @@ -184,7 +227,7 @@ export async function awardPointsForImage( // Create transaction using RPC const { data: transactionData, error: rpcError } = await supabaseAdmin .rpc('create_point_transaction', { - user_id_param: userId, + user_id_param: userUuid, points_param: POINTS.IMAGE_UPLOAD, transaction_type_param: 'image_upload', image_hash_param: imageHash, @@ -200,7 +243,7 @@ export async function awardPointsForImage( const { data: insertData, error: insertError } = await supabaseAdmin .from('point_transactions') .insert({ - user_id: userId, + user_id: userUuid, points: POINTS.IMAGE_UPLOAD, transaction_type: 'image_upload', image_hash: imageHash, @@ -478,4 +521,59 @@ export async function getUserStats(userId: string) { console.error('Unexpected error in getUserStats:', error); return null; } +} + +/** + * Merge duplicate users that have the same email address + * This is useful for handling cases where a user has multiple accounts + * due to logging in with different authentication methods + */ +export async function mergeDuplicateUsersByEmail(email: string) { + try { + if (!email) { + console.warn('Cannot merge users with undefined email'); + return { + success: false, + message: 'Email is required' + }; + } + + console.log('Attempting to merge users with email:', email); + + // Call the Supabase function to merge users + const { data, error } = await supabaseAdmin.rpc('merge_duplicate_users_by_email', { + email_param: email + }); + + if (error) { + console.error('Error merging duplicate users:', error); + return { + success: false, + message: `Failed to merge users: ${error.message}` + }; + } + + console.log('Merge result:', data); + + if (!data || data.success === false) { + return { + success: false, + message: data?.message || 'No user found with this email' + }; + } + + return { + success: true, + message: `Successfully merged ${data.merged_users_count} duplicate user(s) and migrated ${data.migrated_transactions} transaction(s)`, + primaryUserId: data.primary_user_id, + mergedCount: data.merged_users_count, + email: data.email + }; + } catch (error) { + console.error('Unexpected error in mergeDuplicateUsersByEmail:', error); + return { + success: false, + message: 'An unexpected error occurred while merging users' + }; + } } \ No newline at end of file diff --git a/supabase-schema-updates.sql b/supabase-schema-updates.sql index 146f15b..95c7af6 100644 --- a/supabase-schema-updates.sql +++ b/supabase-schema-updates.sql @@ -165,4 +165,68 @@ BEGIN RETURN TRUE; END; +$$ LANGUAGE plpgsql SECURITY DEFINER; + +-- Function to merge duplicate users by email +-- This will find all users with the same email, keep the oldest one, +-- and migrate all transactions to that user +CREATE OR REPLACE FUNCTION merge_duplicate_users_by_email( + email_param TEXT +) +RETURNS json AS $$ +DECLARE + primary_user_id UUID; + duplicate_user record; + result json; + merge_count INT := 0; + transaction_count INT := 0; +BEGIN + -- Find the oldest user with this email to use as the primary + SELECT id INTO primary_user_id + FROM users + WHERE email = email_param + ORDER BY first_login ASC + LIMIT 1; + + -- If no user found, return error + IF primary_user_id IS NULL THEN + RETURN json_build_object( + 'success', FALSE, + 'message', 'No user found with this email' + ); + END IF; + + -- Find all other users with the same email + FOR duplicate_user IN + SELECT id + FROM users + WHERE email = email_param + AND id != primary_user_id + LOOP + -- Migrate this user's transactions to the primary user + UPDATE point_transactions + SET user_id = primary_user_id + WHERE user_id = duplicate_user.id; + + -- Count migrated transactions + GET DIAGNOSTICS transaction_count = ROW_COUNT; + + -- Delete the duplicate user + DELETE FROM users + WHERE id = duplicate_user.id; + + merge_count := merge_count + 1; + END LOOP; + + -- Build the result object + result := json_build_object( + 'success', TRUE, + 'primary_user_id', primary_user_id, + 'merged_users_count', merge_count, + 'migrated_transactions', transaction_count, + 'email', email_param + ); + + RETURN result; +END; $$ LANGUAGE plpgsql SECURITY DEFINER; \ No newline at end of file diff --git a/supabase-schema.sql b/supabase-schema.sql index 23fd31d..95d7f15 100644 --- a/supabase-schema.sql +++ b/supabase-schema.sql @@ -37,12 +37,29 @@ CREATE OR REPLACE FUNCTION ensure_user_exists( RETURNS UUID AS $$ DECLARE user_uuid UUID; + existing_user_id UUID; exists_flag BOOLEAN; BEGIN -- Convert to UUID user_uuid := user_id_param::UUID; - -- Check if user exists + -- First check if a user with this email already exists + SELECT id INTO existing_user_id + FROM users + WHERE email = email_param; + + -- If a user with this email exists, use their ID and update + IF FOUND THEN + UPDATE users + SET last_login = now(), + name = COALESCE(name_param, name) + WHERE id = existing_user_id; + + RETURN existing_user_id; + END IF; + + -- If we get here, no user with this email exists + -- Now check if user with this ID exists SELECT EXISTS ( SELECT 1 FROM users WHERE id = user_uuid ) INTO exists_flag; @@ -52,9 +69,11 @@ BEGIN INSERT INTO users (id, email, name, first_login, last_login) VALUES (user_uuid, email_param, name_param, now(), now()); ELSE - -- Update last_login + -- Update last_login and other fields UPDATE users - SET last_login = now() + SET last_login = now(), + email = email_param, + name = COALESCE(name_param, name) WHERE id = user_uuid; END IF;