How to Send Password Resets via SMS and email using Node.js and Next.js
When you’re building a web application, there’s an immediate decision to make about how to handle Users and Authentication. There are lots of services (Auth0, Clerk) and libraries ( Passport.js) to choose from, and the right choice will depend on the requirements of the application that you’re building. Regardless of the decision here, it’s important that end users have a simple way to reset their passwords and to receive those token notifications on their preferred channel (sms, email, etc).
In this tutorial, I’m going to build a simple and secure password reset flow using Courier and the latest Next.js 13 (app router) that allows the end user to receive a token using either SMS or email. We’re going to cover:
- Creating a new Next.js web application
- Configuring Courier to handle SMS and email notifications
- Using Courier to store user profile and preference data
- Using Vercel KV for token storage
- Routing token notifications based on user preferences for email or SMS
There are a few prerequisites for completing this tutorial:
You can find the full source code for this application on Github and a live demo of this app hosted on Vercel.
Creating a Next.js web application
In order to build a Next.js app, you’ll need to have Node.js installed. My preference these days is to use NVM (Node Version Manager) to install Node.js. It makes it easy to install multiple versions of Node.js and switch between them in your projects.
Once you’ve installed Node.js, open up a terminal and run the following command to install Next.js:
npx create-next-app@latest
You’ll be prompted to answer several questions, but it’s fine to stick to the defaults. Once this process is complete, a new directory will be created and loaded with all of the default files for this app.
Change into this new directory and create a .env.local
file to store secrets for Courier and Vercel. We'll populate this file while we're building and testing on localhost, and you'll just need to remember to migrate these environment variables to whatever platform or infra you deploy your app to.
Get Courier API Credentials
Log-in to your Courier account and click on the gear icon and then API Keys. When you create a Courier account, we automatically create two Workspaces for you, one for testing and one for production. Each workspace has its own set of data and API keys.
For simplicity, we’re going to stick to the “production” workspace. Copy the “published” production API Key and paste into into .env.local
using the following key:
COURIER_AUTH_TOKEN=pk_XXX
The “published” API key means that when you send a notification and reference a template, it will only use the published version of that template. If you’re editing a template and it is auto-saved as a draft (but not published) you can use the “draft” API key to use that draft template when sending. Once again, for the sake of simplicity, we’re going to stick to published templates and the published API key.
Configure Your Email and SMS Providers
Click on “Channels” in the left nav. This is where you can configure the providers you’d like to use to deliver notifications. These providers are grouped into channels, like SMS and email. For the purposes of this tutorial, you need to configure an SMS provider (like Twilio) and an email provider (like Postmark).
Create a Notification Template
Click on “Designer” on the left nav. You’ll see a default notification template called “Welcome to Courier”. Notification templates make it easy for developers to customize what a single notification (i.e. a password reset notification) looks like across different channels like email, SMS, push, etc.
Create a new template and call it “Password Reset Token”. Leave the Subscription Topic set to “General Notifications”. The next screen will allow you to select the channels you’d like to design a template for. Please select “email” and “SMS”.
On the left, you’ll see a list of the channels you selected. Make sure to configure each channel to use the provider you configured above. If you only have one provider for each channel, Courier will default to it and there’s nothing for you to do. If you had multiple providers for a channel, you’d see a warning and be asked to select one.
In your browser’s URL bar, you should see a URL that looks like this:
https://app.courier.com/designer/notifications/XXX/design?channel=YYY
The XXX
is the unique ID of this template. Copy that ID and paste it into your .env.local
:
COURIER_TEMPLATE=XXX
Get Vercel KV Credentials
Log-in to your Vercel account and click on “Storage”. Click “Create Database” and select KV (Durable Redis). Give the database any name you like and stick to the default configuration options for now.
Once your new KV database is created, you’ll see a “Quickstart” section just below the name of your database. In that section click the “.env.local” tab. This displays the environment variables you need to interact with the database from your app. Click “copy snippet” and paste those values into your app’s .env.local
file:
KV_URL="redis://default:xxx@1234.kv.vercel-storage.com:35749"
KV_REST_API_URL="https://1234.kv.vercel-storage.com"
KV_REST_API_TOKEN="yyy"
KV_REST_API_READ_ONLY_TOKEN="zzz"
Let’s Start Coding!
Ok, now that we have our services and configuration out of the way, let’s dive into the code. This app is built on the latest Next.js 13 with the app router. These application will support the following flow:
- Create a dummy user to test the password reset
- Forgot Password page — user can enter an email address or phone number
- Enter Token page — user can enter the token that was sent to them
- Change Password page — user can enter a new password
New User Page
The new Next.js has strict conventions on how to create routes for client-side and server-side code. To create the new user page, first create a new directory under app
called new-user
and then create a file in that directory called page.js
. Paste the following code into the file:
export default function NewUser(request) {
return (
<main className="flex min-h-screen flex-col items-center justify-between p-24">
<p>Hello New User Page</p>
</main>
)
}
Spin-up your local dev server to double-check that everything is working properly. In the root of your project run:
npm run dev
Then open up http://locahost:3000/new-user
in your browser and confirm that you see "Hello New User Page". Once you've verified the app is working, we can move on to building out this page.
At the top of the file, including the following:
'use client'
import { useRouter } from 'next/navigation'
import { useState } from 'react'
The use client
directive tells Next.js that this component should only run on the client-side. Take a minute to familiarize yourself with React.js client and server components and how they fit into the design of the new Next.js. The useRouter
and useState
imports give us tools to handle redirection and displaying error messages.
Now, replace the <p>Hello New User Page</p>
with the following code:
<form onSubmit={onCreateUser} className="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4">
{ error && ( <div className="mb-4 bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative">{ error }</div>) }
<div className="mb-4">
<p>Create a FAKE user so that we can test the password reset flow.</p>
<p>Please enter your REAL email address and phone number in order to see how the demo works.</p>
<p>NOTE: all your data will be purged after 5 minutes.</p>
</div>
<div className="mb-4">
<label htmlFor="name" className="block text-gray-700 text-sm font-bold mb-2">Full Name</label>
<input type="text" name="name" id="name" className="mb-2 shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"></input>
<label htmlFor="email" className="block text-gray-700 text-sm font-bold mb-2">Email Address</label>
<input type="email" name="email" id="email" className="mb-2 shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"></input>
<label htmlFor="phone" className="block text-gray-700 text-sm font-bold mb-2">Mobile Number</label>
<input type="text" name="phone" id="phone" className="mb-2 shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"></input>
<label htmlFor="password" className="block text-gray-700 text-sm font-bold mb-2">Password</label>
<input type="password" name="password" id="password" className="mb-2 shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"></input>
<label htmlFor="preference" className="block text-gray-700 text-sm font-bold mb-2">Notification Preference</label>
<select name="preference" id="preference">
<option value="email">Email</option>
<option value="phone">SMS</option>
</select>
</div>
<input type="submit" value="Create User" className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline"></input>
</form>
Tailwind classes aside, this is a pretty basic HTML form that lets you create a quick and dirty user for the sake of testing the password reset flow. In a real world application, you’ll need to have a proper user management set-up.
The form submission itself triggers a client-side JS function that we will now define. Just below the export default function NewUser(request) {
line, add the following code:
const router = useRouter()
const [ error, setError ] = useState()
async function onCreateUser(event) {
event.preventDefault()
const formData = new FormData(event.target)
const payload = {
name: formData.get('name'),
email: formData.get('email'),
phone: formData.get('phone'),
password: formData.get('password'),
preference: formData.get('preference'),
}
const response = await createUser(payload)
if (response.error) {
setError(response.error)
}
else if (response.redirect) {
router.push(`${response.redirect}?message=${response.message}`)
}
return true
}
The function onCreateUser
does the work of parsing the HTML form, building a JSON payload, calling the createUser
and then either redirecting (success) or displaying an error (failure).
Finally, below the imports at the top of the file, include the following function:
// submit this data to create-user/route.js
async function createUser(payload) {
const res = await fetch('/create-user', { method: 'POST', body: JSON.stringify(payload) })
if (!res.ok) return undefined
return res.json()
}
This function uses JS native fetch to call our backend to create the user. Go ahead and reload http://localhost:3000/new-user
and ensure that the form renders properly. But don't submit it! We haven't written the backend, so let's do that now.
Create User Route
Create a new directory under app
called create-user
and create a file called route.js
. Route handlers are used for backend code and can support GET
, POST
, PUT
, PATCH
, DELETE
, HEAD
, and OPTIONS
HTTP methods. In our case we're going to implement a POST
handler with the following code:
import { NextResponse } from 'next/server'
import { kv } from '@vercel/kv'
import { CourierClient } from '@trycourier/courier'
import { createUser } from '../../models/users'
import { setSession } from '../../session'
const courier = CourierClient({ authorizationToken: process.env.courier_auth_token })
export async function POST(request) {
const data = await request.json()
// get full name, phone number, email and password from form payload
const { name, email, phone, password, preference } = data
// create the User
const user_id = await createUser({ name, email, phone, password, preference })
// create the Courier Profile for this User
await courier.mergeProfile({
recipientId: user_id,
profile: {
phone_number: phone,
email,
name,
// Courier supports storing custom JSON data for Profiles
custom: {
preference
}
}
})
// return response
const response = NextResponse.json({
redirect: '/',
message: 'Your User has been created 👍'
})
setSession(response, 'user_id', user_id)
return response
}
This function takes the data passed-in and uses it to create a new User. Once you’ve created the User and have a unique user_id
you can create a profile in Courier to store this information. Storing a subset of a user's profile information in Courier makes it easy to customize notifications and respect a user's routing preferences.
After the User has been created, a response is sent to the client with information about where to redirect the user to and what message to display. In this case, we’re simply going back to the index page.
Before we can execute this route, we need to implement a simple user model and session service. Remember, this code is just for demonstration purposes, so make sure you’re handling Users and Sessions properly when you build your app.
The User Model
Create a directory at the root of your project called models
and create a new file called users.js
and paste in the following code:
import { kv } from '@vercel/kv'
import { createHash } from 'node:crypto'
async function createUser({ password, name, email, phone, preference }) {
// create unique ID for user
const id = createHash('sha3-256').update(phone ? phone : email).digest('hex')
const key = `users:${ id }:${ email }:${ phone }`
const ex = 5 * 60 // expire this record in 5 minutes
// hash the password
const hashed_password = createHash('sha3-256').update(password).digest('hex')
await kv.set(key, { user_id: key, hashed_password, name, email, phone, preference }, { ex })
return key
}
async function findUserById(user_id) {
const keys = await kv.keys('users:*')
const key = keys.find(k => k.indexOf(user_id) >= 0)
return key ? await kv.get(key) : null
}
async function findUserByEmail(email) {
const keys = await kv.keys('users:*')
const key = keys.find(k => k.indexOf(email) >= 0)
return key ? await kv.get(key) : null
}
async function findUserByPhone(phone) {
const keys = await kv.keys('users:*')
const key = keys.find(k => k.indexOf(phone) >= 0)
return key ? await kv.get(key) : null
}
async function updatePassword(key, password) {
const user = await kv.get(key)
const ex = 5 * 60 // expire this record in 5 minutes
// hash the password
const hashed_password = createHash('sha3-256').update(password).digest('hex')
await kv.set(key, { ...user, hashed_password }, { ex })
}
export {
createUser,
findUserById,
findUserByEmail,
findUserByPhone,
updatePassword
}
Please note that the code above auto-deletes user information after 5 minutes. This is a (not so gentle) reminder that this is not designed for production.
Handling Sessions
Create a file at the root of your project called session.js
and paste the following code:
function getSession(req, attr) {
const cookie = req.cookies.get(attr)
return (cookie ? cookie.value : undefined)
}
function setSession(res, attr, value) {
res.cookies.set(attr, value)
}
export {
getSession,
setSession
}
This code uses cookies to simulate a session. Once again, do not use this in production.
Try Creating a User
Ok, we’ve done a lot of work to wire up Courier, Vercel and our Next.js application. Let’s see if you can create a User! Go to http://localhost:3000/new-user
, fill-out the form and click submit. You should be redirected to the index page.
Now, go back to your Courier account and click on “users” in the left nav. If all went well, you should see the new User you created!
With that out of the way, we can finally get to what you came here for: password reset notifications 📬!
Forgot Password Page
Create a directory under app called forgot-password, create a new file in it called page.js and paste the following code:
'use client'
import { useRouter } from 'next/navigation'
import { useState } from 'react'
async function sendToken(payload) {
const res = await fetch('/send-token', { method: 'POST', body: JSON.stringify(payload) })
if (!res.ok) return undefined
return res.json()
}
export default function ForgotPassword(request) {
const router = useRouter()
const [ error, setError ] = useState()
async function onForgotPassword(event) {
event.preventDefault()
const formData = new FormData(event.target)
const payload = {
email: formData.get('email'),
phone: formData.get('phone')
}
const response = await sendToken(payload)
if (response.error) {
setError(response.error)
}
else if (response.redirect) {
router.push(`${response.redirect}?mode=${response.mode}`)
}
return true
}
return (
<main className="flex min-h-screen flex-col items-center justify-between p-24">
<form method="post" onSubmit={ onForgotPassword } className="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4">
{ error && ( <div className="mb-4 bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative">{ error }</div>) }
<div className="mb-4">Please use the same email or phone number that you used to <a href="/new-user">create your user</a>.</div>
<div className="mb-4">
<label htmlFor="email" className="block text-gray-700 text-sm font-bold mb-2">Email Address</label>
<input type="email" name="email" id="email" className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"></input>
</div>
<div className="mb-4">- or -</div>
<div className="mb-4">
<label htmlFor="phone" className="block text-gray-700 text-sm font-bold mb-2">Mobile Phone</label>
<input type="text" name="phone" id="phone" className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"></input>
</div>
<input type="submit" value="Reset Password" className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline"></input>
</form>
</main>
)
}
This code is functionality identical to the code we used for the new-user
page. The more interesting code is on the backend.
Send Token Route
Create a new directory under app called send-token, create a file called route.js and paste the following code:
import { NextResponse } from 'next/server'
import { kv } from '@vercel/kv'
import { CourierClient } from '@trycourier/courier'
import { findUserByEmail, findUserByPhone } from '../../models/users'
import { setSession } from '../../session'
const courier = CourierClient({ authorizationToken: process.env.courier_auth_token })
export async function POST(request) {
// get phone number and email from form payload
const data = await request.json()
const { email, phone } = data
let user
// look up the user based on phone or email
if (email) {
user = await findUserByEmail(email)
}
else if (phone) {
user = await findUserByPhone(phone)
}
else {
// neither an email nor phone number was submitted, re-direct and display error
return NextResponse.json({
error: 'You must provide an email or phone number'
})
}
if (user) {
const { user_id } = user
// generate reset token
const token = Math.floor(Math.random() * 1000000).toString().padStart(6, '0')
const ex = 5 * 60 // expire this record in 5 minutes
// store in KV cache
await kv.set(`${user_id}:reset`, token, { ex })
// send notification
await courier.send({
message: {
to: {
user_id
},
template: process.env.COURIER_TEMPLATE,
data: {
token
}
}
})
// redirect to enter token page
return NextResponse.json({
redirect: '/enter-token',
mode: (email ? 'email' : 'phone')
})
}
else {
// redirect and display error
return NextResponse.json({
error: 'We could not locate a user with that email address or phone number'
})
}
}
Create a new directory under app called send-token
, create a file called route.js
and paste the following code:
This function uses the user model to retrieve a user based on either an email address or a phone number. A random 6 digit token is generated, stored in Vercel KV and sent to the user for verification using Courier.
to
- required, specifies the recipient(s) of the messagetemplate
- the template to use for this messagedata
- an arbitrary JSON payload of dynamic data
The data
attribute is where we store the token that we've generated. Values in data
are interpolated into the templates that you define when the message is being rendered. Let's switch out of the code (briefly!) to define our SMS and email notification templates.
Building the Email and SMS Notification Templates
Go back to the Courier App, and click “Designer” on the left nav. Edit the “Password Reset Token” template that you created.
For the email template, set the Subject to “Your password reset token” and add a Markdown Block to the body of the message with the following content:
Hello {profile.name}, here is your {token}
You should see {profile.name}
and {token}
be highlighted in green. This means that Courier is recognizing them as variables.
Since we set the name
attribute when creating the profile, it's magically available to us in the template. Cool! Also, since we passed a token
attribute in the send API call, that is also available to us here in this template.
Click on SMS on the left to edit the SMS template. Create a Markdown Block in the body of the message and type out the following:
Hi there {profile.name} 👋 Your password reset token is: {token}
Routing to the Right Channel
Now that we have the template content defined, we need to ensure the messages route to the correct channel based on the user’s preference.
Hover your mouse over “email” on the left, and you’ll see a gear icon appear. Click the gear icon to edit this channel’s settings. Click “conditions” on the left and “add” a new condition.
We are going to “disable” this channel when the profile.preference
property is equal to "phone":
- Flip the enable/disable toggle to disable.
- Select “Profile” for source.
- Select “=” for operator.
- Type in “phone” for value.
Once you’re done entering info, everything is auto-saved. Just click outside of the modal to close it. Repeat the same process for the “sms” channel, but set the value for the conditional to “email”. We have now disabled these channels in the event the user has selected a different one to receive their notifications on.
Click “publish” in the top right corner to make make these changes live.
Enter Token Page
Ok, back to the code! Create a new directory in app called enter-token and a file in it called page.js. The user is redirected to this page and must enter the token they are sent via email or SMS in order to proceed. Paste this code into the file:
'use client'
import { useRouter } from 'next/navigation'
import { useState } from 'react'
async function verifyToken(payload) {
const res = await fetch('/verify-token', { method: 'POST', body: JSON.stringify(payload) })
if (!res.ok) return undefined
return res.json()
}
export default function EnterToken(request) {
const router = useRouter()
const [ error, setError ] = useState()
async function onVerifyToken(event) {
event.preventDefault()
const formData = new FormData(event.target)
const payload = {
token: formData.get('token'),
}
const response = await verifyToken(payload)
if (response.error) {
setError(response.error)
}
else if (response.redirect) {
router.push(response.redirect)
}
return true
}
const mode = request.searchParams?.mode
return (
<main className="flex min-h-screen flex-col items-center justify-between p-24">
<form method="post" onSubmit={ onVerifyToken } className="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4">
{ error && ( <div className="mb-4 bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative">{ error }</div>) }
<div className="mb-4">Check your { mode } and enter token that we have sent you below. </div>
<div className="mb-4">
<label htmlFor="token" className="block text-gray-700 text-sm font-bold mb-2">Token</label>
<input type="token" name="token" id="token" className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"></input>
</div>
<input type="submit" value="Validate Token" className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline"></input>
</form>
</main>
)
}
Nothing to see here, let’s check out the server-side route that handles this form.
Verify Token Route
Create a directory in app called verify-token and a file in it called route.js with the following code:
import { NextResponse } from 'next/server'
import { kv } from '@vercel/kv'
import User from '../../models/users'
import { getSession, setSession } from '../../session'
export async function POST(request) {
// get phone number and email from form payload
const data = await request.json()
const { token } = data
// get user_id from session
const userId = getSession(request, 'user_id')
const storedToken = "" + await kv.get(`${userId}:reset`) // ensure the token is of type string
if (userId && token && (token === storedToken)) {
// redirect to reset password page
const response = NextResponse.json({
redirect: '/new-password'
})
setSession(response, 'authenticated', true)
return response
}
else {
// redirect and display error
return NextResponse.json({
error: 'Token did not match, please try again?'
})
}
}
Here, we get the token from the form submission and the user_id
from the session. We use the user_id
to construct the key, and use the key to retrieve the value stored there. We then check to see if the values of the form submission and stored tokens match. If they don't match, we return an error to the page.
If they DO match, we set a property on our session of authenticated
to the value of true
and forward to the /new-password
page.
Locking Down Routes with Middleware
The last page we are going to build ( new-password
) and the last route we are going to build ( update-password
) should be considered secure. We don't want users to interact with these pages unless they have authenticated themselves by successfully confirming they have received the token we sent them. A recommended way to secure pages and routes in Next.js is by using middleware.
Create a new file in the root of your project called middleware.js
and paste the following code:
import { NextResponse } from 'next/server'
import { getSession } from './session'
export function middleware(request) {
const authenticated = getSession(request, 'authenticated')
if (authenticated) {
return NextResponse.next()
}
else {
const homeUrl = new URL('/', request.url)
homeUrl.searchParams.set('message', 'You are not authorized')
return NextResponse.redirect(homeUrl)
}
}
export const config = {
matcher: ['/new-password', '/reset-password'],
}
This middleware function processes every request that matches /new-password or /reset-password. For matching requests, the middleware checks the session to see if the user is authenticated. If so, it proceeds with the request. If not, it redirects to the index page with an error message.
New Password Page
Create a directory in app
called new-password
and a file in it called page.js
with the following code:
'use client'
import { useRouter } from 'next/navigation'
import { useState } from 'react'
async function resetPassword(payload) {
const res = await fetch('/reset-password', { method: 'POST', body: JSON.stringify(payload) })
if (!res.ok) return undefined
return res.json()
}
export default function NewPassword(request) {
const router = useRouter()
const [ error, setError ] = useState()
async function onResetPassword(event) {
event.preventDefault()
const formData = new FormData(event.target)
const payload = {
newPassword: formData.get('new_password'),
newPasswordConfirm: formData.get('new_password_confirm'),
}
const response = await resetPassword(payload)
if (response.error) {
setError(response.error)
}
else if (response.redirect) {
router.push(`${response.redirect}?message=${response.message}`)
}
return true
}
return (
<main className="flex min-h-screen flex-col items-center justify-between p-24">
<form method="post" onSubmit={ onResetPassword } className="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4">
{ error && ( <div className="mb-4 bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative">{ error }</div>) }
<div className="mb-4">Almost done! Now just enter a new password.</div>
<div className="mb-4">
<label htmlFor="new_password" className="block text-gray-700 text-sm font-bold mb-2">New Password</label>
<input type="password" name="new_password" id="new_password" className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"></input>
</div>
<div className="mb-4">
<label htmlFor="new_password_confirm" className="block text-gray-700 text-sm font-bold mb-2">Confirm Password</label>
<input type="password" name="new_password_confirm" id="new_password_confirm" className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"></input>
</div>
<input type="submit" value="Reset Password" className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline"></input>
</form>
</main>
)
}
Once again, a pretty standard page with a form. Let’s look into the route that processes the new passwords.
Reset Password Route
Create a directory in app
called reset-password
and a file in it called route.js
with the following code:
import { NextResponse } from 'next/server'
import { kv } from '@vercel/kv'
import { updatePassword } from '../../models/users'
import { getSession } from '../../session'
export async function POST(request) {
// get passwords from payload
const data = await request.json()
const { newPassword, newPasswordConfirm } = data
// get user_id from session
const user_id = getSession(request, 'user_id')
// update the user
if (user_id && newPassword && newPasswordConfirm && (newPassword === newPasswordConfirm)) {
await updatePassword(user_id, newPassword)
return NextResponse.json({
redirect: '/',
message: 'Your password has been reset 👍'
})
}
else {
// password don't match
return NextResponse.json({
error: 'Your passwords must match'
})
}
}
Here we get the new password and the confirmed password from the form and the user_id
from the session. If we have a user_id
and the password match, update the user's password! Woo hoo, we did it!
Wrapping Things Up
Phew, we made it! Our goal was to use Courier and Next.js to build a secure password reset flow that allowed the user to receive either an SMS or an email based on their preferences. Let’s review what we covered in this tutorial:
- Creating a fresh Next.js (app router) web application
- Building Next.js client-side pages, server-side routes and middleware
- Configuring Courier to use SMS and email providers
- Creating templates for SMS and email notifications
- Storing user profile and preference data in Courier
- Using Vercel KV for token storage
- Routing token notifications based on user preferences
I hope you enjoyed this tutorial and you can ping me ( crtr0) on Twitter if you have any questions!
You can find the full source code for this application on Github. Pull requests welcome! You can also play with a live demo of this app which is hosted on Vercel.
Originally published at https://www.courier.com.