diff --git a/src/lib/notificationService.ts b/src/lib/notificationService.ts index 1ff6477a..29e0095e 100644 --- a/src/lib/notificationService.ts +++ b/src/lib/notificationService.ts @@ -448,6 +448,125 @@ class NotificationService { } } + /** + * Trigger a system announcement to all users via the "users" topic + * Requires admin or superuser role + */ + async sendSystemAnnouncement(payload: { + title: string; + message: string; + severity: 'info' | 'warning' | 'critical'; + actionUrl?: string; + }): Promise<{ success: boolean; error?: string; announcementId?: string }> { + try { + const novuEnabled = await this.isNovuEnabled(); + if (!novuEnabled) { + logger.warn('Novu not configured, skipping system announcement', { + action: 'send_system_announcement', + title: payload.title + }); + return { success: false, error: 'Novu not configured' }; + } + + const { data, error, requestId } = await invokeWithTracking( + 'notify-system-announcement', + payload + ); + + if (error) { + logger.error('Failed to send system announcement', { + action: 'send_system_announcement', + title: payload.title, + requestId, + error: error.message + }); + throw error; + } + + logger.info('System announcement sent successfully', { + action: 'send_system_announcement', + title: payload.title, + announcementId: data?.announcementId, + requestId + }); + + return { + success: true, + announcementId: data?.announcementId + }; + } catch (error: unknown) { + logger.error('Error sending system announcement', { + action: 'send_system_announcement', + title: payload.title, + error: error instanceof Error ? error.message : String(error) + }); + + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to send system announcement' + }; + } + } + + /** + * Notify moderators about a new report via the "moderation-reports" topic + */ + async notifyModeratorsReport(payload: { + reportId: string; + reportType: string; + reportedEntityType: string; + reportedEntityId: string; + reporterName: string; + reason: string; + entityPreview: string; + reportedAt: string; + }): Promise<{ success: boolean; error?: string }> { + try { + const novuEnabled = await this.isNovuEnabled(); + if (!novuEnabled) { + logger.warn('Novu not configured, skipping report notification', { + action: 'notify_moderators_report', + reportId: payload.reportId + }); + return { success: false, error: 'Novu not configured' }; + } + + const { data, error, requestId } = await invokeWithTracking( + 'notify-moderators-report', + payload + ); + + if (error) { + logger.error('Failed to notify moderators about report', { + action: 'notify_moderators_report', + reportId: payload.reportId, + requestId, + error: error.message + }); + throw error; + } + + logger.info('Moderators notified about report successfully', { + action: 'notify_moderators_report', + reportId: payload.reportId, + requestId + }); + + return { success: true }; + } catch (error: unknown) { + logger.error('Error notifying moderators about report', { + action: 'notify_moderators_report', + reportId: payload.reportId, + error: error instanceof Error ? error.message : String(error) + }); + + return { + success: false, + error: 'Failed to notify moderators about report' + }; + } + } + /** * Check if notifications are enabled */ diff --git a/supabase/config.toml b/supabase/config.toml index 3f16ae8f..d938645b 100644 --- a/supabase/config.toml +++ b/supabase/config.toml @@ -12,6 +12,12 @@ verify_jwt = true [functions.trigger-notification] verify_jwt = true +[functions.notify-system-announcement] +verify_jwt = true + +[functions.notify-moderators-report] +verify_jwt = false + [functions.manage-moderator-topic] verify_jwt = false diff --git a/supabase/functions/notify-system-announcement/index.ts b/supabase/functions/notify-system-announcement/index.ts new file mode 100644 index 00000000..9659d9b6 --- /dev/null +++ b/supabase/functions/notify-system-announcement/index.ts @@ -0,0 +1,183 @@ +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 { startRequest, endRequest } from "../_shared/logger.ts"; + +const corsHeaders = { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type, x-request-id', +}; + +interface AnnouncementPayload { + title: string; + message: string; + severity: 'info' | 'warning' | 'critical'; + actionUrl?: string; +} + +serve(async (req) => { + if (req.method === 'OPTIONS') { + return new Response(null, { headers: corsHeaders }); + } + + const tracking = startRequest('notify-system-announcement'); + + try { + const supabaseUrl = Deno.env.get('SUPABASE_URL')!; + const supabaseServiceKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!; + + const supabase = createClient(supabaseUrl, supabaseServiceKey); + + // Get authorization header + const authHeader = req.headers.get('authorization'); + if (!authHeader) { + throw new Error('Authorization header required'); + } + + // Verify user is admin or superuser + const token = authHeader.replace('Bearer ', ''); + const { data: { user }, error: authError } = await supabase.auth.getUser(token); + + if (authError || !user) { + throw new Error('Unauthorized: Invalid token'); + } + + // Check user role + const { data: roles, error: roleError } = await supabase + .from('user_roles') + .select('role') + .eq('user_id', user.id) + .in('role', ['admin', 'superuser']); + + if (roleError || !roles || roles.length === 0) { + throw new Error('Unauthorized: Admin or superuser role required'); + } + + // Get user profile for logging + const { data: profile } = await supabase + .from('profiles') + .select('username, display_name') + .eq('user_id', user.id) + .single(); + + const payload: AnnouncementPayload = await req.json(); + + // Validate required fields + if (!payload.title || !payload.message || !payload.severity) { + throw new Error('Missing required fields: title, message, or severity'); + } + + if (!['info', 'warning', 'critical'].includes(payload.severity)) { + throw new Error('Invalid severity level. Must be: info, warning, or critical'); + } + + console.log('Processing system announcement:', { + title: payload.title, + severity: payload.severity, + publishedBy: profile?.username || 'unknown', + requestId: tracking.requestId + }); + + // Fetch the workflow ID for system announcements + const { data: template, error: templateError } = await supabase + .from('notification_templates') + .select('workflow_id') + .eq('workflow_id', 'system-announcement') + .eq('is_active', true) + .maybeSingle(); + + if (templateError) { + console.error('Error fetching workflow:', templateError); + throw new Error(`Failed to fetch workflow: ${templateError.message}`); + } + + if (!template) { + console.warn('No active system-announcement workflow found'); + return new Response( + JSON.stringify({ + success: false, + error: 'No active system-announcement workflow configured', + }), + { + headers: { ...corsHeaders, 'Content-Type': 'application/json' }, + status: 400, + } + ); + } + + const announcementId = crypto.randomUUID(); + const publishedAt = new Date().toISOString(); + const publishedBy = profile?.display_name || profile?.username || 'System Admin'; + + // Build notification payload for all users + const notificationPayload = { + announcementId, + title: payload.title, + message: payload.message, + severity: payload.severity, + actionUrl: payload.actionUrl || '', + publishedAt, + publishedBy, + }; + + console.log('Triggering announcement to all users via "users" topic'); + + // Invoke the trigger-notification function with users topic + const { data: result, error: notifyError } = await supabase.functions.invoke( + 'trigger-notification', + { + body: { + workflowId: template.workflow_id, + topicKey: 'users', + payload: notificationPayload, + }, + } + ); + + if (notifyError) { + console.error('Error triggering notification:', notifyError); + throw notifyError; + } + + console.log('System announcement triggered successfully:', result); + + endRequest(tracking, 200); + + return new Response( + JSON.stringify({ + success: true, + transactionId: result?.transactionId, + announcementId, + payload: notificationPayload, + requestId: tracking.requestId + }), + { + headers: { + ...corsHeaders, + 'Content-Type': 'application/json', + 'X-Request-ID': tracking.requestId + }, + status: 200, + } + ); + } catch (error: any) { + console.error('Error in notify-system-announcement:', error); + + endRequest(tracking, error.message.includes('Unauthorized') ? 403 : 500, error.message); + + return new Response( + JSON.stringify({ + success: false, + error: error.message, + requestId: tracking.requestId + }), + { + headers: { + ...corsHeaders, + 'Content-Type': 'application/json', + 'X-Request-ID': tracking.requestId + }, + status: error.message.includes('Unauthorized') ? 403 : 500, + } + ); + } +});