diff --git a/supabase/functions/notify-moderators-submission/index.ts b/supabase/functions/notify-moderators-submission/index.ts index 69f59201..797aece3 100644 --- a/supabase/functions/notify-moderators-submission/index.ts +++ b/supabase/functions/notify-moderators-submission/index.ts @@ -24,89 +24,74 @@ serve(async (req) => { const supabase = createClient(supabaseUrl, supabaseServiceKey); - const payload: NotificationPayload = await req.json(); - const { submission_id, submission_type, submitter_name, action } = payload; + const { submission_id, submission_type, submitter_name, action } = await req.json(); - console.log('Notifying moderators about submission:', { submission_id, submission_type }); + console.log('Notifying moderators about submission:', { submission_id, submission_type, submitter_name, action }); - // Get all moderators, admins, and superusers - const { data: moderators, error: moderatorsError } = await supabase - .from('user_roles') - .select('user_id') - .in('role', ['moderator', 'admin', 'superuser']); + // Get the workflow configuration + const { data: workflow, error: workflowError } = await supabase + .from('notification_templates') + .select('workflow_id') + .eq('category', 'moderation') + .eq('is_active', true) + .single(); - if (moderatorsError) { - console.error('Error fetching moderators:', moderatorsError); - throw moderatorsError; - } - - if (!moderators || moderators.length === 0) { - console.log('No moderators found to notify'); + if (workflowError || !workflow) { + console.error('Error fetching workflow:', workflowError); return new Response( - JSON.stringify({ success: true, count: 0, message: 'No moderators to notify' }), - { headers: { ...corsHeaders, 'Content-Type': 'application/json' }, status: 200 } + JSON.stringify({ + success: false, + error: 'Workflow not found or not active' + }), + { + headers: { ...corsHeaders, 'Content-Type': 'application/json' }, + status: 500, + } ); } - console.log(`Found ${moderators.length} moderators to notify`); - - // Get the moderation-alert template - const { data: template } = await supabase - .from('notification_templates') - .select('workflow_id') - .eq('workflow_id', 'moderation-alert') - .single(); - - if (!template) { - console.warn('moderation-alert workflow not configured in notification_templates'); - } - - // Prepare notification data - const moderationUrl = `${supabaseUrl.replace('.supabase.co', '')}/admin/moderation`; + // Prepare notification payload const notificationPayload = { itemType: submission_type, - submissionId: submission_id, submitterName: submitter_name, + submissionId: submission_id, action: action || 'create', - moderationUrl, + moderationUrl: `https://ydvtmnrszybqnbcqbdcy.supabase.co/admin/moderation`, }; - // Send notifications to all moderators in parallel - const notificationPromises = moderators.map(async (moderator) => { - try { - const { error: notifyError } = await supabase.functions.invoke('trigger-notification', { - body: { - workflowId: 'moderation-alert', - subscriberId: moderator.user_id, - payload: notificationPayload, - }, - }); - - if (notifyError) { - console.error(`Failed to notify moderator ${moderator.user_id}:`, notifyError); - return { success: false, userId: moderator.user_id, error: notifyError }; - } - - console.log(`Successfully notified moderator ${moderator.user_id}`); - return { success: true, userId: moderator.user_id }; - } catch (error) { - console.error(`Exception notifying moderator ${moderator.user_id}:`, error); - return { success: false, userId: moderator.user_id, error }; - } + // Send ONE notification to the moderation-submissions topic + // All subscribers (moderators) will receive it automatically + const { data, error } = await supabase.functions.invoke('trigger-notification', { + body: { + workflowId: workflow.workflow_id, + topicKey: 'moderation-submissions', // Use topic instead of individual subscribers + payload: notificationPayload, + }, }); - const results = await Promise.all(notificationPromises); - const successCount = results.filter(r => r.success).length; - const failCount = results.filter(r => !r.success).length; + if (error) { + console.error('Failed to notify moderators via topic:', error); + return new Response( + JSON.stringify({ + success: false, + error: 'Failed to send notification to topic', + details: error.message + }), + { + headers: { ...corsHeaders, 'Content-Type': 'application/json' }, + status: 500, + } + ); + } - console.log(`Notification summary: ${successCount} sent, ${failCount} failed`); + console.log('Successfully notified all moderators via topic:', data); return new Response( JSON.stringify({ success: true, - count: successCount, - failed: failCount, - details: results, + message: 'Moderator notifications sent via topic', + topicKey: 'moderation-submissions', + transactionId: data?.transactionId, }), { headers: { ...corsHeaders, 'Content-Type': 'application/json' }, diff --git a/supabase/migrations/20251012182032_881fdb13-012e-46a1-aa18-f37f422a8a36.sql b/supabase/migrations/20251012182032_881fdb13-012e-46a1-aa18-f37f422a8a36.sql new file mode 100644 index 00000000..8c544d41 --- /dev/null +++ b/supabase/migrations/20251012182032_881fdb13-012e-46a1-aa18-f37f422a8a36.sql @@ -0,0 +1,79 @@ +-- Create function to sync moderator role changes to Novu topics +CREATE OR REPLACE FUNCTION public.sync_moderator_novu_topic() +RETURNS TRIGGER +LANGUAGE plpgsql +SECURITY DEFINER +SET search_path = public +AS $$ +DECLARE + function_url text; + anon_key text; + action_type text; +BEGIN + -- Only process moderator, admin, and superuser roles + IF (TG_OP = 'INSERT' AND NEW.role IN ('moderator', 'admin', 'superuser')) OR + (TG_OP = 'DELETE' AND OLD.role IN ('moderator', 'admin', 'superuser')) THEN + + -- Build the function URL + function_url := 'https://ydvtmnrszybqnbcqbdcy.supabase.co/functions/v1/manage-moderator-topic'; + + -- Use the public anon key + anon_key := 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InlkdnRtbnJzenlicW5iY3FiZGN5Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTgzMjYzNTYsImV4cCI6MjA3MzkwMjM1Nn0.DM3oyapd_omP5ZzIlrT0H9qBsiQBxBRgw2tYuqgXKX4'; + + -- Determine action type + action_type := CASE + WHEN TG_OP = 'INSERT' THEN 'add' + WHEN TG_OP = 'DELETE' THEN 'remove' + END; + + -- Call edge function asynchronously to add/remove from Novu topics + PERFORM net.http_post( + url := function_url, + headers := jsonb_build_object( + 'Content-Type', 'application/json', + 'Authorization', 'Bearer ' || anon_key, + 'apikey', anon_key + ), + body := jsonb_build_object( + 'userId', CASE WHEN TG_OP = 'INSERT' THEN NEW.user_id ELSE OLD.user_id END, + 'action', action_type + ) + ); + + RAISE LOG 'Triggered Novu topic sync for user % with action %', + CASE WHEN TG_OP = 'INSERT' THEN NEW.user_id ELSE OLD.user_id END, + action_type; + END IF; + + RETURN COALESCE(NEW, OLD); +EXCEPTION + WHEN OTHERS THEN + -- Log error but don't fail the role change + RAISE WARNING 'Failed to sync moderator to Novu topic: %', SQLERRM; + RETURN COALESCE(NEW, OLD); +END; +$$; + +-- Create trigger for role insertions +DROP TRIGGER IF EXISTS sync_moderator_novu_topic_on_insert ON public.user_roles; +CREATE TRIGGER sync_moderator_novu_topic_on_insert +AFTER INSERT ON public.user_roles +FOR EACH ROW +EXECUTE FUNCTION public.sync_moderator_novu_topic(); + +-- Create trigger for role deletions +DROP TRIGGER IF EXISTS sync_moderator_novu_topic_on_delete ON public.user_roles; +CREATE TRIGGER sync_moderator_novu_topic_on_delete +AFTER DELETE ON public.user_roles +FOR EACH ROW +EXECUTE FUNCTION public.sync_moderator_novu_topic(); + +-- Add comments +COMMENT ON FUNCTION public.sync_moderator_novu_topic() IS + 'Automatically adds or removes users from Novu moderation topics when moderator-level roles are granted or revoked'; + +COMMENT ON TRIGGER sync_moderator_novu_topic_on_insert ON public.user_roles IS + 'Adds users to Novu moderation topics when they receive moderator, admin, or superuser roles'; + +COMMENT ON TRIGGER sync_moderator_novu_topic_on_delete ON public.user_roles IS + 'Removes users from Novu moderation topics when their moderator, admin, or superuser roles are revoked'; \ No newline at end of file