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