Merge pull request #6 from edx/adeel/van_77_adding_password_reset_confirmation_page

Adds Reset password functionality.
This commit is contained in:
adeel khan
2020-10-16 11:18:54 +05:00
committed by GitHub
17 changed files with 1040 additions and 0 deletions

View File

@@ -8,9 +8,14 @@ import {
reducer as forgotPasswordReducer,
storeName as forgotPasswordStoreName,
} from '../forgot-password';
import {
reducer as resetPasswordReducer,
storeName as resetPasswordStoreName,
} from '../reset-password';
const createRootReducer = () => combineReducers({
[logistrationStoreName]: logistrationReducer,
[forgotPasswordStoreName]: forgotPasswordReducer,
[resetPasswordStoreName]: resetPasswordReducer,
});
export default createRootReducer;

View File

@@ -2,10 +2,12 @@ import { all } from 'redux-saga/effects';
import { saga as registrationSaga } from '../logistration';
import { saga as forgotPasswordSaga } from '../forgot-password';
import { saga as resetPasswordSaga } from '../reset-password';
export default function* rootSaga() {
yield all([
registrationSaga(),
forgotPasswordSaga(),
resetPasswordSaga(),
]);
}

View File

@@ -14,6 +14,7 @@ import Footer, { messages as footerMessages } from '@edx/frontend-component-foot
import configureStore from './data/configureStore';
import { LoginPage, RegistrationPage, NotFoundPage } from './logistration';
import ForgotPasswordPage from './forgot-password';
import ResetPasswordPage from './reset-password';
import appMessages from './i18n';
import './index.scss';
@@ -37,6 +38,7 @@ subscribe(APP_READY, () => {
<Route path="/login" component={LoginPage} />
<Route path="/register" component={RegistrationPage} />
<Route path="/reset" component={ForgotPasswordPage} />
<Route path="/password_reset_confirm/:token/" component={ResetPasswordPage} />
<Route path="/notfound" component={NotFoundPage} />
<Route path="*" component={NotFoundPage} />
</Switch>

View File

@@ -0,0 +1,49 @@
import React from 'react';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import { Hyperlink } from '@edx/paragon';
import Alert from '../logistration/Alert';
const InvalidTokenMessage = () => {
const loginPasswordLink = (
<Hyperlink destination="/login">
<FormattedMessage
id="logistration.forgot.password.confirmation.support.link"
defaultMessage="sign-in"
description="link text used in message: logistration.reset.password.request.invalid.token.description.message link 'sign-in.'"
/>
</Hyperlink>
);
const forgotPassword = (<strong>Forgot Password</strong>);
return (
<div className="d-flex justify-content-center reset-password-container">
<div className="d-flex flex-column" style={{ width: '400px' }}>
<div className="form-group">
<div className="text-center mt-3">
<Alert className="alert-danger mt-n2">
<h4 style={{ color: '#a0050e' }}>
<FormattedMessage
id="logistration.reset.password.request.invalid.token.header.message"
defaultMessage="Invalid Password Reset Link"
description=""
/>
</h4>
<FormattedMessage
id="logistration.reset.password.request.invalid.token.description.message"
defaultMessage="This password reset link is invalid. It may have been used already. To reset your password, go to the {loginPasswordLink} page and select {forgotPassword}"
description=""
values={{
forgotPassword,
loginPasswordLink,
}}
/>
</Alert>
</div>
</div>
</div>
</div>
);
};
export default InvalidTokenMessage;

View File

@@ -0,0 +1,40 @@
import React from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import Alert from '../logistration/Alert';
const ResetFailureMessage = (props) => {
const errorMessage = props.errors;
return (
<div className="d-flex justify-content-center reset-password-container">
<div className="d-flex flex-column" style={{ width: '400px' }}>
<div className="form-group">
<div className="text-center mt-3">
<Alert className="alert-danger mt-n2">
<div style={{ color: '#a0050e' }}>
<FormattedMessage
id="logistration.reset.password.request.failure.header.message"
defaultMessage="{errorMessage} "
description="whatever"
values={{
errorMessage,
}}
/>
</div>
</Alert>
</div>
</div>
</div>
</div>
);
};
ResetFailureMessage.defaultProps = {
errors: '',
};
ResetFailureMessage.propTypes = {
errors: PropTypes.string,
};
export default ResetFailureMessage;

View File

@@ -0,0 +1,164 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { Button, Input, ValidationFormGroup } from '@edx/paragon';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import messages from './messages';
import { resetPassword, validateToken } from './data/actions';
import { resetPasswordResultSelector } from './data/selectors';
import InvalidTokenMessage from './InvalidToken';
import ResetSuccessMessage from './ResetSuccess';
import ResetFailureMessage from './ResetFailure';
import Spinner from './Spinner';
const ResetPasswordPage = (props) => {
const { intl } = props;
const [newPasswordInput, setNewPasswordValue] = useState('');
const [confirmPasswordInput, setConfirmPasswordValue] = useState('');
const [passwordValid, setPasswordValidValue] = useState(true);
const [passwordMatch, setPasswordMatchValue] = useState(true);
const handleNewPasswordChange = (e) => {
const newPassword = e.target.value;
setNewPasswordValue(newPassword);
const isValid = newPassword.length >= 8 && newPassword.match(/\d+/g);
setPasswordValidValue(isValid !== false);
};
const handleConfirmPasswordChange = (e) => {
const confirmPassword = e.target.value;
setConfirmPasswordValue(confirmPassword);
setPasswordMatchValue(confirmPassword === newPasswordInput);
};
const handleSubmit = (e) => {
e.preventDefault();
if (newPasswordInput === '') {
setPasswordValidValue(false);
return;
}
if (newPasswordInput !== confirmPasswordInput) {
setPasswordMatchValue(false);
return;
}
if (passwordValid && passwordMatch) {
const formPayload = {
new_password1: newPasswordInput,
new_password2: confirmPasswordInput,
};
props.resetPassword(formPayload, props.token);
}
};
if (props.token_status === 'pending') {
const { token } = props.match.params;
if (token) {
props.validateToken(token);
return <Spinner text="Token validation in progress .. " />;
}
} else if (props.token_status === 'invalid') {
return (<InvalidTokenMessage />);
} else if (props.status === 'success') {
return (<ResetSuccessMessage />);
} else {
return (
<>
{props.status === 'failure' ? <ResetFailureMessage errors={props.errors} /> : null}
<div className="d-flex justify-content-center reset-password-container">
<div className="d-flex flex-column" style={{ width: '450px' }}>
<form className="m-4">
<div className="form-group">
<h3 className="text-center mt-3">
{intl.formatMessage(messages['logistration.reset.password.page.heading'])}
</h3>
<p className="mb-4">
{intl.formatMessage(messages['logistration.reset.password.page.instructions'])}
</p>
<div className="d-flex flex-column align-items-start">
<ValidationFormGroup
for="new-password"
invalid={!passwordValid}
invalidMessage={intl.formatMessage(
messages['logistration.reset.password.page.invalid.password.message'],
)}
>
<label htmlFor="reset-password-input" className="h6 mr-1">
{intl.formatMessage(messages['logistration.reset.password.page.new.field.label'])}
</label>
<Input
name="new-password1"
id="reset-password-input"
type="password"
placeholder=""
value={newPasswordInput}
onChange={e => handleNewPasswordChange(e)}
style={{ width: '400px' }}
/>
</ValidationFormGroup>
<ValidationFormGroup
for="confirm-password"
invalid={!passwordMatch}
invalidMessage={intl.formatMessage(messages['logistration.reset.password.page.invalid.match.message'])}
>
<label htmlFor="confirm-password-input" className="h6 mr-1">
{intl.formatMessage(messages['logistration.reset.password.page.confirm.field.label'])}
</label>
<Input
name="new-password2"
id="confirm-password-input"
type="password"
placeholder=""
value={confirmPasswordInput}
onChange={e => handleConfirmPasswordChange(e)}
style={{ width: '400px' }}
/>
</ValidationFormGroup>
</div>
</div>
<Button
className="btn-primary submit"
onClick={e => handleSubmit(e)}
>
{intl.formatMessage(messages['logistration.reset.password.page.submit.button'])}
</Button>
</form>
</div>
</div>
</>
);
}
return null;
};
ResetPasswordPage.defaultProps = {
status: null,
token_status: null,
token: null,
match: null,
errors: null,
};
ResetPasswordPage.propTypes = {
intl: intlShape.isRequired,
resetPassword: PropTypes.func.isRequired,
validateToken: PropTypes.func.isRequired,
token_status: PropTypes.string,
token: PropTypes.string,
match: PropTypes.shape({
params: PropTypes.shape({
token: PropTypes.string,
}),
}),
status: PropTypes.string,
errors: PropTypes.string,
};
export default connect(
resetPasswordResultSelector,
{
resetPassword,
validateToken,
},
)(injectIntl(ResetPasswordPage));

View File

@@ -0,0 +1,47 @@
import React from 'react';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import { Hyperlink } from '@edx/paragon';
import Alert from '../logistration/Alert';
const ResetSuccessMessage = () => {
const loginPasswordLink = (
<Hyperlink destination="/login">
<FormattedMessage
id="logistration.reset.password.confirmation.support.link"
defaultMessage="Sign-in"
description="link text used in message: logistration.reset.password.invalid.token.description.message link 'sign-in.'"
/>
</Hyperlink>
);
return (
<div className="d-flex justify-content-center reset-password-container">
<div className="d-flex flex-column" style={{ width: '400px' }}>
<div className="form-group">
<div className="text-center mt-3">
<Alert className="alert-warning mt-n2">
<h4 style={{ color: 'green' }}>
<FormattedMessage
id="logistration.reset.password.request.success.header.message"
defaultMessage="Password Reset Complete."
description="whatever"
/>
</h4>
<FormattedMessage
id="logistration.reset.password.request.success.header.description.message"
defaultMessage="Your password has been reset. {loginPasswordLink} to your account."
description="whatever"
values={{
loginPasswordLink,
}}
/>
</Alert>
</div>
</div>
</div>
</div>
);
};
export default ResetSuccessMessage;

View File

@@ -0,0 +1,38 @@
import React from 'react';
import PropTypes from 'prop-types';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import { faSpinner } from '@fortawesome/free-solid-svg-icons';
const Spinner = (props) => {
const textMessage = props.text;
return (
<div className="d-flex justify-content-center reset-password-container">
<div className="d-flex flex-column" style={{ width: '400px' }}>
<div className="form-group">
<div className="d-flex flex-column align-items-start">
<h3 className="text-center mt-3">
<FormattedMessage
id="logistration.reset.password.request.token.validation.message"
defaultMessage="{textMessage}"
description=""
values={{
textMessage,
}}
/>
<FontAwesomeIcon icon={faSpinner} spin />
</h3>
</div>
</div>
</div>
</div>
);
};
Spinner.defaultProps = {
text: '',
};
Spinner.propTypes = {
text: PropTypes.string,
};
export default Spinner;

View File

@@ -0,0 +1,44 @@
import { AsyncActionType } from '../../data/utils';
export const RESET_PASSWORD = new AsyncActionType('RESET', 'PASSWORD');
export const VALIDATE_TOKEN = new AsyncActionType('VALIDATE', 'TOKEN');
// Validate confirmation token
export const validateToken = (token) => ({
type: VALIDATE_TOKEN.BASE,
payload: { token },
});
export const validateTokenBegin = () => ({
type: VALIDATE_TOKEN.BEGIN,
});
export const validateTokenSuccess = (tokenStatus, token) => ({
type: VALIDATE_TOKEN.SUCCESS,
payload: { tokenStatus, token },
});
export const validateTokenFailure = (tokenStatus) => ({
type: VALIDATE_TOKEN.FAILURE,
payload: { tokenStatus },
});
// Reset Password
export const resetPassword = (formPayload, token) => ({
type: RESET_PASSWORD.BASE,
payload: { formPayload, token },
});
export const resetPasswordBegin = () => ({
type: RESET_PASSWORD.BEGIN,
});
export const resetPasswordSuccess = data => ({
type: RESET_PASSWORD.SUCCESS,
payload: { data },
});
export const resetPasswordFailure = errors => ({
type: RESET_PASSWORD.FAILURE,
payload: { errors },
});

View File

@@ -0,0 +1,49 @@
import { RESET_PASSWORD, VALIDATE_TOKEN } from './actions';
export const defaultState = {
status: null,
token_status: 'pending',
token: null,
errors: null,
};
const reducer = (state = defaultState, action = null) => {
switch (action.type) {
case VALIDATE_TOKEN.BEGIN:
return {
...state,
token_status: 'pending',
};
case VALIDATE_TOKEN.SUCCESS:
return {
...state,
token_status: 'valid',
token: action.payload.token,
};
case VALIDATE_TOKEN.FAILURE:
return {
...state,
token_status: 'invalid',
};
case RESET_PASSWORD.BEGIN:
return {
...state,
status: 'pending',
};
case RESET_PASSWORD.SUCCESS:
return {
...state,
status: 'success',
};
case RESET_PASSWORD.FAILURE:
return {
...state,
status: 'failure',
errors: action.payload.errors,
};
default:
return state;
}
};
export default reducer;

View File

@@ -0,0 +1,54 @@
import { call, put, takeEvery } from 'redux-saga/effects';
// Actions
import {
VALIDATE_TOKEN,
validateTokenBegin,
validateTokenSuccess,
validateTokenFailure,
RESET_PASSWORD,
resetPasswordBegin,
resetPasswordSuccess,
resetPasswordFailure,
} from './actions';
import { validateToken, resetPassword } from './service';
// Services
export function* handleValidateToken(action) {
try {
yield put(validateTokenBegin());
const data = yield call(validateToken, action.payload.token);
const isValid = data.is_valid;
if (isValid) {
yield put(validateTokenSuccess(isValid, action.payload.token));
} else {
yield put(validateTokenFailure(isValid));
}
} catch (err) {
yield put(validateTokenFailure(err));
}
}
export function* handleResetPassword(action) {
try {
yield put(resetPasswordBegin());
const data = yield call(resetPassword, action.payload.formPayload, action.payload.token);
const resetStatus = data.reset_status;
const resetErrors = data.err_msg;
if (resetStatus) {
yield put(resetPasswordSuccess(resetStatus));
} else {
yield put(resetPasswordFailure(resetErrors));
}
} catch (err) {
yield put(resetPasswordFailure(err));
}
}
export default function* saga() {
yield takeEvery(RESET_PASSWORD.BASE, handleResetPassword);
yield takeEvery(VALIDATE_TOKEN.BASE, handleValidateToken);
}

View File

@@ -0,0 +1,10 @@
import { createSelector } from 'reselect';
export const storeName = 'resetPassword';
export const resetPasswordSelector = state => ({ ...state[storeName] });
export const resetPasswordResultSelector = createSelector(
resetPasswordSelector,
resetPassword => resetPassword,
);

View File

@@ -0,0 +1,41 @@
import { getConfig } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import formurlencoded from 'form-urlencoded';
// eslint-disable-next-line import/prefer-default-export
export async function validateToken(token) {
const requestConfig = {
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
isPublic: true,
};
const { data } = await getAuthenticatedHttpClient()
.post(
`${getConfig().LMS_BASE_URL}/user_api/v1/account/password_reset/token/validate/`,
formurlencoded({ token }),
requestConfig,
)
.catch((e) => {
throw (e);
});
return data;
}
// eslint-disable-next-line import/prefer-default-export
export async function resetPassword(payload, token) {
const requestConfig = {
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
isPublic: true,
};
const { data } = await getAuthenticatedHttpClient()
.post(
`${getConfig().LMS_BASE_URL}/password/reset/${token}/?track=pwreset`,
formurlencoded(payload),
requestConfig,
)
.catch((e) => {
throw (e);
});
return data;
}

View File

@@ -0,0 +1,5 @@
export { default } from './ResetPasswordPage';
export { default as reducer } from './data/reducers';
export { RESET_PASSWORD } from './data/actions';
export { default as saga } from './data/sagas';
export { storeName } from './data/selectors';

View File

@@ -0,0 +1,41 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
'logistration.reset.password.page.heading': {
id: 'logistration.reset.password.page.heading',
defaultMessage: 'Reset Your Password',
description: 'The page heading for the Reset password page.',
},
'logistration.reset.password.page.instructions': {
id: 'logistration.reset.password.page.instructions',
defaultMessage: 'Enter and confirm your new password.',
description: 'Instructions message for reset password page.',
},
'logistration.reset.password.page.invalid.password.message': {
id: 'logistration.reset.password.page.invalid.password.message',
defaultMessage: 'This password is too short. It must contain at least 8 characters. This password must contain at least 1 number',
description: 'Password format error.',
},
'logistration.reset.password.page.invalid.match.message': {
id: 'logistration.reset.password.page.invalid.match.message',
defaultMessage: 'Passwords do not match.',
description: 'Password format error.',
},
'logistration.reset.password.page.new.field.label': {
id: 'logistration.forgot.password.page.new.field.label',
defaultMessage: 'New Password',
description: 'New password field label for the reset password page.',
},
'logistration.reset.password.page.confirm.field.label': {
id: 'logistration.forgot.password.page.confirm.field.label',
defaultMessage: 'Confirm Password',
description: 'Confirm password field label for the reset password page.',
},
'logistration.reset.password.page.submit.button': {
id: 'logistration.reset.password.page.submit.button',
defaultMessage: 'Reset my password',
description: 'Submit button text for the reset password page.',
},
});
export default messages;

