diff --git a/src/ConfigContext.ts b/src/ConfigContext.ts index 061f25286..36fee64d0 100644 --- a/src/ConfigContext.ts +++ b/src/ConfigContext.ts @@ -238,6 +238,15 @@ export async function initializeConfiguration(): Promise { updateConfigContext({ platform }); } } + if (window.location.origin !== configContext.hostedExplorerURL) { + if (window.location.origin === "https://localhost:1234") { + // Special case for localhost, we need to send them to 'hostedExplorer.html'. + updateConfigContext({ hostedExplorerURL: "https://localhost:1234/hostedExplorer.html" }); + } else { + const newOrigin = window.location.origin.endsWith("/") ? window.location.origin : `${window.location.origin}/`; + updateConfigContext({ hostedExplorerURL: newOrigin }); + } + } } catch (error) { console.error("No configuration file found using defaults"); } @@ -245,3 +254,4 @@ export async function initializeConfiguration(): Promise { } export { configContext }; + diff --git a/src/Explorer/OpenFullScreen.tsx b/src/Explorer/OpenFullScreen.tsx index 5474161cd..669de816b 100644 --- a/src/Explorer/OpenFullScreen.tsx +++ b/src/Explorer/OpenFullScreen.tsx @@ -1,20 +1,40 @@ import { PrimaryButton, Stack, Text } from "@fluentui/react"; +import { AuthType } from "AuthType"; +import { configContext } from "ConfigContext"; +import { userContext } from "UserContext"; import * as React from "react"; export const OpenFullScreen: React.FunctionComponent = () => { + const searchParams = new URLSearchParams(); + let hasAccountContext = false; + let requiresConnectionString = false; + + if (userContext.authType === AuthType.AAD) { + if (userContext.subscriptionId && userContext.databaseAccount) { + searchParams.append("subscription", userContext.subscriptionId); + searchParams.append("account", userContext.databaseAccount.id); + searchParams.append("authType", "entra"); + hasAccountContext = true; + } + } else if (userContext.authType === AuthType.MasterKey || userContext.authType === AuthType.ResourceToken) { + searchParams.append("authType", "connectionstring") + requiresConnectionString = true; + } + return ( <>
- Open this database account in a new browser tab with Cosmos DB Explorer. You can connect using your - Microsoft account or a connection string. + Open this database account in a new browser tab with Cosmos DB Explorer. + {requiresConnectionString && " You'll need to provide a connection string."} + {hasAccountContext && " You may be prompted to sign in with Entra ID, and then you'll be redirected back to this account."} + Open tabs and queries will not be carried over, but will remain in this tab. { - window.open("https://cosmos.azure.com/", "_blank"); - }} + href={`${configContext.hostedExplorerURL}?${searchParams.toString()}`} + target="_blank" text="Open" iconProps={{ iconName: "OpenInNewWindow" }} /> diff --git a/src/HostedExplorer.tsx b/src/HostedExplorer.tsx index 4dc26fb4e..f546cd42d 100644 --- a/src/HostedExplorer.tsx +++ b/src/HostedExplorer.tsx @@ -1,6 +1,7 @@ import { initializeIcons } from "@fluentui/react"; import { useBoolean } from "@fluentui/react-hooks"; import { AadAuthorizationFailure } from "Platform/Hosted/Components/AadAuthorizationFailure"; +import { getMsalInstance } from "Utils/AuthorizationUtils"; import * as React from "react"; import { render } from "react-dom"; import ChevronRight from "../images/chevron-right.svg"; @@ -42,6 +43,23 @@ const App: React.FunctionComponent = () => { const ref = React.useRef(); React.useEffect(() => { + (async () => { + const params = new URLSearchParams(window.location.search); + if (params.get("authType") === "entra") { + const msalInstance = await getMsalInstance(); + const response = await msalInstance.handleRedirectPromise(); + if (response) { + console.log("Redirect Promise Response", response); + } + else { + // Send the user to log in immediately + console.log("Starting non-interactive login"); + login(false); + } + return; + } + })(); + // If ref.current is undefined no iframe has been rendered if (ref.current) { // In hosted mode, we can set global properties directly on the child iframe. diff --git a/src/Index.tsx b/src/Index.tsx index d660eaed0..2f9fdc6f3 100644 --- a/src/Index.tsx +++ b/src/Index.tsx @@ -1,4 +1,5 @@ -import React, { useState } from "react"; +import { getMsalInstance } from "Utils/AuthorizationUtils"; +import React, { useEffect, useState } from "react"; import ReactDOM from "react-dom"; import Arrow from "../images/Arrow.svg"; import CosmosDB_20170829 from "../images/CosmosDB_20170829.svg"; @@ -9,10 +10,17 @@ import "../less/index.less"; const Index = (): JSX.Element => { const [navigationSelection, setNavigationSelection] = useState("quickstart"); - const quickstart_click = () => { setNavigationSelection("quickstart"); }; + + useEffect(() => { + (async () => { + const msalInstance = await getMsalInstance(); + console.log("handleRedirectPromise"); + await msalInstance.handleRedirectPromise(); + })(); + }, []); const explorer_click = () => { setNavigationSelection("explorer"); diff --git a/src/UserContext.ts b/src/UserContext.ts index 30a0236dc..d811750e3 100644 --- a/src/UserContext.ts +++ b/src/UserContext.ts @@ -13,12 +13,12 @@ import { CollectionCreation, CollectionCreationDefaults } from "./Shared/Constan interface ThroughputDefaults { fixed: number; unlimited: - | number - | { - collectionThreshold: number; - lessThanOrEqualToThreshold: number; - greatThanThreshold: number; - }; + | number + | { + collectionThreshold: number; + lessThanOrEqualToThreshold: number; + greatThanThreshold: number; + }; unlimitedmax: number; unlimitedmin: number; shared: number; @@ -192,3 +192,4 @@ function apiType(account: DatabaseAccount | undefined): ApiType { } export { updateUserContext, userContext }; + diff --git a/src/Utils/EndpointUtils.ts b/src/Utils/EndpointUtils.ts index 97e733b98..efbcf8652 100644 --- a/src/Utils/EndpointUtils.ts +++ b/src/Utils/EndpointUtils.ts @@ -128,7 +128,7 @@ export const allowedGraphEndpoints: ReadonlyArray = ["https://graph.micr export const allowedArcadiaEndpoints: ReadonlyArray = ["https://workspaceartifacts.projectarcadia.net"]; -export const allowedHostedExplorerEndpoints: ReadonlyArray = ["https://cosmos.azure.com/"]; +export const allowedHostedExplorerEndpoints: ReadonlyArray = ["https://cosmos.azure.com/", "https://localhost:1234/"]; export const allowedMsalRedirectEndpoints: ReadonlyArray = [ "https://cosmos-explorer-preview.azurewebsites.net/", diff --git a/src/hooks/useAADAuth.ts b/src/hooks/useAADAuth.ts index c20f953f7..8fbcc4621 100644 --- a/src/hooks/useAADAuth.ts +++ b/src/hooks/useAADAuth.ts @@ -13,7 +13,7 @@ interface ReturnType { isLoggedIn: boolean; graphToken: string; armToken: string; - login: () => void; + login: (userTriggered?: boolean) => void; logout: () => void; tenantId: string; account: msal.AccountInfo; @@ -37,8 +37,21 @@ export function useAADAuth(): ReturnType { const [armToken, setArmToken] = React.useState(); const [authFailure, setAuthFailure] = React.useState(undefined); + console.log("Current AAD State", { + isLoggedIn, account, tenantId + }); + msalInstance.setActiveAccount(account); - const login = React.useCallback(async () => { + const login = React.useCallback(async (userTriggered: boolean = true) => { + if (!userTriggered) { + console.log("Starting non-interactive login"); + // If the user didn't trigger the login, we don't want to pop up the login dialog + await msalInstance.acquireTokenRedirect({ + redirectUri: configContext.msalRedirectURI, + scopes: [], + }); + return; + } const response = await msalInstance.loginPopup({ redirectUri: configContext.msalRedirectURI, scopes: [], @@ -47,13 +60,13 @@ export function useAADAuth(): ReturnType { setAccount(response.account); setTenantId(response.tenantId); localStorage.setItem("cachedTenantId", response.tenantId); - }, []); + }, [setLoggedIn, setAccount, setTenantId]); const logout = React.useCallback(() => { setLoggedOut(); localStorage.removeItem("cachedTenantId"); msalInstance.logoutRedirect(); - }, []); + }, [setLoggedOut]); const switchTenant = React.useCallback( async (id) => { @@ -66,7 +79,7 @@ export function useAADAuth(): ReturnType { setAccount(response.account); localStorage.setItem("cachedTenantId", response.tenantId); }, - [account, tenantId], + [setTenantId, setAccount], ); const acquireTokens = React.useCallback(async () => { @@ -123,6 +136,19 @@ export function useAADAuth(): ReturnType { } }, [account, tenantId, acquireTokens, authFailure]); + React.useEffect(() => { + (async () => { + // If we're on a redirect, handle it + const response = await msalInstance.handleRedirectPromise(); + if (response) { + setLoggedIn(); + setAccount(response.account); + setTenantId(response.tenantId); + localStorage.setItem("cachedTenantId", response.tenantId); + } + })() + }, [setLoggedIn]) + return { account, tenantId,