Payload CMS -Authentication in Nuxt Using a Custom Plugin

Clearly Innovative
5 min readOct 3, 2023

Overview

This is a companion blog post to support the video on a way to integrate Payload CMS SignIn, SignOut and Create Account in a Nuxt JS Application.

This video assumes that you already have a Payload CMS server running. See Part One of my Payload CMS video series to see the server that the Nuxt Application in this video is interacting with.

Payload CMS — The best way to build a modern backend + admin UI. No black magic, all TypeScript, and fully open-source, Payload is both an app framework and a headless CMS.

It includes the code for the necessary pages and the custom plugin for connecting with Payload CMS to get current user and manage the user information; and the middleware for controlling access to application pages

Video

Installation & Configuration

Create a new Nuxt application

npx nuxi@latest init <project-name>
cd /<project-name>
npm install

Add Nuxt/UI Module Documentation

Install module

npm install @nuxt/ui

Modify Nuxt config

export default defineNuxtConfig({
modules: ['@nuxt/ui']
})

The Code

I have included the main files that need to be modified to get the application running after you delete the App.vue file

The plugin

The main event here is the custom Nuxt Plugin, I have written a custom plugin in Nuxt and set it so that it will be the first one run by starting the name with 01.

The plugin checks for a user using the Payload CMS endpoint customers/me to check for a user; the user will be returned if one exists, otherwise null is returned.

The plugin then stores the user information and the authentication information in state variables using the Nuxt useState function.

There are also two additional functions added to the plugin

  • updateUser sets the current user information after a successful login
  • clearUsers clears the user information and the authentication information after the user logs out

As I stated in the video, refactoring the plugin to include the signIn and signOut functions will make these two functions unnecessary.

// nuxt-app/plugins/01.payload-auth.ts
import { Customer } from "~/payload-types";

export interface CurrentUserAuthInfo {
token: string;
exp: number;
}

export interface CurrentPayloadUserInfo extends CurrentUserAuthInfo {
user: Customer;
}

export default defineNuxtPlugin(async () => {
const currentUser = useState<Customer | null>("currentUser", () => null);
const userAuthInfo = useState<null | CurrentUserAuthInfo>("authInfo", () => {
return {
token: "",
exp: 0,
};
});

async function getUser() {
if (currentUser.value) {
return currentUser.value;
}
try {
const resp = await fetch("http://localhost:3100/api/customers/me", {
method: "GET",
credentials: "include",
headers: {
...useRequestHeaders(),
},
});

if (!resp.ok) {
const errorMsg = (await resp.json())?.errors[0].message;
throw new Error(errorMsg);
}
const userInfo = (await resp.json()) as CurrentPayloadUserInfo;
console.log(userInfo);
userAuthInfo.value = {
token: userInfo.token,
exp: userInfo.exp,
};
currentUser.value = userInfo?.user;
return userInfo?.user;
} catch (error: any) {
console.log("getUser - error", error);
currentUser.value = null;
return currentUser.value;
}
}

await getUser();
console.log("In Payload plugin", currentUser);

return {
provide: {
payloadAuth: {
currentUser,
userAuthInfo,
/**
* called to make sure we have the current user
* information set in the composable.
*/
updateUser: async () => {
await getUser();
},
/**
* clear user information from the composable
*/
clearUser: () => {
currentUser.value = null;
userAuthInfo.value = null;
},
},
},
};
});

The Middleware

The middleware auth.ts has a single purpose which is to redirect the user to the login page.

It works by accessing the plugin using the useNuxtApp hook to access the $payloadAuth plugin we discussed above.

// nuxt-app/middleware/auth.ts

import { defineNuxtRouteMiddleware } from "#app";

export default defineNuxtRouteMiddleware(async (to, from) => {
const { $payloadAuth } = useNuxtApp();
const user = $payloadAuth.currentUser?.value;
console.log('middleware user', user)
if (!user) {
// Redirect to login page
return navigateTo("/login");
}
});

The Login Page

The login page uses NuxtUI to make things look nice, but there is also some verification functionality provided by NuxtUI that we use to make sure we are provided an email and password value to use with the Payload CMS API call.

Important to notice how we access the plugin after a successful login to update the user information in the plugin with the information from the currently authenticated user by calling $payloadAuth.updateUser()

<template>
<UContainer class="mt-6">
<UCard class="m-4">
<template #header>
<h3>Login</h3>
</template>

<UForm
ref="loginInputForm"
:validate="validate"
:state="loginInput"
@submit.prevent="submit"
>
<UFormGroup label="Email" name="email">
<UInput v-model="loginInput.email" />
</UFormGroup>
<UFormGroup label="Password" name="password">
<UInput v-model="loginInput.password" type="password" />
</UFormGroup>
<UButton type="submit" class="mt-8"> Submit </UButton>
</UForm>

<template #footer />
</UCard>
</UContainer>
</template>
<script setup lang="ts">
import { FormError } from "@nuxt/ui/dist/runtime/types/form";
import { ref } from "vue";

const {$payloadAuth} = useNuxtApp();

type LoginInput = {
email: string;
password: string;
};
const loginInputForm = ref();
const loginInput = ref<LoginInput>({
email: "",
password: "",
});

/**
* validate form information
*
* @param state
*/
const validate = (state: LoginInput): FormError[] => {
const errors = [];
if (!state.email) errors.push({ path: "email", message: "Required" });
if (!state.password) errors.push({ path: "password", message: "Required" });

return errors;
};

/**
*
*/
async function submit() {
try {
const resp = await fetch("http://localhost:3100/api/customers/login", {
method: "POST",
credentials: "include",
headers: {
"Content-Type": "application/json",
...useRequestHeaders()
},
body: JSON.stringify({
email: loginInput.value.email,
password: loginInput.value.password,
}),
});

if (!resp.ok) {
const errorMsg = (await resp.json())?.errors[0].message;
throw new Error(errorMsg);
}
const user = await resp.json();
console.log(user);

// set user globally
await $payloadAuth.updateUser()

// goto home
await navigateTo("/");
} catch (error: any) {
alert("Sign In Error " + error.message);
}
}
</script>

The Index/Home Page

The home page is really here to show the information from the current user. We get that information from the $payloadAuth plugin we created.

We have the logOut function that calls the Payload CMS API and then after the logout is completed we use the plugin again to clear out any user information, $payloadAuth.clearUser()

<template>
<UContainer class="mt-6">
HELLO
<p>{{ $payloadAuth.currentUser }}</p>
<UButton @click="handleLogout">SIGN OUT</UButton>
</UContainer>
</template>
<script setup lang="ts">
definePageMeta({
middleware: ["auth"],
alias: ["/", "/index"],
});
const {$payloadAuth} = useNuxtApp();

/**
*
*/
async function handleLogout() {

try {
const resp = await fetch("http://localhost:3100/api/customers/logout", {
method: "POST",
credentials: "include",
headers: {
"Content-Type": "application/json",
},
});

if (!resp.ok) {
const errorMsg = (await resp.json())?.errors[0].message;
throw new Error(errorMsg);
}

// clear user
$payloadAuth.clearUser()

// redirect
navigateTo("/login")

} catch (error: any) {
alert("Sign Out Error " + error.message);
}
}
</script>

Originally published at https://dev.to on October 3, 2023.

--

--

Clearly Innovative

DC based software agency utilizing #Javascript, #HTML5, #Ionicframework, #VueJS , #ReactJS to build solutions. https://www.youtube.com/@AaronSaundersCI