Skip to content

Commit a87f052

Browse files
committed
Add phone verification
1 parent 135bc0f commit a87f052

File tree

23 files changed

+538
-14
lines changed

23 files changed

+538
-14
lines changed

Diff for: components/dashboard/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"query-string": "^7.1.1",
1616
"react": "^17.0.1",
1717
"react-dom": "^17.0.1",
18+
"react-intl-tel-input": "^8.2.0",
1819
"react-router-dom": "^5.2.0",
1920
"xterm": "^4.11.0",
2021
"xterm-addon-fit": "^0.5.0"

Diff for: components/dashboard/src/index.css

+3
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@
7979

8080
textarea,
8181
input[type="text"],
82+
input[type="tel"],
8283
input[type="number"],
8384
input[type="search"],
8485
input[type="password"],
@@ -87,12 +88,14 @@
8788
}
8889
textarea::placeholder,
8990
input[type="text"]::placeholder,
91+
input[type="tel"]::placeholder,
9092
input[type="number"]::placeholder,
9193
input[type="search"]::placeholder,
9294
input[type="password"]::placeholder {
9395
@apply text-gray-400 dark:text-gray-500;
9496
}
9597
input[type="text"].error,
98+
input[type="tel"].error,
9699
input[type="number"].error,
97100
input[type="search"].error,
98101
input[type="password"].error,

Diff for: components/dashboard/src/start/StartPage.tsx

+3
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@
44
* See License-AGPL.txt in the project root for license information.
55
*/
66

