CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
sagemathinc

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/next/components/auth/sign-up.tsx
Views: 687
1
/*
2
* This file is part of CoCalc: Copyright © 2022 Sagemath, Inc.
3
* License: MS-RSL – see LICENSE.md for details
4
*/
5
6
import { Alert, Button, Checkbox, Input } from "antd";
7
import { CSSProperties, useEffect, useRef, useState } from "react";
8
import {
9
GoogleReCaptchaProvider,
10
useGoogleReCaptcha,
11
} from "react-google-recaptcha-v3";
12
import Markdown from "@cocalc/frontend/editors/slate/static-markdown";
13
import {
14
CONTACT_TAG,
15
CONTACT_THESE_TAGS,
16
} from "@cocalc/util/db-schema/accounts";
17
import {
18
is_valid_email_address as isValidEmailAddress,
19
len,
20
plural,
21
smallIntegerToEnglishWord,
22
} from "@cocalc/util/misc";
23
import { COLORS } from "@cocalc/util/theme";
24
import { Strategy } from "@cocalc/util/types/sso";
25
import A from "components/misc/A";
26
import Loading from "components/share/loading";
27
import apiPost from "lib/api/post";
28
import useCustomize from "lib/use-customize";
29
import AuthPageContainer from "./fragments/auth-page-container";
30
import SSO, { RequiredSSO, useRequiredSSO } from "./sso";
31
import Tags from "./tags";
32
33
const LINE: CSSProperties = { margin: "15px 0" } as const;
34
35
interface SignUpProps {
36
minimal?: boolean; // use a minimal interface with less explanation and instructions (e.g., for embedding in other pages)
37
requiresToken?: boolean; // will be determined by API call if not given.
38
onSuccess?: (opts?: {}) => void; // if given, call after sign up *succeeds*.
39
has_site_license?: boolean;
40
publicPathId?: string;
41
showSignIn?: boolean;
42
signInAction?: () => void; // if given, replaces the default sign-in link behavior.
43
requireTags: boolean;
44
}
45
46
export default function SignUp(props: SignUpProps) {
47
const { reCaptchaKey } = useCustomize();
48
49
const body = <SignUp0 {...props} />;
50
if (reCaptchaKey == null) {
51
return body;
52
}
53
54
return (
55
<GoogleReCaptchaProvider reCaptchaKey={reCaptchaKey}>
56
{body}
57
</GoogleReCaptchaProvider>
58
);
59
}
60
61
function SignUp0({
62
requiresToken,
63
minimal,
64
onSuccess,
65
has_site_license,
66
publicPathId,
67
signInAction,
68
showSignIn,
69
requireTags,
70
}: SignUpProps) {
71
const {
72
anonymousSignup,
73
anonymousSignupLicensedShares,
74
siteName,
75
emailSignup,
76
accountCreationInstructions,
77
reCaptchaKey,
78
onCoCalcCom,
79
} = useCustomize();
80
const [tags, setTags] = useState<Set<string>>(new Set());
81
const [signupReason, setSignupReason] = useState<string>("");
82
const [email, setEmail] = useState<string>("");
83
const [registrationToken, setRegistrationToken] = useState<string>("");
84
const [password, setPassword] = useState<string>("");
85
const [firstName, setFirstName] = useState<string>("");
86
const [lastName, setLastName] = useState<string>("");
87
const [signingUp, setSigningUp] = useState<boolean>(false);
88
const [issues, setIssues] = useState<{
89
email?: string;
90
password?: string;
91
error?: string;
92
registrationToken?: string;
93
reCaptcha?: string;
94
}>({});
95
96
const minTags = requireTags ? 1 : 0;
97
const showContact = CONTACT_THESE_TAGS.some((t) => tags.has(t));
98
const requestContact = tags.has(CONTACT_TAG) && showContact;
99
100
const submittable = useRef<boolean>(false);
101
const { executeRecaptcha } = useGoogleReCaptcha();
102
const { strategies } = useCustomize();
103
104
// Sometimes the user if this component knows requiresToken and sometimes they don't.
105
// If they don't, we have to make an API call to figure it out.
106
const [requiresToken2, setRequiresToken2] = useState<boolean | undefined>(
107
requiresToken,
108
);
109
110
useEffect(() => {
111
if (requiresToken2 === undefined) {
112
(async () => {
113
try {
114
setRequiresToken2(await apiPost("/auth/requires-token"));
115
} catch (err) {}
116
})();
117
}
118
}, []);
119
120
// based on email: if user has to sign up via SSO, this will tell which strategy to use.
121
const requiredSSO = useRequiredSSO(strategies, email);
122
123
if (requiresToken2 === undefined || strategies == null) {
124
return <Loading />;
125
}
126
127
// number of tags except for the one name "CONTACT_TAG"
128
const tagsSize = tags.size - (requestContact ? 1 : 0);
129
const needsTags = !minimal && onCoCalcCom && tagsSize < minTags;
130
const what = "role";
131
132
submittable.current = !!(
133
requiredSSO == null &&
134
(!requiresToken2 || registrationToken) &&
135
email &&
136
isValidEmailAddress(email) &&
137
password &&
138
password.length >= 6 &&
139
firstName?.trim() &&
140
lastName?.trim() &&
141
!needsTags
142
);
143
144
async function signUp() {
145
if (signingUp) return;
146
setIssues({});
147
try {
148
setSigningUp(true);
149
150
let reCaptchaToken: undefined | string;
151
if (reCaptchaKey) {
152
if (!executeRecaptcha) {
153
throw Error("Please wait a few seconds, then try again.");
154
}
155
reCaptchaToken = await executeRecaptcha("signup");
156
}
157
158
const result = await apiPost("/auth/sign-up", {
159
terms: true,
160
email,
161
password,
162
firstName,
163
lastName,
164
registrationToken,
165
reCaptchaToken,
166
publicPathId,
167
tags: Array.from(tags),
168
signupReason,
169
});
170
if (result.issues && len(result.issues) > 0) {
171
setIssues(result.issues);
172
} else {
173
onSuccess?.({});
174
}
175
} catch (err) {
176
setIssues({ error: `${err}` });
177
} finally {
178
setSigningUp(false);
179
}
180
}
181
182
if (!emailSignup && strategies.length == 0) {
183
return (
184
<Alert
185
style={{ margin: "30px 15%" }}
186
type="error"
187
showIcon
188
message={"No Account Creation Allowed"}
189
description={
190
<div style={{ fontSize: "14pt", marginTop: "20px" }}>
191
<b>
192
There is no method enabled for creating an account on this server.
193
</b>
194
{(anonymousSignup ||
195
(anonymousSignupLicensedShares && has_site_license)) && (
196
<>
197
<br />
198
<br />
199
However, you can still{" "}
200
<A href="/auth/try">
201
try {siteName} without creating an account.
202
</A>
203
</>
204
)}
205
</div>
206
}
207
/>
208
);
209
}
210
211
function renderFooter() {
212
return (
213
(!minimal || showSignIn) && (
214
<>
215
<div>
216
Already have an account?{" "}
217
{signInAction ? (
218
<a onClick={signInAction}>Sign In</a>
219
) : (
220
<A href="/auth/sign-in">Sign In</A>
221
)}{" "}
222
{anonymousSignup && (
223
<>
224
or{" "}
225
<A href="/auth/try">
226
{" "}
227
try {siteName} without creating an account.{" "}
228
</A>
229
</>
230
)}
231
</div>
232
</>
233
)
234
);
235
}
236
237
function renderError() {
238
return (
239
issues.error && (
240
<Alert style={LINE} type="error" showIcon message={issues.error} />
241
)
242
);
243
}
244
245
function renderSubtitle() {
246
return (
247
<>
248
<h4 style={{ color: COLORS.GRAY_M, marginBottom: "35px" }}>
249
Start collaborating for free today.
250
</h4>
251
{accountCreationInstructions && (
252
<Markdown value={accountCreationInstructions} />
253
)}
254
</>
255
);
256
}
257
258
return (
259
<AuthPageContainer
260
error={renderError()}
261
footer={renderFooter()}
262
subtitle={renderSubtitle()}
263
minimal={minimal}
264
title={`Create a free account with ${siteName}`}
265
>
266
<div>
267
By creating an account, you agree to the{" "}
268
<A external={true} href="/policies/terms">
269
Terms of Service
270
</A>
271
.
272
</div>
273
{!minimal && onCoCalcCom ? (
274
<Tags
275
setTags={setTags}
276
signupReason={signupReason}
277
setSignupReason={setSignupReason}
278
tags={tags}
279
minTags={minTags}
280
what={what}
281
style={{ width: "880px", maxWidth: "100%", marginTop: "20px" }}
282
contact={showContact}
283
warning={needsTags}
284
/>
285
) : undefined}
286
<form>
287
{issues.reCaptcha ? (
288
<Alert
289
style={LINE}
290
type="error"
291
showIcon
292
message={issues.reCaptcha}
293
description={<>You may have to contact the site administrator.</>}
294
/>
295
) : undefined}
296
{issues.registrationToken && (
297
<Alert
298
style={LINE}
299
type="error"
300
showIcon
301
message={issues.registrationToken}
302
description={
303
<>
304
You may have to contact the site administrator for a
305
registration token.
306
</>
307
}
308
/>
309
)}
310
{requiresToken2 && (
311
<div style={LINE}>
312
<p>Registration Token</p>
313
<Input
314
style={{ fontSize: "12pt" }}
315
value={registrationToken}
316
placeholder="Enter your secret registration token"
317
onChange={(e) => setRegistrationToken(e.target.value)}
318
/>
319
</div>
320
)}
321
<EmailOrSSO
322
email={email}
323
setEmail={setEmail}
324
signUp={signUp}
325
strategies={strategies}
326
hideSSO={requiredSSO != null}
327
/>
328
<RequiredSSO strategy={requiredSSO} />
329
{issues.email && (
330
<Alert
331
style={LINE}
332
type="error"
333
showIcon
334
message={issues.email}
335
description={
336
<>
337
Choose a different email address,{" "}
338
<A href="/auth/sign-in">sign in</A>, or{" "}
339
<A href="/auth/password-reset">reset your password</A>.
340
</>
341
}
342
/>
343
)}
344
{requiredSSO == null && (
345
<div style={LINE}>
346
<p>Password</p>
347
<Input.Password
348
style={{ fontSize: "12pt" }}
349
value={password}
350
placeholder="Password"
351
autoComplete="new-password"
352
onChange={(e) => setPassword(e.target.value)}
353
onPressEnter={signUp}
354
/>
355
</div>
356
)}
357
{issues.password && (
358
<Alert style={LINE} type="error" showIcon message={issues.password} />
359
)}
360
{requiredSSO == null && (
361
<div style={LINE}>
362
<p>First name (Given name)</p>
363
<Input
364
style={{ fontSize: "12pt" }}
365
placeholder="First name"
366
value={firstName}
367
onChange={(e) => setFirstName(e.target.value)}
368
onPressEnter={signUp}
369
/>
370
</div>
371
)}
372
{requiredSSO == null && (
373
<div style={LINE}>
374
<p>Last name (Family name)</p>
375
<Input
376
style={{ fontSize: "12pt" }}
377
placeholder="Last name"
378
value={lastName}
379
onChange={(e) => setLastName(e.target.value)}
380
onPressEnter={signUp}
381
/>
382
</div>
383
)}
384
</form>
385
<div style={LINE}>
386
<Button
387
shape="round"
388
size="large"
389
disabled={!submittable.current || signingUp}
390
type="primary"
391
style={{
392
width: "100%",
393
marginTop: "15px",
394
color:
395
!submittable.current || signingUp
396
? COLORS.ANTD_RED_WARN
397
: undefined,
398
}}
399
onClick={signUp}
400
>
401
{needsTags && tagsSize < minTags
402
? `Select at least ${smallIntegerToEnglishWord(minTags)} ${plural(
403
minTags,
404
what,
405
)}`
406
: requiresToken2 && !registrationToken
407
? "Enter the secret registration token"
408
: !email
409
? "How will you sign in?"
410
: !isValidEmailAddress(email)
411
? "Enter a valid email address above"
412
: requiredSSO != null
413
? "You must sign up via SSO"
414
: !password || password.length < 6
415
? "Choose password with at least 6 characters"
416
: !firstName?.trim()
417
? "Enter your first name above"
418
: !lastName?.trim()
419
? "Enter your last name above"
420
: signingUp
421
? ""
422
: "Sign Up!"}
423
{signingUp && (
424
<span style={{ marginLeft: "15px" }}>
425
<Loading>Signing Up...</Loading>
426
</span>
427
)}
428
</Button>
429
</div>
430
</AuthPageContainer>
431
);
432
}
433
434
interface EmailOrSSOProps {
435
email: string;
436
setEmail: (email: string) => void;
437
signUp: () => void;
438
strategies?: Strategy[];
439
hideSSO?: boolean;
440
}
441
442
function EmailOrSSO(props: EmailOrSSOProps) {
443
const { email, setEmail, signUp, strategies = [], hideSSO = false } = props;
444
const { emailSignup } = useCustomize();
445
446
function renderSSO() {
447
if (strategies.length == 0) return;
448
449
const emailStyle: CSSProperties = email
450
? { textAlign: "right", marginBottom: "20px" }
451
: {};
452
453
const style: CSSProperties = {
454
display: hideSSO ? "none" : "block",
455
...emailStyle,
456
};
457
458
return (
459
<div style={{ textAlign: "center", margin: "20px 0" }}>
460
<SSO size={email ? 24 : undefined} style={style} />
461
</div>
462
);
463
}
464
465
return (
466
<div>
467
<div>
468
<p style={{ color: "#444", marginTop: "10px" }}>
469
{hideSSO
470
? "Sign up using your single sign-on provider"
471
: strategies.length > 0 && emailSignup
472
? "Sign up using either your email address or a single sign-on provider."
473
: emailSignup
474
? "Enter the email address you will use to sign in."
475
: "Sign up using a single sign-on provider."}
476
</p>
477
</div>
478
{renderSSO()}
479
{emailSignup ? (
480
<p>
481
<p>Email address</p>
482
<Input
483
style={{ fontSize: "12pt" }}
484
placeholder="Email address"
485
autoComplete="username"
486
value={email}
487
onChange={(e) => setEmail(e.target.value)}
488
onPressEnter={signUp}
489
/>
490
</p>
491
) : undefined}
492
</div>
493
);
494
}
495
496
export function TermsCheckbox({
497
checked,
498
onChange,
499
style,
500
}: {
501
checked?: boolean;
502
onChange?: (boolean) => void;
503
style?: CSSProperties;
504
}) {
505
return (
506
<Checkbox
507
checked={checked}
508
style={style}
509
onChange={(e) => onChange?.(e.target.checked)}
510
>
511
I agree to the{" "}
512
<A external={true} href="/policies/terms">
513
Terms of Service
514
</A>
515
.
516
</Checkbox>
517
);
518
}
519
520