payload-better-auth 3.0.0 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/LogoutButtonClient.d.ts +9 -0
- package/dist/components/LogoutButtonClient.js +91 -0
- package/dist/components/LogoutButtonClient.js.map +1 -0
- package/dist/exports/client.d.ts +1 -0
- package/dist/exports/client.js +1 -0
- package/dist/exports/client.js.map +1 -1
- package/dist/payload/plugin.js +14 -0
- package/dist/payload/plugin.js.map +1 -1
- package/package.json +1 -1
- package/src/components/LogoutButtonClient.tsx +99 -0
- package/src/exports/client.ts +1 -0
- package/src/payload/plugin.ts +18 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { AuthClientOptions } from './BetterAuthLoginServer';
|
|
2
|
+
export interface LogoutButtonClientProps {
|
|
3
|
+
/**
|
|
4
|
+
* Auth client options for Better Auth sign-out.
|
|
5
|
+
* Uses the external (browser-accessible) URL.
|
|
6
|
+
*/
|
|
7
|
+
authClientOptions: AuthClientOptions;
|
|
8
|
+
}
|
|
9
|
+
export declare function LogoutButtonClient({ authClientOptions }: LogoutButtonClientProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { toast } from '@payloadcms/ui';
|
|
4
|
+
import { createAuthClient } from 'better-auth/react';
|
|
5
|
+
import { useRouter } from 'next/navigation.js';
|
|
6
|
+
import { useState } from 'react';
|
|
7
|
+
/**
|
|
8
|
+
* Simple logout icon SVG component
|
|
9
|
+
*/ function LogOutIcon() {
|
|
10
|
+
return /*#__PURE__*/ _jsxs("svg", {
|
|
11
|
+
fill: "none",
|
|
12
|
+
height: "20",
|
|
13
|
+
stroke: "currentColor",
|
|
14
|
+
strokeLinecap: "round",
|
|
15
|
+
strokeLinejoin: "round",
|
|
16
|
+
strokeWidth: "2",
|
|
17
|
+
viewBox: "0 0 24 24",
|
|
18
|
+
width: "20",
|
|
19
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
20
|
+
children: [
|
|
21
|
+
/*#__PURE__*/ _jsx("path", {
|
|
22
|
+
d: "M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"
|
|
23
|
+
}),
|
|
24
|
+
/*#__PURE__*/ _jsx("polyline", {
|
|
25
|
+
points: "16 17 21 12 16 7"
|
|
26
|
+
}),
|
|
27
|
+
/*#__PURE__*/ _jsx("line", {
|
|
28
|
+
x1: "21",
|
|
29
|
+
x2: "9",
|
|
30
|
+
y1: "12",
|
|
31
|
+
y2: "12"
|
|
32
|
+
})
|
|
33
|
+
]
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
export function LogoutButtonClient({ authClientOptions }) {
|
|
37
|
+
const authClient = createAuthClient(authClientOptions);
|
|
38
|
+
const router = useRouter();
|
|
39
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
40
|
+
const handleLogout = async ()=>{
|
|
41
|
+
if (isLoading) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
setIsLoading(true);
|
|
45
|
+
try {
|
|
46
|
+
// Sign out from Better Auth - this clears the session cookie
|
|
47
|
+
const result = await authClient.signOut();
|
|
48
|
+
if (result.error) {
|
|
49
|
+
toast.error(result.error.message || 'Logout failed');
|
|
50
|
+
setIsLoading(false);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
// Redirect to login page after successful sign-out
|
|
54
|
+
router.push('/admin/login');
|
|
55
|
+
router.refresh();
|
|
56
|
+
} catch (error) {
|
|
57
|
+
toast.error(error.message || 'Logout failed');
|
|
58
|
+
setIsLoading(false);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
return /*#__PURE__*/ _jsxs("button", {
|
|
62
|
+
"aria-label": "Log out",
|
|
63
|
+
disabled: isLoading,
|
|
64
|
+
onClick: handleLogout,
|
|
65
|
+
style: {
|
|
66
|
+
alignItems: 'center',
|
|
67
|
+
background: 'transparent',
|
|
68
|
+
border: 'none',
|
|
69
|
+
color: 'inherit',
|
|
70
|
+
cursor: isLoading ? 'wait' : 'pointer',
|
|
71
|
+
display: 'flex',
|
|
72
|
+
fontFamily: 'inherit',
|
|
73
|
+
fontSize: 'inherit',
|
|
74
|
+
gap: '0.5rem',
|
|
75
|
+
opacity: isLoading ? 0.6 : 1,
|
|
76
|
+
padding: '0.75rem 1rem',
|
|
77
|
+
textAlign: 'left',
|
|
78
|
+
transition: 'opacity 0.2s',
|
|
79
|
+
width: '100%'
|
|
80
|
+
},
|
|
81
|
+
type: "button",
|
|
82
|
+
children: [
|
|
83
|
+
/*#__PURE__*/ _jsx(LogOutIcon, {}),
|
|
84
|
+
/*#__PURE__*/ _jsx("span", {
|
|
85
|
+
children: isLoading ? 'Logging out...' : 'Logout'
|
|
86
|
+
})
|
|
87
|
+
]
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
//# sourceMappingURL=LogoutButtonClient.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/components/LogoutButtonClient.tsx"],"sourcesContent":["'use client'\n\nimport { toast } from '@payloadcms/ui'\nimport { createAuthClient } from 'better-auth/react'\nimport { useRouter } from 'next/navigation.js'\nimport { useState } from 'react'\n\nimport type { AuthClientOptions } from './BetterAuthLoginServer'\n\n/**\n * Simple logout icon SVG component\n */\nfunction LogOutIcon() {\n return (\n <svg\n fill=\"none\"\n height=\"20\"\n stroke=\"currentColor\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth=\"2\"\n viewBox=\"0 0 24 24\"\n width=\"20\"\n xmlns=\"http://www.w3.org/2000/svg\"\n >\n <path d=\"M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4\" />\n <polyline points=\"16 17 21 12 16 7\" />\n <line x1=\"21\" x2=\"9\" y1=\"12\" y2=\"12\" />\n </svg>\n )\n}\n\nexport interface LogoutButtonClientProps {\n /**\n * Auth client options for Better Auth sign-out.\n * Uses the external (browser-accessible) URL.\n */\n authClientOptions: AuthClientOptions\n}\n\nexport function LogoutButtonClient({ authClientOptions }: LogoutButtonClientProps) {\n const authClient = createAuthClient(authClientOptions)\n const router = useRouter()\n const [isLoading, setIsLoading] = useState(false)\n\n const handleLogout = async () => {\n if (isLoading) {\n return\n }\n\n setIsLoading(true)\n\n try {\n // Sign out from Better Auth - this clears the session cookie\n const result = await authClient.signOut()\n\n if (result.error) {\n toast.error(result.error.message || 'Logout failed')\n setIsLoading(false)\n return\n }\n\n // Redirect to login page after successful sign-out\n router.push('/admin/login')\n router.refresh()\n } catch (error) {\n toast.error((error as Error).message || 'Logout failed')\n setIsLoading(false)\n }\n }\n\n return (\n <button\n aria-label=\"Log out\"\n disabled={isLoading}\n onClick={handleLogout}\n style={{\n alignItems: 'center',\n background: 'transparent',\n border: 'none',\n color: 'inherit',\n cursor: isLoading ? 'wait' : 'pointer',\n display: 'flex',\n fontFamily: 'inherit',\n fontSize: 'inherit',\n gap: '0.5rem',\n opacity: isLoading ? 0.6 : 1,\n padding: '0.75rem 1rem',\n textAlign: 'left',\n transition: 'opacity 0.2s',\n width: '100%',\n }}\n type=\"button\"\n >\n <LogOutIcon />\n <span>{isLoading ? 'Logging out...' : 'Logout'}</span>\n </button>\n )\n}\n"],"names":["toast","createAuthClient","useRouter","useState","LogOutIcon","svg","fill","height","stroke","strokeLinecap","strokeLinejoin","strokeWidth","viewBox","width","xmlns","path","d","polyline","points","line","x1","x2","y1","y2","LogoutButtonClient","authClientOptions","authClient","router","isLoading","setIsLoading","handleLogout","result","signOut","error","message","push","refresh","button","aria-label","disabled","onClick","style","alignItems","background","border","color","cursor","display","fontFamily","fontSize","gap","opacity","padding","textAlign","transition","type","span"],"mappings":"AAAA;;AAEA,SAASA,KAAK,QAAQ,iBAAgB;AACtC,SAASC,gBAAgB,QAAQ,oBAAmB;AACpD,SAASC,SAAS,QAAQ,qBAAoB;AAC9C,SAASC,QAAQ,QAAQ,QAAO;AAIhC;;CAEC,GACD,SAASC;IACP,qBACE,MAACC;QACCC,MAAK;QACLC,QAAO;QACPC,QAAO;QACPC,eAAc;QACdC,gBAAe;QACfC,aAAY;QACZC,SAAQ;QACRC,OAAM;QACNC,OAAM;;0BAEN,KAACC;gBAAKC,GAAE;;0BACR,KAACC;gBAASC,QAAO;;0BACjB,KAACC;gBAAKC,IAAG;gBAAKC,IAAG;gBAAIC,IAAG;gBAAKC,IAAG;;;;AAGtC;AAUA,OAAO,SAASC,mBAAmB,EAAEC,iBAAiB,EAA2B;IAC/E,MAAMC,aAAazB,iBAAiBwB;IACpC,MAAME,SAASzB;IACf,MAAM,CAAC0B,WAAWC,aAAa,GAAG1B,SAAS;IAE3C,MAAM2B,eAAe;QACnB,IAAIF,WAAW;YACb;QACF;QAEAC,aAAa;QAEb,IAAI;YACF,6DAA6D;YAC7D,MAAME,SAAS,MAAML,WAAWM,OAAO;YAEvC,IAAID,OAAOE,KAAK,EAAE;gBAChBjC,MAAMiC,KAAK,CAACF,OAAOE,KAAK,CAACC,OAAO,IAAI;gBACpCL,aAAa;gBACb;YACF;YAEA,mDAAmD;YACnDF,OAAOQ,IAAI,CAAC;YACZR,OAAOS,OAAO;QAChB,EAAE,OAAOH,OAAO;YACdjC,MAAMiC,KAAK,CAAC,AAACA,MAAgBC,OAAO,IAAI;YACxCL,aAAa;QACf;IACF;IAEA,qBACE,MAACQ;QACCC,cAAW;QACXC,UAAUX;QACVY,SAASV;QACTW,OAAO;YACLC,YAAY;YACZC,YAAY;YACZC,QAAQ;YACRC,OAAO;YACPC,QAAQlB,YAAY,SAAS;YAC7BmB,SAAS;YACTC,YAAY;YACZC,UAAU;YACVC,KAAK;YACLC,SAASvB,YAAY,MAAM;YAC3BwB,SAAS;YACTC,WAAW;YACXC,YAAY;YACZzC,OAAO;QACT;QACA0C,MAAK;;0BAEL,KAACnD;0BACD,KAACoD;0BAAM5B,YAAY,mBAAmB;;;;AAG5C"}
|
package/dist/exports/client.d.ts
CHANGED
package/dist/exports/client.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/exports/client.ts"],"sourcesContent":["export { VerifyEmailInfoViewClient } from '../components/VerifyEmailInfoViewClient'\n"],"names":["VerifyEmailInfoViewClient"],"mappings":"AAAA,SAASA,yBAAyB,QAAQ,0CAAyC"}
|
|
1
|
+
{"version":3,"sources":["../../src/exports/client.ts"],"sourcesContent":["export { LogoutButtonClient } from '../components/LogoutButtonClient'\nexport { VerifyEmailInfoViewClient } from '../components/VerifyEmailInfoViewClient'\n"],"names":["LogoutButtonClient","VerifyEmailInfoViewClient"],"mappings":"AAAA,SAASA,kBAAkB,QAAQ,mCAAkC;AACrE,SAASC,yBAAyB,QAAQ,0CAAyC"}
|
package/dist/payload/plugin.js
CHANGED
|
@@ -113,6 +113,20 @@ export const betterAuthPayloadPlugin = (pluginOptions)=>(config)=>{
|
|
|
113
113
|
} else {
|
|
114
114
|
throw new Error('Payload-better-auth plugin: admin.components.views.verifyEmail property in config already set.');
|
|
115
115
|
}
|
|
116
|
+
// Configure custom logout button that signs out from Better Auth
|
|
117
|
+
if (!config.admin.components.logout) {
|
|
118
|
+
config.admin.components.logout = {};
|
|
119
|
+
}
|
|
120
|
+
if (!config.admin.components.logout.Button) {
|
|
121
|
+
config.admin.components.logout.Button = {
|
|
122
|
+
clientProps: {
|
|
123
|
+
authClientOptions: externalAuthClientOptions
|
|
124
|
+
},
|
|
125
|
+
path: 'payload-better-auth/client#LogoutButtonClient'
|
|
126
|
+
};
|
|
127
|
+
} else {
|
|
128
|
+
throw new Error('Payload-better-auth plugin: admin.components.logout.Button property in config already set.');
|
|
129
|
+
}
|
|
116
130
|
if (!config.admin.routes) {
|
|
117
131
|
config.admin.routes = {};
|
|
118
132
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/payload/plugin.ts"],"sourcesContent":["import type { ClientOptions } from 'better-auth'\nimport type { Access, CollectionConfig, Config } from 'payload'\n\nimport type { EventBus } from '../eventBus/types'\nimport type { SecondaryStorage } from '../storage/types'\n\nimport { createEmailPasswordCollection } from '../collections/BetterAuth/emailPassword'\nimport { createMagicLinkCollection } from '../collections/BetterAuth/magicLink'\nimport { extendUsersCollection } from '../collections/Users/index'\nimport { createDeduplicatedLogger } from '../shared/deduplicatedLogger'\nimport { TIMESTAMP_PREFIX } from '../storage/keys'\n\nexport type BetterAuthClientOptions = {\n /**\n * The external base URL for better-auth, used for client-side requests (from the browser).\n * This should be the publicly accessible URL.\n * @example 'https://auth.example.com'\n */\n externalBaseURL: string\n /**\n * The internal base URL for better-auth, used for server-side requests.\n * This is used when the server needs to reach better-auth internally (e.g., within a container network).\n * @example 'http://auth-service:3000'\n */\n internalBaseURL: string\n} & Omit<ClientOptions, 'baseURL'>\n\nexport type BetterAuthPayloadPluginOptions = {\n /**\n * Custom access rules for Better Auth collections (email_password, magic_link).\n * These override the default debug-mode access (which allows read for authenticated users).\n *\n * @example\n * baCollectionsAccess: {\n * read: ({ req }) => req.user?.role === 'admin',\n * delete: ({ req }) => req.user?.role === 'admin',\n * }\n */\n baCollectionsAccess?: {\n delete?: Access\n read?: Access\n }\n betterAuthClientOptions: BetterAuthClientOptions\n /**\n * Prefix for Better Auth collections (default: '__better_auth').\n * The collections will be named: {prefix}_email_password, {prefix}_magic_link\n */\n collectionPrefix?: string\n /**\n * Enable debug logging and make BA collections visible in admin.\n * When enabled:\n * - Detailed error information will be logged\n * - BA collections are visible under \"Better Auth (DEBUG)\" group\n * - Authenticated users can read BA collections (unless baCollectionsAccess overrides)\n */\n debug?: boolean\n disabled?: boolean\n /**\n * EventBus for timestamp-based coordination between plugins.\n * Both plugins MUST share the same eventBus instance.\n *\n * Available implementations:\n * - `createSqlitePollingEventBus()` - Uses SQLite for cross-process coordination\n *\n * @example\n * // Create shared eventBus (e.g., in a separate file)\n * import { createSqlitePollingEventBus } from 'payload-better-auth'\n * import { DatabaseSync } from 'node:sqlite'\n * const db = new DatabaseSync('.event-bus.db')\n * export const eventBus = createSqlitePollingEventBus({ db })\n */\n eventBus: EventBus\n /**\n * Secondary storage for state coordination between Better Auth and Payload.\n * Both plugins MUST share the same storage instance.\n *\n * Available storage adapters:\n * - `createSqliteStorage()` - Uses Node.js 22+ native SQLite (no external dependencies)\n * - `createRedisStorage(redis)` - Redis-backed, for distributed/multi-server production\n *\n * @example\n * // Development (Node.js 22+)\n * import { createSqliteStorage } from 'payload-better-auth'\n * import { DatabaseSync } from 'node:sqlite'\n * const db = new DatabaseSync('.sync-state.db')\n * const storage = createSqliteStorage({ db })\n *\n * @example\n * // Production (distributed)\n * import { createRedisStorage } from 'payload-better-auth'\n * import Redis from 'ioredis'\n * const storage = createRedisStorage({ redis: new Redis() })\n */\n storage: SecondaryStorage\n}\n\nexport const betterAuthPayloadPlugin =\n (pluginOptions: BetterAuthPayloadPluginOptions) =>\n (config: Config): Config => {\n const { externalBaseURL, internalBaseURL, ...restClientOptions } =\n pluginOptions.betterAuthClientOptions\n const debug = pluginOptions.debug ?? false\n const collectionPrefix = pluginOptions.collectionPrefix ?? '__better_auth'\n const { baCollectionsAccess, eventBus, storage } = pluginOptions\n\n // Create deduplicated logger\n const logger = createDeduplicatedLogger({\n enabled: debug,\n prefix: '[payload]',\n storage,\n })\n\n // Build internal and external auth client options\n const internalAuthClientOptions = { ...restClientOptions, baseURL: internalBaseURL }\n const externalAuthClientOptions = { ...restClientOptions, baseURL: externalBaseURL }\n\n // Log plugin configuration at startup (deduplicated)\n void logger.log('init', `Initialized (baseURL: ${internalBaseURL})`)\n\n // Determine BA collection access:\n // 1. If baCollectionsAccess is provided, use it (overrides debug defaults)\n // 2. If debug is enabled, allow authenticated users to read\n // 3. Otherwise, no custom access (only BA sync agent)\n const effectiveBaAccess: { delete?: Access; read?: Access } | undefined = baCollectionsAccess\n ? baCollectionsAccess\n : debug\n ? { read: ({ req }) => Boolean(req.user) }\n : undefined\n\n // Initialize collections array if not present\n if (!config.collections) {\n config.collections = []\n }\n\n // Create BA collections\n const emailPasswordCollection = createEmailPasswordCollection({\n access: effectiveBaAccess,\n isVisible: debug,\n prefix: collectionPrefix,\n storage,\n })\n const magicLinkCollection = createMagicLinkCollection({\n access: effectiveBaAccess,\n isVisible: debug,\n prefix: collectionPrefix,\n storage,\n })\n\n // Find and extend existing users collection, or create minimal one\n const existingUsersIndex = config.collections.findIndex((col) => col.slug === 'users')\n const existingUsersCollection: CollectionConfig | undefined =\n existingUsersIndex >= 0 ? config.collections[existingUsersIndex] : undefined\n\n const extendedUsersCollection = extendUsersCollection(existingUsersCollection, {\n collectionPrefix,\n storage,\n })\n\n // Replace or add the users collection\n if (existingUsersIndex >= 0) {\n config.collections[existingUsersIndex] = extendedUsersCollection\n } else {\n config.collections.push(extendedUsersCollection)\n }\n\n // Add BA collections\n config.collections.push(emailPasswordCollection)\n config.collections.push(magicLinkCollection)\n\n /**\n * If the plugin is disabled, we still want to keep added collections/fields so the database schema is consistent which is important for migrations.\n * If your plugin heavily modifies the database schema, you may want to remove this property.\n */\n if (pluginOptions.disabled) {\n return config\n }\n\n if (!config.endpoints) {\n config.endpoints = []\n }\n\n if (!config.admin) {\n config.admin = {}\n }\n\n if (!config.admin.user) {\n config.admin.user = extendedUsersCollection.slug\n } else if (config.admin.user !== extendedUsersCollection.slug) {\n throw new Error(\n 'Payload-better-auth plugin: admin.user property already set with conflicting value.',\n )\n }\n\n if (!config.admin.components) {\n config.admin.components = {}\n }\n\n if (!config.admin.components.views) {\n config.admin.components.views = {}\n }\n\n if (!config.admin.components.views.login) {\n config.admin.components.views.login = {\n Component: {\n path: 'payload-better-auth/rsc#BetterAuthLoginServer',\n serverProps: {\n debug,\n externalAuthClientOptions,\n internalAuthClientOptions,\n },\n },\n exact: true,\n path: '/auth',\n }\n } else {\n throw new Error(\n 'Payload-better-auth plugin: admin.components.views.login property in config already set.',\n )\n }\n\n if (!config.admin.components.views.verifyEmail) {\n config.admin.components.views.verifyEmail = {\n Component: 'payload-better-auth/client#VerifyEmailInfoViewClient', // RSC or 'use client' component\n exact: true,\n path: '/auth/verify-email',\n }\n } else {\n throw new Error(\n 'Payload-better-auth plugin: admin.components.views.verifyEmail property in config already set.',\n )\n }\n\n if (!config.admin.routes) {\n config.admin.routes = {}\n }\n\n if (!config.admin.routes.login) {\n config.admin.routes.login = '/auth'\n } else {\n throw new Error(\n 'Payload-better-auth plugin: admin.routes.login property in config already set.',\n )\n }\n\n const incomingOnInit = config.onInit\n\n config.onInit = async (payload) => {\n // Ensure we are executing any existing onInit functions before running our own.\n if (incomingOnInit) {\n await incomingOnInit(payload)\n }\n\n // Set Payload timestamp in storage - Better Auth will see this and trigger reconciliation\n const timestamp = Date.now()\n await storage.set(TIMESTAMP_PREFIX + 'payload', String(timestamp))\n // Also notify via event bus for same-process subscribers\n eventBus.notifyTimestampChange('payload', timestamp)\n await logger.log('ready', 'Ready, triggering Better Auth initialization')\n\n // Trigger Better Auth initialization by calling the warmup endpoint\n // Better Auth plugins are lazy-initialized on first request\n try {\n const warmupUrl = `${internalBaseURL}/api/auth/warmup`\n const response = await fetch(warmupUrl, {\n headers: { 'User-Agent': 'Payload-Better-Auth-Warmup' },\n method: 'GET',\n })\n if (response.ok) {\n const info = (await response.json()) as {\n authMethods: string[]\n initialized: boolean\n timestamp: string\n }\n await logger.log('warmup', 'Better Auth initialized', {\n authMethods: info.authMethods,\n })\n } else {\n await logger.log('warmup-error', 'Better Auth warmup returned error', {\n status: response.status,\n })\n }\n } catch (error) {\n // Log but don't fail - Better Auth will initialize on first real request\n await logger.log(\n 'warmup-error',\n 'Failed to warm up Better Auth (will init on first request)',\n {\n error: error instanceof Error ? error.message : String(error),\n },\n )\n }\n\n // Note: User sync is now handled entirely by the reconcile queue on the Better Auth side.\n // The queue enqueues ensure/delete tasks when users change, and processes them with retries.\n }\n\n return config\n }\n"],"names":["createEmailPasswordCollection","createMagicLinkCollection","extendUsersCollection","createDeduplicatedLogger","TIMESTAMP_PREFIX","betterAuthPayloadPlugin","pluginOptions","config","externalBaseURL","internalBaseURL","restClientOptions","betterAuthClientOptions","debug","collectionPrefix","baCollectionsAccess","eventBus","storage","logger","enabled","prefix","internalAuthClientOptions","baseURL","externalAuthClientOptions","log","effectiveBaAccess","read","req","Boolean","user","undefined","collections","emailPasswordCollection","access","isVisible","magicLinkCollection","existingUsersIndex","findIndex","col","slug","existingUsersCollection","extendedUsersCollection","push","disabled","endpoints","admin","Error","components","views","login","Component","path","serverProps","exact","verifyEmail","routes","incomingOnInit","onInit","payload","timestamp","Date","now","set","String","notifyTimestampChange","warmupUrl","response","fetch","headers","method","ok","info","json","authMethods","status","error","message"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../src/payload/plugin.ts"],"sourcesContent":["import type { ClientOptions } from 'better-auth'\nimport type { Access, CollectionConfig, Config } from 'payload'\n\nimport type { EventBus } from '../eventBus/types'\nimport type { SecondaryStorage } from '../storage/types'\n\nimport { createEmailPasswordCollection } from '../collections/BetterAuth/emailPassword'\nimport { createMagicLinkCollection } from '../collections/BetterAuth/magicLink'\nimport { extendUsersCollection } from '../collections/Users/index'\nimport { createDeduplicatedLogger } from '../shared/deduplicatedLogger'\nimport { TIMESTAMP_PREFIX } from '../storage/keys'\n\nexport type BetterAuthClientOptions = {\n /**\n * The external base URL for better-auth, used for client-side requests (from the browser).\n * This should be the publicly accessible URL.\n * @example 'https://auth.example.com'\n */\n externalBaseURL: string\n /**\n * The internal base URL for better-auth, used for server-side requests.\n * This is used when the server needs to reach better-auth internally (e.g., within a container network).\n * @example 'http://auth-service:3000'\n */\n internalBaseURL: string\n} & Omit<ClientOptions, 'baseURL'>\n\nexport type BetterAuthPayloadPluginOptions = {\n /**\n * Custom access rules for Better Auth collections (email_password, magic_link).\n * These override the default debug-mode access (which allows read for authenticated users).\n *\n * @example\n * baCollectionsAccess: {\n * read: ({ req }) => req.user?.role === 'admin',\n * delete: ({ req }) => req.user?.role === 'admin',\n * }\n */\n baCollectionsAccess?: {\n delete?: Access\n read?: Access\n }\n betterAuthClientOptions: BetterAuthClientOptions\n /**\n * Prefix for Better Auth collections (default: '__better_auth').\n * The collections will be named: {prefix}_email_password, {prefix}_magic_link\n */\n collectionPrefix?: string\n /**\n * Enable debug logging and make BA collections visible in admin.\n * When enabled:\n * - Detailed error information will be logged\n * - BA collections are visible under \"Better Auth (DEBUG)\" group\n * - Authenticated users can read BA collections (unless baCollectionsAccess overrides)\n */\n debug?: boolean\n disabled?: boolean\n /**\n * EventBus for timestamp-based coordination between plugins.\n * Both plugins MUST share the same eventBus instance.\n *\n * Available implementations:\n * - `createSqlitePollingEventBus()` - Uses SQLite for cross-process coordination\n *\n * @example\n * // Create shared eventBus (e.g., in a separate file)\n * import { createSqlitePollingEventBus } from 'payload-better-auth'\n * import { DatabaseSync } from 'node:sqlite'\n * const db = new DatabaseSync('.event-bus.db')\n * export const eventBus = createSqlitePollingEventBus({ db })\n */\n eventBus: EventBus\n /**\n * Secondary storage for state coordination between Better Auth and Payload.\n * Both plugins MUST share the same storage instance.\n *\n * Available storage adapters:\n * - `createSqliteStorage()` - Uses Node.js 22+ native SQLite (no external dependencies)\n * - `createRedisStorage(redis)` - Redis-backed, for distributed/multi-server production\n *\n * @example\n * // Development (Node.js 22+)\n * import { createSqliteStorage } from 'payload-better-auth'\n * import { DatabaseSync } from 'node:sqlite'\n * const db = new DatabaseSync('.sync-state.db')\n * const storage = createSqliteStorage({ db })\n *\n * @example\n * // Production (distributed)\n * import { createRedisStorage } from 'payload-better-auth'\n * import Redis from 'ioredis'\n * const storage = createRedisStorage({ redis: new Redis() })\n */\n storage: SecondaryStorage\n}\n\nexport const betterAuthPayloadPlugin =\n (pluginOptions: BetterAuthPayloadPluginOptions) =>\n (config: Config): Config => {\n const { externalBaseURL, internalBaseURL, ...restClientOptions } =\n pluginOptions.betterAuthClientOptions\n const debug = pluginOptions.debug ?? false\n const collectionPrefix = pluginOptions.collectionPrefix ?? '__better_auth'\n const { baCollectionsAccess, eventBus, storage } = pluginOptions\n\n // Create deduplicated logger\n const logger = createDeduplicatedLogger({\n enabled: debug,\n prefix: '[payload]',\n storage,\n })\n\n // Build internal and external auth client options\n const internalAuthClientOptions = { ...restClientOptions, baseURL: internalBaseURL }\n const externalAuthClientOptions = { ...restClientOptions, baseURL: externalBaseURL }\n\n // Log plugin configuration at startup (deduplicated)\n void logger.log('init', `Initialized (baseURL: ${internalBaseURL})`)\n\n // Determine BA collection access:\n // 1. If baCollectionsAccess is provided, use it (overrides debug defaults)\n // 2. If debug is enabled, allow authenticated users to read\n // 3. Otherwise, no custom access (only BA sync agent)\n const effectiveBaAccess: { delete?: Access; read?: Access } | undefined = baCollectionsAccess\n ? baCollectionsAccess\n : debug\n ? { read: ({ req }) => Boolean(req.user) }\n : undefined\n\n // Initialize collections array if not present\n if (!config.collections) {\n config.collections = []\n }\n\n // Create BA collections\n const emailPasswordCollection = createEmailPasswordCollection({\n access: effectiveBaAccess,\n isVisible: debug,\n prefix: collectionPrefix,\n storage,\n })\n const magicLinkCollection = createMagicLinkCollection({\n access: effectiveBaAccess,\n isVisible: debug,\n prefix: collectionPrefix,\n storage,\n })\n\n // Find and extend existing users collection, or create minimal one\n const existingUsersIndex = config.collections.findIndex((col) => col.slug === 'users')\n const existingUsersCollection: CollectionConfig | undefined =\n existingUsersIndex >= 0 ? config.collections[existingUsersIndex] : undefined\n\n const extendedUsersCollection = extendUsersCollection(existingUsersCollection, {\n collectionPrefix,\n storage,\n })\n\n // Replace or add the users collection\n if (existingUsersIndex >= 0) {\n config.collections[existingUsersIndex] = extendedUsersCollection\n } else {\n config.collections.push(extendedUsersCollection)\n }\n\n // Add BA collections\n config.collections.push(emailPasswordCollection)\n config.collections.push(magicLinkCollection)\n\n /**\n * If the plugin is disabled, we still want to keep added collections/fields so the database schema is consistent which is important for migrations.\n * If your plugin heavily modifies the database schema, you may want to remove this property.\n */\n if (pluginOptions.disabled) {\n return config\n }\n\n if (!config.endpoints) {\n config.endpoints = []\n }\n\n if (!config.admin) {\n config.admin = {}\n }\n\n if (!config.admin.user) {\n config.admin.user = extendedUsersCollection.slug\n } else if (config.admin.user !== extendedUsersCollection.slug) {\n throw new Error(\n 'Payload-better-auth plugin: admin.user property already set with conflicting value.',\n )\n }\n\n if (!config.admin.components) {\n config.admin.components = {}\n }\n\n if (!config.admin.components.views) {\n config.admin.components.views = {}\n }\n\n if (!config.admin.components.views.login) {\n config.admin.components.views.login = {\n Component: {\n path: 'payload-better-auth/rsc#BetterAuthLoginServer',\n serverProps: {\n debug,\n externalAuthClientOptions,\n internalAuthClientOptions,\n },\n },\n exact: true,\n path: '/auth',\n }\n } else {\n throw new Error(\n 'Payload-better-auth plugin: admin.components.views.login property in config already set.',\n )\n }\n\n if (!config.admin.components.views.verifyEmail) {\n config.admin.components.views.verifyEmail = {\n Component: 'payload-better-auth/client#VerifyEmailInfoViewClient', // RSC or 'use client' component\n exact: true,\n path: '/auth/verify-email',\n }\n } else {\n throw new Error(\n 'Payload-better-auth plugin: admin.components.views.verifyEmail property in config already set.',\n )\n }\n\n // Configure custom logout button that signs out from Better Auth\n if (!config.admin.components.logout) {\n config.admin.components.logout = {}\n }\n\n if (!config.admin.components.logout.Button) {\n config.admin.components.logout.Button = {\n clientProps: {\n authClientOptions: externalAuthClientOptions,\n },\n path: 'payload-better-auth/client#LogoutButtonClient',\n }\n } else {\n throw new Error(\n 'Payload-better-auth plugin: admin.components.logout.Button property in config already set.',\n )\n }\n\n if (!config.admin.routes) {\n config.admin.routes = {}\n }\n\n if (!config.admin.routes.login) {\n config.admin.routes.login = '/auth'\n } else {\n throw new Error(\n 'Payload-better-auth plugin: admin.routes.login property in config already set.',\n )\n }\n\n const incomingOnInit = config.onInit\n\n config.onInit = async (payload) => {\n // Ensure we are executing any existing onInit functions before running our own.\n if (incomingOnInit) {\n await incomingOnInit(payload)\n }\n\n // Set Payload timestamp in storage - Better Auth will see this and trigger reconciliation\n const timestamp = Date.now()\n await storage.set(TIMESTAMP_PREFIX + 'payload', String(timestamp))\n // Also notify via event bus for same-process subscribers\n eventBus.notifyTimestampChange('payload', timestamp)\n await logger.log('ready', 'Ready, triggering Better Auth initialization')\n\n // Trigger Better Auth initialization by calling the warmup endpoint\n // Better Auth plugins are lazy-initialized on first request\n try {\n const warmupUrl = `${internalBaseURL}/api/auth/warmup`\n const response = await fetch(warmupUrl, {\n headers: { 'User-Agent': 'Payload-Better-Auth-Warmup' },\n method: 'GET',\n })\n if (response.ok) {\n const info = (await response.json()) as {\n authMethods: string[]\n initialized: boolean\n timestamp: string\n }\n await logger.log('warmup', 'Better Auth initialized', {\n authMethods: info.authMethods,\n })\n } else {\n await logger.log('warmup-error', 'Better Auth warmup returned error', {\n status: response.status,\n })\n }\n } catch (error) {\n // Log but don't fail - Better Auth will initialize on first real request\n await logger.log(\n 'warmup-error',\n 'Failed to warm up Better Auth (will init on first request)',\n {\n error: error instanceof Error ? error.message : String(error),\n },\n )\n }\n\n // Note: User sync is now handled entirely by the reconcile queue on the Better Auth side.\n // The queue enqueues ensure/delete tasks when users change, and processes them with retries.\n }\n\n return config\n }\n"],"names":["createEmailPasswordCollection","createMagicLinkCollection","extendUsersCollection","createDeduplicatedLogger","TIMESTAMP_PREFIX","betterAuthPayloadPlugin","pluginOptions","config","externalBaseURL","internalBaseURL","restClientOptions","betterAuthClientOptions","debug","collectionPrefix","baCollectionsAccess","eventBus","storage","logger","enabled","prefix","internalAuthClientOptions","baseURL","externalAuthClientOptions","log","effectiveBaAccess","read","req","Boolean","user","undefined","collections","emailPasswordCollection","access","isVisible","magicLinkCollection","existingUsersIndex","findIndex","col","slug","existingUsersCollection","extendedUsersCollection","push","disabled","endpoints","admin","Error","components","views","login","Component","path","serverProps","exact","verifyEmail","logout","Button","clientProps","authClientOptions","routes","incomingOnInit","onInit","payload","timestamp","Date","now","set","String","notifyTimestampChange","warmupUrl","response","fetch","headers","method","ok","info","json","authMethods","status","error","message"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { toast } from '@payloadcms/ui'
|
|
4
|
+
import { createAuthClient } from 'better-auth/react'
|
|
5
|
+
import { useRouter } from 'next/navigation.js'
|
|
6
|
+
import { useState } from 'react'
|
|
7
|
+
|
|
8
|
+
import type { AuthClientOptions } from './BetterAuthLoginServer'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Simple logout icon SVG component
|
|
12
|
+
*/
|
|
13
|
+
function LogOutIcon() {
|
|
14
|
+
return (
|
|
15
|
+
<svg
|
|
16
|
+
fill="none"
|
|
17
|
+
height="20"
|
|
18
|
+
stroke="currentColor"
|
|
19
|
+
strokeLinecap="round"
|
|
20
|
+
strokeLinejoin="round"
|
|
21
|
+
strokeWidth="2"
|
|
22
|
+
viewBox="0 0 24 24"
|
|
23
|
+
width="20"
|
|
24
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
25
|
+
>
|
|
26
|
+
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" />
|
|
27
|
+
<polyline points="16 17 21 12 16 7" />
|
|
28
|
+
<line x1="21" x2="9" y1="12" y2="12" />
|
|
29
|
+
</svg>
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface LogoutButtonClientProps {
|
|
34
|
+
/**
|
|
35
|
+
* Auth client options for Better Auth sign-out.
|
|
36
|
+
* Uses the external (browser-accessible) URL.
|
|
37
|
+
*/
|
|
38
|
+
authClientOptions: AuthClientOptions
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function LogoutButtonClient({ authClientOptions }: LogoutButtonClientProps) {
|
|
42
|
+
const authClient = createAuthClient(authClientOptions)
|
|
43
|
+
const router = useRouter()
|
|
44
|
+
const [isLoading, setIsLoading] = useState(false)
|
|
45
|
+
|
|
46
|
+
const handleLogout = async () => {
|
|
47
|
+
if (isLoading) {
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
setIsLoading(true)
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
// Sign out from Better Auth - this clears the session cookie
|
|
55
|
+
const result = await authClient.signOut()
|
|
56
|
+
|
|
57
|
+
if (result.error) {
|
|
58
|
+
toast.error(result.error.message || 'Logout failed')
|
|
59
|
+
setIsLoading(false)
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Redirect to login page after successful sign-out
|
|
64
|
+
router.push('/admin/login')
|
|
65
|
+
router.refresh()
|
|
66
|
+
} catch (error) {
|
|
67
|
+
toast.error((error as Error).message || 'Logout failed')
|
|
68
|
+
setIsLoading(false)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<button
|
|
74
|
+
aria-label="Log out"
|
|
75
|
+
disabled={isLoading}
|
|
76
|
+
onClick={handleLogout}
|
|
77
|
+
style={{
|
|
78
|
+
alignItems: 'center',
|
|
79
|
+
background: 'transparent',
|
|
80
|
+
border: 'none',
|
|
81
|
+
color: 'inherit',
|
|
82
|
+
cursor: isLoading ? 'wait' : 'pointer',
|
|
83
|
+
display: 'flex',
|
|
84
|
+
fontFamily: 'inherit',
|
|
85
|
+
fontSize: 'inherit',
|
|
86
|
+
gap: '0.5rem',
|
|
87
|
+
opacity: isLoading ? 0.6 : 1,
|
|
88
|
+
padding: '0.75rem 1rem',
|
|
89
|
+
textAlign: 'left',
|
|
90
|
+
transition: 'opacity 0.2s',
|
|
91
|
+
width: '100%',
|
|
92
|
+
}}
|
|
93
|
+
type="button"
|
|
94
|
+
>
|
|
95
|
+
<LogOutIcon />
|
|
96
|
+
<span>{isLoading ? 'Logging out...' : 'Logout'}</span>
|
|
97
|
+
</button>
|
|
98
|
+
)
|
|
99
|
+
}
|
package/src/exports/client.ts
CHANGED
package/src/payload/plugin.ts
CHANGED
|
@@ -230,6 +230,24 @@ export const betterAuthPayloadPlugin =
|
|
|
230
230
|
)
|
|
231
231
|
}
|
|
232
232
|
|
|
233
|
+
// Configure custom logout button that signs out from Better Auth
|
|
234
|
+
if (!config.admin.components.logout) {
|
|
235
|
+
config.admin.components.logout = {}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (!config.admin.components.logout.Button) {
|
|
239
|
+
config.admin.components.logout.Button = {
|
|
240
|
+
clientProps: {
|
|
241
|
+
authClientOptions: externalAuthClientOptions,
|
|
242
|
+
},
|
|
243
|
+
path: 'payload-better-auth/client#LogoutButtonClient',
|
|
244
|
+
}
|
|
245
|
+
} else {
|
|
246
|
+
throw new Error(
|
|
247
|
+
'Payload-better-auth plugin: admin.components.logout.Button property in config already set.',
|
|
248
|
+
)
|
|
249
|
+
}
|
|
250
|
+
|
|
233
251
|
if (!config.admin.routes) {
|
|
234
252
|
config.admin.routes = {}
|
|
235
253
|
}
|