@ampath/esm-login-app 8.0.0-next.2
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/.editorconfig +8 -0
- package/.eslintignore +3 -0
- package/.eslintrc +76 -0
- package/.gitattributes +4 -0
- package/.prettierignore +9 -0
- package/.yarn/plugins/@yarnpkg/plugin-outdated.cjs +35 -0
- package/.yarn/versions/643d2b70.yml +21 -0
- package/README.md +4 -0
- package/__mocks__/config.mock.ts +26 -0
- package/__mocks__/locations.mock.ts +540 -0
- package/__mocks__/react-i18next.js +49 -0
- package/dist/1128.js +1 -0
- package/dist/1128.js.map +1 -0
- package/dist/1480.js +1 -0
- package/dist/1578.js +1 -0
- package/dist/1578.js.map +1 -0
- package/dist/1646.js +1 -0
- package/dist/1800.js +1 -0
- package/dist/1800.js.map +1 -0
- package/dist/1869.js +1 -0
- package/dist/1877.js +1 -0
- package/dist/2317.js +1 -0
- package/dist/2416.js +1 -0
- package/dist/2489.js +1 -0
- package/dist/2489.js.map +1 -0
- package/dist/282.js +1 -0
- package/dist/2881.js +1 -0
- package/dist/2997.js +1 -0
- package/dist/2997.js.map +1 -0
- package/dist/3378.js +1 -0
- package/dist/3748.js +1 -0
- package/dist/3748.js.map +1 -0
- package/dist/3963.js +1 -0
- package/dist/4106.js +1 -0
- package/dist/4111.js +1 -0
- package/dist/4169.js +1 -0
- package/dist/4169.js.map +1 -0
- package/dist/434.js +1 -0
- package/dist/4348.js +1 -0
- package/dist/4378.js +1 -0
- package/dist/4378.js.map +1 -0
- package/dist/4383.js +1 -0
- package/dist/4658.js +1 -0
- package/dist/4668.js +1 -0
- package/dist/4668.js.map +1 -0
- package/dist/4870.js +1 -0
- package/dist/4870.js.map +1 -0
- package/dist/4928.js +1 -0
- package/dist/5098.js +1 -0
- package/dist/5098.js.map +1 -0
- package/dist/5117.js +1 -0
- package/dist/5132.js +1 -0
- package/dist/5145.js +1 -0
- package/dist/5503.js +1 -0
- package/dist/556.js +1 -0
- package/dist/5644.js +1 -0
- package/dist/5898.js +1 -0
- package/dist/5898.js.map +1 -0
- package/dist/5940.js +1 -0
- package/dist/5976.js +1 -0
- package/dist/5976.js.map +1 -0
- package/dist/6047.js +1 -0
- package/dist/6237.js +1 -0
- package/dist/6237.js.map +1 -0
- package/dist/6362.js +1 -0
- package/dist/6362.js.map +1 -0
- package/dist/6371.js +1 -0
- package/dist/6377.js +1 -0
- package/dist/6444.js +1 -0
- package/dist/6508.js +1 -0
- package/dist/6724.js +1 -0
- package/dist/6904.js +1 -0
- package/dist/7045.js +1 -0
- package/dist/7144.js +43 -0
- package/dist/7144.js.map +1 -0
- package/dist/7175.js +1 -0
- package/dist/7182.js +1 -0
- package/dist/7251.js +1 -0
- package/dist/7251.js.map +1 -0
- package/dist/749.js +1 -0
- package/dist/749.js.map +1 -0
- package/dist/7742.js +1 -0
- package/dist/7912.js +1 -0
- package/dist/8358.js +1 -0
- package/dist/8359.js +1 -0
- package/dist/8695.js +1 -0
- package/dist/903.js +1 -0
- package/dist/9072.js +1 -0
- package/dist/9510.js +15 -0
- package/dist/9510.js.map +1 -0
- package/dist/9806.js +1 -0
- package/dist/esm-login-app.js +6 -0
- package/dist/esm-login-app.js.buildmanifest.json +1584 -0
- package/dist/esm-login-app.js.map +1 -0
- package/dist/main.js +6 -0
- package/dist/main.js.map +1 -0
- package/dist/routes.json +1 -0
- package/jest.config.js +20 -0
- package/package.json +111 -0
- package/prettier.config.js +8 -0
- package/rspack.config.js +1 -0
- package/src/change-location-link/change-location-link.extension.tsx +32 -0
- package/src/change-location-link/change-location-link.scss +17 -0
- package/src/change-location-link/change-location-link.test.tsx +36 -0
- package/src/change-password/change-password-link.extension.tsx +30 -0
- package/src/change-password/change-password-link.test.tsx +27 -0
- package/src/change-password/change-password-modal.scss +11 -0
- package/src/change-password/change-password.component.tsx +159 -0
- package/src/change-password/change-password.modal.tsx +175 -0
- package/src/change-password/change-password.resource.ts +12 -0
- package/src/change-password/change-password.scss +51 -0
- package/src/change-password/change-password.test.tsx +53 -0
- package/src/common/otp/otp.component.tsx +54 -0
- package/src/common/otp/otp.scss +13 -0
- package/src/common/resend-timer/resend-timer.component.tsx +51 -0
- package/src/common/resend-timer/resend-timer.scss +7 -0
- package/src/config-schema.ts +145 -0
- package/src/declarations.d.ts +1 -0
- package/src/footer.component.tsx +60 -0
- package/src/footer.scss +113 -0
- package/src/index.ts +27 -0
- package/src/loading/loading.component.tsx +11 -0
- package/src/loading/loading.scss +7 -0
- package/src/location-picker/location-picker-view.component.tsx +174 -0
- package/src/location-picker/location-picker.resource.ts +111 -0
- package/src/location-picker/location-picker.scss +94 -0
- package/src/location-picker/location-picker.test.tsx +341 -0
- package/src/login/login.component.tsx +329 -0
- package/src/login/login.scss +167 -0
- package/src/login/login.test.tsx +288 -0
- package/src/login.resource.ts +147 -0
- package/src/logo.component.tsx +23 -0
- package/src/logout/logout.extension.tsx +23 -0
- package/src/logout/logout.scss +12 -0
- package/src/otp/otp.component.tsx +105 -0
- package/src/otp/otp.scss +46 -0
- package/src/redirect-logout/logout.resource.ts +15 -0
- package/src/redirect-logout/redirect-logout.component.tsx +42 -0
- package/src/redirect-logout/redirect-logout.test.tsx +180 -0
- package/src/resources/otp.resource.ts +51 -0
- package/src/root.component.tsx +24 -0
- package/src/routes.json +63 -0
- package/src/setupTests.ts +15 -0
- package/src/test-helpers/render-with-router.tsx +17 -0
- package/src/types.ts +34 -0
- package/src/utils/get-base-url.ts +7 -0
- package/translations/am.json +41 -0
- package/translations/ar.json +41 -0
- package/translations/ar_SY.json +41 -0
- package/translations/bn.json +41 -0
- package/translations/cs.json +41 -0
- package/translations/de.json +41 -0
- package/translations/en.json +41 -0
- package/translations/en_US.json +41 -0
- package/translations/es.json +41 -0
- package/translations/es_MX.json +41 -0
- package/translations/fr.json +41 -0
- package/translations/he.json +41 -0
- package/translations/hi.json +41 -0
- package/translations/hi_IN.json +41 -0
- package/translations/id.json +41 -0
- package/translations/it.json +41 -0
- package/translations/ka.json +41 -0
- package/translations/km.json +41 -0
- package/translations/ku.json +41 -0
- package/translations/ky.json +41 -0
- package/translations/lg.json +41 -0
- package/translations/ne.json +41 -0
- package/translations/pl.json +41 -0
- package/translations/pt.json +41 -0
- package/translations/pt_BR.json +41 -0
- package/translations/qu.json +41 -0
- package/translations/ro_RO.json +41 -0
- package/translations/ru_RU.json +41 -0
- package/translations/si.json +41 -0
- package/translations/sq.json +41 -0
- package/translations/sw.json +41 -0
- package/translations/sw_KE.json +41 -0
- package/translations/tr.json +41 -0
- package/translations/tr_TR.json +41 -0
- package/translations/uk.json +41 -0
- package/translations/uz.json +41 -0
- package/translations/uz@Latn.json +41 -0
- package/translations/uz_UZ.json +41 -0
- package/translations/vi.json +41 -0
- package/translations/zh.json +41 -0
- package/translations/zh_CN.json +41 -0
- package/translations/zh_TW.json +41 -0
- package/tsconfig.json +25 -0
- package/yarnrc.yml +14 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import { Button, InlineNotification, Loading } from "@carbon/react";
|
|
3
|
+
import { useLocation, useNavigate } from "react-router-dom";
|
|
4
|
+
import { useTranslation } from "react-i18next";
|
|
5
|
+
|
|
6
|
+
import styles from "./otp.scss";
|
|
7
|
+
import OTPInput from "../common/otp/otp.component";
|
|
8
|
+
import ResendTimer from "../common/resend-timer/resend-timer.component";
|
|
9
|
+
import Footer from "../footer.component";
|
|
10
|
+
import Logo from "../logo.component";
|
|
11
|
+
import { verifyOtp } from "../resources/otp.resource";
|
|
12
|
+
import { refetchCurrentUser } from "@openmrs/esm-framework";
|
|
13
|
+
|
|
14
|
+
const OtpComponent: React.FC = () => {
|
|
15
|
+
const [otpValue, setOtpValue] = useState("");
|
|
16
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
17
|
+
const [error, setError] = useState<string | null>(null);
|
|
18
|
+
const navigate = useNavigate();
|
|
19
|
+
const { t } = useTranslation();
|
|
20
|
+
const location = useLocation();
|
|
21
|
+
|
|
22
|
+
const { username, password } = location.state || {};
|
|
23
|
+
|
|
24
|
+
const handleOtpChange = (val: React.SetStateAction<string>) => {
|
|
25
|
+
setOtpValue(val);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const handleVerify = async () => {
|
|
29
|
+
setIsLoading(true);
|
|
30
|
+
setError(null);
|
|
31
|
+
try {
|
|
32
|
+
const res = await verifyOtp(username, password, otpValue);
|
|
33
|
+
|
|
34
|
+
if (res.data.success) {
|
|
35
|
+
const sessionStore = await refetchCurrentUser(username, password);
|
|
36
|
+
const session = sessionStore.session;
|
|
37
|
+
|
|
38
|
+
if (!session.sessionLocation) {
|
|
39
|
+
navigate("/login/location");
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let to = "/home";
|
|
44
|
+
if (location.state?.referrer) {
|
|
45
|
+
to = location.state.referrer.startsWith("/")
|
|
46
|
+
? `\${openmrsSpaBase}${location.state.referrer}`
|
|
47
|
+
: location.state.referrer;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
navigate(to);
|
|
51
|
+
} else {
|
|
52
|
+
setError(res.data.message);
|
|
53
|
+
}
|
|
54
|
+
} catch (error) {
|
|
55
|
+
setError(
|
|
56
|
+
error?.message ||
|
|
57
|
+
error?.attributes?.error ||
|
|
58
|
+
"Invalid OTP or credentials"
|
|
59
|
+
);
|
|
60
|
+
} finally {
|
|
61
|
+
setIsLoading(false);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const handleCancel = () => {
|
|
66
|
+
navigate(-1);
|
|
67
|
+
};
|
|
68
|
+
return (
|
|
69
|
+
<>
|
|
70
|
+
<div className={styles.wrapperContainer}>
|
|
71
|
+
<div>
|
|
72
|
+
<div className={styles.logo}>
|
|
73
|
+
<Logo t={t} />
|
|
74
|
+
</div>
|
|
75
|
+
<div className={styles.container}>
|
|
76
|
+
<h2 className={styles.header}>OTP</h2>
|
|
77
|
+
<p>
|
|
78
|
+
Please Check your email <br /> and enter your One Time Password
|
|
79
|
+
</p>
|
|
80
|
+
<OTPInput length={5} onChange={handleOtpChange} />
|
|
81
|
+
{error && (
|
|
82
|
+
<InlineNotification
|
|
83
|
+
kind="error"
|
|
84
|
+
title="Error"
|
|
85
|
+
subtitle={error}
|
|
86
|
+
lowContrast
|
|
87
|
+
onClose={() => setError(null)}
|
|
88
|
+
/>
|
|
89
|
+
)}
|
|
90
|
+
<Button className={styles.button} onClick={handleVerify}>
|
|
91
|
+
{isLoading ? <Loading /> : "Verify"}
|
|
92
|
+
</Button>
|
|
93
|
+
<Button className={styles.button} onClick={handleCancel}>
|
|
94
|
+
Cancel
|
|
95
|
+
</Button>
|
|
96
|
+
<ResendTimer username={username} password={password} />
|
|
97
|
+
<Footer />
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
</>
|
|
102
|
+
);
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
export default OtpComponent;
|
package/src/otp/otp.scss
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
@use '@carbon/layout';
|
|
2
|
+
@use '@carbon/type';
|
|
3
|
+
@use '@openmrs/esm-styleguide/src/vars' as *;
|
|
4
|
+
|
|
5
|
+
.container {
|
|
6
|
+
display: flex;
|
|
7
|
+
flex-direction: column;
|
|
8
|
+
gap: 1rem;
|
|
9
|
+
margin-left: 5rem;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.button {
|
|
13
|
+
width: 15rem;
|
|
14
|
+
display: flex;
|
|
15
|
+
justify-content: center;
|
|
16
|
+
align-items: center;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.header {
|
|
20
|
+
font-weight: bold;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.logo {
|
|
24
|
+
margin-left: 5rem;
|
|
25
|
+
width: 15rem;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.wrapperContainer {
|
|
29
|
+
display: flex;
|
|
30
|
+
gap: 5rem;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.rightSide {
|
|
34
|
+
display: flex;
|
|
35
|
+
justify-content: center;
|
|
36
|
+
align-items: center;
|
|
37
|
+
width: 50rem;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.image {
|
|
41
|
+
width: 60rem;
|
|
42
|
+
height: 40rem;
|
|
43
|
+
border-radius: 5px;
|
|
44
|
+
// max-height: 40rem;
|
|
45
|
+
// object-fit: contain;
|
|
46
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { mutate } from 'swr';
|
|
2
|
+
import { clearCurrentUser, openmrsFetch, refetchCurrentUser, restBaseUrl } from '@openmrs/esm-framework';
|
|
3
|
+
|
|
4
|
+
export async function performLogout() {
|
|
5
|
+
await openmrsFetch(`${restBaseUrl}/session`, {
|
|
6
|
+
method: 'DELETE',
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
// clear the SWR cache on logout, do not revalidate
|
|
10
|
+
// taken from the SWR docs
|
|
11
|
+
mutate(() => true, undefined, { revalidate: false });
|
|
12
|
+
|
|
13
|
+
clearCurrentUser();
|
|
14
|
+
await refetchCurrentUser();
|
|
15
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
import { navigate, setUserLanguage, useConfig, useConnectivity, useSession } from '@openmrs/esm-framework';
|
|
3
|
+
import { clearHistory } from '@openmrs/esm-framework/src/internal';
|
|
4
|
+
import { type ConfigSchema } from '../config-schema';
|
|
5
|
+
import { performLogout } from './logout.resource';
|
|
6
|
+
|
|
7
|
+
const RedirectLogout: React.FC = () => {
|
|
8
|
+
const config = useConfig<ConfigSchema>();
|
|
9
|
+
const isLoginEnabled = useConnectivity();
|
|
10
|
+
const session = useSession();
|
|
11
|
+
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
clearHistory();
|
|
14
|
+
if (!session.authenticated || !isLoginEnabled) {
|
|
15
|
+
if (config.provider.type !== 'oauth2') {
|
|
16
|
+
navigate({ to: '${openmrsSpaBase}/login' });
|
|
17
|
+
}
|
|
18
|
+
} else {
|
|
19
|
+
performLogout()
|
|
20
|
+
.then(() => {
|
|
21
|
+
const defaultLanguage = document.documentElement.getAttribute('data-default-lang');
|
|
22
|
+
|
|
23
|
+
setUserLanguage({
|
|
24
|
+
locale: defaultLanguage,
|
|
25
|
+
authenticated: false,
|
|
26
|
+
sessionId: '',
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
if (config.provider.type !== 'oauth2') {
|
|
30
|
+
navigate({ to: '${openmrsSpaBase}/login' });
|
|
31
|
+
}
|
|
32
|
+
})
|
|
33
|
+
.catch((error) => {
|
|
34
|
+
console.error('Logout failed:', error);
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
}, [config, isLoginEnabled, session]);
|
|
38
|
+
|
|
39
|
+
return null;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export default RedirectLogout;
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { mutate } from 'swr';
|
|
3
|
+
import { render, waitFor } from '@testing-library/react';
|
|
4
|
+
import {
|
|
5
|
+
type FetchResponse,
|
|
6
|
+
type Session,
|
|
7
|
+
clearCurrentUser,
|
|
8
|
+
navigate,
|
|
9
|
+
openmrsFetch,
|
|
10
|
+
refetchCurrentUser,
|
|
11
|
+
restBaseUrl,
|
|
12
|
+
setUserLanguage,
|
|
13
|
+
useConfig,
|
|
14
|
+
useConnectivity,
|
|
15
|
+
useSession,
|
|
16
|
+
} from '@openmrs/esm-framework';
|
|
17
|
+
import RedirectLogout from './redirect-logout.component';
|
|
18
|
+
|
|
19
|
+
jest.mock('swr', () => ({
|
|
20
|
+
mutate: jest.fn(),
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
const mockClearCurrentUser = jest.mocked(clearCurrentUser);
|
|
24
|
+
const mockNavigate = jest.mocked(navigate);
|
|
25
|
+
const mockOpenmrsFetch = jest.mocked(openmrsFetch);
|
|
26
|
+
const mockRefetchCurrentUser = jest.mocked(refetchCurrentUser);
|
|
27
|
+
const mockSetUserLanguage = jest.mocked(setUserLanguage);
|
|
28
|
+
const mockUseConfig = jest.mocked(useConfig);
|
|
29
|
+
const mockUseConnectivity = jest.mocked(useConnectivity);
|
|
30
|
+
const mockUseSession = jest.mocked(useSession);
|
|
31
|
+
|
|
32
|
+
describe('RedirectLogout', () => {
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
mockUseConnectivity.mockReturnValue(true);
|
|
35
|
+
mockOpenmrsFetch.mockResolvedValue({} as FetchResponse<unknown>);
|
|
36
|
+
|
|
37
|
+
mockUseSession.mockReturnValue({
|
|
38
|
+
authenticated: true,
|
|
39
|
+
sessionId: 'xyz',
|
|
40
|
+
} as Session);
|
|
41
|
+
|
|
42
|
+
mockUseConfig.mockReturnValue({
|
|
43
|
+
provider: {
|
|
44
|
+
type: '',
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
Object.defineProperty(document, 'documentElement', {
|
|
49
|
+
configurable: true,
|
|
50
|
+
value: {
|
|
51
|
+
getAttribute: jest.fn().mockReturnValue('km'),
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should redirect to login page upon logout', async () => {
|
|
57
|
+
render(<RedirectLogout />);
|
|
58
|
+
|
|
59
|
+
expect(mockOpenmrsFetch).toHaveBeenCalledWith(`${restBaseUrl}/session`, {
|
|
60
|
+
method: 'DELETE',
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
await waitFor(() => expect(mutate).toHaveBeenCalled());
|
|
64
|
+
|
|
65
|
+
expect(mockClearCurrentUser).toHaveBeenCalled();
|
|
66
|
+
expect(mockRefetchCurrentUser).toHaveBeenCalled();
|
|
67
|
+
expect(mockSetUserLanguage).toHaveBeenCalledWith({
|
|
68
|
+
locale: 'km',
|
|
69
|
+
authenticated: false,
|
|
70
|
+
sessionId: '',
|
|
71
|
+
});
|
|
72
|
+
expect(mockNavigate).toHaveBeenCalledWith({ to: '${openmrsSpaBase}/login' });
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('should not redirect if the configured provider is `oauth2`', async () => {
|
|
76
|
+
mockUseConfig.mockReturnValue({
|
|
77
|
+
provider: {
|
|
78
|
+
type: 'oauth2',
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
render(<RedirectLogout />);
|
|
83
|
+
|
|
84
|
+
expect(mockOpenmrsFetch).toHaveBeenCalledWith(`${restBaseUrl}/session`, {
|
|
85
|
+
method: 'DELETE',
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
await waitFor(() => expect(mutate).toHaveBeenCalled());
|
|
89
|
+
|
|
90
|
+
expect(mockClearCurrentUser).toHaveBeenCalled();
|
|
91
|
+
expect(mockRefetchCurrentUser).toHaveBeenCalled();
|
|
92
|
+
expect(mockSetUserLanguage).toHaveBeenCalledWith({
|
|
93
|
+
locale: 'km',
|
|
94
|
+
authenticated: false,
|
|
95
|
+
sessionId: '',
|
|
96
|
+
});
|
|
97
|
+
expect(mockNavigate).toHaveBeenCalledTimes(0);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('should redirect to login if the session is already unauthenticated', async () => {
|
|
101
|
+
mockUseSession.mockReturnValue({
|
|
102
|
+
authenticated: false,
|
|
103
|
+
} as Session);
|
|
104
|
+
|
|
105
|
+
render(<RedirectLogout />);
|
|
106
|
+
|
|
107
|
+
expect(mockNavigate).toHaveBeenCalledWith({ to: '${openmrsSpaBase}/login' });
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('should redirect to login if the application is offline', async () => {
|
|
111
|
+
mockUseConnectivity.mockReturnValue(false);
|
|
112
|
+
|
|
113
|
+
render(<RedirectLogout />);
|
|
114
|
+
|
|
115
|
+
expect(mockNavigate).toHaveBeenCalledWith({ to: '${openmrsSpaBase}/login' });
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('should handle logout failure gracefully', async () => {
|
|
119
|
+
const consoleError = jest.spyOn(console, 'error').mockImplementation(() => {});
|
|
120
|
+
mockOpenmrsFetch.mockRejectedValue(new Error('Logout failed'));
|
|
121
|
+
|
|
122
|
+
render(<RedirectLogout />);
|
|
123
|
+
|
|
124
|
+
await waitFor(() => {
|
|
125
|
+
expect(consoleError).toHaveBeenCalledWith('Logout failed:', new Error('Logout failed'));
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
consoleError.mockRestore();
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('should handle missing default language attribute', async () => {
|
|
132
|
+
Object.defineProperty(document, 'documentElement', {
|
|
133
|
+
configurable: true,
|
|
134
|
+
value: {
|
|
135
|
+
getAttribute: jest.fn().mockReturnValue(null),
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
render(<RedirectLogout />);
|
|
140
|
+
|
|
141
|
+
await waitFor(() => {
|
|
142
|
+
expect(mockSetUserLanguage).toHaveBeenCalledWith({
|
|
143
|
+
locale: null,
|
|
144
|
+
authenticated: false,
|
|
145
|
+
sessionId: '',
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('should handle config changes appropriately', async () => {
|
|
151
|
+
const { rerender } = render(<RedirectLogout />);
|
|
152
|
+
|
|
153
|
+
mockUseConfig.mockReturnValue({
|
|
154
|
+
provider: {
|
|
155
|
+
type: 'testProvider',
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
rerender(<RedirectLogout />);
|
|
160
|
+
|
|
161
|
+
await waitFor(() => {
|
|
162
|
+
expect(mockNavigate).toHaveBeenCalledWith({ to: '${openmrsSpaBase}/login' });
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('should not redirect to login if user is not authenticated and the provider is oauth2', async () => {
|
|
167
|
+
mockUseSession.mockReturnValue({
|
|
168
|
+
authenticated: false,
|
|
169
|
+
} as Session);
|
|
170
|
+
mockUseConfig.mockReturnValue({
|
|
171
|
+
provider: {
|
|
172
|
+
type: 'oauth2',
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
render(<RedirectLogout />);
|
|
177
|
+
|
|
178
|
+
expect(mockNavigate).toHaveBeenCalledTimes(0);
|
|
179
|
+
});
|
|
180
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { getEtlBaseUrl } from "../utils/get-base-url";
|
|
2
|
+
|
|
3
|
+
const etlBaseUrl = "https://staging.ampath.or.ke/etl-staging/etl/";
|
|
4
|
+
|
|
5
|
+
export async function getOtp(username: string, password: string) {
|
|
6
|
+
// const etlBaseUrl = await getEtlBaseUrl();
|
|
7
|
+
const params = new URLSearchParams({ username });
|
|
8
|
+
const credentials = window.btoa(`${username}:${password}`);
|
|
9
|
+
|
|
10
|
+
const url = `${etlBaseUrl}otp?${params.toString()}`;
|
|
11
|
+
|
|
12
|
+
const res = await fetch(url, {
|
|
13
|
+
method: "GET",
|
|
14
|
+
headers: {
|
|
15
|
+
Authorization: `Basic ${credentials}`,
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
if (!res.ok) {
|
|
20
|
+
throw new Error("Failed to fetch OTP");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const data = await res.json();
|
|
24
|
+
|
|
25
|
+
return data;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function verifyOtp(
|
|
29
|
+
username: string,
|
|
30
|
+
password: string,
|
|
31
|
+
otp: string
|
|
32
|
+
) {
|
|
33
|
+
// const etlBaseUrl = await getEtlBaseUrl();
|
|
34
|
+
const url = etlBaseUrl + "verify-otp";
|
|
35
|
+
const credentials = window.btoa(`${username}:${password}`);
|
|
36
|
+
|
|
37
|
+
const body = { username, otp };
|
|
38
|
+
|
|
39
|
+
const res = await fetch(url, {
|
|
40
|
+
method: "POST",
|
|
41
|
+
headers: {
|
|
42
|
+
"Content-Type": "Application/json",
|
|
43
|
+
Authorization: `Basic ${credentials}`,
|
|
44
|
+
},
|
|
45
|
+
body: JSON.stringify(body),
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const data = await res.json();
|
|
49
|
+
|
|
50
|
+
return data;
|
|
51
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { BrowserRouter, Route, Routes } from "react-router-dom";
|
|
3
|
+
import ChangePassword from "./change-password/change-password.component";
|
|
4
|
+
import LocationPickerView from "./location-picker/location-picker-view.component";
|
|
5
|
+
import Login from "./login/login.component";
|
|
6
|
+
import RedirectLogout from "./redirect-logout/redirect-logout.component";
|
|
7
|
+
import OtpComponent from "./otp/otp.component";
|
|
8
|
+
|
|
9
|
+
const Root: React.FC = () => {
|
|
10
|
+
return (
|
|
11
|
+
<BrowserRouter basename={window.getOpenmrsSpaBase()}>
|
|
12
|
+
<Routes>
|
|
13
|
+
<Route path="login" element={<Login />} />
|
|
14
|
+
<Route path="login/confirm" element={<Login />} />
|
|
15
|
+
<Route path="login/location" element={<LocationPickerView />} />
|
|
16
|
+
<Route path="logout" element={<RedirectLogout />} />
|
|
17
|
+
<Route path="change-password" element={<ChangePassword />} />
|
|
18
|
+
<Route path="login/otp" element={<OtpComponent />} />
|
|
19
|
+
</Routes>
|
|
20
|
+
</BrowserRouter>
|
|
21
|
+
);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export default Root;
|
package/src/routes.json
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json.openmrs.org/routes.schema.json",
|
|
3
|
+
"backendDependencies": {
|
|
4
|
+
"webservices.rest": ">=2.2.0"
|
|
5
|
+
},
|
|
6
|
+
"pages": [
|
|
7
|
+
{
|
|
8
|
+
"component": "root",
|
|
9
|
+
"route": "login",
|
|
10
|
+
"online": true,
|
|
11
|
+
"offline": true
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"component": "root",
|
|
15
|
+
"route": "logout",
|
|
16
|
+
"online": true,
|
|
17
|
+
"offline": true
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"component": "root",
|
|
21
|
+
"route": "change-password",
|
|
22
|
+
"online": true,
|
|
23
|
+
"offline": true
|
|
24
|
+
}
|
|
25
|
+
],
|
|
26
|
+
"extensions": [
|
|
27
|
+
{
|
|
28
|
+
"name": "location-picker",
|
|
29
|
+
"slot": "location-picker",
|
|
30
|
+
"component": "locationPicker",
|
|
31
|
+
"online": true,
|
|
32
|
+
"offline": true
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"name": "logout-button",
|
|
36
|
+
"slot": "user-panel-bottom-slot",
|
|
37
|
+
"component": "logoutButton",
|
|
38
|
+
"online": true,
|
|
39
|
+
"offline": true
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"name": "password-changer",
|
|
43
|
+
"slot": "user-panel-slot",
|
|
44
|
+
"component": "changePasswordLink",
|
|
45
|
+
"online": true,
|
|
46
|
+
"offline": true
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"name": "location-changer",
|
|
50
|
+
"slot": "top-nav-info-slot",
|
|
51
|
+
"component": "changeLocationLink",
|
|
52
|
+
"online": true,
|
|
53
|
+
"offline": true,
|
|
54
|
+
"order": 1
|
|
55
|
+
}
|
|
56
|
+
],
|
|
57
|
+
"modals": [
|
|
58
|
+
{
|
|
59
|
+
"name": "change-password-modal",
|
|
60
|
+
"component": "changePasswordModal"
|
|
61
|
+
}
|
|
62
|
+
]
|
|
63
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import '@testing-library/jest-dom';
|
|
2
|
+
|
|
3
|
+
declare global {
|
|
4
|
+
interface Window {
|
|
5
|
+
openmrsBase: string;
|
|
6
|
+
spaBase: string;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const { getComputedStyle } = window;
|
|
11
|
+
window.getComputedStyle = (element) => getComputedStyle(element);
|
|
12
|
+
window.openmrsBase = '/openmrs';
|
|
13
|
+
window.spaBase = '/spa';
|
|
14
|
+
window.getOpenmrsSpaBase = () => '/openmrs/spa/';
|
|
15
|
+
window.HTMLElement.prototype.scrollIntoView = jest.fn();
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { MemoryRouter } from 'react-router-dom';
|
|
3
|
+
import { render } from '@testing-library/react';
|
|
4
|
+
|
|
5
|
+
export default function renderWithRouter<T = unknown>(
|
|
6
|
+
Component: React.JSXElementConstructor<T>,
|
|
7
|
+
props: T = {} as unknown as T,
|
|
8
|
+
{ route = '/', routes = [route], routeParams = {} } = {},
|
|
9
|
+
) {
|
|
10
|
+
return {
|
|
11
|
+
...render(
|
|
12
|
+
<MemoryRouter initialEntries={routes} initialIndex={(route && routes?.indexOf(route)) || undefined}>
|
|
13
|
+
<Component {...props} />
|
|
14
|
+
</MemoryRouter>,
|
|
15
|
+
),
|
|
16
|
+
};
|
|
17
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type FHIRLocationResource } from '@openmrs/esm-framework';
|
|
2
|
+
|
|
3
|
+
export interface LocationResponse {
|
|
4
|
+
type: string;
|
|
5
|
+
total: number;
|
|
6
|
+
resourceType: string;
|
|
7
|
+
meta: {
|
|
8
|
+
lastUpdated: string;
|
|
9
|
+
};
|
|
10
|
+
link: Array<{
|
|
11
|
+
relation: string;
|
|
12
|
+
url: string;
|
|
13
|
+
}>;
|
|
14
|
+
id: string;
|
|
15
|
+
entry: Array<FHIRLocationResource>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface LocationEntry {
|
|
19
|
+
resource: Resource;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface Resource {
|
|
23
|
+
id: string;
|
|
24
|
+
name: string;
|
|
25
|
+
resourceType: string;
|
|
26
|
+
status: 'active' | 'inactive';
|
|
27
|
+
meta?: {
|
|
28
|
+
tag?: Array<{
|
|
29
|
+
code: string;
|
|
30
|
+
display: string;
|
|
31
|
+
system: string;
|
|
32
|
+
}>;
|
|
33
|
+
};
|
|
34
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"builtWith": "Built with",
|
|
3
|
+
"cancel": "ሰርዝ",
|
|
4
|
+
"change": "Change",
|
|
5
|
+
"changeLocation": "Change location",
|
|
6
|
+
"changePassword": "Change password",
|
|
7
|
+
"changingPassword": "Changing password",
|
|
8
|
+
"confirmPassword": "Confirm new password",
|
|
9
|
+
"continue": "Continue",
|
|
10
|
+
"errorChangingPassword": "Error changing password",
|
|
11
|
+
"footerlogo": "Footer Logo",
|
|
12
|
+
"invalidCredentials": "Invalid username or password",
|
|
13
|
+
"learnMore": "Learn more",
|
|
14
|
+
"locationPreferenceRemoved": "Login location preference removed",
|
|
15
|
+
"locationPreferenceRemovedMessage": "You will need to select a location on each login",
|
|
16
|
+
"locationSaved": "Location saved",
|
|
17
|
+
"locationSaveMessage": "Your preferred location has been saved for future logins",
|
|
18
|
+
"locationUpdated": "Location updated",
|
|
19
|
+
"locationUpdateMessage": "Your preferred login location has been updated",
|
|
20
|
+
"loggingIn": "Logging in",
|
|
21
|
+
"login": "Log in",
|
|
22
|
+
"loginButtonIconDescription": "Log in button",
|
|
23
|
+
"Logout": "Logout",
|
|
24
|
+
"newPassword": "New password",
|
|
25
|
+
"newPasswordRequired": "New password is required",
|
|
26
|
+
"oldPassword": "Old password",
|
|
27
|
+
"oldPasswordRequired": "Old password is required",
|
|
28
|
+
"openmrsLogo": "OpenMRS logo",
|
|
29
|
+
"password": "Password",
|
|
30
|
+
"passwordChangedSuccessfully": "Password changed successfully",
|
|
31
|
+
"passwordConfirmationRequired": "Password confirmation is required",
|
|
32
|
+
"passwordsDoNotMatch": "Passwords do not match",
|
|
33
|
+
"poweredBySubtext": "An open-source medical record system and global community",
|
|
34
|
+
"rememberLocationForFutureLogins": "Remember my location for future logins",
|
|
35
|
+
"selectYourLocation": "Select your location from the list below. Use the search bar to find your location.",
|
|
36
|
+
"showPassword": "Show password",
|
|
37
|
+
"submitting": "Submitting",
|
|
38
|
+
"username": "Username",
|
|
39
|
+
"validValueRequired": "A valid value is required",
|
|
40
|
+
"welcome": "Welcome"
|
|
41
|
+
}
|