This commit is contained in:
zramsay 2025-03-12 10:01:34 -04:00
parent dcd7161a64
commit 09b55f9044
5 changed files with 296 additions and 43 deletions

View File

@ -252,15 +252,25 @@ export async function POST(req: NextRequest): Promise<NextResponse> {
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:', {

View File

@ -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<NextResponse> {
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';

View File

@ -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'
};
}
}

View File

@ -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;

View File

@ -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;