Skip to content

Commit

Permalink
[server] push payload for /select-account via URL param
Browse files Browse the repository at this point in the history
Signed-off-by: Alex Tugarev <[email protected]>
  • Loading branch information
AlexTugarev committed Apr 14, 2021
1 parent e5acbd8 commit 12ffead
Show file tree
Hide file tree
Showing 8 changed files with 195 additions and 39 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,14 @@
<html>
<head>
<meta charset='utf-8'>
<title>Login successful</title>
<title>Done</title>
<script>
window.opener.postMessage("auth-success", `https://${window.location.hostname}`);
if (window.opener) {
const message = new URLSearchParams(window.location.search).get("message");
window.opener.postMessage(message, `https://${window.location.hostname}`);
} else {
console.log("This page is supposed to be opened by Gitpod.")
}
</script>
</head>
<body>
Expand Down
7 changes: 3 additions & 4 deletions components/dashboard/src/Login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,12 @@ export function Login() {
const listener = (event: MessageEvent<any>) => {
// 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());
Expand Down Expand Up @@ -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)}`
Expand Down
26 changes: 12 additions & 14 deletions components/dashboard/src/prebuilds/InstallGitHubApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,35 +10,33 @@ 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`
}).toString();
window.open(url, "gitpod-login");

const result = new Deferred<void>(1000 * 60 * 10 /* 10 min */);
result.promise.catch(e => setModal('error'));
const listener = (event: MessageEvent<any>) => {
// 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);
Expand Down
23 changes: 15 additions & 8 deletions components/dashboard/src/provider-utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand All @@ -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);
}
Expand Down
29 changes: 26 additions & 3 deletions components/dashboard/src/settings/Integrations.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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() {

Expand All @@ -38,6 +40,7 @@ function GitProviders() {
const [allScopes, setAllScopes] = useState<Map<string, string[]>>(new Map());
const [diconnectModal, setDisconnectModal] = useState<{ provider: AuthProviderInfo } | undefined>(undefined);
const [editModal, setEditModal] = useState<{ provider: AuthProviderInfo, prevScopes: Set<string>, nextScopes: Set<string> } | undefined>(undefined);
const [selectAccountModal, setSelectAccountModal] = useState<SelectAccountPayload | undefined>(undefined);

useEffect(() => {
updateAuthProviders();
Expand Down Expand Up @@ -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}`
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -208,6 +227,10 @@ function GitProviders() {

return (<div>

{selectAccountModal && (
<SelectAccountModal {...selectAccountModal} close={() => setSelectAccountModal(undefined)} />
)}

{diconnectModal && (
<Modal visible={true} onClose={() => setDisconnectModal(undefined)}>
<h3 className="pb-2">Disconnect Provider</h3>
Expand Down Expand Up @@ -627,4 +650,4 @@ function GitIntegrationModal(props: ({

function equals(a: Set<string>, b: Set<string>): boolean {
return a.size === b.size && Array.from(a).every(e => b.has(e));
}
}
86 changes: 86 additions & 0 deletions components/dashboard/src/settings/SelectAccountModal.tsx
Original file line number Diff line number Diff line change
@@ -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 (<Modal visible={true} onClose={props.close}>
<h3 className="pb-2">Select Account</h3>
<div className="border-t border-b border-gray-200 mt-2 -mx-6 px-6 py-4">
<p className="pb-2 text-gray-500 text-base">You are trying to authorize a provider that is already connected with another account on Gitpod.</p>

<div className="mt-4 flex rounded-md w-full bg-gray-200 p-4 mx-auto">
<img className="w-4 h-4 m-1 ml-2 mr-4" src={info} />
<span>
Disconnect a provider in one of you accounts, if you like to continue with the other account.
</span>
</div>

<div className="mt-10 mb-6 flex-grow flex flex-row justify-around align-center">
<SelectableCard className="w-2/5 h-56" title="Current Account" selected={useAccount === 'current'} onClick={() => setUseAccount('current')}>
<div className="flex-grow flex flex-col justify-center align-center">
<img className="m-auto rounded-full w-24 h-24 py-4" src={props.currentUser.avatarUrl} alt={props.currentUser.name}/>
<span className="m-auto text-gray-700 text-md font-semibold">{props.currentUser.authName}</span>
<span className="m-auto text-gray-400 text-md">{props.currentUser.authHost}</span>
</div>
</SelectableCard>

<SelectableCard className="w-2/5 h-56" title="Other Account" selected={useAccount === 'other'} onClick={() => setUseAccount('other')}>
<div className="flex-grow flex flex-col justify-center align-center">
<img className="m-auto rounded-full w-24 h-24 py-4" src={props.otherUser.avatarUrl} alt={props.otherUser.name}/>
<span className="m-auto text-gray-700 text-md font-semibold">{props.otherUser.authName}</span>
<span className="m-auto text-gray-400 text-md">{props.otherUser.authHost}</span>
</div>
</SelectableCard>
</div>


</div>

<div className="flex justify-end mt-6">
<button className={"ml-2"} onClick={() => {
if (useAccount === "other") {
continueWithOtherAccount();
} else {
continueWithCurrentAccount();
}
}}>Continue</button>
</div>
</Modal>);
}
50 changes: 43 additions & 7 deletions components/dashboard/src/start/CreateWorkspace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'));

Expand All @@ -23,6 +26,7 @@ export interface CreateWorkspaceProps {
export interface CreateWorkspaceState {
result?: WorkspaceCreationResult;
error?: StartWorkspaceError;
selectAccountError?: SelectAccountPayload;
stillParsing: boolean;
}

Expand Down Expand Up @@ -62,7 +66,43 @@ export default class CreateWorkspace extends React.Component<CreateWorkspaceProp
}
}

async tryAuthorize(host: string, scopes?: string[]) {
try {
await openAuthorizeWindow({
host,
scopes,
onSuccess: () => {
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 (<StartPage phase={StartPhase.Checking}>
<div className="mt-2 flex flex-col space-y-8">
<SelectAccountModal {...this.state.selectAccountError} close={() => {
window.location.href = gitpodHostUrl.asAccessControl().toString();
}} />
</div>
</StartPage>);
}

let phase = StartPhase.Checking;
let statusMessage = <p className="text-base text-gray-400">{this.state.stillParsing ? 'Parsing context …' : 'Preparing workspace …'}</p>;

Expand All @@ -75,14 +115,10 @@ export default class CreateWorkspace extends React.Component<CreateWorkspaceProp
</div>;
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 = <div className="mt-2 flex flex-col space-y-8">
<p className="text-base w-96">Redirecting to authorize with {error.data.host}</p>
<a href={authorizeUrl}><button className="secondary">Authorize with {error.data.host}</button></a>
<button className="secondary" onClick={() => {
this.tryAuthorize(error?.data.host, error?.data.scopes)
}}>Authorize with {error.data.host}</button>
</div>;
break;
case ErrorCodes.USER_BLOCKED:
Expand Down
4 changes: 3 additions & 1 deletion components/server/src/auth/generic-auth-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down

0 comments on commit 12ffead

Please sign in to comment.