Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/src/packages/next/components/auth/sign-up.tsx
Views: 687
/*1* This file is part of CoCalc: Copyright © 2022 Sagemath, Inc.2* License: MS-RSL – see LICENSE.md for details3*/45import { Alert, Button, Checkbox, Input } from "antd";6import { CSSProperties, useEffect, useRef, useState } from "react";7import {8GoogleReCaptchaProvider,9useGoogleReCaptcha,10} from "react-google-recaptcha-v3";11import Markdown from "@cocalc/frontend/editors/slate/static-markdown";12import {13CONTACT_TAG,14CONTACT_THESE_TAGS,15} from "@cocalc/util/db-schema/accounts";16import {17is_valid_email_address as isValidEmailAddress,18len,19plural,20smallIntegerToEnglishWord,21} from "@cocalc/util/misc";22import { COLORS } from "@cocalc/util/theme";23import { Strategy } from "@cocalc/util/types/sso";24import A from "components/misc/A";25import Loading from "components/share/loading";26import apiPost from "lib/api/post";27import useCustomize from "lib/use-customize";28import AuthPageContainer from "./fragments/auth-page-container";29import SSO, { RequiredSSO, useRequiredSSO } from "./sso";30import Tags from "./tags";3132const LINE: CSSProperties = { margin: "15px 0" } as const;3334interface SignUpProps {35minimal?: boolean; // use a minimal interface with less explanation and instructions (e.g., for embedding in other pages)36requiresToken?: boolean; // will be determined by API call if not given.37onSuccess?: (opts?: {}) => void; // if given, call after sign up *succeeds*.38has_site_license?: boolean;39publicPathId?: string;40showSignIn?: boolean;41signInAction?: () => void; // if given, replaces the default sign-in link behavior.42requireTags: boolean;43}4445export default function SignUp(props: SignUpProps) {46const { reCaptchaKey } = useCustomize();4748const body = <SignUp0 {...props} />;49if (reCaptchaKey == null) {50return body;51}5253return (54<GoogleReCaptchaProvider reCaptchaKey={reCaptchaKey}>55{body}56</GoogleReCaptchaProvider>57);58}5960function SignUp0({61requiresToken,62minimal,63onSuccess,64has_site_license,65publicPathId,66signInAction,67showSignIn,68requireTags,69}: SignUpProps) {70const {71anonymousSignup,72anonymousSignupLicensedShares,73siteName,74emailSignup,75accountCreationInstructions,76reCaptchaKey,77onCoCalcCom,78} = useCustomize();79const [tags, setTags] = useState<Set<string>>(new Set());80const [signupReason, setSignupReason] = useState<string>("");81const [email, setEmail] = useState<string>("");82const [registrationToken, setRegistrationToken] = useState<string>("");83const [password, setPassword] = useState<string>("");84const [firstName, setFirstName] = useState<string>("");85const [lastName, setLastName] = useState<string>("");86const [signingUp, setSigningUp] = useState<boolean>(false);87const [issues, setIssues] = useState<{88email?: string;89password?: string;90error?: string;91registrationToken?: string;92reCaptcha?: string;93}>({});9495const minTags = requireTags ? 1 : 0;96const showContact = CONTACT_THESE_TAGS.some((t) => tags.has(t));97const requestContact = tags.has(CONTACT_TAG) && showContact;9899const submittable = useRef<boolean>(false);100const { executeRecaptcha } = useGoogleReCaptcha();101const { strategies } = useCustomize();102103// Sometimes the user if this component knows requiresToken and sometimes they don't.104// If they don't, we have to make an API call to figure it out.105const [requiresToken2, setRequiresToken2] = useState<boolean | undefined>(106requiresToken,107);108109useEffect(() => {110if (requiresToken2 === undefined) {111(async () => {112try {113setRequiresToken2(await apiPost("/auth/requires-token"));114} catch (err) {}115})();116}117}, []);118119// based on email: if user has to sign up via SSO, this will tell which strategy to use.120const requiredSSO = useRequiredSSO(strategies, email);121122if (requiresToken2 === undefined || strategies == null) {123return <Loading />;124}125126// number of tags except for the one name "CONTACT_TAG"127const tagsSize = tags.size - (requestContact ? 1 : 0);128const needsTags = !minimal && onCoCalcCom && tagsSize < minTags;129const what = "role";130131submittable.current = !!(132requiredSSO == null &&133(!requiresToken2 || registrationToken) &&134email &&135isValidEmailAddress(email) &&136password &&137password.length >= 6 &&138firstName?.trim() &&139lastName?.trim() &&140!needsTags141);142143async function signUp() {144if (signingUp) return;145setIssues({});146try {147setSigningUp(true);148149let reCaptchaToken: undefined | string;150if (reCaptchaKey) {151if (!executeRecaptcha) {152throw Error("Please wait a few seconds, then try again.");153}154reCaptchaToken = await executeRecaptcha("signup");155}156157const result = await apiPost("/auth/sign-up", {158terms: true,159email,160password,161firstName,162lastName,163registrationToken,164reCaptchaToken,165publicPathId,166tags: Array.from(tags),167signupReason,168});169if (result.issues && len(result.issues) > 0) {170setIssues(result.issues);171} else {172onSuccess?.({});173}174} catch (err) {175setIssues({ error: `${err}` });176} finally {177setSigningUp(false);178}179}180181if (!emailSignup && strategies.length == 0) {182return (183<Alert184style={{ margin: "30px 15%" }}185type="error"186showIcon187message={"No Account Creation Allowed"}188description={189<div style={{ fontSize: "14pt", marginTop: "20px" }}>190<b>191There is no method enabled for creating an account on this server.192</b>193{(anonymousSignup ||194(anonymousSignupLicensedShares && has_site_license)) && (195<>196<br />197<br />198However, you can still{" "}199<A href="/auth/try">200try {siteName} without creating an account.201</A>202</>203)}204</div>205}206/>207);208}209210function renderFooter() {211return (212(!minimal || showSignIn) && (213<>214<div>215Already have an account?{" "}216{signInAction ? (217<a onClick={signInAction}>Sign In</a>218) : (219<A href="/auth/sign-in">Sign In</A>220)}{" "}221{anonymousSignup && (222<>223or{" "}224<A href="/auth/try">225{" "}226try {siteName} without creating an account.{" "}227</A>228</>229)}230</div>231</>232)233);234}235236function renderError() {237return (238issues.error && (239<Alert style={LINE} type="error" showIcon message={issues.error} />240)241);242}243244function renderSubtitle() {245return (246<>247<h4 style={{ color: COLORS.GRAY_M, marginBottom: "35px" }}>248Start collaborating for free today.249</h4>250{accountCreationInstructions && (251<Markdown value={accountCreationInstructions} />252)}253</>254);255}256257return (258<AuthPageContainer259error={renderError()}260footer={renderFooter()}261subtitle={renderSubtitle()}262minimal={minimal}263title={`Create a free account with ${siteName}`}264>265<div>266By creating an account, you agree to the{" "}267<A external={true} href="/policies/terms">268Terms of Service269</A>270.271</div>272{!minimal && onCoCalcCom ? (273<Tags274setTags={setTags}275signupReason={signupReason}276setSignupReason={setSignupReason}277tags={tags}278minTags={minTags}279what={what}280style={{ width: "880px", maxWidth: "100%", marginTop: "20px" }}281contact={showContact}282warning={needsTags}283/>284) : undefined}285<form>286{issues.reCaptcha ? (287<Alert288style={LINE}289type="error"290showIcon291message={issues.reCaptcha}292description={<>You may have to contact the site administrator.</>}293/>294) : undefined}295{issues.registrationToken && (296<Alert297style={LINE}298type="error"299showIcon300message={issues.registrationToken}301description={302<>303You may have to contact the site administrator for a304registration token.305</>306}307/>308)}309{requiresToken2 && (310<div style={LINE}>311<p>Registration Token</p>312<Input313style={{ fontSize: "12pt" }}314value={registrationToken}315placeholder="Enter your secret registration token"316onChange={(e) => setRegistrationToken(e.target.value)}317/>318</div>319)}320<EmailOrSSO321email={email}322setEmail={setEmail}323signUp={signUp}324strategies={strategies}325hideSSO={requiredSSO != null}326/>327<RequiredSSO strategy={requiredSSO} />328{issues.email && (329<Alert330style={LINE}331type="error"332showIcon333message={issues.email}334description={335<>336Choose a different email address,{" "}337<A href="/auth/sign-in">sign in</A>, or{" "}338<A href="/auth/password-reset">reset your password</A>.339</>340}341/>342)}343{requiredSSO == null && (344<div style={LINE}>345<p>Password</p>346<Input.Password347style={{ fontSize: "12pt" }}348value={password}349placeholder="Password"350autoComplete="new-password"351onChange={(e) => setPassword(e.target.value)}352onPressEnter={signUp}353/>354</div>355)}356{issues.password && (357<Alert style={LINE} type="error" showIcon message={issues.password} />358)}359{requiredSSO == null && (360<div style={LINE}>361<p>First name (Given name)</p>362<Input363style={{ fontSize: "12pt" }}364placeholder="First name"365value={firstName}366onChange={(e) => setFirstName(e.target.value)}367onPressEnter={signUp}368/>369</div>370)}371{requiredSSO == null && (372<div style={LINE}>373<p>Last name (Family name)</p>374<Input375style={{ fontSize: "12pt" }}376placeholder="Last name"377value={lastName}378onChange={(e) => setLastName(e.target.value)}379onPressEnter={signUp}380/>381</div>382)}383</form>384<div style={LINE}>385<Button386shape="round"387size="large"388disabled={!submittable.current || signingUp}389type="primary"390style={{391width: "100%",392marginTop: "15px",393color:394!submittable.current || signingUp395? COLORS.ANTD_RED_WARN396: undefined,397}}398onClick={signUp}399>400{needsTags && tagsSize < minTags401? `Select at least ${smallIntegerToEnglishWord(minTags)} ${plural(402minTags,403what,404)}`405: requiresToken2 && !registrationToken406? "Enter the secret registration token"407408? "How will you sign in?"409: !isValidEmailAddress(email)410? "Enter a valid email address above"411: requiredSSO != null412? "You must sign up via SSO"413: !password || password.length < 6414? "Choose password with at least 6 characters"415: !firstName?.trim()416? "Enter your first name above"417: !lastName?.trim()418? "Enter your last name above"419: signingUp420? ""421: "Sign Up!"}422{signingUp && (423<span style={{ marginLeft: "15px" }}>424<Loading>Signing Up...</Loading>425</span>426)}427</Button>428</div>429</AuthPageContainer>430);431}432433interface EmailOrSSOProps {434email: string;435setEmail: (email: string) => void;436signUp: () => void;437strategies?: Strategy[];438hideSSO?: boolean;439}440441function EmailOrSSO(props: EmailOrSSOProps) {442const { email, setEmail, signUp, strategies = [], hideSSO = false } = props;443const { emailSignup } = useCustomize();444445function renderSSO() {446if (strategies.length == 0) return;447448const emailStyle: CSSProperties = email449? { textAlign: "right", marginBottom: "20px" }450: {};451452const style: CSSProperties = {453display: hideSSO ? "none" : "block",454...emailStyle,455};456457return (458<div style={{ textAlign: "center", margin: "20px 0" }}>459<SSO size={email ? 24 : undefined} style={style} />460</div>461);462}463464return (465<div>466<div>467<p style={{ color: "#444", marginTop: "10px" }}>468{hideSSO469? "Sign up using your single sign-on provider"470: strategies.length > 0 && emailSignup471? "Sign up using either your email address or a single sign-on provider."472: emailSignup473? "Enter the email address you will use to sign in."474: "Sign up using a single sign-on provider."}475</p>476</div>477{renderSSO()}478{emailSignup ? (479<p>480<p>Email address</p>481<Input482style={{ fontSize: "12pt" }}483placeholder="Email address"484autoComplete="username"485value={email}486onChange={(e) => setEmail(e.target.value)}487onPressEnter={signUp}488/>489</p>490) : undefined}491</div>492);493}494495export function TermsCheckbox({496checked,497onChange,498style,499}: {500checked?: boolean;501onChange?: (boolean) => void;502style?: CSSProperties;503}) {504return (505<Checkbox506checked={checked}507style={style}508onChange={(e) => onChange?.(e.target.checked)}509>510I agree to the{" "}511<A external={true} href="/policies/terms">512Terms of Service513</A>514.515</Checkbox>516);517}518519520