Skip to content

Commit

Permalink
[FEATURE] Migrer la route GET /api/password-reset-demands/{temporaryK…
Browse files Browse the repository at this point in the history
…ey} vers src/identity-access-management (PIX-12749).

 #9447
  • Loading branch information
pix-service-auto-merge committed Jul 15, 2024
2 parents 6ed6b6b + 0170ada commit 47b8062
Show file tree
Hide file tree
Showing 36 changed files with 332 additions and 261 deletions.
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

0 comments on commit 47b8062

Please sign in to comment.