136 lines
3.6 KiB
TypeScript
136 lines
3.6 KiB
TypeScript
import { Injectable } from '@nestjs/common'
|
|
import { ConfigService } from '@nestjs/config'
|
|
import { RpcException } from '@nestjs/microservices'
|
|
import { Account } from '@prisma/generated/client'
|
|
import { RpcStatus } from '@teacinema/common'
|
|
import {
|
|
type RefreshRequest,
|
|
type RefreshResponse,
|
|
SendOtpRequest,
|
|
SendOtpResponse,
|
|
VerifyOtpRequest,
|
|
VerifyOtpResponse
|
|
} from '@teacinema/contracts/gen/auth'
|
|
import { PassportService } from '@teacinema/passport'
|
|
|
|
import { AllConfigs } from '@/config'
|
|
import { OtpService } from '@/modules/otp/otp.service'
|
|
|
|
import { AuthRepository } from './auth.repository'
|
|
|
|
@Injectable()
|
|
export class AuthService {
|
|
private readonly ACCESS_TOKEN_TTL: number
|
|
private readonly REFRESH_TOKEN_TTL: number
|
|
|
|
public constructor(
|
|
private readonly configService: ConfigService<AllConfigs>,
|
|
private readonly authRepository: AuthRepository,
|
|
private readonly otpService: OtpService,
|
|
private readonly passportService: PassportService
|
|
) {
|
|
this.ACCESS_TOKEN_TTL = configService.get('passport.accessTtl', {
|
|
infer: true
|
|
})
|
|
this.REFRESH_TOKEN_TTL = configService.get('passport.refreshTtl', {
|
|
infer: true
|
|
})
|
|
}
|
|
|
|
public async sendOtp(data: SendOtpRequest): Promise<SendOtpResponse> {
|
|
const { identifier, type } = data
|
|
|
|
let account: Account | null = null
|
|
const isPhoneType = type === 'phone'
|
|
const isEmailType = type === 'email'
|
|
|
|
if (isPhoneType) {
|
|
account = await this.authRepository.findByPhone(identifier)
|
|
} else {
|
|
account = await this.authRepository.findByEmail(identifier)
|
|
}
|
|
|
|
if (!account) {
|
|
account = await this.authRepository.createAccount({
|
|
email: isEmailType ? identifier : undefined,
|
|
phone: isPhoneType ? identifier : undefined
|
|
})
|
|
}
|
|
|
|
const code = await this.otpService.send(
|
|
identifier,
|
|
type as 'email' | 'phone'
|
|
)
|
|
console.debug('CODE', code)
|
|
|
|
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({
|
|
code: RpcStatus.NOT_FOUND,
|
|
details: '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 this.generateTokens(account.id)
|
|
}
|
|
|
|
private generateTokens(userId: Account['id']) {
|
|
const payload = {
|
|
sub: userId,
|
|
iat: Date.now(),
|
|
exp: Date.now() + this.ACCESS_TOKEN_TTL
|
|
}
|
|
const accessToken = this.passportService.generate(
|
|
payload.sub,
|
|
this.ACCESS_TOKEN_TTL
|
|
)
|
|
const refreshToken = this.passportService.generate(
|
|
payload.sub,
|
|
this.REFRESH_TOKEN_TTL
|
|
)
|
|
|
|
return { accessToken, refreshToken }
|
|
}
|
|
|
|
public refresh(data: RefreshRequest): RefreshResponse {
|
|
const { refreshToken } = data
|
|
const result = this.passportService.verify(refreshToken)
|
|
|
|
if (!result.valid) {
|
|
throw new RpcException({
|
|
code: RpcStatus.UNAUTHENTICATED,
|
|
// @ts-ignore
|
|
details: result.reason
|
|
})
|
|
}
|
|
|
|
return this.generateTokens(result.userId)
|
|
}
|
|
}
|