diff --git a/src/components/admin/NovuMigrationUtility.tsx b/src/components/admin/NovuMigrationUtility.tsx index ecf60543..7f7ea2b5 100644 --- a/src/components/admin/NovuMigrationUtility.tsx +++ b/src/components/admin/NovuMigrationUtility.tsx @@ -1,6 +1,5 @@ import { useState } from 'react'; import { supabase } from '@/integrations/supabase/client'; -import { notificationService } from '@/lib/notificationService'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Progress } from '@/components/ui/progress'; @@ -28,29 +27,31 @@ export function NovuMigrationUtility() { setProgress(0); try { - // First, fetch user IDs that already have Novu subscriber IDs - const { data: existingPrefs, error: prefsError } = await supabase - .from('user_notification_preferences') - .select('user_id') - .not('novu_subscriber_id', 'is', null); + // Call the server-side migration function + const { data: { session } } = await supabase.auth.getSession(); + + if (!session) { + throw new Error('You must be logged in to run the migration'); + } - if (prefsError) throw prefsError; + const response = await fetch( + 'https://ydvtmnrszybqnbcqbdcy.supabase.co/functions/v1/migrate-novu-users', + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${session.access_token}`, + }, + } + ); - const existingUserIds = existingPrefs?.map(p => p.user_id) || []; + const data = await response.json(); - // Fetch users without Novu subscriber IDs - const query = supabase - .from('profiles') - .select('user_id, users:user_id(email)'); + if (!response.ok || !data.success) { + throw new Error(data.error || 'Migration failed'); + } - // Only add the not filter if there are existing user IDs - const { data: users, error: fetchError } = existingUserIds.length > 0 - ? await query.not('user_id', 'in', `(${existingUserIds.join(',')})`) - : await query; - - if (fetchError) throw fetchError; - - if (!users || users.length === 0) { + if (!data.results || data.results.length === 0) { toast({ title: "No users to migrate", description: "All users are already registered with Novu.", @@ -59,55 +60,12 @@ export function NovuMigrationUtility() { return; } - setTotalUsers(users.length); - const migrationResults: MigrationResult[] = []; + setTotalUsers(data.total); + setResults(data.results); + setProgress(100); - // Process users one by one - for (let i = 0; i < users.length; i++) { - const user = users[i]; - const email = (user.users as any)?.email; - - if (!email) { - migrationResults.push({ - userId: user.user_id, - email: 'No email found', - success: false, - error: 'User email not found', - }); - continue; - } - - try { - const result = await notificationService.createSubscriber({ - subscriberId: user.user_id, - email, - data: { userId: user.user_id }, - }); - - migrationResults.push({ - userId: user.user_id, - email, - success: result.success, - error: result.error, - }); - - // Small delay to avoid overwhelming the API - await new Promise(resolve => setTimeout(resolve, 100)); - } catch (error: any) { - migrationResults.push({ - userId: user.user_id, - email, - success: false, - error: error.message, - }); - } - - setProgress(((i + 1) / users.length) * 100); - setResults([...migrationResults]); - } - - const successCount = migrationResults.filter(r => r.success).length; - const failureCount = migrationResults.filter(r => !r.success).length; + const successCount = data.results.filter((r: MigrationResult) => r.success).length; + const failureCount = data.results.filter((r: MigrationResult) => !r.success).length; toast({ title: "Migration completed", diff --git a/supabase/functions/migrate-novu-users/index.ts b/supabase/functions/migrate-novu-users/index.ts new file mode 100644 index 00000000..6cedf1d9 --- /dev/null +++ b/supabase/functions/migrate-novu-users/index.ts @@ -0,0 +1,153 @@ +import { serve } from "https://deno.land/std@0.168.0/http/server.ts"; +import { createClient } from "https://esm.sh/@supabase/supabase-js@2.57.4"; +import { Novu } from "npm:@novu/node@2.0.2"; + +const corsHeaders = { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type', +}; + +serve(async (req) => { + if (req.method === 'OPTIONS') { + return new Response(null, { headers: corsHeaders }); + } + + try { + const supabaseUrl = Deno.env.get('SUPABASE_URL')!; + const supabaseServiceKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!; + const novuApiKey = Deno.env.get('NOVU_API_KEY'); + + if (!novuApiKey) { + throw new Error('NOVU_API_KEY is not configured'); + } + + // Create Supabase client with service role for admin access + const supabase = createClient(supabaseUrl, supabaseServiceKey); + + const novu = new Novu(novuApiKey, { + backendUrl: Deno.env.get('VITE_NOVU_API_URL') || 'https://api.novu.co', + }); + + // Fetch users who don't have Novu subscriber IDs + const { data: existingPrefs, error: prefsError } = await supabase + .from('user_notification_preferences') + .select('user_id') + .not('novu_subscriber_id', 'is', null); + + if (prefsError) throw prefsError; + + const existingUserIds = existingPrefs?.map(p => p.user_id) || []; + + // Fetch all profiles + let query = supabase + .from('profiles') + .select('user_id'); + + // Only add the not filter if there are existing user IDs + if (existingUserIds.length > 0) { + query = query.not('user_id', 'in', `(${existingUserIds.join(',')})`); + } + + const { data: profiles, error: profilesError } = await query; + + if (profilesError) throw profilesError; + + if (!profiles || profiles.length === 0) { + return new Response( + JSON.stringify({ + success: true, + message: 'No users to migrate', + results: [], + }), + { + headers: { ...corsHeaders, 'Content-Type': 'application/json' }, + status: 200, + } + ); + } + + // Fetch user emails from auth.users using service role + const userIds = profiles.map(p => p.user_id); + const { data: authUsers, error: authError } = await supabase.auth.admin.listUsers(); + + if (authError) throw authError; + + const userEmails = new Map( + authUsers.users + .filter(u => userIds.includes(u.id)) + .map(u => [u.id, u.email]) + ); + + // Migrate users + const results = []; + for (const profile of profiles) { + const email = userEmails.get(profile.user_id); + + if (!email) { + results.push({ + userId: profile.user_id, + email: 'No email found', + success: false, + error: 'User email not found', + }); + continue; + } + + try { + const subscriber = await novu.subscribers.identify(profile.user_id, { + email, + data: { userId: profile.user_id }, + }); + + // Update the user's notification preferences with the Novu subscriber ID + await supabase + .from('user_notification_preferences') + .upsert({ + user_id: profile.user_id, + novu_subscriber_id: subscriber.data._id, + }); + + results.push({ + userId: profile.user_id, + email, + success: true, + }); + } catch (error: any) { + results.push({ + userId: profile.user_id, + email, + success: false, + error: error.message, + }); + } + + // Small delay to avoid overwhelming the API + await new Promise(resolve => setTimeout(resolve, 100)); + } + + return new Response( + JSON.stringify({ + success: true, + total: profiles.length, + results, + }), + { + headers: { ...corsHeaders, 'Content-Type': 'application/json' }, + status: 200, + } + ); + } catch (error: any) { + console.error('Error migrating Novu users:', error); + + return new Response( + JSON.stringify({ + success: false, + error: error.message, + }), + { + headers: { ...corsHeaders, 'Content-Type': 'application/json' }, + status: 500, + } + ); + } +});