@squiz/resource-browser 2.1.10-rc.0 → 2.2.1-rc.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 +10 -0
- package/lib/Hooks/useAuth.d.ts +7 -0
- package/lib/Hooks/useAuth.js +56 -0
- package/lib/MainContainer/MainContainer.js +8 -7
- package/lib/Plugin/Plugin.js +3 -1
- package/lib/ResourceBrowserContext/AuthProvider.d.ts +16 -0
- package/lib/ResourceBrowserContext/AuthProvider.js +46 -0
- package/lib/index.css +23 -12
- package/lib/index.d.ts +2 -1
- package/lib/index.js +5 -1
- package/lib/types.d.ts +11 -2
- package/lib/utils/authUtils.d.ts +5 -0
- package/lib/utils/authUtils.js +38 -0
- package/package.json +2 -2
- package/src/Hooks/useAuth.spec.tsx +137 -0
- package/src/Hooks/useAuth.ts +60 -0
- package/src/MainContainer/MainContainer.tsx +15 -13
- package/src/Plugin/Plugin.tsx +2 -1
- package/src/ResourceBrowserContext/AuthProvider.spec.tsx +73 -0
- package/src/ResourceBrowserContext/AuthProvider.tsx +40 -0
- package/src/__mocks__/StorybookHelpers.tsx +1 -1
- package/src/index.scss +0 -16
- package/src/index.spec.tsx +3 -1
- package/src/index.tsx +3 -1
- package/src/types.ts +13 -2
- package/src/utils/authUtils.spec.ts +88 -0
- package/src/utils/authUtils.ts +40 -0
- package/tailwind.config.cjs +1 -0
package/CHANGELOG.md
CHANGED
@@ -3,6 +3,16 @@
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
5
5
|
|
6
|
+
## [2.2.1-rc.0](https://gitlab.squiz.net/dxp/dxp-shared-ui/resource-browser/compare/@squiz/resource-browser@2.2.0-rc.0...@squiz/resource-browser@2.2.1-rc.0) (2024-06-07)
|
7
|
+
|
8
|
+
**Note:** Version bump only for package @squiz/resource-browser
|
9
|
+
|
10
|
+
# [2.2.0-rc.0](https://gitlab.squiz.net/dxp/dxp-shared-ui/resource-browser/compare/@squiz/resource-browser@2.1.10-rc.0...@squiz/resource-browser@2.2.0-rc.0) (2024-06-06)
|
11
|
+
|
12
|
+
### Features
|
13
|
+
|
14
|
+
- **PRODAM:92:** auth provider ([3c7136d](https://gitlab.squiz.net/dxp/dxp-shared-ui/resource-browser/commit/3c7136da5f5ef148f713ad6a39f5d2328eaff4c7))
|
15
|
+
|
6
16
|
## [2.1.10-rc.0](https://gitlab.squiz.net/dxp/dxp-shared-ui/resource-browser/compare/@squiz/resource-browser@2.1.9-rc.0...@squiz/resource-browser@2.1.10-rc.0) (2024-06-06)
|
7
17
|
|
8
18
|
**Note:** Version bump only for package @squiz/resource-browser
|
@@ -0,0 +1,7 @@
|
|
1
|
+
import { AuthenticationConfiguration } from '../types';
|
2
|
+
export declare const useAuth: (authConfig: AuthenticationConfiguration | undefined) => {
|
3
|
+
authToken: string | null;
|
4
|
+
isAuthenticated: boolean;
|
5
|
+
login: () => void;
|
6
|
+
refreshAccessToken: () => Promise<string>;
|
7
|
+
};
|
@@ -0,0 +1,56 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
exports.useAuth = void 0;
|
4
|
+
const react_1 = require("react");
|
5
|
+
const authUtils_1 = require("../utils/authUtils");
|
6
|
+
const useAuth = (authConfig) => {
|
7
|
+
const [authToken, setAuthToken] = (0, react_1.useState)((0, authUtils_1.getCookieValue)('authToken'));
|
8
|
+
const [isAuthenticated, setIsAuthenticated] = (0, react_1.useState)(!!(0, authUtils_1.getCookieValue)('authToken'));
|
9
|
+
const refreshAccessToken = (0, react_1.useCallback)(async () => {
|
10
|
+
const newToken = await (0, authUtils_1.refreshAccessToken)(authConfig);
|
11
|
+
setAuthToken(newToken);
|
12
|
+
setIsAuthenticated(!!newToken);
|
13
|
+
return newToken;
|
14
|
+
}, [authConfig]);
|
15
|
+
const handleLogin = (0, react_1.useCallback)(() => {
|
16
|
+
if (!authConfig?.redirectUrl && !authConfig?.authUrl && !authConfig?.redirectUrl && !authConfig?.scope) {
|
17
|
+
return;
|
18
|
+
}
|
19
|
+
const encodedRedirectUrl = encodeURIComponent(authConfig?.redirectUrl || '');
|
20
|
+
const loginUrl = `${authConfig?.authUrl}?client_id=${authConfig?.clientId}&scope=${authConfig?.scope}&redirect_uri=${encodedRedirectUrl}&response_type=code&state=state`;
|
21
|
+
const popup = window.open(loginUrl, 'Login', 'width=600,height=600');
|
22
|
+
if (!popup) {
|
23
|
+
console.error('Popup failed to open');
|
24
|
+
return;
|
25
|
+
}
|
26
|
+
const checkPopup = setInterval(() => {
|
27
|
+
try {
|
28
|
+
if ((0, authUtils_1.getCookieValue)('authToken') && (0, authUtils_1.getCookieValue)('refreshToken')) {
|
29
|
+
clearInterval(checkPopup);
|
30
|
+
popup.close();
|
31
|
+
setAuthToken((0, authUtils_1.getCookieValue)('authToken'));
|
32
|
+
setIsAuthenticated(true);
|
33
|
+
}
|
34
|
+
}
|
35
|
+
catch (error) {
|
36
|
+
// Ignore cross-origin access errors
|
37
|
+
}
|
38
|
+
if (popup.closed) {
|
39
|
+
clearInterval(checkPopup);
|
40
|
+
console.error('Popup closed before authentication');
|
41
|
+
}
|
42
|
+
}, 1000); // Check every second
|
43
|
+
}, [authConfig]);
|
44
|
+
(0, react_1.useEffect)(() => {
|
45
|
+
refreshAccessToken().catch(() => {
|
46
|
+
setIsAuthenticated(false);
|
47
|
+
});
|
48
|
+
}, [authConfig, refreshAccessToken]);
|
49
|
+
return {
|
50
|
+
authToken,
|
51
|
+
isAuthenticated,
|
52
|
+
login: handleLogin,
|
53
|
+
refreshAccessToken,
|
54
|
+
};
|
55
|
+
};
|
56
|
+
exports.useAuth = useAuth;
|
@@ -40,22 +40,23 @@ function MainContainer({ title, titleAriaProps, allowedTypes, sources, selectedS
|
|
40
40
|
}, [setHeaderPortal]);
|
41
41
|
// MainContainer will either render the source list view if no source is set or the plugins UI if a source has been selected
|
42
42
|
return (react_1.default.createElement("div", { className: "relative flex flex-col h-full text-gray-800" },
|
43
|
-
react_1.default.createElement("div", { className: "flex items-center
|
43
|
+
react_1.default.createElement("div", { className: "flex items-center py-4.5 pl-4.5 pr-10" },
|
44
44
|
react_1.default.createElement("h2", { ...titleAriaProps, className: "text-xl leading-6 text-gray-800 font-semibold mr-6" },
|
45
45
|
!plugin && 'Environment Selector',
|
46
46
|
plugin && title),
|
47
47
|
plugin && selectedSource && (react_1.default.createElement(react_1.default.Fragment, null,
|
48
48
|
sources.length > 1 && (react_1.default.createElement("div", { className: "px-3 border-l border-gray-300 w-300px" },
|
49
49
|
react_1.default.createElement(SourceDropdown_1.default, { sources: sources, selectedSource: selectedSource, onSourceSelect: onSourceSelect }))),
|
50
|
-
plugin.createHeaderPortal && (react_1.default.createElement("div", { ref: setHeaderPortalRef, className: "px-3 border-l border-gray-300 w-300px" })))),
|
50
|
+
plugin.createHeaderPortal && (react_1.default.createElement("div", { ref: setHeaderPortalRef, className: "squiz-rb-plugin px-3 border-l border-gray-300 w-300px" })))),
|
51
51
|
react_1.default.createElement("button", { type: "button", "aria-label": `Close ${title} dialog`, onClick: onClose, className: "absolute top-2 right-2 p-2.5 rounded hover:bg-blue-100 focus:bg-blue-100" },
|
52
52
|
react_1.default.createElement("svg", { width: "14", height: "14", viewBox: "0 0 14 14", fill: "none", xmlns: "http://www.w3.org/2000/svg" },
|
53
53
|
react_1.default.createElement("path", { d: "M13.3 0.710017C13.1131 0.522765 12.8595 0.417532 12.595 0.417532C12.3305 0.417532 12.0768 0.522765 11.89 0.710017L6.99997 5.59002L2.10997 0.700017C1.92314 0.512765 1.66949 0.407532 1.40497 0.407532C1.14045 0.407532 0.886802 0.512765 0.699971 0.700017C0.309971 1.09002 0.309971 1.72002 0.699971 2.11002L5.58997 7.00002L0.699971 11.89C0.309971 12.28 0.309971 12.91 0.699971 13.3C1.08997 13.69 1.71997 13.69 2.10997 13.3L6.99997 8.41002L11.89 13.3C12.28 13.69 12.91 13.69 13.3 13.3C13.69 12.91 13.69 12.28 13.3 11.89L8.40997 7.00002L13.3 2.11002C13.68 1.73002 13.68 1.09002 13.3 0.710017Z", fill: "currentColor" })))),
|
54
|
-
react_1.default.createElement("div", { className: "
|
55
|
-
plugin && selectedSource && SourceBrowser && (react_1.default.createElement(
|
56
|
-
|
57
|
-
|
58
|
-
|
54
|
+
react_1.default.createElement("div", { className: "border-t border-gray-300 overflow-y-hidden" },
|
55
|
+
plugin && selectedSource && SourceBrowser && (react_1.default.createElement("div", { className: "squiz-rb-plugin" },
|
56
|
+
react_1.default.createElement(SourceBrowser, { source: selectedSource, allowedTypes: allowedTypes, headerPortal: plugin.createHeaderPortal && headerPortal ? headerPortal : undefined, preselectedResource: preselectedResource || undefined, onSelected: (resource) => {
|
57
|
+
onChange(resource);
|
58
|
+
onClose();
|
59
|
+
} }))),
|
59
60
|
!selectedSource && react_1.default.createElement(SourceList_1.default, { sources: sources, onSourceSelect: onSourceSelect }))));
|
60
61
|
}
|
61
62
|
exports.default = MainContainer;
|
package/lib/Plugin/Plugin.js
CHANGED
@@ -6,9 +6,11 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.PluginRender = void 0;
|
7
7
|
const react_1 = __importDefault(require("react"));
|
8
8
|
const ResourceBrowserInput_1 = require("../ResourceBrowserInput/ResourceBrowserInput");
|
9
|
+
const AuthProvider_1 = require("../ResourceBrowserContext/AuthProvider");
|
9
10
|
const PluginRender = ({ render, ...props }) => {
|
10
11
|
if (render) {
|
11
|
-
return react_1.default.createElement(
|
12
|
+
return react_1.default.createElement(AuthProvider_1.AuthProvider, { authConfig: props.source },
|
13
|
+
react_1.default.createElement(ResourceBrowserInput_1.ResourceBrowserInput, { ...props }));
|
12
14
|
}
|
13
15
|
else {
|
14
16
|
return react_1.default.createElement(react_1.default.Fragment, null);
|
@@ -0,0 +1,16 @@
|
|
1
|
+
import React, { ReactNode } from 'react';
|
2
|
+
import { ResourceBrowserSource } from '../types';
|
3
|
+
interface AuthContextProps {
|
4
|
+
authToken: string | null;
|
5
|
+
isAuthenticated: boolean;
|
6
|
+
login: () => void;
|
7
|
+
refreshAccessToken: () => Promise<any>;
|
8
|
+
}
|
9
|
+
export declare const AuthContext: React.Context<AuthContextProps | undefined>;
|
10
|
+
export declare const useAuthContext: () => AuthContextProps;
|
11
|
+
interface AuthProviderProps {
|
12
|
+
children: ReactNode;
|
13
|
+
authConfig?: ResourceBrowserSource | null;
|
14
|
+
}
|
15
|
+
export declare const AuthProvider: ({ children, authConfig }: AuthProviderProps) => React.JSX.Element;
|
16
|
+
export {};
|
@@ -0,0 +1,46 @@
|
|
1
|
+
"use strict";
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
3
|
+
if (k2 === undefined) k2 = k;
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
7
|
+
}
|
8
|
+
Object.defineProperty(o, k2, desc);
|
9
|
+
}) : (function(o, m, k, k2) {
|
10
|
+
if (k2 === undefined) k2 = k;
|
11
|
+
o[k2] = m[k];
|
12
|
+
}));
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
15
|
+
}) : function(o, v) {
|
16
|
+
o["default"] = v;
|
17
|
+
});
|
18
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
19
|
+
if (mod && mod.__esModule) return mod;
|
20
|
+
var result = {};
|
21
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
22
|
+
__setModuleDefault(result, mod);
|
23
|
+
return result;
|
24
|
+
};
|
25
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
26
|
+
exports.AuthProvider = exports.useAuthContext = exports.AuthContext = void 0;
|
27
|
+
const react_1 = __importStar(require("react"));
|
28
|
+
const useAuth_1 = require("../Hooks/useAuth");
|
29
|
+
exports.AuthContext = (0, react_1.createContext)(undefined);
|
30
|
+
const useAuthContext = () => {
|
31
|
+
const context = (0, react_1.useContext)(exports.AuthContext);
|
32
|
+
if (!context) {
|
33
|
+
throw new Error('useAuthContext must be used within an AuthProvider');
|
34
|
+
}
|
35
|
+
return context;
|
36
|
+
};
|
37
|
+
exports.useAuthContext = useAuthContext;
|
38
|
+
const AuthProvider = ({ children, authConfig }) => {
|
39
|
+
const authConfiguration = authConfig;
|
40
|
+
const auth = (0, useAuth_1.useAuth)(authConfiguration?.configuration);
|
41
|
+
if (!authConfiguration?.configuration) {
|
42
|
+
return react_1.default.createElement(react_1.default.Fragment, null, children);
|
43
|
+
}
|
44
|
+
return (react_1.default.createElement(exports.AuthContext.Provider, { value: auth }, children));
|
45
|
+
};
|
46
|
+
exports.AuthProvider = AuthProvider;
|
package/lib/index.css
CHANGED
@@ -610,6 +610,9 @@
|
|
610
610
|
.squiz-rb-scope .max-w-\[50rem\]:not(.squiz-rb-plugin *) {
|
611
611
|
max-width: 50rem;
|
612
612
|
}
|
613
|
+
.squiz-rb-scope .max-w-\[52rem\]:not(.squiz-rb-plugin *) {
|
614
|
+
max-width: 52rem;
|
615
|
+
}
|
613
616
|
.squiz-rb-scope .flex-1:not(.squiz-rb-plugin *) {
|
614
617
|
flex: 1 1 0%;
|
615
618
|
}
|
@@ -815,6 +818,14 @@
|
|
815
818
|
padding-top: 0.5rem;
|
816
819
|
padding-bottom: 0.5rem;
|
817
820
|
}
|
821
|
+
.squiz-rb-scope .py-4:not(.squiz-rb-plugin *) {
|
822
|
+
padding-top: 1rem;
|
823
|
+
padding-bottom: 1rem;
|
824
|
+
}
|
825
|
+
.squiz-rb-scope .py-4\.5:not(.squiz-rb-plugin *) {
|
826
|
+
padding-top: 1.125rem;
|
827
|
+
padding-bottom: 1.125rem;
|
828
|
+
}
|
818
829
|
.squiz-rb-scope .py-8:not(.squiz-rb-plugin *) {
|
819
830
|
padding-top: 2rem;
|
820
831
|
padding-bottom: 2rem;
|
@@ -825,12 +836,24 @@
|
|
825
836
|
.squiz-rb-scope .pb-4:not(.squiz-rb-plugin *) {
|
826
837
|
padding-bottom: 1rem;
|
827
838
|
}
|
839
|
+
.squiz-rb-scope .pb-4\.5:not(.squiz-rb-plugin *) {
|
840
|
+
padding-bottom: 1.125rem;
|
841
|
+
}
|
828
842
|
.squiz-rb-scope .pl-4:not(.squiz-rb-plugin *) {
|
829
843
|
padding-left: 1rem;
|
830
844
|
}
|
845
|
+
.squiz-rb-scope .pl-4\.5:not(.squiz-rb-plugin *) {
|
846
|
+
padding-left: 1.125rem;
|
847
|
+
}
|
848
|
+
.squiz-rb-scope .pr-10:not(.squiz-rb-plugin *) {
|
849
|
+
padding-right: 2.5rem;
|
850
|
+
}
|
831
851
|
.squiz-rb-scope .pr-4:not(.squiz-rb-plugin *) {
|
832
852
|
padding-right: 1rem;
|
833
853
|
}
|
854
|
+
.squiz-rb-scope .pr-4\.5:not(.squiz-rb-plugin *) {
|
855
|
+
padding-right: 1.125rem;
|
856
|
+
}
|
834
857
|
.squiz-rb-scope .pt-3:not(.squiz-rb-plugin *) {
|
835
858
|
padding-top: 0.75rem;
|
836
859
|
}
|
@@ -1032,18 +1055,6 @@
|
|
1032
1055
|
--tw-text-opacity: 1;
|
1033
1056
|
color: rgb(112 112 112 / var(--tw-text-opacity));
|
1034
1057
|
}
|
1035
|
-
.squiz-rb-scope .p-4\.5:not(.squiz-rb-plugin *) {
|
1036
|
-
padding: 18px;
|
1037
|
-
}
|
1038
|
-
.squiz-rb-scope .pl-4\.5:not(.squiz-rb-plugin *) {
|
1039
|
-
padding-left: 18px;
|
1040
|
-
}
|
1041
|
-
.squiz-rb-scope .pr-4\.5:not(.squiz-rb-plugin *) {
|
1042
|
-
padding-right: 18px;
|
1043
|
-
}
|
1044
|
-
.squiz-rb-scope .pb-4\.5:not(.squiz-rb-plugin *) {
|
1045
|
-
padding-bottom: 18px;
|
1046
|
-
}
|
1047
1058
|
.squiz-rb-scope .break-word:not(.squiz-rb-plugin *) {
|
1048
1059
|
word-break: break-word;
|
1049
1060
|
overflow-wrap: anywhere;
|
package/lib/index.d.ts
CHANGED
@@ -1,7 +1,8 @@
|
|
1
1
|
import React from 'react';
|
2
2
|
import { ResourceBrowserContext, ResourceBrowserContextProvider } from './ResourceBrowserContext/ResourceBrowserContext';
|
3
3
|
import { ResourceBrowserUnresolvedResource, ResourceBrowserResource } from './types';
|
4
|
-
|
4
|
+
import { AuthProvider, useAuthContext, AuthContext } from './ResourceBrowserContext/AuthProvider';
|
5
|
+
export { ResourceBrowserContext, ResourceBrowserContextProvider, useAuthContext, AuthProvider, AuthContext };
|
5
6
|
export * from './types';
|
6
7
|
export type ResourceBrowserProps = {
|
7
8
|
modalTitle: string;
|
package/lib/index.js
CHANGED
@@ -26,13 +26,17 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
26
26
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
27
27
|
};
|
28
28
|
Object.defineProperty(exports, "__esModule", { value: true });
|
29
|
-
exports.ResourceBrowser = exports.ResourceBrowserContextProvider = exports.ResourceBrowserContext = void 0;
|
29
|
+
exports.ResourceBrowser = exports.AuthContext = exports.AuthProvider = exports.useAuthContext = exports.ResourceBrowserContextProvider = exports.ResourceBrowserContext = void 0;
|
30
30
|
const react_1 = __importStar(require("react"));
|
31
31
|
const ResourceBrowserContext_1 = require("./ResourceBrowserContext/ResourceBrowserContext");
|
32
32
|
Object.defineProperty(exports, "ResourceBrowserContext", { enumerable: true, get: function () { return ResourceBrowserContext_1.ResourceBrowserContext; } });
|
33
33
|
Object.defineProperty(exports, "ResourceBrowserContextProvider", { enumerable: true, get: function () { return ResourceBrowserContext_1.ResourceBrowserContextProvider; } });
|
34
34
|
const useSources_1 = require("./Hooks/useSources");
|
35
35
|
const Plugin_1 = require("./Plugin/Plugin");
|
36
|
+
const AuthProvider_1 = require("./ResourceBrowserContext/AuthProvider");
|
37
|
+
Object.defineProperty(exports, "AuthProvider", { enumerable: true, get: function () { return AuthProvider_1.AuthProvider; } });
|
38
|
+
Object.defineProperty(exports, "useAuthContext", { enumerable: true, get: function () { return AuthProvider_1.useAuthContext; } });
|
39
|
+
Object.defineProperty(exports, "AuthContext", { enumerable: true, get: function () { return AuthProvider_1.AuthContext; } });
|
36
40
|
__exportStar(require("./types"), exports);
|
37
41
|
const ResourceBrowser = (props) => {
|
38
42
|
const { value } = props;
|
package/lib/types.d.ts
CHANGED
@@ -2,11 +2,20 @@ import React, { ReactElement } from 'react';
|
|
2
2
|
import { SquizImageType } from '@squiz/dx-json-schema-lib';
|
3
3
|
export type OnRequestSources = () => Promise<ResourceBrowserSource[]>;
|
4
4
|
export type ResourceBrowserPluginType = 'dam' | 'matrix';
|
5
|
-
export type
|
5
|
+
export type AuthenticationConfiguration = {
|
6
|
+
authUrl: string;
|
7
|
+
redirectUrl: string;
|
8
|
+
clientId: string;
|
9
|
+
scope: string;
|
10
|
+
};
|
11
|
+
export interface ResourceBrowserSource {
|
6
12
|
name?: string;
|
7
13
|
id: string;
|
8
14
|
type: ResourceBrowserPluginType;
|
9
|
-
}
|
15
|
+
}
|
16
|
+
export interface ResourceBrowserSourceWithConfig extends ResourceBrowserSource {
|
17
|
+
configuration?: AuthenticationConfiguration;
|
18
|
+
}
|
10
19
|
export type ResourceBrowserUnresolvedResource = {
|
11
20
|
sourceId: string;
|
12
21
|
resourceId: string;
|
@@ -0,0 +1,5 @@
|
|
1
|
+
import { AuthenticationConfiguration } from '../types';
|
2
|
+
export declare const getCookieValue: (name: string) => string | null;
|
3
|
+
export declare const setCookieValue: (name: string, value: string) => void;
|
4
|
+
export declare const logout: () => void;
|
5
|
+
export declare const refreshAccessToken: (authConfig?: AuthenticationConfiguration) => Promise<string>;
|
@@ -0,0 +1,38 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
exports.refreshAccessToken = exports.logout = exports.setCookieValue = exports.getCookieValue = void 0;
|
4
|
+
const getCookieValue = (name) => {
|
5
|
+
const match = document.cookie.match('(^|;)\\s*' + name + '\\s*=\\s*([^;]+)');
|
6
|
+
return match ? match.pop() : null;
|
7
|
+
};
|
8
|
+
exports.getCookieValue = getCookieValue;
|
9
|
+
const setCookieValue = (name, value) => {
|
10
|
+
document.cookie = `${name}=${value}; Path=/;`;
|
11
|
+
};
|
12
|
+
exports.setCookieValue = setCookieValue;
|
13
|
+
const logout = () => {
|
14
|
+
(0, exports.setCookieValue)('authToken', '');
|
15
|
+
(0, exports.setCookieValue)('refreshToken', '');
|
16
|
+
};
|
17
|
+
exports.logout = logout;
|
18
|
+
const refreshAccessToken = async (authConfig) => {
|
19
|
+
if (!authConfig) {
|
20
|
+
throw new Error('No auth configuration available');
|
21
|
+
}
|
22
|
+
const refreshToken = (0, exports.getCookieValue)('refreshToken');
|
23
|
+
if (!refreshToken) {
|
24
|
+
throw new Error('You are not logged in');
|
25
|
+
}
|
26
|
+
const response = await fetch(`${authConfig.redirectUrl}?grant_type=refresh_token&refresh_token=${refreshToken}`, {
|
27
|
+
method: 'GET',
|
28
|
+
credentials: 'include',
|
29
|
+
});
|
30
|
+
if (!response.ok) {
|
31
|
+
(0, exports.logout)();
|
32
|
+
throw new Error('Failed to refresh token');
|
33
|
+
}
|
34
|
+
const data = await response.json();
|
35
|
+
(0, exports.setCookieValue)('authToken', data.access_token);
|
36
|
+
return data.access_token;
|
37
|
+
};
|
38
|
+
exports.refreshAccessToken = refreshAccessToken;
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@squiz/resource-browser",
|
3
|
-
"version": "2.1
|
3
|
+
"version": "2.2.1-rc.0",
|
4
4
|
"main": "lib/index.js",
|
5
5
|
"types": "lib/index.d.ts",
|
6
6
|
"private": false,
|
@@ -81,5 +81,5 @@
|
|
81
81
|
"volta": {
|
82
82
|
"node": "18.18.0"
|
83
83
|
},
|
84
|
-
"gitHead": "
|
84
|
+
"gitHead": "2ae6e09c8e222b2ef95f12243abe659ed76f0fd0"
|
85
85
|
}
|
@@ -0,0 +1,137 @@
|
|
1
|
+
import { renderHook, act, waitFor } from '@testing-library/react';
|
2
|
+
import { useAuth } from './useAuth';
|
3
|
+
import '@testing-library/jest-dom';
|
4
|
+
import { AuthenticationConfiguration } from '../types';
|
5
|
+
import * as authUtils from '../utils/authUtils';
|
6
|
+
|
7
|
+
jest.mock('../utils/authUtils');
|
8
|
+
|
9
|
+
const mockGetCookieValue = authUtils.getCookieValue as jest.MockedFunction<typeof authUtils.getCookieValue>;
|
10
|
+
const mockRefreshAccessToken = authUtils.refreshAccessToken as jest.MockedFunction<typeof authUtils.refreshAccessToken>;
|
11
|
+
|
12
|
+
describe('useAuth', () => {
|
13
|
+
const authConfig: AuthenticationConfiguration = {
|
14
|
+
authUrl: 'https://auth.example.com',
|
15
|
+
clientId: 'example-client-id',
|
16
|
+
redirectUrl: 'https://example.com/callback',
|
17
|
+
scope: 'offline_access'
|
18
|
+
};
|
19
|
+
|
20
|
+
beforeEach(() => {
|
21
|
+
jest.clearAllMocks();
|
22
|
+
});
|
23
|
+
|
24
|
+
it('should initialize with token and authentication state', async () => {
|
25
|
+
mockGetCookieValue.mockReturnValue('initialAuthToken');
|
26
|
+
|
27
|
+
const { result } = renderHook(() => useAuth(authConfig));
|
28
|
+
|
29
|
+
await waitFor(() => {
|
30
|
+
expect(result.current.authToken).toBe('initialAuthToken');
|
31
|
+
expect(result.current.isAuthenticated).toBe(true);
|
32
|
+
});
|
33
|
+
});
|
34
|
+
|
35
|
+
it('should log error and return when popup fails to open', async () => {
|
36
|
+
const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
37
|
+
jest.spyOn(window, 'open').mockImplementation(() => null);
|
38
|
+
|
39
|
+
const { result } = renderHook(() => useAuth(authConfig));
|
40
|
+
|
41
|
+
result.current.login();
|
42
|
+
|
43
|
+
await waitFor(() => {
|
44
|
+
expect(consoleSpy).toHaveBeenCalledWith('Popup failed to open');
|
45
|
+
});
|
46
|
+
consoleSpy.mockRestore();
|
47
|
+
});
|
48
|
+
|
49
|
+
it('should log error when popup closes before authentication', async () => {
|
50
|
+
jest.useFakeTimers();
|
51
|
+
|
52
|
+
const popupMock = {
|
53
|
+
close: jest.fn(),
|
54
|
+
} as unknown as Window;
|
55
|
+
|
56
|
+
Object.defineProperty(popupMock, 'closed', {
|
57
|
+
get: jest.fn(() => false),
|
58
|
+
set: jest.fn()
|
59
|
+
});
|
60
|
+
|
61
|
+
const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
62
|
+
jest.spyOn(window, 'open').mockImplementation(() => popupMock);
|
63
|
+
mockGetCookieValue.mockReturnValueOnce(null);
|
64
|
+
|
65
|
+
const { result } = renderHook(() => useAuth(authConfig));
|
66
|
+
|
67
|
+
result.current.login();
|
68
|
+
(Object.getOwnPropertyDescriptor(popupMock, 'closed')!.get as jest.Mock).mockReturnValue(true);
|
69
|
+
jest.advanceTimersByTime(1000);
|
70
|
+
|
71
|
+
await waitFor(() => {
|
72
|
+
expect(consoleSpy).toHaveBeenCalledWith('Popup closed before authentication');
|
73
|
+
});
|
74
|
+
|
75
|
+
consoleSpy.mockRestore();
|
76
|
+
});
|
77
|
+
|
78
|
+
it('should login successfully and update state', async () => {
|
79
|
+
jest.useFakeTimers();
|
80
|
+
|
81
|
+
const popupMock = {
|
82
|
+
closed: false,
|
83
|
+
close: jest.fn(),
|
84
|
+
} as unknown as Window;
|
85
|
+
|
86
|
+
jest.spyOn(window, 'open').mockImplementation(() => popupMock);
|
87
|
+
mockGetCookieValue.mockReturnValueOnce(null).mockReturnValueOnce('newAuthToken').mockReturnValueOnce('newRefreshToken');
|
88
|
+
|
89
|
+
const { result } = renderHook(() => useAuth(authConfig));
|
90
|
+
|
91
|
+
result.current.login();
|
92
|
+
|
93
|
+
expect(window.open).toHaveBeenCalledWith(
|
94
|
+
`${authConfig.authUrl}?client_id=${authConfig.clientId}&scope=offline_access&redirect_uri=${encodeURIComponent(authConfig.redirectUrl)}&response_type=code&state=state`,
|
95
|
+
'Login',
|
96
|
+
'width=600,height=600'
|
97
|
+
);
|
98
|
+
|
99
|
+
act(() => {
|
100
|
+
mockGetCookieValue.mockReturnValue('newAuthToken');
|
101
|
+
jest.advanceTimersByTime(1000);
|
102
|
+
(popupMock as any).closed = true;
|
103
|
+
});
|
104
|
+
|
105
|
+
await waitFor(() => {
|
106
|
+
expect(result.current.authToken).toBe('newAuthToken');
|
107
|
+
expect(result.current.isAuthenticated).toBe(true);
|
108
|
+
});
|
109
|
+
|
110
|
+
expect(popupMock.close).toHaveBeenCalled();
|
111
|
+
});
|
112
|
+
|
113
|
+
it('should refresh access token and update state', async () => {
|
114
|
+
mockGetCookieValue.mockReturnValue('initialRefreshToken');
|
115
|
+
mockRefreshAccessToken.mockResolvedValue('newAuthToken');
|
116
|
+
|
117
|
+
const { result } = renderHook(() => useAuth(authConfig));
|
118
|
+
|
119
|
+
await waitFor(() => {
|
120
|
+
expect(mockRefreshAccessToken).toHaveBeenCalledWith(authConfig);
|
121
|
+
expect(result.current.authToken).toBe('newAuthToken');
|
122
|
+
expect(result.current.isAuthenticated).toBe(true);
|
123
|
+
});
|
124
|
+
});
|
125
|
+
|
126
|
+
it('should handle token refresh failure', async () => {
|
127
|
+
mockGetCookieValue.mockReturnValue('initialRefreshToken');
|
128
|
+
mockRefreshAccessToken.mockRejectedValue(new Error('Failed to refresh token'));
|
129
|
+
|
130
|
+
const { result } = renderHook(() => useAuth(authConfig));
|
131
|
+
|
132
|
+
await waitFor(() => {
|
133
|
+
expect(mockRefreshAccessToken).toHaveBeenCalledWith(authConfig);
|
134
|
+
expect(result.current.isAuthenticated).toBe(false);
|
135
|
+
});
|
136
|
+
});
|
137
|
+
});
|
@@ -0,0 +1,60 @@
|
|
1
|
+
import { useState, useCallback, useEffect } from 'react';
|
2
|
+
import { AuthenticationConfiguration } from '../types';
|
3
|
+
import { getCookieValue, refreshAccessToken as refreshTokenUtil } from '../utils/authUtils';
|
4
|
+
|
5
|
+
export const useAuth = (authConfig: AuthenticationConfiguration | undefined) => {
|
6
|
+
const [authToken, setAuthToken] = useState<string | null>(getCookieValue('authToken'));
|
7
|
+
const [isAuthenticated, setIsAuthenticated] = useState<boolean>(!!getCookieValue('authToken'));
|
8
|
+
|
9
|
+
const refreshAccessToken = useCallback(async (): Promise<string> => {
|
10
|
+
const newToken = await refreshTokenUtil(authConfig);
|
11
|
+
setAuthToken(newToken);
|
12
|
+
setIsAuthenticated(!!newToken);
|
13
|
+
return newToken;
|
14
|
+
}, [authConfig]);
|
15
|
+
|
16
|
+
const handleLogin = useCallback((): void => {
|
17
|
+
if (!authConfig?.redirectUrl && !authConfig?.authUrl && !authConfig?.redirectUrl && !authConfig?.scope) {
|
18
|
+
return;
|
19
|
+
}
|
20
|
+
const encodedRedirectUrl = encodeURIComponent(authConfig?.redirectUrl || '');
|
21
|
+
const loginUrl = `${authConfig?.authUrl}?client_id=${authConfig?.clientId}&scope=${authConfig?.scope}&redirect_uri=${encodedRedirectUrl}&response_type=code&state=state`;
|
22
|
+
const popup = window.open(loginUrl, 'Login', 'width=600,height=600');
|
23
|
+
|
24
|
+
if (!popup) {
|
25
|
+
console.error('Popup failed to open');
|
26
|
+
return;
|
27
|
+
}
|
28
|
+
|
29
|
+
const checkPopup = setInterval(() => {
|
30
|
+
try {
|
31
|
+
if (getCookieValue('authToken') && getCookieValue('refreshToken')) {
|
32
|
+
clearInterval(checkPopup);
|
33
|
+
popup.close();
|
34
|
+
setAuthToken(getCookieValue('authToken'));
|
35
|
+
setIsAuthenticated(true);
|
36
|
+
}
|
37
|
+
} catch (error) {
|
38
|
+
// Ignore cross-origin access errors
|
39
|
+
}
|
40
|
+
|
41
|
+
if (popup.closed) {
|
42
|
+
clearInterval(checkPopup);
|
43
|
+
console.error('Popup closed before authentication');
|
44
|
+
}
|
45
|
+
}, 1000); // Check every second
|
46
|
+
}, [authConfig]);
|
47
|
+
|
48
|
+
useEffect(() => {
|
49
|
+
refreshAccessToken().catch(() => {
|
50
|
+
setIsAuthenticated(false);
|
51
|
+
});
|
52
|
+
}, [authConfig, refreshAccessToken]);
|
53
|
+
|
54
|
+
return {
|
55
|
+
authToken,
|
56
|
+
isAuthenticated,
|
57
|
+
login: handleLogin,
|
58
|
+
refreshAccessToken,
|
59
|
+
};
|
60
|
+
};
|
@@ -46,7 +46,7 @@ function MainContainer({
|
|
46
46
|
// MainContainer will either render the source list view if no source is set or the plugins UI if a source has been selected
|
47
47
|
return (
|
48
48
|
<div className="relative flex flex-col h-full text-gray-800">
|
49
|
-
<div className="flex items-center
|
49
|
+
<div className="flex items-center py-4.5 pl-4.5 pr-10">
|
50
50
|
<h2 {...titleAriaProps} className="text-xl leading-6 text-gray-800 font-semibold mr-6">
|
51
51
|
{!plugin && 'Environment Selector'}
|
52
52
|
{plugin && title}
|
@@ -60,7 +60,7 @@ function MainContainer({
|
|
60
60
|
</div>
|
61
61
|
)}
|
62
62
|
{plugin.createHeaderPortal && (
|
63
|
-
<div ref={setHeaderPortalRef} className="px-3 border-l border-gray-300 w-300px"></div>
|
63
|
+
<div ref={setHeaderPortalRef} className="squiz-rb-plugin px-3 border-l border-gray-300 w-300px"></div>
|
64
64
|
)}
|
65
65
|
</>
|
66
66
|
)}
|
@@ -79,18 +79,20 @@ function MainContainer({
|
|
79
79
|
</svg>
|
80
80
|
</button>
|
81
81
|
</div>
|
82
|
-
<div className="
|
82
|
+
<div className="border-t border-gray-300 overflow-y-hidden">
|
83
83
|
{plugin && selectedSource && SourceBrowser && (
|
84
|
-
<
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
84
|
+
<div className="squiz-rb-plugin">
|
85
|
+
<SourceBrowser
|
86
|
+
source={selectedSource}
|
87
|
+
allowedTypes={allowedTypes}
|
88
|
+
headerPortal={plugin.createHeaderPortal && headerPortal ? headerPortal : undefined}
|
89
|
+
preselectedResource={preselectedResource || undefined}
|
90
|
+
onSelected={(resource: ResourceBrowserResource) => {
|
91
|
+
onChange(resource);
|
92
|
+
onClose();
|
93
|
+
}}
|
94
|
+
/>
|
95
|
+
</div>
|
94
96
|
)}
|
95
97
|
{!selectedSource && <SourceList sources={sources} onSourceSelect={onSourceSelect} />}
|
96
98
|
</div>
|
package/src/Plugin/Plugin.tsx
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
import React from 'react';
|
2
2
|
import { ResourceBrowserInput, ResourceBrowserInputProps } from '../ResourceBrowserInput/ResourceBrowserInput';
|
3
|
+
import { AuthProvider } from '../ResourceBrowserContext/AuthProvider';
|
3
4
|
|
4
5
|
/**
|
5
6
|
* This plugin component exsits to deal with React rules of Hooks stupidity.
|
@@ -13,7 +14,7 @@ export type PluginRenderType = ResourceBrowserInputProps & {
|
|
13
14
|
};
|
14
15
|
export const PluginRender = ({ render, ...props }: PluginRenderType) => {
|
15
16
|
if (render) {
|
16
|
-
return <ResourceBrowserInput {...props}
|
17
|
+
return <AuthProvider authConfig={props.source}><ResourceBrowserInput {...props} /></AuthProvider>;
|
17
18
|
} else {
|
18
19
|
return <></>;
|
19
20
|
}
|
@@ -0,0 +1,73 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import { render, screen, fireEvent } from '@testing-library/react';
|
3
|
+
import { AuthProvider, useAuthContext } from './AuthProvider';
|
4
|
+
import {AuthenticationConfiguration, ResourceBrowserSourceWithConfig} from '../types';
|
5
|
+
import { useAuth } from '../Hooks/useAuth';
|
6
|
+
|
7
|
+
jest.mock('../Hooks/useAuth');
|
8
|
+
|
9
|
+
const mockUseAuth = useAuth as jest.MockedFunction<typeof useAuth>;
|
10
|
+
|
11
|
+
describe('AuthContext', () => {
|
12
|
+
const authConfig: AuthenticationConfiguration = {
|
13
|
+
authUrl: 'https://auth.example.com',
|
14
|
+
clientId: 'example-client-id',
|
15
|
+
redirectUrl: 'https://example.com/callback',
|
16
|
+
scope: 'offline'
|
17
|
+
};
|
18
|
+
|
19
|
+
const sourceConfig: ResourceBrowserSourceWithConfig = {
|
20
|
+
id: '1',
|
21
|
+
type: 'dam',
|
22
|
+
configuration: authConfig
|
23
|
+
};
|
24
|
+
|
25
|
+
const authState = {
|
26
|
+
authToken: 'testAuthToken',
|
27
|
+
isAuthenticated: true,
|
28
|
+
login: jest.fn(),
|
29
|
+
refreshAccessToken: jest.fn(),
|
30
|
+
};
|
31
|
+
|
32
|
+
beforeEach(() => {
|
33
|
+
jest.clearAllMocks();
|
34
|
+
mockUseAuth.mockReturnValue(authState);
|
35
|
+
});
|
36
|
+
|
37
|
+
it('should provide auth state and functions to consumers', async () => {
|
38
|
+
const TestComponent = () => {
|
39
|
+
const { authToken, isAuthenticated, login } = useAuthContext();
|
40
|
+
return (
|
41
|
+
<div>
|
42
|
+
<span>{`Token: ${authToken}`}</span>
|
43
|
+
<span>{`Authenticated: ${isAuthenticated}`}</span>
|
44
|
+
<button onClick={login}>Login</button>
|
45
|
+
</div>
|
46
|
+
);
|
47
|
+
};
|
48
|
+
|
49
|
+
render(
|
50
|
+
<AuthProvider authConfig={sourceConfig}>
|
51
|
+
<TestComponent />
|
52
|
+
</AuthProvider>
|
53
|
+
);
|
54
|
+
|
55
|
+
expect(screen.getByText(/Token: testAuthToken/)).toBeInTheDocument();
|
56
|
+
expect(screen.getByText(/Authenticated: true/)).toBeInTheDocument();
|
57
|
+
|
58
|
+
fireEvent.click(screen.getByText('Login'));
|
59
|
+
|
60
|
+
expect(authState.login).toHaveBeenCalled();
|
61
|
+
});
|
62
|
+
|
63
|
+
it('should throw an error if used outside of AuthProvider', () => {
|
64
|
+
jest.spyOn(console, 'error').mockImplementation(() => {});
|
65
|
+
|
66
|
+
const TestComponent = () => {
|
67
|
+
useAuthContext();
|
68
|
+
return <div />;
|
69
|
+
};
|
70
|
+
|
71
|
+
expect(() => render(<TestComponent />)).toThrowError('useAuthContext must be used within an AuthProvider');
|
72
|
+
});
|
73
|
+
});
|
@@ -0,0 +1,40 @@
|
|
1
|
+
import React, { createContext, useContext, ReactNode } from 'react';
|
2
|
+
import { useAuth } from '../Hooks/useAuth';
|
3
|
+
import { ResourceBrowserSource, ResourceBrowserSourceWithConfig } from '../types';
|
4
|
+
|
5
|
+
interface AuthContextProps {
|
6
|
+
authToken: string | null;
|
7
|
+
isAuthenticated: boolean;
|
8
|
+
login: () => void;
|
9
|
+
refreshAccessToken: () => Promise<any>;
|
10
|
+
}
|
11
|
+
|
12
|
+
export const AuthContext = createContext<AuthContextProps | undefined>(undefined);
|
13
|
+
|
14
|
+
export const useAuthContext = (): AuthContextProps => {
|
15
|
+
const context = useContext(AuthContext);
|
16
|
+
if (!context) {
|
17
|
+
throw new Error('useAuthContext must be used within an AuthProvider');
|
18
|
+
}
|
19
|
+
return context;
|
20
|
+
};
|
21
|
+
|
22
|
+
interface AuthProviderProps {
|
23
|
+
children: ReactNode;
|
24
|
+
authConfig?: ResourceBrowserSource | null;
|
25
|
+
}
|
26
|
+
|
27
|
+
export const AuthProvider = ( {children, authConfig}: AuthProviderProps ) => {
|
28
|
+
const authConfiguration = authConfig as ResourceBrowserSourceWithConfig;
|
29
|
+
const auth = useAuth(authConfiguration?.configuration);
|
30
|
+
|
31
|
+
if (!authConfiguration?.configuration) {
|
32
|
+
return <>{children}</>;
|
33
|
+
}
|
34
|
+
|
35
|
+
return (
|
36
|
+
<AuthContext.Provider value={auth}>
|
37
|
+
{children}
|
38
|
+
</AuthContext.Provider>
|
39
|
+
);
|
40
|
+
};
|
@@ -21,7 +21,7 @@ export const createPlugins = (callbackWait: number, headerPortal = false): Resou
|
|
21
21
|
sourceBrowserComponent: () => {
|
22
22
|
return (props) => {
|
23
23
|
return (
|
24
|
-
<div className="h-screen lg:h-[calc(100vh-9rem)] w-screen max-w-[
|
24
|
+
<div className="h-screen lg:h-[calc(100vh-9rem)] w-screen max-w-[52rem]">
|
25
25
|
<div>THIS IS A {type} PLUGIN</div>
|
26
26
|
<button
|
27
27
|
onClick={() => {
|
package/src/index.scss
CHANGED
@@ -18,22 +18,6 @@ svg {
|
|
18
18
|
@apply text-gray-600;
|
19
19
|
}
|
20
20
|
|
21
|
-
.p-4\.5 {
|
22
|
-
padding: 18px;
|
23
|
-
}
|
24
|
-
|
25
|
-
.pl-4\.5 {
|
26
|
-
padding-left: 18px;
|
27
|
-
}
|
28
|
-
|
29
|
-
.pr-4\.5 {
|
30
|
-
padding-right: 18px;
|
31
|
-
}
|
32
|
-
|
33
|
-
.pb-4\.5 {
|
34
|
-
padding-bottom: 18px;
|
35
|
-
}
|
36
|
-
|
37
21
|
// In tailwind there is no break-word as it is deprecated, but break-words which is slightly different does not work here, so I have added the suggested combination here
|
38
22
|
// https://v1.tailwindcss.com/docs/word-break
|
39
23
|
// https://github.com/tailwindlabs/tailwindcss/discussions/2213
|
package/src/index.spec.tsx
CHANGED
@@ -267,7 +267,9 @@ describe('Resource browser input', () => {
|
|
267
267
|
|
268
268
|
// Invoke modal close callback
|
269
269
|
const { onModalStateChange } = (RBI.ResourceBrowserInput as unknown as jest.SpyInstance).mock.calls[0][0];
|
270
|
-
|
270
|
+
act(() => {
|
271
|
+
onModalStateChange(false);
|
272
|
+
});
|
271
273
|
|
272
274
|
await waitFor(() => {
|
273
275
|
expect(RBI.ResourceBrowserInput).toHaveBeenCalledWith(
|
package/src/index.tsx
CHANGED
@@ -4,8 +4,9 @@ import { ResourceBrowserContext, ResourceBrowserContextProvider } from './Resour
|
|
4
4
|
import { ResourceBrowserSource, ResourceBrowserUnresolvedResource, ResourceBrowserResource, ResourceBrowserPlugin } from './types';
|
5
5
|
import { useSources } from './Hooks/useSources';
|
6
6
|
import { PluginRender } from './Plugin/Plugin';
|
7
|
+
import { AuthProvider, useAuthContext, AuthContext } from './ResourceBrowserContext/AuthProvider';
|
7
8
|
|
8
|
-
export { ResourceBrowserContext, ResourceBrowserContextProvider };
|
9
|
+
export { ResourceBrowserContext, ResourceBrowserContextProvider, useAuthContext, AuthProvider, AuthContext };
|
9
10
|
export * from './types';
|
10
11
|
|
11
12
|
export type ResourceBrowserProps = {
|
@@ -121,6 +122,7 @@ export const ResourceBrowser = (props: ResourceBrowserProps) => {
|
|
121
122
|
/>
|
122
123
|
);
|
123
124
|
})}
|
125
|
+
|
124
126
|
</div>
|
125
127
|
);
|
126
128
|
};
|
package/src/types.ts
CHANGED
@@ -4,14 +4,25 @@ import { SquizImageType } from '@squiz/dx-json-schema-lib';
|
|
4
4
|
export type OnRequestSources = () => Promise<ResourceBrowserSource[]>;
|
5
5
|
export type ResourceBrowserPluginType = 'dam' | 'matrix';
|
6
6
|
|
7
|
-
export type
|
7
|
+
export type AuthenticationConfiguration = {
|
8
|
+
authUrl: string;
|
9
|
+
redirectUrl: string;
|
10
|
+
clientId: string;
|
11
|
+
scope: string;
|
12
|
+
}
|
13
|
+
|
14
|
+
export interface ResourceBrowserSource {
|
8
15
|
// Source name; shown on the UI
|
9
16
|
name?: string;
|
10
17
|
// Source identifier for additional information lookups
|
11
18
|
id: string;
|
12
19
|
// Source type e.g. Matrix, DAM etc determines what plugin will be used for resource selection
|
13
20
|
type: ResourceBrowserPluginType;
|
14
|
-
}
|
21
|
+
}
|
22
|
+
|
23
|
+
export interface ResourceBrowserSourceWithConfig extends ResourceBrowserSource {
|
24
|
+
configuration?: AuthenticationConfiguration;
|
25
|
+
}
|
15
26
|
|
16
27
|
export type ResourceBrowserUnresolvedResource = {
|
17
28
|
sourceId: string;
|
@@ -0,0 +1,88 @@
|
|
1
|
+
import { getCookieValue, setCookieValue, logout, refreshAccessToken } from './authUtils';
|
2
|
+
import { AuthenticationConfiguration } from '../types';
|
3
|
+
|
4
|
+
// Mock the global fetch function
|
5
|
+
global.fetch = jest.fn();
|
6
|
+
|
7
|
+
describe('auth-utils', () => {
|
8
|
+
beforeEach(() => {
|
9
|
+
// Clear all cookies before each test
|
10
|
+
document.cookie.split(';').forEach(cookie => {
|
11
|
+
const eqPos = cookie.indexOf('=');
|
12
|
+
const name = eqPos > -1 ? cookie.substr(0, eqPos) : cookie;
|
13
|
+
document.cookie = `${name}=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/`;
|
14
|
+
});
|
15
|
+
});
|
16
|
+
|
17
|
+
describe('getCookieValue', () => {
|
18
|
+
it('should return the value of the cookie if it exists', () => {
|
19
|
+
document.cookie = 'testCookie=testValue';
|
20
|
+
expect(getCookieValue('testCookie')).toBe('testValue');
|
21
|
+
});
|
22
|
+
|
23
|
+
it('should return null if the cookie does not exist', () => {
|
24
|
+
expect(getCookieValue('nonExistentCookie')).toBeNull();
|
25
|
+
});
|
26
|
+
});
|
27
|
+
|
28
|
+
describe('setCookieValue', () => {
|
29
|
+
it('should set the cookie with the given name and value', () => {
|
30
|
+
setCookieValue('testCookie', 'testValue');
|
31
|
+
expect(document.cookie).toContain('testCookie=testValue');
|
32
|
+
});
|
33
|
+
});
|
34
|
+
|
35
|
+
describe('logout', () => {
|
36
|
+
it('should clear authToken and refreshToken cookies', () => {
|
37
|
+
document.cookie = 'authToken=testAuthToken';
|
38
|
+
document.cookie = 'refreshToken=testRefreshToken';
|
39
|
+
logout();
|
40
|
+
expect(getCookieValue('authToken')).toBeNull();
|
41
|
+
expect(getCookieValue('refreshToken')).toBeNull();
|
42
|
+
});
|
43
|
+
});
|
44
|
+
|
45
|
+
describe('refreshAccessToken', () => {
|
46
|
+
const authConfig: AuthenticationConfiguration = {
|
47
|
+
authUrl: 'https://auth.example.com',
|
48
|
+
clientId: 'example-client-id',
|
49
|
+
redirectUrl: 'https://example.com/callback',
|
50
|
+
scope: 'offline_access'
|
51
|
+
};
|
52
|
+
|
53
|
+
it('should throw an error if authConfig is not provided', async () => {
|
54
|
+
await expect(refreshAccessToken()).rejects.toThrow('No auth configuration available');
|
55
|
+
});
|
56
|
+
|
57
|
+
it('should throw an error if refreshToken is not available', async () => {
|
58
|
+
await expect(refreshAccessToken(authConfig)).rejects.toThrow('You are not logged in');
|
59
|
+
});
|
60
|
+
|
61
|
+
it('should refresh the access token and set the authToken cookie', async () => {
|
62
|
+
document.cookie = 'refreshToken=testRefreshToken';
|
63
|
+
|
64
|
+
// Mock fetch response
|
65
|
+
(global.fetch as jest.Mock).mockResolvedValueOnce({
|
66
|
+
ok: true,
|
67
|
+
json: async () => ({ access_token: 'newAccessToken' }),
|
68
|
+
});
|
69
|
+
|
70
|
+
const newToken = await refreshAccessToken(authConfig);
|
71
|
+
expect(newToken).toBe('newAccessToken');
|
72
|
+
expect(getCookieValue('authToken')).toBe('newAccessToken');
|
73
|
+
});
|
74
|
+
|
75
|
+
it('should call logout and throw an error if the fetch response is not ok', async () => {
|
76
|
+
document.cookie = 'refreshToken=testRefreshToken';
|
77
|
+
|
78
|
+
// Mock fetch response
|
79
|
+
(global.fetch as jest.Mock).mockResolvedValueOnce({
|
80
|
+
ok: false,
|
81
|
+
});
|
82
|
+
|
83
|
+
await expect(refreshAccessToken(authConfig)).rejects.toThrow('Failed to refresh token');
|
84
|
+
expect(getCookieValue('authToken')).toBeNull();
|
85
|
+
expect(getCookieValue('refreshToken')).toBeNull();
|
86
|
+
});
|
87
|
+
});
|
88
|
+
});
|
@@ -0,0 +1,40 @@
|
|
1
|
+
import { AuthenticationConfiguration } from '../types';
|
2
|
+
|
3
|
+
export const getCookieValue = (name: string): string | null => {
|
4
|
+
const match = document.cookie.match('(^|;)\\s*' + name + '\\s*=\\s*([^;]+)');
|
5
|
+
return match ? match.pop()! : null;
|
6
|
+
};
|
7
|
+
|
8
|
+
export const setCookieValue = (name: string, value: string): void => {
|
9
|
+
document.cookie = `${name}=${value}; Path=/;`;
|
10
|
+
};
|
11
|
+
|
12
|
+
export const logout = (): void => {
|
13
|
+
setCookieValue('authToken', '');
|
14
|
+
setCookieValue('refreshToken', '');
|
15
|
+
};
|
16
|
+
|
17
|
+
export const refreshAccessToken = async (authConfig?: AuthenticationConfiguration): Promise<string> => {
|
18
|
+
if (!authConfig) {
|
19
|
+
throw new Error('No auth configuration available');
|
20
|
+
}
|
21
|
+
|
22
|
+
const refreshToken = getCookieValue('refreshToken');
|
23
|
+
if (!refreshToken) {
|
24
|
+
throw new Error('You are not logged in');
|
25
|
+
}
|
26
|
+
|
27
|
+
const response = await fetch(`${authConfig.redirectUrl}?grant_type=refresh_token&refresh_token=${refreshToken}`, {
|
28
|
+
method: 'GET',
|
29
|
+
credentials: 'include',
|
30
|
+
});
|
31
|
+
|
32
|
+
if (!response.ok) {
|
33
|
+
logout();
|
34
|
+
throw new Error('Failed to refresh token');
|
35
|
+
}
|
36
|
+
|
37
|
+
const data = await response.json();
|
38
|
+
setCookieValue('authToken', data.access_token);
|
39
|
+
return data.access_token;
|
40
|
+
};
|