Using Directus Auth with NextAuth.js
Published January 10th, 2024
Authentication is an important aspect of web applications, as it provides the ability for users with the right permissions to access certain resources or perform specific actions.
In this article, you'll build an authentication system for a Next.js application using NextAuth.js, and Directus Auth.
Before You Start
You will need the following knowledge and tools:
- Node.js installed on your computer to build the Next.js application
- A Directus project - you can use Directus Cloud or run it yourself.
- A basic knowledge of terminal/CLI commands.
Setting Up Directus
Creating a New Customer Role
Login with your admin credentials and head over to Settings > Access Control, and create a new role for users that can access your Directus app. Name this role Customer
.
Update the description to be Users from Next.js application.
Set any permissions you want this role to have - it will be used for you application's users.
Enabling User Registration
Public user registration is disabled by default. To make use of it, it must first be enabled via your project settings. When enabling it, select your new Customer
role as the role given to newly-registered users.
Setting Up a Next.js Application
Run the following command to initialize a Next.js project:
npx create-next-app@14 next-directus
npx create-next-app@14 next-directus
During installation, when prompted, choose the following configurations:
✔ Would you like to use TypeScript? Yes
✔ Would you like to use ESLint? Yes
✔ Would you like to use Tailwind CSS? No
✔ Would you like to use `src/` directory? No
✔ Would you like to use App Router? (recommended) Yes
✔ Would you like to customize the default import alias (@/*)? Yes
✔ What import alias would you like configured? @/*
✔ Would you like to use TypeScript? Yes
✔ Would you like to use ESLint? Yes
✔ Would you like to use Tailwind CSS? No
✔ Would you like to use `src/` directory? No
✔ Would you like to use App Router? (recommended) Yes
✔ Would you like to customize the default import alias (@/*)? Yes
✔ What import alias would you like configured? @/*
Install the required dependencies:
npm i next-auth @directus/sdk
npm i next-auth @directus/sdk
Create an .env.local
with the following contents:
NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET=YOUR_NEXT_RANDOM_SECRET
USER_ROLE=THE_ROLE_OF_YOUR_CUSTOMER_FROM_DIRECTUS
BACKEND_URL=YOUR_DIRECTUS_URL
NEXT_PUBLIC_URL=http://localhost:3000
NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET=YOUR_NEXT_RANDOM_SECRET
USER_ROLE=THE_ROLE_OF_YOUR_CUSTOMER_FROM_DIRECTUS
BACKEND_URL=YOUR_DIRECTUS_URL
NEXT_PUBLIC_URL=http://localhost:3000
Start the development server:
npm run dev
npm run dev
Configuring the Directus SDK
Create a new directory called lib
. Inside it, create directus.ts
with the following contents to initialize a new Directus SDK instance:
import { createDirectus, rest, authentication } from '@directus/sdk';
const directus = createDirectus(process.env. BACKEND_URL)
.with(authentication("cookie", {credentials: "include", autoRefresh: true}))
.with(rest());
export default directus;
import { createDirectus, rest, authentication } from '@directus/sdk';
const directus = createDirectus(process.env. BACKEND_URL)
.with(authentication("cookie", {credentials: "include", autoRefresh: true}))
.with(rest());
export default directus;
Creating the AuthForm Component
Open the next-directus
project in a code editor. Create a components
directory and an AuthForm
subdirectory. Inside AuthForm
create index.tsx
:
import Link from "next/link";
import { FormEvent, useState } from "react";
interface Data {
first_name?: string;
last_name?: string;
email: string;
password: string;
}
interface AuthFormProps {
title: string;
buttonText: string;
onSubmit: (data: Data) => void;
linkText: string;
linkDescription: string;
linkHref: string;
isFullForm?: boolean;
}
export default function AuthForm({
title,
buttonText,
onSubmit,
linkText,
linkHref,
linkDescription,
isFullForm = true,
}: AuthFormProps) {
const [formData, setFormData] = useState({
first_name: "",
last_name: "",
email: "",
password: "",
});
const handleFormSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
onSubmit(formData);
};
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setFormData({
...formData,
[e.target.name]: e.target.value,
});
};
return (
<form onSubmit={handleFormSubmit}>
<h1>{title}</h1>
{isFullForm && (
<>
<input
type="text"
placeholder="First Name"
name="first_name"
value={formData.first_name}
onChange={handleInputChange}
required
/>
<input
type="text"
placeholder="Last Name"
name="last_name"
value={formData.last_name}
onChange={handleInputChange}
required
/>
</>
)}
<input
type="email"
placeholder="Email Address"
name="email"
value={formData.email}
onChange={handleInputChange}
requir
/>
<input
type="password"
placeholder="Enter your Password"
name="password"
value={formData.password}
required
onChange={handleInputChange}
/>
<button>
{buttonText}
</button>
<p>
{linkDescription}
<Link
href={linkHref}
>
{linkText}
</Link>
</p>
</form>
);
}
import Link from "next/link";
import { FormEvent, useState } from "react";
interface Data {
first_name?: string;
last_name?: string;
email: string;
password: string;
}
interface AuthFormProps {
title: string;
buttonText: string;
onSubmit: (data: Data) => void;
linkText: string;
linkDescription: string;
linkHref: string;
isFullForm?: boolean;
}
export default function AuthForm({
title,
buttonText,
onSubmit,
linkText,
linkHref,
linkDescription,
isFullForm = true,
}: AuthFormProps) {
const [formData, setFormData] = useState({
first_name: "",
last_name: "",
email: "",
password: "",
});
const handleFormSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
onSubmit(formData);
};
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setFormData({
...formData,
[e.target.name]: e.target.value,
});
};
return (
<form onSubmit={handleFormSubmit}>
<h1>{title}</h1>
{isFullForm && (
<>
<input
type="text"
placeholder="First Name"
name="first_name"
value={formData.first_name}
onChange={handleInputChange}
required
/>
<input
type="text"
placeholder="Last Name"
name="last_name"
value={formData.last_name}
onChange={handleInputChange}
required
/>
</>
)}
<input
type="email"
placeholder="Email Address"
name="email"
value={formData.email}
onChange={handleInputChange}
requir
/>
<input
type="password"
placeholder="Enter your Password"
name="password"
value={formData.password}
required
onChange={handleInputChange}
/>
<button>
{buttonText}
</button>
<p>
{linkDescription}
<Link
href={linkHref}
>
{linkText}
</Link>
</p>
</form>
);
}
This form will serve as the AuthForm
for both the registration and login pages.
Implementing Registration
To create the registration functionality for new users to register on the platform, you first need to create a Next API that the UI of the registration page will consume.
Creating the Registration API
Open the app
directory and create a new api
directory with an auth
subdirectory and inside of this auth
directory, create a register
directory with the file route.ts
with the content:
api/auth/register/route.ts
import { NextResponse } from 'next/server';
import { registerUser } from '@directus/sdk';
import directus from "@/lib/directus";
export async function POST(request: Request) {
try {
const { first_name, last_name, email, password } = await request.json();
const result = await directus.request(
registerUser({
first_name,
last_name,
email,
password
})
);
return NextResponse.json({ message: "Account Created!" }, { status: 201 });
} catch (e: any) {
console.log(e);
const code = e.errors[0].extensions.code
if (code === 'RECORD_NOT_UNIQUE') {
return NextResponse.json({ message: "This user already exist" }, { status: 409 });
}
return NextResponse.json({ message: "An unexpected error occurred, please try again" }, { status: 500 });
}
}
import { NextResponse } from 'next/server';
import { registerUser } from '@directus/sdk';
import directus from "@/lib/directus";
export async function POST(request: Request) {
try {
const { first_name, last_name, email, password } = await request.json();
const result = await directus.request(
registerUser({
first_name,
last_name,
email,
password
})
);
return NextResponse.json({ message: "Account Created!" }, { status: 201 });
} catch (e: any) {
console.log(e);
const code = e.errors[0].extensions.code
if (code === 'RECORD_NOT_UNIQUE') {
return NextResponse.json({ message: "This user already exist" }, { status: 409 });
}
return NextResponse.json({ message: "An unexpected error occurred, please try again" }, { status: 500 });
}
}
The code above does the following:
- Gets the
request
data that will be coming from the frontend{ first_name, last_name, email, password }
. - Uses the
directus
SDK to send a request to the backend server to register a new user. - Respond to the frontend of the application if the request was successful or not.
Creating the Registration UI
In the app
directory, create a new directory called register
; inside of this directory, create two new files, form.tsx
and page.tsx
.
The form.tsx
will contain the registration form and the page.tsx
will serve as the page rendered on the browser. Add the following content to form.tsx
:
app/register/form.tsx
'use client';
import AuthForm from '@/components/AuthForm';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
interface Data {
first_name?: string;
last_name?: string;
email?: string;
password?: string;
}
export default function RegistrationForm() {
const router = useRouter();
const [error, setError] = useState('');
const handleFormSubmit = async (data: Data) => {
const response = await fetch(`/api/auth/register`, {
method: 'POST',
body: JSON.stringify({
...data,
}),
});
if (response.status === 201) {
router.push('/');
router.refresh();
} else {
response.status === 409
? setError('A user with this email already exist')
: null;
}
};
return (
<>
{error && <p>{error}</p>}
<AuthForm
title="Register here"
onSubmit={handleFormSubmit}
buttonText="Register"
linkDescription="Already have an account?"
linkText="Login"
linkHref="/login"
/>
</>
);
}
'use client';
import AuthForm from '@/components/AuthForm';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
interface Data {
first_name?: string;
last_name?: string;
email?: string;
password?: string;
}
export default function RegistrationForm() {
const router = useRouter();
const [error, setError] = useState('');
const handleFormSubmit = async (data: Data) => {
const response = await fetch(`/api/auth/register`, {
method: 'POST',
body: JSON.stringify({
...data,
}),
});
if (response.status === 201) {
router.push('/');
router.refresh();
} else {
response.status === 409
? setError('A user with this email already exist')
: null;
}
};
return (
<>
{error && <p>{error}</p>}
<AuthForm
title="Register here"
onSubmit={handleFormSubmit}
buttonText="Register"
linkDescription="Already have an account?"
linkText="Login"
linkHref="/login"
/>
</>
);
}
The code above performs the following actions:
- Renders the
<AuthForm />
component with some customization as a registration form. - Gets the input values from the form and sends a
fetch
request to/api/auth/register
to register a new user. - If the request is successful, it should redirect the user to the login page or throw an error if the request failed
Next, add the following content to the page.tsx
to render the registration form:
app/register/page.tsx
import { getServerSession } from 'next-auth';
import { redirect } from 'next/navigation';
import RegistrationForm from './form';
export default async function RegisterPage() {
const session = await getServerSession();
if (session) {
redirect('/');
}
return (
<div>
<RegistrationForm />
</div>
);
}
import { getServerSession } from 'next-auth';
import { redirect } from 'next/navigation';
import RegistrationForm from './form';
export default async function RegisterPage() {
const session = await getServerSession();
if (session) {
redirect('/');
}
return (
<div>
<RegistrationForm />
</div>
);
}
This should provide you with a UI like this:
In code above:
- Renders the registration form created in the previous step
- Imports
getServerSession
fromnext-auth
to check if a user currently has a session - If a user already has a
session
ongoing, instead of rendering the registration form, it redirects them to the user dashboard(/
)
Go to your Directus project and confirm that a new user has been added to your list of users. Click on the Customer
tab on the left, and it will show you all registered customers on your application.
Implementing Login
With the registration page in place, let's implement the login functionality using the next-auth
package.
Creating the Login API
Head to api/auth
and create a new directory called [...nextauth]
; this directory will be used by the next-auth
package for all login logic for the application.
Inside of the [...nextauth]
, create a new file called options.ts
with the content:
[...nextauth]/options.ts
import type { NextAuthOptions, User } from 'next-auth';
import CredentialsProvider from 'next-auth/providers/credentials';
import { Session } from 'next-auth';
import { JWT } from 'next-auth/jwt';
import directus from '@/lib/directus';
interface CustomSession extends Session {
accessToken?: string;
refreshToken?: string;
}
export const options: NextAuthOptions = {
providers: [
CredentialsProvider({
name: 'Credentials',
credentials: {
email: {},
password: {},
},
async authorize(credentials) {
const email = JSON.stringify(credentials?.email);
const password = JSON.stringify(credentials?.password);
// Add logic here to look up the user from the credentials supplied
const user = await directus.login(email, password);
if (!user) {
throw new Error('Email address or password is invalid');
}
return user as any
},
}),
],
secret: process.env. NEXTAUTH_SECRET,
pages: {
signIn: '/login',
},
callbacks: {
async jwt({
token,
user,
account,
}: {
token: JWT;
user: any;
account: any;
}) {
if (account && user) {
return {
...token,
accessToken: user.access_token,
refreshToken: user.refresh_token,
};
}
return token;
},
async session({ session, token}: { session: CustomSession; token: any }) {
session.accessToken = token.accessToken;
session.refreshToken = token.refreshToken;
return session;
}
},
};
import type { NextAuthOptions, User } from 'next-auth';
import CredentialsProvider from 'next-auth/providers/credentials';
import { Session } from 'next-auth';
import { JWT } from 'next-auth/jwt';
import directus from '@/lib/directus';
interface CustomSession extends Session {
accessToken?: string;
refreshToken?: string;
}
export const options: NextAuthOptions = {
providers: [
CredentialsProvider({
name: 'Credentials',
credentials: {
email: {},
password: {},
},
async authorize(credentials) {
const email = JSON.stringify(credentials?.email);
const password = JSON.stringify(credentials?.password);
// Add logic here to look up the user from the credentials supplied
const user = await directus.login(email, password);
if (!user) {
throw new Error('Email address or password is invalid');
}
return user as any
},
}),
],
secret: process.env. NEXTAUTH_SECRET,
pages: {
signIn: '/login',
},
callbacks: {
async jwt({
token,
user,
account,
}: {
token: JWT;
user: any;
account: any;
}) {
if (account && user) {
return {
...token,
accessToken: user.access_token,
refreshToken: user.refresh_token,
};
}
return token;
},
async session({ session, token}: { session: CustomSession; token: any }) {
session.accessToken = token.accessToken;
session.refreshToken = token.refreshToken;
return session;
}
},
};
Let's break down the options
object for better understanding:
- The
authorize
function performs an async request to the backend auth URLyour-directus-project-url/auth/login
to log in the user using the credentials provided by the request. If a user is found in the database, it returns the user data; otherwise, it throws an error. - The
secret
field providesnext-auth
a secret for signing theJWT
tokens that will be generated when a user is authenticated. - By default,
next-auth
provides its own auth pages for handling authentication; thepages
field can be used to customizenext-auth
to use custom pages provided.(In this application, thesignIn
page is thelogin
page) callbacks
innext-auth
are functions after successful authentication. The code above has two callback functions:- When a user is authenticated by Directus, the Directus API returns an
access_token
and arefresh_token
. Whenevernext-auth
generates itsJWT
token for an authenticated user, theasync jwt
function attaches theaccess_token
andrefresh_token
to theJWT
token generated. The function also fetches theuserData
from Directus using theaccess_token
and attaches the data to theJWT
token. - In NextAuth.js, a session represents the state of authentication for a user; this includes the user details such as
id
,email
etc. Theasync session
function attaches custom fields to thesession.user
object to contain anid
,first_name
, andlast_name
as well as theaccessToken
andrefreshToken
gotten from the Directus.
- When a user is authenticated by Directus, the Directus API returns an
Login logic to authenticate a user and also store its details in a session
is now implemented. You can now use this session
data to check if a user is authenticated or not and whether they have the authorization to view a page or carry out a specific action.
To use this options
object you created, open the route.ts
file and update its content:
[...nextauth]/route.ts
import NextAuth from 'next-auth';
import { options } from './options';
const handler = NextAuth(options);
export { handler as GET, handler as POST };
import NextAuth from 'next-auth';
import { options } from './options';
const handler = NextAuth(options);
export { handler as GET, handler as POST };
Creating the Login UI
In the app
directory, create a new directory called login
; inside of this directory, create two new files, form.tsx
and page.tsx
.
The form.tsx
will contain the login form and the page.tsx
will serve as the page rendered on the browser. Add the following content to form.tsx
:
app/login/form.tsx
'use client';
import Link from 'next/link';
import { signIn } from 'next-auth/react';
import { useRouter } from 'next/navigation';
import AuthForm from '@/components/AuthForm';
import { useState } from 'react';
interface Data {
email?: string;
password?: string;
}
export default function LoginForm() {
const router = useRouter();
const [error, setError] = useState('');
const handleFormSubmit = async (data: Data) => {
const response = await signIn('credentials', {
email: data.email,
password: data.password,
redirect: false,
});
if (!response?.error) {
router.push('/');
router.refresh();
} else {
response.status === 401
? setError('Your email or password is incorrect')
: null;
}
};
return (
<>
{error && <p>{error}</p>}
<AuthForm
title="Login here"
onSubmit={handleFormSubmit}
buttonText="Login"
linkDescription="New here?"
linkText="Create an account"
linkHref="/register"
isFullForm={false}
/>
<div>
<Link
href="/request-reset-password"
>
Forgot password?
</Link>
</div>
</>
);
}
'use client';
import Link from 'next/link';
import { signIn } from 'next-auth/react';
import { useRouter } from 'next/navigation';
import AuthForm from '@/components/AuthForm';
import { useState } from 'react';
interface Data {
email?: string;
password?: string;
}
export default function LoginForm() {
const router = useRouter();
const [error, setError] = useState('');
const handleFormSubmit = async (data: Data) => {
const response = await signIn('credentials', {
email: data.email,
password: data.password,
redirect: false,
});
if (!response?.error) {
router.push('/');
router.refresh();
} else {
response.status === 401
? setError('Your email or password is incorrect')
: null;
}
};
return (
<>
{error && <p>{error}</p>}
<AuthForm
title="Login here"
onSubmit={handleFormSubmit}
buttonText="Login"
linkDescription="New here?"
linkText="Create an account"
linkHref="/register"
isFullForm={false}
/>
<div>
<Link
href="/request-reset-password"
>
Forgot password?
</Link>
</div>
</>
);
}
The above code:
- Renders the
<AuthForm />
component with some customization as a login form. - Gets the input values from the form and uses the
signIn
method fromnext-auth
to authenticate the user. - If the request is successful, it should redirect the user to their dashboard or throw an error if the request failed
In your page.tsx
, update its content to:
app/login/page.tsx
import LoginForm from "./form"
export default async function LoginPage() {
return (
<div>
<LoginForm />
</div>
)
}
import LoginForm from "./form"
export default async function LoginPage() {
return (
<div>
<LoginForm />
</div>
)
}
Test your log in page. When a user logs in, the application will navigate to dashboard page (/
).
Protecting Private Routes
In a typical application, you'd only want authenticated users to be able to access private routes/pages such as /dashboards
and user profile
pages.
To do this in next-auth
, create a new file called middleware.ts
with the content:
export { default } from "next-auth/middleware"
export const config = { matcher: ["/"] }
export { default } from "next-auth/middleware"
export const config = { matcher: ["/"] }
This file will ensure any URL in the matcher
array will be protected from unauthenticated users.
Implementing Forgot Password
To implement a forgot password functionality in your Next.js application, create a new directory in the app
directory called request-reset-password
with two files, form.tsx
and page.tsx
.
Update the form.tsx
to the following:
app/request-reset-password/form.tsx
'use client';
import Link from 'next/link';
import { FormEvent, useState } from 'react';
import { passwordRequest } from '@directus/sdk';
import directus from '@/lib/directus';
export default function RequestResetPasswordForm() {
const [email, setEmail] = useState('');
const [success, setSuccess] = useState('');
const [error, setError] = useState('');
const reset_url = `${process.env.NEXT_PUBLIC_URL}/reset-password`;
const handleFormSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
try {
const response = await directus.request(
passwordRequest(email, reset_url)
);
setSuccess(
'An email with a password reset link has been sent to your email!'
);
} catch (e: any) {
console.log(e);
if (e) {
setError('An error occurred, please try again!');
}
}
};
return (
<form onSubmit={handleFormSubmit}>
<h1>Reset your password</h1>
{success && <p>{success}</p>}
{error && <p>{error}</p>}
<p>
Enter your registered email and a reset password link will be sent to
you
</p>
<input
type="email"
placeholder="Email Address"
name="email"
required
onChange={(e) => setEmail(e.target.value)}
/>
<button>Send Reset Link</button>
<Link href="/login">Login page</Link>
</form>
);
}
'use client';
import Link from 'next/link';
import { FormEvent, useState } from 'react';
import { passwordRequest } from '@directus/sdk';
import directus from '@/lib/directus';
export default function RequestResetPasswordForm() {
const [email, setEmail] = useState('');
const [success, setSuccess] = useState('');
const [error, setError] = useState('');
const reset_url = `${process.env.NEXT_PUBLIC_URL}/reset-password`;
const handleFormSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
try {
const response = await directus.request(
passwordRequest(email, reset_url)
);
setSuccess(
'An email with a password reset link has been sent to your email!'
);
} catch (e: any) {
console.log(e);
if (e) {
setError('An error occurred, please try again!');
}
}
};
return (
<form onSubmit={handleFormSubmit}>
<h1>Reset your password</h1>
{success && <p>{success}</p>}
{error && <p>{error}</p>}
<p>
Enter your registered email and a reset password link will be sent to
you
</p>
<input
type="email"
placeholder="Email Address"
name="email"
required
onChange={(e) => setEmail(e.target.value)}
/>
<button>Send Reset Link</button>
<Link href="/login">Login page</Link>
</form>
);
}
The above code:
- Renders a form to collect the email input from the user.
- Fires a request using the Directus SDK to the Directus backend to reset the user password with an
email
andreset_url
as request parameters. - If the request is successful or failed, it should display a success or error message on the screen
In your page.tsx
, update its content to:
import RequestResetPasswordForm from "./form"
export default async function RequestPasswordResetPage() {
return (
<div>
<RequestResetPasswordForm />
</div>
)
}
import RequestResetPasswordForm from "./form"
export default async function RequestPasswordResetPage() {
return (
<div>
<RequestResetPasswordForm />
</div>
)
}
Filling out the reset password form and clicking on the reset button will trigger Directus to send a reset email with a link and a token to the user using the email configurations in your Directus setup configurations.
Resetting Passwords
Now that the password reset request is successful, let's create a page where users can reset their password with the url
they receive in their emails.
Create a new directory in the app
directory called reset-password
with two files, form.tsx
and page.tsx
. In the form.tsx
add the following:
app/reset-password/form.tsx
'use client';
import { FormEvent, useState } from 'react';
import { passwordReset } from '@directus/sdk';
import directus from '@/lib/directus';
import { useRouter } from 'next/navigation';
export default function RequestResetForm({ token }: { token: string }) {
const [newPassword, setNewPassword] = useState('');
const [success, setSuccess] = useState('');
const [error, setError] = useState('');
const reset_token = token;
const router = useRouter();
const handleFormSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
try {
const response = await directus.request(
passwordReset(reset_token, newPassword)
);
setSuccess(
'Password successfully reset, redirecting you to login page...'
);
setTimeout(() => router.push('/login'), 1000);
} catch (e: any) {
console.log(e);
setError(
'The reset password token is invalid, please request for a new password reset link!'
);
}
};
return (
<form onSubmit={handleFormSubmit}>
<h1>Provide a new password for your account</h1>
{success && <p>{success}</p>}
{error && <p>{error}</p>}
<p>Enter your new password for your account</p>
<input
type="password"
placeholder="Enter your new password"
name="password"
required
onChange={(e) => setNewPassword(e.target.value)}
autoComplete="new-password"
/>
<button>Create new password</button>
</form>
);
}
'use client';
import { FormEvent, useState } from 'react';
import { passwordReset } from '@directus/sdk';
import directus from '@/lib/directus';
import { useRouter } from 'next/navigation';
export default function RequestResetForm({ token }: { token: string }) {
const [newPassword, setNewPassword] = useState('');
const [success, setSuccess] = useState('');
const [error, setError] = useState('');
const reset_token = token;
const router = useRouter();
const handleFormSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
try {
const response = await directus.request(
passwordReset(reset_token, newPassword)
);
setSuccess(
'Password successfully reset, redirecting you to login page...'
);
setTimeout(() => router.push('/login'), 1000);
} catch (e: any) {
console.log(e);
setError(
'The reset password token is invalid, please request for a new password reset link!'
);
}
};
return (
<form onSubmit={handleFormSubmit}>
<h1>Provide a new password for your account</h1>
{success && <p>{success}</p>}
{error && <p>{error}</p>}
<p>Enter your new password for your account</p>
<input
type="password"
placeholder="Enter your new password"
name="password"
required
onChange={(e) => setNewPassword(e.target.value)}
autoComplete="new-password"
/>
<button>Create new password</button>
</form>
);
}
- The
reset-password/form.tsx
accepts a token and sends a request to Directus using the Directus SDK with thetoken
andnewPassword
as parameters for changing the user's password. - If this request is successful, it redirects the user to the login page to log in with their new password.
Inside of the page.tsx
, update the content to be:
import { redirect } from 'next/navigation';
import ResetPasswordForm from './form';
export default async function ResetPasswordPage({
searchParams,
}: {
searchParams: { token: string };
}) {
const { token } = searchParams;
if (!token) redirect('/login');
return (
<div>
<ResetPasswordForm token={token} />
</div>
);
}
import { redirect } from 'next/navigation';
import ResetPasswordForm from './form';
export default async function ResetPasswordPage({
searchParams,
}: {
searchParams: { token: string };
}) {
const { token } = searchParams;
if (!token) redirect('/login');
return (
<div>
<ResetPasswordForm token={token} />
</div>
);
}
The page.tsx
components checks if a token is present in the reset-password
url; if it is present, it displays the ResetPasswordPage
. Otherwise, it redirects the user to the login page.
Summary
In this article, you've successfully built an authentication system with password reset functionality using Next.js
, NextAuth.js
, and Directus
. This is just a glimpse of what you can implement with Directus Directus runs entirely as a backend service, meaning you can build complex backend services that will serve your frontend application with any database of your choice
Some possible steps you can consider to improve this application:
- Improving the functionality of the authentication system to accept OAuth providers like Google and Twitter.
- Improving error handling to show more descriptive errors to your users.
- Create new
Item
models that yourCustomers
can use to create their data from your frontend application. - The default Directus user comes with a list of default fields such as
first_name
,last_name
,password
,email
, and others. You can also extend thedirectus_users
schema to contain other fields to suit your needs.