Compare commits
No commits in common. "a4b9f46afd04152236ffbe15f10da6240d5e4439" and "3a10dd136b5b0769ceb23b325ada7459a881b223" have entirely different histories.
a4b9f46afd
...
3a10dd136b
@ -21,7 +21,6 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@nestjs-modules/mailer": "^2.0.2",
|
||||
"@nestjs/axios": "^4.0.1",
|
||||
"@nestjs/common": "^11.0.1",
|
||||
"@nestjs/config": "^4.0.3",
|
||||
"@nestjs/core": "^11.0.1",
|
||||
@ -32,7 +31,6 @@
|
||||
"@teacinema/core": "^1.0.9",
|
||||
"amqp-connection-manager": "^5.0.0",
|
||||
"amqplib": "^0.10.9",
|
||||
"axios": "^1.13.5",
|
||||
"handlebars": "^4.7.8",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.1",
|
||||
|
||||
@ -5,7 +5,6 @@ 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'
|
||||
import { SmsModule } from './infra/sms/sms.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@ -16,8 +15,7 @@ import { SmsModule } from './infra/sms/sms.module';
|
||||
}),
|
||||
RmqModule,
|
||||
NotificationsModule,
|
||||
MailModule,
|
||||
SmsModule
|
||||
MailModule
|
||||
]
|
||||
})
|
||||
export class AppModule {}
|
||||
|
||||
@ -25,10 +25,6 @@ export default () => {
|
||||
password: env.SMTP_PASSWORD,
|
||||
fromAddress: env.SMTP_FROM_ADDRESS,
|
||||
secure: env.SMTP_SECURE
|
||||
},
|
||||
exolve: {
|
||||
apiKey: env.EXOLVE_API_KEY,
|
||||
sender: env.EXOLVE_SENDER
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,10 +0,0 @@
|
||||
import { ConfigService } from '@nestjs/config'
|
||||
|
||||
import { SmsOptions } from '../../infra/sms/interfaces'
|
||||
|
||||
export function getExolveConfig(configService: ConfigService): SmsOptions {
|
||||
return {
|
||||
apiKey: configService.get('exolve.apiKey') ?? '',
|
||||
sender: configService.get('exolve.sender') ?? ''
|
||||
}
|
||||
}
|
||||
@ -1,2 +1 @@
|
||||
export * from './mailer.factory'
|
||||
export * from './exolve.factory'
|
||||
|
||||
@ -21,8 +21,5 @@ export default z.object({
|
||||
SMTP_SECURE: z
|
||||
.string()
|
||||
.default('false')
|
||||
.transform(value => value === 'true'),
|
||||
// MTS Exolve
|
||||
EXOLVE_API_KEY: z.string().nonempty(),
|
||||
EXOLVE_SENDER: z.string().nonempty()
|
||||
.transform(value => value === 'true')
|
||||
})
|
||||
|
||||
@ -19,13 +19,4 @@ export class MailService {
|
||||
html
|
||||
})
|
||||
}
|
||||
|
||||
public async sendEmailChange(email: string, code: string) {
|
||||
const html = this.templateService.render('email-change', { code })
|
||||
await this.transporter.sendMail({
|
||||
to: email,
|
||||
subject: 'Подтверждение смены почты',
|
||||
html
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,14 +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>
|
||||
<h1>Подтверждение смены почты</h1>
|
||||
<p>Ваш код подтверждения для смены почты: {{code}}</p>
|
||||
</body>
|
||||
</html>
|
||||
@ -1 +0,0 @@
|
||||
export * from './sms.constants'
|
||||
@ -1 +0,0 @@
|
||||
export const SMS_OPTIONS = Symbol('SMS_OPTIONS');
|
||||
@ -1,3 +0,0 @@
|
||||
export * from './sms-options.interface'
|
||||
export * from './sms-async-options'
|
||||
export * from './send-sms.interface'
|
||||
@ -1,10 +0,0 @@
|
||||
export interface SendSmsRequest {
|
||||
sender?: string
|
||||
destination: string
|
||||
text: string
|
||||
}
|
||||
|
||||
export interface SendSmsResponse {
|
||||
message_id: string
|
||||
template_resource_id: string
|
||||
}
|
||||
@ -1,8 +0,0 @@
|
||||
import { FactoryProvider, ModuleMetadata } from '@nestjs/common'
|
||||
|
||||
import { SmsOptions } from './sms-options.interface'
|
||||
|
||||
export interface SmsAsyncOptions extends Pick<ModuleMetadata, 'imports'> {
|
||||
useFactory: (...args: any[]) => Promise<SmsOptions> | SmsOptions
|
||||
inject?: FactoryProvider['inject']
|
||||
}
|
||||
@ -1,4 +0,0 @@
|
||||
export interface SmsOptions {
|
||||
apiKey: string;
|
||||
sender: string;
|
||||
}
|
||||
@ -1,40 +0,0 @@
|
||||
import { HttpModule } from '@nestjs/axios'
|
||||
import { DynamicModule, Module } from '@nestjs/common'
|
||||
|
||||
import { SMS_OPTIONS } from './constants'
|
||||
import { SmsAsyncOptions, SmsOptions } from './interfaces'
|
||||
import { SmsService } from './sms.service'
|
||||
|
||||
@Module({})
|
||||
export class SmsModule {
|
||||
public static register(options: SmsOptions): DynamicModule {
|
||||
return {
|
||||
module: SmsModule,
|
||||
imports: [HttpModule],
|
||||
providers: [
|
||||
SmsService,
|
||||
{
|
||||
provide: SMS_OPTIONS,
|
||||
useValue: options
|
||||
}
|
||||
],
|
||||
exports: [SmsService]
|
||||
}
|
||||
}
|
||||
|
||||
public static registerAsync(options: SmsAsyncOptions): DynamicModule {
|
||||
return {
|
||||
module: SmsModule,
|
||||
imports: [HttpModule, ...(options.imports ?? [])],
|
||||
providers: [
|
||||
SmsService,
|
||||
{
|
||||
provide: SMS_OPTIONS,
|
||||
useFactory: options.useFactory,
|
||||
inject: options.inject ?? []
|
||||
}
|
||||
],
|
||||
exports: [SmsService]
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,108 +0,0 @@
|
||||
import { HttpService } from '@nestjs/axios'
|
||||
import { Inject, Injectable, Logger } from '@nestjs/common'
|
||||
import {
|
||||
catchError,
|
||||
delay,
|
||||
firstValueFrom,
|
||||
retryWhen,
|
||||
scan,
|
||||
throwError,
|
||||
timeout
|
||||
} from 'rxjs'
|
||||
|
||||
import { SMS_OPTIONS } from './constants'
|
||||
import type { SendSmsRequest, SendSmsResponse, SmsOptions } from './interfaces'
|
||||
|
||||
@Injectable()
|
||||
export class SmsService {
|
||||
private readonly BASE_URL: string
|
||||
private readonly logger = new Logger(SmsService.name)
|
||||
|
||||
constructor(
|
||||
private readonly httpService: HttpService,
|
||||
@Inject(SMS_OPTIONS) private readonly options: SmsOptions
|
||||
) {
|
||||
this.BASE_URL = 'https://api.exolve.ru'
|
||||
}
|
||||
|
||||
public async sendOtp(phone: string, code: string) {
|
||||
return this.send({
|
||||
destination: phone,
|
||||
text: `Ваш код подтверждения: ${code}`
|
||||
})
|
||||
}
|
||||
|
||||
public async sendPhoneChanged(phone: string, code: string) {
|
||||
return this.send({
|
||||
destination: phone,
|
||||
text: `Ваш код подтвердления для изменения номера телефона: ${code}`
|
||||
})
|
||||
}
|
||||
|
||||
public send(data: SendSmsRequest): Promise<SendSmsResponse> {
|
||||
const payload = {
|
||||
number: data.sender ?? this.options.sender,
|
||||
destination: data.destination.replace('+', ''),
|
||||
text: data.text
|
||||
}
|
||||
|
||||
return this.request<SendSmsResponse>(
|
||||
'POST',
|
||||
'/messaging/v1/SendSMS',
|
||||
payload
|
||||
)
|
||||
}
|
||||
|
||||
private async request<T>(
|
||||
method: 'GET' | 'POST',
|
||||
path: string,
|
||||
body: any
|
||||
): Promise<T> {
|
||||
const url = `${this.BASE_URL}${path}`
|
||||
|
||||
try {
|
||||
const request = this.httpService
|
||||
.request<T>({
|
||||
url,
|
||||
method,
|
||||
data: body,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${this.options.apiKey}`
|
||||
}
|
||||
})
|
||||
.pipe(
|
||||
timeout(7000),
|
||||
retryWhen(errors =>
|
||||
errors.pipe(
|
||||
scan((retryCount, error) => {
|
||||
if (retryCount >= 2) {
|
||||
throw error
|
||||
}
|
||||
|
||||
this.logger.warn(
|
||||
`Retry request (${method} ${path}): ${retryCount + 1}/3`
|
||||
)
|
||||
|
||||
return retryCount + 1
|
||||
}, 0),
|
||||
delay(500)
|
||||
)
|
||||
),
|
||||
catchError(error => {
|
||||
const detail = error.response?.data ?? error.message ?? error
|
||||
this.logger.error(
|
||||
`Exolve API error (${method} ${path})\n${JSON.stringify(detail)}`
|
||||
)
|
||||
return throwError(() => error)
|
||||
})
|
||||
)
|
||||
|
||||
const response = await firstValueFrom(request)
|
||||
return response.data
|
||||
} catch (e) {
|
||||
this.logger.error(`Request failed (${method} ${path})\n${e.message}`)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,10 +1,6 @@
|
||||
import { Controller, Logger } from '@nestjs/common'
|
||||
import { Controller } from '@nestjs/common'
|
||||
import { Ctx, EventPattern, Payload, RmqContext } from '@nestjs/microservices'
|
||||
import type {
|
||||
EmailChangedEvent,
|
||||
OtpRequestedEvent,
|
||||
PhoneChangedEvent
|
||||
} from '@teacinema/contracts'
|
||||
import type { OtpRequestedEvent } from '@teacinema/contracts'
|
||||
|
||||
import { RmqService } from '../../infra/rmq/rmq.service'
|
||||
|
||||
@ -12,8 +8,6 @@ import { NotificationsService } from './notifications.service'
|
||||
|
||||
@Controller()
|
||||
export class NotificationsController {
|
||||
private readonly logger = new Logger(NotificationsController.name)
|
||||
|
||||
constructor(
|
||||
private readonly notificationsService: NotificationsService,
|
||||
private readonly rmqService: RmqService
|
||||
@ -28,35 +22,7 @@ export class NotificationsController {
|
||||
await this.notificationsService.sendOtp(data)
|
||||
this.rmqService.ack(ctx)
|
||||
} catch (e) {
|
||||
this.logger.error(`OTP processing error ${e.message ?? e}}`)
|
||||
this.rmqService.nack(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
@EventPattern('account.phone.changed')
|
||||
public async phoneChangedRequested(
|
||||
@Payload() data: PhoneChangedEvent,
|
||||
@Ctx() ctx: RmqContext
|
||||
) {
|
||||
try {
|
||||
await this.notificationsService.phoneChanged(data)
|
||||
this.rmqService.ack(ctx)
|
||||
} catch (e) {
|
||||
this.logger.error(`Phone changing processing error ${e.message ?? e}}`)
|
||||
this.rmqService.nack(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
@EventPattern('account.email.changed')
|
||||
public async emaileChangedRequested(
|
||||
@Payload() data: EmailChangedEvent,
|
||||
@Ctx() ctx: RmqContext
|
||||
) {
|
||||
try {
|
||||
await this.notificationsService.emailChanged(data)
|
||||
this.rmqService.ack(ctx)
|
||||
} catch (e) {
|
||||
this.logger.error(`Email changing processing error ${e.message ?? e}}`)
|
||||
console.log('OTP processing error', e.message ?? e)
|
||||
this.rmqService.nack(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,22 +1,13 @@
|
||||
import { Module } from '@nestjs/common'
|
||||
import { ConfigService } from '@nestjs/config'
|
||||
|
||||
import { getExolveConfig } from '../../config/factories'
|
||||
import { MailModule } from '../../infra/mail/mail.module'
|
||||
import { RmqService } from '../../infra/rmq/rmq.service'
|
||||
import { SmsModule } from '../../infra/sms/sms.module'
|
||||
|
||||
import { NotificationsController } from './notifications.controller'
|
||||
import { NotificationsService } from './notifications.service'
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
MailModule,
|
||||
SmsModule.registerAsync({
|
||||
useFactory: getExolveConfig,
|
||||
inject: [ConfigService]
|
||||
})
|
||||
],
|
||||
imports: [MailModule],
|
||||
controllers: [NotificationsController],
|
||||
providers: [NotificationsService, RmqService]
|
||||
})
|
||||
|
||||
@ -1,51 +1,20 @@
|
||||
import { Injectable, Logger } from '@nestjs/common'
|
||||
import {
|
||||
EmailChangedEvent,
|
||||
OtpRequestedEvent,
|
||||
PhoneChangedEvent
|
||||
} from '@teacinema/contracts'
|
||||
import { Injectable } from '@nestjs/common'
|
||||
import { OtpRequestedEvent } from '@teacinema/contracts'
|
||||
|
||||
import { MailService } from '../../infra/mail/mail.service'
|
||||
import { SmsService } from '../../infra/sms/sms.service'
|
||||
|
||||
@Injectable()
|
||||
export class NotificationsService {
|
||||
private readonly logger = new Logger(NotificationsService.name)
|
||||
|
||||
constructor(
|
||||
private readonly mailService: MailService,
|
||||
private readonly smsService: SmsService
|
||||
) {}
|
||||
constructor(private readonly mailService: MailService) {}
|
||||
|
||||
public async sendOtp(data: OtpRequestedEvent) {
|
||||
const { identifier, type, code } = data
|
||||
|
||||
this.logger.log(`Send OTP to ${identifier} via ${type}`)
|
||||
|
||||
switch (type) {
|
||||
case 'email':
|
||||
await this.mailService.sendOtp(identifier, code)
|
||||
break
|
||||
case 'phone':
|
||||
await this.smsService.sendOtp(identifier, code)
|
||||
break
|
||||
default:
|
||||
this.logger.error(`Unknown notification type: ${type}`)
|
||||
throw new Error(`Unknown type: ${type}`)
|
||||
if (type === 'email') {
|
||||
console.log('SEND EMAIL', data)
|
||||
await this.mailService.sendOtp(identifier, code)
|
||||
} else {
|
||||
console.log('SMS')
|
||||
}
|
||||
}
|
||||
|
||||
public async phoneChanged(data: PhoneChangedEvent) {
|
||||
const { phone, code } = data
|
||||
|
||||
this.logger.log(`Change phone to ${phone} via phone`)
|
||||
return await this.smsService.sendPhoneChanged(phone, code)
|
||||
}
|
||||
|
||||
public async emailChanged(data: EmailChangedEvent) {
|
||||
const { email, code } = data
|
||||
|
||||
this.logger.log(`Change email to ${email} via email`)
|
||||
return await this.mailService.sendEmailChange(email, code)
|
||||
}
|
||||
}
|
||||
|
||||
30
yarn.lock
30
yarn.lock
@ -1025,11 +1025,6 @@
|
||||
preview-email "^3.0.19"
|
||||
pug "^3.0.2"
|
||||
|
||||
"@nestjs/axios@^4.0.1":
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@nestjs/axios/-/axios-4.0.1.tgz#7ff73f47727b67dc04410ac6e9b3329401ebbb65"
|
||||
integrity sha512-68pFJgu+/AZbWkGu65Z3r55bTsCPlgyKaV4BSG8yUAD72q1PPuyVRgUwFv6BxdnibTUHlyxm06FmYWNC+bjN7A==
|
||||
|
||||
"@nestjs/cli@^11.0.0":
|
||||
version "11.0.16"
|
||||
resolved "https://registry.yarnpkg.com/@nestjs/cli/-/cli-11.0.16.tgz#bd92b679638b5b93d0db6b9252d089412dc4e9f4"
|
||||
@ -1190,9 +1185,9 @@
|
||||
"@sinonjs/commons" "^3.0.1"
|
||||
|
||||
"@teacinema/contracts@^1.1.0":
|
||||
version "1.1.1"
|
||||
resolved "https://git.ksv741.keenetic.pro/api/packages/teacinema/npm/%40teacinema%2Fcontracts/-/1.1.1/contracts-1.1.1.tgz#69fc54e0e096cad5ecc5d8bb654d982b6c70996b"
|
||||
integrity sha512-yK3Th430sWumcnm2/jyEx8dtaA59hmCvCAnul+q/JbW+C2s/AyhBEBoxToDVxx/KKTDixDePJ3O3EnNnIHGgRw==
|
||||
version "1.1.0"
|
||||
resolved "https://git.ksv741.keenetic.pro/api/packages/teacinema/npm/%40teacinema%2Fcontracts/-/1.1.0/contracts-1.1.0.tgz#0d2f03f283dc9ee048eeb770152127f2037b7133"
|
||||
integrity sha512-x90R4R96AgrUDzOjUT7ePtc7phG3Gx61sI8/CZ1hETF3Ok/TWTHblxX/CIZVcOormBrxEh1FfU39P8/nfno91g==
|
||||
dependencies:
|
||||
"@nestjs/microservices" "^11.1.12"
|
||||
protoc "33.4.0"
|
||||
@ -2045,15 +2040,6 @@ asynckit@^0.4.0:
|
||||
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
|
||||
integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==
|
||||
|
||||
axios@^1.13.5:
|
||||
version "1.13.5"
|
||||
resolved "https://registry.yarnpkg.com/axios/-/axios-1.13.5.tgz#5e464688fa127e11a660a2c49441c009f6567a43"
|
||||
integrity sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==
|
||||
dependencies:
|
||||
follow-redirects "^1.15.11"
|
||||
form-data "^4.0.5"
|
||||
proxy-from-env "^1.1.0"
|
||||
|
||||
babel-jest@30.2.0:
|
||||
version "30.2.0"
|
||||
resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-30.2.0.tgz#fd44a1ec9552be35ead881f7381faa7d8f3b95ac"
|
||||
@ -3339,11 +3325,6 @@ flatted@^3.2.9:
|
||||
resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.3.tgz#67c8fad95454a7c7abebf74bb78ee74a44023358"
|
||||
integrity sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==
|
||||
|
||||
follow-redirects@^1.15.11:
|
||||
version "1.15.11"
|
||||
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.11.tgz#777d73d72a92f8ec4d2e410eb47352a56b8e8340"
|
||||
integrity sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==
|
||||
|
||||
foreground-child@^3.1.0:
|
||||
version "3.3.1"
|
||||
resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.3.1.tgz#32e8e9ed1b68a3497befb9ac2b6adf92a638576f"
|
||||
@ -5526,11 +5507,6 @@ proxy-addr@^2.0.7:
|
||||
forwarded "0.2.0"
|
||||
ipaddr.js "1.9.1"
|
||||
|
||||
proxy-from-env@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2"
|
||||
integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
|
||||
|
||||
pug-attrs@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/pug-attrs/-/pug-attrs-3.0.0.tgz#b10451e0348165e31fad1cc23ebddd9dc7347c41"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user