인증 코드 기반 가입을 구축하는 방법

처음에 계정을 만드는 동안 시스템은 사용자가 이메일 주소가 존재하는지 또는 사용자가 이 메일 계정을 소유하고 있는지 확인하지 않고 이메일 주소를 추가하도록 허용할 수 있습니다.

해결책



사용자의 이메일로 전송된 임의의 4자리 숫자를 포함하는 확인 코드를 생성합니다. 앱은 확인 페이지에 코드를 입력하도록 요청하고 일단 승인되면 계정이 생성됩니다.

레시피



노드가 있는 서버 측


  • 먼저 문자열이어야 하는 6개의 임의 숫자를 저장할 상수를 만듭니다.

  • const randomCode = Math.floor(100000 + Math.random() * 900000).toString()
    


  • 6자리를 암호화한 다음 필요한 모든 정보와 함께 데이터베이스에 저장합니다.

  • const hash = await bcrypt.hash(randomCode, Number(10))
    


    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()
    
    


    DB 예시

      const schema = new Schema({
        emailId: {
          type: String,
        },
        token: {
          type: String,
          required: true,
        },
        createdAt: {
          type: Date,
          expires: 3600,
          default: Date.now,
        },
      })
    



  • 이메일을 보내십시오.

    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)
    

    메일편에서


  • 이메일 전송 프로세스의 경우 AWS는 이메일 html 템플릿을 처리하는 옵션입니다. basic template here을 참조하십시오.
  • AWS 액세스 및 SES 기능을 구성합니다.

  • 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 })
    

  • 해당 서비스에서 템플릿 로드를 시작하겠습니다.

  • async function getTemplate(templateUrl) {
      return fs.readFileSync(templateUrl, 'utf8')
    }
    

  • 템플릿으로 본문을 빌드하는 기능을 추가합니다.

  • 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
    }
    

  • 템플릿 빌드를 실행하는 함수를 추가합니다.

  • 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
    }
    

  • 모든 기능을 혼합하고 가입 프로세스에 필요한 보내기 기능을 만듭니다.
    > 참고: Amazon SES는 값이 정의되지 않은 것을 좋아하지 않으므로 값이 정의되지 않은 경우 필드를 전혀 보내지 않거나 최소한 빈 문자열을 보냅니다.

  • 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()
    }
    


  • 이메일 응답을 확인하여 처리하십시오.

  •   if (!sendEmailResponse || !sendEmailResponse.MessageId) {
        throw Boom.conflict('Could not send email')
      }
    


  • Email Preview

  • React를 사용하는 클라이언트 측



  • 계정을 만드는 데 필요한 정보가 포함된 양식이 포함된 가입 페이지를 만들고 위치 및 기록 기능을 사용하여 정보를 보냅니다.

      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



  • verifyCode 반응 구성 요소를 만들고 위치에서 정보를 가져옵니다.

    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({})
    

    useEffect 아래는 위치에서 정보가 있는 경우 로드하고, 정보가 없는 경우 페이지가 리디렉션됩니다.

    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])
    


  • 확인 코드를 채우는 데 필요한 양식을 만듭니다.

    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 구성 요소, PinInput을 사용하여 코드 값을 검색합니다.

    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

  • UseToast 에 대한 참조를 생성하면 이 chakraUI 기능을 통해 오류를 쉽게 처리할 수 있습니다.

      const toast = useToast()
    


  • 서버, onRequestCode(코드를 요청하고 사용자의 이메일로 전송됨) 및 onSubmit(코드가 일치하는 경우 새 계정이 생성됨)에서 정보를 검색하는 나머지 기능을 만듭니다.
  • 요청 코드

  • 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 [email protected]',
          })
        } finally {
          reset()
        }
      },
    [email, name, toast, reset]
    )
    

    이 기능은 "requestVerificationCode"라는 서비스를 의미하며 서버에 코드를 요청하고 참조된 이메일 주소로 전송되는 것을 의미합니다.

    이는 "forceSend"라는 값을 호출합니다. 이를 통해 서버는 기본적으로 5분마다 코드를 보낼 수만 있기 때문에 페이지가 "true"로 설정된 작업을 통해 코드를 요청할 수 있습니다.

    오류 처리에 주의하십시오. 서버의 응답과 일치해야 합니다.

    이 함수는 로드당 한 번 useEffect에 의해 호출되므로 useCallback을 사용하여 함수를 콜백으로 설정하는 것이 좋습니다.

      useEffect(() => {
      onRequestCode(false)
      }, [onRequestCode])
    

  • onSubmit 및 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',
            })
          }
        }
      }
    

    이 "onSubmit"기능은 코드가 서버의 코드와 일치하는지 확인하는 서비스를 사용합니다. 일치하는 경우 이제 아래 "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 [email protected]',
            })
            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 [email protected]',
          })
        } finally {
          reset()
        }
      }
    

    이 "onSignUp"기능은 존재하지 않는 경우 새 계정을 생성합니다.

  • 마지막으로 구성 요소가 마운트 해제되면 위치 값을 정리해야 합니다.

  •     useEffect(() => {
          return () => {
            reset()
            location.state = null
          }
        }, [location, reset])
    

    좋은 웹페이지 즐겨찾기