Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision
  • 208-evenements-affichage
  • 23-a-valider-avec-erwan-check-plus-precis-du-cnfs-id-ainsi-que-des-plages-de-rdv-ouvertes
  • 484-statistiques-requete
  • 50-exception-handling
  • 712-evenements-page-de-saisie
  • V2.5
  • client-476-add-localization-filter
  • dependencies/nestjs
  • dev
  • master
  • openshift-test-deploy
  • pnpm
  • renovate/bitnami-mongodb-4.x
  • renovate/bitnami-mongodb-8.x
  • renovate/devdependencies-(non-major)
  • renovate/elastic-elasticsearch-8.x
  • renovate/elasticsearch-7.x
  • renovate/eslint-9.x
  • renovate/express-5.x
  • renovate/ghcr.io-browserless-chromium-2.x
  • renovate/ghost-5.x
  • renovate/gouvfr-anct-timetable-to-osm-opening-hours-2.x
  • renovate/jest-junit-16.x
  • renovate/luxon-3.x
  • renovate/major-jest-monorepo
  • renovate/major-nest-monorepo
  • renovate/major-typescript-eslint-monorepo
  • renovate/mongo-express-1.x
  • renovate/mysql-9.x
  • renovate/nestjs-schematics-11.x
  • renovate/npm-11.x
  • renovate/tsconfig-paths-4.x
  • send-mail-to-structure-after-orientation
  • tests/users-controllers
  • v1.1.0
  • v1.10.0
  • v1.11.0
  • v1.12.0
  • v1.13.0
  • v1.15.0
  • v1.15.1
  • v1.16.0
  • v1.17.0
  • v1.2.0
  • v1.3.0
  • v1.4.0
  • v1.5.0
  • v1.6.0
  • v1.6.1
  • v1.7.0
  • v1.8.0
  • v1.9.0
  • v1.9.1
  • v2.0.0
  • v2.0.0-beta1
  • v2.0.0-beta1.1
  • v2.0.0-beta2
  • v2.0.0-beta3
  • v2.0.0-beta4
  • v2.0.0-beta5
  • v2.0.1
  • v2.0.2
  • v2.0.3
  • v2.1.0
  • v2.1.1
  • v2.1.2
  • v2.1.3
  • v2.2.0
  • v2.3.0
  • v2.3.1
  • v2.3.2
  • v2.4.0
  • v2.4.1
  • v2.4.2
  • v2.5.0
  • v3.0.0
  • v3.0.1
  • v3.1.0
  • v3.2.0
  • v3.3.0
  • v3.3.1
  • v3.4.0
  • v3.4.1
  • v3.4.2
  • v3.4.3
  • v4.0.0
  • v4.0.1
  • v4.0.3
88 results

Target

Select target project
  • web-et-numerique/factory/pamn_plateforme-des-acteurs-de-la-mediation-numerique/pamn_server
1 result
Select Git revision
  • 208-evenements-affichage
  • 23-a-valider-avec-erwan-check-plus-precis-du-cnfs-id-ainsi-que-des-plages-de-rdv-ouvertes
  • 484-statistiques-requete
  • 50-exception-handling
  • 712-evenements-page-de-saisie
  • V2.5
  • client-476-add-localization-filter
  • dependencies/nestjs
  • dev
  • master
  • openshift-test-deploy
  • pnpm
  • renovate/bitnami-mongodb-4.x
  • renovate/bitnami-mongodb-8.x
  • renovate/devdependencies-(non-major)
  • renovate/elastic-elasticsearch-8.x
  • renovate/elasticsearch-7.x
  • renovate/eslint-9.x
  • renovate/express-5.x
  • renovate/ghcr.io-browserless-chromium-2.x
  • renovate/ghost-5.x
  • renovate/gouvfr-anct-timetable-to-osm-opening-hours-2.x
  • renovate/jest-junit-16.x
  • renovate/luxon-3.x
  • renovate/major-jest-monorepo
  • renovate/major-nest-monorepo
  • renovate/major-typescript-eslint-monorepo
  • renovate/mongo-express-1.x
  • renovate/mysql-9.x
  • renovate/nestjs-schematics-11.x
  • renovate/npm-11.x
  • renovate/tsconfig-paths-4.x
  • send-mail-to-structure-after-orientation
  • tests/users-controllers
  • v1.1.0
  • v1.10.0
  • v1.11.0
  • v1.12.0
  • v1.13.0
  • v1.15.0
  • v1.15.1
  • v1.16.0
  • v1.17.0
  • v1.2.0
  • v1.3.0
  • v1.4.0
  • v1.5.0
  • v1.6.0
  • v1.6.1
  • v1.7.0
  • v1.8.0
  • v1.9.0
  • v1.9.1
  • v2.0.0
  • v2.0.0-beta1
  • v2.0.0-beta1.1
  • v2.0.0-beta2
  • v2.0.0-beta3
  • v2.0.0-beta4
  • v2.0.0-beta5
  • v2.0.1
  • v2.0.2
  • v2.0.3
  • v2.1.0
  • v2.1.1
  • v2.1.2
  • v2.1.3
  • v2.2.0
  • v2.3.0
  • v2.3.1
  • v2.3.2
  • v2.4.0
  • v2.4.1
  • v2.4.2
  • v2.5.0
  • v3.0.0
  • v3.0.1
  • v3.1.0
  • v3.2.0
  • v3.3.0
  • v3.3.1
  • v3.4.0
  • v3.4.1
  • v3.4.2
  • v3.4.3
  • v4.0.0
  • v4.0.1
  • v4.0.3
