Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Properly log out on Account Deletion #2692

Merged
merged 2 commits into from
Jan 13, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ export class AccessControl extends React.Component<AccessControlProps, AccessCon
case ErrorCodes.SETUP_REQUIRED:
window.location.href = new GitpodHostUrl(window.location.toString()).with({ pathname: "first-steps" }).toString();
break;
case ErrorCodes.USER_DELETED:
window.location.href = new GitpodHostUrl(window.location.toString()).asApiLogout().toString();
break;
case ErrorCodes.NOT_AUTHENTICATED:
this.redirectToLogin();
break;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,9 @@ export class CreateWorkspace extends React.Component<CreateWorkspaceProps, Creat
const thisUrl = window.location.toString();
window.location.href = new GitpodHostUrl(thisUrl).withApi({ pathname: "/tos", search: `mode=update&returnTo=${encodeURIComponent(thisUrl)}` }).toString();
return;
case ErrorCodes.USER_DELETED:
window.location.href = new GitpodHostUrl(window.location.toString()).asApiLogout().toString();
return;
case ErrorCodes.NOT_AUTHENTICATED:
if (data) {
this.setState({
Expand Down
3 changes: 3 additions & 0 deletions components/dashboard/src/components/create/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ export async function start(Component: React.ComponentType<CreateWorkspaceProps>
case ErrorCodes.SETUP_REQUIRED:
window.location.href = new GitpodHostUrl(window.location.toString()).with({ pathname: "first-steps" }).toString();
break;
case ErrorCodes.USER_DELETED:
window.location.href = new GitpodHostUrl(window.location.toString()).asApiLogout().toString();
break;
case ErrorCodes.NOT_AUTHENTICATED:
// redirect to website
return redirectNotAuthenticated(service);
Expand Down
148 changes: 74 additions & 74 deletions components/dashboard/src/components/delete-account-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* See License-AGPL.txt in the project root for license information.
*/

import { GitpodService } from "@gitpod/gitpod-protocol";
import { GitpodService, User } from "@gitpod/gitpod-protocol";
import * as React from 'react';
import Typography from "@material-ui/core/Typography";
import Button from "@material-ui/core/Button";
Expand All @@ -15,15 +15,16 @@ import DialogActions from "@material-ui/core/DialogActions";
import CircularProgress from "@material-ui/core/CircularProgress";
import TextField from "@material-ui/core/TextField";
import { GitpodHostUrl } from "@gitpod/gitpod-protocol/lib/util/gitpod-host-url";
import { WithBranding } from "./with-branding";

export interface DeleteAccountViewProps {
service: GitpodService;
user: User;
}

type DeletionStatus = "confirmation" | "in-progress" | "done" | "error";
interface DeleteAccountViewState {
status?: DeletionStatus;
username?: string;
enteredUsername?: string;
}

Expand All @@ -34,94 +35,93 @@ export class DeleteAccountView extends React.Component<DeleteAccountViewProps, D
this.state = {};
}

componentWillMount() {
this.getUsername();
}

render() {
return <React.Fragment>
<Dialog open={!!this.state.status}>
{this.state.status == "confirmation" && <React.Fragment>
<DialogTitle>We are sorry to see you go.</DialogTitle>
<DialogContent>
<Typography variant="body1" style={{width:"100%"}}>
<div><i>Beware:</i> unexpected bad things will happen if you don't read this!</div>
<div>
<ul>
<li>Deleting your account will delete all of your old workspaces. Once your account is deleted we can no longer restore those workspaces.</li>
<li>If you bought a subscription through GitHub, please cancel your current plan in the GitHub marketplace once you have deleted your account.</li>
</ul>
</div>
<div>
Please type in your username <b>{this.state.username}</b> to confirm: <br />
<TextField value={this.state.enteredUsername} onChange={e => this.setState({enteredUsername: (e.target as HTMLInputElement).value})} />
</div>
</Typography>
</DialogContent>
<DialogActions>
<Button onClick={() => this.setState({status: undefined})} variant="outlined" color="primary">Cancel</Button>
<Button
onClick={() => this.deleteAccount()} variant="outlined"
color="secondary" disabled={this.state.username != this.state.enteredUsername}>
I'm Sure. Delete My Account!
<Dialog open={!!this.state.status}>
{this.state.status === "confirmation" && <React.Fragment>
<DialogTitle>We are sorry to see you go</DialogTitle>
<DialogContent>
<Typography variant="body1" style={{ width: "100%" }}>
<div><i>Beware:</i> unexpected bad things will happen if you don't read this!</div>
<div>
<ul>
<li>Deleting your account will delete all of your old workspaces. Once your account is deleted we can no longer restore those workspaces.</li>
<li>If you bought a subscription through GitHub, please cancel your current plan in the GitHub marketplace once you have deleted your account.</li>
</ul>
</div>
<div>
Please type in your username <b>{this.props.user.name}</b> to confirm: <br />
<TextField value={this.state.enteredUsername} onChange={e => this.setState({ enteredUsername: (e.target as HTMLInputElement).value })} />
</div>
</Typography>
</DialogContent>
<DialogActions>
<Button onClick={() => this.setState({ status: undefined })} variant="outlined" color="primary">Cancel</Button>
<Button
onClick={() => this.deleteAccount()} variant="outlined"
color="secondary" disabled={this.props.user.name !== this.state.enteredUsername}>
I'm Sure. Delete My Account!
</Button>
</DialogActions>
</React.Fragment>}
</DialogActions>
</React.Fragment>}

{this.state.status == "in-progress" && <React.Fragment>
<DialogTitle>We are sorry to see you go.</DialogTitle>
<DialogContent>
<CircularProgress />
</DialogContent>
</React.Fragment>}
{this.state.status === "in-progress" && <React.Fragment>
<DialogTitle>We are sorry to see you go</DialogTitle>
<DialogContent>
<CircularProgress />
</DialogContent>
</React.Fragment>}

{this.state.status == "done" && <React.Fragment>
<DialogTitle>We are sorry to see you go.</DialogTitle>
<DialogContent>
<Typography variant="body1">
<div>Your account has been deleted &mdash; all that's left is to log out using the finish button below.</div>
<div>If you ever decide to give Gitpod another try, just sign up like you did the first time. No hard feelings 🙂</div>
</Typography>
</DialogContent>
<DialogActions>
<Button onClick={() => window.location.href=new GitpodHostUrl(window.location.toString()).withApi({pathname: '/logout'}).toString()} color="secondary" variant="outlined">Finish</Button>
</DialogActions>
</React.Fragment>}
{this.state.status === "done" && <React.Fragment>
<DialogTitle>We are sorry to see you go</DialogTitle>
<DialogContent>
<Typography variant="body1">
<div>Redirecting…</div>
</Typography>
</DialogContent>
</React.Fragment>}

{this.state.status == "error" && <React.Fragment>
<DialogTitle>Something went wrong.</DialogTitle>
<DialogContent>
<Typography variant="body1">
<div>We are sorry - something went wrong while deleting your account. </div>
<div>Please get in touch at <a href="mailto:support@gitpod.io">support@gitpod.io</a>.</div>
</Typography>
</DialogContent>
<DialogActions>
<Button onClick={() => this.setState({ status: undefined })} color="secondary" variant="outlined">Ok</Button>
</DialogActions>
</React.Fragment>}
</Dialog>
{this.state.status === "error" && <React.Fragment>
<DialogTitle>Something went wrong</DialogTitle>
<DialogContent>
<Typography variant="body1">
<div>We are sorry – something went wrong while deleting your account.</div>
<div>Please get in touch at <a href="mailto:support@gitpod.io">support@gitpod.io</a>.</div>
<div>As a precaution, your browser will still be redirected to the logout.</div>
csweichel marked this conversation as resolved.
Show resolved Hide resolved
</Typography>
</DialogContent>
<DialogActions>
<Button onClick={this.doLogout} color="secondary" variant="outlined">Ok</Button>
</DialogActions>
</React.Fragment>}
</Dialog>

<div style={{ textAlign: "right", marginTop: '1em' }}>
<a href="javascript:void(0)" onClick={() => this.setState({ status: "confirmation" })} style={{
fontSize: '0.875rem',
pointerEvents: this.state.status ? 'none' : 'auto',
}}>delete my account</a>
</div>
</React.Fragment>
<div style={{ textAlign: "right", marginTop: '1em' }}>
<a href="javascript:void(0)" onClick={() => this.setState({ status: "confirmation" })} style={{
fontSize: '0.875rem',
pointerEvents: this.state.status ? 'none' : 'auto',
}}>delete my account</a>
</div>
</React.Fragment>;
}

protected async getUsername() {
const user = await this.props.service.server.getLoggedInUser();
this.setState({ username: user.name });
}
protected doLogout = () => {
(async () => {
const branding = await WithBranding.getBranding(this.props.service);
const target = branding?.homepage ?? "https://www.gitpod.io/";
window.location.href = new GitpodHostUrl(window.location.toString()).withApi({ pathname: '/logout', search: `returnTo=${target}` }).toString();
})();
};

protected async deleteAccount() {
this.setState({ status: "in-progress" });
try {
await this.props.service.server.deleteAccount();

this.setState({ status: "done" });

// immediately log out and redirect to configured homepage URL.
this.doLogout();
} catch (err) {
console.log(err);
this.setState({ status: "error" });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@ export class InstallGithubApp extends React.Component<{}, InstallGithubAppState>
}).toString();
window.location.href = url.toString();
break;
case ErrorCodes.USER_DELETED:
window.location.href = new GitpodHostUrl(window.location.toString()).asApiLogout().toString();
break;
default:
}
}
Expand Down
5 changes: 0 additions & 5 deletions components/dashboard/src/components/repositories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,9 @@

import * as React from 'react';
import { WhitelistedRepository, GitpodService, DisposableCollection, GitpodServer } from '@gitpod/gitpod-protocol';
import { ErrorCodes } from '@gitpod/gitpod-protocol/lib/messaging/error';
import Grid from '@material-ui/core/Grid';
import Paper from '@material-ui/core/Paper';
import Typography from '@material-ui/core/Typography';
import { ResponseError } from 'vscode-jsonrpc';
import RepositoryEntry from './repository-entry';
import { log } from '@gitpod/gitpod-protocol/lib/util/logging';

Expand Down Expand Up @@ -43,9 +41,6 @@ export class FeaturedRepositories extends React.Component<FeaturedRepositoryProp
this.setState({ repositories });
} catch (err) {
log.error(err);
if (err instanceof ResponseError && err.code === ErrorCodes.NOT_AUTHENTICATED) {
return;
}
}
}

