auth-service/src/modules/auth/auth.service.ts
2026-02-08 10:54:33 +03:00

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