Skip to content
Snippets Groups Projects
Commit 7302113a authored by Jérémie BRISON's avatar Jérémie BRISON
Browse files

Merge branch 'feat/reset-password' into 'dev'

Feat/reset password

See merge request web-et-numerique/pamn_plateforme-des-acteurs-de-la-mediation-numerique/pamn_server!8
parents edd8d041 feceec81
No related branches found
No related tags found
3 merge requests!27Recette,!26Dev,!8Feat/reset password
......@@ -17,5 +17,9 @@ export const config = {
ejs: 'changeEmail.ejs',
json: 'changeEmail.json',
},
resetPassword: {
ejs: 'resetPassword.ejs',
json: 'resetPassword.json',
},
},
};
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 : '' %>/reset-password?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.
{
"subject": "Réinitialisation de mot de passe"
}
import { IsNotEmpty } from 'class-validator';
import { IsNotEmpty, IsString } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class PasswordChangeDto {
@IsNotEmpty() readonly newPassword: string;
@IsNotEmpty() readonly oldPassword: string;
@ApiProperty({ type: String })
@IsNotEmpty()
@IsString()
readonly newPassword: string;
@ApiProperty({ type: String })
@IsNotEmpty()
@IsString()
readonly oldPassword: string;
}
import { IsEmail, IsNotEmpty } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
import { IsEmail, IsNotEmpty, IsString } from 'class-validator';
export class CreateUserDto {
@IsNotEmpty() readonly password: string;
@IsNotEmpty() @IsEmail() email: string;
@ApiProperty({ type: String })
@IsNotEmpty()
@IsString()
readonly password: string;
@IsNotEmpty()
@IsEmail()
@ApiProperty({ type: String })
email: string;
}
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;
}
import { IsNotEmpty, IsEmail } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class PasswordResetDto {
@ApiProperty({ type: String, example: 'toto@mii.com' })
@IsNotEmpty()
@IsEmail()
readonly email: string;
}
......@@ -6,5 +6,6 @@ export interface IUser extends Document {
password: string;
emailVerified: boolean;
validationToken: string;
resetPasswordToken: string;
role: number;
}
......@@ -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;
}
......
......@@ -3,6 +3,8 @@ 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);
}
}
......@@ -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!!' }; //NOSONAR
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!!' }; //NOSONAR
jest.spyOn(service, 'findByLogin').mockImplementation(async (): Promise<User> => result);
......@@ -117,4 +119,56 @@ describe('UsersService', () => {
expect(await service.changeUserPassword('add3d', 'azertyU1!d', 'azertyU1!d')).toBe(result);
});
});
describe('sendResetPasswordEmail', () => {
it('should not send email', async () => {
const result = new HttpException('Email sent if account exist', HttpStatus.OK);
jest.spyOn(service, 'sendResetPasswordEmail').mockImplementation(async (): Promise<HttpException> => result);
expect(await service.sendResetPasswordEmail('test@mii.com')).toBe(result);
});
it('should send email', async () => {
const result = new HttpException('Email sent if account exist', HttpStatus.OK);
jest.spyOn(service, 'sendResetPasswordEmail').mockImplementation(async (): Promise<HttpException> => result);
expect(await service.sendResetPasswordEmail('test@mii.com')).toBe(result);
});
});
describe('validatePasswordResetToken', () => {
it('should not validate new password: token does`nt exist', async () => {
const result = new HttpException('Invalid token', HttpStatus.UNAUTHORIZED);
jest.spyOn(service, 'validatePasswordResetToken').mockImplementation(async (): Promise<HttpException> => result);
expect(
await service.validatePasswordResetToken(
'test@mii.com',
'5def4cb41106f89c212679e164911776618bd529e4f78e2883f7dd01776612a1b4a2ad7edabf2a3e3638aa605966c7a4b69d5f07d9617334e58332ba5f9305'
)
).toBe(result);
});
it('should not validate new password: weak password', async () => {
const result = 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
);
jest.spyOn(service, 'validatePasswordResetToken').mockImplementation(async (): Promise<HttpException> => result);
expect(
await service.validatePasswordResetToken(
'test@mii.com',
'5def4cb41106f89c212679e164911776618bd529e4f78e2883f7dd01776612a1b4a2ad7edabf2a3e3638aa605966c7a4b69d5f07d9617334e58332ba5f9305a6'
)
).toBe(result);
});
it('should validate new password', async () => {
const result = new HttpException('Password Reset', HttpStatus.OK);
jest.spyOn(service, 'validatePasswordResetToken').mockImplementation(async (): Promise<HttpException> => result);
expect(
await service.validatePasswordResetToken(
'test@mii.com',
'5def4cb41106f89c212679e164911776618bd529e4f78e2883f7dd01776612a1b4a2ad7edabf2a3e3638aa605966c7a4b69d5f07d9617334e58332ba5f9305a6'
)
).toBe(result);
});
});
});
......@@ -149,4 +149,52 @@ export class UsersService {
user.password = await this.hashPassword(newPassword);
user.save();
}
/**
* Send reset password email based on ejs template
* @param email string
*/
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);
}
/**
* Change password with the given token and password
* Token existence and password strength are verified
* @param password string
* @param token string
*/
public async validatePasswordResetToken(password: string, token: string): Promise<HttpException> {
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);
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment