From ffea481f8275c93748c114488c150d8b9ace3edd Mon Sep 17 00:00:00 2001 From: Hugo SUBTIL <ext.sopra.husubtil@grandlyon.com> Date: Mon, 14 Dec 2020 15:46:20 +0100 Subject: [PATCH] feat: add password reset --- src/configuration/config.ts | 4 +++ src/mailer/mail-templates/resetPassword.ejs | 12 +++++++ src/mailer/mail-templates/resetPassword.json | 3 ++ src/users/reset-password-apply.dto.ts | 13 +++++++ src/users/user.interface.ts | 1 + src/users/user.schema.ts | 3 ++ src/users/users.controller.ts | 16 ++++++++- src/users/users.service.spec.ts | 2 ++ src/users/users.service.ts | 38 ++++++++++++++++++++ 9 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 src/mailer/mail-templates/resetPassword.ejs create mode 100644 src/mailer/mail-templates/resetPassword.json create mode 100644 src/users/reset-password-apply.dto.ts diff --git a/src/configuration/config.ts b/src/configuration/config.ts index 9cc7f1d62..b6d29acca 100644 --- a/src/configuration/config.ts +++ b/src/configuration/config.ts @@ -17,5 +17,9 @@ export const config = { ejs: 'changeEmail.ejs', json: 'changeEmail.json', }, + resetPassword: { + ejs: 'resetPassword.ejs', + json: 'resetPassword.json', + }, }, }; diff --git a/src/mailer/mail-templates/resetPassword.ejs b/src/mailer/mail-templates/resetPassword.ejs new file mode 100644 index 000000000..f632bbb4c --- /dev/null +++ b/src/mailer/mail-templates/resetPassword.ejs @@ -0,0 +1,12 @@ +Bonjour<br /> +<br /> +Vous avez demandé une réinitialisation de votre mot de passe pour le +<em>Réseau des Acteurs de la Médiation Numérique de la Métropole de Lyon</em>. Pour changer de mot de passe, merci de +cliquer sur le lien suivant : +<a + href="<%= config.protocol %>://<%= config.host %><%= config.port ? ':' + config.port : '' %>/users/reset-password/apply?token=<%= token %>" + >ce lien</a +><br /> +Si vous n'avez pas demander de réinitiallisation de votre mot de passe, merci d'ignorer cet email. +<br /> +Ce mail est un mail automatique. Merci de ne pas y répondre. diff --git a/src/mailer/mail-templates/resetPassword.json b/src/mailer/mail-templates/resetPassword.json new file mode 100644 index 000000000..c5fe69522 --- /dev/null +++ b/src/mailer/mail-templates/resetPassword.json @@ -0,0 +1,3 @@ +{ + "subject": "Réinitialisation de mot de passe" +} diff --git a/src/users/reset-password-apply.dto.ts b/src/users/reset-password-apply.dto.ts new file mode 100644 index 000000000..7fd1ead4f --- /dev/null +++ b/src/users/reset-password-apply.dto.ts @@ -0,0 +1,13 @@ +import { IsNotEmpty, IsString } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; + +export class PasswordResetApplyDto { + @ApiProperty({ type: String }) + @IsNotEmpty() + @IsString() + readonly password: string; + @ApiProperty({ type: String }) + @IsNotEmpty() + @IsString() + readonly token: string; +} diff --git a/src/users/user.interface.ts b/src/users/user.interface.ts index 0fe05c6f8..84dd109ce 100644 --- a/src/users/user.interface.ts +++ b/src/users/user.interface.ts @@ -6,5 +6,6 @@ export interface IUser extends Document { password: string; emailVerified: boolean; validationToken: string; + resetPasswordToken: string; role: number; } diff --git a/src/users/user.schema.ts b/src/users/user.schema.ts index abb11e91a..a37539e4d 100644 --- a/src/users/user.schema.ts +++ b/src/users/user.schema.ts @@ -14,6 +14,9 @@ export class User { @Prop({ default: null }) validationToken: string; + @Prop({ default: null }) + resetPasswordToken: string; + @Prop({ enum: [UserRole.admin, UserRole.user], default: UserRole.user }) role: number; } diff --git a/src/users/users.controller.ts b/src/users/users.controller.ts index 18efa8f93..c870c5551 100644 --- a/src/users/users.controller.ts +++ b/src/users/users.controller.ts @@ -1,8 +1,10 @@ -import { Body, Controller, Get, Param, Post, Query, Req, Request, UseGuards } from '@nestjs/common'; +import { Body, Controller, Get, Param, Post, Query, Request, UseGuards } from '@nestjs/common'; import { ApiOperation, ApiParam, ApiResponse } from '@nestjs/swagger'; import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; import { PasswordChangeDto } from './change-password.dto'; import { CreateUserDto } from './create-user.dto'; +import { PasswordResetApplyDto } from './reset-password-apply.dto'; +import { PasswordResetDto } from './reset-password.dto'; import { UsersService } from './users.service'; @Controller('users') @@ -44,4 +46,16 @@ export class UsersController { passwordChangeDto.newPassword ); } + + @Post('reset-password') + @ApiResponse({ status: 200, description: 'Email sent if account exist' }) + public async resetPassword(@Body() passwordReset: PasswordResetDto) { + return this.usersService.sendResetPasswordEmail(passwordReset.email); + } + + @Post('reset-password/apply') + @ApiResponse({ status: 200, description: 'Email sent if account exist' }) + public async resetPasswordApply(@Body() passwordResetApplyDto: PasswordResetApplyDto) { + return this.usersService.validatePasswordResetToken(passwordResetApplyDto.password, passwordResetApplyDto.token); + } } diff --git a/src/users/users.service.spec.ts b/src/users/users.service.spec.ts index a0d07977e..c282c4a4a 100644 --- a/src/users/users.service.spec.ts +++ b/src/users/users.service.spec.ts @@ -38,6 +38,7 @@ describe('UsersService', () => { emailVerified: false, email: 'jacques.dupont@mii.com', password: '$2a$12$vLQjJ9zAWyUwiXLeQDa6w.yazDArYIpf2WnQF1jRHOjBxADEjUEA3', + resetPasswordToken: null, }; const userDto: CreateUserDto = { email: 'jacques.dupont@mii.com', password: 'test1A!!' }; jest.spyOn(service, 'create').mockImplementation(async (): Promise<User> => result); @@ -71,6 +72,7 @@ describe('UsersService', () => { email: 'jacques.dupont@mii.com', password: '$2a$12$vLQjJ9zAWyUwiXLeQDa6w.yazDArYIpf2WnQF1jRHOjBxADEjUEA3', role: 0, + resetPasswordToken: null, }; const loginDto: LoginDto = { email: 'jacques.dupont@mii.com', password: 'test1A!!' }; jest.spyOn(service, 'findByLogin').mockImplementation(async (): Promise<User> => result); diff --git a/src/users/users.service.ts b/src/users/users.service.ts index 60780de2b..e9aa009ec 100644 --- a/src/users/users.service.ts +++ b/src/users/users.service.ts @@ -149,4 +149,42 @@ export class UsersService { user.password = await this.hashPassword(newPassword); user.save(); } + + public async sendResetPasswordEmail(email: string): Promise<HttpException> { + const user = await this.findOne(email); + if (user) { + const config = this.mailerService.config; + const ejsPath = this.mailerService.getTemplateLocation(config.templates.resetPassword.ejs); + const jsonConfig = this.mailerService.loadJsonConfig(config.templates.resetPassword.json); + + const token = crypto.randomBytes(64).toString('hex'); + const html = await ejs.renderFile(ejsPath, { + config, + token: token, + }); + this.mailerService.send(user.email, jsonConfig.subject, html); + + // Save token + user.resetPasswordToken = token; + user.save(); + } + throw new HttpException('Email sent if account exist', HttpStatus.OK); + } + + public async validatePasswordResetToken(password: string, token: string): Promise<any> { + const user = await this.userModel.findOne({ resetPasswordToken: token }).exec(); + if (user) { + if (!this.isStrongPassword(password)) { + throw new HttpException( + 'Weak password, it must contain ne lowercase alphabetical character, one uppercase alphabetical character, one numeric character, one special character and be eight characters or longer', + HttpStatus.UNPROCESSABLE_ENTITY + ); + } + user.password = await this.hashPassword(password); + user.resetPasswordToken = null; + user.save(); + throw new HttpException('Password Reset', HttpStatus.OK); + } + throw new HttpException('Invalid token', HttpStatus.UNAUTHORIZED); + } } -- GitLab