In the previous posts we've created a registration and login form. Now, we're going to integrate the SendGrid API to dispatch an email with a unique token. We can then check the token and flag the account as verified. This step will require you to set up a SendGrid account. The free trial has some limits, but we should be well inside the bounds of 100 emails a day for our demo.
Create a registered
event
An event function will be able to safely send data messages asynchronously to other Lambdas using AWS SNS, a pub/sub system. This is nice for a registration flow because it can execute in the background without making the user wait for that operation to complete. The email will be sent while the user is doing other things on the site.
Let's create a new event function, registered
, in our app.arc file. Our app.arc
file is getting bigger, but is still very readable and you know all of the things that your app is responsible for.
@app
login-flow
@events
registered
@http
get /
get /register
post /register
get /admin
get /logout
get /login
post /login
get /verify/:token
@tables
data
scopeID *String
dataID **String
ttl TTL
// src/events/registered/index.js
let data = require('@begin/data')
let arc = require('@architect/functions')
let mail = require('@sendgrid/mail')
exports.handler = arc.events.subscribe(registered)
async function registered(event) {
let email = event.key
// make a call to SendGrid API
mail.setApiKey(process.env.SENDGRID_API_KEY)
try {
let fiveMinutes = 300000
let ttl = (Date.now() + fiveMinutes) / 1000
let token = await data.set({ table: 'tokens', email, ttl })
let result = await mail.send({
to: email,
from: 'paul@begin.com',
subject: 'Welcome to the service',
text: `verify your email ${process.env.BASE_URL}/verify/${token.key}`,
});
console.log(result, 'made it here')
} catch (error) {
console.error(error);
if (error.response) {
console.error(error.response.body)
}
}
}
For the API key we will place it in the .arc-env file and be sure not to commit this file. The .arc-env
file is located at the root of the project and will load environment variables into the Lambda function during testing and can be added to the Begin console for staging and production. Documentation on using SendGrid can be found here, but all we need for this example is an API Key that you get from your SendGrid console.
# arc.env
@testing
SENDGRID_API_KEY XXXXXXXXXXXXXXXX
BASE_URL http://localhost:3333
We will use @architect/functions
to perform the service discovery of the SNS topic at runtime. Let's go ahead and install the dependencies we will need into the function folder.
cd src/events/registered
npm init -y
npm i @architect/functions @begin/data @sendgrid/mail
The function registered
is subscribed to the SNS topic also called registered
and will receive the event payload published from post-register
In this event function, we create a token and save it with a TTL of five minutes. The token will be destroyed by DynamoDB and the link will no longer be valid.
We're also using SendGrid to handle the email delivery of a verification email that will contain a link to the verify page with the token.
We also have to go back to post-register
and subscribe to our event function.
// src/http/post-register/index.js
let arc = require('@architect/functions')
let data = require('@begin/data')
let bcrypt = require('bcryptjs')
exports.handler = arc.http.async(valid, register)
// check to see if account exists
async function valid(req) {
let result = await data.get({
table: 'accounts',
key: req.body.email
})
if (result) {
return {
location: `/?error=exists`
}
}
}
async function register(req) {
// salt the password and generate a hash
let salt = bcrypt.genSaltSync(10)
let hash = bcrypt.hashSync(req.body.password, salt)
//save hash and email account to db
let result = await data.set({
table: 'accounts',
key: req.body.email,
password: hash
})
// publish a 'registered' event to dispatch email
await arc.events.publish({ name: 'registered', payload: result })
return {
session: {
account: {
email: req.body.email
}
},
location: '/admin'
}
}
create get-verify-000token
The link in the email will go to a dynamic URL constructed with the base URL of your deployed app and localhost:3333 during local testing. Let's go ahead and build this view and the backend logic to verify the token. The route is built with a path parameter where the email will supply the user with the full URL with the token in the path. Also be sure to install @architect/functions
to the function folder with a package.json
file.
// src/http/get-verify-000token
let arc = require('@architect/functions')
let data = require('@begin/data')
let layout = require('@architect/views/layout')
exports.handler = arc.http.async(verify)
async function verify(req) {
// read token in request parameter
let token = req.params.token
// read token out of the database
let result = await data.get({
table: 'tokens',
key:token
})
// verify token from the database against the path param
if(result.key === token) {
let account = await data.get({
table:'accounts',
key: result.email,
})
// write `verified:true` into the database
let verified = await data.set({
table: 'accounts',
key: account.key,
password: account.password,
verified: true
})
console.log(verified)
return {
html: layout({
account: req.session.account,
body: '<p>verified email</p>'
})
}
} else {
return {
html: layout({
account: req.session.account,
body: '<p>verifying email ... token expired</p>'
})
}
}
}
Stay Tuned
In the next part of our series, we will create an account deletion feature and a password reset for our users using the same token generation pattern that verifies the account.
Top comments (0)