@ampath/esm-login-app 8.0.0-next.2 → 8.0.0-next.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/dist/4bc56e5b0b0e91da.png +0 -0
  2. package/dist/5940.js +1 -1
  3. package/dist/5976.js +1 -1
  4. package/dist/5976.js.map +1 -1
  5. package/dist/647e55b5cedf5df2.png +0 -0
  6. package/dist/7144.js +12 -12
  7. package/dist/7144.js.map +1 -1
  8. package/dist/7244.js +1 -0
  9. package/dist/7244.js.map +1 -0
  10. package/dist/a6792134b9df70c4.png +0 -0
  11. package/dist/acd6ab71c5f6bcb6.jpg +0 -0
  12. package/dist/d0bf081185f017f3.jpg +0 -0
  13. package/dist/d48e253df6a333a7.png +0 -0
  14. package/dist/esm-login-app.js +2 -2
  15. package/dist/esm-login-app.js.buildmanifest.json +48 -34
  16. package/dist/main.js +3 -3
  17. package/dist/main.js.map +1 -1
  18. package/dist/routes.json +1 -1
  19. package/package.json +1 -1
  20. package/src/assets/Taifa-Care.png +0 -0
  21. package/src/assets/ampath-logo.png +0 -0
  22. package/src/assets/dha.png +0 -0
  23. package/src/assets/gok.png +0 -0
  24. package/src/assets/medicine.jpg +0 -0
  25. package/src/assets/openmrs.jpg +0 -0
  26. package/src/common/resend-timer/resend-timer.component.tsx +7 -2
  27. package/src/config-schema.ts +32 -37
  28. package/src/declarations.d.ts +4 -0
  29. package/src/forgot-password/forgot-password.component.tsx +113 -0
  30. package/src/forgot-password/forgot-password.resource.ts +11 -0
  31. package/src/forgot-password/forgot-password.scss +51 -0
  32. package/src/forgot-password/reset-password/reset-password.component.tsx +131 -0
  33. package/src/forgot-password/reset-password/reset-password.resource.ts +11 -0
  34. package/src/login/login.component.tsx +246 -205
  35. package/src/login/login.scss +110 -4
  36. package/src/logo.component.tsx +12 -11
  37. package/src/otp/otp.component.tsx +41 -30
  38. package/src/otp/otp.module.scss +61 -0
  39. package/src/resources/otp.resource.ts +77 -26
  40. package/src/root.component.tsx +4 -0
  41. package/src/utils/get-base-url.ts +17 -2
  42. package/yarnrc.yml +2 -2
  43. package/dist/3748.js +0 -1
  44. package/dist/3748.js.map +0 -1
  45. package/src/otp/otp.scss +0 -46
@@ -2,6 +2,12 @@
2
2
  @use '@carbon/type';
3
3
  @use '@openmrs/esm-styleguide/src/vars' as *;
4
4
 
5
+ /* Hide OpenMRS top nav on login */
6
+ :global(.hide-top-nav #omrs-top-nav-app-container) {
7
+ display: none;
8
+ height: 0;
9
+ }
10
+
5
11
  .bodyShort01 {
6
12
  @include type.type-style('body-compact-01');
7
13
  }
@@ -34,10 +40,12 @@
34
40
 
35
41
  .container {
36
42
  display: flex;
37
- flex-direction: column;
38
- justify-content: center;
43
+ flex-direction: row;
44
+ justify-content: flex-start;
39
45
  align-items: center;
40
46
  position: relative;
47
+ margin-left: 10rem;
48
+ min-height: 100vh;
41
49
  }
42
50
 
43
51
  .center {
@@ -45,13 +53,13 @@
45
53
  }
46
54
 
47
55
  .logo {
48
- margin-bottom: layout.$spacing-08;
56
+ // margin-bottom: layout.$spacing-08;
49
57
  height: layout.$spacing-11;
50
58
  width: 16rem;
51
59
  }
52
60
 
53
61
  .logoImg {
54
- margin-bottom: layout.$spacing-09;
62
+ // margin-bottom: layout.$spacing-03;
55
63
  max-width: 100%;
56
64
  }
57
65
 
@@ -165,3 +173,101 @@
165
173
  position: absolute;
166
174
  pointer-events: none;
167
175
  }
