mirror of
https://github.com/HamzaSha1/zod-backend.git
synced 2025-07-16 18:36:18 +00:00
Merge branch 'waiting-list' into feat/neoleap-integration
This commit is contained in:
@ -6,11 +6,12 @@ import { AccessTokenGuard } from '~/common/guards';
|
||||
import { ApiDataResponse, ApiLangRequestHeader } from '~/core/decorators';
|
||||
import { ResponseFactory } from '~/core/utils';
|
||||
import {
|
||||
AppleLoginRequestDto,
|
||||
CreateUnverifiedUserRequestDto,
|
||||
DisableBiometricRequestDto,
|
||||
EnableBiometricRequestDto,
|
||||
ForgetPasswordRequestDto,
|
||||
LoginRequestDto,
|
||||
GoogleLoginRequestDto,
|
||||
RefreshTokenRequestDto,
|
||||
SendForgetPasswordOtpRequestDto,
|
||||
SendLoginOtpRequestDto,
|
||||
@ -57,6 +58,22 @@ export class AuthController {
|
||||
return ResponseFactory.data(new LoginResponseDto(token, user));
|
||||
}
|
||||
|
||||
@Post('login/google')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ApiDataResponse(LoginResponseDto)
|
||||
async loginWithGoogle(@Body() data: GoogleLoginRequestDto) {
|
||||
const [token, user] = await this.authService.loginWithGoogle(data);
|
||||
return ResponseFactory.data(new LoginResponseDto(token, user));
|
||||
}
|
||||
|
||||
@Post('login/apple')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ApiDataResponse(LoginResponseDto)
|
||||
async loginWithApple(@Body() data: AppleLoginRequestDto) {
|
||||
const [token, user] = await this.authService.loginWithApple(data);
|
||||
return ResponseFactory.data(new LoginResponseDto(token, user));
|
||||
}
|
||||
|
||||
@Post('register/set-email')
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
@UseGuards(AccessTokenGuard)
|
||||
@ -128,12 +145,6 @@ export class AuthController {
|
||||
return ResponseFactory.data(new LoginResponseDto(res, user));
|
||||
}
|
||||
|
||||
@Post('login')
|
||||
async login(@Body() loginDto: LoginRequestDto) {
|
||||
const [res, user] = await this.authService.login(loginDto);
|
||||
return ResponseFactory.data(new LoginResponseDto(res, user));
|
||||
}
|
||||
|
||||
@Post('logout')
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
@UseGuards(AccessTokenGuard)
|
||||
|
14
src/auth/dtos/request/apple-additional-data.request.dto.ts
Normal file
14
src/auth/dtos/request/apple-additional-data.request.dto.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsNotEmpty, IsString } from 'class-validator';
|
||||
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
|
||||
export class AppleAdditionalData {
|
||||
@ApiProperty({ example: 'Ahmad' })
|
||||
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'customer.firstName' }) })
|
||||
@IsNotEmpty({ message: i18n('validation.IsNotEmpty', { path: 'general', property: 'customer.firstName' }) })
|
||||
firstName!: string;
|
||||
|
||||
@ApiProperty({ example: 'Khan' })
|
||||
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'customer.lastName' }) })
|
||||
@IsNotEmpty({ message: i18n('validation.IsNotEmpty', { path: 'general', property: 'customer.lastName' }) })
|
||||
lastName!: string;
|
||||
}
|
21
src/auth/dtos/request/apple-login.request.dto.ts
Normal file
21
src/auth/dtos/request/apple-login.request.dto.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { Type } from 'class-transformer';
|
||||
import { IsNotEmpty, IsOptional, IsString, ValidateNested } from 'class-validator';
|
||||
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
|
||||
import { AppleAdditionalData } from './apple-additional-data.request.dto';
|
||||
|
||||
export class AppleLoginRequestDto {
|
||||
@ApiProperty({ example: 'apple_token' })
|
||||
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'auth.appleToken' }) })
|
||||
@IsNotEmpty({ message: i18n('validation.IsNotEmpty', { path: 'general', property: 'auth.appleToken' }) })
|
||||
appleToken!: string;
|
||||
|
||||
@ApiProperty({ type: AppleAdditionalData })
|
||||
@ValidateNested({
|
||||
each: true,
|
||||
message: i18n('validation.ValidateNested', { path: 'general', property: 'auth.apple.additionalData' }),
|
||||
})
|
||||
@IsOptional()
|
||||
@Type(() => AppleAdditionalData)
|
||||
additionalData?: AppleAdditionalData;
|
||||
}
|
10
src/auth/dtos/request/google-login.request.dto.ts
Normal file
10
src/auth/dtos/request/google-login.request.dto.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsNotEmpty, IsString } from 'class-validator';
|
||||
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
|
||||
|
||||
export class GoogleLoginRequestDto {
|
||||
@ApiProperty({ example: 'google_token' })
|
||||
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'auth.googleToken' }) })
|
||||
@IsNotEmpty({ message: i18n('validation.IsNotEmpty', { path: 'general', property: 'auth.googleToken' }) })
|
||||
googleToken!: string;
|
||||
}
|
@ -1,7 +1,9 @@
|
||||
export * from './apple-login.request.dto';
|
||||
export * from './create-unverified-user.request.dto';
|
||||
export * from './disable-biometric.request.dto';
|
||||
export * from './enable-biometric.request.dto';
|
||||
export * from './forget-password.request.dto';
|
||||
export * from './google-login.request.dto';
|
||||
export * from './login.request.dto';
|
||||
export * from './refresh-token.request.dto';
|
||||
export * from './send-forget-password-otp.request.dto';
|
||||
|
@ -12,10 +12,12 @@ import { DeviceService, UserService, UserTokenService } from '~/user/services';
|
||||
import { User } from '../../user/entities';
|
||||
import { PASSCODE_REGEX } from '../constants';
|
||||
import {
|
||||
AppleLoginRequestDto,
|
||||
CreateUnverifiedUserRequestDto,
|
||||
DisableBiometricRequestDto,
|
||||
EnableBiometricRequestDto,
|
||||
ForgetPasswordRequestDto,
|
||||
GoogleLoginRequestDto,
|
||||
LoginRequestDto,
|
||||
SendForgetPasswordOtpRequestDto,
|
||||
SendLoginOtpRequestDto,
|
||||
@ -24,7 +26,7 @@ import {
|
||||
VerifyLoginOtpRequestDto,
|
||||
VerifyUserRequestDto,
|
||||
} from '../dtos/request';
|
||||
import { GrantType, Roles } from '../enums';
|
||||
import { Roles } from '../enums';
|
||||
import { IJwtPayload, ILoginResponse } from '../interfaces';
|
||||
import { removePadding, verifySignature } from '../utils';
|
||||
import { Oauth2Service } from './oauth2.service';
|
||||
@ -243,29 +245,6 @@ export class AuthService {
|
||||
this.logger.log(`Passcode updated successfully for user with email ${email}`);
|
||||
}
|
||||
|
||||
async login(loginDto: LoginRequestDto): Promise<[ILoginResponse, User]> {
|
||||
let user: User;
|
||||
let tokens: ILoginResponse;
|
||||
|
||||
if (loginDto.grantType === GrantType.PASSWORD || loginDto.grantType === GrantType.BIOMETRIC) {
|
||||
throw new BadRequestException('AUTH.GRANT_TYPE_NOT_SUPPORTED_YET');
|
||||
}
|
||||
|
||||
if (loginDto.grantType === GrantType.GOOGLE) {
|
||||
this.logger.log(`Logging in user with email ${loginDto.email} using google`);
|
||||
[tokens, user] = await this.loginWithGoogle(loginDto);
|
||||
}
|
||||
|
||||
if (loginDto.grantType === GrantType.APPLE) {
|
||||
this.logger.log(`Logging in user with email ${loginDto.email} using apple`);
|
||||
[tokens, user] = await this.loginWithApple(loginDto);
|
||||
}
|
||||
|
||||
this.logger.log(`User with email ${loginDto.email} logged in successfully`);
|
||||
|
||||
return [tokens!, user!];
|
||||
}
|
||||
|
||||
async setJuniorPasscode(body: setJuniorPasswordRequestDto) {
|
||||
this.logger.log(`Setting passcode for junior with qrToken ${body.qrToken}`);
|
||||
const juniorId = await this.userTokenService.validateToken(body.qrToken, UserType.JUNIOR);
|
||||
@ -403,65 +382,96 @@ export class AuthService {
|
||||
return [tokens, user];
|
||||
}
|
||||
|
||||
private async loginWithGoogle(loginDto: LoginRequestDto): Promise<[ILoginResponse, User]> {
|
||||
const { email, sub } = await this.oauth2Service.verifyGoogleToken(loginDto.googleToken);
|
||||
const [existingUser, isJunior] = await Promise.all([
|
||||
async loginWithGoogle(loginDto: GoogleLoginRequestDto): Promise<[ILoginResponse, User]> {
|
||||
const {
|
||||
email,
|
||||
sub,
|
||||
given_name: firstName,
|
||||
family_name: lastName,
|
||||
} = await this.oauth2Service.verifyGoogleToken(loginDto.googleToken);
|
||||
const [existingUser, isJunior, existingUserWithEmail] = await Promise.all([
|
||||
this.userService.findUser({ googleId: sub }),
|
||||
this.userService.findUser({ email, roles: ArrayContains([Roles.JUNIOR]) }),
|
||||
this.userService.findUser({ email }),
|
||||
]);
|
||||
|
||||
if (isJunior && email) {
|
||||
if (isJunior) {
|
||||
this.logger.error(`User with email ${email} is an already registered junior`);
|
||||
throw new BadRequestException('USER.JUNIOR_UPGRADE_NOT_SUPPORTED_YET');
|
||||
}
|
||||
|
||||
if (!existingUser) {
|
||||
this.logger.debug(`User with google id ${sub} not found, creating new user`);
|
||||
const user = await this.userService.createGoogleUser(sub, email);
|
||||
if (!existingUser && existingUserWithEmail) {
|
||||
this.logger.error(`User with email ${email} already exists adding google id to existing user`);
|
||||
await this.userService.updateUser(existingUserWithEmail.id, { googleId: sub });
|
||||
const tokens = await this.generateAuthToken(existingUserWithEmail);
|
||||
return [tokens, existingUserWithEmail];
|
||||
}
|
||||
|
||||
if (!existingUser && !existingUserWithEmail) {
|
||||
this.logger.debug(`User with google id ${sub} or email ${email} not found, creating new user`);
|
||||
const user = await this.userService.createGoogleUser(sub, email, firstName, lastName);
|
||||
|
||||
const tokens = await this.generateAuthToken(user);
|
||||
|
||||
return [tokens, user];
|
||||
}
|
||||
|
||||
const tokens = await this.generateAuthToken(existingUser);
|
||||
const tokens = await this.generateAuthToken(existingUser!);
|
||||
|
||||
return [tokens, existingUser];
|
||||
return [tokens, existingUser!];
|
||||
}
|
||||
|
||||
private async loginWithApple(loginDto: LoginRequestDto): Promise<[ILoginResponse, User]> {
|
||||
async loginWithApple(loginDto: AppleLoginRequestDto): Promise<[ILoginResponse, User]> {
|
||||
const { sub, email } = await this.oauth2Service.verifyAppleToken(loginDto.appleToken);
|
||||
|
||||
const [existingUser, isJunior] = await Promise.all([
|
||||
const [existingUserWithSub, isJunior] = await Promise.all([
|
||||
this.userService.findUser({ appleId: sub }),
|
||||
this.userService.findUser({ email, roles: ArrayContains([Roles.JUNIOR]) }),
|
||||
]);
|
||||
|
||||
if (isJunior && email) {
|
||||
this.logger.error(`User with email ${email} is an already registered junior`);
|
||||
if (isJunior) {
|
||||
this.logger.error(`User with apple id ${sub} is an already registered junior`);
|
||||
throw new BadRequestException('USER.JUNIOR_UPGRADE_NOT_SUPPORTED_YET');
|
||||
}
|
||||
|
||||
if (!existingUser) {
|
||||
if (email) {
|
||||
const existingUserWithEmail = await this.userService.findUser({ email });
|
||||
|
||||
if (existingUserWithEmail && !existingUserWithSub) {
|
||||
{
|
||||
this.logger.error(`User with email ${email} already exists adding apple id to existing user`);
|
||||
await this.userService.updateUser(existingUserWithEmail.id, { appleId: sub });
|
||||
const tokens = await this.generateAuthToken(existingUserWithEmail);
|
||||
return [tokens, existingUserWithEmail];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!existingUserWithSub) {
|
||||
// Apple only provides email if user authorized zod for the first time
|
||||
if (!email) {
|
||||
if (!email || !loginDto.additionalData) {
|
||||
this.logger.error(`User authorized zod before but his email is not stored in the database`);
|
||||
throw new BadRequestException('AUTH.APPLE_RE-CONSENT_REQUIRED');
|
||||
}
|
||||
|
||||
this.logger.debug(`User with apple id ${sub} not found, creating new user`);
|
||||
const user = await this.userService.createAppleUser(sub, email);
|
||||
const user = await this.userService.createAppleUser(
|
||||
sub,
|
||||
email,
|
||||
loginDto.additionalData.firstName,
|
||||
loginDto.additionalData.lastName,
|
||||
);
|
||||
|
||||
const tokens = await this.generateAuthToken(user);
|
||||
|
||||
return [tokens, user];
|
||||
}
|
||||
|
||||
const tokens = await this.generateAuthToken(existingUser);
|
||||
const tokens = await this.generateAuthToken(existingUserWithSub);
|
||||
|
||||
this.logger.log(`User with apple id ${sub} logged in successfully`);
|
||||
|
||||
return [tokens, existingUser];
|
||||
return [tokens, existingUserWithSub];
|
||||
}
|
||||
|
||||
private async generateAuthToken(user: User) {
|
||||
@ -499,4 +509,6 @@ export class AuthService {
|
||||
throw new BadRequestException('AUTH.INVALID_PASSCODE');
|
||||
}
|
||||
}
|
||||
|
||||
private validateGoogleToken(googleToken: string) {}
|
||||
}
|
||||
|
@ -41,7 +41,8 @@
|
||||
"ALREADY_REJECTED": "تم رفض طلب تغيير المصروف بالفعل."
|
||||
},
|
||||
"CUSTOMER": {
|
||||
"NOT_FOUND": "لم يتم العثور على العميل."
|
||||
"NOT_FOUND": "لم يتم العثور على العميل.",
|
||||
"ALREADY_EXISTS": "العميل موجود بالفعل."
|
||||
},
|
||||
|
||||
"GIFT": {
|
||||
|
@ -40,7 +40,8 @@
|
||||
"ALREADY_REJECTED": "The allowance change request has already been rejected."
|
||||
},
|
||||
"CUSTOMER": {
|
||||
"NOT_FOUND": "The customer was not found."
|
||||
"NOT_FOUND": "The customer was not found.",
|
||||
"ALREADY_EXISTS": "The customer already exists."
|
||||
},
|
||||
|
||||
"GIFT": {
|
||||
|
@ -22,7 +22,10 @@
|
||||
"fcmToken": "FCM token",
|
||||
"refreshToken": "Refresh token",
|
||||
"qrToken": "QR token",
|
||||
"passcode": "Passcode"
|
||||
"passcode": "Passcode",
|
||||
"apple": {
|
||||
"additionalData": "Additional data"
|
||||
}
|
||||
},
|
||||
|
||||
"customer": {
|
||||
|
@ -4,6 +4,7 @@ import * as bcrypt from 'bcrypt';
|
||||
import moment from 'moment';
|
||||
import { FindOptionsWhere } from 'typeorm';
|
||||
import { Transactional } from 'typeorm-transactional';
|
||||
import { CountryIso } from '~/common/enums';
|
||||
import { NotificationsService } from '~/common/modules/notification/services';
|
||||
import { CustomerService } from '~/customer/services';
|
||||
import { CreateUnverifiedUserRequestDto, VerifyUserRequestDto } from '../../auth/dtos/request';
|
||||
@ -180,7 +181,7 @@ export class UserService {
|
||||
});
|
||||
}
|
||||
|
||||
async createGoogleUser(googleId: string, email: string) {
|
||||
async createGoogleUser(googleId: string, email: string, firstName: string, lastName: string) {
|
||||
this.logger.log(`Creating google user with googleId ${googleId} and email ${email}`);
|
||||
const user = await this.userRepository.createUser({
|
||||
googleId,
|
||||
@ -189,10 +190,16 @@ export class UserService {
|
||||
isEmailVerified: true,
|
||||
});
|
||||
|
||||
await this.customerService.createGuardianCustomer(user.id, {
|
||||
firstName,
|
||||
lastName,
|
||||
countryOfResidence: CountryIso.SAUDI_ARABIA,
|
||||
});
|
||||
|
||||
return this.findUserOrThrow({ id: user.id });
|
||||
}
|
||||
|
||||
async createAppleUser(appleId: string, email: string) {
|
||||
async createAppleUser(appleId: string, email: string, firstName: string, lastName: string) {
|
||||
this.logger.log(`Creating apple user with appleId ${appleId} and email ${email}`);
|
||||
const user = await this.userRepository.createUser({
|
||||
appleId,
|
||||
@ -201,6 +208,11 @@ export class UserService {
|
||||
isEmailVerified: true,
|
||||
});
|
||||
|
||||
await this.customerService.createGuardianCustomer(user.id, {
|
||||
firstName,
|
||||
lastName,
|
||||
countryOfResidence: CountryIso.SAUDI_ARABIA,
|
||||
});
|
||||
return this.findUserOrThrow({ id: user.id });
|
||||
}
|
||||
|
||||
@ -213,6 +225,15 @@ export class UserService {
|
||||
return this.userRepository.update(userId, { password: hashedPasscode, salt, isProfileCompleted: true });
|
||||
}
|
||||
|
||||
async updateUser(userId: string, data: Partial<User>) {
|
||||
this.logger.log(`Updating user ${userId} with data ${JSON.stringify(data)}`);
|
||||
const { affected } = await this.userRepository.update(userId, data);
|
||||
if (affected === 0) {
|
||||
this.logger.error(`User with id ${userId} not found`);
|
||||
throw new BadRequestException('USER.NOT_FOUND');
|
||||
}
|
||||
}
|
||||
|
||||
private sendCheckerAccountCreatedEmail(email: string, token: string) {
|
||||
return this.notificationsService.sendEmailAsync({
|
||||
to: email,
|
||||
|
Reference in New Issue
Block a user