mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 08:48:11 +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/schedule": "^6.1.3",
|
||||||
"@nestjs/swagger": "^7.4.0",
|
"@nestjs/swagger": "^7.4.0",
|
||||||
"@prisma/client": "^6.19.3",
|
"@prisma/client": "^6.19.3",
|
||||||
"@sendgrid/mail": "^8.1.0",
|
|
||||||
"axios": "^1.7.7",
|
"axios": "^1.7.7",
|
||||||
"bcrypt": "^5.1.1",
|
"bcrypt": "^5.1.1",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
|
|||||||
@@ -1,199 +1,10 @@
|
|||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import * as sgMail from '@sendgrid/mail';
|
|
||||||
import { HttpService } from '@nestjs/axios';
|
|
||||||
import { firstValueFrom } from 'rxjs';
|
|
||||||
|
|
||||||
export interface NotificationChannel {
|
export interface NotificationChannel {
|
||||||
send(recipient: string, subject: string, body: string, context?: Record<string, unknown>): Promise<boolean>;
|
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()
|
@Injectable()
|
||||||
export class PushAdapter implements NotificationChannel {
|
export class PushAdapter implements NotificationChannel {
|
||||||
private readonly logger = new Logger(PushAdapter.name);
|
private readonly logger = new Logger(PushAdapter.name);
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { HttpModule } from '@nestjs/axios';
|
|
||||||
import { ClientsModule, Transport } from '@nestjs/microservices';
|
import { ClientsModule, Transport } from '@nestjs/microservices';
|
||||||
import { NotificationsController } from './notifications.controller';
|
import { NotificationsController } from './notifications.controller';
|
||||||
import { NotificationsService } from './notifications.service';
|
import { NotificationsService } from './notifications.service';
|
||||||
import { EmailAdapter, SmsAdapter, PushAdapter } from './notification.adapters';
|
import { PushAdapter } from './notification.adapters';
|
||||||
import { EmailClientService } from './email-client.service';
|
import { EmailClientService } from './email-client.service';
|
||||||
import { SmsClientService } from './sms-client.service';
|
import { SmsClientService } from './sms-client.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
HttpModule.register({ timeout: 10_000 }),
|
|
||||||
ClientsModule.register([
|
ClientsModule.register([
|
||||||
{
|
{
|
||||||
name: 'EMAIL_SERVICE',
|
name: 'EMAIL_SERVICE',
|
||||||
@@ -34,8 +32,6 @@ import { SmsClientService } from './sms-client.service';
|
|||||||
controllers: [NotificationsController],
|
controllers: [NotificationsController],
|
||||||
providers: [
|
providers: [
|
||||||
NotificationsService,
|
NotificationsService,
|
||||||
EmailAdapter,
|
|
||||||
SmsAdapter,
|
|
||||||
PushAdapter,
|
PushAdapter,
|
||||||
EmailClientService,
|
EmailClientService,
|
||||||
SmsClientService,
|
SmsClientService,
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
import { OnEvent } from '@nestjs/event-emitter';
|
import { OnEvent } from '@nestjs/event-emitter';
|
||||||
import { PrismaService } from '../../common/prisma.service';
|
import { PrismaService } from '../../common/prisma.service';
|
||||||
import { SendNotificationDto, NotificationCategoryEnum } from './notifications.dto';
|
|
||||||
import { PushAdapter, NotificationChannel } from './notification.adapters';
|
import { PushAdapter, NotificationChannel } from './notification.adapters';
|
||||||
import { EmailClientService } from './email-client.service';
|
import { EmailClientService } from './email-client.service';
|
||||||
import { SmsClientService } from './sms-client.service';
|
import { SmsClientService } from './sms-client.service';
|
||||||
@@ -83,37 +82,6 @@ export class NotificationsService {
|
|||||||
return { sent: sentChannels.length > 0, channels: sentChannels };
|
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(
|
private async createInAppNotification(
|
||||||
recipient: string,
|
recipient: string,
|
||||||
title: 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) {
|
getForPassenger(passengerId: string) {
|
||||||
return this.prisma.notification.findMany({
|
return this.prisma.notification.findMany({
|
||||||
where: { passengerId },
|
where: { passengerId },
|
||||||
|
|||||||
35
pnpm-lock.yaml
generated
35
pnpm-lock.yaml
generated
@@ -452,9 +452,6 @@ importers:
|
|||||||
'@prisma/client':
|
'@prisma/client':
|
||||||
specifier: ^6.19.3
|
specifier: ^6.19.3
|
||||||
version: 6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.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:
|
axios:
|
||||||
specifier: ^1.7.7
|
specifier: ^1.7.7
|
||||||
version: 1.17.0
|
version: 1.17.0
|
||||||
@@ -3756,18 +3753,6 @@ packages:
|
|||||||
'@sec-ant/readable-stream@0.4.1':
|
'@sec-ant/readable-stream@0.4.1':
|
||||||
resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==}
|
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':
|
'@sinclair/typebox@0.27.10':
|
||||||
resolution: {integrity: sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==}
|
resolution: {integrity: sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==}
|
||||||
|
|
||||||
@@ -16642,26 +16627,6 @@ snapshots:
|
|||||||
|
|
||||||
'@sec-ant/readable-stream@0.4.1': {}
|
'@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': {}
|
'@sinclair/typebox@0.27.10': {}
|
||||||
|
|
||||||
'@sindresorhus/merge-streams@4.0.0': {}
|
'@sindresorhus/merge-streams@4.0.0': {}
|
||||||
|
|||||||
Reference in New Issue
Block a user