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"
|
"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",
|
||||||
"@nestjs/mapped-types": "*",
|
"@nestjs/mapped-types": "*",
|
||||||
"@nestjs/microservices": "^11.1.14",
|
"@nestjs/microservices": "^11.1.14",
|
||||||
"@nestjs/platform-express": "^11.0.1",
|
"@nestjs/platform-express": "^11.0.1",
|
||||||
"@teacinema/contracts": "^1.1.0",
|
|
||||||
"@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"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3.2.0",
|
"@eslint/eslintrc": "^3.2.0",
|
||||||
|
|||||||
@ -1,21 +1,16 @@
|
|||||||
import { Module } from '@nestjs/common'
|
import { Module } from '@nestjs/common'
|
||||||
import { ConfigModule } from '@nestjs/config'
|
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 { RmqModule } from './infra/rmq/rmq.module'
|
||||||
import { NotificationsModule } from './modules/notifications/notifications.module'
|
import { NotificationsModule } from './modules/notifications/notifications.module'
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
ConfigModule.forRoot({
|
ConfigModule.forRoot({
|
||||||
isGlobal: true,
|
isGlobal: true
|
||||||
load: [configuration],
|
|
||||||
expandVariables: true
|
|
||||||
}),
|
}),
|
||||||
RmqModule,
|
RmqModule,
|
||||||
NotificationsModule,
|
NotificationsModule
|
||||||
MailModule
|
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class AppModule {}
|
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>({
|
app.connectMicroservice<MicroserviceOptions>({
|
||||||
transport: Transport.RMQ,
|
transport: Transport.RMQ,
|
||||||
options: {
|
options: {
|
||||||
urls: [config.get('rmq.url')],
|
urls: [config.getOrThrow<string>('RMQ_URL')],
|
||||||
queue: config.get('rmq.queue'),
|
queue: config.getOrThrow<string>('RMQ_QUEUE'),
|
||||||
queueOptions: {
|
queueOptions: {
|
||||||
durable: false
|
durable: false
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import { Controller } from '@nestjs/common'
|
import { Controller } from '@nestjs/common'
|
||||||
import { Ctx, EventPattern, Payload, RmqContext } from '@nestjs/microservices'
|
import { Ctx, EventPattern, Payload, RmqContext } from '@nestjs/microservices'
|
||||||
import type { OtpRequestedEvent } from '@teacinema/contracts'
|
|
||||||
|
|
||||||
import { RmqService } from '../../infra/rmq/rmq.service'
|
import { RmqService } from '../../infra/rmq/rmq.service'
|
||||||
|
|
||||||
@ -14,12 +13,9 @@ export class NotificationsController {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
@EventPattern('auth.otp.requested')
|
@EventPattern('auth.otp.requested')
|
||||||
public async otpRequested(
|
public otpRequested(@Payload() data: any, @Ctx() ctx: RmqContext) {
|
||||||
@Payload() data: OtpRequestedEvent,
|
|
||||||
@Ctx() ctx: RmqContext
|
|
||||||
) {
|
|
||||||
try {
|
try {
|
||||||
await this.notificationsService.sendOtp(data)
|
console.log('OTP received', 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,13 +1,11 @@
|
|||||||
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,20 +1,4 @@
|
|||||||
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