Skip to content

Commit 4fb4a03

Browse files
committed
[server/dashboard] stop moving identities
* allow our users to disconnect provider identities from their accounts * when a user tries to connect with a provider, for which there is already a connection to anther account, we redirect to an assistance page. a summary should help to review both accounts. in the end, any user may decide to move to a single account by disconnecting the provider identities from the other account. this way we can guarantee to not automagically lock out users from accounts with subscriptions or any meaningful data. * show `Connected as` * update terms renderer Signed-off-by: Alex Tugarev <[email protected]>
1 parent f89fb4a commit 4fb4a03

14 files changed

+494
-32
lines changed
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<!doctype html>
2+
<!--
3+
Copyright (c) 2021 Gitpod GmbH. All rights reserved.
4+
Licensed under the GNU Affero General Public License (AGPL).
5+
See License-AGPL.txt in the project root for license information.
6+
-->
7+
8+
<html lang="en">
9+
<head>
10+
<meta charset="utf-8">
11+
<meta name="viewport" content="user-scalable=0, initial-scale=1, minimum-scale=1, width=device-width, height=device-height">
12+
<!-- PWA primary color -->
13+
<meta name="theme-color" content="#000000">
14+
<link rel="manifest" href="/manifest.json">
15+
<link rel="apple-touch-icon" type="image/png" href="/images/apple-touch-icon.png" sizes="180x180"/>
16+
<link rel="icon" type="image/png" href="/images/gitpod-196x196.png" sizes="196x196"/>
17+
<link rel="icon" type="image/svg+xml" href="/images/gitpod.svg" sizes="any"/>
18+
<link rel="stylesheet" href="/styles.css"/>
19+
<link rel="stylesheet" href="//fonts.googleapis.com/css?family=Montserrat" />
20+
<title>Select An Account - Gitpod</title>
21+
<meta name="description" content="Describe your dev environment as code and get fully prebuilt, ready-to-code development environments for any GitLab, GitHub, and Bitbucket project.">
22+
<meta name="keywords" content="dev environment, development environment, devops, cloud ide, github ide, gitlab ide, javascript, online ide, web ide, code review">
23+
</head>
24+
<body>
25+
<noscript>
26+
You need to enable JavaScript to run this app.
27+
</noscript>
28+
<div id="root"></div>
29+
<script crossorigin="anonymous" src="/libs/moment.min.js"></script>
30+
<script crossorigin="anonymous" src="/libs/react.production.min.js"></script>
31+
<script crossorigin="anonymous" src="/libs/react-dom.production.min.js"></script>
32+
<script src="/select-account.js"></script>
33+
</body>
34+
</html>

