Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FEATURE] Migrer la route GET /api/password-reset-demands/{temporaryKey} vers src/identity-access-management (PIX-12749). #9447

3 changes: 0 additions & 3 deletions api/lib/application/error-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -198,9 +198,6 @@ function _mapToHttpError(error) {
if (error instanceof DomainErrors.UserNotFoundError) {
return new HttpErrors.NotFoundError(error.message);
}
if (error instanceof DomainErrors.PasswordResetDemandNotFoundError) {
return new HttpErrors.NotFoundError(error.message);
}
if (error instanceof DomainErrors.AlreadyRegisteredEmailAndUsernameError) {
return new HttpErrors.BadRequestError(error.message);
}
Expand Down
12 changes: 1 addition & 11 deletions api/lib/application/passwords/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,12 @@ import Joi from 'joi';
import XRegExp from 'xregexp';

import { config } from '../../config.js';
import { passwordController } from './password-controller.js';

const { passwordValidationPattern } = config.account;

import { passwordController } from './password-controller.js';

const register = async function (server) {
server.route([
{
method: 'GET',
path: '/api/password-reset-demands/{temporaryKey}',
config: {
auth: false,
handler: passwordController.checkResetDemand,
tags: ['api', 'passwords'],
},
},
{
method: 'POST',
path: '/api/expired-password-updates',
Expand Down
9 changes: 1 addition & 8 deletions api/lib/application/passwords/password-controller.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,5 @@
import * as userSerializer from '../../../src/shared/infrastructure/serializers/jsonapi/user-serializer.js';
import { usecases } from '../../domain/usecases/index.js';

const checkResetDemand = async function (request, h, dependencies = { userSerializer }) {
const temporaryKey = request.params.temporaryKey;
const user = await usecases.getUserByResetPasswordDemand({ temporaryKey });
return dependencies.userSerializer.serialize(user);
};

const updateExpiredPassword = async function (request, h) {
const passwordResetToken = request.payload.data.attributes['password-reset-token'];
const newPassword = request.payload.data.attributes['new-password'];
Expand All @@ -24,6 +17,6 @@ const updateExpiredPassword = async function (request, h) {
.created();
};

const passwordController = { checkResetDemand, updateExpiredPassword };
const passwordController = { updateExpiredPassword };

export { passwordController };
16 changes: 1 addition & 15 deletions api/lib/domain/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -671,20 +671,6 @@ class FileValidationError extends DomainError {
}
}

class PasswordResetDemandNotFoundError extends DomainError {
constructor(message = "La demande de réinitialisation de mot de passe n'existe pas.") {
super(message);
}

getErrorMessage() {
return {
data: {
temporaryKey: ['Cette demande de réinitialisation n’existe pas.'],
},
};
}
}

// FIXME: used ?
class SessionWithIdAndInformationOnMassImportError extends DomainError {
constructor(message = 'Merci de ne pas renseigner les informations de session') {
Expand All @@ -709,6 +695,7 @@ class UserAlreadyLinkedToCandidateInSessionError extends DomainError {
super(message);
}
}

class UserNotAuthorizedToUpdateEmailError extends DomainError {
constructor(message = 'User is not authorized to update email') {
super(message);
Expand Down Expand Up @@ -1063,7 +1050,6 @@ export {
OrganizationNotFoundError,
OrganizationTagNotFound,
OrganizationWithoutEmailError,
PasswordResetDemandNotFoundError,
SendingEmailError,
SendingEmailToInvalidDomainError,
SendingEmailToInvalidEmailAddressError,
Expand Down
13 changes: 0 additions & 13 deletions api/lib/domain/usecases/get-user-by-reset-password-demand.js

This file was deleted.

2 changes: 1 addition & 1 deletion api/lib/domain/usecases/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ import { accountRecoveryDemandRepository } from '../../../src/identity-access-ma
import * as authenticationMethodRepository from '../../../src/identity-access-management/infrastructure/repositories/authentication-method.repository.js';
import { emailValidationDemandRepository } from '../../../src/identity-access-management/infrastructure/repositories/email-validation-demand.repository.js';
import * as oidcProviderRepository from '../../../src/identity-access-management/infrastructure/repositories/oidc-provider-repository.js';
import * as resetPasswordDemandRepository from '../../../src/identity-access-management/infrastructure/repositories/reset-password-demand.repository.js';
import { resetPasswordDemandRepository } from '../../../src/identity-access-management/infrastructure/repositories/reset-password-demand.repository.js';
import * as userRepository from '../../../src/identity-access-management/infrastructure/repositories/user.repository.js';
import { userToCreateRepository } from '../../../src/identity-access-management/infrastructure/repositories/user-to-create.repository.js';
import { organizationForAdminRepository } from '../../../src/organizational-entities/infrastructure/repositories/organization-for-admin.repository.js';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
MissingOrInvalidCredentialsError,
MissingUserAccountError,
PasswordNotMatching,
PasswordResetDemandNotFoundError,
UserCantBeCreatedError,
UserShouldChangePasswordError,
} from '../domain/errors.js';
Expand All @@ -32,6 +33,10 @@ const authenticationDomainErrorMappingConfiguration = [
name: PasswordNotMatching.name,
httpErrorFn: (error) => new HttpErrors.UnauthorizedError(error.message),
},
{
name: PasswordResetDemandNotFoundError.name,
httpErrorFn: (error) => new HttpErrors.NotFoundError(error.message),
},
{
name: UserCantBeCreatedError.name,
httpErrorFn: (error) => new HttpErrors.UnauthorizedError(error.message),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import * as userSerializer from '../../../shared/infrastructure/serializers/jsonapi/user-serializer.js';
import { extractLocaleFromRequest } from '../../../shared/infrastructure/utils/request-response-utils.js';
import { usecases } from '../../domain/usecases/index.js';
import * as resetPasswordSerializer from '../../infrastructure/serializers/jsonapi/reset-password.serializer.js';

const checkResetDemand = async function (request, h, dependencies = { userSerializer }) {
const temporaryKey = request.params.temporaryKey;
const user = await usecases.getUserByResetPasswordDemand({ temporaryKey });
return dependencies.userSerializer.serialize(user);
};
const createResetPasswordDemand = async function (request, h, dependencies = { resetPasswordSerializer }) {
const { email } = request.payload.data.attributes;
const locale = extractLocaleFromRequest(request);
Expand All @@ -15,4 +21,4 @@ const createResetPasswordDemand = async function (request, h, dependencies = { r
return h.response(serializedPayload).created();
};

export const passwordController = { createResetPasswordDemand };
export const passwordController = { checkResetDemand, createResetPasswordDemand };
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@ export const passwordRoutes = [
data: {
attributes: {
email: Joi.string().email().required(),
// TODO supprimer "temporary-key" car il est généré dans le usecase associé à cette route
'temporary-key': [Joi.string(), null],
},
type: Joi.string(),
},
Expand All @@ -25,4 +23,17 @@ export const passwordRoutes = [
tags: ['identity-access-management', 'api', 'password'],
},
},
{
method: 'GET',
path: '/api/password-reset-demands/{temporaryKey}',
config: {
auth: false,
handler: (request, h) => passwordController.checkResetDemand(request, h),
notes: [
'Route publique',
'Cette route permet la redirection vers le formulaire de reset de mot de passe si la demande est bien dans la liste',
],
tags: ['api', 'passwords'],
},
},
];
15 changes: 15 additions & 0 deletions api/src/identity-access-management/domain/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,20 @@ class PasswordNotMatching extends DomainError {
}
}

class PasswordResetDemandNotFoundError extends DomainError {
constructor(message = "La demande de réinitialisation de mot de passe n'existe pas.") {
super(message);
}

getErrorMessage() {
return {
data: {
temporaryKey: ['Cette demande de réinitialisation n’existe pas.'],
},
};
}
}

class UserCantBeCreatedError extends DomainError {
constructor(message = "L'utilisateur ne peut pas être créé") {
super(message);
Expand All @@ -51,6 +65,7 @@ export {
MissingOrInvalidCredentialsError,
MissingUserAccountError,
PasswordNotMatching,
PasswordResetDemandNotFoundError,
UserCantBeCreatedError,
UserShouldChangePasswordError,
};
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import jsonwebtoken from 'jsonwebtoken';

import { config } from '../../../shared/config.js';
import { cryptoService } from '../../../shared/domain/services/crypto-service.js';
import * as passwordResetDemandRepository from '../../infrastructure/repositories/reset-password-demand.repository.js';

const generateTemporaryKey = async function () {
const randomBytesBuffer = await cryptoService.randomBytes(16);
Expand All @@ -16,14 +15,11 @@ const generateTemporaryKey = async function () {
);
};

const invalidateOldResetPasswordDemand = function (
userEmail,
resetPasswordDemandRepository = passwordResetDemandRepository,
) {
const invalidateOldResetPasswordDemand = function (userEmail, resetPasswordDemandRepository) {
return resetPasswordDemandRepository.markAsBeingUsed(userEmail);
};

const verifyDemand = function (temporaryKey, resetPasswordDemandRepository = passwordResetDemandRepository) {
const verifyDemand = function (temporaryKey, resetPasswordDemandRepository) {
return resetPasswordDemandRepository.findByTemporaryKey(temporaryKey).then((fetchedDemand) => fetchedDemand.toJSON());
};

Expand All @@ -34,11 +30,7 @@ const verifyDemand = function (temporaryKey, resetPasswordDemandRepository = pas
* @param {ResetPasswordDemandRepository} resetPasswordDemandRepository
* @return {Promise<*>}
*/
const hasUserAPasswordResetDemandInProgress = function (
email,
temporaryKey,
resetPasswordDemandRepository = passwordResetDemandRepository,
) {
const hasUserAPasswordResetDemandInProgress = function (email, temporaryKey, resetPasswordDemandRepository) {
return resetPasswordDemandRepository.findByUserEmail(email, temporaryKey);
};

Expand All @@ -49,4 +41,10 @@ const hasUserAPasswordResetDemandInProgress = function (
* @property invalidateOldResetPasswordDemand
* @property verifyDemand
*/
export { generateTemporaryKey, hasUserAPasswordResetDemandInProgress, invalidateOldResetPasswordDemand, verifyDemand };
const resetPasswordService = {
generateTemporaryKey,
hasUserAPasswordResetDemandInProgress,
invalidateOldResetPasswordDemand,
verifyDemand,
};
export { resetPasswordService };
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/**
* @typedef {function} getUserByResetPasswordDemandUseCase
* @param {Object} params
* @param {string} params.temporaryKey
* @param {ResetPasswordService} params.resetPasswordService
* @param {TokenService} params.tokenService
* @param {UserRepository} params.userRepository
* @param {resetPasswordDemandRepository} params.resetPasswordDemandRepository
* @returns {Promise<User|UserNotFoundError>}
*/
export const getUserByResetPasswordDemand = async function ({
temporaryKey,
resetPasswordService,
tokenService,
userRepository,
resetPasswordDemandRepository,
}) {
await tokenService.decodeIfValid(temporaryKey);
const { email } = await resetPasswordService.verifyDemand(temporaryKey, resetPasswordDemandRepository);
return userRepository.getByEmail(email);
};
4 changes: 2 additions & 2 deletions api/src/identity-access-management/domain/usecases/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,13 @@ import { accountRecoveryDemandRepository } from '../../infrastructure/repositori
import * as authenticationMethodRepository from '../../infrastructure/repositories/authentication-method.repository.js';
import { emailValidationDemandRepository } from '../../infrastructure/repositories/email-validation-demand.repository.js';
import { oidcProviderRepository } from '../../infrastructure/repositories/oidc-provider-repository.js';
import * as resetPasswordDemandRepository from '../../infrastructure/repositories/reset-password-demand.repository.js';
import { resetPasswordDemandRepository } from '../../infrastructure/repositories/reset-password-demand.repository.js';
import * as userRepository from '../../infrastructure/repositories/user.repository.js';
import { userToCreateRepository } from '../../infrastructure/repositories/user-to-create.repository.js';
import { authenticationSessionService } from '../services/authentication-session.service.js';
import { pixAuthenticationService } from '../services/pix-authentication-service.js';
import { refreshTokenService } from '../services/refresh-token-service.js';
import * as resetPasswordService from '../services/reset-password.service.js';
import { resetPasswordService } from '../services/reset-password.service.js';
import { scoAccountRecoveryService } from '../services/sco-account-recovery.service.js';
import { addOidcProviderValidator } from '../validators/add-oidc-provider.validator.js';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { UserNotAuthorizedToUpdatePasswordError } from '../../../shared/domain/e
* resetPasswordService: ResetPasswordService,
* authenticationMethodRepository: AuthenticationMethodRepository,
* userRepository: UserRepository,
* resetPasswordDemandRepository: ResetPasswordDemandRepository,
* }} params
* @return {Promise<void>}
* @throws {UserNotAuthorizedToUpdatePasswordError}
Expand All @@ -21,6 +22,7 @@ export const updateUserPassword = async function ({
resetPasswordService,
authenticationMethodRepository,
userRepository,
resetPasswordDemandRepository,
}) {
const hashedPassword = await cryptoService.hashPassword(password);
const user = await userRepository.get(userId);
Expand All @@ -29,13 +31,17 @@ export const updateUserPassword = async function ({
throw new UserNotAuthorizedToUpdatePasswordError();
}

await resetPasswordService.hasUserAPasswordResetDemandInProgress(user.email, temporaryKey);
await resetPasswordService.hasUserAPasswordResetDemandInProgress(
user.email,
temporaryKey,
resetPasswordDemandRepository,
);

await authenticationMethodRepository.updateChangedPassword({
userId: user.id,
hashedPassword,
});
await resetPasswordService.invalidateOldResetPasswordDemand(user.email);
await resetPasswordService.invalidateOldResetPasswordDemand(user.email, resetPasswordDemandRepository);

user.markEmailAsValid();
await userRepository.update(user.mapToDatabaseDto());
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { knex } from '../../../../db/knex-database-connection.js';
import { PasswordResetDemandNotFoundError } from '../../../../lib/domain/errors.js';
import { ResetPasswordDemand } from '../../../../lib/infrastructure/orm-models/ResetPasswordDemand.js';
import { PasswordResetDemandNotFoundError } from '../../domain/errors.js';
import { ResetPasswordDemand as ResetPasswordDemandModel } from '../../domain/models/ResetPasswordDemand.js';

const RESET_PASSWORD_DEMANDS_TABLE_NAME = 'reset-password-demands';
Expand Down Expand Up @@ -48,8 +48,14 @@ const findByUserEmail = function (email, temporaryKey) {

/**
* @typedef {Object} ResetPasswordDemandRepository
* @property {function} create
* @property {function} findByTemporaryKey
* @property {function} findByUserEmail
* @property {function} markAsBeingUsed
*/
export { create, findByTemporaryKey, findByUserEmail, markAsBeingUsed };
const resetPasswordDemandRepository = { create, findByTemporaryKey, findByUserEmail, markAsBeingUsed };

export { resetPasswordDemandRepository };

function _toDomain(data) {
return new ResetPasswordDemandModel(data);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import * as resetPasswordService from '../../../../src/identity-access-management/domain/services/reset-password.service.js';
import { resetPasswordService } from '../../../../src/identity-access-management/domain/services/reset-password.service.js';
import { tokenService } from '../../../../src/shared/domain/services/token-service.js';
import { createServer, databaseBuilder, expect } from '../../../test-helper.js';

Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { resetPasswordService } from '../../../../../src/identity-access-management/domain/services/reset-password.service.js';
import { config } from '../../../../../src/shared/config.js';
import { createServer, databaseBuilder, expect } from '../../../../test-helper.js';

Expand Down Expand Up @@ -53,4 +54,25 @@ describe('Acceptance | Identity Access Management | Application | Route | passwo
});
});
});

describe('GET /api/password-reset-demands/{temporaryKey}', function () {
it('returns 200 http status code', async function () {
// given
const temporaryKey = await resetPasswordService.generateTemporaryKey();
const options = {
method: 'GET',
url: `/api/password-reset-demands/${temporaryKey}`,
};
const userId = databaseBuilder.factory.buildUser({ email }).id;
databaseBuilder.factory.buildAuthenticationMethod.withPixAsIdentityProviderAndHashedPassword({ userId });
databaseBuilder.factory.buildResetPasswordDemand({ temporaryKey, email });
await databaseBuilder.commit();

// when
const response = await server.inject(options);

// then
expect(response.statusCode).to.equal(200);
});
});
});
Loading