@openedx/frontend-app-authn 1.0.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -0
- package/README.rst +204 -0
- package/package.json +79 -0
- package/src/Main.tsx +23 -0
- package/src/__mocks__/file.js +1 -0
- package/src/__mocks__/svg.js +1 -0
- package/src/app.scss +6 -0
- package/src/app.ts +43 -0
- package/src/base-container/components/default-layout/DefaultLayout.test.jsx +48 -0
- package/src/base-container/components/default-layout/LargeLayout.jsx +42 -0
- package/src/base-container/components/default-layout/MediumLayout.jsx +49 -0
- package/src/base-container/components/default-layout/SmallLayout.jsx +37 -0
- package/src/base-container/components/default-layout/index.jsx +3 -0
- package/src/base-container/components/default-layout/messages.js +16 -0
- package/src/base-container/components/image-layout/ExtraSmallLayout.jsx +31 -0
- package/src/base-container/components/image-layout/LargeLayout.jsx +32 -0
- package/src/base-container/components/image-layout/MediumLayout.jsx +32 -0
- package/src/base-container/components/image-layout/SmallLayout.jsx +31 -0
- package/src/base-container/components/image-layout/index.jsx +4 -0
- package/src/base-container/components/image-layout/index.scss +37 -0
- package/src/base-container/components/image-layout/messages.js +16 -0
- package/src/base-container/components/welcome-page-layout/LargeLayout.jsx +46 -0
- package/src/base-container/components/welcome-page-layout/MediumLayout.jsx +49 -0
- package/src/base-container/components/welcome-page-layout/SmallLayout.jsx +38 -0
- package/src/base-container/components/welcome-page-layout/index.jsx +3 -0
- package/src/base-container/components/welcome-page-layout/messages.js +21 -0
- package/src/base-container/index.jsx +70 -0
- package/src/base-container/tests/BaseContainer.test.jsx +44 -0
- package/src/common-components/EmbeddedRegistrationRoute.jsx +26 -0
- package/src/common-components/EnterpriseSSO.jsx +110 -0
- package/src/common-components/FormGroup.jsx +106 -0
- package/src/common-components/InstitutionLogistration.jsx +102 -0
- package/src/common-components/NotFoundPage.jsx +15 -0
- package/src/common-components/PasswordField.jsx +175 -0
- package/src/common-components/RedirectLogistration.jsx +94 -0
- package/src/common-components/RegisterFaIcons.jsx +8 -0
- package/src/common-components/SocialAuthProviders.jsx +75 -0
- package/src/common-components/ThirdPartyAuth.jsx +123 -0
- package/src/common-components/ThirdPartyAuthAlert.jsx +49 -0
- package/src/common-components/UnAuthOnlyRoute.jsx +41 -0
- package/src/common-components/data/actions.js +27 -0
- package/src/common-components/data/reducers.js +63 -0
- package/src/common-components/data/sagas.js +32 -0
- package/src/common-components/data/selectors.js +28 -0
- package/src/common-components/data/service.js +23 -0
- package/src/common-components/data/tests/reducer.test.js +82 -0
- package/src/common-components/data/tests/sagas.test.js +71 -0
- package/src/common-components/index.jsx +14 -0
- package/src/common-components/messages.jsx +127 -0
- package/src/common-components/tests/EmbeddedRegistrationRoute.test.jsx +76 -0
- package/src/common-components/tests/FormField.test.jsx +289 -0
- package/src/common-components/tests/SocialAuthProviders.test.jsx +75 -0
- package/src/common-components/tests/ThirdPartyAuthAlert.test.jsx +39 -0
- package/src/common-components/tests/UnAuthOnlyRoute.test.jsx +73 -0
- package/src/common-components/tests/__snapshots__/SocialAuthProviders.test.jsx.snap +156 -0
- package/src/common-components/tests/__snapshots__/ThirdPartyAuthAlert.test.jsx.snap +53 -0
- package/src/constants.ts +1 -0
- package/src/data/configureStore.js +33 -0
- package/src/data/constants.js +38 -0
- package/src/data/countries.ts +59 -0
- package/src/data/oneTrust.js +3 -0
- package/src/data/reducers.js +36 -0
- package/src/data/sagas.js +19 -0
- package/src/data/tests/cookies.test.js +42 -0
- package/src/data/tests/dataUtils.test.js +19 -0
- package/src/data/tests/reduxUtils.test.js +14 -0
- package/src/data/utils/cookies.js +12 -0
- package/src/data/utils/dataUtils.js +81 -0
- package/src/data/utils/index.js +11 -0
- package/src/data/utils/reduxUtils.js +34 -0
- package/src/data/utils/useMobileResponsive.js +24 -0
- package/src/field-renderer/FieldRenderer.jsx +156 -0
- package/src/field-renderer/index.jsx +2 -0
- package/src/field-renderer/tests/FieldRenderer.test.jsx +201 -0
- package/src/forgot-password/ForgotPasswordAlert.jsx +97 -0
- package/src/forgot-password/ForgotPasswordPage.jsx +193 -0
- package/src/forgot-password/data/actions.js +32 -0
- package/src/forgot-password/data/reducers.js +58 -0
- package/src/forgot-password/data/sagas.js +35 -0
- package/src/forgot-password/data/selectors.js +10 -0
- package/src/forgot-password/data/service.js +21 -0
- package/src/forgot-password/data/tests/reducers.test.js +34 -0
- package/src/forgot-password/data/tests/sagas.test.js +67 -0
- package/src/forgot-password/index.js +5 -0
- package/src/forgot-password/messages.js +128 -0
- package/src/forgot-password/tests/ForgotPasswordPage.test.jsx +269 -0
- package/src/i18n/index.js +25 -0
- package/src/index.ts +3 -0
- package/src/login/AccountActivationMessage.jsx +78 -0
- package/src/login/ChangePasswordPrompt.jsx +91 -0
- package/src/login/LoginFailure.jsx +231 -0
- package/src/login/LoginPage.jsx +368 -0
- package/src/login/data/actions.js +39 -0
- package/src/login/data/constants.js +20 -0
- package/src/login/data/reducers.js +76 -0
- package/src/login/data/sagas.js +45 -0
- package/src/login/data/service.js +24 -0
- package/src/login/data/tests/reducers.test.js +155 -0
- package/src/login/data/tests/sagas.test.js +110 -0
- package/src/login/index.js +5 -0
- package/src/login/messages.jsx +211 -0
- package/src/login/tests/AccountActivationMessage.test.jsx +122 -0
- package/src/login/tests/ChangePasswordPrompt.test.jsx +75 -0
- package/src/login/tests/LoginFailure.test.jsx +323 -0
- package/src/login/tests/LoginPage.test.jsx +839 -0
- package/src/logistration/Logistration.jsx +189 -0
- package/src/logistration/Logistration.test.jsx +275 -0
- package/src/progressive-profiling/ProgressiveProfiling.jsx +313 -0
- package/src/progressive-profiling/ProgressiveProfilingPageModal.jsx +56 -0
- package/src/progressive-profiling/data/actions.js +22 -0
- package/src/progressive-profiling/data/reducers.js +38 -0
- package/src/progressive-profiling/data/sagas.js +24 -0
- package/src/progressive-profiling/data/selectors.js +14 -0
- package/src/progressive-profiling/data/service.js +17 -0
- package/src/progressive-profiling/index.js +5 -0
- package/src/progressive-profiling/messages.jsx +57 -0
- package/src/progressive-profiling/tests/ProgressiveProfiling.test.jsx +322 -0
- package/src/register/RegistrationFields/CountryField/CountryField.jsx +164 -0
- package/src/register/RegistrationFields/CountryField/CountryField.test.jsx +204 -0
- package/src/register/RegistrationFields/CountryField/validator.js +32 -0
- package/src/register/RegistrationFields/EmailField/EmailField.jsx +140 -0
- package/src/register/RegistrationFields/EmailField/EmailField.test.jsx +236 -0
- package/src/register/RegistrationFields/EmailField/constants.js +131 -0
- package/src/register/RegistrationFields/EmailField/validator.js +126 -0
- package/src/register/RegistrationFields/EmailField/validator.test.js +39 -0
- package/src/register/RegistrationFields/HonorCodeField/HonorCode.jsx +104 -0
- package/src/register/RegistrationFields/HonorCodeField/HonorCode.test.jsx +70 -0
- package/src/register/RegistrationFields/NameField/NameField.jsx +68 -0
- package/src/register/RegistrationFields/NameField/NameField.test.jsx +146 -0
- package/src/register/RegistrationFields/NameField/validator.js +22 -0
- package/src/register/RegistrationFields/TermsOfServiceField/TermsOfService.jsx +58 -0
- package/src/register/RegistrationFields/TermsOfServiceField/TermsOfService.test.jsx +55 -0
- package/src/register/RegistrationFields/UsernameField/UsernameField.jsx +162 -0
- package/src/register/RegistrationFields/UsernameField/UsernameField.test.jsx +319 -0
- package/src/register/RegistrationFields/UsernameField/validator.js +16 -0
- package/src/register/RegistrationFields/index.js +6 -0
- package/src/register/RegistrationPage.jsx +428 -0
- package/src/register/RegistrationPage.test.jsx +854 -0
- package/src/register/components/ConfigurableRegistrationForm.jsx +244 -0
- package/src/register/components/RegistrationFailure.jsx +80 -0
- package/src/register/components/tests/ConfigurableRegistrationForm.test.jsx +401 -0
- package/src/register/components/tests/RegistrationFailure.test.jsx +212 -0
- package/src/register/components/tests/ThirdPartyAuth.test.jsx +439 -0
- package/src/register/data/actions.js +85 -0
- package/src/register/data/constants.js +13 -0
- package/src/register/data/reducers.js +140 -0
- package/src/register/data/sagas.js +58 -0
- package/src/register/data/selectors.js +34 -0
- package/src/register/data/service.js +45 -0
- package/src/register/data/tests/reducers.test.js +277 -0
- package/src/register/data/tests/sagas.test.js +239 -0
- package/src/register/data/tests/utils.test.js +98 -0
- package/src/register/data/utils.js +153 -0
- package/src/register/index.js +4 -0
- package/src/register/messages.jsx +206 -0
- package/src/reset-password/ResetPasswordFailure.jsx +55 -0
- package/src/reset-password/ResetPasswordPage.jsx +224 -0
- package/src/reset-password/ResetPasswordSuccess.jsx +19 -0
- package/src/reset-password/data/actions.js +50 -0
- package/src/reset-password/data/constants.js +16 -0
- package/src/reset-password/data/reducers.js +44 -0
- package/src/reset-password/data/sagas.js +67 -0
- package/src/reset-password/data/selectors.js +10 -0
- package/src/reset-password/data/service.js +61 -0
- package/src/reset-password/data/tests/sagas.test.js +185 -0
- package/src/reset-password/index.js +5 -0
- package/src/reset-password/messages.js +88 -0
- package/src/reset-password/tests/ResetPasswordPage.test.jsx +241 -0
- package/src/routes.jsx +79 -0
- package/src/sass/_base_component.scss +202 -0
- package/src/sass/_forgot_password.scss +3 -0
- package/src/sass/_login_page.scss +3 -0
- package/src/sass/_progressive_profiling_page.scss +30 -0
- package/src/sass/_registration.scss +123 -0
- package/src/sass/_reset_password.scss +3 -0
- package/src/sass/_style.scss +465 -0
- package/src/setupTest.js +68 -0
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { getSiteConfig } from '@openedx/frontend-base';
|
|
2
|
+
import { fireEvent, render } from '@testing-library/react';
|
|
3
|
+
|
|
4
|
+
import FieldRenderer from '../FieldRenderer';
|
|
5
|
+
|
|
6
|
+
describe('FieldRendererTests', () => {
|
|
7
|
+
let value = '';
|
|
8
|
+
|
|
9
|
+
const changeHandler = (e) => {
|
|
10
|
+
if (e.target.type === 'checkbox') {
|
|
11
|
+
value = e.target.checked;
|
|
12
|
+
} else {
|
|
13
|
+
value = e.target.value;
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
value = '';
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('should render select field type', () => {
|
|
22
|
+
const fieldData = {
|
|
23
|
+
type: 'select',
|
|
24
|
+
label: 'Year of Birth',
|
|
25
|
+
name: 'yob-field',
|
|
26
|
+
options: [['1997', '1997'], ['1998', '1998']],
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const { container } = render(<FieldRenderer value={value} fieldData={fieldData} onChangeHandler={changeHandler} />);
|
|
30
|
+
const input = container.querySelector('select#yob-field');
|
|
31
|
+
const label = container.querySelector('label');
|
|
32
|
+
fireEvent.change(input, { target: { value: 1997 } });
|
|
33
|
+
|
|
34
|
+
expect(input.type).toEqual('select-one');
|
|
35
|
+
expect(label.textContent).toContain(fieldData.label);
|
|
36
|
+
expect(value).toEqual('1997');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should return null if no options are provided for select field', () => {
|
|
40
|
+
const fieldData = {
|
|
41
|
+
type: 'select',
|
|
42
|
+
label: 'Year of Birth',
|
|
43
|
+
name: 'yob-field',
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const { container } = render(<FieldRenderer fieldData={fieldData} onChangeHandler={() => { }} />);
|
|
47
|
+
expect(container.innerHTML).toEqual('');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should render textarea field', () => {
|
|
51
|
+
const fieldData = {
|
|
52
|
+
type: 'textarea',
|
|
53
|
+
label: 'Why do you want to join this platform?',
|
|
54
|
+
name: 'goals-field',
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const { container } = render(<FieldRenderer value={value} fieldData={fieldData} onChangeHandler={changeHandler} />);
|
|
58
|
+
const input = container.querySelector('#goals-field');
|
|
59
|
+
const label = container.querySelector('label');
|
|
60
|
+
fireEvent.change(input, { target: { value: 'These are my goals.' } });
|
|
61
|
+
|
|
62
|
+
expect(input.type).toEqual(fieldData.type);
|
|
63
|
+
expect(label.textContent).toContain('Why do you want to join this platform?');
|
|
64
|
+
expect(value).toEqual('These are my goals.');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should render an input field', () => {
|
|
68
|
+
const fieldData = {
|
|
69
|
+
type: 'text',
|
|
70
|
+
label: 'Company',
|
|
71
|
+
name: 'company-field',
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const { container } = render(<FieldRenderer value={value} fieldData={fieldData} onChangeHandler={changeHandler} />);
|
|
75
|
+
const input = container.querySelector('input#company-field');
|
|
76
|
+
const label = container.querySelector('label');
|
|
77
|
+
fireEvent.change(input, { target: { value: 'ABC' } });
|
|
78
|
+
|
|
79
|
+
expect(input.type).toEqual(fieldData.type);
|
|
80
|
+
expect(label.textContent).toContain(fieldData.label);
|
|
81
|
+
expect(value).toEqual('ABC');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('should render checkbox field', () => {
|
|
85
|
+
const fieldData = {
|
|
86
|
+
type: 'checkbox',
|
|
87
|
+
label: `I agree that ${getSiteConfig().siteName} may send me marketing messages.`,
|
|
88
|
+
name: 'marketing-emails-opt-in-field',
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const { container } = render(<FieldRenderer value={value} fieldData={fieldData} onChangeHandler={changeHandler} />);
|
|
92
|
+
const input = container.querySelector('input#marketing-emails-opt-in-field');
|
|
93
|
+
const label = container.querySelector('label');
|
|
94
|
+
fireEvent.click(input);
|
|
95
|
+
|
|
96
|
+
expect(input.type).toEqual(fieldData.type);
|
|
97
|
+
expect(label.textContent).toContain(fieldData.label);
|
|
98
|
+
expect(value).toEqual(true);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('should return null if field type is unknown', () => {
|
|
102
|
+
const fieldData = {
|
|
103
|
+
type: 'unknown',
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const { container } = render(<FieldRenderer fieldData={fieldData} onChangeHandler={() => { }} />);
|
|
107
|
+
expect(container.innerHTML).toContain('');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('should run onBlur and onFocus functions for a field if given', () => {
|
|
111
|
+
const fieldData = { type: 'text', label: 'Test', name: 'test-field' };
|
|
112
|
+
let functionValue = '';
|
|
113
|
+
|
|
114
|
+
const onBlur = (e) => {
|
|
115
|
+
functionValue = `${e.target.name} blurred`;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const onFocus = (e) => {
|
|
119
|
+
functionValue = `${e.target.name} focussed`;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const { container } = render(
|
|
123
|
+
<FieldRenderer
|
|
124
|
+
handleFocus={onFocus}
|
|
125
|
+
handleBlur={onBlur}
|
|
126
|
+
value={value}
|
|
127
|
+
fieldData={fieldData}
|
|
128
|
+
onChangeHandler={changeHandler}
|
|
129
|
+
/>,
|
|
130
|
+
);
|
|
131
|
+
const input = container.querySelector('#test-field');
|
|
132
|
+
|
|
133
|
+
fireEvent.focus(input);
|
|
134
|
+
expect(functionValue).toEqual('test-field focussed');
|
|
135
|
+
|
|
136
|
+
fireEvent.blur(input);
|
|
137
|
+
expect(functionValue).toEqual('test-field blurred');
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('should render error message for required text fields', () => {
|
|
141
|
+
const fieldData = { type: 'text', label: 'First Name', name: 'first-name-field' };
|
|
142
|
+
|
|
143
|
+
const { container } = render(
|
|
144
|
+
<FieldRenderer
|
|
145
|
+
isRequired
|
|
146
|
+
fieldData={fieldData}
|
|
147
|
+
onChangeHandler={changeHandler}
|
|
148
|
+
errorMessage="Enter your first name"
|
|
149
|
+
/>,
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
expect(container.querySelector(`#${fieldData.name}-error`).textContent).toEqual('Enter your first name');
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('should render error message for required select fields', () => {
|
|
156
|
+
const fieldData = {
|
|
157
|
+
type: 'select', label: 'Preference', name: 'preference-field', options: [['a', 'Opt 1'], ['b', 'Opt 2']],
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const { container } = render(
|
|
161
|
+
<FieldRenderer
|
|
162
|
+
isRequired
|
|
163
|
+
fieldData={fieldData}
|
|
164
|
+
onChangeHandler={changeHandler}
|
|
165
|
+
errorMessage="Select your preference"
|
|
166
|
+
/>,
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
expect(container.querySelector(`#${fieldData.name}-error`).textContent).toEqual('Select your preference');
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('should render error message for required textarea fields', () => {
|
|
173
|
+
const fieldData = { type: 'textarea', label: 'Goals', name: 'goals-field' };
|
|
174
|
+
|
|
175
|
+
const { container } = render(
|
|
176
|
+
<FieldRenderer
|
|
177
|
+
isRequired
|
|
178
|
+
fieldData={fieldData}
|
|
179
|
+
onChangeHandler={changeHandler}
|
|
180
|
+
errorMessage="Tell us your goals"
|
|
181
|
+
/>,
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
expect(container.querySelector(`#${fieldData.name}-error`).textContent).toEqual('Tell us your goals');
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('should render error message for required checkbox fields', () => {
|
|
188
|
+
const fieldData = { type: 'checkbox', label: 'Honor Code', name: 'honor-code-field' };
|
|
189
|
+
|
|
190
|
+
const { container } = render(
|
|
191
|
+
<FieldRenderer
|
|
192
|
+
isRequired
|
|
193
|
+
fieldData={fieldData}
|
|
194
|
+
onChangeHandler={changeHandler}
|
|
195
|
+
errorMessage="You must agree to our Honor Code"
|
|
196
|
+
/>,
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
expect(container.querySelector(`#${fieldData.name}-error`).textContent).toEqual('You must agree to our Honor Code');
|
|
200
|
+
});
|
|
201
|
+
});
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { FormattedMessage, useAppConfig, useIntl } from '@openedx/frontend-base';
|
|
2
|
+
import { Alert } from '@openedx/paragon';
|
|
3
|
+
import { CheckCircle, Error } from '@openedx/paragon/icons';
|
|
4
|
+
import PropTypes from 'prop-types';
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
COMPLETE_STATE, FORBIDDEN_STATE, FORM_SUBMISSION_ERROR, INTERNAL_SERVER_ERROR,
|
|
8
|
+
} from '../data/constants';
|
|
9
|
+
import { PASSWORD_RESET } from '../reset-password/data/constants';
|
|
10
|
+
import messages from './messages';
|
|
11
|
+
|
|
12
|
+
const ForgotPasswordAlert = (props) => {
|
|
13
|
+
const { formatMessage } = useIntl();
|
|
14
|
+
const { email, emailError } = props;
|
|
15
|
+
let message = '';
|
|
16
|
+
let heading = formatMessage(messages['forgot.password.error.alert.title']);
|
|
17
|
+
let { status } = props;
|
|
18
|
+
|
|
19
|
+
if (emailError) {
|
|
20
|
+
status = FORM_SUBMISSION_ERROR;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
switch (status) {
|
|
24
|
+
case COMPLETE_STATE:
|
|
25
|
+
heading = formatMessage(messages['confirmation.message.title']);
|
|
26
|
+
message = (
|
|
27
|
+
<FormattedMessage
|
|
28
|
+
id="forgot.password.confirmation.message"
|
|
29
|
+
defaultMessage="We sent an email to {email} with instructions to reset your password.
|
|
30
|
+
If you do not receive a password reset message after 1 minute, verify that you entered
|
|
31
|
+
the correct email address, or check your spam folder. If you need further assistance, {supportLink}."
|
|
32
|
+
description="Forgot password confirmation message"
|
|
33
|
+
values={{
|
|
34
|
+
email: <span className="data-hj-suppress">{email}</span>,
|
|
35
|
+
supportLink: (
|
|
36
|
+
<Alert.Link href={useAppConfig().PASSWORD_RESET_SUPPORT_LINK} target="_blank">
|
|
37
|
+
{formatMessage(messages['confirmation.support.link'])}
|
|
38
|
+
</Alert.Link>
|
|
39
|
+
),
|
|
40
|
+
}}
|
|
41
|
+
/>
|
|
42
|
+
);
|
|
43
|
+
break;
|
|
44
|
+
case INTERNAL_SERVER_ERROR:
|
|
45
|
+
message = formatMessage(messages['internal.server.error']);
|
|
46
|
+
break;
|
|
47
|
+
case FORBIDDEN_STATE:
|
|
48
|
+
heading = formatMessage(messages['forgot.password.error.message.title']);
|
|
49
|
+
message = formatMessage(messages['forgot.password.request.in.progress.message']);
|
|
50
|
+
break;
|
|
51
|
+
case FORM_SUBMISSION_ERROR:
|
|
52
|
+
message = formatMessage(messages['extend.field.errors'], { emailError });
|
|
53
|
+
break;
|
|
54
|
+
case PASSWORD_RESET.INVALID_TOKEN:
|
|
55
|
+
heading = formatMessage(messages['invalid.token.heading']);
|
|
56
|
+
message = formatMessage(messages['invalid.token.error.message']);
|
|
57
|
+
break;
|
|
58
|
+
case PASSWORD_RESET.FORBIDDEN_REQUEST:
|
|
59
|
+
heading = formatMessage(messages['token.validation.rate.limit.error.heading']);
|
|
60
|
+
message = formatMessage(messages['token.validation.rate.limit.error']);
|
|
61
|
+
break;
|
|
62
|
+
case PASSWORD_RESET.INTERNAL_SERVER_ERROR:
|
|
63
|
+
heading = formatMessage(messages['token.validation.internal.sever.error.heading']);
|
|
64
|
+
message = formatMessage(messages['token.validation.internal.sever.error']);
|
|
65
|
+
break;
|
|
66
|
+
default:
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (message) {
|
|
71
|
+
return (
|
|
72
|
+
<Alert
|
|
73
|
+
id="validation-errors"
|
|
74
|
+
className="mb-4"
|
|
75
|
+
variant={`${status === 'complete' ? 'success' : 'danger'}`}
|
|
76
|
+
icon={status === 'complete' ? CheckCircle : Error}
|
|
77
|
+
>
|
|
78
|
+
<Alert.Heading>{heading}</Alert.Heading>
|
|
79
|
+
<p>{message}</p>
|
|
80
|
+
</Alert>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
return null;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
ForgotPasswordAlert.defaultProps = {
|
|
87
|
+
email: '',
|
|
88
|
+
emailError: '',
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
ForgotPasswordAlert.propTypes = {
|
|
92
|
+
status: PropTypes.string.isRequired,
|
|
93
|
+
email: PropTypes.string,
|
|
94
|
+
emailError: PropTypes.string,
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export default ForgotPasswordAlert;
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import { connect } from 'react-redux';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
useAppConfig,
|
|
6
|
+
getSiteConfig, sendPageEvent, sendTrackEvent, useIntl
|
|
7
|
+
} from '@openedx/frontend-base';
|
|
8
|
+
import {
|
|
9
|
+
Form,
|
|
10
|
+
Hyperlink,
|
|
11
|
+
Icon,
|
|
12
|
+
StatefulButton,
|
|
13
|
+
Tab,
|
|
14
|
+
Tabs,
|
|
15
|
+
} from '@openedx/paragon';
|
|
16
|
+
import { ChevronLeft } from '@openedx/paragon/icons';
|
|
17
|
+
import PropTypes from 'prop-types';
|
|
18
|
+
import { Helmet } from 'react-helmet';
|
|
19
|
+
import { useNavigate } from 'react-router-dom';
|
|
20
|
+
|
|
21
|
+
import BaseContainer from '../base-container';
|
|
22
|
+
import { FormGroup } from '../common-components';
|
|
23
|
+
import { DEFAULT_STATE, LOGIN_PAGE, VALID_EMAIL_REGEX } from '../data/constants';
|
|
24
|
+
import { updatePathWithQueryParams, windowScrollTo } from '../data/utils';
|
|
25
|
+
import { forgotPassword, setForgotPasswordFormData } from './data/actions';
|
|
26
|
+
import { forgotPasswordResultSelector } from './data/selectors';
|
|
27
|
+
import ForgotPasswordAlert from './ForgotPasswordAlert';
|
|
28
|
+
import messages from './messages';
|
|
29
|
+
|
|
30
|
+
const ForgotPasswordPage = (props) => {
|
|
31
|
+
const platformName = getSiteConfig().siteName;
|
|
32
|
+
const emailRegex = new RegExp(VALID_EMAIL_REGEX, 'i');
|
|
33
|
+
const {
|
|
34
|
+
status, submitState, emailValidationError,
|
|
35
|
+
} = props;
|
|
36
|
+
|
|
37
|
+
const { formatMessage } = useIntl();
|
|
38
|
+
const [email, setEmail] = useState(props.email);
|
|
39
|
+
const [bannerEmail, setBannerEmail] = useState('');
|
|
40
|
+
const [formErrors, setFormErrors] = useState('');
|
|
41
|
+
const [validationError, setValidationError] = useState(emailValidationError);
|
|
42
|
+
const navigate = useNavigate();
|
|
43
|
+
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
sendPageEvent('login_and_registration', 'reset');
|
|
46
|
+
sendTrackEvent('edx.bi.password_reset_form.viewed', { category: 'user-engagement' });
|
|
47
|
+
}, []);
|
|
48
|
+
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
setValidationError(emailValidationError);
|
|
51
|
+
}, [emailValidationError]);
|
|
52
|
+
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
if (status === 'complete') {
|
|
55
|
+
setEmail('');
|
|
56
|
+
}
|
|
57
|
+
}, [status]);
|
|
58
|
+
|
|
59
|
+
const getValidationMessage = (value) => {
|
|
60
|
+
let error = '';
|
|
61
|
+
|
|
62
|
+
if (value === '') {
|
|
63
|
+
error = formatMessage(messages['forgot.password.empty.email.field.error']);
|
|
64
|
+
} else if (!emailRegex.test(value)) {
|
|
65
|
+
error = formatMessage(messages['forgot.password.page.invalid.email.message']);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return error;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const handleBlur = () => {
|
|
72
|
+
props.setForgotPasswordFormData({ email, emailValidationError: getValidationMessage(email) });
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const handleFocus = () => props.setForgotPasswordFormData({ emailValidationError: '' });
|
|
76
|
+
|
|
77
|
+
const handleSubmit = (e) => {
|
|
78
|
+
e.preventDefault();
|
|
79
|
+
setBannerEmail(email);
|
|
80
|
+
|
|
81
|
+
const error = getValidationMessage(email);
|
|
82
|
+
if (error) {
|
|
83
|
+
setFormErrors(error);
|
|
84
|
+
props.setForgotPasswordFormData({ email, emailValidationError: error });
|
|
85
|
+
windowScrollTo({ left: 0, top: 0, behavior: 'smooth' });
|
|
86
|
+
} else {
|
|
87
|
+
props.forgotPassword(email);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const tabTitle = (
|
|
92
|
+
<div className="d-inline-flex flex-wrap align-items-center">
|
|
93
|
+
<Icon src={ChevronLeft} />
|
|
94
|
+
<span className="ml-2">{formatMessage(messages['sign.in.text'])}</span>
|
|
95
|
+
</div>
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<BaseContainer>
|
|
100
|
+
<Helmet>
|
|
101
|
+
<title>
|
|
102
|
+
{formatMessage(
|
|
103
|
+
messages['forgot.password.page.title'],
|
|
104
|
+
{ siteName: getSiteConfig().siteName }
|
|
105
|
+
)}
|
|
106
|
+
</title>
|
|
107
|
+
</Helmet>
|
|
108
|
+
<div>
|
|
109
|
+
<Tabs activeKey="" id="controlled-tab" onSelect={(key) => navigate(updatePathWithQueryParams(key))}>
|
|
110
|
+
<Tab title={tabTitle} eventKey={LOGIN_PAGE} />
|
|
111
|
+
</Tabs>
|
|
112
|
+
<div id="main-content" className="main-content">
|
|
113
|
+
<Form id="forget-password-form" name="forget-password-form" className="mw-xs">
|
|
114
|
+
<ForgotPasswordAlert email={bannerEmail} emailError={formErrors} status={status} />
|
|
115
|
+
<h2 className="h4">
|
|
116
|
+
{formatMessage(messages['forgot.password.page.heading'])}
|
|
117
|
+
</h2>
|
|
118
|
+
<p className="mb-4">
|
|
119
|
+
{formatMessage(messages['forgot.password.page.instructions'])}
|
|
120
|
+
</p>
|
|
121
|
+
<FormGroup
|
|
122
|
+
floatingLabel={formatMessage(messages['forgot.password.page.email.field.label'])}
|
|
123
|
+
name="email"
|
|
124
|
+
value={email}
|
|
125
|
+
autoComplete="on"
|
|
126
|
+
errorMessage={validationError}
|
|
127
|
+
handleChange={(e) => setEmail(e.target.value)}
|
|
128
|
+
handleBlur={handleBlur}
|
|
129
|
+
handleFocus={handleFocus}
|
|
130
|
+
helpText={[formatMessage(messages['forgot.password.email.help.text'], { platformName })]}
|
|
131
|
+
/>
|
|
132
|
+
<StatefulButton
|
|
133
|
+
id="submit-forget-password"
|
|
134
|
+
name="submit-forget-password"
|
|
135
|
+
type="submit"
|
|
136
|
+
variant="brand"
|
|
137
|
+
className="forgot-password--button"
|
|
138
|
+
state={submitState}
|
|
139
|
+
labels={{
|
|
140
|
+
default: formatMessage(messages['forgot.password.page.submit.button']),
|
|
141
|
+
pending: '',
|
|
142
|
+
}}
|
|
143
|
+
onClick={handleSubmit}
|
|
144
|
+
onMouseDown={(e) => e.preventDefault()}
|
|
145
|
+
/>
|
|
146
|
+
{(useAppConfig().LOGIN_ISSUE_SUPPORT_LINK) && (
|
|
147
|
+
<Hyperlink
|
|
148
|
+
id="forgot-password"
|
|
149
|
+
name="forgot-password"
|
|
150
|
+
className="ml-4 font-weight-500 text-body"
|
|
151
|
+
destination={useAppConfig().LOGIN_ISSUE_SUPPORT_LINK}
|
|
152
|
+
target="_blank"
|
|
153
|
+
showLaunchIcon={false}
|
|
154
|
+
>
|
|
155
|
+
{formatMessage(messages['need.help.sign.in.text'])}
|
|
156
|
+
</Hyperlink>
|
|
157
|
+
)}
|
|
158
|
+
<p className="mt-5.5 small text-gray-700">
|
|
159
|
+
{formatMessage(messages['additional.help.text'], { platformName })}
|
|
160
|
+
<span>
|
|
161
|
+
<Hyperlink isInline destination={`mailto:${useAppConfig().INFO_EMAIL}`}>{useAppConfig().INFO_EMAIL}</Hyperlink>
|
|
162
|
+
</span>
|
|
163
|
+
</p>
|
|
164
|
+
</Form>
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
167
|
+
</BaseContainer>
|
|
168
|
+
);
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
ForgotPasswordPage.propTypes = {
|
|
172
|
+
email: PropTypes.string,
|
|
173
|
+
emailValidationError: PropTypes.string,
|
|
174
|
+
forgotPassword: PropTypes.func.isRequired,
|
|
175
|
+
setForgotPasswordFormData: PropTypes.func.isRequired,
|
|
176
|
+
status: PropTypes.string,
|
|
177
|
+
submitState: PropTypes.string,
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
ForgotPasswordPage.defaultProps = {
|
|
181
|
+
email: '',
|
|
182
|
+
emailValidationError: '',
|
|
183
|
+
status: null,
|
|
184
|
+
submitState: DEFAULT_STATE,
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
export default connect(
|
|
188
|
+
forgotPasswordResultSelector,
|
|
189
|
+
{
|
|
190
|
+
forgotPassword,
|
|
191
|
+
setForgotPasswordFormData,
|
|
192
|
+
},
|
|
193
|
+
)(ForgotPasswordPage);
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { AsyncActionType } from '../../data/utils';
|
|
2
|
+
|
|
3
|
+
export const FORGOT_PASSWORD = new AsyncActionType('FORGOT', 'PASSWORD');
|
|
4
|
+
export const FORGOT_PASSWORD_PERSIST_FORM_DATA = 'FORGOT_PASSWORD_PERSIST_FORM_DATA';
|
|
5
|
+
|
|
6
|
+
// Forgot Password
|
|
7
|
+
export const forgotPassword = email => ({
|
|
8
|
+
type: FORGOT_PASSWORD.BASE,
|
|
9
|
+
payload: { email },
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
export const forgotPasswordBegin = () => ({
|
|
13
|
+
type: FORGOT_PASSWORD.BEGIN,
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export const forgotPasswordSuccess = email => ({
|
|
17
|
+
type: FORGOT_PASSWORD.SUCCESS,
|
|
18
|
+
payload: { email },
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
export const forgotPasswordForbidden = () => ({
|
|
22
|
+
type: FORGOT_PASSWORD.FORBIDDEN,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
export const forgotPasswordServerError = () => ({
|
|
26
|
+
type: FORGOT_PASSWORD.FAILURE,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
export const setForgotPasswordFormData = (forgotPasswordFormData) => ({
|
|
30
|
+
type: FORGOT_PASSWORD_PERSIST_FORM_DATA,
|
|
31
|
+
payload: { forgotPasswordFormData },
|
|
32
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { FORGOT_PASSWORD, FORGOT_PASSWORD_PERSIST_FORM_DATA } from './actions';
|
|
2
|
+
import { INTERNAL_SERVER_ERROR, PENDING_STATE } from '../../data/constants';
|
|
3
|
+
import { PASSWORD_RESET_FAILURE } from '../../reset-password/data/actions';
|
|
4
|
+
|
|
5
|
+
export const defaultState = {
|
|
6
|
+
status: '',
|
|
7
|
+
submitState: '',
|
|
8
|
+
email: '',
|
|
9
|
+
emailValidationError: '',
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const reducer = (state = defaultState, action = null) => {
|
|
13
|
+
if (action !== null) {
|
|
14
|
+
switch (action.type) {
|
|
15
|
+
case FORGOT_PASSWORD.BEGIN:
|
|
16
|
+
return {
|
|
17
|
+
email: state.email,
|
|
18
|
+
status: 'pending',
|
|
19
|
+
submitState: PENDING_STATE,
|
|
20
|
+
};
|
|
21
|
+
case FORGOT_PASSWORD.SUCCESS:
|
|
22
|
+
return {
|
|
23
|
+
...defaultState,
|
|
24
|
+
status: 'complete',
|
|
25
|
+
};
|
|
26
|
+
case FORGOT_PASSWORD.FORBIDDEN:
|
|
27
|
+
return {
|
|
28
|
+
email: state.email,
|
|
29
|
+
status: 'forbidden',
|
|
30
|
+
};
|
|
31
|
+
case FORGOT_PASSWORD.FAILURE:
|
|
32
|
+
return {
|
|
33
|
+
email: state.email,
|
|
34
|
+
status: INTERNAL_SERVER_ERROR,
|
|
35
|
+
};
|
|
36
|
+
case PASSWORD_RESET_FAILURE:
|
|
37
|
+
return {
|
|
38
|
+
status: action.payload.errorCode,
|
|
39
|
+
};
|
|
40
|
+
case FORGOT_PASSWORD_PERSIST_FORM_DATA: {
|
|
41
|
+
const { forgotPasswordFormData } = action.payload;
|
|
42
|
+
return {
|
|
43
|
+
...state,
|
|
44
|
+
...forgotPasswordFormData,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
default:
|
|
48
|
+
return {
|
|
49
|
+
...defaultState,
|
|
50
|
+
email: state.email,
|
|
51
|
+
emailValidationError: state.emailValidationError,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return state;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export default reducer;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { logError, logInfo } from '@openedx/frontend-base';
|
|
2
|
+
import { call, put, takeEvery } from 'redux-saga/effects';
|
|
3
|
+
|
|
4
|
+
// Actions
|
|
5
|
+
import {
|
|
6
|
+
FORGOT_PASSWORD,
|
|
7
|
+
forgotPasswordBegin,
|
|
8
|
+
forgotPasswordForbidden,
|
|
9
|
+
forgotPasswordServerError,
|
|
10
|
+
forgotPasswordSuccess,
|
|
11
|
+
} from './actions';
|
|
12
|
+
import { forgotPassword } from './service';
|
|
13
|
+
|
|
14
|
+
// Services
|
|
15
|
+
export function* handleForgotPassword(action) {
|
|
16
|
+
try {
|
|
17
|
+
yield put(forgotPasswordBegin());
|
|
18
|
+
|
|
19
|
+
yield call(forgotPassword, action.payload.email);
|
|
20
|
+
|
|
21
|
+
yield put(forgotPasswordSuccess(action.payload.email));
|
|
22
|
+
} catch (e) {
|
|
23
|
+
if (e.response && e.response.status === 403) {
|
|
24
|
+
yield put(forgotPasswordForbidden());
|
|
25
|
+
logInfo(e);
|
|
26
|
+
} else {
|
|
27
|
+
yield put(forgotPasswordServerError());
|
|
28
|
+
logError(e);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export default function* saga() {
|
|
34
|
+
yield takeEvery(FORGOT_PASSWORD.BASE, handleForgotPassword);
|
|
35
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { createSelector } from 'reselect';
|
|
2
|
+
|
|
3
|
+
export const storeName = 'forgotPassword';
|
|
4
|
+
|
|
5
|
+
export const forgotPasswordSelector = state => ({ ...state[storeName] });
|
|
6
|
+
|
|
7
|
+
export const forgotPasswordResultSelector = createSelector(
|
|
8
|
+
forgotPasswordSelector,
|
|
9
|
+
forgotPassword => forgotPassword,
|
|
10
|
+
);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { getAuthenticatedHttpClient, getSiteConfig } from '@openedx/frontend-base';
|
|
2
|
+
import formurlencoded from 'form-urlencoded';
|
|
3
|
+
|
|
4
|
+
export async function forgotPassword(email) {
|
|
5
|
+
const requestConfig = {
|
|
6
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
7
|
+
isPublic: true,
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const { data } = await getAuthenticatedHttpClient()
|
|
11
|
+
.post(
|
|
12
|
+
`${getSiteConfig().lmsBaseUrl}/account/password`,
|
|
13
|
+
formurlencoded({ email }),
|
|
14
|
+
requestConfig,
|
|
15
|
+
)
|
|
16
|
+
.catch((e) => {
|
|
17
|
+
throw (e);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
return data;
|
|
21
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import {
|
|
2
|
+
FORGOT_PASSWORD_PERSIST_FORM_DATA,
|
|
3
|
+
} from '../actions';
|
|
4
|
+
import reducer from '../reducers';
|
|
5
|
+
|
|
6
|
+
describe('forgot password reducer', () => {
|
|
7
|
+
it('should set email and emailValidationError', () => {
|
|
8
|
+
const state = {
|
|
9
|
+
status: '',
|
|
10
|
+
submitState: '',
|
|
11
|
+
email: '',
|
|
12
|
+
emailValidationError: '',
|
|
13
|
+
};
|
|
14
|
+
const forgotPasswordFormData = {
|
|
15
|
+
email: 'test@gmail',
|
|
16
|
+
emailValidationError: 'Enter a valid email address',
|
|
17
|
+
};
|
|
18
|
+
const action = {
|
|
19
|
+
type: FORGOT_PASSWORD_PERSIST_FORM_DATA,
|
|
20
|
+
payload: { forgotPasswordFormData },
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
expect(
|
|
24
|
+
reducer(state, action),
|
|
25
|
+
).toEqual(
|
|
26
|
+
{
|
|
27
|
+
status: '',
|
|
28
|
+
submitState: '',
|
|
29
|
+
email: 'test@gmail',
|
|
30
|
+
emailValidationError: 'Enter a valid email address',
|
|
31
|
+
},
|
|
32
|
+
);
|
|
33
|
+
});
|
|
34
|
+
});
|