@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,231 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
FormattedMessage, getAuthService, getSiteConfig, useIntl
|
|
5
|
+
} from '@openedx/frontend-base';
|
|
6
|
+
import { Alert, Hyperlink } from '@openedx/paragon';
|
|
7
|
+
import { Error } from '@openedx/paragon/icons';
|
|
8
|
+
import PropTypes from 'prop-types';
|
|
9
|
+
|
|
10
|
+
import { windowScrollTo } from '../data/utils';
|
|
11
|
+
import ChangePasswordPrompt from './ChangePasswordPrompt';
|
|
12
|
+
import {
|
|
13
|
+
ACCOUNT_LOCKED_OUT,
|
|
14
|
+
ALLOWED_DOMAIN_LOGIN_ERROR,
|
|
15
|
+
FAILED_LOGIN_ATTEMPT,
|
|
16
|
+
FORBIDDEN_REQUEST,
|
|
17
|
+
INACTIVE_USER,
|
|
18
|
+
INCORRECT_EMAIL_PASSWORD,
|
|
19
|
+
INTERNAL_SERVER_ERROR,
|
|
20
|
+
INVALID_FORM,
|
|
21
|
+
NON_COMPLIANT_PASSWORD_EXCEPTION,
|
|
22
|
+
NUDGE_PASSWORD_CHANGE,
|
|
23
|
+
REQUIRE_PASSWORD_CHANGE,
|
|
24
|
+
TPA_AUTHENTICATION_FAILURE,
|
|
25
|
+
} from './data/constants';
|
|
26
|
+
import messages from './messages';
|
|
27
|
+
|
|
28
|
+
const LoginFailureMessage = (props) => {
|
|
29
|
+
const { formatMessage } = useIntl();
|
|
30
|
+
const authService = getAuthService();
|
|
31
|
+
const {
|
|
32
|
+
context,
|
|
33
|
+
errorCode,
|
|
34
|
+
errorCount, // This is used to trigger the useEffect, facilitating the scrolling to the top.
|
|
35
|
+
} = props;
|
|
36
|
+
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
windowScrollTo({ left: 0, top: 0, behavior: 'smooth' });
|
|
39
|
+
}, [errorCode, errorCount]);
|
|
40
|
+
|
|
41
|
+
if (!errorCode) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let resetLink = (
|
|
46
|
+
<Hyperlink destination="reset" isInline>
|
|
47
|
+
{formatMessage(messages['login.incorrect.credentials.error.reset.link.text'])}
|
|
48
|
+
</Hyperlink>
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
let errorMessage;
|
|
52
|
+
switch (errorCode) {
|
|
53
|
+
case NON_COMPLIANT_PASSWORD_EXCEPTION: {
|
|
54
|
+
errorMessage = (
|
|
55
|
+
<>
|
|
56
|
+
<strong>{formatMessage(messages['non.compliant.password.title'])}</strong>
|
|
57
|
+
<p>{formatMessage(messages['non.compliant.password.message'])}</p>
|
|
58
|
+
</>
|
|
59
|
+
);
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
case FORBIDDEN_REQUEST:
|
|
63
|
+
errorMessage = <p>{formatMessage(messages['login.rate.limit.reached.message'])}</p>;
|
|
64
|
+
break;
|
|
65
|
+
case INACTIVE_USER: {
|
|
66
|
+
const supportLink = (
|
|
67
|
+
<a href={context.supportLink}>
|
|
68
|
+
{formatMessage(messages['contact.support.link'], { platformName: context.platformName })}
|
|
69
|
+
</a>
|
|
70
|
+
);
|
|
71
|
+
errorMessage = (
|
|
72
|
+
<p>
|
|
73
|
+
<FormattedMessage
|
|
74
|
+
id="login.inactive.user.error"
|
|
75
|
+
defaultMessage="In order to sign in, you need to activate your account.{lineBreak}
|
|
76
|
+
{lineBreak}We just sent an activation link to {email}. If you do not receive an email,
|
|
77
|
+
check your spam folders or {supportLink}."
|
|
78
|
+
description="An error message shown to users when they sign in if they have not yet activated their account. It attempts to explain to them how to activate their account by looking for an email from the system."
|
|
79
|
+
values={{
|
|
80
|
+
lineBreak: <br />,
|
|
81
|
+
email: <strong className="data-hj-suppress">{context.email}</strong>,
|
|
82
|
+
supportLink,
|
|
83
|
+
}}
|
|
84
|
+
/>
|
|
85
|
+
</p>
|
|
86
|
+
);
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
case ALLOWED_DOMAIN_LOGIN_ERROR: {
|
|
90
|
+
const url = `${getSiteConfig().lmsBaseUrl}/dashboard/?tpa_hint=${context.tpaHint}`;
|
|
91
|
+
const tpaLink = (
|
|
92
|
+
<a href={url}>
|
|
93
|
+
{formatMessage(messages['tpa.account.link'], { provider: context.provider })}
|
|
94
|
+
</a>
|
|
95
|
+
);
|
|
96
|
+
errorMessage = (
|
|
97
|
+
<p>
|
|
98
|
+
<FormattedMessage
|
|
99
|
+
id="allowed.domain.login.error"
|
|
100
|
+
description="Display this error message when staff user try to login through password"
|
|
101
|
+
defaultMessage="As {allowedDomain} user, You must login with your {allowedDomain} {tpaLink}."
|
|
102
|
+
values={{ allowedDomain: context.allowedDomain, tpaLink }}
|
|
103
|
+
/>
|
|
104
|
+
</p>
|
|
105
|
+
);
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
case INVALID_FORM:
|
|
109
|
+
errorMessage = <p>{formatMessage(messages['login.form.invalid.error.message'])}</p>;
|
|
110
|
+
break;
|
|
111
|
+
case FAILED_LOGIN_ATTEMPT: {
|
|
112
|
+
resetLink = (
|
|
113
|
+
<Hyperlink destination="reset" isInline>
|
|
114
|
+
{formatMessage(messages['login.incorrect.credentials.error.before.account.blocked.text'])}
|
|
115
|
+
</Hyperlink>
|
|
116
|
+
);
|
|
117
|
+
errorMessage = (
|
|
118
|
+
<>
|
|
119
|
+
<p>
|
|
120
|
+
<FormattedMessage
|
|
121
|
+
id="login.incorrect.credentials.error.attempts.text.1"
|
|
122
|
+
description="Error message for incorrect email or password"
|
|
123
|
+
defaultMessage="The username, email or password you entered is incorrect. You have {remainingAttempts} more sign in
|
|
124
|
+
attempts before your account is temporarily locked."
|
|
125
|
+
values={{ remainingAttempts: context.remainingAttempts }}
|
|
126
|
+
/>
|
|
127
|
+
</p>
|
|
128
|
+
<p>
|
|
129
|
+
<FormattedMessage
|
|
130
|
+
id="login.incorrect.credentials.error.attempts.text.2"
|
|
131
|
+
description="Part of error message for incorrect email or password"
|
|
132
|
+
defaultMessage="If you've forgotten your password, {resetLink}"
|
|
133
|
+
values={{ resetLink }}
|
|
134
|
+
/>
|
|
135
|
+
</p>
|
|
136
|
+
</>
|
|
137
|
+
);
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
case ACCOUNT_LOCKED_OUT: {
|
|
141
|
+
errorMessage = (
|
|
142
|
+
<>
|
|
143
|
+
<p>{formatMessage(messages['account.locked.out.message.1'])}</p>
|
|
144
|
+
<p>
|
|
145
|
+
<FormattedMessage
|
|
146
|
+
id="account.locked.out.message.2"
|
|
147
|
+
description="Part of message for when user account has been locked out after multiple failed login attempts"
|
|
148
|
+
defaultMessage="To be on the safe side, you can {resetLink} before trying again."
|
|
149
|
+
values={{ resetLink }}
|
|
150
|
+
/>
|
|
151
|
+
</p>
|
|
152
|
+
</>
|
|
153
|
+
);
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
case INCORRECT_EMAIL_PASSWORD:
|
|
157
|
+
if (context.failureCount <= 1) {
|
|
158
|
+
errorMessage = <p>{formatMessage(messages['login.incorrect.credentials.error'])}</p>;
|
|
159
|
+
} else if (context.failureCount === 2) {
|
|
160
|
+
errorMessage = (
|
|
161
|
+
<p>
|
|
162
|
+
<FormattedMessage
|
|
163
|
+
id="login.incorrect.credentials.error.with.reset.link"
|
|
164
|
+
defaultMessage="The username, email, or password you entered is incorrect. Please try again or {resetLink}."
|
|
165
|
+
description="An error message shown to users if some part of their login information was incorrect."
|
|
166
|
+
values={{ resetLink }}
|
|
167
|
+
/>
|
|
168
|
+
</p>
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
break;
|
|
172
|
+
case NUDGE_PASSWORD_CHANGE:
|
|
173
|
+
// Need to clear the CSRF token here to fetch a new one because token is already rotated after successful login.
|
|
174
|
+
if (authService) {
|
|
175
|
+
authService.getCsrfTokenService().clearCsrfTokenCache();
|
|
176
|
+
}
|
|
177
|
+
return (
|
|
178
|
+
<ChangePasswordPrompt
|
|
179
|
+
redirectUrl={context.redirectUrl}
|
|
180
|
+
variant="nudge"
|
|
181
|
+
/>
|
|
182
|
+
);
|
|
183
|
+
case REQUIRE_PASSWORD_CHANGE:
|
|
184
|
+
return <ChangePasswordPrompt />;
|
|
185
|
+
case TPA_AUTHENTICATION_FAILURE:
|
|
186
|
+
errorMessage = (
|
|
187
|
+
<p>
|
|
188
|
+
{formatMessage(messages['login.tpa.authentication.failure'], {
|
|
189
|
+
platform_name: getSiteConfig().siteName,
|
|
190
|
+
lineBreak: <br />,
|
|
191
|
+
errorMessage: context.errorMessage,
|
|
192
|
+
})}
|
|
193
|
+
</p>
|
|
194
|
+
);
|
|
195
|
+
break;
|
|
196
|
+
case INTERNAL_SERVER_ERROR:
|
|
197
|
+
default:
|
|
198
|
+
errorMessage = <p>{formatMessage(messages['internal.server.error.message'])}</p>;
|
|
199
|
+
break;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return (
|
|
203
|
+
<Alert id="login-failure-alert" className="mb-5" variant="danger" icon={Error}>
|
|
204
|
+
<Alert.Heading>{formatMessage(messages['login.failure.header.title'])}</Alert.Heading>
|
|
205
|
+
{errorMessage}
|
|
206
|
+
</Alert>
|
|
207
|
+
);
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
LoginFailureMessage.defaultProps = {
|
|
211
|
+
context: {},
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
LoginFailureMessage.propTypes = {
|
|
215
|
+
context: PropTypes.shape({
|
|
216
|
+
supportLink: PropTypes.string,
|
|
217
|
+
platformName: PropTypes.string,
|
|
218
|
+
tpaHint: PropTypes.string,
|
|
219
|
+
provider: PropTypes.string,
|
|
220
|
+
allowedDomain: PropTypes.string,
|
|
221
|
+
remainingAttempts: PropTypes.number,
|
|
222
|
+
failureCount: PropTypes.number,
|
|
223
|
+
errorMessage: PropTypes.string,
|
|
224
|
+
email: PropTypes.string,
|
|
225
|
+
redirectUrl: PropTypes.string,
|
|
226
|
+
}),
|
|
227
|
+
errorCode: PropTypes.string.isRequired,
|
|
228
|
+
errorCount: PropTypes.number.isRequired,
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
export default LoginFailureMessage;
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
2
|
+
import { connect } from 'react-redux';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
getSiteConfig, injectIntl, sendPageEvent, sendTrackEvent, useIntl
|
|
6
|
+
} from '@openedx/frontend-base';
|
|
7
|
+
import {
|
|
8
|
+
Form, StatefulButton,
|
|
9
|
+
} from '@openedx/paragon';
|
|
10
|
+
import PropTypes from 'prop-types';
|
|
11
|
+
import { Helmet } from 'react-helmet';
|
|
12
|
+
import Skeleton from 'react-loading-skeleton';
|
|
13
|
+
import { Link } from 'react-router-dom';
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
FormGroup,
|
|
17
|
+
InstitutionLogistration,
|
|
18
|
+
PasswordField,
|
|
19
|
+
RedirectLogistration,
|
|
20
|
+
ThirdPartyAuthAlert,
|
|
21
|
+
} from '../common-components';
|
|
22
|
+
import { getThirdPartyAuthContext } from '../common-components/data/actions';
|
|
23
|
+
import { thirdPartyAuthContextSelector } from '../common-components/data/selectors';
|
|
24
|
+
import EnterpriseSSO from '../common-components/EnterpriseSSO';
|
|
25
|
+
import ThirdPartyAuth from '../common-components/ThirdPartyAuth';
|
|
26
|
+
import {
|
|
27
|
+
DEFAULT_STATE, PENDING_STATE, RESET_PAGE,
|
|
28
|
+
} from '../data/constants';
|
|
29
|
+
import {
|
|
30
|
+
getActivationStatus,
|
|
31
|
+
getAllPossibleQueryParams,
|
|
32
|
+
getTpaHint,
|
|
33
|
+
getTpaProvider,
|
|
34
|
+
updatePathWithQueryParams,
|
|
35
|
+
} from '../data/utils';
|
|
36
|
+
import ResetPasswordSuccess from '../reset-password/ResetPasswordSuccess';
|
|
37
|
+
import AccountActivationMessage from './AccountActivationMessage';
|
|
38
|
+
import {
|
|
39
|
+
backupLoginFormBegin,
|
|
40
|
+
dismissPasswordResetBanner,
|
|
41
|
+
loginRequest,
|
|
42
|
+
} from './data/actions';
|
|
43
|
+
import { INVALID_FORM, TPA_AUTHENTICATION_FAILURE } from './data/constants';
|
|
44
|
+
import LoginFailureMessage from './LoginFailure';
|
|
45
|
+
import messages from './messages';
|
|
46
|
+
|
|
47
|
+
const LoginPage = (props) => {
|
|
48
|
+
const {
|
|
49
|
+
backedUpFormData,
|
|
50
|
+
loginErrorCode,
|
|
51
|
+
loginErrorContext,
|
|
52
|
+
loginResult,
|
|
53
|
+
shouldBackupState,
|
|
54
|
+
thirdPartyAuthContext: {
|
|
55
|
+
providers,
|
|
56
|
+
currentProvider,
|
|
57
|
+
secondaryProviders,
|
|
58
|
+
finishAuthUrl,
|
|
59
|
+
platformName,
|
|
60
|
+
errorMessage: thirdPartyErrorMessage,
|
|
61
|
+
},
|
|
62
|
+
thirdPartyAuthApiStatus,
|
|
63
|
+
institutionLogin,
|
|
64
|
+
showResetPasswordSuccessBanner,
|
|
65
|
+
submitState,
|
|
66
|
+
// Actions
|
|
67
|
+
backupFormState,
|
|
68
|
+
handleInstitutionLogin,
|
|
69
|
+
getTPADataFromBackend,
|
|
70
|
+
} = props;
|
|
71
|
+
const { formatMessage } = useIntl();
|
|
72
|
+
const activationMsgType = getActivationStatus();
|
|
73
|
+
const queryParams = useMemo(() => getAllPossibleQueryParams(), []);
|
|
74
|
+
|
|
75
|
+
const [formFields, setFormFields] = useState({ ...backedUpFormData.formFields });
|
|
76
|
+
const [errorCode, setErrorCode] = useState({ type: '', count: 0, context: {} });
|
|
77
|
+
const [errors, setErrors] = useState({ ...backedUpFormData.errors });
|
|
78
|
+
const tpaHint = getTpaHint();
|
|
79
|
+
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
sendPageEvent('login_and_registration', 'login');
|
|
82
|
+
}, []);
|
|
83
|
+
|
|
84
|
+
useEffect(() => {
|
|
85
|
+
const payload = { ...queryParams };
|
|
86
|
+
if (tpaHint) {
|
|
87
|
+
payload.tpa_hint = tpaHint;
|
|
88
|
+
}
|
|
89
|
+
getTPADataFromBackend(payload);
|
|
90
|
+
}, [getTPADataFromBackend, queryParams, tpaHint]);
|
|
91
|
+
/**
|
|
92
|
+
* Backup the login form in redux when login page is toggled.
|
|
93
|
+
*/
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
if (shouldBackupState) {
|
|
96
|
+
backupFormState({
|
|
97
|
+
formFields: { ...formFields },
|
|
98
|
+
errors: { ...errors },
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}, [shouldBackupState, formFields, errors, backupFormState]);
|
|
102
|
+
|
|
103
|
+
useEffect(() => {
|
|
104
|
+
if (loginErrorCode) {
|
|
105
|
+
setErrorCode(prevState => ({
|
|
106
|
+
type: loginErrorCode,
|
|
107
|
+
count: prevState.count + 1,
|
|
108
|
+
context: { ...loginErrorContext },
|
|
109
|
+
}));
|
|
110
|
+
}
|
|
111
|
+
}, [loginErrorCode, loginErrorContext]);
|
|
112
|
+
|
|
113
|
+
useEffect(() => {
|
|
114
|
+
if (thirdPartyErrorMessage) {
|
|
115
|
+
setErrorCode((prevState) => ({
|
|
116
|
+
type: TPA_AUTHENTICATION_FAILURE,
|
|
117
|
+
count: prevState.count + 1,
|
|
118
|
+
context: {
|
|
119
|
+
errorMessage: thirdPartyErrorMessage,
|
|
120
|
+
},
|
|
121
|
+
}));
|
|
122
|
+
}
|
|
123
|
+
}, [thirdPartyErrorMessage]);
|
|
124
|
+
|
|
125
|
+
const validateFormFields = (payload) => {
|
|
126
|
+
const { emailOrUsername, password } = payload;
|
|
127
|
+
const fieldErrors = { ...errors };
|
|
128
|
+
|
|
129
|
+
if (emailOrUsername === '') {
|
|
130
|
+
fieldErrors.emailOrUsername = formatMessage(messages['email.validation.message']);
|
|
131
|
+
} else if (emailOrUsername.length < 2) {
|
|
132
|
+
fieldErrors.emailOrUsername = formatMessage(messages['username.or.email.format.validation.less.chars.message']);
|
|
133
|
+
}
|
|
134
|
+
if (password === '') {
|
|
135
|
+
fieldErrors.password = formatMessage(messages['password.validation.message']);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return { ...fieldErrors };
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const handleSubmit = (event) => {
|
|
142
|
+
event.preventDefault();
|
|
143
|
+
if (showResetPasswordSuccessBanner) {
|
|
144
|
+
props.dismissPasswordResetBanner();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const formData = { ...formFields };
|
|
148
|
+
const validationErrors = validateFormFields(formData);
|
|
149
|
+
if (validationErrors.emailOrUsername || validationErrors.password) {
|
|
150
|
+
setErrors({ ...validationErrors });
|
|
151
|
+
setErrorCode(prevState => ({ type: INVALID_FORM, count: prevState.count + 1, context: {} }));
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// add query params to the payload
|
|
156
|
+
const payload = {
|
|
157
|
+
email_or_username: formData.emailOrUsername,
|
|
158
|
+
password: formData.password,
|
|
159
|
+
...queryParams,
|
|
160
|
+
};
|
|
161
|
+
props.loginRequest(payload);
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const handleOnChange = (event) => {
|
|
165
|
+
const { name, value } = event.target;
|
|
166
|
+
setFormFields(prevState => ({ ...prevState, [name]: value }));
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const handleOnFocus = (event) => {
|
|
170
|
+
const { name } = event.target;
|
|
171
|
+
setErrors(prevErrors => ({ ...prevErrors, [name]: '' }));
|
|
172
|
+
};
|
|
173
|
+
const trackForgotPasswordLinkClick = () => {
|
|
174
|
+
sendTrackEvent('edx.bi.password-reset_form.toggled', { category: 'user-engagement' });
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const { provider, skipHintedLogin } = getTpaProvider(tpaHint, providers, secondaryProviders);
|
|
178
|
+
|
|
179
|
+
if (tpaHint) {
|
|
180
|
+
if (thirdPartyAuthApiStatus === PENDING_STATE) {
|
|
181
|
+
return <Skeleton height={36} />;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (skipHintedLogin) {
|
|
185
|
+
window.location.href = getSiteConfig().lmsBaseUrl + provider.loginUrl;
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (provider) {
|
|
190
|
+
return <EnterpriseSSO provider={provider} />;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (institutionLogin) {
|
|
195
|
+
return (
|
|
196
|
+
<InstitutionLogistration
|
|
197
|
+
secondaryProviders={secondaryProviders}
|
|
198
|
+
headingTitle={formatMessage(messages['institution.login.page.title'])}
|
|
199
|
+
/>
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
return (
|
|
203
|
+
<>
|
|
204
|
+
<Helmet>
|
|
205
|
+
<title>{formatMessage(messages['login.page.title'], { siteName: getSiteConfig().siteName })}</title>
|
|
206
|
+
</Helmet>
|
|
207
|
+
<RedirectLogistration
|
|
208
|
+
success={loginResult.success}
|
|
209
|
+
redirectUrl={loginResult.redirectUrl}
|
|
210
|
+
finishAuthUrl={finishAuthUrl}
|
|
211
|
+
/>
|
|
212
|
+
<div className="mw-xs mt-3 mb-2">
|
|
213
|
+
<LoginFailureMessage
|
|
214
|
+
errorCode={errorCode.type}
|
|
215
|
+
errorCount={errorCode.count}
|
|
216
|
+
context={errorCode.context}
|
|
217
|
+
/>
|
|
218
|
+
<ThirdPartyAuthAlert
|
|
219
|
+
currentProvider={currentProvider}
|
|
220
|
+
platformName={platformName}
|
|
221
|
+
/>
|
|
222
|
+
<AccountActivationMessage
|
|
223
|
+
messageType={activationMsgType}
|
|
224
|
+
/>
|
|
225
|
+
{showResetPasswordSuccessBanner && <ResetPasswordSuccess />}
|
|
226
|
+
<Form id="sign-in-form" name="sign-in-form">
|
|
227
|
+
<FormGroup
|
|
228
|
+
name="emailOrUsername"
|
|
229
|
+
value={formFields.emailOrUsername}
|
|
230
|
+
autoComplete="on"
|
|
231
|
+
handleChange={handleOnChange}
|
|
232
|
+
handleFocus={handleOnFocus}
|
|
233
|
+
errorMessage={errors.emailOrUsername}
|
|
234
|
+
floatingLabel={formatMessage(messages['login.user.identity.label'])}
|
|
235
|
+
/>
|
|
236
|
+
<PasswordField
|
|
237
|
+
name="password"
|
|
238
|
+
value={formFields.password}
|
|
239
|
+
autoComplete="off"
|
|
240
|
+
showScreenReaderText={false}
|
|
241
|
+
showRequirements={false}
|
|
242
|
+
handleChange={handleOnChange}
|
|
243
|
+
handleFocus={handleOnFocus}
|
|
244
|
+
errorMessage={errors.password}
|
|
245
|
+
floatingLabel={formatMessage(messages['login.password.label'])}
|
|
246
|
+
/>
|
|
247
|
+
<StatefulButton
|
|
248
|
+
name="sign-in"
|
|
249
|
+
id="sign-in"
|
|
250
|
+
type="submit"
|
|
251
|
+
variant="brand"
|
|
252
|
+
className="login-button-width"
|
|
253
|
+
state={submitState}
|
|
254
|
+
labels={{
|
|
255
|
+
default: formatMessage(messages['sign.in.button']),
|
|
256
|
+
pending: '',
|
|
257
|
+
}}
|
|
258
|
+
onClick={handleSubmit}
|
|
259
|
+
onMouseDown={(event) => event.preventDefault()}
|
|
260
|
+
/>
|
|
261
|
+
<Link
|
|
262
|
+
id="forgot-password"
|
|
263
|
+
name="forgot-password"
|
|
264
|
+
className="btn btn-link font-weight-500 text-body"
|
|
265
|
+
to={updatePathWithQueryParams(RESET_PAGE)}
|
|
266
|
+
onClick={trackForgotPasswordLinkClick}
|
|
267
|
+
>
|
|
268
|
+
{formatMessage(messages['forgot.password'])}
|
|
269
|
+
</Link>
|
|
270
|
+
<ThirdPartyAuth
|
|
271
|
+
currentProvider={currentProvider}
|
|
272
|
+
providers={providers}
|
|
273
|
+
secondaryProviders={secondaryProviders}
|
|
274
|
+
handleInstitutionLogin={handleInstitutionLogin}
|
|
275
|
+
thirdPartyAuthApiStatus={thirdPartyAuthApiStatus}
|
|
276
|
+
isLoginPage
|
|
277
|
+
/>
|
|
278
|
+
</Form>
|
|
279
|
+
</div>
|
|
280
|
+
</>
|
|
281
|
+
);
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
const mapStateToProps = state => {
|
|
285
|
+
const loginPageState = state.login;
|
|
286
|
+
return {
|
|
287
|
+
backedUpFormData: loginPageState.loginFormData,
|
|
288
|
+
loginErrorCode: loginPageState.loginErrorCode,
|
|
289
|
+
loginErrorContext: loginPageState.loginErrorContext,
|
|
290
|
+
loginResult: loginPageState.loginResult,
|
|
291
|
+
shouldBackupState: loginPageState.shouldBackupState,
|
|
292
|
+
showResetPasswordSuccessBanner: loginPageState.showResetPasswordSuccessBanner,
|
|
293
|
+
submitState: loginPageState.submitState,
|
|
294
|
+
thirdPartyAuthContext: thirdPartyAuthContextSelector(state),
|
|
295
|
+
thirdPartyAuthApiStatus: state.commonComponents.thirdPartyAuthApiStatus,
|
|
296
|
+
};
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
LoginPage.propTypes = {
|
|
300
|
+
backedUpFormData: PropTypes.shape({
|
|
301
|
+
formFields: PropTypes.shape({}),
|
|
302
|
+
errors: PropTypes.shape({}),
|
|
303
|
+
}),
|
|
304
|
+
loginErrorCode: PropTypes.string,
|
|
305
|
+
loginErrorContext: PropTypes.shape({
|
|
306
|
+
email: PropTypes.string,
|
|
307
|
+
redirectUrl: PropTypes.string,
|
|
308
|
+
context: PropTypes.shape({}),
|
|
309
|
+
}),
|
|
310
|
+
loginResult: PropTypes.shape({
|
|
311
|
+
redirectUrl: PropTypes.string,
|
|
312
|
+
success: PropTypes.bool,
|
|
313
|
+
}),
|
|
314
|
+
shouldBackupState: PropTypes.bool,
|
|
315
|
+
showResetPasswordSuccessBanner: PropTypes.bool,
|
|
316
|
+
submitState: PropTypes.string,
|
|
317
|
+
thirdPartyAuthApiStatus: PropTypes.string,
|
|
318
|
+
institutionLogin: PropTypes.bool.isRequired,
|
|
319
|
+
thirdPartyAuthContext: PropTypes.shape({
|
|
320
|
+
currentProvider: PropTypes.string,
|
|
321
|
+
errorMessage: PropTypes.string,
|
|
322
|
+
platformName: PropTypes.string,
|
|
323
|
+
providers: PropTypes.arrayOf(PropTypes.shape({})),
|
|
324
|
+
secondaryProviders: PropTypes.arrayOf(PropTypes.shape({})),
|
|
325
|
+
finishAuthUrl: PropTypes.string,
|
|
326
|
+
}),
|
|
327
|
+
// Actions
|
|
328
|
+
backupFormState: PropTypes.func.isRequired,
|
|
329
|
+
dismissPasswordResetBanner: PropTypes.func.isRequired,
|
|
330
|
+
loginRequest: PropTypes.func.isRequired,
|
|
331
|
+
getTPADataFromBackend: PropTypes.func.isRequired,
|
|
332
|
+
handleInstitutionLogin: PropTypes.func.isRequired,
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
LoginPage.defaultProps = {
|
|
336
|
+
backedUpFormData: {
|
|
337
|
+
formFields: {
|
|
338
|
+
emailOrUsername: '', password: '',
|
|
339
|
+
},
|
|
340
|
+
errors: {
|
|
341
|
+
emailOrUsername: '', password: '',
|
|
342
|
+
},
|
|
343
|
+
},
|
|
344
|
+
loginErrorCode: null,
|
|
345
|
+
loginErrorContext: {},
|
|
346
|
+
loginResult: {},
|
|
347
|
+
shouldBackupState: false,
|
|
348
|
+
showResetPasswordSuccessBanner: false,
|
|
349
|
+
submitState: DEFAULT_STATE,
|
|
350
|
+
thirdPartyAuthApiStatus: PENDING_STATE,
|
|
351
|
+
thirdPartyAuthContext: {
|
|
352
|
+
currentProvider: null,
|
|
353
|
+
errorMessage: null,
|
|
354
|
+
finishAuthUrl: null,
|
|
355
|
+
providers: [],
|
|
356
|
+
secondaryProviders: [],
|
|
357
|
+
},
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
export default connect(
|
|
361
|
+
mapStateToProps,
|
|
362
|
+
{
|
|
363
|
+
backupFormState: backupLoginFormBegin,
|
|
364
|
+
dismissPasswordResetBanner,
|
|
365
|
+
loginRequest,
|
|
366
|
+
getTPADataFromBackend: getThirdPartyAuthContext,
|
|
367
|
+
},
|
|
368
|
+
)(injectIntl(LoginPage));
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { AsyncActionType } from '../../data/utils';
|
|
2
|
+
|
|
3
|
+
export const BACKUP_LOGIN_DATA = new AsyncActionType('LOGIN', 'BACKUP_LOGIN_DATA');
|
|
4
|
+
export const LOGIN_REQUEST = new AsyncActionType('LOGIN', 'REQUEST');
|
|
5
|
+
export const DISMISS_PASSWORD_RESET_BANNER = 'DISMISS_PASSWORD_RESET_BANNER';
|
|
6
|
+
|
|
7
|
+
// Backup login form data
|
|
8
|
+
export const backupLoginForm = () => ({
|
|
9
|
+
type: BACKUP_LOGIN_DATA.BASE,
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
export const backupLoginFormBegin = (data) => ({
|
|
13
|
+
type: BACKUP_LOGIN_DATA.BEGIN,
|
|
14
|
+
payload: { ...data },
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
// Login
|
|
18
|
+
export const loginRequest = creds => ({
|
|
19
|
+
type: LOGIN_REQUEST.BASE,
|
|
20
|
+
payload: { creds },
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
export const loginRequestBegin = () => ({
|
|
24
|
+
type: LOGIN_REQUEST.BEGIN,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
export const loginRequestSuccess = (redirectUrl, success) => ({
|
|
28
|
+
type: LOGIN_REQUEST.SUCCESS,
|
|
29
|
+
payload: { redirectUrl, success },
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
export const loginRequestFailure = (loginError) => ({
|
|
33
|
+
type: LOGIN_REQUEST.FAILURE,
|
|
34
|
+
payload: { loginError },
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
export const dismissPasswordResetBanner = () => ({
|
|
38
|
+
type: DISMISS_PASSWORD_RESET_BANNER,
|
|
39
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Login Error Codes
|
|
2
|
+
export const INACTIVE_USER = 'inactive-user';
|
|
3
|
+
export const INTERNAL_SERVER_ERROR = 'internal-server-error';
|
|
4
|
+
export const INVALID_FORM = 'invalid-form';
|
|
5
|
+
export const NON_COMPLIANT_PASSWORD_EXCEPTION = 'NonCompliantPasswordException';
|
|
6
|
+
export const FORBIDDEN_REQUEST = 'forbidden-request';
|
|
7
|
+
export const FAILED_LOGIN_ATTEMPT = 'failed-login-attempt';
|
|
8
|
+
export const ACCOUNT_LOCKED_OUT = 'account-locked-out';
|
|
9
|
+
export const INCORRECT_EMAIL_PASSWORD = 'incorrect-email-or-password';
|
|
10
|
+
export const NUDGE_PASSWORD_CHANGE = 'nudge-password-change';
|
|
11
|
+
export const REQUIRE_PASSWORD_CHANGE = 'require-password-change';
|
|
12
|
+
export const ALLOWED_DOMAIN_LOGIN_ERROR = 'allowed-domain-login-error';
|
|
13
|
+
export const TPA_AUTHENTICATION_FAILURE = 'tpa-authentication-failure';
|
|
14
|
+
|
|
15
|
+
// Account Activation Message
|
|
16
|
+
export const ACCOUNT_ACTIVATION_MESSAGE = {
|
|
17
|
+
INFO: 'info',
|
|
18
|
+
SUCCESS: 'success',
|
|
19
|
+
ERROR: 'error',
|
|
20
|
+
};
|