Path: blob/master/src/packages/next/pages/api/v2/auth/sign-up.ts
1454 views
/*1* This file is part of CoCalc: Copyright © 2020 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 isDomainExclusiveSSO from "@cocalc/server/auth/is-domain-exclusive-sso";38import passwordStrength from "@cocalc/server/auth/password-strength";39import reCaptcha from "@cocalc/server/auth/recaptcha";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";4748import getAccountId from "lib/account/get-account";49import { apiRoute, apiRouteOperation } from "lib/api";50import assertTrusted from "lib/api/assert-trusted";51import getParams from "lib/api/get-params";52import {53SignUpInputSchema,54SignUpOutputSchema,55} from "lib/api/schema/accounts/sign-up";56import { SignUpIssues } from "lib/types/sign-up";57import { getAccount, signUserIn } from "./sign-in";58import { MAX_PASSWORD_LENGTH } from "@cocalc/util/auth";5960export async function signUp(req, res) {61let {62terms,63email,64password,65firstName,66lastName,67registrationToken,68tags,69publicPathId,70signupReason,71} = getParams(req);7273password = (password ?? "").trim();74email = (email ?? "").toLowerCase().trim();75firstName = (firstName ? firstName : "Anonymous").trim();76lastName = (77lastName ? lastName : `User-${Math.round(Date.now() / 1000)}`78).trim();79registrationToken = (registrationToken ?? "").trim();8081// if email is empty, then trying to create an anonymous account,82// which may be allowed, depending on server settings.83const isAnonymous = !email;8485if (!isAnonymous && email && password) {86// Maybe there is already an account with this email and password?87try {88const account_id = await getAccount(email, password);89await signUserIn(req, res, account_id);90return;91} catch (_err) {92// fine -- just means they don't already have an account.93}94}9596if (!isAnonymous) {97const issues = checkObviousConditions({ terms, email, password });98if (len(issues) > 0) {99res.json({ issues });100return;101}102}103104// The UI doesn't let users try to make an account via signUp if105// email isn't enabled. However, they might try to directly POST106// to the API, so we check here as well.107const { email_signup, anonymous_signup, anonymous_signup_licensed_shares } =108await getServerSettings();109110const owner_id = await getAccountId(req);111if (owner_id) {112if (isAnonymous) {113res.json({114issues: {115api: "Creation of anonymous accounts via the API is not allowed.",116},117});118return;119}120// no captcha required -- api access121// We ONLY allow creation without checking the captcha122// for trusted users.123try {124await assertTrusted(owner_id);125} catch (err) {126res.json({127issues: {128api: `${err}`,129},130});131return;132}133} else {134try {135await reCaptcha(req);136} catch (err) {137res.json({138issues: {139reCaptcha: err.message,140},141});142return;143}144}145146if (isAnonymous) {147// Check anonymous sign up conditions.148if (!anonymous_signup) {149if (150anonymous_signup_licensed_shares &&151publicPathId &&152(await hasSiteLicenseId(publicPathId))153) {154// an unlisted public path with a license when anonymous_signup_licensed_shares is set is allowed155} else {156res.json({157issues: {158email: "Anonymous account creation is disabled.",159},160});161return;162}163}164} else {165// Check the email sign up conditions.166if (!email_signup) {167res.json({168issues: {169email: "Email account creation is disabled.",170},171});172return;173}174const exclusive = await isDomainExclusiveSSO(email);175if (exclusive) {176res.json({177issues: {178email: `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});181return;182}183184if (!(await isAccountAvailable(email))) {185res.json({186issues: { email: `Email address "${email}" already in use.` },187});188return;189}190}191192try {193await redeemRegistrationToken(registrationToken);194} catch (err) {195res.json({196issues: {197registrationToken: `Issue with registration token -- ${err.message}`,198},199});200return;201}202203try {204const account_id = v4();205await createAccount({206email,207password,208firstName,209lastName,210account_id,211tags,212signupReason,213owner_id,214});215216if (email) {217try {218await 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?222console.log(`WARNING: failed to send welcome email to ${email}`, err);223}224}225if (!owner_id) {226await signUserIn(req, res, account_id); // sets a cookie227}228res.json({ account_id });229} catch (err) {230res.json({ error: err.message });231}232}233234export function checkObviousConditions({235terms,236email,237password,238}): SignUpIssues {239const issues: SignUpIssues = {};240if (!terms) {241issues.terms = "You must agree to the terms of usage.";242}243if (!email || !isValidEmailAddress(email)) {244issues.email = `You must provide a valid email address -- '${email}' is not valid.`;245}246if (!password || password.length < 6) {247issues.password = "Your password must not be very easy to guess.";248} else if (password.length > MAX_PASSWORD_LENGTH) {249issues.password = `Your password must be at most ${MAX_PASSWORD_LENGTH} characters long.`;250} else {251const { score, help } = passwordStrength(password);252if (score <= 2) {253issues.password = help ? help : "Your password is too easy to guess.";254}255}256return issues;257}258259async function hasSiteLicenseId(id: string): Promise<boolean> {260return !!(await getSiteLicenseId(id));261}262263export default apiRoute({264signUp: apiRouteOperation({265method: "POST",266openApiOperation: {267tags: ["Accounts", "Admin"],268},269})270.input({271contentType: "application/json",272body: SignUpInputSchema,273})274.outputs([275{276status: 200,277contentType: "application/json",278body: SignUpOutputSchema,279},280])281.handler(signUp),282});283284285