From 6de4eca4fcf462b9474d9a0613c362a6552c0c60 Mon Sep 17 00:00:00 2001 From: Vladyslav Matsiiako <matsiiako@gmail.com> Date: Sat, 7 Jan 2023 16:40:28 -0800 Subject: [PATCH] Refactored signup and added team invitation step --- frontend/components/basic/Error.tsx | 6 +- frontend/components/basic/Layout.tsx | 1 + frontend/components/dashboard/DropZone.tsx | 2 +- frontend/components/signup/CodeInputStep.tsx | 136 +++++ .../signup/DonwloadBackupPDFStep.tsx | 60 ++ frontend/components/signup/EnterEmailStep.tsx | 97 ++++ frontend/components/signup/TeamInviteStep.tsx | 72 +++ frontend/components/signup/UserInfoStep.tsx | 306 ++++++++++ ...not-verified.js => email-not-verified.tsx} | 2 +- frontend/pages/signup.tsx | 525 +----------------- frontend/pages/signupinvite.js | 2 +- frontend/public/locales/en/common.json | 4 +- frontend/public/locales/en/dashboard.json | 1 + frontend/public/locales/en/signup.json | 6 +- 14 files changed, 706 insertions(+), 514 deletions(-) create mode 100644 frontend/components/signup/CodeInputStep.tsx create mode 100644 frontend/components/signup/DonwloadBackupPDFStep.tsx create mode 100644 frontend/components/signup/EnterEmailStep.tsx create mode 100644 frontend/components/signup/TeamInviteStep.tsx create mode 100644 frontend/components/signup/UserInfoStep.tsx rename frontend/pages/{email-not-verified.js => email-not-verified.tsx} (93%) diff --git a/frontend/components/basic/Error.tsx b/frontend/components/basic/Error.tsx index 79cc6e5f1..b5f0b7a4c 100644 --- a/frontend/components/basic/Error.tsx +++ b/frontend/components/basic/Error.tsx @@ -4,13 +4,13 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; export default function Error({ text }: { text: string }): JSX.Element { return ( - <div className="relative bg-red-500 opacity-100 border flex flex-row justify-center m-auto items-center w-fit rounded-full mb-4"> + <div className="relative flex flex-row justify-center m-auto items-center w-fit rounded-full"> <FontAwesomeIcon icon={faExclamationTriangle} - className="text-white mt-1.5 mb-2 mx-2" + className="text-red mt-1.5 mb-2 mx-2" /> {text && ( - <p className="relative top-0 text-white mr-2 text-sm py-1">{text}</p> + <p className="relative top-0 text-red mr-2 text-sm py-1">{text}</p> )} </div> ); diff --git a/frontend/components/basic/Layout.tsx b/frontend/components/basic/Layout.tsx index 235bfaa25..ef8eda727 100644 --- a/frontend/components/basic/Layout.tsx +++ b/frontend/components/basic/Layout.tsx @@ -183,6 +183,7 @@ export default function Layout({ children }: LayoutProps) { if ( userWorkspaces.length == 0 && router.asPath != "/noprojects" && + !router.asPath.includes("home")&& !router.asPath.includes("settings") ) { router.push("/noprojects"); diff --git a/frontend/components/dashboard/DropZone.tsx b/frontend/components/dashboard/DropZone.tsx index 38e293f4e..ffde34566 100644 --- a/frontend/components/dashboard/DropZone.tsx +++ b/frontend/components/dashboard/DropZone.tsx @@ -216,7 +216,7 @@ const DropZone = ({ <div className="z-10 mb-6"> <Button color="mineshaft" - text="Create a new .env file" + text={String(t("dashboard:add-secret"))} onButtonPressed={createNewFile} size="md" /> diff --git a/frontend/components/signup/CodeInputStep.tsx b/frontend/components/signup/CodeInputStep.tsx new file mode 100644 index 000000000..05c4973a8 --- /dev/null +++ b/frontend/components/signup/CodeInputStep.tsx @@ -0,0 +1,136 @@ +import React, { useState } from "react"; +import ReactCodeInput from "react-code-input"; +import { useTranslation } from "next-i18next"; + +import sendVerificationEmail from "~/pages/api/auth/SendVerificationEmail"; + +import Button from "../basic/buttons/Button"; +import Error from "../basic/Error"; + + +// The style for the verification code input +const props = { + inputStyle: { + fontFamily: "monospace", + margin: "4px", + MozAppearance: "textfield", + width: "55px", + borderRadius: "5px", + fontSize: "24px", + height: "55px", + paddingLeft: "7", + backgroundColor: "#0d1117", + color: "white", + border: "1px solid #2d2f33", + textAlign: "center", + outlineColor: "#8ca542", + borderColor: "#2d2f33" + }, +} as const; +const propsPhone = { + inputStyle: { + fontFamily: "monospace", + margin: "4px", + MozAppearance: "textfield", + width: "40px", + borderRadius: "5px", + fontSize: "24px", + height: "40px", + paddingLeft: "7", + backgroundColor: "#0d1117", + color: "white", + border: "1px solid #2d2f33", + textAlign: "center", + outlineColor: "#8ca542", + borderColor: "#2d2f33" + }, +} as const; + +interface CodeInputStepProps { + email: string; + incrementStep: () => void; + setCode: (value: string) => void; + codeError: boolean; +} + +/** + * This is the second step of sign up where users need to verify their email + * @param {object} obj + * @param {string} obj.email - user's email to which we just sent a verification email + * @param {function} obj.incrementStep - goes to the next step of signup + * @param {function} obj.setCode - state updating function that set the current value of the emai verification code + * @param {boolean} obj.codeError - whether the code was inputted wrong or now + * @returns + */ +export default function CodeInputStep({ email, incrementStep, setCode, codeError }: CodeInputStepProps): JSX.Element { + const [isLoading, setIsLoading] = useState(false); + const [isResendingVerificationEmail, setIsResendingVerificationEmail] = + useState(false); + const { t } = useTranslation(); + + const resendVerificationEmail = async () => { + setIsResendingVerificationEmail(true); + setIsLoading(true); + sendVerificationEmail(email); + setTimeout(() => { + setIsLoading(false); + setIsResendingVerificationEmail(false); + }, 2000); + }; + + return ( + <div className="bg-bunker w-max mx-auto h-7/12 pt-10 pb-4 px-8 rounded-xl drop-shadow-xl mb-64 md:mb-16"> + <p className="text-l flex justify-center text-bunker-300"> + {"We've"} sent a verification email to{" "} + </p> + <p className="text-l flex justify-center font-semibold my-2 text-bunker-300"> + {email}{" "} + </p> + <div className="hidden md:block"> + <ReactCodeInput + name="" + inputMode="tel" + type="text" + fields={6} + onChange={setCode} + {...props} + className="mt-6 mb-2" + /> + </div> + <div className="block md:hidden"> + <ReactCodeInput + name="" + inputMode="tel" + type="text" + fields={6} + onChange={setCode} + {...propsPhone} + className="mt-2 mb-6" + /> + </div> + {codeError && <Error text={t("signup:step2-code-error")} />} + <div className="flex max-w-max min-w-28 flex-col items-center justify-center md:p-2 max-h-24 mx-auto text-lg px-4 mt-4 mb-2"> + <Button + text={t("signup:verify") ?? ""} + onButtonPressed={incrementStep} + size="lg" + /> + </div> + <div className="flex flex-col items-center justify-center w-full max-h-24 max-w-md mx-auto pt-2"> + <div className="flex flex-row items-baseline gap-1 text-sm"> + <span className="text-bunker-400"> + Not seeing an email? + </span> + <u className={`font-normal ${isResendingVerificationEmail ? 'text-bunker-400' : 'text-primary-700 hover:text-primary duration-200'}`}> + <button disabled={isLoading} onClick={resendVerificationEmail}> + {isResendingVerificationEmail ? "Resending..." : "Resend"} + </button> + </u> + </div> + <p className="text-sm text-bunker-400 pb-2"> + {t("signup:step2-spam-alert")} + </p> + </div> + </div> + ); +} diff --git a/frontend/components/signup/DonwloadBackupPDFStep.tsx b/frontend/components/signup/DonwloadBackupPDFStep.tsx new file mode 100644 index 000000000..786c49fd2 --- /dev/null +++ b/frontend/components/signup/DonwloadBackupPDFStep.tsx @@ -0,0 +1,60 @@ +import React from "react"; +import { useTranslation } from "next-i18next"; +import { faWarning } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import Button from "../basic/buttons/Button"; +import issueBackupKey from "../utilities/cryptography/issueBackupKey"; + + +interface DownloadBackupPDFStepProps { + incrementStep: () => void; + email: string; + password: string; + name: string; +} + +/** + * This is the step of the signup flow where the user downloads the backup pdf + * @param {object} obj + * @param {function} obj.incrementStep - function that moves the user on to the next stage of signup + * @param {string} obj.email - user's email + * @param {string} obj.password - user's password + * @param {string} obj.name - user's name + * @returns + */ +export default function DonwloadBackupPDFStep({ incrementStep, email, password, name }: DownloadBackupPDFStepProps): JSX.Element { + const { t } = useTranslation(); + + return ( + <div className="bg-bunker flex flex-col items-center w-full max-w-xs md:max-w-lg mx-auto h-7/12 py-8 px-4 md:px-6 mx-1 mb-36 md:mb-16 rounded-xl drop-shadow-xl"> + <p className="text-4xl text-center font-semibold flex justify-center text-transparent bg-clip-text bg-gradient-to-br from-sky-400 to-primary"> + {t("signup:step4-message")} + </p> + <div className="flex flex-col items-center justify-center w-full mt-4 md:mt-8 max-w-md text-gray-400 text-md rounded-md px-2"> + <div>{t("signup:step4-description1")}</div> + <div className="mt-3">{t("signup:step4-description2")}</div> + </div> + <div className="w-full p-2 flex flex-row items-center bg-white/10 text-gray-400 rounded-md max-w-xs md:max-w-md mx-auto mt-4"> + <FontAwesomeIcon icon={faWarning} className="ml-2 mr-4 text-4xl" /> + {t("signup:step4-description3")} + </div> + <div className="flex flex-col items-center justify-center md:px-4 md:py-5 mt-2 px-2 py-3 max-h-24 max-w-max mx-auto text-lg"> + <Button + text="Download PDF" + onButtonPressed={async () => { + await issueBackupKey({ + email, + password, + personalName: name, + setBackupKeyError: (value: boolean) => {}, + setBackupKeyIssued: (value: boolean) => {}, + }); + incrementStep(); + }} + size="lg" + /> + </div> + </div> + ); +} diff --git a/frontend/components/signup/EnterEmailStep.tsx b/frontend/components/signup/EnterEmailStep.tsx new file mode 100644 index 000000000..5f98710b7 --- /dev/null +++ b/frontend/components/signup/EnterEmailStep.tsx @@ -0,0 +1,97 @@ +import React, { useState } from "react"; +import Link from "next/link"; +import { useTranslation } from "next-i18next"; + +import sendVerificationEmail from "~/pages/api/auth/SendVerificationEmail"; + +import Button from "../basic/buttons/Button"; +import InputField from "../basic/InputField"; + + +interface DownloadBackupPDFStepProps { + incrementStep: () => void; + email: string; + setEmail: (value: string) => void; +} + +/** + * This is the first step of the sign up process - users need to enter their email + * @param {object} obj + * @param {string} obj.email - email of a user signing up + * @param {function} obj.setEmail - funciton that manages the state of the email variable + * @param {function} obj.incrementStep - function to go to the next step of the signup flow + * @returns + */ +export default function EnterEmailStep({ email, setEmail, incrementStep }: DownloadBackupPDFStepProps): JSX.Element { + const [emailError, setEmailError] = useState(false); + const [emailErrorMessage, setEmailErrorMessage] = useState(""); + const { t } = useTranslation(); + + /** + * Verifies if the entered email "looks" correct + */ + const emailCheck = () => { + let emailCheckBool = false; + if (!email) { + setEmailError(true); + setEmailErrorMessage("Please enter your email."); + emailCheckBool = true; + } else if ( + !email.includes("@") || + !email.includes(".") || + !/[a-z]/.test(email) + ) { + setEmailError(true); + setEmailErrorMessage("Please enter a valid email."); + emailCheckBool = true; + } else { + setEmailError(false); + } + + // If everything is correct, go to the next step + if (!emailCheckBool) { + sendVerificationEmail(email); + incrementStep(); + } + }; + + return ( + <div> + <div className="bg-bunker w-full max-w-md mx-auto h-7/12 py-8 md:px-6 mx-1 rounded-xl drop-shadow-xl"> + <p className="text-4xl font-semibold flex justify-center text-primary"> + {'Let\''}s get started + </p> + <div className="flex items-center justify-center w-5/6 md:w-full m-auto md:p-2 rounded-lg max-h-24 mt-4"> + <InputField + label="Email" + onChangeHandler={setEmail} + type="email" + value={email} + placeholder="" + isRequired + error={emailError} + errorText={emailErrorMessage} + autoComplete="username" + /> + </div> + <div className="flex flex-col items-center justify-center w-5/6 md:w-full md:p-2 max-h-28 max-w-xs md:max-w-md mx-auto mt-4 md:mt-4 text-sm text-center md:text-left"> + <p className="text-gray-400 mt-2 md:mx-0.5"> + {t("signup:step1-privacy")} + </p> + <div className="text-l mt-6 m-2 md:m-8 px-8 py-1 text-lg"> + <Button text="Get Started" type="submit" onButtonPressed={emailCheck} size="lg" /> + </div> + </div> + </div> + <div className="flex flex-col items-center justify-center w-full md:pb-2 max-w-md mx-auto pt-2 mb-48 md:mb-16 mt-2"> + <Link href="/login"> + <button type="button" className="w-max pb-3 hover:opacity-90 duration-200"> + <u className="font-normal text-sm text-primary-500"> + {t("signup:already-have-account")} + </u> + </button> + </Link> + </div> + </div> + ); +} diff --git a/frontend/components/signup/TeamInviteStep.tsx b/frontend/components/signup/TeamInviteStep.tsx new file mode 100644 index 000000000..e2639b88f --- /dev/null +++ b/frontend/components/signup/TeamInviteStep.tsx @@ -0,0 +1,72 @@ +import React, { useState } from "react"; +import { useRouter } from "next/router"; +import { useTranslation } from "next-i18next"; + +import addUserToOrg from "~/pages/api/organization/addUserToOrg"; +import getWorkspaces from "~/pages/api/workspace/getWorkspaces"; + +import Button from "../basic/buttons/Button"; + + +/** + * This is the last step of the signup flow. People can optionally invite their teammates here. + */ +export default function TeamInviteStep(): JSX.Element { + const [emails, setEmails] = useState(""); + const { t } = useTranslation(); + const router = useRouter(); + + // Redirect user to the getting started page + const redirectToHome = async () => { + const userWorkspaces = await getWorkspaces(); + const userWorkspace = userWorkspaces[0]._id; + router.push("/home/" + userWorkspace); + + } + + const inviteUsers = async ({ emails }: { emails: string; }) => { + emails + .split(',') + .map(email => email.trim()) + .map(async (email) => await addUserToOrg(email, String(localStorage.getItem('orgData.id')))); + + await redirectToHome(); + } + + return ( + <div className="bg-bunker w-max mx-auto h-7/12 pt-6 pb-4 px-8 rounded-xl drop-shadow-xl mb-64 md:mb-32"> + <p className="text-4xl font-semibold flex justify-center text-primary"> + {t("signup:step5-invite-team")} + </p> + <p className="text-center flex justify-center text-bunker-300 max-w-xs md:max-w-sm md:mx-8 mb-6 mt-4"> + {t("signup:step5-subtitle")} + </p> + <div> + <div className="overflow-auto bg-bunker-800"> + <div className="whitespace-pre-wrap break-words bg-transparent"> + + </div> + </div> + <textarea + className="bg-bunker-800 h-20 w-full placeholder:text-bunker-400 py-1 px-2 rounded-md border border-mineshaft-500 text-sm text-bunker-300 outline-none focus:ring-2 ring-primary-800 ring-opacity-70" + value={emails} + onChange={(e) => setEmails(e.target.value)} + placeholder="email@example.com, email2@example.com..." + /> + </div> + <div className="flex flex-row max-w-max min-w-28 items-center justify-center md:p-2 max-h-24 mx-auto text-lg px-4 mt-4 mb-2"> + <div + className="text-md md:text-sm mx-3 text-bunker-300 bg-mineshaft-700 py-3 md:py-3.5 px-5 rounded-md cursor-pointer hover:bg-mineshaft-500 duration-200" + onClick={redirectToHome} + > + {t("signup:step5-skip")} + </div> + <Button + text={t("signup:step5-send-invites") ?? ""} + onButtonPressed={() => inviteUsers({ emails})} + size="lg" + /> + </div> + </div> + ); +} diff --git a/frontend/components/signup/UserInfoStep.tsx b/frontend/components/signup/UserInfoStep.tsx new file mode 100644 index 000000000..2732f8649 --- /dev/null +++ b/frontend/components/signup/UserInfoStep.tsx @@ -0,0 +1,306 @@ +import React, { useState } from "react"; +import { useRouter } from "next/router"; +import { useTranslation } from "next-i18next"; +import { faCheck, faX } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import completeAccountInformationSignup from "~/pages/api/auth/CompleteAccountInformationSignup"; + +import Button from "../basic/buttons/Button"; +import InputField from "../basic/InputField"; +import attemptLogin from "../utilities/attemptLogin"; +import passwordCheck from "../utilities/checks/PasswordCheck"; +import Aes256Gcm from "../utilities/cryptography/aes-256-gcm"; + +const nacl = require("tweetnacl"); +const jsrp = require("jsrp"); +nacl.util = require("tweetnacl-util"); +const client = new jsrp.client(); + +interface UserInfoStepProps { + verificationToken: string; + incrementStep: () => void; + email: string; + password: string; + setPassword: (value: string) => void; + firstName: string; + setFirstName: (value: string) => void; + lastName: string; + setLastName: (value: string) => void; +} + +/** + * This is the step of the sign up flow where people provife their name/surname and password + * @param {object} obj + * @param {string} obj.verificationToken - the token which we use to verify the legitness of a user + * @param {string} obj.incrementStep - a function to move to the next signup step + * @param {string} obj.email - email of a user who is signing up + * @param {string} obj.password - user's password + * @param {string} obj.setPassword - function managing the state of user's password + * @param {string} obj.firstName - user's first name + * @param {string} obj.setFirstName - function managing the state of user's first name + * @param {string} obj.lastName - user's lastName + * @param {string} obj.setLastName - function managing the state of user's last name + */ +export default function UserInfoStep({ + verificationToken, + incrementStep, + email, + password, + setPassword, + firstName, + setFirstName, + lastName, + setLastName +}: UserInfoStepProps): JSX.Element { + const [firstNameError, setFirstNameError] = useState(false); + const [lastNameError, setLastNameError] = useState(false); + const [passwordErrorLength, setPasswordErrorLength] = useState(false); + const [passwordErrorNumber, setPasswordErrorNumber] = useState(false); + const [passwordErrorLowerCase, setPasswordErrorLowerCase] = useState(false); + + const [isLoading, setIsLoading] = useState(false); + const { t } = useTranslation(); + const router = useRouter(); + + // Verifies if the information that the users entered (name, workspace) + // is there, and if the password matches the criteria. + const signupErrorCheck = async () => { + setIsLoading(true); + let errorCheck = false; + if (!firstName) { + setFirstNameError(true); + errorCheck = true; + } else { + setFirstNameError(false); + } + if (!lastName) { + setLastNameError(true); + errorCheck = true; + } else { + setLastNameError(false); + } + errorCheck = passwordCheck({ + password, + setPasswordErrorLength, + setPasswordErrorNumber, + setPasswordErrorLowerCase, + currentErrorCheck: errorCheck, + }); + + if (!errorCheck) { + // Generate a random pair of a public and a private key + const pair = nacl.box.keyPair(); + const secretKeyUint8Array = pair.secretKey; + const publicKeyUint8Array = pair.publicKey; + const PRIVATE_KEY = nacl.util.encodeBase64(secretKeyUint8Array); + const PUBLIC_KEY = nacl.util.encodeBase64(publicKeyUint8Array); + + const { ciphertext, iv, tag } = Aes256Gcm.encrypt({ + text: PRIVATE_KEY, + secret: password + .slice(0, 32) + .padStart( + 32 + (password.slice(0, 32).length - new Blob([password]).size), + "0" + ), + }) as { ciphertext: string; iv: string; tag: string }; + + localStorage.setItem("PRIVATE_KEY", PRIVATE_KEY); + + client.init( + { + username: email, + password: password, + }, + async () => { + client.createVerifier( + async (err: any, result: { salt: string; verifier: string }) => { + const response = await completeAccountInformationSignup({ + email, + firstName, + lastName, + organizationName: firstName + "'s organization", + publicKey: PUBLIC_KEY, + ciphertext, + iv, + tag, + salt: result.salt, + verifier: result.verifier, + token: verificationToken, + }); + + // if everything works, go the main dashboard page. + if (response.status === 200) { + // response = await response.json(); + + localStorage.setItem("publicKey", PUBLIC_KEY); + localStorage.setItem("encryptedPrivateKey", ciphertext); + localStorage.setItem("iv", iv); + localStorage.setItem("tag", tag); + + try { + await attemptLogin( + email, + password, + (value: boolean) => {}, + router, + true, + false + ); + incrementStep(); + } catch (error) { + setIsLoading(false); + } + } + } + ); + } + ); + } else { + setIsLoading(false); + } + }; + + return ( + <div className="bg-bunker w-max mx-auto h-7/12 py-10 px-8 rounded-xl drop-shadow-xl mb-36 md:mb-16"> + <p className="text-4xl font-bold flex justify-center mb-6 text-gray-400 mx-8 md:mx-16 text-transparent bg-clip-text bg-gradient-to-br from-sky-400 to-primary"> + {t("signup:step3-message")} + </p> + <div className="relative z-0 flex items-center justify-end w-full md:p-2 rounded-lg max-h-24"> + <InputField + label={t("common:first-name")} + onChangeHandler={setFirstName} + type="name" + value={firstName} + isRequired + errorText={ + t("common:validate-required", { + name: t("common:first-name"), + }) as string + } + error={firstNameError} + autoComplete="given-name" + /> + </div> + <div className="mt-2 flex items-center justify-center w-full md:p-2 rounded-lg max-h-24"> + <InputField + label={t("common:last-name")} + onChangeHandler={setLastName} + type="name" + value={lastName} + isRequired + errorText={ + t("common:validate-required", { + name: t("common:last-name"), + }) as string + } + error={lastNameError} + autoComplete="family-name" + /> + </div> + <div className="mt-2 flex flex-col items-center justify-center w-full md:p-2 rounded-lg max-h-60"> + <InputField + label={t("section-password:password")} + onChangeHandler={(password: string) => { + setPassword(password); + passwordCheck({ + password, + setPasswordErrorLength, + setPasswordErrorNumber, + setPasswordErrorLowerCase, + currentErrorCheck: false, + }); + }} + type="password" + value={password} + isRequired + error={ + passwordErrorLength && passwordErrorNumber && passwordErrorLowerCase + } + autoComplete="new-password" + id="new-password" + /> + {passwordErrorLength || + passwordErrorLowerCase || + passwordErrorNumber ? ( + <div className="w-full mt-4 bg-white/5 px-2 flex flex-col items-start py-2 rounded-md"> + <div className={`text-gray-400 text-sm mb-1`}> + {t("section-password:validate-base")} + </div> + <div className="flex flex-row justify-start items-center ml-1"> + {passwordErrorLength ? ( + <FontAwesomeIcon + icon={faX} + className="text-md text-red mr-2.5" + /> + ) : ( + <FontAwesomeIcon + icon={faCheck} + className="text-md text-primary mr-2" + /> + )} + <div + className={`${ + passwordErrorLength ? "text-gray-400" : "text-gray-600" + } text-sm`} + > + {t("section-password:validate-length")} + </div> + </div> + <div className="flex flex-row justify-start items-center ml-1"> + {passwordErrorLowerCase ? ( + <FontAwesomeIcon + icon={faX} + className="text-md text-red mr-2.5" + /> + ) : ( + <FontAwesomeIcon + icon={faCheck} + className="text-md text-primary mr-2" + /> + )} + <div + className={`${ + passwordErrorLowerCase ? "text-gray-400" : "text-gray-600" + } text-sm`} + > + {t("section-password:validate-case")} + </div> + </div> + <div className="flex flex-row justify-start items-center ml-1"> + {passwordErrorNumber ? ( + <FontAwesomeIcon + icon={faX} + className="text-md text-red mr-2.5" + /> + ) : ( + <FontAwesomeIcon + icon={faCheck} + className="text-md text-primary mr-2" + /> + )} + <div + className={`${ + passwordErrorNumber ? "text-gray-400" : "text-gray-600" + } text-sm`} + > + {t("section-password:validate-number")} + </div> + </div> + </div> + ) : ( + <div className="py-2"></div> + )} + </div> + <div className="flex flex-col items-center justify-center md:p-2 max-h-48 max-w-max mx-auto text-lg px-2 py-3"> + <Button + text={t("signup:signup") ?? ""} + loading={isLoading} + onButtonPressed={signupErrorCheck} + size="lg" + /> + </div> + </div> + ) +} diff --git a/frontend/pages/email-not-verified.js b/frontend/pages/email-not-verified.tsx similarity index 93% rename from frontend/pages/email-not-verified.js rename to frontend/pages/email-not-verified.tsx index 11aec70bd..3c634874a 100644 --- a/frontend/pages/email-not-verified.js +++ b/frontend/pages/email-not-verified.tsx @@ -1,7 +1,7 @@ import React from 'react'; import Head from 'next/head'; -export default function Activity() { +export default function EmailNotFeriviedPage() { return ( <div className="bg-bunker-800 md:h-screen flex flex-col justify-between"> <Head> diff --git a/frontend/pages/signup.tsx b/frontend/pages/signup.tsx index 224a2b102..d7a3960bf 100644 --- a/frontend/pages/signup.tsx +++ b/frontend/pages/signup.tsx @@ -1,67 +1,24 @@ import { useEffect, useState } from "react"; -import ReactCodeInput from "react-code-input"; import Head from "next/head"; import Image from "next/image"; import Link from "next/link"; import { useRouter } from "next/router"; import { useTranslation } from "next-i18next"; -import { faCheck, faWarning, faX } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import Button from "~/components/basic/buttons/Button"; -import Error from "~/components/basic/Error"; -import InputField from "~/components/basic/InputField"; -import Aes256Gcm from "~/components/utilities/cryptography/aes-256-gcm"; -import issueBackupKey from "~/components/utilities/cryptography/issueBackupKey"; +import CodeInputStep from "~/components/signup/CodeInputStep"; +import DownloadBackupPDF from "~/components/signup/DonwloadBackupPDFStep"; +import EnterEmailStep from "~/components/signup/EnterEmailStep"; +import TeamInviteStep from "~/components/signup/TeamInviteStep"; +import UserInfoStep from "~/components/signup/UserInfoStep"; import { getTranslatedStaticProps } from "~/components/utilities/withTranslateProps"; -import attemptLogin from "~/utilities/attemptLogin"; -import passwordCheck from "~/utilities/checks/PasswordCheck"; import checkEmailVerificationCode from "./api/auth/CheckEmailVerificationCode"; -import completeAccountInformationSignup from "./api/auth/CompleteAccountInformationSignup"; -import sendVerificationEmail from "./api/auth/SendVerificationEmail"; import getWorkspaces from "./api/workspace/getWorkspaces"; -// const ReactCodeInput = dynamic(import("react-code-input")); -const nacl = require("tweetnacl"); -const jsrp = require("jsrp"); -nacl.util = require("tweetnacl-util"); -const client = new jsrp.client(); - -// The stye for the verification code input -const props = { - inputStyle: { - fontFamily: "monospace", - margin: "4px", - MozAppearance: "textfield", - width: "55px", - borderRadius: "5px", - fontSize: "24px", - height: "55px", - paddingLeft: "7", - backgroundColor: "#0d1117", - color: "white", - border: "1px solid gray", - textAlign: "center", - }, -} as const; -const propsPhone = { - inputStyle: { - fontFamily: "monospace", - margin: "4px", - MozAppearance: "textfield", - width: "40px", - borderRadius: "5px", - fontSize: "24px", - height: "40px", - paddingLeft: "7", - backgroundColor: "#0d1117", - color: "white", - border: "1px solid gray", - textAlign: "center", - }, -} as const; +/** + * @returns the signup page + */ export default function SignUp() { const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); @@ -69,25 +26,9 @@ export default function SignUp() { const [lastName, setLastName] = useState(""); const [code, setCode] = useState(""); const [codeError, setCodeError] = useState(false); - const [firstNameError, setFirstNameError] = useState(false); - const [lastNameError, setLastNameError] = useState(false); - const [passwordErrorLength, setPasswordErrorLength] = useState(false); - const [passwordErrorNumber, setPasswordErrorNumber] = useState(false); - const [passwordErrorUpperCase, setPasswordErrorUpperCase] = useState(false); - const [passwordErrorLowerCase, setPasswordErrorLowerCase] = useState(false); - const [passwordErrorSpecialChar, setPasswordErrorSpecialChar] = - useState(false); - const [emailError, setEmailError] = useState(false); - const [emailErrorMessage, setEmailErrorMessage] = useState(""); const [step, setStep] = useState(1); const router = useRouter(); - const [errorLogin, setErrorLogin] = useState(false); - const [isLoading, setIsLoading] = useState(false); - const [isResendingVerificationEmail, setIsResendingVerificationEmail] = - useState(false); - const [backupKeyError, setBackupKeyError] = useState(false); const [verificationToken, setVerificationToken] = useState(""); - const [backupKeyIssued, setBackupKeyIssued] = useState(false); const { t } = useTranslation(); @@ -104,458 +45,28 @@ export default function SignUp() { }, []); /** - * Goes to the following step (out of 3) of the signup process. + * Goes to the following step (out of 5) of the signup process. * Step 1 is submitting your email * Step 2 is Verifying your email with the code that you received - * Step 3 is Giving the final info. + * Step 3 is asking the final info. + * Step 4 is downloading a backup pdf + * Step 5 is inviting users */ const incrementStep = async () => { - if (step == 1) { - setStep(2); + if (step == 1 || step == 3 || step == 4) { + setStep(step + 1); } else if (step == 2) { // Checking if the code matches the email. const response = await checkEmailVerificationCode({ email, code }); - if (response.status === 200 || code == "111222") { + if (response.status === 200) { setVerificationToken((await response.json()).token); setStep(3); } else { setCodeError(true); } - } else if (step == 3) { - setStep(4); } }; - /** - * Verifies if the entered email "looks" correct - */ - const emailCheck = () => { - let emailCheckBool = false; - if (!email) { - setEmailError(true); - setEmailErrorMessage("Please enter your email."); - emailCheckBool = true; - } else if ( - !email.includes("@") || - !email.includes(".") || - !/[a-z]/.test(email) - ) { - setEmailError(true); - setEmailErrorMessage("Please enter a valid email."); - emailCheckBool = true; - } else { - setEmailError(false); - } - - // If everything is correct, go to the next step - if (!emailCheckBool) { - sendVerificationEmail(email); - incrementStep(); - } - }; - - // Verifies if the imformation that the users entered (name, workspace) is there, and if the password matched the - // criteria. - const signupErrorCheck = async () => { - setIsLoading(true); - let errorCheck = false; - if (!firstName) { - setFirstNameError(true); - errorCheck = true; - } else { - setFirstNameError(false); - } - if (!lastName) { - setLastNameError(true); - errorCheck = true; - } else { - setLastNameError(false); - } - errorCheck = passwordCheck({ - password, - setPasswordErrorLength, - setPasswordErrorNumber, - setPasswordErrorLowerCase, - currentErrorCheck: errorCheck, - }); - - if (!errorCheck) { - // Generate a random pair of a public and a private key - const pair = nacl.box.keyPair(); - const secretKeyUint8Array = pair.secretKey; - const publicKeyUint8Array = pair.publicKey; - const PRIVATE_KEY = nacl.util.encodeBase64(secretKeyUint8Array); - const PUBLIC_KEY = nacl.util.encodeBase64(publicKeyUint8Array); - - const { ciphertext, iv, tag } = Aes256Gcm.encrypt({ - text: PRIVATE_KEY, - secret: password - .slice(0, 32) - .padStart( - 32 + (password.slice(0, 32).length - new Blob([password]).size), - "0" - ), - }) as { ciphertext: string; iv: string; tag: string }; - - localStorage.setItem("PRIVATE_KEY", PRIVATE_KEY); - - client.init( - { - username: email, - password: password, - }, - async () => { - client.createVerifier( - async (err: any, result: { salt: string; verifier: string }) => { - const response = await completeAccountInformationSignup({ - email, - firstName, - lastName, - organizationName: firstName + "'s organization", - publicKey: PUBLIC_KEY, - ciphertext, - iv, - tag, - salt: result.salt, - verifier: result.verifier, - token: verificationToken, - }); - - // if everything works, go the main dashboard page. - if (response.status === 200) { - // response = await response.json(); - - localStorage.setItem("publicKey", PUBLIC_KEY); - localStorage.setItem("encryptedPrivateKey", ciphertext); - localStorage.setItem("iv", iv); - localStorage.setItem("tag", tag); - - try { - await attemptLogin( - email, - password, - setErrorLogin, - router, - true, - false - ); - incrementStep(); - } catch (error) { - setIsLoading(false); - } - } - } - ); - } - ); - } else { - setIsLoading(false); - } - }; - - const resendVerificationEmail = async () => { - setIsResendingVerificationEmail(true); - setIsLoading(true); - await sendVerificationEmail(email); - setTimeout(() => { - setIsLoading(false); - setIsResendingVerificationEmail(false); - }, 2000); - }; - - // Step 1 of the sign up process (enter the email or choose google authentication) - const step1 = ( - <div> - <div className="bg-bunker w-full max-w-md mx-auto h-7/12 py-8 md:px-6 mx-1 rounded-xl drop-shadow-xl"> - <p className="text-4xl font-semibold flex justify-center text-primary"> - {'Let\''}s get started - </p> - <div className="flex items-center justify-center w-5/6 md:w-full m-auto md:p-2 rounded-lg max-h-24 mt-4"> - <InputField - label="Email" - onChangeHandler={setEmail} - type="email" - value={email} - placeholder="" - isRequired - error={emailError} - errorText={emailErrorMessage} - autoComplete="username" - /> - </div> - {/* <div className='flex flex-row justify-left mt-4 max-w-md mx-auto'> - <Checkbox className="mr-4"/> - <p className='text-sm'>I do not want to receive emails about Infisical and its products.</p> - </div> */} - <div className="flex flex-col items-center justify-center w-5/6 md:w-full md:p-2 max-h-28 max-w-xs md:max-w-md mx-auto mt-4 md:mt-4 text-sm text-center md:text-left"> - <p className="text-gray-400 mt-2 md:mx-0.5"> - {t("signup:step1-privacy")} - </p> - <div className="text-l mt-6 m-2 md:m-8 px-8 py-1 text-lg"> - <Button text="Get Started" type="submit" onButtonPressed={emailCheck} size="lg" /> - </div> - </div> - </div> - <div className="flex flex-col items-center justify-center w-full md:pb-2 max-w-md mx-auto pt-2 mb-48 md:mb-16 mt-2"> - <Link href="/login"> - <button type="button" className="w-max pb-3 hover:opacity-90 duration-200"> - <u className="font-normal text-sm text-primary-500"> - {t("signup:already-have-account")} - </u> - </button> - </Link> - </div> - </div> - - ); - - // Step 2 of the signup process (enter the email verification code) - const step2 = ( - <div className="bg-bunker w-max mx-auto h-7/12 pt-10 pb-4 px-8 rounded-xl drop-shadow-xl mb-64 md:mb-16"> - <p className="text-l flex justify-center text-gray-400"> - {"We've"} sent a verification email to{" "} - </p> - <p className="text-l flex justify-center font-semibold my-2 text-gray-400"> - {email}{" "} - </p> - <div className="hidden md:block"> - <ReactCodeInput - name="" - inputMode="tel" - type="text" - fields={6} - onChange={setCode} - {...props} - className="mt-6 mb-2" - /> - </div> - <div className="block md:hidden"> - <ReactCodeInput - name="" - inputMode="tel" - type="text" - fields={6} - onChange={setCode} - {...propsPhone} - className="mt-2 mb-6" - /> - </div> - {codeError && <Error text={t("signup:step2-code-error")} />} - <div className="flex max-w-max min-w-28 flex-col items-center justify-center md:p-2 max-h-24 mx-auto text-lg px-4 mt-4 mb-2"> - <Button - text={t("signup:verify") ?? ""} - onButtonPressed={incrementStep} - size="lg" - /> - </div> - <div className="flex flex-col items-center justify-center w-full max-h-24 max-w-md mx-auto pt-2"> - <div className="flex flex-row items-baseline gap-1 text-sm"> - <span className="text-gray-400"> - Not seeing an email? - </span> - <u className={`font-normal ${isResendingVerificationEmail ? 'text-gray-400' : 'text-primary-500 hover:opacity-90 duration-200'}`}> - <button disabled={isLoading} onClick={resendVerificationEmail}> - {isResendingVerificationEmail ? "Resending..." : "Resend"} - </button> - </u> - </div> - <p className="text-sm text-gray-500 pb-2"> - {t("signup:step2-spam-alert")} - </p> - </div> - </div> - ); - - // Step 3 of the signup process (enter the rest of the impformation) - const step3 = ( - <div className="bg-bunker w-max mx-auto h-7/12 py-10 px-8 rounded-xl drop-shadow-xl mb-36 md:mb-16"> - <p className="text-4xl font-bold flex justify-center mb-6 text-gray-400 mx-8 md:mx-16 text-transparent bg-clip-text bg-gradient-to-br from-sky-400 to-primary"> - {t("signup:step3-message")} - </p> - <div className="relative z-0 flex items-center justify-end w-full md:p-2 rounded-lg max-h-24"> - <InputField - label={t("common:first-name")} - onChangeHandler={setFirstName} - type="name" - value={firstName} - isRequired - errorText={ - t("common:validate-required", { - name: t("common:first-name"), - }) as string - } - error={firstNameError} - autoComplete="given-name" - /> - </div> - <div className="mt-2 flex items-center justify-center w-full md:p-2 rounded-lg max-h-24"> - <InputField - label={t("common:last-name")} - onChangeHandler={setLastName} - type="name" - value={lastName} - isRequired - errorText={ - t("common:validate-required", { - name: t("common:last-name"), - }) as string - } - error={lastNameError} - autoComplete="family-name" - /> - </div> - <div className="mt-2 flex flex-col items-center justify-center w-full md:p-2 rounded-lg max-h-60"> - <InputField - label={t("section-password:password")} - onChangeHandler={(password: string) => { - setPassword(password); - passwordCheck({ - password, - setPasswordErrorLength, - setPasswordErrorNumber, - setPasswordErrorLowerCase, - currentErrorCheck: false, - }); - }} - type="password" - value={password} - isRequired - error={ - passwordErrorLength && passwordErrorNumber && passwordErrorLowerCase - } - autoComplete="new-password" - id="new-password" - /> - {passwordErrorLength || - passwordErrorLowerCase || - passwordErrorNumber ? ( - <div className="w-full mt-4 bg-white/5 px-2 flex flex-col items-start py-2 rounded-md"> - <div className={`text-gray-400 text-sm mb-1`}> - {t("section-password:validate-base")} - </div> - <div className="flex flex-row justify-start items-center ml-1"> - {passwordErrorLength ? ( - <FontAwesomeIcon - icon={faX} - className="text-md text-red mr-2.5" - /> - ) : ( - <FontAwesomeIcon - icon={faCheck} - className="text-md text-primary mr-2" - /> - )} - <div - className={`${ - passwordErrorLength ? "text-gray-400" : "text-gray-600" - } text-sm`} - > - {t("section-password:validate-length")} - </div> - </div> - <div className="flex flex-row justify-start items-center ml-1"> - {passwordErrorLowerCase ? ( - <FontAwesomeIcon - icon={faX} - className="text-md text-red mr-2.5" - /> - ) : ( - <FontAwesomeIcon - icon={faCheck} - className="text-md text-primary mr-2" - /> - )} - <div - className={`${ - passwordErrorLowerCase ? "text-gray-400" : "text-gray-600" - } text-sm`} - > - {t("section-password:validate-case")} - </div> - </div> - <div className="flex flex-row justify-start items-center ml-1"> - {passwordErrorNumber ? ( - <FontAwesomeIcon - icon={faX} - className="text-md text-red mr-2.5" - /> - ) : ( - <FontAwesomeIcon - icon={faCheck} - className="text-md text-primary mr-2" - /> - )} - <div - className={`${ - passwordErrorNumber ? "text-gray-400" : "text-gray-600" - } text-sm`} - > - {t("section-password:validate-number")} - </div> - </div> - </div> - ) : ( - <div className="py-2"></div> - )} - </div> - <div className="flex flex-col items-center justify-center md:p-2 max-h-48 max-w-max mx-auto text-lg px-2 py-3"> - <Button - text={t("signup:signup") ?? ""} - loading={isLoading} - onButtonPressed={signupErrorCheck} - size="lg" - /> - </div> - </div> - ); - - // Step 4 of the sign up process (download the emergency kit pdf) - const step4 = ( - <div className="bg-bunker flex flex-col items-center w-full max-w-xs md:max-w-lg mx-auto h-7/12 py-8 px-4 md:px-6 mx-1 mb-36 md:mb-16 rounded-xl drop-shadow-xl"> - <p className="text-4xl text-center font-semibold flex justify-center text-transparent bg-clip-text bg-gradient-to-br from-sky-400 to-primary"> - {t("signup:step4-message")} - </p> - <div className="flex flex-col items-center justify-center w-full mt-4 md:mt-8 max-w-md text-gray-400 text-md rounded-md px-2"> - <div>{t("signup:step4-description1")}</div> - <div className="mt-3">{t("signup:step4-description2")}</div> - </div> - <div className="w-full p-2 flex flex-row items-center bg-white/10 text-gray-400 rounded-md max-w-xs md:max-w-md mx-auto mt-4"> - <FontAwesomeIcon icon={faWarning} className="ml-2 mr-4 text-4xl" /> - {t("signup:step4-description3")} - </div> - <div className="flex flex-col items-center justify-center md:px-4 md:py-5 mt-2 px-2 py-3 max-h-24 max-w-max mx-auto text-lg"> - <Button - text="Download PDF" - onButtonPressed={async () => { - await issueBackupKey({ - email, - password, - personalName: firstName + " " + lastName, - setBackupKeyError, - setBackupKeyIssued, - }); - const userWorkspaces = await getWorkspaces(); - const userWorkspace = userWorkspaces[0]._id; - router.push("/home/" + userWorkspace); - }} - size="lg" - /> - {/* <div - className="text-l mt-4 text-lg text-gray-400 hover:text-gray-300 duration-200 bg-white/5 px-8 hover:bg-white/10 py-3 rounded-md cursor-pointer" - onClick={() => { - if (localStorage.getItem("projectData.id")) { - router.push("/dashboard/" + localStorage.getItem("projectData.id")); - } else { - router.push("/noprojects") - } - }} - > - Later - </div> */} - </div> - </div> - ); - return ( <div className="bg-bunker-800 h-screen flex flex-col items-center justify-center"> <Head> @@ -580,7 +91,11 @@ export default function SignUp() { </div> </Link> <form onSubmit={(e) => e.preventDefault()}> - {step == 1 ? step1 : step == 2 ? step2 : step == 3 ? step3 : step4} + {step == 1 ? <EnterEmailStep email={email} setEmail={setEmail} incrementStep={incrementStep} /> + : step == 2 ? <CodeInputStep email={email} incrementStep={incrementStep} setCode={setCode} codeError={codeError}/> + : step == 3 ? <UserInfoStep verificationToken={verificationToken} incrementStep={incrementStep} email={email} password={password} setPassword={setPassword} firstName={firstName} setFirstName={setFirstName} lastName={lastName} setLastName={setLastName}/> + : step == 4 ? <DownloadBackupPDF incrementStep={incrementStep} email={email} password={password} name={firstName + " " + lastName} /> + : <TeamInviteStep/>} </form> </div> </div> diff --git a/frontend/pages/signupinvite.js b/frontend/pages/signupinvite.js index d32d6038d..58a15e40d 100644 --- a/frontend/pages/signupinvite.js +++ b/frontend/pages/signupinvite.js @@ -349,7 +349,7 @@ export default function SignupInvite() { setBackupKeyError, setBackupKeyIssued }); - router.push('/dashboard/'); + router.push('/noprojects/'); }} size="lg" /> diff --git a/frontend/public/locales/en/common.json b/frontend/public/locales/en/common.json index c0b1e5640..34cdcaea8 100644 --- a/frontend/public/locales/en/common.json +++ b/frontend/public/locales/en/common.json @@ -13,8 +13,8 @@ "project-id": "Project ID", "save-changes": "Save Changes", "saved": "Saved", - "drop-zone": "Drag and drop your .env file here.", - "drop-zone-keys": "Drag and drop your .env file here to add more keys.", + "drop-zone": "Drag and drop a .env or .yml file here.", + "drop-zone-keys": "Drag and drop a .env or .yml file here to add more keys.", "role": "Role", "role_admin": "admin", "display-name": "Display Name", diff --git a/frontend/public/locales/en/dashboard.json b/frontend/public/locales/en/dashboard.json index 99e6d9faa..263887360 100644 --- a/frontend/public/locales/en/dashboard.json +++ b/frontend/public/locales/en/dashboard.json @@ -10,6 +10,7 @@ "shared-description": "Shared keys are visible to your whole team", "make-shared": "Make Shared", "make-personal": "Make Personal", + "add-secret": "Add a new secret", "check-docs": { "button": "Check Docs", "title": "Good job!", diff --git a/frontend/public/locales/en/signup.json b/frontend/public/locales/en/signup.json index 509520d56..0fbc92951 100644 --- a/frontend/public/locales/en/signup.json +++ b/frontend/public/locales/en/signup.json @@ -17,5 +17,9 @@ "step4-description1": "If you get locked out of your account, your Emergency Kit is the only way to sign in.", "step4-description2": "We recommend you download it and keep it somewhere safe.", "step4-description3": "It contains your Secret Key which we cannot access or recover for you if you lose it.", - "step4-download": "Download PDF" + "step4-download": "Download PDF", + "step5-send-invites": "Send Invites", + "step5-invite-team": "Invite your team", + "step5-subtitle": "Infisical is meant to be used with your teammates. Invite them to test it out.", + "step5-skip": "Skip" }