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
- First create a constant to store 6 random digits, need to be a string.
const randomCode = Math.floor(100000 + Math.random() * 900000).toString()
- Encrypt the 6 digits and then store into the database with all the information needed.
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 example
const schema = new Schema({
emailId: {
type: String,
},
token: {
type: String,
required: true,
},
createdAt: {
type: Date,
expires: 3600,
default: Date.now,
},
})
-
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
- For the email sent procces , AWS is our option handled a email html template is needed, see basic template here
- 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 })
- At the corresponding service let's start loading the template.
async function getTemplate(templateUrl) { return fs.readFileSync(templateUrl, 'utf8') }
- 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 }
- 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 }
- 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() }
Check the email respone to handle it.
if (!sendEmailResponse || !sendEmailResponse.MessageId) {
throw Boom.conflict('Could not send email')
}
Client side with React
-
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 } })
NOTE: Read reactrouter documentation https://v5.reactrouter.com/web/api/history
-
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])
-
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/installationJSX 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't receive the code? <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> )
-
Create the reference for UseToast , this chakraUI feature let us handle the errors easily.
const toast = useToast()
-
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)
- 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])
- 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.
Finally, be sure to clean the location value once the component did unmount.
useEffect(() => {
return () => {
reset()
location.state = null
}
}, [location, reset])
Top comments (0)