Skip to content

Commit 312837a

Browse files
committed
[server] push payload for /select-account via URL param
Signed-off-by: Alex Tugarev <[email protected]>
1 parent 0836179 commit 312837a

File tree

8 files changed

+195
-39
lines changed

8 files changed

+195
-39
lines changed

components/dashboard/public/login-success/index.html renamed to components/dashboard/public/flow-result/index.html

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,14 @@
88
<html>
99
<head>
1010
<meta charset='utf-8'>
11-
<title>Login successful</title>
11+
<title>Done</title>
1212
<script>
13-
window.opener.postMessage("auth-success", `https://${window.location.hostname}`);
13+
if (window.opener) {
14+
const message = new URLSearchParams(window.location.search).get("message");
15+
window.opener.postMessage(message, `https://${window.location.hostname}`);
16+
} else {
17+
console.log("This page is supposed to be opened by Gitpod.")
18+
}
1419
</script>
1520
</head>
1621
<body>

components/dashboard/src/Login.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,13 +50,12 @@ export function Login() {
5050
const listener = (event: MessageEvent<any>) => {
5151
// todo: check event.origin
5252

53-
if (event.data === "auth-success") {
53+
if (event.data === "success") {
5454
if (event.source && "close" in event.source && event.source.close) {
5555
console.log(`try to close window`);
5656
event.source.close();
57-
} else {
58-
// todo: not here, but add a button to the /login-success page to close, if this should not work as expected
5957
}
58+
6059
(async () => {
6160
await getGitpodService().reconnect();
6261
setUser(await getGitpodService().server.getLoggedInUser());
@@ -139,7 +138,7 @@ export function Login() {
139138
}
140139

141140
function getLoginUrl(host: string) {
142-
const returnTo = gitpodHostUrl.with({ pathname: 'login-success' }).toString();
141+
const returnTo = gitpodHostUrl.with({ pathname: 'flow-result', search: 'message=success' }).toString();
143142
return gitpodHostUrl.withApi({
144143
pathname: '/login',
145144
search: `host=${host}&returnTo=${encodeURIComponent(returnTo)}`

components/dashboard/src/prebuilds/InstallGitHubApp.tsx

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,35 +10,33 @@ import { Deferred } from "@gitpod/gitpod-protocol/lib/util/deferred";
1010
import { getGitpodService, gitpodHostUrl } from "../service/service";
1111
import { useState } from "react";
1212
import info from "../images/info.svg";
13+
import { openAuthorizeWindow } from "../provider-utils";
1314

1415
async function registerApp(installationId: string, setModal: (modal: 'done' | string | undefined) => void) {
1516
try {
1617
await getGitpodService().server.registerGithubApp(installationId);
1718

18-
const returnTo = encodeURIComponent(gitpodHostUrl.with({ pathname: `login-success` }).toString());
19+
const returnTo = encodeURIComponent(gitpodHostUrl.with({ pathname: 'flow-result', search: 'message=success' }).toString());
1920
const url = gitpodHostUrl.withApi({
2021
pathname: '/authorize',
2122
search: `returnTo=${returnTo}&host=github.com&scopes=repo`
2223
}).toString();
2324
window.open(url, "gitpod-login");
2425

2526
const result = new Deferred<void>(1000 * 60 * 10 /* 10 min */);
26-
result.promise.catch(e => setModal('error'));
27-
const listener = (event: MessageEvent<any>) => {
28-
// todo: check event.origin
29-
if (event.data === "auth-success") {
30-
if (event.source && "close" in event.source && event.source.close) {
31-
console.log(`try to close window`);
32-
event.source.close();
33-
} else {
34-
// todo: not here, but add a button to the /login-success page to close, if this should not work as expected
35-
}
36-
window.removeEventListener("message", listener);
27+
28+
openAuthorizeWindow({
29+
host: "github.com",
30+
scopes: ["repo"],
31+
onSuccess: () => {
3732
setModal('done');
3833
result.resolve();
34+
},
35+
onError: (error) => {
36+
setModal(error);
3937
}
40-
};
41-
window.addEventListener("message", listener);
38+
})
39+
4240
return result.promise;
4341
} catch (e) {
4442
setModal(e.message);

components/dashboard/src/provider-utils.tsx

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,14 @@ function simplifyProviderName(host: string) {
3636
}
3737
}
3838

39-
async function openAuthorizeWindow({ host, scopes, onSuccess, onError }: { host: string, scopes?: string[], onSuccess?: () => void, onError?: (error?: string) => void }) {
40-
const returnTo = gitpodHostUrl.with({ pathname: 'login-success' }).toString();
39+
async function openAuthorizeWindow({ host, scopes, onSuccess, onError }: { host: string, scopes?: string[], onSuccess?: (payload?: string) => void, onError?: (error?: string) => void }) {
40+
const returnTo = gitpodHostUrl.with({ pathname: 'flow-result', search: 'message=success' }).toString();
4141
const url = gitpodHostUrl.withApi({
4242
pathname: '/authorize',
4343
search: `returnTo=${encodeURIComponent(returnTo)}&host=${host}&override=true&scopes=${(scopes || []).join(',')}`
4444
}).toString();
4545

46-
const newWindow = window.open(url, "gitpod-connect");
46+
const newWindow = window.open(url, "gitpod-auth-window");
4747
if (!newWindow) {
4848
console.log(`Failed to open the authorize window for ${host}`);
4949
onError && onError("failed");
@@ -53,17 +53,24 @@ async function openAuthorizeWindow({ host, scopes, onSuccess, onError }: { host:
5353
const eventListener = (event: MessageEvent) => {
5454
// todo: check event.origin
5555

56-
if (event.data === "auth-success") {
56+
const killAuthWindow = () => {
5757
window.removeEventListener("message", eventListener);
58-
58+
5959
if (event.source && "close" in event.source && event.source.close) {
60-
console.log(`Authorization OK. Closing child window.`);
60+
console.log(`Received Auth Window Result. Closing Window.`);
6161
event.source.close();
62-
} else {
63-
// todo: add a button to the /login-success page to close, if this should not work as expected
6462
}
63+
}
64+
65+
if (typeof event.data === "string" && event.data.startsWith("success")) {
66+
killAuthWindow();
6567
onSuccess && onSuccess();
6668
}
69+
if (typeof event.data === "string" && event.data.startsWith("error:")) {
70+
const errorText = atob(event.data.substring("error:".length));
71+
killAuthWindow();
72+
onError && onError(errorText);
73+
}
6774
};
6875
window.addEventListener("message", eventListener);
6976
}

components/dashboard/src/settings/Integrations.tsx

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
*/
66

77
import { AuthProviderEntry, AuthProviderInfo } from "@gitpod/gitpod-protocol";
8+
import { SelectAccountPayload } from "@gitpod/gitpod-protocol/lib/auth";
89
import React, { useContext, useEffect, useState } from "react";
910
import ContextMenu, { ContextMenuEntry } from "../components/ContextMenu";
1011
import { getGitpodService, gitpodHostUrl } from "../service/service";
@@ -17,6 +18,7 @@ import { openAuthorizeWindow } from "../provider-utils";
1718
import CheckBox from '../components/CheckBox';
1819
import { PageWithSubMenu } from "../components/PageWithSubMenu";
1920
import settingsMenu from "./settings-menu";
21+
import { SelectAccountModal } from "./SelectAccountModal";
2022

2123
export default function Integrations() {
2224

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

4245
useEffect(() => {
4346
updateAuthProviders();
@@ -120,7 +123,7 @@ function GitProviders() {
120123

121124
const disconnect = async (ap: AuthProviderInfo) => {
122125
setDisconnectModal(undefined);
123-
const returnTo = gitpodHostUrl.with({ pathname: 'login-success' }).toString();
126+
const returnTo = gitpodHostUrl.with({ pathname: 'flow-result', search: 'message=success' }).toString();
124127
const deauthorizeUrl = gitpodHostUrl.withApi({
125128
pathname: '/deauthorize',
126129
search: `returnTo=${returnTo}&host=${ap.host}`
@@ -152,7 +155,23 @@ function GitProviders() {
152155

153156
const doAuthorize = async (host: string, scopes?: string[]) => {
154157
try {
155-
await openAuthorizeWindow({ host, scopes, onSuccess: () => updateUser() });
158+
await openAuthorizeWindow({
159+
host,
160+
scopes,
161+
onSuccess: () => updateUser(),
162+
onError: (error) => {
163+
if (typeof error === "string") {
164+
try {
165+
const payload = JSON.parse(error);
166+
if (SelectAccountPayload.is(payload)) {
167+
setSelectAccountModal(payload)
168+
}
169+
} catch (error) {
170+
console.log(error);
171+
}
172+
}
173+
}
174+
});
156175
} catch (error) {
157176
console.log(error)
158177
}
@@ -208,6 +227,10 @@ function GitProviders() {
208227

209228
return (<div>
210229

230+
{selectAccountModal && (
231+
<SelectAccountModal {...selectAccountModal} close={() => setSelectAccountModal(undefined)} />
232+
)}
233+
211234
{diconnectModal && (
212235
<Modal visible={true} onClose={() => setDisconnectModal(undefined)}>
213236
<h3 className="pb-2">Disconnect Provider</h3>
@@ -627,4 +650,4 @@ function GitIntegrationModal(props: ({
627650

628651
function equals(a: Set<string>, b: Set<string>): boolean {
629652
return a.size === b.size && Array.from(a).every(e => b.has(e));
630-
}
653+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/**
2+
* Copyright (c) 2021 Gitpod GmbH. All rights reserved.
3+
* Licensed under the GNU Affero General Public License (AGPL).
4+
* See License-AGPL.txt in the project root for license information.
5+
*/
6+
7+
import { SelectAccountPayload } from "@gitpod/gitpod-protocol/lib/auth";
8+
import { useEffect, useState } from "react";
9+
import { gitpodHostUrl } from "../service/service";
10+
import Modal from "../components/Modal";
11+
import SelectableCard from "../components/SelectableCard";
12+
import info from '../images/info.svg';
13+
14+
export function SelectAccountModal(props: SelectAccountPayload & {
15+
close: () => void
16+
}) {
17+
18+
const [useAccount, setUseAccount] = useState<"current" | "other">("current");
19+
20+
useEffect(() => {
21+
}, []);
22+
23+
const continueWithCurrentAccount = () => {
24+
props.close();
25+
};
26+
27+
const continueWithOtherAccount = () => {
28+
const accessControlUrl = gitpodHostUrl.asAccessControl().toString();
29+
30+
const loginUrl = gitpodHostUrl.withApi({
31+
pathname: '/login',
32+
search: `host=${props.otherUser.authHost}&returnTo=${encodeURIComponent(accessControlUrl)}`
33+
}).toString();
34+
35+
const logoutUrl = gitpodHostUrl.withApi({
36+
pathname: "/logout",
37+
search: `returnTo=${encodeURIComponent(loginUrl)}`
38+
}).toString();
39+
40+
window.location.href = logoutUrl;
41+
};
42+
43+
return (<Modal visible={true} onClose={props.close}>
44+
<h3 className="pb-2">Select Account</h3>
45+
<div className="border-t border-b border-gray-200 mt-2 -mx-6 px-6 py-4">
46+
<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>
47+
48+
<div className="mt-4 flex rounded-md w-full bg-gray-200 p-4 mx-auto">
49+
<img className="w-4 h-4 m-1 ml-2 mr-4" src={info} />
50+
<span>
51+
Disconnect a provider in one of you accounts, if you like to continue with the other account.
52+
</span>
53+
</div>
54+
55+
<div className="mt-10 mb-6 flex-grow flex flex-row justify-around align-center">
56+
<SelectableCard className="w-2/5 h-56" title="Current Account" selected={useAccount === 'current'} onClick={() => setUseAccount('current')}>
57+
<div className="flex-grow flex flex-col justify-center align-center">
58+
<img className="m-auto rounded-full w-24 h-24 py-4" src={props.currentUser.avatarUrl} alt={props.currentUser.name}/>
59+
<span className="m-auto text-gray-700 text-md font-semibold">{props.currentUser.authName}</span>
60+
<span className="m-auto text-gray-400 text-md">{props.currentUser.authHost}</span>
61+
</div>
62+
</SelectableCard>
63+
64+
<SelectableCard className="w-2/5 h-56" title="Other Account" selected={useAccount === 'other'} onClick={() => setUseAccount('other')}>
65+
<div className="flex-grow flex flex-col justify-center align-center">
66+
<img className="m-auto rounded-full w-24 h-24 py-4" src={props.otherUser.avatarUrl} alt={props.otherUser.name}/>
67+
<span className="m-auto text-gray-700 text-md font-semibold">{props.otherUser.authName}</span>
68+
<span className="m-auto text-gray-400 text-md">{props.otherUser.authHost}</span>
69+
</div>
70+
</SelectableCard>
71+
</div>
72+
73+
74+
</div>
75+
76+
<div className="flex justify-end mt-6">
77+
<button className={"ml-2"} onClick={() => {
78+
if (useAccount === "other") {
79+
continueWithOtherAccount();
80+
} else {
81+
continueWithCurrentAccount();
82+
}
83+
}}>Continue</button>
84+
</div>
85+
</Modal>);
86+
}

components/dashboard/src/start/CreateWorkspace.tsx

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ import { getGitpodService, gitpodHostUrl } from "../service/service";
1313
import { UserContext } from "../user-context";
1414
import { StartPage, StartPhase, StartWorkspaceError } from "./StartPage";
1515
import StartWorkspace from "./StartWorkspace";
16+
import { openAuthorizeWindow } from "../provider-utils";
17+
import { SelectAccountPayload } from "@gitpod/gitpod-protocol/lib/auth";
18+
import { SelectAccountModal } from "../settings/SelectAccountModal";
1619

1720
const WorkspaceLogs = React.lazy(() => import('./WorkspaceLogs'));
1821

@@ -23,6 +26,7 @@ export interface CreateWorkspaceProps {
2326
export interface CreateWorkspaceState {
2427
result?: WorkspaceCreationResult;
2528
error?: StartWorkspaceError;
29+
selectAccountError?: SelectAccountPayload;
2630
stillParsing: boolean;
2731
}
2832

@@ -62,7 +66,43 @@ export default class CreateWorkspace extends React.Component<CreateWorkspaceProp
6266
}
6367
}
6468

69+
async tryAuthorize(host: string, scopes?: string[]) {
70+
try {
71+
await openAuthorizeWindow({
72+
host,
73+
scopes,
74+
onSuccess: () => {
75+
window.location.href = window.location.toString();
76+
},
77+
onError: (error) => {
78+
if (typeof error === "string") {
79+
try {
80+
const payload = JSON.parse(error);
81+
if (SelectAccountPayload.is(payload)) {
82+
this.setState({ selectAccountError: payload });
83+
}
84+
} catch (error) {
85+
console.log(error);
86+
}
87+
}
88+
}
89+
});
90+
} catch (error) {
91+
console.log(error)
92+
}
93+
};
94+
6595
render() {
96+
if (SelectAccountPayload.is(this.state.selectAccountError)) {
97+
return (<StartPage phase={StartPhase.Checking}>
98+
<div className="mt-2 flex flex-col space-y-8">
99+
<SelectAccountModal {...this.state.selectAccountError} close={() => {
100+
window.location.href = gitpodHostUrl.asAccessControl().toString();
101+
}} />
102+
</div>
103+
</StartPage>);
104+
}
105+
66106
let phase = StartPhase.Checking;
67107
let statusMessage = <p className="text-base text-gray-400">{this.state.stillParsing ? 'Parsing context …' : 'Preparing workspace …'}</p>;
68108

@@ -75,14 +115,10 @@ export default class CreateWorkspace extends React.Component<CreateWorkspaceProp
75115
</div>;
76116
break;
77117
case ErrorCodes.NOT_AUTHENTICATED:
78-
const authorizeUrl = gitpodHostUrl.withApi({
79-
pathname: '/authorize',
80-
search: `returnTo=${encodeURIComponent(window.location.toString())}&host=${error.data.host}&scopes=${error.data.scopes.join(',')}`
81-
}).toString();
82-
window.location.href = authorizeUrl;
83118
statusMessage = <div className="mt-2 flex flex-col space-y-8">
84-
<p className="text-base w-96">Redirecting to authorize with {error.data.host}</p>
85-
<a href={authorizeUrl}><button className="secondary">Authorize with {error.data.host}</button></a>
119+
<button className="secondary" onClick={() => {
120+
this.tryAuthorize(error?.data.host, error?.data.scopes)
121+
}}>Authorize with {error.data.host}</button>
86122
</div>;
87123
break;
88124
case ErrorCodes.USER_BLOCKED:

components/server/src/auth/generic-auth-provider.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -340,7 +340,9 @@ export class GenericAuthProvider implements AuthProvider {
340340

341341
if (SelectAccountException.is(err)) {
342342
this.selectAccountCookie.set(response, err.payload);
343-
const url = this.env.hostUrl.with({ pathname: '/select-account' }).toString();
343+
344+
// option 1: send as GET param on redirect
345+
const url = this.env.hostUrl.with({ pathname: '/flow-result', search: "message=error:" + Buffer.from(JSON.stringify(err.payload), "utf-8").toString('base64') }).toString();
344346
response.redirect(url);
345347
return;
346348
}

0 commit comments

Comments
 (0)