diff --git a/src/components/admin/NovuMigrationUtility.tsx b/src/components/admin/NovuMigrationUtility.tsx new file mode 100644 index 00000000..859c90cc --- /dev/null +++ b/src/components/admin/NovuMigrationUtility.tsx @@ -0,0 +1,196 @@ +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'; +import { Alert, AlertDescription } from '@/components/ui/alert'; +import { CheckCircle2, XCircle, AlertCircle, Loader2 } from 'lucide-react'; +import { useToast } from '@/hooks/use-toast'; + +interface MigrationResult { + userId: string; + email: string; + success: boolean; + error?: string; +} + +export function NovuMigrationUtility() { + const { toast } = useToast(); + const [isRunning, setIsRunning] = useState(false); + const [progress, setProgress] = useState(0); + const [results, setResults] = useState([]); + const [totalUsers, setTotalUsers] = useState(0); + + const runMigration = async () => { + setIsRunning(true); + setResults([]); + setProgress(0); + + try { + // Fetch users without Novu subscriber IDs + const { data: users, error: fetchError } = await supabase + .from('profiles') + .select('user_id, users:user_id(email)') + .not('user_id', 'in', + supabase + .from('user_notification_preferences') + .select('user_id') + .not('novu_subscriber_id', 'is', null) + ); + + if (fetchError) throw fetchError; + + if (!users || users.length === 0) { + toast({ + title: "No users to migrate", + description: "All users are already registered with Novu.", + }); + setIsRunning(false); + return; + } + + setTotalUsers(users.length); + const migrationResults: MigrationResult[] = []; + + // 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; + + toast({ + title: "Migration completed", + description: `Successfully migrated ${successCount} users. ${failureCount} failures.`, + }); + } catch (error: any) { + toast({ + variant: "destructive", + title: "Migration failed", + description: error.message, + }); + } finally { + setIsRunning(false); + } + }; + + const successCount = results.filter(r => r.success).length; + const failureCount = results.filter(r => !r.success).length; + + return ( + + + Novu User Migration + + Register existing users with Novu notification service + + + + + + + This utility will register all existing users who don't have a Novu subscriber ID. + The process is non-blocking and will continue even if individual registrations fail. + + + + + + {isRunning && totalUsers > 0 && ( +
+
+ Progress + {Math.round(progress)}% +
+ +

+ Processing {results.length} of {totalUsers} users +

+
+ )} + + {results.length > 0 && ( +
+
+
+ + {successCount} succeeded +
+
+ + {failureCount} failed +
+
+ +
+ {results.map((result, idx) => ( +
+ {result.email} + {result.success ? ( + + ) : ( +
+ + {result.error} +
+ )} +
+ ))} +
+
+ )} +
+
+ ); +} diff --git a/src/integrations/supabase/types.ts b/src/integrations/supabase/types.ts index 1df40cca..6c7d1730 100644 --- a/src/integrations/supabase/types.ts +++ b/src/integrations/supabase/types.ts @@ -1362,19 +1362,6 @@ export type Database = { } Returns: undefined } - migrate_existing_users_to_novu: { - Args: Record - Returns: { - email: string - error_message: string - success: boolean - user_id: string - }[] - } - register_novu_subscriber: { - Args: { _user_id: string } - Returns: boolean - } update_company_ratings: { Args: { target_company_id: string } Returns: undefined diff --git a/src/pages/AdminSettings.tsx b/src/pages/AdminSettings.tsx index 0271145f..de6d9d5e 100644 --- a/src/pages/AdminSettings.tsx +++ b/src/pages/AdminSettings.tsx @@ -11,6 +11,7 @@ import { AdminHeader } from '@/components/layout/AdminHeader'; import { useAuth } from '@/hooks/useAuth'; import { useUserRole } from '@/hooks/useUserRole'; import { useAdminSettings } from '@/hooks/useAdminSettings'; +import { NovuMigrationUtility } from '@/components/admin/NovuMigrationUtility'; import { Loader2, Save, Clock, Users, Bell, Shield, Settings, Trash2, Plug } from 'lucide-react'; export default function AdminSettings() { @@ -517,6 +518,8 @@ export default function AdminSettings() {

No integration settings configured yet.

)} + + diff --git a/src/pages/Auth.tsx b/src/pages/Auth.tsx index 962ba1e2..1f1e51e5 100644 --- a/src/pages/Auth.tsx +++ b/src/pages/Auth.tsx @@ -12,6 +12,7 @@ import { Zap, Mail, Lock, User, AlertCircle, Eye, EyeOff } from 'lucide-react'; import { supabase } from '@/integrations/supabase/client'; import { useToast } from '@/hooks/use-toast'; import { TurnstileCaptcha } from '@/components/auth/TurnstileCaptcha'; +import { notificationService } from '@/lib/notificationService'; export default function Auth() { const [searchParams] = useSearchParams(); const navigate = useNavigate(); @@ -140,6 +141,21 @@ export default function Auth() { }); if (error) throw error; + + // Register user with Novu (non-blocking) + if (data.user) { + notificationService.createSubscriber({ + subscriberId: data.user.id, + email: formData.email, + firstName: formData.displayName || formData.username, + data: { + username: formData.username, + } + }).catch(err => { + console.error('Failed to register Novu subscriber:', err); + // Don't block signup if Novu registration fails + }); + } toast({ title: "Welcome to ThrillWiki!", diff --git a/supabase/migrations/20251001131458_b82530cf-f652-48b2-8cd6-391ea391c9b0.sql b/supabase/migrations/20251001131458_b82530cf-f652-48b2-8cd6-391ea391c9b0.sql new file mode 100644 index 00000000..0bb388ec --- /dev/null +++ b/supabase/migrations/20251001131458_b82530cf-f652-48b2-8cd6-391ea391c9b0.sql @@ -0,0 +1,26 @@ +-- Drop the problematic functions that try to call edge functions from database +DROP FUNCTION IF EXISTS public.migrate_existing_users_to_novu(); +DROP FUNCTION IF EXISTS public.register_novu_subscriber(uuid); + +-- Update handle_new_user to only create the profile (remove Novu registration) +CREATE OR REPLACE FUNCTION public.handle_new_user() +RETURNS trigger +LANGUAGE plpgsql +SECURITY DEFINER +SET search_path = public +AS $$ +BEGIN + -- Create profile record + INSERT INTO public.profiles (user_id, username, display_name) + VALUES ( + NEW.id, + COALESCE(NEW.raw_user_meta_data ->> 'username', 'user_' || substring(NEW.id::text, 1, 8)), + COALESCE(NEW.raw_user_meta_data ->> 'display_name', NEW.raw_user_meta_data ->> 'name') + ); + + -- Novu registration will be handled by the frontend + RETURN NEW; +END; +$$; + +COMMENT ON FUNCTION public.handle_new_user IS 'Creates user profile on signup. Novu registration is handled by frontend.'; \ No newline at end of file