Collection List
Used in this guide:Next.js 15.3.4
Payload CMS
Helper functions:
import { FieldHook } from "payload";
export const populateName: FieldHook = async ({ data, value }) => {
if (!value && data?.email) {
return data.email.split("@")[0];
}
return value;
}export function getRootUrl() {
const rootUrl = process.env.NODE_ENV === "development"
? process.env.NEXT_PUBLIC_ROOT_URL_DEV
: process.env.NEXT_PUBLIC_ROOT_URL_PROD;
return rootUrl;
}Access Control Helpers:
import { PayloadRequest } from "payload";
export const isAdminUser = (req: PayloadRequest) => {
if (req.user?.collection === "users" && req.user?.role === "admin") {
return true;
} else {
return false;
}
}
export const isStaffUser = (req: PayloadRequest) => {
if (
req.user?.collection === "users" &&
(req.user?.role === "admin" || req.user?.role === "editor")
) {
return true;
} else {
return false;
}
}
export const isAdminAndSelf = (req: PayloadRequest) => {
if (req.user?.collection === "users" && req.user?.role === "admin") {
return true;
}
if (req.user && req.user.collection === "users") {
return {
id: {
equals: req.user.id,
}
}
}
else {
return false;
}
}
export const isAdminAndSelfProfile = (req: PayloadRequest) => {
// admins: full access
if (req.user?.collection === "users" && req.user?.role === "admin") return true;
// logged-in user: restrict to their own profile doc
if (req.user?.collection === "users") {
const p = req.user.profile as unknown;
const profileId =
typeof p === "object" && p !== null && "id" in p
? // populated (depth >= 1)
(p as { id: string | number }).id
: // unpopulated (just an ID)
(p as string | number);
return { id: { equals: profileId } };
}
return false;
};Users Collection
import { isAdminAndSelf, isAdminUser } from "@/lib/payload/accessControl";
import { populateName } from "@/lib/payload/utility";
import { getRootUrl } from "@/lib/utility";
import { CollectionAfterChangeHook, CollectionBeforeValidateHook, CollectionConfig, FieldHook } from "payload";
const autoLinkProfile: FieldHook = async ({
operation, value, previousValue, siblingData, req,
}) => {
if (operation !== "create" && operation !== "update") return value ?? previousValue;
if (value ?? previousValue) return value ?? previousValue;
if (!siblingData?.email) {
throw new Error("Cannot create profile: user email is missing");
}
try {
const profile = await req.payload.create({
collection: "profiles",
data: {
email: siblingData.email,
}
});
return profile.id;
} catch (error) {
req.payload.logger.error(`Failed to create profile for ${siblingData.email}:
${(error as Error).message}`)
return previousValue ?? value ?? null;
}
}
const attachProfileOnSignup: CollectionBeforeValidateHook = async ({
operation, data, req
}) => {
if (operation !== "create") return data;
if (!data) {
throw new Error("No data provided");
}
const email = data?.email;
const firstName = data?.firstName;
const lastName = data?.lastName;
if (!email) return data;
const existing = await req.payload.find({
collection: "profiles",
where: { email: { equals: email }},
limit: 1,
});
let profileId: string | number;
if (existing.docs.length) {
profileId = existing.docs[0].id;
await req.payload.update({
collection: "profiles",
id: profileId,
data: {
firstName: firstName,
lastName: lastName,
}
})
} else {
const created = await req.payload.create({
collection: "profiles",
data: {
email: email,
firstName: firstName ?? data?.name,
lastName: lastName,
}
});
profileId = created.id;
}
delete (data as Record<string, unknown>).firstName;
delete (data as Record<string, unknown>).lastName;
const next = { ...data, profile: profileId };
return next;
}
const autoUpdateProfile: CollectionAfterChangeHook = async ({
operation, doc, previousDoc, req
}) => {
if (operation !== "update") return;
if (!doc.profile) return;
if (doc.email === previousDoc.email) return;
const profileId = typeof doc.profile === "object"
? doc.profile.id
: doc.profile;
await req.payload.update({
collection: "profiles",
id: profileId,
data: {
email: doc.email,
}
})
}
const Users: CollectionConfig = {
slug: "users",
auth: {
depth: 1,
tokenExpiration: 604800,
maxLoginAttempts: 5,
lockTime: 600,
verify: {
generateEmailHTML: ({ token, user }) => {
const userName = user?.name ?? user?.email ?? "friend";
const verificationURL = getRootUrl() + `/verify?token=${token}`;
return `Hi ${userName}, please verify your email by clicking here: <a href="${verificationURL}">${verificationURL}</a>`;
},
generateEmailSubject: ({ user }) => {
const userName = user?.name ?? "friend";
return `Hi ${userName}, please verify your email for mysite.com`;
},
},
forgotPassword: {
generateEmailHTML: (args) => {
const token = args?.token ?? "";
const userName = args?.user?.name ?? args?.user?.email ?? "friend";
const resetPasswordURL = getRootUrl() + `/reset-password?token=${token}`;
return `
<div>
<h1>Reset your password</h1>
<p>Hi, ${userName}</p>
<p>Click the link below to reset your password.</p>
<p>
<a href="${resetPasswordURL}">Reset Password</a>
<p>
</div>
`
},
generateEmailSubject: (args) => {
const userName = args?.user?.name ?? args?.user?.email ?? "friend";
return `Hi ${userName}, reset your password for mysite.com`
}
}
},
admin: {
useAsTitle: "name"
},
access: {
// create: ({ req }) => isAdminUser(req),
create: ({}) => true,
read: ({ req }) => isAdminAndSelf(req),
update: ({ req }) => isAdminAndSelf(req),
delete: ({ req }) => isAdminUser(req),
},
hooks: {
afterChange: [autoUpdateProfile],
beforeValidate: [attachProfileOnSignup],
},
fields: [
{
name: "name",
type: "text",
required: false,
hooks: {
afterRead: [populateName],
beforeChange: [populateName],
}
},
{
name: "role",
type: "select",
options: [
"admin",
"editor",
"web-user"
],
defaultValue: "web-user",
},
{
name: "profile",
type: "relationship",
relationTo: "profiles",
hasMany: false,
admin: {
readOnly: true,
},
hooks: {
beforeChange: [autoLinkProfile],
// afterRead: [autoBackFillProfile],
},
access: {
create: () => false,
update: () => false,
}
}
]
}
export default Users;