feat: Implement Novu notification system

This commit is contained in:
gpt-engineer-app[bot]
2025-10-01 12:26:12 +00:00
parent cae084964e
commit e2f0df22cc
13 changed files with 1407 additions and 253 deletions

View File

@@ -1,5 +1,17 @@
project_id = "ydvtmnrszybqnbcqbdcy"
[functions.create-novu-subscriber]
verify_jwt = true
[functions.update-novu-preferences]
verify_jwt = true
[functions.trigger-notification]
verify_jwt = true
[functions.novu-webhook]
verify_jwt = false
[functions.detect-location]
verify_jwt = false

View File

@@ -0,0 +1,64 @@
import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
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 novuApiKey = Deno.env.get('NOVU_API_KEY');
if (!novuApiKey) {
throw new Error('NOVU_API_KEY is not configured');
}
const novu = new Novu(novuApiKey, {
backendUrl: Deno.env.get('VITE_NOVU_API_URL') || 'https://api.novu.co',
});
const { subscriberId, email, firstName, lastName, phone, avatar, data } = await req.json();
console.log('Creating Novu subscriber:', { subscriberId, email });
const subscriber = await novu.subscribers.identify(subscriberId, {
email,
firstName,
lastName,
phone,
avatar,
data,
});
console.log('Subscriber created successfully:', subscriber.data);
return new Response(
JSON.stringify({
success: true,
subscriberId: subscriber.data._id,
}),
{
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
status: 200,
}
);
} catch (error: any) {
console.error('Error creating Novu subscriber:', error);
return new Response(
JSON.stringify({
success: false,
error: error.message,
}),
{
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
status: 500,
}
);
}
});

View File

@@ -0,0 +1,106 @@
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";
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 supabase = createClient(supabaseUrl, supabaseServiceKey);
const event = await req.json();
console.log('Received Novu webhook event:', event.type);
// Handle different webhook events
switch (event.type) {
case 'notification.sent':
await handleNotificationSent(supabase, event);
break;
case 'notification.delivered':
await handleNotificationDelivered(supabase, event);
break;
case 'notification.read':
await handleNotificationRead(supabase, event);
break;
case 'notification.failed':
await handleNotificationFailed(supabase, event);
break;
default:
console.log('Unhandled event type:', event.type);
}
return new Response(
JSON.stringify({ success: true }),
{
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
status: 200,
}
);
} catch (error: any) {
console.error('Error processing webhook:', error);
return new Response(
JSON.stringify({
success: false,
error: error.message,
}),
{
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
status: 500,
}
);
}
});
async function handleNotificationSent(supabase: any, event: any) {
const { transactionId, channel } = event.data;
await supabase
.from('notification_logs')
.update({ status: 'sent' })
.eq('novu_transaction_id', transactionId);
}
async function handleNotificationDelivered(supabase: any, event: any) {
const { transactionId } = event.data;
await supabase
.from('notification_logs')
.update({
status: 'delivered',
delivered_at: new Date().toISOString(),
})
.eq('novu_transaction_id', transactionId);
}
async function handleNotificationRead(supabase: any, event: any) {
const { transactionId } = event.data;
await supabase
.from('notification_logs')
.update({
read_at: new Date().toISOString(),
})
.eq('novu_transaction_id', transactionId);
}
async function handleNotificationFailed(supabase: any, event: any) {
const { transactionId, error } = event.data;
await supabase
.from('notification_logs')
.update({
status: 'failed',
error_message: error,
})
.eq('novu_transaction_id', transactionId);
}

View File

@@ -0,0 +1,63 @@
import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
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 novuApiKey = Deno.env.get('NOVU_API_KEY');
if (!novuApiKey) {
throw new Error('NOVU_API_KEY is not configured');
}
const novu = new Novu(novuApiKey, {
backendUrl: Deno.env.get('VITE_NOVU_API_URL') || 'https://api.novu.co',
});
const { workflowId, subscriberId, payload, overrides } = await req.json();
console.log('Triggering notification:', { workflowId, subscriberId });
const result = await novu.trigger(workflowId, {
to: {
subscriberId,
},
payload,
overrides,
});
console.log('Notification triggered successfully:', result.data);
return new Response(
JSON.stringify({
success: true,
transactionId: result.data.transactionId,
}),
{
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
status: 200,
}
);
} catch (error: any) {
console.error('Error triggering notification:', error);
return new Response(
JSON.stringify({
success: false,
error: error.message,
}),
{
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
status: 500,
}
);
}
});

View File

@@ -0,0 +1,88 @@
import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
import { Novu } from "npm:@novu/node@2.0.2";
import { createClient } from "https://esm.sh/@supabase/supabase-js@2.57.4";
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 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) {
throw new Error('NOVU_API_KEY is not configured');
}
const novu = new Novu(novuApiKey, {
backendUrl: Deno.env.get('VITE_NOVU_API_URL') || 'https://api.novu.co',
});
const supabase = createClient(supabaseUrl, supabaseServiceKey);
const { userId, preferences } = await req.json();
console.log('Updating preferences for user:', userId);
// Get Novu subscriber ID from database
const { data: prefData, error: prefError } = await supabase
.from('user_notification_preferences')
.select('novu_subscriber_id')
.eq('user_id', userId)
.single();
if (prefError || !prefData?.novu_subscriber_id) {
throw new Error('Novu subscriber not found');
}
const subscriberId = prefData.novu_subscriber_id;
// Update channel preferences in Novu
const channelPrefs = preferences.channelPreferences;
await novu.subscribers.updatePreference(
subscriberId,
{
enabled: true,
channels: {
email: channelPrefs.email,
sms: channelPrefs.sms,
in_app: channelPrefs.in_app,
push: channelPrefs.push,
},
}
);
console.log('Preferences updated successfully');
return new Response(
JSON.stringify({
success: true,
}),
{
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
status: 200,
}
);
} catch (error: any) {
console.error('Error updating Novu preferences:', error);
return new Response(
JSON.stringify({
success: false,
error: error.message,
}),
{
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
status: 500,
}
);
}
});

