diff --git a/site/src/components/Resources/AppLink/AppLink.tsx b/site/src/components/Resources/AppLink/AppLink.tsx index 9075fec95928a..5e49d43816334 100644 --- a/site/src/components/Resources/AppLink/AppLink.tsx +++ b/site/src/components/Resources/AppLink/AppLink.tsx @@ -4,7 +4,7 @@ import { makeStyles } from "@mui/styles"; import Tooltip from "@mui/material/Tooltip"; import ErrorOutlineIcon from "@mui/icons-material/ErrorOutline"; import { PrimaryAgentButton } from "components/Resources/AgentButton"; -import { FC } from "react"; +import { FC, useState } from "react"; import { combineClasses } from "utils/combineClasses"; import * as TypesGen from "../../../api/typesGenerated"; import { generateRandomString } from "../../../utils/random"; @@ -12,6 +12,7 @@ import { BaseIcon } from "./BaseIcon"; import { ShareIcon } from "./ShareIcon"; import { useProxy } from "contexts/ProxyContext"; import { createAppLinkHref } from "utils/apps"; +import { getApiKey } from "api/api"; const Language = { appTitle: (appName: string, identifier: string): string => @@ -28,6 +29,7 @@ export const AppLink: FC = ({ app, workspace, agent }) => { const { proxy } = useProxy(); const preferredPathBase = proxy.preferredPathAppURL; const appsHost = proxy.preferredWildcardHostname; + const [fetchingSessionToken, setFetchingSessionToken] = useState(false); const styles = useStyles(); const username = workspace.owner_name; @@ -72,6 +74,9 @@ export const AppLink: FC = ({ app, workspace, agent }) => { primaryTooltip = "Your admin has not configured subdomain application access"; } + if (fetchingSessionToken) { + canClick = false; + } const isPrivateApp = app.sharing_level === "owner"; @@ -96,13 +101,38 @@ export const AppLink: FC = ({ app, workspace, agent }) => { className={canClick ? styles.link : styles.disabledLink} onClick={ canClick - ? (event) => { + ? async (event) => { event.preventDefault(); - window.open( - href, - Language.appTitle(appDisplayName, generateRandomString(12)), - "width=900,height=600", - ); + // This is an external URI like "vscode://", so + // it needs to be opened with the browser protocol handler. + if (app.external && !app.url.startsWith("http")) { + // If the protocol is external the browser does not + // redirect the user from the page. + + // This is a magic undocumented string that is replaced + // with a brand-new session token from the backend. + // This only exists for external URLs, and should only + // be used internally, and is highly subject to break. + const magicTokenString = "$SESSION_TOKEN"; + const hasMagicToken = href.indexOf(magicTokenString); + let url = href; + if (hasMagicToken !== -1) { + setFetchingSessionToken(true); + const key = await getApiKey(); + url = href.replaceAll(magicTokenString, key.key); + setFetchingSessionToken(false); + } + window.location.href = url; + } else { + window.open( + href, + Language.appTitle( + appDisplayName, + generateRandomString(12), + ), + "width=900,height=600", + ); + } } : undefined }