feat: add email notifications
This commit is contained in:
parent
4797e2feda
commit
3a10dd136b
@ -20,6 +20,7 @@
|
|||||||
"test:e2e": "jest --config ./test/jest-e2e.json"
|
"test:e2e": "jest --config ./test/jest-e2e.json"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@nestjs-modules/mailer": "^2.0.2",
|
||||||
"@nestjs/common": "^11.0.1",
|
"@nestjs/common": "^11.0.1",
|
||||||
"@nestjs/config": "^4.0.3",
|
"@nestjs/config": "^4.0.3",
|
||||||
"@nestjs/core": "^11.0.1",
|
"@nestjs/core": "^11.0.1",
|
||||||
@ -30,6 +31,7 @@
|
|||||||
"@teacinema/core": "^1.0.9",
|
"@teacinema/core": "^1.0.9",
|
||||||
"amqp-connection-manager": "^5.0.0",
|
"amqp-connection-manager": "^5.0.0",
|
||||||
"amqplib": "^0.10.9",
|
"amqplib": "^0.10.9",
|
||||||
|
"handlebars": "^4.7.8",
|
||||||
"reflect-metadata": "^0.2.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
"rxjs": "^7.8.1",
|
"rxjs": "^7.8.1",
|
||||||
"zod": "^4.3.6"
|
"zod": "^4.3.6"
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { Module } from '@nestjs/common'
|
|||||||
import { ConfigModule } from '@nestjs/config'
|
import { ConfigModule } from '@nestjs/config'
|
||||||
|
|
||||||
import configuration from './config/configuration'
|
import configuration from './config/configuration'
|
||||||
|
import { MailModule } from './infra/mail/mail.module'
|
||||||
import { RmqModule } from './infra/rmq/rmq.module'
|
import { RmqModule } from './infra/rmq/rmq.module'
|
||||||
import { NotificationsModule } from './modules/notifications/notifications.module'
|
import { NotificationsModule } from './modules/notifications/notifications.module'
|
||||||
|
|
||||||
@ -13,7 +14,8 @@ import { NotificationsModule } from './modules/notifications/notifications.modul
|
|||||||
expandVariables: true
|
expandVariables: true
|
||||||
}),
|
}),
|
||||||
RmqModule,
|
RmqModule,
|
||||||
NotificationsModule
|
NotificationsModule,
|
||||||
|
MailModule
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class AppModule {}
|
export class AppModule {}
|
||||||
|
|||||||
@ -17,6 +17,14 @@ export default () => {
|
|||||||
rmq: {
|
rmq: {
|
||||||
url: env.RMQ_URL,
|
url: env.RMQ_URL,
|
||||||
queue: env.RMQ_QUEUE
|
queue: env.RMQ_QUEUE
|
||||||
|
},
|
||||||
|
smtp: {
|
||||||
|
host: env.SMTP_HOST,
|
||||||
|
port: env.SMTP_PORT,
|
||||||
|
username: env.SMTP_USERNAME,
|
||||||
|
password: env.SMTP_PASSWORD,
|
||||||
|
fromAddress: env.SMTP_FROM_ADDRESS,
|
||||||
|
secure: env.SMTP_SECURE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
1
src/config/factories/index.ts
Normal file
1
src/config/factories/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './mailer.factory'
|
||||||
19
src/config/factories/mailer.factory.ts
Normal file
19
src/config/factories/mailer.factory.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { MailerOptions } from '@nestjs-modules/mailer'
|
||||||
|
import { ConfigService } from '@nestjs/config'
|
||||||
|
|
||||||
|
export function getMailerConfig(configService: ConfigService): MailerOptions {
|
||||||
|
return {
|
||||||
|
transport: {
|
||||||
|
host: configService.get('smtp.host'),
|
||||||
|
port: configService.get('smtp.port'),
|
||||||
|
secure: configService.get('smtp.secure'),
|
||||||
|
auth: {
|
||||||
|
user: configService.get('smtp.username'),
|
||||||
|
pass: configService.get('smtp.password')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
defaults: {
|
||||||
|
from: `Teacinema ${configService.get('smtp.fromAddress')}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -7,7 +7,19 @@ export enum Enviroment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default z.object({
|
export default z.object({
|
||||||
|
// Node
|
||||||
NODE_ENV: z.enum(Enviroment).default(Enviroment.Development),
|
NODE_ENV: z.enum(Enviroment).default(Enviroment.Development),
|
||||||
|
// RabbitMQ
|
||||||
RMQ_URL: z.string().nonempty(),
|
RMQ_URL: z.string().nonempty(),
|
||||||
RMQ_QUEUE: z.string().nonempty()
|
RMQ_QUEUE: z.string().nonempty(),
|
||||||
|
// SMTP
|
||||||
|
SMTP_HOST: z.string().nonempty(),
|
||||||
|
SMTP_PORT: z.coerce.number().nonnegative(),
|
||||||
|
SMTP_USERNAME: z.string().nonempty(),
|
||||||
|
SMTP_PASSWORD: z.string().nonempty(),
|
||||||
|
SMTP_FROM_ADDRESS: z.string().email().nonempty(),
|
||||||
|
SMTP_SECURE: z
|
||||||
|
.string()
|
||||||
|
.default('false')
|
||||||
|
.transform(value => value === 'true')
|
||||||
})
|
})
|
||||||
|
|||||||
20
src/infra/mail/mail.module.ts
Normal file
20
src/infra/mail/mail.module.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { MailerModule } from '@nestjs-modules/mailer'
|
||||||
|
import { Module } from '@nestjs/common'
|
||||||
|
import { ConfigService } from '@nestjs/config'
|
||||||
|
import { getMailerConfig } from 'src/config/factories'
|
||||||
|
|
||||||
|
import { MailService } from './mail.service'
|
||||||
|
import { TemplateService } from './template.service'
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
MailerModule.forRootAsync({
|
||||||
|
useFactory: (configService: ConfigService) =>
|
||||||
|
getMailerConfig(configService),
|
||||||
|
inject: [ConfigService]
|
||||||
|
})
|
||||||
|
],
|
||||||
|
providers: [MailService, TemplateService],
|
||||||
|
exports: [MailService]
|
||||||
|
})
|
||||||
|
export class MailModule {}
|
||||||
22
src/infra/mail/mail.service.ts
Normal file
22
src/infra/mail/mail.service.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { MailerService } from '@nestjs-modules/mailer'
|
||||||
|
import { Injectable } from '@nestjs/common'
|
||||||
|
|
||||||
|
import { TemplateService } from './template.service'
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class MailService {
|
||||||
|
constructor(
|
||||||
|
private readonly transporter: MailerService,
|
||||||
|
private readonly templateService: TemplateService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public async sendOtp(email: string, code: string) {
|
||||||
|
const html = this.templateService.render('otp', { code })
|
||||||
|
|
||||||
|
await this.transporter.sendMail({
|
||||||
|
to: email,
|
||||||
|
subject: 'Ваш код подтверждения',
|
||||||
|
html
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
23
src/infra/mail/template.service.ts
Normal file
23
src/infra/mail/template.service.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { Injectable } from '@nestjs/common'
|
||||||
|
import * as Handlebars from 'handlebars'
|
||||||
|
import fs from 'node:fs'
|
||||||
|
import path from 'node:path'
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class TemplateService {
|
||||||
|
private cache = new Map<string, Handlebars.TemplateDelegate>()
|
||||||
|
|
||||||
|
public render(templateName: string, context?: Record<string, any>) {
|
||||||
|
if (!this.cache.has(templateName)) {
|
||||||
|
const templatePath = path.join(
|
||||||
|
process.cwd(),
|
||||||
|
'src/infra/mail/templates',
|
||||||
|
`${templateName}.hbs`
|
||||||
|
)
|
||||||
|
const file = fs.readFileSync(templatePath, 'utf-8')
|
||||||
|
this.cache.set(templateName, Handlebars.compile(file))
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.cache.get(templateName)!(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
35
src/infra/mail/templates/otp.hbs
Normal file
35
src/infra/mail/templates/otp.hbs
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport"
|
||||||
|
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||||
|
<title>Ваш код подтверждения</title>
|
||||||
|
</head>
|
||||||
|
<body style="margin: 0; padding: 0;font-family: Arial, sans-serif;">
|
||||||
|
<table style="width: 100%;" cellspacing="0" cellpadding="0">
|
||||||
|
<tr>
|
||||||
|
<td align="center" style="padding: 40px">
|
||||||
|
|
||||||
|
<h1 style="font-size: 22px;font-weight: bold; margin: 0 0 25px">
|
||||||
|
Код подтверждения
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<p style="font-size: 15px; margin: 0 0 15px">Ваш код: </p>
|
||||||
|
<div style="font-size: 36px;font-weight: bold; margin: 0 0 15px; letter-spacing: 6px">{{code}}</div>
|
||||||
|
|
||||||
|
<p style="font-size: 13px; color: #555; margin: 30px 0 0">
|
||||||
|
Если это были не вы - просто проигнорируйте письм
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p style="font-size: 13px; margin: 8px 0 0">
|
||||||
|
<a href="https://teacinema.ru" target="_blank" style="text-decoration: none; color: #000; font-weight: bold">
|
||||||
|
teacinema.ru
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -14,12 +14,12 @@ export class NotificationsController {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
@EventPattern('auth.otp.requested')
|
@EventPattern('auth.otp.requested')
|
||||||
public otpRequested(
|
public async otpRequested(
|
||||||
@Payload() data: OtpRequestedEvent,
|
@Payload() data: OtpRequestedEvent,
|
||||||
@Ctx() ctx: RmqContext
|
@Ctx() ctx: RmqContext
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
console.log('OTP received', data)
|
await this.notificationsService.sendOtp(data)
|
||||||
this.rmqService.ack(ctx)
|
this.rmqService.ack(ctx)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log('OTP processing error', e.message ?? e)
|
console.log('OTP processing error', e.message ?? e)
|
||||||
|
|||||||
@ -1,11 +1,13 @@
|
|||||||
import { Module } from '@nestjs/common'
|
import { Module } from '@nestjs/common'
|
||||||
|
|
||||||
|
import { MailModule } from '../../infra/mail/mail.module'
|
||||||
import { RmqService } from '../../infra/rmq/rmq.service'
|
import { RmqService } from '../../infra/rmq/rmq.service'
|
||||||
|
|
||||||
import { NotificationsController } from './notifications.controller'
|
import { NotificationsController } from './notifications.controller'
|
||||||
import { NotificationsService } from './notifications.service'
|
import { NotificationsService } from './notifications.service'
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
|
imports: [MailModule],
|
||||||
controllers: [NotificationsController],
|
controllers: [NotificationsController],
|
||||||
providers: [NotificationsService, RmqService]
|
providers: [NotificationsService, RmqService]
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,4 +1,20 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common'
|
||||||
|
import { OtpRequestedEvent } from '@teacinema/contracts'
|
||||||
|
|
||||||
|
import { MailService } from '../../infra/mail/mail.service'
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class NotificationsService {}
|
export class NotificationsService {
|
||||||
|
constructor(private readonly mailService: MailService) {}
|
||||||
|
|
||||||
|
public async sendOtp(data: OtpRequestedEvent) {
|
||||||
|
const { identifier, type, code } = data
|
||||||
|
|
||||||
|
if (type === 'email') {
|
||||||
|
console.log('SEND EMAIL', data)
|
||||||
|
await this.mailService.sendOtp(identifier, code)
|
||||||
|
} else {
|
||||||
|
console.log('SMS')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user