7+
import { ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error";
78
import { useEffect } from "react";
89
import Alert from "../components/Alert";
910
import gitpodIconUA from "../icons/gitpod.svg";
1011
import { gitpodHostUrl } from "../service/service";
12+
import { VerifyModal } from "./VerifyModal";
1113

1214
export enum StartPhase {
1315
Checking = 0,
@@ -106,6 +108,7 @@ export function StartPage(props: StartPageProps) {
106108
{typeof phase === "number" && phase < StartPhase.IdeReady && (
107109
<ProgressBar phase={phase} error={!!error} />
108110
)}
111+
{error && error.code === ErrorCodes.NEEDS_VERIFICATION && <VerifyModal />}
109112
{error && <StartError error={error} />}
110113
{props.children}
111114
{props.showLatestIdeWarning && (

Diff for: components/dashboard/src/start/VerifyModal.tsx

+201
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
/**
2+
* Copyright (c) 2022 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 { useState } from "react";
8+
import Alert, { AlertType } from "../components/Alert";
9+
import Modal from "../components/Modal";
10+
import { getGitpodService } from "../service/service";
11+
import PhoneInput from "react-intl-tel-input";
12+
import "react-intl-tel-input/dist/main.css";
13+
import "./phone-input.css";
14+
15+
interface VerifyModalState {
16+
phoneNumber?: string;
17+
phoneNumberValid?: boolean;
18+
sent?: Date;
19+
message?: {
20+
type: AlertType;
21+
text: string;
22+
};
23+
token?: string;
24+
verified?: boolean;
25+
}
26+
27+
export function VerifyModal() {
28+
const [state, setState] = useState<VerifyModalState>({});
29+
30+
if (!state.sent) {
31+
const sendCode = () => {
32+
getGitpodService().server.sendPhoneNumberVerificationToken(state.phoneNumber || "");
33+
setState({
34+
...state,
35+
sent: new Date(),
36+
});
37+
return true;
38+
};
39+
return (
40+
<Modal
41+
onClose={() => {}}
42+
closeable={false}
43+
onEnter={sendCode}
44+
title="User Validation Required"
45+
buttons={
46+
<div>
47+
<button className="ml-2" disabled={!state.phoneNumberValid} onClick={sendCode}>
48+
Send Code via SMS
49+
</button>
50+
</div>
51+
}
52+
visible={true}
53+
>
54+
<Alert type="warning" className="mt-2">
55+
To use Gitpod you'll need to validate your account with your phone number. This is required to
56+
discourage and reduce abuse on Gitpod infrastructure.
57+
</Alert>
58+
<div className="mt-2 -mx-6 px-6 pt-4">
59+
Enter a mobile phone number you would like to use to verify your account.
60+
</div>
61+
<div className="mt-4">
62+
<h4>Mobile Phone Number</h4>
63+
<PhoneInput
64+
autoFocus={true}
65+
containerClassName={"allow-dropdown w-full intl-tel-input"}
66+
inputClassName={"w-full"}
67+
allowDropdown={true}
68+
defaultCountry={""}
69+
autoHideDialCode={false}
70+
onPhoneNumberChange={(isValid, phoneNumberRaw, countryData) => {
71+
let phoneNumber = phoneNumberRaw;
72+
if (!phoneNumber.startsWith("+") && !phoneNumber.startsWith("00")) {
73+
phoneNumber = "+" + countryData.dialCode + phoneNumber;
74+
}
75+
setState({
76+
...state,
77+
phoneNumber,
78+
phoneNumberValid: isValid,
79+
});
80+
}}
81+
/>
82+
</div>
83+
</Modal>
84+
);
85+
} else if (!state.verified) {
86+
const isTokenFilled = () => {
87+
return state.token && /\d{6}/.test(state.token);
88+
};
89+
const verifyToken = async () => {
90+
const verified = await getGitpodService().server.verifyPhoneNumberVerificationToken(
91+
state.phoneNumber!,
92+
state.token!,
93+
);
94+
if (verified) {
95+
setState({
96+
...state,
97+
verified: true,
98+
message: undefined,
99+
});
100+
} else {
101+
setState({
102+
...state,
103+
message: {
104+
type: "error",
105+
text: `Invalid verification code.`,
106+
},
107+
});
108+
}
109+
return verified;
110+
};
111+
112+
const reset = () => {
113+
setState({
114+
...state,
115+
sent: undefined,
116+
message: undefined,
117+
token: undefined,
118+
});
119+
};
120+
return (
121+
<Modal
122+
onClose={() => {}}
123+
closeable={false}
124+
onEnter={verifyToken}
125+
title="User Validation Required"
126+
buttons={
127+
<div>
128+
<button className="ml-2" disabled={!isTokenFilled()} onClick={verifyToken}>
129+
Validate Account
130+
</button>
131+
</div>
132+
}
133+
visible={true}
134+
>
135+
<Alert type="warning" className="mt-2">
136+
To use Gitpod you'll need to validate your account with your phone number. This is required to
137+
discourage and reduce abuse on Gitpod infrastructure.
138+
</Alert>
139+
<div className="pt-4">
140+
<button className="gp-link" onClick={reset}>
141+
&larr; Use a different phone number
142+
</button>
143+
</div>
144+
<div className="pt-4">
145+
Enter the verification code we sent to {state.phoneNumber}.<br />
146+
Having trouble?{" "}
147+
<a className="gp-link" href="https://www.gitpod.io/contact/support">
148+
Contact support
149+
</a>
150+
</div>
151+
{state.message ? (
152+
<Alert type={state.message.type} className="mt-4 py-3">
153+
{state.message.text}
154+
</Alert>
155+
) : (
156+
<></>
157+
)}
158+
<div className="mt-4">
159+
<h4>Verification Code</h4>
160+
<input
161+
autoFocus={true}
162+
className="w-full"
163+
type="text"
164+
placeholder="enter token send via sms"
165+
onChange={(v) => {
166+
setState({
167+
...state,
168+
token: v.currentTarget.value,
169+
});
170+
}}
171+
/>
172+
</div>
173+
</Modal>
174+
);
175+
} else {
176+
const continueStartWorkspace = () => {
177+
window.location.reload();
178+
return true;
179+
};
180+
return (
181+
<Modal
182+
onClose={continueStartWorkspace}
183+
closeable={false}
184+
onEnter={continueStartWorkspace}
185+
title="User Validation Successful"
186+
buttons={
187+
<div>
188+
<button className="ml-2" onClick={continueStartWorkspace}>
189+
Continue
190+
</button>
191+
</div>
192+
}
193+
visible={true}
194+
>
195+
<Alert type="message" className="mt-2">
196+
Your account has been successfully verified.
197+
</Alert>
198+
</Modal>
199+
);
200+
}
201+
}

Diff for: components/dashboard/src/start/phone-input.css

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/**
2+
* Copyright (c) 2022 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+
.country-list {
8+
width: 29rem !important;
9+
}
10+
11+
input[type="tel"],
12+
.country-list {
13+
@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;
14+
}
15+
16+
input[type="tel"]::placeholder {
17+
@apply text-gray-400 dark:text-gray-500;
18+
}

Diff for: components/gitpod-protocol/src/gitpod-service.ts

+2
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,8 @@ export interface GitpodServer extends JsonRpcServer<GitpodClient>, AdminServer,
8585
getLoggedInUser(): Promise<User>;
8686
getTerms(): Promise<Terms>;
8787
updateLoggedInUser(user: Partial<User>): Promise<User>;
88+
sendPhoneNumberVerificationToken(phoneNumber: string): Promise<void>;
89+
verifyPhoneNumberVerificationToken(phoneNumber: string, token: string): Promise<boolean>;
8890
getAuthProviders(): Promise<AuthProviderInfo[]>;
8991
getOwnAuthProviders(): Promise<AuthProviderEntry[]>;
9092
updateOwnAuthProvider(params: GitpodServer.UpdateOwnAuthProviderParams): Promise<AuthProviderEntry>;

Diff for: components/gitpod-protocol/src/messaging/error.ts

+3
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ export namespace ErrorCodes {
2626
// 410 No User
2727
export const SETUP_REQUIRED = 410;
2828

29+
// 411 No User
30+
export const NEEDS_VERIFICATION = 411;
31+
2932
// 429 Too Many Requests
3033
export const TOO_MANY_REQUESTS = 429;
3134

Diff for: components/gitpod-protocol/src/protocol.ts

+2
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,8 @@ export interface AdditionalUserData {
206206
workspaceClasses?: WorkspaceClasses;
207207
// additional user profile data
208208
profile?: ProfileDetails;
209+
// verification date
210+
lastVerificationTime?: string;
209211
}
210212

211213
// The format in which we store User Profiles in

Diff for: components/server/ee/src/billing/entitlement-service.ts

+9
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
} from "@gitpod/gitpod-protocol";
1313
import { log } from "@gitpod/gitpod-protocol/lib/util/logging";
1414
import { inject, injectable } from "inversify";
15+
import { VerificationService } from "../../../src/auth/verification-service";
1516
import { EntitlementService, MayStartWorkspaceResult } from "../../../src/billing/entitlement-service";
1617
import { Config } from "../../../src/config";
1718
import { BillingModes } from "./billing-mode";
@@ -31,13 +32,21 @@ export class EntitlementServiceImpl implements EntitlementService {
3132
@inject(EntitlementServiceChargebee) protected readonly chargebee: EntitlementServiceChargebee;
3233
@inject(EntitlementServiceLicense) protected readonly license: EntitlementServiceLicense;
3334
@inject(EntitlementServiceUBP) protected readonly ubp: EntitlementServiceUBP;
35+
@inject(VerificationService) protected readonly verificationService: VerificationService;
3436

3537
async mayStartWorkspace(
3638
user: User,
3739
date: Date = new Date(),
3840
runningInstances: Promise<WorkspaceInstance[]>,
3941
): Promise<MayStartWorkspaceResult> {
4042
try {
43+
const verification = await this.verificationService.needsVerification(user);
44+
if (verification) {
45+
return {
46+
mayStart: false,
47+
needsVerification: true,
48+
};
49+
}
4150
const billingMode = await this.billingModes.getBillingModeForUser(user, date);
4251
let result;
4352
switch (billingMode.mode) {

Diff for: components/server/ee/src/workspace/gitpod-server-impl.ts

+3
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,9 @@ export class GitpodServerEEImpl extends GitpodServerImpl {
266266
if (result.mayStart) {
267267
return; // green light from entitlement service
268268
}
269+
if (!!result.needsVerification) {
270+
throw new ResponseError(ErrorCodes.NEEDS_VERIFICATION, `Please verify your account.`);
271+
}
269272
if (!!result.oufOfCredits) {
270273
throw new ResponseError(
271274
ErrorCodes.NOT_ENOUGH_CREDIT,

Diff for: components/server/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@
8181
"reflect-metadata": "^0.1.10",
8282
"stripe": "^9.0.0",
8383
"swot-js": "^1.0.3",
84+
"twilio": "^3.78.0",
8485
"uuid": "^8.3.2",
8586
"vscode-ws-jsonrpc": "^0.2.0",
8687
"ws": "^7.4.6"

Diff for: components/server/src/auth/rate-limiter.ts

+2
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ function getConfig(config: RateLimiterConfig): RateLimiterConfig {
5151
getLoggedInUser: { group: "default", points: 1 },
5252
getTerms: { group: "default", points: 1 },
5353
updateLoggedInUser: { group: "default", points: 1 },
54+
sendPhoneNumberVerificationToken: { group: "default", points: 1 },
55+
verifyPhoneNumberVerificationToken: { group: "default", points: 1 },
5456
getAuthProviders: { group: "default", points: 1 },
5557
getOwnAuthProviders: { group: "default", points: 1 },
5658
updateOwnAuthProvider: { group: "default", points: 1 },

0 commit comments

Comments
 (0)