diff --git a/package.json b/package.json index 9b765c3..31b5751 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,8 @@ "@teacinema/common": "^1.0.0", "@teacinema/contracts": "^1.0.0", "@teacinema/passport": "^1.0.0", + "amqp-connection-manager": "^5.0.0", + "amqplib": "^0.10.9", "class-transformer": "^0.5.1", "class-validator": "^0.14.3", "dotenv-expand": "^12.0.3", diff --git a/src/app.module.ts b/src/app.module.ts index 822145e..b2d8f27 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -6,8 +6,10 @@ import { grpcEnv, passportEnv, redisEnv, + rmqEnv, telegramEnv } from './config' +import { MessagingModule } from './infra/messaging/messaging.module' import { PrismaModule } from './infra/prisma/prisma.module' import { RedisModule } from './infra/redis/redis.module' import { AccountModule } from './modules/account/account.module' @@ -20,7 +22,7 @@ import { TokenModule } from './modules/token/token.module' imports: [ ConfigModule.forRoot({ isGlobal: true, - load: [dabataseEnv, grpcEnv, redisEnv, passportEnv, telegramEnv] + load: [dabataseEnv, grpcEnv, redisEnv, passportEnv, telegramEnv, rmqEnv] }), PrismaModule, RedisModule, @@ -28,7 +30,8 @@ import { TokenModule } from './modules/token/token.module' OtpModule, AccountModule, TelegramModule, - TokenModule + TokenModule, + MessagingModule ] }) export class AppModule {} diff --git a/src/config/env/index.ts b/src/config/env/index.ts index b432198..072daf1 100644 --- a/src/config/env/index.ts +++ b/src/config/env/index.ts @@ -3,3 +3,4 @@ export * from './database.env' export * from './redis.env' export * from './passport.env' export * from './telegram.env' +export * from './rmq.env' diff --git a/src/config/env/rmq.env.ts b/src/config/env/rmq.env.ts new file mode 100644 index 0000000..c05b0eb --- /dev/null +++ b/src/config/env/rmq.env.ts @@ -0,0 +1,14 @@ +import { registerAs } from '@nestjs/config' + +import { RmqValidator } from '@/config/validators' +import { validateEnv } from '@/shared/utils' + +import { RmqConfig } from '../interfaces/rmq.interface' + +export const rmqEnv = registerAs('rmq', () => { + validateEnv(process.env, RmqValidator) + + return { + url: process.env.RMQ_URL + } +}) diff --git a/src/config/interfaces/all-configs.interface.ts b/src/config/interfaces/all-configs.interface.ts index 482afa0..82de28c 100644 --- a/src/config/interfaces/all-configs.interface.ts +++ b/src/config/interfaces/all-configs.interface.ts @@ -2,6 +2,7 @@ import { DatabaseConfig } from './database.interface' import { GrpcConfig } from './grpc.interface' import { PassportConfig } from './passport.interface' import { RedisConfig } from './redis.interface' +import { RmqConfig } from './rmq.interface' import { TelegramConfig } from './telegram.interface' export interface AllConfigs { @@ -10,4 +11,5 @@ export interface AllConfigs { redis: RedisConfig passport: PassportConfig telegram: TelegramConfig + rmq: RmqConfig } diff --git a/src/config/interfaces/rmq.interface.ts b/src/config/interfaces/rmq.interface.ts new file mode 100644 index 0000000..19abf89 --- /dev/null +++ b/src/config/interfaces/rmq.interface.ts @@ -0,0 +1,3 @@ +export interface RmqConfig { + url: string +} diff --git a/src/config/validators/index.ts b/src/config/validators/index.ts index 8edc784..f84dd43 100644 --- a/src/config/validators/index.ts +++ b/src/config/validators/index.ts @@ -3,3 +3,4 @@ export * from './database.validator' export * from './redis.validator' export * from './passport.validator' export * from './telegram.validator' +export * from './rmq.validator' diff --git a/src/config/validators/rmq.validator.ts b/src/config/validators/rmq.validator.ts new file mode 100644 index 0000000..b7f355a --- /dev/null +++ b/src/config/validators/rmq.validator.ts @@ -0,0 +1,6 @@ +import { IsString } from 'class-validator' + +export class RmqValidator { + @IsString() + public RMQ_URL: string +} diff --git a/src/infra/messaging/messaging.module.ts b/src/infra/messaging/messaging.module.ts new file mode 100644 index 0000000..7f4ad87 --- /dev/null +++ b/src/infra/messaging/messaging.module.ts @@ -0,0 +1,32 @@ +import { Global, Module } from '@nestjs/common' +import { ConfigService } from '@nestjs/config' +import { ClientsModule, Transport } from '@nestjs/microservices' + +import { AllConfigs } from '@/config' + +import { MessagingService } from './messaging.service' + +@Global() +@Module({ + imports: [ + ClientsModule.registerAsync([ + { + name: 'NOTIFICATION_CLIENT', + useFactory: (configService: ConfigService) => ({ + transport: Transport.RMQ, + options: { + urls: [configService.get('rmq.url', { infer: true })], + queue: 'notifications_queue', + queueOptions: { + durable: false + } + } + }), + inject: [ConfigService] + } + ]) + ], + providers: [MessagingService], + exports: [MessagingService] +}) +export class MessagingModule {} diff --git a/src/infra/messaging/messaging.service.ts b/src/infra/messaging/messaging.service.ts new file mode 100644 index 0000000..e5cd658 --- /dev/null +++ b/src/infra/messaging/messaging.service.ts @@ -0,0 +1,14 @@ +import { Inject, Injectable } from '@nestjs/common' +import { ClientProxy } from '@nestjs/microservices' +import { OtpRequestedEvent } from '@teacinema/contracts' + +@Injectable() +export class MessagingService { + constructor( + @Inject('NOTIFICATION_CLIENT') private readonly client: ClientProxy + ) {} + + public otpRequested(data: OtpRequestedEvent) { + return this.client.emit('auth.otp.requested', data) + } +} diff --git a/src/modules/auth/auth.service.ts b/src/modules/auth/auth.service.ts index edd62b1..5bea759 100644 --- a/src/modules/auth/auth.service.ts +++ b/src/modules/auth/auth.service.ts @@ -11,6 +11,7 @@ import { VerifyOtpResponse } from '@teacinema/contracts/gen/auth' +import { MessagingService } from '@/infra/messaging/messaging.service' import { OtpService } from '@/modules/otp/otp.service' import { UserRepository } from '@/shared/utils/repositories' @@ -21,7 +22,8 @@ export class AuthService { public constructor( private readonly userRepository: UserRepository, private readonly otpService: OtpService, - private readonly tokenService: TokenService + private readonly tokenService: TokenService, + private readonly messagingService: MessagingService ) {} public async sendOtp(data: SendOtpRequest): Promise { @@ -44,11 +46,15 @@ export class AuthService { }) } - const code = await this.otpService.send( + const { code } = await this.otpService.send( identifier, type as 'email' | 'phone' ) - console.debug('CODE', code) + this.messagingService.otpRequested({ + code: code.toString(), + identifier, + type: type as 'email' | 'phone' + }) return { ok: true } } diff --git a/yarn.lock b/yarn.lock index 6d30dc2..7243952 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1383,9 +1383,9 @@ integrity sha512-q3DURJbSk3k8MNWFIYaSM4LEcBgPbWa+HJmBz/nzYT4kuYitJVSXxpZ97kr0Ea+81AZwAU3JhQKlkK0SBWUi0A== "@teacinema/contracts@^1.0.0": - version "1.0.9" - resolved "https://git.ksv741.keenetic.pro/api/packages/teacinema/npm/%40teacinema%2Fcontracts/-/1.0.9/contracts-1.0.9.tgz#14b136d0c08552147b0de637a7af6d340d538527" - integrity sha512-9BwWvXaDKgRcef6+1ep9vUDWBdOndqheeTX5mXK5OOvLXbLOlzYoqAUoT0ZwaRwOCD+Je/LDrqcdti6eoss71Q== + 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" @@ -2087,6 +2087,21 @@ ajv@^6.12.4, ajv@^6.12.5: json-schema-traverse "^0.4.1" uri-js "^4.2.2" +amqp-connection-manager@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/amqp-connection-manager/-/amqp-connection-manager-5.0.0.tgz#3bf72f76e1e041768080227fffab1f9baea70baa" + integrity sha512-88yQzqa5RSBgnLl504XjvCQJ7d+osskdwvg35Lwm1LRbfLjNU9p7SQUMSP82BB7mseiq9tIUPJ3HE3eXQbpjEw== + dependencies: + promise-breaker "^6.0.0" + +amqplib@^0.10.9: + version "0.10.9" + resolved "https://registry.yarnpkg.com/amqplib/-/amqplib-0.10.9.tgz#5b744c21d624f9307d0399e4d339b7354675831c" + integrity sha512-jwSftI4QjS3mizvnSnOrPGYiUnm1vI2OP1iXeOUz5pb74Ua0nbf6nPyyTzuiCLEE3fMpaJORXh2K/TQ08H5xGA== + dependencies: + buffer-more-ints "~1.0.0" + url-parse "~1.5.10" + ansi-colors@4.1.3: version "4.1.3" resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.3.tgz#37611340eb2243e70cc604cad35d63270d48781b" @@ -2332,6 +2347,11 @@ buffer-from@^1.0.0: resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== +buffer-more-ints@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/buffer-more-ints/-/buffer-more-ints-1.0.0.tgz#ef4f8e2dddbad429ed3828a9c55d44f05c611422" + integrity sha512-EMetuGFz5SLsT0QTnXzINh4Ksr+oo4i+UGTXEshiGCQWnsgSs7ZhJ8fzlwQ+OzEMs0MpDAMr1hxnblp5a4vcHg== + buffer@^5.5.0: version "5.7.1" resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" @@ -4897,6 +4917,11 @@ prisma@^7.3.0: mysql2 "3.15.3" postgres "3.4.7" +promise-breaker@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/promise-breaker/-/promise-breaker-6.0.0.tgz#107d2b70f161236abdb4ac5a736c7eb8df489d0f" + integrity sha512-BthzO9yTPswGf7etOBiHCVuugs2N01/Q/94dIPls48z2zCmrnDptUUZzfIb+41xq0MnYZ/BzmOd6ikDR4ibNZA== + proper-lockfile@4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/proper-lockfile/-/proper-lockfile-4.1.2.tgz#c8b9de2af6b2f1601067f98e01ac66baa223141f" @@ -4959,6 +4984,11 @@ qs@^6.14.0, qs@^6.14.1: dependencies: side-channel "^1.1.0" +querystringify@^2.1.1: + version "2.2.0" + resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6" + integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ== + randombytes@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" @@ -5045,6 +5075,11 @@ require-from-string@^2.0.2: resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== +requires-port@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" + integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ== + resolve-cwd@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d" @@ -5773,6 +5808,14 @@ uri-js@^4.2.2: dependencies: punycode "^2.1.0" +url-parse@~1.5.10: + version "1.5.10" + resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.10.tgz#9d3c2f736c1d75dd3bd2be507dcc111f1e2ea9c1" + integrity sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ== + dependencies: + querystringify "^2.1.1" + requires-port "^1.0.0" + util-deprecate@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"