From 99ad17f0f9528d93ccfab169d75236b0d9bc8c27 Mon Sep 17 00:00:00 2001 From: Abdalhamid Alhamad Date: Thu, 7 Aug 2025 15:25:45 +0300 Subject: [PATCH] feat: add change password api --- src/auth/controllers/auth.controller.ts | 11 ++++- .../request/change-password.request.dto.ts | 23 ++++++++++ src/auth/dtos/request/index.ts | 1 + src/auth/services/auth.service.ts | 43 +++++++++++++++++++ src/i18n/ar/app.json | 3 ++ src/i18n/en/app.json | 3 ++ 6 files changed, 83 insertions(+), 1 deletion(-) create mode 100644 src/auth/dtos/request/change-password.request.dto.ts diff --git a/src/auth/controllers/auth.controller.ts b/src/auth/controllers/auth.controller.ts index 61a1985..122d34f 100644 --- a/src/auth/controllers/auth.controller.ts +++ b/src/auth/controllers/auth.controller.ts @@ -1,11 +1,12 @@ import { Body, Controller, HttpCode, HttpStatus, Post, Req, UseGuards } from '@nestjs/common'; import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; import { Request } from 'express'; -import { Public } from '~/common/decorators'; +import { AuthenticatedUser, Public } from '~/common/decorators'; import { AccessTokenGuard } from '~/common/guards'; import { ApiDataResponse, ApiLangRequestHeader } from '~/core/decorators'; import { ResponseFactory } from '~/core/utils'; import { + ChangePasswordRequestDto, CreateUnverifiedUserRequestDto, ForgetPasswordRequestDto, LoginRequestDto, @@ -17,6 +18,7 @@ import { import { SendForgetPasswordOtpResponseDto, SendRegisterOtpResponseDto } from '../dtos/response'; import { LoginResponseDto } from '../dtos/response/login.response.dto'; import { VerifyForgetPasswordOtpResponseDto } from '../dtos/response/verify-forget-password-otp.response.dto'; +import { IJwtPayload } from '../interfaces'; import { AuthService } from '../services'; @Controller('auth') @@ -64,6 +66,13 @@ export class AuthController { return this.authService.resetPassword(forgetPasswordDto); } + @Post('change-password') + @HttpCode(HttpStatus.NO_CONTENT) + @UseGuards(AccessTokenGuard) + async changePassword(@AuthenticatedUser() { sub }: IJwtPayload, @Body() forgetPasswordDto: ChangePasswordRequestDto) { + return this.authService.changePassword(sub, forgetPasswordDto); + } + @Post('refresh-token') @Public() async refreshToken(@Body() { refreshToken }: RefreshTokenRequestDto) { diff --git a/src/auth/dtos/request/change-password.request.dto.ts b/src/auth/dtos/request/change-password.request.dto.ts new file mode 100644 index 0000000..58bed32 --- /dev/null +++ b/src/auth/dtos/request/change-password.request.dto.ts @@ -0,0 +1,23 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsString, Matches } from 'class-validator'; +import { i18nValidationMessage as i18n } from 'nestjs-i18n'; +import { PASSWORD_REGEX } from '~/auth/constants'; + +export class ChangePasswordRequestDto { + @ApiProperty({ example: 'currentPassword@123' }) + @IsString({ message: i18n('validation.IsString', { path: 'general', property: 'auth.currentPassword' }) }) + @IsNotEmpty({ message: i18n('validation.IsNotEmpty', { path: 'general', property: 'auth.currentPassword' }) }) + currentPassword!: string; + + @ApiProperty({ example: 'Abcd1234@' }) + @Matches(PASSWORD_REGEX, { + message: i18n('validation.Matches', { path: 'general', property: 'auth.newPassword' }), + }) + newPassword!: string; + + @ApiProperty({ example: 'Abcd1234@' }) + @Matches(PASSWORD_REGEX, { + message: i18n('validation.Matches', { path: 'general', property: 'auth.confirmNewPassword' }), + }) + confirmNewPassword!: string; +} diff --git a/src/auth/dtos/request/index.ts b/src/auth/dtos/request/index.ts index 09274d4..767d1f4 100644 --- a/src/auth/dtos/request/index.ts +++ b/src/auth/dtos/request/index.ts @@ -1,4 +1,5 @@ export * from './apple-login.request.dto'; +export * from './change-password.request.dto'; export * from './create-unverified-user.request.dto'; export * from './disable-biometric.request.dto'; export * from './enable-biometric.request.dto'; diff --git a/src/auth/services/auth.service.ts b/src/auth/services/auth.service.ts index e096196..e6570ac 100644 --- a/src/auth/services/auth.service.ts +++ b/src/auth/services/auth.service.ts @@ -11,6 +11,7 @@ import { UserType } from '~/user/enums'; import { DeviceService, UserService, UserTokenService } from '~/user/services'; import { User } from '../../user/entities'; import { + ChangePasswordRequestDto, CreateUnverifiedUserRequestDto, DisableBiometricRequestDto, EnableBiometricRequestDto, @@ -172,6 +173,7 @@ export class AuthService { return { token, user }; } + async resetPassword({ countryCode, phoneNumber, @@ -191,6 +193,15 @@ export class AuthService { throw new BadRequestException('AUTH.PASSWORD_MISMATCH'); } + const isOldPassword = bcrypt.compareSync(password, user.password); + + if (isOldPassword) { + this.logger.error( + `New password cannot be the same as the current password for user with phone number ${user.fullPhoneNumber}`, + ); + throw new BadRequestException('AUTH.PASSWORD_SAME_AS_CURRENT'); + } + const hashedPassword = bcrypt.hashSync(password, user.salt); await this.userService.setPassword(user.id, hashedPassword, user.salt); @@ -198,6 +209,38 @@ export class AuthService { this.logger.log(`Passcode updated successfully for user with phone number ${user.fullPhoneNumber}`); } + async changePassword(userId: string, { currentPassword, newPassword, confirmNewPassword }: ChangePasswordRequestDto) { + const user = await this.userService.findUserOrThrow({ id: userId }); + + if (!user.isPasswordSet) { + this.logger.error(`Password not set for user with id ${userId}`); + throw new BadRequestException('AUTH.PASSWORD_NOT_SET'); + } + + if (currentPassword === newPassword) { + this.logger.error('New password cannot be the same as current password'); + throw new BadRequestException('AUTH.PASSWORD_SAME_AS_CURRENT'); + } + + if (newPassword !== confirmNewPassword) { + this.logger.error('New password and confirm new password do not match'); + throw new BadRequestException('AUTH.PASSWORD_MISMATCH'); + } + + this.logger.log(`Validating current password for user with id ${userId}`); + const isCurrentPasswordValid = bcrypt.compareSync(currentPassword, user.password); + + if (!isCurrentPasswordValid) { + this.logger.error(`Invalid current password for user with id ${userId}`); + throw new UnauthorizedException('AUTH.INVALID_CURRENT_PASSWORD'); + } + + const salt = bcrypt.genSaltSync(SALT_ROUNDS); + const hashedNewPassword = bcrypt.hashSync(newPassword, salt); + await this.userService.setPassword(user.id, hashedNewPassword, salt); + this.logger.log(`Password changed successfully for user with id ${userId}`); + } + 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); diff --git a/src/i18n/ar/app.json b/src/i18n/ar/app.json index 797784d..2b8c4cf 100644 --- a/src/i18n/ar/app.json +++ b/src/i18n/ar/app.json @@ -9,6 +9,9 @@ "BIOMETRIC_NOT_ENABLED": "المصادقة البيومترية لم يتم تفعيلها على حسابك. يرجى تفعيلها للمتابعة.", "INVALID_BIOMETRIC": "البيانات البيومترية المقدمة غير صالحة. يرجى المحاولة مرة أخرى أو إعادة إعداد المصادقة البيومترية.", "PASSWORD_MISMATCH": "كلمات المرور التي أدخلتها غير متطابقة. يرجى إدخال كلمات المرور مرة أخرى.", + "INVALID_CURRENT_PASSWORD": "كلمة المرور الحالية التي أدخلتها غير صحيحة. يرجى المحاولة مرة أخرى.", + "PASSWORD_NOT_SET": "لم يتم تعيين كلمة مرور لهذا الحساب. يرجى تعيين كلمة مرور للمتابعة.", + "PASSWORD_SAME_AS_CURRENT": "كلمة المرور الجديدة لا يمكن أن تكون نفس كلمة المرور الحالية.", "INVALID_PASSCODE": "رمز المرور الذي أدخلته غير صحيح. يرجى المحاولة مرة أخرى.", "PASSCODE_ALREADY_SET": "تم تعيين رمز المرور بالفعل.", "APPLE_RE-CONSENT_REQUIRED": "إعادة الموافقة على آبل مطلوبة. يرجى إلغاء تطبيقك من إعدادات معرف آبل الخاصة بك والمحاولة مرة أخرى.", diff --git a/src/i18n/en/app.json b/src/i18n/en/app.json index 668b1e8..1bee066 100644 --- a/src/i18n/en/app.json +++ b/src/i18n/en/app.json @@ -9,6 +9,9 @@ "BIOMETRIC_NOT_ENABLED": "Biometric authentication has not been activated for your account. Please enable it to proceed.", "INVALID_BIOMETRIC": "The biometric data provided is invalid. Please try again or reconfigure your biometric settings.", "PASSWORD_MISMATCH": "The passwords you entered do not match. Please re-enter the passwords.", + "INVALID_CURRENT_PASSWORD": "The current password you entered is incorrect. Please try again.", + "PASSWORD_NOT_SET": "A password has not been set for this account. Please set a password to proceed.", + "PASSWORD_SAME_AS_CURRENT": "The new password cannot be the same as the current password.", "INVALID_PASSCODE": "The passcode you entered is incorrect. Please try again.", "PASSCODE_ALREADY_SET": "The pass code has already been set.", "APPLE_RE-CONSENT_REQUIRED": "Apple re-consent is required. Please revoke the app from your Apple ID settings and try again.",