From 8f931d1e9d6e20fe8e54ac7558c21fc8765af792 Mon Sep 17 00:00:00 2001 From: Eunju Huss <eunju@sunet.se> Date: Thu, 16 May 2024 10:17:33 +0900 Subject: [PATCH 01/41] Update heading and paragraph --- src/components/Dashboard/ChangePasswordCustom.tsx | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/components/Dashboard/ChangePasswordCustom.tsx b/src/components/Dashboard/ChangePasswordCustom.tsx index 63bcc9237..a3e7d1547 100644 --- a/src/components/Dashboard/ChangePasswordCustom.tsx +++ b/src/components/Dashboard/ChangePasswordCustom.tsx @@ -46,18 +46,6 @@ export default function ChangePasswordCustomForm(props: ChangePasswordCustomForm return ( <form id="passwordsview-form" role="form" onSubmit={props.formProps.handleSubmit}> - <fieldset> - <FinalField - name="old" - component={TextInput} - componentClass="input" - type="password" - id="old-password-field" - label={<FormattedMessage defaultMessage="Current password" description="chpass old password label" />} - validate={required} - autoComplete="current-password" - /> - </fieldset> <div className="password-format"> <label> <FormattedMessage From 1d014de4dfc362014da49228fbfcc17554cdf650 Mon Sep 17 00:00:00 2001 From: Eunju Huss <eunju@sunet.se> Date: Thu, 16 May 2024 10:17:54 +0900 Subject: [PATCH 02/41] Remove current password input --- src/components/Dashboard/ChangePassword.tsx | 21 ++++++++++++++++--- .../Dashboard/ChangePasswordSuggested.tsx | 12 ----------- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/src/components/Dashboard/ChangePassword.tsx b/src/components/Dashboard/ChangePassword.tsx index b4a89d36e..7aa25f63d 100644 --- a/src/components/Dashboard/ChangePassword.tsx +++ b/src/components/Dashboard/ChangePassword.tsx @@ -29,9 +29,24 @@ function ChangePassword() { return ( <React.Fragment> - <h4> - <FormattedMessage defaultMessage="Change your current password" description="Dashboard change password" /> - </h4> + <section className="intro"> + <h1> + <FormattedMessage + description="Change password - headline" + defaultMessage="Change password: Suggested password" + /> + </h1> + <div className="lead"> + <p> + <FormattedMessage + description="Change password - lead" + defaultMessage={`To enhance security, we have generated a new password for you. You can either copy the + suggested password provided, or if your prefer, create a custom password by toggling + the Custom Password switch.`} + /> + </p> + </div> + </section> <div id="changePasswordDialog"> <ChangePasswordForm finish_url={finish_url} /> </div> diff --git a/src/components/Dashboard/ChangePasswordSuggested.tsx b/src/components/Dashboard/ChangePasswordSuggested.tsx index a35a7ec80..7a58fb2d9 100644 --- a/src/components/Dashboard/ChangePasswordSuggested.tsx +++ b/src/components/Dashboard/ChangePasswordSuggested.tsx @@ -9,18 +9,6 @@ export default function ChangePasswordSuggestedForm(props: ChangePasswordChildFo return ( <form id="passwordsview-form" role="form" onSubmit={props.formProps.handleSubmit}> - <fieldset> - <FinalField<string> - name="old" - component={TextInput} - componentClass="input" - type="password" - id="old-password-field" - label={<FormattedMessage defaultMessage="Current password" description="chpass old password label" />} - validate={required} - autoComplete="current-password" - /> - </fieldset> <fieldset> <FinalField<string> name="suggested" From 50386efdcc782c9ff10c14ac11647613b98af387 Mon Sep 17 00:00:00 2001 From: Eunju Huss <eunju@sunet.se> Date: Thu, 16 May 2024 10:24:06 +0900 Subject: [PATCH 03/41] Adjust heading text --- src/components/Dashboard/ChangePassword.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Dashboard/ChangePassword.tsx b/src/components/Dashboard/ChangePassword.tsx index 7aa25f63d..a010ba090 100644 --- a/src/components/Dashboard/ChangePassword.tsx +++ b/src/components/Dashboard/ChangePassword.tsx @@ -33,7 +33,7 @@ function ChangePassword() { <h1> <FormattedMessage description="Change password - headline" - defaultMessage="Change password: Suggested password" + defaultMessage="Change password: Password options" /> </h1> <div className="lead"> From 384c49d414d64bac7b4efd3128ee04ae04e68f4d Mon Sep 17 00:00:00 2001 From: Eunju Huss <eunju@sunet.se> Date: Thu, 16 May 2024 13:28:20 +0900 Subject: [PATCH 04/41] Remove old password requirement for change password endpoint --- src/apis/eduidSecurity.ts | 1 - src/components/Common/NewPasswordForm.tsx | 26 +++++--- src/components/Dashboard/ChangePassword.tsx | 3 +- .../Dashboard/ChangePasswordForm.tsx | 55 ++++++++++------- .../Dashboard/ChangePasswordSuggested.tsx | 61 +++++++++++++------ 5 files changed, 93 insertions(+), 53 deletions(-) diff --git a/src/apis/eduidSecurity.ts b/src/apis/eduidSecurity.ts index be7ef4a7e..a4bebd136 100644 --- a/src/apis/eduidSecurity.ts +++ b/src/apis/eduidSecurity.ts @@ -181,7 +181,6 @@ export const fetchSuggestedPassword = createAsyncThunk< /*********************************************************************************************************************/ export interface ChangePasswordPayload { - old_password: string; new_password: string; } diff --git a/src/components/Common/NewPasswordForm.tsx b/src/components/Common/NewPasswordForm.tsx index d5db90ca4..0ceaa61eb 100644 --- a/src/components/Common/NewPasswordForm.tsx +++ b/src/components/Common/NewPasswordForm.tsx @@ -2,7 +2,6 @@ import { ExtraSecurityAlternatives } from "apis/eduidResetPassword"; import CustomInput from "components/Common/CustomInput"; import EduIDButton from "components/Common/EduIDButton"; import { GoBackButton } from "components/ResetPassword/GoBackButton"; -import { FormApi, SubmissionErrors } from "final-form"; import { emptyStringPattern } from "helperFunctions/validation/regexPatterns"; import { Field as FinalField, Form as FinalForm } from "react-final-form"; import { FormattedMessage } from "react-intl"; @@ -14,15 +13,17 @@ export interface NewPasswordFormData { } interface NewPasswordFormProps { - readonly goBack?: () => void; - readonly extra_security?: ExtraSecurityAlternatives; - readonly suggested_password: string | undefined; - readonly submitNewPasswordForm: ( - values: NewPasswordFormData, - form: FormApi<NewPasswordFormData, Partial<NewPasswordFormData>>, - callback?: ((errors?: SubmissionErrors) => void) | undefined - ) => void | Promise<void>; - readonly submitButtonText: React.ReactChild; + goBack?: () => void; + extra_security?: ExtraSecurityAlternatives; + suggested_password: string | undefined; + submitNewPasswordForm: any; + // submitNewPasswordForm: ( + // values: NewPasswordFormData, + // form: FormApi<NewPasswordFormData, Partial<NewPasswordFormData>>, + // callback?: ((errors?: SubmissionErrors) => void) | undefined + // ) => void | Promise<void>; + submitButtonText: React.ReactChild; + handleCancel?: any; } export function NewPasswordForm(props: NewPasswordFormProps): JSX.Element { @@ -61,6 +62,11 @@ export function NewPasswordForm(props: NewPasswordFormProps): JSX.Element { {props.extra_security && Object.keys(props.extra_security).length > 0 && ( <GoBackButton onClickHandler={props.goBack} /> )} + {props.handleCancel && ( + <EduIDButton buttonstyle="secondary" onClick={props.handleCancel}> + <FormattedMessage defaultMessage="cancel" description="button cancel" /> + </EduIDButton> + )} <EduIDButton buttonstyle="primary" id="new-password-button" disabled={formProps.invalid}> {props.submitButtonText} </EduIDButton> diff --git a/src/components/Dashboard/ChangePassword.tsx b/src/components/Dashboard/ChangePassword.tsx index a010ba090..266539458 100644 --- a/src/components/Dashboard/ChangePassword.tsx +++ b/src/components/Dashboard/ChangePassword.tsx @@ -41,8 +41,7 @@ function ChangePassword() { <FormattedMessage description="Change password - lead" defaultMessage={`To enhance security, we have generated a new password for you. You can either copy the - suggested password provided, or if your prefer, create a custom password by toggling - the Custom Password switch.`} + suggested password provided, or if your prefer, create a custom password.`} /> </p> </div> diff --git a/src/components/Dashboard/ChangePasswordForm.tsx b/src/components/Dashboard/ChangePasswordForm.tsx index d3ed187f8..93cdb059c 100644 --- a/src/components/Dashboard/ChangePasswordForm.tsx +++ b/src/components/Dashboard/ChangePasswordForm.tsx @@ -1,11 +1,9 @@ import { changePassword } from "apis/eduidSecurity"; -import EduIDButton from "components/Common/EduIDButton"; import { useAppDispatch, useAppSelector } from "eduid-hooks"; import React, { useState } from "react"; import { Form as FinalForm, FormRenderProps } from "react-final-form"; import { FormattedMessage } from "react-intl"; import { useNavigate } from "react-router-dom"; -import { ButtonGroup } from "reactstrap"; import ChangePasswordCustomForm from "./ChangePasswordCustom"; import ChangePasswordSuggestedForm from "./ChangePasswordSuggested"; @@ -16,6 +14,7 @@ export interface ChangePasswordFormProps { // These are the props we pass to the sub-components with the different forms export interface ChangePasswordChildFormProps { formProps: FormRenderProps<ChangePasswordFormData>; + handleCancel?: any; } interface ChangePasswordFormData { @@ -57,6 +56,10 @@ function ChangePasswordForm(props: ChangePasswordFormProps) { const initialValues = { suggested }; + function handleSwitchChange() { + setRenderSuggested(!renderSuggested); + } + return ( <FinalForm<ChangePasswordFormData> onSubmit={handleSubmitPasswords} @@ -66,30 +69,36 @@ function ChangePasswordForm(props: ChangePasswordFormProps) { return ( <React.Fragment> + <fieldset> + <form> + <label className="toggle flex-between" htmlFor="change-custom-password"> + <legend> + <FormattedMessage defaultMessage="Create a custom password?" description="change custom passowrd" /> + </legend> + <input + onChange={handleSwitchChange} + className="toggle-checkbox" + type="checkbox" + checked={!renderSuggested} + id="change-custom-password" + /> + <div className="toggle-switch"></div> + </label> + </form> + <p className="help-text"> + <FormattedMessage + defaultMessage="Toggle the custom password switch to set your own password." + description="Change password toggle" + /> + </p> + </fieldset> {renderSuggested ? ( - <ChangePasswordSuggestedForm {...child_props} /> + <ChangePasswordSuggestedForm {...child_props} handleCancel={handleCancel} /> ) : ( - <ChangePasswordCustomForm {...child_props} /> + <ChangePasswordCustomForm {...child_props} handleCancel={handleCancel} /> )} - <div id="password-suggestion"> - <ButtonGroup> - <EduIDButton buttonstyle="link" className="normal-case" id="pwmode-button" onClick={togglePasswordType}> - {renderSuggested ? ( - <FormattedMessage - description="chpass custom password" - defaultMessage="I don't want a suggested password" - /> - ) : ( - <FormattedMessage - description="chpass suggest password" - defaultMessage="Suggest a password for me" - /> - )} - </EduIDButton> - </ButtonGroup> - </div> - <div id="chpass-form" className="tabpane buttons"> + {/* <div id="chpass-form" className="tabpane buttons"> <EduIDButton buttonstyle="secondary" onClick={handleCancel}> <FormattedMessage defaultMessage="cancel" description="button cancel" /> </EduIDButton> @@ -102,7 +111,7 @@ function ChangePasswordForm(props: ChangePasswordFormProps) { > <FormattedMessage defaultMessage="Save" description="button save" /> </EduIDButton> - </div> + </div> */} </React.Fragment> ); }} diff --git a/src/components/Dashboard/ChangePasswordSuggested.tsx b/src/components/Dashboard/ChangePasswordSuggested.tsx index 7a58fb2d9..25e8820cc 100644 --- a/src/components/Dashboard/ChangePasswordSuggested.tsx +++ b/src/components/Dashboard/ChangePasswordSuggested.tsx @@ -1,27 +1,54 @@ -import TextInput from "components/Common/EduIDTextInput"; -import { Field as FinalField } from "react-final-form"; +import { CopyToClipboard } from "components/Common/CopyToClipboard"; +import { NewPasswordForm } from "components/Common/NewPasswordForm"; +import { useAppSelector } from "eduid-hooks"; +import { useRef } from "react"; import { FormattedMessage } from "react-intl"; import { ChangePasswordChildFormProps } from "./ChangePasswordForm"; export default function ChangePasswordSuggestedForm(props: ChangePasswordChildFormProps) { + const ref = useRef<HTMLInputElement>(null); + const suggested_password = useAppSelector((state) => state.chpass.suggested_password); // Form field validator const required = (value: string) => (value ? undefined : "required"); - + console.log("props", props); return ( - <form id="passwordsview-form" role="form" onSubmit={props.formProps.handleSubmit}> - <fieldset> - <FinalField<string> - name="suggested" - component={TextInput} - componentClass="input" - type="text" - id="suggested-password-field" - className="suggested-password" - label={<FormattedMessage defaultMessage="Suggested password" description="chpass suggested password" />} - disabled={true} - autoComplete="new-password" + <> + <div className="reset-password-input"> + <label htmlFor="copy-new-password"> + <FormattedMessage defaultMessage="New password" description="Set new password" /> + </label> + <input + name="copy-new-password" + id="copy-new-password" + ref={ref} + defaultValue={suggested_password ? suggested_password : ""} + readOnly={true} /> - </fieldset> - </form> + <CopyToClipboard ref={ref} /> + </div> + <NewPasswordForm + suggested_password={suggested_password} + submitNewPasswordForm={props.formProps.handleSubmit} + submitButtonText={ + <FormattedMessage defaultMessage="accept password" description="Set new password (accept button)" /> + } + handleCancel={props.handleCancel} + /> + </> + // <form id="passwordsview-form" role="form" onSubmit={props.formProps.handleSubmit}> + // <fieldset> + // <FinalField<string> + // name="suggested" + // component={TextInput} + // componentClass="input" + // type="text" + // id="suggested-password-field" + // className="suggested-password" + // label={<FormattedMessage defaultMessage="Suggested password" description="chpass suggested password" />} + // disabled={true} + // autoComplete="new-password" + // /> + // </fieldset> + // </form> ); } From 1dcb2a2fd419819c78b4d5d902641bf5fe238840 Mon Sep 17 00:00:00 2001 From: Eunju Huss <eunju@sunet.se> Date: Thu, 16 May 2024 15:02:11 +0900 Subject: [PATCH 05/41] Add border-bottom --- src/styles/_ChangePassword.scss | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/styles/_ChangePassword.scss b/src/styles/_ChangePassword.scss index 436f549e1..0460354ca 100644 --- a/src/styles/_ChangePassword.scss +++ b/src/styles/_ChangePassword.scss @@ -11,7 +11,9 @@ #changePasswordDialog { // background: yellow; & fieldset { - margin-bottom: 1rem; + padding-bottom: 2rem; + margin-bottom: 2rem; + border-bottom: 1px solid #d8d8d8; } } From 8d9c831b2af61ffa8460d38a343242752444be09 Mon Sep 17 00:00:00 2001 From: Eunju Huss <eunju@sunet.se> Date: Thu, 16 May 2024 15:02:33 +0900 Subject: [PATCH 06/41] Remove ChangePasswordForm --- src/components/Dashboard/ChangePassword.tsx | 161 +++++++++++++++--- .../Dashboard/ChangePasswordCustom.tsx | 19 ++- .../Dashboard/ChangePasswordForm.tsx | 122 ------------- .../Dashboard/ChangePasswordSuggested.tsx | 31 +--- 4 files changed, 160 insertions(+), 173 deletions(-) delete mode 100644 src/components/Dashboard/ChangePasswordForm.tsx diff --git a/src/components/Dashboard/ChangePassword.tsx b/src/components/Dashboard/ChangePassword.tsx index 266539458..11688946f 100644 --- a/src/components/Dashboard/ChangePassword.tsx +++ b/src/components/Dashboard/ChangePassword.tsx @@ -1,17 +1,39 @@ -import { fetchSuggestedPassword } from "apis/eduidSecurity"; +import { changePassword, fetchSuggestedPassword } from "apis/eduidSecurity"; import { useAppDispatch, useAppSelector } from "eduid-hooks"; -import React, { useEffect } from "react"; +import React, { useEffect, useState } from "react"; +import { Form as FinalForm, FormRenderProps } from "react-final-form"; import { FormattedMessage, useIntl } from "react-intl"; -import ChangePasswordForm from "./ChangePasswordForm"; +import { useNavigate } from "react-router-dom"; +import ChangePasswordCustomForm from "./ChangePasswordCustom"; +import ChangePasswordSuggestedForm from "./ChangePasswordSuggested"; // exported for use in tests export const finish_url = "/profile/security"; +export interface ChangePasswordFormProps { + finish_url: string; // URL to direct browser to when user cancels password change, or completes it +} + +export interface ChangePasswordChildFormProps { + formProps: FormRenderProps<ChangePasswordFormData>; + handleCancel?: any; +} + +interface ChangePasswordFormData { + old?: string; // used by both modes + custom?: string; // used with custom password + score?: number; // used with custom password + suggested?: string; // used with suggested password +} + function ChangePassword() { const suggested_password = useAppSelector((state) => state.chpass.suggested_password); const is_app_loaded = useAppSelector((state) => state.config.is_app_loaded); const dispatch = useAppDispatch(); const intl = useIntl(); + const suggested = useAppSelector((state) => state.chpass.suggested_password); + const [renderSuggested, setRenderSuggested] = useState(true); // toggle display of custom or suggested password forms + const navigate = useNavigate(); useEffect(() => { document.title = intl.formatMessage({ @@ -27,29 +49,118 @@ function ChangePassword() { } }, [suggested_password, is_app_loaded]); + function togglePasswordType() { + // Toggle between rendering the suggested password form, or the custom password form + setRenderSuggested(!renderSuggested); + } + + async function handleSubmitPasswords(values: ChangePasswordFormData) { + console.log("values", values); + // Use the right form field for the currently displayed password mode + const newPassword = renderSuggested ? values.suggested : values.custom; + // Callback from sub-component when the user clicks on the button to change password + if (newPassword) { + const response = await dispatch(changePassword({ new_password: newPassword })); + if (changePassword.fulfilled.match(response)) { + navigate(finish_url); + } + } + } + + function handleCancel(event: React.MouseEvent<HTMLElement>) { + // Callback from sub-component when the user clicks on the button to abort changing password + event.preventDefault(); + // TODO: should clear passwords from form to avoid browser password manager asking user to save the password + navigate(finish_url); + } + + const initialValues = { suggested }; + + function handleSwitchChange() { + setRenderSuggested(!renderSuggested); + } + return ( - <React.Fragment> - <section className="intro"> - <h1> - <FormattedMessage - description="Change password - headline" - defaultMessage="Change password: Password options" - /> - </h1> - <div className="lead"> - <p> - <FormattedMessage - description="Change password - lead" - defaultMessage={`To enhance security, we have generated a new password for you. You can either copy the - suggested password provided, or if your prefer, create a custom password.`} - /> - </p> - </div> - </section> - <div id="changePasswordDialog"> - <ChangePasswordForm finish_url={finish_url} /> - </div> - </React.Fragment> + <FinalForm<ChangePasswordFormData> + onSubmit={handleSubmitPasswords} + initialValues={initialValues} + render={(formProps) => { + const child_props: ChangePasswordChildFormProps = { formProps }; + + return ( + <React.Fragment> + {renderSuggested ? ( + <section className="intro"> + <h1> + <FormattedMessage + description="Change password - headline" + defaultMessage="Change password: Suggested password" + /> + </h1> + <div className="lead"> + <p> + <FormattedMessage + description="Change password - lead" + defaultMessage={`A strong password has been generated for you. To proceed you will need to copy + the password in to the Repeat new password field and click Accept Password and save it for + future use. Note: spaces in the generated password are there for legibility and will be + removed automatically if entered.`} + /> + </p> + </div> + </section> + ) : ( + <section className="intro"> + <h1> + <FormattedMessage + description="Change password - headline" + defaultMessage="Change password: Custom password" + /> + </h1> + <div className="lead"> + <p> + <FormattedMessage + description="Change password - lead" + defaultMessage={`To enhance security, we have generated a new password for you. You can either copy the + suggested password provided, or if your prefer, create a custom password.`} + /> + </p> + </div> + </section> + )} + + <fieldset> + <form> + <label className="toggle flex-between" htmlFor="change-custom-password"> + <legend> + <FormattedMessage defaultMessage="Create a custom password?" description="change custom password" /> + </legend> + <input + onChange={handleSwitchChange} + className="toggle-checkbox" + type="checkbox" + checked={!renderSuggested} + id="change-custom-password" + /> + <div className="toggle-switch"></div> + </label> + </form> + <p className="help-text"> + <FormattedMessage + defaultMessage="Toggle the custom password switch to set your own password." + description="Change password toggle" + /> + </p> + </fieldset> + {renderSuggested ? ( + <ChangePasswordSuggestedForm {...child_props} handleCancel={handleCancel} /> + ) : ( + <ChangePasswordCustomForm {...child_props} handleCancel={handleCancel} /> + )} + </React.Fragment> + ); + }} + /> ); } diff --git a/src/components/Dashboard/ChangePasswordCustom.tsx b/src/components/Dashboard/ChangePasswordCustom.tsx index a3e7d1547..db2079aac 100644 --- a/src/components/Dashboard/ChangePasswordCustom.tsx +++ b/src/components/Dashboard/ChangePasswordCustom.tsx @@ -1,9 +1,10 @@ +import EduIDButton from "components/Common/EduIDButton"; import TextInput from "components/Common/EduIDTextInput"; import PasswordStrengthMeter, { PasswordStrengthData } from "components/Common/PasswordStrengthMeter"; import { useState } from "react"; import { Field as FinalField } from "react-final-form"; import { FormattedMessage } from "react-intl"; -import { ChangePasswordChildFormProps } from "./ChangePasswordForm"; +import { ChangePasswordChildFormProps } from "./ChangePassword"; // eslint-disable-next-line @typescript-eslint/no-empty-interface interface ChangePasswordCustomFormProps extends ChangePasswordChildFormProps {} @@ -45,7 +46,7 @@ export default function ChangePasswordCustomForm(props: ChangePasswordCustomForm } return ( - <form id="passwordsview-form" role="form" onSubmit={props.formProps.handleSubmit}> + <form id="passwordsview-form" onSubmit={props.formProps.handleSubmit}> <div className="password-format"> <label> <FormattedMessage @@ -111,6 +112,20 @@ export default function ChangePasswordCustomForm(props: ChangePasswordCustomForm /> </div> </fieldset> + <div id="chpass-form" className="tabpane buttons"> + <EduIDButton buttonstyle="secondary" onClick={props.handleCancel}> + <FormattedMessage defaultMessage="cancel" description="button cancel" /> + </EduIDButton> + <EduIDButton + type="submit" + id="chpass-button" + buttonstyle="primary" + disabled={props.formProps.submitting || props.formProps.invalid} + onClick={props.formProps.handleSubmit} + > + <FormattedMessage defaultMessage="Save" description="button save" /> + </EduIDButton> + </div> </form> ); } diff --git a/src/components/Dashboard/ChangePasswordForm.tsx b/src/components/Dashboard/ChangePasswordForm.tsx deleted file mode 100644 index 93cdb059c..000000000 --- a/src/components/Dashboard/ChangePasswordForm.tsx +++ /dev/null @@ -1,122 +0,0 @@ -import { changePassword } from "apis/eduidSecurity"; -import { useAppDispatch, useAppSelector } from "eduid-hooks"; -import React, { useState } from "react"; -import { Form as FinalForm, FormRenderProps } from "react-final-form"; -import { FormattedMessage } from "react-intl"; -import { useNavigate } from "react-router-dom"; -import ChangePasswordCustomForm from "./ChangePasswordCustom"; -import ChangePasswordSuggestedForm from "./ChangePasswordSuggested"; - -export interface ChangePasswordFormProps { - finish_url: string; // URL to direct browser to when user cancels password change, or completes it -} - -// These are the props we pass to the sub-components with the different forms -export interface ChangePasswordChildFormProps { - formProps: FormRenderProps<ChangePasswordFormData>; - handleCancel?: any; -} - -interface ChangePasswordFormData { - old?: string; // used by both modes - custom?: string; // used with custom password - score?: number; // used with custom password - suggested?: string; // used with suggested password -} - -function ChangePasswordForm(props: ChangePasswordFormProps) { - const suggested = useAppSelector((state) => state.chpass.suggested_password); - const [renderSuggested, setRenderSuggested] = useState(true); // toggle display of custom or suggested password forms - const dispatch = useAppDispatch(); - const navigate = useNavigate(); - - function togglePasswordType() { - // Toggle between rendering the suggested password form, or the custom password form - setRenderSuggested(!renderSuggested); - } - - async function handleSubmitPasswords(values: ChangePasswordFormData) { - // Use the right form field for the currently displayed password mode - const newPassword = renderSuggested ? values.suggested : values.custom; - // Callback from sub-component when the user clicks on the button to change password - if (values.old && newPassword) { - const response = await dispatch(changePassword({ old_password: values.old, new_password: newPassword })); - if (changePassword.fulfilled.match(response)) { - navigate(props.finish_url); - } - } - } - - function handleCancel(event: React.MouseEvent<HTMLElement>) { - // Callback from sub-component when the user clicks on the button to abort changing password - event.preventDefault(); - // TODO: should clear passwords from form to avoid browser password manager asking user to save the password - navigate(props.finish_url); - } - - const initialValues = { suggested }; - - function handleSwitchChange() { - setRenderSuggested(!renderSuggested); - } - - return ( - <FinalForm<ChangePasswordFormData> - onSubmit={handleSubmitPasswords} - initialValues={initialValues} - render={(formProps) => { - const child_props: ChangePasswordChildFormProps = { formProps }; - - return ( - <React.Fragment> - <fieldset> - <form> - <label className="toggle flex-between" htmlFor="change-custom-password"> - <legend> - <FormattedMessage defaultMessage="Create a custom password?" description="change custom passowrd" /> - </legend> - <input - onChange={handleSwitchChange} - className="toggle-checkbox" - type="checkbox" - checked={!renderSuggested} - id="change-custom-password" - /> - <div className="toggle-switch"></div> - </label> - </form> - <p className="help-text"> - <FormattedMessage - defaultMessage="Toggle the custom password switch to set your own password." - description="Change password toggle" - /> - </p> - </fieldset> - {renderSuggested ? ( - <ChangePasswordSuggestedForm {...child_props} handleCancel={handleCancel} /> - ) : ( - <ChangePasswordCustomForm {...child_props} handleCancel={handleCancel} /> - )} - - {/* <div id="chpass-form" className="tabpane buttons"> - <EduIDButton buttonstyle="secondary" onClick={handleCancel}> - <FormattedMessage defaultMessage="cancel" description="button cancel" /> - </EduIDButton> - <EduIDButton - type="submit" - id="chpass-button" - buttonstyle="primary" - disabled={formProps.submitting || formProps.invalid} - onClick={formProps.handleSubmit} - > - <FormattedMessage defaultMessage="Save" description="button save" /> - </EduIDButton> - </div> */} - </React.Fragment> - ); - }} - /> - ); -} - -export default ChangePasswordForm; diff --git a/src/components/Dashboard/ChangePasswordSuggested.tsx b/src/components/Dashboard/ChangePasswordSuggested.tsx index 25e8820cc..a44212a7d 100644 --- a/src/components/Dashboard/ChangePasswordSuggested.tsx +++ b/src/components/Dashboard/ChangePasswordSuggested.tsx @@ -1,18 +1,18 @@ import { CopyToClipboard } from "components/Common/CopyToClipboard"; import { NewPasswordForm } from "components/Common/NewPasswordForm"; import { useAppSelector } from "eduid-hooks"; -import { useRef } from "react"; +import React, { useRef } from "react"; import { FormattedMessage } from "react-intl"; -import { ChangePasswordChildFormProps } from "./ChangePasswordForm"; +import { ChangePasswordChildFormProps } from "./ChangePassword"; export default function ChangePasswordSuggestedForm(props: ChangePasswordChildFormProps) { const ref = useRef<HTMLInputElement>(null); const suggested_password = useAppSelector((state) => state.chpass.suggested_password); // Form field validator const required = (value: string) => (value ? undefined : "required"); - console.log("props", props); + return ( - <> + <React.Fragment> <div className="reset-password-input"> <label htmlFor="copy-new-password"> <FormattedMessage defaultMessage="New password" description="Set new password" /> @@ -21,7 +21,7 @@ export default function ChangePasswordSuggestedForm(props: ChangePasswordChildFo name="copy-new-password" id="copy-new-password" ref={ref} - defaultValue={suggested_password ? suggested_password : ""} + defaultValue={suggested_password} readOnly={true} /> <CopyToClipboard ref={ref} /> @@ -29,26 +29,9 @@ export default function ChangePasswordSuggestedForm(props: ChangePasswordChildFo <NewPasswordForm suggested_password={suggested_password} submitNewPasswordForm={props.formProps.handleSubmit} - submitButtonText={ - <FormattedMessage defaultMessage="accept password" description="Set new password (accept button)" /> - } + submitButtonText={<FormattedMessage defaultMessage="Ok" description="Set new password (ok button)" />} handleCancel={props.handleCancel} /> - </> - // <form id="passwordsview-form" role="form" onSubmit={props.formProps.handleSubmit}> - // <fieldset> - // <FinalField<string> - // name="suggested" - // component={TextInput} - // componentClass="input" - // type="text" - // id="suggested-password-field" - // className="suggested-password" - // label={<FormattedMessage defaultMessage="Suggested password" description="chpass suggested password" />} - // disabled={true} - // autoComplete="new-password" - // /> - // </fieldset> - // </form> + </React.Fragment> ); } From 42861d851cc88a5b7ed71f8ca6df85e0c3262755 Mon Sep 17 00:00:00 2001 From: Eunju Huss <eunju@sunet.se> Date: Thu, 16 May 2024 15:08:09 +0900 Subject: [PATCH 07/41] Fix issue, A form label must be associated with a control --- src/components/Dashboard/ChangePassword.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/components/Dashboard/ChangePassword.tsx b/src/components/Dashboard/ChangePassword.tsx index 11688946f..c16f4923e 100644 --- a/src/components/Dashboard/ChangePassword.tsx +++ b/src/components/Dashboard/ChangePassword.tsx @@ -132,9 +132,7 @@ function ChangePassword() { <fieldset> <form> <label className="toggle flex-between" htmlFor="change-custom-password"> - <legend> - <FormattedMessage defaultMessage="Create a custom password?" description="change custom password" /> - </legend> + <FormattedMessage defaultMessage="Create a custom password?" description="change custom password" /> <input onChange={handleSwitchChange} className="toggle-checkbox" From ccf81b8bc1f6b488d144016ad2896abf1b543378 Mon Sep 17 00:00:00 2001 From: Eunju Huss <eunju@sunet.se> Date: Thu, 16 May 2024 15:24:25 +0900 Subject: [PATCH 08/41] Ajust css and paragraph text --- src/components/Dashboard/ChangePassword.tsx | 7 +++---- src/styles/_ChangePassword.scss | 11 ++++------- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/src/components/Dashboard/ChangePassword.tsx b/src/components/Dashboard/ChangePassword.tsx index c16f4923e..8b46a28ab 100644 --- a/src/components/Dashboard/ChangePassword.tsx +++ b/src/components/Dashboard/ChangePassword.tsx @@ -55,7 +55,6 @@ function ChangePassword() { } async function handleSubmitPasswords(values: ChangePasswordFormData) { - console.log("values", values); // Use the right form field for the currently displayed password mode const newPassword = renderSuggested ? values.suggested : values.custom; // Callback from sub-component when the user clicks on the button to change password @@ -121,15 +120,15 @@ function ChangePassword() { <p> <FormattedMessage description="Change password - lead" - defaultMessage={`To enhance security, we have generated a new password for you. You can either copy the - suggested password provided, or if your prefer, create a custom password.`} + defaultMessage={`When creating your own password. make sure it's strong enough to keep your + accounts safe.`} /> </p> </div> </section> )} - <fieldset> + <fieldset className="toggle-change-password-options"> <form> <label className="toggle flex-between" htmlFor="change-custom-password"> <FormattedMessage defaultMessage="Create a custom password?" description="change custom password" /> diff --git a/src/styles/_ChangePassword.scss b/src/styles/_ChangePassword.scss index 0460354ca..fb62c67d8 100644 --- a/src/styles/_ChangePassword.scss +++ b/src/styles/_ChangePassword.scss @@ -8,13 +8,10 @@ margin-bottom: 1rem; } -#changePasswordDialog { - // background: yellow; - & fieldset { - padding-bottom: 2rem; - margin-bottom: 2rem; - border-bottom: 1px solid #d8d8d8; - } +fieldset.toggle-change-password-options { + padding-bottom: 2rem; + margin-bottom: 2rem; + border-bottom: 1px solid #d8d8d8; } #password-custom-help { From c5b0f80228c7274f1cab9286c08d2f0ae91174f1 Mon Sep 17 00:00:00 2001 From: Eunju Huss <eunju@sunet.se> Date: Thu, 16 May 2024 18:49:28 +0900 Subject: [PATCH 09/41] Save temporary --- src/components/Dashboard/ChangePassword.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/Dashboard/ChangePassword.tsx b/src/components/Dashboard/ChangePassword.tsx index 8b46a28ab..e9b5adbdd 100644 --- a/src/components/Dashboard/ChangePassword.tsx +++ b/src/components/Dashboard/ChangePassword.tsx @@ -20,7 +20,6 @@ export interface ChangePasswordChildFormProps { } interface ChangePasswordFormData { - old?: string; // used by both modes custom?: string; // used with custom password score?: number; // used with custom password suggested?: string; // used with suggested password From f09ec0cd70d2abaefb0034f78289790b98530729 Mon Sep 17 00:00:00 2001 From: Eunju Huss <eunju@sunet.se> Date: Fri, 17 May 2024 08:51:11 +0900 Subject: [PATCH 10/41] Remove container --- src/components/Dashboard/ChangePassword.tsx | 9 +-------- src/components/Dashboard/ChangePasswordDisplay.tsx | 2 +- src/components/IndexMain.tsx | 4 ++-- 3 files changed, 4 insertions(+), 11 deletions(-) diff --git a/src/components/Dashboard/ChangePassword.tsx b/src/components/Dashboard/ChangePassword.tsx index e9b5adbdd..ea5d9e138 100644 --- a/src/components/Dashboard/ChangePassword.tsx +++ b/src/components/Dashboard/ChangePassword.tsx @@ -25,7 +25,7 @@ interface ChangePasswordFormData { suggested?: string; // used with suggested password } -function ChangePassword() { +export function ChangePassword() { const suggested_password = useAppSelector((state) => state.chpass.suggested_password); const is_app_loaded = useAppSelector((state) => state.config.is_app_loaded); const dispatch = useAppDispatch(); @@ -48,11 +48,6 @@ function ChangePassword() { } }, [suggested_password, is_app_loaded]); - function togglePasswordType() { - // Toggle between rendering the suggested password form, or the custom password form - setRenderSuggested(!renderSuggested); - } - async function handleSubmitPasswords(values: ChangePasswordFormData) { // Use the right form field for the currently displayed password mode const newPassword = renderSuggested ? values.suggested : values.custom; @@ -159,5 +154,3 @@ function ChangePassword() { /> ); } - -export const ChangePasswordContainer = ChangePassword; diff --git a/src/components/Dashboard/ChangePasswordDisplay.tsx b/src/components/Dashboard/ChangePasswordDisplay.tsx index 84e80581a..479c19eeb 100644 --- a/src/components/Dashboard/ChangePasswordDisplay.tsx +++ b/src/components/Dashboard/ChangePasswordDisplay.tsx @@ -14,7 +14,7 @@ function ChangePasswordDisplay(props: ChangePasswordDisplayProps) { function handleAcceptModal() { const chpassURL = config.authn_service_url + "chpass"; - // the "chpass" path will route to the ChangePasswordContainer when we get back + // the "chpass" path will route to the ChangePassword when we get back const nextURL = config.dashboard_link + "chpass"; const url = chpassURL + "?next=" + encodeURIComponent(nextURL); window.location.assign(url); diff --git a/src/components/IndexMain.tsx b/src/components/IndexMain.tsx index c1f4e5cfc..90b7e33d3 100644 --- a/src/components/IndexMain.tsx +++ b/src/components/IndexMain.tsx @@ -12,7 +12,7 @@ import { PageNotFound } from "./Common/PageNotFound"; import { Settings } from "./Common/Settings"; import Splash from "./Common/Splash"; import { AdvancedSettings } from "./Dashboard/AdvancedSettings"; -import { ChangePasswordContainer } from "./Dashboard/ChangePassword"; +import { ChangePassword } from "./Dashboard/ChangePassword"; import Start from "./Dashboard/DashboardStart"; import VerifyIdentity from "./Dashboard/VerifyIdentity"; import { Help } from "./Help"; @@ -61,7 +61,7 @@ export function IndexMain(): JSX.Element { <Route path={settingsPath} element={<Settings />} /> <Route path="/profile/settings/" element={<Navigate to={settingsPath} />} /> <Route path={identityPath} element={<VerifyIdentity />} /> - <Route path="/profile/chpass/" element={<ChangePasswordContainer />} /> + <Route path="/profile/chpass/" element={<ChangePassword />} /> <Route path="/profile/ext-return/:app_name/:authn_id" element={<ExternalReturnHandler />} /> {/* Navigates for old paths. TODO: redirect in backend server instead */} <Route path="/profile/security/" element={<Navigate to="/profile/settings/" />} /> From e4b167ebac6759cdbb1d0402419685a3839e42b9 Mon Sep 17 00:00:00 2001 From: Eunju Huss <eunju@sunet.se> Date: Fri, 17 May 2024 09:56:25 +0900 Subject: [PATCH 11/41] Rename class for reusability --- src/components/Dashboard/ChangePasswordSuggested.tsx | 6 ++---- src/components/ResetPassword/SetNewPassword.tsx | 2 +- src/styles/_ResetPassword.scss | 2 +- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/components/Dashboard/ChangePasswordSuggested.tsx b/src/components/Dashboard/ChangePasswordSuggested.tsx index a44212a7d..219106072 100644 --- a/src/components/Dashboard/ChangePasswordSuggested.tsx +++ b/src/components/Dashboard/ChangePasswordSuggested.tsx @@ -8,14 +8,12 @@ import { ChangePasswordChildFormProps } from "./ChangePassword"; export default function ChangePasswordSuggestedForm(props: ChangePasswordChildFormProps) { const ref = useRef<HTMLInputElement>(null); const suggested_password = useAppSelector((state) => state.chpass.suggested_password); - // Form field validator - const required = (value: string) => (value ? undefined : "required"); return ( <React.Fragment> - <div className="reset-password-input"> + <div className="copy-password-input"> <label htmlFor="copy-new-password"> - <FormattedMessage defaultMessage="New password" description="Set new password" /> + <FormattedMessage defaultMessage="new password" description="new password" /> </label> <input name="copy-new-password" diff --git a/src/components/ResetPassword/SetNewPassword.tsx b/src/components/ResetPassword/SetNewPassword.tsx index 90f5a88e9..d89f134d4 100644 --- a/src/components/ResetPassword/SetNewPassword.tsx +++ b/src/components/ResetPassword/SetNewPassword.tsx @@ -100,7 +100,7 @@ export function SetNewPassword(): JSX.Element | null { </p> </div> </section> - <div className="reset-password-input"> + <div className="copy-password-input"> <label htmlFor="copy-new-password"> <FormattedMessage defaultMessage="New password" description="Set new password" /> </label> diff --git a/src/styles/_ResetPassword.scss b/src/styles/_ResetPassword.scss index 07ea14473..0b6082e37 100644 --- a/src/styles/_ResetPassword.scss +++ b/src/styles/_ResetPassword.scss @@ -152,7 +152,7 @@ a#resend-phone:not(.button-active) { } } -.reset-password-input { +.copy-password-input { position: relative; // not needed since Splash is relative but best leave for safety. } From fbc1799d90cc84975a33c5b1debc169d1ef61fde Mon Sep 17 00:00:00 2001 From: Eunju Huss <eunju@sunet.se> Date: Fri, 17 May 2024 10:05:55 +0900 Subject: [PATCH 12/41] Change textInput to customInput --- src/components/Common/InputWrapper.tsx | 2 ++ src/components/Dashboard/ChangePasswordCustom.tsx | 11 ++++------- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/components/Common/InputWrapper.tsx b/src/components/Common/InputWrapper.tsx index fb8194a0d..c6054b288 100644 --- a/src/components/Common/InputWrapper.tsx +++ b/src/components/Common/InputWrapper.tsx @@ -8,6 +8,7 @@ export interface InputWrapperProps extends FieldRenderProps<string> { helpBlock?: React.ReactNode; // help text show above input autoComplete?: "current-password" | "new-password" | "username"; children?: React.ReactNode; + passwordStrengthMeter?: React.ReactNode; } /** @@ -56,6 +57,7 @@ function RenderErrorMessage(props: InputWrapperProps): JSX.Element | null { <span role="alert" aria-invalid="true" tabIndex={0} className="input-validate-error"> {errorMsg || submitErrorMsg} </span> + {props.passwordStrengthMeter && props.passwordStrengthMeter} </FormText> ); } diff --git a/src/components/Dashboard/ChangePasswordCustom.tsx b/src/components/Dashboard/ChangePasswordCustom.tsx index db2079aac..91d7e95ea 100644 --- a/src/components/Dashboard/ChangePasswordCustom.tsx +++ b/src/components/Dashboard/ChangePasswordCustom.tsx @@ -1,5 +1,5 @@ +import CustomInput from "components/Common/CustomInput"; import EduIDButton from "components/Common/EduIDButton"; -import TextInput from "components/Common/EduIDTextInput"; import PasswordStrengthMeter, { PasswordStrengthData } from "components/Common/PasswordStrengthMeter"; import { useState } from "react"; import { Field as FinalField } from "react-final-form"; @@ -21,9 +21,6 @@ export default function ChangePasswordCustomForm(props: ChangePasswordCustomForm props.formProps.form.change("custom", props.formProps.values.custom); } - // Form field validators - const required = (value?: string) => (value ? undefined : "required"); - function strongEnough(value?: string): string | undefined { // check that the custom password is strong enough, using a score computed in the // PasswordStrengthMeter component. @@ -86,13 +83,13 @@ export default function ChangePasswordCustomForm(props: ChangePasswordCustomForm <div> <FinalField name="custom" - component={TextInput} + component={CustomInput} componentClass="input" type="password" label={ <FormattedMessage defaultMessage="Enter new password" description="chpass form custom password label" /> } - helpBlock={ + passwordStrengthMeter={ <PasswordStrengthMeter password={props.formProps.values.custom} passStateUp={updatePasswordData} /> } id="custom-password-field" @@ -101,7 +98,7 @@ export default function ChangePasswordCustomForm(props: ChangePasswordCustomForm /> <FinalField name="repeat" - component={TextInput} + component={CustomInput} componentClass="input" type="password" id="repeat-password-field" From f7a2824fa7a62c2c76f3a19637a9c569c8a17831 Mon Sep 17 00:00:00 2001 From: Eunju Huss <eunju@sunet.se> Date: Fri, 17 May 2024 10:19:37 +0900 Subject: [PATCH 13/41] Add InputType --- src/components/Common/CustomInput.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/Common/CustomInput.tsx b/src/components/Common/CustomInput.tsx index 8b6198d1a..cf98e95df 100644 --- a/src/components/Common/CustomInput.tsx +++ b/src/components/Common/CustomInput.tsx @@ -2,6 +2,8 @@ import { FieldRenderProps } from "react-final-form"; import { Input } from "reactstrap"; import { InputWrapper } from "./InputWrapper"; +type InputType = "text" | "password" | "email"; + export default function CustomInput(props: FieldRenderProps<string>): JSX.Element { // the InputWrapper renders it's children plus a label, helpBlock and any error message from the field validation return ( @@ -16,7 +18,7 @@ const InputElement = (props: FieldRenderProps<string>): JSX.Element => { <Input {...props.input} id={props.input.name} - type={props.type} + type={props.input.type as InputType} placeholder={props.placeholder} aria-required={props.input.required} valid={props.meta.valid} From ec992e873749ea9ea53d1145e15c6d4224dbfde7 Mon Sep 17 00:00:00 2001 From: Eunju Huss <eunju@sunet.se> Date: Fri, 17 May 2024 12:20:14 +0900 Subject: [PATCH 14/41] Add splash, when suggested_password is absentm the spinner spins --- src/components/Dashboard/ChangePassword.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/Dashboard/ChangePassword.tsx b/src/components/Dashboard/ChangePassword.tsx index ea5d9e138..0026a9e98 100644 --- a/src/components/Dashboard/ChangePassword.tsx +++ b/src/components/Dashboard/ChangePassword.tsx @@ -1,4 +1,5 @@ import { changePassword, fetchSuggestedPassword } from "apis/eduidSecurity"; +import Splash from "components/Common/Splash"; import { useAppDispatch, useAppSelector } from "eduid-hooks"; import React, { useEffect, useState } from "react"; import { Form as FinalForm, FormRenderProps } from "react-final-form"; @@ -81,7 +82,7 @@ export function ChangePassword() { const child_props: ChangePasswordChildFormProps = { formProps }; return ( - <React.Fragment> + <Splash showChildren={Boolean(suggested_password)}> {renderSuggested ? ( <section className="intro"> <h1> @@ -148,7 +149,7 @@ export function ChangePassword() { ) : ( <ChangePasswordCustomForm {...child_props} handleCancel={handleCancel} /> )} - </React.Fragment> + </Splash> ); }} /> From 9cc87aadc5033339cce42da7009eb931192c3f81 Mon Sep 17 00:00:00 2001 From: Eunju Huss <eunju@sunet.se> Date: Fri, 17 May 2024 15:13:17 +0900 Subject: [PATCH 15/41] Render passwordStrengthMeter if it exists --- src/components/Common/InputWrapper.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Common/InputWrapper.tsx b/src/components/Common/InputWrapper.tsx index c6054b288..063ac4a72 100644 --- a/src/components/Common/InputWrapper.tsx +++ b/src/components/Common/InputWrapper.tsx @@ -54,10 +54,10 @@ function RenderErrorMessage(props: InputWrapperProps): JSX.Element | null { return ( <FormText> + {props.passwordStrengthMeter && props.passwordStrengthMeter} <span role="alert" aria-invalid="true" tabIndex={0} className="input-validate-error"> {errorMsg || submitErrorMsg} </span> - {props.passwordStrengthMeter && props.passwordStrengthMeter} </FormText> ); } From 02816579eaac5742bb1546b22d1f3aab8a6134f5 Mon Sep 17 00:00:00 2001 From: Eunju Huss <eunju@sunet.se> Date: Fri, 17 May 2024 15:16:05 +0900 Subject: [PATCH 16/41] Change the meter max to 5 and render error message if props.password is not undefined --- src/components/Common/PasswordStrengthMeter.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/components/Common/PasswordStrengthMeter.tsx b/src/components/Common/PasswordStrengthMeter.tsx index 73c7ed6ea..84b53e844 100644 --- a/src/components/Common/PasswordStrengthMeter.tsx +++ b/src/components/Common/PasswordStrengthMeter.tsx @@ -21,7 +21,6 @@ function PasswordStrengthMeter(props: PasswordStrengthMeterProps) { const emails = useAppSelector((state) => state.emails.emails); const [pwScore, setPwScore] = useState(0); const intl = useIntl(); - const pwStrengthMessages = ["pwfield.terrible", "pwfield.bad", "pwfield.weak", "pwfield.good", "pwfield.strong"]; useEffect(() => { @@ -53,9 +52,9 @@ function PasswordStrengthMeter(props: PasswordStrengthMeterProps) { return ( <React.Fragment> - <meter max="4" value={pwScore} id="password-strength-meter" key="0" /> + <meter max="5" value={pwScore} id="password-strength-meter" key="0" /> <div className="form-field-error-area" key="1"> - <FormText>{intl.formatMessage({ id: pwStrengthMessages[pwScore] })}</FormText> + {props.password !== undefined && <FormText>{intl.formatMessage({ id: pwStrengthMessages[pwScore] })}</FormText>} </div> </React.Fragment> ); From 34f5df3a20acc7da55fbce3164e00d99c90a0aad Mon Sep 17 00:00:00 2001 From: Eunju Huss <eunju@sunet.se> Date: Fri, 17 May 2024 15:17:45 +0900 Subject: [PATCH 17/41] Change div-> fieldset and add className --- .../Dashboard/ChangePasswordCustom.tsx | 65 +++++++++---------- 1 file changed, 32 insertions(+), 33 deletions(-) diff --git a/src/components/Dashboard/ChangePasswordCustom.tsx b/src/components/Dashboard/ChangePasswordCustom.tsx index 91d7e95ea..53ee53ff4 100644 --- a/src/components/Dashboard/ChangePasswordCustom.tsx +++ b/src/components/Dashboard/ChangePasswordCustom.tsx @@ -26,8 +26,7 @@ export default function ChangePasswordCustomForm(props: ChangePasswordCustomForm // PasswordStrengthMeter component. if (!value) { return "required"; - } - if (passwordData.isTooWeak !== false) { + } else if (passwordData.isTooWeak !== false) { return "chpass.low-password-entropy"; } } @@ -44,7 +43,7 @@ export default function ChangePasswordCustomForm(props: ChangePasswordCustomForm return ( <form id="passwordsview-form" onSubmit={props.formProps.handleSubmit}> - <div className="password-format"> + <fieldset className="password-format"> <label> <FormattedMessage defaultMessage="Tip: Choose a strong password" @@ -77,37 +76,37 @@ export default function ChangePasswordCustomForm(props: ChangePasswordCustomForm return <li key={list.key}>{list}</li>; })} </ul> - </div> + </fieldset> - <fieldset> - <div> - <FinalField - name="custom" - component={CustomInput} - componentClass="input" - type="password" - label={ - <FormattedMessage defaultMessage="Enter new password" description="chpass form custom password label" /> - } - passwordStrengthMeter={ - <PasswordStrengthMeter password={props.formProps.values.custom} passStateUp={updatePasswordData} /> - } - id="custom-password-field" - validate={strongEnough} - autoComplete="new-password" - /> - <FinalField - name="repeat" - component={CustomInput} - componentClass="input" - type="password" - id="repeat-password-field" - label={ - <FormattedMessage defaultMessage="Repeat new password" description="chpass form custom password repeat" /> - } - validate={mustMatch} - /> - </div> + <fieldset className="change-password-custom-inputs"> + <FinalField + name="custom" + component={CustomInput} + componentClass="input" + type="password" + label={ + <FormattedMessage defaultMessage="Enter new password" description="chpass form custom password label" /> + } + passwordStrengthMeter={ + <PasswordStrengthMeter password={props.formProps.values.custom} passStateUp={updatePasswordData} /> + } + id="custom-password-field" + validate={strongEnough} + autoComplete="new-password" + required={true} + /> + <FinalField + name="repeat" + component={CustomInput} + componentClass="input" + type="password" + id="repeat-password-field" + label={ + <FormattedMessage defaultMessage="Repeat new password" description="chpass form custom password repeat" /> + } + validate={mustMatch} + required={true} + /> </fieldset> <div id="chpass-form" className="tabpane buttons"> <EduIDButton buttonstyle="secondary" onClick={props.handleCancel}> From 2f9c760a004d1ff24340e35598e54e33cb14c5ed Mon Sep 17 00:00:00 2001 From: Eunju Huss <eunju@sunet.se> Date: Fri, 17 May 2024 15:18:42 +0900 Subject: [PATCH 18/41] Add meter color --- src/styles/_ChangePassword.scss | 44 ++++++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/src/styles/_ChangePassword.scss b/src/styles/_ChangePassword.scss index fb62c67d8..803a6cc47 100644 --- a/src/styles/_ChangePassword.scss +++ b/src/styles/_ChangePassword.scss @@ -21,7 +21,49 @@ fieldset.toggle-change-password-options { meter { width: 100%; - // background: yellow; +} + +meter[value="1"]::-webkit-meter-optimum-value { + background: red; + border-top-left-radius: 10px; + border-bottom-left-radius: 10px; +} +meter[value="2"]::-webkit-meter-optimum-value { + background: yellow; + border-top-left-radius: 10px; + border-bottom-left-radius: 10px; +} +meter[value="3"]::-webkit-meter-optimum-value { + background: orange; + border-top-left-radius: 10px; + border-bottom-left-radius: 10px; +} +meter[value="4"]::-webkit-meter-optimum-value { + background: green; + border-top-left-radius: 10px; + border-bottom-left-radius: 10px; +} + +/* Gecko based browsers */ +meter[value="1"]::-moz-meter-bar { + background: red; + border-top-left-radius: 10px; + border-bottom-left-radius: 10px; +} +meter[value="2"]::-moz-meter-bar { + background: yellow; + border-top-left-radius: 10px; + border-bottom-left-radius: 10px; +} +meter[value="3"]::-moz-meter-bar { + background: orange; + border-top-left-radius: 10px; + border-bottom-left-radius: 10px; +} +meter[value="4"]::-moz-meter-bar { + background: green; + border-top-left-radius: 10px; + border-bottom-left-radius: 10px; } @media (max-width: 375px) { From 2ca477885c72dc1f5094d63233f73cf5c6aa5a07 Mon Sep 17 00:00:00 2001 From: Eunju Huss <eunju@sunet.se> Date: Mon, 20 May 2024 15:08:09 +0900 Subject: [PATCH 19/41] Moved strengthMeter below error message --- src/components/Common/InputWrapper.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Common/InputWrapper.tsx b/src/components/Common/InputWrapper.tsx index 063ac4a72..c6054b288 100644 --- a/src/components/Common/InputWrapper.tsx +++ b/src/components/Common/InputWrapper.tsx @@ -54,10 +54,10 @@ function RenderErrorMessage(props: InputWrapperProps): JSX.Element | null { return ( <FormText> - {props.passwordStrengthMeter && props.passwordStrengthMeter} <span role="alert" aria-invalid="true" tabIndex={0} className="input-validate-error"> {errorMsg || submitErrorMsg} </span> + {props.passwordStrengthMeter && props.passwordStrengthMeter} </FormText> ); } From 6087875c6853cd086a70de52de2c13b729532f7e Mon Sep 17 00:00:00 2001 From: Eunju Huss <eunju@sunet.se> Date: Mon, 20 May 2024 15:08:53 +0900 Subject: [PATCH 20/41] Add export for PasswordInputElement --- src/components/Common/PasswordInput.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Common/PasswordInput.tsx b/src/components/Common/PasswordInput.tsx index ad8ae4fb4..452228732 100644 --- a/src/components/Common/PasswordInput.tsx +++ b/src/components/Common/PasswordInput.tsx @@ -50,7 +50,7 @@ export function WrappedPasswordInput(props: FieldRenderProps<string>): JSX.Eleme * @param props * @returns */ -function PasswordInputElement(props: InputProps): JSX.Element { +export function PasswordInputElement(props: InputProps): JSX.Element { const [showPassword, setShowPassword] = useState(false); function toggleShowPassword() { From d992986b0b2abafec4e763207a13438bee7898a3 Mon Sep 17 00:00:00 2001 From: Eunju Huss <eunju@sunet.se> Date: Mon, 20 May 2024 15:09:50 +0900 Subject: [PATCH 21/41] Create NewPAsswordInput --- src/components/Common/NewPasswordInput.tsx | 12 ++++++++++++ src/components/Dashboard/ChangePasswordCustom.tsx | 8 +++++--- 2 files changed, 17 insertions(+), 3 deletions(-) create mode 100644 src/components/Common/NewPasswordInput.tsx diff --git a/src/components/Common/NewPasswordInput.tsx b/src/components/Common/NewPasswordInput.tsx new file mode 100644 index 000000000..f24b2caa5 --- /dev/null +++ b/src/components/Common/NewPasswordInput.tsx @@ -0,0 +1,12 @@ +import { FieldRenderProps } from "react-final-form"; +import { InputWrapper } from "./InputWrapper"; +import { PasswordInputElement } from "./PasswordInput"; + +export default function NewPasswordInput(props: FieldRenderProps<string>): JSX.Element { + // the InputWrapper renders it's children plus a label, helpBlock and any error message from the field validation + return ( + <InputWrapper {...props}> + <PasswordInputElement {...props} /> + </InputWrapper> + ); +} diff --git a/src/components/Dashboard/ChangePasswordCustom.tsx b/src/components/Dashboard/ChangePasswordCustom.tsx index 53ee53ff4..8c3657b26 100644 --- a/src/components/Dashboard/ChangePasswordCustom.tsx +++ b/src/components/Dashboard/ChangePasswordCustom.tsx @@ -1,5 +1,5 @@ -import CustomInput from "components/Common/CustomInput"; import EduIDButton from "components/Common/EduIDButton"; +import NewPasswordInput from "components/Common/NewPasswordInput"; import PasswordStrengthMeter, { PasswordStrengthData } from "components/Common/PasswordStrengthMeter"; import { useState } from "react"; import { Field as FinalField } from "react-final-form"; @@ -28,6 +28,8 @@ export default function ChangePasswordCustomForm(props: ChangePasswordCustomForm return "required"; } else if (passwordData.isTooWeak !== false) { return "chpass.low-password-entropy"; + } else { + return; } } @@ -81,7 +83,7 @@ export default function ChangePasswordCustomForm(props: ChangePasswordCustomForm <fieldset className="change-password-custom-inputs"> <FinalField name="custom" - component={CustomInput} + component={NewPasswordInput} componentClass="input" type="password" label={ @@ -97,7 +99,7 @@ export default function ChangePasswordCustomForm(props: ChangePasswordCustomForm /> <FinalField name="repeat" - component={CustomInput} + component={NewPasswordInput} componentClass="input" type="password" id="repeat-password-field" From 44302c64c672125fb41a432191fa80566c84be1e Mon Sep 17 00:00:00 2001 From: Eunju Huss <eunju@sunet.se> Date: Mon, 20 May 2024 15:25:13 +0900 Subject: [PATCH 22/41] Add placeholder for passwordinputs --- .../Dashboard/ChangePasswordCustom.tsx | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/components/Dashboard/ChangePasswordCustom.tsx b/src/components/Dashboard/ChangePasswordCustom.tsx index 8c3657b26..f5ff64507 100644 --- a/src/components/Dashboard/ChangePasswordCustom.tsx +++ b/src/components/Dashboard/ChangePasswordCustom.tsx @@ -3,7 +3,7 @@ import NewPasswordInput from "components/Common/NewPasswordInput"; import PasswordStrengthMeter, { PasswordStrengthData } from "components/Common/PasswordStrengthMeter"; import { useState } from "react"; import { Field as FinalField } from "react-final-form"; -import { FormattedMessage } from "react-intl"; +import { FormattedMessage, useIntl } from "react-intl"; import { ChangePasswordChildFormProps } from "./ChangePassword"; // eslint-disable-next-line @typescript-eslint/no-empty-interface @@ -11,6 +11,19 @@ interface ChangePasswordCustomFormProps extends ChangePasswordChildFormProps {} export default function ChangePasswordCustomForm(props: ChangePasswordCustomFormProps) { const [passwordData, setPasswordData] = useState<PasswordStrengthData>({}); + const intl = useIntl(); + + const new_password_placeholder = intl.formatMessage({ + id: "placeholder.new_password_placeholder", + defaultMessage: "enter new password", + description: "placeholder text for new password", + }); + + const repeat_new_password_placeholder = intl.formatMessage({ + id: "placeholder.repeat_new_password_placeholder", + defaultMessage: "repeat new password", + description: "placeholder text for repeat new password", + }); function updatePasswordData(value: PasswordStrengthData) { // This function is called when the password strength meter has calculated password strength @@ -96,6 +109,7 @@ export default function ChangePasswordCustomForm(props: ChangePasswordCustomForm validate={strongEnough} autoComplete="new-password" required={true} + placeHolder={new_password_placeholder} /> <FinalField name="repeat" @@ -108,6 +122,7 @@ export default function ChangePasswordCustomForm(props: ChangePasswordCustomForm } validate={mustMatch} required={true} + placeHolder={repeat_new_password_placeholder} /> </fieldset> <div id="chpass-form" className="tabpane buttons"> From d27176f09129c084089acd13e1a12c0c77a271cc Mon Sep 17 00:00:00 2001 From: Eunju Huss <eunju@sunet.se> Date: Mon, 20 May 2024 18:05:08 +0900 Subject: [PATCH 23/41] Adjust meter styling --- src/styles/_ChangePassword.scss | 15 +++++++++++++++ src/styles/_inputs.scss | 6 ++++++ 2 files changed, 21 insertions(+) diff --git a/src/styles/_ChangePassword.scss b/src/styles/_ChangePassword.scss index 803a6cc47..1d18b0400 100644 --- a/src/styles/_ChangePassword.scss +++ b/src/styles/_ChangePassword.scss @@ -21,6 +21,7 @@ fieldset.toggle-change-password-options { meter { width: 100%; + height: 2.5rem; } meter[value="1"]::-webkit-meter-optimum-value { @@ -42,6 +43,9 @@ meter[value="4"]::-webkit-meter-optimum-value { background: green; border-top-left-radius: 10px; border-bottom-left-radius: 10px; + + small.text-muted { + color: white !important; + } } /* Gecko based browsers */ @@ -64,6 +68,17 @@ meter[value="4"]::-moz-meter-bar { background: green; border-top-left-radius: 10px; border-bottom-left-radius: 10px; + + small.text-muted { + color: white !important; + } +} + +.form-field-error-area { + margin-top: -32px; + position: absolute; + z-index: 1; + font-size: 0.8rem; + padding-left: 10px; } @media (max-width: 375px) { diff --git a/src/styles/_inputs.scss b/src/styles/_inputs.scss index c00d3f218..8982b9df5 100644 --- a/src/styles/_inputs.scss +++ b/src/styles/_inputs.scss @@ -157,6 +157,12 @@ input[readonly]:focus-visible { } } +fieldset.change-password-custom-inputs { + span.input-validate-error { + position: relative !important; + } +} + // login username-pw / reset-password set-new-password .password-input { display: flex; From 66a4f92480f7dcf8ff8ebaa3262f17b7ed28d826 Mon Sep 17 00:00:00 2001 From: Eunju Huss <eunju@sunet.se> Date: Mon, 20 May 2024 18:06:09 +0900 Subject: [PATCH 24/41] Clean up after removing ChangePasswordForm component --- src/tests/ChangePasswordForm-test.tsx | 25 ++++--------------------- 1 file changed, 4 insertions(+), 21 deletions(-) diff --git a/src/tests/ChangePasswordForm-test.tsx b/src/tests/ChangePasswordForm-test.tsx index 3f06f91f2..330c6c9f7 100644 --- a/src/tests/ChangePasswordForm-test.tsx +++ b/src/tests/ChangePasswordForm-test.tsx @@ -1,33 +1,16 @@ -import { finish_url } from "components/Dashboard/ChangePassword"; -import ChangePasswordForm from "components/Dashboard/ChangePasswordForm"; +import { ChangePassword } from "components/Dashboard/ChangePassword"; + import { fireEvent, render, screen, waitFor } from "./helperFunctions/DashboardTestApp-rtl"; test("renders ChangePasswordForm, suggested password value is field in suggested-password-field", () => { - render(<ChangePasswordForm finish_url={finish_url} />); - - const oldPasswordInput = screen.getByLabelText(/Current password/i) as HTMLInputElement; - expect(oldPasswordInput.value).toBe(""); + render(<ChangePassword />); const suggestedPasswordInput = screen.getByLabelText(/Suggested password/i) as HTMLInputElement; expect(suggestedPasswordInput.value).toBeDefined(); }); -test("save button will be enabled once current password field is filled", () => { - render(<ChangePasswordForm finish_url={finish_url} />); - - const input = screen.getByLabelText(/Current password/i) as HTMLInputElement; - // change password save button is initially disabled - const savePasswordButton = screen.getByRole("button", { name: /save/i }); - expect(savePasswordButton).toBeDisabled(); - - fireEvent.change(input, { target: { value: "current password" } }); - expect(input.value).toBe("current password"); - - expect(savePasswordButton).toBeEnabled(); -}); - test("renders custom password form after clicking do not want a suggested password", async () => { - render(<ChangePasswordForm finish_url={finish_url} />); + render(<ChangePassword />); const customPasswordButton = screen.getByRole("button", { name: /I don't want a suggested password/i }); expect(customPasswordButton).toBeInTheDocument(); From 8cba1715e4cfb8ee6c87ca16254ae9b6d66417f1 Mon Sep 17 00:00:00 2001 From: Eunju Huss <eunju@sunet.se> Date: Tue, 21 May 2024 13:06:40 +0900 Subject: [PATCH 25/41] Resolved issue where error message no longer occure after input value strings are removed --- src/components/Common/InputWrapper.tsx | 5 +++-- src/components/Common/PasswordStrengthMeter.tsx | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/components/Common/InputWrapper.tsx b/src/components/Common/InputWrapper.tsx index c6054b288..ca30fd871 100644 --- a/src/components/Common/InputWrapper.tsx +++ b/src/components/Common/InputWrapper.tsx @@ -41,8 +41,9 @@ function RenderLabelAndHelpText(props: InputWrapperProps): JSX.Element { function RenderErrorMessage(props: InputWrapperProps): JSX.Element | null { const intl = useIntl(); const { meta } = props; - if ((!meta.error && !meta.submitError) || (!meta.touched && !meta.dirty)) { - // no error, no message + + if ((!meta.error && !meta.submitError && !props.passwordStrengthMeter) || (!meta.touched && !meta.dirty)) { + // no error, no message only for !props.passwordStrengthMeter return null; } diff --git a/src/components/Common/PasswordStrengthMeter.tsx b/src/components/Common/PasswordStrengthMeter.tsx index 84b53e844..ac0d8c890 100644 --- a/src/components/Common/PasswordStrengthMeter.tsx +++ b/src/components/Common/PasswordStrengthMeter.tsx @@ -52,7 +52,7 @@ function PasswordStrengthMeter(props: PasswordStrengthMeterProps) { return ( <React.Fragment> - <meter max="5" value={pwScore} id="password-strength-meter" key="0" /> + <meter max="4" value={pwScore} id="password-strength-meter" key="0" /> <div className="form-field-error-area" key="1"> {props.password !== undefined && <FormText>{intl.formatMessage({ id: pwStrengthMessages[pwScore] })}</FormText>} </div> From 889ae058d71f5a94a3a6fc1f4d113b92e6f0f8dd Mon Sep 17 00:00:00 2001 From: Eunju Huss <eunju@sunet.se> Date: Tue, 21 May 2024 13:35:40 +0900 Subject: [PATCH 26/41] Update test due to component changes --- src/tests/ChangePasswordForm-test.tsx | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/src/tests/ChangePasswordForm-test.tsx b/src/tests/ChangePasswordForm-test.tsx index 330c6c9f7..c7cacec27 100644 --- a/src/tests/ChangePasswordForm-test.tsx +++ b/src/tests/ChangePasswordForm-test.tsx @@ -1,21 +1,36 @@ import { ChangePassword } from "components/Dashboard/ChangePassword"; +import { initialState as configInitialState } from "slices/IndexConfig"; import { fireEvent, render, screen, waitFor } from "./helperFunctions/DashboardTestApp-rtl"; test("renders ChangePasswordForm, suggested password value is field in suggested-password-field", () => { - render(<ChangePassword />); - - const suggestedPasswordInput = screen.getByLabelText(/Suggested password/i) as HTMLInputElement; + render(<ChangePassword />, { + state: { + config: { ...configInitialState, is_app_loaded: true }, + chpass: { + suggested_password: "test password", + }, + }, + }); + expect(screen.getByRole("heading")).toHaveTextContent(/^Change password: Suggested password/); + const suggestedPasswordInput = screen.getByLabelText(/Repeat new password/i) as HTMLInputElement; expect(suggestedPasswordInput.value).toBeDefined(); }); test("renders custom password form after clicking do not want a suggested password", async () => { - render(<ChangePassword />); - const customPasswordButton = screen.getByRole("button", { name: /I don't want a suggested password/i }); + render(<ChangePassword />, { + state: { + config: { ...configInitialState, is_app_loaded: true }, + chpass: { + suggested_password: "test password", + }, + }, + }); + const customPasswordButton = screen.getByRole("checkbox", { name: /Create a custom password?/i }); expect(customPasswordButton).toBeInTheDocument(); fireEvent.click(customPasswordButton); - expect(screen.getByText(/Suggest a password for me/i)).toBeInTheDocument(); + expect(customPasswordButton).toBeChecked(); const newPasswordInput = screen.getByLabelText(/Enter new password/i) as HTMLInputElement; const repeatNewPasswordInput = screen.getByLabelText(/Repeat new password/i) as HTMLInputElement; From 46f992fd110c5cf15d2478d02823f2f0ea96392c Mon Sep 17 00:00:00 2001 From: Eunju Huss <eunju@sunet.se> Date: Wed, 22 May 2024 13:33:21 +0900 Subject: [PATCH 27/41] Adjust meter error text position and color, font size --- src/components/Common/PasswordStrengthMeter.tsx | 4 ++-- src/styles/_ChangePassword.scss | 15 +++------------ src/styles/_anti-bootstrap.scss | 6 +++--- 3 files changed, 8 insertions(+), 17 deletions(-) diff --git a/src/components/Common/PasswordStrengthMeter.tsx b/src/components/Common/PasswordStrengthMeter.tsx index ac0d8c890..d14debab9 100644 --- a/src/components/Common/PasswordStrengthMeter.tsx +++ b/src/components/Common/PasswordStrengthMeter.tsx @@ -49,13 +49,13 @@ function PasswordStrengthMeter(props: PasswordStrengthMeterProps) { const data: PasswordStrengthData = { score: score, isTooWeak: entropy < minRequiredEntropy }; props.passStateUp(data); }, [pdata, emails, minRequiredEntropy, props.password]); - + console.log("2jjj"); return ( <React.Fragment> - <meter max="4" value={pwScore} id="password-strength-meter" key="0" /> <div className="form-field-error-area" key="1"> {props.password !== undefined && <FormText>{intl.formatMessage({ id: pwStrengthMessages[pwScore] })}</FormText>} </div> + <meter max="4" value={pwScore} id="password-strength-meter" key="0" /> </React.Fragment> ); } diff --git a/src/styles/_ChangePassword.scss b/src/styles/_ChangePassword.scss index 1d18b0400..7179ad7cf 100644 --- a/src/styles/_ChangePassword.scss +++ b/src/styles/_ChangePassword.scss @@ -43,9 +43,6 @@ meter[value="4"]::-webkit-meter-optimum-value { background: green; border-top-left-radius: 10px; border-bottom-left-radius: 10px; - + small.text-muted { - color: white !important; - } } /* Gecko based browsers */ @@ -68,17 +65,11 @@ meter[value="4"]::-moz-meter-bar { background: green; border-top-left-radius: 10px; border-bottom-left-radius: 10px; - + small.text-muted { - color: white !important; - } } -.form-field-error-area { - margin-top: -32px; - position: absolute; - z-index: 1; - font-size: 0.8rem; - padding-left: 10px; +.form-field-error-area small.text-muted { + font-size: 0.9rem !important; + color: #d2143a !important; } @media (max-width: 375px) { diff --git a/src/styles/_anti-bootstrap.scss b/src/styles/_anti-bootstrap.scss index 0398f2e9f..cbd565984 100644 --- a/src/styles/_anti-bootstrap.scss +++ b/src/styles/_anti-bootstrap.scss @@ -30,9 +30,9 @@ input { small { font-size: 100% !important; - &.text-muted { - color: $txt-black !important; - } + // &.text-muted { + // color: $txt-black !important; + // } &.form-text { margin-top: 0 !important; From 5ad1bac068dd64c9cce5188d8145da9f27a4bb48 Mon Sep 17 00:00:00 2001 From: Eunju Huss <eunju@sunet.se> Date: Wed, 22 May 2024 15:13:48 +0900 Subject: [PATCH 28/41] Create a separate toggle component for reusability --- src/components/Dashboard/ChangePassword.tsx | 26 ++-------------- .../Dashboard/ChangePasswordSwitchToggle.tsx | 30 +++++++++++++++++++ 2 files changed, 33 insertions(+), 23 deletions(-) create mode 100644 src/components/Dashboard/ChangePasswordSwitchToggle.tsx diff --git a/src/components/Dashboard/ChangePassword.tsx b/src/components/Dashboard/ChangePassword.tsx index 0026a9e98..ca14328a1 100644 --- a/src/components/Dashboard/ChangePassword.tsx +++ b/src/components/Dashboard/ChangePassword.tsx @@ -7,6 +7,7 @@ import { FormattedMessage, useIntl } from "react-intl"; import { useNavigate } from "react-router-dom"; import ChangePasswordCustomForm from "./ChangePasswordCustom"; import ChangePasswordSuggestedForm from "./ChangePasswordSuggested"; +import { ChangePasswordSwitchToggle } from "./ChangePasswordSwitchToggle"; // exported for use in tests export const finish_url = "/profile/security"; @@ -17,7 +18,7 @@ export interface ChangePasswordFormProps { export interface ChangePasswordChildFormProps { formProps: FormRenderProps<ChangePasswordFormData>; - handleCancel?: any; + handleCancel?: (event: React.MouseEvent<HTMLElement>) => void; } interface ChangePasswordFormData { @@ -122,28 +123,7 @@ export function ChangePassword() { </div> </section> )} - - <fieldset className="toggle-change-password-options"> - <form> - <label className="toggle flex-between" htmlFor="change-custom-password"> - <FormattedMessage defaultMessage="Create a custom password?" description="change custom password" /> - <input - onChange={handleSwitchChange} - className="toggle-checkbox" - type="checkbox" - checked={!renderSuggested} - id="change-custom-password" - /> - <div className="toggle-switch"></div> - </label> - </form> - <p className="help-text"> - <FormattedMessage - defaultMessage="Toggle the custom password switch to set your own password." - description="Change password toggle" - /> - </p> - </fieldset> + <ChangePasswordSwitchToggle handleSwitchChange={handleSwitchChange} renderSuggested={renderSuggested} /> {renderSuggested ? ( <ChangePasswordSuggestedForm {...child_props} handleCancel={handleCancel} /> ) : ( diff --git a/src/components/Dashboard/ChangePasswordSwitchToggle.tsx b/src/components/Dashboard/ChangePasswordSwitchToggle.tsx new file mode 100644 index 000000000..93bab3fb3 --- /dev/null +++ b/src/components/Dashboard/ChangePasswordSwitchToggle.tsx @@ -0,0 +1,30 @@ +import { FormattedMessage } from "react-intl"; + +export function ChangePasswordSwitchToggle(props: { + handleSwitchChange: (event: React.ChangeEvent<HTMLInputElement>) => void; + renderSuggested: boolean; +}) { + return ( + <fieldset className="toggle-change-password-options"> + <form> + <label className="toggle flex-between" htmlFor="change-custom-password"> + <FormattedMessage defaultMessage="Create a custom password?" description="change custom password" /> + <input + onChange={props.handleSwitchChange} + className="toggle-checkbox" + type="checkbox" + checked={!props.renderSuggested} + id="change-custom-password" + /> + <div className="toggle-switch"></div> + </label> + </form> + <p className="help-text"> + <FormattedMessage + defaultMessage="Toggle the custom password switch to set your own password." + description="Change password toggle" + /> + </p> + </fieldset> + ); +} From b835cafee05a9da4d58b7c7015cd469f4e6657be Mon Sep 17 00:00:00 2001 From: Eunju Huss <eunju@sunet.se> Date: Wed, 22 May 2024 15:14:53 +0900 Subject: [PATCH 29/41] Clean up --- src/components/Common/PasswordStrengthMeter.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Common/PasswordStrengthMeter.tsx b/src/components/Common/PasswordStrengthMeter.tsx index d14debab9..c43a735d8 100644 --- a/src/components/Common/PasswordStrengthMeter.tsx +++ b/src/components/Common/PasswordStrengthMeter.tsx @@ -49,7 +49,7 @@ function PasswordStrengthMeter(props: PasswordStrengthMeterProps) { const data: PasswordStrengthData = { score: score, isTooWeak: entropy < minRequiredEntropy }; props.passStateUp(data); }, [pdata, emails, minRequiredEntropy, props.password]); - console.log("2jjj"); + return ( <React.Fragment> <div className="form-field-error-area" key="1"> From 486fc11e21cd0f3bc63288ca4f22e0db8b41ecfd Mon Sep 17 00:00:00 2001 From: Eunju Huss <eunju@sunet.se> Date: Thu, 23 May 2024 14:05:20 +0900 Subject: [PATCH 30/41] Add a condition to prevent showing the error message --- src/components/Dashboard/ChangePasswordCustom.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/components/Dashboard/ChangePasswordCustom.tsx b/src/components/Dashboard/ChangePasswordCustom.tsx index f5ff64507..6834b8cd6 100644 --- a/src/components/Dashboard/ChangePasswordCustom.tsx +++ b/src/components/Dashboard/ChangePasswordCustom.tsx @@ -39,10 +39,8 @@ export default function ChangePasswordCustomForm(props: ChangePasswordCustomForm // PasswordStrengthMeter component. if (!value) { return "required"; - } else if (passwordData.isTooWeak !== false) { + } else if (passwordData.isTooWeak && passwordData.score !== 4) { return "chpass.low-password-entropy"; - } else { - return; } } From 8b424773c3574e0c13caabfc5fefef18bad5a95d Mon Sep 17 00:00:00 2001 From: Eunju Huss <eunju@sunet.se> Date: Thu, 23 May 2024 14:15:24 +0900 Subject: [PATCH 31/41] Add hyphen, to prevent spelling errors --- src/components/Dashboard/ChangePasswordCustom.tsx | 4 ++-- src/components/Dashboard/NinForm.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/Dashboard/ChangePasswordCustom.tsx b/src/components/Dashboard/ChangePasswordCustom.tsx index 6834b8cd6..1c868f89c 100644 --- a/src/components/Dashboard/ChangePasswordCustom.tsx +++ b/src/components/Dashboard/ChangePasswordCustom.tsx @@ -55,7 +55,7 @@ export default function ChangePasswordCustomForm(props: ChangePasswordCustomForm } return ( - <form id="passwordsview-form" onSubmit={props.formProps.handleSubmit}> + <form id="passwords-view-form" onSubmit={props.formProps.handleSubmit}> <fieldset className="password-format"> <label> <FormattedMessage @@ -123,7 +123,7 @@ export default function ChangePasswordCustomForm(props: ChangePasswordCustomForm placeHolder={repeat_new_password_placeholder} /> </fieldset> - <div id="chpass-form" className="tabpane buttons"> + <div id="chpass-form" className="tab-pane buttons"> <EduIDButton buttonstyle="secondary" onClick={props.handleCancel}> <FormattedMessage defaultMessage="cancel" description="button cancel" /> </EduIDButton> diff --git a/src/components/Dashboard/NinForm.tsx b/src/components/Dashboard/NinForm.tsx index 796ff37a4..99b33208f 100644 --- a/src/components/Dashboard/NinForm.tsx +++ b/src/components/Dashboard/NinForm.tsx @@ -67,7 +67,7 @@ function NinForm(): JSX.Element { render={({ handleSubmit, pristine, invalid }) => { return ( <form onSubmit={handleSubmit} className="single-input-form x-adjust"> - <fieldset id="nins-form" className="tabpane"> + <fieldset id="nins-form" className="tab-pane"> <FinalField component={CustomInput} componentClass="input" From 8f7a4a85a916ed34017b3f3190741bfbedb1149a Mon Sep 17 00:00:00 2001 From: Eunju Huss <eunju@sunet.se> Date: Thu, 23 May 2024 15:10:19 +0900 Subject: [PATCH 32/41] Add handleCancel type --- src/components/Common/NewPasswordForm.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Common/NewPasswordForm.tsx b/src/components/Common/NewPasswordForm.tsx index 0ceaa61eb..a820376f1 100644 --- a/src/components/Common/NewPasswordForm.tsx +++ b/src/components/Common/NewPasswordForm.tsx @@ -23,7 +23,7 @@ interface NewPasswordFormProps { // callback?: ((errors?: SubmissionErrors) => void) | undefined // ) => void | Promise<void>; submitButtonText: React.ReactChild; - handleCancel?: any; + handleCancel?: (event: React.MouseEvent<HTMLElement>) => void; } export function NewPasswordForm(props: NewPasswordFormProps): JSX.Element { From 051a832bff546791842fa357c1e988a3a3726dbd Mon Sep 17 00:00:00 2001 From: Eunju Huss <eunju@sunet.se> Date: Thu, 23 May 2024 18:47:11 +0900 Subject: [PATCH 33/41] Introduce new endpoint for changing password --- src/apis/eduidSecurity.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/apis/eduidSecurity.ts b/src/apis/eduidSecurity.ts index a4bebd136..42cd6150a 100644 --- a/src/apis/eduidSecurity.ts +++ b/src/apis/eduidSecurity.ts @@ -174,7 +174,7 @@ export const fetchSuggestedPassword = createAsyncThunk< undefined, { dispatch: EduIDAppDispatch; state: EduIDAppRootState } >("chpass/fetchSuggestedPassword", async (args, thunkAPI) => { - return makeSecurityRequest<SuggestedPasswordResponse>(thunkAPI, "suggested-password") + return makeSecurityRequest<SuggestedPasswordResponse>(thunkAPI, "change-password/suggested-password") .then((response) => response.payload.suggested_password) .catch((err) => thunkAPI.rejectWithValue(err)); }); @@ -198,7 +198,7 @@ export const changePassword = createAsyncThunk< ChangePasswordPayload, { dispatch: EduIDAppDispatch; state: EduIDAppRootState } >("chpass/changePassword", async (args, thunkAPI) => { - return makeSecurityRequest<ChangePasswordResponse>(thunkAPI, "change-password", args) + return makeSecurityRequest<ChangePasswordResponse>(thunkAPI, "change-password/set-password", args) .then((response) => response.payload) .catch((err) => thunkAPI.rejectWithValue(err)); }); From 1e96f34ffe833230ff34a1d37df3267f462d2e97 Mon Sep 17 00:00:00 2001 From: Eunju Huss <eunju@sunet.se> Date: Fri, 24 May 2024 10:43:05 +0900 Subject: [PATCH 34/41] Create reusable ConfirmInfo component --- src/components/Common/ConfirmUserInfo.tsx | 49 ++++++++++++++++++ .../ResetPassword/SetNewPassword.tsx | 29 +---------- src/components/Signup/SignupApp.tsx | 1 - src/components/Signup/SignupUserCreated.tsx | 51 ++----------------- 4 files changed, 56 insertions(+), 74 deletions(-) create mode 100644 src/components/Common/ConfirmUserInfo.tsx diff --git a/src/components/Common/ConfirmUserInfo.tsx b/src/components/Common/ConfirmUserInfo.tsx new file mode 100644 index 000000000..44653bac5 --- /dev/null +++ b/src/components/Common/ConfirmUserInfo.tsx @@ -0,0 +1,49 @@ +import { idUserEmail, idUserPassword } from "components/Signup/SignupUserCreated"; +import { FormattedMessage } from "react-intl"; + +interface EmailProps { + email?: string; +} + +interface ConfirmUserInfoProps { + readonly email_address: string; + readonly new_password: string; +} + +export const EmailFieldset = ({ email }: EmailProps): JSX.Element => { + return ( + <fieldset> + <label htmlFor={idUserEmail}> + <FormattedMessage defaultMessage="Email address" description="Email label" /> + </label> + <div className="display-data"> + <output id={idUserEmail}>{email}</output> + </div> + </fieldset> + ); +}; + +export function ConfirmUserInfo(props: ConfirmUserInfoProps) { + return ( + <div id="email-display"> + <EmailFieldset email={props.email_address} /> + <fieldset> + <label htmlFor={idUserPassword}> + <FormattedMessage defaultMessage="Password" description="Password label" /> + </label> + <div className="display-data"> + <mark className="force-select-all"> + <output id={idUserPassword}>{props.new_password}</output> + </mark> + </div> + <input + autoComplete="new-password" + type="password" + name="display-none-new-password" + id="display-none-new-password" + defaultValue={props.new_password ? props.new_password : ""} + /> + </fieldset> + </div> + ); +} diff --git a/src/components/ResetPassword/SetNewPassword.tsx b/src/components/ResetPassword/SetNewPassword.tsx index d89f134d4..93278cd9a 100644 --- a/src/components/ResetPassword/SetNewPassword.tsx +++ b/src/components/ResetPassword/SetNewPassword.tsx @@ -4,6 +4,7 @@ import { postSetNewPasswordExtraSecurityPhone, postSetNewPasswordExtraSecurityToken, } from "apis/eduidResetPassword"; +import { ConfirmUserInfo } from "components/Common/ConfirmUserInfo"; import { CopyToClipboard } from "components/Common/CopyToClipboard"; import EduIDButton from "components/Common/EduIDButton"; import { NewPasswordForm, NewPasswordFormData } from "components/Common/NewPasswordForm"; @@ -150,33 +151,7 @@ export function ResetPasswordSuccess(): JSX.Element { </p> </div> </section> - <div id="email-display"> - <fieldset> - <label htmlFor="user-email"> - <FormattedMessage defaultMessage="Email address" description="Email label" /> - </label> - <div className="display-data"> - <output id="user-email">{email_address}</output> - </div> - </fieldset> - <fieldset> - <label htmlFor="user-password"> - <FormattedMessage defaultMessage="Password" description="Password label" /> - </label> - <div className="display-data"> - <mark className="force-select-all"> - <output id="user-password">{new_password}</output> - </mark> - </div> - <input - autoComplete="new-password" - type="password" - name="display-none-new-password" - id="display-none-new-password" - defaultValue={new_password} - /> - </fieldset> - </div> + <ConfirmUserInfo email_address={email_address as string} new_password={new_password as string} /> <div className="buttons"> <EduIDButton id="reset-password-finished" buttonstyle="link" className="normal-case" type="submit"> <FormattedMessage defaultMessage="Go to eduid to login" description="go to eudID link text" /> diff --git a/src/components/Signup/SignupApp.tsx b/src/components/Signup/SignupApp.tsx index 100d78489..a463075d6 100644 --- a/src/components/Signup/SignupApp.tsx +++ b/src/components/Signup/SignupApp.tsx @@ -14,7 +14,6 @@ import { SignupConfirmPassword, SignupUserCreated } from "./SignupUserCreated"; export function SignupApp(): JSX.Element { const signupContext = useContext(SignupGlobalStateContext); const [state] = useActor(signupContext.signupService); - const intl = useIntl(); useEffect(() => { diff --git a/src/components/Signup/SignupUserCreated.tsx b/src/components/Signup/SignupUserCreated.tsx index fdc7cfb27..2ff8b2c50 100644 --- a/src/components/Signup/SignupUserCreated.tsx +++ b/src/components/Signup/SignupUserCreated.tsx @@ -1,4 +1,5 @@ import { createUserRequest } from "apis/eduidSignup"; +import { ConfirmUserInfo, EmailFieldset } from "components/Common/ConfirmUserInfo"; import { CopyToClipboard } from "components/Common/CopyToClipboard"; import EduIDButton from "components/Common/EduIDButton"; import { NewPasswordForm, NewPasswordFormData } from "components/Common/NewPasswordForm"; @@ -13,23 +14,6 @@ export const idUserEmail = "user-email"; export const idUserPassword = "user-password"; export const idFinishedButton = "finished-button"; -interface EmailProps { - email?: string; -} - -const EmailFieldset = ({ email }: EmailProps): JSX.Element => { - return ( - <fieldset> - <label htmlFor={idUserEmail}> - <FormattedMessage defaultMessage="Email address" description="Email label" /> - </label> - <div className="display-data"> - <output id={idUserEmail}>{email}</output> - </div> - </fieldset> - ); -}; - export function SignupConfirmPassword() { const dispatch = useAppDispatch(); const signupContext = useContext(SignupGlobalStateContext); @@ -70,14 +54,6 @@ export function SignupConfirmPassword() { </div> <div id="email-display"> <EmailFieldset email={signupState?.email.address} /> - {/* <fieldset> - <label htmlFor={idUserEmail}> - <FormattedMessage defaultMessage="Email address" description="Email label" /> - </label> - <div className="display-data"> - <output id={idUserEmail}>{signupState?.email.address}</output> - </div> - </fieldset> */} <fieldset> <label htmlFor={idUserPassword}> <FormattedMessage defaultMessage="Password" description="Password label" /> @@ -125,27 +101,10 @@ export function SignupUserCreated(): JSX.Element { /> </p> </div> - <div id="email-display"> - <EmailFieldset email={signupState?.email.address} /> - <fieldset> - <label htmlFor={idUserPassword}> - <FormattedMessage defaultMessage="Password" description="Password label" /> - </label> - <div className="display-data"> - <mark className="force-select-all"> - <output id={idUserPassword}>{formatPassword(signupState?.credentials.password)}</output> - </mark> - </div> - <input - autoComplete="new-password" - type="password" - name="display-none-new-password" - id="display-none-new-password" - defaultValue={signupState?.credentials.password ? formatPassword(signupState?.credentials.password) : ""} - /> - </fieldset> - </div> - + <ConfirmUserInfo + email_address={signupState?.email.address as string} + new_password={formatPassword(signupState?.credentials.password)} + /> <div className="buttons"> <EduIDButton id={idFinishedButton} buttonstyle="link" className="normal-case" type="submit"> <FormattedMessage defaultMessage="Go to eduid to login" description="go to eudID link text" /> From 11e3a6d1ff5e6cece6d06b5ed19c5d55c9d38611 Mon Sep 17 00:00:00 2001 From: Eunju Huss <eunju@sunet.se> Date: Fri, 24 May 2024 14:15:53 +0900 Subject: [PATCH 35/41] Add new ChangePasswordSuccess page --- .../Dashboard/ChangePasswordSuccess.tsx | 39 +++++++++++++++++++ src/components/IndexMain.tsx | 2 + 2 files changed, 41 insertions(+) create mode 100644 src/components/Dashboard/ChangePasswordSuccess.tsx diff --git a/src/components/Dashboard/ChangePasswordSuccess.tsx b/src/components/Dashboard/ChangePasswordSuccess.tsx new file mode 100644 index 000000000..63a7fa278 --- /dev/null +++ b/src/components/Dashboard/ChangePasswordSuccess.tsx @@ -0,0 +1,39 @@ +import { ConfirmUserInfo } from "components/Common/ConfirmUserInfo"; +import EduIDButton from "components/Common/EduIDButton"; +import { useAppSelector } from "eduid-hooks"; +import { FormattedMessage } from "react-intl"; +import { useLocation } from "react-router-dom"; +import { finish_url } from "./ChangePassword"; + +export function ChangePasswordSuccess(): JSX.Element { + const emails = useAppSelector((state) => state.emails.emails); + const location = useLocation(); + const password = location.state; + + return ( + <form method="GET" action={finish_url}> + <section className="intro"> + <h1> + <FormattedMessage + defaultMessage="Change Password: Completed" + description="Change password set new password success heading" + /> + </h1> + <div className="lead"> + <p> + <FormattedMessage + defaultMessage={`These is your new password for eduID. Make sure to store your password securely for future use`} + description="Change password set new password success lead" + /> + </p> + </div> + </section> + <ConfirmUserInfo email_address={emails.filter((mail) => mail.primary)[0].email} new_password={password} /> + <div className="buttons"> + <EduIDButton id="change-password-finished" buttonstyle="link" className="normal-case" type="submit"> + <FormattedMessage defaultMessage="Go to dashboard" description="Go to dashboard" /> + </EduIDButton> + </div> + </form> + ); +} diff --git a/src/components/IndexMain.tsx b/src/components/IndexMain.tsx index 90b7e33d3..c49cab74c 100644 --- a/src/components/IndexMain.tsx +++ b/src/components/IndexMain.tsx @@ -13,6 +13,7 @@ import { Settings } from "./Common/Settings"; import Splash from "./Common/Splash"; import { AdvancedSettings } from "./Dashboard/AdvancedSettings"; import { ChangePassword } from "./Dashboard/ChangePassword"; +import { ChangePasswordSuccess } from "./Dashboard/ChangePasswordSuccess"; import Start from "./Dashboard/DashboardStart"; import VerifyIdentity from "./Dashboard/VerifyIdentity"; import { Help } from "./Help"; @@ -62,6 +63,7 @@ export function IndexMain(): JSX.Element { <Route path="/profile/settings/" element={<Navigate to={settingsPath} />} /> <Route path={identityPath} element={<VerifyIdentity />} /> <Route path="/profile/chpass/" element={<ChangePassword />} /> + <Route path="/profile/chpass/success" element={<ChangePasswordSuccess />} /> <Route path="/profile/ext-return/:app_name/:authn_id" element={<ExternalReturnHandler />} /> {/* Navigates for old paths. TODO: redirect in backend server instead */} <Route path="/profile/security/" element={<Navigate to="/profile/settings/" />} /> From e8ddac4b5b791b29276957511976036d6a2b5d0a Mon Sep 17 00:00:00 2001 From: Eunju Huss <eunju@sunet.se> Date: Fri, 24 May 2024 14:16:37 +0900 Subject: [PATCH 36/41] An error occured,the page is being redirected to the dashboard --- src/components/Dashboard/ChangePassword.tsx | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/components/Dashboard/ChangePassword.tsx b/src/components/Dashboard/ChangePassword.tsx index ca14328a1..baca091e9 100644 --- a/src/components/Dashboard/ChangePassword.tsx +++ b/src/components/Dashboard/ChangePassword.tsx @@ -45,19 +45,28 @@ export function ChangePassword() { useEffect(() => { if (is_app_loaded && suggested_password === undefined) { - // call fetchSuggestedPassword once state.config.security_service_url is initialised - dispatch(fetchSuggestedPassword()); + handleSuggestedPassword(); } }, [suggested_password, is_app_loaded]); + async function handleSuggestedPassword() { + const response = await dispatch(fetchSuggestedPassword()); + if (fetchSuggestedPassword.rejected.match(response)) { + navigate(finish_url); + } + } + async function handleSubmitPasswords(values: ChangePasswordFormData) { // Use the right form field for the currently displayed password mode const newPassword = renderSuggested ? values.suggested : values.custom; + // Callback from sub-component when the user clicks on the button to change password if (newPassword) { const response = await dispatch(changePassword({ new_password: newPassword })); if (changePassword.fulfilled.match(response)) { - navigate(finish_url); + navigate("/profile/chpass/success", { + state: newPassword, + }); } } } @@ -65,7 +74,7 @@ export function ChangePassword() { function handleCancel(event: React.MouseEvent<HTMLElement>) { // Callback from sub-component when the user clicks on the button to abort changing password event.preventDefault(); - // TODO: should clear passwords from form to avoid browser password manager asking user to save the password + navigate(finish_url); } From 9fc34f15a3c11325b4de61c5c7c1571c1ffa6db6 Mon Sep 17 00:00:00 2001 From: Eunju Huss <eunju@sunet.se> Date: Fri, 24 May 2024 14:55:18 +0900 Subject: [PATCH 37/41] To properly retrieve the value of props.formProps.values.custom, add a useEffect to ensure it is obtained without delay --- src/components/Dashboard/ChangePasswordCustom.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/components/Dashboard/ChangePasswordCustom.tsx b/src/components/Dashboard/ChangePasswordCustom.tsx index 1c868f89c..9e3b6acaa 100644 --- a/src/components/Dashboard/ChangePasswordCustom.tsx +++ b/src/components/Dashboard/ChangePasswordCustom.tsx @@ -1,7 +1,7 @@ import EduIDButton from "components/Common/EduIDButton"; import NewPasswordInput from "components/Common/NewPasswordInput"; import PasswordStrengthMeter, { PasswordStrengthData } from "components/Common/PasswordStrengthMeter"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import { Field as FinalField } from "react-final-form"; import { FormattedMessage, useIntl } from "react-intl"; import { ChangePasswordChildFormProps } from "./ChangePassword"; @@ -11,8 +11,14 @@ interface ChangePasswordCustomFormProps extends ChangePasswordChildFormProps {} export default function ChangePasswordCustomForm(props: ChangePasswordCustomFormProps) { const [passwordData, setPasswordData] = useState<PasswordStrengthData>({}); + const [repeatNewPassword, setRepeatNewPassword] = useState<string>(); const intl = useIntl(); + useEffect(() => { + setRepeatNewPassword(props.formProps.values.custom); + }, [props.formProps.values.custom]); + + useEffect(() => {}, [props.formProps.values.custom]); const new_password_placeholder = intl.formatMessage({ id: "placeholder.new_password_placeholder", defaultMessage: "enter new password", @@ -49,7 +55,7 @@ export default function ChangePasswordCustomForm(props: ChangePasswordCustomForm if (!value) { return "required"; } - if (value !== props.formProps.values.custom) { + if (value !== repeatNewPassword) { return "chpass.different-repeat"; } } From dfd509faeb2db500722191ccb3c42e67218e41ff Mon Sep 17 00:00:00 2001 From: Eunju Huss <eunju@sunet.se> Date: Fri, 24 May 2024 15:14:47 +0900 Subject: [PATCH 38/41] Remove unnecessary content --- src/components/Dashboard/ChangePassword.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/Dashboard/ChangePassword.tsx b/src/components/Dashboard/ChangePassword.tsx index baca091e9..0b9659626 100644 --- a/src/components/Dashboard/ChangePassword.tsx +++ b/src/components/Dashboard/ChangePassword.tsx @@ -107,8 +107,7 @@ export function ChangePassword() { description="Change password - lead" defaultMessage={`A strong password has been generated for you. To proceed you will need to copy the password in to the Repeat new password field and click Accept Password and save it for - future use. Note: spaces in the generated password are there for legibility and will be - removed automatically if entered.`} + future use.`} /> </p> </div> From 1da235c3f732dffabf93140619f9dc556168ab56 Mon Sep 17 00:00:00 2001 From: Eunju Huss <eunju@sunet.se> Date: Mon, 27 May 2024 11:46:11 +0900 Subject: [PATCH 39/41] Change the form to fina form and add validation to the form to fix it not working properly --- .../Dashboard/ChangePasswordCustom.tsx | 216 +++++++++--------- 1 file changed, 108 insertions(+), 108 deletions(-) diff --git a/src/components/Dashboard/ChangePasswordCustom.tsx b/src/components/Dashboard/ChangePasswordCustom.tsx index 9e3b6acaa..450420137 100644 --- a/src/components/Dashboard/ChangePasswordCustom.tsx +++ b/src/components/Dashboard/ChangePasswordCustom.tsx @@ -1,8 +1,8 @@ import EduIDButton from "components/Common/EduIDButton"; import NewPasswordInput from "components/Common/NewPasswordInput"; import PasswordStrengthMeter, { PasswordStrengthData } from "components/Common/PasswordStrengthMeter"; -import { useEffect, useState } from "react"; -import { Field as FinalField } from "react-final-form"; +import { emptyStringPattern } from "helperFunctions/validation/regexPatterns"; +import { Field as FinalField, Form as FinalForm } from "react-final-form"; import { FormattedMessage, useIntl } from "react-intl"; import { ChangePasswordChildFormProps } from "./ChangePassword"; @@ -10,15 +10,8 @@ import { ChangePasswordChildFormProps } from "./ChangePassword"; interface ChangePasswordCustomFormProps extends ChangePasswordChildFormProps {} export default function ChangePasswordCustomForm(props: ChangePasswordCustomFormProps) { - const [passwordData, setPasswordData] = useState<PasswordStrengthData>({}); - const [repeatNewPassword, setRepeatNewPassword] = useState<string>(); const intl = useIntl(); - useEffect(() => { - setRepeatNewPassword(props.formProps.values.custom); - }, [props.formProps.values.custom]); - - useEffect(() => {}, [props.formProps.values.custom]); const new_password_placeholder = intl.formatMessage({ id: "placeholder.new_password_placeholder", defaultMessage: "enter new password", @@ -36,113 +29,120 @@ export default function ChangePasswordCustomForm(props: ChangePasswordCustomForm // on the current value in the form. We need to trigger validation of the field again at this // point, since validation uses this calculated value (and will already have executed when we // get here). - setPasswordData(value); props.formProps.form.change("custom", props.formProps.values.custom); } - function strongEnough(value?: string): string | undefined { - // check that the custom password is strong enough, using a score computed in the - // PasswordStrengthMeter component. - if (!value) { - return "required"; - } else if (passwordData.isTooWeak && passwordData.score !== 4) { - return "chpass.low-password-entropy"; + function validateNewPassword(values: any) { + const errors: any = {}; + if (values !== undefined) { + ["custom", "repeat"].forEach((inputName) => { + if (!values[inputName] || emptyStringPattern.test(values[inputName])) { + errors[inputName] = "required"; + } else if (values["custom"].replace(/\s/g, "") !== values["repeat"].replace(/\s/g, "")) { + // Remove whitespace from both passwords before comparing + errors["repeat"] = "chpass.different-repeat"; + } + }); } - } - function mustMatch(value?: string): string | undefined { - // validate that the repeated password is the same as the first one (called 'custom') - if (!value) { - return "required"; - } - if (value !== repeatNewPassword) { - return "chpass.different-repeat"; - } + return errors; } return ( - <form id="passwords-view-form" onSubmit={props.formProps.handleSubmit}> - <fieldset className="password-format"> - <label> - <FormattedMessage - defaultMessage="Tip: Choose a strong password" - description="help text for custom password label" - /> - </label> - <ul id="password-custom-help"> - {[ - <FormattedMessage - key={1} - defaultMessage={`Use upper- and lowercase characters, but not at the beginning or end`} - description="help text for custom password tips" - />, - <FormattedMessage - key={2} - defaultMessage={`Add digits somewhere, but not at the beginning or end`} - description="help text for custom password tips" - />, - <FormattedMessage - key={3} - defaultMessage={`Add special characters, such as @ $ \\ + _ %`} - description="help text for custom password tips" - />, - <FormattedMessage - key={4} - defaultMessage={`Spaces are ignored`} - description="help text for custom password tips" - />, - ].map((list) => { - return <li key={list.key}>{list}</li>; - })} - </ul> - </fieldset> + <FinalForm<any> + onSubmit={props.formProps.handleSubmit} + validate={validateNewPassword} + render={(formProps) => { + return ( + <form id="passwords-view-form" onSubmit={formProps.handleSubmit}> + <fieldset className="password-format"> + <label> + <FormattedMessage + defaultMessage="Tip: Choose a strong password" + description="help text for custom password label" + /> + </label> + <ul id="password-custom-help"> + {[ + <FormattedMessage + key={1} + defaultMessage={`Use upper- and lowercase characters, but not at the beginning or end`} + description="help text for custom password tips" + />, + <FormattedMessage + key={2} + defaultMessage={`Add digits somewhere, but not at the beginning or end`} + description="help text for custom password tips" + />, + <FormattedMessage + key={3} + defaultMessage={`Add special characters, such as @ $ \\ + _ %`} + description="help text for custom password tips" + />, + <FormattedMessage + key={4} + defaultMessage={`Spaces are ignored`} + description="help text for custom password tips" + />, + ].map((list) => { + return <li key={list.key}>{list}</li>; + })} + </ul> + </fieldset> - <fieldset className="change-password-custom-inputs"> - <FinalField - name="custom" - component={NewPasswordInput} - componentClass="input" - type="password" - label={ - <FormattedMessage defaultMessage="Enter new password" description="chpass form custom password label" /> - } - passwordStrengthMeter={ - <PasswordStrengthMeter password={props.formProps.values.custom} passStateUp={updatePasswordData} /> - } - id="custom-password-field" - validate={strongEnough} - autoComplete="new-password" - required={true} - placeHolder={new_password_placeholder} - /> - <FinalField - name="repeat" - component={NewPasswordInput} - componentClass="input" - type="password" - id="repeat-password-field" - label={ - <FormattedMessage defaultMessage="Repeat new password" description="chpass form custom password repeat" /> - } - validate={mustMatch} - required={true} - placeHolder={repeat_new_password_placeholder} - /> - </fieldset> - <div id="chpass-form" className="tab-pane buttons"> - <EduIDButton buttonstyle="secondary" onClick={props.handleCancel}> - <FormattedMessage defaultMessage="cancel" description="button cancel" /> - </EduIDButton> - <EduIDButton - type="submit" - id="chpass-button" - buttonstyle="primary" - disabled={props.formProps.submitting || props.formProps.invalid} - onClick={props.formProps.handleSubmit} - > - <FormattedMessage defaultMessage="Save" description="button save" /> - </EduIDButton> - </div> - </form> + <fieldset className="change-password-custom-inputs"> + <FinalField + name="custom" + component={NewPasswordInput} + componentClass="input" + type="password" + label={ + <FormattedMessage + defaultMessage="Enter new password" + description="chpass form custom password label" + /> + } + passwordStrengthMeter={ + <PasswordStrengthMeter password={formProps.values.custom} passStateUp={updatePasswordData} /> + } + id="custom-password-field" + autoComplete="new-password" + required={true} + placeHolder={new_password_placeholder} + /> + <FinalField + name="repeat" + component={NewPasswordInput} + componentClass="input" + type="password" + id="repeat-password-field" + label={ + <FormattedMessage + defaultMessage="Repeat new password" + description="chpass form custom password repeat" + /> + } + required={true} + placeHolder={repeat_new_password_placeholder} + /> + </fieldset> + <div id="chpass-form" className="tab-pane buttons"> + <EduIDButton buttonstyle="secondary" onClick={props.handleCancel}> + <FormattedMessage defaultMessage="cancel" description="button cancel" /> + </EduIDButton> + <EduIDButton + type="submit" + id="chpass-button" + buttonstyle="primary" + disabled={formProps.submitting || formProps.invalid} + onClick={formProps.handleSubmit} + > + <FormattedMessage defaultMessage="Save" description="button save" /> + </EduIDButton> + </div> + </form> + ); + }} + /> ); } From d698c209df0b61f07623488c6fbd44268e9b4bbe Mon Sep 17 00:00:00 2001 From: Eunju Huss <eunju@sunet.se> Date: Tue, 28 May 2024 14:45:31 +0900 Subject: [PATCH 40/41] Add more types --- src/components/Dashboard/ChangePasswordCustom.tsx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/components/Dashboard/ChangePasswordCustom.tsx b/src/components/Dashboard/ChangePasswordCustom.tsx index 450420137..8b122614a 100644 --- a/src/components/Dashboard/ChangePasswordCustom.tsx +++ b/src/components/Dashboard/ChangePasswordCustom.tsx @@ -1,6 +1,6 @@ import EduIDButton from "components/Common/EduIDButton"; import NewPasswordInput from "components/Common/NewPasswordInput"; -import PasswordStrengthMeter, { PasswordStrengthData } from "components/Common/PasswordStrengthMeter"; +import PasswordStrengthMeter from "components/Common/PasswordStrengthMeter"; import { emptyStringPattern } from "helperFunctions/validation/regexPatterns"; import { Field as FinalField, Form as FinalForm } from "react-final-form"; import { FormattedMessage, useIntl } from "react-intl"; @@ -24,7 +24,7 @@ export default function ChangePasswordCustomForm(props: ChangePasswordCustomForm description: "placeholder text for repeat new password", }); - function updatePasswordData(value: PasswordStrengthData) { + function updatePasswordData() { // This function is called when the password strength meter has calculated password strength // on the current value in the form. We need to trigger validation of the field again at this // point, since validation uses this calculated value (and will already have executed when we @@ -32,13 +32,13 @@ export default function ChangePasswordCustomForm(props: ChangePasswordCustomForm props.formProps.form.change("custom", props.formProps.values.custom); } - function validateNewPassword(values: any) { - const errors: any = {}; + function validateNewPassword(values: { custom?: string; repeat?: string }) { + const errors: { custom?: string; repeat?: string } = {}; if (values !== undefined) { - ["custom", "repeat"].forEach((inputName) => { - if (!values[inputName] || emptyStringPattern.test(values[inputName])) { + (["custom", "repeat"] as Array<keyof typeof values>).forEach((inputName) => { + if (!values[inputName] || emptyStringPattern.test(values[inputName] as string)) { errors[inputName] = "required"; - } else if (values["custom"].replace(/\s/g, "") !== values["repeat"].replace(/\s/g, "")) { + } else if (values["custom"]?.replace(/\s/g, "") !== values["repeat"]?.replace(/\s/g, "")) { // Remove whitespace from both passwords before comparing errors["repeat"] = "chpass.different-repeat"; } From d901237f4945b2927d7f0a992da0123f2499a639 Mon Sep 17 00:00:00 2001 From: Eunju Huss <eunju@sunet.se> Date: Fri, 31 May 2024 13:28:06 +0900 Subject: [PATCH 41/41] Fix issues --- src/components/Common/InputWrapper.tsx | 3 +-- src/components/Common/NewPasswordForm.tsx | 12 ++++++------ .../Dashboard/ChangePasswordSwitchToggle.tsx | 4 ++-- src/styles/_anti-bootstrap.scss | 4 ---- src/tests/ChangePasswordForm-test.tsx | 6 ++++-- 5 files changed, 13 insertions(+), 16 deletions(-) diff --git a/src/components/Common/InputWrapper.tsx b/src/components/Common/InputWrapper.tsx index ca30fd871..d65b957be 100644 --- a/src/components/Common/InputWrapper.tsx +++ b/src/components/Common/InputWrapper.tsx @@ -8,7 +8,6 @@ export interface InputWrapperProps extends FieldRenderProps<string> { helpBlock?: React.ReactNode; // help text show above input autoComplete?: "current-password" | "new-password" | "username"; children?: React.ReactNode; - passwordStrengthMeter?: React.ReactNode; } /** @@ -58,7 +57,7 @@ function RenderErrorMessage(props: InputWrapperProps): JSX.Element | null { <span role="alert" aria-invalid="true" tabIndex={0} className="input-validate-error"> {errorMsg || submitErrorMsg} </span> - {props.passwordStrengthMeter && props.passwordStrengthMeter} + {props.passwordStrengthMeter ? props.passwordStrengthMeter : null} </FormText> ); } diff --git a/src/components/Common/NewPasswordForm.tsx b/src/components/Common/NewPasswordForm.tsx index a820376f1..f7b89aa44 100644 --- a/src/components/Common/NewPasswordForm.tsx +++ b/src/components/Common/NewPasswordForm.tsx @@ -13,17 +13,17 @@ export interface NewPasswordFormData { } interface NewPasswordFormProps { - goBack?: () => void; - extra_security?: ExtraSecurityAlternatives; - suggested_password: string | undefined; - submitNewPasswordForm: any; + readonly goBack?: () => void; + readonly extra_security?: ExtraSecurityAlternatives; + readonly suggested_password: string | undefined; + readonly submitNewPasswordForm: any; // submitNewPasswordForm: ( // values: NewPasswordFormData, // form: FormApi<NewPasswordFormData, Partial<NewPasswordFormData>>, // callback?: ((errors?: SubmissionErrors) => void) | undefined // ) => void | Promise<void>; - submitButtonText: React.ReactChild; - handleCancel?: (event: React.MouseEvent<HTMLElement>) => void; + readonly submitButtonText: React.ReactChild; + readonly handleCancel?: (event: React.MouseEvent<HTMLElement>) => void; } export function NewPasswordForm(props: NewPasswordFormProps): JSX.Element { diff --git a/src/components/Dashboard/ChangePasswordSwitchToggle.tsx b/src/components/Dashboard/ChangePasswordSwitchToggle.tsx index 93bab3fb3..6a0a99ca9 100644 --- a/src/components/Dashboard/ChangePasswordSwitchToggle.tsx +++ b/src/components/Dashboard/ChangePasswordSwitchToggle.tsx @@ -1,8 +1,8 @@ import { FormattedMessage } from "react-intl"; export function ChangePasswordSwitchToggle(props: { - handleSwitchChange: (event: React.ChangeEvent<HTMLInputElement>) => void; - renderSuggested: boolean; + readonly handleSwitchChange: (event: React.ChangeEvent<HTMLInputElement>) => void; + readonly renderSuggested: boolean; }) { return ( <fieldset className="toggle-change-password-options"> diff --git a/src/styles/_anti-bootstrap.scss b/src/styles/_anti-bootstrap.scss index cbd565984..37f47cb5e 100644 --- a/src/styles/_anti-bootstrap.scss +++ b/src/styles/_anti-bootstrap.scss @@ -30,10 +30,6 @@ input { small { font-size: 100% !important; - // &.text-muted { - // color: $txt-black !important; - // } - &.form-text { margin-top: 0 !important; } diff --git a/src/tests/ChangePasswordForm-test.tsx b/src/tests/ChangePasswordForm-test.tsx index c7cacec27..fe20ed1e3 100644 --- a/src/tests/ChangePasswordForm-test.tsx +++ b/src/tests/ChangePasswordForm-test.tsx @@ -3,12 +3,14 @@ import { initialState as configInitialState } from "slices/IndexConfig"; import { fireEvent, render, screen, waitFor } from "./helperFunctions/DashboardTestApp-rtl"; +const suggestPassword = "test-password"; + test("renders ChangePasswordForm, suggested password value is field in suggested-password-field", () => { render(<ChangePassword />, { state: { config: { ...configInitialState, is_app_loaded: true }, chpass: { - suggested_password: "test password", + suggested_password: suggestPassword, }, }, }); @@ -22,7 +24,7 @@ test("renders custom password form after clicking do not want a suggested passwo state: { config: { ...configInitialState, is_app_loaded: true }, chpass: { - suggested_password: "test password", + suggested_password: suggestPassword, }, }, });