payload-authjs
Version:
A Payload CMS 3 plugin for Auth.js 5
486 lines (485 loc) • 18.1 kB
JavaScript
import crypto from "crypto";
import { getPayload } from "payload";
import { transformObject } from "./utils/transformObject";
/**
* Auth.js Database Adapter for Payload CMS
*
* @see https://authjs.dev/guides/creating-a-database-adapter
*/ export function PayloadAdapter({ payload, payloadConfig, userCollectionSlug = "users" }) {
// Get the Payload instance
if (!payload && payloadConfig) {
payload = getPayload({
config: payloadConfig
});
}
if (!payload) {
throw new Error("PayloadAdapter requires either a `payload` instance or a `payloadConfig` to be provided");
}
// Create a logger
const logger = (async ()=>(await payload).logger.child({
name: "payload-authjs (PayloadAdapter)"
}))();
return {
// #region User management
async createUser (user) {
(await logger).debug({
userId: user.id,
user
}, `Creating user '${user.id}'`);
let payloadUser;
if (!(await payload).collections[userCollectionSlug]?.config.auth.disableLocalStrategy && !user.password) {
// If the local strategy is enabled and the user does not have a password, bypass the password check
payloadUser = await createUserAndBypassPasswordCheck(payload, {
collection: userCollectionSlug,
data: user
});
} else {
payloadUser = await (await payload).create({
collection: userCollectionSlug,
data: user
});
}
return toAdapterUser(payloadUser);
},
async getUser (userId) {
(await logger).debug({
userId
}, `Getting user by id '${userId}'`);
const payloadUser = await (await payload).findByID({
collection: userCollectionSlug,
id: userId,
select: {
accounts: false,
sessions: false,
verificationTokens: false
},
disableErrors: true
});
return payloadUser ? toAdapterUser(payloadUser) : null;
},
async getUserByEmail (email) {
(await logger).debug({
email
}, `Getting user by email '${email}'`);
const payloadUser = (await (await payload).find({
collection: userCollectionSlug,
where: {
email: {
equals: email
}
},
select: {
accounts: false,
sessions: false,
verificationTokens: false
},
limit: 1
})).docs.at(0);
return payloadUser ? toAdapterUser(payloadUser) : null;
},
async getUserByAccount ({ provider, providerAccountId }) {
(await logger).debug({
provider,
providerAccountId
}, `Getting user by account '${providerAccountId}' of provider '${provider}'`);
const payloadUser = (await (await payload).find({
collection: userCollectionSlug,
where: {
"accounts.provider": {
equals: provider
},
"accounts.providerAccountId": {
equals: providerAccountId
}
},
select: {
accounts: false,
sessions: false,
verificationTokens: false
},
limit: 1
})).docs.at(0);
return payloadUser ? toAdapterUser(payloadUser) : null;
},
async updateUser (user) {
(await logger).debug({
userId: user.id,
user
}, `Updating user '${user.id}'`);
const payloadUser = await (await payload).update({
collection: userCollectionSlug,
id: user.id,
data: user,
select: {
accounts: false,
sessions: false,
verificationTokens: false
}
});
return payloadUser ? toAdapterUser(payloadUser) : null;
},
async deleteUser (userId) {
(await logger).debug({
userId
}, `Deleting user '${userId}'`);
await (await payload).delete({
collection: userCollectionSlug,
id: userId
});
},
async linkAccount (account) {
(await logger).debug({
userId: account.userId,
account
}, `Linking account for user '${account.userId}'`);
let payloadUser = await (await payload).findByID({
collection: userCollectionSlug,
id: account.userId,
select: {
id: true,
accounts: true
},
disableErrors: true
});
if (!payloadUser) {
throw new Error(`Failed to link account: User '${account.userId}' not found`);
}
payloadUser = await (await payload).update({
collection: userCollectionSlug,
id: payloadUser.id,
data: {
accounts: [
...payloadUser.accounts || [],
account
]
},
select: {
id: true,
accounts: true
}
});
const createdAccount = payloadUser.accounts?.find((a)=>a.provider === account.provider && a.providerAccountId === account.providerAccountId);
return createdAccount ? toAdapterAccount(createdAccount) : account;
},
async unlinkAccount ({ provider, providerAccountId }) {
(await logger).debug({
provider,
providerAccountId
}, `Unlinking account '${providerAccountId}' of provider '${provider}'`);
let payloadUser = (await (await payload).find({
collection: userCollectionSlug,
where: {
"accounts.provider": {
equals: provider
},
"accounts.providerAccountId": {
equals: providerAccountId
}
},
select: {
id: true,
accounts: true
},
limit: 1
})).docs.at(0);
if (!payloadUser) {
throw new Error(`Failed to unlink account: Account '${providerAccountId}' of provider '${provider}' not found`);
}
payloadUser = await (await payload).update({
collection: userCollectionSlug,
id: payloadUser.id,
data: {
accounts: payloadUser.accounts?.filter((account)=>!(account.provider === provider && account.providerAccountId === providerAccountId))
},
select: {
id: true
}
});
},
// #endregion
// #region Database session management
async createSession (session) {
(await logger).debug({
userId: session.userId,
session
}, `Creating session for user '${session.userId}'`);
let payloadUser = await (await payload).findByID({
collection: userCollectionSlug,
id: session.userId,
select: {
id: true,
sessions: true
},
disableErrors: true
});
if (!payloadUser) {
throw new Error(`Failed to create session: User '${session.userId}' not found`);
}
payloadUser = await (await payload).update({
collection: userCollectionSlug,
id: payloadUser.id,
data: {
sessions: [
...payloadUser.sessions || [],
session
]
},
select: {
id: true,
sessions: true
}
});
const createdSession = payloadUser.sessions?.find((s)=>s.sessionToken === session.sessionToken);
return createdSession ? toAdapterSession(payloadUser, createdSession) : session;
},
async getSessionAndUser (sessionToken) {
(await logger).debug({
sessionToken
}, `Getting session and user by session token '${sessionToken}'`);
const payloadUser = (await (await payload).find({
collection: userCollectionSlug,
where: {
"sessions.sessionToken": {
equals: sessionToken
}
},
select: {
accounts: false,
verificationTokens: false
},
limit: 1
})).docs.at(0);
if (!payloadUser) {
return null;
}
const session = payloadUser.sessions?.find((s)=>s.sessionToken === sessionToken);
if (!session) {
return null;
}
return {
user: toAdapterUser(payloadUser),
session: toAdapterSession(payloadUser, session)
};
},
async updateSession (session) {
(await logger).debug({
userId: session.userId,
session
}, `Updating session '${session.sessionToken}'`);
let payloadUser = (await (await payload).find({
collection: userCollectionSlug,
where: {
"sessions.sessionToken": {
equals: session.sessionToken
}
},
select: {
id: true,
sessions: true
},
limit: 1
})).docs.at(0);
if (!payloadUser) {
throw new Error(`Failed to update session: Session '${session.sessionToken}' not found`);
}
payloadUser = await (await payload).update({
collection: userCollectionSlug,
id: payloadUser.id,
data: {
sessions: payloadUser.sessions?.map((s)=>s.sessionToken === session.sessionToken ? session : s)
},
select: {
id: true,
sessions: true
}
});
const updatedSession = payloadUser.sessions?.find((s)=>s.sessionToken === session.sessionToken);
return updatedSession ? toAdapterSession(payloadUser, updatedSession) : null;
},
async deleteSession (sessionToken) {
(await logger).debug({
sessionToken
}, `Deleting session with token '${sessionToken}'`);
let payloadUser = (await (await payload).find({
collection: userCollectionSlug,
where: {
"sessions.sessionToken": {
equals: sessionToken
}
},
select: {
id: true,
sessions: true
},
limit: 1
})).docs.at(0);
if (!payloadUser) {
throw new Error(`Failed to delete session: Session '${sessionToken}' not found`);
}
payloadUser = await (await payload).update({
collection: userCollectionSlug,
id: payloadUser.id,
data: {
sessions: payloadUser.sessions?.filter((session)=>session.sessionToken !== sessionToken)
},
select: {
id: true
}
});
},
// #endregion
// #region Verification tokens
async createVerificationToken ({ identifier: email, ...token }) {
(await logger).debug({
email,
token
}, `Creating verification token for email '${email}'`);
let payloadUser = (await (await payload).find({
collection: userCollectionSlug,
where: {
email: {
equals: email
}
},
select: {
id: true,
verificationTokens: true
},
limit: 1
})).docs.at(0);
if (!payloadUser) {
const user = {
id: crypto.randomUUID(),
email,
verificationTokens: [
token
]
};
if (!(await payload).collections[userCollectionSlug]?.config.auth.disableLocalStrategy) {
// If the local strategy is enabled, bypass the password check
payloadUser = await createUserAndBypassPasswordCheck(payload, {
collection: userCollectionSlug,
data: user
});
} else {
payloadUser = await (await payload).create({
collection: userCollectionSlug,
data: user,
select: {
id: true,
email: true,
verificationTokens: true
}
});
}
} else {
payloadUser = await (await payload).update({
collection: userCollectionSlug,
id: payloadUser.id,
data: {
verificationTokens: [
...payloadUser.verificationTokens || [],
token
]
},
select: {
id: true,
email: true,
verificationTokens: true
}
});
}
const createdToken = payloadUser.verificationTokens?.find((t)=>t.token === token.token);
return createdToken ? toAdapterVerificationToken(payloadUser.email, createdToken) : {
identifier: email,
...token
};
},
async useVerificationToken ({ identifier: email, token }) {
(await logger).debug({
email,
token
}, `Using verification token for email '${email}'`);
let payloadUser = (await (await payload).find({
collection: userCollectionSlug,
where: {
email: {
equals: email
},
"verificationTokens.token": {
equals: token
}
},
select: {
id: true,
verificationTokens: true
},
limit: 1
})).docs.at(0);
if (!payloadUser) {
return null;
}
const verificationToken = payloadUser.verificationTokens?.find((t)=>t.token === token);
payloadUser = await (await payload).update({
collection: userCollectionSlug,
id: payloadUser.id,
data: {
verificationTokens: payloadUser.verificationTokens?.filter((t)=>t.token !== token)
},
select: {
id: true,
email: true
}
});
return verificationToken ? toAdapterVerificationToken(payloadUser.email, verificationToken) : null;
}
};
}
function toAdapterUser(user) {
return transformObject(user, [
"accounts",
"sessions",
"verificationTokens"
]);
}
function toAdapterAccount(account) {
return transformObject(account);
}
function toAdapterSession(user, session) {
return {
...transformObject(session),
userId: user.id
};
}
function toAdapterVerificationToken(email, token) {
return {
identifier: email,
...transformObject(token)
};
}
/**
* Create a user and bypass the password check
* This is because payload requires a password to be set when creating a user
*
* @see https://github.com/payloadcms/payload/blob/main/packages/payload/src/collections/operations/create.ts#L254
* @see https://github.com/payloadcms/payload/blob/main/packages/payload/src/auth/strategies/local/generatePasswordSaltHash.ts
*/ const createUserAndBypassPasswordCheck = async (payload, { collection, data })=>{
// Generate a random password
data.password = crypto.randomBytes(32).toString("hex");
// Create the user
const user = await (await payload).create({
collection,
data
});
// Remove the salt and hash after the user was created
await (await payload).update({
collection,
id: user.id,
data: {
salt: null,
hash: null
}
});
return user;
};
//# sourceMappingURL=PayloadAdapter.js.map