Serverless Login with OpenJS Architect, Part 4
Tags: aws, javascript, node, serverless
Published: 2020-11-05 19:21:03

Welcome to the final section where we will implement a password reset feature. It's going to be the same pattern as the email verification, where we will send the user an email with an expiring token URL. That token URL will confirm the user's email and give them a form to submit a new password. 

## Adding new routes 
Let's start by taking a look at the new state of our `app.arc` file.
```bash
@app
begin-app

@events
registered

@http
get /
get /register
post /register
get /admin
get /logout
get /login
post /login
get /verify/:token
get /reset
get /reset/:token
post /register/nuke
post /reset-password

@tables
data
  scopeID *String
  dataID **String
  ttl TTL
```
## Create `get-reset`
`get-reset` is a function that gives the user a form to submit their email to receive a reset URL. 

```javascript
// src/http/get-reset/index.js
let arc = require('@architect/functions')
let layout = require('@architect/views/layout')

exports.handler = arc.http.async(reset)

let form = `
  <form action=/reset method=post>
  <h2>Reset your password</h2>
  <p> You will receive an email with a link to reset </p>
  <input name=email type=email placeholder="add your email" required>
  <button>Reset password</button>
`
async function reset(req) {

  return {
    html: layout({
      account: req.session.account,
      body: form
    })
  }
}
```
## Create `post-reset` function 
`post-reset` is the function handler that will catch the form data from `get-reset` and dispatch the email to the user. This will look a lot like the code we used for `post-register`

```javascript
// src/http/post-reset/index.js

let arc = require('@architect/functions')
let data = require('@begin/data')
let mail = require('@sendgrid/mail')

exports.handler = arc.http.async(reset)

async function reset (req) {
  let email = req.body.email
  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: 'Reset your password',
      text: `Reset your password by clicking this link ${process.env.BASE_URL}/reset/${token.key}`,
    });
    console.log(result, 'made it here')
  } catch (error) {
    console.error(error);

    if (error.response) {
      console.error(error.response.body)
    }
  }
  return {
    location: `/`
  }
}
```


## Create `get-reset-000token` function 

This function will look similar to `get-verify-000token` because it has the same flow. Let's take a look at the function below: 

```javascript
// src/http/get-reset-000token/index.js

let arc = require('@architect/functions')
let data = require('@begin/data')
let layout = require('@architect/views/layout')

exports.handler = arc.http.async(reset)

async function reset(req) {
  //read the token from request params and database
  let token = req.params.token
  let result = await data.get({
    table: 'tokens',
    key: token
  })
  // match token from params against the database 
  if (result.key === token) {
    return {
      html: layout({
        account: req.session.account,
        body: `<h2>Choose a new password<h2>
        <form action=/reset-password method=post>
        <input name=password type=password required>
        <input name=confirm type=password required>
        <input type=hidden name=token value=${token}>
        <button>Reset Password</button>
        </form>`
      })
    }
  } else {
    return {
      html: layout({
        account: req.session.account,
        body: '<p>verifying email ... token expired</p>'
      })
    }
  }
}
```

## Create `post-reset-password` function
This function catches the form data from `get-reset-000token` so that we can save the new password into the database. 

```javascript
let arc = require('@architect/functions')
let data = require('@begin/data')
let bcrypt = require('bcryptjs')

exports.handler = arc.http.async(reset)

async function reset(req) {

  //confirm values are the same and validate token to get email
  if (req.body.password === req.body.confirm) {

    //look up email
    let result = await data.get({
      table: 'tokens',
      key: req.body.token
    })
    let email = result.email

    //look up account for verified flag
    let account = await data.get({
      table: 'accounts',
      key: email
    })

    // save the new password to the account record
    let salt = bcrypt.genSaltSync(10)
    let hash = bcrypt.hashSync(req.body.password, salt)

    await data.set({
      table: 'accounts',
      key: email,
      password: hash,
      verified: account.verified
    })

    return {
      session: {
        account: {
          email: req.body.email
        }
      },
      location: '/admin'
    }
  } else {
    return {
      location: '/?password=nomatch'
    }
  }
}
```

## Delete the account 
Now we're going to add a feature to delete the account from the `/admin` protected route. 

First we'll have to create a `post-reset-nuke` Lambda function to catch the form data from `/admin` 

```javascript
let arc = require('@architect/functions')
let data = require('@begin/data')
let bcrypt = require('bcryptjs');

exports.handler = arc.http.async(nuke)

async function nuke(req) {
  let result = await data.get({
    table: 'accounts',
    key: req.body.email
  })

  if (!result) {
    return {
      session: {},
      location: '/?notfound'
    }
  }

  let hash = result.password
  let good = bcrypt.compareSync(req.body.password, hash)

  if (good) {
    await data.destroy({
      table: 'accounts',
      key: req.body.email
    })
    console.log('account destroyed')
    return {
      session: {},
      location: '/'
    }
  }
  else {
    return {
      session: {},
      location: '/?badpassword'
    }
  }
}
```
## Wrapping it all up

By now you have a full app where users can register an account, login, logout, reset passwords, and receive verification emails, and delete their account. Congratulations! You've done it. If you have any questions leave them in the comments below, and check out the full repo here: [https://github.com/pchinjr/serverless-login-flow](https://github.com/pchinjr/serverless-login-flow)