88 results
Show changes
Commits on Source (8)
......@@ -2711,8 +2711,7 @@
"asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=",
"dev": true
"integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k="
},
"at-least-node": {
"version": "1.0.0",
......@@ -3724,7 +3723,6 @@
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dev": true,
"requires": {
"delayed-stream": "~1.0.0"
}
......@@ -4726,8 +4724,7 @@
"delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=",
"dev": true
"integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk="
},
"delegates": {
"version": "1.0.0",
......@@ -6009,13 +6006,12 @@
}
},
"form-data": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz",
"integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==",
"dev": true,
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.0.tgz",
"integrity": "sha512-CKMFDglpbMi6PyN+brwB9Q/GOw0eAnsrEZDgcsH5Krhz5Od/haKHAX0NmQfha2zPPz0JpWzA7GJHGSnvCRLWsg==",
"requires": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.6",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
}
},
......@@ -11360,6 +11356,17 @@
"uuid": "^3.3.2"
},
"dependencies": {
"form-data": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz",
"integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==",
"dev": true,
"requires": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.6",
"mime-types": "^2.1.12"
}
},
"qs": {
"version": "6.5.2",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
......
......@@ -34,6 +34,7 @@
"class-validator": "^0.12.2",
"dotenv": "^8.2.0",
"ejs": "^3.1.5",
"form-data": "^3.0.0",
"luxon": "^1.25.0",
"mongoose": "^5.10.15",
"passport": "^0.4.1",
......
......@@ -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"
}
......@@ -2,6 +2,7 @@ import { HttpService, Injectable, Logger } from '@nestjs/common';
import { AxiosResponse } from 'axios';
import * as fs from 'fs';
import * as path from 'path';
import * as FormData from 'form-data';
import { ConfigurationService } from '../configuration/configuration.service';
@Injectable()
export class MailerService {
......@@ -20,22 +21,27 @@ export class MailerService {
* @param {string} html
* @param {string} text
*/
public send(to: string, subject: string, html: string): Promise<AxiosResponse<any>> {
public async send(to: string, subject: string, html: string): Promise<AxiosResponse<any>> {
const formData = new FormData();
const data = JSON.stringify({
from: this.config.from,
// eslint-disable-next-line camelcase
from_email: this.config.from,
// eslint-disable-next-line camelcase
from_name: this.config.from_name,
to: to,
to: [{ email: to }],
subject: subject,
content: html,
});
formData.append('metadata', data);
const contentLength = formData.getLengthSync();
Logger.log(`Send mail : ${subject}`, 'Mailer');
return new Promise((resolve, reject) => {
this.httpService
.post(process.env.MAIL_URL, data, {
.post(process.env.MAIL_URL, formData, {
headers: {
'Content-Type': 'application/json',
'Content-Length': contentLength,
Authorization: 'Bearer ' + process.env.MAIL_TOKEN,
...formData.getHeaders(),
},
})
.subscribe(
......
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);
}
}