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
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
})
}
}
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: `/`
}
}
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>'
})
}
}
}
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'
}
}
}
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'
}
}
}
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
Top comments (0)