DEV Community

Cover image for How to Build a Verification Code Based Sign Up
ICCHA Technologies
ICCHA Technologies

Posted on • Updated on

How to Build a Verification Code Based Sign Up

While creating an account at first a system may allow a user to add any email address without verifying if it even exists or if the user owns this mail account.

Solution

Create verification code containing 4 random digits sent to the email of the user, the app will request now to type the code in the verification page, once it is approved the account is created.

Recipe

Server side with node

  1. First create a constant to store 6 random digits, need to be a string.
const randomCode = Math.floor(100000 + Math.random() * 900000).toString()
Enter fullscreen mode Exit fullscreen mode
  1. Encrypt the 6 digits and then store into the database with all the information needed.
const hash = await bcrypt.hash(randomCode, Number(10))
Enter fullscreen mode Exit fullscreen mode

Check bcrypt library at https://www.npmjs.com/package/bcrypt used for the encryption.

  await new Token({
    emailId: email,
    token: hash,
    createdAt: Date.now(),
  }).save()

Enter fullscreen mode Exit fullscreen mode

DB example

  const schema = new Schema({
    emailId: {
      type: String,
    },
    token: {
      type: String,
      required: true,
    },
    createdAt: {
      type: Date,
      expires: 3600,
      default: Date.now,
    },
  })
Enter fullscreen mode Exit fullscreen mode
  1. Send the email.

    const emailOptions = {
        subject: 'CoNectar: Verify Your Account',
        data: {
          verification_code: randomCode,
          name: name,
        },
        toAddresses: [email],
        fromAddress: process.env.AWS_SES_SENDER || '',
        templateUrl: path.join(__dirname, '..', 'templates', 'verification-code.html'),
      }
    
      const sendEmailResponse = await emailService.send(emailOptions)
    

    At the email service

    1. For the email sent procces , AWS is our option handled a email html template is needed, see basic template here
    2. Configure your AWS access and SES functionality.
    let AWS = require('aws-sdk')
    
    AWS.config.update({
      secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
      accessKeyId: process.env.AWS_ACCESS_KEY_ID,
      region: process.env.AWS_REGION,
    })
    
    const SES = new AWS.SES({ region: process.env.AWS_REGION })
    
    1. At the corresponding service let's start loading the template.
    async function getTemplate(templateUrl) {
      return fs.readFileSync(templateUrl, 'utf8')
    }
    
    1. Add a function that builds the body with the template.
    function buildList(listName, list, template) {
      let newTemplate = template
    
      const startTag = `{{${listName}}}`
      const valueTag = `{{${listName}-value}}`
      const endTag = `{{${listName}-end}}`
    
      const startTagPos = newTemplate.indexOf(startTag)
      if (startTagPos === -1) return template
    
      const contentEndPos = newTemplate.indexOf(endTag)
      if (contentEndPos === -1) return template
    
      const contentStartPos = startTagPos + startTag.length
      const endTagPos = contentEndPos + endTag.length
    
      const content = newTemplate.slice(contentStartPos, contentEndPos)
    
      let expandedContent = ''
      list.map((value) => (expandedContent += content.replace(valueTag, value)))
    
      newTemplate = newTemplate.slice(0, startTagPos) + expandedContent + newTemplate.slice(endTagPos)
      return newTemplate
    }
    
    1. Add a function that runs the build of the template.
    function transformContent(content, data) {
      if (!content) return ''
    
      for (let key in data) {
        if (data.hasOwnProperty(key)) {
          if (Array.isArray(data[key])) {
            content = buildList(key, data[key], content)
            continue
          }
          const replacer = `[[${key}]]`
          const value = `${data[key]}`
          content = content ? content.replace(replacer, value) : ''
        }
      }
    
      return content
    }
    
    1. Mix all the functions and create the send function needed to the sign up process. > NOTE: Amazon SES does not like undefined as value so, do not send the field at all in case the value is undefined or, at least send empty string.
    async function send(options) {
      let template, htmlBody
    
      if (!options.textOnly) {
        template = options.template || (await getTemplate(options.templateUrl))
        htmlBody = options.data ? transformContent(template, options.data) : template
      }
    
      const plaintext = options.data
        ? transformContent(options.plaintext, options.data)
        : options.plaintext || ''
      let params = {
        Destination: {
          ToAddresses: options.toAddresses,
        },
        Message: {
          Body: {
            ...(options.textOnly
              ? {
                  Text: {
                    Charset: 'UTF-8',
                    Data: plaintext,
                  },
                }
              : {
                  Html: {
                    Charset: 'UTF-8',
                    Data: htmlBody,
                  },
                }),
          },
          Subject: {
            Charset: 'UTF-8',
            Data: options.subject,
          },
        },
        Source: options.fromAddress || process.env.CDP_SENDER_EMAIL,
      }
    
      return SES.sendEmail(params).promise()
    }
    
  2. Check the email respone to handle it.

  if (!sendEmailResponse || !sendEmailResponse.MessageId) {
    throw Boom.conflict('Could not send email')
  }
