diff --git a/api/src/identity-access-management/application/password/password.controller.js b/api/src/identity-access-management/application/password/password.controller.js index 2d588f3bc4a..de7b11c3798 100644 --- a/api/src/identity-access-management/application/password/password.controller.js +++ b/api/src/identity-access-management/application/password/password.controller.js @@ -9,7 +9,7 @@ const checkResetDemand = async function (request, h, dependencies = { userSerial return dependencies.userSerializer.serialize(user); }; const createResetPasswordDemand = async function (request, h, dependencies = { resetPasswordSerializer }) { - const { email } = request.payload.data.attributes; + const email = request.payload.email; const locale = extractLocaleFromRequest(request); const resetPasswordDemand = await usecases.createResetPasswordDemand({ diff --git a/api/src/identity-access-management/application/password/password.route.js b/api/src/identity-access-management/application/password/password.route.js index c25664d94cd..031f98e6401 100644 --- a/api/src/identity-access-management/application/password/password.route.js +++ b/api/src/identity-access-management/application/password/password.route.js @@ -11,6 +11,10 @@ export const passwordRoutes = [ handler: (request, h) => passwordController.createResetPasswordDemand(request, h), validate: { payload: Joi.object({ + email: Joi.when('data.attributes.email', { + then: Joi.string().email().default(Joi.ref('data.attributes.email')), + otherwise: Joi.string().email().required(), + }), data: { attributes: { email: Joi.string().email().required(), diff --git a/api/tests/identity-access-management/acceptance/application/password/password.route.test.js b/api/tests/identity-access-management/acceptance/application/password/password.route.test.js index 700b61b7a94..438d7bbcf03 100644 --- a/api/tests/identity-access-management/acceptance/application/password/password.route.test.js +++ b/api/tests/identity-access-management/acceptance/application/password/password.route.test.js @@ -13,44 +13,85 @@ describe('Acceptance | Identity Access Management | Application | Route | passwo describe('POST /api/password-reset-demands', function () { let options; - beforeEach(async function () { - options = { - method: 'POST', - url: '/api/password-reset-demands', - payload: { - data: { - attributes: { email }, - }, - }, - }; + context('with simple payload', function () { + beforeEach(async function () { + options = { + method: 'POST', + url: '/api/password-reset-demands', + payload: { email }, + }; - config.mailing.enabled = false; + config.mailing.enabled = false; - const userId = databaseBuilder.factory.buildUser({ email }).id; - databaseBuilder.factory.buildAuthenticationMethod.withPixAsIdentityProviderAndHashedPassword({ userId }); - await databaseBuilder.commit(); - }); + const userId = databaseBuilder.factory.buildUser({ email }).id; + databaseBuilder.factory.buildAuthenticationMethod.withPixAsIdentityProviderAndHashedPassword({ userId }); + await databaseBuilder.commit(); + }); + + context('when given email doesn’t exist', function () { + it('replies with 404', async function () { + // given + options.payload.email = 'unknown@example.net'; - context('when email provided is unknown', function () { - it('replies with 404', async function () { - // given - options.payload.data.attributes.email = 'unknown@example.net'; + // when + const response = await server.inject(options); + + // then + expect(response.statusCode).to.equal(404); + }); + }); - // when - const response = await server.inject(options); + context('when given email exists', function () { + it('replies with 201', async function () { + // when + const response = await server.inject(options); - // then - expect(response.statusCode).to.equal(404); + // then + expect(response.statusCode).to.equal(201); + }); }); }); - context('when existing email is provided', function () { - it('replies with 201', async function () { - // when - const response = await server.inject(options); + context('with deprecated ember-data-centric payload', function () { + beforeEach(async function () { + options = { + method: 'POST', + url: '/api/password-reset-demands', + payload: { + data: { + attributes: { email }, + }, + }, + }; + + config.mailing.enabled = false; + + const userId = databaseBuilder.factory.buildUser({ email }).id; + databaseBuilder.factory.buildAuthenticationMethod.withPixAsIdentityProviderAndHashedPassword({ userId }); + await databaseBuilder.commit(); + }); + + context('when given email doesn’t exist', function () { + it('replies with 404', async function () { + // given + options.payload.data.attributes.email = 'unknown@example.net'; + + // when + const response = await server.inject(options); + + // then + expect(response.statusCode).to.equal(404); + }); + }); + + context('when given email exists', function () { + it('replies with 201', async function () { + // when + const response = await server.inject(options); - // then - expect(response.statusCode).to.equal(201); + // then + expect(response.statusCode).to.equal(201); + }); }); }); }); diff --git a/api/tests/identity-access-management/integration/application/password/password.controller.test.js b/api/tests/identity-access-management/integration/application/password/password.controller.test.js index 0ec0058d9cd..f79a3931ae2 100644 --- a/api/tests/identity-access-management/integration/application/password/password.controller.test.js +++ b/api/tests/identity-access-management/integration/application/password/password.controller.test.js @@ -20,12 +20,7 @@ describe('Integration | Identity Access Management | Application | Controller | const headers = { 'accept-language': 'fr', }; - const payload = { - data: { - type: 'password-reset-demands', - attributes: { email }, - }, - }; + const payload = { email }; it('returns a 201 HTTP status code with a response', async function () { // given diff --git a/api/tests/identity-access-management/unit/application/password/password.controller.test.js b/api/tests/identity-access-management/unit/application/password/password.controller.test.js index 5f0b637ece2..211b4b15e39 100644 --- a/api/tests/identity-access-management/unit/application/password/password.controller.test.js +++ b/api/tests/identity-access-management/unit/application/password/password.controller.test.js @@ -41,11 +41,7 @@ describe('Unit | Identity Access Management | Application | Controller | passwor headers: { 'accept-language': locale, }, - payload: { - data: { - attributes: { email }, - }, - }, + payload: { email }, }; const resetPasswordDemand = { id: 1, diff --git a/mon-pix/app/components/authentication/password-reset-demand-form.gjs b/mon-pix/app/components/authentication/password-reset-demand-form.gjs new file mode 100644 index 00000000000..ffb61b74e33 --- /dev/null +++ b/mon-pix/app/components/authentication/password-reset-demand-form.gjs @@ -0,0 +1,122 @@ +import PixButton from '@1024pix/pix-ui/components/pix-button'; +import PixButtonLink from '@1024pix/pix-ui/components/pix-button-link'; +import PixInput from '@1024pix/pix-ui/components/pix-input'; +import PixMessage from '@1024pix/pix-ui/components/pix-message'; +import { on } from '@ember/modifier'; +import { action } from '@ember/object'; +import { service } from '@ember/service'; +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; +import { t } from 'ember-intl'; +import ENV from 'mon-pix/config/environment'; + +import isEmailValid from '../../utils/email-validator.js'; + +export default class PasswordResetDemandForm extends Component { + @service intl; + + @tracked isLoading = false; + @tracked errorMessage; + @tracked emailInputPlaceholder = this.intl.t( + 'components.authentication.password-reset-demand-form.fields.email.placeholder', + ); + @tracked emailInputvalidationStatus; + @tracked emailInputvalidationErrorMessage; + + email; + + @action + handleEmailChange(event) { + this.email = event.target.value; + this.emailInputvalidationStatus = isEmailValid(this.email) ? 'success' : 'error'; + this.emailInputvalidationErrorMessage = this.intl.t( + 'components.authentication.password-reset-demand-form.fields.email.error-message-invalid', + ); + } + + @action + async handlePasswordResetDemand(event) { + if (event) event.preventDefault(); + + this.errorMessage = null; + + const email = this.email.trim(); + if (!email || this.emailInputvalidationStatus === 'error') { + return; + } + + try { + this.isLoading = true; + const response = await window.fetch(`${ENV.APP.API_HOST}/api/password-reset-demands`, { + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + method: 'POST', + body: JSON.stringify({ email }), + }); + if (response.status == 404) { + this.errorMessage = this.intl.t('components.authentication.password-reset-demand-form.404-message'); + } else if (!response.ok) { + throw new Error(`Response status: ${response.status}`); + } + } catch (error) { + this.errorMessage = this.intl.t('common.api-error-messages.internal-server-error'); + } finally { + this.isLoading = false; + } + } + + +} diff --git a/mon-pix/app/components/authentication/password-reset-demand.scss b/mon-pix/app/components/authentication/password-reset-demand.scss new file mode 100644 index 00000000000..581c7d5de1a --- /dev/null +++ b/mon-pix/app/components/authentication/password-reset-demand.scss @@ -0,0 +1,41 @@ +.authentication-password-reset-demand-form { + input { + padding: var(--pix-spacing-3x); + } + + &__rule { + @extend %pix-body-xs; + + color: var(--pix-neutral-500); + } + + &__error { + margin-top: var(--pix-spacing-4x); + } + + &__input-block { + margin-top: var(--pix-spacing-4x); + + .pix-input { + width: 100%; + } + } + + &__button { + width: 100%; + margin-top: var(--pix-spacing-4x); + } + + &__help { + @extend %pix-body-s; + + margin-top: var(--pix-spacing-4x); + color: var(--pix-neutral-70); + text-align: center; + } + + &__help-contact-us-link { + display: inline; + padding: 0; + } +} \ No newline at end of file diff --git a/mon-pix/app/components/form-textfield.hbs b/mon-pix/app/components/form-textfield.hbs index 299dbd29f90..fa8f2ef20bd 100644 --- a/mon-pix/app/components/form-textfield.hbs +++ b/mon-pix/app/components/form-textfield.hbs @@ -1,3 +1,5 @@ +{{! DEPRECATED COMPONENT: DON’T USE IT! USE COMPONENTS OF THE DESIGN SYSTEM INSTEAD!! }} + {{! template-lint-disable require-input-label no-unknown-arguments-for-builtin-components }}