diff --git a/components/dashboard/public/login-success/index.html b/components/dashboard/public/flow-result/index.html similarity index 52% rename from components/dashboard/public/login-success/index.html rename to components/dashboard/public/flow-result/index.html index 0e3dbf00f6541a..9e135a0cf822fd 100644 --- a/components/dashboard/public/login-success/index.html +++ b/components/dashboard/public/flow-result/index.html @@ -8,9 +8,14 @@ - Login successful + Done diff --git a/components/dashboard/src/Login.tsx b/components/dashboard/src/Login.tsx index d3926b900584c2..0efca69e5fce77 100644 --- a/components/dashboard/src/Login.tsx +++ b/components/dashboard/src/Login.tsx @@ -50,13 +50,12 @@ export function Login() { const listener = (event: MessageEvent) => { // todo: check event.origin - if (event.data === "auth-success") { + if (event.data === "success") { if (event.source && "close" in event.source && event.source.close) { console.log(`try to close window`); event.source.close(); - } else { - // todo: not here, but add a button to the /login-success page to close, if this should not work as expected } + (async () => { await getGitpodService().reconnect(); setUser(await getGitpodService().server.getLoggedInUser()); @@ -139,7 +138,7 @@ export function Login() { } function getLoginUrl(host: string) { - const returnTo = gitpodHostUrl.with({ pathname: 'login-success' }).toString(); + const returnTo = gitpodHostUrl.with({ pathname: 'flow-result', search: 'message=success' }).toString(); return gitpodHostUrl.withApi({ pathname: '/login', search: `host=${host}&returnTo=${encodeURIComponent(returnTo)}` diff --git a/components/dashboard/src/prebuilds/InstallGitHubApp.tsx b/components/dashboard/src/prebuilds/InstallGitHubApp.tsx index ca7ba69c517112..587defefe0762f 100644 --- a/components/dashboard/src/prebuilds/InstallGitHubApp.tsx +++ b/components/dashboard/src/prebuilds/InstallGitHubApp.tsx @@ -10,12 +10,13 @@ import { Deferred } from "@gitpod/gitpod-protocol/lib/util/deferred"; import { getGitpodService, gitpodHostUrl } from "../service/service"; import { useState } from "react"; import info from "../images/info.svg"; +import { openAuthorizeWindow } from "../provider-utils"; async function registerApp(installationId: string, setModal: (modal: 'done' | string | undefined) => void) { try { await getGitpodService().server.registerGithubApp(installationId); - const returnTo = encodeURIComponent(gitpodHostUrl.with({ pathname: `login-success` }).toString()); + const returnTo = encodeURIComponent(gitpodHostUrl.with({ pathname: 'flow-result', search: 'message=success' }).toString()); const url = gitpodHostUrl.withApi({ pathname: '/authorize', search: `returnTo=${returnTo}&host=github.com&scopes=repo` @@ -23,22 +24,19 @@ async function registerApp(installationId: string, setModal: (modal: 'done' | st window.open(url, "gitpod-login"); const result = new Deferred(1000 * 60 * 10 /* 10 min */); - result.promise.catch(e => setModal('error')); - const listener = (event: MessageEvent) => { - // todo: check event.origin - if (event.data === "auth-success") { - if (event.source && "close" in event.source && event.source.close) { - console.log(`try to close window`); - event.source.close(); - } else { - // todo: not here, but add a button to the /login-success page to close, if this should not work as expected - } - window.removeEventListener("message", listener); + + openAuthorizeWindow({ + host: "github.com", + scopes: ["repo"], + onSuccess: () => { setModal('done'); result.resolve(); + }, + onError: (error) => { + setModal(error); } - }; - window.addEventListener("message", listener); + }) + return result.promise; } catch (e) { setModal(e.message); diff --git a/components/dashboard/src/provider-utils.tsx b/components/dashboard/src/provider-utils.tsx index c4ecbc4415cbb9..309bffe67481d4 100644 --- a/components/dashboard/src/provider-utils.tsx +++ b/components/dashboard/src/provider-utils.tsx @@ -36,14 +36,14 @@ function simplifyProviderName(host: string) { } } -async function openAuthorizeWindow({ host, scopes, onSuccess, onError }: { host: string, scopes?: string[], onSuccess?: () => void, onError?: (error?: string) => void }) { - const returnTo = gitpodHostUrl.with({ pathname: 'login-success' }).toString(); +async function openAuthorizeWindow({ host, scopes, onSuccess, onError }: { host: string, scopes?: string[], onSuccess?: (payload?: string) => void, onError?: (error?: string) => void }) { + const returnTo = gitpodHostUrl.with({ pathname: 'flow-result', search: 'message=success' }).toString(); const url = gitpodHostUrl.withApi({ pathname: '/authorize', search: `returnTo=${encodeURIComponent(returnTo)}&host=${host}&override=true&scopes=${(scopes || []).join(',')}` }).toString(); - const newWindow = window.open(url, "gitpod-connect"); + const newWindow = window.open(url, "gitpod-auth-window"); if (!newWindow) { console.log(`Failed to open the authorize window for ${host}`); onError && onError("failed"); @@ -53,17 +53,24 @@ async function openAuthorizeWindow({ host, scopes, onSuccess, onError }: { host: const eventListener = (event: MessageEvent) => { // todo: check event.origin - if (event.data === "auth-success") { + const killAuthWindow = () => { window.removeEventListener("message", eventListener); - + if (event.source && "close" in event.source && event.source.close) { - console.log(`Authorization OK. Closing child window.`); + console.log(`Received Auth Window Result. Closing Window.`); event.source.close(); - } else { - // todo: add a button to the /login-success page to close, if this should not work as expected } + } + + if (typeof event.data === "string" && event.data.startsWith("success")) { + killAuthWindow(); onSuccess && onSuccess(); } + if (typeof event.data === "string" && event.data.startsWith("error:")) { + const errorText = atob(event.data.substring("error:".length)); + killAuthWindow(); + onError && onError(errorText); + } }; window.addEventListener("message", eventListener); } diff --git a/components/dashboard/src/settings/Integrations.tsx b/components/dashboard/src/settings/Integrations.tsx index f0820d7dbf0b27..ffcd32e37c272e 100644 --- a/components/dashboard/src/settings/Integrations.tsx +++ b/components/dashboard/src/settings/Integrations.tsx @@ -5,6 +5,7 @@ */ import { AuthProviderEntry, AuthProviderInfo } from "@gitpod/gitpod-protocol"; +import { SelectAccountPayload } from "@gitpod/gitpod-protocol/lib/auth"; import React, { useContext, useEffect, useState } from "react"; import ContextMenu, { ContextMenuEntry } from "../components/ContextMenu"; import { getGitpodService, gitpodHostUrl } from "../service/service"; @@ -17,6 +18,7 @@ import { openAuthorizeWindow } from "../provider-utils"; import CheckBox from '../components/CheckBox'; import { PageWithSubMenu } from "../components/PageWithSubMenu"; import settingsMenu from "./settings-menu"; +import { SelectAccountModal } from "./SelectAccountModal"; export default function Integrations() { @@ -38,6 +40,7 @@ function GitProviders() { const [allScopes, setAllScopes] = useState>(new Map()); const [diconnectModal, setDisconnectModal] = useState<{ provider: AuthProviderInfo } | undefined>(undefined); const [editModal, setEditModal] = useState<{ provider: AuthProviderInfo, prevScopes: Set, nextScopes: Set } | undefined>(undefined); + const [selectAccountModal, setSelectAccountModal] = useState(undefined); useEffect(() => { updateAuthProviders(); @@ -120,7 +123,7 @@ function GitProviders() { const disconnect = async (ap: AuthProviderInfo) => { setDisconnectModal(undefined); - const returnTo = gitpodHostUrl.with({ pathname: 'login-success' }).toString(); + const returnTo = gitpodHostUrl.with({ pathname: 'flow-result', search: 'message=success' }).toString(); const deauthorizeUrl = gitpodHostUrl.withApi({ pathname: '/deauthorize', search: `returnTo=${returnTo}&host=${ap.host}` @@ -152,7 +155,23 @@ function GitProviders() { const doAuthorize = async (host: string, scopes?: string[]) => { try { - await openAuthorizeWindow({ host, scopes, onSuccess: () => updateUser() }); + await openAuthorizeWindow({ + host, + scopes, + onSuccess: () => updateUser(), + onError: (error) => { + if (typeof error === "string") { + try { + const payload = JSON.parse(error); + if (SelectAccountPayload.is(payload)) { + setSelectAccountModal(payload) + } + } catch (error) { + console.log(error); + } + } + } + }); } catch (error) { console.log(error) } @@ -208,6 +227,10 @@ function GitProviders() { return (
+ {selectAccountModal && ( + setSelectAccountModal(undefined)} /> + )} + {diconnectModal && ( setDisconnectModal(undefined)}>

Disconnect Provider

@@ -627,4 +650,4 @@ function GitIntegrationModal(props: ({ function equals(a: Set, b: Set): boolean { return a.size === b.size && Array.from(a).every(e => b.has(e)); -} \ No newline at end of file +} diff --git a/components/dashboard/src/settings/SelectAccountModal.tsx b/components/dashboard/src/settings/SelectAccountModal.tsx new file mode 100644 index 00000000000000..b8aec66ab76501 --- /dev/null +++ b/components/dashboard/src/settings/SelectAccountModal.tsx @@ -0,0 +1,86 @@ +/** + * Copyright (c) 2021 Gitpod GmbH. All rights reserved. + * Licensed under the GNU Affero General Public License (AGPL). + * See License-AGPL.txt in the project root for license information. + */ + + import { SelectAccountPayload } from "@gitpod/gitpod-protocol/lib/auth"; + import { useEffect, useState } from "react"; + import { gitpodHostUrl } from "../service/service"; + import Modal from "../components/Modal"; + import SelectableCard from "../components/SelectableCard"; + import info from '../images/info.svg'; + +export function SelectAccountModal(props: SelectAccountPayload & { + close: () => void +}) { + + const [useAccount, setUseAccount] = useState<"current" | "other">("current"); + + useEffect(() => { + }, []); + + const continueWithCurrentAccount = () => { + props.close(); + }; + + const continueWithOtherAccount = () => { + const accessControlUrl = gitpodHostUrl.asAccessControl().toString(); + + const loginUrl = gitpodHostUrl.withApi({ + pathname: '/login', + search: `host=${props.otherUser.authHost}&returnTo=${encodeURIComponent(accessControlUrl)}` + }).toString(); + + const logoutUrl = gitpodHostUrl.withApi({ + pathname: "/logout", + search: `returnTo=${encodeURIComponent(loginUrl)}` + }).toString(); + + window.location.href = logoutUrl; + }; + + return ( +

Select Account

+
+

You are trying to authorize a provider that is already connected with another account on Gitpod.

+ +
+ + + Disconnect a provider in one of you accounts, if you like to continue with the other account. + +
+ +
+ setUseAccount('current')}> +
+ {props.currentUser.name}/ + {props.currentUser.authName} + {props.currentUser.authHost} +
+
+ + setUseAccount('other')}> +
+ {props.otherUser.name}/ + {props.otherUser.authName} + {props.otherUser.authHost} +
+
+
+ + +
+ +
+ +
+
); +} \ No newline at end of file diff --git a/components/dashboard/src/start/CreateWorkspace.tsx b/components/dashboard/src/start/CreateWorkspace.tsx index e3ea3ca4b247c1..52a6d4c72b22f1 100644 --- a/components/dashboard/src/start/CreateWorkspace.tsx +++ b/components/dashboard/src/start/CreateWorkspace.tsx @@ -13,6 +13,9 @@ import { getGitpodService, gitpodHostUrl } from "../service/service"; import { UserContext } from "../user-context"; import { StartPage, StartPhase, StartWorkspaceError } from "./StartPage"; import StartWorkspace from "./StartWorkspace"; +import { openAuthorizeWindow } from "../provider-utils"; +import { SelectAccountPayload } from "@gitpod/gitpod-protocol/lib/auth"; +import { SelectAccountModal } from "../settings/SelectAccountModal"; const WorkspaceLogs = React.lazy(() => import('./WorkspaceLogs')); @@ -23,6 +26,7 @@ export interface CreateWorkspaceProps { export interface CreateWorkspaceState { result?: WorkspaceCreationResult; error?: StartWorkspaceError; + selectAccountError?: SelectAccountPayload; stillParsing: boolean; } @@ -62,7 +66,43 @@ export default class CreateWorkspace extends React.Component { + window.location.href = window.location.toString(); + }, + onError: (error) => { + if (typeof error === "string") { + try { + const payload = JSON.parse(error); + if (SelectAccountPayload.is(payload)) { + this.setState({ selectAccountError: payload }); + } + } catch (error) { + console.log(error); + } + } + } + }); + } catch (error) { + console.log(error) + } + }; + render() { + if (SelectAccountPayload.is(this.state.selectAccountError)) { + return ( +
+ { + window.location.href = gitpodHostUrl.asAccessControl().toString(); + }} /> +
+
); + } + let phase = StartPhase.Checking; let statusMessage =

