Compare commits
No commits in common. "3a10dd136b5b0769ceb23b325ada7459a881b223" and "0786a0dfefd5a2a4bf6931290a2c2a0187234f14" have entirely different histories.
3a10dd136b
...
0786a0dfef
@ -20,21 +20,17 @@
|
||||
"test:e2e": "jest --config ./test/jest-e2e.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nestjs-modules/mailer": "^2.0.2",
|
||||
"@nestjs/common": "^11.0.1",
|
||||
"@nestjs/config": "^4.0.3",
|
||||
"@nestjs/core": "^11.0.1",
|
||||
"@nestjs/mapped-types": "*",
|
||||
"@nestjs/microservices": "^11.1.14",
|
||||
"@nestjs/platform-express": "^11.0.1",
|
||||
"@teacinema/contracts": "^1.1.0",
|
||||
"@teacinema/core": "^1.0.9",
|
||||
"amqp-connection-manager": "^5.0.0",
|
||||
"amqplib": "^0.10.9",
|
||||
"handlebars": "^4.7.8",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.1",
|
||||
"zod": "^4.3.6"
|
||||
"rxjs": "^7.8.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3.2.0",
|
||||
|
||||
@ -1,21 +1,16 @@
|
||||
import { Module } from '@nestjs/common'
|
||||
import { ConfigModule } from '@nestjs/config'
|
||||
|
||||
import configuration from './config/configuration'
|
||||
import { MailModule } from './infra/mail/mail.module'
|
||||
import { RmqModule } from './infra/rmq/rmq.module'
|
||||
import { NotificationsModule } from './modules/notifications/notifications.module'
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ConfigModule.forRoot({
|
||||
isGlobal: true,
|
||||
load: [configuration],
|
||||
expandVariables: true
|
||||
isGlobal: true
|
||||
}),
|
||||
RmqModule,
|
||||
NotificationsModule,
|
||||
MailModule
|
||||
NotificationsModule
|
||||
]
|
||||
})
|
||||
export class AppModule {}
|
||||
|
||||
@ -1,30 +0,0 @@
|
||||
import validationSchema from './validation.schema'
|
||||
|
||||
export default () => {
|
||||
const parsed = validationSchema.safeParse(process.env)
|
||||
|
||||
if (!parsed.success) {
|
||||
console.error('Invalid enviroment variables', parsed.error.format())
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const env = parsed.data
|
||||
|
||||
return {
|
||||
app: {
|
||||
nodeEnv: env.NODE_ENV
|
||||
},
|
||||
rmq: {
|
||||
url: env.RMQ_URL,
|
||||
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 +0,0 @@
|
||||
export * from './mailer.factory'
|
||||
@ -1,19 +0,0 @@
|
||||
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')}`
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,25 +0,0 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
export enum Enviroment {
|
||||
Development = 'development',
|
||||
Production = 'production',
|
||||
Test = 'test'
|
||||
}
|
||||
|
||||
export default z.object({
|
||||
// Node
|
||||
NODE_ENV: z.enum(Enviroment).default(Enviroment.Development),
|
||||
// RabbitMQ
|
||||
RMQ_URL: 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')
|
||||
})
|
||||
@ -1,20 +0,0 @@
|
||||
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 {}
|
||||
@ -1,22 +0,0 @@
|
||||
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
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -1,23 +0,0 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
@ -1,35 +0,0 @@
|
||||
<!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>
|
||||
@ -12,8 +12,8 @@ async function bootstrap() {
|
||||
app.connectMicroservice<MicroserviceOptions>({
|
||||
transport: Transport.RMQ,
|
||||
options: {
|
||||
urls: [config.get('rmq.url')],
|
||||
queue: config.get('rmq.queue'),
|
||||
urls: [config.getOrThrow<string>('RMQ_URL')],
|
||||
queue: config.getOrThrow<string>('RMQ_QUEUE'),
|
||||
queueOptions: {
|
||||
durable: false
|
||||
},
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import { Controller } from '@nestjs/common'
|
||||
import { Ctx, EventPattern, Payload, RmqContext } from '@nestjs/microservices'
|
||||
import type { OtpRequestedEvent } from '@teacinema/contracts'
|
||||
|
||||
import { RmqService } from '../../infra/rmq/rmq.service'
|
||||
|
||||
@ -14,12 +13,9 @@ export class NotificationsController {
|
||||
) {}
|
||||
|
||||
@EventPattern('auth.otp.requested')
|
||||
public async otpRequested(
|
||||
@Payload() data: OtpRequestedEvent,
|
||||
@Ctx() ctx: RmqContext
|
||||
) {
|
||||
public otpRequested(@Payload() data: any, @Ctx() ctx: RmqContext) {
|
||||
try {
|
||||
await this.notificationsService.sendOtp(data)
|
||||
console.log('OTP received', data)
|
||||
this.rmqService.ack(ctx)
|
||||
} catch (e) {
|
||||
console.log('OTP processing error', e.message ?? e)
|
||||
|
||||
@ -1,13 +1,11 @@
|
||||
import { Module } from '@nestjs/common'
|
||||
|
||||
import { MailModule } from '../../infra/mail/mail.module'
|
||||
import { RmqService } from '../../infra/rmq/rmq.service'
|
||||
|
||||
import { NotificationsController } from './notifications.controller'
|
||||
import { NotificationsService } from './notifications.service'
|
||||
|
||||
@Module({
|
||||
imports: [MailModule],
|
||||
controllers: [NotificationsController],
|
||||
providers: [NotificationsService, RmqService]
|
||||
})
|
||||
|
||||
@ -1,20 +1,4 @@
|
||||
import { Injectable } from '@nestjs/common'
|
||||
import { OtpRequestedEvent } from '@teacinema/contracts'
|
||||
|
||||
import { MailService } from '../../infra/mail/mail.service'
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
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')
|
||||
}
|
||||
}
|
||||
}
|
||||
export class NotificationsService {}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user