Skip to content

Commit

Permalink
[FEATURE] Ajouter un nouveau composant formulaire de demande de réini…
Browse files Browse the repository at this point in the history
…tialisation de mot de passe (PIX-14111)

 #10355
  • Loading branch information
pix-service-auto-merge authored Oct 22, 2024
2 parents 0a29a6b + f76df29 commit 13b0edd
Show file tree
Hide file tree
Showing 19 changed files with 608 additions and 87 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = '[email protected]';

context('when email provided is unknown', function () {
it('replies with 404', async function () {
// given
options.payload.data.attributes.email = '[email protected]';
// 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 = '[email protected]';

// 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);
});
});
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
122 changes: 122 additions & 0 deletions mon-pix/app/components/authentication/password-reset-demand-form.gjs
Original file line number Diff line number Diff line change
@@ -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;
}
}

<template>
<form {{on "submit" this.handlePasswordResetDemand}} class="authentication-password-reset-demand-form">
<p class="authentication-password-reset-demand-form__rule">
{{t "components.authentication.password-reset-demand-form.rule"}}
</p>

{{#if this.errorMessage}}
<PixMessage
@type="error"
@withIcon={{true}}
class="authentication-password-reset-demand-form__error"
role="alert"
>
{{this.errorMessage}}
</PixMessage>
{{/if}}
<div class="authentication-password-reset-demand-form__input-block">
<PixInput
@value={{this.email}}
type="email"
{{on "change" this.handleEmailChange}}
@validationStatus={{this.emailInputvalidationStatus}}
@errorMessage={{this.emailInputvalidationErrorMessage}}
placeholder={{this.emailInputPlaceholder}}
required={{true}}
>
<:label>{{t "components.authentication.password-reset-demand-form.fields.email.label"}}</:label>
</PixInput>
</div>
<div>
<PixButton
@type="submit"
@size="large"
@isLoading={{this.isLoading}}
class="authentication-password-reset-demand-form__button"
>
{{t "components.authentication.password-reset-demand-form.actions.receive-reset-button"}}
</PixButton>
</div>
<p class="authentication-password-reset-demand-form__help">
{{t "components.authentication.password-reset-demand-form.no-email-question"}}
<PixButtonLink
@variant="tertiary"
@href="{{t 'components.authentication.password-reset-demand-form.contact-us-link.link-url'}}"
target="_blank"
class="authentication-password-reset-demand-form__help-contact-us-link"
>
{{t "components.authentication.password-reset-demand-form.contact-us-link.link-text"}}
</PixButtonLink>
</p>
</form>
</template>
}
41 changes: 41 additions & 0 deletions mon-pix/app/components/authentication/password-reset-demand.scss
Original file line number Diff line number Diff line change
@@ -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;
}
}
2 changes: 2 additions & 0 deletions mon-pix/app/components/form-textfield.hbs
Original file line number Diff line number Diff line change
@@ -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 }}
<div class="form-textfield">
<label for="{{@textfieldName}}" class="form-textfield__label">
Expand Down
2 changes: 2 additions & 0 deletions mon-pix/app/components/form-textfield.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// DEPRECATED COMPONENT: DON’T USE IT! USE COMPONENTS OF THE DESIGN SYSTEM INSTEAD!

import { action } from '@ember/object';
import { isEmpty } from '@ember/utils';
import Component from '@glimmer/component';
Expand Down
10 changes: 10 additions & 0 deletions mon-pix/app/controllers/password-reset-demand.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import Controller from '@ember/controller';
import { service } from '@ember/service';

export default class LoginController extends Controller {
@service featureToggles;

get isNewAuthenticationDesignEnabled() {
return this.featureToggles.featureToggles.isNewAuthenticationDesignEnabled;
}
}
5 changes: 3 additions & 2 deletions mon-pix/app/styles/app.scss
Original file line number Diff line number Diff line change
Expand Up @@ -136,13 +136,14 @@ of an adaptative/mobile-first approach — refactoring is welcome here */

/* Styles for colocated components */
@import 'authentication-layout';
@import 'authentication/new-password-input/password-checklist';
@import 'authentication/new-password-input/password-rule';
@import 'authentication/oidc-provider-selector';
@import 'authentication/other-authentication-providers';
@import 'authentication/password-reset-demand';
@import 'authentication/signin-form';
@import 'authentication/signup-form';
@import 'authentication/sso-selection-form';
@import 'authentication/new-password-input/password-checklist';
@import 'authentication/new-password-input/password-rule';

/* pages */
@import 'pages/assessment-results';
Expand Down
25 changes: 21 additions & 4 deletions mon-pix/app/templates/password-reset-demand.hbs
Original file line number Diff line number Diff line change
@@ -1,4 +1,21 @@
{{page-title (t "pages.password-reset-demand.page-title")}}
<div class="sign-form-page">
<PasswordResetDemandForm />
</div>
{{page-title (t "pages.password-reset-demand.title")}}

{{#if this.isNewAuthenticationDesignEnabled}}
<AuthenticationLayout class="signin-page-layout">
<:header>
<PixButtonLink @variant="secondary" @route="authentication.login">
{{t "common.actions.login"}}
</PixButtonLink>
</:header>

<:content>
<h1 class="pix-title-m">{{t "pages.password-reset-demand.title"}}</h1>
<Authentication::PasswordResetDemandForm />
</:content>
</AuthenticationLayout>

{{else}}
<div class="sign-form-page">
<PasswordResetDemandForm />
</div>
{{/if}}
Loading

0 comments on commit 13b0edd

Please sign in to comment.