Add password compliance workflow (#40)
This commit is contained in:
@@ -1,8 +1,11 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Alert, Hyperlink } from '@edx/paragon';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { NON_COMPLIANT_PASSWORD_EXCEPTION } from './data/constants';
|
||||
import messages from './messages';
|
||||
|
||||
const processLink = (link) => {
|
||||
let matches;
|
||||
@@ -13,6 +16,9 @@ const processLink = (link) => {
|
||||
};
|
||||
|
||||
const LoginFailureMessage = (props) => {
|
||||
const errorMessage = props.errors;
|
||||
const { errorCode, intl } = props;
|
||||
|
||||
const Link = (args) => (
|
||||
<>
|
||||
{args.beforeLink}
|
||||
@@ -23,26 +29,47 @@ const LoginFailureMessage = (props) => {
|
||||
</>
|
||||
);
|
||||
|
||||
const errorMessage = props.errors;
|
||||
let errorList = errorMessage.trim().split('\n');
|
||||
errorList = errorList.map((error) => {
|
||||
let matches;
|
||||
if (error.includes('a href')) {
|
||||
matches = processLink(error);
|
||||
const [beforeLink, link, linkText, afterLink] = matches;
|
||||
return (
|
||||
<li key={error}>
|
||||
<Link // eslint-disable-line jsx-a11y/anchor-is-valid
|
||||
beforeLink={beforeLink}
|
||||
link={link}
|
||||
linkText={linkText}
|
||||
afterLink={afterLink}
|
||||
let errorList;
|
||||
|
||||
switch (errorCode) {
|
||||
case NON_COMPLIANT_PASSWORD_EXCEPTION:
|
||||
errorList = (
|
||||
<li key="password-non-compliance">
|
||||
<FormattedMessage
|
||||
id="login.non.compliant.password.error"
|
||||
defaultMessage="{passwordComplaintRequirements} {lineBreak}Your current password does not meet the new security
|
||||
requirements. We just sent a password-reset message to the email address associated with this account.
|
||||
Thank you for helping us keep your data safe."
|
||||
values={{
|
||||
passwordComplaintRequirements: <strong>{intl.formatMessage(messages['logistration.non.compliant.password.title'])}</strong>,
|
||||
lineBreak: <br />,
|
||||
}}
|
||||
/>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
return <li key={error}>{error}</li>;
|
||||
});
|
||||
break;
|
||||
default:
|
||||
// TODO: use errorCode instead of processing errorMessages on frontend
|
||||
errorList = errorMessage.trim().split('\n');
|
||||
errorList = errorList.map((error) => {
|
||||
let matches;
|
||||
if (error.includes('a href')) {
|
||||
matches = processLink(error);
|
||||
const [beforeLink, link, linkText, afterLink] = matches;
|
||||
return (
|
||||
<li key={error}>
|
||||
<Link // eslint-disable-line jsx-a11y/anchor-is-valid
|
||||
beforeLink={beforeLink}
|
||||
link={link}
|
||||
linkText={linkText}
|
||||
afterLink={afterLink}
|
||||
/>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
return <li key={error}>{error}</li>;
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Alert variant="danger">
|
||||
@@ -62,9 +89,12 @@ const LoginFailureMessage = (props) => {
|
||||
|
||||
LoginFailureMessage.defaultProps = {
|
||||
errors: '',
|
||||
errorCode: null,
|
||||
};
|
||||
LoginFailureMessage.propTypes = {
|
||||
errors: PropTypes.string,
|
||||
errorCode: PropTypes.string,
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default LoginFailureMessage;
|
||||
export default injectIntl(LoginFailureMessage);
|
||||
|
||||
@@ -7,7 +7,7 @@ import { connect } from 'react-redux';
|
||||
|
||||
import ConfirmationAlert from './ConfirmationAlert';
|
||||
import { getThirdPartyAuthContext, loginRequest } from './data/actions';
|
||||
import { loginRequestSelector, thirdPartyAuthContextSelector } from './data/selectors';
|
||||
import { loginErrorSelector, loginRequestSelector, thirdPartyAuthContextSelector } from './data/selectors';
|
||||
import InstitutionLogistration, { RenderInstitutionButton } from './InstitutionLogistration';
|
||||
import LoginHelpLinks from './LoginHelpLinks';
|
||||
import LoginFailureMessage from './LoginFailure';
|
||||
@@ -140,7 +140,9 @@ class LoginPage extends React.Component {
|
||||
platformName={thirdPartyAuthContext.platformName}
|
||||
/>
|
||||
)}
|
||||
{this.props.loginError ? <LoginFailureMessage errors={this.props.loginError} /> : null}
|
||||
{this.props.loginError
|
||||
? <LoginFailureMessage errors={this.props.loginError.value} errorCode={this.props.loginError.errorCode} />
|
||||
: null}
|
||||
{this.props.forgotPassword.status === 'complete' ? <ConfirmationAlert email={this.props.forgotPassword.email} /> : null}
|
||||
<div className="d-flex flex-row">
|
||||
<p>
|
||||
@@ -250,7 +252,10 @@ LoginPage.propTypes = {
|
||||
}),
|
||||
getThirdPartyAuthContext: PropTypes.func.isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
loginError: PropTypes.string,
|
||||
loginError: PropTypes.shape({
|
||||
value: PropTypes.string,
|
||||
errorCode: PropTypes.string,
|
||||
}),
|
||||
loginRequest: PropTypes.func.isRequired,
|
||||
loginResult: PropTypes.shape({
|
||||
redirectUrl: PropTypes.string,
|
||||
@@ -270,10 +275,11 @@ const mapStateToProps = state => {
|
||||
const forgotPassword = forgotPasswordResultSelector(state);
|
||||
const loginResult = loginRequestSelector(state);
|
||||
const thirdPartyAuthContext = thirdPartyAuthContextSelector(state);
|
||||
const loginError = loginErrorSelector(state);
|
||||
return {
|
||||
loginError: state.logistration.loginError,
|
||||
submitState: state.logistration.submitState,
|
||||
forgotPassword,
|
||||
loginError,
|
||||
loginResult,
|
||||
thirdPartyAuthContext,
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// #COLORS
|
||||
// ----------------------------
|
||||
$link-blue: #23419f;
|
||||
$mfe-font-color: #23419f;
|
||||
$font-blue: #126f9a;
|
||||
$white: #FFFFFF;
|
||||
|
||||
@@ -16,7 +17,7 @@ $apple-black: #000000;
|
||||
$apple-focus-black: $apple-black;
|
||||
|
||||
.font-color {
|
||||
color: $font-blue;
|
||||
color: $mfe-font-color;
|
||||
}
|
||||
|
||||
.login-container {
|
||||
|
||||
2
src/logistration/data/constants.js
Normal file
2
src/logistration/data/constants.js
Normal file
@@ -0,0 +1,2 @@
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export const NON_COMPLIANT_PASSWORD_EXCEPTION = 'NonCompliantPasswordException';
|
||||
@@ -1,5 +1,7 @@
|
||||
import { call, put, takeEvery } from 'redux-saga/effects';
|
||||
|
||||
import { camelCaseObject } from '@edx/frontend-platform';
|
||||
|
||||
// Actions
|
||||
import {
|
||||
REGISTER_NEW_USER,
|
||||
@@ -51,7 +53,7 @@ export function* handleLoginRequest(action) {
|
||||
} catch (e) {
|
||||
const statusCodes = [400];
|
||||
if (e.response && statusCodes.includes(e.response.status)) {
|
||||
yield put(loginRequestFailure(e.response.data.value));
|
||||
yield put(loginRequestFailure(camelCaseObject(e.response.data)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,11 @@ export const loginRequestSelector = createSelector(
|
||||
logistration => logistration.loginResult,
|
||||
);
|
||||
|
||||
export const loginErrorSelector = createSelector(
|
||||
logistrationSelector,
|
||||
logistration => logistration.loginError,
|
||||
);
|
||||
|
||||
export const registrationRequestSelector = createSelector(
|
||||
logistrationSelector,
|
||||
logistration => logistration.registrationResult,
|
||||
|
||||
@@ -100,7 +100,12 @@ const messages = defineMessages({
|
||||
'logistration.login.institution.login.sign.in.with': {
|
||||
id: 'logistration.login.institution.login.sign.in.with',
|
||||
defaultMessage: 'or sign in with',
|
||||
description: 'gives hint about other sign options ',
|
||||
description: 'gives hint about other sign options',
|
||||
},
|
||||
'logistration.non.compliant.password.title': {
|
||||
id: 'logistration.non.compliant.password.title',
|
||||
defaultMessage: 'We recently changed our password requirements',
|
||||
description: 'A title that appears in bold before error message for non-compliant password',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
41
src/logistration/tests/LoginFailure.test.jsx
Normal file
41
src/logistration/tests/LoginFailure.test.jsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import React from 'react';
|
||||
import renderer from 'react-test-renderer';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import LoginFailureMessage from '../LoginFailure';
|
||||
import { NON_COMPLIANT_PASSWORD_EXCEPTION } from '../data/constants';
|
||||
|
||||
describe('LoginFailureMessage', () => {
|
||||
let props = {};
|
||||
|
||||
beforeEach(() => {
|
||||
props = {
|
||||
errors: 'Some error occurred logging you in.',
|
||||
};
|
||||
});
|
||||
|
||||
it('should match non compliant password error message snapshot', () => {
|
||||
props = {
|
||||
...props,
|
||||
errorCode: NON_COMPLIANT_PASSWORD_EXCEPTION,
|
||||
};
|
||||
|
||||
const tree = renderer.create(
|
||||
<IntlProvider locale="en">
|
||||
<LoginFailureMessage {...props} />
|
||||
</IntlProvider>,
|
||||
).toJSON();
|
||||
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should match direct render of error message snapshot', () => {
|
||||
const tree = renderer.create(
|
||||
<IntlProvider locale="en">
|
||||
<LoginFailureMessage {...props} />
|
||||
</IntlProvider>,
|
||||
).toJSON();
|
||||
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -109,7 +109,7 @@ describe('LoginPage', () => {
|
||||
...initialState,
|
||||
logistration: {
|
||||
...initialState.logistration,
|
||||
loginError: 'Email or password is incorrect.',
|
||||
loginError: { value: 'Email or password is incorrect.' },
|
||||
},
|
||||
});
|
||||
|
||||
@@ -122,7 +122,7 @@ describe('LoginPage', () => {
|
||||
...initialState,
|
||||
logistration: {
|
||||
...initialState.logistration,
|
||||
loginError: 'To be on the safe side, you can reset your password <a href="/reset">here</a> before you try again.\n',
|
||||
loginError: { value: 'To be on the safe side, you can reset your password <a href="/reset">here</a> before you try again.\n' },
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`LoginFailureMessage should match direct render of error message snapshot 1`] = `
|
||||
<div
|
||||
className="fade alert alert-danger show"
|
||||
role="alert"
|
||||
>
|
||||
<div>
|
||||
<h4
|
||||
style={
|
||||
Object {
|
||||
"color": "#a0050e",
|
||||
}
|
||||
}
|
||||
>
|
||||
<span>
|
||||
We couldn't sign you in.
|
||||
</span>
|
||||
</h4>
|
||||
<ul>
|
||||
<li>
|
||||
Some error occurred logging you in.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`LoginFailureMessage should match non compliant password error message snapshot 1`] = `
|
||||
<div
|
||||
className="fade alert alert-danger show"
|
||||
role="alert"
|
||||
>
|
||||
<div>
|
||||
<h4
|
||||
style={
|
||||
Object {
|
||||
"color": "#a0050e",
|
||||
}
|
||||
}
|
||||
>
|
||||
<span>
|
||||
We couldn't sign you in.
|
||||
</span>
|
||||
</h4>
|
||||
<ul>
|
||||
<li>
|
||||
<span>
|
||||
<strong>
|
||||
We recently changed our password requirements
|
||||
</strong>
|
||||
|
||||
<br />
|
||||
Your current password does not meet the new security requirements. We just sent a password-reset message to the email address associated with this account. Thank you for helping us keep your data safe.
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
Reference in New Issue
Block a user