diff --git a/package-lock.json b/package-lock.json index def974b..2080cb9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -40,6 +40,7 @@ "moment": "^2.30.1", "nestjs-i18n": "^10.4.9", "nestjs-pino": "^4.1.0", + "nestjs-twilio": "^4.4.0", "nodemailer": "^6.9.16", "oci-common": "^2.99.0", "oci-sdk": "^2.99.0", @@ -3808,7 +3809,6 @@ "node_modules/axios": { "version": "1.7.7", "license": "MIT", - "peer": true, "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", @@ -6350,7 +6350,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=4.0" }, @@ -10036,6 +10035,19 @@ "pino-http": "^6.4.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0" } }, + "node_modules/nestjs-twilio": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/nestjs-twilio/-/nestjs-twilio-4.4.0.tgz", + "integrity": "sha512-TtT+mgVaIsiGNX1J8zkjVhIBxJPsChfU8gfu6dbPyEtde9ewgb5sxhAreOE6STT5U95OiSAlFcqKoqlARCIFxA==", + "license": "MIT", + "dependencies": { + "twilio": "^5.0.2" + }, + "peerDependencies": { + "@nestjs/common": ">=9.0.0", + "@nestjs/core": ">=9.0.0" + } + }, "node_modules/nice-try": { "version": "1.0.5", "license": "MIT", @@ -15120,8 +15132,7 @@ }, "node_modules/proxy-from-env": { "version": "1.1.0", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/pug": { "version": "3.0.3", @@ -16020,6 +16031,12 @@ "dev": true, "license": "MIT" }, + "node_modules/scmp": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/scmp/-/scmp-2.1.0.tgz", + "integrity": "sha512-o/mRQGk9Rcer/jEEw/yw4mwo3EU/NvYvp577/Btqrym9Qy5/MdWGBqipbALgd2lrdWTJ5/gqDusxfnQBxOxT2Q==", + "license": "BSD-3-Clause" + }, "node_modules/secure-json-parse": { "version": "2.7.0", "license": "BSD-3-Clause" @@ -17032,6 +17049,46 @@ "version": "0.14.5", "license": "Unlicense" }, + "node_modules/twilio": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/twilio/-/twilio-5.4.0.tgz", + "integrity": "sha512-kEmxzdOLTzXzUEXIkBVwT1Itxlbp+rtGrQogNfPtSE3EjoEsxrxB/9tdMIEbrsioL8CzTk/+fiKNJekAyHxjuQ==", + "license": "MIT", + "dependencies": { + "axios": "^1.7.4", + "dayjs": "^1.11.9", + "https-proxy-agent": "^5.0.0", + "jsonwebtoken": "^9.0.2", + "qs": "^6.9.4", + "scmp": "^2.1.0", + "xmlbuilder": "^13.0.2" + }, + "engines": { + "node": ">=14.0" + } + }, + "node_modules/twilio/node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "license": "MIT", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, "node_modules/type-check": { "version": "0.4.0", "dev": true, @@ -17800,6 +17857,15 @@ "dev": true, "license": "ISC" }, + "node_modules/xmlbuilder": { + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-13.0.2.tgz", + "integrity": "sha512-Eux0i2QdDYKbdbA6AM6xE4m6ZTZr4G4xF9kahI2ukSEMCzwce2eX9WlTI5J3s+NU7hpasFsr8hWIONae7LluAQ==", + "license": "MIT", + "engines": { + "node": ">=6.0" + } + }, "node_modules/xtend": { "version": "4.0.2", "license": "MIT", diff --git a/package.json b/package.json index 69fc180..636ba95 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "moment": "^2.30.1", "nestjs-i18n": "^10.4.9", "nestjs-pino": "^4.1.0", + "nestjs-twilio": "^4.4.0", "nodemailer": "^6.9.16", "oci-common": "^2.99.0", "oci-sdk": "^2.99.0", diff --git a/src/app.module.ts b/src/app.module.ts index c042142..1131163 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -26,6 +26,7 @@ import { JuniorModule } from './junior/junior.module'; import { MoneyRequestModule } from './money-request/money-request.module'; import { SavingGoalsModule } from './saving-goals/saving-goals.module'; import { TaskModule } from './task/task.module'; + @Module({ controllers: [], imports: [ diff --git a/src/common/modules/notification/notification.module.ts b/src/common/modules/notification/notification.module.ts index 5494cfa..4b1f1ab 100644 --- a/src/common/modules/notification/notification.module.ts +++ b/src/common/modules/notification/notification.module.ts @@ -1,15 +1,25 @@ import { Module } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; import { TypeOrmModule } from '@nestjs/typeorm'; +import { TwilioModule } from 'nestjs-twilio'; import { AuthModule } from '~/auth/auth.module'; +import { buildTwilioOptions } from '~/core/module-options'; import { NotificationsController } from './controllers'; import { Notification } from './entities'; import { NotificationsRepository } from './repositories'; import { FirebaseService } from './services/firebase.service'; import { NotificationsService } from './services/notifications.service'; - +import { TwilioService } from './services/twilio.service'; @Module({ - imports: [TypeOrmModule.forFeature([Notification]), AuthModule], - providers: [NotificationsService, FirebaseService, NotificationsRepository], + imports: [ + TypeOrmModule.forFeature([Notification]), + AuthModule, + TwilioModule.forRootAsync({ + useFactory: buildTwilioOptions, + inject: [ConfigService], + }), + ], + providers: [NotificationsService, FirebaseService, NotificationsRepository, TwilioService], exports: [NotificationsService], controllers: [NotificationsController], }) diff --git a/src/common/modules/notification/services/notifications.service.ts b/src/common/modules/notification/services/notifications.service.ts index fd39f4a..46f8880 100644 --- a/src/common/modules/notification/services/notifications.service.ts +++ b/src/common/modules/notification/services/notifications.service.ts @@ -4,6 +4,7 @@ import { PageOptionsRequestDto } from '~/core/dtos'; import { Notification } from '../entities'; import { NotificationsRepository } from '../repositories'; import { FirebaseService } from './firebase.service'; +import { TwilioService } from './twilio.service'; @Injectable() export class NotificationsService { @@ -11,6 +12,7 @@ export class NotificationsService { private readonly deviceService: DeviceService, private readonly firebaseService: FirebaseService, private readonly notificationRepository: NotificationsRepository, + private readonly twilioService: TwilioService, ) {} async sendPushNotification(userId: string, title: string, body: string) { @@ -25,6 +27,10 @@ export class NotificationsService { return this.firebaseService.sendNotification(tokens, title, body); } + async sendSMS(to: string, body: string) { + await this.twilioService.sendSMS(to, body); + } + async getNotifications(userId: string, pageOptionsDto: PageOptionsRequestDto) { const [[notifications, count], unreadCount] = await Promise.all([ this.notificationRepository.getNotifications(userId, pageOptionsDto), diff --git a/src/common/modules/notification/services/twilio.service.ts b/src/common/modules/notification/services/twilio.service.ts new file mode 100644 index 0000000..53c5d5d --- /dev/null +++ b/src/common/modules/notification/services/twilio.service.ts @@ -0,0 +1,21 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { TwilioService as TwilioApiService } from 'nestjs-twilio'; +import { Environment } from '~/core/enums'; +@Injectable() +export class TwilioService { + private logger = new Logger(TwilioService.name); + constructor(private readonly twilioService: TwilioApiService, private readonly configService: ConfigService) {} + private from = this.configService.getOrThrow('TWILIO_PHONE_NUMBER'); + sendSMS(to: string, body: string) { + if (this.configService.get('NODE_ENV') === Environment.DEV) { + this.logger.log(`Skipping SMS sending in DEV environment. Message: ${body} to: ${to}`); + return; + } + return this.twilioService.client.messages.create({ + body, + to, + from: this.from, + }); + } +} diff --git a/src/core/module-options/index.ts b/src/core/module-options/index.ts index 5d98421..a9d66eb 100644 --- a/src/core/module-options/index.ts +++ b/src/core/module-options/index.ts @@ -1,4 +1,5 @@ export * from '././keyv-options'; export * from './config-options'; export * from './logger-options'; +export * from './twilio-options'; export * from './typeorm-options'; diff --git a/src/core/module-options/twilio-options.ts b/src/core/module-options/twilio-options.ts new file mode 100644 index 0000000..85e5062 --- /dev/null +++ b/src/core/module-options/twilio-options.ts @@ -0,0 +1,9 @@ +import { ConfigService } from '@nestjs/config'; +import { TwilioModuleOptions } from 'nestjs-twilio'; + +export function buildTwilioOptions(config: ConfigService): TwilioModuleOptions { + return { + accountSid: config.get('TWILIO_ACCOUNT_SID'), + authToken: config.get('TWILIO_AUTH_TOKEN'), + }; +}