From 6602414779036f003791dee804035a8388390a4e Mon Sep 17 00:00:00 2001 From: Abdalhameed Ahmad Date: Sat, 23 Aug 2025 21:52:59 +0300 Subject: [PATCH] feat: finialize creating juniors --- src/auth/controllers/auth.controller.ts | 20 ++++++++++++- src/auth/dtos/request/index.ts | 2 +- .../dtos/request/junior-login.request.dto.ts | 12 ++++++++ .../set-junior-password.request.dto.ts | 9 ++++-- .../dtos/request/set-passcode.request.dto.ts | 15 ---------- src/auth/services/auth.service.ts | 28 +++++++++++++++-- .../notification-created.listener.ts | 2 +- .../services/notifications.service.ts | 1 - .../modules/otp/services/otp.service.ts | 1 - .../repositories/customer.repository.ts | 4 +-- src/customer/services/customer.service.ts | 2 +- .../dtos/request/create-junior.request.dto.ts | 30 ++----------------- .../dtos/response/junior.response.dto.ts | 16 +++++++--- src/junior/repositories/junior.repository.ts | 2 +- src/junior/services/junior.service.ts | 21 ++++--------- src/junior/services/qrcode.service.ts | 1 - src/user/services/user.service.ts | 7 ++++- 17 files changed, 96 insertions(+), 77 deletions(-) create mode 100644 src/auth/dtos/request/junior-login.request.dto.ts delete mode 100644 src/auth/dtos/request/set-passcode.request.dto.ts diff --git a/src/auth/controllers/auth.controller.ts b/src/auth/controllers/auth.controller.ts index 3094142..8f9bf35 100644 --- a/src/auth/controllers/auth.controller.ts +++ b/src/auth/controllers/auth.controller.ts @@ -9,9 +9,11 @@ import { ChangePasswordRequestDto, CreateUnverifiedUserRequestDto, ForgetPasswordRequestDto, + JuniorLoginRequestDto, LoginRequestDto, RefreshTokenRequestDto, SendForgetPasswordOtpRequestDto, + setJuniorPasswordRequestDto, VerifyForgetPasswordOtpRequestDto, VerifyUserRequestDto, } from '../dtos/request'; @@ -69,10 +71,26 @@ export class AuthController { @Post('change-password') @HttpCode(HttpStatus.NO_CONTENT) @UseGuards(AccessTokenGuard) - async changePassword(@AuthenticatedUser() { sub }: IJwtPayload, @Body() forgetPasswordDto: ChangePasswordRequestDto) { + changePassword(@AuthenticatedUser() { sub }: IJwtPayload, @Body() forgetPasswordDto: ChangePasswordRequestDto) { return this.authService.changePassword(sub, forgetPasswordDto); } + @Post('junior/set-password') + @HttpCode(HttpStatus.NO_CONTENT) + @Public() + setJuniorPasscode(@Body() setPassworddto: setJuniorPasswordRequestDto) { + return this.authService.setJuniorPassword(setPassworddto); + } + + @Post('junior/login') + @HttpCode(HttpStatus.OK) + @ApiDataResponse(LoginResponseDto) + async juniorLogin(@Body() juniorLoginDto: JuniorLoginRequestDto) { + const [res, user] = await this.authService.juniorLogin(juniorLoginDto); + + return ResponseFactory.data(new LoginResponseDto(res, user)); + } + @Post('refresh-token') @Public() async refreshToken(@Body() { refreshToken }: RefreshTokenRequestDto) { diff --git a/src/auth/dtos/request/index.ts b/src/auth/dtos/request/index.ts index fac6c67..6659d12 100644 --- a/src/auth/dtos/request/index.ts +++ b/src/auth/dtos/request/index.ts @@ -1,11 +1,11 @@ export * from './change-password.request.dto'; export * from './create-unverified-user.request.dto'; export * from './forget-password.request.dto'; +export * from './junior-login.request.dto'; export * from './login.request.dto'; export * from './refresh-token.request.dto'; export * from './send-forget-password-otp.request.dto'; export * from './set-junior-password.request.dto'; -export * from './set-passcode.request.dto'; export * from './verify-forget-password-otp.request.dto'; export * from './verify-otp.request.dto'; export * from './verify-user.request.dto'; diff --git a/src/auth/dtos/request/junior-login.request.dto.ts b/src/auth/dtos/request/junior-login.request.dto.ts new file mode 100644 index 0000000..b53a842 --- /dev/null +++ b/src/auth/dtos/request/junior-login.request.dto.ts @@ -0,0 +1,12 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsEmail, IsString } from 'class-validator'; +import { i18nValidationMessage as i18n } from 'nestjs-i18n'; +export class JuniorLoginRequestDto { + @ApiProperty({ example: 'test@junior.com' }) + @IsEmail({}, { message: i18n('validation.IsEmail', { path: 'general', property: 'auth.email' }) }) + email!: string; + + @ApiProperty({ example: 'Abcd1234@' }) + @IsString({ message: i18n('validation.IsString', { path: 'general', property: 'auth.password' }) }) + password!: string; +} diff --git a/src/auth/dtos/request/set-junior-password.request.dto.ts b/src/auth/dtos/request/set-junior-password.request.dto.ts index c78560e..966fb58 100644 --- a/src/auth/dtos/request/set-junior-password.request.dto.ts +++ b/src/auth/dtos/request/set-junior-password.request.dto.ts @@ -1,8 +1,11 @@ -import { ApiProperty } from '@nestjs/swagger'; +import { ApiProperty, PickType } from '@nestjs/swagger'; import { IsNotEmpty, IsString } from 'class-validator'; import { i18nValidationMessage as i18n } from 'nestjs-i18n'; -import { SetPasscodeRequestDto } from './set-passcode.request.dto'; -export class setJuniorPasswordRequestDto extends SetPasscodeRequestDto { +import { ChangePasswordRequestDto } from './change-password.request.dto'; +export class setJuniorPasswordRequestDto extends PickType(ChangePasswordRequestDto, [ + 'newPassword', + 'confirmNewPassword', +]) { @ApiProperty() @IsString({ message: i18n('validation.IsNumberString', { path: 'general', property: 'auth.qrToken' }) }) @IsNotEmpty({ message: i18n('validation.IsNotEmpty', { path: 'general', property: 'auth.qrToken' }) }) diff --git a/src/auth/dtos/request/set-passcode.request.dto.ts b/src/auth/dtos/request/set-passcode.request.dto.ts deleted file mode 100644 index aca81d3..0000000 --- a/src/auth/dtos/request/set-passcode.request.dto.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsNumberString, MaxLength, MinLength } from 'class-validator'; -import { i18nValidationMessage as i18n } from 'nestjs-i18n'; -const PASSCODE_LENGTH = 6; - -export class SetPasscodeRequestDto { - @ApiProperty({ example: '123456' }) - @IsNumberString( - { no_symbols: true }, - { message: i18n('validation.IsNumberString', { path: 'general', property: 'auth.passcode' }) }, - ) - @MinLength(PASSCODE_LENGTH, { message: i18n('validation.MinLength', { path: 'general', property: 'auth.passcode' }) }) - @MaxLength(PASSCODE_LENGTH, { message: i18n('validation.MaxLength', { path: 'general', property: 'auth.passcode' }) }) - passcode!: string; -} diff --git a/src/auth/services/auth.service.ts b/src/auth/services/auth.service.ts index 3ce8b63..5330bed 100644 --- a/src/auth/services/auth.service.ts +++ b/src/auth/services/auth.service.ts @@ -14,6 +14,7 @@ import { ChangePasswordRequestDto, CreateUnverifiedUserRequestDto, ForgetPasswordRequestDto, + JuniorLoginRequestDto, LoginRequestDto, SendForgetPasswordOtpRequestDto, setJuniorPasswordRequestDto, @@ -196,11 +197,14 @@ export class AuthService { this.logger.log(`Password changed successfully for user with id ${userId}`); } - async setJuniorPasscode(body: setJuniorPasswordRequestDto) { + async setJuniorPassword(body: setJuniorPasswordRequestDto) { this.logger.log(`Setting passcode for junior with qrToken ${body.qrToken}`); + if (body.newPassword != body.confirmNewPassword) { + throw new BadRequestException('AUTH.PASSWORD_MISMATCH'); + } const juniorId = await this.userTokenService.validateToken(body.qrToken, UserType.JUNIOR); const salt = bcrypt.genSaltSync(SALT_ROUNDS); - const hashedPasscode = bcrypt.hashSync(body.passcode, salt); + const hashedPasscode = bcrypt.hashSync(body.newPassword, salt); await this.userService.setPassword(juniorId!, hashedPasscode, salt); await this.userTokenService.invalidateToken(body.qrToken); this.logger.log(`Passcode set successfully for junior with id ${juniorId}`); @@ -278,6 +282,26 @@ export class AuthService { return [tokens, user]; } + async juniorLogin(juniorLoginDto: JuniorLoginRequestDto): Promise<[ILoginResponse, User]> { + const user = await this.userService.findUser({ email: juniorLoginDto.email }); + + if (!user || !user.roles.includes(Roles.JUNIOR)) { + throw new UnauthorizedException('AUTH.INVALID_CREDENTIALS'); + } + + this.logger.log(`validating password for user with email ${juniorLoginDto.email}`); + const isPasswordValid = bcrypt.compareSync(juniorLoginDto.password, user.password); + + if (!isPasswordValid) { + this.logger.error(`Invalid password for user with email ${juniorLoginDto.email}`); + throw new UnauthorizedException('AUTH.INVALID_CREDENTIALS'); + } + + const tokens = await this.generateAuthToken(user); + this.logger.log(`Password validated successfully for user`); + return [tokens, user]; + } + private async generateAuthToken(user: User) { this.logger.log(`Generating auth token for user with id ${user.id}`); const [accessToken, refreshToken] = await Promise.all([ diff --git a/src/common/modules/notification/listeners/notification-created.listener.ts b/src/common/modules/notification/listeners/notification-created.listener.ts index 03d9a7c..a009389 100644 --- a/src/common/modules/notification/listeners/notification-created.listener.ts +++ b/src/common/modules/notification/listeners/notification-created.listener.ts @@ -21,7 +21,7 @@ export class NotificationCreatedListener { /** * Handles the NOTIFICATION_CREATED event by calling the appropriate channel logic. */ - async handle(event: IEventInterface) { + handle(event: IEventInterface) { this.logger.log( `Handling ${EventType.NOTIFICATION_CREATED} event for notification ${event.id} (channel: ${event.channel})`, ); diff --git a/src/common/modules/notification/services/notifications.service.ts b/src/common/modules/notification/services/notifications.service.ts index 1259be5..ba192bc 100644 --- a/src/common/modules/notification/services/notifications.service.ts +++ b/src/common/modules/notification/services/notifications.service.ts @@ -68,7 +68,6 @@ export class NotificationsService { }); this.logger.log(`emitting ${EventType.NOTIFICATION_CREATED} event`); - return this.redisPubSubService.publishEvent(EventType.NOTIFICATION_CREATED, { ...notification, data: { otp }, diff --git a/src/common/modules/otp/services/otp.service.ts b/src/common/modules/otp/services/otp.service.ts index 869cab3..5d194e4 100644 --- a/src/common/modules/otp/services/otp.service.ts +++ b/src/common/modules/otp/services/otp.service.ts @@ -19,7 +19,6 @@ export class OtpService { async generateAndSendOtp(sendOtpRequest: ISendOtp): Promise { this.logger.log(`invalidate OTP for ${sendOtpRequest.recipient} and ${sendOtpRequest.otpType}`); await this.otpRepository.invalidateOtp(sendOtpRequest); - this.logger.log(`Generating OTP for ${sendOtpRequest.recipient}`); const otp = this.useMock ? DEFAULT_OTP_DIGIT.repeat(DEFAULT_OTP_LENGTH) : generateRandomOtp(DEFAULT_OTP_LENGTH); diff --git a/src/customer/repositories/customer.repository.ts b/src/customer/repositories/customer.repository.ts index 0068996..e2cfc19 100644 --- a/src/customer/repositories/customer.repository.ts +++ b/src/customer/repositories/customer.repository.ts @@ -14,8 +14,7 @@ export class CustomerRepository { findOne(where: FindOptionsWhere) { return this.customerRepository.findOne({ where, - relations: ['user', 'cards'], - + relations: ['user', 'cards'], }); } @@ -30,6 +29,7 @@ export class CustomerRepository { lastName: body.lastName, dateOfBirth: body.dateOfBirth, countryOfResidence: body.countryOfResidence, + gender: body.gender, }), ); } diff --git a/src/customer/services/customer.service.ts b/src/customer/services/customer.service.ts index 6d77eda..6aa3e59 100644 --- a/src/customer/services/customer.service.ts +++ b/src/customer/services/customer.service.ts @@ -30,7 +30,7 @@ export class CustomerService { return this.findCustomerById(userId); } - async createJuniorCustomer(guardianId: string, juniorId: string, body: CreateJuniorRequestDto) { + createJuniorCustomer(guardianId: string, juniorId: string, body: CreateJuniorRequestDto) { this.logger.log(`Creating junior customer for user ${juniorId}`); return this.customerRepository.createCustomer(juniorId, body, false); diff --git a/src/junior/dtos/request/create-junior.request.dto.ts b/src/junior/dtos/request/create-junior.request.dto.ts index 3b502b3..408ccf2 100644 --- a/src/junior/dtos/request/create-junior.request.dto.ts +++ b/src/junior/dtos/request/create-junior.request.dto.ts @@ -1,25 +1,9 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsDateString, IsEmail, IsEnum, IsNotEmpty, IsOptional, IsString, IsUUID, Matches } from 'class-validator'; +import { IsDateString, IsEmail, IsEnum, IsNotEmpty, IsOptional, IsString, IsUUID } from 'class-validator'; import { i18nValidationMessage as i18n } from 'nestjs-i18n'; -import { COUNTRY_CODE_REGEX } from '~/auth/constants'; -import { IsValidPhoneNumber } from '~/core/decorators/validations'; import { Gender } from '~/customer/enums'; import { Relationship } from '~/junior/enums'; export class CreateJuniorRequestDto { - @ApiProperty({ example: '+962' }) - @Matches(COUNTRY_CODE_REGEX, { - message: i18n('validation.Matches', { path: 'general', property: 'auth.countryCode' }), - }) - @IsOptional() - countryCode: string = '+966'; - - @ApiProperty({ example: '787259134' }) - @IsValidPhoneNumber({ - message: i18n('validation.IsValidPhoneNumber', { path: 'general', property: 'auth.phoneNumber' }), - }) - @IsOptional() - phoneNumber!: string; - @ApiProperty({ example: 'John' }) @IsString({ message: i18n('validation.IsString', { path: 'general', property: 'customer.firstName' }) }) @IsNotEmpty({ message: i18n('validation.IsNotEmpty', { path: 'general', property: 'customer.firstName' }) }) @@ -30,9 +14,9 @@ export class CreateJuniorRequestDto { @IsNotEmpty({ message: i18n('validation.IsNotEmpty', { path: 'general', property: 'customer.lastName' }) }) lastName!: string; - @ApiProperty({ example: 'MALE' }) + @ApiProperty({ enum: Gender }) @IsEnum(Gender, { message: i18n('validation.IsEnum', { path: 'general', property: 'customer.gender' }) }) - gender!: string; + gender!: Gender; @ApiProperty({ example: '2020-01-01' }) @IsDateString({}, { message: i18n('validation.IsDateString', { path: 'general', property: 'customer.dateOfBirth' }) }) @@ -46,14 +30,6 @@ export class CreateJuniorRequestDto { @IsEnum(Relationship, { message: i18n('validation.IsEnum', { path: 'general', property: 'junior.relationship' }) }) relationship!: Relationship; - @ApiProperty({ example: 'bf342-3f3f-3f3f-3f3f' }) - @IsUUID('4', { message: i18n('validation.IsUUID', { path: 'general', property: 'junior.civilIdFrontId' }) }) - civilIdFrontId!: string; - - @ApiProperty({ example: 'bf342-3f3f-3f3f-3f3f' }) - @IsUUID('4', { message: i18n('validation.IsUUID', { path: 'general', property: 'junior.civilIdBackId' }) }) - civilIdBackId!: string; - @ApiProperty({ example: '123e4567-e89b-12d3-a456-426614174000' }) @IsUUID('4', { message: i18n('validation.IsUUID', { path: 'general', property: 'customer.profilePictureId' }) }) @IsOptional() diff --git a/src/junior/dtos/response/junior.response.dto.ts b/src/junior/dtos/response/junior.response.dto.ts index b520190..52ea9a1 100644 --- a/src/junior/dtos/response/junior.response.dto.ts +++ b/src/junior/dtos/response/junior.response.dto.ts @@ -7,10 +7,16 @@ export class JuniorResponseDto { @ApiProperty({ example: 'id' }) id!: string; - @ApiProperty({ example: 'fullName' }) - fullName!: string; + @ApiProperty({ example: 'FirstName' }) + firstName!: string; - @ApiProperty({ example: 'relationship' }) + @ApiProperty({ example: 'LastName' }) + lastName!: string; + + @ApiProperty({ example: 'test@junior.com' }) + email!: string; + + @ApiProperty({ enum: Relationship }) relationship!: Relationship; @ApiProperty({ type: DocumentMetaResponseDto }) @@ -18,7 +24,9 @@ export class JuniorResponseDto { constructor(junior: Junior) { this.id = junior.id; - this.fullName = `${junior.customer.firstName} ${junior.customer.lastName}`; + this.firstName = junior.customer.firstName; + this.lastName = junior.customer.lastName; + this.email = junior.customer.user.email; this.relationship = junior.relationship; this.profilePicture = junior.customer.user.profilePicture ? new DocumentMetaResponseDto(junior.customer.user.profilePicture) diff --git a/src/junior/repositories/junior.repository.ts b/src/junior/repositories/junior.repository.ts index cd1e3df..63db76d 100644 --- a/src/junior/repositories/junior.repository.ts +++ b/src/junior/repositories/junior.repository.ts @@ -20,7 +20,7 @@ export class JuniorRepository { } findJuniorById(juniorId: string, withGuardianRelation = false, guardianId?: string) { - const relations = ['customer', 'customer.user', 'theme', 'theme.avatar']; + const relations = ['customer', 'customer.user', 'theme', 'theme.avatar', 'customer.user.profilePicture']; if (withGuardianRelation) { relations.push('guardian', 'guardian.customer', 'guardian.customer.user'); } diff --git a/src/junior/services/junior.service.ts b/src/junior/services/junior.service.ts index 4c7a5c8..f236189 100644 --- a/src/junior/services/junior.service.ts +++ b/src/junior/services/junior.service.ts @@ -1,11 +1,9 @@ import { BadRequestException, Injectable, Logger } from '@nestjs/common'; -import { FindOptionsWhere } from 'typeorm'; import { Transactional } from 'typeorm-transactional'; import { Roles } from '~/auth/enums'; import { PageOptionsRequestDto } from '~/core/dtos'; import { CustomerService } from '~/customer/services'; import { DocumentService, OciService } from '~/document/services'; -import { User } from '~/user/entities'; import { UserType } from '~/user/enums'; import { UserService } from '~/user/services'; import { UserTokenService } from '~/user/services/user-token.service'; @@ -31,26 +29,18 @@ export class JuniorService { async createJuniors(body: CreateJuniorRequestDto, guardianId: string) { this.logger.log(`Creating junior for guardian ${guardianId}`); - const searchConditions: FindOptionsWhere[] = [{ email: body.email }]; - - if (body.phoneNumber && body.countryCode) { - searchConditions.push({ - phoneNumber: body.phoneNumber, - countryCode: body.countryCode, - }); - } - - const existingUser = await this.userService.findUser(searchConditions); + const existingUser = await this.userService.findUser({ email: body.email }); if (existingUser) { - this.logger.error(`User with email ${body.email} or phone number ${body.phoneNumber} already exists`); + this.logger.error(`User with email ${body.email} already exists`); throw new BadRequestException('USER.ALREADY_EXISTS'); } const user = await this.userService.createUser({ email: body.email, - countryCode: body.countryCode, - phoneNumber: body.phoneNumber, + firstName: body.firstName, + lastName: body.lastName, + profilePictureId: body.profilePictureId, roles: [Roles.JUNIOR], }); @@ -75,6 +65,7 @@ export class JuniorService { this.logger.error(`Junior ${juniorId} not found`); throw new BadRequestException('JUNIOR.NOT_FOUND'); } + await this.prepareJuniorImages([junior]); this.logger.log(`Junior ${juniorId} found successfully`); return junior; diff --git a/src/junior/services/qrcode.service.ts b/src/junior/services/qrcode.service.ts index 7001f61..f71e06a 100644 --- a/src/junior/services/qrcode.service.ts +++ b/src/junior/services/qrcode.service.ts @@ -10,7 +10,6 @@ export class QrcodeService { this.logger.log(`Generating QR code for token ${token}`); const link = await this.branchIoService.createBranchLink(token); - return qrcode.toDataURL(link); } } diff --git a/src/user/services/user.service.ts b/src/user/services/user.service.ts index 5f2f70a..46b4834 100644 --- a/src/user/services/user.service.ts +++ b/src/user/services/user.service.ts @@ -236,13 +236,18 @@ export class UserService { throw new BadRequestException('USER.EMAIL_ALREADY_VERIFIED'); } - await this.otpService.verifyOtp({ + const isOtpValid = await this.otpService.verifyOtp({ userId, value: otp, scope: OtpScope.VERIFY_EMAIL, otpType: OtpType.EMAIL, }); + if (!isOtpValid) { + this.logger.error(`Invalid OTP for user with email ${user.email}`); + throw new BadRequestException('OTP.INVALID_OTP'); + } + await this.userRepository.update(userId, { isEmailVerified: true }); this.logger.log(`Email for user ${userId} verified successfully`); }