@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,102 @@
1
+ import { getSiteConfig, useIntl } from '@openedx/frontend-base';
2
+ import { Button, Hyperlink, Icon } from '@openedx/paragon';
3
+ import { Institution } from '@openedx/paragon/icons';
4
+ import PropTypes from 'prop-types';
5
+
6
+ import messages from './messages';
7
+
8
+ /**
9
+ * This component renders the Institution login button
10
+ * */
11
+ export const RenderInstitutionButton = props => {
12
+ const { onSubmitHandler, buttonTitle } = props;
13
+
14
+ return (
15
+ <Button
16
+ className="btn-sm text-body p-0 mb-4 border-0"
17
+ variant="link"
18
+ data-event-name="institution_login"
19
+ onClick={onSubmitHandler}
20
+ >
21
+ <Icon src={Institution} className="institute-icon" />
22
+ {buttonTitle}
23
+ </Button>
24
+ );
25
+ };
26
+
27
+ /**
28
+ * This component renders the page list of available institutions for login
29
+ * */
30
+ const InstitutionLogistration = props => {
31
+ const lmsBaseUrl = getSiteConfig().lmsBaseUrl;
32
+ const { formatMessage } = useIntl();
33
+ const {
34
+ secondaryProviders,
35
+ headingTitle,
36
+ } = props;
37
+
38
+ return (
39
+ <>
40
+ <div className="d-flex justify-content-left mb-4 mt-2">
41
+ <div className="flex-column">
42
+ <h4 className="mb-2 font-weight-bold institutions__heading">
43
+ {headingTitle}
44
+ </h4>
45
+ <p className="mb-2">
46
+ {formatMessage(messages['institution.login.page.sub.heading'])}
47
+ </p>
48
+ </div>
49
+ </div>
50
+ <div className="mb-5">
51
+ <table className="pgn__data-table table-striped table-borderless">
52
+ <tbody>
53
+ {secondaryProviders.map(provider => (
54
+ <tr key={provider} className="pgn__data-table-row">
55
+ <td>
56
+ <Hyperlink
57
+ className="btn nav-item p-0 mb-1 institutions--provider-link"
58
+ destination={lmsBaseUrl + provider.loginUrl}
59
+ >
60
+ {provider.name}
61
+ </Hyperlink>
62
+ </td>
63
+ </tr>
64
+ ))}
65
+ </tbody>
66
+ </table>
67
+ </div>
68
+ </>
69
+ );
70
+ };
71
+
72
+ const LogistrationDefaultProps = {
73
+ secondaryProviders: [],
74
+ buttonTitle: '',
75
+ };
76
+ const LogistrationProps = {
77
+ secondaryProviders: PropTypes.arrayOf(PropTypes.shape({
78
+ name: PropTypes.string.isRequired,
79
+ loginUrl: PropTypes.string.isRequired,
80
+ })),
81
+ };
82
+
83
+ RenderInstitutionButton.propTypes = {
84
+ ...LogistrationProps,
85
+ buttonTitle: PropTypes.string,
86
+ onSubmitHandler: PropTypes.func.isRequired,
87
+
88
+ };
89
+ RenderInstitutionButton.defaultProps = {
90
+ ...LogistrationDefaultProps,
91
+ };
92
+
93
+ InstitutionLogistration.propTypes = {
94
+ ...LogistrationProps,
95
+ headingTitle: PropTypes.string,
96
+ };
97
+ InstitutionLogistration.defaultProps = {
98
+ ...LogistrationDefaultProps,
99
+ headingTitle: '',
100
+ };
101
+
102
+ export default InstitutionLogistration;
@@ -0,0 +1,15 @@
1
+ import { FormattedMessage } from '@openedx/frontend-base';
2
+
3
+ const NotFoundPage = () => (
4
+ <div className="container-fluid d-flex py-5 justify-content-center align-items-start text-center">
5
+ <p className="my-0 py-5 text-muted mw-32em">
6
+ <FormattedMessage
7
+ id="error.notfound.message"
8
+ defaultMessage="The page you're looking for is unavailable or there's an error in the URL. Please check the URL and try again."
9
+ description="error message when a page does not exist"
10
+ />
11
+ </p>
12
+ </div>
13
+ );
14
+
15
+ export default NotFoundPage;
@@ -0,0 +1,175 @@
1
+ import { useState } from 'react';
2
+ import { useDispatch, useSelector } from 'react-redux';
3
+
4
+ import { useIntl } from '@openedx/frontend-base';
5
+ import {
6
+ Form, Icon, IconButton, OverlayTrigger, Tooltip, useToggle,
7
+ } from '@openedx/paragon';
8
+ import {
9
+ Check, Remove, Visibility, VisibilityOff,
10
+ } from '@openedx/paragon/icons';
11
+ import PropTypes from 'prop-types';
12
+
13
+ import { LETTER_REGEX, NUMBER_REGEX } from '../data/constants';
14
+ import { clearRegistrationBackendError, fetchRealtimeValidations } from '../register/data/actions';
15
+ import { validatePasswordField } from '../register/data/utils';
16
+ import messages from './messages';
17
+
18
+ const PasswordField = (props) => {
19
+ const { formatMessage } = useIntl();
20
+ const dispatch = useDispatch();
21
+
22
+ const validationApiRateLimited = useSelector(state => state.register.validationApiRateLimited);
23
+ const [isPasswordHidden, setHiddenTrue, setHiddenFalse] = useToggle(true);
24
+ const [showTooltip, setShowTooltip] = useState(false);
25
+
26
+ const handleBlur = (e) => {
27
+ const { name, value } = e.target;
28
+ if (name === props.name && e.relatedTarget?.name === 'passwordIcon') {
29
+ return; // Do not run validations on password icon click
30
+ }
31
+
32
+ let passwordValue = value;
33
+ if (name === 'passwordIcon') {
34
+ // To validate actual password value when onBlur is triggered by focusing out the password icon
35
+ passwordValue = props.value;
36
+ }
37
+
38
+ if (props.handleBlur) {
39
+ props.handleBlur({
40
+ target: {
41
+ name: props.name,
42
+ value: passwordValue,
43
+ },
44
+ });
45
+ }
46
+
47
+ setShowTooltip(props.showRequirements && false);
48
+ if (props.handleErrorChange) { // If rendering from register page
49
+ const fieldError = validatePasswordField(passwordValue, formatMessage);
50
+ if (fieldError) {
51
+ props.handleErrorChange('password', fieldError);
52
+ } else if (!validationApiRateLimited) {
53
+ dispatch(fetchRealtimeValidations({ password: passwordValue }));
54
+ }
55
+ }
56
+ };
57
+
58
+ const handleFocus = (e) => {
59
+ if (e.target?.name === 'passwordIcon') {
60
+ return; // Do not clear error on password icon focus
61
+ }
62
+
63
+ if (props.handleFocus) {
64
+ props.handleFocus(e);
65
+ }
66
+ if (props.handleErrorChange) {
67
+ props.handleErrorChange('password', '');
68
+ dispatch(clearRegistrationBackendError('password'));
69
+ }
70
+ setTimeout(() => setShowTooltip(props.showRequirements && true), 150);
71
+ };
72
+
73
+ const HideButton = (
74
+ <IconButton
75
+ onFocus={handleFocus}
76
+ onBlur={handleBlur}
77
+ name="passwordIcon"
78
+ src={VisibilityOff}
79
+ iconAs={Icon}
80
+ onClick={setHiddenTrue}
81
+ size="sm"
82
+ variant="secondary"
83
+ alt={formatMessage(messages['hide.password'])}
84
+ />
85
+ );
86
+
87
+ const ShowButton = (
88
+ <IconButton
89
+ onFocus={handleFocus}
90
+ onBlur={handleBlur}
91
+ name="passwordIcon"
92
+ src={Visibility}
93
+ iconAs={Icon}
94
+ onClick={setHiddenFalse}
95
+ size="sm"
96
+ variant="secondary"
97
+ alt={formatMessage(messages['show.password'])}
98
+ />
99
+ );
100
+
101
+ const placement = window.innerWidth < 768 ? 'top' : 'left';
102
+ const tooltip = (
103
+ <Tooltip id={`password-requirement-${placement}`}>
104
+ <span id="letter-check" className="d-flex align-items-center">
105
+ {LETTER_REGEX.test(props.value) ? <Icon className="text-success mr-1" src={Check} /> : <Icon className="mr-1 text-light-700" src={Remove} />}
106
+ {formatMessage(messages['one.letter'])}
107
+ </span>
108
+ <span id="number-check" className="d-flex align-items-center">
109
+ {NUMBER_REGEX.test(props.value) ? <Icon className="text-success mr-1" src={Check} /> : <Icon className="mr-1 text-light-700" src={Remove} />}
110
+ {formatMessage(messages['one.number'])}
111
+ </span>
112
+ <span id="characters-check" className="d-flex align-items-center">
113
+ {props.value.length >= 8 ? <Icon className="text-success mr-1" src={Check} /> : <Icon className="mr-1 text-light-700" src={Remove} />}
114
+ {formatMessage(messages['eight.characters'])}
115
+ </span>
116
+ </Tooltip>
117
+ );
118
+
119
+ return (
120
+ <Form.Group controlId={props.name} isInvalid={props.errorMessage !== ''}>
121
+ <OverlayTrigger key="tooltip" placement={placement} overlay={tooltip} show={showTooltip}>
122
+ <Form.Control
123
+ as="input"
124
+ className="form-group__form-field"
125
+ type={isPasswordHidden ? 'password' : 'text'}
126
+ name={props.name}
127
+ value={props.value}
128
+ autoComplete={props.autoComplete}
129
+ aria-invalid={props.errorMessage !== ''}
130
+ onFocus={handleFocus}
131
+ onBlur={handleBlur}
132
+ onChange={props.handleChange}
133
+ controlClassName={props.borderClass}
134
+ trailingElement={isPasswordHidden ? ShowButton : HideButton}
135
+ floatingLabel={props.floatingLabel}
136
+ />
137
+ </OverlayTrigger>
138
+ {props.errorMessage !== '' && (
139
+ <Form.Control.Feedback key="error" className="form-text-size" hasIcon={false} feedback-for={props.name} type="invalid">
140
+ {props.errorMessage}
141
+ {props.showScreenReaderText && <span className="sr-only">{formatMessage(messages['password.sr.only.helping.text'])}</span>}
142
+ </Form.Control.Feedback>
143
+ )}
144
+ </Form.Group>
145
+ );
146
+ };
147
+
148
+ PasswordField.defaultProps = {
149
+ borderClass: '',
150
+ errorMessage: '',
151
+ handleBlur: null,
152
+ handleFocus: null,
153
+ handleChange: () => {},
154
+ handleErrorChange: null,
155
+ showRequirements: true,
156
+ showScreenReaderText: true,
157
+ autoComplete: null,
158
+ };
159
+
160
+ PasswordField.propTypes = {
161
+ borderClass: PropTypes.string,
162
+ errorMessage: PropTypes.string,
163
+ floatingLabel: PropTypes.string.isRequired,
164
+ handleBlur: PropTypes.func,
165
+ handleFocus: PropTypes.func,
166
+ handleChange: PropTypes.func,
167
+ handleErrorChange: PropTypes.func,
168
+ name: PropTypes.string.isRequired,
169
+ showRequirements: PropTypes.bool,
170
+ value: PropTypes.string.isRequired,
171
+ autoComplete: PropTypes.string,
172
+ showScreenReaderText: PropTypes.bool,
173
+ };
174
+
175
+ export default PasswordField;
@@ -0,0 +1,94 @@
1
+ import { useAppConfig, getSiteConfig } from '@openedx/frontend-base';
2
+ import PropTypes from 'prop-types';
3
+ import { Navigate } from 'react-router-dom';
4
+
5
+ import {
6
+ AUTHN_PROGRESSIVE_PROFILING, REDIRECT,
7
+ } from '../data/constants';
8
+ import { setCookie } from '../data/utils';
9
+
10
+ const RedirectLogistration = (props) => {
11
+ const {
12
+ authenticatedUser,
13
+ finishAuthUrl,
14
+ redirectUrl,
15
+ redirectToProgressiveProfilingPage,
16
+ success,
17
+ optionalFields,
18
+ educationLevel,
19
+ userId,
20
+ registrationEmbedded,
21
+ host,
22
+ } = props;
23
+ let finalRedirectUrl = '';
24
+
25
+ if (success) {
26
+ // If we're in a third party auth pipeline, we must complete the pipeline
27
+ // once user has successfully logged in. Otherwise, redirect to the specified redirect url.
28
+ // Note: For multiple enterprise use case, we need to make sure that user first visits the
29
+ // enterprise selection page and then complete the auth workflow
30
+ if (finishAuthUrl && !redirectUrl.includes(finishAuthUrl)) {
31
+ finalRedirectUrl = getSiteConfig().lmsBaseUrl + finishAuthUrl;
32
+ } else {
33
+ finalRedirectUrl = redirectUrl;
34
+ }
35
+
36
+ // Redirect to Progressive Profiling after successful registration
37
+ if (redirectToProgressiveProfilingPage) {
38
+ // TODO: Do we still need this cookie?
39
+ setCookie('van-504-returning-user', true, useAppConfig().SESSION_COOKIE_DOMAIN);
40
+
41
+ if (registrationEmbedded) {
42
+ window.parent.postMessage({
43
+ action: REDIRECT,
44
+ redirectUrl: useAppConfig().POST_REGISTRATION_REDIRECT_URL,
45
+ }, host);
46
+ return null;
47
+ }
48
+ const registrationResult = { redirectUrl: finalRedirectUrl, success };
49
+ return (
50
+ <Navigate
51
+ to={AUTHN_PROGRESSIVE_PROFILING}
52
+ state={{
53
+ registrationResult,
54
+ optionalFields,
55
+ authenticatedUser,
56
+ }}
57
+ replace
58
+ />
59
+ );
60
+ }
61
+
62
+ window.location.href = finalRedirectUrl;
63
+ }
64
+
65
+ return null;
66
+ };
67
+
68
+ RedirectLogistration.defaultProps = {
69
+ authenticatedUser: {},
70
+ educationLevel: null,
71
+ finishAuthUrl: null,
72
+ success: false,
73
+ redirectUrl: '',
74
+ redirectToProgressiveProfilingPage: false,
75
+ optionalFields: {},
76
+ userId: null,
77
+ registrationEmbedded: false,
78
+ host: '',
79
+ };
80
+
81
+ RedirectLogistration.propTypes = {
82
+ authenticatedUser: PropTypes.shape({}),
83
+ educationLevel: PropTypes.string,
84
+ finishAuthUrl: PropTypes.string,
85
+ success: PropTypes.bool,
86
+ redirectUrl: PropTypes.string,
87
+ redirectToProgressiveProfilingPage: PropTypes.bool,
88
+ optionalFields: PropTypes.shape({}),
89
+ userId: PropTypes.number,
90
+ registrationEmbedded: PropTypes.bool,
91
+ host: PropTypes.string,
92
+ };
93
+
94
+ export default RedirectLogistration;
@@ -0,0 +1,8 @@
1
+ import { library } from '@fortawesome/fontawesome-svg-core';
2
+ import {
3
+ faApple, faFacebook, faGoogle, faMicrosoft,
4
+ } from '@fortawesome/free-brands-svg-icons';
5
+
6
+ export default function registerIcons() {
7
+ library.add(faApple, faFacebook, faGoogle, faMicrosoft);
8
+ }
@@ -0,0 +1,75 @@
1
+ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
2
+ import { getSiteConfig, useIntl } from '@openedx/frontend-base';
3
+ import { Icon } from '@openedx/paragon';
4
+ import { Login } from '@openedx/paragon/icons';
5
+ import PropTypes from 'prop-types';
6
+
7
+ import { LOGIN_PAGE, SUPPORTED_ICON_CLASSES } from '../data/constants';
8
+ import messages from './messages';
9
+
10
+ const SocialAuthProviders = (props) => {
11
+ const { formatMessage } = useIntl();
12
+ const { referrer, socialAuthProviders } = props;
13
+
14
+ function handleSubmit(e) {
15
+ e.preventDefault();
16
+
17
+ const url = e.currentTarget.dataset.providerUrl;
18
+ window.location.href = getSiteConfig().lmsBaseUrl + url;
19
+ }
20
+
21
+ const socialAuth = socialAuthProviders.map((provider, index) => (
22
+ <button
23
+ id={provider.id}
24
+ key={provider.id}
25
+ type="button"
26
+ className={`btn-social btn-${provider.id} ${index % 2 === 0 ? 'mr-3' : ''}`}
27
+ data-provider-url={referrer === LOGIN_PAGE ? provider.loginUrl : provider.registerUrl}
28
+ onClick={handleSubmit}
29
+ >
30
+ {provider.iconImage ? (
31
+ <div aria-hidden="true">
32
+ <img className="btn-tpa__image-icon" src={provider.iconImage} alt={`icon ${provider.name}`} />
33
+ </div>
34
+ )
35
+ : (
36
+ <div className="btn-tpa__font-container" aria-hidden="true">
37
+ {SUPPORTED_ICON_CLASSES.includes(provider.iconClass) ? (
38
+ <FontAwesomeIcon icon={['fab', provider.iconClass]} />)
39
+ : (
40
+ <Icon className="h-75" src={Login} />
41
+ )}
42
+ </div>
43
+ )}
44
+ <span id="provider-name" className="notranslate mr-auto pl-2" aria-hidden="true">{provider.name}</span>
45
+ <span className="sr-only">
46
+ {referrer === LOGIN_PAGE
47
+ ? formatMessage(messages['sso.sign.in.with'], { providerName: provider.name })
48
+ : formatMessage(messages['sso.create.account.using'], { providerName: provider.name })}
49
+ </span>
50
+ </button>
51
+ ));
52
+
53
+ // eslint-disable-next-line react/jsx-no-useless-fragment
54
+ return <>{socialAuth}</>;
55
+ };
56
+
57
+ SocialAuthProviders.defaultProps = {
58
+ referrer: LOGIN_PAGE,
59
+ socialAuthProviders: [],
60
+ };
61
+
62
+ SocialAuthProviders.propTypes = {
63
+ referrer: PropTypes.string,
64
+ socialAuthProviders: PropTypes.arrayOf(PropTypes.shape({
65
+ id: PropTypes.string,
66
+ name: PropTypes.string,
67
+ iconClass: PropTypes.string,
68
+ iconImage: PropTypes.string,
69
+ loginUrl: PropTypes.string,
70
+ registerUrl: PropTypes.string,
71
+ skipRegistrationForm: PropTypes.bool,
72
+ })),
73
+ };
74
+
75
+ export default SocialAuthProviders;
@@ -0,0 +1,123 @@
1
+ import { useAppConfig, getSiteConfig, useIntl } from '@openedx/frontend-base';
2
+ import {
3
+ Hyperlink, Icon,
4
+ } from '@openedx/paragon';
5
+ import { Institution } from '@openedx/paragon/icons';
6
+ import classNames from 'classnames';
7
+ import PropTypes from 'prop-types';
8
+ import Skeleton from 'react-loading-skeleton';
9
+
10
+ import {
11
+ ENTERPRISE_LOGIN_URL, LOGIN_PAGE, PENDING_STATE, REGISTER_PAGE,
12
+ } from '../data/constants';
13
+ import messages from './messages';
14
+
15
+ import {
16
+ RenderInstitutionButton,
17
+ SocialAuthProviders,
18
+ } from './index';
19
+
20
+ /**
21
+ * This component renders the Single sign-on (SSO) buttons for the providers passed.
22
+ * */
23
+ const ThirdPartyAuth = (props) => {
24
+ const { formatMessage } = useIntl();
25
+ const {
26
+ providers,
27
+ secondaryProviders,
28
+ currentProvider,
29
+ handleInstitutionLogin,
30
+ thirdPartyAuthApiStatus,
31
+ isLoginPage,
32
+ } = props;
33
+ const isInstitutionAuthActive = !!secondaryProviders.length && !currentProvider;
34
+ const isSocialAuthActive = !!providers.length && !currentProvider;
35
+ const isEnterpriseLoginDisabled = useAppConfig().DISABLE_ENTERPRISE_LOGIN;
36
+ const enterpriseLoginURL = getSiteConfig().lmsBaseUrl + ENTERPRISE_LOGIN_URL;
37
+ const isThirdPartyAuthActive = isSocialAuthActive || (isEnterpriseLoginDisabled && isInstitutionAuthActive);
38
+
39
+ return (
40
+ <>
41
+ {((isEnterpriseLoginDisabled && isInstitutionAuthActive) || isSocialAuthActive) && (
42
+ <div className="mt-4 mb-3 h4">
43
+ {isLoginPage
44
+ ? formatMessage(messages['login.other.options.heading'])
45
+ : formatMessage(messages['registration.other.options.heading'])}
46
+ </div>
47
+ )}
48
+ {(isLoginPage && !isEnterpriseLoginDisabled && isSocialAuthActive) && (
49
+ <Hyperlink
50
+ className={classNames(
51
+ 'btn btn-link btn-sm text-body p-0',
52
+ { 'mb-0': thirdPartyAuthApiStatus === PENDING_STATE },
53
+ { 'mb-4': thirdPartyAuthApiStatus !== PENDING_STATE },
54
+ )}
55
+ destination={enterpriseLoginURL}
56
+ >
57
+ <Icon src={Institution} className="institute-icon" />
58
+ {formatMessage(messages['enterprise.login.btn.text'])}
59
+ </Hyperlink>
60
+ )}
61
+
62
+ {thirdPartyAuthApiStatus === PENDING_STATE && isThirdPartyAuthActive ? (
63
+ <div className="mt-4">
64
+ <Skeleton className="tpa-skeleton" height={36} count={2} />
65
+ </div>
66
+ ) : (
67
+ <>
68
+ {(isEnterpriseLoginDisabled && isInstitutionAuthActive) && (
69
+ <RenderInstitutionButton
70
+ onSubmitHandler={handleInstitutionLogin}
71
+ buttonTitle={formatMessage(messages['institution.login.button'])}
72
+ />
73
+ )}
74
+ {isSocialAuthActive && (
75
+ <div className="row m-0">
76
+ <SocialAuthProviders
77
+ socialAuthProviders={providers}
78
+ referrer={isLoginPage ? LOGIN_PAGE : REGISTER_PAGE}
79
+ />
80
+ </div>
81
+ )}
82
+ </>
83
+ )}
84
+ </>
85
+ );
86
+ };
87
+
88
+ ThirdPartyAuth.defaultProps = {
89
+ currentProvider: null,
90
+ providers: [],
91
+ secondaryProviders: [],
92
+ thirdPartyAuthApiStatus: PENDING_STATE,
93
+ isLoginPage: false,
94
+ };
95
+
96
+ ThirdPartyAuth.propTypes = {
97
+ currentProvider: PropTypes.string,
98
+ handleInstitutionLogin: PropTypes.func.isRequired,
99
+ providers: PropTypes.arrayOf(
100
+ PropTypes.shape({
101
+ id: PropTypes.string,
102
+ name: PropTypes.string,
103
+ iconClass: PropTypes.string,
104
+ iconImage: PropTypes.string,
105
+ loginUrl: PropTypes.string,
106
+ registerUrl: PropTypes.string,
107
+ }),
108
+ ),
109
+ secondaryProviders: PropTypes.arrayOf(
110
+ PropTypes.shape({
111
+ id: PropTypes.string,
112
+ name: PropTypes.string,
113
+ iconClass: PropTypes.string,
114
+ iconImage: PropTypes.string,
115
+ loginUrl: PropTypes.string,
116
+ registerUrl: PropTypes.string,
117
+ }),
118
+ ),
119
+ thirdPartyAuthApiStatus: PropTypes.string,
120
+ isLoginPage: PropTypes.bool,
121
+ };
122
+
123
+ export default ThirdPartyAuth;
@@ -0,0 +1,49 @@
1
+ import { getSiteConfig, useIntl } from '@openedx/frontend-base';
2
+ import { Alert } from '@openedx/paragon';
3
+ import PropTypes from 'prop-types';
4
+
5
+ import { LOGIN_PAGE, REGISTER_PAGE } from '../data/constants';
6
+ import messages from './messages';
7
+
8
+ const ThirdPartyAuthAlert = (props) => {
9
+ const { formatMessage } = useIntl();
10
+ const { currentProvider, referrer } = props;
11
+ const platformName = getSiteConfig().siteName;
12
+ let message;
13
+
14
+ if (referrer === LOGIN_PAGE) {
15
+ message = formatMessage(messages['login.third.party.auth.account.not.linked'], { currentProvider, platformName });
16
+ } else {
17
+ message = formatMessage(messages['register.third.party.auth.account.not.linked'], { currentProvider, platformName });
18
+ }
19
+
20
+ if (!currentProvider) {
21
+ return null;
22
+ }
23
+
24
+ return (
25
+ <>
26
+ <Alert id="tpa-alert" className={referrer === REGISTER_PAGE ? 'alert-success mt-n2 mb-5' : 'alert-warning mt-n2 mb-5'}>
27
+ {referrer === REGISTER_PAGE ? (
28
+ <Alert.Heading>{formatMessage(messages['tpa.alert.heading'])}</Alert.Heading>
29
+ ) : null}
30
+ <p>{message}</p>
31
+ </Alert>
32
+ {referrer === REGISTER_PAGE ? (
33
+ <h4 className="mt-4 mb-4">{formatMessage(messages['registration.using.tpa.form.heading'])}</h4>
34
+ ) : null}
35
+ </>
36
+ );
37
+ };
38
+
39
+ ThirdPartyAuthAlert.defaultProps = {
40
+ currentProvider: '',
41
+ referrer: LOGIN_PAGE,
42
+ };
43
+
44
+ ThirdPartyAuthAlert.propTypes = {
45
+ currentProvider: PropTypes.string,
46
+ referrer: PropTypes.string,
47
+ };
48
+
49
+ export default ThirdPartyAuthAlert;
@@ -0,0 +1,41 @@
1
+ import { useEffect, useState } from 'react';
2
+
3
+ import { fetchAuthenticatedUser, getAuthenticatedUser, getSiteConfig } from '@openedx/frontend-base';
4
+ import PropTypes from 'prop-types';
5
+
6
+ import {
7
+ DEFAULT_REDIRECT_URL,
8
+ } from '../data/constants';
9
+
10
+ /**
11
+ * This wrapper redirects the requester to our default redirect url if they are
12
+ * already authenticated.
13
+ */
14
+ const UnAuthOnlyRoute = ({ children }) => {
15
+ const [authUser, setAuthUser] = useState({});
16
+ const [isReady, setIsReady] = useState(false);
17
+
18
+ useEffect(() => {
19
+ fetchAuthenticatedUser({ forceRefresh: !!getAuthenticatedUser() }).then((authenticatedUser) => {
20
+ setAuthUser(authenticatedUser);
21
+ setIsReady(true);
22
+ });
23
+ }, []);
24
+
25
+ if (isReady) {
26
+ if (authUser && authUser.username) {
27
+ global.location.href = getSiteConfig().lmsBaseUrl.concat(DEFAULT_REDIRECT_URL);
28
+ return null;
29
+ }
30
+
31
+ return children;
32
+ }
33
+
34
+ return null;
35
+ };
36
+
37
+ UnAuthOnlyRoute.propTypes = {
38
+ children: PropTypes.node.isRequired,
39
+ };
40
+
41
+ export default UnAuthOnlyRoute;