@openmrs/esm-login-app 10.0.1-pre.5607 → 10.0.1-pre.5619
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/.turbo/turbo-build.log +1 -1
- package/dist/3077.js +10 -10
- package/dist/3077.js.map +1 -1
- package/dist/9834.js +1 -1
- package/dist/9834.js.map +1 -1
- package/dist/openmrs-esm-login-app.js +2 -2
- package/dist/openmrs-esm-login-app.js.buildmanifest.json +7 -7
- package/dist/routes.json +1 -1
- package/package.json +2 -2
- package/src/login.resource.ts +1 -24
- package/src/redirect-logout/logout.resource.ts +2 -11
- package/src/redirect-logout/redirect-logout.test.tsx +7 -23
- package/src/two-factor-auth/verification-challenge/totp-verification-challenge-page.component.tsx +4 -15
- package/src/two-factor-auth/verification-challenge/totp-verification-challenge-page.test.tsx +30 -42
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openmrs/esm-login-app",
|
|
3
|
-
"version": "10.0.1-pre.
|
|
3
|
+
"version": "10.0.1-pre.5619",
|
|
4
4
|
"license": "MPL-2.0",
|
|
5
5
|
"description": "The login microfrontend for the OpenMRS SPA",
|
|
6
6
|
"browser": "dist/openmrs-esm-login-app.js",
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
"@vitest/coverage-v8": "^4.1.2",
|
|
60
60
|
"cross-env": "^10.1.0",
|
|
61
61
|
"happy-dom": "^20.6.0",
|
|
62
|
-
"openmrs": "10.0.1-pre.
|
|
62
|
+
"openmrs": "10.0.1-pre.5619",
|
|
63
63
|
"react": "^18.3.1",
|
|
64
64
|
"react-dom": "^18.3.1",
|
|
65
65
|
"react-i18next": "^16.0.0",
|
package/src/login.resource.ts
CHANGED
|
@@ -2,15 +2,7 @@ import { useEffect, useMemo } from 'react';
|
|
|
2
2
|
import { useTranslation } from 'react-i18next';
|
|
3
3
|
import useSwrInfinite, { type SWRInfiniteResponse } from 'swr/infinite';
|
|
4
4
|
import useSwrImmutable from 'swr/immutable';
|
|
5
|
-
import {
|
|
6
|
-
fhirBaseUrl,
|
|
7
|
-
openmrsFetch,
|
|
8
|
-
refetchCurrentUser,
|
|
9
|
-
restBaseUrl,
|
|
10
|
-
type FetchResponse,
|
|
11
|
-
type Session,
|
|
12
|
-
useDebounce,
|
|
13
|
-
} from '@openmrs/esm-framework';
|
|
5
|
+
import { fhirBaseUrl, openmrsFetch, type FetchResponse, useDebounce } from '@openmrs/esm-framework';
|
|
14
6
|
import type { LocationEntry, LocationResponse } from './types';
|
|
15
7
|
|
|
16
8
|
// "swr/infinite" doesn't export InfiniteKeyedMutator directly
|
|
@@ -106,21 +98,6 @@ export function useLoginLocations(
|
|
|
106
98
|
return memoizedLocations;
|
|
107
99
|
}
|
|
108
100
|
|
|
109
|
-
export async function performLogin(username: string, password: string): Promise<{ data: Session }> {
|
|
110
|
-
const abortController = new AbortController();
|
|
111
|
-
const token = window.btoa(`${username}:${password}`);
|
|
112
|
-
const url = `${restBaseUrl}/session`;
|
|
113
|
-
|
|
114
|
-
return openmrsFetch(url, {
|
|
115
|
-
headers: {
|
|
116
|
-
Authorization: `Basic ${token}`,
|
|
117
|
-
},
|
|
118
|
-
signal: abortController.signal,
|
|
119
|
-
}).then((res) => {
|
|
120
|
-
refetchCurrentUser();
|
|
121
|
-
return res;
|
|
122
|
-
});
|
|
123
|
-
}
|
|
124
101
|
export function useValidateLocationUuid(userPreferredLocationUuid: string) {
|
|
125
102
|
const url = userPreferredLocationUuid ? `${fhirBaseUrl}/Location?_id=${userPreferredLocationUuid}` : null;
|
|
126
103
|
const { data, error, isLoading } = useSwrImmutable<FetchResponse<LocationResponse>>(url, openmrsFetch, {
|
|
@@ -1,19 +1,10 @@
|
|
|
1
1
|
import { mutate } from 'swr';
|
|
2
|
-
import {
|
|
2
|
+
import { logout } from '@openmrs/esm-framework/src/internal';
|
|
3
3
|
|
|
4
4
|
export async function performLogout() {
|
|
5
|
-
await
|
|
6
|
-
method: 'DELETE',
|
|
7
|
-
});
|
|
5
|
+
await logout();
|
|
8
6
|
|
|
9
7
|
// clear the SWR cache on logout, do not revalidate
|
|
10
8
|
// taken from the SWR docs
|
|
11
9
|
mutate(() => true, undefined, { revalidate: false });
|
|
12
|
-
|
|
13
|
-
clearCurrentUser();
|
|
14
|
-
try {
|
|
15
|
-
await refetchCurrentUser();
|
|
16
|
-
} catch (_) {
|
|
17
|
-
// do nothing, silence the user-visible error
|
|
18
|
-
}
|
|
19
10
|
}
|
|
@@ -2,33 +2,23 @@ import React from 'react';
|
|
|
2
2
|
import { mutate } from 'swr';
|
|
3
3
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
4
4
|
import { render, waitFor } from '@testing-library/react';
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
type Session,
|
|
8
|
-
clearCurrentUser,
|
|
9
|
-
navigate,
|
|
10
|
-
openmrsFetch,
|
|
11
|
-
restBaseUrl,
|
|
12
|
-
setUserLanguage,
|
|
13
|
-
useConfig,
|
|
14
|
-
useSession,
|
|
15
|
-
} from '@openmrs/esm-framework';
|
|
5
|
+
import { type Session, navigate, setUserLanguage, useConfig, useSession } from '@openmrs/esm-framework';
|
|
6
|
+
import { logout } from '@openmrs/esm-framework/src/internal';
|
|
16
7
|
import RedirectLogout from './redirect-logout.component';
|
|
17
8
|
|
|
18
9
|
vi.mock('swr', () => ({
|
|
19
10
|
mutate: vi.fn(),
|
|
20
11
|
}));
|
|
21
12
|
|
|
22
|
-
const
|
|
13
|
+
const mockLogout = vi.mocked(logout);
|
|
23
14
|
const mockNavigate = vi.mocked(navigate);
|
|
24
|
-
const mockOpenmrsFetch = vi.mocked(openmrsFetch);
|
|
25
15
|
const mockSetUserLanguage = vi.mocked(setUserLanguage);
|
|
26
16
|
const mockUseConfig = vi.mocked(useConfig);
|
|
27
17
|
const mockUseSession = vi.mocked(useSession);
|
|
28
18
|
|
|
29
19
|
describe('RedirectLogout', () => {
|
|
30
20
|
beforeEach(() => {
|
|
31
|
-
|
|
21
|
+
mockLogout.mockResolvedValue(undefined);
|
|
32
22
|
|
|
33
23
|
mockUseSession.mockReturnValue({
|
|
34
24
|
authenticated: true,
|
|
@@ -47,13 +37,10 @@ describe('RedirectLogout', () => {
|
|
|
47
37
|
it('should redirect to login page upon logout', async () => {
|
|
48
38
|
render(<RedirectLogout />);
|
|
49
39
|
|
|
50
|
-
expect(
|
|
51
|
-
method: 'DELETE',
|
|
52
|
-
});
|
|
40
|
+
expect(mockLogout).toHaveBeenCalled();
|
|
53
41
|
|
|
54
42
|
await waitFor(() => expect(mutate).toHaveBeenCalled());
|
|
55
43
|
|
|
56
|
-
expect(mockClearCurrentUser).toHaveBeenCalled();
|
|
57
44
|
expect(mockSetUserLanguage).toHaveBeenCalledWith({
|
|
58
45
|
locale: 'km',
|
|
59
46
|
authenticated: false,
|
|
@@ -71,13 +58,10 @@ describe('RedirectLogout', () => {
|
|
|
71
58
|
|
|
72
59
|
render(<RedirectLogout />);
|
|
73
60
|
|
|
74
|
-
expect(
|
|
75
|
-
method: 'DELETE',
|
|
76
|
-
});
|
|
61
|
+
expect(mockLogout).toHaveBeenCalled();
|
|
77
62
|
|
|
78
63
|
await waitFor(() => expect(mutate).toHaveBeenCalled());
|
|
79
64
|
|
|
80
|
-
expect(mockClearCurrentUser).toHaveBeenCalled();
|
|
81
65
|
expect(mockSetUserLanguage).toHaveBeenCalledWith({
|
|
82
66
|
locale: 'km',
|
|
83
67
|
authenticated: false,
|
|
@@ -98,7 +82,7 @@ describe('RedirectLogout', () => {
|
|
|
98
82
|
|
|
99
83
|
it('should handle logout failure gracefully', async () => {
|
|
100
84
|
const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
101
|
-
|
|
85
|
+
mockLogout.mockRejectedValue(new Error('Logout failed'));
|
|
102
86
|
|
|
103
87
|
render(<RedirectLogout />);
|
|
104
88
|
|
package/src/two-factor-auth/verification-challenge/totp-verification-challenge-page.component.tsx
CHANGED
|
@@ -4,13 +4,12 @@ import { useTranslation } from 'react-i18next';
|
|
|
4
4
|
import { Button, Checkbox } from '@carbon/react';
|
|
5
5
|
import {
|
|
6
6
|
useConfig,
|
|
7
|
-
openmrsFetch,
|
|
8
|
-
refetchCurrentUser,
|
|
9
7
|
navigate as openmrsNavigate,
|
|
10
8
|
ArrowRightIcon,
|
|
11
9
|
OpenmrsFetchError,
|
|
12
10
|
ArrowLeftIcon,
|
|
13
11
|
} from '@openmrs/esm-framework';
|
|
12
|
+
import { verifyTotpCode } from '@openmrs/esm-framework/src/internal';
|
|
14
13
|
import type { ConfigSchema } from '../../config-schema';
|
|
15
14
|
import VerificationCodeInput from './verification-code-input.component';
|
|
16
15
|
import LoginPageWrapper from '../../login-page-wrapper/login-page-wrapper.component';
|
|
@@ -33,20 +32,10 @@ const TotpVerificationChallengePage: React.FC = () => {
|
|
|
33
32
|
setIsVerifying(true);
|
|
34
33
|
setVerificationError('');
|
|
35
34
|
|
|
36
|
-
const
|
|
35
|
+
const session = await verifyTotpCode(code, rememberDevice);
|
|
37
36
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
headers: {
|
|
41
|
-
'X-Totp-Code': code,
|
|
42
|
-
},
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
if (response.data && response.data.authenticated) {
|
|
46
|
-
const sessionStore = await refetchCurrentUser();
|
|
47
|
-
const session = sessionStore?.session;
|
|
48
|
-
|
|
49
|
-
if (session?.sessionLocation) {
|
|
37
|
+
if (session?.authenticated) {
|
|
38
|
+
if (session.sessionLocation) {
|
|
50
39
|
let to = loginLinks?.loginSuccess || '/home';
|
|
51
40
|
const referrer = location?.state?.referrer || sessionStorage.getItem('loginReferrer');
|
|
52
41
|
|
package/src/two-factor-auth/verification-challenge/totp-verification-challenge-page.test.tsx
CHANGED
|
@@ -3,14 +3,8 @@ import { beforeEach, describe, vi, it, expect } from 'vitest';
|
|
|
3
3
|
import userEvent from '@testing-library/user-event';
|
|
4
4
|
import { render, screen } from '@testing-library/react';
|
|
5
5
|
import { MemoryRouter } from 'react-router-dom';
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
openmrsFetch,
|
|
9
|
-
type FetchResponse,
|
|
10
|
-
OpenmrsFetchError,
|
|
11
|
-
refetchCurrentUser,
|
|
12
|
-
navigate as openmrsNavigate,
|
|
13
|
-
} from '@openmrs/esm-framework';
|
|
6
|
+
import { useConfig, type Session, navigate as openmrsNavigate } from '@openmrs/esm-framework';
|
|
7
|
+
import { verifyTotpCode } from '@openmrs/esm-framework/src/internal';
|
|
14
8
|
import type { ConfigSchema } from '../../config-schema';
|
|
15
9
|
import TotpVerificationChallengePage from './totp-verification-challenge-page.component';
|
|
16
10
|
|
|
@@ -20,8 +14,7 @@ vi.mock('@openmrs/esm-framework', async () => {
|
|
|
20
14
|
...actual,
|
|
21
15
|
useConfig: vi.fn(),
|
|
22
16
|
interpolateUrl: vi.fn(),
|
|
23
|
-
|
|
24
|
-
refetchCurrentUser: vi.fn(),
|
|
17
|
+
verifyTotpCode: vi.fn(),
|
|
25
18
|
navigate: vi.fn(),
|
|
26
19
|
};
|
|
27
20
|
});
|
|
@@ -67,10 +60,8 @@ describe('TotpVerificationChallengePage', () => {
|
|
|
67
60
|
expect(verifyButton).toBeDisabled();
|
|
68
61
|
});
|
|
69
62
|
|
|
70
|
-
it('should
|
|
71
|
-
vi.mocked(
|
|
72
|
-
data: { authenticated: true },
|
|
73
|
-
} as unknown as FetchResponse<unknown>);
|
|
63
|
+
it('should ask the server to remember the device only when the checkbox is checked', async () => {
|
|
64
|
+
vi.mocked(verifyTotpCode).mockResolvedValue({ authenticated: true } as Session);
|
|
74
65
|
|
|
75
66
|
const { user } = setup();
|
|
76
67
|
const inputs = screen.getAllByRole('textbox');
|
|
@@ -81,27 +72,21 @@ describe('TotpVerificationChallengePage', () => {
|
|
|
81
72
|
const verifyButton = screen.getByRole('button', { name: /verify/i });
|
|
82
73
|
const rememberMeCheckbox = screen.getByLabelText(/Remember this device/i);
|
|
83
74
|
await user.click(verifyButton);
|
|
84
|
-
expect(
|
|
85
|
-
'/ws/rest/v1/session?rememberMe=true',
|
|
86
|
-
expect.objectContaining({ method: 'GET' }),
|
|
87
|
-
);
|
|
75
|
+
expect(verifyTotpCode).toHaveBeenLastCalledWith('123456', true);
|
|
88
76
|
|
|
89
77
|
await user.click(rememberMeCheckbox);
|
|
90
78
|
expect(rememberMeCheckbox).not.toBeChecked();
|
|
91
79
|
|
|
92
80
|
await user.click(verifyButton);
|
|
93
|
-
expect(
|
|
81
|
+
expect(verifyTotpCode).toHaveBeenLastCalledWith('123456', false);
|
|
94
82
|
});
|
|
95
83
|
|
|
96
84
|
it('should use the original referrer from sessionStorage and clear it after successful verification', async () => {
|
|
97
85
|
sessionStorage.setItem('loginReferrer', '/patient-chart');
|
|
98
|
-
vi.mocked(
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
vi.mocked(refetchCurrentUser).mockResolvedValue({
|
|
103
|
-
session: { sessionLocation: { uuid: 'location-123', display: 'Pharmacy' } },
|
|
104
|
-
} as any);
|
|
86
|
+
vi.mocked(verifyTotpCode).mockResolvedValue({
|
|
87
|
+
authenticated: true,
|
|
88
|
+
sessionLocation: { uuid: 'location-123', display: 'Pharmacy' },
|
|
89
|
+
} as Session);
|
|
105
90
|
|
|
106
91
|
const { user } = setup();
|
|
107
92
|
|
|
@@ -116,13 +101,10 @@ describe('TotpVerificationChallengePage', () => {
|
|
|
116
101
|
});
|
|
117
102
|
|
|
118
103
|
it('should verify successfully and navigate to home if the user has a session location', async () => {
|
|
119
|
-
vi.mocked(
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
vi.mocked(refetchCurrentUser).mockResolvedValue({
|
|
124
|
-
session: { sessionLocation: { uuid: 'location-123', display: 'Pharmacy' } },
|
|
125
|
-
} as any);
|
|
104
|
+
vi.mocked(verifyTotpCode).mockResolvedValue({
|
|
105
|
+
authenticated: true,
|
|
106
|
+
sessionLocation: { uuid: 'location-123', display: 'Pharmacy' },
|
|
107
|
+
} as Session);
|
|
126
108
|
|
|
127
109
|
const { user } = setup();
|
|
128
110
|
|
|
@@ -132,15 +114,21 @@ describe('TotpVerificationChallengePage', () => {
|
|
|
132
114
|
|
|
133
115
|
const verifyButton = screen.getByRole('button', { name: /verify/i });
|
|
134
116
|
await user.click(verifyButton);
|
|
135
|
-
expect(
|
|
136
|
-
expect.stringContaining('/ws/rest/v1/session'),
|
|
137
|
-
expect.objectContaining({
|
|
138
|
-
method: 'GET',
|
|
139
|
-
headers: { 'X-Totp-Code': '123456' },
|
|
140
|
-
}),
|
|
141
|
-
);
|
|
142
|
-
|
|
143
|
-
expect(refetchCurrentUser).toHaveBeenCalled();
|
|
117
|
+
expect(verifyTotpCode).toHaveBeenCalledWith('123456', true);
|
|
144
118
|
expect(openmrsNavigate).toHaveBeenCalledWith({ to: '/home' });
|
|
145
119
|
});
|
|
120
|
+
|
|
121
|
+
it('should show an error and stay on the page when the code is not accepted', async () => {
|
|
122
|
+
vi.mocked(verifyTotpCode).mockResolvedValue({ authenticated: false } as Session);
|
|
123
|
+
|
|
124
|
+
const { user } = setup();
|
|
125
|
+
|
|
126
|
+
const inputs = screen.getAllByRole('textbox');
|
|
127
|
+
await user.click(inputs[0]);
|
|
128
|
+
await user.paste('123456');
|
|
129
|
+
await user.click(screen.getByRole('button', { name: /verify/i }));
|
|
130
|
+
|
|
131
|
+
expect(screen.getByText(/We could not verify that code/i)).toBeInTheDocument();
|
|
132
|
+
expect(openmrsNavigate).not.toHaveBeenCalled();
|
|
133
|
+
});
|
|
146
134
|
});
|