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:
2025-11-17 00:31:23 +00:00
parent 052ba46bdc
commit d61c21c54d
21 changed files with 1716 additions and 3 deletions

View 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();