@delmaredigital/payload-better-auth 0.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/LICENSE +21 -0
- package/README.md +734 -0
- package/dist/adapter/index.d.mts +70 -0
- package/dist/adapter/index.d.ts +70 -0
- package/dist/adapter/index.js +368 -0
- package/dist/adapter/index.js.map +1 -0
- package/dist/adapter/index.mjs +366 -0
- package/dist/adapter/index.mjs.map +1 -0
- package/dist/client.d.mts +1 -0
- package/dist/client.d.ts +1 -0
- package/dist/client.js +12 -0
- package/dist/client.js.map +1 -0
- package/dist/client.mjs +3 -0
- package/dist/client.mjs.map +1 -0
- package/dist/index.d.mts +120 -0
- package/dist/index.d.ts +120 -0
- package/dist/index.js +611 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +603 -0
- package/dist/index.mjs.map +1 -0
- package/dist/plugin/index.d.mts +78 -0
- package/dist/plugin/index.d.ts +78 -0
- package/dist/plugin/index.js +86 -0
- package/dist/plugin/index.js.map +1 -0
- package/dist/plugin/index.mjs +82 -0
- package/dist/plugin/index.mjs.map +1 -0
- package/package.json +73 -0
package/README.md
ADDED
|
@@ -0,0 +1,734 @@
|
|
|
1
|
+
# @delmaredigital/payload-better-auth
|
|
2
|
+
|
|
3
|
+
Better Auth adapter and plugins for Payload CMS. Enables seamless integration between Better Auth and Payload.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Table of Contents
|
|
8
|
+
|
|
9
|
+
- [Installation](#installation)
|
|
10
|
+
- [Quick Start](#quick-start)
|
|
11
|
+
- [API Reference](#api-reference)
|
|
12
|
+
- [Admin Panel Integration](#admin-panel-integration)
|
|
13
|
+
- [Plugin Compatibility](#plugin-compatibility)
|
|
14
|
+
- [License](#license)
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
### Requirements
|
|
21
|
+
|
|
22
|
+
| Dependency | Version |
|
|
23
|
+
|------------|---------|
|
|
24
|
+
| `better-auth` | >= 1.0.0 |
|
|
25
|
+
| `payload` | >= 3.0.0 |
|
|
26
|
+
| `next` | >= 15.4.0 |
|
|
27
|
+
| `react` | >= 18.0.0 |
|
|
28
|
+
|
|
29
|
+
### Install
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pnpm add @delmaredigital/payload-better-auth better-auth
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Or install from GitHub:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pnpm add github:delmaredigital/payload-better-auth
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Local Development
|
|
42
|
+
|
|
43
|
+
For local development with hot reloading:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
# In the package directory
|
|
47
|
+
cd path/to/payload-better-auth
|
|
48
|
+
pnpm link --global
|
|
49
|
+
|
|
50
|
+
# In your project
|
|
51
|
+
pnpm link --global @delmaredigital/payload-better-auth
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Quick Start
|
|
57
|
+
|
|
58
|
+
### Step 1: Create Your Auth Configuration
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
// src/lib/auth/config.ts
|
|
62
|
+
import type { BetterAuthOptions } from 'better-auth'
|
|
63
|
+
|
|
64
|
+
export const betterAuthOptions: Partial<BetterAuthOptions> = {
|
|
65
|
+
user: {
|
|
66
|
+
modelName: 'users',
|
|
67
|
+
additionalFields: {
|
|
68
|
+
role: { type: 'string', defaultValue: 'user' },
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
session: {
|
|
72
|
+
modelName: 'sessions',
|
|
73
|
+
expiresIn: 60 * 60 * 24 * 30, // 30 days
|
|
74
|
+
},
|
|
75
|
+
account: { modelName: 'accounts' },
|
|
76
|
+
verification: { modelName: 'verifications' },
|
|
77
|
+
emailAndPassword: { enabled: true },
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export const collectionSlugs = {
|
|
81
|
+
user: 'users',
|
|
82
|
+
session: 'sessions',
|
|
83
|
+
account: 'accounts',
|
|
84
|
+
verification: 'verifications',
|
|
85
|
+
} as const
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Step 2: Create the Auth Instance Factory
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
// src/lib/auth/index.ts
|
|
92
|
+
import { betterAuth } from 'better-auth'
|
|
93
|
+
import type { BasePayload } from 'payload'
|
|
94
|
+
import { payloadAdapter } from '@delmaredigital/payload-better-auth'
|
|
95
|
+
import { betterAuthOptions, collectionSlugs } from './config'
|
|
96
|
+
|
|
97
|
+
export function createAuth(payload: BasePayload) {
|
|
98
|
+
return betterAuth({
|
|
99
|
+
...betterAuthOptions,
|
|
100
|
+
database: payloadAdapter({
|
|
101
|
+
payloadClient: payload,
|
|
102
|
+
adapterConfig: {
|
|
103
|
+
collections: collectionSlugs,
|
|
104
|
+
enableDebugLogs: process.env.NODE_ENV === 'development',
|
|
105
|
+
idType: 'number', // Use Payload's default SERIAL IDs
|
|
106
|
+
},
|
|
107
|
+
}),
|
|
108
|
+
// Use serial/integer IDs (Payload default) instead of UUID
|
|
109
|
+
advanced: {
|
|
110
|
+
database: {
|
|
111
|
+
generateId: 'serial',
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
secret: process.env.BETTER_AUTH_SECRET,
|
|
115
|
+
trustedOrigins: [process.env.NEXT_PUBLIC_APP_URL || ''],
|
|
116
|
+
})
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Step 3: Configure Payload
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
// src/payload.config.ts
|
|
124
|
+
import { buildConfig } from 'payload'
|
|
125
|
+
import {
|
|
126
|
+
betterAuthCollections,
|
|
127
|
+
createBetterAuthPlugin,
|
|
128
|
+
} from '@delmaredigital/payload-better-auth'
|
|
129
|
+
import { betterAuthOptions } from './lib/auth/config'
|
|
130
|
+
import { createAuth } from './lib/auth'
|
|
131
|
+
import { Users } from './collections/Users'
|
|
132
|
+
|
|
133
|
+
export default buildConfig({
|
|
134
|
+
collections: [Users /* ... other collections */],
|
|
135
|
+
plugins: [
|
|
136
|
+
// Auto-generate sessions, accounts, verifications collections
|
|
137
|
+
betterAuthCollections({
|
|
138
|
+
betterAuthOptions,
|
|
139
|
+
skipCollections: ['user'], // We define Users ourselves
|
|
140
|
+
}),
|
|
141
|
+
// Initialize Better Auth in Payload's lifecycle
|
|
142
|
+
createBetterAuthPlugin({
|
|
143
|
+
createAuth,
|
|
144
|
+
}),
|
|
145
|
+
],
|
|
146
|
+
db: postgresAdapter({
|
|
147
|
+
pool: { connectionString: process.env.DATABASE_URL },
|
|
148
|
+
// Use Payload defaults - Better Auth adapter handles ID conversion
|
|
149
|
+
}),
|
|
150
|
+
})
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### Step 4: Create Your Users Collection
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
// src/collections/Users.ts
|
|
157
|
+
import type { CollectionConfig } from 'payload'
|
|
158
|
+
import { betterAuthStrategy } from '@delmaredigital/payload-better-auth'
|
|
159
|
+
|
|
160
|
+
export const Users: CollectionConfig = {
|
|
161
|
+
slug: 'users',
|
|
162
|
+
auth: {
|
|
163
|
+
disableLocalStrategy: true,
|
|
164
|
+
strategies: [betterAuthStrategy()],
|
|
165
|
+
},
|
|
166
|
+
access: {
|
|
167
|
+
read: ({ req }) => {
|
|
168
|
+
if (!req.user) return false
|
|
169
|
+
if (req.user.role === 'admin') return true
|
|
170
|
+
return { id: { equals: req.user.id } }
|
|
171
|
+
},
|
|
172
|
+
admin: ({ req }) => req.user?.role === 'admin',
|
|
173
|
+
},
|
|
174
|
+
fields: [
|
|
175
|
+
{ name: 'email', type: 'email', required: true, unique: true },
|
|
176
|
+
{ name: 'emailVerified', type: 'checkbox', defaultValue: false },
|
|
177
|
+
{ name: 'name', type: 'text' },
|
|
178
|
+
{ name: 'image', type: 'text' },
|
|
179
|
+
{
|
|
180
|
+
name: 'role',
|
|
181
|
+
type: 'select',
|
|
182
|
+
defaultValue: 'user',
|
|
183
|
+
options: [
|
|
184
|
+
{ label: 'User', value: 'user' },
|
|
185
|
+
{ label: 'Admin', value: 'admin' },
|
|
186
|
+
],
|
|
187
|
+
},
|
|
188
|
+
],
|
|
189
|
+
}
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### Step 5: Create the Auth API Route
|
|
193
|
+
|
|
194
|
+
```ts
|
|
195
|
+
// src/app/api/auth/[...all]/route.ts
|
|
196
|
+
import { getPayload } from 'payload'
|
|
197
|
+
import config from '@payload-config'
|
|
198
|
+
import type { NextRequest } from 'next/server'
|
|
199
|
+
import type { PayloadWithAuth } from '@delmaredigital/payload-better-auth'
|
|
200
|
+
|
|
201
|
+
export async function GET(request: NextRequest) {
|
|
202
|
+
const payload = (await getPayload({ config })) as PayloadWithAuth
|
|
203
|
+
return payload.betterAuth.handler(request)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export async function POST(request: NextRequest) {
|
|
207
|
+
const payload = (await getPayload({ config })) as PayloadWithAuth
|
|
208
|
+
return payload.betterAuth.handler(request)
|
|
209
|
+
}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### Step 6: Client-Side Auth
|
|
213
|
+
|
|
214
|
+
```ts
|
|
215
|
+
// src/lib/auth/client.ts
|
|
216
|
+
'use client'
|
|
217
|
+
|
|
218
|
+
import { createAuthClient } from '@delmaredigital/payload-better-auth/client'
|
|
219
|
+
|
|
220
|
+
export const authClient = createAuthClient({
|
|
221
|
+
baseURL:
|
|
222
|
+
typeof window !== 'undefined'
|
|
223
|
+
? window.location.origin
|
|
224
|
+
: process.env.NEXT_PUBLIC_APP_URL,
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
export const { useSession, signIn, signUp, signOut } = authClient
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### Step 7: Server-Side Session Access
|
|
231
|
+
|
|
232
|
+
```ts
|
|
233
|
+
// In a server component or API route
|
|
234
|
+
import { headers } from 'next/headers'
|
|
235
|
+
import { getPayload } from 'payload'
|
|
236
|
+
import { getServerSession } from '@delmaredigital/payload-better-auth'
|
|
237
|
+
|
|
238
|
+
export default async function Dashboard() {
|
|
239
|
+
const payload = await getPayload({ config })
|
|
240
|
+
const headersList = await headers()
|
|
241
|
+
const session = await getServerSession(payload, headersList)
|
|
242
|
+
|
|
243
|
+
if (!session) {
|
|
244
|
+
redirect('/login')
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return <div>Hello {session.user.name}</div>
|
|
248
|
+
}
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
## API Reference
|
|
254
|
+
|
|
255
|
+
### `payloadAdapter(config)`
|
|
256
|
+
|
|
257
|
+
Creates a Better Auth database adapter that uses Payload collections.
|
|
258
|
+
|
|
259
|
+
```ts
|
|
260
|
+
payloadAdapter({
|
|
261
|
+
payloadClient: payload,
|
|
262
|
+
adapterConfig: {
|
|
263
|
+
collections: { user: 'users', session: 'sessions' },
|
|
264
|
+
enableDebugLogs: false,
|
|
265
|
+
idType: 'number',
|
|
266
|
+
},
|
|
267
|
+
})
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
| Option | Type | Description |
|
|
271
|
+
|--------|------|-------------|
|
|
272
|
+
| `payloadClient` | `BasePayload \| () => Promise<BasePayload>` | Payload instance or factory function |
|
|
273
|
+
| `adapterConfig.collections` | `Record<string, string>` | Map Better Auth model names to Payload collection slugs |
|
|
274
|
+
| `adapterConfig.enableDebugLogs` | `boolean` | Enable debug logging (default: `false`) |
|
|
275
|
+
| `adapterConfig.idType` | `'number' \| 'text'` | `'number'` for SERIAL (recommended), `'text'` for UUID |
|
|
276
|
+
|
|
277
|
+
**ID Type Options:**
|
|
278
|
+
- `'number'` (recommended) - Works with Payload's default SERIAL IDs. Requires `generateId: 'serial'` in Better Auth config.
|
|
279
|
+
- `'text'` - Works with UUID IDs. Requires `idType: 'uuid'` in Payload's database adapter.
|
|
280
|
+
|
|
281
|
+
### `betterAuthCollections(options)`
|
|
282
|
+
|
|
283
|
+
Payload plugin that auto-generates collections from Better Auth schema.
|
|
284
|
+
|
|
285
|
+
```ts
|
|
286
|
+
betterAuthCollections({
|
|
287
|
+
betterAuthOptions,
|
|
288
|
+
slugOverrides: { user: 'users' },
|
|
289
|
+
skipCollections: ['user'],
|
|
290
|
+
adminGroup: 'Auth',
|
|
291
|
+
})
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
| Option | Type | Description |
|
|
295
|
+
|--------|------|-------------|
|
|
296
|
+
| `betterAuthOptions` | `BetterAuthOptions` | Your Better Auth options |
|
|
297
|
+
| `slugOverrides` | `Record<string, string>` | Override collection names |
|
|
298
|
+
| `skipCollections` | `string[]` | Collections to skip generating (default: `['user']`) |
|
|
299
|
+
| `adminGroup` | `string` | Admin panel group name (default: `'Auth'`) |
|
|
300
|
+
| `access` | `CollectionConfig['access']` | Custom access control for generated collections |
|
|
301
|
+
|
|
302
|
+
### `createBetterAuthPlugin(options)`
|
|
303
|
+
|
|
304
|
+
Payload plugin that initializes Better Auth during Payload's `onInit`.
|
|
305
|
+
|
|
306
|
+
```ts
|
|
307
|
+
createBetterAuthPlugin({
|
|
308
|
+
createAuth: (payload) => betterAuth({ ... }),
|
|
309
|
+
})
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
| Option | Type | Description |
|
|
313
|
+
|--------|------|-------------|
|
|
314
|
+
| `createAuth` | `(payload: BasePayload) => Auth` | Factory function that creates the Better Auth instance |
|
|
315
|
+
|
|
316
|
+
### `betterAuthStrategy(options?)`
|
|
317
|
+
|
|
318
|
+
Payload auth strategy for Better Auth session validation.
|
|
319
|
+
|
|
320
|
+
```ts
|
|
321
|
+
betterAuthStrategy({
|
|
322
|
+
usersCollection: 'users',
|
|
323
|
+
})
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
| Option | Type | Description |
|
|
327
|
+
|--------|------|-------------|
|
|
328
|
+
| `usersCollection` | `string` | The collection slug for users (default: `'users'`) |
|
|
329
|
+
|
|
330
|
+
### `getServerSession(payload, headers)`
|
|
331
|
+
|
|
332
|
+
Get the current session on the server.
|
|
333
|
+
|
|
334
|
+
```ts
|
|
335
|
+
const session = await getServerSession(payload, headersList)
|
|
336
|
+
// Returns: { user: { id, email, name, ... }, session: { id, expiresAt, ... } } | null
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
### `getServerUser(payload, headers)`
|
|
340
|
+
|
|
341
|
+
Get the current user on the server (shorthand for `session.user`).
|
|
342
|
+
|
|
343
|
+
```ts
|
|
344
|
+
const user = await getServerUser(payload, headersList)
|
|
345
|
+
// Returns: { id, email, name, ... } | null
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
---
|
|
349
|
+
|
|
350
|
+
## Admin Panel Integration
|
|
351
|
+
|
|
352
|
+
When using `disableLocalStrategy: true` in your Users collection, you need custom admin authentication components since Payload's default login form won't work.
|
|
353
|
+
|
|
354
|
+
### Why Custom Components Are Needed
|
|
355
|
+
|
|
356
|
+
With `disableLocalStrategy: true`:
|
|
357
|
+
- Payload's default login form is disabled
|
|
358
|
+
- Users must authenticate via Better Auth
|
|
359
|
+
- A custom login page is needed at `/admin/login`
|
|
360
|
+
- A custom logout button is needed to clear Better Auth sessions
|
|
361
|
+
|
|
362
|
+
<details>
|
|
363
|
+
<summary><strong>Step 1: Create BeforeLogin Component</strong></summary>
|
|
364
|
+
|
|
365
|
+
This component redirects unauthenticated users from Payload's login to your custom login page:
|
|
366
|
+
|
|
367
|
+
```tsx
|
|
368
|
+
// src/components/admin/BeforeLogin.tsx
|
|
369
|
+
'use client'
|
|
370
|
+
|
|
371
|
+
import { useEffect } from 'react'
|
|
372
|
+
import { useRouter } from 'next/navigation'
|
|
373
|
+
|
|
374
|
+
export default function BeforeLogin() {
|
|
375
|
+
const router = useRouter()
|
|
376
|
+
|
|
377
|
+
useEffect(() => {
|
|
378
|
+
router.replace('/admin/login')
|
|
379
|
+
}, [router])
|
|
380
|
+
|
|
381
|
+
return (
|
|
382
|
+
<div style={{ display: 'flex', minHeight: '100vh', alignItems: 'center', justifyContent: 'center' }}>
|
|
383
|
+
<div>Redirecting to login...</div>
|
|
384
|
+
</div>
|
|
385
|
+
)
|
|
386
|
+
}
|
|
387
|
+
```
|
|
388
|
+
</details>
|
|
389
|
+
|
|
390
|
+
<details>
|
|
391
|
+
<summary><strong>Step 2: Create Custom Logout Button</strong></summary>
|
|
392
|
+
|
|
393
|
+
**IMPORTANT**: The logout button must only trigger logout **on click**, not on mount. Triggering logout on mount would cause an infinite redirect loop since this component is rendered in the admin panel header.
|
|
394
|
+
|
|
395
|
+
```tsx
|
|
396
|
+
// src/components/admin/Logout.tsx
|
|
397
|
+
'use client'
|
|
398
|
+
|
|
399
|
+
import { useState } from 'react'
|
|
400
|
+
import { useRouter } from 'next/navigation'
|
|
401
|
+
import { signOut } from '@/lib/auth/client'
|
|
402
|
+
|
|
403
|
+
export default function Logout() {
|
|
404
|
+
const router = useRouter()
|
|
405
|
+
const [isLoading, setIsLoading] = useState(false)
|
|
406
|
+
|
|
407
|
+
async function handleLogout() {
|
|
408
|
+
if (isLoading) return
|
|
409
|
+
setIsLoading(true)
|
|
410
|
+
|
|
411
|
+
try {
|
|
412
|
+
await signOut()
|
|
413
|
+
router.push('/admin/login')
|
|
414
|
+
} catch (error) {
|
|
415
|
+
console.error('Logout error:', error)
|
|
416
|
+
setIsLoading(false)
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
return (
|
|
421
|
+
<button
|
|
422
|
+
onClick={handleLogout}
|
|
423
|
+
disabled={isLoading}
|
|
424
|
+
type="button"
|
|
425
|
+
className="btn btn--style-secondary btn--icon-style-without-border btn--size-small btn--withoutPopup"
|
|
426
|
+
>
|
|
427
|
+
{isLoading ? 'Logging out...' : 'Log out'}
|
|
428
|
+
</button>
|
|
429
|
+
)
|
|
430
|
+
}
|
|
431
|
+
```
|
|
432
|
+
</details>
|
|
433
|
+
|
|
434
|
+
<details>
|
|
435
|
+
<summary><strong>Step 3: Create Admin Login Page</strong></summary>
|
|
436
|
+
|
|
437
|
+
```tsx
|
|
438
|
+
// src/app/(frontend)/admin/login/page.tsx
|
|
439
|
+
'use client'
|
|
440
|
+
|
|
441
|
+
import { useEffect, useState, type FormEvent } from 'react'
|
|
442
|
+
import { useRouter } from 'next/navigation'
|
|
443
|
+
import { useSession, signIn } from '@/lib/auth/client'
|
|
444
|
+
|
|
445
|
+
export default function AdminLoginPage() {
|
|
446
|
+
const { data: session, isPending } = useSession()
|
|
447
|
+
const router = useRouter()
|
|
448
|
+
const [email, setEmail] = useState('')
|
|
449
|
+
const [password, setPassword] = useState('')
|
|
450
|
+
const [error, setError] = useState<string | null>(null)
|
|
451
|
+
const [isLoading, setIsLoading] = useState(false)
|
|
452
|
+
|
|
453
|
+
useEffect(() => {
|
|
454
|
+
if (session?.user) {
|
|
455
|
+
const user = session.user as { role?: string }
|
|
456
|
+
if (user.role === 'admin') {
|
|
457
|
+
router.push('/admin')
|
|
458
|
+
} else {
|
|
459
|
+
setError('Access denied. Admin role required.')
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}, [session, router])
|
|
463
|
+
|
|
464
|
+
async function handleSubmit(e: FormEvent) {
|
|
465
|
+
e.preventDefault()
|
|
466
|
+
setError(null)
|
|
467
|
+
setIsLoading(true)
|
|
468
|
+
|
|
469
|
+
try {
|
|
470
|
+
const result = await signIn.email({ email, password })
|
|
471
|
+
if (result.error) {
|
|
472
|
+
setError(result.error.message || 'Invalid credentials')
|
|
473
|
+
setIsLoading(false)
|
|
474
|
+
return
|
|
475
|
+
}
|
|
476
|
+
router.refresh()
|
|
477
|
+
} catch {
|
|
478
|
+
setError('An unexpected error occurred')
|
|
479
|
+
setIsLoading(false)
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
if (isPending) {
|
|
484
|
+
return <div>Loading...</div>
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
return (
|
|
488
|
+
<form onSubmit={handleSubmit}>
|
|
489
|
+
<h1>Admin Login</h1>
|
|
490
|
+
<input
|
|
491
|
+
type="email"
|
|
492
|
+
value={email}
|
|
493
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
494
|
+
placeholder="Email"
|
|
495
|
+
required
|
|
496
|
+
/>
|
|
497
|
+
<input
|
|
498
|
+
type="password"
|
|
499
|
+
value={password}
|
|
500
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
501
|
+
placeholder="Password"
|
|
502
|
+
required
|
|
503
|
+
/>
|
|
504
|
+
{error && <div style={{ color: 'red' }}>{error}</div>}
|
|
505
|
+
<button type="submit" disabled={isLoading}>
|
|
506
|
+
{isLoading ? 'Signing in...' : 'Sign in'}
|
|
507
|
+
</button>
|
|
508
|
+
</form>
|
|
509
|
+
)
|
|
510
|
+
}
|
|
511
|
+
```
|
|
512
|
+
</details>
|
|
513
|
+
|
|
514
|
+
<details>
|
|
515
|
+
<summary><strong>Step 4: Configure Payload Admin Components</strong></summary>
|
|
516
|
+
|
|
517
|
+
```ts
|
|
518
|
+
// payload.config.ts
|
|
519
|
+
export default buildConfig({
|
|
520
|
+
admin: {
|
|
521
|
+
user: Users.slug,
|
|
522
|
+
components: {
|
|
523
|
+
beforeLogin: ['@/components/admin/BeforeLogin'],
|
|
524
|
+
logout: {
|
|
525
|
+
Button: '@/components/admin/Logout',
|
|
526
|
+
},
|
|
527
|
+
},
|
|
528
|
+
},
|
|
529
|
+
// ... rest of config
|
|
530
|
+
})
|
|
531
|
+
```
|
|
532
|
+
</details>
|
|
533
|
+
|
|
534
|
+
---
|
|
535
|
+
|
|
536
|
+
## Plugin Compatibility
|
|
537
|
+
|
|
538
|
+
| Plugin | Status | Notes |
|
|
539
|
+
|--------|--------|-------|
|
|
540
|
+
| OAuth | Works | Uses existing accounts table |
|
|
541
|
+
| Magic Link | Works | Uses verifications table |
|
|
542
|
+
| Email Verification | Works | Uses verifications table |
|
|
543
|
+
| Email OTP | Works | Uses verifications table |
|
|
544
|
+
| Password Reset | Works | Uses verifications table |
|
|
545
|
+
| API Keys | Needs join | See [API Keys](#api-keys) below |
|
|
546
|
+
| Organizations | Needs joins | See [Organizations](#organizations) below |
|
|
547
|
+
| 2FA/TOTP | Needs join | See [Two-Factor Auth](#two-factor-auth-totp) below |
|
|
548
|
+
|
|
549
|
+
### Enabling Plugins That Need Joins
|
|
550
|
+
|
|
551
|
+
Some Better Auth plugins expect to access related data via joins (e.g., `user.apiKeys`). Payload handles this via `join` fields. Below are the patterns for each plugin.
|
|
552
|
+
|
|
553
|
+
<details>
|
|
554
|
+
<summary><strong>API Keys</strong></summary>
|
|
555
|
+
|
|
556
|
+
The API Keys plugin creates an `apiKey` model with a `userId` reference.
|
|
557
|
+
|
|
558
|
+
**1. Add to your Better Auth config:**
|
|
559
|
+
|
|
560
|
+
```ts
|
|
561
|
+
// src/lib/auth/config.ts
|
|
562
|
+
import { apiKey } from 'better-auth/plugins'
|
|
563
|
+
|
|
564
|
+
export const betterAuthOptions: Partial<BetterAuthOptions> = {
|
|
565
|
+
// ... existing config
|
|
566
|
+
plugins: [apiKey()],
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
export const collectionSlugs = {
|
|
570
|
+
user: 'users',
|
|
571
|
+
session: 'sessions',
|
|
572
|
+
account: 'accounts',
|
|
573
|
+
verification: 'verifications',
|
|
574
|
+
apiKey: 'api-keys', // Add this
|
|
575
|
+
} as const
|
|
576
|
+
```
|
|
577
|
+
|
|
578
|
+
**2. Add join field to your Users collection:**
|
|
579
|
+
|
|
580
|
+
```ts
|
|
581
|
+
// src/collections/Users.ts
|
|
582
|
+
export const Users: CollectionConfig = {
|
|
583
|
+
slug: 'users',
|
|
584
|
+
// ... existing config
|
|
585
|
+
fields: [
|
|
586
|
+
// ... existing fields
|
|
587
|
+
{
|
|
588
|
+
name: 'apiKeys',
|
|
589
|
+
type: 'join',
|
|
590
|
+
collection: 'api-keys',
|
|
591
|
+
on: 'user', // The field in api-keys that references users
|
|
592
|
+
},
|
|
593
|
+
],
|
|
594
|
+
}
|
|
595
|
+
```
|
|
596
|
+
|
|
597
|
+
**3. Update slugOverrides in betterAuthCollections:**
|
|
598
|
+
|
|
599
|
+
```ts
|
|
600
|
+
betterAuthCollections({
|
|
601
|
+
betterAuthOptions,
|
|
602
|
+
slugOverrides: { apiKey: 'api-keys' },
|
|
603
|
+
skipCollections: ['user'],
|
|
604
|
+
})
|
|
605
|
+
```
|
|
606
|
+
</details>
|
|
607
|
+
|
|
608
|
+
<details>
|
|
609
|
+
<summary><strong>Two-Factor Auth (TOTP)</strong></summary>
|
|
610
|
+
|
|
611
|
+
The Two-Factor plugin creates a `twoFactor` model with a `userId` reference.
|
|
612
|
+
|
|
613
|
+
**1. Add to your Better Auth config:**
|
|
614
|
+
|
|
615
|
+
```ts
|
|
616
|
+
// src/lib/auth/config.ts
|
|
617
|
+
import { twoFactor } from 'better-auth/plugins'
|
|
618
|
+
|
|
619
|
+
export const betterAuthOptions: Partial<BetterAuthOptions> = {
|
|
620
|
+
// ... existing config
|
|
621
|
+
plugins: [twoFactor()],
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
export const collectionSlugs = {
|
|
625
|
+
// ... existing slugs
|
|
626
|
+
twoFactor: 'two-factors', // Add this
|
|
627
|
+
} as const
|
|
628
|
+
```
|
|
629
|
+
|
|
630
|
+
**2. Add join field to your Users collection:**
|
|
631
|
+
|
|
632
|
+
```ts
|
|
633
|
+
// src/collections/Users.ts
|
|
634
|
+
export const Users: CollectionConfig = {
|
|
635
|
+
slug: 'users',
|
|
636
|
+
fields: [
|
|
637
|
+
// ... existing fields
|
|
638
|
+
{
|
|
639
|
+
name: 'twoFactor',
|
|
640
|
+
type: 'join',
|
|
641
|
+
collection: 'two-factors',
|
|
642
|
+
on: 'user',
|
|
643
|
+
},
|
|
644
|
+
],
|
|
645
|
+
}
|
|
646
|
+
```
|
|
647
|
+
</details>
|
|
648
|
+
|
|
649
|
+
<details>
|
|
650
|
+
<summary><strong>Organizations</strong></summary>
|
|
651
|
+
|
|
652
|
+
The Organizations plugin creates multiple models: `organization`, `member`, and `invitation`.
|
|
653
|
+
|
|
654
|
+
**1. Add to your Better Auth config:**
|
|
655
|
+
|
|
656
|
+
```ts
|
|
657
|
+
// src/lib/auth/config.ts
|
|
658
|
+
import { organization } from 'better-auth/plugins'
|
|
659
|
+
|
|
660
|
+
export const betterAuthOptions: Partial<BetterAuthOptions> = {
|
|
661
|
+
// ... existing config
|
|
662
|
+
plugins: [organization()],
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
export const collectionSlugs = {
|
|
666
|
+
// ... existing slugs
|
|
667
|
+
organization: 'organizations',
|
|
668
|
+
member: 'members',
|
|
669
|
+
invitation: 'invitations',
|
|
670
|
+
} as const
|
|
671
|
+
```
|
|
672
|
+
|
|
673
|
+
**2. Add join fields to your Users collection:**
|
|
674
|
+
|
|
675
|
+
```ts
|
|
676
|
+
// src/collections/Users.ts
|
|
677
|
+
export const Users: CollectionConfig = {
|
|
678
|
+
slug: 'users',
|
|
679
|
+
fields: [
|
|
680
|
+
// ... existing fields
|
|
681
|
+
{
|
|
682
|
+
name: 'memberships',
|
|
683
|
+
type: 'join',
|
|
684
|
+
collection: 'members',
|
|
685
|
+
on: 'user',
|
|
686
|
+
},
|
|
687
|
+
],
|
|
688
|
+
}
|
|
689
|
+
```
|
|
690
|
+
|
|
691
|
+
**3. Create an Organizations collection (or let it auto-generate and add joins):**
|
|
692
|
+
|
|
693
|
+
```ts
|
|
694
|
+
// src/collections/Organizations.ts
|
|
695
|
+
export const Organizations: CollectionConfig = {
|
|
696
|
+
slug: 'organizations',
|
|
697
|
+
admin: { useAsTitle: 'name', group: 'Auth' },
|
|
698
|
+
fields: [
|
|
699
|
+
{ name: 'name', type: 'text', required: true },
|
|
700
|
+
{ name: 'slug', type: 'text', unique: true },
|
|
701
|
+
{ name: 'logo', type: 'text' },
|
|
702
|
+
{ name: 'metadata', type: 'json' },
|
|
703
|
+
{
|
|
704
|
+
name: 'members',
|
|
705
|
+
type: 'join',
|
|
706
|
+
collection: 'members',
|
|
707
|
+
on: 'organization',
|
|
708
|
+
},
|
|
709
|
+
{
|
|
710
|
+
name: 'invitations',
|
|
711
|
+
type: 'join',
|
|
712
|
+
collection: 'invitations',
|
|
713
|
+
on: 'organization',
|
|
714
|
+
},
|
|
715
|
+
],
|
|
716
|
+
}
|
|
717
|
+
```
|
|
718
|
+
</details>
|
|
719
|
+
|
|
720
|
+
### General Pattern for Joins
|
|
721
|
+
|
|
722
|
+
When a Better Auth plugin creates a model with a foreign key (e.g., `userId`, `organizationId`), you need to:
|
|
723
|
+
|
|
724
|
+
1. **Map the collection slug** in `collectionSlugs` config
|
|
725
|
+
2. **Add a join field** to the parent collection pointing to the child collection
|
|
726
|
+
3. **Specify the `on` field** - this is the relationship field name in the child collection (without `Id` suffix)
|
|
727
|
+
|
|
728
|
+
The auto-generated collections create relationship fields like `user` (from `userId`), so your join's `on` property should match that field name.
|
|
729
|
+
|
|
730
|
+
---
|
|
731
|
+
|
|
732
|
+
## License
|
|
733
|
+
|
|
734
|
+
MIT
|