From 585f75bc910456b31af7eeeb05b93a6b9bc75d39 Mon Sep 17 00:00:00 2001
From: Steve Faulkner <471400+southpolesteve@users.noreply.github.com>
Date: Wed, 30 Dec 2020 19:12:22 -0600
Subject: [PATCH] Checkpoint
---
.eslintignore | 1 -
.../AccountSwitch/AccountSwitchComponent.tsx | 3 +-
src/HostedExplorer.tsx | 49 +++++++---
src/Main.tsx | 36 +------
.../Helpers/ConnectionStringParser.test.ts | 20 ++--
.../Hosted/Helpers/ConnectionStringParser.ts | 78 ++++++++-------
src/Platform/Hosted/Main.ts | 94 +++----------------
src/contexts/authContext.tsx | 47 ++++------
src/hooks/usePortalAccessToken.tsx | 36 +++++++
9 files changed, 156 insertions(+), 208 deletions(-)
create mode 100644 src/hooks/usePortalAccessToken.tsx
diff --git a/.eslintignore b/.eslintignore
index a0943ff18..8d665326b 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -241,7 +241,6 @@ src/Platform/Hosted/Authorization.ts
src/Platform/Hosted/DataAccessUtility.ts
src/Platform/Hosted/ExplorerFactory.ts
src/Platform/Hosted/Helpers/ConnectionStringParser.test.ts
-src/Platform/Hosted/Helpers/ConnectionStringParser.ts
src/Platform/Hosted/HostedUtils.test.ts
src/Platform/Hosted/HostedUtils.ts
src/Platform/Hosted/Main.ts
diff --git a/src/Explorer/Controls/AccountSwitch/AccountSwitchComponent.tsx b/src/Explorer/Controls/AccountSwitch/AccountSwitchComponent.tsx
index 5af0e352a..b2ff5be6e 100644
--- a/src/Explorer/Controls/AccountSwitch/AccountSwitchComponent.tsx
+++ b/src/Explorer/Controls/AccountSwitch/AccountSwitchComponent.tsx
@@ -1,10 +1,9 @@
import { StyleConstants } from "../../../Common/Constants";
-import { DatabaseAccount, Subscription } from "../../../Contracts/DataModels";
import * as React from "react";
import { DefaultButton, IButtonStyles, IButtonProps } from "office-ui-fabric-react/lib/Button";
import { IContextualMenuProps } from "office-ui-fabric-react/lib/ContextualMenu";
-import { Dropdown, IDropdownOption, IDropdownProps } from "office-ui-fabric-react/lib/Dropdown";
+import { Dropdown, IDropdownProps } from "office-ui-fabric-react/lib/Dropdown";
import { useSubscriptions } from "../../../hooks/useSubscriptions";
import { useDatabaseAccounts } from "../../../hooks/useDatabaseAccounts";
diff --git a/src/HostedExplorer.tsx b/src/HostedExplorer.tsx
index 0ee8b82a1..270cba4fa 100644
--- a/src/HostedExplorer.tsx
+++ b/src/HostedExplorer.tsx
@@ -14,7 +14,6 @@ import * as React from "react";
import { render } from "react-dom";
import FeedbackIcon from "../images/Feedback.svg";
import ConnectIcon from "../images/HostedConnectwhite.svg";
-import ChevronRight from "../images/chevron-right.svg";
import "../less/hostedexplorer.less";
import { CommandButtonComponent } from "./Explorer/Controls/CommandButton/CommandButtonComponent";
import "./Explorer/Menus/NavBar/MeControlComponent.less";
@@ -22,12 +21,17 @@ import { useGraphPhoto } from "./hooks/useGraphPhoto";
import "./Shared/appInsights";
import { AccountSwitchComponent } from "./Explorer/Controls/AccountSwitch/AccountSwitchComponent";
import { AuthContext, AuthProvider } from "./contexts/authContext";
+import { usePortalAccessToken } from "./hooks/usePortalAccessToken";
+import { AuthType } from "./AuthType";
initializeIcons();
const App: React.FunctionComponent = () => {
+ const params = new URLSearchParams(window.location.search);
+ const encryptedToken = params && params.get("key");
+ const encryptedTokenMetadata = usePortalAccessToken(encryptedToken);
const [isOpen, { setTrue: openPanel, setFalse: dismissPanel }] = useBoolean(false);
- const { isLoggedIn, login, account, logout } = React.useContext(AuthContext);
+ const { isLoggedIn, aadlogin: login, account, aadlogout: logout } = React.useContext(AuthContext);
const [isConnectionStringVisible, { setTrue: showConnectionString }] = useBoolean(false);
const photo = useGraphPhoto();
@@ -121,11 +125,17 @@ const App: React.FunctionComponent = () => {
Microsoft Azure
Cosmos DB
- {isLoggedIn &&
}
+ {(isLoggedIn || encryptedTokenMetadata?.accountName) && (
+
+ )}
{isLoggedIn && (
- REPLACE ME - Connection string mode;
+
+ )}
+ {!isLoggedIn && encryptedTokenMetadata?.accountName && (
+
+ {encryptedTokenMetadata?.accountName}
)}
@@ -186,9 +196,27 @@ const App: React.FunctionComponent = () => {
- {isLoggedIn ? (
-
LOGGED IN!
- ) : (
+ {encryptedTokenMetadata && !isLoggedIn && (
+
+ )}
+ {!encryptedTokenMetadata && isLoggedIn && (
+
+ )}
+ {!isLoggedIn && !encryptedTokenMetadata && (
@@ -227,13 +255,6 @@ const App: React.FunctionComponent = () => {
)}
- {/* */}
diff --git a/src/Main.tsx b/src/Main.tsx
index 442a42ebf..9c20b4671 100644
--- a/src/Main.tsx
+++ b/src/Main.tsx
@@ -84,42 +84,16 @@ window.authType = AuthType.AAD;
const App: React.FunctionComponent = () => {
useEffect(() => {
initializeConfiguration().then(config => {
+ let explorer: Explorer;
if (config.platform === Platform.Hosted) {
- try {
- Hosted.initializeExplorer().then(
- (explorer: Explorer) => {
- applyExplorerBindings(explorer);
- Hosted.configureTokenValidationDisplayPrompt(explorer);
- },
- (error: unknown) => {
- try {
- const uninitializedExplorer: Explorer = Hosted.getUninitializedExplorerForGuestAccess();
- window.dataExplorer = uninitializedExplorer;
- ko.applyBindings(uninitializedExplorer);
- BindingHandlersRegisterer.registerBindingHandlers();
- if (window.authType !== AuthType.AAD) {
- uninitializedExplorer.isRefreshingExplorer(false);
- uninitializedExplorer.displayConnectExplorerForm();
- }
- } catch (e) {
- console.log(e);
- }
- console.error(error);
- }
- );
- } catch (e) {
- console.log(e);
- }
+ explorer = Hosted.initializeExplorer();
} else if (config.platform === Platform.Emulator) {
window.authType = AuthType.MasterKey;
- const explorer = Emulator.initializeExplorer();
- applyExplorerBindings(explorer);
+ explorer = Emulator.initializeExplorer();
} else if (config.platform === Platform.Portal) {
- TelemetryProcessor.trace(Action.InitializeDataExplorer, ActionModifiers.Open, {});
- const explorer = Portal.initializeExplorer();
- TelemetryProcessor.trace(Action.InitializeDataExplorer, ActionModifiers.IFrameReady, {});
- applyExplorerBindings(explorer);
+ explorer = Portal.initializeExplorer();
}
+ applyExplorerBindings(explorer);
});
}, []);
diff --git a/src/Platform/Hosted/Helpers/ConnectionStringParser.test.ts b/src/Platform/Hosted/Helpers/ConnectionStringParser.test.ts
index 0291396d1..da534c045 100644
--- a/src/Platform/Hosted/Helpers/ConnectionStringParser.test.ts
+++ b/src/Platform/Hosted/Helpers/ConnectionStringParser.test.ts
@@ -1,12 +1,12 @@
import * as DataModels from "../../../Contracts/DataModels";
-import { ConnectionStringParser } from "./ConnectionStringParser";
+import { parseConnectionString } from "./ConnectionStringParser";
describe("ConnectionStringParser", () => {
const mockAccountName: string = "Test";
const mockMasterKey: string = "some-key";
it("should parse a valid sql account connection string", () => {
- const metadata: DataModels.AccessInputMetadata = ConnectionStringParser.parseConnectionString(
+ const metadata = parseConnectionString(
`AccountEndpoint=https://${mockAccountName}.documents.azure.com:443/;AccountKey=${mockMasterKey};`
);
@@ -15,7 +15,7 @@ describe("ConnectionStringParser", () => {
});
it("should parse a valid mongo account connection string", () => {
- const metadata: DataModels.AccessInputMetadata = ConnectionStringParser.parseConnectionString(
+ const metadata = parseConnectionString(
`mongodb://${mockAccountName}:${mockMasterKey}@${mockAccountName}.documents.azure.com:10255`
);
@@ -24,7 +24,7 @@ describe("ConnectionStringParser", () => {
});
it("should parse a valid compute mongo account connection string", () => {
- const metadata: DataModels.AccessInputMetadata = ConnectionStringParser.parseConnectionString(
+ const metadata = parseConnectionString(
`mongodb://${mockAccountName}:${mockMasterKey}@${mockAccountName}.mongo.cosmos.azure.com:10255`
);
@@ -33,7 +33,7 @@ describe("ConnectionStringParser", () => {
});
it("should parse a valid graph account connection string", () => {
- const metadata: DataModels.AccessInputMetadata = ConnectionStringParser.parseConnectionString(
+ const metadata = parseConnectionString(
`AccountEndpoint=https://${mockAccountName}.documents.azure.com:443/;AccountKey=${mockMasterKey};ApiKind=Gremlin;`
);
@@ -42,7 +42,7 @@ describe("ConnectionStringParser", () => {
});
it("should parse a valid table account connection string", () => {
- const metadata: DataModels.AccessInputMetadata = ConnectionStringParser.parseConnectionString(
+ const metadata = parseConnectionString(
`DefaultEndpointsProtocol=https;AccountName=${mockAccountName};AccountKey=${mockMasterKey};TableEndpoint=https://${mockAccountName}.table.cosmosdb.azure.com:443/;`
);
@@ -51,7 +51,7 @@ describe("ConnectionStringParser", () => {
});
it("should parse a valid cassandra account connection string", () => {
- const metadata: DataModels.AccessInputMetadata = ConnectionStringParser.parseConnectionString(
+ const metadata = parseConnectionString(
`AccountEndpoint=${mockAccountName}.cassandra.cosmosdb.azure.com;AccountKey=${mockMasterKey};`
);
@@ -60,15 +60,13 @@ describe("ConnectionStringParser", () => {
});
it("should fail to parse an invalid connection string", () => {
- const metadata: DataModels.AccessInputMetadata = ConnectionStringParser.parseConnectionString(
- "some-rogue-connection-string"
- );
+ const metadata = parseConnectionString("some-rogue-connection-string");
expect(metadata).toBe(undefined);
});
it("should fail to parse an empty connection string", () => {
- const metadata: DataModels.AccessInputMetadata = ConnectionStringParser.parseConnectionString("");
+ const metadata = parseConnectionString("");
expect(metadata).toBe(undefined);
});
diff --git a/src/Platform/Hosted/Helpers/ConnectionStringParser.ts b/src/Platform/Hosted/Helpers/ConnectionStringParser.ts
index 423a85f09..861f724d1 100644
--- a/src/Platform/Hosted/Helpers/ConnectionStringParser.ts
+++ b/src/Platform/Hosted/Helpers/ConnectionStringParser.ts
@@ -1,50 +1,48 @@
import * as Constants from "../../../Common/Constants";
-import * as DataModels from "../../../Contracts/DataModels";
+import { AccessInputMetadata, ApiKind } from "../../../Contracts/DataModels";
-export class ConnectionStringParser {
- public static parseConnectionString(connectionString: string): DataModels.AccessInputMetadata {
- if (!!connectionString) {
- try {
- const accessInput: DataModels.AccessInputMetadata = {} as DataModels.AccessInputMetadata;
- const connectionStringParts = connectionString.split(";");
+export function parseConnectionString(connectionString: string): AccessInputMetadata {
+ if (connectionString) {
+ try {
+ const accessInput = {} as AccessInputMetadata;
+ const connectionStringParts = connectionString.split(";");
- connectionStringParts.forEach((connectionStringPart: string) => {
- if (RegExp(Constants.EndpointsRegex.sql).test(connectionStringPart)) {
- accessInput.accountName = connectionStringPart.match(Constants.EndpointsRegex.sql)[1];
- accessInput.apiKind = DataModels.ApiKind.SQL;
- } else if (RegExp(Constants.EndpointsRegex.mongo).test(connectionStringPart)) {
- const matches: string[] = connectionStringPart.match(Constants.EndpointsRegex.mongo);
- accessInput.accountName = matches && matches.length > 1 && matches[2];
- accessInput.apiKind = DataModels.ApiKind.MongoDB;
- } else if (RegExp(Constants.EndpointsRegex.mongoCompute).test(connectionStringPart)) {
- const matches: string[] = connectionStringPart.match(Constants.EndpointsRegex.mongoCompute);
- accessInput.accountName = matches && matches.length > 1 && matches[2];
- accessInput.apiKind = DataModels.ApiKind.MongoDBCompute;
- } else if (Constants.EndpointsRegex.cassandra.some(regex => RegExp(regex).test(connectionStringPart))) {
- Constants.EndpointsRegex.cassandra.forEach(regex => {
- if (RegExp(regex).test(connectionStringPart)) {
- accessInput.accountName = connectionStringPart.match(regex)[1];
- accessInput.apiKind = DataModels.ApiKind.Cassandra;
- }
- });
- } else if (RegExp(Constants.EndpointsRegex.table).test(connectionStringPart)) {
- accessInput.accountName = connectionStringPart.match(Constants.EndpointsRegex.table)[1];
- accessInput.apiKind = DataModels.ApiKind.Table;
- } else if (connectionStringPart.indexOf("ApiKind=Gremlin") >= 0) {
- accessInput.apiKind = DataModels.ApiKind.Graph;
- }
- });
-
- if (Object.keys(accessInput).length === 0) {
- return undefined;
+ connectionStringParts.forEach((connectionStringPart: string) => {
+ if (RegExp(Constants.EndpointsRegex.sql).test(connectionStringPart)) {
+ accessInput.accountName = connectionStringPart.match(Constants.EndpointsRegex.sql)[1];
+ accessInput.apiKind = ApiKind.SQL;
+ } else if (RegExp(Constants.EndpointsRegex.mongo).test(connectionStringPart)) {
+ const matches: string[] = connectionStringPart.match(Constants.EndpointsRegex.mongo);
+ accessInput.accountName = matches && matches.length > 1 && matches[2];
+ accessInput.apiKind = ApiKind.MongoDB;
+ } else if (RegExp(Constants.EndpointsRegex.mongoCompute).test(connectionStringPart)) {
+ const matches: string[] = connectionStringPart.match(Constants.EndpointsRegex.mongoCompute);
+ accessInput.accountName = matches && matches.length > 1 && matches[2];
+ accessInput.apiKind = ApiKind.MongoDBCompute;
+ } else if (Constants.EndpointsRegex.cassandra.some(regex => RegExp(regex).test(connectionStringPart))) {
+ Constants.EndpointsRegex.cassandra.forEach(regex => {
+ if (RegExp(regex).test(connectionStringPart)) {
+ accessInput.accountName = connectionStringPart.match(regex)[1];
+ accessInput.apiKind = ApiKind.Cassandra;
+ }
+ });
+ } else if (RegExp(Constants.EndpointsRegex.table).test(connectionStringPart)) {
+ accessInput.accountName = connectionStringPart.match(Constants.EndpointsRegex.table)[1];
+ accessInput.apiKind = ApiKind.Table;
+ } else if (connectionStringPart.indexOf("ApiKind=Gremlin") >= 0) {
+ accessInput.apiKind = ApiKind.Graph;
}
+ });
- return accessInput;
- } catch (error) {
+ if (Object.keys(accessInput).length === 0) {
return undefined;
}
- }
- return undefined;
+ return accessInput;
+ } catch (error) {
+ return undefined;
+ }
}
+
+ return undefined;
}
diff --git a/src/Platform/Hosted/Main.ts b/src/Platform/Hosted/Main.ts
index 672efba95..b1c6859fa 100644
--- a/src/Platform/Hosted/Main.ts
+++ b/src/Platform/Hosted/Main.ts
@@ -43,84 +43,40 @@ export default class Main {
return false;
}
- public static initializeExplorer(): Q.Promise {
+ public static initializeExplorer(): Explorer {
window.addEventListener("message", this._handleMessage.bind(this), false);
this._features = {};
- const params = new URLSearchParams(window.parent.location.search);
- const deferred: Q.Deferred = Q.defer();
- let authType: string = null;
-
- // Encrypted token flow
- if (!!params && params.has("key")) {
- Main._encryptedToken = encodeURIComponent(params.get("key"));
- SessionStorageUtility.setEntryString(StorageKey.EncryptedKeyToken, Main._encryptedToken);
- authType = AuthType.EncryptedToken;
- } else if (Main._hasCachedEncryptedKey()) {
- Main._encryptedToken = SessionStorageUtility.getEntryString(StorageKey.EncryptedKeyToken);
- authType = AuthType.EncryptedToken;
- }
-
- // Aad flow
- if (AuthHeadersUtil.isUserSignedIn()) {
- authType = AuthType.AAD;
- }
+ const params = new URLSearchParams(window.location.search);
+ let authType: string = params && params.get("authType");
if (params) {
this._features = Main.extractFeatures(params);
}
+ // Encrypted token flow
+ if (params && params.has("key")) {
+ Main._encryptedToken = encodeURIComponent(params.get("key"));
+ Main._accessInputMetadata = JSON.parse(params.get("metadata"));
+ authType = AuthType.EncryptedToken;
+ }
+
(window).authType = authType;
if (!authType) {
- return Q.reject("Sign in needed");
+ throw new Error("Sign in needed");
}
const explorer: Explorer = this._instantiateExplorer();
if (authType === AuthType.EncryptedToken) {
- sendMessage({
- type: MessageTypes.UpdateAccountSwitch,
- props: {
- authType: AuthType.EncryptedToken,
- displayText: "Loading..."
- }
- });
updateUserContext({
accessToken: Main._encryptedToken
});
- Main._getAccessInputMetadata(Main._encryptedToken).then(
- () => {
- const expiryTimestamp: number =
- Main._accessInputMetadata && parseInt(Main._accessInputMetadata.expiryTimestamp);
- if (authType === AuthType.EncryptedToken && (isNaN(expiryTimestamp) || expiryTimestamp <= 0)) {
- return deferred.reject("Token expired");
- }
-
- Main._initDataExplorerFrameInputs(explorer);
- deferred.resolve(explorer);
- },
- (error: any) => {
- console.error(error);
- deferred.reject(error);
- }
- );
+ Main._initDataExplorerFrameInputs(explorer);
} else if (authType === AuthType.AAD) {
- sendMessage({
- type: MessageTypes.GetAccessAadRequest
- });
- if (this._getAadAccessDeferred != null) {
- // already request aad access, don't duplicate
- return Q(null);
- }
this._explorer = explorer;
- this._getAadAccessDeferred = Q.defer();
- return this._getAadAccessDeferred.promise.finally(() => {
- this._getAadAccessDeferred = null;
- });
} else {
Main._initDataExplorerFrameInputs(explorer);
- deferred.resolve(explorer);
}
-
- return deferred.promise;
+ return explorer;
}
public static extractFeatures(params: URLSearchParams): { [key: string]: string } {
@@ -137,21 +93,6 @@ export default class Main {
return features;
}
- public static configureTokenValidationDisplayPrompt(explorer: Explorer): void {
- const authType: AuthType = (window).authType;
- if (
- !explorer ||
- !Main._encryptedToken ||
- !Main._accessInputMetadata ||
- Main._accessInputMetadata.expiryTimestamp == null ||
- authType !== AuthType.EncryptedToken
- ) {
- return;
- }
-
- Main._showGuestAccessTokenRenewalPromptInMs(explorer, parseInt(Main._accessInputMetadata.expiryTimestamp));
- }
-
public static parseResourceTokenConnectionString(connectionString: string): resourceTokenConnectionStringProperties {
let accountEndpoint: string;
let collectionId: string;
@@ -234,7 +175,6 @@ export default class Main {
}
const masterKey: string = Main._getMasterKeyFromConnectionString(connectionString);
- Main.configureTokenValidationDisplayPrompt(explorer);
Main._setExplorerReady(explorer, masterKey);
deferred.resolve();
@@ -398,14 +338,6 @@ export default class Main {
return explorer;
}
- private static _showGuestAccessTokenRenewalPromptInMs(explorer: Explorer, interval: number): void {
- if (interval != null && !isNaN(interval)) {
- setTimeout(() => {
- explorer.displayGuestAccessTokenRenewalPrompt();
- }, interval);
- }
- }
-
private static _hasCachedEncryptedKey(): boolean {
return SessionStorageUtility.hasItem(StorageKey.EncryptedKeyToken);
}
diff --git a/src/contexts/authContext.tsx b/src/contexts/authContext.tsx
index c792b9eef..a8e1a07b1 100644
--- a/src/contexts/authContext.tsx
+++ b/src/contexts/authContext.tsx
@@ -9,7 +9,7 @@ const msal = new Msal.UserAgentApplication({
auth: {
authority: "https://login.microsoft.com/common",
clientId: "203f1145-856a-4232-83d4-a43568fba23d",
- redirectUri: "https://dataexplorer-dev.azurewebsites.net"
+ redirectUri: "https://dataexplorer-dev.azurewebsites.net" // TODO! This should only be set development
}
});
@@ -18,16 +18,16 @@ interface AuthContext {
account?: Msal.Account;
graphToken?: string;
armToken?: string;
- logout: () => unknown;
- login: () => unknown;
+ aadlogout: () => unknown;
+ aadlogin: () => unknown;
}
export const AuthContext = createContext({
isLoggedIn: false,
- login: () => {
+ aadlogin: () => {
throw Error(defaultError);
},
- logout: () => {
+ aadlogout: () => {
throw Error(defaultError);
}
});
@@ -38,36 +38,27 @@ export const AuthProvider: React.FunctionComponent = ({ children }) => {
const [graphToken, setGraphToken] = useState();
const [armToken, setArmToken] = useState();
- const login = useCallback(() => {
- msal.loginPopup().then(response => {
- setLoggedIn();
- setAccount(response.account);
- msal
- .acquireTokenSilent({ scopes: ["https://graph.windows.net//.default"] })
- .then(resp => {
- setGraphToken(resp.accessToken);
- })
- .catch(e => {
- console.error(e);
- });
- msal
- .acquireTokenSilent({ scopes: ["https://management.azure.com//.default"] })
- .then(resp => {
- setArmToken(resp.accessToken);
- })
- .catch(e => {
- console.error(e);
- });
- });
+ const aadlogin = useCallback(async () => {
+ const response = await msal.loginPopup();
+ setLoggedIn();
+ setAccount(response.account);
+
+ const [graphTokenResponse, armTokenResponse] = await Promise.all([
+ msal.acquireTokenSilent({ scopes: ["https://graph.windows.net//.default"] }),
+ msal.acquireTokenSilent({ scopes: ["https://management.azure.com//.default"] })
+ ]);
+
+ setGraphToken(graphTokenResponse.accessToken);
+ setArmToken(armTokenResponse.accessToken);
}, []);
- const logout = useCallback(() => {
+ const aadlogout = useCallback(() => {
msal.logout();
setLoggedOut();
}, []);
return (
-
+
{children}
);
diff --git a/src/hooks/usePortalAccessToken.tsx b/src/hooks/usePortalAccessToken.tsx
new file mode 100644
index 000000000..994192031
--- /dev/null
+++ b/src/hooks/usePortalAccessToken.tsx
@@ -0,0 +1,36 @@
+import { useEffect, useState } from "react";
+import { ApiEndpoints } from "../Common/Constants";
+import { configContext } from "../ConfigContext";
+import { AccessInputMetadata } from "../Contracts/DataModels";
+
+const url = `${configContext.BACKEND_ENDPOINT}${ApiEndpoints.guestRuntimeProxy}/accessinputmetadata?_=1609359229955`;
+
+export async function fetchAccessData(portalToken: string): Promise {
+ const headers = new Headers();
+ // Portal encrypted token API quirk: The token header must be URL encoded
+ headers.append("x-ms-encrypted-auth-token", encodeURIComponent(portalToken));
+
+ const options = {
+ method: "GET",
+ headers: headers
+ };
+
+ return (
+ fetch(url, options)
+ .then(response => response.json())
+ // Portal encrypted token API quirk: The response is double JSON encoded
+ .then(json => JSON.parse(json))
+ .catch(error => console.log(error))
+ );
+}
+
+export function usePortalAccessToken(token: string): AccessInputMetadata {
+ const [state, setState] = useState();
+
+ useEffect(() => {
+ if (token) {
+ fetchAccessData(token).then(response => setState(response));
+ }
+ }, [token]);
+ return state;
+}