feat: add smtp and fix dynamic link

This commit is contained in:
Abdalhamid Alhamad
2025-01-06 16:46:35 +03:00
parent 25ef549417
commit 3ab0f179d8
11 changed files with 123 additions and 21 deletions

View File

@ -10,6 +10,8 @@
"include": "config", "include": "config",
"exclude": "**/*.md" "exclude": "**/*.md"
}, },
{ "include": "common/modules/**/templates/*", "watchAssets": true }
,
"i18n", "i18n",
"files" "files"
] ]

View File

@ -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 { ApiBearerAuth, ApiResponse, ApiTags } from '@nestjs/swagger';
import { IJwtPayload } from '~/auth/interfaces'; import { IJwtPayload } from '~/auth/interfaces';
import { AuthenticatedUser } from '~/common/decorators'; import { AuthenticatedUser } from '~/common/decorators';
import { AccessTokenGuard } from '~/common/guards'; import { AccessTokenGuard } from '~/common/guards';
import { PageOptionsRequestDto } from '~/core/dtos'; import { PageOptionsRequestDto } from '~/core/dtos';
import { SendEmailRequestDto } from '../dtos/request';
import { NotificationsPageResponseDto } from '../dtos/response'; import { NotificationsPageResponseDto } from '../dtos/response';
import { NotificationsService } from '../services/notifications.service'; import { NotificationsService } from '../services/notifications.service';
@ -33,4 +34,11 @@ export class NotificationsController {
markAsRead(@AuthenticatedUser() { sub }: IJwtPayload) { markAsRead(@AuthenticatedUser() { sub }: IJwtPayload) {
return this.notificationsService.markAsRead(sub); 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);
}
} }

View File

@ -0,0 +1 @@
export * from './send-email.request.dto';

View File

@ -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<string, any>;
}

View File

@ -1,15 +1,15 @@
import { MailerModule } from '@nestjs-modules/mailer';
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { ConfigService } from '@nestjs/config'; import { ConfigService } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm'; import { TypeOrmModule } from '@nestjs/typeorm';
import { TwilioModule } from 'nestjs-twilio'; 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 { UserModule } from '~/user/user.module';
import { NotificationsController } from './controllers'; import { NotificationsController } from './controllers';
import { Notification } from './entities'; import { Notification } from './entities';
import { NotificationsRepository } from './repositories'; import { NotificationsRepository } from './repositories';
import { FirebaseService } from './services/firebase.service'; import { FirebaseService, NotificationsService, TwilioService } from './services';
import { NotificationsService } from './services/notifications.service';
import { TwilioService } from './services/twilio.service';
@Module({ @Module({
imports: [ imports: [
TypeOrmModule.forFeature([Notification]), TypeOrmModule.forFeature([Notification]),
@ -17,6 +17,10 @@ import { TwilioService } from './services/twilio.service';
useFactory: buildTwilioOptions, useFactory: buildTwilioOptions,
inject: [ConfigService], inject: [ConfigService],
}), }),
MailerModule.forRootAsync({
useFactory: buildMailerOptions,
inject: [ConfigService],
}),
UserModule, UserModule,
], ],
providers: [NotificationsService, FirebaseService, NotificationsRepository, TwilioService], providers: [NotificationsService, FirebaseService, NotificationsRepository, TwilioService],

View File

@ -0,0 +1,3 @@
export * from './firebase.service';
export * from './notifications.service';
export * from './twilio.service';

View File

