Path: blob/master/src/packages/next/pages/api/v2/auth/sign-up.ts
5963 views
/*1* This file is part of CoCalc: Copyright © 2020-2026 Sagemath, Inc.2* License: MS-RSL – see LICENSE.md for details3*/45/*6Sign up for a new account:780. If email/password matches an existing account, just sign them in. Reduces confusion.91. Reject if password is absurdly weak.102. Query the database to make sure the email address is not already taken.113. Generate a random account_id. Do not check it is not already taken, since that's12highly unlikely, and the insert in 4 would fail anyways.134. Write account to the database.145. Sign user in (if not being used via the API).1516This can also be used via the API, but the client must have a minimum balance17of at least - $100.181920API Usage:2122curl -u sk_abcdefQWERTY090900000000: \23-d firstName=John00 \24-d lastName=Doe00 \25-d [email protected] \26-d password=xyzabc09090 \27-d terms=true https://cocalc.com/api/v2/auth/sign-up2829TIP: If you want to pass in an email like [email protected], use '%2B' in place of '+'.30*/3132import { v4 } from "uuid";3334import { getServerSettings } from "@cocalc/database/settings/server-settings";35import createAccount from "@cocalc/server/accounts/create-account";36import isAccountAvailable from "@cocalc/server/auth/is-account-available";37import passwordStrength from "@cocalc/server/auth/password-strength";38import reCaptcha from "@cocalc/server/auth/recaptcha";39import { isExclusiveSSOEmail } from "@cocalc/server/auth/throttle";40import redeemRegistrationToken from "@cocalc/server/auth/tokens/redeem";41import sendWelcomeEmail from "@cocalc/server/email/welcome-email";42import getSiteLicenseId from "@cocalc/server/public-paths/site-license-id";43import {44is_valid_email_address as isValidEmailAddress,45len,46} from "@cocalc/util/misc";47import getAccountId from "lib/account/get-account";48import { apiRoute, apiRouteOperation } from "lib/api";49import assertTrusted from "lib/api/assert-trusted";50import getParams from "lib/api/get-params";51import {52SignUpInputSchema,53SignUpOutputSchema,54} from "lib/api/schema/accounts/sign-up";55import { SignUpIssues } from "lib/types/sign-up";56import { getAccount, signUserIn } from "./sign-in";57import {58MAX_PASSWORD_LENGTH,59MIN_PASSWORD_LENGTH,60MIN_PASSWORD_STRENGTH,61} from "@cocalc/util/auth";6263export async function signUp(req, res) {64let {65terms,66email,67password,68firstName,69lastName,70registrationToken,71tags,72publicPathId,73signupReason,74} = getParams(req);7576password = (password ?? "").trim();77email = (email ?? "").toLowerCase().trim();78firstName = (firstName ? firstName : "Anonymous").trim();79lastName = (80lastName ? lastName : `User-${Math.round(Date.now() / 1000)}`81).trim();82registrationToken = (registrationToken ?? "").trim();8384// if email is empty, then trying to create an anonymous account,85// which may be allowed, depending on server settings.86const isAnonymous = !email;8788if (!isAnonymous && email && password) {89// Maybe there is already an account with this email and password?90try {91const account_id = await getAccount(email, password);92await signUserIn(req, res, account_id);93return;94} catch (_err) {95// fine -- just means they don't already have an account.96}97}9899if (!isAnonymous) {100const issues = checkObviousConditions({ terms, email, password });101if (len(issues) > 0) {102res.json({ issues });103return;104}105}106107// The UI doesn't let users try to make an account via signUp if108// email isn't enabled. However, they might try to directly POST109// to the API, so we check here as well.110const { email_signup, anonymous_signup, anonymous_signup_licensed_shares } =111await getServerSettings();112113const owner_id = await getAccountId(req);114if (owner_id) {115if (isAnonymous) {116res.json({117issues: {118api: "Creation of anonymous accounts via the API is not allowed.",119},120});121return;122}123// no captcha required -- api access124// We ONLY allow creation without checking the captcha125// for trusted users.126try {127await assertTrusted(owner_id);128} catch (err) {129res.json({130issues: {131api: `${err}`,132},133});134return;135}136} else {137try {138await reCaptcha(req);139} catch (err) {140res.json({141issues: {142reCaptcha: err.message,143},144});145return;146}147}148149if (isAnonymous) {150// Check anonymous sign up conditions.151if (!anonymous_signup) {152if (153anonymous_signup_licensed_shares &&154publicPathId &&155(await hasSiteLicenseId(publicPathId))156) {157// an unlisted public path with a license when anonymous_signup_licensed_shares is set is allowed158} else {159res.json({160issues: {161email: "Anonymous account creation is disabled.",162},163});164return;165}166}167} else {168// Check the email sign up conditions.169if (!email_signup) {170res.json({171issues: {172email: "Email account creation is disabled.",173},174});175return;176}177const exclusive = await isExclusiveSSOEmail(email);178if (exclusive) {179const name = exclusive.display ?? exclusive.name;180res.json({181issues: {182email: `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.`,183},184});185return;186}187188if (!(await isAccountAvailable(email))) {189res.json({190issues: { email: `Email address "${email}" already in use.` },191});192return;193}194}195196let tokenInfo;197try {198tokenInfo = await redeemRegistrationToken(registrationToken);199} catch (err) {200res.json({201issues: {202registrationToken: `Issue with registration token -- ${err.message}`,203},204});205return;206}207208try {209const account_id = v4();210await createAccount({211email,212password,213firstName,214lastName,215account_id,216tags,217signupReason,218owner_id,219ephemeral: tokenInfo?.ephemeral,220customize: tokenInfo?.customize,221});222223if (email) {224try {225await sendWelcomeEmail(email, account_id);226} catch (err) {227// Expected to fail, e.g., when sendgrid or smtp not configured yet.228// TODO: should log using debug instead of console?229console.log(`WARNING: failed to send welcome email to ${email}`, err);230}231}232if (!owner_id) {233await signUserIn(req, res, account_id); // sets a cookie234}235res.json({ account_id });236} catch (err) {237res.json({ error: err.message });238}239}240241export function checkObviousConditions({242terms,243email,244password,245}): SignUpIssues {246const issues: SignUpIssues = {};247if (!terms) {248issues.terms = "You must agree to the terms of usage.";249}250if (!email || !isValidEmailAddress(email)) {251issues.email = `You must provide a valid email address -- '${email}' is not valid.`;252}253if (!password || password.length < MIN_PASSWORD_LENGTH) {254issues.password = "Your password must not be very easy to guess.";255} else if (password.length > MAX_PASSWORD_LENGTH) {256issues.password = `Your password must be at most ${MAX_PASSWORD_LENGTH} characters long.`;257} else {258const { score, help } = passwordStrength(password);259if (score <= MIN_PASSWORD_STRENGTH) {260issues.password = help ? help : "Your password is too easy to guess.";261}262}263return issues;264}265266async function hasSiteLicenseId(id: string): Promise<boolean> {267return !!(await getSiteLicenseId(id));268}269270export default apiRoute({271signUp: apiRouteOperation({272method: "POST",273openApiOperation: {274tags: ["Accounts", "Admin"],275},276})277.input({278contentType: "application/json",279body: SignUpInputSchema,280})281.outputs([282{283status: 200,284contentType: "application/json",285body: SignUpOutputSchema,286},287])288.handler(signUp),289});290291292