Update reset password page (#303)
* Update reset password page * add tests * fix success message
This commit is contained in:
committed by
Waheed Ahmed
parent
b16285bb47
commit
92163ac7dc
2
.env
2
.env
@@ -15,7 +15,7 @@ SEGMENT_KEY=null
|
||||
SITE_NAME=null
|
||||
USER_INFO_COOKIE_NAME=null
|
||||
AUTHN_MINIMAL_HEADER=true
|
||||
LOGIN_ISSUE_SUPPORT_LINK=null
|
||||
LOGIN_ISSUE_SUPPORT_LINK=''
|
||||
REGISTRATION_OPTIONAL_FIELDS=''
|
||||
USER_SURVEY_COOKIE_NAME=null
|
||||
COOKIE_DOMAIN=null
|
||||
|
||||
@@ -10,7 +10,7 @@ const MediumScreenHeader = (props) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="medium-screen-header">
|
||||
<div className="medium-screen-header mb-4">
|
||||
<img alt="edx" className="logo" src={getConfig().LOGO_WHITE_URL} />
|
||||
<div className="row mt-4">
|
||||
<svg className="svg-line pl-5">
|
||||
|
||||
@@ -9,7 +9,6 @@ export { default as InstitutionLogistration } from './InstitutionLogistration';
|
||||
export { RenderInstitutionButton } from './InstitutionLogistration';
|
||||
export { default as AuthnValidationFormGroup } from './AuthnValidationFormGroup';
|
||||
export { default as APIFailureMessage } from './APIFailureMessage';
|
||||
export { default as AlertDismissible } from './AlertDismissible';
|
||||
export { default as reducer } from './data/reducers';
|
||||
export { default as saga } from './data/sagas';
|
||||
export { storeName } from './data/selectors';
|
||||
|
||||
94
src/forgot-password/ForgotPasswordAlert.jsx
Normal file
94
src/forgot-password/ForgotPasswordAlert.jsx
Normal file
@@ -0,0 +1,94 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Alert, Icon } from '@edx/paragon';
|
||||
import { CheckCircle, Info } from '@edx/paragon/icons';
|
||||
|
||||
import messages from './messages';
|
||||
import { INTERNAL_SERVER_ERROR } from '../data/constants';
|
||||
import { PASSWORD_RESET } from '../reset-password/data/constants';
|
||||
|
||||
const ForgotPasswordAlert = (props) => {
|
||||
const { email, emailError, intl } = props;
|
||||
let { status } = props;
|
||||
|
||||
if (emailError) {
|
||||
status = 'form-submission-error';
|
||||
}
|
||||
|
||||
let message = '';
|
||||
let heading = intl.formatMessage(messages['forgot.password.error.alert.title']);
|
||||
switch (status) {
|
||||
case 'complete':
|
||||
heading = intl.formatMessage(messages['confirmation.message.title']);
|
||||
message = (
|
||||
<FormattedMessage
|
||||
id="forgot.password.confirmation.message"
|
||||
defaultMessage="We sent an email to {email} with instructions to reset your password.
|
||||
If you do not receive a password reset message after 1 minute, verify that you entered
|
||||
the correct email address, or check your spam folder. If you need further assistance, {supportLink}."
|
||||
description="Forgot password confirmation message"
|
||||
values={{
|
||||
email: <span className="data-hj-suppress">{email}</span>,
|
||||
supportLink: (
|
||||
<Alert.Link className="alert-link" href={getConfig().PASSWORD_RESET_SUPPORT_LINK}>
|
||||
{intl.formatMessage(messages['confirmation.support.link'])}
|
||||
</Alert.Link>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
case INTERNAL_SERVER_ERROR:
|
||||
message = intl.formatMessage(messages['internal.server.error']);
|
||||
break;
|
||||
case 'forbidden':
|
||||
heading = intl.formatMessage(messages['forgot.password.error.message.title']);
|
||||
message = intl.formatMessage(messages['forgot.password.request.in.progress.message']);
|
||||
break;
|
||||
case 'form-submission-error':
|
||||
message = intl.formatMessage(messages['extend.field.errors'], { emailError });
|
||||
break;
|
||||
case PASSWORD_RESET.INVALID_TOKEN:
|
||||
heading = intl.formatMessage(messages['invalid.token.heading']);
|
||||
message = intl.formatMessage(messages['invalid.token.error.message']);
|
||||
break;
|
||||
case PASSWORD_RESET.FORBIDDEN_REQUEST:
|
||||
heading = intl.formatMessage(messages['token.validation.rate.limit.error.heading']);
|
||||
message = intl.formatMessage(messages['token.validation.rate.limit.error']);
|
||||
break;
|
||||
case PASSWORD_RESET.INTERNAL_SERVER_ERROR:
|
||||
heading = intl.formatMessage(messages['token.validation.internal.sever.error.heading']);
|
||||
message = intl.formatMessage(messages['token.validation.internal.sever.error']);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (message) {
|
||||
return (
|
||||
<Alert id="validation-errors" className="mb-5" variant={`${status === 'complete' ? 'success' : 'danger'}`}>
|
||||
{status === 'complete' ? <Icon src={CheckCircle} className="alert-icon" /> : <Icon src={Info} className="alert-icon" />}
|
||||
<Alert.Heading>{heading}</Alert.Heading>
|
||||
<p>{message}</p>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
ForgotPasswordAlert.defaultProps = {
|
||||
email: '',
|
||||
emailError: '',
|
||||
};
|
||||
|
||||
ForgotPasswordAlert.propTypes = {
|
||||
status: PropTypes.string.isRequired,
|
||||
email: PropTypes.string,
|
||||
intl: intlShape.isRequired,
|
||||
emailError: PropTypes.string,
|
||||
};
|
||||
|
||||
export default injectIntl(ForgotPasswordAlert);
|
||||
@@ -9,59 +9,26 @@ import { Link } from 'react-router-dom';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { sendPageEvent } from '@edx/frontend-platform/analytics';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Alert,
|
||||
Form,
|
||||
StatefulButton,
|
||||
Hyperlink,
|
||||
Icon,
|
||||
} from '@edx/paragon';
|
||||
import { Info } from '@edx/paragon/icons';
|
||||
import { Form, StatefulButton, Hyperlink } from '@edx/paragon';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faSpinner, faChevronLeft } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
import { forgotPassword } from './data/actions';
|
||||
import { forgotPasswordResultSelector } from './data/selectors';
|
||||
import RequestInProgressAlert from './RequestInProgressAlert';
|
||||
import SuccessAlert from './SuccessAlert';
|
||||
|
||||
import messages from './messages';
|
||||
import { FormGroup } from '../common-components';
|
||||
import APIFailureMessage from '../common-components/APIFailureMessage';
|
||||
import { INTERNAL_SERVER_ERROR, LOGIN_PAGE, VALID_EMAIL_REGEX } from '../data/constants';
|
||||
import { DEFAULT_STATE, LOGIN_PAGE, VALID_EMAIL_REGEX } from '../data/constants';
|
||||
import { updatePathWithQueryParams, windowScrollTo } from '../data/utils';
|
||||
import ForgotPasswordAlert from './ForgotPasswordAlert';
|
||||
|
||||
const ForgotPasswordPage = (props) => {
|
||||
const { intl } = props;
|
||||
let { status } = props;
|
||||
const { intl, status, submitState } = props;
|
||||
|
||||
const platformName = getConfig().SITE_NAME;
|
||||
const regex = new RegExp(VALID_EMAIL_REGEX, 'i');
|
||||
const [validationError, setValidationError] = useState('');
|
||||
|
||||
const renderAlertMessages = (errors, email) => {
|
||||
const header = intl.formatMessage(messages['forgot.password.error.alert.title']);
|
||||
if (status === 'complete') {
|
||||
status = 'default';
|
||||
return (
|
||||
<SuccessAlert email={email} />
|
||||
);
|
||||
}
|
||||
if (errors.email) {
|
||||
return (
|
||||
<Alert variant="danger">
|
||||
<Icon src={Info} className="alert-icon" />
|
||||
<Alert.Heading>{header}</Alert.Heading>
|
||||
<p>{`${errors.email}${intl.formatMessage(messages['extend.field.errors'])}`}</p>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
if (status === INTERNAL_SERVER_ERROR) {
|
||||
return <APIFailureMessage header={header} errorCode={INTERNAL_SERVER_ERROR} />;
|
||||
}
|
||||
return status === 'forbidden' ? <RequestInProgressAlert /> : null;
|
||||
};
|
||||
|
||||
const getValidationMessage = (email) => {
|
||||
let error = '';
|
||||
|
||||
@@ -109,46 +76,42 @@ const ForgotPasswordPage = (props) => {
|
||||
{ siteName: getConfig().SITE_NAME })}
|
||||
</title>
|
||||
</Helmet>
|
||||
<div className="d-flex justify-content-center">
|
||||
<div className="d-flex flex-column">
|
||||
<Form className="mw-xs">
|
||||
{ renderAlertMessages(errors, values.email) }
|
||||
<h3>
|
||||
{intl.formatMessage(messages['forgot.password.page.heading'])}
|
||||
</h3>
|
||||
<p className="mb-4">
|
||||
{intl.formatMessage(messages['forgot.password.page.instructions'])}
|
||||
</p>
|
||||
<FormGroup
|
||||
floatingLabel={intl.formatMessage(messages['forgot.password.page.email.field.label'])}
|
||||
name="email"
|
||||
errorMessage={validationError}
|
||||
value={values.email}
|
||||
handleBlur={() => getValidationMessage(values.email)}
|
||||
handleChange={e => setFieldValue('email', e.target.value)}
|
||||
helpText={[intl.formatMessage(messages['forgot.password.email.help.text'], { platformName })]}
|
||||
/>
|
||||
<StatefulButton
|
||||
type="submit"
|
||||
variant="brand"
|
||||
className="login-button-width"
|
||||
state={status}
|
||||
labels={{
|
||||
default: intl.formatMessage(messages['forgot.password.page.submit.button']),
|
||||
}}
|
||||
icons={{ pending: <FontAwesomeIcon icon={faSpinner} spin /> }}
|
||||
onClick={handleSubmit}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
/>
|
||||
<Hyperlink id="forgot-password" className="btn btn-link font-weight-500 text-body" destination={getConfig().LOGIN_ISSUE_SUPPORT_LINK}>
|
||||
{intl.formatMessage(messages['need.help.sign.in.text'])}
|
||||
</Hyperlink>
|
||||
<p className="mt-5 one-rem-font">{intl.formatMessage(messages['additional.help.text'])}
|
||||
<span><Hyperlink destination={`mailto:${getConfig().INFO_EMAIL}`}>{getConfig().INFO_EMAIL}</Hyperlink></span>
|
||||
</p>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
<Form className="mw-xs">
|
||||
<ForgotPasswordAlert email={values.email} emailError={errors.email} status={status} />
|
||||
<h4>
|
||||
{intl.formatMessage(messages['forgot.password.page.heading'])}
|
||||
</h4>
|
||||
<p className="mb-4">
|
||||
{intl.formatMessage(messages['forgot.password.page.instructions'])}
|
||||
</p>
|
||||
<FormGroup
|
||||
floatingLabel={intl.formatMessage(messages['forgot.password.page.email.field.label'])}
|
||||
name="email"
|
||||
errorMessage={validationError}
|
||||
value={values.email}
|
||||
handleBlur={() => getValidationMessage(values.email)}
|
||||
handleChange={e => setFieldValue('email', e.target.value)}
|
||||
helpText={[intl.formatMessage(messages['forgot.password.email.help.text'], { platformName })]}
|
||||
/>
|
||||
<StatefulButton
|
||||
type="submit"
|
||||
variant="brand"
|
||||
className="login-button-width"
|
||||
state={submitState}
|
||||
labels={{
|
||||
default: intl.formatMessage(messages['forgot.password.page.submit.button']),
|
||||
}}
|
||||
icons={{ pending: <FontAwesomeIcon icon={faSpinner} spin /> }}
|
||||
onClick={handleSubmit}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
/>
|
||||
<Hyperlink id="forgot-password" className="btn btn-link font-weight-500 text-body" destination={getConfig().LOGIN_ISSUE_SUPPORT_LINK}>
|
||||
{intl.formatMessage(messages['need.help.sign.in.text'])}
|
||||
</Hyperlink>
|
||||
<p className="mt-5 one-rem-font">{intl.formatMessage(messages['additional.help.text'])}
|
||||
<span><Hyperlink destination={`mailto:${getConfig().INFO_EMAIL}`}>{getConfig().INFO_EMAIL}</Hyperlink></span>
|
||||
</p>
|
||||
</Form>
|
||||
</>
|
||||
)}
|
||||
</Formik>
|
||||
@@ -161,10 +124,12 @@ ForgotPasswordPage.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
forgotPassword: PropTypes.func.isRequired,
|
||||
status: PropTypes.string,
|
||||
submitState: PropTypes.string,
|
||||
};
|
||||
|
||||
ForgotPasswordPage.defaultProps = {
|
||||
status: null,
|
||||
submitState: DEFAULT_STATE,
|
||||
};
|
||||
|
||||
export default connect(
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Alert } from '@edx/paragon';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
const RequestInProgressAlert = (props) => {
|
||||
const { intl } = props;
|
||||
|
||||
return (
|
||||
<Alert variant="danger">
|
||||
<Alert.Heading>{intl.formatMessage(messages['forgot.password.error.message.title'])}</Alert.Heading>
|
||||
<ul>
|
||||
<li>{intl.formatMessage(messages['forgot.password.request.in.progress.message'])}</li>
|
||||
</ul>
|
||||
</Alert>
|
||||
);
|
||||
};
|
||||
|
||||
RequestInProgressAlert.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(RequestInProgressAlert);
|
||||
@@ -1,44 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
|
||||
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Alert, Icon } from '@edx/paragon';
|
||||
import { CheckCircle } from '@edx/paragon/icons';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
const SuccessAlert = (props) => {
|
||||
const { email, intl } = props;
|
||||
|
||||
return (
|
||||
<Alert id="forgotpassword-success-alert" variant="success">
|
||||
<Icon src={CheckCircle} className="alert-icon" />
|
||||
<Alert.Heading>{intl.formatMessage(messages['confirmation.message.title'])}</Alert.Heading>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="forgot.password.confirmation.message"
|
||||
defaultMessage="We sent an email to {email} with instructions to reset your password.
|
||||
If you do not receive a password reset message after 1 minute, verify that you entered
|
||||
the correct email address, or check your spam folder. If you need further assistance, {supportLink}."
|
||||
description="Forgot password confirmation message"
|
||||
values={{
|
||||
email: <span className="data-hj-suppress">{email}</span>,
|
||||
supportLink: (
|
||||
<Alert.Link className="alert-link" href={getConfig().PASSWORD_RESET_SUPPORT_LINK}>
|
||||
{intl.formatMessage(messages['confirmation.support.link'])}
|
||||
</Alert.Link>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
</Alert>
|
||||
);
|
||||
};
|
||||
|
||||
SuccessAlert.propTypes = {
|
||||
email: PropTypes.string.isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(SuccessAlert);
|
||||
@@ -1,8 +1,10 @@
|
||||
import { FORGOT_PASSWORD } from './actions';
|
||||
import { INTERNAL_SERVER_ERROR } from '../../data/constants';
|
||||
import { INTERNAL_SERVER_ERROR, PENDING_STATE } from '../../data/constants';
|
||||
import { PASSWORD_RESET_FAILURE } from '../../reset-password/data/actions';
|
||||
|
||||
export const defaultState = {
|
||||
status: null,
|
||||
status: '',
|
||||
submitState: '',
|
||||
};
|
||||
|
||||
const reducer = (state = defaultState, action = null) => {
|
||||
@@ -11,6 +13,7 @@ const reducer = (state = defaultState, action = null) => {
|
||||
case FORGOT_PASSWORD.BEGIN:
|
||||
return {
|
||||
status: 'pending',
|
||||
submitState: PENDING_STATE,
|
||||
};
|
||||
case FORGOT_PASSWORD.SUCCESS:
|
||||
return {
|
||||
@@ -25,8 +28,12 @@ const reducer = (state = defaultState, action = null) => {
|
||||
return {
|
||||
status: INTERNAL_SERVER_ERROR,
|
||||
};
|
||||
case PASSWORD_RESET_FAILURE:
|
||||
return {
|
||||
status: action.payload.errorCode,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
return defaultState;
|
||||
}
|
||||
}
|
||||
return state;
|
||||
|
||||
@@ -21,7 +21,6 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Enter a valid email address',
|
||||
description: 'Invalid email address message for input field.',
|
||||
},
|
||||
|
||||
'forgot.password.page.email.field.label': {
|
||||
id: 'forgot.password.page.email.field.label',
|
||||
defaultMessage: 'Email',
|
||||
@@ -78,7 +77,6 @@ const messages = defineMessages({
|
||||
defaultMessage: 'contact technical support',
|
||||
description: 'Technical support link text',
|
||||
},
|
||||
|
||||
'need.help.sign.in.text': {
|
||||
id: 'need.help.sign.in.text',
|
||||
defaultMessage: 'Need help signing in?',
|
||||
@@ -91,14 +89,55 @@ const messages = defineMessages({
|
||||
},
|
||||
'sign.in.text': {
|
||||
id: 'sign.in.text',
|
||||
defaultMessage: 'Sign In',
|
||||
defaultMessage: 'Sign in',
|
||||
description: 'login page link on password page',
|
||||
},
|
||||
'extend.field.errors': {
|
||||
id: 'extend.field.errors',
|
||||
defaultMessage: ' below.',
|
||||
defaultMessage: '{emailError} below.',
|
||||
description: 'extends the field error for alert message',
|
||||
},
|
||||
|
||||
// Reset password token validation failure
|
||||
'invalid.token.heading': {
|
||||
id: 'invalid.token.heading',
|
||||
defaultMessage: 'Invalid password reset link',
|
||||
description: 'Alert heading when reset password link is invalid',
|
||||
},
|
||||
'invalid.token.error.message': {
|
||||
id: 'invalid.token.error.message',
|
||||
defaultMessage: 'This link has expired. Enter your email below to receive a new link.',
|
||||
description: 'Alert message when reset password link has expired',
|
||||
},
|
||||
'token.validation.rate.limit.error.heading': {
|
||||
id: 'token.validation.rate.limit.error.heading',
|
||||
defaultMessage: 'Too many requests',
|
||||
description: 'Too many request at server end point',
|
||||
},
|
||||
'token.validation.rate.limit.error': {
|
||||
id: 'token.validation.rate.limit.error',
|
||||
defaultMessage: 'An error has occurred because of too many requests. Please try again after some time.',
|
||||
description: 'Error message that appears when server responds with 429 error code',
|
||||
},
|
||||
'token.validation.internal.sever.error.heading': {
|
||||
id: 'token.validation.internal.sever.error.heading',
|
||||
defaultMessage: 'Token validation failure',
|
||||
description: 'Failed to validate reset password token error message.',
|
||||
},
|
||||
'token.validation.internal.sever.error': {
|
||||
id: 'token.validation.internal.sever.error',
|
||||
defaultMessage: 'An error has occurred. Try refreshing the page, or check your internet connection.',
|
||||
description: 'Error message that appears when server responds with 500 error code',
|
||||
},
|
||||
// Error messages
|
||||
'internal.server.error': {
|
||||
id: 'internal.server.error',
|
||||
defaultMessage: 'An error has occurred. Try refreshing the page, or check your internet connection.',
|
||||
description: 'Error message that appears when server responds with 500 error code',
|
||||
},
|
||||
'rate.limit.error': {
|
||||
id: 'rate.limit.error',
|
||||
defaultMessage: 'An error has occurred because of too many requests. Please try again after some time.',
|
||||
description: 'Error message that appears when server responds with 429 error code',
|
||||
},
|
||||
});
|
||||
export default messages;
|
||||
|
||||
@@ -2,9 +2,9 @@ import React from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { Provider } from 'react-redux';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import renderer from 'react-test-renderer';
|
||||
import { mount } from 'enzyme';
|
||||
import configureStore from 'redux-mock-store';
|
||||
import { mergeConfig } from '@edx/frontend-platform';
|
||||
import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import CookiePolicyBanner from '@edx/frontend-component-cookie-policy-banner';
|
||||
import * as analytics from '@edx/frontend-platform/analytics';
|
||||
@@ -25,6 +25,11 @@ const initialState = {
|
||||
};
|
||||
|
||||
describe('ForgotPasswordPage', () => {
|
||||
mergeConfig({
|
||||
LOGIN_ISSUE_SUPPORT_LINK: '',
|
||||
INFO_EMAIL: '',
|
||||
});
|
||||
|
||||
let props = {};
|
||||
let store = {};
|
||||
|
||||
@@ -44,32 +49,6 @@ describe('ForgotPasswordPage', () => {
|
||||
};
|
||||
});
|
||||
|
||||
it('should match default section snapshot', () => {
|
||||
const tree = renderer.create(reduxWrapper(<IntlForgotPasswordPage {...props} />))
|
||||
.toJSON();
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should match forbidden section snapshot', () => {
|
||||
props = {
|
||||
...props,
|
||||
status: 'forbidden',
|
||||
};
|
||||
const tree = renderer.create(reduxWrapper(<IntlForgotPasswordPage {...props} />))
|
||||
.toJSON();
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should match pending section snapshot', () => {
|
||||
props = {
|
||||
...props,
|
||||
status: 'pending',
|
||||
};
|
||||
const tree = renderer.create(reduxWrapper(<IntlForgotPasswordPage {...props} />))
|
||||
.toJSON();
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should display need other help signing in button', () => {
|
||||
const wrapper = mount(reduxWrapper(<IntlForgotPasswordPage {...props} />));
|
||||
expect(wrapper.find('#forgot-password.btn-link').first().text()).toEqual('Need help signing in?');
|
||||
@@ -96,7 +75,7 @@ describe('ForgotPasswordPage', () => {
|
||||
+ 'An error has occurred. Try refreshing the page, or check your internet connection.';
|
||||
const wrapper = mount(reduxWrapper(<IntlForgotPasswordPage {...props} />));
|
||||
|
||||
expect(wrapper.find('#internal-server-error').first().text()).toEqual(expectedMessage);
|
||||
expect(wrapper.find('#validation-errors').first().text()).toEqual(expectedMessage);
|
||||
});
|
||||
|
||||
it('should display empty email validation message', async () => {
|
||||
|
||||
@@ -42,6 +42,7 @@ import {
|
||||
getAllPossibleQueryParam,
|
||||
} from '../data/utils';
|
||||
import { forgotPasswordResultSelector } from '../forgot-password';
|
||||
import ResetPasswordSuccess from '../reset-password/ResetPasswordSuccess';
|
||||
|
||||
class LoginPage extends React.Component {
|
||||
constructor(props, context) {
|
||||
@@ -206,7 +207,8 @@ class LoginPage extends React.Component {
|
||||
{this.props.loginError ? <LoginFailureMessage loginError={this.props.loginError} /> : null}
|
||||
{submitState === DEFAULT_STATE && this.state.isSubmitted ? windowScrollTo({ left: 0, top: 0, behavior: 'smooth' }) : null}
|
||||
{activationMsgType && <AccountActivationMessage messageType={activationMsgType} />}
|
||||
<Form className="test">
|
||||
{this.props.resetPassword && !this.props.loginError ? <ResetPasswordSuccess /> : null}
|
||||
<Form>
|
||||
<FormGroup
|
||||
name="email"
|
||||
value={this.state.emailOrUsername}
|
||||
@@ -288,6 +290,7 @@ LoginPage.defaultProps = {
|
||||
forgotPassword: null,
|
||||
loginResult: null,
|
||||
loginError: null,
|
||||
resetPassword: false,
|
||||
submitState: DEFAULT_STATE,
|
||||
thirdPartyAuthApiStatus: 'pending',
|
||||
thirdPartyAuthContext: {
|
||||
@@ -313,6 +316,7 @@ LoginPage.propTypes = {
|
||||
redirectUrl: PropTypes.string,
|
||||
success: PropTypes.bool,
|
||||
}),
|
||||
resetPassword: PropTypes.bool,
|
||||
submitState: PropTypes.string,
|
||||
thirdPartyAuthApiStatus: PropTypes.string,
|
||||
thirdPartyAuthContext: PropTypes.shape({
|
||||
@@ -338,6 +342,7 @@ const mapStateToProps = state => {
|
||||
loginError,
|
||||
loginResult,
|
||||
thirdPartyAuthContext,
|
||||
resetPassword: state.login.resetPassword,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { LOGIN_REQUEST } from './actions';
|
||||
|
||||
import { DEFAULT_STATE, PENDING_STATE } from '../../data/constants';
|
||||
import { RESET_PASSWORD } from '../../reset-password';
|
||||
|
||||
export const defaultState = {
|
||||
loginError: null,
|
||||
loginResult: {},
|
||||
resetPassword: false,
|
||||
};
|
||||
|
||||
const reducer = (state = defaultState, action) => {
|
||||
@@ -13,6 +15,7 @@ const reducer = (state = defaultState, action) => {
|
||||
return {
|
||||
...state,
|
||||
submitState: PENDING_STATE,
|
||||
resetPassword: false,
|
||||
};
|
||||
case LOGIN_REQUEST.SUCCESS:
|
||||
return {
|
||||
@@ -30,8 +33,15 @@ const reducer = (state = defaultState, action) => {
|
||||
...state,
|
||||
loginError: null,
|
||||
};
|
||||
case RESET_PASSWORD.SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
resetPassword: true,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
return {
|
||||
...state,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { Alert } from '@edx/paragon';
|
||||
|
||||
import messages from './messages';
|
||||
import { LOGIN_PAGE } from '../data/constants';
|
||||
|
||||
const InvalidTokenMessage = props => {
|
||||
const { intl } = props;
|
||||
|
||||
const loginPasswordLink = (
|
||||
<Alert.Link href={LOGIN_PAGE}>
|
||||
{intl.formatMessage(messages['forgot.password.confirmation.sign.in.link'])}
|
||||
</Alert.Link>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>{intl.formatMessage(messages['reset.password.page.title'],
|
||||
{ siteName: getConfig().SITE_NAME })}
|
||||
</title>
|
||||
</Helmet>
|
||||
<div className="d-flex justify-content-center m-4">
|
||||
<div className="d-flex flex-column mw-500">
|
||||
<Alert variant="danger">
|
||||
<Alert.Heading> {intl.formatMessage(messages['reset.password.request.invalid.token.header'])}</Alert.Heading>
|
||||
<FormattedMessage
|
||||
id="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="Invalid password reset link help text message."
|
||||
values={{
|
||||
forgotPassword: <strong> {intl.formatMessage(messages['reset.password.request.forgot.password.text'])} </strong>,
|
||||
loginPasswordLink,
|
||||
}}
|
||||
/>
|
||||
</Alert>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
InvalidTokenMessage.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(InvalidTokenMessage);
|
||||
59
src/reset-password/ResetPasswordFailure.jsx
Normal file
59
src/reset-password/ResetPasswordFailure.jsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Alert, Icon } from '@edx/paragon';
|
||||
import { Info } from '@edx/paragon/icons';
|
||||
|
||||
import { FORM_SUBMISSION_ERROR, PASSWORD_RESET, PASSWORD_VALIDATION_ERROR } from './data/constants';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
const ResetPasswordFailure = (props) => {
|
||||
const { errorCode, errorMsg, intl } = props;
|
||||
|
||||
let errorMessage = null;
|
||||
let heading = intl.formatMessage(messages['reset.password.failure.heading']);
|
||||
switch (errorCode) {
|
||||
case PASSWORD_RESET.FORBIDDEN_REQUEST:
|
||||
heading = intl.formatMessage(messages['reset.server.rate.limit.error']);
|
||||
errorMessage = intl.formatMessage(messages['rate.limit.error']);
|
||||
break;
|
||||
case PASSWORD_RESET.INTERNAL_SERVER_ERROR:
|
||||
errorMessage = intl.formatMessage(messages['internal.server.error']);
|
||||
break;
|
||||
case PASSWORD_VALIDATION_ERROR:
|
||||
errorMessage = errorMsg;
|
||||
break;
|
||||
case FORM_SUBMISSION_ERROR:
|
||||
errorMessage = intl.formatMessage(messages['reset.password.form.submission.error']);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (errorMessage) {
|
||||
return (
|
||||
<Alert id="validation-errors" className="mb-5" variant="danger">
|
||||
<Icon src={Info} className="alert-icon" />
|
||||
<Alert.Heading>{heading}</Alert.Heading>
|
||||
<p>{errorMessage}</p>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
ResetPasswordFailure.defaultProps = {
|
||||
errorCode: null,
|
||||
errorMsg: null,
|
||||
};
|
||||
|
||||
ResetPasswordFailure.propTypes = {
|
||||
errorCode: PropTypes.string,
|
||||
errorMsg: PropTypes.string,
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(ResetPasswordFailure);
|
||||
@@ -2,185 +2,158 @@ import React, { useEffect, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { connect } from 'react-redux';
|
||||
import {
|
||||
Alert, Form, StatefulButton,
|
||||
} from '@edx/paragon';
|
||||
import { Link, Redirect } from 'react-router-dom';
|
||||
|
||||
import { Form, Spinner, StatefulButton } from '@edx/paragon';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { getQueryParameters, getConfig } from '@edx/frontend-platform';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faSpinner } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faChevronLeft, faSpinner } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
import messages from './messages';
|
||||
import { resetPassword, validateToken } from './data/actions';
|
||||
import { resetPasswordResultSelector } from './data/selectors';
|
||||
import { validatePassword } from './data/service';
|
||||
import InvalidTokenMessage from './InvalidToken';
|
||||
import ResetSuccessMessage from './ResetSuccess';
|
||||
import ResetPasswordFailure from './ResetPasswordFailure';
|
||||
import { PasswordField } from '../common-components';
|
||||
import {
|
||||
AuthnValidationFormGroup,
|
||||
APIFailureMessage,
|
||||
} from '../common-components';
|
||||
import Spinner from './Spinner';
|
||||
import { API_RATELIMIT_ERROR, INTERNAL_SERVER_ERROR } from '../data/constants';
|
||||
import { windowScrollTo } from '../data/utils';
|
||||
LETTER_REGEX, LOGIN_PAGE, NUMBER_REGEX, RESET_PAGE,
|
||||
} from '../data/constants';
|
||||
import {
|
||||
FORM_SUBMISSION_ERROR, PASSWORD_RESET_ERROR, PASSWORD_VALIDATION_ERROR, TOKEN_STATE,
|
||||
} from './data/constants';
|
||||
import { updatePathWithQueryParams, windowScrollTo } from '../data/utils';
|
||||
|
||||
const ResetPasswordPage = (props) => {
|
||||
const { intl } = props;
|
||||
const params = getQueryParameters();
|
||||
|
||||
const [newPasswordInput, setNewPasswordValue] = useState('');
|
||||
const [confirmPasswordInput, setConfirmPasswordValue] = useState('');
|
||||
const [passwordValid, setPasswordValidValue] = useState(true);
|
||||
const [passwordMatch, setPasswordMatchValue] = useState(true);
|
||||
const [validationMessage, setvalidationMessage] = useState('');
|
||||
const [bannerErrorMessage, setbannerErrorMessage] = useState('');
|
||||
const [newPassword, setNewPassword] = useState('');
|
||||
const [confirmPassword, setConfirmPassword] = useState('');
|
||||
const [formErrors, setFormErrors] = useState({});
|
||||
const [errorCode, setErrorCode] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (props.status === 'failure'
|
||||
&& props.errors !== INTERNAL_SERVER_ERROR
|
||||
&& props.errors !== API_RATELIMIT_ERROR) {
|
||||
setbannerErrorMessage(props.errors);
|
||||
setvalidationMessage(props.errors);
|
||||
setPasswordValidValue(false);
|
||||
if (props.status !== TOKEN_STATE.PENDING && props.status !== PASSWORD_RESET_ERROR) {
|
||||
setErrorCode(props.status);
|
||||
}
|
||||
}, [props.status]);
|
||||
if (props.status === PASSWORD_VALIDATION_ERROR) {
|
||||
formErrors.newPassword = intl.formatMessage(messages['password.validation.message']);
|
||||
setFormErrors({ ...formErrors });
|
||||
}
|
||||
}, [props.errorMsg]);
|
||||
|
||||
const validatePasswordFromBackend = async (newPassword) => {
|
||||
let errorMessage;
|
||||
const validatePasswordFromBackend = async (password) => {
|
||||
let errorMessage = '';
|
||||
try {
|
||||
errorMessage = await validatePassword(newPassword);
|
||||
errorMessage = await validatePassword(password);
|
||||
} catch (err) {
|
||||
errorMessage = '';
|
||||
}
|
||||
setPasswordValidValue(!errorMessage);
|
||||
setvalidationMessage(errorMessage);
|
||||
setFormErrors({ ...formErrors, newPassword: errorMessage });
|
||||
};
|
||||
|
||||
const handleNewPasswordChange = (e) => {
|
||||
const newPassword = e.target.value;
|
||||
setNewPasswordValue(newPassword);
|
||||
};
|
||||
|
||||
const handleNewPasswordOnBlur = (e) => {
|
||||
const newPassword = e.target.value;
|
||||
setNewPasswordValue(newPassword);
|
||||
|
||||
if (newPassword === '') {
|
||||
setPasswordValidValue(false);
|
||||
setvalidationMessage(intl.formatMessage(messages['reset.password.empty.new.password.field.error']));
|
||||
} else {
|
||||
validatePasswordFromBackend(newPassword);
|
||||
const validateInput = (name, value) => {
|
||||
switch (name) {
|
||||
case 'newPassword':
|
||||
if (!value || !LETTER_REGEX.test(value) || !NUMBER_REGEX.test(value) || value.length < 8) {
|
||||
formErrors.newPassword = intl.formatMessage(messages['password.validation.message']);
|
||||
} else {
|
||||
validatePasswordFromBackend(value);
|
||||
}
|
||||
break;
|
||||
case 'confirmPassword':
|
||||
if (!value) {
|
||||
formErrors.confirmPassword = intl.formatMessage(messages['confirm.your.password']);
|
||||
} else if (value !== newPassword) {
|
||||
formErrors.confirmPassword = intl.formatMessage(messages['passwords.do.not.match']);
|
||||
} else {
|
||||
formErrors.confirmPassword = '';
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
setFormErrors({ ...formErrors });
|
||||
return !Object.values(formErrors).some(x => (x !== ''));
|
||||
};
|
||||
|
||||
const handleConfirmPasswordChange = (e) => {
|
||||
const confirmPassword = e.target.value;
|
||||
setConfirmPasswordValue(confirmPassword);
|
||||
setPasswordMatchValue(confirmPassword === newPasswordInput);
|
||||
const { value } = e.target;
|
||||
|
||||
setConfirmPassword(value);
|
||||
validateInput('confirmPassword', value);
|
||||
};
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
windowScrollTo({ left: 0, top: 0, behavior: 'smooth' });
|
||||
if (newPasswordInput === '') {
|
||||
setPasswordValidValue(false);
|
||||
setvalidationMessage(intl.formatMessage(messages['reset.password.empty.new.password.field.error']));
|
||||
setbannerErrorMessage(intl.formatMessage(messages['reset.password.empty.new.password.field.error']));
|
||||
return;
|
||||
}
|
||||
if (newPasswordInput !== confirmPasswordInput) {
|
||||
setPasswordMatchValue(false);
|
||||
return;
|
||||
}
|
||||
const isPasswordValid = validateInput('newPassword', newPassword);
|
||||
const isPasswordConfirmed = validateInput('confirmPassword', confirmPassword);
|
||||
|
||||
const formPayload = {
|
||||
new_password1: newPasswordInput,
|
||||
new_password2: confirmPasswordInput,
|
||||
};
|
||||
props.resetPassword(formPayload, props.token, params);
|
||||
if (isPasswordValid && isPasswordConfirmed) {
|
||||
const formPayload = {
|
||||
new_password1: newPassword,
|
||||
new_password2: confirmPassword,
|
||||
};
|
||||
const params = getQueryParameters();
|
||||
props.resetPassword(formPayload, props.token, params);
|
||||
} else {
|
||||
setErrorCode(FORM_SUBMISSION_ERROR);
|
||||
windowScrollTo({ left: 0, top: 0, behavior: 'smooth' });
|
||||
}
|
||||
};
|
||||
|
||||
if (props.status === 'token-pending') {
|
||||
if (props.status === TOKEN_STATE.PENDING) {
|
||||
const { token } = props.match.params;
|
||||
if (token) {
|
||||
props.validateToken(token);
|
||||
return <Spinner />;
|
||||
return <Spinner animation="border" variant="primary" className="mt-5" />;
|
||||
}
|
||||
} else if (props.status === 'invalid' && props.errors === INTERNAL_SERVER_ERROR) {
|
||||
return (
|
||||
<APIFailureMessage header={intl.formatMessage(messages['reset.password.token.validation.sever.error'])} errorCode={INTERNAL_SERVER_ERROR} />
|
||||
);
|
||||
} else if (props.status === 'invalid' && props.errors === API_RATELIMIT_ERROR) {
|
||||
return (
|
||||
<APIFailureMessage header={intl.formatMessage(messages['reset.server.ratelimit.error'])} errorCode={API_RATELIMIT_ERROR} />
|
||||
);
|
||||
} else if (props.status === 'invalid') {
|
||||
return <InvalidTokenMessage />;
|
||||
} else if (props.status === PASSWORD_RESET_ERROR) {
|
||||
return <Redirect to={RESET_PAGE} />;
|
||||
} else if (props.status === 'success') {
|
||||
return <ResetSuccessMessage />;
|
||||
return <Redirect to={LOGIN_PAGE} />;
|
||||
} else {
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<Helmet>
|
||||
<title>{intl.formatMessage(messages['reset.password.page.title'],
|
||||
{ siteName: getConfig().SITE_NAME })}
|
||||
</title>
|
||||
</Helmet>
|
||||
{props.status === 'failure' && props.errors === INTERNAL_SERVER_ERROR ? (
|
||||
<APIFailureMessage header={intl.formatMessage(messages['reset.password.request.server.error'])} errorCode={INTERNAL_SERVER_ERROR} />
|
||||
) : null}
|
||||
{props.status === 'failure' && props.errors === API_RATELIMIT_ERROR ? (
|
||||
<APIFailureMessage header={intl.formatMessage(messages['reset.server.ratelimit.error'])} errorCode={API_RATELIMIT_ERROR} />
|
||||
) : null}
|
||||
<div id="main" className="d-flex justify-content-center m-4">
|
||||
<div className="d-flex flex-column mw-500">
|
||||
{bannerErrorMessage ? (
|
||||
<Alert id="validation-errors" variant="danger">
|
||||
<Alert.Heading>{intl.formatMessage(messages['forgot.password.empty.new.password.error.heading'])}</Alert.Heading>
|
||||
<ul><li>{bannerErrorMessage}</li></ul>
|
||||
</Alert>
|
||||
) : null}
|
||||
<span className="nav nav-tabs">
|
||||
<Link className="nav-item nav-link" to={updatePathWithQueryParams(LOGIN_PAGE)}>
|
||||
<FontAwesomeIcon className="mr-2" icon={faChevronLeft} /> {intl.formatMessage(messages['sign.in'])}
|
||||
</Link>
|
||||
</span>
|
||||
<div id="main-content" className="main-content">
|
||||
<div className="mw-xs">
|
||||
<ResetPasswordFailure errorCode={errorCode} errorMsg={props.errorMsg} />
|
||||
<h4>{intl.formatMessage(messages['reset.password'])}</h4>
|
||||
<p className="mb-4">{intl.formatMessage(messages['reset.password.page.instructions'])}</p>
|
||||
<Form>
|
||||
<h1 className="mt-3 h3">
|
||||
{intl.formatMessage(messages['reset.password.page.heading'])}
|
||||
</h1>
|
||||
<p className="mb-4">
|
||||
{intl.formatMessage(messages['reset.password.page.instructions'])}
|
||||
</p>
|
||||
<AuthnValidationFormGroup
|
||||
label={intl.formatMessage(messages['reset.password.page.new.field.label'])}
|
||||
for="reset-password-input"
|
||||
name="new-password1"
|
||||
type="password"
|
||||
invalid={!passwordValid}
|
||||
ariaInvalid={!passwordValid}
|
||||
invalidMessage={validationMessage}
|
||||
value={newPasswordInput}
|
||||
onChange={e => handleNewPasswordChange(e)}
|
||||
onBlur={e => handleNewPasswordOnBlur(e)}
|
||||
className="w-100"
|
||||
inputFieldStyle="border-gray-600"
|
||||
<PasswordField
|
||||
name="newPassword"
|
||||
value={newPassword}
|
||||
handleChange={(e) => setNewPassword(e.target.value)}
|
||||
handleBlur={(e) => validateInput(e.target.name, e.target.value)}
|
||||
errorMessage={formErrors.newPassword}
|
||||
floatingLabel={intl.formatMessage(messages['new.password.label'])}
|
||||
/>
|
||||
<AuthnValidationFormGroup
|
||||
label={intl.formatMessage(messages['reset.password.page.confirm.field.label'])}
|
||||
for="confirm-password-input"
|
||||
name="new-password2"
|
||||
type="password"
|
||||
invalid={!passwordMatch}
|
||||
ariaInvalid={!passwordMatch}
|
||||
invalidMessage={intl.formatMessage(messages['reset.password.page.invalid.match.message'])}
|
||||
value={confirmPasswordInput}
|
||||
onChange={e => handleConfirmPasswordChange(e)}
|
||||
className="w-100"
|
||||
inputFieldStyle="border-gray-600"
|
||||
<PasswordField
|
||||
name="confirmPassword"
|
||||
value={confirmPassword}
|
||||
handleChange={handleConfirmPasswordChange}
|
||||
errorMessage={formErrors.confirmPassword}
|
||||
showRequirements={false}
|
||||
floatingLabel={intl.formatMessage(messages['confirm.password.label'])}
|
||||
/>
|
||||
<StatefulButton
|
||||
type="submit"
|
||||
className="btn-primary"
|
||||
variant="brand"
|
||||
state={props.status}
|
||||
labels={{
|
||||
default: intl.formatMessage(messages['reset.password.page.submit.button']),
|
||||
default: intl.formatMessage(messages['reset.password']),
|
||||
}}
|
||||
icons={{ pending: <FontAwesomeIcon icon={faSpinner} spin /> }}
|
||||
onClick={e => handleSubmit(e)}
|
||||
@@ -189,7 +162,7 @@ const ResetPasswordPage = (props) => {
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
@@ -199,7 +172,7 @@ ResetPasswordPage.defaultProps = {
|
||||
status: null,
|
||||
token: null,
|
||||
match: null,
|
||||
errors: null,
|
||||
errorMsg: null,
|
||||
};
|
||||
|
||||
ResetPasswordPage.propTypes = {
|
||||
@@ -213,7 +186,7 @@ ResetPasswordPage.propTypes = {
|
||||
}),
|
||||
}),
|
||||
status: PropTypes.string,
|
||||
errors: PropTypes.string,
|
||||
errorMsg: PropTypes.string,
|
||||
};
|
||||
|
||||
export default connect(
|
||||
|
||||
25
src/reset-password/ResetPasswordSuccess.jsx
Normal file
25
src/reset-password/ResetPasswordSuccess.jsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import React from 'react';
|
||||
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Alert } from '@edx/paragon';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
const ResetPasswordSuccess = (props) => {
|
||||
const { intl } = props;
|
||||
|
||||
return (
|
||||
<Alert id="reset-password-success" variant="success" className="mb-4">
|
||||
<Alert.Heading>
|
||||
{intl.formatMessage(messages['reset.password.success.heading'])}
|
||||
</Alert.Heading>
|
||||
<p>{intl.formatMessage(messages['reset.password.success'])}</p>
|
||||
</Alert>
|
||||
);
|
||||
};
|
||||
|
||||
ResetPasswordSuccess.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(ResetPasswordSuccess);
|
||||
@@ -1,55 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Helmet } from 'react-helmet';
|
||||
|
||||
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { Alert } from '@edx/paragon';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
const ResetSuccessMessage = (props) => {
|
||||
const { intl } = props;
|
||||
|
||||
const loginPasswordLink = (
|
||||
<Alert.Link href="/login" className="font-weight-normal text-info">
|
||||
<FormattedMessage
|
||||
id="reset.password.confirmation.support.link"
|
||||
defaultMessage="Sign in to your account."
|
||||
description="link text used in message: reset.password.invalid.token.description.message link 'sign-in.'"
|
||||
/>
|
||||
</Alert.Link>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>{intl.formatMessage(messages['reset.password.page.title'],
|
||||
{ siteName: getConfig().SITE_NAME })}
|
||||
</title>
|
||||
</Helmet>
|
||||
<div className="d-flex justify-content-center m-4">
|
||||
<div className="d-flex flex-column">
|
||||
<div className="text-left mw-500">
|
||||
<Alert variant="success">
|
||||
<Alert.Heading>
|
||||
{intl.formatMessage(messages['reset.password.request.success.header.message'])}
|
||||
</Alert.Heading>
|
||||
<FormattedMessage
|
||||
id="reset.password.request.success.header.description.message"
|
||||
defaultMessage="Your password has been reset. {loginPasswordLink}"
|
||||
description="message when reset password is successful."
|
||||
values={{ loginPasswordLink }}
|
||||
/>
|
||||
</Alert>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
ResetSuccessMessage.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(ResetSuccessMessage);
|
||||
@@ -1,12 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Spinner as ParagonSpinner } from '@edx/paragon';
|
||||
|
||||
const Spinner = () => (
|
||||
<div className="container position-absolute h-90">
|
||||
<div className="d-flex justify-content-center align-items-center h-90">
|
||||
<ParagonSpinner animation="border" variant="primary" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default Spinner;
|
||||
@@ -2,6 +2,12 @@ import { AsyncActionType } from '../../data/utils';
|
||||
|
||||
export const RESET_PASSWORD = new AsyncActionType('RESET', 'PASSWORD');
|
||||
export const VALIDATE_TOKEN = new AsyncActionType('VALIDATE', 'TOKEN');
|
||||
export const PASSWORD_RESET_FAILURE = 'PASSWORD_RESET_FAILURE';
|
||||
|
||||
export const passwordResetFailure = (errorCode) => ({
|
||||
type: PASSWORD_RESET_FAILURE,
|
||||
payload: { errorCode },
|
||||
});
|
||||
|
||||
// Validate confirmation token
|
||||
export const validateToken = (token) => ({
|
||||
@@ -18,9 +24,9 @@ export const validateTokenSuccess = (tokenStatus, token) => ({
|
||||
payload: { tokenStatus, token },
|
||||
});
|
||||
|
||||
export const validateTokenFailure = errors => ({
|
||||
export const validateTokenFailure = errorCode => ({
|
||||
type: VALIDATE_TOKEN.FAILURE,
|
||||
payload: { errors },
|
||||
payload: { errorCode },
|
||||
});
|
||||
|
||||
// Reset Password
|
||||
@@ -38,7 +44,7 @@ export const resetPasswordSuccess = data => ({
|
||||
payload: { data },
|
||||
});
|
||||
|
||||
export const resetPasswordFailure = errors => ({
|
||||
export const resetPasswordFailure = (errorCode, errorMsg = null) => ({
|
||||
type: RESET_PASSWORD.FAILURE,
|
||||
payload: { errors },
|
||||
payload: { errorCode, errorMsg: errorMsg || errorCode },
|
||||
});
|
||||
|
||||
15
src/reset-password/data/constants.js
Normal file
15
src/reset-password/data/constants.js
Normal file
@@ -0,0 +1,15 @@
|
||||
export const TOKEN_STATE = {
|
||||
PENDING: 'token-pending',
|
||||
VALID: 'token-valid',
|
||||
};
|
||||
|
||||
// password reset error codes
|
||||
export const FORM_SUBMISSION_ERROR = 'form-submission-error';
|
||||
export const PASSWORD_RESET_ERROR = 'password-reset-error';
|
||||
export const PASSWORD_VALIDATION_ERROR = 'password-validation-failure';
|
||||
|
||||
export const PASSWORD_RESET = {
|
||||
INVALID_TOKEN: 'invalid-token',
|
||||
INTERNAL_SERVER_ERROR: 'password-reset-internal-server-error',
|
||||
FORBIDDEN_REQUEST: 'password-reset-rate-limit-error',
|
||||
};
|
||||
@@ -1,9 +1,10 @@
|
||||
import { RESET_PASSWORD, VALIDATE_TOKEN } from './actions';
|
||||
import { RESET_PASSWORD, VALIDATE_TOKEN, PASSWORD_RESET_FAILURE } from './actions';
|
||||
import { PASSWORD_RESET_ERROR, TOKEN_STATE } from './constants';
|
||||
|
||||
export const defaultState = {
|
||||
status: 'token-pending',
|
||||
status: TOKEN_STATE.PENDING,
|
||||
token: null,
|
||||
errors: null,
|
||||
errorMsg: null,
|
||||
};
|
||||
|
||||
const reducer = (state = defaultState, action = null) => {
|
||||
@@ -11,14 +12,13 @@ const reducer = (state = defaultState, action = null) => {
|
||||
case VALIDATE_TOKEN.SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
status: 'valid',
|
||||
status: TOKEN_STATE.VALID,
|
||||
token: action.payload.token,
|
||||
};
|
||||
case VALIDATE_TOKEN.FAILURE:
|
||||
case PASSWORD_RESET_FAILURE:
|
||||
return {
|
||||
...state,
|
||||
status: 'invalid',
|
||||
errors: action.payload.errors,
|
||||
status: PASSWORD_RESET_ERROR,
|
||||
};
|
||||
case RESET_PASSWORD.BEGIN:
|
||||
return {
|
||||
@@ -33,8 +33,8 @@ const reducer = (state = defaultState, action = null) => {
|
||||
case RESET_PASSWORD.FAILURE:
|
||||
return {
|
||||
...state,
|
||||
status: 'failure',
|
||||
errors: action.payload.errors,
|
||||
status: action.payload.errorCode,
|
||||
errorMsg: action.payload.errorMsg,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
|
||||
@@ -6,15 +6,15 @@ import {
|
||||
VALIDATE_TOKEN,
|
||||
validateTokenBegin,
|
||||
validateTokenSuccess,
|
||||
validateTokenFailure,
|
||||
RESET_PASSWORD,
|
||||
resetPasswordBegin,
|
||||
resetPasswordSuccess,
|
||||
resetPasswordFailure,
|
||||
passwordResetFailure,
|
||||
} from './actions';
|
||||
|
||||
import { validateToken, resetPassword } from './service';
|
||||
import { INTERNAL_SERVER_ERROR, API_RATELIMIT_ERROR } from '../../data/constants';
|
||||
import { PASSWORD_RESET, PASSWORD_VALIDATION_ERROR } from './constants';
|
||||
|
||||
// Services
|
||||
export function* handleValidateToken(action) {
|
||||
@@ -25,15 +25,14 @@ export function* handleValidateToken(action) {
|
||||
if (isValid) {
|
||||
yield put(validateTokenSuccess(isValid, action.payload.token));
|
||||
} else {
|
||||
yield put(validateTokenFailure(isValid));
|
||||
yield put(passwordResetFailure(PASSWORD_RESET.INVALID_TOKEN));
|
||||
}
|
||||
} catch (err) {
|
||||
const statusCodes = [429];
|
||||
if (err.response && statusCodes.includes(err.response.status)) {
|
||||
yield put(validateTokenFailure(API_RATELIMIT_ERROR));
|
||||
if (err.response && err.response.status === 429) {
|
||||
yield put(passwordResetFailure(PASSWORD_RESET.FORBIDDEN_REQUEST));
|
||||
logInfo(err);
|
||||
} else {
|
||||
yield put(validateTokenFailure(INTERNAL_SERVER_ERROR));
|
||||
yield put(passwordResetFailure(PASSWORD_RESET.INTERNAL_SERVER_ERROR));
|
||||
logError(err);
|
||||
}
|
||||
}
|
||||
@@ -49,15 +48,14 @@ export function* handleResetPassword(action) {
|
||||
if (resetStatus) {
|
||||
yield put(resetPasswordSuccess(resetStatus));
|
||||
} else {
|
||||
yield put(resetPasswordFailure(resetErrors));
|
||||
yield put(resetPasswordFailure(PASSWORD_VALIDATION_ERROR, resetErrors));
|
||||
}
|
||||
} catch (err) {
|
||||
const statusCodes = [429];
|
||||
if (err.response && statusCodes.includes(err.response.status)) {
|
||||
yield put(resetPasswordFailure(API_RATELIMIT_ERROR));
|
||||
if (err.response && err.response.status === 429) {
|
||||
yield put(resetPasswordFailure(PASSWORD_RESET.FORBIDDEN_REQUEST));
|
||||
logInfo(err);
|
||||
} else {
|
||||
yield put(resetPasswordFailure(INTERNAL_SERVER_ERROR));
|
||||
yield put(resetPasswordFailure(PASSWORD_RESET.INTERNAL_SERVER_ERROR));
|
||||
logError(err);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,14 +3,14 @@ import { runSaga } from 'redux-saga';
|
||||
import {
|
||||
resetPasswordBegin,
|
||||
resetPasswordSuccess,
|
||||
resetPasswordFailure,
|
||||
validateTokenBegin,
|
||||
validateTokenFailure,
|
||||
passwordResetFailure, resetPasswordFailure,
|
||||
} from '../actions';
|
||||
import { PASSWORD_RESET } from '../constants';
|
||||
import { handleResetPassword, handleValidateToken } from '../sagas';
|
||||
import * as api from '../service';
|
||||
|
||||
import initializeMockLogging from '../../../setupTest';
|
||||
import { API_RATELIMIT_ERROR, INTERNAL_SERVER_ERROR } from '../../../data/constants';
|
||||
|
||||
const { loggingService } = initializeMockLogging();
|
||||
|
||||
@@ -57,7 +57,7 @@ describe('handleResetPassword', () => {
|
||||
response: {
|
||||
status: 500,
|
||||
data: {
|
||||
errorCode: INTERNAL_SERVER_ERROR,
|
||||
errorCode: PASSWORD_RESET.INTERNAL_SERVER_ERROR,
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -73,7 +73,7 @@ describe('handleResetPassword', () => {
|
||||
|
||||
expect(loggingService.logError).toHaveBeenCalled();
|
||||
expect(resetPassword).toHaveBeenCalledTimes(1);
|
||||
expect(dispatched).toEqual([resetPasswordBegin(), resetPasswordFailure(INTERNAL_SERVER_ERROR)]);
|
||||
expect(dispatched).toEqual([resetPasswordBegin(), resetPasswordFailure(PASSWORD_RESET.INTERNAL_SERVER_ERROR)]);
|
||||
resetPassword.mockClear();
|
||||
});
|
||||
|
||||
@@ -82,7 +82,7 @@ describe('handleResetPassword', () => {
|
||||
response: {
|
||||
status: 429,
|
||||
data: {
|
||||
errorCode: API_RATELIMIT_ERROR,
|
||||
errorCode: PASSWORD_RESET.FORBIDDEN_REQUEST,
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -98,7 +98,7 @@ describe('handleResetPassword', () => {
|
||||
|
||||
expect(loggingService.logInfo).toHaveBeenCalled();
|
||||
expect(resetPassword).toHaveBeenCalledTimes(1);
|
||||
expect(dispatched).toEqual([resetPasswordBegin(), resetPasswordFailure(API_RATELIMIT_ERROR)]);
|
||||
expect(dispatched).toEqual([resetPasswordBegin(), resetPasswordFailure(PASSWORD_RESET.FORBIDDEN_REQUEST)]);
|
||||
resetPassword.mockClear();
|
||||
});
|
||||
});
|
||||
@@ -121,7 +121,7 @@ describe('handleValidateToken', () => {
|
||||
response: {
|
||||
status: 500,
|
||||
data: {
|
||||
errorCode: INTERNAL_SERVER_ERROR,
|
||||
errorCode: PASSWORD_RESET.INTERNAL_SERVER_ERROR,
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -136,16 +136,16 @@ describe('handleValidateToken', () => {
|
||||
);
|
||||
|
||||
expect(validateToken).toHaveBeenCalledTimes(1);
|
||||
expect(dispatched).toEqual([validateTokenBegin(), validateTokenFailure(INTERNAL_SERVER_ERROR)]);
|
||||
expect(dispatched).toEqual([validateTokenBegin(), passwordResetFailure(PASSWORD_RESET.INTERNAL_SERVER_ERROR)]);
|
||||
validateToken.mockClear();
|
||||
});
|
||||
|
||||
it('should call service and dispatch ratelimit error', async () => {
|
||||
it('should call service and dispatch rate limit error', async () => {
|
||||
const errorResponse = {
|
||||
response: {
|
||||
status: 429,
|
||||
data: {
|
||||
errorCode: API_RATELIMIT_ERROR,
|
||||
errorCode: PASSWORD_RESET.FORBIDDEN_REQUEST,
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -161,7 +161,7 @@ describe('handleValidateToken', () => {
|
||||
|
||||
expect(loggingService.logInfo).toHaveBeenCalled();
|
||||
expect(validateToken).toHaveBeenCalledTimes(1);
|
||||
expect(dispatched).toEqual([validateTokenBegin(), validateTokenFailure(API_RATELIMIT_ERROR)]);
|
||||
expect(dispatched).toEqual([validateTokenBegin(), passwordResetFailure(PASSWORD_RESET.FORBIDDEN_REQUEST)]);
|
||||
validateToken.mockClear();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,45 +1,51 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
'sign.in': {
|
||||
id: 'sign.in',
|
||||
defaultMessage: 'Sign in',
|
||||
description: 'Sign in toggle text',
|
||||
},
|
||||
'reset.password.page.title': {
|
||||
id: 'reset.password.page.title',
|
||||
defaultMessage: 'Reset Password | {siteName}',
|
||||
description: 'page title',
|
||||
},
|
||||
'reset.password.page.heading': {
|
||||
id: 'reset.password.page.heading',
|
||||
defaultMessage: 'Reset your password',
|
||||
description: 'The page heading for the Reset password page.',
|
||||
'reset.password': {
|
||||
id: 'reset.password',
|
||||
defaultMessage: 'Reset password',
|
||||
description: 'The page heading and button text for reset password page.',
|
||||
},
|
||||
'reset.password.page.instructions': {
|
||||
id: 'reset.password.page.instructions',
|
||||
defaultMessage: 'Enter and confirm your new password.',
|
||||
description: 'Instructions message for reset password page.',
|
||||
},
|
||||
'reset.password.page.invalid.match.message': {
|
||||
id: 'reset.password.page.invalid.match.message',
|
||||
defaultMessage: 'Passwords do not match.',
|
||||
description: 'Password format error.',
|
||||
},
|
||||
'reset.password.page.new.field.label': {
|
||||
id: 'forgot.password.page.new.field.label',
|
||||
'new.password.label': {
|
||||
id: 'new.password.label',
|
||||
defaultMessage: 'New password',
|
||||
description: 'New password field label for the reset password page.',
|
||||
},
|
||||
'reset.password.page.confirm.field.label': {
|
||||
id: 'forgot.password.page.confirm.field.label',
|
||||
'confirm.password.label': {
|
||||
id: 'confirm.password.label',
|
||||
defaultMessage: 'Confirm password',
|
||||
description: 'Confirm password field label for the reset password page.',
|
||||
},
|
||||
'reset.password.page.submit.button': {
|
||||
id: 'reset.password.page.submit.button',
|
||||
defaultMessage: 'Reset my password',
|
||||
description: 'Submit button text for the reset password page.',
|
||||
// validation errors
|
||||
'password.validation.message': {
|
||||
id: 'password.validation.message',
|
||||
defaultMessage: 'Password criteria has not been met',
|
||||
description: 'Error message for empty or invalid password',
|
||||
},
|
||||
'reset.password.request.success.header.message': {
|
||||
id: 'reset.password.request.success.header.message',
|
||||
defaultMessage: 'Password reset complete.',
|
||||
description: 'header message when reset is successful.',
|
||||
'passwords.do.not.match': {
|
||||
id: 'passwords.do.not.match',
|
||||
defaultMessage: 'Passwords do not match',
|
||||
description: 'Password format error.',
|
||||
},
|
||||
'confirm.your.password': {
|
||||
id: 'confirm.your.password',
|
||||
defaultMessage: 'Confirm your password',
|
||||
description: 'Field validation message when confirm password is empty',
|
||||
},
|
||||
'forgot.password.confirmation.sign.in.link': {
|
||||
id: 'forgot.password.confirmation.sign.in.link',
|
||||
@@ -61,10 +67,16 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Please enter your new password.',
|
||||
description: 'Error message that appears when user tries to submit form with empty New Password field',
|
||||
},
|
||||
'forgot.password.empty.new.password.error.heading': {
|
||||
id: 'forgot.password.empty.new.password.error.heading',
|
||||
// alert banner strings
|
||||
'reset.password.failure.heading': {
|
||||
id: 'reset.password.failure.heading',
|
||||
defaultMessage: 'We couldn\'t reset your password.',
|
||||
description: 'Heading that appears above error message when user submits empty form.',
|
||||
description: 'Heading for reset password request failure',
|
||||
},
|
||||
'reset.password.form.submission.error': {
|
||||
id: 'reset.password.form.submission.error',
|
||||
defaultMessage: 'Please check your responses and try again.',
|
||||
description: 'Error message for reset password page',
|
||||
},
|
||||
'reset.password.request.server.error': {
|
||||
id: 'reset.password.request.server.error',
|
||||
@@ -76,11 +88,31 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Token validation failure',
|
||||
description: 'Failed to validate reset password token error message.',
|
||||
},
|
||||
'reset.server.ratelimit.error': {
|
||||
id: 'reset.server.ratelimit.error',
|
||||
'reset.server.rate.limit.error': {
|
||||
id: 'reset.server.rate.limit.error',
|
||||
defaultMessage: 'Too many requests.',
|
||||
description: 'Too many request at server end point',
|
||||
},
|
||||
'reset.password.success.heading': {
|
||||
id: 'reset.password.success.heading',
|
||||
defaultMessage: 'Password reset complete.',
|
||||
description: 'Heading for alert box when reset password is successful',
|
||||
},
|
||||
'reset.password.success': {
|
||||
id: 'reset.password.success',
|
||||
defaultMessage: 'Your password has been reset. Sign in to your account.',
|
||||
description: 'Reset password success message',
|
||||
},
|
||||
'internal.server.error': {
|
||||
id: 'internal.server.error',
|
||||
defaultMessage: 'An error has occurred. Try refreshing the page, or check your internet connection.',
|
||||
description: 'Error message that appears when server responds with 500 error code',
|
||||
},
|
||||
'rate.limit.error': {
|
||||
id: 'rate.limit.error',
|
||||
defaultMessage: 'An error has occurred because of too many requests. Please try again after some time.',
|
||||
description: 'Error message that appears when server responds with 429 error code',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import renderer from 'react-test-renderer';
|
||||
import configureStore from 'redux-mock-store';
|
||||
import { mount } from 'enzyme';
|
||||
import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import CookiePolicyBanner from '@edx/frontend-component-cookie-policy-banner';
|
||||
import * as auth from '@edx/frontend-platform/auth';
|
||||
import { resetPassword } from '../data/actions';
|
||||
import { APIFailureMessage } from '../../common-components';
|
||||
|
||||
import { mount } from 'enzyme';
|
||||
import configureStore from 'redux-mock-store';
|
||||
import { Provider } from 'react-redux';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
|
||||
import * as auth from '@edx/frontend-platform/auth';
|
||||
import CookiePolicyBanner from '@edx/frontend-component-cookie-policy-banner';
|
||||
import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { resetPassword } from '../data/actions';
|
||||
import { PASSWORD_RESET, TOKEN_STATE } from '../data/constants';
|
||||
import ResetPasswordPage from '../ResetPasswordPage';
|
||||
import { API_RATELIMIT_ERROR, INTERNAL_SERVER_ERROR } from '../../data/constants';
|
||||
|
||||
jest.mock('@edx/frontend-platform/auth');
|
||||
|
||||
@@ -22,22 +22,19 @@ describe('ResetPasswordPage', () => {
|
||||
let props = {};
|
||||
let store = {};
|
||||
|
||||
const emptyFieldError = 'Please enter your new password.';
|
||||
const validationMessage = 'This password is too short. It must contain at least 8 characters. This password must contain at least 1 number.';
|
||||
|
||||
const reduxWrapper = children => (
|
||||
<IntlProvider locale="en">
|
||||
<Provider store={store}>{children}</Provider>
|
||||
<MemoryRouter>
|
||||
<Provider store={store}>{children}</Provider>
|
||||
</MemoryRouter>
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
const submitForm = async (password) => {
|
||||
const submitForm = (password) => {
|
||||
const resetPasswordPage = mount(reduxWrapper(<IntlResetPasswordPage {...props} />));
|
||||
await act(async () => {
|
||||
resetPasswordPage.find('input#reset-password-input').simulate('blur', { target: { value: password } });
|
||||
});
|
||||
resetPasswordPage.find('input#confirm-password-input').simulate('change', { target: { value: password } });
|
||||
resetPasswordPage.find('button.btn-primary').simulate('click');
|
||||
resetPasswordPage.find('input#newPassword').simulate('change', { target: { value: password, name: 'newPassword' } });
|
||||
resetPasswordPage.find('input#confirmPassword').simulate('change', { target: { value: password, name: 'confirmPassword' } });
|
||||
resetPasswordPage.find('button.btn-brand').simulate('click');
|
||||
|
||||
return resetPasswordPage;
|
||||
};
|
||||
@@ -47,7 +44,6 @@ describe('ResetPasswordPage', () => {
|
||||
props = {
|
||||
resetPassword: jest.fn(),
|
||||
status: null,
|
||||
token_status: 'pending',
|
||||
token: null,
|
||||
errors: null,
|
||||
match: {
|
||||
@@ -60,120 +56,16 @@ describe('ResetPasswordPage', () => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
// ******** form submission tests ********
|
||||
|
||||
it('show spinner component during token validation', () => {
|
||||
props = {
|
||||
...props,
|
||||
token_status: 'pending',
|
||||
match: {
|
||||
params: {
|
||||
token: 'test-token',
|
||||
},
|
||||
it('with valid inputs resetPassword action is dispatched', async () => {
|
||||
const password = 'test-password-1';
|
||||
|
||||
store = mockStore({
|
||||
resetPassword: {
|
||||
status: TOKEN_STATE.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 pending reset message section snapshot', () => {
|
||||
props = {
|
||||
...props,
|
||||
token_status: 'valid',
|
||||
status: 'pending',
|
||||
};
|
||||
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 display invalid password message', async () => {
|
||||
props = {
|
||||
...props,
|
||||
token_status: 'valid',
|
||||
};
|
||||
|
||||
auth.getHttpClient = jest.fn(() => ({
|
||||
post: async () => ({
|
||||
data: {
|
||||
validation_decisions: {
|
||||
password: validationMessage,
|
||||
},
|
||||
},
|
||||
catch: () => {},
|
||||
}),
|
||||
}));
|
||||
|
||||
const resetPasswordPage = mount(reduxWrapper(<IntlResetPasswordPage {...props} />));
|
||||
|
||||
// Focus out of empty field
|
||||
await act(async () => {
|
||||
await resetPasswordPage.find('input#reset-password-input').simulate('blur');
|
||||
});
|
||||
resetPasswordPage.update();
|
||||
expect(resetPasswordPage.find('#reset-password-input-invalid-feedback').text()).toEqual(emptyFieldError);
|
||||
|
||||
// Enter non-compliant password
|
||||
await act(async () => {
|
||||
await resetPasswordPage.find('input#reset-password-input').simulate('blur', { target: { value: 'invalid' } });
|
||||
});
|
||||
expect(resetPasswordPage.find('#reset-password-input-invalid-feedback').text()).toEqual(validationMessage);
|
||||
});
|
||||
|
||||
it('should display error message on empty form submission', () => {
|
||||
const bannerMessage = 'We couldn\'t reset your password.'.concat(emptyFieldError);
|
||||
props = {
|
||||
...props,
|
||||
token_status: 'valid',
|
||||
token: 'token',
|
||||
};
|
||||
|
||||
const resetPasswordPage = mount(reduxWrapper(<IntlResetPasswordPage {...props} />));
|
||||
resetPasswordPage.find('button.btn-primary').simulate('click');
|
||||
|
||||
resetPasswordPage.update();
|
||||
expect(resetPasswordPage.find('#reset-password-input-invalid-feedback').text()).toEqual(emptyFieldError);
|
||||
expect(resetPasswordPage.find('#validation-errors').first().text()).toEqual(bannerMessage);
|
||||
});
|
||||
|
||||
it('with valid inputs resetPassword action is dispatch', async () => {
|
||||
const newPassword = 'test-password1';
|
||||
props = {
|
||||
...props,
|
||||
token_status: 'valid',
|
||||
token: 'token',
|
||||
};
|
||||
|
||||
auth.getHttpClient = jest.fn(() => ({
|
||||
post: async () => ({
|
||||
@@ -182,110 +74,83 @@ describe('ResetPasswordPage', () => {
|
||||
}),
|
||||
}));
|
||||
|
||||
const formPayload = {
|
||||
new_password1: newPassword,
|
||||
new_password2: newPassword,
|
||||
};
|
||||
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
const resetPasswordPage = submitForm(password);
|
||||
|
||||
const resetPasswordPage = await submitForm(newPassword);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(resetPassword(formPayload, props.token, {}));
|
||||
expect(store.dispatch).toHaveBeenCalledWith(resetPassword(
|
||||
{ new_password1: password, new_password2: password }, props.token, {},
|
||||
));
|
||||
resetPasswordPage.unmount();
|
||||
});
|
||||
|
||||
it('should dispatch resetPassword action if validations have reached rate limit', async () => {
|
||||
const password = 'test-password';
|
||||
// ******** test reset password field validations ********
|
||||
|
||||
auth.getHttpClient = jest.fn(() => ({
|
||||
post: async () => {
|
||||
throw new Error('error');
|
||||
it('should show error messages for required fields on empty form submission', () => {
|
||||
store = mockStore({
|
||||
resetPassword: {
|
||||
status: TOKEN_STATE.VALID,
|
||||
},
|
||||
}));
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
});
|
||||
const resetPasswordPage = mount(reduxWrapper(<IntlResetPasswordPage {...props} />));
|
||||
resetPasswordPage.find('button.btn-brand').simulate('click');
|
||||
|
||||
props = {
|
||||
...props,
|
||||
token_status: 'valid',
|
||||
token: 'token',
|
||||
};
|
||||
|
||||
const resetPasswordPage = await submitForm(password);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(
|
||||
resetPassword({ new_password1: password, new_password2: password }, props.token, {}),
|
||||
expect(resetPasswordPage.find('#validation-errors').first().text()).toEqual(
|
||||
'We couldn\'t reset your password.Please check your responses and try again.',
|
||||
);
|
||||
|
||||
resetPasswordPage.unmount();
|
||||
expect(resetPasswordPage.find('div[feedback-for="newPassword"]').text()).toEqual('Password criteria has not been met');
|
||||
expect(resetPasswordPage.find('div[feedback-for="confirmPassword"]').text()).toEqual('Confirm your password');
|
||||
});
|
||||
|
||||
it('should not update the banner message on focus out', async () => {
|
||||
const bannerMessage = 'We couldn\'t reset your password.'.concat(validationMessage);
|
||||
props = {
|
||||
...props,
|
||||
token_status: 'valid',
|
||||
token: 'token',
|
||||
errors: validationMessage,
|
||||
status: 'failure',
|
||||
};
|
||||
it('should show error message when new and confirm password do not match', () => {
|
||||
store = mockStore({
|
||||
resetPassword: {
|
||||
status: TOKEN_STATE.VALID,
|
||||
},
|
||||
});
|
||||
const resetPasswordPage = mount(reduxWrapper(<IntlResetPasswordPage {...props} />));
|
||||
resetPasswordPage.find('input#confirmPassword').simulate(
|
||||
'change', { target: { value: 'password-mismatch', name: 'confirmPassword' } },
|
||||
);
|
||||
expect(resetPasswordPage.find('div[feedback-for="confirmPassword"]').text()).toEqual('Passwords do not match');
|
||||
});
|
||||
|
||||
// ******** alert message tests ********
|
||||
|
||||
it('should show reset password rate limit error', () => {
|
||||
store = mockStore({
|
||||
resetPassword: {
|
||||
status: PASSWORD_RESET.FORBIDDEN_REQUEST,
|
||||
},
|
||||
});
|
||||
|
||||
const resetPasswordPage = mount(reduxWrapper(<IntlResetPasswordPage {...props} />));
|
||||
expect(resetPasswordPage.find('#validation-errors').first().text()).toEqual(bannerMessage);
|
||||
|
||||
await act(async () => {
|
||||
await resetPasswordPage.find('input#reset-password-input').simulate('blur', { target: { value: '' } });
|
||||
});
|
||||
// On blur event, the banner message remains same
|
||||
expect(resetPasswordPage.find('#reset-password-input-invalid-feedback').text()).toEqual(emptyFieldError);
|
||||
expect(resetPasswordPage.find('#validation-errors').first().text()).toEqual(bannerMessage);
|
||||
expect(resetPasswordPage.find('#validation-errors').first().text()).toEqual(
|
||||
'Too many requests.An error has occurred because of too many requests. Please try again after some time.',
|
||||
);
|
||||
});
|
||||
|
||||
it('should show reset password internal server error', () => {
|
||||
store = mockStore({
|
||||
resetPassword: {
|
||||
status: PASSWORD_RESET.INTERNAL_SERVER_ERROR,
|
||||
},
|
||||
});
|
||||
|
||||
const resetPasswordPage = mount(reduxWrapper(<IntlResetPasswordPage {...props} />));
|
||||
expect(resetPasswordPage.find('#validation-errors').first().text()).toEqual(
|
||||
'We couldn\'t reset your password.An error has occurred. Try refreshing the page, or check your internet connection.',
|
||||
);
|
||||
});
|
||||
|
||||
// ******** miscellaneous tests ********
|
||||
|
||||
it('check cookie rendered', () => {
|
||||
const resetPasswordPage = mount(reduxWrapper(<IntlResetPasswordPage {...props} />));
|
||||
expect(resetPasswordPage.find(<CookiePolicyBanner />)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should display error banner on server error', () => {
|
||||
const bannerMessage = 'Failed to reset passwordAn error has occurred. Try refreshing the page, or check your internet connection.';
|
||||
props = {
|
||||
...props,
|
||||
status: 'failure',
|
||||
errors: INTERNAL_SERVER_ERROR,
|
||||
};
|
||||
|
||||
it('show spinner during token validation', () => {
|
||||
const resetPasswordPage = mount(reduxWrapper(<IntlResetPasswordPage {...props} />));
|
||||
resetPasswordPage.find('button.btn-primary').simulate('click');
|
||||
|
||||
resetPasswordPage.update();
|
||||
expect(resetPasswordPage.find('#internal-server-error').first().text()).toEqual(bannerMessage);
|
||||
});
|
||||
|
||||
it('check api failure banner rendered', () => {
|
||||
props = {
|
||||
...props,
|
||||
status: 'invalid',
|
||||
errors: INTERNAL_SERVER_ERROR,
|
||||
};
|
||||
const resetPasswordPage = mount(reduxWrapper(<IntlResetPasswordPage {...props} />));
|
||||
expect(resetPasswordPage.find(<APIFailureMessage />)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('check failure banner rendered on validate token api ratelimit', () => {
|
||||
props = {
|
||||
...props,
|
||||
status: 'invalid',
|
||||
errors: API_RATELIMIT_ERROR,
|
||||
};
|
||||
const resetPasswordPage = mount(reduxWrapper(<IntlResetPasswordPage {...props} />));
|
||||
expect(resetPasswordPage.find(<APIFailureMessage />)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('check failure banner rendered on reset password api ratelimit', () => {
|
||||
props = {
|
||||
...props,
|
||||
status: 'failure',
|
||||
errors: API_RATELIMIT_ERROR,
|
||||
};
|
||||
const resetPasswordPage = mount(reduxWrapper(<IntlResetPasswordPage {...props} />));
|
||||
expect(resetPasswordPage.find(<APIFailureMessage />)).toBeTruthy();
|
||||
expect(resetPasswordPage.find('div.spinner-header')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,464 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ResetPasswordPage should match invalid token message section snapshot 1`] = `
|
||||
<div
|
||||
className="d-flex justify-content-center m-4"
|
||||
id="main"
|
||||
>
|
||||
<div
|
||||
className="d-flex flex-column mw-500"
|
||||
>
|
||||
<form
|
||||
className=""
|
||||
>
|
||||
<h1
|
||||
className="mt-3 h3"
|
||||
>
|
||||
Reset your password
|
||||
</h1>
|
||||
<p
|
||||
className="mb-4"
|
||||
>
|
||||
Enter and confirm your new password.
|
||||
</p>
|
||||
<div
|
||||
className="form-group w-100"
|
||||
>
|
||||
<label
|
||||
className="pgn__form-label pt-10 focus-out"
|
||||
htmlFor="reset-password-input"
|
||||
>
|
||||
New password
|
||||
</label>
|
||||
<input
|
||||
aria-describedby=""
|
||||
aria-invalid={false}
|
||||
autoComplete="on"
|
||||
className="form-control border-gray-600"
|
||||
id="reset-password-input"
|
||||
name="new-password1"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
required={true}
|
||||
type="password"
|
||||
value=""
|
||||
/>
|
||||
<span />
|
||||
</div>
|
||||
<div
|
||||
className="form-group w-100"
|
||||
>
|
||||
<label
|
||||
className="pgn__form-label pt-10 focus-out"
|
||||
htmlFor="confirm-password-input"
|
||||
>
|
||||
Confirm password
|
||||
</label>
|
||||
<input
|
||||
aria-describedby=""
|
||||
aria-invalid={false}
|
||||
autoComplete="on"
|
||||
className="form-control border-gray-600"
|
||||
id="confirm-password-input"
|
||||
name="new-password2"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
required={true}
|
||||
type="password"
|
||||
value=""
|
||||
/>
|
||||
<span />
|
||||
<strong
|
||||
className="invalid-feedback"
|
||||
id="confirm-password-input-invalid-feedback"
|
||||
>
|
||||
Passwords do not match.
|
||||
</strong>
|
||||
</div>
|
||||
<button
|
||||
aria-disabled={false}
|
||||
aria-live="assertive"
|
||||
className="pgn__stateful-btn pgn__stateful-btn-state-null btn-primary btn btn-primary"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
type="submit"
|
||||
>
|
||||
<span
|
||||
className="d-flex align-items-center justify-content-center"
|
||||
>
|
||||
<span>
|
||||
Reset my password
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`ResetPasswordPage should match pending reset message section snapshot 1`] = `
|
||||
<div
|
||||
className="d-flex justify-content-center m-4"
|
||||
id="main"
|
||||
>
|
||||
<div
|
||||
className="d-flex flex-column mw-500"
|
||||
>
|
||||
<form
|
||||
className=""
|
||||
>
|
||||
<h1
|
||||
className="mt-3 h3"
|
||||
>
|
||||
Reset your password
|
||||
</h1>
|
||||
<p
|
||||
className="mb-4"
|
||||
>
|
||||
Enter and confirm your new password.
|
||||
</p>
|
||||
<div
|
||||
className="form-group w-100"
|
||||
>
|
||||
<label
|
||||
className="pgn__form-label pt-10 focus-out"
|
||||
htmlFor="reset-password-input"
|
||||
>
|
||||
New password
|
||||
</label>
|
||||
<input
|
||||
aria-describedby=""
|
||||
aria-invalid={false}
|
||||
autoComplete="on"
|
||||
className="form-control border-gray-600"
|
||||
id="reset-password-input"
|
||||
name="new-password1"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
required={true}
|
||||
type="password"
|
||||
value=""
|
||||
/>
|
||||
<span />
|
||||
</div>
|
||||
<div
|
||||
className="form-group w-100"
|
||||
>
|
||||
<label
|
||||
className="pgn__form-label pt-10 focus-out"
|
||||
htmlFor="confirm-password-input"
|
||||
>
|
||||
Confirm password
|
||||
</label>
|
||||
<input
|
||||
aria-describedby=""
|
||||
aria-invalid={false}
|
||||
autoComplete="on"
|
||||
className="form-control border-gray-600"
|
||||
id="confirm-password-input"
|
||||
name="new-password2"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
required={true}
|
||||
type="password"
|
||||
value=""
|
||||
/>
|
||||
<span />
|
||||
<strong
|
||||
className="invalid-feedback"
|
||||
id="confirm-password-input-invalid-feedback"
|
||||
>
|
||||
Passwords do not match.
|
||||
</strong>
|
||||
</div>
|
||||
<button
|
||||
aria-disabled={true}
|
||||
aria-live="assertive"
|
||||
className="pgn__stateful-btn pgn__stateful-btn-state-pending btn-primary disabled btn btn-primary"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
type="submit"
|
||||
>
|
||||
<span
|
||||
className="d-flex align-items-center justify-content-center"
|
||||
>
|
||||
<span
|
||||
className="pgn__stateful-btn-icon"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-spinner fa-w-16 fa-spin "
|
||||
data-icon="spinner"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M304 48c0 26.51-21.49 48-48 48s-48-21.49-48-48 21.49-48 48-48 48 21.49 48 48zm-48 368c-26.51 0-48 21.49-48 48s21.49 48 48 48 48-21.49 48-48-21.49-48-48-48zm208-208c-26.51 0-48 21.49-48 48s21.49 48 48 48 48-21.49 48-48-21.49-48-48-48zM96 256c0-26.51-21.49-48-48-48S0 229.49 0 256s21.49 48 48 48 48-21.49 48-48zm12.922 99.078c-26.51 0-48 21.49-48 48s21.49 48 48 48 48-21.49 48-48c0-26.509-21.491-48-48-48zm294.156 0c-26.51 0-48 21.49-48 48s21.49 48 48 48 48-21.49 48-48c0-26.509-21.49-48-48-48zM108.922 60.922c-26.51 0-48 21.49-48 48s21.49 48 48 48 48-21.49 48-48-21.491-48-48-48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<span>
|
||||
Reset my password
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`ResetPasswordPage should match reset password default section snapshot 1`] = `
|
||||
<div
|
||||
className="d-flex justify-content-center m-4"
|
||||
id="main"
|
||||
>
|
||||
<div
|
||||
className="d-flex flex-column mw-500"
|
||||
>
|
||||
<form
|
||||
className=""
|
||||
>
|
||||
<h1
|
||||
className="mt-3 h3"
|
||||
>
|
||||
Reset your password
|
||||
</h1>
|
||||
<p
|
||||
className="mb-4"
|
||||
>
|
||||
Enter and confirm your new password.
|
||||
</p>
|
||||
<div
|
||||
className="form-group w-100"
|
||||
>
|
||||
<label
|
||||
className="pgn__form-label pt-10 focus-out"
|
||||
htmlFor="reset-password-input"
|
||||
>
|
||||
New password
|
||||
</label>
|
||||
<input
|
||||
aria-describedby=""
|
||||
aria-invalid={false}
|
||||
autoComplete="on"
|
||||
className="form-control border-gray-600"
|
||||
id="reset-password-input"
|
||||
name="new-password1"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
required={true}
|
||||
type="password"
|
||||
value=""
|
||||
/>
|
||||
<span />
|
||||
</div>
|
||||
<div
|
||||
className="form-group w-100"
|
||||
>
|
||||
<label
|
||||
className="pgn__form-label pt-10 focus-out"
|
||||
htmlFor="confirm-password-input"
|
||||
>
|
||||
Confirm password
|
||||
</label>
|
||||
<input
|
||||
aria-describedby=""
|
||||
aria-invalid={false}
|
||||
autoComplete="on"
|
||||
className="form-control border-gray-600"
|
||||
id="confirm-password-input"
|
||||
name="new-password2"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
required={true}
|
||||
type="password"
|
||||
value=""
|
||||
/>
|
||||
<span />
|
||||
<strong
|
||||
className="invalid-feedback"
|
||||
id="confirm-password-input-invalid-feedback"
|
||||
>
|
||||
Passwords do not match.
|
||||
</strong>
|
||||
</div>
|
||||
<button
|
||||
aria-disabled={false}
|
||||
aria-live="assertive"
|
||||
className="pgn__stateful-btn pgn__stateful-btn-state-null btn-primary btn btn-primary"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
type="submit"
|
||||
>
|
||||
<span
|
||||
className="d-flex align-items-center justify-content-center"
|
||||
>
|
||||
<span>
|
||||
Reset my password
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`ResetPasswordPage should match successful reset message section snapshot 1`] = `
|
||||
<div
|
||||
className="d-flex justify-content-center m-4"
|
||||
>
|
||||
<div
|
||||
className="d-flex flex-column"
|
||||
>
|
||||
<div
|
||||
className="text-left mw-500"
|
||||
>
|
||||
<div
|
||||
className="fade alert alert-success show"
|
||||
role="alert"
|
||||
>
|
||||
<div
|
||||
className="alert-heading h4"
|
||||
>
|
||||
Password reset complete.
|
||||
</div>
|
||||
<span>
|
||||
Your password has been reset.
|
||||
<a
|
||||
className="font-weight-normal text-info alert-link"
|
||||
href="/login"
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
>
|
||||
<span>
|
||||
Sign in to your account.
|
||||
</span>
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`ResetPasswordPage show spinner component during token validation 1`] = `
|
||||
<div
|
||||
className="d-flex justify-content-center m-4"
|
||||
id="main"
|
||||
>
|
||||
<div
|
||||
className="d-flex flex-column mw-500"
|
||||
>
|
||||
<form
|
||||
className=""
|
||||
>
|
||||
<h1
|
||||
className="mt-3 h3"
|
||||
>
|
||||
Reset your password
|
||||
</h1>
|
||||
<p
|
||||
className="mb-4"
|
||||
>
|
||||
Enter and confirm your new password.
|
||||
</p>
|
||||
<div
|
||||
className="form-group w-100"
|
||||
>
|
||||
<label
|
||||
className="pgn__form-label pt-10 focus-out"
|
||||
htmlFor="reset-password-input"
|
||||
>
|
||||
New password
|
||||
</label>
|
||||
<input
|
||||
aria-describedby=""
|
||||
aria-invalid={false}
|
||||
autoComplete="on"
|
||||
className="form-control border-gray-600"
|
||||
id="reset-password-input"
|
||||
name="new-password1"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
required={true}
|
||||
type="password"
|
||||
value=""
|
||||
/>
|
||||
<span />
|
||||
</div>
|
||||
<div
|
||||
className="form-group w-100"
|
||||
>
|
||||
<label
|
||||
className="pgn__form-label pt-10 focus-out"
|
||||
htmlFor="confirm-password-input"
|
||||
>
|
||||
Confirm password
|
||||
</label>
|
||||
<input
|
||||
aria-describedby=""
|
||||
aria-invalid={false}
|
||||
autoComplete="on"
|
||||
className="form-control border-gray-600"
|
||||
id="confirm-password-input"
|
||||
name="new-password2"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
required={true}
|
||||
type="password"
|
||||
value=""
|
||||
/>
|
||||
<span />
|
||||
<strong
|
||||
className="invalid-feedback"
|
||||
id="confirm-password-input-invalid-feedback"
|
||||
>
|
||||
Passwords do not match.
|
||||
</strong>
|
||||
</div>
|
||||
<button
|
||||
aria-disabled={false}
|
||||
aria-live="assertive"
|
||||
className="pgn__stateful-btn pgn__stateful-btn-state-null btn-primary btn btn-primary"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
type="submit"
|
||||
>
|
||||
<span
|
||||
className="d-flex align-items-center justify-content-center"
|
||||
>
|
||||
<span>
|
||||
Reset my password
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
Reference in New Issue
Block a user