/**
* Moderation Notification Service
*
* Handles creating in-app notifications and sending emails
* for moderation actions (post deletion, bans, warnings, etc.)
*/
import { render } from '@react-email/render';
import { createAdminClient } from '@/lib/supabase-server';
import { sendDigestEmail } from '@/lib/email/resend-client';
import {
PostDeletedEmail,
PostLockedEmail,
PostWarningEmail,
AccountSuspendedEmail,
AccountBannedEmail,
AccountUnbannedEmail,
AppealResolvedEmail,
} from '@/lib/email/moderation-templates';
// ============================================
// TYPES
// ============================================
export type ModerationNotificationType =
| 'post_deleted'
| 'post_locked'
| 'post_warning'
| 'account_suspended'
| 'account_banned'
| 'account_unbanned'
| 'appeal_resolved';
interface BaseNotifyParams {
userId: string;
reason: string;
moderatorId?: string;
}
interface PostNotifyParams extends BaseNotifyParams {
postId: string;
postTitle?: string;
}
interface AccountNotifyParams extends BaseNotifyParams {
expiresAt?: string;
}
interface AppealNotifyParams {
userId: string;
approved: boolean;
reviewNotes?: string;
}
interface NotifyResult {
success: boolean;
notificationId?: string;
emailSent?: boolean;
error?: string;
}
// ============================================
// HELPER FUNCTIONS
// ============================================
/**
* Get user details for notification
*/
async function getUserDetails(userId: string) {
const supabase = createAdminClient();
const { data, error } = await supabase
.from('user_profiles')
.select('email, display_name, username')
.eq('id', userId)
.single();
if (error) {
console.error('Error fetching user details:', error);
return null;
}
return {
email: data.email,
name: data.display_name || data.username || 'User',
};
}
/**
* Check if user wants moderation notifications
*/
async function shouldSendNotification(userId: string): Promise<{ inApp: boolean; email: boolean }> {
const supabase = createAdminClient();
const { data, error } = await supabase
.from('notification_preferences')
.select('in_app_enabled, email_enabled, notify_moderation_actions')
.eq('user_id', userId)
.single();
if (error || !data) {
// Default to sending notifications if preferences not found
return { inApp: true, email: true };
}
return {
inApp: data.in_app_enabled && (data.notify_moderation_actions ?? true),
email: data.email_enabled && (data.notify_moderation_actions ?? true),
};
}
/**
* Create in-app notification via database function
*/
async function createInAppNotification(
userId: string,
type: ModerationNotificationType,
title: string,
message: string,
postId?: string,
moderatorId?: string
): Promise<string | null> {
const supabase = createAdminClient();
const { data, error } = await supabase.rpc('create_moderation_notification', {
p_user_id: userId,
p_type: type,
p_title: title,
p_message: message,
p_related_post_id: postId || null,
p_moderator_id: moderatorId || null,
});
if (error) {
console.error('Error creating moderation notification:', error);
return null;
}
return data;
}
// ============================================
// POST MODERATION NOTIFICATIONS
// ============================================
/**
* Notify user that their post was deleted
*/
export async function notifyPostDeleted({
userId,
postId,
postTitle,
reason,
moderatorId,
}: PostNotifyParams): Promise<NotifyResult> {
try {
const [user, prefs] = await Promise.all([
getUserDetails(userId),
shouldSendNotification(userId),
]);
if (!user) {
return { success: false, error: 'User not found' };
}
let notificationId: string | null = null;
let emailSent = false;
// Create in-app notification
if (prefs.inApp) {
notificationId = await createInAppNotification(
userId,
'post_deleted',
'Post Removed',
`Your post "${postTitle || 'Untitled'}" has been removed: ${reason}`,
postId,
moderatorId
);
}
// Send email
if (prefs.email && user.email) {
const html = await render(
PostDeletedEmail({
userName: user.name,
postTitle,
postId,
reason,
appealUrl: `https://canadagpt.ca/appeal`,
})
);
const result = await sendDigestEmail({
to: user.email,
subject: 'Your post has been removed from CanadaGPT',
html,
});
emailSent = result.success;
}
return {
success: true,
notificationId: notificationId || undefined,
emailSent,
};
} catch (error) {
console.error('Error sending post deleted notification:', error);
return { success: false, error: String(error) };
}
}
/**
* Notify user that their post was locked
*/
export async function notifyPostLocked({
userId,
postId,
postTitle,
reason,
moderatorId,
}: PostNotifyParams): Promise<NotifyResult> {
try {
const [user, prefs] = await Promise.all([
getUserDetails(userId),
shouldSendNotification(userId),
]);
if (!user) {
return { success: false, error: 'User not found' };
}
let notificationId: string | null = null;
let emailSent = false;
// Create in-app notification
if (prefs.inApp) {
notificationId = await createInAppNotification(
userId,
'post_locked',
'Post Locked',
`Your post "${postTitle || 'Untitled'}" has been locked: ${reason}`,
postId,
moderatorId
);
}
// Send email
if (prefs.email && user.email) {
const html = await render(
PostLockedEmail({
userName: user.name,
postTitle,
postId,
reason,
})
);
const result = await sendDigestEmail({
to: user.email,
subject: 'Your post has been locked on CanadaGPT',
html,
});
emailSent = result.success;
}
return {
success: true,
notificationId: notificationId || undefined,
emailSent,
};
} catch (error) {
console.error('Error sending post locked notification:', error);
return { success: false, error: String(error) };
}
}
/**
* Notify user about a warning on their post
*/
export async function notifyPostWarning({
userId,
postId,
postTitle,
reason,
moderatorId,
}: PostNotifyParams): Promise<NotifyResult> {
try {
const [user, prefs] = await Promise.all([
getUserDetails(userId),
shouldSendNotification(userId),
]);
if (!user) {
return { success: false, error: 'User not found' };
}
let notificationId: string | null = null;
let emailSent = false;
// Create in-app notification
if (prefs.inApp) {
notificationId = await createInAppNotification(
userId,
'post_warning',
'Warning: Community Guidelines',
`Your post "${postTitle || 'Untitled'}" has been flagged: ${reason}`,
postId,
moderatorId
);
}
// Send email
if (prefs.email && user.email) {
const html = await render(
PostWarningEmail({
userName: user.name,
postTitle,
postId,
reason,
})
);
const result = await sendDigestEmail({
to: user.email,
subject: 'Warning regarding your post on CanadaGPT',
html,
});
emailSent = result.success;
}
return {
success: true,
notificationId: notificationId || undefined,
emailSent,
};
} catch (error) {
console.error('Error sending post warning notification:', error);
return { success: false, error: String(error) };
}
}
// ============================================
// ACCOUNT MODERATION NOTIFICATIONS
// ============================================
/**
* Notify user that their account was suspended (temporary)
*/
export async function notifyAccountSuspended({
userId,
reason,
expiresAt,
moderatorId,
}: AccountNotifyParams): Promise<NotifyResult> {
try {
const [user, prefs] = await Promise.all([
getUserDetails(userId),
shouldSendNotification(userId),
]);
if (!user) {
return { success: false, error: 'User not found' };
}
let notificationId: string | null = null;
let emailSent = false;
const expiresFormatted = expiresAt
? new Date(expiresAt).toLocaleDateString('en-CA', {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
})
: undefined;
// Create in-app notification
if (prefs.inApp) {
notificationId = await createInAppNotification(
userId,
'account_suspended',
'Account Suspended',
`Your account has been temporarily suspended${expiresFormatted ? ` until ${expiresFormatted}` : ''}: ${reason}`,
undefined,
moderatorId
);
}
// Send email
if (prefs.email && user.email) {
const html = await render(
AccountSuspendedEmail({
userName: user.name,
reason,
expiresAt: expiresFormatted,
appealUrl: `https://canadagpt.ca/appeal`,
})
);
const result = await sendDigestEmail({
to: user.email,
subject: 'Your CanadaGPT account has been suspended',
html,
});
emailSent = result.success;
}
return {
success: true,
notificationId: notificationId || undefined,
emailSent,
};
} catch (error) {
console.error('Error sending account suspended notification:', error);
return { success: false, error: String(error) };
}
}
/**
* Notify user that their account was permanently banned
*/
export async function notifyAccountBanned({
userId,
reason,
moderatorId,
}: AccountNotifyParams): Promise<NotifyResult> {
try {
const user = await getUserDetails(userId);
if (!user) {
return { success: false, error: 'User not found' };
}
// Always send ban notification regardless of preferences
let notificationId: string | null = null;
let emailSent = false;
// Create in-app notification
notificationId = await createInAppNotification(
userId,
'account_banned',
'Account Permanently Banned',
`Your account has been permanently banned: ${reason}`,
undefined,
moderatorId
);
// Send email (always send for bans)
if (user.email) {
const html = await render(
AccountBannedEmail({
userName: user.name,
reason,
appealUrl: `https://canadagpt.ca/appeal`,
})
);
const result = await sendDigestEmail({
to: user.email,
subject: 'Your CanadaGPT account has been permanently banned',
html,
});
emailSent = result.success;
}
return {
success: true,
notificationId: notificationId || undefined,
emailSent,
};
} catch (error) {
console.error('Error sending account banned notification:', error);
return { success: false, error: String(error) };
}
}
/**
* Notify user that their account was unbanned
*/
export async function notifyAccountUnbanned({
userId,
reason,
moderatorId,
}: AccountNotifyParams): Promise<NotifyResult> {
try {
const user = await getUserDetails(userId);
if (!user) {
return { success: false, error: 'User not found' };
}
// Always send unban notification
let notificationId: string | null = null;
let emailSent = false;
// Create in-app notification
notificationId = await createInAppNotification(
userId,
'account_unbanned',
'Account Reinstated',
`Your account has been reinstated: ${reason}`,
undefined,
moderatorId
);
// Send email (always send for unbans)
if (user.email) {
const html = await render(
AccountUnbannedEmail({
userName: user.name,
reason,
})
);
const result = await sendDigestEmail({
to: user.email,
subject: 'Your CanadaGPT account has been reinstated',
html,
});
emailSent = result.success;
}
return {
success: true,
notificationId: notificationId || undefined,
emailSent,
};
} catch (error) {
console.error('Error sending account unbanned notification:', error);
return { success: false, error: String(error) };
}
}
/**
* Notify user about their appeal decision
*/
export async function notifyAppealResolved({
userId,
approved,
reviewNotes,
}: AppealNotifyParams): Promise<NotifyResult> {
try {
const user = await getUserDetails(userId);
if (!user) {
return { success: false, error: 'User not found' };
}
// Always send appeal resolution notification
let notificationId: string | null = null;
let emailSent = false;
// Create in-app notification
notificationId = await createInAppNotification(
userId,
'appeal_resolved',
approved ? 'Appeal Approved' : 'Appeal Denied',
approved
? 'Your appeal has been approved. Your account has been reinstated.'
: 'Your appeal has been reviewed and denied. The original decision stands.',
undefined,
undefined
);
// Send email
if (user.email) {
const html = await render(
AppealResolvedEmail({
userName: user.name,
approved,
reviewNotes,
})
);
const result = await sendDigestEmail({
to: user.email,
subject: approved
? 'Your CanadaGPT appeal has been approved'
: 'Your CanadaGPT appeal has been reviewed',
html,
});
emailSent = result.success;
}
return {
success: true,
notificationId: notificationId || undefined,
emailSent,
};
} catch (error) {
console.error('Error sending appeal resolved notification:', error);
return { success: false, error: String(error) };
}
}