176
+
177
+ .wrapperContainer {
178
+ display: flex;
179
+ flex-direction: row;
180
+ gap: 10rem;
181
+ max-width: 100vw;
182
+ max-height: 100vh;
183
+ }
184
+
185
+ .image {
186
+ width: 100%;
187
+ height: 100%;
188
+ max-width: 100vw;
189
+ max-height: 100vh;
190
+ object-fit: cover;
191
+ }
192
+
193
+ @media (max-width: 1024px) {
194
+ .image {
195
+ max-width: 40rem;
196
+ }
197
+ }
198
+
199
+ @media (max-width: 768px) {
200
+ .image {
201
+ max-width: 100%;
202
+ }
203
+ }
204
+
205
+ .logoContainer {
206
+ display: flex;
207
+ flex-direction: column;
208
+ }
209
+
210
+ .poweredBy {
211
+ display: flex;
212
+ align-items: center;
213
+ justify-self: flex-end;
214
+ font-size: 0.75rem;
215
+ color: blue;
216
+ opacity: 0.85;
217
+ white-space: nowrap;
218
+ font-weight: bold;
219
+ margin-bottom: layout.$spacing-08;
220
+ }
221
+
222
+ .poweredByLogo {
223
+ margin-left: 0.5rem;
224
+ width: 2rem;
225
+ height: auto;
226
+ object-fit: contain;
227
+ border-radius: 0.5rem;
228
+ }
229
+
230
+ .logoContainer {
231
+ display: flex;
232
+ flex-direction: column;
233
+ }
234
+
235
+ .logoSection {
236
+ position: fixed;
237
+ bottom: layout.$spacing-01;
238
+ display: flex;
239
+ justify-content: space-between;
240
+ align-items: center;
241
+ padding: 0 layout.$spacing-04;
242
+ gap: clamp(8rem, 27vw, 26rem);
243
+ }
244
+
245
+ .leftLogos {
246
+ display: flex;
247
+ flex-direction: row;
248
+ gap: 1rem;
249
+ border: 2px solid lightgray;
250
+ border-radius: 0.5rem;
251
+ }
252
+
253
+ .supportingLogos {
254
+ margin-left: 0.5rem;
255
+ width: 3rem;
256
+ height: auto;
257
+ object-fit: contain;
258
+ border-radius: 0.5rem;
259
+ }
260
+ .dhaLogo {
261
+ margin-left: 0.5rem;
262
+ width: 4rem;
263
+ height: auto;
264
+ object-fit: contain;
265
+ border-radius: 0.5rem;
266
+ }
267
+ .openmsrsLogo {
268
+ margin-left: 0.5rem;
269
+ width: 6rem;
270
+ height: auto;
271
+ object-fit: contain;
272
+ border-radius: 0.5rem;
273
+ }
@@ -3,20 +3,21 @@ import { interpolateUrl, useConfig } from '@openmrs/esm-framework';
3
3
  import { type TFunction } from 'i18next';
4
4
  import { type ConfigSchema } from './config-schema';
5
5
  import styles from './login/login.scss';
6
+ import taifaCare from './assets/Taifa-Care.png';
7
+ import amrsLogo from './assets/ampath-logo.png';
6
8
 
