@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.
Files changed (177) hide show
  1. package/LICENSE +661 -0
  2. package/README.rst +204 -0
  3. package/package.json +79 -0
  4. package/src/Main.tsx +23 -0
  5. package/src/__mocks__/file.js +1 -0
  6. package/src/__mocks__/svg.js +1 -0
  7. package/src/app.scss +6 -0
  8. package/src/app.ts +43 -0
  9. package/src/base-container/components/default-layout/DefaultLayout.test.jsx +48 -0
  10. package/src/base-container/components/default-layout/LargeLayout.jsx +42 -0
  11. package/src/base-container/components/default-layout/MediumLayout.jsx +49 -0
  12. package/src/base-container/components/default-layout/SmallLayout.jsx +37 -0
  13. package/src/base-container/components/default-layout/index.jsx +3 -0
  14. package/src/base-container/components/default-layout/messages.js +16 -0
  15. package/src/base-container/components/image-layout/ExtraSmallLayout.jsx +31 -0
  16. package/src/base-container/components/image-layout/LargeLayout.jsx +32 -0
  17. package/src/base-container/components/image-layout/MediumLayout.jsx +32 -0
  18. package/src/base-container/components/image-layout/SmallLayout.jsx +31 -0
  19. package/src/base-container/components/image-layout/index.jsx +4 -0
  20. package/src/base-container/components/image-layout/index.scss +37 -0
  21. package/src/base-container/components/image-layout/messages.js +16 -0
  22. package/src/base-container/components/welcome-page-layout/LargeLayout.jsx +46 -0
  23. package/src/base-container/components/welcome-page-layout/MediumLayout.jsx +49 -0
  24. package/src/base-container/components/welcome-page-layout/SmallLayout.jsx +38 -0
  25. package/src/base-container/components/welcome-page-layout/index.jsx +3 -0
  26. package/src/base-container/components/welcome-page-layout/messages.js +21 -0
  27. package/src/base-container/index.jsx +70 -0
  28. package/src/base-container/tests/BaseContainer.test.jsx +44 -0
  29. package/src/common-components/EmbeddedRegistrationRoute.jsx +26 -0
  30. package/src/common-components/EnterpriseSSO.jsx +110 -0
  31. package/src/common-components/FormGroup.jsx +106 -0
  32. package/src/common-components/InstitutionLogistration.jsx +102 -0
  33. package/src/common-components/NotFoundPage.jsx +15 -0
  34. package/src/common-components/PasswordField.jsx +175 -0
  35. package/src/common-components/RedirectLogistration.jsx +94 -0
  36. package/src/common-components/RegisterFaIcons.jsx +8 -0
  37. package/src/common-components/SocialAuthProviders.jsx +75 -0
  38. package/src/common-components/ThirdPartyAuth.jsx +123 -0
  39. package/src/common-components/ThirdPartyAuthAlert.jsx +49 -0
  40. package/src/common-components/UnAuthOnlyRoute.jsx +41 -0
  41. package/src/common-components/data/actions.js +27 -0
  42. package/src/common-components/data/reducers.js +63 -0
  43. package/src/common-components/data/sagas.js +32 -0
  44. package/src/common-components/data/selectors.js +28 -0
  45. package/src/common-components/data/service.js +23 -0
  46. package/src/common-components/data/tests/reducer.test.js +82 -0
  47. package/src/common-components/data/tests/sagas.test.js +71 -0
  48. package/src/common-components/index.jsx +14 -0
  49. package/src/common-components/messages.jsx +127 -0
  50. package/src/common-components/tests/EmbeddedRegistrationRoute.test.jsx +76 -0
  51. package/src/common-components/tests/FormField.test.jsx +289 -0
  52. package/src/common-components/tests/SocialAuthProviders.test.jsx +75 -0
  53. package/src/common-components/tests/ThirdPartyAuthAlert.test.jsx +39 -0
  54. package/src/common-components/tests/UnAuthOnlyRoute.test.jsx +73 -0
  55. package/src/common-components/tests/__snapshots__/SocialAuthProviders.test.jsx.snap +156 -0
  56. package/src/common-components/tests/__snapshots__/ThirdPartyAuthAlert.test.jsx.snap +53 -0
  57. package/src/constants.ts +1 -0
  58. package/src/data/configureStore.js +33 -0
  59. package/src/data/constants.js +38 -0
  60. package/src/data/countries.ts +59 -0
  61. package/src/data/oneTrust.js +3 -0
  62. package/src/data/reducers.js +36 -0
  63. package/src/data/sagas.js +19 -0
  64. package/src/data/tests/cookies.test.js +42 -0
  65. package/src/data/tests/dataUtils.test.js +19 -0
  66. package/src/data/tests/reduxUtils.test.js +14 -0
  67. package/src/data/utils/cookies.js +12 -0
  68. package/src/data/utils/dataUtils.js +81 -0
  69. package/src/data/utils/index.js +11 -0
  70. package/src/data/utils/reduxUtils.js +34 -0
  71. package/src/data/utils/useMobileResponsive.js +24 -0
  72. package/src/field-renderer/FieldRenderer.jsx +156 -0
  73. package/src/field-renderer/index.jsx +2 -0
  74. package/src/field-renderer/tests/FieldRenderer.test.jsx +201 -0
  75. package/src/forgot-password/ForgotPasswordAlert.jsx +97 -0
  76. package/src/forgot-password/ForgotPasswordPage.jsx +193 -0
  77. package/src/forgot-password/data/actions.js +32 -0
  78. package/src/forgot-password/data/reducers.js +58 -0
  79. package/src/forgot-password/data/sagas.js +35 -0
  80. package/src/forgot-password/data/selectors.js +10 -0
  81. package/src/forgot-password/data/service.js +21 -0
  82. package/src/forgot-password/data/tests/reducers.test.js +34 -0
  83. package/src/forgot-password/data/tests/sagas.test.js +67 -0
  84. package/src/forgot-password/index.js +5 -0
  85. package/src/forgot-password/messages.js +128 -0
  86. package/src/forgot-password/tests/ForgotPasswordPage.test.jsx +269 -0
  87. package/src/i18n/index.js +25 -0
  88. package/src/index.ts +3 -0
  89. package/src/login/AccountActivationMessage.jsx +78 -0
  90. package/src/login/ChangePasswordPrompt.jsx +91 -0
  91. package/src/login/LoginFailure.jsx +231 -0
  92. package/src/login/LoginPage.jsx +368 -0
  93. package/src/login/data/actions.js +39 -0
  94. package/src/login/data/constants.js +20 -0
  95. package/src/login/data/reducers.js +76 -0
  96. package/src/login/data/sagas.js +45 -0
  97. package/src/login/data/service.js +24 -0
  98. package/src/login/data/tests/reducers.test.js +155 -0
  99. package/src/login/data/tests/sagas.test.js +110 -0
  100. package/src/login/index.js +5 -0
  101. package/src/login/messages.jsx +211 -0
  102. package/src/login/tests/AccountActivationMessage.test.jsx +122 -0
  103. package/src/login/tests/ChangePasswordPrompt.test.jsx +75 -0
  104. package/src/login/tests/LoginFailure.test.jsx +323 -0
  105. package/src/login/tests/LoginPage.test.jsx +839 -0
  106. package/src/logistration/Logistration.jsx +189 -0
  107. package/src/logistration/Logistration.test.jsx +275 -0
  108. package/src/progressive-profiling/ProgressiveProfiling.jsx +313 -0
  109. package/src/progressive-profiling/ProgressiveProfilingPageModal.jsx +56 -0
  110. package/src/progressive-profiling/data/actions.js +22 -0
  111. package/src/progressive-profiling/data/reducers.js +38 -0
  112. package/src/progressive-profiling/data/sagas.js +24 -0
  113. package/src/progressive-profiling/data/selectors.js +14 -0
  114. package/src/progressive-profiling/data/service.js +17 -0
  115. package/src/progressive-profiling/index.js +5 -0
  116. package/src/progressive-profiling/messages.jsx +57 -0
  117. package/src/progressive-profiling/tests/ProgressiveProfiling.test.jsx +322 -0
  118. package/src/register/RegistrationFields/CountryField/CountryField.jsx +164 -0
  119. package/src/register/RegistrationFields/CountryField/CountryField.test.jsx +204 -0
  120. package/src/register/RegistrationFields/CountryField/validator.js +32 -0
  121. package/src/register/RegistrationFields/EmailField/EmailField.jsx +140 -0
  122. package/src/register/RegistrationFields/EmailField/EmailField.test.jsx +236 -0
  123. package/src/register/RegistrationFields/EmailField/constants.js +131 -0
  124. package/src/register/RegistrationFields/EmailField/validator.js +126 -0
  125. package/src/register/RegistrationFields/EmailField/validator.test.js +39 -0
  126. package/src/register/RegistrationFields/HonorCodeField/HonorCode.jsx +104 -0
  127. package/src/register/RegistrationFields/HonorCodeField/HonorCode.test.jsx +70 -0
  128. package/src/register/RegistrationFields/NameField/NameField.jsx +68 -0
  129. package/src/register/RegistrationFields/NameField/NameField.test.jsx +146 -0
  130. package/src/register/RegistrationFields/NameField/validator.js +22 -0
  131. package/src/register/RegistrationFields/TermsOfServiceField/TermsOfService.jsx +58 -0
  132. package/src/register/RegistrationFields/TermsOfServiceField/TermsOfService.test.jsx +55 -0
  133. package/src/register/RegistrationFields/UsernameField/UsernameField.jsx +162 -0
  134. package/src/register/RegistrationFields/UsernameField/UsernameField.test.jsx +319 -0
  135. package/src/register/RegistrationFields/UsernameField/validator.js +16 -0
  136. package/src/register/RegistrationFields/index.js +6 -0
  137. package/src/register/RegistrationPage.jsx +428 -0
  138. package/src/register/RegistrationPage.test.jsx +854 -0
  139. package/src/register/components/ConfigurableRegistrationForm.jsx +244 -0
  140. package/src/register/components/RegistrationFailure.jsx +80 -0
  141. package/src/register/components/tests/ConfigurableRegistrationForm.test.jsx +401 -0
  142. package/src/register/components/tests/RegistrationFailure.test.jsx +212 -0
  143. package/src/register/components/tests/ThirdPartyAuth.test.jsx +439 -0
  144. package/src/register/data/actions.js +85 -0
  145. package/src/register/data/constants.js +13 -0
  146. package/src/register/data/reducers.js +140 -0
  147. package/src/register/data/sagas.js +58 -0
  148. package/src/register/data/selectors.js +34 -0
  149. package/src/register/data/service.js +45 -0
  150. package/src/register/data/tests/reducers.test.js +277 -0
  151. package/src/register/data/tests/sagas.test.js +239 -0
  152. package/src/register/data/tests/utils.test.js +98 -0
  153. package/src/register/data/utils.js +153 -0
  154. package/src/register/index.js +4 -0
  155. package/src/register/messages.jsx +206 -0
  156. package/src/reset-password/ResetPasswordFailure.jsx +55 -0
  157. package/src/reset-password/ResetPasswordPage.jsx +224 -0
  158. package/src/reset-password/ResetPasswordSuccess.jsx +19 -0
  159. package/src/reset-password/data/actions.js +50 -0
  160. package/src/reset-password/data/constants.js +16 -0
  161. package/src/reset-password/data/reducers.js +44 -0
  162. package/src/reset-password/data/sagas.js +67 -0
  163. package/src/reset-password/data/selectors.js +10 -0
  164. package/src/reset-password/data/service.js +61 -0
  165. package/src/reset-password/data/tests/sagas.test.js +185 -0
  166. package/src/reset-password/index.js +5 -0
  167. package/src/reset-password/messages.js +88 -0
  168. package/src/reset-password/tests/ResetPasswordPage.test.jsx +241 -0
  169. package/src/routes.jsx +79 -0
  170. package/src/sass/_base_component.scss +202 -0
  171. package/src/sass/_forgot_password.scss +3 -0
  172. package/src/sass/_login_page.scss +3 -0
  173. package/src/sass/_progressive_profiling_page.scss +30 -0
  174. package/src/sass/_registration.scss +123 -0
  175. package/src/sass/_reset_password.scss +3 -0
  176. package/src/sass/_style.scss +465 -0
  177. package/src/setupTest.js +68 -0
