Most Popular
Courier makes it easy to send SMS, email, push and in-app notifications with a single API call. We have a REST API, client SDKs and Docs that you'll love 💜
Sign-up
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:
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.
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:
1npx 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.
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:
1COURIER_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.
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).
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
:
1COURIER_TEMPLATE=XXX
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:
1KV_URL="redis://default:xxx@1234.kv.vercel-storage.com:35749"2KV_REST_API_URL="https://1234.kv.vercel-storage.com"3KV_REST_API_TOKEN="yyy"4KV_REST_API_READ_ONLY_TOKEN="zzz"
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:
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:
1export default function NewUser(request) {2return (3<main className="flex min-h-screen flex-col items-center justify-between p-24">4<p>Hello New User Page</p>5</main>6)7}
Spin-up your local dev server to double-check that everything is working properly. In the root of your project run:
1npm 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:
1'use client'2import { useRouter } from 'next/navigation'3import { 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:
1<form onSubmit={onCreateUser} className="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4">2{ error && ( <div className="mb-4 bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative">{ error }</div>) }3<div className="mb-4">4<p>Create a FAKE user so that we can test the password reset flow.</p>5<p>Please enter your REAL email address and phone number in order to see how the demo works.</p>6<p>NOTE: all your data will be purged after 5 minutes.</p>7</div>8<div className="mb-4">9<label htmlFor="name" className="block text-gray-700 text-sm font-bold mb-2">Full Name</label>10<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>11<label htmlFor="email" className="block text-gray-700 text-sm font-bold mb-2">Email Address</label>12<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>13<label htmlFor="phone" className="block text-gray-700 text-sm font-bold mb-2">Mobile Number</label>14<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>15<label htmlFor="password" className="block text-gray-700 text-sm font-bold mb-2">Password</label>16<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>17<label htmlFor="preference" className="block text-gray-700 text-sm font-bold mb-2">Notification Preference</label>18<select name="preference" id="preference">19<option value="email">Email</option>20<option value="phone">SMS</option>21</select>22</div>23<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>24</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:
1const router = useRouter()2const [ error, setError ] = useState()34async function onCreateUser(event) {5event.preventDefault()6const formData = new FormData(event.target)7const payload = {8name: formData.get('name'),9email: formData.get('email'),10phone: formData.get('phone'),11password: formData.get('password'),12preference: formData.get('preference'),13}14const response = await createUser(payload)15if (response.error) {16setError(response.error)17}18else if (response.redirect) {19router.push(`${response.redirect}?message=${response.message}`)20}21return true22}
The router
allows us to trigger client-side routing when it's time to move on to the next page. The error
and setError
hooks allow us to display error messages to the user.
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:
1// submit this data to create-user/route.js2async function createUser(payload) {3const res = await fetch('/create-user', { method: 'POST', body: JSON.stringify(payload) })4if (!res.ok) return undefined5return res.json()6}
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 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:
1import { NextResponse } from 'next/server'2import { kv } from '@vercel/kv'3import { CourierClient } from '@trycourier/courier'4import { createUser } from '../../models/users'5import { setSession } from '../../session'67const courier = CourierClient({ authorizationToken: process.env.courier_auth_token })89export async function POST(request) {10const data = await request.json()11// get full name, phone number, email and password from form payload12const { name, email, phone, password, preference } = data13// create the User14const user_id = await createUser({ name, email, phone, password, preference })15// create the Courier Profile for this User16await courier.mergeProfile({17recipientId: user_id,18profile: {19phone_number: phone,20email,21name,22// Courier supports storing custom JSON data for Profiles23custom: {24preference25}26}27})28// return response29const response = NextResponse.json({30redirect: '/',31message: 'Your User has been created 👍'32})33setSession(response, 'user_id', user_id)34return response35}
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.
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:
1import { kv } from '@vercel/kv'2import { createHash } from 'node:crypto'34async function createUser({ password, name, email, phone, preference }) {5// create unique ID for user6const id = createHash('sha3-256').update(phone ? phone : email).digest('hex')7const key = `users:${ id }:${ email }:${ phone }`8const ex = 5 * 60 // expire this record in 5 minutes9// hash the password10const hashed_password = createHash('sha3-256').update(password).digest('hex')11await kv.set(key, { user_id: key, hashed_password, name, email, phone, preference }, { ex })12return key13}1415async function findUserById(user_id) {16const keys = await kv.keys('users:*')17const key = keys.find(k => k.indexOf(user_id) >= 0)18return key ? await kv.get(key) : null19}2021async function findUserByEmail(email) {22const keys = await kv.keys('users:*')23const key = keys.find(k => k.indexOf(email) >= 0)24return key ? await kv.get(key) : null25}2627async function findUserByPhone(phone) {28const keys = await kv.keys('users:*')29const key = keys.find(k => k.indexOf(phone) >= 0)30return key ? await kv.get(key) : null31}3233async function updatePassword(key, password) {34const user = await kv.get(key)35const ex = 5 * 60 // expire this record in 5 minutes36// hash the password37const hashed_password = createHash('sha3-256').update(password).digest('hex')38await kv.set(key, { ...user, hashed_password }, { ex })39}4041export {42createUser,43findUserById,44findUserByEmail,45findUserByPhone,46updatePassword47}
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.
Create a file at the root of your project called session.js
and paste the following code:
1function getSession(req, attr) {2const cookie = req.cookies.get(attr)3return (cookie ? cookie.value : undefined)4}56function setSession(res, attr, value) {7res.cookies.set(attr, value)8}910export {11getSession,12setSession13}
This code uses cookies to simulate a session. Once again, do not use this in production.
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 📬!
Create a directory under app
called forgot-password
, create a new file in it called page.js
and paste the following code:
1'use client'2import { useRouter } from 'next/navigation'3import { useState } from 'react'45async function sendToken(payload) {6const res = await fetch('/send-token', { method: 'POST', body: JSON.stringify(payload) })7if (!res.ok) return undefined8return res.json()9}1011export default function ForgotPassword(request) {12const router = useRouter()13const [ error, setError ] = useState()1415async function onForgotPassword(event) {16event.preventDefault()17const formData = new FormData(event.target)18const payload = {19email: formData.get('email'),20phone: formData.get('phone')21}22const response = await sendToken(payload)23if (response.error) {24setError(response.error)25}26else if (response.redirect) {27router.push(`${response.redirect}?mode=${response.mode}`)28}29return true30}31return (32<main className="flex min-h-screen flex-col items-center justify-between p-24">33<form method="post" onSubmit={ onForgotPassword } className="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4">34{ error && ( <div className="mb-4 bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative">{ error }</div>) }35<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>36<div className="mb-4">37<label htmlFor="email" className="block text-gray-700 text-sm font-bold mb-2">Email Address</label>38<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>39</div>40<div className="mb-4">- or -</div>41<div className="mb-4">42<label htmlFor="phone" className="block text-gray-700 text-sm font-bold mb-2">Mobile Phone</label>43<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>44</div>45<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>46</form>47</main>48)49}
This code is functionality identical to the code we used for the new-user
page. The more interesting code is on the backend.
Create a new directory under app called send-token
, create a file called route.js
and paste the following code:
1import { NextResponse } from 'next/server'2import { kv } from '@vercel/kv'3import { CourierClient } from '@trycourier/courier'4import { findUserByEmail, findUserByPhone } from '../../models/users'5import { setSession } from '../../session'67const courier = CourierClient({ authorizationToken: process.env.courier_auth_token })89export async function POST(request) {10// get phone number and email from form payload11const data = await request.json()12const { email, phone } = data13let user14// look up the user based on phone or email15if (email) {16user = await findUserByEmail(email)17}18else if (phone) {19user = await findUserByPhone(phone)20}21else {22// neither an email nor phone number was submitted, re-direct and display error23return NextResponse.json({24error: 'You must provide an email or phone number'25})26}2728if (user) {29const { user_id } = user30// generate reset token31const token = Math.floor(Math.random() * 1000000).toString().padStart(6, '0')32const ex = 5 * 60 // expire this record in 5 minutes33// store in KV cache34await kv.set(`${user_id}:reset`, token, { ex })35// send notification36await courier.send({37message: {38to: {39user_id40},41template: process.env.COURIER_TEMPLATE,42data: {43token44}45}46})47// redirect to enter token page48return NextResponse.json({49redirect: '/enter-token',50mode: (email ? 'email' : 'phone')51})52}53else {54// redirect and display error55return NextResponse.json({56error: 'We could not locate a user with that email address or phone number'57})58}59}
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.
Vercel KV is a durable Redis store that's great for the use case of storing tokens and auto-purging them after a few minutes. The code in our function sets a key (who’s value is the user_id
appended with :reset
) with the value of the token. The ex
attribute specifies when the key/value should be automatically purged from the DB.
Let's take a sec to break down this Courier send
API call. The top-level attribute that we pass in this API call is a message
object. The message
object supports several properties that you can use to send and route messages, but in this case we only use 3:
to
- required, specifies the recipient(s) of the messagetemplate
- the template to use for this messagedata
- an arbitrary JSON payload of dynamic dataSince the user's email address and phone number have already been stored in the user's Courier Profile, we only need to pass a user_id
in the to
portion of the message. Courier will figure out whether to use the email address or phone number based on the user's preference that we stored in custom.preference
.
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.
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:
1Hello {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:
1Hi there {profile.name} 👋 Your password reset token is: {token}
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":
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.
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:
1'use client'2import { useRouter } from 'next/navigation'3import { useState } from 'react'45async function verifyToken(payload) {6const res = await fetch('/verify-token', { method: 'POST', body: JSON.stringify(payload) })7if (!res.ok) return undefined8return res.json()9}1011export default function EnterToken(request) {12const router = useRouter()13const [ error, setError ] = useState()1415async function onVerifyToken(event) {16event.preventDefault()17const formData = new FormData(event.target)18const payload = {19token: formData.get('token'),20}21const response = await verifyToken(payload)22if (response.error) {23setError(response.error)24}25else if (response.redirect) {26router.push(response.redirect)27}28return true29}3031const mode = request.searchParams?.mode32return (33<main className="flex min-h-screen flex-col items-center justify-between p-24">34<form method="post" onSubmit={ onVerifyToken } className="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4">35{ error && ( <div className="mb-4 bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative">{ error }</div>) }36<div className="mb-4">Check your { mode } and enter token that we have sent you below. </div>37<div className="mb-4">38<label htmlFor="token" className="block text-gray-700 text-sm font-bold mb-2">Token</label>39<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>40</div>41<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>42</form>43</main>44)45}
Nothing to see here, let’s check out the server-side route that handles this form.
Create a directory in app
called verify-token
and a file in it called route.js
with the following code:
1import { NextResponse } from 'next/server'2import { kv } from '@vercel/kv'3import User from '../../models/users'4import { getSession, setSession } from '../../session'56export async function POST(request) {7// get phone number and email from form payload8const data = await request.json()9const { token } = data10// get user_id from session11const userId = getSession(request, 'user_id')12const storedToken = "" + await kv.get(`${userId}:reset`) // ensure the token is of type string13if (userId && token && (token === storedToken)) {14// redirect to reset password page15const response = NextResponse.json({16redirect: '/new-password'17})18setSession(response, 'authenticated', true)19return response20}21else {22// redirect and display error23return NextResponse.json({24error: 'Token did not match, please try again?'25})26}27}
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.
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:
1import { NextResponse } from 'next/server'2import { getSession } from './session'34export function middleware(request) {5const authenticated = getSession(request, 'authenticated')6if (authenticated) {7return NextResponse.next()8}9else {10const homeUrl = new URL('/', request.url)11homeUrl.searchParams.set('message', 'You are not authorized')12return NextResponse.redirect(homeUrl)13}14}1516export const config = {17matcher: ['/new-password', '/reset-password'],18}
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.
Create a directory in app
called new-password
and a file in it called page.js
with the following code:
1'use client'2import { useRouter } from 'next/navigation'3import { useState } from 'react'45async function resetPassword(payload) {6const res = await fetch('/reset-password', { method: 'POST', body: JSON.stringify(payload) })7if (!res.ok) return undefined8return res.json()9}1011export default function NewPassword(request) {12const router = useRouter()13const [ error, setError ] = useState()1415async function onResetPassword(event) {16event.preventDefault()17const formData = new FormData(event.target)18const payload = {19newPassword: formData.get('new_password'),20newPasswordConfirm: formData.get('new_password_confirm'),21}22const response = await resetPassword(payload)23if (response.error) {24setError(response.error)25}26else if (response.redirect) {27router.push(`${response.redirect}?message=${response.message}`)28}29return true30}3132return (33<main className="flex min-h-screen flex-col items-center justify-between p-24">34<form method="post" onSubmit={ onResetPassword } className="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4">35{ error && ( <div className="mb-4 bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative">{ error }</div>) }36<div className="mb-4">Almost done! Now just enter a new password.</div>37<div className="mb-4">38<label htmlFor="new_password" className="block text-gray-700 text-sm font-bold mb-2">New Password</label>39<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>40</div>41<div className="mb-4">42<label htmlFor="new_password_confirm" className="block text-gray-700 text-sm font-bold mb-2">Confirm Password</label>43<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>44</div>45<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>46</form>47</main>48)49}
Once again, a pretty standard page with a form. Let’s look into the route that processes the new passwords.
Create a directory in app
called reset-password
and a file in it called route.js
with the following code:
1import { NextResponse } from 'next/server'2import { kv } from '@vercel/kv'3import { updatePassword } from '../../models/users'4import { getSession } from '../../session'56export async function POST(request) {7// get passwords from payload8const data = await request.json()9const { newPassword, newPasswordConfirm } = data10// get user_id from session11const user_id = getSession(request, 'user_id')12// update the user13if (user_id && newPassword && newPasswordConfirm && (newPassword === newPasswordConfirm)) {14await updatePassword(user_id, newPassword)15return NextResponse.json({16redirect: '/',17message: 'Your password has been reset 👍'18})19}20else {21// password don't match22return NextResponse.json({23error: 'Your passwords must match'24})25}26}
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!
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:
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.
Courier makes it easy to send SMS, email, push and in-app notifications with a single API call. We have a REST API, client SDKs and Docs that you'll love 💜
Sign-up
How to Set Up Automatic Push Notifications Based on Segment Events
Push notifications have carved their own niche as a powerful tool for continuous user engagement. Regardless of whether an app is actively in use, they deliver your messages straight to your user's device. Two key players that can combine to enhance your push notification strategy are Segment and Courier. In this tutorial, we show you how to set up Courier to listen to your Segment events and then send push notifications to an Android device based on data from these events.
Sarah Barber
November 17, 2023
How to Send Firebase Notifications to iOS Devices Using Courier
This tutorial explains how to send push notifications to iOS devices from your iOS application code using Firebase FCM and Courier’s iOS SDK.
Martina Caccamo
November 01, 2023
Free Tools
Comparison Guides
Send up to 10,000 notifications every month, for free.
Get started for free
Send up to 10,000 notifications every month, for free.
Get started for free
© 2024 Courier. All rights reserved.