Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemathinc
GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/next/pages/api/v2/auth/sign-up.ts
5963 views
1
/*
2
* This file is part of CoCalc: Copyright © 2020-2026 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 passwordStrength from "@cocalc/server/auth/password-strength";
39
import reCaptcha from "@cocalc/server/auth/recaptcha";
40
import { isExclusiveSSOEmail } from "@cocalc/server/auth/throttle";
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
import getAccountId from "lib/account/get-account";
49
import { apiRoute, apiRouteOperation } from "lib/api";
50
import assertTrusted from "lib/api/assert-trusted";
51
import getParams from "lib/api/get-params";
52
import {
53
SignUpInputSchema,
54
SignUpOutputSchema,
55
} from "lib/api/schema/accounts/sign-up";
56
import { SignUpIssues } from "lib/types/sign-up";
57
import { getAccount, signUserIn } from "./sign-in";
58
import {
59
MAX_PASSWORD_LENGTH,
60
MIN_PASSWORD_LENGTH,
61
MIN_PASSWORD_STRENGTH,
62
} from "@cocalc/util/auth";
63
64
export async function signUp(req, res) {
65
let {
66
terms,
67
email,
68
password,
69
firstName,
70
lastName,
71
registrationToken,
72
tags,
73
publicPathId,
74
signupReason,
75
} = getParams(req);
76
77
password = (password ?? "").trim();
78
email = (email ?? "").toLowerCase().trim();
79
firstName = (firstName ? firstName : "Anonymous").trim();
80
lastName = (
81
lastName ? lastName : `User-${Math.round(Date.now() / 1000)}`
82
).trim();
83
registrationToken = (registrationToken ?? "").trim();
84
85
// if email is empty, then trying to create an anonymous account,
86
// which may be allowed, depending on server settings.
87
const isAnonymous = !email;
88
89
if (!isAnonymous && email && password) {
90
// Maybe there is already an account with this email and password?
91
try {
92
const account_id = await getAccount(email, password);
93
await signUserIn(req, res, account_id);
94
return;
95
} catch (_err) {
96
// fine -- just means they don't already have an account.
97
}
98
}
99
100
if (!isAnonymous) {
101
const issues = checkObviousConditions({ terms, email, password });
102
if (len(issues) > 0) {
103
res.json({ issues });
104
return;
105
}
106
}
107
108
// The UI doesn't let users try to make an account via signUp if
109
// email isn't enabled. However, they might try to directly POST
110
// to the API, so we check here as well.
111
const { email_signup, anonymous_signup, anonymous_signup_licensed_shares } =
112
await getServerSettings();
113
114
const owner_id = await getAccountId(req);
115
if (owner_id) {
116
if (isAnonymous) {
117
res.json({
118
issues: {
119
api: "Creation of anonymous accounts via the API is not allowed.",
120
},
121
});
122
return;
123
}
124
// no captcha required -- api access
125
// We ONLY allow creation without checking the captcha
126
// for trusted users.
127
try {
128
await assertTrusted(owner_id);
129
} catch (err) {
130
res.json({
131
issues: {
132
api: `${err}`,
133
},
134
});
135
return;
136
}
137
} else {
138
try {
139
await reCaptcha(req);
140
} catch (err) {
141
res.json({
142
issues: {
143
reCaptcha: err.message,
144
},
145
});
146
return;
147
}
148
}
149
150
if (isAnonymous) {
151
// Check anonymous sign up conditions.
152
if (!anonymous_signup) {
153
if (
154
anonymous_signup_licensed_shares &&
155
publicPathId &&
156
(await hasSiteLicenseId(publicPathId))
157
) {
158
// an unlisted public path with a license when anonymous_signup_licensed_shares is set is allowed
159
} else {
160
res.json({
161
issues: {
162
email: "Anonymous account creation is disabled.",
163
},
164
});
165
return;
166
}
167
}
168
} else {
169
// Check the email sign up conditions.
170
if (!email_signup) {
171
res.json({
172
issues: {
173
email: "Email account creation is disabled.",
174
},
175
});
176
return;
177
}
178
const exclusive = await isExclusiveSSOEmail(email);
179
if (exclusive) {
180
const name = exclusive.display ?? exclusive.name;
181
res.json({
182
issues: {
183
email: `To sign up with "${name}", you have to use the corresponding single sign on mechanism. Delete your email address above, then click the SSO icon.`,
184
},
185
});
186
return;
187
}
188
189
if (!(await isAccountAvailable(email))) {
190
res.json({
191
issues: { email: `Email address "${email}" already in use.` },
192
});
193
return;
194
}
195
}
196
197
let tokenInfo;
198
try {
199
tokenInfo = await redeemRegistrationToken(registrationToken);
200
} catch (err) {
201
res.json({
202
issues: {
203
registrationToken: `Issue with registration token -- ${err.message}`,
204
},
205
});
206
return;
207
}
208
209
try {
210
const account_id = v4();
211
await createAccount({
212
email,
213
password,
214
firstName,
215
lastName,
216
account_id,
217
tags,
218
signupReason,
219
owner_id,
220
ephemeral: tokenInfo?.ephemeral,
221
customize: tokenInfo?.customize,
222
});
223
224
if (email) {
225
try {
226
await sendWelcomeEmail(email, account_id);
227
} catch (err) {
228
// Expected to fail, e.g., when sendgrid or smtp not configured yet.
229
// TODO: should log using debug instead of console?
230
console.log(`WARNING: failed to send welcome email to ${email}`, err);
231
}
232
}
233
if (!owner_id) {
234
await signUserIn(req, res, account_id); // sets a cookie
235
}
236
res.json({ account_id });
237
} catch (err) {
238
res.json({ error: err.message });
239
}
240
}
241
242
export function checkObviousConditions({
243
terms,
244
email,
245
password,
246
}): SignUpIssues {
247
const issues: SignUpIssues = {};
248
if (!terms) {
249
issues.terms = "You must agree to the terms of usage.";
250
}
251
if (!email || !isValidEmailAddress(email)) {
252
issues.email = `You must provide a valid email address -- '${email}' is not valid.`;
253
}
254
if (!password || password.length < MIN_PASSWORD_LENGTH) {
255
issues.password = "Your password must not be very easy to guess.";
256
} else if (password.length > MAX_PASSWORD_LENGTH) {
257
issues.password = `Your password must be at most ${MAX_PASSWORD_LENGTH} characters long.`;
258
} else {
259
const { score, help } = passwordStrength(password);
260
if (score <= MIN_PASSWORD_STRENGTH) {
261
issues.password = help ? help : "Your password is too easy to guess.";
262
}
263
}
264
return issues;
265
}
266
267
async function hasSiteLicenseId(id: string): Promise<boolean> {
268
return !!(await getSiteLicenseId(id));
269
}
270
271
export default apiRoute({
272
signUp: apiRouteOperation({
273
method: "POST",
274
openApiOperation: {
275
tags: ["Accounts", "Admin"],
276
},
277
})
278
.input({
279
contentType: "application/json",
280
body: SignUpInputSchema,
281
})
282
.outputs([
283
{
284
status: 200,
285
contentType: "application/json",
286
body: SignUpOutputSchema,
287
},
288
])
289
.handler(signUp),
290
});
291
292