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