{this.state.stillParsing ? 'Parsing context …' : 'Preparing workspace …'}

; @@ -75,14 +115,10 @@ export default class CreateWorkspace extends React.Component; break; case ErrorCodes.NOT_AUTHENTICATED: - const authorizeUrl = gitpodHostUrl.withApi({ - pathname: '/authorize', - search: `returnTo=${encodeURIComponent(window.location.toString())}&host=${error.data.host}&scopes=${error.data.scopes.join(',')}` - }).toString(); - window.location.href = authorizeUrl; statusMessage =
-

Redirecting to authorize with {error.data.host} …

- +
; break; case ErrorCodes.USER_BLOCKED: diff --git a/components/server/src/auth/generic-auth-provider.ts b/components/server/src/auth/generic-auth-provider.ts index 996ad848654d42..b62227fddb3b74 100644 --- a/components/server/src/auth/generic-auth-provider.ts +++ b/components/server/src/auth/generic-auth-provider.ts @@ -340,7 +340,9 @@ export class GenericAuthProvider implements AuthProvider { if (SelectAccountException.is(err)) { this.selectAccountCookie.set(response, err.payload); - const url = this.env.hostUrl.with({ pathname: '/select-account' }).toString(); + + // option 1: send as GET param on redirect + const url = this.env.hostUrl.with({ pathname: '/flow-result', search: "message=error:" + Buffer.from(JSON.stringify(err.payload), "utf-8").toString('base64') }).toString(); response.redirect(url); return; }