UNPKG

@payload-auth/better-auth-plugin

Version:

A Payload CMS plugin for Better Auth

1,080 lines 64 kB
import { baseCollectionSlugs, betterAuthPluginSlugs } from './config.js'; import { betterAuthStrategy } from './auth-strategy.js'; /** * Builds the required collections based on the BetterAuth options and plugins */ export function buildCollectionConfigs({ incomingCollections, requiredCollectionSlugs, pluginOptions, sanitizedBAOptions }) { const userSlug = pluginOptions.users?.slug ?? baseCollectionSlugs.users; const accountSlug = pluginOptions.accounts?.slug ?? baseCollectionSlugs.accounts; const sessionSlug = pluginOptions.sessions?.slug ?? baseCollectionSlugs.sessions; const verificationSlug = pluginOptions.verifications?.slug ?? baseCollectionSlugs.verifications; const baPlugins = sanitizedBAOptions.plugins ?? null; const enhancedCollections = []; requiredCollectionSlugs.forEach((slug)=>{ switch(slug){ case baseCollectionSlugs.users: const existingUserCollection = incomingCollections.find((collection)=>collection.slug === userSlug); const usersCollection = { ...existingUserCollection, slug: userSlug, admin: { ...existingUserCollection?.admin, defaultColumns: [ 'email' ], useAsTitle: 'email', hidden: pluginOptions.users?.hidden ?? false, components: {} }, auth: { ...existingUserCollection && typeof existingUserCollection.auth === 'object' ? existingUserCollection.auth : {}, disableLocalStrategy: true, strategies: [ betterAuthStrategy(pluginOptions.users?.adminRoles ?? [ 'admin' ], userSlug) ] }, fields: [ ...existingUserCollection?.fields ?? [], { name: 'betterAuthAdminButtons', type: 'ui', admin: { components: { Field: { path: '@payload-auth/better-auth-plugin/client#AdminButtons', clientProps: ()=>{ return { userSlug }; } } }, condition: ()=>{ // Only show the impersonate button if the admin plugin is enabled return (baPlugins && baPlugins.some((plugin)=>plugin.id === 'admin')) ?? false; } } }, { name: 'name', type: 'text', label: 'Name', admin: { description: 'Users chosen display name' } }, { name: 'email', type: 'text', required: true, unique: true, index: true, label: 'Email', admin: { description: 'The email of the user' } }, { name: 'emailVerified', type: 'checkbox', required: true, defaultValue: false, label: 'Email Verified', admin: { description: 'Whether the email of the user has been verified' } }, { name: 'image', type: 'text', label: 'Image', admin: { description: 'The image of the user' } }, { name: 'role', type: 'select', required: true, defaultValue: 'user', options: [ ...(pluginOptions.users?.roles ?? [ { label: 'Admin', value: 'admin' }, { label: 'User', value: 'user' } ]).map((role)=>{ if (typeof role === 'string') { return { label: role.charAt(0).toUpperCase() + role.slice(1), value: role }; } return role; }) ], label: 'Role', admin: { description: 'The role of the user' } } ], timestamps: true }; if (baPlugins) { baPlugins.forEach((plugin)=>{ switch(plugin.id){ case 'two-factor': usersCollection.fields.push({ name: 'twoFactorEnabled', type: 'checkbox', defaultValue: false, label: 'Two Factor Enabled', admin: { description: 'Whether the user has two factor authentication enabled' } }); break; case 'username': usersCollection.fields.push({ name: 'username', type: 'text', unique: true, required: false, label: 'Username', admin: { description: 'The username of the user' } }, { name: 'displayUsername', type: 'text', required: true, label: 'Display Username', admin: { description: 'The display username of the user' } }); break; case 'anonymous': usersCollection.fields.push({ name: 'isAnonymous', type: 'checkbox', defaultValue: false, label: 'Is Anonymous', admin: { description: 'Whether the user is anonymous.' } }); break; case 'phone-number': usersCollection.fields.push({ name: 'phoneNumber', type: 'text', label: 'Phone Number', admin: { description: 'The phone number of the user' } }, { name: 'phoneNumberVerified', type: 'checkbox', defaultValue: false, label: 'Phone Number Verified', admin: { description: 'Whether the phone number of the user has been verified' } }); break; case 'admin': usersCollection.fields.push({ name: 'banned', type: 'checkbox', defaultValue: false, label: 'Banned', admin: { description: 'Whether the user is banned from the platform' } }, { name: 'banReason', type: 'text', label: 'Ban Reason', admin: { description: 'The reason for the ban' } }, { name: 'banExpires', type: 'date', label: 'Ban Expires', admin: { description: 'The date and time when the ban will expire' } }); break; case 'harmony-email': usersCollection.fields.push({ name: 'normalizedEmail', type: 'text', required: false, unique: true, admin: { readOnly: true, description: 'The normalized email of the user' } }); break; default: break; } }); } enhancedCollections.push(usersCollection); break; case baseCollectionSlugs.accounts: const existingAccountCollection = incomingCollections.find((collection)=>collection.slug === accountSlug); const accountCollection = { slug: accountSlug, admin: { ...existingAccountCollection?.admin, hidden: pluginOptions.accounts?.hidden, useAsTitle: 'accountId', description: 'Accounts are used to store user accounts for authentication providers' }, fields: [ ...existingAccountCollection?.fields ?? [], { name: 'user', type: 'relationship', relationTo: userSlug, required: true, index: true, label: 'User', admin: { readOnly: true, description: 'The user that the account belongs to' } }, { name: 'accountId', type: 'text', label: 'Account ID', required: true, index: true, admin: { readOnly: true, description: 'The id of the account as provided by the SSO or equal to userId for credential accounts' } }, { name: 'providerId', type: 'text', required: true, label: 'Provider ID', admin: { readOnly: true, description: 'The id of the provider as provided by the SSO' } }, { name: 'accessToken', type: 'text', label: 'Access Token', admin: { readOnly: true, description: 'The access token of the account. Returned by the provider' } }, { name: 'refreshToken', type: 'text', label: 'Refresh Token', admin: { readOnly: true, description: 'The refresh token of the account. Returned by the provider' } }, { name: 'accessTokenExpiresAt', type: 'date', label: 'Access Token Expires At', admin: { readOnly: true, description: 'The date and time when the access token will expire' } }, { name: 'refreshTokenExpiresAt', type: 'date', label: 'Refresh Token Expires At', admin: { readOnly: true, description: 'The date and time when the refresh token will expire' } }, { name: 'scope', type: 'text', label: 'Scope', admin: { readOnly: true, description: 'The scope of the account. Returned by the provider' } }, { name: 'idToken', type: 'text', label: 'ID Token', admin: { readOnly: true, description: 'The id token for the account. Returned by the provider' } }, { name: 'password', type: 'text', label: 'Password', admin: { readOnly: true, description: 'The hashed password of the account. Mainly used for email and password authentication' } } ], timestamps: true, ...existingAccountCollection }; enhancedCollections.push(accountCollection); break; case baseCollectionSlugs.sessions: const existingSessionCollection = incomingCollections.find((collection)=>collection.slug === sessionSlug); const sessionCollection = { slug: sessionSlug, admin: { ...existingSessionCollection?.admin, hidden: pluginOptions.sessions?.hidden, description: 'Sessions are active sessions for users. They are used to authenticate users with a session token' }, fields: [ ...existingSessionCollection?.fields ?? [], { name: 'user', type: 'relationship', relationTo: userSlug, required: true, index: true, admin: { readOnly: true, description: 'The user that the session belongs to' } }, { name: 'token', type: 'text', required: true, unique: true, index: true, label: 'Token', admin: { description: 'The unique session token', readOnly: true } }, { name: 'expiresAt', type: 'date', required: true, label: 'Expires At', admin: { description: 'The date and time when the session will expire', readOnly: true } }, { name: 'ipAddress', type: 'text', label: 'IP Address', admin: { description: 'The IP address of the device', readOnly: true } }, { name: 'userAgent', type: 'text', label: 'User Agent', admin: { description: 'The user agent information of the device', readOnly: true } } ], timestamps: true, ...existingSessionCollection }; if (baPlugins) { baPlugins.forEach((plugin)=>{ switch(plugin.id){ case 'admin': sessionCollection.fields.push({ name: 'impersonatedBy', type: 'relationship', relationTo: userSlug, required: false, label: 'Impersonated By', admin: { readOnly: true, description: 'The admin who is impersonating this session' } }); break; case 'organization': sessionCollection.fields.push({ name: 'activeOrganization', type: 'relationship', relationTo: betterAuthPluginSlugs.organizations, label: 'Active Organization', admin: { readOnly: true, description: 'The currently active organization for the session' } }); break; default: break; } }); } enhancedCollections.push(sessionCollection); break; case baseCollectionSlugs.verifications: const existingVerificationCollection = incomingCollections.find((collection)=>collection.slug === verificationSlug); const verificationCollection = { slug: verificationSlug, admin: { ...existingVerificationCollection?.admin, hidden: pluginOptions.verifications?.hidden, useAsTitle: 'identifier', description: 'Verifications are used to verify authentication requests' }, fields: [ ...existingVerificationCollection?.fields ?? [], { name: 'identifier', type: 'text', required: true, index: true, label: 'Identifier', admin: { description: 'The identifier of the verification request', readOnly: true } }, { name: 'value', type: 'text', required: true, label: 'Value', admin: { description: 'The value to be verified', readOnly: true } }, { name: 'expiresAt', type: 'date', required: true, label: 'Expires At', admin: { description: 'The date and time when the verification request will expire', readOnly: true } } ], timestamps: true, ...existingVerificationCollection }; enhancedCollections.push(verificationCollection); break; case betterAuthPluginSlugs.organizations: const organizationCollection = { slug: betterAuthPluginSlugs.organizations, admin: { hidden: pluginOptions.hidePluginCollections ?? false, useAsTitle: 'name', description: 'Organizations are groups of users that share access to certain resources.' }, fields: [ { name: 'name', type: 'text', required: true, label: 'Name', admin: { description: 'The name of the organization.' } }, { name: 'slug', type: 'text', unique: true, index: true, label: 'Slug', admin: { description: 'The slug of the organization.' } }, { name: 'logo', type: 'text', label: 'Logo', admin: { description: 'The logo of the organization.' } }, { name: 'metadata', type: 'json', label: 'Metadata', admin: { description: 'Additional metadata for the organization.' } } ], timestamps: true }; enhancedCollections.push(organizationCollection); break; case betterAuthPluginSlugs.members: const memberCollection = { slug: betterAuthPluginSlugs.members, admin: { hidden: pluginOptions.hidePluginCollections ?? false, useAsTitle: 'organization', description: 'Members of an organization.' }, fields: [ { name: 'organization', type: 'relationship', relationTo: betterAuthPluginSlugs.organizations, required: true, index: true, label: 'Organization', admin: { readOnly: true, description: 'The organization that the member belongs to.' } }, { name: 'user', type: 'relationship', relationTo: userSlug, required: true, index: true, label: 'User', admin: { readOnly: true, description: 'The user that is a member of the organization.' } }, { name: 'team', type: 'relationship', relationTo: betterAuthPluginSlugs.teams, required: false, label: 'Team', admin: { description: 'The team that the member belongs to.' } }, { name: 'role', type: 'text', required: true, defaultValue: 'member', label: 'Role', admin: { description: 'The role of the member in the organization.' } } ], timestamps: true }; enhancedCollections.push(memberCollection); break; case betterAuthPluginSlugs.invitations: const invitationCollection = { slug: betterAuthPluginSlugs.invitations, admin: { hidden: pluginOptions.hidePluginCollections ?? false, useAsTitle: 'email', description: 'Invitations to join an organization' }, fields: [ { name: 'email', type: 'text', required: true, index: true, label: 'Email', admin: { description: 'The email of the user being invited.', readOnly: true } }, { name: 'inviter', type: 'relationship', relationTo: userSlug, required: true, label: 'Inviter', admin: { description: 'The user who invited the user.', readOnly: true } }, { name: 'organization', type: 'relationship', relationTo: betterAuthPluginSlugs.organizations, required: true, index: true, label: 'Organization', admin: { description: 'The organization that the user is being invited to.', readOnly: true } }, { name: 'role', type: 'text', required: true, label: 'Role', admin: { description: 'The role of the user being invited.', readOnly: true } }, { name: 'status', type: 'text', required: true, defaultValue: 'pending', label: 'Status', admin: { description: 'The status of the invitation.', readOnly: true } }, { name: 'expiresAt', type: 'date', required: true, label: 'Expires At', admin: { description: 'The date and time when the invitation will expire.', readOnly: true } } ], timestamps: true }; enhancedCollections.push(invitationCollection); break; case betterAuthPluginSlugs.teams: const teamCollection = { slug: betterAuthPluginSlugs.teams, admin: { hidden: pluginOptions.hidePluginCollections ?? false, useAsTitle: 'name', description: 'Teams are groups of users that share access to certain resources.' }, fields: [ { name: 'name', type: 'text', required: true, label: 'Name', admin: { description: 'The name of the team.' } }, { name: 'organization', type: 'relationship', relationTo: betterAuthPluginSlugs.organizations, required: true, label: 'Organization', admin: { readOnly: true, description: 'The organization that the team belongs to.' } } ], timestamps: true }; enhancedCollections.push(teamCollection); break; case betterAuthPluginSlugs.jwks: const jwksCollection = { slug: betterAuthPluginSlugs.jwks, admin: { hidden: pluginOptions.hidePluginCollections ?? false, useAsTitle: 'publicKey', description: 'JWKS are used to verify the signature of the JWT token' }, fields: [ { name: 'publicKey', type: 'text', required: true, index: true, label: 'Public Key', admin: { description: 'The public part of the web key' } }, { name: 'privateKey', type: 'text', required: true, label: 'Private Key', admin: { description: 'The private part of the web key' } } ], timestamps: true }; enhancedCollections.push(jwksCollection); break; case betterAuthPluginSlugs.apiKeys: const apiKeyCollection = { slug: betterAuthPluginSlugs.apiKeys, admin: { hidden: pluginOptions.hidePluginCollections ?? false, useAsTitle: 'name', description: 'API keys are used to authenticate requests to the API.' }, fields: [ { name: 'name', type: 'text', label: 'Name', admin: { readOnly: true, description: 'The name of the API key.' } }, { name: 'start', type: 'text', label: 'Starting Characters', admin: { readOnly: true, description: 'The starting characters of the API key. Useful for showing the first few characters of the API key in the UI for the users to easily identify.' } }, { name: 'prefix', type: 'text', label: 'Prefix', admin: { readOnly: true, description: 'The API Key prefix. Stored as plain text.' } }, { name: 'key', type: 'text', required: true, label: 'API Key', admin: { readOnly: true, description: 'The hashed API key itself.' } }, { name: 'user', type: 'relationship', relationTo: userSlug, required: true, label: 'User', admin: { readOnly: true, description: 'The user associated with the API key.' } }, { name: 'refillInterval', type: 'number', label: 'Refill Interval', admin: { readOnly: true, description: 'The interval to refill the key in milliseconds.' } }, { name: 'refillAmount', type: 'number', label: 'Refill Amount', admin: { readOnly: true, description: 'The amount to refill the remaining count of the key.' } }, { name: 'lastRefillAt', type: 'date', label: 'Last Refill At', admin: { readOnly: true, description: 'The date and time when the key was last refilled.' } }, { name: 'enabled', type: 'checkbox', defaultValue: true, label: 'Enabled', admin: { readOnly: true, description: 'Whether the API key is enabled.' } }, { name: 'rateLimitEnabled', type: 'checkbox', defaultValue: true, label: 'Rate Limit Enabled', admin: { readOnly: true, description: 'Whether the API key has rate limiting enabled.' } }, { name: 'rateLimitTimeWindow', type: 'number', label: 'Rate Limit Time Window', admin: { readOnly: true, description: 'The time window in milliseconds for the rate limit.' } }, { name: 'rateLimitMax', type: 'number', label: 'The maximum number of requests allowed within the `rateLimitTimeWindow`.', admin: { readOnly: true, description: 'The maximum number of requests allowed within the rate limit time window.' } }, { name: 'requstCount', type: 'number', label: 'Request Count', required: true, admin: { readOnly: true, description: 'The number of requests made within the rate limit time window.' } }, { name: 'remaining', type: 'number', label: 'Remaining Requests', admin: { readOnly: true, description: 'The number of requests remaining.' } }, { name: 'lastRequest', type: 'date', label: 'Last Request At', admin: { readOnly: true, description: 'The date and time of the last request made to the key.' } }, { name: 'expiresAt', type: 'date', label: 'Expires At', admin: { readOnly: true, description: 'The date and time of when the API key will expire.' } }, { name: 'permissions', type: 'text', label: 'Permissions', admin: { readOnly: true, description: 'The permissions for the API key.' } }, { name: 'metadata', type: 'json', label: 'Metadata', admin: { readOnly: true, description: 'Any additional metadata you want to store with the key.' } } ], timestamps: true }; enhancedCollections.push(apiKeyCollection); break; case betterAuthPluginSlugs.twoFactors: const twoFactorCollection = { slug: betterAuthPluginSlugs.twoFactors, admin: { hidden: pluginOptions.hidePluginCollections ?? false, useAsTitle: 'secret', description: 'Two factor authentication secrets' }, fields: [ { name: 'user', type: 'relationship', relationTo: userSlug, required: true, label: 'User', admin: { readOnly: true, description: 'The user that the two factor authentication secret belongs to' } }, { name: 'secret', type: 'text', label: 'Secret', index: true, admin: { readOnly: true, description: 'The secret used to generate the TOTP code.' } }, { name: 'backupCodes', type: 'text', required: true, label: 'Backup Codes', admin: { readOnly: true, description: 'The backup codes used to recover access to the account if the user loses access to their phone or email' } } ], timestamps: true }; enhancedCollections.push(twoFactorCollection); break; case betterAuthPluginSlugs.oauthAccessTokens: const oauthAccessTokenCollection = { slug: betterAuthPluginSlugs.oauthAccessTokens, admin: { hidden: pluginOptions.hidePluginCollections ?? false, useAsTitle: 'accessToken', description: 'OAuth access tokens for custom OAuth clients' }, fields: [ { name: 'accessToken', type: 'text', required: true, index: true, label: 'Access Token', admin: { readOnly: true, description: 'Access token issued to the client' } }, { name: 'refreshToken', type: 'text', required: true, label: 'Refresh Token', admin: { readOnly: true, description: 'Refresh token issued to the client' } }, { name: 'accessTokenExpiresAt', type: 'date', required: true, label: 'Access Token Expires At', admin: { readOnly: true, description: 'Expiration date of the access token' } }, { name: 'refreshTokenExpiresAt', type: 'date', required: true, label: 'Refresh Token Expires At', admin: { readOnly: true, description: 'Expiration date of the refresh token' } }, { name: 'client', type: 'relationship', relationTo: betterAuthPluginSlugs.oauthApplications, required: true, label: 'Client', admin: { readOnly: true, description: 'OAuth application associated with the access token' } }, { name: 'user', type: 'relationship', relationTo: userSlug, required: true, label: 'User', admin: { readOnly: true, description: 'User associated with the access token' } }, { name: 'scopes', type: 'text', required: true, label: 'Scopes', admin: { description: 'Comma-separated list of scopes granted' } } ], timestamps: true }; enhancedCollections.push(oauthAccessTokenCollection); break; case betterAuthPluginSlugs.oauthApplications: const oauthApplicationCollection = { slug: betterAuthPluginSlugs.oauthApplications, admin: { hidden: pluginOptions.hidePluginCollections ?? false, useAsTitle: 'name', description: 'OAuth applications are custom OAuth clients' }, fields: [ { name: 'clientId', type: 'text', unique: true, index: true, required: tr