Enter fullscreen mode Exit fullscreen mode
  1. Email Preview

Client side with React

  1. Create a sign up page containing a form with the information needed to create an account, send the information using location and history features.

      let userPayload = {
      name: userLogin.name.value,
      username: userLogin.username.value,
      email: userLogin.email.value,
      password: userLogin.password.value,
      photo: profileImage && profileImage instanceof File ? profileImage : null,
    }
    history.push({ pathname: '/verify-code', state: { ...userPayload } })
    

    Sign up example Preview

    NOTE: Read reactrouter documentation https://v5.reactrouter.com/web/api/history

  2. Create the verifyCode react component and get the information from location.

    const history = useHistory()
    const location = useLocation()
    
    const [verificationCode, setVerificationCode] = useState('') // Needed to store the code
    const [email, setEmail] = useState('')
    const [name, setName] = useState('')
    const [payload, setIsPayload] = useState({})
    

    Below useEffect will load the information from location if exists, in case that there is no information , the page will be redirected.

    useEffect(() => {
      if (
        !location.state ||
        !location.state.email ||
        !location.state.name ||
        !location.state.username ||
        !location.state.password
      ) {
        history.push('/')
      } else {
        setEmail(location.state.email)
        setName(location.state.name)
        setIsPayload(location.state)
      }
    }, [location, history])
    
  3. Create the form needed to fill the verification code.

    Note: we use react-hook-form to handle the verification form, see https://react-hook-form.com/ for reference.

      const {
      handleSubmit,
      reset,
      formState: { isSubmitting },
      } = useForm()
    

    Note: We are using some features from ChakraUI, see the documentation below:
    https://chakra-ui.com/guides/first-steps
    Imported: FormControl, Center, useToast, PinInput, PinInputField.

    Note: We are using some features from TailwindCSS, see the documentation below:
    https://tailwindcss.com/docs/installation

    JSX component for the form usages, we use PinInput to retrieve the code value.

    return (
      <div className="flex flex-1 justify-center items-center h-full w-full">
        <div className="flex flex-col w-full max-w-md px-4 py-8 bg-white rounded-lg shadow-2xl dark:bg-gray-800 sm:px-6 md:px-8 lg:px-10">
          <div className="self-center mb-2 text-4xl font-medium text-gray-600 sm:text-3xl dark:text-white">
            Verification code
          </div>
          <div className="self-center mb-4 text-sm font-medium text-gray-400 dark:text-white">
            Please check your email for the verification code.
          </div>
          <div className="my-4">
            <form onSubmit={handleSubmit(onSubmit)} action="#" autoComplete="off">
              <FormControl>
                <div className="flex flex-col mb-6">
                  <div className="flex-auto mb-2">
                    <Center>
                      <PinInput
                        value={verificationCode}
                        onChange={handleChange}
                        className="flex-auto"
                      >
                        <PinInputField className="text-gray-600" />
                        <PinInputField className="text-gray-600" />
                        <PinInputField className="text-gray-600" />
                        <PinInputField className="text-gray-600" />
                        <PinInputField className="text-gray-600" />
                        <PinInputField className="text-gray-600" />
                      </PinInput>
                    </Center>
                  </div>
                </div>
              </FormControl>
              <div className={'flex w-full'}>
                <Button
                  disabled={verificationCode.length < 6}
                  text="Verify"
                  isLoading={isSubmitting}
                  type="submit"
                />
              </div>
            </form>
          </div>
          <div className="my-4">
            <div className="flex w-full">
              <p className="text-sm font-medium text-gray-600">
                Didn&#x27;t receive the code?&nbsp;
                <button
                  onClick={() => onRequestCode(true)}
                  className="text-purple-600 hover:text-purple-800 focus:text-gray-600"
                >
                  click here to request a new code
                </button>
              </p>
            </div>
          </div>
        </div>
      </div>
    )
    

    Verification code example Preview

  4. Create the reference for UseToast , this chakraUI feature let us handle the errors easily.

      const toast = useToast()
    
  5. Create the remaining functions to retrieve the infomration from the server, onRequestCode (it will request the code and it will be send to the user's email ) and onSubmit (if the codes matches the new acocunt will be created)

    1. OnRequestCode
    const onRequestCode = useCallback(
      async (forceSend = false) => {
        try {
          if (email && name) {
            const response = await requestVerificationCode({
              email: email,
              name: name,
              forceSend: forceSend,
            })
            if (response.statusText === 'Created') {
              toast({
                duration: 5000,
                status: 'success',
                position: 'top-right',
                variant: 'left-accent',
                title: 'SUCCESS: Check your email for the verification code',
                description: 'Please check your inbox messages.',
              })
            } else if (response.status === 400) {
              toast({
                duration: 5000,
                status: 'error',
                position: 'top-right',
                variant: 'left-accent',
                title: 'WARNING: Verification code already sent',
                description: 'Please check your email or try again later.',
              })
            }
          }
        } catch (error) {
          toast({
            duration: 5000,
            status: 'error',
            position: 'top-right',
            variant: 'left-accent',
            title: 'ERROR: Oops! Something Went Wrong',
            description: 'Please contact support at support@conectar.ch',
          })
        } finally {
          reset()
        }
      },
    [email, name, toast, reset]
    )
    

    This function refers to a service called "requestVerificationCode", means to request the code to the server and it to be sent to the referenced email address.

    It has a value call "forceSend", this allow the page to request a code thorugh an action once set to "true" because the server only allows to send a code every 5 minutes by default.

    Becareful with the error handling, it need to match the server's response.

    This function is call by an useEffect once per load, that's why its recommended to set the function as a callback using useCallback.

      useEffect(() => {
      onRequestCode(false)
      }, [onRequestCode])
    
    1. onSubmit and OnSignup
       const onSubmit = async (data) => {
        try {
          const response = await tokenVerificationCode({
            email,
            verificationCode,
          })
          if (response.data?.checkCode) {
            toast({
              duration: 5000,
              status: 'success',
              position: 'top-right',
              variant: 'left-accent',
              title: 'SUCCESS: your verification code has been verified',
            })
            onSignUp()
          }
        } catch (error) {
          reset()
          if (error.response.data.statusCode === 400) {
            toast({
              duration: 5000,
              status: 'error',
              position: 'top-right',
              variant: 'left-accent',
              title: 'ERROR: Invalid or expired verification code',
            })
          }
        }
      }
    

    This "onSubmit" function will use a service that check if the code matches with the one from the server, if it is maching it will now be forwarded to the below function "onSignUp"

      const onSignUp = async () => {
        try {
          const response = await signup(payload)
          if (response.ok) {
            history.push({ pathname: '/login' })
            toast({
              duration: 5000,
              status: 'success',
              position: 'top-right',
              variant: 'left-accent',
              title: 'SUCCESS: Your account has been created',
              description: 'Please login.',
            })
          } else {
            toast({
              duration: 5000,
              status: 'error',
              position: 'top-right',
              variant: 'left-accent',
              title: 'ERROR: Email is already in use!',
              description: 'Please contact support at support@conectar.ch',
            })
            history.push({ pathname: '/login' })
          }
        } catch (error) {
          toast({
            duration: 5000,
            status: 'error',
            position: 'top-right',
            variant: 'left-accent',
            title: 'ERROR: Oops! Something Went Wrong',
            description: error.message + ', Please contact support at support@conectar.ch',
          })
        } finally {
          reset()
        }
      }
    

    This "onSignUp" function will create the new account if does'nt exist.

  6. Finally, be sure to clean the location value once the component did unmount.

    useEffect(() => {
      return () => {
        reset()
        location.state = null
      }
    }, [location, reset])
Enter fullscreen mode Exit fullscreen mode

Discussion (0)