UNPKG

payload-authjs

Version:
486 lines (485 loc) 18.1 kB
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