components/dashboard/public/styles.css

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -826,7 +826,6 @@ footer .logo-icon {
826826

827827

828828
.access-control__card-container {
829-
width: 31%;
830829
min-width: 300px;
831830
margin-bottom: 30px !important;
832831
}

components/dashboard/src/components/access-control/access-control.tsx

Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import CardContent from '@material-ui/core/CardActions';
2020
import DialogContentText from '@material-ui/core/DialogContentText';
2121
import DialogContent from '@material-ui/core/DialogContent';
2222
import Dialog from '@material-ui/core/Dialog';
23+
import HighlightOffOutlined from '@material-ui/icons/HighlightOffOutlined';
2324
import DialogTitle from '@material-ui/core/DialogTitle';
2425
import DialogActions from '@material-ui/core/DialogActions';
2526
import { themeMode } from '../../withRoot';
@@ -37,6 +38,9 @@ interface AccessControlState {
3738
newScopes?: Map<string, Set<string>>;
3839
notification?: { hostToBeReviewed: string } | { updatedHost: string; updatedScopes: string[] };
3940
user?: User;
41+
disconnectDialog?: {
42+
authHost: string;
43+
};
4044
}
4145
interface AccessControlProps {
4246
service: GitpodService;
@@ -185,7 +189,7 @@ export class AccessControl extends React.Component<AccessControlProps, AccessCon
185189
<div>
186190
<Toolbar style={{ padding: 0 }}>
187191
<div style={{ width: '100%', justifyContent: 'space-between', marginTop: 30 }}>
188-
<Typography variant="h4" style={{ textAlign: 'center' }}>Access Control</Typography>
192+
<Typography variant="h4">Access Control</Typography>
189193
</div>
190194
</Toolbar>
191195
<Toolbar style={{ display: 'flex', width: '100%', justifyContent: 'space-between' }}>
@@ -195,6 +199,7 @@ export class AccessControl extends React.Component<AccessControlProps, AccessCon
195199
</Toolbar>
196200
{this.renderTokenContainer()}
197201
{this.renderInfoDialog()}
202+
{this.renderDisconnectDialog()}
198203
</div>
199204
);
200205
}
@@ -323,7 +328,7 @@ export class AccessControl extends React.Component<AccessControlProps, AccessCon
323328
const icon = this.getIcon(provider);
324329
const dirty = !this.equals(oldScopes, newScopes);
325330
const identity = this.state.user && this.state.user.identities.find(i => i.authProviderId === provider.authProviderId);
326-
return (<Card key={this.renderKey++}
331+
return (<Card key={`provider-token-${this.renderKey++}`}
327332
style={{
328333
verticalAlign: "top",
329334
textAlign: 'center',
@@ -370,6 +375,15 @@ export class AccessControl extends React.Component<AccessControlProps, AccessCon
370375
</CardContent>
371376
</Grid>
372377

378+
{identity && (<Grid item direction="column">
379+
<span style={{ fontSize: "80%" }}>
380+
Connected as <strong>{identity.authName}</strong>
381+
<IconButton style={{ padding: '4px', marginLeft: '2px' }} onClick={() => this.setState({ disconnectDialog: { authHost: provider.host } })} title="Disconnect">
382+
<HighlightOffOutlined fontSize="small" style={{ verticalAlign: 'middle', color: 'var(--font-color2)' }} />
383+
</IconButton>
384+
</span>
385+
</Grid>)}
386+
373387
<Grid item direction="column">
374388
<CardActions style={{ display: 'block', textAlign: 'center', paddingTop: 15, paddingRight: 10, paddingBottom: 12 }} disableActionSpacing={true}>
375389
{!provider.isReadonly && (
@@ -447,4 +461,63 @@ export class AccessControl extends React.Component<AccessControlProps, AccessCon
447461
search: `returnTo=${returnTo}&host=${provider}&override=true&scopes=${scopes.join(',')}`
448462
}).toString();
449463
}
464+
465+
protected renderDisconnectDialog() {
466+
const { disconnectDialog, user, authProviders } = this.state;
467+
const authHost = disconnectDialog?.authHost;
468+
const authProvider = authProviders.find(a => a.host === authHost);
469+
if (!disconnectDialog || !user || !authProvider) {
470+
return;
471+
}
472+
473+
let message: JSX.Element;
474+
const handleCancel = () => {
475+
this.setState({
476+
disconnectDialog: undefined
477+
});
478+
};
479+
let buttonLabel: string;
480+
let handleButton: () => void;
481+
482+
const thisUrl = new GitpodHostUrl(new URL(window.location.toString()));
483+
const otherIdentitiesOfUser = user.identities.filter(i => i.authProviderId !== authProvider.authProviderId);
484+
if (otherIdentitiesOfUser.length === 0) {
485+
message = (<DialogContentText>
486+
Disconnecting the single remaining provider would make your account unreachable. Please go the settings, if you want to delete the account.
487+
</DialogContentText>);
488+
489+
const settingsUrl = thisUrl.asSettings().toString();
490+
491+
buttonLabel = "Settings";
492+
handleButton = () => window.location.href = settingsUrl;
493+
} else {
494+
message = (<DialogContentText>
495+
You are about to disconnect {authHost}.
496+
</DialogContentText>);
497+
498+
const returnTo = encodeURIComponent(thisUrl.with({ search: `updated=${authHost}` }).toString());
499+
const deauthorizeUrl = thisUrl.withApi({
500+
pathname: '/deauthorize',
501+
search: `returnTo=${returnTo}&host=${authHost}`
502+
}).toString();
503+
504+
buttonLabel = "Proceed";
505+
handleButton = () => window.location.href = deauthorizeUrl;
506+
}
507+
508+
return (
509+
<Dialog
510+
key="diconnect-dialog"
511+
open={!!disconnectDialog}
512+
onClose={handleCancel}
513+
>
514+
<DialogTitle>Disconnect {authHost}</DialogTitle>
515+
<DialogContent>{message}</DialogContent>
516+
<DialogActions>
517+
<Button onClick={handleButton} variant="outlined" color="secondary" autoFocus>{buttonLabel}</Button>
518+
<Button onClick={handleCancel} variant="outlined" color="primary" autoFocus>Cancel</Button>
519+
</DialogActions>
520+
</Dialog>
521+
);
522+
}
450523
}

components/dashboard/src/components/tos/terms-of-service.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -79,11 +79,15 @@ export class TermsOfService extends React.Component<TermsOfServiceProps, TermsOf
7979

8080
protected actionUrl = this.gitpodHost.withApi({ pathname: '/tos/proceed' }).toString();
8181
render() {
82-
const content = this.renderMd(this.state.terms?.content);
8382
const acceptsTos = this.state.acceptsTos;
84-
const updateMessage = this.renderMd(this.state.terms?.updateMessage);
8583
const update = this.state.isUpdate;
8684

85+
let tosContentRendered = this.renderMd(update ? this.state.terms?.updateMessage : this.state.terms?.content);
86+
87+
if (!update && this.state.userInfo?.authHost) {
88+
tosContentRendered = tosContentRendered.replace("{{AUTH_HOST}}", this.state.userInfo?.authHost);
89+
}
90+
8791
let userSection: JSX.Element = <div></div>;
8892
if (this.state.userInfo) {
8993
const avatarUrl: string | undefined = this.state.userInfo.avatarUrl;
@@ -133,7 +137,7 @@ export class TermsOfService extends React.Component<TermsOfServiceProps, TermsOf
133137
<form ref={this.formRef} action={this.actionUrl} method="post" id="accept-tos-form">
134138
<input type="hidden" id="flowId" name="flowId" value={this.state.flowId} />
135139
<div className="tos-checks">
136-
<Typography className="tos-content" dangerouslySetInnerHTML={{ __html: update ? updateMessage : content }} />
140+
<Typography className="tos-content" dangerouslySetInnerHTML={{ __html: tosContentRendered }} />
137141
<p>
138142
<label style={{ display: 'none', alignItems: 'center' }}>
139143
<Checkbox
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
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 "reflect-metadata";
8+
import * as React from 'react';
9+
import * as Cookies from 'js-cookie';
10+
11+
import { createGitpodService } from './service-factory';
12+
import { ApplicationFrame } from "./components/page-frame";
13+
import { GitpodHostUrl } from "@gitpod/gitpod-protocol/lib/util/gitpod-host-url";
14+
15+
import Avatar from "@material-ui/core/Avatar";
16+
import Card from "@material-ui/core/Card";
17+
import CardContent from "@material-ui/core/CardContent";
18+
import Typography from "@material-ui/core/Typography";
19+
import { ButtonWithProgress } from "./components/button-with-progress";
20+
import { themeMode } from './withRoot';
21+
22+
import { renderEntrypoint } from "./entrypoint";
23+
24+
const service = createGitpodService();
25+
const accountHints = (() => {
26+
try {
27+
const hints = Cookies.getJSON('SelectAccountCookie');
28+
if (accountHints || "currentUser" in hints || "otherUser" in hints) {
29+
return hints;
30+
}
31+
} catch (error) {
32+
console.log(error);
33+
}
34+
})();
35+
36+
export class ProceedWithAccount extends React.Component<{}, {}> {
37+
38+
render() {
39+
if (!accountHints) {
40+
return (
41+
<ApplicationFrame service={service}>
42+
<h3>Oh no! We are missing a cookie here. 🍪</h3>
43+
<h2>A detailed errors message is missing.</h2>
44+
</ApplicationFrame>
45+
);
46+
}
47+
const { currentUser, otherUser } = accountHints;
48+
49+
const accessControlUrl = new GitpodHostUrl(window.location.href).asAccessControl().toString();
50+
51+
const loginUrl = new GitpodHostUrl(window.location.href).withApi({
52+
pathname: '/login/',
53+
search: `host=${otherUser.authHost}&returnTo=${encodeURIComponent(accessControlUrl)}`
54+
}).toString();
55+
56+
const logoutUrl = new GitpodHostUrl(window.location.toString()).withApi({
57+
pathname: "/logout",
58+
search: `returnTo=${encodeURIComponent(loginUrl)}`
59+
}).toString();
60+
61+
const onGoBackClicked = () => {
62+
window.location.href = accessControlUrl;
63+
};
64+
const onSwitchAccountsClicked = () => {
65+
window.location.href = logoutUrl;
66+
};
67+
68+
return (
69+
<ApplicationFrame service={service}>
70+
<div className='content content-area'>
71+
72+
<h3 style={{ textAlign: "center" }}>Select An Account</h3>
73+
74+
<Typography component="p" style={{ textAlign: "center", fontSize: "110%", marginTop: "50px" }}>
75+
You are attempting to authorize a provider that is already connected with your other account on Gitpod.
76+
<br />
77+
Please switch to the other account or disconnect the provider from your current account in Access Control.
78+
</Typography>
79+
80+
<div style={{ display: "flex", marginTop: "50px", width: "100%" }}>
81+
<div style={{ flex: "50%" }}>
82+
<div style={{
83+
width: "300px",
84+
margin: "0 20px 0 auto"
85+
}}>
86+
<Typography component="p" style={{ textAlign: "center", padding: "20px", fontSize: "110%" }}>
87+
Current account
88+
</Typography>
89+
<Card>
90+
<CardContent
91+
style={{
92+
textAlign: "center"
93+
}}
94+
>
95+
<div>
96+
<Avatar
97+
style={{
98+
margin: "40px auto 40px",
99+
width: 110,
100+
height: 110
101+
}}
102+
alt={currentUser.name} src={currentUser.avatarUrl}
103+
/>
104+
<Typography component="p" style={{ textAlign: "center", padding: "20px", fontSize: "110%" }}>
105+
Connected as <strong>@{currentUser.authName}</strong>
106+
<br />
107+
from <strong>{currentUser.authHost}</strong>
108+
</Typography>
109+
</div>
110+
</CardContent>
111+
</Card>
112+
</div>
113+
</div>
114+
<div style={{ flex: "50%" }}>
115+
<div style={{
116+
width: "300px",
117+
margin: "0 auto 0 20px"
118+
}}>
119+
<Typography component="p" style={{ textAlign: "center", padding: "20px", fontSize: "110%" }}>
120+
Other account
121+
</Typography>
122+
<Card>
123+
<CardContent
124+
style={{
125+
textAlign: "center"
126+
}}
127+
>
128+
<div>
129+
<Avatar
130+
style={{
131+
margin: "40px auto 40px",
132+
width: 110,
133+
height: 110
134+
}}
135+
alt={otherUser.name} src={otherUser.avatarUrl}
136+
/>
137+
<Typography component="p" style={{ textAlign: "center", padding: "20px", fontSize: "110%" }}>
138+
Connected as <strong>@{otherUser.authName}</strong>
139+
<br />
140+
from <strong>{otherUser.authHost}</strong>
141+
</Typography>
142+
</div>
143+
</CardContent>
144+
</Card>
145+
</div>
146+
</div>
147+
</div>
148+
149+
<div style={{ marginTop: "100px", textAlign: "center" }}>
150+
<ButtonWithProgress
151+
className='button'
152+
onClick={onGoBackClicked}
153+
variant='outlined'
154+
color={'primary'}>
155+
{'Back To Access Control'}
156+
</ButtonWithProgress>
157+
158+
<ButtonWithProgress
159+
style={{ marginLeft: "20px" }}
160+
className='button'
161+
onClick={onSwitchAccountsClicked}
162+
variant='outlined'
163+
color={'secondary'}>
164+
{'Switch To Your Other Account'}
165+
</ButtonWithProgress>
166+
</div>
167+
168+
169+
</div>
170+
</ApplicationFrame>
171+
);
172+
}
173+
174+
protected getAuthProviderType(authProviderType: string): string {
175+
switch (authProviderType) {
176+
case "GitHub": return themeMode === 'light' ? "/images/github.svg" : "/images/github.dark.svg";
177+
case "GitLab": return themeMode === 'light' ? "/images/gitlab.svg" : "/images/gitlab.dark.svg";
178+
case "Bitbucket": return themeMode === 'light' ? "/images/bitbucket.svg" : "/images/bitbucket.dark.svg";
179+
default: return "";
180+
}
181+
}
182+
}
183+
184+
renderEntrypoint(ProceedWithAccount);

components/dashboard/webpack.entrypoints.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ module.exports = function entrypoints(srcPath, eeSrcPath, isOSSBuild) {
1212
'create-workspace-from-ref': `${srcPath}/create-workspace-from-ref.tsx`,
1313
'404': `${srcPath}/404.tsx`,
1414
'sorry': `${srcPath}/sorry.tsx`,
15+
'select-account': `${srcPath}/select-account.tsx`,
1516
'blocked': `${srcPath}/blocked.tsx`,
1617
'bootanimation': `${srcPath}/bootanimation.ts`,
1718
'access-control': `${srcPath}/access-control.tsx`,

0 commit comments

Comments
 (0)