mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-09-19 17:12:47 +01:00
Move Portal Backend calls into a PortalBackendClient helper
fetchEncryptedToken and isAccountRestrictedForConnectionStringLogin lived in ConnectExplorer.tsx, so HostedExplorer imported a network call from a React component. Merge them with fetchAccessData into src/Platform/Hosted/Helpers/PortalBackendClient.ts along with the PortalBackendError type, leaving ConnectExplorer as UI only.
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
jest.mock("./hooks/useAADAuth");
|
jest.mock("./hooks/useAADAuth");
|
||||||
jest.mock("./hooks/useConfig");
|
jest.mock("./hooks/useConfig");
|
||||||
jest.mock("./Platform/Hosted/Helpers/PortalAccessData");
|
jest.mock("./Platform/Hosted/Helpers/PortalBackendClient");
|
||||||
jest.mock("./Platform/Hosted/Components/ConnectExplorer");
|
jest.mock("./Platform/Hosted/Components/ConnectExplorer");
|
||||||
jest.mock("./Shared/appInsights");
|
jest.mock("./Shared/appInsights");
|
||||||
jest.mock("./Platform/Hosted/Components/AccountSwitcher", () => ({
|
jest.mock("./Platform/Hosted/Components/AccountSwitcher", () => ({
|
||||||
@@ -28,8 +28,8 @@ import React from "react";
|
|||||||
import { useAADAuth } from "./hooks/useAADAuth";
|
import { useAADAuth } from "./hooks/useAADAuth";
|
||||||
import { useConfig } from "./hooks/useConfig";
|
import { useConfig } from "./hooks/useConfig";
|
||||||
import { App } from "./HostedExplorer";
|
import { App } from "./HostedExplorer";
|
||||||
import { ConnectExplorer, fetchEncryptedToken } from "./Platform/Hosted/Components/ConnectExplorer";
|
import { ConnectExplorer } from "./Platform/Hosted/Components/ConnectExplorer";
|
||||||
import { fetchAccessData } from "./Platform/Hosted/Helpers/PortalAccessData";
|
import { fetchAccessData, fetchEncryptedToken } from "./Platform/Hosted/Helpers/PortalBackendClient";
|
||||||
|
|
||||||
const mockFetchEncryptedToken = fetchEncryptedToken as jest.MockedFunction<typeof fetchEncryptedToken>;
|
const mockFetchEncryptedToken = fetchEncryptedToken as jest.MockedFunction<typeof fetchEncryptedToken>;
|
||||||
|
|
||||||
|
|||||||
@@ -12,16 +12,19 @@ import { AccessInputMetadata, DatabaseAccount } from "./Contracts/DataModels";
|
|||||||
import "./Explorer/Menus/NavBar/MeControlComponent.less";
|
import "./Explorer/Menus/NavBar/MeControlComponent.less";
|
||||||
import { HostedExplorerChildFrame } from "./HostedExplorerChildFrame";
|
import { HostedExplorerChildFrame } from "./HostedExplorerChildFrame";
|
||||||
import { AccountSwitcher } from "./Platform/Hosted/Components/AccountSwitcher";
|
import { AccountSwitcher } from "./Platform/Hosted/Components/AccountSwitcher";
|
||||||
import { ConnectExplorer, fetchEncryptedToken } from "./Platform/Hosted/Components/ConnectExplorer";
|
import { ConnectExplorer } from "./Platform/Hosted/Components/ConnectExplorer";
|
||||||
import { DirectoryPickerPanel } from "./Platform/Hosted/Components/DirectoryPickerPanel";
|
import { DirectoryPickerPanel } from "./Platform/Hosted/Components/DirectoryPickerPanel";
|
||||||
import { FeedbackCommandButton } from "./Platform/Hosted/Components/FeedbackCommandButton";
|
import { FeedbackCommandButton } from "./Platform/Hosted/Components/FeedbackCommandButton";
|
||||||
import { MeControl } from "./Platform/Hosted/Components/MeControl";
|
import { MeControl } from "./Platform/Hosted/Components/MeControl";
|
||||||
import { SignInButton } from "./Platform/Hosted/Components/SignInButton";
|
import { SignInButton } from "./Platform/Hosted/Components/SignInButton";
|
||||||
import "./Platform/Hosted/ConnectScreen.less";
|
import "./Platform/Hosted/ConnectScreen.less";
|
||||||
import { parseConnectionString } from "./Platform/Hosted/Helpers/ConnectionStringParser";
|
import { parseConnectionString } from "./Platform/Hosted/Helpers/ConnectionStringParser";
|
||||||
import { fetchAccessData } from "./Platform/Hosted/Helpers/PortalAccessData";
|
import { fetchAccessData, fetchEncryptedToken } from "./Platform/Hosted/Helpers/PortalBackendClient";
|
||||||
import { isResourceTokenConnectionString } from "./Platform/Hosted/Helpers/ResourceTokenUtils";
|
import { isResourceTokenConnectionString } from "./Platform/Hosted/Helpers/ResourceTokenUtils";
|
||||||
import { extractMasterKeyfromConnectionString, isDirectConnectionStringLoginApi } from "./Platform/Hosted/HostedUtils";
|
import {
|
||||||
|
extractMasterKeyFromDirectLoginConnectionString,
|
||||||
|
isDirectConnectionStringLoginApi,
|
||||||
|
} from "./Platform/Hosted/HostedUtils";
|
||||||
import "./Shared/appInsights";
|
import "./Shared/appInsights";
|
||||||
import { allowedHostedExplorerEndpoints } from "./Utils/EndpointUtils";
|
import { allowedHostedExplorerEndpoints } from "./Utils/EndpointUtils";
|
||||||
import { useAADAuth } from "./hooks/useAADAuth";
|
import { useAADAuth } from "./hooks/useAADAuth";
|
||||||
@@ -140,7 +143,7 @@ const App: React.FunctionComponent = () => {
|
|||||||
authType: AuthType.ConnectionString,
|
authType: AuthType.ConnectionString,
|
||||||
encryptedToken,
|
encryptedToken,
|
||||||
encryptedTokenMetadata: accountMetadata,
|
encryptedTokenMetadata: accountMetadata,
|
||||||
masterKey: extractMasterKeyfromConnectionString(connectionString),
|
masterKey: extractMasterKeyFromDirectLoginConnectionString(connectionString),
|
||||||
};
|
};
|
||||||
} else if (authType === AuthType.ResourceToken) {
|
} else if (authType === AuthType.ResourceToken) {
|
||||||
frameWindow.hostedConfig = {
|
frameWindow.hostedConfig = {
|
||||||
|
|||||||
@@ -5,10 +5,14 @@ import * as React from "react";
|
|||||||
import ConnectImage from "../../../../images/HdeConnectCosmosDB.svg";
|
import ConnectImage from "../../../../images/HdeConnectCosmosDB.svg";
|
||||||
import ErrorImage from "../../../../images/error.svg";
|
import ErrorImage from "../../../../images/error.svg";
|
||||||
import { AuthType } from "../../../AuthType";
|
import { AuthType } from "../../../AuthType";
|
||||||
import { HttpHeaders } from "../../../Common/Constants";
|
|
||||||
import { configContext } from "../../../ConfigContext";
|
|
||||||
import { AccessInputMetadata } from "../../../Contracts/DataModels";
|
import { AccessInputMetadata } from "../../../Contracts/DataModels";
|
||||||
|
import { isAuthorizationError } from "../../../Utils/AuthorizationUtils";
|
||||||
import { parseConnectionString } from "../Helpers/ConnectionStringParser";
|
import { parseConnectionString } from "../Helpers/ConnectionStringParser";
|
||||||
|
import {
|
||||||
|
fetchEncryptedToken,
|
||||||
|
isAccountRestrictedForConnectionStringLogin,
|
||||||
|
PortalBackendError,
|
||||||
|
} from "../Helpers/PortalBackendClient";
|
||||||
import { isResourceTokenConnectionString } from "../Helpers/ResourceTokenUtils";
|
import { isResourceTokenConnectionString } from "../Helpers/ResourceTokenUtils";
|
||||||
import { isDirectConnectionStringLoginApi } from "../HostedUtils";
|
import { isDirectConnectionStringLoginApi } from "../HostedUtils";
|
||||||
|
|
||||||
@@ -21,36 +25,6 @@ interface Props {
|
|||||||
setAccountMetadata: (metadata: AccessInputMetadata) => void;
|
setAccountMetadata: (metadata: AccessInputMetadata) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Turns a failed Portal Backend response into an error that carries the message returned by the service.
|
|
||||||
const errorFromResponse = async (response: Response): Promise<Error> =>
|
|
||||||
new Error((await response.text()) || response.statusText);
|
|
||||||
|
|
||||||
export const fetchEncryptedToken = async (connectionString: string): Promise<string> => {
|
|
||||||
const headers = new Headers();
|
|
||||||
headers.append(HttpHeaders.connectionString, connectionString);
|
|
||||||
headers.append(HttpHeaders.authorization, connectionString);
|
|
||||||
const url = configContext.PORTAL_BACKEND_ENDPOINT + "/api/connectionstring/token/generatetoken";
|
|
||||||
const response = await fetch(url, { headers, method: "POST" });
|
|
||||||
if (!response.ok) {
|
|
||||||
throw await errorFromResponse(response);
|
|
||||||
}
|
|
||||||
|
|
||||||
const encryptedTokenResponse: string = await response.json();
|
|
||||||
return decodeURIComponent(encryptedTokenResponse);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const isAccountRestrictedForConnectionStringLogin = async (connectionString: string): Promise<boolean> => {
|
|
||||||
const headers = new Headers();
|
|
||||||
headers.append(HttpHeaders.connectionString, connectionString);
|
|
||||||
const url = configContext.PORTAL_BACKEND_ENDPOINT + "/api/guest/accountrestrictions/checkconnectionstringlogin";
|
|
||||||
const response = await fetch(url, { headers, method: "POST" });
|
|
||||||
if (!response.ok) {
|
|
||||||
throw await errorFromResponse(response);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (await response.text()).toLowerCase() === "true";
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ConnectExplorer: React.FunctionComponent<Props> = ({
|
export const ConnectExplorer: React.FunctionComponent<Props> = ({
|
||||||
setEncryptedToken,
|
setEncryptedToken,
|
||||||
login,
|
login,
|
||||||
@@ -109,7 +83,16 @@ export const ConnectExplorer: React.FunctionComponent<Props> = ({
|
|||||||
setEncryptedToken(encryptedToken);
|
setEncryptedToken(encryptedToken);
|
||||||
setAuthType(AuthType.ConnectionString);
|
setAuthType(AuthType.ConnectionString);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
// A 401 or 403 means the credentials or Portal Backend were not authorized.
|
||||||
|
// Any other failure is on the backend, so rather than block the login
|
||||||
|
// we sign in with the metadata parsed from the connection string.
|
||||||
|
if (!metadata || (error instanceof PortalBackendError && isAuthorizationError(error.statusCode))) {
|
||||||
setErrorMessage(getErrorMessage(error));
|
setErrorMessage(getErrorMessage(error));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setAccountMetadata(metadata);
|
||||||
|
setAuthType(AuthType.ConnectionString);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ export function parseConnectionString(connectionString: string): AccessInputMeta
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tables connection strings only carry the table endpoint, so the document endpoint that data
|
// Table connection strings only carry the table endpoint, so the document endpoint that data plane
|
||||||
// operations go through has to be derived from the account name. Gremlin accounts additionally
|
// operations go through has to be derived from the account name. Gremlin accounts additionally
|
||||||
// need the Gremlin endpoint, which is never part of the connection string.
|
// need the Gremlin endpoint, which is never part of the connection string.
|
||||||
if (accessInput.accountName) {
|
if (accessInput.accountName) {
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
import { HttpHeaders } from "../../../Common/Constants";
|
|
||||||
import { configContext } from "../../../ConfigContext";
|
|
||||||
import { AccessInputMetadata } from "../../../Contracts/DataModels";
|
|
||||||
|
|
||||||
export async function fetchAccessData(portalToken: string): Promise<AccessInputMetadata> {
|
|
||||||
const headers = new Headers();
|
|
||||||
// Portal encrypted token API quirk: The token header must be URL encoded
|
|
||||||
headers.append(HttpHeaders.guestAccessToken, encodeURIComponent(portalToken));
|
|
||||||
headers.append(HttpHeaders.authorization, encodeURIComponent(portalToken));
|
|
||||||
const url: string = `${configContext.PORTAL_BACKEND_ENDPOINT}/api/connectionstring/runtimeproxy/accessinputmetadata`;
|
|
||||||
const options = {
|
|
||||||
method: "GET",
|
|
||||||
headers: headers,
|
|
||||||
};
|
|
||||||
|
|
||||||
return fetch(url, options)
|
|
||||||
.then((response) => response.json())
|
|
||||||
.catch((error) => console.error(error));
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import { HttpHeaders } from "../../../Common/Constants";
|
||||||
|
import { configContext } from "../../../ConfigContext";
|
||||||
|
import { AccessInputMetadata } from "../../../Contracts/DataModels";
|
||||||
|
|
||||||
|
// A failed Portal Backend response. Carries the status so callers can tell a rejected connection string
|
||||||
|
// from a service failure.
|
||||||
|
export class PortalBackendError extends Error {
|
||||||
|
constructor(
|
||||||
|
message: string,
|
||||||
|
public readonly statusCode: number,
|
||||||
|
) {
|
||||||
|
super(message);
|
||||||
|
// Set the prototype explicitly so `instanceof` works.
|
||||||
|
// https://github.com/Microsoft/TypeScript/wiki/FAQ#why-doesnt-extending-built-ins-like-error-array-and-map-work
|
||||||
|
Object.setPrototypeOf(this, PortalBackendError.prototype);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async fromResponse(response: Response): Promise<PortalBackendError> {
|
||||||
|
return new PortalBackendError(await response.text(), response.status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchAccessData(portalToken: string): Promise<AccessInputMetadata> {
|
||||||
|
const headers = new Headers();
|
||||||
|
// Portal encrypted token API quirk: The token header must be URL encoded
|
||||||
|
headers.append(HttpHeaders.guestAccessToken, encodeURIComponent(portalToken));
|
||||||
|
headers.append(HttpHeaders.authorization, encodeURIComponent(portalToken));
|
||||||
|
const url: string = `${configContext.PORTAL_BACKEND_ENDPOINT}/api/connectionstring/runtimeproxy/accessinputmetadata`;
|
||||||
|
const options = {
|
||||||
|
method: "GET",
|
||||||
|
headers: headers,
|
||||||
|
};
|
||||||
|
|
||||||
|
return fetch(url, options)
|
||||||
|
.then((response) => response.json())
|
||||||
|
.catch((error) => console.error(error));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchEncryptedToken(connectionString: string): Promise<string> {
|
||||||
|
const headers = new Headers();
|
||||||
|
headers.append(HttpHeaders.connectionString, connectionString);
|
||||||
|
headers.append(HttpHeaders.authorization, connectionString);
|
||||||
|
const url = configContext.PORTAL_BACKEND_ENDPOINT + "/api/connectionstring/token/generatetoken";
|
||||||
|
const response = await fetch(url, { headers, method: "POST" });
|
||||||
|
if (!response.ok) {
|
||||||
|
throw await PortalBackendError.fromResponse(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
const encryptedTokenResponse: string = await response.json();
|
||||||
|
return decodeURIComponent(encryptedTokenResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function isAccountRestrictedForConnectionStringLogin(connectionString: string): Promise<boolean> {
|
||||||
|
const headers = new Headers();
|
||||||
|
headers.append(HttpHeaders.connectionString, connectionString);
|
||||||
|
const url = configContext.PORTAL_BACKEND_ENDPOINT + "/api/guest/accountrestrictions/checkconnectionstringlogin";
|
||||||
|
const response = await fetch(url, { headers, method: "POST" });
|
||||||
|
if (!response.ok) {
|
||||||
|
throw await PortalBackendError.fromResponse(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (await response.text()).toLowerCase() === "true";
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { AccessInputMetadata, ApiKind } from "../../Contracts/DataModels";
|
import { AccessInputMetadata, ApiKind } from "../../Contracts/DataModels";
|
||||||
import {
|
import {
|
||||||
extractMasterKeyfromConnectionString,
|
extractMasterKeyFromDirectLoginConnectionString,
|
||||||
getDatabaseAccountPropertiesFromMetadata,
|
getDatabaseAccountPropertiesFromMetadata,
|
||||||
isDirectConnectionStringLoginApi,
|
isDirectConnectionStringLoginApi,
|
||||||
} from "./HostedUtils";
|
} from "./HostedUtils";
|
||||||
@@ -35,13 +35,13 @@ describe("getDatabaseAccountPropertiesFromMetadata", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("extractMasterKeyfromConnectionString", () => {
|
describe("extractMasterKeyFromDirectLoginConnectionString", () => {
|
||||||
const mockAccountName = "Test";
|
const mockAccountName = "Test";
|
||||||
const mockKey = "abc123+/=someKey==";
|
const mockKey = "abc123+/=someKey==";
|
||||||
|
|
||||||
it("extracts the account key from a SQL connection string", () => {
|
it("extracts the account key from a SQL connection string", () => {
|
||||||
expect(
|
expect(
|
||||||
extractMasterKeyfromConnectionString(
|
extractMasterKeyFromDirectLoginConnectionString(
|
||||||
`AccountEndpoint=https://${mockAccountName}.documents.azure.com:443/;AccountKey=${mockKey};`,
|
`AccountEndpoint=https://${mockAccountName}.documents.azure.com:443/;AccountKey=${mockKey};`,
|
||||||
),
|
),
|
||||||
).toBe(mockKey);
|
).toBe(mockKey);
|
||||||
@@ -49,7 +49,7 @@ describe("extractMasterKeyfromConnectionString", () => {
|
|||||||
|
|
||||||
it("extracts the account key from a Table connection string", () => {
|
it("extracts the account key from a Table connection string", () => {
|
||||||
expect(
|
expect(
|
||||||
extractMasterKeyfromConnectionString(
|
extractMasterKeyFromDirectLoginConnectionString(
|
||||||
`DefaultEndpointsProtocol=https;AccountName=${mockAccountName};AccountKey=${mockKey};TableEndpoint=https://${mockAccountName}.table.cosmosdb.azure.com:443/;`,
|
`DefaultEndpointsProtocol=https;AccountName=${mockAccountName};AccountKey=${mockKey};TableEndpoint=https://${mockAccountName}.table.cosmosdb.azure.com:443/;`,
|
||||||
),
|
),
|
||||||
).toBe(mockKey);
|
).toBe(mockKey);
|
||||||
@@ -57,7 +57,7 @@ describe("extractMasterKeyfromConnectionString", () => {
|
|||||||
|
|
||||||
it("extracts the account key from a Gremlin connection string", () => {
|
it("extracts the account key from a Gremlin connection string", () => {
|
||||||
expect(
|
expect(
|
||||||
extractMasterKeyfromConnectionString(
|
extractMasterKeyFromDirectLoginConnectionString(
|
||||||
`AccountEndpoint=https://${mockAccountName}.documents.azure.com:443/;AccountKey=${mockKey};ApiKind=Gremlin;`,
|
`AccountEndpoint=https://${mockAccountName}.documents.azure.com:443/;AccountKey=${mockKey};ApiKind=Gremlin;`,
|
||||||
),
|
),
|
||||||
).toBe(mockKey);
|
).toBe(mockKey);
|
||||||
@@ -65,12 +65,14 @@ describe("extractMasterKeyfromConnectionString", () => {
|
|||||||
|
|
||||||
it("returns undefined when there is no account key", () => {
|
it("returns undefined when there is no account key", () => {
|
||||||
expect(
|
expect(
|
||||||
extractMasterKeyfromConnectionString(`AccountEndpoint=https://${mockAccountName}.documents.azure.com:443/;`),
|
extractMasterKeyFromDirectLoginConnectionString(
|
||||||
|
`AccountEndpoint=https://${mockAccountName}.documents.azure.com:443/;`,
|
||||||
|
),
|
||||||
).toBeUndefined();
|
).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns undefined for an empty connection string", () => {
|
it("returns undefined for an empty connection string", () => {
|
||||||
expect(extractMasterKeyfromConnectionString("")).toBeUndefined();
|
expect(extractMasterKeyFromDirectLoginConnectionString("")).toBeUndefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -40,9 +40,10 @@ export function getDatabaseAccountKindFromExperience(apiExperience: typeof userC
|
|||||||
return AccountKind.GlobalDocumentDB;
|
return AccountKind.GlobalDocumentDB;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extracts the account key from any Cosmos connection string. The account key value cannot contain a
|
// Returns the master key carried by SQL, Table, and Gremlin connection strings. Mongo and
|
||||||
// semicolon, so we capture everything up to the next connection-string delimiter.
|
// Cassandra do not use an AccountKey token, so they must not be passed here. The key value cannot
|
||||||
export function extractMasterKeyfromConnectionString(connectionString: string): string | undefined {
|
// contain a semicolon, so we capture everything up to the next delimiter.
|
||||||
|
export function extractMasterKeyFromDirectLoginConnectionString(connectionString: string): string | undefined {
|
||||||
const matchedParts = connectionString?.match(/AccountKey=([^;]*)/);
|
const matchedParts = connectionString?.match(/AccountKey=([^;]*)/);
|
||||||
return (matchedParts && matchedParts.length > 1 && matchedParts[1]) || undefined;
|
return (matchedParts && matchedParts.length > 1 && matchedParts[1]) || undefined;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,6 +76,22 @@ describe("AuthorizationUtils", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("isAuthorizationError()", () => {
|
||||||
|
it("should return true for 401 and 403", () => {
|
||||||
|
expect(AuthorizationUtils.isAuthorizationError(401)).toBe(true);
|
||||||
|
expect(AuthorizationUtils.isAuthorizationError(403)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false for other statuses", () => {
|
||||||
|
expect(AuthorizationUtils.isAuthorizationError(404)).toBe(false);
|
||||||
|
expect(AuthorizationUtils.isAuthorizationError(500)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false when there is no status", () => {
|
||||||
|
expect(AuthorizationUtils.isAuthorizationError(undefined)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("decryptJWTToken()", () => {
|
describe("decryptJWTToken()", () => {
|
||||||
it("should throw an error if token is undefined", () => {
|
it("should throw an error if token is undefined", () => {
|
||||||
expect(() => AuthorizationUtils.decryptJWTToken(undefined)).toThrow();
|
expect(() => AuthorizationUtils.decryptJWTToken(undefined)).toThrow();
|
||||||
|
|||||||
@@ -28,6 +28,10 @@ export function getAuthorizationHeader(): ViewModels.AuthorizationTokenHeaderMet
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isAuthorizationError(statusCode: number): boolean {
|
||||||
|
return statusCode === Constants.HttpStatusCodes.Unauthorized || statusCode === Constants.HttpStatusCodes.Forbidden;
|
||||||
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||||
export function decryptJWTToken(token: string) {
|
export function decryptJWTToken(token: string) {
|
||||||
if (!token) {
|
if (!token) {
|
||||||
|
|||||||
@@ -66,7 +66,7 @@
|
|||||||
"./src/Platform/Hosted/Components/MeControl.test.tsx",
|
"./src/Platform/Hosted/Components/MeControl.test.tsx",
|
||||||
"./src/Platform/Hosted/Components/MeControl.tsx",
|
"./src/Platform/Hosted/Components/MeControl.tsx",
|
||||||
"./src/Platform/Hosted/Components/SignInButton.tsx",
|
"./src/Platform/Hosted/Components/SignInButton.tsx",
|
||||||
"./src/Platform/Hosted/Helpers/PortalAccessData.ts",
|
"./src/Platform/Hosted/Helpers/PortalBackendClient.ts",
|
||||||
"./src/Platform/Hosted/HostedUtils.test.ts",
|
"./src/Platform/Hosted/HostedUtils.test.ts",
|
||||||
"./src/Platform/Hosted/HostedUtils.ts",
|
"./src/Platform/Hosted/HostedUtils.ts",
|
||||||
"./src/Platform/Hosted/extractFeatures.test.ts",
|
"./src/Platform/Hosted/extractFeatures.test.ts",
|
||||||
|
|||||||
Reference in New Issue
Block a user