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, 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 { 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 { 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) } }