loading...

Serverless Login with OpenJS Architect, Part 4

pchinjr profile image Paul Chin Jr. ・4 min read

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.

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

Create get-reset

get-reset is a function that gives the user a form to submit their email to receive a reset URL.

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

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

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

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:

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

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.

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

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

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

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

Discussion

pic
Editor guide