|
|
import { compare, hashSync } from 'bcrypt'; |
|
|
import { Redis } from 'ioredis'; |
|
|
import * as Joi from 'joi'; |
|
|
import { CustomPrismaService } from 'nestjs-prisma'; |
|
|
|
|
|
import { InjectRedis } from '@liaoliaots/nestjs-redis'; |
|
|
import { Inject, Injectable } from '@nestjs/common'; |
|
|
import { Role } from '@prisma/client'; |
|
|
|
|
|
import { BizException } from '@/common/exceptions/biz.exception'; |
|
|
import { EmailService } from '@/libs/email/email.service'; |
|
|
import { JwtService } from '@/libs/jwt/jwt.service'; |
|
|
import { SmsService } from '@/libs/sms/sms.service'; |
|
|
import { ExtendedPrismaClient } from '@/processors/database/prisma.extension'; |
|
|
|
|
|
import { IAccountStatus } from 'shared'; |
|
|
import { ErrorCodeEnum } from 'shared/dist/error-code'; |
|
|
|
|
|
type ByPassword = { |
|
|
identity: string; |
|
|
password: string; |
|
|
}; |
|
|
|
|
|
const SALT_ROUNDS = 10; |
|
|
|
|
|
const emailSchema = Joi.string().email().required(); |
|
|
const phoneSchema = Joi.string() |
|
|
.pattern(/^[0-9]{11}$/) |
|
|
.required(); |
|
|
|
|
|
const getPhoneOrEmail = (identity: string) => { |
|
|
const emailValidation = emailSchema.validate(identity); |
|
|
const phoneValidation = phoneSchema.validate(identity); |
|
|
|
|
|
if (!emailValidation.error) { |
|
|
return { email: identity.trim().toLowerCase(), phone: undefined }; |
|
|
} else if (!phoneValidation.error) { |
|
|
return { email: undefined, phone: identity.trim() }; |
|
|
} else { |
|
|
throw Error('Invalid identity'); |
|
|
} |
|
|
}; |
|
|
|
|
|
function generateRandomSixDigitNumber() { |
|
|
const min = 100000; |
|
|
const max = 999999; |
|
|
return Math.floor(Math.random() * (max - min + 1)) + min; |
|
|
} |
|
|
|
|
|
@Injectable() |
|
|
export class AuthService { |
|
|
constructor( |
|
|
@InjectRedis() private readonly redis: Redis, |
|
|
@Inject('PrismaService') |
|
|
private prisma: CustomPrismaService<ExtendedPrismaClient>, |
|
|
private jwt: JwtService, |
|
|
private emailService: EmailService, |
|
|
private smsService: SmsService, |
|
|
) {} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async #signWithCheck(user: any): Promise<{ |
|
|
token: string; |
|
|
status: IAccountStatus; |
|
|
}> { |
|
|
let status: IAccountStatus = 'ok'; |
|
|
if (!user.email && !user.phone) { |
|
|
status = 'bind'; |
|
|
} else if (!user.password) { |
|
|
status = 'password'; |
|
|
} |
|
|
return { |
|
|
token: await this.jwt.sign({ id: user.id, role: user.role }), |
|
|
status, |
|
|
}; |
|
|
} |
|
|
|
|
|
async #verifyCode(identity: string, code: string) { |
|
|
const isValid = (await this.redis.get(identity)) === code; |
|
|
|
|
|
if (!isValid) { |
|
|
throw new BizException(ErrorCodeEnum.CodeValidationError); |
|
|
} else { |
|
|
await this.redis.del(identity); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
async newValidateCode(identity: string) { |
|
|
const { email, phone } = getPhoneOrEmail(identity); |
|
|
if (!email && !phone) { |
|
|
return { |
|
|
success: false, |
|
|
}; |
|
|
} |
|
|
const ttl = await this.redis.ttl(identity); |
|
|
|
|
|
if (600 - ttl < 60) { |
|
|
return { |
|
|
success: false, |
|
|
ttl, |
|
|
}; |
|
|
} else { |
|
|
const newTtl = 10 * 60; |
|
|
const code = generateRandomSixDigitNumber(); |
|
|
await this.redis.setex(identity, newTtl, code); |
|
|
|
|
|
if (email) { |
|
|
await this.emailService.sendCode(identity, code); |
|
|
} else if (phone) { |
|
|
await this.smsService.sendCode(identity, code); |
|
|
} |
|
|
return { |
|
|
success: true, |
|
|
ttl: newTtl, |
|
|
}; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
async WithValidateCode(identity: string, code: string) { |
|
|
const { email, phone } = getPhoneOrEmail(identity); |
|
|
|
|
|
await this.#verifyCode(identity, code); |
|
|
|
|
|
const existUser = await this.prisma.client.user.findMany({ |
|
|
where: { |
|
|
OR: [{ email }, { phone }], |
|
|
}, |
|
|
}); |
|
|
|
|
|
let user; |
|
|
if (existUser.length != 1) { |
|
|
|
|
|
user = await this.prisma.client.user.create({ |
|
|
data: { |
|
|
email: email, |
|
|
phone: phone, |
|
|
role: Role.User, |
|
|
}, |
|
|
}); |
|
|
} else { |
|
|
user = existUser[0]; |
|
|
} |
|
|
return this.#signWithCheck(user); |
|
|
} |
|
|
|
|
|
|
|
|
async loginPassword({ identity, password }: ByPassword) { |
|
|
const { email, phone } = getPhoneOrEmail(identity); |
|
|
|
|
|
const user = await this.prisma.client.user.findMany({ |
|
|
where: { |
|
|
OR: [{ email }, { phone }], |
|
|
}, |
|
|
}); |
|
|
if (user.length != 1) { |
|
|
throw Error('User does not exist'); |
|
|
} |
|
|
const isPasswordCorrect = await compare(password, user[0].password); |
|
|
if (!isPasswordCorrect) { |
|
|
throw Error('Password is incorrect'); |
|
|
} |
|
|
return this.#signWithCheck(user[0]); |
|
|
} |
|
|
|
|
|
|
|
|
async bindPassword(userId: number, password: string) { |
|
|
const user = await this.prisma.client.user.findUniqueOrThrow({ |
|
|
where: { |
|
|
id: userId, |
|
|
}, |
|
|
}); |
|
|
if (user.password) { |
|
|
throw Error('Password already exists'); |
|
|
} |
|
|
return await this.prisma.client.user.update({ |
|
|
where: { |
|
|
id: userId, |
|
|
}, |
|
|
data: { |
|
|
password: hashSync(password, SALT_ROUNDS), |
|
|
}, |
|
|
}); |
|
|
} |
|
|
|
|
|
async updateName(userId: number, name: string) { |
|
|
await this.prisma.client.user.update({ |
|
|
where: { |
|
|
id: userId, |
|
|
}, |
|
|
data: { |
|
|
name: name, |
|
|
}, |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
async changePassword(userId: number, password: string) { |
|
|
await this.prisma.client.user.update({ |
|
|
where: { |
|
|
id: userId, |
|
|
}, |
|
|
data: { |
|
|
password: hashSync(password, SALT_ROUNDS), |
|
|
}, |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
async forgetPassword(identity: string, code: string, password: string) { |
|
|
const { email, phone } = getPhoneOrEmail(identity); |
|
|
|
|
|
await this.#verifyCode(identity, code); |
|
|
|
|
|
const existUser = await this.prisma.client.user.findMany({ |
|
|
where: { |
|
|
OR: [{ email }, { phone }], |
|
|
}, |
|
|
}); |
|
|
|
|
|
let user; |
|
|
if (existUser.length != 1) { |
|
|
throw new BizException(ErrorCodeEnum.UserNotExist); |
|
|
} else { |
|
|
user = existUser[0]; |
|
|
} |
|
|
await this.changePassword(user.id, password); |
|
|
|
|
|
return this.#signWithCheck(user); |
|
|
} |
|
|
|
|
|
|
|
|
async bindIdentity(userId: number, identity: string, password?: string) { |
|
|
const { email, phone } = getPhoneOrEmail(identity); |
|
|
|
|
|
const user = await this.prisma.client.user.findUniqueOrThrow({ |
|
|
where: { |
|
|
id: userId, |
|
|
}, |
|
|
}); |
|
|
|
|
|
if (email) { |
|
|
if (user.email) { |
|
|
throw new BizException(ErrorCodeEnum.BindEmailExist); |
|
|
} |
|
|
await this.prisma.client.user.update({ |
|
|
where: { |
|
|
id: userId, |
|
|
}, |
|
|
data: { |
|
|
email: email, |
|
|
}, |
|
|
}); |
|
|
} |
|
|
|
|
|
if (phone) { |
|
|
if (user.phone) { |
|
|
throw new BizException(ErrorCodeEnum.BindPhoneExist); |
|
|
} |
|
|
await this.prisma.client.user.update({ |
|
|
where: { |
|
|
id: userId, |
|
|
}, |
|
|
data: { |
|
|
phone: phone, |
|
|
}, |
|
|
}); |
|
|
} |
|
|
|
|
|
if (password) { |
|
|
await this.prisma.client.user.update({ |
|
|
where: { |
|
|
id: userId, |
|
|
}, |
|
|
data: { |
|
|
password: hashSync(password, SALT_ROUNDS), |
|
|
}, |
|
|
}); |
|
|
} |
|
|
} |
|
|
} |
|
|
|