Merge pull request #6 from edx/adeel/van_77_adding_password_reset_confirmation_page
Adds Reset password functionality.
This commit is contained in:
@@ -8,9 +8,14 @@ import {
|
||||
reducer as forgotPasswordReducer,
|
||||
storeName as forgotPasswordStoreName,
|
||||
} from '../forgot-password';
|
||||
import {
|
||||
reducer as resetPasswordReducer,
|
||||
storeName as resetPasswordStoreName,
|
||||
} from '../reset-password';
|
||||
|
||||
const createRootReducer = () => combineReducers({
|
||||
[logistrationStoreName]: logistrationReducer,
|
||||
[forgotPasswordStoreName]: forgotPasswordReducer,
|
||||
[resetPasswordStoreName]: resetPasswordReducer,
|
||||
});
|
||||
export default createRootReducer;
|
||||
|
||||
@@ -2,10 +2,12 @@ import { all } from 'redux-saga/effects';
|
||||
|
||||
import { saga as registrationSaga } from '../logistration';
|
||||
import { saga as forgotPasswordSaga } from '../forgot-password';
|
||||
import { saga as resetPasswordSaga } from '../reset-password';
|
||||
|
||||
export default function* rootSaga() {
|
||||
yield all([
|
||||
registrationSaga(),
|
||||
forgotPasswordSaga(),
|
||||
resetPasswordSaga(),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import Footer, { messages as footerMessages } from '@edx/frontend-component-foot
|
||||
import configureStore from './data/configureStore';
|
||||
import { LoginPage, RegistrationPage, NotFoundPage } from './logistration';
|
||||
import ForgotPasswordPage from './forgot-password';
|
||||
import ResetPasswordPage from './reset-password';
|
||||
import appMessages from './i18n';
|
||||
|
||||
import './index.scss';
|
||||
@@ -37,6 +38,7 @@ subscribe(APP_READY, () => {
|
||||
<Route path="/login" component={LoginPage} />
|
||||
<Route path="/register" component={RegistrationPage} />
|
||||
<Route path="/reset" component={ForgotPasswordPage} />
|
||||
<Route path="/password_reset_confirm/:token/" component={ResetPasswordPage} />
|
||||
<Route path="/notfound" component={NotFoundPage} />
|
||||
<Route path="*" component={NotFoundPage} />
|
||||
</Switch>
|
||||
|
||||
49
src/reset-password/InvalidToken.jsx
Normal file
49
src/reset-password/InvalidToken.jsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import { Hyperlink } from '@edx/paragon';
|
||||
|
||||
import Alert from '../logistration/Alert';
|
||||
|
||||
const InvalidTokenMessage = () => {
|
||||
const loginPasswordLink = (
|
||||
<Hyperlink destination="/login">
|
||||
<FormattedMessage
|
||||
id="logistration.forgot.password.confirmation.support.link"
|
||||
defaultMessage="sign-in"
|
||||
description="link text used in message: logistration.reset.password.request.invalid.token.description.message link 'sign-in.'"
|
||||
/>
|
||||
</Hyperlink>
|
||||
);
|
||||
|
||||
const forgotPassword = (<strong>Forgot Password</strong>);
|
||||
return (
|
||||
<div className="d-flex justify-content-center reset-password-container">
|
||||
<div className="d-flex flex-column" style={{ width: '400px' }}>
|
||||
<div className="form-group">
|
||||
<div className="text-center mt-3">
|
||||
<Alert className="alert-danger mt-n2">
|
||||
<h4 style={{ color: '#a0050e' }}>
|
||||
<FormattedMessage
|
||||
id="logistration.reset.password.request.invalid.token.header.message"
|
||||
defaultMessage="Invalid Password Reset Link"
|
||||
description=""
|
||||
/>
|
||||
</h4>
|
||||
<FormattedMessage
|
||||
id="logistration.reset.password.request.invalid.token.description.message"
|
||||
defaultMessage="This password reset link is invalid. It may have been used already. To reset your password, go to the {loginPasswordLink} page and select {forgotPassword}"
|
||||
description=""
|
||||
values={{
|
||||
forgotPassword,
|
||||
loginPasswordLink,
|
||||
}}
|
||||
/>
|
||||
</Alert>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default InvalidTokenMessage;
|
||||
40
src/reset-password/ResetFailure.jsx
Normal file
40
src/reset-password/ResetFailure.jsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import Alert from '../logistration/Alert';
|
||||
|
||||
const ResetFailureMessage = (props) => {
|
||||
const errorMessage = props.errors;
|
||||
return (
|
||||
<div className="d-flex justify-content-center reset-password-container">
|
||||
<div className="d-flex flex-column" style={{ width: '400px' }}>
|
||||
<div className="form-group">
|
||||
<div className="text-center mt-3">
|
||||
<Alert className="alert-danger mt-n2">
|
||||
<div style={{ color: '#a0050e' }}>
|
||||
<FormattedMessage
|
||||
id="logistration.reset.password.request.failure.header.message"
|
||||
defaultMessage="{errorMessage} "
|
||||
description="whatever"
|
||||
values={{
|
||||
errorMessage,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Alert>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ResetFailureMessage.defaultProps = {
|
||||
errors: '',
|
||||
};
|
||||
ResetFailureMessage.propTypes = {
|
||||
errors: PropTypes.string,
|
||||
};
|
||||
|
||||
export default ResetFailureMessage;
|
||||
164
src/reset-password/ResetPasswordPage.jsx
Normal file
164
src/reset-password/ResetPasswordPage.jsx
Normal file
@@ -0,0 +1,164 @@
|
||||
import React, { useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { Button, Input, ValidationFormGroup } from '@edx/paragon';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import messages from './messages';
|
||||
import { resetPassword, validateToken } from './data/actions';
|
||||
import { resetPasswordResultSelector } from './data/selectors';
|
||||
import InvalidTokenMessage from './InvalidToken';
|
||||
import ResetSuccessMessage from './ResetSuccess';
|
||||
import ResetFailureMessage from './ResetFailure';
|
||||
import Spinner from './Spinner';
|
||||
|
||||
|
||||
const ResetPasswordPage = (props) => {
|
||||
const { intl } = props;
|
||||
|
||||
const [newPasswordInput, setNewPasswordValue] = useState('');
|
||||
const [confirmPasswordInput, setConfirmPasswordValue] = useState('');
|
||||
const [passwordValid, setPasswordValidValue] = useState(true);
|
||||
const [passwordMatch, setPasswordMatchValue] = useState(true);
|
||||
|
||||
const handleNewPasswordChange = (e) => {
|
||||
const newPassword = e.target.value;
|
||||
setNewPasswordValue(newPassword);
|
||||
const isValid = newPassword.length >= 8 && newPassword.match(/\d+/g);
|
||||
setPasswordValidValue(isValid !== false);
|
||||
};
|
||||
const handleConfirmPasswordChange = (e) => {
|
||||
const confirmPassword = e.target.value;
|
||||
setConfirmPasswordValue(confirmPassword);
|
||||
setPasswordMatchValue(confirmPassword === newPasswordInput);
|
||||
};
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
if (newPasswordInput === '') {
|
||||
setPasswordValidValue(false);
|
||||
return;
|
||||
}
|
||||
if (newPasswordInput !== confirmPasswordInput) {
|
||||
setPasswordMatchValue(false);
|
||||
return;
|
||||
}
|
||||
if (passwordValid && passwordMatch) {
|
||||
const formPayload = {
|
||||
new_password1: newPasswordInput,
|
||||
new_password2: confirmPasswordInput,
|
||||
};
|
||||
props.resetPassword(formPayload, props.token);
|
||||
}
|
||||
};
|
||||
|
||||
if (props.token_status === 'pending') {
|
||||
const { token } = props.match.params;
|
||||
if (token) {
|
||||
props.validateToken(token);
|
||||
return <Spinner text="Token validation in progress .. " />;
|
||||
}
|
||||
} else if (props.token_status === 'invalid') {
|
||||
return (<InvalidTokenMessage />);
|
||||
} else if (props.status === 'success') {
|
||||
return (<ResetSuccessMessage />);
|
||||
} else {
|
||||
return (
|
||||
<>
|
||||
{props.status === 'failure' ? <ResetFailureMessage errors={props.errors} /> : null}
|
||||
<div className="d-flex justify-content-center reset-password-container">
|
||||
<div className="d-flex flex-column" style={{ width: '450px' }}>
|
||||
<form className="m-4">
|
||||
<div className="form-group">
|
||||
<h3 className="text-center mt-3">
|
||||
{intl.formatMessage(messages['logistration.reset.password.page.heading'])}
|
||||
</h3>
|
||||
<p className="mb-4">
|
||||
{intl.formatMessage(messages['logistration.reset.password.page.instructions'])}
|
||||
</p>
|
||||
<div className="d-flex flex-column align-items-start">
|
||||
<ValidationFormGroup
|
||||
for="new-password"
|
||||
invalid={!passwordValid}
|
||||
invalidMessage={intl.formatMessage(
|
||||
messages['logistration.reset.password.page.invalid.password.message'],
|
||||
)}
|
||||
>
|
||||
<label htmlFor="reset-password-input" className="h6 mr-1">
|
||||
{intl.formatMessage(messages['logistration.reset.password.page.new.field.label'])}
|
||||
</label>
|
||||
<Input
|
||||
name="new-password1"
|
||||
id="reset-password-input"
|
||||
type="password"
|
||||
placeholder=""
|
||||
value={newPasswordInput}
|
||||
onChange={e => handleNewPasswordChange(e)}
|
||||
style={{ width: '400px' }}
|
||||
/>
|
||||
</ValidationFormGroup>
|
||||
<ValidationFormGroup
|
||||
for="confirm-password"
|
||||
invalid={!passwordMatch}
|
||||
invalidMessage={intl.formatMessage(messages['logistration.reset.password.page.invalid.match.message'])}
|
||||
>
|
||||
<label htmlFor="confirm-password-input" className="h6 mr-1">
|
||||
{intl.formatMessage(messages['logistration.reset.password.page.confirm.field.label'])}
|
||||
</label>
|
||||
<Input
|
||||
name="new-password2"
|
||||
id="confirm-password-input"
|
||||
type="password"
|
||||
placeholder=""
|
||||
value={confirmPasswordInput}
|
||||
onChange={e => handleConfirmPasswordChange(e)}
|
||||
style={{ width: '400px' }}
|
||||
/>
|
||||
</ValidationFormGroup>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
className="btn-primary submit"
|
||||
onClick={e => handleSubmit(e)}
|
||||
>
|
||||
{intl.formatMessage(messages['logistration.reset.password.page.submit.button'])}
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
ResetPasswordPage.defaultProps = {
|
||||
status: null,
|
||||
token_status: null,
|
||||
token: null,
|
||||
match: null,
|
||||
errors: null,
|
||||
};
|
||||
|
||||
ResetPasswordPage.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
resetPassword: PropTypes.func.isRequired,
|
||||
validateToken: PropTypes.func.isRequired,
|
||||
token_status: PropTypes.string,
|
||||
token: PropTypes.string,
|
||||
match: PropTypes.shape({
|
||||
params: PropTypes.shape({
|
||||
token: PropTypes.string,
|
||||
}),
|
||||
}),
|
||||
status: PropTypes.string,
|
||||
errors: PropTypes.string,
|
||||
};
|
||||
|
||||
export default connect(
|
||||
resetPasswordResultSelector,
|
||||
{
|
||||
resetPassword,
|
||||
validateToken,
|
||||
},
|
||||
)(injectIntl(ResetPasswordPage));
|
||||
47
src/reset-password/ResetSuccess.jsx
Normal file
47
src/reset-password/ResetSuccess.jsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import { Hyperlink } from '@edx/paragon';
|
||||
|
||||
import Alert from '../logistration/Alert';
|
||||
|
||||
const ResetSuccessMessage = () => {
|
||||
const loginPasswordLink = (
|
||||
<Hyperlink destination="/login">
|
||||
<FormattedMessage
|
||||
id="logistration.reset.password.confirmation.support.link"
|
||||
defaultMessage="Sign-in"
|
||||
description="link text used in message: logistration.reset.password.invalid.token.description.message link 'sign-in.'"
|
||||
/>
|
||||
</Hyperlink>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="d-flex justify-content-center reset-password-container">
|
||||
<div className="d-flex flex-column" style={{ width: '400px' }}>
|
||||
<div className="form-group">
|
||||
<div className="text-center mt-3">
|
||||
<Alert className="alert-warning mt-n2">
|
||||
<h4 style={{ color: 'green' }}>
|
||||
<FormattedMessage
|
||||
id="logistration.reset.password.request.success.header.message"
|
||||
defaultMessage="Password Reset Complete."
|
||||
description="whatever"
|
||||
/>
|
||||
</h4>
|
||||
<FormattedMessage
|
||||
id="logistration.reset.password.request.success.header.description.message"
|
||||
defaultMessage="Your password has been reset. {loginPasswordLink} to your account."
|
||||
description="whatever"
|
||||
values={{
|
||||
loginPasswordLink,
|
||||
}}
|
||||
/>
|
||||
</Alert>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ResetSuccessMessage;
|
||||
38
src/reset-password/Spinner.jsx
Normal file
38
src/reset-password/Spinner.jsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import { faSpinner } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
const Spinner = (props) => {
|
||||
const textMessage = props.text;
|
||||
return (
|
||||
<div className="d-flex justify-content-center reset-password-container">
|
||||
<div className="d-flex flex-column" style={{ width: '400px' }}>
|
||||
<div className="form-group">
|
||||
<div className="d-flex flex-column align-items-start">
|
||||
<h3 className="text-center mt-3">
|
||||
<FormattedMessage
|
||||
id="logistration.reset.password.request.token.validation.message"
|
||||
defaultMessage="{textMessage}"
|
||||
description=""
|
||||
values={{
|
||||
textMessage,
|
||||
}}
|
||||
/>
|
||||
<FontAwesomeIcon icon={faSpinner} spin />
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Spinner.defaultProps = {
|
||||
text: '',
|
||||
};
|
||||
Spinner.propTypes = {
|
||||
text: PropTypes.string,
|
||||
};
|
||||
export default Spinner;
|
||||
44
src/reset-password/data/actions.js
Normal file
44
src/reset-password/data/actions.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import { AsyncActionType } from '../../data/utils';
|
||||
|
||||
export const RESET_PASSWORD = new AsyncActionType('RESET', 'PASSWORD');
|
||||
export const VALIDATE_TOKEN = new AsyncActionType('VALIDATE', 'TOKEN');
|
||||
|
||||
// Validate confirmation token
|
||||
export const validateToken = (token) => ({
|
||||
type: VALIDATE_TOKEN.BASE,
|
||||
payload: { token },
|
||||
});
|
||||
|
||||
export const validateTokenBegin = () => ({
|
||||
type: VALIDATE_TOKEN.BEGIN,
|
||||
});
|
||||
|
||||
export const validateTokenSuccess = (tokenStatus, token) => ({
|
||||
type: VALIDATE_TOKEN.SUCCESS,
|
||||
payload: { tokenStatus, token },
|
||||
});
|
||||
|
||||
export const validateTokenFailure = (tokenStatus) => ({
|
||||
type: VALIDATE_TOKEN.FAILURE,
|
||||
payload: { tokenStatus },
|
||||
});
|
||||
|
||||
// Reset Password
|
||||
export const resetPassword = (formPayload, token) => ({
|
||||
type: RESET_PASSWORD.BASE,
|
||||
payload: { formPayload, token },
|
||||
});
|
||||
|
||||
export const resetPasswordBegin = () => ({
|
||||
type: RESET_PASSWORD.BEGIN,
|
||||
});
|
||||
|
||||
export const resetPasswordSuccess = data => ({
|
||||
type: RESET_PASSWORD.SUCCESS,
|
||||
payload: { data },
|
||||
});
|
||||
|
||||
export const resetPasswordFailure = errors => ({
|
||||
type: RESET_PASSWORD.FAILURE,
|
||||
payload: { errors },
|
||||
});
|
||||
49
src/reset-password/data/reducers.js
Normal file
49
src/reset-password/data/reducers.js
Normal file
@@ -0,0 +1,49 @@
|
||||
import { RESET_PASSWORD, VALIDATE_TOKEN } from './actions';
|
||||
|
||||
export const defaultState = {
|
||||
status: null,
|
||||
token_status: 'pending',
|
||||
token: null,
|
||||
errors: null,
|
||||
};
|
||||
|
||||
const reducer = (state = defaultState, action = null) => {
|
||||
switch (action.type) {
|
||||
case VALIDATE_TOKEN.BEGIN:
|
||||
return {
|
||||
...state,
|
||||
token_status: 'pending',
|
||||
};
|
||||
case VALIDATE_TOKEN.SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
token_status: 'valid',
|
||||
token: action.payload.token,
|
||||
};
|
||||
case VALIDATE_TOKEN.FAILURE:
|
||||
return {
|
||||
...state,
|
||||
token_status: 'invalid',
|
||||
};
|
||||
case RESET_PASSWORD.BEGIN:
|
||||
return {
|
||||
...state,
|
||||
status: 'pending',
|
||||
};
|
||||
case RESET_PASSWORD.SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
status: 'success',
|
||||
};
|
||||
case RESET_PASSWORD.FAILURE:
|
||||
return {
|
||||
...state,
|
||||
status: 'failure',
|
||||
errors: action.payload.errors,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default reducer;
|
||||
54
src/reset-password/data/sagas.js
Normal file
54
src/reset-password/data/sagas.js
Normal file
@@ -0,0 +1,54 @@
|
||||
import { call, put, takeEvery } from 'redux-saga/effects';
|
||||
|
||||
// Actions
|
||||
import {
|
||||
VALIDATE_TOKEN,
|
||||
validateTokenBegin,
|
||||
validateTokenSuccess,
|
||||
validateTokenFailure,
|
||||
RESET_PASSWORD,
|
||||
resetPasswordBegin,
|
||||
resetPasswordSuccess,
|
||||
resetPasswordFailure,
|
||||
} from './actions';
|
||||
|
||||
import { validateToken, resetPassword } from './service';
|
||||
|
||||
|
||||
// Services
|
||||
export function* handleValidateToken(action) {
|
||||
try {
|
||||
yield put(validateTokenBegin());
|
||||
const data = yield call(validateToken, action.payload.token);
|
||||
const isValid = data.is_valid;
|
||||
if (isValid) {
|
||||
yield put(validateTokenSuccess(isValid, action.payload.token));
|
||||
} else {
|
||||
yield put(validateTokenFailure(isValid));
|
||||
}
|
||||
} catch (err) {
|
||||
yield put(validateTokenFailure(err));
|
||||
}
|
||||
}
|
||||
|
||||
export function* handleResetPassword(action) {
|
||||
try {
|
||||
yield put(resetPasswordBegin());
|
||||
const data = yield call(resetPassword, action.payload.formPayload, action.payload.token);
|
||||
const resetStatus = data.reset_status;
|
||||
const resetErrors = data.err_msg;
|
||||
|
||||
if (resetStatus) {
|
||||
yield put(resetPasswordSuccess(resetStatus));
|
||||
} else {
|
||||
yield put(resetPasswordFailure(resetErrors));
|
||||
}
|
||||
} catch (err) {
|
||||
yield put(resetPasswordFailure(err));
|
||||
}
|
||||
}
|
||||
|
||||
export default function* saga() {
|
||||
yield takeEvery(RESET_PASSWORD.BASE, handleResetPassword);
|
||||
yield takeEvery(VALIDATE_TOKEN.BASE, handleValidateToken);
|
||||
}
|
||||
10
src/reset-password/data/selectors.js
Normal file
10
src/reset-password/data/selectors.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import { createSelector } from 'reselect';
|
||||
|
||||
export const storeName = 'resetPassword';
|
||||
|
||||
export const resetPasswordSelector = state => ({ ...state[storeName] });
|
||||
|
||||
export const resetPasswordResultSelector = createSelector(
|
||||
resetPasswordSelector,
|
||||
resetPassword => resetPassword,
|
||||
);
|
||||
41
src/reset-password/data/service.js
Normal file
41
src/reset-password/data/service.js
Normal file
@@ -0,0 +1,41 @@
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import formurlencoded from 'form-urlencoded';
|
||||
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export async function validateToken(token) {
|
||||
const requestConfig = {
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
isPublic: true,
|
||||
};
|
||||
|
||||
const { data } = await getAuthenticatedHttpClient()
|
||||
.post(
|
||||
`${getConfig().LMS_BASE_URL}/user_api/v1/account/password_reset/token/validate/`,
|
||||
formurlencoded({ token }),
|
||||
requestConfig,
|
||||
)
|
||||
.catch((e) => {
|
||||
throw (e);
|
||||
});
|
||||
return data;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export async function resetPassword(payload, token) {
|
||||
const requestConfig = {
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
isPublic: true,
|
||||
};
|
||||
|
||||
const { data } = await getAuthenticatedHttpClient()
|
||||
.post(
|
||||
`${getConfig().LMS_BASE_URL}/password/reset/${token}/?track=pwreset`,
|
||||
formurlencoded(payload),
|
||||
requestConfig,
|
||||
)
|
||||
.catch((e) => {
|
||||
throw (e);
|
||||
});
|
||||
return data;
|
||||
}
|
||||
5
src/reset-password/index.js
Normal file
5
src/reset-password/index.js
Normal file
@@ -0,0 +1,5 @@
|
||||
export { default } from './ResetPasswordPage';
|
||||
export { default as reducer } from './data/reducers';
|
||||
export { RESET_PASSWORD } from './data/actions';
|
||||
export { default as saga } from './data/sagas';
|
||||
export { storeName } from './data/selectors';
|
||||
41
src/reset-password/messages.js
Normal file
41
src/reset-password/messages.js
Normal file
@@ -0,0 +1,41 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
'logistration.reset.password.page.heading': {
|
||||
id: 'logistration.reset.password.page.heading',
|
||||
defaultMessage: 'Reset Your Password',
|
||||
description: 'The page heading for the Reset password page.',
|
||||
},
|
||||
'logistration.reset.password.page.instructions': {
|
||||
id: 'logistration.reset.password.page.instructions',
|
||||
defaultMessage: 'Enter and confirm your new password.',
|
||||
description: 'Instructions message for reset password page.',
|
||||
},
|
||||
'logistration.reset.password.page.invalid.password.message': {
|
||||
id: 'logistration.reset.password.page.invalid.password.message',
|
||||
defaultMessage: 'This password is too short. It must contain at least 8 characters. This password must contain at least 1 number',
|
||||
description: 'Password format error.',
|
||||
},
|
||||
'logistration.reset.password.page.invalid.match.message': {
|
||||
id: 'logistration.reset.password.page.invalid.match.message',
|
||||
defaultMessage: 'Passwords do not match.',
|
||||
description: 'Password format error.',
|
||||
},
|
||||
'logistration.reset.password.page.new.field.label': {
|
||||
id: 'logistration.forgot.password.page.new.field.label',
|
||||
defaultMessage: 'New Password',
|
||||
description: 'New password field label for the reset password page.',
|
||||
},
|
||||
'logistration.reset.password.page.confirm.field.label': {
|
||||
id: 'logistration.forgot.password.page.confirm.field.label',
|
||||
defaultMessage: 'Confirm Password',
|
||||
description: 'Confirm password field label for the reset password page.',
|
||||
},
|
||||
'logistration.reset.password.page.submit.button': {
|
||||
id: 'logistration.reset.password.page.submit.button',
|
||||
defaultMessage: 'Reset my password',
|
||||
description: 'Submit button text for the reset password page.',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
76
src/reset-password/tests/ResetPasswordPage.test.jsx
Normal file
76
src/reset-password/tests/ResetPasswordPage.test.jsx
Normal file
@@ -0,0 +1,76 @@
|
||||
import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
import renderer from 'react-test-renderer';
|
||||
import configureStore from 'redux-mock-store';
|
||||
import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import ResetPasswordPage from '../ResetPasswordPage';
|
||||
|
||||
jest.mock('../data/selectors', () => jest.fn().mockImplementation(() => ({ resetPasswordSelector: () => ({}) })));
|
||||
|
||||
const IntlResetPasswordPage = injectIntl(ResetPasswordPage);
|
||||
const mockStore = configureStore();
|
||||
|
||||
describe('ResetPasswordPage', () => {
|
||||
let props = {};
|
||||
let store = {};
|
||||
|
||||
const reduxWrapper = children => (
|
||||
<IntlProvider locale="en">
|
||||
<Provider store={store}>{children}</Provider>
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
store = mockStore();
|
||||
props = {
|
||||
resetPassword: jest.fn(),
|
||||
status: null,
|
||||
token_status: 'pending',
|
||||
token: null,
|
||||
};
|
||||
});
|
||||
|
||||
it('should match reset password default section snapshot', () => {
|
||||
props = {
|
||||
...props,
|
||||
token: 'token',
|
||||
token_status: 'valid',
|
||||
};
|
||||
const tree = renderer.create(reduxWrapper(<IntlResetPasswordPage {...props} />))
|
||||
.toJSON();
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should match invalid token message section snapshot', () => {
|
||||
props = {
|
||||
...props,
|
||||
token_status: 'invalid',
|
||||
};
|
||||
const tree = renderer.create(reduxWrapper(<IntlResetPasswordPage {...props} />))
|
||||
.toJSON();
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should match successful reset message section snapshot', () => {
|
||||
props = {
|
||||
...props,
|
||||
token_status: 'valid',
|
||||
status: 'success',
|
||||
};
|
||||
const tree = renderer.create(reduxWrapper(<IntlResetPasswordPage {...props} />))
|
||||
.toJSON();
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should match unsuccessful reset message section snapshot', () => {
|
||||
props = {
|
||||
...props,
|
||||
token_status: 'valid',
|
||||
status: 'failure',
|
||||
};
|
||||
const tree = renderer.create(reduxWrapper(<IntlResetPasswordPage {...props} />))
|
||||
.toJSON();
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,373 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ResetPasswordPage should match invalid token message section snapshot 1`] = `
|
||||
<div
|
||||
className="d-flex justify-content-center reset-password-container"
|
||||
>
|
||||
<div
|
||||
className="d-flex flex-column"
|
||||
style={
|
||||
Object {
|
||||
"width": "400px",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="text-center mt-3"
|
||||
>
|
||||
<div
|
||||
className="alert d-flex align-items-start alert-danger mt-n2"
|
||||
>
|
||||
<div />
|
||||
<div>
|
||||
<h4
|
||||
style={
|
||||
Object {
|
||||
"color": "#a0050e",
|
||||
}
|
||||
}
|
||||
>
|
||||
<span>
|
||||
Invalid Password Reset Link
|
||||
</span>
|
||||
</h4>
|
||||
<span>
|
||||
This password reset link is invalid. It may have been used already. To reset your password, go to the
|
||||
<a
|
||||
href="/login"
|
||||
onClick={[Function]}
|
||||
target="_self"
|
||||
>
|
||||
<span>
|
||||
sign-in
|
||||
</span>
|
||||
</a>
|
||||
page and select
|
||||
<strong>
|
||||
Forgot Password
|
||||
</strong>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`ResetPasswordPage should match reset password default section snapshot 1`] = `
|
||||
<div
|
||||
className="d-flex justify-content-center reset-password-container"
|
||||
>
|
||||
<div
|
||||
className="d-flex flex-column"
|
||||
style={
|
||||
Object {
|
||||
"width": "450px",
|
||||
}
|
||||
}
|
||||
>
|
||||
<form
|
||||
className="m-4"
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<h3
|
||||
className="text-center mt-3"
|
||||
>
|
||||
Reset Your Password
|
||||
</h3>
|
||||
<p
|
||||
className="mb-4"
|
||||
>
|
||||
Enter and confirm your new password.
|
||||
</p>
|
||||
<div
|
||||
className="d-flex flex-column align-items-start"
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<label
|
||||
className="h6 mr-1"
|
||||
htmlFor="reset-password-input"
|
||||
>
|
||||
New Password
|
||||
</label>
|
||||
<input
|
||||
aria-describedby=""
|
||||
className="form-control"
|
||||
id="reset-password-input"
|
||||
name="new-password1"
|
||||
onChange={[Function]}
|
||||
placeholder=""
|
||||
style={
|
||||
Object {
|
||||
"width": "400px",
|
||||
}
|
||||
}
|
||||
type="password"
|
||||
value=""
|
||||
/>
|
||||
<strong
|
||||
className="invalid-feedback"
|
||||
id="new-password-invalid-feedback"
|
||||
>
|
||||
This password is too short. It must contain at least 8 characters. This password must contain at least 1 number
|
||||
</strong>
|
||||
</div>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<label
|
||||
className="h6 mr-1"
|
||||
htmlFor="confirm-password-input"
|
||||
>
|
||||
Confirm Password
|
||||
</label>
|
||||
<input
|
||||
aria-describedby=""
|
||||
className="form-control"
|
||||
id="confirm-password-input"
|
||||
name="new-password2"
|
||||
onChange={[Function]}
|
||||
placeholder=""
|
||||
style={
|
||||
Object {
|
||||
"width": "400px",
|
||||
}
|
||||
}
|
||||
type="password"
|
||||
value=""
|
||||
/>
|
||||
<strong
|
||||
className="invalid-feedback"
|
||||
id="confirm-password-invalid-feedback"
|
||||
>
|
||||
Passwords do not match.
|
||||
</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
className="btn btn-primary submit"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
type="button"
|
||||
>
|
||||
Reset my password
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`ResetPasswordPage should match successful reset message section snapshot 1`] = `
|
||||
<div
|
||||
className="d-flex justify-content-center reset-password-container"
|
||||
>
|
||||
<div
|
||||
className="d-flex flex-column"
|
||||
style={
|
||||
Object {
|
||||
"width": "400px",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="text-center mt-3"
|
||||
>
|
||||
<div
|
||||
className="alert d-flex align-items-start alert-warning mt-n2"
|
||||
>
|
||||
<div />
|
||||
<div>
|
||||
<h4
|
||||
style={
|
||||
Object {
|
||||
"color": "green",
|
||||
}
|
||||
}
|
||||
>
|
||||
<span>
|
||||
Password Reset Complete.
|
||||
</span>
|
||||
</h4>
|
||||
<span>
|
||||
Your password has been reset.
|
||||
<a
|
||||
href="/login"
|
||||
onClick={[Function]}
|
||||
target="_self"
|
||||
>
|
||||
<span>
|
||||
Sign-in
|
||||
</span>
|
||||
</a>
|
||||
to your account.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`ResetPasswordPage should match unsuccessful reset message section snapshot 1`] = `
|
||||
Array [
|
||||
<div
|
||||
className="d-flex justify-content-center reset-password-container"
|
||||
>
|
||||
<div
|
||||
className="d-flex flex-column"
|
||||
style={
|
||||
Object {
|
||||
"width": "400px",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="text-center mt-3"
|
||||
>
|
||||
<div
|
||||
className="alert d-flex align-items-start alert-danger mt-n2"
|
||||
>
|
||||
<div />
|
||||
<div>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"color": "#a0050e",
|
||||
}
|
||||
}
|
||||
>
|
||||
<span>
|
||||
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
<div
|
||||
className="d-flex justify-content-center reset-password-container"
|
||||
>
|
||||
<div
|
||||
className="d-flex flex-column"
|
||||
style={
|
||||
Object {
|
||||
"width": "450px",
|
||||
}
|
||||
}
|
||||
>
|
||||
<form
|
||||
className="m-4"
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<h3
|
||||
className="text-center mt-3"
|
||||
>
|
||||
Reset Your Password
|
||||
</h3>
|
||||
<p
|
||||
className="mb-4"
|
||||
>
|
||||
Enter and confirm your new password.
|
||||
</p>
|
||||
<div
|
||||
className="d-flex flex-column align-items-start"
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<label
|
||||
className="h6 mr-1"
|
||||
htmlFor="reset-password-input"
|
||||
>
|
||||
New Password
|
||||
</label>
|
||||
<input
|
||||
aria-describedby=""
|
||||
className="form-control"
|
||||
id="reset-password-input"
|
||||
name="new-password1"
|
||||
onChange={[Function]}
|
||||
placeholder=""
|
||||
style={
|
||||
Object {
|
||||
"width": "400px",
|
||||
}
|
||||
}
|
||||
type="password"
|
||||
value=""
|
||||
/>
|
||||
<strong
|
||||
className="invalid-feedback"
|
||||
id="new-password-invalid-feedback"
|
||||
>
|
||||
This password is too short. It must contain at least 8 characters. This password must contain at least 1 number
|
||||
</strong>
|
||||
</div>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<label
|
||||
className="h6 mr-1"
|
||||
htmlFor="confirm-password-input"
|
||||
>
|
||||
Confirm Password
|
||||
</label>
|
||||
<input
|
||||
aria-describedby=""
|
||||
className="form-control"
|
||||
id="confirm-password-input"
|
||||
name="new-password2"
|
||||
onChange={[Function]}
|
||||
placeholder=""
|
||||
style={
|
||||
Object {
|
||||
"width": "400px",
|
||||
}
|
||||
}
|
||||
type="password"
|
||||
value=""
|
||||
/>
|
||||
<strong
|
||||
className="invalid-feedback"
|
||||
id="confirm-password-invalid-feedback"
|
||||
>
|
||||
Passwords do not match.
|
||||
</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
className="btn btn-primary submit"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
type="button"
|
||||
>
|
||||
Reset my password
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>,
|
||||
]
|
||||
`;
|
||||
Reference in New Issue
Block a user