Add password compliance workflow (#40)

This commit is contained in:
Zainab Amir
2020-12-03 15:55:48 +05:00
committed by GitHub
parent 46d0ed8e7d
commit 84bb6edf90
10 changed files with 181 additions and 29 deletions

View File

@@ -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);

View File

@@ -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,
};

View File

@@ -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 {

View File

@@ -0,0 +1,2 @@
// eslint-disable-next-line import/prefer-default-export
export const NON_COMPLIANT_PASSWORD_EXCEPTION = 'NonCompliantPasswordException';

View File

@@ -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)));
}
}
}

View File

@@ -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,

View File

@@ -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',
},
});

View 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();
});
});

View File

@@ -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' },
},
});

View File

@@ -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>
`;