View File

@@ -0,0 +1,76 @@
import React from 'react';
import { Provider } from 'react-redux';
import renderer from 'react-test-renderer';
import configureStore from 'redux-mock-store';
import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n';
import ResetPasswordPage from '../ResetPasswordPage';
jest.mock('../data/selectors', () => jest.fn().mockImplementation(() => ({ resetPasswordSelector: () => ({}) })));
const IntlResetPasswordPage = injectIntl(ResetPasswordPage);
const mockStore = configureStore();
describe('ResetPasswordPage', () => {
let props = {};
let store = {};
const reduxWrapper = children => (
<IntlProvider locale="en">
<Provider store={store}>{children}</Provider>
</IntlProvider>
);
beforeEach(() => {
store = mockStore();
props = {
resetPassword: jest.fn(),
status: null,
token_status: 'pending',
token: null,
};
});
it('should match reset password default section snapshot', () => {
props = {
...props,
token: 'token',
token_status: 'valid',
};
const tree = renderer.create(reduxWrapper(<IntlResetPasswordPage {...props} />))
.toJSON();
expect(tree).toMatchSnapshot();
});
it('should match invalid token message section snapshot', () => {
props = {
...props,
token_status: 'invalid',
};
const tree = renderer.create(reduxWrapper(<IntlResetPasswordPage {...props} />))
.toJSON();
expect(tree).toMatchSnapshot();
});
it('should match successful reset message section snapshot', () => {
props = {
...props,
token_status: 'valid',
status: 'success',
};
const tree = renderer.create(reduxWrapper(<IntlResetPasswordPage {...props} />))
.toJSON();
expect(tree).toMatchSnapshot();
});
it('should match unsuccessful reset message section snapshot', () => {
props = {
...props,
token_status: 'valid',
status: 'failure',
};
const tree = renderer.create(reduxWrapper(<IntlResetPasswordPage {...props} />))
.toJSON();
expect(tree).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,373 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ResetPasswordPage should match invalid token message section snapshot 1`] = `
<div
className="d-flex justify-content-center reset-password-container"
>
<div
className="d-flex flex-column"
style={
Object {
"width": "400px",
}
}
>
<div
className="form-group"
>
<div
className="text-center mt-3"
>
<div
className="alert d-flex align-items-start alert-danger mt-n2"
>
<div />
<div>
<h4
style={
Object {
"color": "#a0050e",
}
}
>
<span>
Invalid Password Reset Link
</span>
</h4>
<span>
This password reset link is invalid. It may have been used already. To reset your password, go to the
<a
href="/login"
onClick={[Function]}
target="_self"
>
<span>
sign-in
</span>
</a>
page and select
<strong>
Forgot Password
</strong>
</span>
</div>
</div>
</div>
</div>
</div>
</div>
`;
exports[`ResetPasswordPage should match reset password default section snapshot 1`] = `
<div
className="d-flex justify-content-center reset-password-container"
>
<div
className="d-flex flex-column"
style={
Object {
"width": "450px",
}
}
>
<form
className="m-4"
>
<div
className="form-group"
>
<h3
className="text-center mt-3"
>
Reset Your Password
</h3>
<p
className="mb-4"
>
Enter and confirm your new password.
</p>
<div
className="d-flex flex-column align-items-start"
>
<div
className="form-group"
>
<label
className="h6 mr-1"
htmlFor="reset-password-input"
>
New Password
</label>
<input
aria-describedby=""
className="form-control"
id="reset-password-input"
name="new-password1"
onChange={[Function]}
placeholder=""
style={
Object {
"width": "400px",
}
}
type="password"
value=""
/>
<strong
className="invalid-feedback"
id="new-password-invalid-feedback"
>
This password is too short. It must contain at least 8 characters. This password must contain at least 1 number
</strong>
</div>
<div
className="form-group"
>
<label
className="h6 mr-1"
htmlFor="confirm-password-input"
>
Confirm Password
</label>
<input
aria-describedby=""
className="form-control"
id="confirm-password-input"
name="new-password2"
onChange={[Function]}
placeholder=""
style={
Object {
"width": "400px",
}
}
type="password"
value=""
/>
<strong
className="invalid-feedback"
id="confirm-password-invalid-feedback"
>
Passwords do not match.
</strong>
</div>
</div>
</div>
<button
className="btn btn-primary submit"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
Reset my password
</button>
</form>
</div>
</div>
`;
exports[`ResetPasswordPage should match successful reset message section snapshot 1`] = `
<div
className="d-flex justify-content-center reset-password-container"
>
<div
className="d-flex flex-column"
style={
Object {
"width": "400px",
}
}
>
<div
className="form-group"
>
<div
className="text-center mt-3"
>
<div
className="alert d-flex align-items-start alert-warning mt-n2"
>
<div />
<div>
<h4
style={
Object {
"color": "green",
}
}
>
<span>
Password Reset Complete.
</span>
</h4>
<span>
Your password has been reset.
<a
href="/login"
onClick={[Function]}
target="_self"
>
<span>
Sign-in
</span>
</a>
to your account.
</span>
</div>
</div>
</div>
</div>
</div>
</div>
`;
exports[`ResetPasswordPage should match unsuccessful reset message section snapshot 1`] = `
Array [
<div
className="d-flex justify-content-center reset-password-container"
>
<div
className="d-flex flex-column"
style={
Object {
"width": "400px",
}
}
>
<div
className="form-group"
>
<div
className="text-center mt-3"
>
<div
className="alert d-flex align-items-start alert-danger mt-n2"
>
<div />
<div>
<div
style={
Object {
"color": "#a0050e",
}
}
>
<span>
</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>,
<div
className="d-flex justify-content-center reset-password-container"
>
<div
className="d-flex flex-column"
style={
Object {
"width": "450px",
}
}
>
<form
className="m-4"
>
<div
className="form-group"
>
<h3
className="text-center mt-3"
>
Reset Your Password
</h3>
<p
className="mb-4"
>
Enter and confirm your new password.
</p>
<div
className="d-flex flex-column align-items-start"
>
<div
className="form-group"
>
<label
className="h6 mr-1"
htmlFor="reset-password-input"
>
New Password
</label>
<input
aria-describedby=""
className="form-control"
id="reset-password-input"
name="new-password1"
onChange={[Function]}
placeholder=""
style={
Object {
"width": "400px",
}
}
type="password"
value=""
/>
<strong
className="invalid-feedback"
id="new-password-invalid-feedback"
>
This password is too short. It must contain at least 8 characters. This password must contain at least 1 number
</strong>
</div>
<div
className="form-group"
>
<label
className="h6 mr-1"
htmlFor="confirm-password-input"
>
Confirm Password
</label>
<input
aria-describedby=""
className="form-control"
id="confirm-password-input"
name="new-password2"
onChange={[Function]}
placeholder=""
style={
Object {
"width": "400px",
}
}
type="password"
value=""
/>
<strong
className="invalid-feedback"
id="confirm-password-invalid-feedback"
>
Passwords do not match.
</strong>
</div>
</div>
</div>
<button
className="btn btn-primary submit"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
Reset my password
</button>
</form>
</div>
</div>,
]
`;