diff --git a/supabase/config.toml b/supabase/config.toml index 9f79d0ef..9a011d32 100644 --- a/supabase/config.toml +++ b/supabase/config.toml @@ -9,6 +9,12 @@ verify_jwt = true [functions.trigger-notification] verify_jwt = true +[functions.manage-moderator-topic] +verify_jwt = false + +[functions.sync-all-moderators-to-topic] +verify_jwt = true + [functions.novu-webhook] verify_jwt = false diff --git a/supabase/functions/manage-moderator-topic/index.ts b/supabase/functions/manage-moderator-topic/index.ts new file mode 100644 index 00000000..6b33190b --- /dev/null +++ b/supabase/functions/manage-moderator-topic/index.ts @@ -0,0 +1,99 @@ +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/api@1.6.0"; + +const corsHeaders = { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type', +}; + +const TOPICS = { + MODERATION_SUBMISSIONS: 'moderation-submissions', + MODERATION_REPORTS: 'moderation-reports', +} as const; + +serve(async (req) => { + if (req.method === 'OPTIONS') { + return new Response(null, { headers: corsHeaders }); + } + + try { + const novuApiKey = Deno.env.get('NOVU_API_KEY'); + if (!novuApiKey) { + throw new Error('NOVU_API_KEY is not configured'); + } + + const novu = new Novu({ secretKey: novuApiKey }); + + const { userId, action } = await req.json(); + + if (!userId || !action) { + throw new Error('Missing required fields: userId, action'); + } + + if (action !== 'add' && action !== 'remove') { + throw new Error('Action must be either "add" or "remove"'); + } + + console.log(`${action === 'add' ? 'Adding' : 'Removing'} user ${userId} ${action === 'add' ? 'to' : 'from'} moderator topics`); + + const topics = [TOPICS.MODERATION_SUBMISSIONS, TOPICS.MODERATION_REPORTS]; + const results = []; + + for (const topicKey of topics) { + try { + if (action === 'add') { + // Add subscriber to topic + await novu.topics.addSubscribers(topicKey, { + subscribers: [userId], + }); + console.log(`Added ${userId} to topic ${topicKey}`); + results.push({ topic: topicKey, action: 'added', success: true }); + } else { + // Remove subscriber from topic + await novu.topics.removeSubscribers(topicKey, { + subscribers: [userId], + }); + console.log(`Removed ${userId} from topic ${topicKey}`); + results.push({ topic: topicKey, action: 'removed', success: true }); + } + } catch (error: any) { + console.error(`Error ${action}ing user ${userId} ${action === 'add' ? 'to' : 'from'} topic ${topicKey}:`, error); + results.push({ + topic: topicKey, + action: action === 'add' ? 'added' : 'removed', + success: false, + error: error.message + }); + } + } + + const allSuccess = results.every(r => r.success); + + return new Response( + JSON.stringify({ + success: allSuccess, + userId, + action, + results, + }), + { + headers: { ...corsHeaders, 'Content-Type': 'application/json' }, + status: allSuccess ? 200 : 207, // 207 = Multi-Status (partial success) + } + ); + } catch (error: any) { + console.error('Error managing moderator topic:', error); + + return new Response( + JSON.stringify({ + success: false, + error: error.message, + }), + { + headers: { ...corsHeaders, 'Content-Type': 'application/json' }, + status: 500, + } + ); + } +}); diff --git a/supabase/functions/sync-all-moderators-to-topic/index.ts b/supabase/functions/sync-all-moderators-to-topic/index.ts new file mode 100644 index 00000000..2583d45b --- /dev/null +++ b/supabase/functions/sync-all-moderators-to-topic/index.ts @@ -0,0 +1,121 @@ +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/api@1.6.0"; + +const corsHeaders = { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type', +}; + +const TOPICS = { + MODERATION_SUBMISSIONS: 'moderation-submissions', + MODERATION_REPORTS: 'moderation-reports', +} as const; + +serve(async (req) => { + if (req.method === 'OPTIONS') { + return new Response(null, { headers: corsHeaders }); + } + + try { + const novuApiKey = Deno.env.get('NOVU_API_KEY'); + const supabaseUrl = Deno.env.get('SUPABASE_URL'); + const supabaseServiceKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY'); + + if (!novuApiKey || !supabaseUrl || !supabaseServiceKey) { + throw new Error('Missing required environment variables'); + } + + const novu = new Novu({ secretKey: novuApiKey }); + const supabase = createClient(supabaseUrl, supabaseServiceKey); + + console.log('Starting moderator sync to Novu topics...'); + + // Get all moderators, admins, and superusers + const { data: moderatorRoles, error: rolesError } = await supabase + .from('user_roles') + .select('user_id, role') + .in('role', ['moderator', 'admin', 'superuser']); + + if (rolesError) { + throw new Error(`Failed to fetch moderator roles: ${rolesError.message}`); + } + + // Get unique user IDs (a user might have multiple moderator-level roles) + const uniqueUserIds = [...new Set(moderatorRoles.map(r => r.user_id))]; + + console.log(`Found ${uniqueUserIds.length} unique moderators to sync`); + + const topics = [TOPICS.MODERATION_SUBMISSIONS, TOPICS.MODERATION_REPORTS]; + const results = { + totalUsers: uniqueUserIds.length, + topics: [] as any[], + }; + + for (const topicKey of topics) { + try { + // Ensure topic exists (Novu will create it if it doesn't) + await novu.topics.create({ key: topicKey, name: topicKey }); + console.log(`Topic ${topicKey} ready`); + } catch (error: any) { + // Topic might already exist, which is fine + if (!error.message?.includes('already exists')) { + console.warn(`Note about topic ${topicKey}:`, error.message); + } + } + + // Add all moderators to the topic in batches + const batchSize = 100; + let successCount = 0; + let errorCount = 0; + + for (let i = 0; i < uniqueUserIds.length; i += batchSize) { + const batch = uniqueUserIds.slice(i, i + batchSize); + + try { + await novu.topics.addSubscribers(topicKey, { + subscribers: batch, + }); + successCount += batch.length; + console.log(`Added batch of ${batch.length} users to ${topicKey}`); + } catch (error: any) { + errorCount += batch.length; + console.error(`Error adding batch to ${topicKey}:`, error.message); + } + } + + results.topics.push({ + topicKey, + successCount, + errorCount, + }); + } + + console.log('Sync completed:', results); + + return new Response( + JSON.stringify({ + success: true, + message: 'Moderator sync completed', + results, + }), + { + headers: { ...corsHeaders, 'Content-Type': 'application/json' }, + status: 200, + } + ); + } catch (error: any) { + console.error('Error syncing moderators to topics:', error); + + return new Response( + JSON.stringify({ + success: false, + error: error.message, + }), + { + headers: { ...corsHeaders, 'Content-Type': 'application/json' }, + status: 500, + } + ); + } +}); diff --git a/supabase/functions/trigger-notification/index.ts b/supabase/functions/trigger-notification/index.ts index 0851a2db..6876c426 100644 --- a/supabase/functions/trigger-notification/index.ts +++ b/supabase/functions/trigger-notification/index.ts @@ -22,14 +22,21 @@ serve(async (req) => { secretKey: novuApiKey }); - const { workflowId, subscriberId, payload, overrides } = await req.json(); + const { workflowId, subscriberId, topicKey, payload, overrides } = await req.json(); - console.log('Triggering notification:', { workflowId, subscriberId }); + // Support both individual subscribers and topics + if (!subscriberId && !topicKey) { + throw new Error('Either subscriberId or topicKey must be provided'); + } + + const recipient = subscriberId + ? { subscriberId } + : { topicKey }; + + console.log('Triggering notification:', { workflowId, recipient }); const result = await novu.trigger({ - to: { - subscriberId, - }, + to: recipient, workflowId, payload, overrides,