Skip to content

Commit 979777c

Browse files
committed
Add phone verification
1 parent 135bc0f commit 979777c

33 files changed

+639
-25
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/admin/UserDetail.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ export default function UserDetail(p: { user: User }) {
123123
<h3>{user.fullName}</h3>
124124
{user.blocked ? <Label text="Blocked" color="red" /> : null}{" "}
125125
{user.markedDeleted ? <Label text="Deleted" color="red" /> : null}
126+
{user.additionalData?.lastVerificationTime ? <Label text="Verified" color="green" /> : null}
126127
</div>
127128
<p>
128129
{user.identities

Diff for: components/dashboard/src/components/Alert.tsx

+9
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,11 @@ import { ReactComponent as Exclamation } from "../images/exclamation.svg";
99
import { ReactComponent as Exclamation2 } from "../images/exclamation2.svg";
1010
import { ReactComponent as InfoSvg } from "../images/info.svg";
1111
import { ReactComponent as XSvg } from "../images/x.svg";
12+
import { ReactComponent as Check } from "../images/check-circle.svg";
1213

1314
export type AlertType =
15+
// Green
16+
| "success"
1417
// Yellow
1518
| "warning"
1619
// Gray alert
@@ -40,6 +43,12 @@ interface AlertInfo {
4043
}
4144

4245
const infoMap: Record<AlertType, AlertInfo> = {
46+
success: {
47+
bgCls: "bg-green-100 dark:bg-green-800",
48+
txtCls: "text-green-700 dark:text-green-50",
49+
icon: <Check className="w-4 h-4"></Check>,
50+
iconColor: "text-green-700 dark:text-green-100",
51+
},
4352
warning: {
4453
bgCls: "bg-yellow-100 dark:bg-yellow-700",
4554
txtCls: "text-yellow-600 dark:text-yellow-50",

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

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

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

+19-1
Original file line numberDiff line numberDiff line change
@@ -206,8 +206,26 @@ export interface AdditionalUserData {
206206
workspaceClasses?: WorkspaceClasses;
207207
// additional user profile data
208208
profile?: ProfileDetails;
209+
// verification date
210+
lastVerificationTime?: string;
211+
// verification phone number
212+
verificationPhoneNumber?: string;
213+
}
214+
export namespace AdditionalUserData {
215+
export function set(user: User, partialData: Partial<AdditionalUserData>): User {
216+
if (!user.additionalData) {
217+
user.additionalData = {
218+
...partialData,
219+
};
220+
} else {
221+
user.additionalData = {
222+
...user.additionalData,
223+
...partialData,
224+
};
225+
}
226+
return user;
227+
}
209228
}
210-
211229
// The format in which we store User Profiles in
212230
export interface ProfileDetails {
213231
// when was the last time the user updated their profile information or has been nudged to do so.

Diff for: components/gitpod-protocol/src/util/timeutil.spec.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,16 @@
77
import * as chai from "chai";
88
const expect = chai.expect;
99
import { suite, test } from "mocha-typescript";
10-
import { oneMonthLater } from "./timeutil";
10+
import { daysBefore, hoursBefore, oneMonthLater } from "./timeutil";
1111

1212
@suite()
1313
export class TimeutilSpec {
14+
@test
15+
testDaysBefore() {
16+
const now = new Date().toISOString();
17+
expect(daysBefore(now, 2)).to.be.eq(hoursBefore(now, 48));
18+
}
19+
1420
@test
1521
testTimeutil() {
1622
// targeting a 1st, 1th of Jan => 1st of Feb

0 commit comments

Comments
 (0)