@payload-auth/better-auth-plugin
Version:
A Payload CMS plugin for Better Auth
1,080 lines • 64 kB
JavaScript
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