@ampath/esm-login-app 8.0.0-next.10
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/3219.js +1 -0
- package/dist/3219.js.map +1 -0
- package/dist/3378.js +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/4bc56e5b0b0e91da.png +0 -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/647e55b5cedf5df2.png +0 -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/a6792134b9df70c4.png +0 -0
- package/dist/acd6ab71c5f6bcb6.jpg +0 -0
- package/dist/d0bf081185f017f3.jpg +0 -0
- package/dist/d48e253df6a333a7.png +0 -0
- package/dist/esm-login-app.js +6 -0
- package/dist/esm-login-app.js.buildmanifest.json +1598 -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/assets/Taifa-Care.png +0 -0
- package/src/assets/ampath-logo.png +0 -0
- package/src/assets/dha.png +0 -0
- package/src/assets/gok.png +0 -0
- package/src/assets/medicine.jpg +0 -0
- package/src/assets/openmrs.jpg +0 -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 +56 -0
- package/src/common/resend-timer/resend-timer.scss +7 -0
- package/src/config-schema.ts +140 -0
- package/src/declarations.d.ts +5 -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 +350 -0
- package/src/login/login.scss +246 -0
- package/src/login/login.test.tsx +288 -0
- package/src/login.resource.ts +147 -0
- package/src/logo.component.tsx +22 -0
- package/src/logout/logout.extension.tsx +23 -0
- package/src/logout/logout.scss +12 -0
- package/src/otp/otp.component.tsx +108 -0
- package/src/otp/otp.module.scss +44 -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 +90 -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 +17 -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,23 @@
|
|
|
1
|
+
import React, { useCallback } from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import { Button, SwitcherItem } from '@carbon/react';
|
|
4
|
+
import { navigate } from '@openmrs/esm-framework';
|
|
5
|
+
import styles from './logout.scss';
|
|
6
|
+
|
|
7
|
+
const Logout: React.FC = () => {
|
|
8
|
+
const { t } = useTranslation();
|
|
9
|
+
|
|
10
|
+
const handleLogout = useCallback(() => {
|
|
11
|
+
navigate({ to: '${openmrsSpaBase}/logout' });
|
|
12
|
+
}, []);
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<SwitcherItem aria-label={t('Logout', 'Logout')}>
|
|
16
|
+
<Button className={styles.logout} onClick={handleLogout} kind="ghost">
|
|
17
|
+
{t('Logout', 'Logout')}
|
|
18
|
+
</Button>
|
|
19
|
+
</SwitcherItem>
|
|
20
|
+
);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export default Logout;
|
|
@@ -0,0 +1,108 @@
|
|
|
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.module.scss';
|
|
7
|
+
import OTPInput from '../common/otp/otp.component';
|
|
8
|
+
import ResendTimer from '../common/resend-timer/resend-timer.component';
|
|
9
|
+
import Logo from '../logo.component';
|
|
10
|
+
import { verifyOtp } from '../resources/otp.resource';
|
|
11
|
+
import { refetchCurrentUser } from '@openmrs/esm-framework';
|
|
12
|
+
|
|
13
|
+
import image from '../assets/medicine.jpg';
|
|
14
|
+
|
|
15
|
+
const OtpComponent: React.FC = () => {
|
|
16
|
+
const [otpValue, setOtpValue] = useState('');
|
|
17
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
18
|
+
const [error, setError] = useState<string | null>(null);
|
|
19
|
+
const navigate = useNavigate();
|
|
20
|
+
const { t } = useTranslation();
|
|
21
|
+
const location = useLocation();
|
|
22
|
+
|
|
23
|
+
const { username, password } = location.state || {};
|
|
24
|
+
|
|
25
|
+
const handleOtpChange = (val: React.SetStateAction<string>) => {
|
|
26
|
+
setOtpValue(val);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const handleVerify = async () => {
|
|
30
|
+
setIsLoading(true);
|
|
31
|
+
setError(null);
|
|
32
|
+
try {
|
|
33
|
+
const res = await verifyOtp(username, password, otpValue);
|
|
34
|
+
|
|
35
|
+
if (res.data.success) {
|
|
36
|
+
const sessionStore = await refetchCurrentUser(username, password);
|
|
37
|
+
const session = sessionStore.session;
|
|
38
|
+
|
|
39
|
+
if (!session.sessionLocation) {
|
|
40
|
+
navigate('/login/location');
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
let to = '/home';
|
|
45
|
+
if (location.state?.referrer) {
|
|
46
|
+
to = location.state.referrer.startsWith('/')
|
|
47
|
+
? `\${openmrsSpaBase}${location.state.referrer}`
|
|
48
|
+
: location.state.referrer;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
navigate(to);
|
|
52
|
+
} else {
|
|
53
|
+
setError(res.data.message);
|
|
54
|
+
}
|
|
55
|
+
} catch (error) {
|
|
56
|
+
setError(error?.message || error?.attributes?.error || 'Invalid OTP or credentials');
|
|
57
|
+
} finally {
|
|
58
|
+
setIsLoading(false);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const handleCancel = () => {
|
|
63
|
+
const fallback = 'login';
|
|
64
|
+
if (window.history.length > 1) {
|
|
65
|
+
navigate(-1);
|
|
66
|
+
} else {
|
|
67
|
+
navigate(fallback, { replace: true });
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<>
|
|
73
|
+
<div className={styles.wrapperContainer}>
|
|
74
|
+
<div className={styles.leftSide}>
|
|
75
|
+
<div className={styles.logo}>
|
|
76
|
+
<Logo t={t} />
|
|
77
|
+
</div>
|
|
78
|
+
<div className={styles.container}>
|
|
79
|
+
<h2 className={styles.header}>OTP</h2>
|
|
80
|
+
<p>
|
|
81
|
+
Please Check your email <br /> and enter your One Time Password
|
|
82
|
+
</p>
|
|
83
|
+
<OTPInput length={5} onChange={handleOtpChange} />
|
|
84
|
+
{error && (
|
|
85
|
+
<InlineNotification
|
|
86
|
+
kind="error"
|
|
87
|
+
title="Error"
|
|
88
|
+
subtitle={error}
|
|
89
|
+
lowContrast
|
|
90
|
+
onClose={() => setError(null)}
|
|
91
|
+
/>
|
|
92
|
+
)}
|
|
93
|
+
<Button className={styles.button} onClick={handleVerify}>
|
|
94
|
+
{isLoading ? <Loading /> : 'Verify'}
|
|
95
|
+
</Button>
|
|
96
|
+
<Button className={styles.button} onClick={handleCancel}>
|
|
97
|
+
Cancel
|
|
98
|
+
</Button>
|
|
99
|
+
<ResendTimer username={username} password={password} />
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
<img className={styles.image} src={image} alt="TAIFA CARE" />
|
|
103
|
+
</div>
|
|
104
|
+
</>
|
|
105
|
+
);
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
export default OtpComponent;
|
|
@@ -0,0 +1,44 @@
|
|
|
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
|
+
.leftSide {
|
|
12
|
+
margin-top: 10rem;
|
|
13
|
+
margin-left: 10rem;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.button {
|
|
17
|
+
width: 15rem;
|
|
18
|
+
display: flex;
|
|
19
|
+
justify-content: center;
|
|
20
|
+
align-items: center;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.header {
|
|
24
|
+
font-weight: bold;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.logo {
|
|
28
|
+
margin-left: 5rem;
|
|
29
|
+
width: 15rem;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.wrapperContainer {
|
|
33
|
+
display: flex;
|
|
34
|
+
flex-direction: row;
|
|
35
|
+
gap: 10rem;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.image {
|
|
39
|
+
width: 54rem;
|
|
40
|
+
height: 55rem;
|
|
41
|
+
max-height: 60rem;
|
|
42
|
+
object-fit: cover;
|
|
43
|
+
flex-shrink: 0;
|
|
44
|
+
}
|
|
@@ -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,90 @@
|
|
|
1
|
+
import { openmrsFetch } from '@openmrs/esm-framework';
|
|
2
|
+
import { getEtlBaseUrl, getSubDomain } from '../utils/get-base-url';
|
|
3
|
+
|
|
4
|
+
const EMAIL_ATTRIBUTE_TYPE_UUID = 'ecabe213-160b-11ef-ad65-a0d3c1fcd41c';
|
|
5
|
+
|
|
6
|
+
export async function getEmail(uuid: string, username: string, password: string): Promise<string> {
|
|
7
|
+
const subDomain = await getSubDomain();
|
|
8
|
+
const credentials = window.btoa(`${username}:${password}`);
|
|
9
|
+
try {
|
|
10
|
+
if (!uuid) return;
|
|
11
|
+
|
|
12
|
+
const url = `${subDomain}/amrs/ws/rest/v1/person/${uuid}?v=custom:attributes`;
|
|
13
|
+
|
|
14
|
+
const res = await openmrsFetch(url, {
|
|
15
|
+
method: 'GET',
|
|
16
|
+
headers: {
|
|
17
|
+
Authorization: `Basic ${credentials}`,
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const data = await res.json();
|
|
22
|
+
|
|
23
|
+
if (!res.ok) {
|
|
24
|
+
throw new Error(data.message);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const emailAttr = data.attributes?.find(
|
|
28
|
+
(attr) =>
|
|
29
|
+
!attr.voided && attr.attributeType?.uuid === EMAIL_ATTRIBUTE_TYPE_UUID && typeof attr.value === 'string',
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
const email = emailAttr?.value;
|
|
33
|
+
|
|
34
|
+
if (email) {
|
|
35
|
+
return email;
|
|
36
|
+
} else {
|
|
37
|
+
throw new Error('Your email has not been configured. Please contact system administrator for assistance.');
|
|
38
|
+
}
|
|
39
|
+
} catch (error) {
|
|
40
|
+
throw new Error(error.message ?? 'Failed to fetch email');
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function getOtp(username: string, password: string, email: string) {
|
|
45
|
+
const etlBaseUrl = await getEtlBaseUrl();
|
|
46
|
+
const params = new URLSearchParams({ username, email });
|
|
47
|
+
const credentials = window.btoa(`${username}:${password}`);
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
const url = `${etlBaseUrl}/otp?${params.toString()}`;
|
|
51
|
+
|
|
52
|
+
const res = await openmrsFetch(url, {
|
|
53
|
+
method: 'GET',
|
|
54
|
+
headers: {
|
|
55
|
+
Authorization: `Basic ${credentials}`,
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const data = await res.json();
|
|
60
|
+
|
|
61
|
+
if (!res.ok) {
|
|
62
|
+
throw new Error(data.message);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return data.data.message;
|
|
66
|
+
} catch (error) {
|
|
67
|
+
throw new Error(error.message);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function verifyOtp(username: string, password: string, otp: string) {
|
|
72
|
+
const etlBaseUrl = await getEtlBaseUrl();
|
|
73
|
+
const url = etlBaseUrl + '/verify-otp';
|
|
74
|
+
const credentials = window.btoa(`${username}:${password}`);
|
|
75
|
+
|
|
76
|
+
const body = { username, otp };
|
|
77
|
+
|
|
78
|
+
const res = await openmrsFetch(url, {
|
|
79
|
+
method: 'POST',
|
|
80
|
+
headers: {
|
|
81
|
+
'Content-Type': 'Application/json',
|
|
82
|
+
Authorization: `Basic ${credentials}`,
|
|
83
|
+
},
|
|
84
|
+
body: JSON.stringify(body),
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const data = await res.json();
|
|
88
|
+
|
|
89
|
+
return data;
|
|
90
|
+
}
|
|
@@ -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,17 @@
|
|
|
1
|
+
import { getConfig } from '@openmrs/esm-framework';
|
|
2
|
+
import { moduleName } from '../';
|
|
3
|
+
|
|
4
|
+
export async function getEtlBaseUrl() {
|
|
5
|
+
const { etlBaseUrl } = await getConfig(moduleName);
|
|
6
|
+
return etlBaseUrl ?? null;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export async function getOtpEnabledStatus() {
|
|
10
|
+
const { enabled } = await getConfig(moduleName);
|
|
11
|
+
return enabled ?? null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function getSubDomain() {
|
|
15
|
+
const { subDomain } = await getConfig(moduleName);
|
|
16
|
+
return subDomain ?? null;
|
|
17
|
+
}
|