7
9
  const Logo: React.FC<{ t: TFunction }> = ({ t }) => {
8
10
  const { logo } = useConfig<ConfigSchema>();
9
- return logo.src ? (
10
- <img
11
- alt={logo.alt ? t(logo.alt) : t('openmrsLogo', 'OpenMRS logo')}
12
- className={styles.logoImg}
13
- src={interpolateUrl(logo.src)}
14
- />
15
- ) : (
16
- <svg role="img" className={styles.logo}>
17
- <title>{t('openmrsLogo', 'OpenMRS logo')}</title>
18
- <use href="#omrs-logo-full-color"></use>
19
- </svg>
11
+ return (
12
+ <div className={styles.logoContainer}>
13
+ <img alt={logo.alt ? t(logo.alt) : t('openmrsLogo', 'OpenMRS logo')} className={styles.logoImg} src={taifaCare} />
14
+ <div>
15
+ <span className={styles.poweredBy}>
16
+ {t('poweredBy', 'Powered by AMRS')}{' '}
17
+ <img src={amrsLogo} alt={t('taifaCare', 'TAIFA CARE logo')} className={styles.poweredByLogo} />
18
+ </span>
19
+ </div>
20
+ </div>
20
21
  );
21
22
  };
22
23
 
@@ -1,30 +1,38 @@
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";
1
+ import React, { useEffect, 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
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";
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';
13
14
 
14
15
  const OtpComponent: React.FC = () => {
15
- const [otpValue, setOtpValue] = useState("");
16
+ const [otpValue, setOtpValue] = useState('');
16
17
  const [isLoading, setIsLoading] = useState(false);
17
18
  const [error, setError] = useState<string | null>(null);
18
19
  const navigate = useNavigate();
19
20
  const { t } = useTranslation();
20
21
  const location = useLocation();
21
22
 
22
- const { username, password } = location.state || {};
23
+ const { username, password, message } = location.state || {};
23
24
 
24
25
  const handleOtpChange = (val: React.SetStateAction<string>) => {
25
26
  setOtpValue(val);
26
27
  };
27
28
 
29
+ useEffect(() => {
30
+ document.body.classList.add('hide-top-nav');
31
+ return () => {
32
+ document.body.classList.remove('hide-top-nav');
33
+ };
34
+ }, []);
35
+
28
36
  const handleVerify = async () => {
29
37
  setIsLoading(true);
30
38
  setError(null);
@@ -36,47 +44,50 @@ const OtpComponent: React.FC = () => {
36
44
  const session = sessionStore.session;
37
45
 
38
46
  if (!session.sessionLocation) {
39
- navigate("/login/location");
47
+ navigate('/login/location');
40
48
  return;
41
49
  }
42
50
 
43
- let to = "/home";
51
+ let to = '/home';
44
52
  if (location.state?.referrer) {
45
- to = location.state.referrer.startsWith("/")
46
- ? `\${openmrsSpaBase}${location.state.referrer}`
47
- : location.state.referrer;
53
+ to = location.state.referrer;
48
54
  }
55
+ // if (location.state?.referrer) {
56
+ // to = location.state.referrer.startsWith('/')
57
+ // ? `\${openmrsSpaBase}${location.state.referrer}`
58
+ // : location.state.referrer;
59
+ // }
49
60
 
50
61
  navigate(to);
51
62
  } else {
52
63
  setError(res.data.message);
53
64
  }
54
65
  } catch (error) {
55
- setError(
56
- error?.message ||
57
- error?.attributes?.error ||
58
- "Invalid OTP or credentials"
59
- );
66
+ setError(error?.message || error?.attributes?.error || 'Invalid OTP or credentials');
60
67
  } finally {
61
68
  setIsLoading(false);
62
69
  }
63
70
  };
64
71
 
65
72
  const handleCancel = () => {
66
- navigate(-1);
73
+ const fallback = 'login';
74
+ if (window.history.length > 1) {
75
+ navigate(-1);
76
+ } else {
77
+ navigate(fallback, { replace: true });
78
+ }
67
79
  };
80
+
68
81
  return (
69
82
  <>
70
83
  <div className={styles.wrapperContainer}>
71
- <div>
84
+ <div className={styles.leftSide}>
72
85
  <div className={styles.logo}>
73
86
  <Logo t={t} />
74
87
  </div>
75
88
  <div className={styles.container}>
76
89
  <h2 className={styles.header}>OTP</h2>
77
- <p>
78
- Please Check your email <br /> and enter your One Time Password
79
- </p>
90
+ <p>{message || 'Enter the OTP sent to your registered email and phone number to complete login.'}</p>
80
91
  <OTPInput length={5} onChange={handleOtpChange} />
81
92
  {error && (
82
93
  <InlineNotification
@@ -88,15 +99,15 @@ const OtpComponent: React.FC = () => {
88
99
  />
89
100
  )}
90
101
  <Button className={styles.button} onClick={handleVerify}>
91
- {isLoading ? <Loading /> : "Verify"}
102
+ {isLoading ? <Loading /> : 'Verify'}
92
103
  </Button>
93
104
  <Button className={styles.button} onClick={handleCancel}>
94
105
  Cancel
95
106
  </Button>
96
107
  <ResendTimer username={username} password={password} />
97
- <Footer />
98
108
  </div>
99
109
  </div>
110
+ <img className={styles.image} src={image} alt="TAIFA CARE" />
100
111
  </div>
101
112
  </>
102
113
  );
@@ -0,0 +1,61 @@
1
+ @use '@carbon/layout';
2
+ @use '@carbon/type';
3
+ @use '@openmrs/esm-styleguide/src/vars' as *;
4
+
5
+ :global(.hide-top-nav #omrs-top-nav-app-container) {
6
+ display: none;
7
+ height: 0;
8
+ }
9
+
10
+ .container {
11
+ display: flex;
12
+ flex-direction: column;
13
+ gap: 1rem;
14
+ margin-left: 5rem;
15
+ }
16
+ .leftSide {
17
+ margin-top: clamp(5rem, 10vw, 20rem);
18
+ margin-left: clamp(2rem, 5vw, 10rem);
19
+ }
20
+
21
+ .button {
22
+ width: 15rem;
23
+ display: flex;
24
+ justify-content: center;
25
+ align-items: center;
26
+ }
27
+
28
+ .header {
29
+ font-weight: bold;
30
+ }
31
+
32
+ .logo {
33
+ margin-left: 5rem;
34
+ width: 15rem;
35
+ }
36
+
37
+ .wrapperContainer {
38
+ display: flex;
39
+ flex-direction: row;
40
+ gap: 10rem;
41
+ }
42
+
43
+ .image {
44
+ width: 100%;
45
+ height: 100%;
46
+ max-width: 100vw;
47
+ max-height: 100vh;
48
+ object-fit: cover;
49
+ }
50
+
51
+ @media (max-width: 1024px) {
52
+ .image {
53
+ max-width: 40rem;
54
+ }
55
+ }
56
+
57
+ @media (max-width: 768px) {
58
+ .image {
59
+ max-width: 100%;
60
+ }
61
+ }
@@ -1,45 +1,96 @@
1
- import { getEtlBaseUrl } from "../utils/get-base-url";
1
+ import { openmrsFetch } from '@openmrs/esm-framework';
2
+ import { getEtlBaseUrl, getOtpKey, getSubDomain } from '../utils/get-base-url';
2
3
 
3
- const etlBaseUrl = "https://staging.ampath.or.ke/etl-staging/etl/";
4
+ const EMAIL_ATTRIBUTE_TYPE_UUID = 'ecabe213-160b-11ef-ad65-a0d3c1fcd41c';
5
+ const PHONE_NUMBER_ATTRIBUTE_TYPE_UUID = '72a759a8-1359-11df-a1f1-0026b9348838';
4
6
 
5
- export async function getOtp(username: string, password: string) {
6
- // const etlBaseUrl = await getEtlBaseUrl();
7
- const params = new URLSearchParams({ username });
7
+ type ContactInfo = {
8
+ email?: string | null;
9
+ phone?: string | null;
10
+ };
11
+
12
+ export async function getEmailAndPhone(uuid: string, username: string, password: string): Promise<ContactInfo> {
13
+ const subDomain = await getSubDomain();
8
14
  const credentials = window.btoa(`${username}:${password}`);
15
+ try {
16
+ if (!uuid) return;
17
+ const url = `${subDomain}/amrs/ws/rest/v1/person/${uuid}?v=custom:attributes`;
9
18
 
10
- const url = `${etlBaseUrl}otp?${params.toString()}`;
19
+ const res = await openmrsFetch(url, {
20
+ method: 'GET',
21
+ headers: {
22
+ Authorization: `Basic ${credentials}`,
23
+ },
24
+ });
11
25
 
12
- const res = await fetch(url, {
13
- method: "GET",
14
- headers: {
15
- Authorization: `Basic ${credentials}`,
16
- },
17
- });
26
+ const data = await res.json();
27
+
28
+ if (!res.ok) {
29
+ throw new Error(data.message);
30
+ }
31
+
32
+ const emailAttr = data.attributes?.find(
33
+ (attr) =>
34
+ !attr.voided && attr.attributeType?.uuid === EMAIL_ATTRIBUTE_TYPE_UUID && typeof attr.value === 'string',
35
+ );
36
+
37
+ const phoneAttr = data.attributes?.find(
38
+ (attr) => !attr.voided && attr.attributeType?.uuid === PHONE_NUMBER_ATTRIBUTE_TYPE_UUID,
39
+ );
40
+ const email = emailAttr?.value ?? null;
41
+ const phone = phoneAttr?.value ?? null;
42
+
43
+ if (!email && !phone) {
44
+ throw new Error(
45
+ 'Neither email nor phone number has been configured. Please contact system administrator for assistance.',
46
+ );
47
+ }
18
48
 
19
- if (!res.ok) {
20
- throw new Error("Failed to fetch OTP");
49
+ return { email, phone };
50
+ } catch (error) {
51
+ throw new Error(error.message ?? 'Failed to fetch contact info');
21
52
  }
53
+ }
22
54
 
23
- const data = await res.json();
55
+ export async function getOtp(username: string, password: string, email: string, phone: string) {
56
+ const etlBaseUrl = await getEtlBaseUrl();
57
+ const key = await getOtpKey();
58
+ const params = new URLSearchParams({ username, email, phone, key });
59
+ const credentials = window.btoa(`${username}:${password}`);
24
60
 
25
- return data;
61
+ try {
62
+ const url = `${etlBaseUrl}/otp?${params.toString()}`;
63
+
64
+ const res = await openmrsFetch(url, {
65
+ method: 'GET',
66
+ headers: {
67
+ Authorization: `Basic ${credentials}`,
68
+ },
69
+ });
70
+
71
+ const data = await res.json();
72
+
73
+ if (!res.ok) {
74
+ throw new Error(data.message);
75
+ }
76
+
77
+ return data.data;
78
+ } catch (error) {
79
+ throw new Error(error.message);
80
+ }
26
81
  }
27
82
 
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";
83
+ export async function verifyOtp(username: string, password: string, otp: string) {
84
+ const etlBaseUrl = await getEtlBaseUrl();
85
+ const url = etlBaseUrl + '/verify-otp';
35
86
  const credentials = window.btoa(`${username}:${password}`);
36
87
 
37
88
  const body = { username, otp };
38
89
 
39
- const res = await fetch(url, {
40
- method: "POST",
90
+ const res = await openmrsFetch(url, {
91
+ method: 'POST',
41
92
  headers: {
42
- "Content-Type": "Application/json",
93
+ 'Content-Type': 'Application/json',
43
94
  Authorization: `Basic ${credentials}`,
44
95
  },
45
96
  body: JSON.stringify(body),
@@ -5,6 +5,8 @@ import LocationPickerView from "./location-picker/location-picker-view.component
5
5
  import Login from "./login/login.component";
6
6
  import RedirectLogout from "./redirect-logout/redirect-logout.component";
7
7
  import OtpComponent from "./otp/otp.component";
8
+ import ForgotPassword from "./forgot-password/forgot-password.component";
9
+ import ResetPassword from "./forgot-password/reset-password/reset-password.component";
8
10
 
9
11
  const Root: React.FC = () => {
10
12
  return (
@@ -16,6 +18,8 @@ const Root: React.FC = () => {
16
18
  <Route path="logout" element={<RedirectLogout />} />
17
19
  <Route path="change-password" element={<ChangePassword />} />
18
20
  <Route path="login/otp" element={<OtpComponent />} />
21
+ <Route path="login/forgot-password" element={<ForgotPassword />} />
22
+ <Route path="login/reset-password/:activationKey" element={<ResetPassword />} />
19
23
  </Routes>
20
24
  </BrowserRouter>
21
25
  );
@@ -1,7 +1,22 @@
1
- import { getConfig } from "@openmrs/esm-framework";
2
- import { moduleName } from "../..";
1
+ import { getConfig } from '@openmrs/esm-framework';
2
+ import { moduleName } from '../';
3
3
 
4
4
  export async function getEtlBaseUrl() {
5
5
  const { etlBaseUrl } = await getConfig(moduleName);
6
6
  return etlBaseUrl ?? null;
7
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
+ }
18
+
19
+ export async function getOtpKey() {
20
+ const { otpKey } = await getConfig(moduleName);
21
+ return otpKey ?? null;
22
+ }
package/yarnrc.yml CHANGED
@@ -4,11 +4,11 @@ enableGlobalCache: false
4
4
 
5
5
  nodeLinker: node-modules
6
6
 
7
- npmPublishRegistry: "https://registry.npmjs.org"
7
+ npmPublishRegistry: 'https://registry.npmjs.org'
8
8
 
9
9
  plugins:
10
10
  - checksum: c13ed363e15a826d9f779e7e7aca9dbee1d6d54813261d6f495da2fa94b01fa7579e516587ae2df5834f5d63d5d90cb392135190e8878b81dbd830c3c9f57809
11
11
  path: .yarn/plugins/@yarnpkg/plugin-outdated.cjs
12
- spec: "https://go.mskelton.dev/yarn-outdated/v4"
12
+ spec: 'https://go.mskelton.dev/yarn-outdated/v4'
13
13
 
14
14
  yarnPath: .yarn/releases/yarn-4.10.3.cjs