Serverless Login with OpenJS Architect, Part 3
Published: 2020-11-02 16:28:33

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](https://dev-to-uploads.s3.amazonaws.com/i/r3u196m3fpkhhy55c4lu.png)

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
```

```javascript
// 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](https://sendgrid.com/docs/for-developers/sending-email/api-getting-started/), 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. 

```bash
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. 

```javascript
// 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.

```javascript
// 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.