@squiz/resource-browser 3.1.1 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/lib/Hooks/useAuth.js +38 -9
- package/lib/Plugin/Plugin.d.ts +2 -1
- package/lib/index.css +20 -1
- package/lib/index.js +2 -2
- package/lib/utils/authUtils.js +7 -2
- package/package.json +1 -1
- package/src/Hooks/useAuth.spec.tsx +146 -2
- package/src/Hooks/useAuth.ts +41 -9
- package/src/Plugin/Plugin.spec.tsx +2 -0
- package/src/Plugin/Plugin.tsx +2 -1
- package/src/index.spec.tsx +144 -1
- package/src/index.tsx +3 -1
- package/src/utils/authUtils.spec.ts +17 -0
- package/src/utils/authUtils.ts +6 -2
package/CHANGELOG.md
CHANGED
@@ -1,5 +1,18 @@
|
|
1
1
|
# Change Log
|
2
2
|
|
3
|
+
## 3.2.0
|
4
|
+
|
5
|
+
### Minor Changes
|
6
|
+
|
7
|
+
- ce4521d: Outdated token fix
|
8
|
+
|
9
|
+
## 3.1.2
|
10
|
+
|
11
|
+
### Patch Changes
|
12
|
+
|
13
|
+
- c1ebb09: format scope as expected for oAuth calls (space delimited)
|
14
|
+
- 213ea12: change plugin render comparison to type not object ref
|
15
|
+
|
3
16
|
## 3.1.1
|
4
17
|
|
5
18
|
### Patch Changes
|
package/lib/Hooks/useAuth.js
CHANGED
@@ -6,20 +6,28 @@ const react_1 = require("react");
|
|
6
6
|
const authUtils_1 = require("../utils/authUtils");
|
7
7
|
const useAuth = (authConfig) => {
|
8
8
|
const [authToken, setAuthToken] = (0, react_1.useState)((0, authUtils_1.getCookieValue)('authToken'));
|
9
|
-
const [isAuthenticated, setIsAuthenticated] = (0, react_1.useState)(!!
|
9
|
+
const [isAuthenticated, setIsAuthenticated] = (0, react_1.useState)(!!authToken);
|
10
10
|
const refreshAccessToken = (0, react_1.useCallback)(async () => {
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
11
|
+
try {
|
12
|
+
const newToken = await (0, authUtils_1.refreshAccessToken)(authConfig);
|
13
|
+
setAuthToken(newToken);
|
14
|
+
setIsAuthenticated(!!newToken);
|
15
|
+
return newToken;
|
16
|
+
}
|
17
|
+
catch {
|
18
|
+
setAuthToken(null);
|
19
|
+
setIsAuthenticated(false);
|
20
|
+
throw new Error('Session expired. Please log in again.');
|
21
|
+
}
|
15
22
|
}, [authConfig]);
|
16
23
|
const handleLogin = (0, react_1.useCallback)(() => {
|
17
|
-
if (!authConfig?.redirectUrl
|
24
|
+
if (!authConfig?.redirectUrl || !authConfig?.authUrl) {
|
18
25
|
console.error('Auth config is misconfigured');
|
19
26
|
return;
|
20
27
|
}
|
21
28
|
const encodedRedirectUrl = encodeURIComponent(authConfig.redirectUrl);
|
22
|
-
const
|
29
|
+
const scope = authConfig?.scope?.split(';').join(' '); // Saved in scope1;scope2;scope3 format, sent in scope1 scope2 scope3 format
|
30
|
+
const loginUrl = `${authConfig?.authUrl}?client_id=${authConfig?.clientId}&scope=${scope}&redirect_uri=${encodedRedirectUrl}&response_type=code&state=state`;
|
23
31
|
const popup = window.open(loginUrl, 'Login', 'width=600,height=600');
|
24
32
|
if (!popup) {
|
25
33
|
console.error('Popup failed to open');
|
@@ -27,10 +35,12 @@ const useAuth = (authConfig) => {
|
|
27
35
|
}
|
28
36
|
const checkPopup = setInterval(() => {
|
29
37
|
try {
|
30
|
-
|
38
|
+
const token = (0, authUtils_1.getCookieValue)('authToken');
|
39
|
+
const refreshToken = (0, authUtils_1.getCookieValue)('refreshToken');
|
40
|
+
if (token && refreshToken) {
|
31
41
|
clearInterval(checkPopup);
|
32
42
|
popup.close();
|
33
|
-
setAuthToken(
|
43
|
+
setAuthToken(token);
|
34
44
|
setIsAuthenticated(true);
|
35
45
|
}
|
36
46
|
}
|
@@ -43,6 +53,25 @@ const useAuth = (authConfig) => {
|
|
43
53
|
}
|
44
54
|
}, 1000); // Check every second
|
45
55
|
}, [authConfig]);
|
56
|
+
const syncAuthState = () => {
|
57
|
+
const token = (0, authUtils_1.getCookieValue)('authToken');
|
58
|
+
setAuthToken(token);
|
59
|
+
setIsAuthenticated(!!token);
|
60
|
+
};
|
61
|
+
(0, react_1.useEffect)(() => {
|
62
|
+
const handleTabChange = () => {
|
63
|
+
if (!document.hidden) {
|
64
|
+
syncAuthState();
|
65
|
+
}
|
66
|
+
};
|
67
|
+
syncAuthState();
|
68
|
+
document.addEventListener('visibilitychange', handleTabChange);
|
69
|
+
window.addEventListener('focus', syncAuthState);
|
70
|
+
return () => {
|
71
|
+
document.removeEventListener('visibilitychange', handleTabChange);
|
72
|
+
window.removeEventListener('focus', syncAuthState);
|
73
|
+
};
|
74
|
+
}, []);
|
46
75
|
(0, react_1.useEffect)(() => {
|
47
76
|
refreshAccessToken().catch(() => {
|
48
77
|
setIsAuthenticated(false);
|
package/lib/Plugin/Plugin.d.ts
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
import React from 'react';
|
2
2
|
import { ResourceBrowserInputProps } from '../ResourceBrowserInput/ResourceBrowserInput';
|
3
|
-
import { InlineType } from '../types';
|
3
|
+
import { ResourceBrowserPluginType, InlineType } from '../types';
|
4
4
|
/**
|
5
5
|
* This plugin component exsits to deal with React rules of Hooks stupidity.
|
6
6
|
*
|
@@ -9,6 +9,7 @@ import { InlineType } from '../types';
|
|
9
9
|
* needs to render its UI etc.
|
10
10
|
*/
|
11
11
|
export type PluginRenderType = ResourceBrowserInputProps & {
|
12
|
+
type: ResourceBrowserPluginType | null;
|
12
13
|
render: boolean;
|
13
14
|
inline: boolean;
|
14
15
|
inlineType?: InlineType;
|
package/lib/index.css
CHANGED
@@ -4805,6 +4805,9 @@
|
|
4805
4805
|
white-space: nowrap;
|
4806
4806
|
border-width: 0;
|
4807
4807
|
}
|
4808
|
+
.squiz-rb-scope .visible:not(.squiz-rb-plugin *) {
|
4809
|
+
visibility: visible;
|
4810
|
+
}
|
4808
4811
|
.squiz-rb-scope .fixed:not(.squiz-rb-plugin *) {
|
4809
4812
|
position: fixed;
|
4810
4813
|
}
|
@@ -5663,6 +5666,9 @@
|
|
5663
5666
|
border: none;
|
5664
5667
|
padding: 0;
|
5665
5668
|
}
|
5669
|
+
.squiz-rb-scope .image-card:disabled:not(.squiz-rb-plugin *) {
|
5670
|
+
cursor: not-allowed;
|
5671
|
+
}
|
5666
5672
|
.squiz-rb-scope .image-card--selected:not(.squiz-rb-plugin *) {
|
5667
5673
|
background-color: #e6f1fa;
|
5668
5674
|
}
|
@@ -5727,6 +5733,9 @@
|
|
5727
5733
|
margin: 0.25rem 0.5rem;
|
5728
5734
|
border-bottom: 1px solid #e0e0e0;
|
5729
5735
|
}
|
5736
|
+
.squiz-rb-scope .selection-list__item:first-child:not(.squiz-rb-plugin *) {
|
5737
|
+
margin-top: 8px;
|
5738
|
+
}
|
5730
5739
|
.squiz-rb-scope .selection-list__item:last-child:not(.squiz-rb-plugin *) {
|
5731
5740
|
border-bottom: none;
|
5732
5741
|
}
|
@@ -6164,6 +6173,7 @@
|
|
6164
6173
|
margin: auto;
|
6165
6174
|
}
|
6166
6175
|
.squiz-rb-scope .image-info__title:not(.squiz-rb-plugin *) {
|
6176
|
+
padding: 0 2.2rem;
|
6167
6177
|
margin: 1rem 0;
|
6168
6178
|
color: #3d3d3d;
|
6169
6179
|
}
|
@@ -6172,6 +6182,7 @@
|
|
6172
6182
|
color: #707070;
|
6173
6183
|
}
|
6174
6184
|
.squiz-rb-scope .image-info__details-list:not(.squiz-rb-plugin *) {
|
6185
|
+
width: 270px;
|
6175
6186
|
display: flex;
|
6176
6187
|
flex-direction: column;
|
6177
6188
|
margin: 1rem 0;
|
@@ -6186,10 +6197,11 @@
|
|
6186
6197
|
display: flex;
|
6187
6198
|
}
|
6188
6199
|
.squiz-rb-scope .image-info__details-value:not(.squiz-rb-plugin *) {
|
6200
|
+
margin-left: 20px;
|
6189
6201
|
color: #3d3d3d;
|
6190
6202
|
}
|
6191
6203
|
.squiz-rb-scope .image-variant__list:not(.squiz-rb-plugin *) {
|
6192
|
-
width:
|
6204
|
+
width: 280px;
|
6193
6205
|
margin-bottom: 1rem;
|
6194
6206
|
}
|
6195
6207
|
.squiz-rb-scope .image-variant__item:not(.squiz-rb-plugin *) {
|
@@ -6199,6 +6211,9 @@
|
|
6199
6211
|
width: 100%;
|
6200
6212
|
justify-content: space-between;
|
6201
6213
|
}
|
6214
|
+
.squiz-rb-scope .selection-list__item:focus-visible:not(.squiz-rb-plugin *) {
|
6215
|
+
outline-width: 0px;
|
6216
|
+
}
|
6202
6217
|
.squiz-rb-scope .selection-list__item--selected .image-variant__checkmark:not(.squiz-rb-plugin *) {
|
6203
6218
|
visibility: visible;
|
6204
6219
|
}
|
@@ -6253,6 +6268,10 @@
|
|
6253
6268
|
overflow-y: auto;
|
6254
6269
|
}
|
6255
6270
|
}
|
6271
|
+
.squiz-rb-scope .divider-container:not(.squiz-rb-plugin *) {
|
6272
|
+
padding: 0 1.25rem;
|
6273
|
+
width: 100%;
|
6274
|
+
}
|
6256
6275
|
.squiz-rb-scope .resource-picker:not(.squiz-rb-plugin *) {
|
6257
6276
|
display: grid;
|
6258
6277
|
grid-template-columns: 24px 1fr;
|
package/lib/index.js
CHANGED
@@ -119,7 +119,7 @@ const ResourceBrowser = (props) => {
|
|
119
119
|
}, [reloadSources]);
|
120
120
|
// Render a default "plugin" and one for each item in the plugins array. They are conditionally rendered based on what is selected
|
121
121
|
return (react_1.default.createElement("div", { className: "squiz-rb-scope" },
|
122
|
-
react_1.default.createElement(Plugin_1.PluginRender, { key: "default", render: plugin === null, inline: !!inline, inlineType: inlineType, ...props, source: source, sources: sources, setSource: handleSourceSelect, isLoading: isLoading, isOtherSourceValue: false, error: sourcesError || error, plugin: plugin, pluginMode: mode, searchEnabled: searchEnabled, useResource: () => {
|
122
|
+
react_1.default.createElement(Plugin_1.PluginRender, { key: "default", type: null, render: plugin === null, inline: !!inline, inlineType: inlineType, ...props, source: source, sources: sources, setSource: handleSourceSelect, isLoading: isLoading, isOtherSourceValue: false, error: sourcesError || error, plugin: plugin, pluginMode: mode, searchEnabled: searchEnabled, useResource: () => {
|
123
123
|
return {
|
124
124
|
data: null,
|
125
125
|
error: null,
|
@@ -127,7 +127,7 @@ const ResourceBrowser = (props) => {
|
|
127
127
|
};
|
128
128
|
}, isModalOpen: isModalOpen, onModalStateChange: handleModalStateChange, onRetry: handleReset }),
|
129
129
|
plugins.map((thisPlugin) => {
|
130
|
-
return (react_1.default.createElement(Plugin_1.PluginRender, { key: thisPlugin.type, render: thisPlugin === plugin, inline: !!inline, inlineType: inlineType, ...props, value: value && source ? (value.sourceId === source.id ? value : null) : null, isOtherSourceValue: value && source ? (value.sourceId !== source.id ? true : false) : false, source: source, sources: sources, setSource: handleSourceSelect, isLoading: isLoading, error: sourcesError || error, plugin: plugin, pluginMode: mode, searchEnabled: searchEnabled, useResource: thisPlugin.useResolveResource, isModalOpen: isModalOpen, onModalStateChange: handleModalStateChange, onRetry: handleReset }));
|
130
|
+
return (react_1.default.createElement(Plugin_1.PluginRender, { key: thisPlugin.type, type: thisPlugin.type, render: thisPlugin.type === plugin?.type, inline: !!inline, inlineType: inlineType, ...props, value: value && source ? (value.sourceId === source.id ? value : null) : null, isOtherSourceValue: value && source ? (value.sourceId !== source.id ? true : false) : false, source: source, sources: sources, setSource: handleSourceSelect, isLoading: isLoading, error: sourcesError || error, plugin: plugin, pluginMode: mode, searchEnabled: searchEnabled, useResource: thisPlugin.useResolveResource, isModalOpen: isModalOpen, onModalStateChange: handleModalStateChange, onRetry: handleReset }));
|
131
131
|
})));
|
132
132
|
};
|
133
133
|
exports.ResourceBrowser = ResourceBrowser;
|
package/lib/utils/authUtils.js
CHANGED
@@ -2,8 +2,13 @@
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
3
3
|
exports.refreshAccessToken = exports.logout = exports.setCookieValue = exports.getCookieValue = void 0;
|
4
4
|
const getCookieValue = (name) => {
|
5
|
-
|
6
|
-
|
5
|
+
try {
|
6
|
+
const match = document.cookie.match('(^|;)\\s*' + name + '\\s*=\\s*([^;]+)');
|
7
|
+
return match ? match.pop() : null;
|
8
|
+
}
|
9
|
+
catch (error) {
|
10
|
+
return null;
|
11
|
+
}
|
7
12
|
};
|
8
13
|
exports.getCookieValue = getCookieValue;
|
9
14
|
const setCookieValue = (name, value) => {
|
package/package.json
CHANGED
@@ -14,7 +14,7 @@ describe('useAuth', () => {
|
|
14
14
|
authUrl: 'https://auth.example.com',
|
15
15
|
clientId: 'example-client-id',
|
16
16
|
redirectUrl: 'https://example.com/callback',
|
17
|
-
scope: '
|
17
|
+
scope: 'offline',
|
18
18
|
};
|
19
19
|
|
20
20
|
beforeEach(() => {
|
@@ -107,7 +107,7 @@ describe('useAuth', () => {
|
|
107
107
|
result.current.login();
|
108
108
|
|
109
109
|
expect(window.open).toHaveBeenCalledWith(
|
110
|
-
`${authConfig.authUrl}?client_id=${authConfig.clientId}&scope=
|
110
|
+
`${authConfig.authUrl}?client_id=${authConfig.clientId}&scope=offline&redirect_uri=${encodeURIComponent(authConfig.redirectUrl)}&response_type=code&state=state`,
|
111
111
|
'Login',
|
112
112
|
'width=600,height=600',
|
113
113
|
);
|
@@ -126,6 +126,50 @@ describe('useAuth', () => {
|
|
126
126
|
expect(popupMock.close).toHaveBeenCalled();
|
127
127
|
});
|
128
128
|
|
129
|
+
it('splits the scope from dxp-console and sends in expected format (space delimited)', async () => {
|
130
|
+
jest.useFakeTimers();
|
131
|
+
|
132
|
+
const popupMock = {
|
133
|
+
closed: false,
|
134
|
+
close: jest.fn(),
|
135
|
+
} as unknown as Window;
|
136
|
+
|
137
|
+
jest.spyOn(window, 'open').mockImplementation(() => popupMock);
|
138
|
+
mockGetCookieValue.mockReturnValueOnce(null).mockReturnValueOnce('newAuthToken').mockReturnValueOnce('newRefreshToken');
|
139
|
+
|
140
|
+
const { result } = renderHook(() => useAuth({ ...authConfig, scope: 'offline;asset:read' }));
|
141
|
+
|
142
|
+
result.current.login();
|
143
|
+
|
144
|
+
expect(window.open).toHaveBeenCalledWith(
|
145
|
+
`${authConfig.authUrl}?client_id=${authConfig.clientId}&scope=offline asset:read&redirect_uri=${encodeURIComponent(authConfig.redirectUrl)}&response_type=code&state=state`,
|
146
|
+
'Login',
|
147
|
+
'width=600,height=600',
|
148
|
+
);
|
149
|
+
});
|
150
|
+
|
151
|
+
it('Works with an empty scope', async () => {
|
152
|
+
jest.useFakeTimers();
|
153
|
+
|
154
|
+
const popupMock = {
|
155
|
+
closed: false,
|
156
|
+
close: jest.fn(),
|
157
|
+
} as unknown as Window;
|
158
|
+
|
159
|
+
jest.spyOn(window, 'open').mockImplementation(() => popupMock);
|
160
|
+
mockGetCookieValue.mockReturnValueOnce(null).mockReturnValueOnce('newAuthToken').mockReturnValueOnce('newRefreshToken');
|
161
|
+
|
162
|
+
const { result } = renderHook(() => useAuth({ ...authConfig, scope: '' }));
|
163
|
+
|
164
|
+
result.current.login();
|
165
|
+
|
166
|
+
expect(window.open).toHaveBeenCalledWith(
|
167
|
+
`${authConfig.authUrl}?client_id=${authConfig.clientId}&scope=&redirect_uri=${encodeURIComponent(authConfig.redirectUrl)}&response_type=code&state=state`,
|
168
|
+
'Login',
|
169
|
+
'width=600,height=600',
|
170
|
+
);
|
171
|
+
});
|
172
|
+
|
129
173
|
it('should refresh access token and update state', async () => {
|
130
174
|
mockGetCookieValue.mockReturnValue('initialRefreshToken');
|
131
175
|
mockRefreshAccessToken.mockResolvedValue('newAuthToken');
|
@@ -150,4 +194,104 @@ describe('useAuth', () => {
|
|
150
194
|
expect(result.current.isAuthenticated).toBe(false);
|
151
195
|
});
|
152
196
|
});
|
197
|
+
|
198
|
+
it('should update state when the page gains focus', async () => {
|
199
|
+
const { result } = renderHook(() => useAuth(authConfig));
|
200
|
+
|
201
|
+
mockGetCookieValue.mockReturnValueOnce('focusedAuthToken');
|
202
|
+
|
203
|
+
act(() => {
|
204
|
+
window.dispatchEvent(new Event('focus'));
|
205
|
+
});
|
206
|
+
|
207
|
+
await waitFor(() => {
|
208
|
+
expect(result.current.authToken).toBe('focusedAuthToken');
|
209
|
+
expect(result.current.isAuthenticated).toBe(true);
|
210
|
+
});
|
211
|
+
});
|
212
|
+
|
213
|
+
it('should update state when the document becomes visible', async () => {
|
214
|
+
mockGetCookieValue.mockReturnValueOnce(null);
|
215
|
+
const { result } = renderHook(() => useAuth(authConfig));
|
216
|
+
|
217
|
+
mockGetCookieValue.mockReturnValue('visibleAuthToken');
|
218
|
+
|
219
|
+
Object.defineProperty(document, 'hidden', { configurable: true, value: false });
|
220
|
+
|
221
|
+
act(() => {
|
222
|
+
document.dispatchEvent(new Event('visibilitychange'));
|
223
|
+
});
|
224
|
+
|
225
|
+
await waitFor(() => {
|
226
|
+
expect(result.current.authToken).toBe('visibleAuthToken');
|
227
|
+
expect(result.current.isAuthenticated).toBe(true);
|
228
|
+
});
|
229
|
+
});
|
230
|
+
|
231
|
+
it('should clean up event listeners on unmount', async () => {
|
232
|
+
const removeEventListenerSpy = jest.spyOn(window, 'removeEventListener');
|
233
|
+
|
234
|
+
const { unmount } = renderHook(() => useAuth(authConfig));
|
235
|
+
|
236
|
+
unmount();
|
237
|
+
|
238
|
+
expect(removeEventListenerSpy).toHaveBeenCalledWith('focus', expect.any(Function));
|
239
|
+
});
|
240
|
+
|
241
|
+
it('should log error when authConfig is misconfigured', async () => {
|
242
|
+
const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
243
|
+
|
244
|
+
const invalidConfig = { clientId: 'example-client-id', scope: 'offline' };
|
245
|
+
|
246
|
+
// @ts-ignore
|
247
|
+
const { result } = renderHook(() => useAuth(invalidConfig));
|
248
|
+
|
249
|
+
result.current.login();
|
250
|
+
|
251
|
+
await waitFor(() => {
|
252
|
+
expect(consoleSpy).toHaveBeenCalledWith('Auth config is misconfigured');
|
253
|
+
});
|
254
|
+
|
255
|
+
consoleSpy.mockRestore();
|
256
|
+
});
|
257
|
+
|
258
|
+
it('should handle missing refreshToken when logging in', async () => {
|
259
|
+
jest.useFakeTimers();
|
260
|
+
|
261
|
+
const popupMock = {
|
262
|
+
closed: false,
|
263
|
+
close: jest.fn(),
|
264
|
+
} as unknown as Window;
|
265
|
+
|
266
|
+
jest.spyOn(window, 'open').mockImplementation(() => popupMock);
|
267
|
+
mockGetCookieValue.mockReturnValueOnce(null).mockReturnValueOnce('newAuthToken').mockReturnValueOnce(null);
|
268
|
+
|
269
|
+
const { result } = renderHook(() => useAuth(authConfig));
|
270
|
+
|
271
|
+
result.current.login();
|
272
|
+
|
273
|
+
act(() => {
|
274
|
+
mockGetCookieValue.mockReturnValue('newAuthToken');
|
275
|
+
jest.advanceTimersByTime(1000);
|
276
|
+
(popupMock as any).closed = true;
|
277
|
+
});
|
278
|
+
|
279
|
+
await waitFor(() => {
|
280
|
+
expect(result.current.authToken).toBe(null);
|
281
|
+
expect(result.current.isAuthenticated).toBe(false);
|
282
|
+
});
|
283
|
+
|
284
|
+
expect(popupMock.close).toHaveBeenCalled();
|
285
|
+
});
|
286
|
+
|
287
|
+
it('should throw an error when getCookieValue throws unexpectedly', () => {
|
288
|
+
jest.spyOn(authUtils, 'getCookieValue').mockImplementation(() => {
|
289
|
+
throw new Error('Unexpected error');
|
290
|
+
});
|
291
|
+
const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
292
|
+
|
293
|
+
expect(() => renderHook(() => useAuth(authConfig))).toThrow('Unexpected error');
|
294
|
+
|
295
|
+
consoleSpy.mockRestore();
|
296
|
+
});
|
153
297
|
});
|
package/src/Hooks/useAuth.ts
CHANGED
@@ -5,22 +5,29 @@ import { getCookieValue, refreshAccessToken as refreshTokenUtil } from '../utils
|
|
5
5
|
|
6
6
|
export const useAuth = (authConfig: AuthenticationConfiguration | undefined) => {
|
7
7
|
const [authToken, setAuthToken] = useState<string | null>(getCookieValue('authToken'));
|
8
|
-
const [isAuthenticated, setIsAuthenticated] = useState<boolean>(!!
|
8
|
+
const [isAuthenticated, setIsAuthenticated] = useState<boolean>(!!authToken);
|
9
9
|
|
10
10
|
const refreshAccessToken = useCallback(async (): Promise<string> => {
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
11
|
+
try {
|
12
|
+
const newToken = await refreshTokenUtil(authConfig);
|
13
|
+
setAuthToken(newToken);
|
14
|
+
setIsAuthenticated(!!newToken);
|
15
|
+
return newToken;
|
16
|
+
} catch {
|
17
|
+
setAuthToken(null);
|
18
|
+
setIsAuthenticated(false);
|
19
|
+
throw new Error('Session expired. Please log in again.');
|
20
|
+
}
|
15
21
|
}, [authConfig]);
|
16
22
|
|
17
23
|
const handleLogin = useCallback((): void => {
|
18
|
-
if (!authConfig?.redirectUrl
|
24
|
+
if (!authConfig?.redirectUrl || !authConfig?.authUrl) {
|
19
25
|
console.error('Auth config is misconfigured');
|
20
26
|
return;
|
21
27
|
}
|
22
28
|
const encodedRedirectUrl = encodeURIComponent(authConfig.redirectUrl);
|
23
|
-
const
|
29
|
+
const scope = authConfig?.scope?.split(';').join(' '); // Saved in scope1;scope2;scope3 format, sent in scope1 scope2 scope3 format
|
30
|
+
const loginUrl = `${authConfig?.authUrl}?client_id=${authConfig?.clientId}&scope=${scope}&redirect_uri=${encodedRedirectUrl}&response_type=code&state=state`;
|
24
31
|
const popup = window.open(loginUrl, 'Login', 'width=600,height=600');
|
25
32
|
|
26
33
|
if (!popup) {
|
@@ -30,10 +37,12 @@ export const useAuth = (authConfig: AuthenticationConfiguration | undefined) =>
|
|
30
37
|
|
31
38
|
const checkPopup = setInterval(() => {
|
32
39
|
try {
|
33
|
-
|
40
|
+
const token = getCookieValue('authToken');
|
41
|
+
const refreshToken = getCookieValue('refreshToken');
|
42
|
+
if (token && refreshToken) {
|
34
43
|
clearInterval(checkPopup);
|
35
44
|
popup.close();
|
36
|
-
setAuthToken(
|
45
|
+
setAuthToken(token);
|
37
46
|
setIsAuthenticated(true);
|
38
47
|
}
|
39
48
|
} catch (error) {
|
@@ -47,6 +56,29 @@ export const useAuth = (authConfig: AuthenticationConfiguration | undefined) =>
|
|
47
56
|
}, 1000); // Check every second
|
48
57
|
}, [authConfig]);
|
49
58
|
|
59
|
+
const syncAuthState = () => {
|
60
|
+
const token = getCookieValue('authToken');
|
61
|
+
setAuthToken(token);
|
62
|
+
setIsAuthenticated(!!token);
|
63
|
+
};
|
64
|
+
|
65
|
+
useEffect(() => {
|
66
|
+
const handleTabChange = () => {
|
67
|
+
if (!document.hidden) {
|
68
|
+
syncAuthState();
|
69
|
+
}
|
70
|
+
};
|
71
|
+
syncAuthState();
|
72
|
+
|
73
|
+
document.addEventListener('visibilitychange', handleTabChange);
|
74
|
+
window.addEventListener('focus', syncAuthState);
|
75
|
+
|
76
|
+
return () => {
|
77
|
+
document.removeEventListener('visibilitychange', handleTabChange);
|
78
|
+
window.removeEventListener('focus', syncAuthState);
|
79
|
+
};
|
80
|
+
}, []);
|
81
|
+
|
50
82
|
useEffect(() => {
|
51
83
|
refreshAccessToken().catch(() => {
|
52
84
|
setIsAuthenticated(false);
|
@@ -19,6 +19,7 @@ describe('Plugin', () => {
|
|
19
19
|
|
20
20
|
it('Does render ResourceBrowserInput if render is true', async () => {
|
21
21
|
const props = {
|
22
|
+
type: null,
|
22
23
|
modalTitle: 'Asset picker',
|
23
24
|
value: null,
|
24
25
|
isOtherSourceValue: false,
|
@@ -52,6 +53,7 @@ describe('Plugin', () => {
|
|
52
53
|
|
53
54
|
it('Does render ResourceBrowserInlineButton if inline is true', async () => {
|
54
55
|
const props = {
|
56
|
+
type: null,
|
55
57
|
modalTitle: 'Asset picker',
|
56
58
|
value: null,
|
57
59
|
isOtherSourceValue: false,
|
package/src/Plugin/Plugin.tsx
CHANGED
@@ -2,7 +2,7 @@ import React from 'react';
|
|
2
2
|
import { ResourceBrowserInput, ResourceBrowserInputProps } from '../ResourceBrowserInput/ResourceBrowserInput';
|
3
3
|
import { ResourceBrowserInlineButton } from '../ResourceBrowserInlineButton/ResourceBrowserInlineButton';
|
4
4
|
import { AuthProvider } from '../ResourceBrowserContext/AuthProvider';
|
5
|
-
import { InlineType } from '../types';
|
5
|
+
import { ResourceBrowserPluginType, InlineType } from '../types';
|
6
6
|
|
7
7
|
/**
|
8
8
|
* This plugin component exsits to deal with React rules of Hooks stupidity.
|
@@ -12,6 +12,7 @@ import { InlineType } from '../types';
|
|
12
12
|
* needs to render its UI etc.
|
13
13
|
*/
|
14
14
|
export type PluginRenderType = ResourceBrowserInputProps & {
|
15
|
+
type: ResourceBrowserPluginType | null;
|
15
16
|
render: boolean;
|
16
17
|
inline: boolean;
|
17
18
|
inlineType?: InlineType;
|
package/src/index.spec.tsx
CHANGED
@@ -8,6 +8,8 @@ import { ResourceBrowserPlugin, ResourceBrowserSource, ResourceBrowserSourceWith
|
|
8
8
|
import * as RBI from './ResourceBrowserInput/ResourceBrowserInput';
|
9
9
|
jest.spyOn(RBI, 'ResourceBrowserInput');
|
10
10
|
|
11
|
+
import * as Plugin from './Plugin/Plugin';
|
12
|
+
|
11
13
|
var useSourceReloadMock = jest.fn();
|
12
14
|
jest.mock('./Hooks/useSources', () => {
|
13
15
|
const actual = jest.requireActual('./Hooks/useSources');
|
@@ -45,12 +47,22 @@ describe('Resource browser input', () => {
|
|
45
47
|
renderResourceLauncher: jest.fn(),
|
46
48
|
} as unknown as ResourceBrowserPlugin;
|
47
49
|
|
50
|
+
const mockMatrixPlugin = {
|
51
|
+
type: 'matrix',
|
52
|
+
resolveResource: mockResolveResource,
|
53
|
+
renderSelectedResource: mockRenderSelectedResource,
|
54
|
+
sourceBrowserComponent: mockSourceBrowserComponent,
|
55
|
+
useResolveResource: mockUseResolveResource,
|
56
|
+
sourceSearchComponent: jest.fn(),
|
57
|
+
renderResourceLauncher: jest.fn(),
|
58
|
+
} as ResourceBrowserPlugin;
|
59
|
+
|
48
60
|
const renderComponent = (props: Partial<ResourceBrowserProps> = {}, searchEnabled?: boolean) => {
|
49
61
|
return renderWithContext(
|
50
62
|
<ResourceBrowser modalTitle="Asset picker" value={null} onChange={mockChange} onClear={mockOnClear} {...props} />,
|
51
63
|
{
|
52
64
|
onRequestSources: mockRequestSources,
|
53
|
-
plugins: [mockDamPlugin],
|
65
|
+
plugins: [mockMatrixPlugin, mockDamPlugin],
|
54
66
|
searchEnabled: !!searchEnabled,
|
55
67
|
},
|
56
68
|
);
|
@@ -524,4 +536,135 @@ describe('Resource browser input', () => {
|
|
524
536
|
expect(useSourceReloadMock).toHaveBeenCalled();
|
525
537
|
});
|
526
538
|
});
|
539
|
+
|
540
|
+
describe('Resource browser plugin', () => {
|
541
|
+
beforeEach(() => {
|
542
|
+
jest.spyOn(Plugin, 'PluginRender');
|
543
|
+
});
|
544
|
+
afterEach(() => {
|
545
|
+
(Plugin.PluginRender as jest.Mock).mockRestore();
|
546
|
+
});
|
547
|
+
|
548
|
+
it('Will default to a non plugin based render for initial load and selection of first source', async () => {
|
549
|
+
const sourcesInput = [mockSource({ type: 'dam' }), mockSource({ type: 'matrix' })];
|
550
|
+
mockRequestSources.mockResolvedValue(sourcesInput);
|
551
|
+
renderComponent();
|
552
|
+
|
553
|
+
// Will render a default with no selected source
|
554
|
+
await waitFor(() => {
|
555
|
+
expect(Plugin.PluginRender).toHaveBeenCalledWith(
|
556
|
+
expect.objectContaining({
|
557
|
+
render: true,
|
558
|
+
type: null,
|
559
|
+
plugin: null,
|
560
|
+
}),
|
561
|
+
{},
|
562
|
+
);
|
563
|
+
|
564
|
+
expect(Plugin.PluginRender).toHaveBeenCalledWith(
|
565
|
+
expect.objectContaining({
|
566
|
+
render: false,
|
567
|
+
type: 'dam',
|
568
|
+
}),
|
569
|
+
{},
|
570
|
+
);
|
571
|
+
|
572
|
+
expect(Plugin.PluginRender).toHaveBeenCalledWith(
|
573
|
+
expect.objectContaining({
|
574
|
+
render: false,
|
575
|
+
type: 'matrix',
|
576
|
+
}),
|
577
|
+
{},
|
578
|
+
);
|
579
|
+
});
|
580
|
+
});
|
581
|
+
|
582
|
+
it('Will only send render=true to the Plugin for the currently selected source', async () => {
|
583
|
+
const sourcesInput = [mockSource({ type: 'dam' }), mockSource({ type: 'matrix' })];
|
584
|
+
mockRequestSources.mockResolvedValue(sourcesInput);
|
585
|
+
|
586
|
+
// Render with an input so it will default a source
|
587
|
+
renderComponent({ value: { sourceId: sourcesInput[0].id, resourceId: '123456' } });
|
588
|
+
|
589
|
+
// Will render a default with no selected source
|
590
|
+
await waitFor(() => {
|
591
|
+
expect(Plugin.PluginRender).toHaveBeenCalledWith(
|
592
|
+
expect.objectContaining({
|
593
|
+
render: false,
|
594
|
+
type: null,
|
595
|
+
}),
|
596
|
+
{},
|
597
|
+
);
|
598
|
+
|
599
|
+
expect(Plugin.PluginRender).toHaveBeenCalledWith(
|
600
|
+
expect.objectContaining({
|
601
|
+
render: true,
|
602
|
+
type: 'dam',
|
603
|
+
}),
|
604
|
+
{},
|
605
|
+
);
|
606
|
+
|
607
|
+
expect(Plugin.PluginRender).toHaveBeenCalledWith(
|
608
|
+
expect.objectContaining({
|
609
|
+
render: false,
|
610
|
+
type: 'matrix',
|
611
|
+
}),
|
612
|
+
{},
|
613
|
+
);
|
614
|
+
});
|
615
|
+
|
616
|
+
// renderComponent({ value: { sourceId: sourcesInput[0].id, resourceId: '123456' } });
|
617
|
+
});
|
618
|
+
|
619
|
+
it('Will match plugin to render based on type', async () => {
|
620
|
+
const sourcesInput = [mockSource({ type: 'dam' }), mockSource({ type: 'matrix' })];
|
621
|
+
const calculatedSources = sourcesInput.map((source) => calculateExpectedSource(source));
|
622
|
+
mockRequestSources.mockResolvedValue(sourcesInput);
|
623
|
+
renderComponent();
|
624
|
+
|
625
|
+
// Modify the source so the plugin object comparison doesnt match
|
626
|
+
calculatedSources[0] = {
|
627
|
+
...calculatedSources[0],
|
628
|
+
plugin: {
|
629
|
+
// @ts-ignore
|
630
|
+
test: 'test',
|
631
|
+
...calculatedSources[0].plugin,
|
632
|
+
},
|
633
|
+
};
|
634
|
+
|
635
|
+
// Get the provided callback
|
636
|
+
const { setSource } = (RBI.ResourceBrowserInput as unknown as jest.SpyInstance).mock.calls[0][0];
|
637
|
+
// Invoke it
|
638
|
+
act(() => {
|
639
|
+
setSource(calculatedSources[0]);
|
640
|
+
});
|
641
|
+
|
642
|
+
// Will render a default with no selected source
|
643
|
+
await waitFor(() => {
|
644
|
+
expect(Plugin.PluginRender).toHaveBeenCalledWith(
|
645
|
+
expect.objectContaining({
|
646
|
+
render: true,
|
647
|
+
type: null,
|
648
|
+
}),
|
649
|
+
{},
|
650
|
+
);
|
651
|
+
|
652
|
+
expect(Plugin.PluginRender).toHaveBeenCalledWith(
|
653
|
+
expect.objectContaining({
|
654
|
+
render: true,
|
655
|
+
type: 'dam',
|
656
|
+
}),
|
657
|
+
{},
|
658
|
+
);
|
659
|
+
|
660
|
+
expect(Plugin.PluginRender).toHaveBeenCalledWith(
|
661
|
+
expect.objectContaining({
|
662
|
+
render: false,
|
663
|
+
type: 'matrix',
|
664
|
+
}),
|
665
|
+
{},
|
666
|
+
);
|
667
|
+
});
|
668
|
+
});
|
669
|
+
});
|
527
670
|
});
|
package/src/index.tsx
CHANGED
@@ -129,6 +129,7 @@ export const ResourceBrowser = (props: ResourceBrowserProps) => {
|
|
129
129
|
<div className="squiz-rb-scope">
|
130
130
|
<PluginRender
|
131
131
|
key="default"
|
132
|
+
type={null}
|
132
133
|
render={plugin === null}
|
133
134
|
inline={!!inline}
|
134
135
|
inlineType={inlineType}
|
@@ -157,7 +158,8 @@ export const ResourceBrowser = (props: ResourceBrowserProps) => {
|
|
157
158
|
return (
|
158
159
|
<PluginRender
|
159
160
|
key={thisPlugin.type}
|
160
|
-
|
161
|
+
type={thisPlugin.type}
|
162
|
+
render={thisPlugin.type === plugin?.type}
|
161
163
|
inline={!!inline}
|
162
164
|
inlineType={inlineType}
|
163
165
|
{...props}
|
@@ -23,6 +23,23 @@ describe('auth-utils', () => {
|
|
23
23
|
it('should return null if the cookie does not exist', () => {
|
24
24
|
expect(getCookieValue('nonExistentCookie')).toBeNull();
|
25
25
|
});
|
26
|
+
|
27
|
+
it('should return null if an error occurs when reading cookies', () => {
|
28
|
+
const originalCookieDescriptor = Object.getOwnPropertyDescriptor(Document.prototype, 'cookie');
|
29
|
+
|
30
|
+
Object.defineProperty(document, 'cookie', {
|
31
|
+
get: () => {
|
32
|
+
throw new Error('Cookie read error');
|
33
|
+
},
|
34
|
+
configurable: true,
|
35
|
+
});
|
36
|
+
|
37
|
+
expect(getCookieValue('anyCookie')).toBeNull();
|
38
|
+
|
39
|
+
if (originalCookieDescriptor) {
|
40
|
+
Object.defineProperty(document, 'cookie', originalCookieDescriptor);
|
41
|
+
}
|
42
|
+
});
|
26
43
|
});
|
27
44
|
|
28
45
|
describe('setCookieValue', () => {
|
package/src/utils/authUtils.ts
CHANGED
@@ -1,8 +1,12 @@
|
|
1
1
|
import { AuthenticationConfiguration } from '../types';
|
2
2
|
|
3
3
|
export const getCookieValue = (name: string): string | null => {
|
4
|
-
|
5
|
-
|
4
|
+
try {
|
5
|
+
const match = document.cookie.match('(^|;)\\s*' + name + '\\s*=\\s*([^;]+)');
|
6
|
+
return match ? match.pop()! : null;
|
7
|
+
} catch (error) {
|
8
|
+
return null;
|
9
|
+
}
|
6
10
|
};
|
7
11
|
|
8
12
|
export const setCookieValue = (name: string, value: string): void => {
|