@@ -0,0 +1,224 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { connect } from 'react-redux';
3
+
4
+ import { getSiteConfig, useIntl } from '@openedx/frontend-base';
5
+ import {
6
+ Form,
7
+ Icon,
8
+ Spinner,
9
+ StatefulButton,
10
+ Tab,
11
+ Tabs,
12
+ } from '@openedx/paragon';
13
+ import { ChevronLeft } from '@openedx/paragon/icons';
14
+ import PropTypes from 'prop-types';
15
+ import { Helmet } from 'react-helmet';
16
+ import { useNavigate, useParams } from 'react-router-dom';
17
+
18
+ import BaseContainer from '../base-container';
19
+ import { PasswordField } from '../common-components';
20
+ import {
21
+ LETTER_REGEX, LOGIN_PAGE, NUMBER_REGEX, RESET_PAGE,
22
+ } from '../data/constants';
23
+ import { getAllPossibleQueryParams, updatePathWithQueryParams, windowScrollTo } from '../data/utils';
24
+ import { resetPassword, validateToken } from './data/actions';
25
+ import {
26
+ FORM_SUBMISSION_ERROR, PASSWORD_RESET_ERROR, PASSWORD_VALIDATION_ERROR, TOKEN_STATE,
27
+ } from './data/constants';
28
+ import { resetPasswordResultSelector } from './data/selectors';
29
+ import { validatePassword } from './data/service';
30
+ import messages from './messages';
31
+ import ResetPasswordFailure from './ResetPasswordFailure';
32
+
33
+ const ResetPasswordPage = (props) => {
34
+ const { formatMessage } = useIntl();
35
+ const newPasswordError = formatMessage(messages['password.validation.message']);
36
+
37
+ const [newPassword, setNewPassword] = useState('');
38
+ const [confirmPassword, setConfirmPassword] = useState('');
39
+ const [formErrors, setFormErrors] = useState({});
40
+ const [errorCode, setErrorCode] = useState(null);
41
+ const { token } = useParams();
42
+ const navigate = useNavigate();
43
+
44
+ useEffect(() => {
45
+ if (props.status === PASSWORD_RESET_ERROR) {
46
+ navigate(updatePathWithQueryParams(RESET_PAGE));
47
+ }
48
+ if (props.status === 'success') {
49
+ navigate(updatePathWithQueryParams(LOGIN_PAGE));
50
+ }
51
+ if (props.status !== TOKEN_STATE.PENDING) {
52
+ setErrorCode(props.status);
53
+ }
54
+ if (props.status === PASSWORD_VALIDATION_ERROR) {
55
+ setFormErrors({ newPassword: newPasswordError });
56
+ }
57
+ }, [props.status]);
58
+
59
+ const validatePasswordFromBackend = async (password) => {
60
+ let errorMessage = '';
61
+ try {
62
+ const payload = {
63
+ reset_password_page: true,
64
+ password,
65
+ };
66
+ errorMessage = await validatePassword(payload);
67
+ } catch (err) {
68
+ errorMessage = '';
69
+ }
70
+ setFormErrors({ ...formErrors, newPassword: errorMessage });
71
+ };
72
+
73
+ const validateInput = (name, value) => {
74
+ switch (name) {
75
+ case 'newPassword':
76
+ if (!value || !LETTER_REGEX.test(value) || !NUMBER_REGEX.test(value) || value.length < 8) {
77
+ formErrors.newPassword = formatMessage(messages['password.validation.message']);
78
+ } else {
79
+ validatePasswordFromBackend(value);
80
+ }
81
+ break;
82
+ case 'confirmPassword':
83
+ if (!value) {
84
+ formErrors.confirmPassword = formatMessage(messages['confirm.your.password']);
85
+ } else if (value !== newPassword) {
86
+ formErrors.confirmPassword = formatMessage(messages['passwords.do.not.match']);
87
+ } else {
88
+ formErrors.confirmPassword = '';
89
+ }
90
+ break;
91
+ default:
92
+ break;
93
+ }
94
+ setFormErrors({ ...formErrors });
95
+ return !Object.values(formErrors).some(x => (x !== ''));
96
+ };
97
+
98
+ const handleOnBlur = (event) => {
99
+ const { name, value } = event.target;
100
+ validateInput(name, value);
101
+ };
102
+
103
+ const handleConfirmPasswordChange = (e) => {
104
+ const { value } = e.target;
105
+
106
+ setConfirmPassword(value);
107
+ validateInput('confirmPassword', value);
108
+ };
109
+
110
+ const handleOnFocus = (e) => {
111
+ setFormErrors({ ...formErrors, [e.target.name]: '' });
112
+ };
113
+
114
+ const handleSubmit = (e) => {
115
+ e.preventDefault();
116
+
117
+ const isPasswordValid = validateInput('newPassword', newPassword);
118
+ const isPasswordConfirmed = validateInput('confirmPassword', confirmPassword);
119
+
120
+ if (isPasswordValid && isPasswordConfirmed) {
121
+ const formPayload = {
122
+ new_password1: newPassword,
123
+ new_password2: confirmPassword,
124
+ };
125
+ const params = getAllPossibleQueryParams();
126
+ props.resetPassword(formPayload, props.token, params);
127
+ } else {
128
+ setErrorCode(FORM_SUBMISSION_ERROR);
129
+ windowScrollTo({ left: 0, top: 0, behavior: 'smooth' });
130
+ }
131
+ };
132
+
133
+ const tabTitle = (
134
+ <div className="d-inline-flex flex-wrap align-items-center">
135
+ <Icon src={ChevronLeft} />
136
+ <span className="ml-2">{formatMessage(messages['sign.in'])}</span>
137
+ </div>
138
+ );
139
+
140
+ if (props.status === TOKEN_STATE.PENDING && token) {
141
+ props.validateToken(token);
142
+ return (
143
+ <Spinner animation="border" variant="primary" className="spinner--position-centered" />
144
+ );
145
+ } else {
146
+ return (
147
+ <BaseContainer>
148
+ <div>
149
+ <Helmet>
150
+ <title>
151
+ {formatMessage(messages['reset.password.page.title'], { siteName: getSiteConfig().siteName })}
152
+ </title>
153
+ </Helmet>
154
+ <Tabs activeKey="" id="controlled-tab" onSelect={(key) => navigate(updatePathWithQueryParams(key))}>
155
+ <Tab title={tabTitle} eventKey={LOGIN_PAGE} />
156
+ </Tabs>
157
+ <div id="main-content" className="main-content">
158
+ <div className="mw-xs">
159
+ <ResetPasswordFailure errorCode={errorCode} errorMsg={props.errorMsg} />
160
+ <h4>{formatMessage(messages['reset.password'])}</h4>
161
+ <p className="mb-4">{formatMessage(messages['reset.password.page.instructions'])}</p>
162
+ <Form id="set-reset-password-form" name="set-reset-password-form">
163
+ <PasswordField
164
+ name="newPassword"
165
+ value={newPassword}
166
+ handleChange={(e) => setNewPassword(e.target.value)}
167
+ handleBlur={handleOnBlur}
168
+ handleFocus={handleOnFocus}
169
+ errorMessage={formErrors.newPassword}
170
+ floatingLabel={formatMessage(messages['new.password.label'])}
171
+ />
172
+ <PasswordField
173
+ name="confirmPassword"
174
+ value={confirmPassword}
175
+ handleChange={handleConfirmPasswordChange}
176
+ handleFocus={handleOnFocus}
177
+ errorMessage={formErrors.confirmPassword}
178
+ showRequirements={false}
179
+ floatingLabel={formatMessage(messages['confirm.password.label'])}
180
+ />
181
+ <StatefulButton
182
+ id="submit-new-password"
183
+ name="submit-new-password"
184
+ type="submit"
185
+ variant="brand"
186
+ className="reset-password--button"
187
+ state={props.status}
188
+ labels={{
189
+ default: formatMessage(messages['reset.password']),
190
+ pending: '',
191
+ }}
192
+ onClick={e => handleSubmit(e)}
193
+ onMouseDown={(e) => e.preventDefault()}
194
+ />
195
+ </Form>
196
+ </div>
197
+ </div>
198
+ </div>
199
+ </BaseContainer>
200
+ );
201
+ }
202
+ };
203
+
204
+ ResetPasswordPage.defaultProps = {
205
+ status: null,
206
+ token: null,
207
+ errorMsg: null,
208
+ };
209
+
210
+ ResetPasswordPage.propTypes = {
211
+ resetPassword: PropTypes.func.isRequired,
212
+ validateToken: PropTypes.func.isRequired,
213
+ token: PropTypes.string,
214
+ status: PropTypes.string,
215
+ errorMsg: PropTypes.string,
216
+ };
217
+
218
+ export default connect(
219
+ resetPasswordResultSelector,
220
+ {
221
+ resetPassword,
222
+ validateToken,
223
+ },
224
+ )(ResetPasswordPage);
@@ -0,0 +1,19 @@
1
+ import { useIntl } from '@openedx/frontend-base';
2
+ import { Alert } from '@openedx/paragon';
3
+
4
+ import messages from './messages';
5
+
6
+ const ResetPasswordSuccess = () => {
7
+ const { formatMessage } = useIntl();
8
+
9
+ return (
10
+ <Alert id="reset-password-success" variant="success" className="mb-5">
11
+ <Alert.Heading>
12
+ {formatMessage(messages['reset.password.success.heading'])}
13
+ </Alert.Heading>
14
+ <p>{formatMessage(messages['reset.password.success'])}</p>
15
+ </Alert>
16
+ );
17
+ };
18
+
19
+ export default ResetPasswordSuccess;
@@ -0,0 +1,50 @@
1
+ import { AsyncActionType } from '../../data/utils';
2
+
3
+ export const RESET_PASSWORD = new AsyncActionType('RESET', 'PASSWORD');
4
+ export const VALIDATE_TOKEN = new AsyncActionType('VALIDATE', 'TOKEN');
5
+ export const PASSWORD_RESET_FAILURE = 'PASSWORD_RESET_FAILURE';
6
+
7
+ export const passwordResetFailure = (errorCode) => ({
8
+ type: PASSWORD_RESET_FAILURE,
9
+ payload: { errorCode },
10
+ });
11
+
12
+ // Validate confirmation token
13
+ export const validateToken = (token) => ({
14
+ type: VALIDATE_TOKEN.BASE,
15
+ payload: { token },
16
+ });
17
+
18
+ export const validateTokenBegin = () => ({
19
+ type: VALIDATE_TOKEN.BEGIN,
20
+ });
21
+
22
+ export const validateTokenSuccess = (tokenStatus, token) => ({
23
+ type: VALIDATE_TOKEN.SUCCESS,
24
+ payload: { tokenStatus, token },
25
+ });
26
+
27
+ export const validateTokenFailure = errorCode => ({
28
+ type: VALIDATE_TOKEN.FAILURE,
29
+ payload: { errorCode },
30
+ });
31
+
32
+ // Reset Password
33
+ export const resetPassword = (formPayload, token, params) => ({
34
+ type: RESET_PASSWORD.BASE,
35
+ payload: { formPayload, token, params },
36
+ });
37
+
38
+ export const resetPasswordBegin = () => ({
39
+ type: RESET_PASSWORD.BEGIN,
40
+ });
41
+
42
+ export const resetPasswordSuccess = data => ({
43
+ type: RESET_PASSWORD.SUCCESS,
44
+ payload: { data },
45
+ });
46
+
47
+ export const resetPasswordFailure = (errorCode, errorMsg = null) => ({
48
+ type: RESET_PASSWORD.FAILURE,
49
+ payload: { errorCode, errorMsg: errorMsg ?? errorCode },
50
+ });
@@ -0,0 +1,16 @@
1
+ export const TOKEN_STATE = {
2
+ PENDING: 'token-pending',
3
+ VALID: 'token-valid',
4
+ };
5
+
6
+ // password reset error codes
7
+ export const FORM_SUBMISSION_ERROR = 'form-submission-error';
8
+ export const PASSWORD_RESET_ERROR = 'password-reset-error';
9
+ export const SUCCESS = 'success';
10
+ export const PASSWORD_VALIDATION_ERROR = 'password-validation-failure';
11
+
12
+ export const PASSWORD_RESET = {
13
+ INVALID_TOKEN: 'invalid-token',
14
+ INTERNAL_SERVER_ERROR: 'password-reset-internal-server-error',
15
+ FORBIDDEN_REQUEST: 'password-reset-rate-limit-error',
16
+ };
@@ -0,0 +1,44 @@
1
+ import { PASSWORD_RESET_FAILURE, RESET_PASSWORD, VALIDATE_TOKEN } from './actions';
2
+ import { PASSWORD_RESET_ERROR, TOKEN_STATE } from './constants';
3
+
4
+ export const defaultState = {
5
+ status: TOKEN_STATE.PENDING,
6
+ token: null,
7
+ errorMsg: null,
8
+ };
9
+
10
+ const reducer = (state = defaultState, action = null) => {
11
+ switch (action.type) {
12
+ case VALIDATE_TOKEN.SUCCESS:
13
+ return {
14
+ ...state,
15
+ status: TOKEN_STATE.VALID,
16
+ token: action.payload.token,
17
+ };
18
+ case PASSWORD_RESET_FAILURE:
19
+ return {
20
+ ...state,
21
+ status: PASSWORD_RESET_ERROR,
22
+ };
23
+ case RESET_PASSWORD.BEGIN:
24
+ return {
25
+ ...state,
26
+ status: 'pending',
27
+ };
28
+ case RESET_PASSWORD.SUCCESS:
29
+ return {
30
+ ...state,
31
+ status: 'success',
32
+ };
33
+ case RESET_PASSWORD.FAILURE:
34
+ return {
35
+ ...state,
36
+ status: action.payload.errorCode,
37
+ errorMsg: action.payload.errorMsg,
38
+ };
39
+ default:
40
+ return state;
41
+ }
42
+ };
43
+
44
+ export default reducer;
@@ -0,0 +1,67 @@
1
+ import { logError, logInfo } from '@openedx/frontend-base';
2
+ import { call, put, takeEvery } from 'redux-saga/effects';
3
+
4
+ import {
5
+ passwordResetFailure,
6
+ RESET_PASSWORD,
7
+ resetPasswordBegin,
8
+ resetPasswordFailure,
9
+ resetPasswordSuccess,
10
+ VALIDATE_TOKEN,
11
+ validateTokenBegin,
12
+ validateTokenSuccess,
13
+ } from './actions';
14
+ import { PASSWORD_RESET, PASSWORD_VALIDATION_ERROR } from './constants';
15
+ import { resetPassword, validateToken } from './service';
16
+
17
+ // Services
18
+ export function* handleValidateToken(action) {
19
+ try {
20
+ yield put(validateTokenBegin());
21
+ const data = yield call(validateToken, action.payload.token);
22
+ const isValid = data.is_valid;
23
+ if (isValid) {
24
+ yield put(validateTokenSuccess(isValid, action.payload.token));
25
+ } else {
26
+ yield put(passwordResetFailure(PASSWORD_RESET.INVALID_TOKEN));
27
+ }
28
+ } catch (err) {
29
+ if (err.response && err.response.status === 429) {
30
+ yield put(passwordResetFailure(PASSWORD_RESET.FORBIDDEN_REQUEST));
31
+ logInfo(err);
32
+ } else {
33
+ yield put(passwordResetFailure(PASSWORD_RESET.INTERNAL_SERVER_ERROR));
34
+ logError(err);
35
+ }
36
+ }
37
+ }
38
+
39
+ export function* handleResetPassword(action) {
40
+ try {
41
+ yield put(resetPasswordBegin());
42
+ const data = yield call(resetPassword, action.payload.formPayload, action.payload.token, action.payload.params);
43
+ const resetStatus = data.reset_status;
44
+ const resetErrors = data.err_msg;
45
+
46
+ if (resetStatus) {
47
+ yield put(resetPasswordSuccess(resetStatus));
48
+ } else if (data.token_invalid) {
49
+ yield put(passwordResetFailure(PASSWORD_RESET.INVALID_TOKEN));
50
+ } else {
51
+ yield put(resetPasswordFailure(PASSWORD_VALIDATION_ERROR, resetErrors));
52
+ }
53
+ } catch (err) {
54
+ if (err.response && err.response.status === 429) {
55
+ yield put(resetPasswordFailure(PASSWORD_RESET.FORBIDDEN_REQUEST));
56
+ logInfo(err);
57
+ } else {
58
+ yield put(resetPasswordFailure(PASSWORD_RESET.INTERNAL_SERVER_ERROR));
59
+ logError(err);
60
+ }
61
+ }
62
+ }
63
+
64
+ export default function* saga() {
65
+ yield takeEvery(RESET_PASSWORD.BASE, handleResetPassword);
66
+ yield takeEvery(VALIDATE_TOKEN.BASE, handleValidateToken);
67
+ }
@@ -0,0 +1,10 @@
1
+ import { createSelector } from 'reselect';
2
+
3
+ export const storeName = 'resetPassword';
4
+
5
+ export const resetPasswordSelector = state => ({ ...state[storeName] });
6
+
7
+ export const resetPasswordResultSelector = createSelector(
8
+ resetPasswordSelector,
9
+ resetPassword => resetPassword,
10
+ );
@@ -0,0 +1,61 @@
1
+ import { getSiteConfig, getHttpClient } from '@openedx/frontend-base';
2
+ import formurlencoded from 'form-urlencoded';
3
+
4
+ export async function validateToken(token) {
5
+ const requestConfig = {
6
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
7
+ };
8
+
9
+ const { data } = await getHttpClient()
10
+ .post(
11
+ `${getSiteConfig().lmsBaseUrl}/user_api/v1/account/password_reset/token/validate/`,
12
+ formurlencoded({ token }),
13
+ requestConfig,
14
+ )
15
+ .catch((e) => {
16
+ throw (e);
17
+ });
18
+ return data;
19
+ }
20
+
21
+ export async function resetPassword(payload, token, queryParams) {
22
+ const requestConfig = {
23
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
24
+ };
25
+ const url = new URL(`${getSiteConfig().lmsBaseUrl}/password/reset/${token}/`);
26
+
27
+ if (queryParams.is_account_recovery) {
28
+ url.searchParams.append('is_account_recovery', true);
29
+ }
30
+
31
+ const { data } = await getHttpClient()
32
+ .post(url.href, formurlencoded(payload), requestConfig)
33
+ .catch((e) => {
34
+ throw (e);
35
+ });
36
+ return data;
37
+ }
38
+
39
+ export async function validatePassword(payload) {
40
+ const requestConfig = {
41
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
42
+ };
43
+ const { data } = await getHttpClient()
44
+ .post(
45
+ `${getSiteConfig().lmsBaseUrl}/api/user/v1/validation/registration`,
46
+ formurlencoded(payload),
47
+ requestConfig,
48
+ )
49
+ .catch((e) => {
50
+ throw (e);
51
+ });
52
+
53
+ let errorMessage = '';
54
+ // Be careful about grabbing this message, since we could have received an HTTP error or the
55
+ // endpoint didn't give us what we expect. We only care if we get a clear error message.
56
+ if (data.validation_decisions?.password) {
57
+ errorMessage = data.validation_decisions.password;
58
+ }
59
+
60
+ return errorMessage;
61
+ }
@@ -0,0 +1,185 @@
1
+ import { runSaga } from 'redux-saga';
2
+
3
+ import { initializeMockServices } from '../../../setupTest';
4
+ import {
5
+ passwordResetFailure,
6
+ resetPasswordBegin,
7
+ resetPasswordFailure,
8
+ resetPasswordSuccess, validateTokenBegin,
9
+ } from '../actions';
10
+ import { PASSWORD_RESET } from '../constants';
11
+ import { handleResetPassword, handleValidateToken } from '../sagas';
12
+ import * as api from '../service';
13
+
14
+ const { loggingService } = initializeMockServices();
15
+
16
+ describe('handleResetPassword', () => {
17
+ const params = {
18
+ payload: {
19
+ formPayload: {
20
+ new_password1: 'new_password1',
21
+ new_password2: 'new_password1',
22
+ },
23
+ token: 'token',
24
+ params: {},
25
+ },
26
+ };
27
+
28
+ const responseData = {
29
+ reset_status: true,
30
+ err_msg: '',
31
+ };
32
+
33
+ beforeEach(() => {
34
+ loggingService.logError.mockReset();
35
+ loggingService.logInfo.mockReset();
36
+ });
37
+
38
+ it('should call service and dispatch success action', async () => {
39
+ const resetPassword = jest.spyOn(api, 'resetPassword')
40
+ .mockImplementation(() => Promise.resolve(responseData));
41
+
42
+ const dispatched = [];
43
+ await runSaga(
44
+ { dispatch: (action) => dispatched.push(action) },
45
+ handleResetPassword,
46
+ params,
47
+ );
48
+
49
+ expect(resetPassword).toHaveBeenCalledTimes(1);
50
+ expect(dispatched).toEqual([resetPasswordBegin(), resetPasswordSuccess(true)]);
51
+ resetPassword.mockClear();
52
+ });
53
+
54
+ it('should call service and dispatch internal server error action', async () => {
55
+ const errorResponse = {
56
+ response: {
57
+ status: 500,
58
+ data: {
59
+ errorCode: PASSWORD_RESET.INTERNAL_SERVER_ERROR,
60
+ },
61
+ },
62
+ };
63
+ const resetPassword = jest.spyOn(api, 'resetPassword')
64
+ .mockImplementation(() => Promise.reject(errorResponse));
65
+
66
+ const dispatched = [];
67
+ await runSaga(
68
+ { dispatch: (action) => dispatched.push(action) },
69
+ handleResetPassword,
70
+ params,
71
+ );
72
+
73
+ expect(loggingService.logError).toHaveBeenCalled();
74
+ expect(resetPassword).toHaveBeenCalledTimes(1);
75
+ expect(dispatched).toEqual([resetPasswordBegin(), resetPasswordFailure(PASSWORD_RESET.INTERNAL_SERVER_ERROR)]);
76
+ resetPassword.mockClear();
77
+ });
78
+
79
+ it('should call service and dispatch invalid token error', async () => {
80
+ responseData.reset_status = false;
81
+ responseData.token_invalid = true;
82
+
83
+ const resetPassword = jest.spyOn(api, 'resetPassword')
84
+ .mockImplementation(() => Promise.resolve(responseData));
85
+
86
+ const dispatched = [];
87
+ await runSaga(
88
+ { dispatch: (action) => dispatched.push(action) },
89
+ handleResetPassword,
90
+ params,
91
+ );
92
+
93
+ expect(resetPassword).toHaveBeenCalledTimes(1);
94
+ expect(dispatched).toEqual([resetPasswordBegin(), passwordResetFailure(PASSWORD_RESET.INVALID_TOKEN)]);
95
+ resetPassword.mockClear();
96
+ });
97
+
98
+ it('should call service and dispatch ratelimit error', async () => {
99
+ const errorResponse = {
100
+ response: {
101
+ status: 429,
102
+ data: {
103
+ errorCode: PASSWORD_RESET.FORBIDDEN_REQUEST,
104
+ },
105
+ },
106
+ };
107
+ const resetPassword = jest.spyOn(api, 'resetPassword')
108
+ .mockImplementation(() => Promise.reject(errorResponse));
109
+
110
+ const dispatched = [];
111
+ await runSaga(
112
+ { dispatch: (action) => dispatched.push(action) },
113
+ handleResetPassword,
114
+ params,
115
+ );
116
+
117
+ expect(loggingService.logInfo).toHaveBeenCalled();
118
+ expect(resetPassword).toHaveBeenCalledTimes(1);
119
+ expect(dispatched).toEqual([resetPasswordBegin(), resetPasswordFailure(PASSWORD_RESET.FORBIDDEN_REQUEST)]);
120
+ resetPassword.mockClear();
121
+ });
122
+ });
123
+
124
+ describe('handleValidateToken', () => {
125
+ const params = {
126
+ payload: {
127
+ token: 'token',
128
+ params: {},
129
+ },
130
+ };
131
+
132
+ beforeEach(() => {
133
+ loggingService.logError.mockReset();
134
+ loggingService.logInfo.mockReset();
135
+ });
136
+
137
+ it('check internal server error on api failure', async () => {
138
+ const errorResponse = {
139
+ response: {
140
+ status: 500,
141
+ data: {
142
+ errorCode: PASSWORD_RESET.INTERNAL_SERVER_ERROR,
143
+ },
144
+ },
145
+ };
146
+ const validateToken = jest.spyOn(api, 'validateToken')
147
+ .mockImplementation(() => Promise.reject(errorResponse));
148
+
149
+ const dispatched = [];
150
+ await runSaga(
151
+ { dispatch: (action) => dispatched.push(action) },
152
+ handleValidateToken,
153
+ params,
154
+ );
155
+
156
+ expect(validateToken).toHaveBeenCalledTimes(1);
157
+ expect(dispatched).toEqual([validateTokenBegin(), passwordResetFailure(PASSWORD_RESET.INTERNAL_SERVER_ERROR)]);
158
+ validateToken.mockClear();
159
+ });
160
+
161
+ it('should call service and dispatch rate limit error', async () => {
162
+ const errorResponse = {
163
+ response: {
164
+ status: 429,
165
+ data: {
166
+ errorCode: PASSWORD_RESET.FORBIDDEN_REQUEST,
167
+ },
168
+ },
169
+ };
170
+ const validateToken = jest.spyOn(api, 'validateToken')
171
+ .mockImplementation(() => Promise.reject(errorResponse));
172
+
173
+ const dispatched = [];
174
+ await runSaga(
175
+ { dispatch: (action) => dispatched.push(action) },
176
+ handleValidateToken,
177
+ params,
178
+ );
179
+
180
+ expect(loggingService.logInfo).toHaveBeenCalled();
181
+ expect(validateToken).toHaveBeenCalledTimes(1);
182
+ expect(dispatched).toEqual([validateTokenBegin(), passwordResetFailure(PASSWORD_RESET.FORBIDDEN_REQUEST)]);
183
+ validateToken.mockClear();
184
+ });
185
+ });
@@ -0,0 +1,5 @@
1
+ export { default as ResetPasswordPage } from './ResetPasswordPage';
2
+ export { default as reducer } from './data/reducers';
3
+ export { RESET_PASSWORD } from './data/actions';
4
+ export { default as saga } from './data/sagas';
5
+ export { storeName } from './data/selectors';