Skip to content

Commit

Permalink
Add phone verification
Browse files Browse the repository at this point in the history
  • Loading branch information
svenefftinge committed Aug 19, 2022
1 parent 135bc0f commit 7f2ab4d
Show file tree
Hide file tree
Showing 29 changed files with 538 additions and 20 deletions.
1 change: 1 addition & 0 deletions components/dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"query-string": "^7.1.1",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"react-intl-tel-input": "^8.2.0",
"react-router-dom": "^5.2.0",
"xterm": "^4.11.0",
"xterm-addon-fit": "^0.5.0"
Expand Down
4 changes: 4 additions & 0 deletions components/dashboard/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -79,20 +79,24 @@

textarea,
input[type="text"],
input[type="tel"],
input[type="number"],
input[type="search"],
input[type="password"],
.country-list,
select {
@apply block w-56 text-gray-600 dark:text-gray-400 bg-white dark:bg-gray-800 rounded-md border border-gray-300 dark:border-gray-500 focus:border-gray-400 dark:focus:border-gray-400 focus:ring-0;
}
textarea::placeholder,
input[type="text"]::placeholder,
input[type="tel"]::placeholder,
input[type="number"]::placeholder,
input[type="search"]::placeholder,
input[type="password"]::placeholder {
@apply text-gray-400 dark:text-gray-500;
}
input[type="text"].error,
input[type="tel"].error,
input[type="number"].error,
input[type="search"].error,
input[type="password"].error,
Expand Down
3 changes: 3 additions & 0 deletions components/dashboard/src/start/StartPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@
* See License-AGPL.txt in the project root for license information.
*/

import { ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error";
import { useEffect } from "react";
import Alert from "../components/Alert";
import gitpodIconUA from "../icons/gitpod.svg";
import { gitpodHostUrl } from "../service/service";
import { VerifyModal } from "./VerifyModal";

export enum StartPhase {
Checking = 0,
Expand Down Expand Up @@ -106,6 +108,7 @@ export function StartPage(props: StartPageProps) {
{typeof phase === "number" && phase < StartPhase.IdeReady && (
<ProgressBar phase={phase} error={!!error} />
)}
{error && error.code === ErrorCodes.NEEDS_VERIFICATION && <VerifyModal />}
{error && <StartError error={error} />}
{props.children}
{props.showLatestIdeWarning && (
Expand Down
201 changes: 201 additions & 0 deletions components/dashboard/src/start/VerifyModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
/**
* Copyright (c) 2022 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 { useState } from "react";
import Alert, { AlertType } from "../components/Alert";
import Modal from "../components/Modal";
import { getGitpodService } from "../service/service";
import PhoneInput from "react-intl-tel-input";
import "react-intl-tel-input/dist/main.css";
import "./phone-input.css";

interface VerifyModalState {
phoneNumber?: string;
phoneNumberValid?: boolean;
sent?: Date;
message?: {
type: AlertType;
text: string;
};
token?: string;
verified?: boolean;
}

export function VerifyModal() {
const [state, setState] = useState<VerifyModalState>({});

if (!state.sent) {
const sendCode = () => {
getGitpodService().server.sendPhoneNumberVerificationToken(state.phoneNumber || "");
setState({
...state,
sent: new Date(),
});
return true;
};
return (
<Modal
onClose={() => {}}
closeable={false}
onEnter={sendCode}
title="User Validation Required"
buttons={
<div>
<button className="ml-2" disabled={!state.phoneNumberValid} onClick={sendCode}>
Send Code via SMS
</button>
</div>
}
visible={true}
>
<Alert type="warning" className="mt-2">
To use Gitpod you'll need to validate your account with your phone number. This is required to
discourage and reduce abuse on Gitpod infrastructure.
</Alert>
<div className="mt-2 -mx-6 px-6 pt-4">
Enter a mobile phone number you would like to use to verify your account.
</div>
<div className="mt-4">
<h4>Mobile Phone Number</h4>
<PhoneInput
autoFocus={true}
containerClassName={"allow-dropdown w-full intl-tel-input"}
inputClassName={"w-full"}
allowDropdown={true}
defaultCountry={""}
autoHideDialCode={false}
onPhoneNumberChange={(isValid, phoneNumberRaw, countryData) => {
let phoneNumber = phoneNumberRaw;
if (!phoneNumber.startsWith("+") && !phoneNumber.startsWith("00")) {
phoneNumber = "+" + countryData.dialCode + phoneNumber;
}
setState({
...state,
phoneNumber,
phoneNumberValid: isValid,
});
}}
/>
</div>
</Modal>
);
} else if (!state.verified) {
const isTokenFilled = () => {
return state.token && /\d{6}/.test(state.token);
};
const verifyToken = async () => {
const verified = await getGitpodService().server.verifyPhoneNumberVerificationToken(
state.phoneNumber!,
state.token!,
);
if (verified) {
setState({
...state,
verified: true,
message: undefined,
});
} else {
setState({
...state,
message: {
type: "error",
text: `Invalid verification code.`,
},
});
}
return verified;
};

const reset = () => {
setState({
...state,
sent: undefined,
message: undefined,
token: undefined,
});
};
return (
<Modal
onClose={() => {}}
closeable={false}
onEnter={verifyToken}
title="User Validation Required"
buttons={
<div>
<button className="ml-2" disabled={!isTokenFilled()} onClick={verifyToken}>
Validate Account
</button>
</div>
}
visible={true}
>
<Alert type="warning" className="mt-2">
To use Gitpod you'll need to validate your account with your phone number. This is required to
discourage and reduce abuse on Gitpod infrastructure.
</Alert>
<div className="pt-4">
<button className="gp-link" onClick={reset}>
&larr; Use a different phone number
</button>
</div>
<div className="pt-4">
Enter the verification code we sent to {state.phoneNumber}.<br />
Having trouble?{" "}
<a className="gp-link" href="https://www.gitpod.io/contact/support">
Contact support
</a>
</div>
{state.message ? (
<Alert type={state.message.type} className="mt-4 py-3">
{state.message.text}
</Alert>
) : (
<></>
)}
<div className="mt-4">
<h4>Verification Code</h4>
<input
autoFocus={true}
className="w-full"
type="text"
placeholder="enter token send via sms"
onChange={(v) => {
setState({
...state,
token: v.currentTarget.value,
});
}}
/>
</div>
</Modal>
);
} else {
const continueStartWorkspace = () => {
window.location.reload();
return true;
};
return (
<Modal
onClose={continueStartWorkspace}
closeable={false}
onEnter={continueStartWorkspace}
title="User Validation Successful"
buttons={
<div>
<button className="ml-2" onClick={continueStartWorkspace}>
Continue
</button>
</div>
}
visible={true}
>
<Alert type="message" className="mt-2">
Your account has been successfully verified.
</Alert>
</Modal>
);
}
}
9 changes: 9 additions & 0 deletions components/dashboard/src/start/phone-input.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/**
* Copyright (c) 2022 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.
*/

.country-list {
width: 29rem !important;
}
3 changes: 3 additions & 0 deletions components/gitpod-protocol/src/gitpod-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,9 @@ export interface GitpodServer extends JsonRpcServer<GitpodClient>, AdminServer,
getLoggedInUser(): Promise<User>;
getTerms(): Promise<Terms>;
updateLoggedInUser(user: Partial<User>): Promise<User>;
needsVerification(): Promise<boolean>;
sendPhoneNumberVerificationToken(phoneNumber: string): Promise<void>;
verifyPhoneNumberVerificationToken(phoneNumber: string, token: string): Promise<boolean>;
getAuthProviders(): Promise<AuthProviderInfo[]>;
getOwnAuthProviders(): Promise<AuthProviderEntry[]>;
updateOwnAuthProvider(params: GitpodServer.UpdateOwnAuthProviderParams): Promise<AuthProviderEntry>;
Expand Down
3 changes: 3 additions & 0 deletions components/gitpod-protocol/src/messaging/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ export namespace ErrorCodes {
// 410 No User
export const SETUP_REQUIRED = 410;

// 411 No User
export const NEEDS_VERIFICATION = 411;

// 429 Too Many Requests
export const TOO_MANY_REQUESTS = 429;

Expand Down
2 changes: 2 additions & 0 deletions components/gitpod-protocol/src/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,8 @@ export interface AdditionalUserData {
workspaceClasses?: WorkspaceClasses;
// additional user profile data
profile?: ProfileDetails;
// verification date
lastVerificationTime?: string;
}

// The format in which we store User Profiles in
Expand Down
2 changes: 1 addition & 1 deletion components/gitpod-protocol/src/util/logging.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

const inspect: (object: any) => string = require("util").inspect; // undefined in frontend

let plainLogging: boolean = false; // set to true during development to get non JSON output
let plainLogging: boolean = true; // set to true during development to get non JSON output
let jsonLogging: boolean = false;
let component: string | undefined;
let version: string | undefined;
Expand Down
9 changes: 9 additions & 0 deletions components/server/ee/src/billing/entitlement-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
} from "@gitpod/gitpod-protocol";
import { log } from "@gitpod/gitpod-protocol/lib/util/logging";
import { inject, injectable } from "inversify";
import { VerificationService } from "../../../src/auth/verification-service";
import { EntitlementService, MayStartWorkspaceResult } from "../../../src/billing/entitlement-service";
import { Config } from "../../../src/config";
import { BillingModes } from "./billing-mode";
Expand All @@ -31,13 +32,21 @@ export class EntitlementServiceImpl implements EntitlementService {
@inject(EntitlementServiceChargebee) protected readonly chargebee: EntitlementServiceChargebee;
@inject(EntitlementServiceLicense) protected readonly license: EntitlementServiceLicense;
@inject(EntitlementServiceUBP) protected readonly ubp: EntitlementServiceUBP;
@inject(VerificationService) protected readonly verificationService: VerificationService;

async mayStartWorkspace(
user: User,
date: Date = new Date(),
runningInstances: Promise<WorkspaceInstance[]>,
): Promise<MayStartWorkspaceResult> {
try {
const verification = await this.verificationService.needsVerification(user);
if (verification) {
return {
mayStart: false,
needsVerification: true,
};
}
const billingMode = await this.billingModes.getBillingModeForUser(user, date);
let result;
switch (billingMode.mode) {
Expand Down
2 changes: 2 additions & 0 deletions components/server/ee/src/container-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ import { SnapshotService } from "./workspace/snapshot-service";
import { BitbucketAppSupport } from "./bitbucket/bitbucket-app-support";
import { UserCounter } from "./user/user-counter";
import { BitbucketServerApp } from "./prebuilds/bitbucket-server-app";
import { VerificationService } from "../../src/auth/verification-service";
import { EntitlementService } from "../../src/billing/entitlement-service";
import { EntitlementServiceChargebee } from "./billing/entitlement-service-chargebee";
import { BillingModes, BillingModesImpl } from "./billing/billing-mode";
Expand Down Expand Up @@ -86,6 +87,7 @@ export const productionEEContainerModule = new ContainerModule((bind, unbind, is
bind(BitbucketAppSupport).toSelf().inSingletonScope();
bind(GitHubEnterpriseApp).toSelf().inSingletonScope();
bind(BitbucketServerApp).toSelf().inSingletonScope();
bind(VerificationService).toSelf().inSingletonScope();

bind(UserCounter).toSelf().inSingletonScope();

Expand Down
3 changes: 3 additions & 0 deletions components/server/ee/src/workspace/gitpod-server-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,9 @@ export class GitpodServerEEImpl extends GitpodServerImpl {
if (result.mayStart) {
return; // green light from entitlement service
}
if (!!result.needsVerification) {
throw new ResponseError(ErrorCodes.NEEDS_VERIFICATION, `Please verify your account.`);
}
if (!!result.oufOfCredits) {
throw new ResponseError(
ErrorCodes.NOT_ENOUGH_CREDIT,
Expand Down
1 change: 1 addition & 0 deletions components/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@
"reflect-metadata": "^0.1.10",
"stripe": "^9.0.0",
"swot-js": "^1.0.3",
"twilio": "^3.78.0",
"uuid": "^8.3.2",
"vscode-ws-jsonrpc": "^0.2.0",
"ws": "^7.4.6"
Expand Down
3 changes: 3 additions & 0 deletions components/server/src/auth/rate-limiter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ function getConfig(config: RateLimiterConfig): RateLimiterConfig {
getLoggedInUser: { group: "default", points: 1 },
getTerms: { group: "default", points: 1 },
updateLoggedInUser: { group: "default", points: 1 },
needsVerification: { group: "default", points: 1 },
sendPhoneNumberVerificationToken: { group: "default", points: 1 },
verifyPhoneNumberVerificationToken: { group: "default", points: 1 },
getAuthProviders: { group: "default", points: 1 },
getOwnAuthProviders: { group: "default", points: 1 },
updateOwnAuthProvider: { group: "default", points: 1 },
Expand Down
Loading

0 comments on commit 7f2ab4d

Please sign in to comment.