diff --git a/src/auth/auth.controller.ts b/src/auth/auth.controller.ts index 5a2a473..8998688 100644 --- a/src/auth/auth.controller.ts +++ b/src/auth/auth.controller.ts @@ -27,6 +27,7 @@ import { ResetPasswordDto, RefreshTokenDto, VerifyEmailDto, + ChangePasswordDto, } from './dto'; import { VerifyTwoFactorDto, @@ -201,6 +202,36 @@ export class AuthController { return this.authService.resetPassword(dto); } + // ========================================== + // CHANGE PASSWORD + // ========================================== + @Post('change-password') + @UseGuards(JwtAuthGuard) + @HttpCode(HttpStatus.OK) + @ApiBearerAuth('JWT-auth') + @ApiOperation({ summary: 'Change password for authenticated user' }) + @ApiBody({ type: ChangePasswordDto }) + @ApiResponse({ + status: 200, + description: 'Password changed successfully', + schema: { + example: { + success: true, + data: { + message: 'Password changed successfully', + }, + }, + }, + }) + @ApiResponse({ status: 400, description: 'Current password is incorrect' }) + @ApiResponse({ status: 401, description: 'Unauthorized' }) + async changePassword( + @CurrentUser('id') userId: string, + @Body() dto: ChangePasswordDto, + ) { + return this.authService.changePassword(userId, dto); + } + // ========================================== // VERIFY EMAIL // ========================================== diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts index 0b9a8f3..ee8c6c0 100644 --- a/src/auth/auth.service.ts +++ b/src/auth/auth.service.ts @@ -22,6 +22,7 @@ import { ResetPasswordDto, RefreshTokenDto, VerifyEmailDto, + ChangePasswordDto, } from './dto'; import { UserRole, AuthProvider, UserStatus } from '@prisma/client'; import { TwoFactorService } from './two-factor/two-factor.service'; @@ -449,6 +450,62 @@ export class AuthService { } } + // ========================================== + // CHANGE PASSWORD + // ========================================== + async changePassword(userId: string, dto: ChangePasswordDto) { + const user = await this.prisma.user.findUnique({ + where: { id: userId }, + }); + + if (!user) { + throw new NotFoundException('User not found'); + } + + if (!user.password) { + throw new BadRequestException( + 'This account uses social login. Password cannot be changed.', + ); + } + + // Verify current password + const isCurrentPasswordValid = await argon2.verify( + user.password, + dto.currentPassword, + ); + + if (!isCurrentPasswordValid) { + throw new BadRequestException('Current password is incorrect'); + } + + // Check if new password is same as current + const isSamePassword = await argon2.verify(user.password, dto.newPassword); + if (isSamePassword) { + throw new BadRequestException( + 'New password must be different from current password', + ); + } + + // Hash new password + const hashedPassword = await argon2.hash(dto.newPassword); + + // Update password + await this.prisma.user.update({ + where: { id: userId }, + data: { password: hashedPassword }, + }); + + // Emit event + this.eventEmitter.emit('password.changed', { + userId: user.id, + email: user.email, + }); + + return { + message: 'Password changed successfully', + }; + } + // ========================================== // VERIFY EMAIL // ========================================== diff --git a/src/auth/dto/change-password.dto.ts b/src/auth/dto/change-password.dto.ts new file mode 100644 index 0000000..095443a --- /dev/null +++ b/src/auth/dto/change-password.dto.ts @@ -0,0 +1,24 @@ +import { IsNotEmpty, IsString, MinLength, Matches } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; + +export class ChangePasswordDto { + @ApiProperty({ + description: 'Current password', + example: 'CurrentPass123!', + }) + @IsNotEmpty({ message: 'Current password is required' }) + @IsString() + currentPassword: string; + + @ApiProperty({ + description: 'New password - minimum 8 characters with uppercase, lowercase, and number', + example: 'NewPass123!', + }) + @IsNotEmpty({ message: 'New password is required' }) + @IsString() + @MinLength(8, { message: 'Password must be at least 8 characters long' }) + @Matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/, { + message: 'Password must contain at least one uppercase letter, one lowercase letter, and one number', + }) + newPassword: string; +} diff --git a/src/auth/dto/index.ts b/src/auth/dto/index.ts index 139c7ba..c0682b2 100644 --- a/src/auth/dto/index.ts +++ b/src/auth/dto/index.ts @@ -5,3 +5,4 @@ export * from './forgot-password.dto'; export * from './reset-password.dto'; export * from './refresh-token.dto'; export * from './verify-email.dto'; +export * from './change-password.dto';