From 3ab0f179d8ffea85e5ab986e4d024f60795bceb4 Mon Sep 17 00:00:00 2001 From: Abdalhamid Alhamad Date: Mon, 6 Jan 2025 16:46:35 +0300 Subject: [PATCH] feat: add smtp and fix dynamic link --- nest-cli.json | 2 ++ .../controllers/notifications.controller.ts | 10 +++++- .../notification/dtos/request/index.ts | 1 + .../dtos/request/send-email.request.dto.ts | 20 ++++++++++++ .../notification/notification.module.ts | 12 ++++--- .../modules/notification/services/index.ts | 3 ++ .../services/notifications.service.ts | 32 ++++++++++++++++++- .../modules/notification/templates/otp.hbs | 21 ++++++++++++ src/core/module-options/index.ts | 1 + src/core/module-options/mailer-options.ts | 24 ++++++++++++++ src/junior/services/branch-io.service.ts | 18 ++--------- 11 files changed, 123 insertions(+), 21 deletions(-) create mode 100644 src/common/modules/notification/dtos/request/index.ts create mode 100644 src/common/modules/notification/dtos/request/send-email.request.dto.ts create mode 100644 src/common/modules/notification/templates/otp.hbs create mode 100644 src/core/module-options/mailer-options.ts diff --git a/nest-cli.json b/nest-cli.json index 8c5c0eb..ee012c5 100644 --- a/nest-cli.json +++ b/nest-cli.json @@ -10,6 +10,8 @@ "include": "config", "exclude": "**/*.md" }, + { "include": "common/modules/**/templates/*", "watchAssets": true } +, "i18n", "files" ] diff --git a/src/common/modules/notification/controllers/notifications.controller.ts b/src/common/modules/notification/controllers/notifications.controller.ts index a862315..8c9ce92 100644 --- a/src/common/modules/notification/controllers/notifications.controller.ts +++ b/src/common/modules/notification/controllers/notifications.controller.ts @@ -1,9 +1,10 @@ -import { Controller, Get, HttpCode, HttpStatus, Post, Query, UseGuards } from '@nestjs/common'; +import { Body, Controller, Get, HttpCode, HttpStatus, Post, Query, UseGuards } from '@nestjs/common'; import { ApiBearerAuth, ApiResponse, ApiTags } from '@nestjs/swagger'; import { IJwtPayload } from '~/auth/interfaces'; import { AuthenticatedUser } from '~/common/decorators'; import { AccessTokenGuard } from '~/common/guards'; import { PageOptionsRequestDto } from '~/core/dtos'; +import { SendEmailRequestDto } from '../dtos/request'; import { NotificationsPageResponseDto } from '../dtos/response'; import { NotificationsService } from '../services/notifications.service'; @@ -33,4 +34,11 @@ export class NotificationsController { markAsRead(@AuthenticatedUser() { sub }: IJwtPayload) { return this.notificationsService.markAsRead(sub); } + + @Post('email') + @UseGuards(AccessTokenGuard) + @HttpCode(HttpStatus.NO_CONTENT) + sendEmail(@AuthenticatedUser() { sub }: IJwtPayload, @Body() body: SendEmailRequestDto) { + return this.notificationsService.sendEmail(body); + } } diff --git a/src/common/modules/notification/dtos/request/index.ts b/src/common/modules/notification/dtos/request/index.ts new file mode 100644 index 0000000..506e234 --- /dev/null +++ b/src/common/modules/notification/dtos/request/index.ts @@ -0,0 +1 @@ +export * from './send-email.request.dto'; diff --git a/src/common/modules/notification/dtos/request/send-email.request.dto.ts b/src/common/modules/notification/dtos/request/send-email.request.dto.ts new file mode 100644 index 0000000..169e674 --- /dev/null +++ b/src/common/modules/notification/dtos/request/send-email.request.dto.ts @@ -0,0 +1,20 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsEmail, IsOptional, IsString } from 'class-validator'; + +export class SendEmailRequestDto { + @ApiProperty({ example: 'test@test.com' }) + @IsEmail() + to!: string; + + @ApiProperty({ example: 'Test Subject' }) + @IsString() + subject!: string; + + @ApiProperty({ example: 'test' }) + @IsString() + template!: string; + + @ApiProperty({ example: { name: 'John Doe' } }) + @IsOptional() + data!: Record; +} diff --git a/src/common/modules/notification/notification.module.ts b/src/common/modules/notification/notification.module.ts index b5b866a..c8033c3 100644 --- a/src/common/modules/notification/notification.module.ts +++ b/src/common/modules/notification/notification.module.ts @@ -1,15 +1,15 @@ +import { MailerModule } from '@nestjs-modules/mailer'; import { Module } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { TypeOrmModule } from '@nestjs/typeorm'; import { TwilioModule } from 'nestjs-twilio'; -import { buildTwilioOptions } from '~/core/module-options'; +import { buildMailerOptions, buildTwilioOptions } from '~/core/module-options'; import { UserModule } from '~/user/user.module'; 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'; +import { FirebaseService, NotificationsService, TwilioService } from './services'; + @Module({ imports: [ TypeOrmModule.forFeature([Notification]), @@ -17,6 +17,10 @@ import { TwilioService } from './services/twilio.service'; useFactory: buildTwilioOptions, inject: [ConfigService], }), + MailerModule.forRootAsync({ + useFactory: buildMailerOptions, + inject: [ConfigService], + }), UserModule, ], providers: [NotificationsService, FirebaseService, NotificationsRepository, TwilioService], diff --git a/src/common/modules/notification/services/index.ts b/src/common/modules/notification/services/index.ts index e69de29..772a718 100644 --- a/src/common/modules/notification/services/index.ts +++ b/src/common/modules/notification/services/index.ts @@ -0,0 +1,3 @@ +export * from './firebase.service'; +export * from './notifications.service'; +export * from './twilio.service'; diff --git a/src/common/modules/notification/services/notifications.service.ts b/src/common/modules/notification/services/notifications.service.ts index f56a518..cec2123 100644 --- a/src/common/modules/notification/services/notifications.service.ts +++ b/src/common/modules/notification/services/notifications.service.ts @@ -1,3 +1,4 @@ +import { MailerService } from '@nestjs-modules/mailer'; import { Injectable, Logger } from '@nestjs/common'; import { EventEmitter2, OnEvent } from '@nestjs/event-emitter'; import { PageOptionsRequestDto } from '~/core/dtos'; @@ -5,6 +6,7 @@ import { DeviceService } from '~/user/services'; import { OTP_BODY, OTP_TITLE } from '../../otp/constants'; import { OtpType } from '../../otp/enums'; import { ISendOtp } from '../../otp/interfaces'; +import { SendEmailRequestDto } from '../dtos/request'; import { Notification } from '../entities'; import { EventType, NotificationChannel, NotificationScope } from '../enums'; import { NotificationsRepository } from '../repositories'; @@ -20,6 +22,7 @@ export class NotificationsService { private readonly twilioService: TwilioService, private readonly eventEmitter: EventEmitter2, private readonly deviceService: DeviceService, + private readonly mailerService: MailerService, ) {} async sendPushNotification(userId: string, title: string, body: string) { @@ -41,6 +44,17 @@ export class NotificationsService { await this.twilioService.sendSMS(to, body); } + async sendEmail({ to, subject, data, template }: SendEmailRequestDto) { + this.logger.log(`Sending email to ${to}`); + await this.mailerService.sendMail({ + to, + subject, + template, + context: { ...data }, + }); + this.logger.log(`Email sent to ${to}`); + } + async getNotifications(userId: string, pageOptionsDto: PageOptionsRequestDto) { this.logger.log(`Getting notifications for user ${userId}`); const [[notifications, count], unreadCount] = await Promise.all([ @@ -78,8 +92,17 @@ export class NotificationsService { return this.eventEmitter.emit(EventType.NOTIFICATION_CREATED, notification); } + private getTemplateFromNotification(notification: Notification) { + switch (notification.scope) { + case NotificationScope.OTP: + return 'otp'; + default: + return 'otp'; + } + } + @OnEvent(EventType.NOTIFICATION_CREATED) - handleNotificationCreatedEvent(notification: Notification) { + handleNotificationCreatedEvent(notification: Notification, data?: any) { this.logger.log( `Handling ${EventType.NOTIFICATION_CREATED} event for notification ${notification.id} and type ${notification.channel}`, ); @@ -88,6 +111,13 @@ export class NotificationsService { return this.sendSMS(notification.recipient!, notification.message); case NotificationChannel.PUSH: return this.sendPushNotification(notification.userId, notification.title, notification.message); + case NotificationChannel.EMAIL: + return this.sendEmail({ + to: notification.recipient!, + subject: notification.title, + data, + template: this.getTemplateFromNotification(notification), + }); } } } diff --git a/src/common/modules/notification/templates/otp.hbs b/src/common/modules/notification/templates/otp.hbs new file mode 100644 index 0000000..e849df4 --- /dev/null +++ b/src/common/modules/notification/templates/otp.hbs @@ -0,0 +1,21 @@ + +
+

