mirror of
https://github.com/HamzaSha1/zod-backend.git
synced 2025-07-16 18:36:18 +00:00
feat: add smtp and fix dynamic link
This commit is contained in:
@ -10,6 +10,8 @@
|
|||||||
"include": "config",
|
"include": "config",
|
||||||
"exclude": "**/*.md"
|
"exclude": "**/*.md"
|
||||||
},
|
},
|
||||||
|
{ "include": "common/modules/**/templates/*", "watchAssets": true }
|
||||||
|
,
|
||||||
"i18n",
|
"i18n",
|
||||||
"files"
|
"files"
|
||||||
]
|
]
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
1
src/common/modules/notification/dtos/request/index.ts
Normal file
1
src/common/modules/notification/dtos/request/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './send-email.request.dto';
|
@ -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>;
|
||||||
|
}
|
@ -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],
|
||||||
|
@ -0,0 +1,3 @@
|
|||||||
|
export * from './firebase.service';
|
||||||
|
export * from './notifications.service';
|
||||||
|
export * from './twilio.service';
|
||||||
|
@ -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),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
21
src/common/modules/notification/templates/otp.hbs
Normal file
21
src/common/modules/notification/templates/otp.hbs
Normal 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>
|
@ -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';
|
||||||
|
24
src/core/module-options/mailer-options.ts
Normal file
24
src/core/module-options/mailer-options.ts
Normal 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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
@ -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',
|
||||||
|
Reference in New Issue
Block a user