mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 09:30:59 +00:00
217 lines
6.6 KiB
TypeScript
217 lines
6.6 KiB
TypeScript
import { Injectable, Logger } from '@nestjs/common';
|
|
import { ConfigService } from '@nestjs/config';
|
|
import * as sgMail from '@sendgrid/mail';
|
|
import { HttpService } from '@nestjs/axios';
|
|
import { firstValueFrom } from 'rxjs';
|
|
|
|
export interface NotificationChannel {
|
|
send(recipient: string, subject: string, body: string, context?: Record<string, unknown>): Promise<boolean>;
|
|
}
|
|
|
|
@Injectable()
|
|
export class EmailAdapter implements NotificationChannel {
|
|
private readonly logger = new Logger(EmailAdapter.name);
|
|
|
|
constructor(private readonly config: ConfigService) {
|
|
const apiKey = this.config.get<string>('SENDGRID_API_KEY');
|
|
if (apiKey) {
|
|
sgMail.setApiKey(apiKey);
|
|
this.logger.log('SendGrid Email adapter initialized');
|
|
} else {
|
|
this.logger.warn('SENDGRID_API_KEY not configured - emails will be logged only');
|
|
}
|
|
}
|
|
|
|
async send(
|
|
recipient: string,
|
|
subject: string,
|
|
body: string,
|
|
context?: Record<string, unknown>,
|
|
): Promise<boolean> {
|
|
const apiKey = this.config.get<string>('SENDGRID_API_KEY');
|
|
const fromEmail = this.config.get<string>('SENDGRID_FROM_EMAIL') || 'noreply@edr-platform.com';
|
|
|
|
if (!apiKey) {
|
|
this.logger.log(`[EMAIL MOCK] To: ${recipient} | Subject: ${subject} | Body: ${body.substring(0, 100)}`);
|
|
return true;
|
|
}
|
|
|
|
try {
|
|
const msg: sgMail.MailDataRequired = {
|
|
to: recipient,
|
|
from: fromEmail,
|
|
subject,
|
|
text: body,
|
|
html: this.formatHtml(body, context),
|
|
};
|
|
|
|
await sgMail.send(msg);
|
|
this.logger.log(`Email sent successfully to ${recipient}`);
|
|
return true;
|
|
} catch (err) {
|
|
const message = err instanceof Error ? err.message : String(err);
|
|
this.logger.error(`Failed to send email to ${recipient}: ${message}`);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private formatHtml(body: string, context?: Record<string, unknown>): string {
|
|
const contextHtml = context
|
|
? `<div style="margin-top: 20px; padding: 10px; background: #f5f5f5; border-radius: 4px;">
|
|
<small>${JSON.stringify(context, null, 2)}</small>
|
|
</div>`
|
|
: '';
|
|
|
|
return `
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<style>
|
|
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
|
|
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
|
|
.header { background: #0066cc; color: white; padding: 20px; text-align: center; }
|
|
.content { padding: 20px; background: white; }
|
|
.footer { text-align: center; padding: 20px; color: #666; font-size: 12px; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<div class="header">
|
|
<h2>Ethio-Djibouti Railway</h2>
|
|
</div>
|
|
<div class="content">
|
|
${body.replace(/\n/g, '<br>')}
|
|
${contextHtml}
|
|
</div>
|
|
<div class="footer">
|
|
<p>© 2024 Ethio-Djibouti Railway. All rights reserved.</p>
|
|
</div>
|
|
</div>
|
|
</body>
|
|
</html>
|
|
`;
|
|
}
|
|
}
|
|
|
|
@Injectable()
|
|
export class SmsAdapter implements NotificationChannel {
|
|
private readonly logger = new Logger(SmsAdapter.name);
|
|
|
|
constructor(
|
|
private readonly config: ConfigService,
|
|
private readonly http: HttpService,
|
|
) {
|
|
const provider = this.config.get<string>('SMS_PROVIDER');
|
|
this.logger.log(`SMS adapter initialized with provider: ${provider || 'MOCK'}`);
|
|
}
|
|
|
|
async send(
|
|
recipient: string,
|
|
subject: string,
|
|
body: string,
|
|
_context?: Record<string, unknown>,
|
|
): Promise<boolean> {
|
|
const provider = this.config.get<string>('SMS_PROVIDER');
|
|
const apiKey = this.config.get<string>('SMS_API_KEY');
|
|
|
|
if (!provider || !apiKey) {
|
|
this.logger.log(`[SMS MOCK] To: ${recipient} | Message: ${body.substring(0, 100)}`);
|
|
return true;
|
|
}
|
|
|
|
try {
|
|
switch (provider.toLowerCase()) {
|
|
case 'twilio':
|
|
return await this.sendViaTwilio(recipient, body);
|
|
case 'africastalking':
|
|
return await this.sendViaAfricasTalking(recipient, body);
|
|
default:
|
|
this.logger.warn(`Unknown SMS provider: ${provider}`);
|
|
return false;
|
|
}
|
|
} catch (err) {
|
|
const message = err instanceof Error ? err.message : String(err);
|
|
this.logger.error(`Failed to send SMS to ${recipient}: ${message}`);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private async sendViaTwilio(to: string, body: string): Promise<boolean> {
|
|
const accountSid = this.config.get<string>('TWILIO_ACCOUNT_SID');
|
|
const authToken = this.config.get<string>('TWILIO_AUTH_TOKEN');
|
|
const fromNumber = this.config.get<string>('TWILIO_FROM_NUMBER');
|
|
|
|
const url = `https://api.twilio.com/2010-04-01/Accounts/${accountSid}/Messages.json`;
|
|
const auth = Buffer.from(`${accountSid}:${authToken}`).toString('base64');
|
|
|
|
const response = await firstValueFrom(
|
|
this.http.post(
|
|
url,
|
|
new URLSearchParams({
|
|
To: to,
|
|
From: fromNumber || '',
|
|
Body: body,
|
|
}),
|
|
{
|
|
headers: {
|
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
'Authorization': `Basic ${auth}`,
|
|
},
|
|
},
|
|
),
|
|
);
|
|
|
|
return response.status === 201;
|
|
}
|
|
|
|
private async sendViaAfricasTalking(to: string, body: string): Promise<boolean> {
|
|
const apiKey = this.config.get<string>('SMS_API_KEY');
|
|
const username = this.config.get<string>('AFRICASTALKING_USERNAME');
|
|
const from = this.config.get<string>('AFRICASTALKING_FROM');
|
|
|
|
const url = 'https://api.africastalking.com/version1/messaging';
|
|
|
|
const response = await firstValueFrom(
|
|
this.http.post(
|
|
url,
|
|
new URLSearchParams({
|
|
username: username || '',
|
|
to,
|
|
message: body,
|
|
from: from || '',
|
|
}),
|
|
{
|
|
headers: {
|
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
'apiKey': apiKey || '',
|
|
},
|
|
},
|
|
),
|
|
);
|
|
|
|
return response.status === 201;
|
|
}
|
|
}
|
|
|
|
@Injectable()
|
|
export class PushAdapter implements NotificationChannel {
|
|
private readonly logger = new Logger(PushAdapter.name);
|
|
|
|
constructor(private readonly config: ConfigService) {
|
|
this.logger.log('Push notification adapter initialized');
|
|
}
|
|
|
|
async send(
|
|
recipient: string,
|
|
subject: string,
|
|
body: string,
|
|
context?: Record<string, unknown>,
|
|
): Promise<boolean> {
|
|
// Push notifications would typically use FCM/APNS
|
|
// For now, just log
|
|
this.logger.log(`[PUSH MOCK] To: ${recipient} | Title: ${subject} | Body: ${body.substring(0, 100)}`);
|
|
return true;
|
|
}
|
|
}
|