Skip to content
Snippets Groups Projects
Commit 7e8a1731 authored by Hugo SUBTIL's avatar Hugo SUBTIL
Browse files

Merge branch 'feat/change-email' into 'dev'

Feat/change email

See merge request web-et-numerique/pamn_plateforme-des-acteurs-de-la-mediation-numerique/pamn_server!9
parents 7302113a e7ae157a
No related branches found
No related tags found
3 merge requests!27Recette,!26Dev,!9Feat/change email
import { IsEmail, IsNotEmpty } from 'class-validator';
export class EmailChangeDto {
@IsNotEmpty()
@IsEmail()
readonly newEmail: string;
@IsNotEmpty()
@IsEmail()
readonly oldEmail: string;
}
......@@ -8,4 +8,6 @@ export interface IUser extends Document {
validationToken: string;
resetPasswordToken: string;
role: number;
changeEmailToken: string;
newEmail: string;
}
......@@ -19,6 +19,12 @@ export class User {
@Prop({ enum: [UserRole.admin, UserRole.user], default: UserRole.user })
role: number;
@Prop({ default: null })
changeEmailToken: string;
@Prop({ default: null })
newEmail: string;
}
export const UserSchema = SchemaFactory.createForClass(User);
......@@ -2,6 +2,7 @@ import { Body, Controller, Get, Param, Post, Query, Request, UseGuards } from '@
import { ApiOperation, ApiParam, ApiResponse } from '@nestjs/swagger';
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
import { PasswordChangeDto } from './change-password.dto';
import { EmailChangeDto } from './change-email.dto';
import { CreateUserDto } from './create-user.dto';
import { PasswordResetApplyDto } from './reset-password-apply.dto';
import { PasswordResetDto } from './reset-password.dto';
......@@ -47,6 +48,22 @@ export class UsersController {
);
}
@UseGuards(JwtAuthGuard)
@Post('change-email')
@ApiResponse({ status: 201, description: 'Email confirmation send' })
@ApiResponse({ status: 401, description: 'Invalid Email' })
public async changeEmail(@Request() req, @Body() emailChangeDto: EmailChangeDto) {
return this.usersService.changeUserEmail(emailChangeDto);
}
@UseGuards(JwtAuthGuard)
@Post('verify-change-email')
@ApiResponse({ status: 201, description: 'Email changed' })
@ApiResponse({ status: 401, description: 'Invalid Token' })
public async verifyAndUpdateEmail(@Request() req, @Query('token') token: string) {
return this.usersService.verifyAndUpdateUserEmail(token);
}
@Post('reset-password')
@ApiResponse({ status: 200, description: 'Email sent if account exist' })
public async resetPassword(@Body() passwordReset: PasswordResetDto) {
......
......@@ -6,6 +6,7 @@ import { getModelToken } from '@nestjs/mongoose';
import { CreateUserDto } from './create-user.dto';
import { HttpException, HttpStatus } from '@nestjs/common';
import { LoginDto } from '../auth/login-dto';
import { EmailChangeDto } from './change-email.dto';
describe('UsersService', () => {
let service: UsersService;
......@@ -38,6 +39,8 @@ describe('UsersService', () => {
emailVerified: false,
email: 'jacques.dupont@mii.com',
password: '$2a$12$vLQjJ9zAWyUwiXLeQDa6w.yazDArYIpf2WnQF1jRHOjBxADEjUEA3',
newEmail: '',
changeEmailToken: '',
resetPasswordToken: null,
};
const userDto: CreateUserDto = { email: 'jacques.dupont@mii.com', password: 'test1A!!' }; //NOSONAR
......@@ -72,6 +75,8 @@ describe('UsersService', () => {
email: 'jacques.dupont@mii.com',
password: '$2a$12$vLQjJ9zAWyUwiXLeQDa6w.yazDArYIpf2WnQF1jRHOjBxADEjUEA3',
role: 0,
newEmail: '',
changeEmailToken: '',
resetPasswordToken: null,
};
const loginDto: LoginDto = { email: 'jacques.dupont@mii.com', password: 'test1A!!' }; //NOSONAR
......@@ -120,6 +125,59 @@ describe('UsersService', () => {
});
});
describe('changeUserEmail', () => {
it('should find and add token', async () => {
const result = {
validationToken: '',
emailVerified: true,
email: 'jacques.dupont@mii.com',
password: '$2a$12$vLQjJ9zAWyUwiXLeQDa6w.yazDArYIpf2WnQF1jRHOjBxADEjUEA3',
role: 0,
newEmail: 'test.dupont@mail.com',
resetPasswordToken: '',
changeEmailToken:
'9bb3542bdc5ca8801ad4cee00403c1052bc95dee768dcbb65b1f719870578ed79f71f52fdc3e6bf02fd200a72b8b6f56fc26950df30c8cd7e427a485f80181b9',
};
const emailDto: EmailChangeDto = { newEmail: 'test.dupont@mail.com', oldEmail: 'jacques.dupont@mii.com' }; //NOSONAR
jest.spyOn(service, 'changeUserEmail').mockImplementation(async (): Promise<User> => result);
expect(await service.changeUserEmail(emailDto)).toBe(result);
});
it('user does not exist, should be unauthorized issue', async () => {
const result: HttpException = new HttpException('Email sent if account exist', HttpStatus.UNAUTHORIZED);
const emailDto: EmailChangeDto = { newEmail: 'test.dupont@mail.com', oldEmail: 'jacques.dupont@mii.com' }; //NOSONAR
jest.spyOn(service, 'changeUserEmail').mockImplementation(async (): Promise<any> => result);
expect(await service.changeUserEmail(emailDto)).toBe(result);
});
it('email already used, should be not acceptable issue', async () => {
const result: HttpException = new HttpException('Email already used', HttpStatus.NOT_ACCEPTABLE);
const emailDto: EmailChangeDto = { newEmail: 'jacques.dupont@mii.com', oldEmail: 'jacques.dupont@mii.com' }; //NOSONAR
jest.spyOn(service, 'changeUserEmail').mockImplementation(async (): Promise<any> => result);
expect(await service.changeUserEmail(emailDto)).toBe(result);
});
it('should change email', async () => {
const result = {
validationToken: '',
emailVerified: true,
email: 'test.dupont@mail.com',
password: '$2a$12$vLQjJ9zAWyUwiXLeQDa6w.yazDArYIpf2WnQF1jRHOjBxADEjUEA3',
role: 0,
newEmail: '',
resetPasswordToken: '',
changeEmailToken: '',
};
const token =
'9bb3542bdc5ca8801ad4cee00403c1052bc95dee768dcbb65b1f719870578ed79f71f52fdc3e6bf02fd200a72b8b6f56fc26950df30c8cd7e427a485f80181b9'; //NOSONAR
jest.spyOn(service, 'verifyAndUpdateUserEmail').mockImplementation(async (): Promise<User> => result);
expect(await service.verifyAndUpdateUserEmail(token)).toBe(result);
});
it('should not change email', async () => {
const result: HttpException = new HttpException('Invalid token', HttpStatus.UNAUTHORIZED);
const token = '9bb3542bdc5ca8801aa72b8b6f56fc26950df30c8cd7e427a485f80181b9FAKETOKEN'; //NOSONAR
jest.spyOn(service, 'verifyAndUpdateUserEmail').mockImplementation(async (): Promise<any> => result);
expect(await service.verifyAndUpdateUserEmail(token)).toBe(result);
});
});
describe('sendResetPasswordEmail', () => {
it('should not send email', async () => {
const result = new HttpException('Email sent if account exist', HttpStatus.OK);
......
......@@ -9,6 +9,7 @@ import { CreateUserDto } from './create-user.dto';
import { User } from './user.schema';
import { MailerService } from '../mailer/mailer.service';
import { IUser } from './user.interface';
import { EmailChangeDto } from './change-email.dto';
@Injectable()
export class UsersService {
......@@ -134,6 +135,43 @@ export class UsersService {
}
}
public async changeUserEmail(emailDto: EmailChangeDto): Promise<any> {
const user = await this.findOne(emailDto.oldEmail);
const alreadyUsed = await this.findOne(emailDto.newEmail);
if (user) {
if (!alreadyUsed) {
const config = this.mailerService.config;
const ejsPath = this.mailerService.getTemplateLocation(config.templates.changeEmail.ejs);
const jsonConfig = this.mailerService.loadJsonConfig(config.templates.changeEmail.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);
user.changeEmailToken = token;
user.newEmail = emailDto.newEmail;
user.save();
return user;
}
throw new HttpException('Email already used', HttpStatus.NOT_ACCEPTABLE);
}
throw new HttpException('Email sent if account exist', HttpStatus.UNAUTHORIZED);
}
public async verifyAndUpdateUserEmail(token: string): Promise<any> {
const user = await this.userModel.findOne({ changeEmailToken: token }).exec();
if (user) {
user.email = user.newEmail;
user.newEmail = null;
user.changeEmailToken = null;
user.save();
return user;
} else {
throw new HttpException('Invalid token', HttpStatus.UNAUTHORIZED);
}
}
public async changeUserPassword(userId: string, oldPassword: string, newPassword: string): Promise<any> {
const user = await this.findById(userId, true);
const arePasswordEqual = await this.comparePassword(oldPassword, user.password);
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment