Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

update change pw #1730

Closed
wants to merge 42 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
8f931d1
Update heading and paragraph
eunjuhuss May 16, 2024
1d014de
Remove current password input
eunjuhuss May 16, 2024
50386ef
Adjust heading text
eunjuhuss May 16, 2024
384c49d
Remove old password requirement for change password endpoint
eunjuhuss May 16, 2024
1dcb2a2
Add border-bottom
eunjuhuss May 16, 2024
8d9c831
Remove ChangePasswordForm
eunjuhuss May 16, 2024
42861d8
Fix issue, A form label must be associated with a control
eunjuhuss May 16, 2024
ccf81b8
Ajust css and paragraph text
eunjuhuss May 16, 2024
c5b0f80
Save temporary
eunjuhuss May 16, 2024
f09ec0c
Remove container
eunjuhuss May 16, 2024
e4b167e
Rename class for reusability
eunjuhuss May 17, 2024
fbc1799
Change textInput to customInput
eunjuhuss May 17, 2024
f7a2824
Add InputType
eunjuhuss May 17, 2024
ec992e8
Add splash, when suggested_password is absentm the spinner spins
eunjuhuss May 17, 2024
9cc87aa
Render passwordStrengthMeter if it exists
eunjuhuss May 17, 2024
0281657
Change the meter max to 5 and render error message if props.password …
eunjuhuss May 17, 2024
34f5df3
Change div-> fieldset and add className
eunjuhuss May 17, 2024
2f9c760
Add meter color
eunjuhuss May 17, 2024
2ca4778
Moved strengthMeter below error message
eunjuhuss May 20, 2024
6087875
Add export for PasswordInputElement
eunjuhuss May 20, 2024
d992986
Create NewPAsswordInput
eunjuhuss May 20, 2024
44302c6
Add placeholder for passwordinputs
eunjuhuss May 20, 2024
d27176f
Adjust meter styling
eunjuhuss May 20, 2024
66a4f92
Clean up after removing ChangePasswordForm component
eunjuhuss May 20, 2024
8cba171
Resolved issue where error message no longer occure after input value…
eunjuhuss May 21, 2024
889ae05
Update test due to component changes
eunjuhuss May 21, 2024
46f992f
Adjust meter error text position and color, font size
eunjuhuss May 22, 2024
5ad1bac
Create a separate toggle component for reusability
eunjuhuss May 22, 2024
b835caf
Clean up
eunjuhuss May 22, 2024
486fc11
Add a condition to prevent showing the error message
eunjuhuss May 23, 2024
8b42477
Add hyphen, to prevent spelling errors
eunjuhuss May 23, 2024
8f7a4a8
Add handleCancel type
eunjuhuss May 23, 2024
051a832
Introduce new endpoint for changing password
eunjuhuss May 23, 2024
1e96f34
Create reusable ConfirmInfo component
eunjuhuss May 24, 2024
11e3a6d
Add new ChangePasswordSuccess page
eunjuhuss May 24, 2024
e8ddac4
An error occured,the page is being redirected to the dashboard
eunjuhuss May 24, 2024
9fc34f1
To properly retrieve the value of props.formProps.values.custom, add …
eunjuhuss May 24, 2024
dfd509f
Remove unnecessary content
eunjuhuss May 24, 2024
1da235c
Change the form to fina form and add validation to the form to fix it…
eunjuhuss May 27, 2024
d698c20
Add more types
eunjuhuss May 28, 2024
0fb4d79
Fix conflict
eunjuhuss May 31, 2024
d901237
Fix issues
eunjuhuss May 31, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions src/apis/eduidSecurity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,14 +174,13 @@ 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));
});

/*********************************************************************************************************************/
export interface ChangePasswordPayload {
old_password: string;
new_password: string;
}

Expand All @@ -199,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));
});
Expand Down
49 changes: 49 additions & 0 deletions src/components/Common/ConfirmUserInfo.tsx
Original file line number Diff line number Diff line change
@@ -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>
);
}
4 changes: 3 additions & 1 deletion src/components/Common/CustomInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -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}
Expand Down
6 changes: 4 additions & 2 deletions src/components/Common/InputWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,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;
}

Expand All @@ -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 : null}
</FormText>
);
}
18 changes: 12 additions & 6 deletions src/components/Common/NewPasswordForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -17,12 +16,14 @@ 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 submitNewPasswordForm: any;
// submitNewPasswordForm: (
// values: NewPasswordFormData,
// form: FormApi<NewPasswordFormData, Partial<NewPasswordFormData>>,
// callback?: ((errors?: SubmissionErrors) => void) | undefined
// ) => void | Promise<void>;
readonly submitButtonText: React.ReactChild;
readonly handleCancel?: (event: React.MouseEvent<HTMLElement>) => void;
}

export function NewPasswordForm(props: NewPasswordFormProps): JSX.Element {
Expand Down Expand Up @@ -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>
Expand Down
12 changes: 12 additions & 0 deletions src/components/Common/NewPasswordInput.tsx
Original file line number Diff line number Diff line change
@@ -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>
);
}
2 changes: 1 addition & 1 deletion src/components/Common/PasswordInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
5 changes: 2 additions & 3 deletions src/components/Common/PasswordStrengthMeter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => {
Expand Down Expand Up @@ -53,10 +52,10 @@ function PasswordStrengthMeter(props: PasswordStrengthMeterProps) {

return (
<React.Fragment>
<meter max="4" 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>
<meter max="4" value={pwScore} id="password-strength-meter" key="0" />
</React.Fragment>
);
}
Expand Down
135 changes: 119 additions & 16 deletions src/components/Dashboard/ChangePassword.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,40 @@
import { fetchSuggestedPassword } from "apis/eduidSecurity";
import { changePassword, fetchSuggestedPassword } from "apis/eduidSecurity";
import Splash from "components/Common/Splash";
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";
import { ChangePasswordSwitchToggle } from "./ChangePasswordSwitchToggle";

// exported for use in tests
export const finish_url = "/profile/security";

function ChangePassword() {
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?: (event: React.MouseEvent<HTMLElement>) => void;
}

interface ChangePasswordFormData {
custom?: string; // used with custom password
score?: number; // used with custom password
suggested?: string; // used with suggested password
}

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();
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({
Expand All @@ -22,21 +45,101 @@ 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("/profile/chpass/success", {
state: newPassword,
});
}
}
}

function handleCancel(event: React.MouseEvent<HTMLElement>) {
// Callback from sub-component when the user clicks on the button to abort changing password
event.preventDefault();

navigate(finish_url);
}

const initialValues = { suggested };

function handleSwitchChange() {
setRenderSuggested(!renderSuggested);
}

return (
<React.Fragment>
<h4>
<FormattedMessage defaultMessage="Change your current password" description="Dashboard change password" />
</h4>
<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 (
<Splash showChildren={Boolean(suggested_password)}>
{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.`}
/>
</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={`When creating your own password. make sure it's strong enough to keep your
accounts safe.`}
/>
</p>
</div>
</section>
)}
<ChangePasswordSwitchToggle handleSwitchChange={handleSwitchChange} renderSuggested={renderSuggested} />
{renderSuggested ? (
<ChangePasswordSuggestedForm {...child_props} handleCancel={handleCancel} />
) : (
<ChangePasswordCustomForm {...child_props} handleCancel={handleCancel} />
)}
</Splash>
);
}}
/>
);
}

export const ChangePasswordContainer = ChangePassword;
Loading
Loading