DEV Community

loading...
Cover image for Serverless Login with OpenJS Architect, Part 3

Serverless Login with OpenJS Architect, Part 3

Paul Chin Jr.
Curious Human. #Serverless #JavaScript Dev @ http://arc.codes; Dev Rel at http://begin.com; Prophet of Nicolas Cage; #PraiseCage
ใƒป4 min read

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.

Alt Text

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
Enter fullscreen mode Exit fullscreen mode
// 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)
    }
  }
}

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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>'
      })
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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.

Discussion (0)