Skip to content

Commit 0924951

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

File tree

23 files changed

+556
-14
lines changed

23 files changed

+556
-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

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

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+
.intl-tel-input,
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+
.intl-tel-input::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)