Expand Down
12 changes: 9 additions & 3 deletions components/dashboard/src/components/settings/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import * as React from 'react';
import "reflect-metadata";
import debounce = require('lodash.debounce');
import { CancellationTokenSource, CancellationToken } from 'vscode-jsonrpc/lib/cancellation'
import { CancellationTokenSource, CancellationToken } from 'vscode-jsonrpc/lib/cancellation';
import { UserEnvVars } from '../user-env-vars';
import { ApplicationFrame } from '../page-frame';
import { ErrorCodes } from '@gitpod/gitpod-protocol/lib/messaging/error';
Expand All @@ -29,7 +29,7 @@ interface SettingsProps {

interface SettingsState {
user?: User;
hasIDESettingsPermission?: boolean
hasIDESettingsPermission?: boolean;
}

export class Settings extends React.Component<SettingsProps, SettingsState> {
Expand Down Expand Up @@ -57,6 +57,9 @@ export class Settings extends React.Component<SettingsProps, SettingsState> {
search: 'returnTo=' + encodeURIComponent(window.location.toString())
}).toString();
break;
case ErrorCodes.USER_DELETED:
window.location.href = new GitpodHostUrl(window.location.toString()).asApiLogout().toString();
break;
default:
}
}
Expand All @@ -82,7 +85,7 @@ export class Settings extends React.Component<SettingsProps, SettingsState> {
<h3 style={{ marginTop: 50 }}>Feature Preview</h3>
{this.state.user && <FeatureSettings service={this.props.service} user={this.state.user} onChange={this.onChange} />}

<DeleteAccountView service={this.props.service} />
{this.state.user && <DeleteAccountView service={this.props.service} user={this.state.user} />}
</Paper>
</ApplicationFrame>
);
Expand All @@ -103,6 +106,9 @@ export class Settings extends React.Component<SettingsProps, SettingsState> {
private commitTokenSource: CancellationTokenSource | undefined;
private commit = debounce(async (update: Partial<User>, token: CancellationToken) => {
try {
if (token.isCancellationRequested) {
return;
}
const user = await this.props.service.server.updateLoggedInUser(update);
if (token.isCancellationRequested) {
return;
Expand Down
3 changes: 3 additions & 0 deletions components/dashboard/src/components/start-workspace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,9 @@ export class StartWorkspace extends React.Component<StartWorkspaceProps, StartWo
this.redirectTo(url);
}
return;
case ErrorCodes.USER_DELETED:
window.location.href = new GitpodHostUrl(window.location.toString()).asApiLogout().toString();
return;
default:
}
}
Expand Down
3 changes: 3 additions & 0 deletions components/dashboard/src/components/workspaces.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,9 @@ class Workspaces extends React.Component<WorkspacesProps, WorkspacesState> {
return;
case ErrorCodes.USER_BLOCKED:
window.location.href = getBlockedUrl();
return;
case ErrorCodes.USER_DELETED:
window.location.href = new GitpodHostUrl(window.location.toString()).asApiLogout().toString();
return;
default:
return;
Expand Down
4 changes: 4 additions & 0 deletions components/gitpod-protocol/src/util/gitpod-host-url.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,4 +154,8 @@ export class GitpodHostUrl {
return this.with({ pathname: '/sorry', hash: message });
}

asApiLogout(): GitpodHostUrl {
return this.withApi(url => ({ pathname: '/logout/' }));
}

}
14 changes: 13 additions & 1 deletion components/server/src/storage/gcloud-storage-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,22 @@ export class GCloudStorageClient implements StorageClient {
}

async deleteBucket(bucketName: string): Promise<any> {
const { response, err } = await this.try(() => this.storage.bucket(bucketName).delete().then(responses => responses[0]));
log.info(`Deleting a bucket: ${bucketName}`);
const bucket = this.storage.bucket(bucketName);
AlexTugarev marked this conversation as resolved.
Show resolved Hide resolved
try {
// try deleting all files first, otherwise bucket deletion will fail
await bucket.deleteFiles({ force: true });
// try again, just because the backend is lazy
await bucket.deleteFiles({ force: true });
} catch(err) {
log.error(`Failed to empty a bucket: ${bucketName}`, err);
}

const { response, err } = await this.try(() => bucket.delete().then(responses => responses[0]));
if (response) {
this.checkStatus(response, 'delete bucket', [204, 404])
} else if (err) {
log.error(`Failed to delete a bucket: ${bucketName}`, err);
throw err;
}
}
Expand Down
3 changes: 3 additions & 0 deletions components/server/src/user/user-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -490,6 +490,9 @@ export class UserController {
if (returnToURL.toLowerCase().startsWith(`${hostUrl.protocol}//${hostUrl.host}`.toLowerCase())) {
return returnToURL;
}
if (returnToURL.toLowerCase().startsWith(this.env.brandingConfig.homepage.toLowerCase())) {
return returnToURL;
}
}
}
}