feat: implement notification system with Mailgun email integration
- Add notifications table with migration - Create NotificationService with area-of-interest matching - Integrate Mailgun for email delivery - Add notification API endpoints (get, mark as read, unread count) - Create scheduled jobs for event reminders and cleanup - Build NotificationBell component for frontend - Add notification triggers for reports and events - Implement comprehensive test script - Update Header with notification bell - Add email templates with HTML formatting Requirements satisfied: - 10.1: Users can define areas of interest (1-50km radius) - 10.2: Notifications sent within 10 minutes - 10.3: Notifications for new reports/events in user area - 10.4: Configurable notification preferences (email/in-app) Task 33 completed
This commit is contained in:
170
server/src/services/emailService.ts
Normal file
170
server/src/services/emailService.ts
Normal file
@@ -0,0 +1,170 @@
|
||||
import Mailgun from 'mailgun.js';
|
||||
import formData from 'form-data';
|
||||
|
||||
const mailgun = new Mailgun(formData);
|
||||
|
||||
// Initialize Mailgun client
|
||||
const mg = mailgun.client({
|
||||
username: 'api',
|
||||
key: process.env.MAILGUN_API_KEY || 'dd27be7a81dd7b0980ada31fec9a9bd9-e80d8b76-fd80c63b',
|
||||
});
|
||||
|
||||
const MAILGUN_DOMAIN = process.env.MAILGUN_DOMAIN || 'sandbox-123.mailgun.org';
|
||||
const FROM_EMAIL = process.env.FROM_EMAIL || 'Beach Litter Platform <noreply@beachlitter.ie>';
|
||||
|
||||
interface EmailData {
|
||||
to: string;
|
||||
subject: string;
|
||||
text: string;
|
||||
html?: string;
|
||||
}
|
||||
|
||||
export class EmailService {
|
||||
/**
|
||||
* Send an email using Mailgun
|
||||
*/
|
||||
async sendEmail(data: EmailData): Promise<boolean> {
|
||||
try {
|
||||
const messageData = {
|
||||
from: FROM_EMAIL,
|
||||
to: data.to,
|
||||
subject: data.subject,
|
||||
text: data.text,
|
||||
html: data.html || this.generateHtmlFromText(data.text),
|
||||
};
|
||||
|
||||
const response = await mg.messages.create(MAILGUN_DOMAIN, messageData);
|
||||
|
||||
console.log(`[Email] Successfully sent to ${data.to}:`, response.id);
|
||||
return true;
|
||||
} catch (error: any) {
|
||||
console.error(`[Email] Failed to send to ${data.to}:`, error.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send notification email
|
||||
*/
|
||||
async sendNotificationEmail(
|
||||
to: string,
|
||||
firstName: string | null,
|
||||
title: string,
|
||||
message: string,
|
||||
relatedType?: 'report' | 'event',
|
||||
relatedId?: string
|
||||
): Promise<boolean> {
|
||||
const greeting = firstName ? `Hi ${firstName}` : 'Hi';
|
||||
|
||||
const text = `
|
||||
${greeting},
|
||||
|
||||
${title}
|
||||
|
||||
${message}
|
||||
|
||||
${relatedType && relatedId ? `View details: ${this.getDetailsUrl(relatedType, relatedId)}` : ''}
|
||||
|
||||
---
|
||||
Beach Litter Mapping Platform
|
||||
To manage your notification preferences, visit your profile settings.
|
||||
`.trim();
|
||||
|
||||
const html = `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
|
||||
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
|
||||
.header { background-color: #0ea5e9; color: white; padding: 20px; text-align: center; }
|
||||
.content { padding: 20px; background-color: #f9fafb; }
|
||||
.message { background-color: white; padding: 15px; border-radius: 5px; margin: 15px 0; }
|
||||
.button { display: inline-block; padding: 12px 24px; background-color: #0ea5e9; color: white; text-decoration: none; border-radius: 5px; margin: 15px 0; }
|
||||
.footer { text-align: center; padding: 20px; font-size: 12px; color: #666; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>🏖️ Beach Litter Platform</h1>
|
||||
</div>
|
||||
<div class="content">
|
||||
<p>${greeting},</p>
|
||||
<div class="message">
|
||||
<h2>${title}</h2>
|
||||
<p>${message}</p>
|
||||
</div>
|
||||
${relatedType && relatedId ? `
|
||||
<a href="${this.getDetailsUrl(relatedType, relatedId)}" class="button">View Details</a>
|
||||
` : ''}
|
||||
</div>
|
||||
<div class="footer">
|
||||
<p>Beach Litter Mapping Platform</p>
|
||||
<p>To manage your notification preferences, visit your profile settings.</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`.trim();
|
||||
|
||||
return this.sendEmail({
|
||||
to,
|
||||
subject: title,
|
||||
text,
|
||||
html,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate basic HTML from plain text
|
||||
*/
|
||||
private generateHtmlFromText(text: string): string {
|
||||
return `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
|
||||
<div style="max-width: 600px; margin: 0 auto; padding: 20px;">
|
||||
${text.split('\n').map(line => `<p>${line}</p>`).join('')}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get URL for viewing details
|
||||
*/
|
||||
private getDetailsUrl(type: 'report' | 'event', id: string): string {
|
||||
const baseUrl = process.env.FRONTEND_URL || 'http://localhost:5173';
|
||||
|
||||
if (type === 'report') {
|
||||
return `${baseUrl}/map?reportId=${id}`;
|
||||
} else {
|
||||
return `${baseUrl}/events?eventId=${id}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test email configuration
|
||||
*/
|
||||
async testConnection(): Promise<boolean> {
|
||||
try {
|
||||
// Send a test email to verify configuration
|
||||
const testEmail = process.env.TEST_EMAIL || 'test@example.com';
|
||||
|
||||
await this.sendEmail({
|
||||
to: testEmail,
|
||||
subject: 'Beach Litter Platform - Email Test',
|
||||
text: 'This is a test email from the Beach Litter Mapping Platform. If you received this, email notifications are working correctly!',
|
||||
});
|
||||
|
||||
return true;
|
||||
} catch (error: any) {
|
||||
console.error('[Email] Connection test failed:', error.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const emailService = new EmailService();
|
||||
Reference in New Issue
Block a user