Skip to content

Commit a3bcdf3

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

File tree

23 files changed

+557
-14
lines changed

23 files changed

+557
-14
lines changed

components/dashboard/package.json

Lines changed: 1 addition & 0 deletions
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"

components/dashboard/src/index.css

Lines changed: 3 additions & 0 deletions
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,

components/dashboard/src/start/StartPage.tsx

Lines changed: 3 additions & 0 deletions
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 && (
Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
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 = async () => {
32+
try {
33+
await getGitpodService().server.sendPhoneNumberVerificationToken(state.phoneNumber || "");
34+
setState({
35+
...state,
36+
message: undefined,
37+
sent: new Date(),
38+
});
39+
return true;
40+
} catch (err) {
41+
setState({
42+
sent: undefined,
43+
message: {
44+
type: "error",
45+
text: err.toString(),
46+
},
47+
});
48+
return false;
49+
}
50+
};
51+
return (
52+
<Modal
53+
onClose={() => {}}
54+
closeable={false}
55+
onEnter={sendCode}
56+
title="User Validation Required"
57+
buttons={
58+
<div>
59+
<button className="ml-2" disabled={!state.phoneNumberValid} onClick={sendCode}>
60+
Send Code via SMS
61+
</button>
62+
</div>
63+
}
64+
visible={true}
65+
>
66+
<Alert type="warning" className="mt-2">
67+
To use Gitpod you'll need to validate your account with your phone number. This is required to
68+
discourage and reduce abuse on Gitpod infrastructure.
69+
</Alert>
70+
<div className="mt-2">Enter a mobile phone number you would like to use to verify your account.</div>
71+
{state.message ? (
72+
<Alert type={state.message.type} className="mt-4 py-3">
73+
{state.message.text}
74+
</Alert>
75+
) : (
76+
<></>
77+
)}
78+
<div className="mt-4">
79+
<h4>Mobile Phone Number</h4>
80+
{/* HACK: Below we are adding a dummy dom element that is not visible, to reference the classes so they are not removed by purgeCSS. */}
81+
<input type="tel" className="hidden intl-tel-input country-list" />
82+
<PhoneInput
83+
autoFocus={true}
84+
containerClassName={"allow-dropdown w-full intl-tel-input"}
85+
inputClassName={"w-full"}
86+
allowDropdown={true}
87+
defaultCountry={""}
88+
autoHideDialCode={false}
89+
onPhoneNumberChange={(isValid, phoneNumberRaw, countryData) => {
90+
let phoneNumber = phoneNumberRaw;
91+
if (!phoneNumber.startsWith("+") && !phoneNumber.startsWith("00")) {
92+
phoneNumber = "+" + countryData.dialCode + phoneNumber;
93+
}
94+
setState({
95+
...state,
96+
phoneNumber,
97+
phoneNumberValid: isValid,
98+
});
99+
}}
100+
/>
101+
</div>
102+
</Modal>
103+
);
104+
} else if (!state.verified) {
105+
const isTokenFilled = () => {
106+
return state.token && /\d{6}/.test(state.token);
107+
};
108+
const verifyToken = async () => {
109+
const verified = await getGitpodService().server.verifyPhoneNumberVerificationToken(
110+
state.phoneNumber!,
111+
state.token!,
112+
);
113+
if (verified) {
114+
setState({
115+
...state,
116+
verified: true,
117+
message: undefined,
118+
});
119+
} else {
120+
setState({
121+
...state,
122+
message: {
123+
type: "error",
124+
text: `Invalid verification code.`,
125+
},
126+
});
127+
}
128+
return verified;
129+
};
130+
131+
const reset = () => {
132+
setState({
133+
...state,
134+
sent: undefined,
135+
message: undefined,
136+
token: undefined,
137+
});
138+
};
139+
return (
140+
<Modal
141+
onClose={() => {}}
142+
closeable={false}
143+
onEnter={verifyToken}
144+
title="User Validation Required"
145+
buttons={
146+
<div>
147+
<button className="ml-2" disabled={!isTokenFilled()} onClick={verifyToken}>
148+
Validate Account
149+
</button>
150+
</div>
151+
}
152+
visible={true}
153+
>
154+
<Alert type="warning" className="mt-2">
155+
To use Gitpod you'll need to validate your account with your phone number. This is required to
156+
discourage and reduce abuse on Gitpod infrastructure.
157+
</Alert>
158+
<div className="pt-4">
159+
<button className="gp-link" onClick={reset}>
160+
&larr; Use a different phone number
161+
</button>
162+
</div>
163+
<div className="pt-4">
164+
Enter the verification code we sent to {state.phoneNumber}.<br />
165+
Having trouble?{" "}
166+
<a className="gp-link" href="https://www.gitpod.io/contact/support">
167+
Contact support
168+
</a>
169+
</div>
170+
{state.message ? (
171+
<Alert type={state.message.type} className="mt-4 py-3">
172+
{state.message.text}
173+
</Alert>
174+
) : (
175+
<></>
176+
)}
177+
<div className="mt-4">
178+
<h4>Verification Code</h4>
179+
<input
180+
autoFocus={true}
181+
className="w-full"
182+
type="text"
183+
placeholder="Enter code sent via SMS"
184+
onChange={(v) => {
185+
setState({
186+
...state,
187+
token: v.currentTarget.value,
188+
});
189+
}}
190+
/>
191+
</div>
192+
</Modal>
193+
);
194+
} else {
195+
const continueStartWorkspace = () => {
196+
window.location.reload();
197+
return true;
198+
};
199+
return (
200+
<Modal
201+
onClose={continueStartWorkspace}
202+
closeable={false}
203+
onEnter={continueStartWorkspace}
204+
title="User Validation Successful"
205+
buttons={
206+
<div>
207+
<button className="ml-2" onClick={continueStartWorkspace}>
208+
Continue
209+
</button>
210+
</div>
211+
}
212+
visible={true}
213+
>
214+
<Alert type="message" className="mt-2">
215+
Your account has been successfully verified.
216+
</Alert>
217+
</Modal>
218+
);
219+
}
220+
}
Lines changed: 18 additions & 0 deletions
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+
}

components/gitpod-protocol/src/gitpod-service.ts

Lines changed: 2 additions & 0 deletions
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>;

components/gitpod-protocol/src/messaging/error.ts

Lines changed: 3 additions & 0 deletions
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

components/gitpod-protocol/src/protocol.ts

Lines changed: 2 additions & 0 deletions
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

components/server/ee/src/billing/entitlement-service.ts

Lines changed: 9 additions & 0 deletions
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) {

components/server/ee/src/workspace/gitpod-server-impl.ts

Lines changed: 3 additions & 0 deletions
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,

components/server/package.json

Lines changed: 1 addition & 0 deletions
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"

components/server/src/auth/rate-limiter.ts

Lines changed: 2 additions & 0 deletions
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)