Your OTP Code

+

To verify your account, please use the following One-Time Password (OTP):

+
{{otp}}
+
+ + + diff --git a/src/core/module-options/index.ts b/src/core/module-options/index.ts index a9d66eb..489b4c9 100644 --- a/src/core/module-options/index.ts +++ b/src/core/module-options/index.ts @@ -1,5 +1,6 @@ export * from '././keyv-options'; export * from './config-options'; export * from './logger-options'; +export * from './mailer-options'; export * from './twilio-options'; export * from './typeorm-options'; diff --git a/src/core/module-options/mailer-options.ts b/src/core/module-options/mailer-options.ts new file mode 100644 index 0000000..efd35c1 --- /dev/null +++ b/src/core/module-options/mailer-options.ts @@ -0,0 +1,24 @@ +import { HandlebarsAdapter } from '@nestjs-modules/mailer/dist/adapters/handlebars.adapter'; +import { ConfigService } from '@nestjs/config'; +import path from 'path'; + +export function buildMailerOptions(config: ConfigService) { + return { + transport: { + from: config.getOrThrow('MAIL_FROM'), + host: config.getOrThrow('MAIL_HOST'), + port: config.getOrThrow('MAIL_PORT'), + auth: { + user: config.getOrThrow('MAIL_USER'), + pass: config.getOrThrow('MAIL_PASSWORD'), + }, + }, + template: { + dir: path.join(__dirname, '../../common/modules/notification/templates'), + adapter: new HandlebarsAdapter(), + options: { + strict: true, + }, + }, + }; +} diff --git a/src/junior/services/branch-io.service.ts b/src/junior/services/branch-io.service.ts index 8f7e4c7..df34a49 100644 --- a/src/junior/services/branch-io.service.ts +++ b/src/junior/services/branch-io.service.ts @@ -7,29 +7,17 @@ export class BranchIoService { private readonly logger = new Logger(BranchIoService.name); private readonly branchIoKey = this.configService.getOrThrow('BRANCH_IO_KEY'); private readonly branchIoUrl = this.configService.getOrThrow('BRANCH_IO_URL'); - private readonly zodBaseUrl = this.configService.getOrThrow('ZOD_BASE_URL'); - private readonly andrioPackageName = this.configService.getOrThrow('ANDROID_PACKAGE_NAME'); - private readonly iosPackageName = this.configService.getOrThrow('IOS_PACKAGE_NAME'); - private readonly androidDeeplinkPath = this.configService.getOrThrow('ANDRIOD_JUNIOR_DEEPLINK_PATH'); - private readonly iosDeeplinkPath = this.configService.getOrThrow('IOS_JUNIOR_DEEPLINK_PATH'); constructor(private readonly configService: ConfigService, private readonly httpService: HttpService) {} async createBranchLink(token: string) { this.logger.log(`Creating branch link`); const payload = { branch_key: this.branchIoKey, - channel: 'Website', - feature: 'Share', + channel: 'junior', + feature: 'invite', alias: token, - data: { - $desktop_url: `${this.zodBaseUrl}/juniors/qr-code/${token}/validate`, - $android_package: this.andrioPackageName, - $ios_package: this.iosPackageName, - $android_deeplink_path: this.androidDeeplinkPath, - $ios_url: this.iosDeeplinkPath, - token: token, - }, }; + const response = await this.httpService.axiosRef({ url: this.branchIoUrl, method: 'POST',