mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
refactor: ( notifications ) remove dead SendGrid/Twilio adapters and legacy code
This commit is contained in:
@@ -34,7 +34,6 @@
|
||||
"@nestjs/schedule": "^6.1.3",
|
||||
"@nestjs/swagger": "^7.4.0",
|
||||
"@prisma/client": "^6.19.3",
|
||||
"@sendgrid/mail": "^8.1.0",
|
||||
"axios": "^1.7.7",
|
||||
"bcrypt": "^5.1.1",
|
||||
"class-transformer": "^0.5.1",
|
||||
|
||||
@@ -1,199 +1,10 @@
|
||||
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);
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { HttpModule } from '@nestjs/axios';
|
||||
import { ClientsModule, Transport } from '@nestjs/microservices';
|
||||
import { NotificationsController } from './notifications.controller';
|
||||
import { NotificationsService } from './notifications.service';
|
||||
import { EmailAdapter, SmsAdapter, PushAdapter } from './notification.adapters';
|
||||
import { PushAdapter } from './notification.adapters';
|
||||
import { EmailClientService } from './email-client.service';
|
||||
import { SmsClientService } from './sms-client.service';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
HttpModule.register({ timeout: 10_000 }),
|
||||
ClientsModule.register([
|
||||
{
|
||||
name: 'EMAIL_SERVICE',
|
||||
@@ -34,8 +32,6 @@ import { SmsClientService } from './sms-client.service';
|
||||
controllers: [NotificationsController],
|
||||
providers: [
|
||||
NotificationsService,
|
||||
EmailAdapter,
|
||||
SmsAdapter,
|
||||
PushAdapter,
|
||||
EmailClientService,
|
||||
SmsClientService,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { OnEvent } from '@nestjs/event-emitter';
|
||||
import { PrismaService } from '../../common/prisma.service';
|
||||
import { SendNotificationDto, NotificationCategoryEnum } from './notifications.dto';
|
||||
import { PushAdapter, NotificationChannel } from './notification.adapters';
|
||||
import { EmailClientService } from './email-client.service';
|
||||
import { SmsClientService } from './sms-client.service';
|
||||
@@ -83,37 +82,6 @@ export class NotificationsService {
|
||||
return { sent: sentChannels.length > 0, channels: sentChannels };
|
||||
}
|
||||
|
||||
/**
|
||||
* Legacy method for backward compatibility
|
||||
*/
|
||||
async sendDirect(dto: SendNotificationDto) {
|
||||
const notification = await this.prisma.notification.create({
|
||||
data: {
|
||||
passengerId: dto.passengerId,
|
||||
title: dto.title,
|
||||
body: dto.body,
|
||||
category: dto.category as any,
|
||||
deepLink: dto.deepLink,
|
||||
metadata: dto.metadata,
|
||||
},
|
||||
});
|
||||
|
||||
const passenger = await this.prisma.passenger.findUnique({
|
||||
where: { id: dto.passengerId },
|
||||
include: { user: true },
|
||||
});
|
||||
|
||||
if (passenger?.user) {
|
||||
await this.emailClient.sendEmail({
|
||||
to: passenger.user.email,
|
||||
subject: this.sanitize(dto.title),
|
||||
text: this.sanitize(dto.body),
|
||||
});
|
||||
}
|
||||
|
||||
return notification;
|
||||
}
|
||||
|
||||
private async createInAppNotification(
|
||||
recipient: string,
|
||||
title: string,
|
||||
@@ -221,12 +189,6 @@ export class NotificationsService {
|
||||
}
|
||||
}
|
||||
|
||||
private sanitize(value: string): string {
|
||||
return value
|
||||
.replace(/[\r\n]/g, ' ')
|
||||
.replace(/[<>&"']/g, (c) => ({ '<': '<', '>': '>', '&': '&', '"': '"', "'": ''' }[c] ?? c));
|
||||
}
|
||||
|
||||
getForPassenger(passengerId: string) {
|
||||
return this.prisma.notification.findMany({
|
||||
where: { passengerId },
|
||||
|
||||
35
pnpm-lock.yaml
generated
35
pnpm-lock.yaml
generated
@@ -452,9 +452,6 @@ importers:
|
||||
'@prisma/client':
|
||||
specifier: ^6.19.3
|
||||
version: 6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3)
|
||||
'@sendgrid/mail':
|
||||
specifier: ^8.1.0
|
||||
version: 8.1.6
|
||||
axios:
|
||||
specifier: ^1.7.7
|
||||
version: 1.17.0
|
||||
@@ -3756,18 +3753,6 @@ packages:
|
||||
'@sec-ant/readable-stream@0.4.1':
|
||||
resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==}
|
||||
|
||||
'@sendgrid/client@8.1.6':
|
||||
resolution: {integrity: sha512-/BHu0hqwXNHr2aLhcXU7RmmlVqrdfrbY9KpaNj00KZHlVOVoRxRVrpOCabIB+91ISXJ6+mLM9vpaVUhK6TwBWA==}
|
||||
engines: {node: '>=12.*'}
|
||||
|
||||
'@sendgrid/helpers@8.0.0':
|
||||
resolution: {integrity: sha512-Ze7WuW2Xzy5GT5WRx+yEv89fsg/pgy3T1E3FS0QEx0/VvRmigMZ5qyVGhJz4SxomegDkzXv/i0aFPpHKN8qdAA==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
|
||||
'@sendgrid/mail@8.1.6':
|
||||
resolution: {integrity: sha512-/ZqxUvKeEztU9drOoPC/8opEPOk+jLlB2q4+xpx6HVLq6aFu3pMpalkTpAQz8XfRfpLp8O25bh6pGPcHDCYpqg==}
|
||||
engines: {node: '>=12.*'}
|
||||
|
||||
'@sinclair/typebox@0.27.10':
|
||||
resolution: {integrity: sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==}
|
||||
|
||||
@@ -16642,26 +16627,6 @@ snapshots:
|
||||
|
||||
'@sec-ant/readable-stream@0.4.1': {}
|
||||
|
||||
'@sendgrid/client@8.1.6':
|
||||
dependencies:
|
||||
'@sendgrid/helpers': 8.0.0
|
||||
axios: 1.17.0
|
||||
transitivePeerDependencies:
|
||||
- debug
|
||||
- supports-color
|
||||
|
||||
'@sendgrid/helpers@8.0.0':
|
||||
dependencies:
|
||||
deepmerge: 4.3.1
|
||||
|
||||
'@sendgrid/mail@8.1.6':
|
||||
dependencies:
|
||||
'@sendgrid/client': 8.1.6
|
||||
'@sendgrid/helpers': 8.0.0
|
||||
transitivePeerDependencies:
|
||||
- debug
|
||||
- supports-color
|
||||
|
||||
'@sinclair/typebox@0.27.10': {}
|
||||
|
||||
'@sindresorhus/merge-streams@4.0.0': {}
|
||||
|
||||
Reference in New Issue
Block a user