feat: add otp code sending and verification
This commit is contained in:
parent
62b06982ae
commit
bbbfb889ff
@ -4,13 +4,15 @@ import { ConfigModule } from '@nestjs/config'
|
|||||||
import { PrismaModule } from './infra/prisma/prisma.module'
|
import { PrismaModule } from './infra/prisma/prisma.module'
|
||||||
import { RedisModule } from './infra/redis/redis.module'
|
import { RedisModule } from './infra/redis/redis.module'
|
||||||
import { AuthModule } from './modules/auth/auth.module'
|
import { AuthModule } from './modules/auth/auth.module'
|
||||||
|
import { OtpModule } from './modules/otp/otp.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
ConfigModule.forRoot({ isGlobal: true }),
|
ConfigModule.forRoot({ isGlobal: true }),
|
||||||
PrismaModule,
|
PrismaModule,
|
||||||
RedisModule,
|
RedisModule,
|
||||||
AuthModule
|
AuthModule,
|
||||||
|
OtpModule
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class AppModule {}
|
export class AppModule {}
|
||||||
|
|||||||
@ -2,7 +2,9 @@ import { Controller } from '@nestjs/common'
|
|||||||
import { GrpcMethod } from '@nestjs/microservices'
|
import { GrpcMethod } from '@nestjs/microservices'
|
||||||
import type {
|
import type {
|
||||||
SendOtpRequest,
|
SendOtpRequest,
|
||||||
SendOtpResponse
|
SendOtpResponse,
|
||||||
|
VerifyOtpRequest,
|
||||||
|
VerifyOtpResponse
|
||||||
} from '@teacinema/contracts/gen/auth'
|
} from '@teacinema/contracts/gen/auth'
|
||||||
|
|
||||||
import { AuthService } from './auth.service'
|
import { AuthService } from './auth.service'
|
||||||
@ -15,4 +17,9 @@ export class AuthController {
|
|||||||
public sendOtp(data: SendOtpRequest): Promise<SendOtpResponse> {
|
public sendOtp(data: SendOtpRequest): Promise<SendOtpResponse> {
|
||||||
return this.authService.sendOtp(data)
|
return this.authService.sendOtp(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GrpcMethod('AuthService', 'VerifyOtp')
|
||||||
|
public VerifyOtp(data: VerifyOtpRequest): Promise<VerifyOtpResponse> {
|
||||||
|
return this.authService.verifyOtp(data)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,11 +1,13 @@
|
|||||||
import { Module } from '@nestjs/common'
|
import { Module } from '@nestjs/common'
|
||||||
|
|
||||||
|
import { OtpService } from '@/modules/otp/otp.service'
|
||||||
|
|
||||||
import { AuthController } from './auth.controller'
|
import { AuthController } from './auth.controller'
|
||||||
import { AuthRepository } from './auth.repository'
|
import { AuthRepository } from './auth.repository'
|
||||||
import { AuthService } from './auth.service'
|
import { AuthService } from './auth.service'
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
controllers: [AuthController],
|
controllers: [AuthController],
|
||||||
providers: [AuthService, AuthRepository]
|
providers: [AuthService, AuthRepository, OtpService]
|
||||||
})
|
})
|
||||||
export class AuthModule {}
|
export class AuthModule {}
|
||||||
|
|||||||
@ -1,6 +1,9 @@
|
|||||||
import { Injectable } from '@nestjs/common'
|
import { Injectable } from '@nestjs/common'
|
||||||
import { Account } from '@prisma/generated/client'
|
import { Account } from '@prisma/generated/client'
|
||||||
import { AccountCreateInput } from '@prisma/generated/models/Account'
|
import {
|
||||||
|
AccountCreateInput,
|
||||||
|
AccountUpdateInput
|
||||||
|
} from '@prisma/generated/models/Account'
|
||||||
|
|
||||||
import { PrismaService } from '@/infra/prisma/prisma.service'
|
import { PrismaService } from '@/infra/prisma/prisma.service'
|
||||||
|
|
||||||
@ -23,4 +26,11 @@ export class AuthRepository {
|
|||||||
public createAccount(data: AccountCreateInput) {
|
public createAccount(data: AccountCreateInput) {
|
||||||
return this.prismaService.account.create({ data })
|
return this.prismaService.account.create({ data })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public updateAccount(id: Account['id'], data: AccountUpdateInput) {
|
||||||
|
return this.prismaService.account.update({
|
||||||
|
where: { id },
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,12 +1,23 @@
|
|||||||
import { Injectable } from '@nestjs/common'
|
import { Injectable } from '@nestjs/common'
|
||||||
|
import { RpcException } from '@nestjs/microservices'
|
||||||
import { Account } from '@prisma/generated/client'
|
import { Account } from '@prisma/generated/client'
|
||||||
import { SendOtpRequest, SendOtpResponse } from '@teacinema/contracts/gen/auth'
|
import {
|
||||||
|
SendOtpRequest,
|
||||||
|
SendOtpResponse,
|
||||||
|
VerifyOtpRequest,
|
||||||
|
VerifyOtpResponse
|
||||||
|
} from '@teacinema/contracts/gen/auth'
|
||||||
|
|
||||||
|
import { OtpService } from '@/modules/otp/otp.service'
|
||||||
|
|
||||||
import { AuthRepository } from './auth.repository'
|
import { AuthRepository } from './auth.repository'
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AuthService {
|
export class AuthService {
|
||||||
public constructor(private readonly authRepository: AuthRepository) {}
|
public constructor(
|
||||||
|
private readonly authRepository: AuthRepository,
|
||||||
|
private readonly otpService: OtpService
|
||||||
|
) {}
|
||||||
|
|
||||||
public async sendOtp(data: SendOtpRequest): Promise<SendOtpResponse> {
|
public async sendOtp(data: SendOtpRequest): Promise<SendOtpResponse> {
|
||||||
const { identifier, type } = data
|
const { identifier, type } = data
|
||||||
@ -28,6 +39,43 @@ export class AuthService {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const code = await this.otpService.send(
|
||||||
|
identifier,
|
||||||
|
type as 'email' | 'phone'
|
||||||
|
)
|
||||||
|
console.debug('CODE', code)
|
||||||
|
|
||||||
return { ok: true }
|
return { ok: true }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async verifyOtp(data: VerifyOtpRequest): Promise<VerifyOtpResponse> {
|
||||||
|
const { identifier, code, type } = data
|
||||||
|
|
||||||
|
await this.otpService.verify(identifier, code, type as 'email' | 'phone')
|
||||||
|
|
||||||
|
const isPhoneType = type === 'phone'
|
||||||
|
const isEmailType = type === 'email'
|
||||||
|
|
||||||
|
const account = isPhoneType
|
||||||
|
? await this.authRepository.findByPhone(identifier)
|
||||||
|
: await this.authRepository.findByEmail(identifier)
|
||||||
|
|
||||||
|
if (!account) {
|
||||||
|
throw new RpcException('Account not found')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPhoneType && !account.isPhoneVerified) {
|
||||||
|
await this.authRepository.updateAccount(account.id, {
|
||||||
|
isPhoneVerified: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isEmailType && !account.isEmailVerified) {
|
||||||
|
await this.authRepository.updateAccount(account.id, {
|
||||||
|
isEmailVerified: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return { accessToken: '123456', refreshToken: '123456' }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
8
src/modules/otp/otp.module.ts
Normal file
8
src/modules/otp/otp.module.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { Module } from '@nestjs/common'
|
||||||
|
|
||||||
|
import { OtpService } from './otp.service'
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
providers: [OtpService]
|
||||||
|
})
|
||||||
|
export class OtpModule {}
|
||||||
52
src/modules/otp/otp.service.ts
Normal file
52
src/modules/otp/otp.service.ts
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import { Injectable } from '@nestjs/common'
|
||||||
|
import { RpcException } from '@nestjs/microservices'
|
||||||
|
import { createHash } from 'node:crypto'
|
||||||
|
|
||||||
|
import { RedisService } from '@/infra/redis/redis.service'
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class OtpService {
|
||||||
|
public constructor(private readonly redisService: RedisService) {}
|
||||||
|
|
||||||
|
public async send(indentifier: string, type: 'email' | 'phone') {
|
||||||
|
const { code, hash } = this.generateCode()
|
||||||
|
|
||||||
|
await this.redisService.set(
|
||||||
|
`otp:${type}:${indentifier}`,
|
||||||
|
hash,
|
||||||
|
'EX',
|
||||||
|
60 * 5
|
||||||
|
)
|
||||||
|
|
||||||
|
return code
|
||||||
|
}
|
||||||
|
|
||||||
|
public async verify(
|
||||||
|
indentifier: string,
|
||||||
|
code: string,
|
||||||
|
type: 'email' | 'phone'
|
||||||
|
) {
|
||||||
|
const stroredHash = await this.redisService.get(
|
||||||
|
`otp:${type}:${indentifier}`
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!stroredHash) {
|
||||||
|
throw new RpcException('Invalid or expired code')
|
||||||
|
}
|
||||||
|
|
||||||
|
const hash = createHash('sha256').update(code).digest('hex')
|
||||||
|
if (stroredHash !== hash) {
|
||||||
|
throw new RpcException('Invalid or expired code')
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.redisService.del(`otp:${type}:${indentifier}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateCode() {
|
||||||
|
// todo посмотреть готовые бибилотеки для генерации friendly otp
|
||||||
|
const code = Math.floor(100_000 + Math.random() * 900_000)
|
||||||
|
const hash = createHash('sha256').update(String(code)).digest('hex')
|
||||||
|
|
||||||
|
return { code, hash }
|
||||||
|
}
|
||||||
|
}
|
||||||
19
yarn.lock
19
yarn.lock
@ -1355,9 +1355,9 @@
|
|||||||
integrity sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==
|
integrity sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==
|
||||||
|
|
||||||
"@teacinema/contracts@^1.0.0":
|
"@teacinema/contracts@^1.0.0":
|
||||||
version "1.0.0"
|
version "1.0.1"
|
||||||
resolved "https://git.ksv741.keenetic.pro/api/packages/teacinema/npm/%40teacinema%2Fcontracts/-/1.0.0/contracts-1.0.0.tgz#9395c331ec94b604d1946d26b683dd93e04a6225"
|
resolved "https://git.ksv741.keenetic.pro/api/packages/teacinema/npm/%40teacinema%2Fcontracts/-/1.0.1/contracts-1.0.1.tgz#25021fecb1d33d0505fd11ca3d8201660cb1bcbe"
|
||||||
integrity sha512-zhYE0SIWjJl+ujp+yf0dT3ua4hWpR19gy//CwVcLO9bvfh+bZRYttlXE6XXWGucHv6208+sNNdkpwSwVyKyqNg==
|
integrity sha512-gvdbQ3Xowr4/h9Kq2P+pO2lr9vyWvIvRoU74mG3LSq2IiKKuwQUTxRaJnzI5MdtNqiq0WIUMo30cfqnBzNAJ1A==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@nestjs/microservices" "^11.1.12"
|
"@nestjs/microservices" "^11.1.12"
|
||||||
protoc "33.4.0"
|
protoc "33.4.0"
|
||||||
@ -1365,9 +1365,9 @@
|
|||||||
ts-proto "2.11.0"
|
ts-proto "2.11.0"
|
||||||
|
|
||||||
"@teacinema/core@^1.0.8":
|
"@teacinema/core@^1.0.8":
|
||||||
version "1.0.8"
|
version "1.0.9"
|
||||||
resolved "https://git.ksv741.keenetic.pro/api/packages/teacinema/npm/%40teacinema%2Fcore/-/1.0.8/core-1.0.8.tgz#928bd0e84be7857c479f3e914b36c457e3922779"
|
resolved "https://git.ksv741.keenetic.pro/api/packages/teacinema/npm/%40teacinema%2Fcore/-/1.0.9/core-1.0.9.tgz#deec0675c6cc03fe2511a6b8dc5d6b6763998708"
|
||||||
integrity sha512-cd8mArdF7vzScaDAP7QRD0N91oT9kZdc/J32HU4LSyyr+y600CBtC7PhDCHrDiWte22mqirw27cKD7ujW1eoTA==
|
integrity sha512-HM6JMOsvWL8xC2RxabIk4zdNjfRsBf9Gc9UEGk7O7X/ZLEvnt7hOVE7p3gsTuMQXeUK4qNjltm2vOU5U8M2ufA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@trivago/prettier-plugin-sort-imports" "^5.2.2"
|
"@trivago/prettier-plugin-sort-imports" "^5.2.2"
|
||||||
|
|
||||||
@ -4188,11 +4188,16 @@ lodash.merge@^4.6.2:
|
|||||||
resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
|
resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
|
||||||
integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==
|
integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==
|
||||||
|
|
||||||
lodash@4.17.21, lodash@^4.17.21:
|
lodash@4.17.21:
|
||||||
version "4.17.21"
|
version "4.17.21"
|
||||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
|
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
|
||||||
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
|
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
|
||||||
|
|
||||||
|
lodash@^4.17.21:
|
||||||
|
version "4.17.23"
|
||||||
|
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.23.tgz#f113b0378386103be4f6893388c73d0bde7f2c5a"
|
||||||
|
integrity sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==
|
||||||
|
|
||||||
log-symbols@^4.1.0:
|
log-symbols@^4.1.0:
|
||||||
version "4.1.0"
|
version "4.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503"
|
resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user