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/pages/api/v2/auth/sign-up.ts
Views: 687
1
/*
2
* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.
3
* License: MS-RSL – see LICENSE.md for details
4
*/
5
6
/*
7
Sign up for a new account:
8
9
0. If email/password matches an existing account, just sign them in. Reduces confusion.
10
1. Reject if password is absurdly weak.
11
2. Query the database to make sure the email address is not already taken.
12
3. Generate a random account_id. Do not check it is not already taken, since that's
13
highly unlikely, and the insert in 4 would fail anyways.
14
4. Write account to the database.
15
5. Sign user in (if not being used via the API).
16
17
This can also be used via the API, but the client must have a minimum balance
18
of at least - $100.
19
20
21
API Usage:
22
23
curl -u sk_abcdefQWERTY090900000000: \
24
-d firstName=John00 \
25
-d lastName=Doe00 \
26
-d [email protected] \
27
-d password=xyzabc09090 \
28
-d terms=true https://cocalc.com/api/v2/auth/sign-up
29
30
TIP: If you want to pass in an email like [email protected], use '%2B' in place of '+'.
31
*/
32
33
import { v4 } from "uuid";
34
35
import { getServerSettings } from "@cocalc/database/settings/server-settings";
36
import createAccount from "@cocalc/server/accounts/create-account";
37
import isAccountAvailable from "@cocalc/server/auth/is-account-available";
38
import isDomainExclusiveSSO from "@cocalc/server/auth/is-domain-exclusive-sso";
39
import passwordStrength from "@cocalc/server/auth/password-strength";
40
import reCaptcha from "@cocalc/server/auth/recaptcha";
41
import redeemRegistrationToken from "@cocalc/server/auth/tokens/redeem";
42
import sendWelcomeEmail from "@cocalc/server/email/welcome-email";
43
import getSiteLicenseId from "@cocalc/server/public-paths/site-license-id";
44
import {
45
is_valid_email_address as isValidEmailAddress,
46
len,
47
} from "@cocalc/util/misc";
48
49
import getAccountId from "lib/account/get-account";
50
import { apiRoute, apiRouteOperation } from "lib/api";
51
import assertTrusted from "lib/api/assert-trusted";
52
import getParams from "lib/api/get-params";
53
import {
54
SignUpInputSchema,
55
SignUpOutputSchema,
56
} from "lib/api/schema/accounts/sign-up";
57
import { SignUpIssues } from "lib/types/sign-up";
58
import { getAccount, signUserIn } from "./sign-in";
59
60
export async function signUp(req, res) {
61
let {
62
terms,
63
email,
64
password,
65
firstName,
66
lastName,
67
registrationToken,
68
tags,
69
publicPathId,
70
signupReason,
71
} = getParams(req);
72
73
password = (password ?? "").trim();
74
email = (email ?? "").toLowerCase().trim();
75
firstName = (firstName ? firstName : "Anonymous").trim();
76
lastName = (
77
lastName ? lastName : `User-${Math.round(Date.now() / 1000)}`
78
).trim();
79
registrationToken = (registrationToken ?? "").trim();
80
81
// if email is empty, then trying to create an anonymous account,
82
// which may be allowed, depending on server settings.
83
const isAnonymous = !email;
84
85
if (!isAnonymous && email && password) {
86
// Maybe there is already an account with this email and password?
87
try {
88
const account_id = await getAccount(email, password);
89
await signUserIn(req, res, account_id);
90
return;
91
} catch (_err) {
92
// fine -- just means they don't already have an account.
93
}
94
}
95
96
if (!isAnonymous) {
97
const issues = checkObviousConditions({ terms, email, password });
98
if (len(issues) > 0) {
99
res.json({ issues });
100
return;
101
}
102
}
103
104
// The UI doesn't let users try to make an account via signUp if
105
// email isn't enabled. However, they might try to directly POST
106
// to the API, so we check here as well.
107
const { email_signup, anonymous_signup, anonymous_signup_licensed_shares } =
108
await getServerSettings();
109
110
const owner_id = await getAccountId(req);
111
if (owner_id) {
112
if (isAnonymous) {
113
res.json({
114
issues: {
115
api: "Creation of anonymous accounts via the API is not allowed.",
116
},
117
});
118
return;
119
}
120
// no captcha required -- api access
121
// We ONLY allow creation without checking the captcha
122
// for trusted users.
123
try {
124
await assertTrusted(owner_id);
125
} catch (err) {
126
res.json({
127
issues: {
128
api: `${err}`,
129
},
130
});
131
return;
132
}
133
} else {
134
try {
135
await reCaptcha(req);
136
} catch (err) {
137
res.json({
138
issues: {
139
reCaptcha: err.message,
140
},
141
});
142
return;
143
}
144
}
145
146
if (isAnonymous) {
147
// Check anonymous sign up conditions.
148
if (!anonymous_signup) {
149
if (
150
anonymous_signup_licensed_shares &&
151
publicPathId &&
152
(await hasSiteLicenseId(publicPathId))
153
) {
154
// an unlisted public path with a license when anonymous_signup_licensed_shares is set is allowed
155
} else {
156
res.json({
157
issues: {
158
email: "Anonymous account creation is disabled.",
159
},
160
});
161
return;
162
}
163
}
164
} else {
165
// Check the email sign up conditions.
166
if (!email_signup) {
167
res.json({
168
issues: {
169
email: "Email account creation is disabled.",
170
},
171
});
172
return;
173
}
174
const exclusive = await isDomainExclusiveSSO(email);
175
if (exclusive) {
176
res.json({
177
issues: {
178
email: `To sign up with "@${exclusive}", you have to use the corresponding single sign on mechanism. Delete your email address above, then click the SSO icon.`,
179
},
180
});
181
return;
182
}
183
184
if (!(await isAccountAvailable(email))) {
185
res.json({
186
issues: { email: `Email address "${email}" already in use.` },
187
});
188
return;
189
}
190
}
191
192
try {
193
await redeemRegistrationToken(registrationToken);
194
} catch (err) {
195
res.json({
196
issues: {
197
registrationToken: `Issue with registration token -- ${err.message}`,
198
},
199
});
200
return;
201
}
202
203
try {
204
const account_id = v4();
205
await createAccount({
206
email,
207
password,
208
firstName,
209
lastName,
210
account_id,
211
tags,
212
signupReason,
213
owner_id,
214
});
215
216
if (email) {
217
try {
218
await sendWelcomeEmail(email, account_id);
219
} catch (err) {
220
// Expected to fail, e.g., when sendgrid or smtp not configured yet.
221
// TODO: should log using debug instead of console?
222
console.log(`WARNING: failed to send welcome email to ${email}`, err);
223
}
224
}
225
if (!owner_id) {
226
await signUserIn(req, res, account_id); // sets a cookie
227
}
228
res.json({ account_id });
229
} catch (err) {
230
res.json({ error: err.message });
231
}
232
}
233
234
export function checkObviousConditions({
235
terms,
236
email,
237
password,
238
}): SignUpIssues {
239
const issues: SignUpIssues = {};
240
if (!terms) {
241
issues.terms = "You must agree to the terms of usage.";
242
}
243
if (!email || !isValidEmailAddress(email)) {
244
issues.email = `You must provide a valid email address -- '${email}' is not valid.`;
245
}
246
if (!password || password.length < 6) {
247
issues.password = "Your password must not be very easy to guess.";
248
} else {
249
const { score, help } = passwordStrength(password);
250
if (score <= 2) {
251
issues.password = help ? help : "Your password is too easy to guess.";
252
}
253
}
254
return issues;
255
}
256
257
async function hasSiteLicenseId(id: string): Promise<boolean> {
258
return !!(await getSiteLicenseId(id));
259
}
260
261
export default apiRoute({
262
signUp: apiRouteOperation({
263
method: "POST",
264
openApiOperation: {
265
tags: ["Accounts", "Admin"],
266
},
267
})
268
.input({
269
contentType: "application/json",
270
body: SignUpInputSchema,
271
})
272
.outputs([
273
{
274
status: 200,
275
contentType: "application/json",
276
body: SignUpOutputSchema,
277
},
278
])
279
.handler(signUp),
280
});
281
282