@ -1,3 +1,4 @@
import { MailerService } from '@nestjs-modules/mailer';
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { EventEmitter2, OnEvent } from '@nestjs/event-emitter'; import { EventEmitter2, OnEvent } from '@nestjs/event-emitter';
import { PageOptionsRequestDto } from '~/core/dtos'; import { PageOptionsRequestDto } from '~/core/dtos';
@ -5,6 +6,7 @@ import { DeviceService } from '~/user/services';
import { OTP_BODY, OTP_TITLE } from '../../otp/constants'; import { OTP_BODY, OTP_TITLE } from '../../otp/constants';
import { OtpType } from '../../otp/enums'; import { OtpType } from '../../otp/enums';
import { ISendOtp } from '../../otp/interfaces'; import { ISendOtp } from '../../otp/interfaces';
import { SendEmailRequestDto } from '../dtos/request';
import { Notification } from '../entities'; import { Notification } from '../entities';
import { EventType, NotificationChannel, NotificationScope } from '../enums'; import { EventType, NotificationChannel, NotificationScope } from '../enums';
import { NotificationsRepository } from '../repositories'; import { NotificationsRepository } from '../repositories';
@ -20,6 +22,7 @@ export class NotificationsService {
private readonly twilioService: TwilioService, private readonly twilioService: TwilioService,
private readonly eventEmitter: EventEmitter2, private readonly eventEmitter: EventEmitter2,
private readonly deviceService: DeviceService, private readonly deviceService: DeviceService,
private readonly mailerService: MailerService,
) {} ) {}
async sendPushNotification(userId: string, title: string, body: string) { async sendPushNotification(userId: string, title: string, body: string) {
@ -41,6 +44,17 @@ export class NotificationsService {
await this.twilioService.sendSMS(to, body); 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) { async getNotifications(userId: string, pageOptionsDto: PageOptionsRequestDto) {
this.logger.log(`Getting notifications for user ${userId}`); this.logger.log(`Getting notifications for user ${userId}`);
const [[notifications, count], unreadCount] = await Promise.all([ const [[notifications, count], unreadCount] = await Promise.all([
@ -78,8 +92,17 @@ export class NotificationsService {
return this.eventEmitter.emit(EventType.NOTIFICATION_CREATED, notification); 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) @OnEvent(EventType.NOTIFICATION_CREATED)
handleNotificationCreatedEvent(notification: Notification) { handleNotificationCreatedEvent(notification: Notification, data?: any) {
this.logger.log( this.logger.log(
`Handling ${EventType.NOTIFICATION_CREATED} event for notification ${notification.id} and type ${notification.channel}`, `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); return this.sendSMS(notification.recipient!, notification.message);
case NotificationChannel.PUSH: case NotificationChannel.PUSH:
return this.sendPushNotification(notification.userId, notification.title, notification.message); 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),
});
} }
} }
} }

View File

@ -0,0 +1,21 @@
<body>
<div class="otp">
<h1 class="title">Your OTP Code</h1>
<p class="message">To verify your account, please use the following One-Time Password (OTP):</p>
<div class="otp-code">{{otp}}</div>
</div>
<style>
.otp {
text-align: center;
font-family: sans-serif;
font-size: 16px;
line-height: 1.5;
}
.otp-code {
font-size: 24px;
font-weight: bold;
margin-top: 20px;
}
</style>
</body>

View File

@ -1,5 +1,6 @@
export * from '././keyv-options'; export * from '././keyv-options';
export * from './config-options'; export * from './config-options';
export * from './logger-options'; export * from './logger-options';
export * from './mailer-options';
export * from './twilio-options'; export * from './twilio-options';
export * from './typeorm-options'; export * from './typeorm-options';

View File

@ -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<string>('MAIL_FROM'),
host: config.getOrThrow<string>('MAIL_HOST'),
port: config.getOrThrow<number>('MAIL_PORT'),
auth: {
user: config.getOrThrow<string>('MAIL_USER'),
pass: config.getOrThrow<string>('MAIL_PASSWORD'),
},
},
template: {
dir: path.join(__dirname, '../../common/modules/notification/templates'),
adapter: new HandlebarsAdapter(),
options: {
strict: true,
},
},
};
}

View File

@ -7,29 +7,17 @@ export class BranchIoService {
private readonly logger = new Logger(BranchIoService.name); private readonly logger = new Logger(BranchIoService.name);
private readonly branchIoKey = this.configService.getOrThrow<string>('BRANCH_IO_KEY'); private readonly branchIoKey = this.configService.getOrThrow<string>('BRANCH_IO_KEY');
private readonly branchIoUrl = this.configService.getOrThrow<string>('BRANCH_IO_URL'); private readonly branchIoUrl = this.configService.getOrThrow<string>('BRANCH_IO_URL');
private readonly zodBaseUrl = this.configService.getOrThrow<string>('ZOD_BASE_URL');
private readonly andrioPackageName = this.configService.getOrThrow<string>('ANDROID_PACKAGE_NAME');
private readonly iosPackageName = this.configService.getOrThrow<string>('IOS_PACKAGE_NAME');
private readonly androidDeeplinkPath = this.configService.getOrThrow<string>('ANDRIOD_JUNIOR_DEEPLINK_PATH');
private readonly iosDeeplinkPath = this.configService.getOrThrow<string>('IOS_JUNIOR_DEEPLINK_PATH');
constructor(private readonly configService: ConfigService, private readonly httpService: HttpService) {} constructor(private readonly configService: ConfigService, private readonly httpService: HttpService) {}
async createBranchLink(token: string) { async createBranchLink(token: string) {
this.logger.log(`Creating branch link`); this.logger.log(`Creating branch link`);
const payload = { const payload = {
branch_key: this.branchIoKey, branch_key: this.branchIoKey,
channel: 'Website', channel: 'junior',
feature: 'Share', feature: 'invite',
alias: token, 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({ const response = await this.httpService.axiosRef({
url: this.branchIoUrl, url: this.branchIoUrl,
method: 'POST', method: 'POST',