import { BadRequestException, Injectable, Logger, UnauthorizedException, } from '@nestjs/common'; import { hashSync, compareSync } from 'bcrypt'; import { JwtService } from '@nestjs/jwt'; import { LoginTicket, OAuth2Client as GoogleOAuth2Client, OAuth2Client, } from 'google-auth-library'; import { RedisClientService } from 'src/cache/cache.service'; import { EMAIL_EXISTS, EXP_FIRST_LOGIN, PHONE_EXISTS, TENANT_EXISTS, TENANT_NOTFOUND, USER_NOTFOUND, VERIFY_GOOGLE_TOKEN_FAILED, VERIFY_TENANT_FAILED, VERIFY_USER_FAILED, } from 'src/share/eCode'; import { AppConfigService } from 'src/config/app/app-config.service'; import { UserEntity } from 'src/modules/user/entities/user.entity'; import { PrismaService } from 'src/prisma/prisma.service'; import { convertShortTimeToSecond } from 'src/share/tool'; import { ChangePassword, TenantSignUp, UserConfirmSignUp, UserLogin, UserSignUp } from './dto/login.dto'; import { eUserType } from 'src/common/auth/type'; import { AuthPayload } from './decorators/auth.payload'; import { bool } from 'joi'; @Injectable() export class AuthService { private readonly logger = new Logger(AuthService.name); private readonly oAuth2client: GoogleOAuth2Client; constructor( private readonly redisService: RedisClientService, private readonly jwtService: JwtService, private readonly configService: AppConfigService, private prisma: PrismaService, ) { // const { clientId, clientSecret } = this.configService.get('googleAuth'); this.oAuth2client = new OAuth2Client({ clientId: process.env.GOOGLE_CLIENT_ID, clientSecret: process.env.GOOGLE_SECRET, }); } async generateToken( account: string, dataToken: AuthPayload, first_login = Date.now(), ) { const tid = hashSync(account, 10); const payload = { tid, ...dataToken, first_login, }; const payloadRefreshToken = { tid, ...dataToken, first_login, }; const [access_token, refresh_token] = await Promise.all([ this.jwtService.signAsync(payload, { secret: this.configService.jwtAccessSecret, expiresIn: this.configService.jwtExpiresIn, }), this.jwtService.signAsync(payloadRefreshToken, { secret: this.configService.jwtRefreshSecret, expiresIn: this.configService.jwtRefreshIn, }), ]); return { access_token, refresh_token, tid }; } async userLoginByGoogleOAuth2(token: string): Promise<{ access_token: string; refresh_token: string; user: UserEntity; }> { try { const googleLoginTicket: LoginTicket = await this.oAuth2client.verifyIdToken({ idToken: token, audience: process.env.GOOGLE_CLIENT_ID, }); const { email } = googleLoginTicket.getPayload(); let user = await this.prisma.user.findFirst({ where: { email, }, }); if (!user) { user = await this.prisma.user.create({ data: { email, fullname: email, }, }); } const { access_token, refresh_token, tid } = await this.generateToken( user.email, { id: user.id, account: user.email, type: eUserType.USER, containsPassword: Boolean(user.password), }, ); await this.redisService.markLogin(user.id, tid); return { user: user, access_token, refresh_token, }; } catch (error) { this.logger.error(error); if (error instanceof BadRequestException) { throw error; } throw new UnauthorizedException(VERIFY_GOOGLE_TOKEN_FAILED); } } async userLogin(data: UserLogin): Promise<{ access_token: string; refresh_token: string; user: UserEntity; }> { try { const user = await this.prisma.user.findFirst({ where: { username: data.username, }, }); if (!user) { throw new BadRequestException(USER_NOTFOUND); } const { access_token, refresh_token, tid } = await this.generateToken( user.username, { id: user.id, account: user.username, type: eUserType.USER, containsPassword: Boolean(user.password), }, ); // Check pass if (!compareSync(data.password, user.password)) { throw new UnauthorizedException(VERIFY_TENANT_FAILED); } await this.redisService.markLogin(user.id, tid); return { access_token, refresh_token, user: user, }; } catch (error) { this.logger.error(error); if (error instanceof BadRequestException) { throw error; } throw new UnauthorizedException(VERIFY_USER_FAILED); } } async tenantLogin(data: UserLogin): Promise<{ access_token: string; refresh_token: string; }> { try { const tenant = await this.prisma.tenant.findFirst({ where: { username: data.username, }, }); if (!tenant) { throw new BadRequestException(TENANT_NOTFOUND); } const { access_token, refresh_token, tid } = await this.generateToken( tenant.username, { id: tenant.id, account: tenant.username, type: eUserType.TENANT, containsPassword: Boolean(tenant.password), }, ); // Check pass if (!compareSync(data.password, tenant.password)) { throw new UnauthorizedException(VERIFY_TENANT_FAILED); } await this.redisService.markLogin(tenant.id, tid); return { access_token, refresh_token, }; } catch (error) { this.logger.error(error); if (error instanceof BadRequestException) { throw error; } throw new UnauthorizedException(VERIFY_TENANT_FAILED); } } async refreshToken(refreshToken: string) { let decodedRefreshToken = null; try { decodedRefreshToken = await this.jwtService.verifyAsync(refreshToken, { secret: this.configService.jwtRefreshSecret, }); const { id: sub, tid, type } = decodedRefreshToken; if (!(await this.redisService.checkRefreshTokenFound(sub, tid))) { throw new UnauthorizedException('Decoded token not found'); } //check first login const dateExp: any = new Date(decodedRefreshToken.first_login); const timeAutoLogout = this.configService.jwtRefreshIn || '30d'; dateExp.setSeconds( dateExp.getSeconds() + convertShortTimeToSecond(timeAutoLogout), ); if (Date.now() > dateExp) { const { sub, tid } = decodedRefreshToken; await this.logout(sub, tid); throw new UnauthorizedException(EXP_FIRST_LOGIN); } const user = type == eUserType.USER ? await this.prisma.user.findFirstOrThrow({ where: { email: decodedRefreshToken.account, }, }) : await this.prisma.tenant.findFirstOrThrow({ where: { username: decodedRefreshToken.account, }, }); const { access_token, refresh_token, tid: _tid, } = await this.generateToken( decodedRefreshToken.account, { id: user.id, account: type == eUserType.USER ? user.email : user.username, type, containsPassword: Boolean(user.password), }, decodedRefreshToken.first_login, ); await this.logout(sub, tid); await this.redisService.markLogin(user.id, _tid); return { access_token, refresh_token }; } catch (error) { console.log(error); this.logger.error(error.message); throw new UnauthorizedException(error.message); } } async logout(uid: string, tid: string) { try { if (!tid) return false; await this.redisService.delMarkLogin(uid, tid); return true; } catch (err) { this.logger.error(err.message); return false; } } async signUpTenant(data: TenantSignUp) { const { username, password } = data; delete data.password_confirmation; return await this.prisma.$transaction(async (tx) => { const teannt = await tx.tenant.findFirst({ where: { username, }, }); if (teannt) { throw new BadRequestException(TENANT_EXISTS); } const newTenant = await tx.tenant.create({ data: { ...data, password: hashSync(password, 10), }, }); return { ...newTenant, password: undefined }; }); } async signUpUser(data: UserSignUp) { const { username, password } = data; delete data.password_confirmation; const userQuery: any[] = [ { username: username, }, ]; if (data.phone) { userQuery.push({ phone: data.phone }); } if (data.email) { userQuery.push({ email: data.email }); } return await this.prisma.$transaction(async (tx) => { const user = await tx.user.findFirst({ where: { OR: userQuery, }, }); if (user) { if (user.username == username) { throw new BadRequestException(TENANT_EXISTS); } if (user.phone == data.phone) { throw new BadRequestException(PHONE_EXISTS); } if (user.email == data.email) { throw new BadRequestException(EMAIL_EXISTS); } throw new BadRequestException(TENANT_EXISTS); } const newUser = await tx.user.create({ data: { ...data, password: hashSync(password, 10), }, }); return { ...newUser, password: undefined }; }); } async confirmSignUpUser(data: UserConfirmSignUp) { this.logger.log('confirmSignUpUser', data); } async changePassword(data: ChangePassword, auth: AuthPayload) { const { old_password, password } = data; return await this.prisma.$transaction(async (tx) => { const user = await tx.user.findFirst({ where: { id: +auth.id, }, }); if (!user) { throw new BadRequestException(USER_NOTFOUND); } if (!user.password?.length) { return; } if (!compareSync(old_password, user.password)) { throw new UnauthorizedException(VERIFY_USER_FAILED); } const newUser = await tx.user.update({ where: { id: +auth.id, }, data: { password: hashSync(password, 10), }, }); return { ...newUser, password: undefined }; }); } }