Skip to content

Commit 8e00464

Browse files
committed
Add phone verification
1 parent dda2ebd commit 8e00464

File tree

40 files changed

+723
-17
lines changed

40 files changed

+723
-17
lines changed

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"

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.lastVerificationTime ? <Label text="Verified" color="green" /> : null}
126127
</div>
127128
<p>
128129
{user.identities

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",

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,

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 && (
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
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+
try {
119+
const verified = await getGitpodService().server.verifyPhoneNumberVerificationToken(
120+
state.phoneNumber!,
121+
state.token!,
122+
);
123+
if (verified) {
124+
setState({
125+
...state,
126+
verified: true,
127+
message: undefined,
128+
});
129+
} else {
130+
setState({
131+
...state,
132+
message: {
133+
type: "error",
134+
text: `Invalid verification code.`,
135+
},
136+
});
137+
}
138+
return verified;
139+
} catch (err) {
140+
setState({
141+
sent: undefined,
142+
sending: false,
143+
message: {
144+
type: "error",
145+
text: err.toString(),
146+
},
147+
});
148+
return false;
149+
}
150+
};
151+
152+
const reset = () => {
153+
setState({
154+
...state,
155+
sent: undefined,
156+
message: undefined,
157+
token: undefined,
158+
});
159+
};
160+
return (
161+
<Modal
162+
onClose={() => {}}
163+
closeable={false}
164+
onEnter={verifyToken}
165+
title="User Validation Required"
166+
buttons={
167+
<div>
168+
<button className="ml-2" disabled={!isTokenFilled()} onClick={verifyToken}>
169+
Validate Account
170+
</button>
171+
</div>
172+
}
173+
visible={true}
174+
>
175+
<Alert type="warning" className="mt-2">
176+
To use Gitpod you'll need to validate your account with your phone number. This is required to
177+
discourage and reduce abuse on Gitpod infrastructure.
178+
</Alert>
179+
<div className="pt-4">
180+
<button className="gp-link" onClick={reset}>
181+
&larr; Use a different phone number
182+
</button>
183+
</div>
184+
<div className="text-gray-600 dark:text-gray-400 pt-4">
185+
Enter the verification code we sent to {state.phoneNumber}.<br />
186+
Having trouble?{" "}
187+
<a className="gp-link" href="https://www.gitpod.io/contact/support">
188+
Contact support
189+
</a>
190+
</div>
191+
{state.message ? (
192+
<Alert type={state.message.type} className="mt-4 py-3">
193+
{state.message.text}
194+
</Alert>
195+
) : (
196+
<></>
197+
)}
198+
<div className="mt-4">
199+
<h4>Verification Code</h4>
200+
<input
201+
autoFocus={true}
202+
className="w-full"
203+
type="text"
204+
placeholder="Enter code sent via SMS"
205+
onChange={(v) => {
206+
setState({
207+
...state,
208+
token: v.currentTarget.value,
209+
});
210+
}}
211+
/>
212+
</div>
213+
</Modal>
214+
);
215+
} else {
216+
const continueStartWorkspace = () => {
217+
window.location.reload();
218+
return true;
219+
};
220+
return (
221+
<Modal
222+
onClose={continueStartWorkspace}
223+
closeable={false}
224+
onEnter={continueStartWorkspace}
225+
title="User Validation Successful"
226+
buttons={
227+
<div>
228+
<button className="ml-2" onClick={continueStartWorkspace}>
229+
Continue
230+
</button>
231+
</div>
232+
}
233+
visible={true}
234+
>
235+
<Alert type="success" className="mt-2">
236+
Your account has been successfully verified.
237+
</Alert>
238+
</Modal>
239+
);
240+
}
241+
}
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-db/src/typeorm/entity/db-user.ts

+12
Original file line numberDiff line numberDiff line change
@@ -84,4 +84,16 @@ export class DBUser implements User {
8484
transformer: Transformer.MAP_EMPTY_STR_TO_UNDEFINED,
8585
})
8686
usageAttributionId?: string;
87+
88+
@Column({
89+
default: "",
90+
transformer: Transformer.MAP_EMPTY_STR_TO_UNDEFINED,
91+
})
92+
lastVerificationTime?: string;
93+
94+
@Column({
95+
default: "",
96+
transformer: Transformer.MAP_EMPTY_STR_TO_UNDEFINED,
97+
})
98+
verificationPhoneNumber?: string;
8799
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
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 { MigrationInterface, QueryRunner } from "typeorm";
8+
import { columnExists } from "./helper/helper";
9+
10+
const D_B_USER = "d_b_user";
11+
const COL_VERIFICATIONTIME = "lastVerificationTime";
12+
const COL_PHONE_NUMBER = "verificationPhoneNumber";
13+
14+
export class PhoneVerification1661519441407 implements MigrationInterface {
15+
public async up(queryRunner: QueryRunner): Promise<void> {
16+
if (!(await columnExists(queryRunner, D_B_USER, COL_VERIFICATIONTIME))) {
17+
await queryRunner.query(
18+
`ALTER TABLE ${D_B_USER} ADD COLUMN ${COL_VERIFICATIONTIME} varchar(30) NOT NULL DEFAULT '', ALGORITHM=INPLACE, LOCK=NONE `,
19+
);
20+
}
21+
if (!(await columnExists(queryRunner, D_B_USER, COL_PHONE_NUMBER))) {
22+
await queryRunner.query(
23+
`ALTER TABLE ${D_B_USER} ADD COLUMN ${COL_PHONE_NUMBER} varchar(30) NOT NULL DEFAULT '', ALGORITHM=INPLACE, LOCK=NONE `,
24+
);
25+
}
26+
}
27+
28+
public async down(queryRunner: QueryRunner): Promise<void> {}
29+
}

0 commit comments

Comments
 (0)