View File

@@ -0,0 +1,143 @@
-- Create notification_templates table
CREATE TABLE public.notification_templates (
id UUID NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY,
workflow_id TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
description TEXT,
category TEXT NOT NULL,
novu_workflow_id TEXT,
is_active BOOLEAN DEFAULT true,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()
);
-- Create notification_logs table
CREATE TABLE public.notification_logs (
id UUID NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY,
user_id UUID NOT NULL,
template_id UUID REFERENCES public.notification_templates(id),
novu_transaction_id TEXT,
channel TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'pending',
payload JSONB,
error_message TEXT,
delivered_at TIMESTAMP WITH TIME ZONE,
read_at TIMESTAMP WITH TIME ZONE,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()
);
-- Create notification_channels table
CREATE TABLE public.notification_channels (
id UUID NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY,
channel_type TEXT NOT NULL,
name TEXT NOT NULL,
description TEXT,
is_enabled BOOLEAN DEFAULT true,
configuration JSONB DEFAULT '{}'::jsonb,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()
);
-- Create user_notification_preferences table (enhanced)
CREATE TABLE public.user_notification_preferences (
id UUID NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY,
user_id UUID NOT NULL UNIQUE,
novu_subscriber_id TEXT,
channel_preferences JSONB NOT NULL DEFAULT '{
"in_app": true,
"email": true,
"push": false,
"sms": false
}'::jsonb,
workflow_preferences JSONB NOT NULL DEFAULT '{}'::jsonb,
frequency_settings JSONB NOT NULL DEFAULT '{
"digest": "daily",
"max_per_hour": 10
}'::jsonb,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()
);
-- Enable RLS
ALTER TABLE public.notification_templates ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.notification_logs ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.notification_channels ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.user_notification_preferences ENABLE ROW LEVEL SECURITY;
-- RLS Policies for notification_templates
CREATE POLICY "Public read access to notification templates"
ON public.notification_templates FOR SELECT
USING (true);
CREATE POLICY "Admins can manage notification templates"
ON public.notification_templates FOR ALL
USING (is_moderator(auth.uid()));
-- RLS Policies for notification_logs
CREATE POLICY "Users can view their own notification logs"
ON public.notification_logs FOR SELECT
USING (auth.uid() = user_id);
CREATE POLICY "Moderators can view all notification logs"
ON public.notification_logs FOR SELECT
USING (is_moderator(auth.uid()));
CREATE POLICY "System can insert notification logs"
ON public.notification_logs FOR INSERT
WITH CHECK (true);
-- RLS Policies for notification_channels
CREATE POLICY "Public read access to notification channels"
ON public.notification_channels FOR SELECT
USING (true);
CREATE POLICY "Admins can manage notification channels"
ON public.notification_channels FOR ALL
USING (is_moderator(auth.uid()));
-- RLS Policies for user_notification_preferences
CREATE POLICY "Users can manage their own notification preferences"
ON public.user_notification_preferences FOR ALL
USING (auth.uid() = user_id);
CREATE POLICY "Moderators can view all notification preferences"
ON public.user_notification_preferences FOR SELECT
USING (is_moderator(auth.uid()));
-- Create indexes
CREATE INDEX idx_notification_logs_user_id ON public.notification_logs(user_id);
CREATE INDEX idx_notification_logs_status ON public.notification_logs(status);
CREATE INDEX idx_notification_logs_created_at ON public.notification_logs(created_at DESC);
CREATE INDEX idx_user_notification_preferences_user_id ON public.user_notification_preferences(user_id);
CREATE INDEX idx_user_notification_preferences_novu_subscriber_id ON public.user_notification_preferences(novu_subscriber_id);
-- Create trigger for updated_at
CREATE TRIGGER update_notification_templates_updated_at
BEFORE UPDATE ON public.notification_templates
FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column();
CREATE TRIGGER update_notification_channels_updated_at
BEFORE UPDATE ON public.notification_channels
FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column();
CREATE TRIGGER update_user_notification_preferences_updated_at
BEFORE UPDATE ON public.user_notification_preferences
FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column();
-- Insert default notification channels
INSERT INTO public.notification_channels (channel_type, name, description) VALUES
('in_app', 'In-App Notifications', 'Real-time notifications within the application'),
('email', 'Email Notifications', 'Email notifications sent to user''s registered email'),
('push', 'Push Notifications', 'Browser push notifications'),
('sms', 'SMS Notifications', 'Text message notifications (optional)');
-- Insert default notification templates
INSERT INTO public.notification_templates (workflow_id, name, description, category) VALUES
('review-reply', 'Review Reply', 'Notification when someone replies to your review', 'engagement'),
('new-follower', 'New Follower', 'Notification when someone follows you', 'social'),
('system-announcement', 'System Announcement', 'Important system-wide announcements', 'system'),
('weekly-digest', 'Weekly Digest', 'Weekly summary of activity', 'digest'),
('monthly-digest', 'Monthly Digest', 'Monthly summary of activity', 'digest'),
('moderation-alert', 'Moderation Alert', 'Notification for moderators about pending content', 'moderation'),
('content-approved', 'Content Approved', 'Notification when your submitted content is approved', 'moderation'),
('content-rejected', 'Content Rejected', 'Notification when your submitted content is rejected', 'moderation');