@delmaredigital/payload-better-auth 0.3.6 → 0.3.8
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/README.md +60 -12
- package/dist/adapter/collections.d.ts.map +1 -1
- package/dist/adapter/collections.js +126 -88
- package/dist/adapter/collections.js.map +1 -1
- package/dist/adapter/index.js +197 -150
- package/dist/adapter/index.js.map +1 -1
- package/dist/components/BeforeLogin.d.ts +1 -1
- package/dist/components/BeforeLogin.d.ts.map +1 -1
- package/dist/components/BeforeLogin.js +15 -7
- package/dist/components/BeforeLogin.js.map +1 -1
- package/dist/components/LoginView.d.ts +2 -2
- package/dist/components/LoginView.d.ts.map +1 -1
- package/dist/components/LoginView.js +660 -218
- package/dist/components/LoginView.js.map +1 -1
- package/dist/components/LoginViewWrapper.d.ts +1 -1
- package/dist/components/LoginViewWrapper.d.ts.map +1 -1
- package/dist/components/LoginViewWrapper.js +14 -4
- package/dist/components/LoginViewWrapper.js.map +1 -1
- package/dist/components/LogoutButton.d.ts +1 -1
- package/dist/components/LogoutButton.d.ts.map +1 -1
- package/dist/components/LogoutButton.js +19 -11
- package/dist/components/LogoutButton.js.map +1 -1
- package/dist/components/PasskeyRegisterButton.d.ts +2 -2
- package/dist/components/PasskeyRegisterButton.d.ts.map +1 -1
- package/dist/components/PasskeyRegisterButton.js +20 -16
- package/dist/components/PasskeyRegisterButton.js.map +1 -1
- package/dist/components/PasskeySignInButton.d.ts +2 -2
- package/dist/components/PasskeySignInButton.d.ts.map +1 -1
- package/dist/components/PasskeySignInButton.js +14 -12
- package/dist/components/PasskeySignInButton.js.map +1 -1
- package/dist/components/auth/ForgotPasswordView.d.ts +1 -1
- package/dist/components/auth/ForgotPasswordView.d.ts.map +1 -1
- package/dist/components/auth/ForgotPasswordView.js +133 -43
- package/dist/components/auth/ForgotPasswordView.js.map +1 -1
- package/dist/components/auth/ResetPasswordView.d.ts +1 -1
- package/dist/components/auth/ResetPasswordView.d.ts.map +1 -1
- package/dist/components/auth/ResetPasswordView.js +154 -50
- package/dist/components/auth/ResetPasswordView.js.map +1 -1
- package/dist/components/auth/index.js +2 -2
- package/dist/components/auth/index.js.map +1 -1
- package/dist/components/management/ApiKeysManagementClient.d.ts +2 -2
- package/dist/components/management/ApiKeysManagementClient.d.ts.map +1 -1
- package/dist/components/management/ApiKeysManagementClient.js +539 -222
- package/dist/components/management/ApiKeysManagementClient.js.map +1 -1
- package/dist/components/management/PasskeysManagementClient.d.ts +2 -2
- package/dist/components/management/PasskeysManagementClient.d.ts.map +1 -1
- package/dist/components/management/PasskeysManagementClient.js +215 -92
- package/dist/components/management/PasskeysManagementClient.js.map +1 -1
- package/dist/components/management/SecurityNavLinks.d.ts +1 -1
- package/dist/components/management/SecurityNavLinks.d.ts.map +1 -1
- package/dist/components/management/SecurityNavLinks.js +51 -24
- package/dist/components/management/SecurityNavLinks.js.map +1 -1
- package/dist/components/management/TwoFactorManagementClient.d.ts +2 -2
- package/dist/components/management/TwoFactorManagementClient.d.ts.map +1 -1
- package/dist/components/management/TwoFactorManagementClient.js +270 -111
- package/dist/components/management/TwoFactorManagementClient.js.map +1 -1
- package/dist/components/management/index.js +2 -2
- package/dist/components/management/index.js.map +1 -1
- package/dist/components/management/views/ApiKeysView.d.ts +1 -1
- package/dist/components/management/views/ApiKeysView.d.ts.map +1 -1
- package/dist/components/management/views/ApiKeysView.js +19 -4
- package/dist/components/management/views/ApiKeysView.js.map +1 -1
- package/dist/components/management/views/PasskeysView.d.ts +1 -1
- package/dist/components/management/views/PasskeysView.d.ts.map +1 -1
- package/dist/components/management/views/PasskeysView.js +16 -4
- package/dist/components/management/views/PasskeysView.js.map +1 -1
- package/dist/components/management/views/TwoFactorView.d.ts +1 -1
- package/dist/components/management/views/TwoFactorView.d.ts.map +1 -1
- package/dist/components/management/views/TwoFactorView.js +16 -4
- package/dist/components/management/views/TwoFactorView.js.map +1 -1
- package/dist/components/management/views/index.js +2 -2
- package/dist/components/management/views/index.js.map +1 -1
- package/dist/components/twoFactor/TwoFactorSetupView.d.ts +1 -1
- package/dist/components/twoFactor/TwoFactorSetupView.d.ts.map +1 -1
- package/dist/components/twoFactor/TwoFactorSetupView.js +240 -87
- package/dist/components/twoFactor/TwoFactorSetupView.js.map +1 -1
- package/dist/components/twoFactor/TwoFactorVerifyView.d.ts +1 -1
- package/dist/components/twoFactor/TwoFactorVerifyView.d.ts.map +1 -1
- package/dist/components/twoFactor/TwoFactorVerifyView.js +108 -45
- package/dist/components/twoFactor/TwoFactorVerifyView.js.map +1 -1
- package/dist/components/twoFactor/index.js +2 -2
- package/dist/components/twoFactor/index.js.map +1 -1
- package/dist/exports/client.d.ts +2356 -2
- package/dist/exports/client.d.ts.map +1 -1
- package/dist/exports/client.js +48 -8
- package/dist/exports/client.js.map +1 -1
- package/dist/exports/components.js +2 -2
- package/dist/exports/components.js.map +1 -1
- package/dist/exports/management.js +3 -3
- package/dist/exports/management.js.map +1 -1
- package/dist/exports/rsc.js +2 -2
- package/dist/exports/rsc.js.map +1 -1
- package/dist/generated-types.js +4 -2
- package/dist/generated-types.js.map +1 -1
- package/dist/index.js +6 -6
- package/dist/index.js.map +1 -1
- package/dist/plugin/index.d.ts +35 -2
- package/dist/plugin/index.d.ts.map +1 -1
- package/dist/plugin/index.js +198 -162
- package/dist/plugin/index.js.map +1 -1
- package/dist/scripts/generate-types.js +66 -50
- package/dist/scripts/generate-types.js.map +1 -1
- package/dist/types/apiKey.js +7 -2
- package/dist/types/apiKey.js.map +1 -1
- package/dist/types/betterAuth.js +23 -2
- package/dist/types/betterAuth.js.map +1 -1
- package/dist/utils/access.js +78 -81
- package/dist/utils/access.js.map +1 -1
- package/dist/utils/apiKeyAccess.js +65 -72
- package/dist/utils/apiKeyAccess.js.map +1 -1
- package/dist/utils/betterAuthDefaults.js +8 -8
- package/dist/utils/betterAuthDefaults.js.map +1 -1
- package/dist/utils/detectAuthConfig.js +8 -11
- package/dist/utils/detectAuthConfig.js.map +1 -1
- package/dist/utils/detectEnabledPlugins.js +6 -7
- package/dist/utils/detectEnabledPlugins.js.map +1 -1
- package/dist/utils/firstUserAdmin.js +18 -20
- package/dist/utils/firstUserAdmin.js.map +1 -1
- package/dist/utils/generateScopes.js +40 -41
- package/dist/utils/generateScopes.js.map +1 -1
- package/dist/utils/session.js +8 -9
- package/dist/utils/session.js.map +1 -1
- package/package.json +97 -26
- package/src/adapter/collections.ts +621 -0
- package/src/adapter/index.ts +712 -0
- package/src/components/BeforeLogin.tsx +39 -0
- package/src/components/LoginView.tsx +1516 -0
- package/src/components/LoginViewWrapper.tsx +35 -0
- package/src/components/LogoutButton.tsx +58 -0
- package/src/components/PasskeyRegisterButton.tsx +105 -0
- package/src/components/PasskeySignInButton.tsx +96 -0
- package/src/components/auth/ForgotPasswordView.tsx +274 -0
- package/src/components/auth/ResetPasswordView.tsx +331 -0
- package/src/components/auth/index.ts +8 -0
- package/src/components/management/ApiKeysManagementClient.tsx +988 -0
- package/src/components/management/PasskeysManagementClient.tsx +409 -0
- package/src/components/management/SecurityNavLinks.tsx +117 -0
- package/src/components/management/TwoFactorManagementClient.tsx +560 -0
- package/src/components/management/index.ts +20 -0
- package/src/components/management/views/ApiKeysView.tsx +57 -0
- package/src/components/management/views/PasskeysView.tsx +42 -0
- package/src/components/management/views/TwoFactorView.tsx +42 -0
- package/src/components/management/views/index.ts +10 -0
- package/src/components/twoFactor/TwoFactorSetupView.tsx +515 -0
- package/src/components/twoFactor/TwoFactorVerifyView.tsx +238 -0
- package/src/components/twoFactor/index.ts +8 -0
- package/src/exports/client.ts +77 -0
- package/src/exports/components.ts +30 -0
- package/src/exports/management.ts +25 -0
- package/src/exports/rsc.ts +11 -0
- package/src/generated-types.ts +269 -0
- package/src/index.ts +135 -0
- package/src/plugin/index.ts +834 -0
- package/src/scripts/generate-types.ts +269 -0
- package/src/types/apiKey.ts +63 -0
- package/src/types/betterAuth.ts +253 -0
- package/src/utils/access.ts +410 -0
- package/src/utils/apiKeyAccess.ts +443 -0
- package/src/utils/betterAuthDefaults.ts +102 -0
- package/src/utils/detectAuthConfig.ts +47 -0
- package/src/utils/detectEnabledPlugins.ts +69 -0
- package/src/utils/firstUserAdmin.ts +164 -0
- package/src/utils/generateScopes.ts +150 -0
- package/src/utils/session.ts +91 -0
package/README.md
CHANGED
|
@@ -209,7 +209,7 @@ export default buildConfig({
|
|
|
209
209
|
})
|
|
210
210
|
```
|
|
211
211
|
|
|
212
|
-
> **⚠️
|
|
212
|
+
> **⚠️ Note:** The plugin automatically injects its own login page, logout button, and redirect handling. Don't add a custom `beforeLogin` in Payload's `admin.components` directly - use the plugin's options instead (see [Customization](#customization) for `disableLoginView`, `loginViewComponent`, etc.).
|
|
213
213
|
|
|
214
214
|
### Step 4: Client-Side Auth
|
|
215
215
|
|
|
@@ -228,12 +228,29 @@ export const { useSession, signIn, signUp, signOut, twoFactor, passkey } = authC
|
|
|
228
228
|
|
|
229
229
|
**Note:** `createPayloadAuthClient()` automatically uses `window.location.origin` as the base URL, so it works seamlessly across local dev, Vercel previews, and production without any configuration.
|
|
230
230
|
|
|
231
|
-
|
|
231
|
+
**Adding custom plugins (e.g., Stripe):**
|
|
232
|
+
|
|
233
|
+
For custom plugins with full TypeScript support, use `createAuthClient` with `payloadAuthPlugins`:
|
|
232
234
|
|
|
233
235
|
```ts
|
|
234
|
-
|
|
236
|
+
// src/lib/auth/client.ts
|
|
237
|
+
'use client'
|
|
238
|
+
|
|
239
|
+
import { createAuthClient, payloadAuthPlugins } from '@delmaredigital/payload-better-auth/client'
|
|
240
|
+
import { stripeClient } from '@better-auth/stripe/client'
|
|
241
|
+
|
|
242
|
+
// Spread payloadAuthPlugins to include defaults (twoFactor, apiKey, passkey)
|
|
243
|
+
// Then add your custom plugins - full type safety!
|
|
244
|
+
export const authClient = createAuthClient({
|
|
245
|
+
plugins: [...payloadAuthPlugins, stripeClient({ subscription: true })],
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
// authClient.subscription is fully typed
|
|
249
|
+
export const { useSession, signIn, signUp, signOut, twoFactor, passkey, subscription } = authClient
|
|
235
250
|
```
|
|
236
251
|
|
|
252
|
+
This approach uses Better Auth's native `createAuthClient` with our default plugins, giving you full type inference for any custom plugins you add.
|
|
253
|
+
|
|
237
254
|
### Step 5: Server-Side Session Access
|
|
238
255
|
|
|
239
256
|
```ts
|
|
@@ -350,12 +367,23 @@ betterAuthCollections({
|
|
|
350
367
|
| `betterAuthOptions` | `BetterAuthOptions` | Your Better Auth options |
|
|
351
368
|
| `skipCollections` | `string[]` | Collections to skip generating (default: `['user']`) |
|
|
352
369
|
| `adminGroup` | `string` | Admin panel group name (default: `'Auth'`) |
|
|
353
|
-
| `access` | `CollectionConfig['access']` | Custom access control for generated collections |
|
|
370
|
+
| `access` | `CollectionConfig['access']` | Custom access control for generated collections. **Note**: Replaces default access entirely (see caution below). |
|
|
354
371
|
| `usePlural` | `boolean` | Pluralize collection slugs (default: `true`) |
|
|
355
372
|
| `configureSaveToJWT` | `boolean` | Auto-configure `saveToJWT` for session-critical fields (default: `true`) |
|
|
356
373
|
| `firstUserAdmin` | `boolean \| FirstUserAdminOptions` | Make first registered user an admin (default: `true`) |
|
|
357
374
|
| `customizeCollection` | `(modelKey, collection) => CollectionConfig` | Customize generated collections |
|
|
358
375
|
|
|
376
|
+
> **⚠️ Caution on Custom Access:**
|
|
377
|
+
> When providing the `access` option, it **completely replaces** the default access object for all auto-generated collections. It does not merge with or override individual properties.
|
|
378
|
+
>
|
|
379
|
+
> By default, the plugin sets:
|
|
380
|
+
> - `read`: `isAdmin()`
|
|
381
|
+
> - `delete`: `isAdmin()`
|
|
382
|
+
> - `create`: `() => false` (Manual creation disabled - Better Auth manages these)
|
|
383
|
+
> - `update`: `() => false` (Manual update disabled - Better Auth manages these)
|
|
384
|
+
>
|
|
385
|
+
> You must explicitly handle all access types to ensure your collections remain secure and functional.
|
|
386
|
+
|
|
359
387
|
**First User Admin:**
|
|
360
388
|
|
|
361
389
|
By default, the first user to register is automatically assigned the admin role. This provides a better out-of-the-box experience - no need to manually update the database to create your first admin.
|
|
@@ -428,7 +456,7 @@ createBetterAuthPlugin({
|
|
|
428
456
|
| `admin.login.afterLoginPath` | `string` | `'/admin'` | Redirect path after successful login |
|
|
429
457
|
| `admin.login.requiredRole` | `string \| string[] \| null` | `'admin'` | Required role(s) for admin access. Array = any role matches (unless `requireAllRoles`). Set to `null` to disable. |
|
|
430
458
|
| `admin.login.requireAllRoles` | `boolean` | `false` | When `requiredRole` is an array, require ALL roles (true) or ANY role (false). |
|
|
431
|
-
| `admin.login.enablePasskey` | `boolean` | `false` | Enable passkey (WebAuthn) sign-in option |
|
|
459
|
+
| `admin.login.enablePasskey` | `boolean \| 'auto'` | `false` | Enable passkey (WebAuthn) sign-in option. `'auto'` detects if passkey plugin is available. |
|
|
432
460
|
| `admin.login.enableSignUp` | `boolean \| 'auto'` | `'auto'` | Enable user registration. `'auto'` detects if sign-up endpoint is available. |
|
|
433
461
|
| `admin.login.defaultSignUpRole` | `string` | `'user'` | Default role assigned to new users during registration |
|
|
434
462
|
| `admin.login.enableForgotPassword` | `boolean \| 'auto'` | `'auto'` | Enable forgot password link. `'auto'` detects if endpoint is available. |
|
|
@@ -679,6 +707,17 @@ This is useful when you need:
|
|
|
679
707
|
- Integration with external identity providers
|
|
680
708
|
- Custom branding or UI requirements
|
|
681
709
|
|
|
710
|
+
**Frontend Login (outside admin panel):**
|
|
711
|
+
|
|
712
|
+
For user-facing login pages (not the Payload admin), you don't need to configure anything in the plugin. Just use the auth client directly in your own React components:
|
|
713
|
+
|
|
714
|
+
```tsx
|
|
715
|
+
import { authClient } from '@/lib/auth/client'
|
|
716
|
+
|
|
717
|
+
// Use authClient.signIn.email(), authClient.signUp.email(), etc.
|
|
718
|
+
// See "Handling 2FA in Custom Login Forms" section below for a complete example
|
|
719
|
+
```
|
|
720
|
+
|
|
682
721
|
### Custom Admin Components
|
|
683
722
|
|
|
684
723
|
Override specific admin components while keeping others auto-injected:
|
|
@@ -1152,15 +1191,18 @@ export const auth = betterAuth({
|
|
|
1152
1191
|
|
|
1153
1192
|
### User Registration
|
|
1154
1193
|
|
|
1155
|
-
The `LoginView`
|
|
1194
|
+
The `LoginView` **automatically detects** if user registration is available by checking Better Auth's sign-up endpoint. If your Better Auth config has `emailAndPassword.enabled: true` (and not `disableSignUp: true`), the "Create account" link appears automatically.
|
|
1156
1195
|
|
|
1157
|
-
**
|
|
1196
|
+
**No configuration needed** for most cases - it just works based on your Better Auth settings.
|
|
1197
|
+
|
|
1198
|
+
**Optional overrides** (only if you need to force show/hide):
|
|
1158
1199
|
```typescript
|
|
1159
1200
|
createBetterAuthPlugin({
|
|
1160
1201
|
createAuth,
|
|
1161
1202
|
admin: {
|
|
1162
1203
|
login: {
|
|
1163
|
-
|
|
1204
|
+
// These options override auto-detection (usually not needed)
|
|
1205
|
+
enableSignUp: 'auto', // 'auto' (default) | true | false
|
|
1164
1206
|
defaultSignUpRole: 'user', // Role assigned to new users (default: 'user')
|
|
1165
1207
|
},
|
|
1166
1208
|
},
|
|
@@ -1168,22 +1210,28 @@ createBetterAuthPlugin({
|
|
|
1168
1210
|
```
|
|
1169
1211
|
|
|
1170
1212
|
**Notes:**
|
|
1213
|
+
- `'auto'` (default): Detects availability from Better Auth's sign-up endpoint
|
|
1214
|
+
- `true`: Always show registration (even if Better Auth returns 404)
|
|
1215
|
+
- `false`: Never show registration
|
|
1171
1216
|
- New users are assigned `defaultSignUpRole` (default: `'user'`)
|
|
1172
1217
|
- If email verification is required, users see a success message to check their email
|
|
1173
1218
|
- Role-based access control still applies - users without `requiredRole` see "Access Denied"
|
|
1174
1219
|
|
|
1175
1220
|
### Password Reset Flow
|
|
1176
1221
|
|
|
1177
|
-
The `LoginView`
|
|
1222
|
+
The `LoginView` **automatically detects** if password reset is available by checking Better Auth's reset endpoint. The "Forgot password?" link appears automatically when available.
|
|
1178
1223
|
|
|
1179
|
-
**
|
|
1224
|
+
**No configuration needed** for most cases - it just works based on your Better Auth settings.
|
|
1225
|
+
|
|
1226
|
+
**Optional overrides** (only if you need to force show/hide or use a custom URL):
|
|
1180
1227
|
```typescript
|
|
1181
1228
|
createBetterAuthPlugin({
|
|
1182
1229
|
createAuth,
|
|
1183
1230
|
admin: {
|
|
1184
1231
|
login: {
|
|
1185
|
-
|
|
1186
|
-
|
|
1232
|
+
// These options override auto-detection (usually not needed)
|
|
1233
|
+
enableForgotPassword: 'auto', // 'auto' (default) | true | false
|
|
1234
|
+
resetPasswordUrl: '/custom-reset', // Optional: redirect to custom page instead of inline form
|
|
1187
1235
|
},
|
|
1188
1236
|
},
|
|
1189
1237
|
})
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"collections.d.ts","sourceRoot":"","sources":["../../src/adapter/collections.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAU,gBAAgB,EAAS,MAAM,EAA8B,MAAM,SAAS,CAAA;AAClG,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAA;AAEpD,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAA;
|
|
1
|
+
{"version":3,"file":"collections.d.ts","sourceRoot":"","sources":["../../src/adapter/collections.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAU,gBAAgB,EAAS,MAAM,EAA8B,MAAM,SAAS,CAAA;AAClG,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAA;AAEpD,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAA;AAGvE,YAAY,EAAE,qBAAqB,EAAE,CAAA;AAErC,MAAM,MAAM,4BAA4B,GAAG;IACzC;;;OAGG;IACH,iBAAiB,CAAC,EAAE,iBAAiB,CAAA;IAErC;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,EAAE,CAAA;IAE1B;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;IAEnB;;;OAGG;IACH,MAAM,CAAC,EAAE,gBAAgB,CAAC,QAAQ,CAAC,CAAA;IAEnC;;;;OAIG;IACH,SAAS,CAAC,EAAE,OAAO,CAAA;IAEnB;;;;OAIG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAA;IAE5B;;;;;;;;;;;;;;;;;;;;;;;;OAwBG;IACH,cAAc,CAAC,EAAE,OAAO,GAAG,qBAAqB,CAAA;IAEhD;;;;;;;;;;;;;;;;;;OAkBG;IACH,mBAAmB,CAAC,EAAE,CACpB,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,gBAAgB,KACzB,gBAAgB,CAAA;CACtB,CAAA;AA2XD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,GAAE,4BAAiC,GACzC,MAAM,CAuGR"}
|
|
@@ -2,69 +2,82 @@
|
|
|
2
2
|
* Auto-generate Payload collections from Better Auth schema
|
|
3
3
|
*
|
|
4
4
|
* @packageDocumentation
|
|
5
|
-
*/
|
|
6
|
-
import {
|
|
5
|
+
*/ import { getAuthTables } from 'better-auth/db';
|
|
6
|
+
import { isAdmin } from '../utils/access.js';
|
|
7
7
|
/**
|
|
8
8
|
* Creates a beforeChange hook that makes the first user an admin.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
return async ({ data, operation, req }) => {
|
|
9
|
+
*/ function createFirstUserAdminHook(options, usersSlug) {
|
|
10
|
+
const { adminRole = 'admin', defaultRole = 'user', roleField = 'role' } = options;
|
|
11
|
+
return async ({ data, operation, req })=>{
|
|
13
12
|
if (operation !== 'create') {
|
|
14
13
|
return data;
|
|
15
14
|
}
|
|
16
15
|
try {
|
|
17
16
|
const { totalDocs } = await req.payload.count({
|
|
18
17
|
collection: usersSlug,
|
|
19
|
-
overrideAccess: true
|
|
18
|
+
overrideAccess: true
|
|
20
19
|
});
|
|
21
20
|
if (totalDocs === 0) {
|
|
22
21
|
// First user becomes admin
|
|
23
22
|
return {
|
|
24
23
|
...data,
|
|
25
|
-
[roleField]: adminRole
|
|
24
|
+
[roleField]: adminRole
|
|
26
25
|
};
|
|
27
26
|
}
|
|
28
27
|
// Subsequent users get default role if not already set
|
|
29
28
|
return {
|
|
30
29
|
...data,
|
|
31
|
-
[roleField]: data[roleField] ?? defaultRole
|
|
30
|
+
[roleField]: data[roleField] ?? defaultRole
|
|
32
31
|
};
|
|
33
|
-
}
|
|
34
|
-
catch (error) {
|
|
32
|
+
} catch (error) {
|
|
35
33
|
// On error, don't block user creation - just use provided or default role
|
|
36
34
|
console.warn('[betterAuthCollections] Failed to check user count:', error);
|
|
37
35
|
return {
|
|
38
36
|
...data,
|
|
39
|
-
[roleField]: data[roleField] ?? defaultRole
|
|
37
|
+
[roleField]: data[roleField] ?? defaultRole
|
|
40
38
|
};
|
|
41
39
|
}
|
|
42
40
|
};
|
|
43
41
|
}
|
|
44
42
|
/**
|
|
45
43
|
* Inject the first-user-admin hook into a collection's hooks.
|
|
46
|
-
*/
|
|
47
|
-
function injectFirstUserAdminHook(collection, options, usersSlug) {
|
|
44
|
+
*/ function injectFirstUserAdminHook(collection, options, usersSlug) {
|
|
48
45
|
const hook = createFirstUserAdminHook(options, usersSlug);
|
|
49
46
|
const existingHooks = collection.hooks?.beforeChange ?? [];
|
|
50
47
|
return {
|
|
51
48
|
...collection,
|
|
52
49
|
hooks: {
|
|
53
50
|
...collection.hooks,
|
|
54
|
-
beforeChange: [
|
|
55
|
-
|
|
51
|
+
beforeChange: [
|
|
52
|
+
hook,
|
|
53
|
+
...Array.isArray(existingHooks) ? existingHooks : [
|
|
54
|
+
existingHooks
|
|
55
|
+
]
|
|
56
|
+
]
|
|
57
|
+
}
|
|
56
58
|
};
|
|
57
59
|
}
|
|
58
60
|
/**
|
|
59
61
|
* Determine if a field should be saved to JWT.
|
|
60
62
|
* Session-critical fields are included, large data fields are excluded.
|
|
61
|
-
*/
|
|
62
|
-
function getSaveToJWT(modelKey, fieldName) {
|
|
63
|
+
*/ function getSaveToJWT(modelKey, fieldName) {
|
|
63
64
|
// Session fields - include core session data
|
|
64
65
|
if (modelKey === 'session') {
|
|
65
|
-
const includeFields = [
|
|
66
|
-
|
|
67
|
-
|
|
66
|
+
const includeFields = [
|
|
67
|
+
'token',
|
|
68
|
+
'expiresAt',
|
|
69
|
+
'user',
|
|
70
|
+
'userId',
|
|
71
|
+
'ipAddress',
|
|
72
|
+
'userAgent',
|
|
73
|
+
'activeOrganizationId',
|
|
74
|
+
'activeTeamId'
|
|
75
|
+
];
|
|
76
|
+
const excludeFields = [
|
|
77
|
+
'createdAt',
|
|
78
|
+
'updatedAt'
|
|
79
|
+
];
|
|
80
|
+
if (includeFields.some((f)=>fieldName === f || fieldName.endsWith(f.charAt(0).toUpperCase() + f.slice(1)))) {
|
|
68
81
|
return true;
|
|
69
82
|
}
|
|
70
83
|
if (excludeFields.includes(fieldName)) {
|
|
@@ -73,8 +86,19 @@ function getSaveToJWT(modelKey, fieldName) {
|
|
|
73
86
|
}
|
|
74
87
|
// User fields - include essential auth data
|
|
75
88
|
if (modelKey === 'user') {
|
|
76
|
-
const includeFields = [
|
|
77
|
-
|
|
89
|
+
const includeFields = [
|
|
90
|
+
'role',
|
|
91
|
+
'email',
|
|
92
|
+
'emailVerified',
|
|
93
|
+
'name',
|
|
94
|
+
'twoFactorEnabled',
|
|
95
|
+
'banned'
|
|
96
|
+
];
|
|
97
|
+
const excludeFields = [
|
|
98
|
+
'image',
|
|
99
|
+
'password',
|
|
100
|
+
'banReason'
|
|
101
|
+
];
|
|
78
102
|
if (includeFields.includes(fieldName)) {
|
|
79
103
|
return true;
|
|
80
104
|
}
|
|
@@ -95,17 +119,15 @@ function getSaveToJWT(modelKey, fieldName) {
|
|
|
95
119
|
}
|
|
96
120
|
/**
|
|
97
121
|
* Simple pluralization (add 's' suffix)
|
|
98
|
-
*/
|
|
99
|
-
|
|
100
|
-
if (name.endsWith('s'))
|
|
101
|
-
return name;
|
|
122
|
+
*/ function pluralize(name) {
|
|
123
|
+
if (name.endsWith('s')) return name;
|
|
102
124
|
return `${name}s`;
|
|
103
125
|
}
|
|
104
126
|
function mapFieldType(type, fieldName, hasReferences) {
|
|
105
127
|
if (hasReferences) {
|
|
106
128
|
return 'relationship';
|
|
107
129
|
}
|
|
108
|
-
switch
|
|
130
|
+
switch(type){
|
|
109
131
|
case 'boolean':
|
|
110
132
|
return 'checkbox';
|
|
111
133
|
case 'number':
|
|
@@ -113,15 +135,15 @@ function mapFieldType(type, fieldName, hasReferences) {
|
|
|
113
135
|
case 'date':
|
|
114
136
|
return 'date';
|
|
115
137
|
case 'string':
|
|
116
|
-
if (fieldName === 'email')
|
|
117
|
-
return 'email';
|
|
138
|
+
if (fieldName === 'email') return 'email';
|
|
118
139
|
return 'text';
|
|
119
140
|
case 'json':
|
|
120
141
|
case 'object':
|
|
121
142
|
return 'json';
|
|
122
143
|
case 'string[]':
|
|
123
144
|
case 'array':
|
|
124
|
-
return 'json'
|
|
145
|
+
return 'json' // Payload doesn't have native string array, use JSON
|
|
146
|
+
;
|
|
125
147
|
default:
|
|
126
148
|
return 'text';
|
|
127
149
|
}
|
|
@@ -135,8 +157,12 @@ function generateCollection(modelKey, table, usePlural, adminGroup, customAccess
|
|
|
135
157
|
const baseName = table.modelName ?? modelKey;
|
|
136
158
|
const slug = usePlural ? pluralize(baseName) : baseName;
|
|
137
159
|
const fields = [];
|
|
138
|
-
for (const [fieldKey, fieldDef] of Object.entries(table.fields))
|
|
139
|
-
if ([
|
|
160
|
+
for (const [fieldKey, fieldDef] of Object.entries(table.fields)){
|
|
161
|
+
if ([
|
|
162
|
+
'id',
|
|
163
|
+
'createdAt',
|
|
164
|
+
'updatedAt'
|
|
165
|
+
].includes(fieldKey)) {
|
|
140
166
|
continue;
|
|
141
167
|
}
|
|
142
168
|
const fieldName = fieldDef.fieldName ?? fieldKey;
|
|
@@ -147,8 +173,7 @@ function generateCollection(modelKey, table, usePlural, adminGroup, customAccess
|
|
|
147
173
|
let relationTo;
|
|
148
174
|
if (fieldDef.references?.model) {
|
|
149
175
|
relationTo = usePlural ? pluralize(fieldDef.references.model) : fieldDef.references.model;
|
|
150
|
-
}
|
|
151
|
-
else {
|
|
176
|
+
} else {
|
|
152
177
|
relationTo = extractRelationTarget(fieldKey, usePlural);
|
|
153
178
|
}
|
|
154
179
|
const relFieldName = fieldName.replace(/(_id|Id)$/, '');
|
|
@@ -159,7 +184,9 @@ function generateCollection(modelKey, table, usePlural, adminGroup, customAccess
|
|
|
159
184
|
relationTo,
|
|
160
185
|
required: fieldDef.required ?? false,
|
|
161
186
|
index: true,
|
|
162
|
-
...
|
|
187
|
+
...saveToJWT !== undefined && {
|
|
188
|
+
saveToJWT
|
|
189
|
+
}
|
|
163
190
|
});
|
|
164
191
|
continue;
|
|
165
192
|
}
|
|
@@ -167,10 +194,11 @@ function generateCollection(modelKey, table, usePlural, adminGroup, customAccess
|
|
|
167
194
|
const field = {
|
|
168
195
|
name: fieldName,
|
|
169
196
|
type: fieldType,
|
|
170
|
-
...
|
|
197
|
+
...saveToJWT !== undefined && {
|
|
198
|
+
saveToJWT
|
|
199
|
+
}
|
|
171
200
|
};
|
|
172
|
-
if (fieldDef.required)
|
|
173
|
-
field.required = true;
|
|
201
|
+
if (fieldDef.required) field.required = true;
|
|
174
202
|
if (fieldDef.unique) {
|
|
175
203
|
field.unique = true;
|
|
176
204
|
field.index = true;
|
|
@@ -180,8 +208,7 @@ function generateCollection(modelKey, table, usePlural, adminGroup, customAccess
|
|
|
180
208
|
if (typeof defaultValue === 'function') {
|
|
181
209
|
try {
|
|
182
210
|
defaultValue = defaultValue();
|
|
183
|
-
}
|
|
184
|
-
catch {
|
|
211
|
+
} catch {
|
|
185
212
|
defaultValue = undefined;
|
|
186
213
|
}
|
|
187
214
|
}
|
|
@@ -191,33 +218,37 @@ function generateCollection(modelKey, table, usePlural, adminGroup, customAccess
|
|
|
191
218
|
}
|
|
192
219
|
fields.push(field);
|
|
193
220
|
}
|
|
194
|
-
const titleField = [
|
|
221
|
+
const titleField = [
|
|
222
|
+
'name',
|
|
223
|
+
'email',
|
|
224
|
+
'title',
|
|
225
|
+
'identifier'
|
|
226
|
+
].find((f)=>fields.some((field)=>'name' in field && field.name === f));
|
|
195
227
|
// Default access: admin-only read/delete, disabled manual create/update via admin UI
|
|
196
228
|
// The adapter uses overrideAccess: true for programmatic operations from Better Auth
|
|
197
229
|
const defaultAccess = {
|
|
198
|
-
read: (
|
|
199
|
-
create: ()
|
|
200
|
-
update: ()
|
|
201
|
-
delete: (
|
|
230
|
+
read: isAdmin(),
|
|
231
|
+
create: ()=>false,
|
|
232
|
+
update: ()=>false,
|
|
233
|
+
delete: isAdmin()
|
|
202
234
|
};
|
|
203
235
|
return {
|
|
204
236
|
slug,
|
|
205
237
|
admin: {
|
|
206
238
|
useAsTitle: titleField ?? 'id',
|
|
207
239
|
group: adminGroup,
|
|
208
|
-
description: `Auto-generated from Better Auth schema (${modelKey})
|
|
240
|
+
description: `Auto-generated from Better Auth schema (${modelKey})`
|
|
209
241
|
},
|
|
210
242
|
access: customAccess ?? defaultAccess,
|
|
211
243
|
fields,
|
|
212
|
-
timestamps: true
|
|
244
|
+
timestamps: true
|
|
213
245
|
};
|
|
214
246
|
}
|
|
215
247
|
/**
|
|
216
248
|
* Get existing field names from a collection, handling nested field structures.
|
|
217
|
-
*/
|
|
218
|
-
function getExistingFieldNames(fields) {
|
|
249
|
+
*/ function getExistingFieldNames(fields) {
|
|
219
250
|
const names = new Set();
|
|
220
|
-
for (const field of fields)
|
|
251
|
+
for (const field of fields){
|
|
221
252
|
if ('name' in field && field.name) {
|
|
222
253
|
names.add(field.name);
|
|
223
254
|
}
|
|
@@ -227,21 +258,22 @@ function getExistingFieldNames(fields) {
|
|
|
227
258
|
/**
|
|
228
259
|
* Augment an existing collection with missing fields from Better Auth schema.
|
|
229
260
|
* This ensures user-defined collections (like 'users') get plugin fields automatically.
|
|
230
|
-
*/
|
|
231
|
-
function augmentCollectionWithMissingFields(collection, table, usePlural, modelKey, configureSaveToJWT = true) {
|
|
261
|
+
*/ function augmentCollectionWithMissingFields(collection, table, usePlural, modelKey, configureSaveToJWT = true) {
|
|
232
262
|
const existingFieldNames = getExistingFieldNames(collection.fields);
|
|
233
263
|
const missingFields = [];
|
|
234
|
-
for (const [fieldKey, fieldDef] of Object.entries(table.fields))
|
|
264
|
+
for (const [fieldKey, fieldDef] of Object.entries(table.fields)){
|
|
235
265
|
// Skip standard fields that Payload handles
|
|
236
|
-
if ([
|
|
266
|
+
if ([
|
|
267
|
+
'id',
|
|
268
|
+
'createdAt',
|
|
269
|
+
'updatedAt'
|
|
270
|
+
].includes(fieldKey)) {
|
|
237
271
|
continue;
|
|
238
272
|
}
|
|
239
273
|
const fieldName = fieldDef.fieldName ?? fieldKey;
|
|
240
274
|
const hasReferences = fieldDef.references !== undefined;
|
|
241
275
|
// For reference fields, check the name without Id suffix
|
|
242
|
-
const payloadFieldName = hasReferences
|
|
243
|
-
? fieldName.replace(/(_id|Id)$/, '')
|
|
244
|
-
: fieldName;
|
|
276
|
+
const payloadFieldName = hasReferences ? fieldName.replace(/(_id|Id)$/, '') : fieldName;
|
|
245
277
|
// Skip if field already exists
|
|
246
278
|
if (existingFieldNames.has(payloadFieldName)) {
|
|
247
279
|
continue;
|
|
@@ -252,8 +284,7 @@ function augmentCollectionWithMissingFields(collection, table, usePlural, modelK
|
|
|
252
284
|
let relationTo;
|
|
253
285
|
if (fieldDef.references?.model) {
|
|
254
286
|
relationTo = usePlural ? pluralize(fieldDef.references.model) : fieldDef.references.model;
|
|
255
|
-
}
|
|
256
|
-
else {
|
|
287
|
+
} else {
|
|
257
288
|
relationTo = extractRelationTarget(fieldKey, usePlural);
|
|
258
289
|
}
|
|
259
290
|
const saveToJWT = configureSaveToJWT ? getSaveToJWT(modelKey, payloadFieldName) : undefined;
|
|
@@ -264,23 +295,25 @@ function augmentCollectionWithMissingFields(collection, table, usePlural, modelK
|
|
|
264
295
|
required: fieldDef.required ?? false,
|
|
265
296
|
index: true,
|
|
266
297
|
admin: {
|
|
267
|
-
description: `Auto-added by Better Auth (${fieldKey})
|
|
298
|
+
description: `Auto-added by Better Auth (${fieldKey})`
|
|
268
299
|
},
|
|
269
|
-
...
|
|
300
|
+
...saveToJWT !== undefined && {
|
|
301
|
+
saveToJWT
|
|
302
|
+
}
|
|
270
303
|
});
|
|
271
|
-
}
|
|
272
|
-
else {
|
|
304
|
+
} else {
|
|
273
305
|
const saveToJWT = configureSaveToJWT ? getSaveToJWT(modelKey, payloadFieldName) : undefined;
|
|
274
306
|
const field = {
|
|
275
307
|
name: payloadFieldName,
|
|
276
308
|
type: fieldType,
|
|
277
309
|
admin: {
|
|
278
|
-
description: `Auto-added by Better Auth (${fieldKey})
|
|
310
|
+
description: `Auto-added by Better Auth (${fieldKey})`
|
|
279
311
|
},
|
|
280
|
-
...
|
|
312
|
+
...saveToJWT !== undefined && {
|
|
313
|
+
saveToJWT
|
|
314
|
+
}
|
|
281
315
|
};
|
|
282
|
-
if (fieldDef.required)
|
|
283
|
-
field.required = true;
|
|
316
|
+
if (fieldDef.required) field.required = true;
|
|
284
317
|
if (fieldDef.unique) {
|
|
285
318
|
field.unique = true;
|
|
286
319
|
field.index = true;
|
|
@@ -290,8 +323,7 @@ function augmentCollectionWithMissingFields(collection, table, usePlural, modelK
|
|
|
290
323
|
if (typeof defaultValue === 'function') {
|
|
291
324
|
try {
|
|
292
325
|
defaultValue = defaultValue();
|
|
293
|
-
}
|
|
294
|
-
catch {
|
|
326
|
+
} catch {
|
|
295
327
|
defaultValue = undefined;
|
|
296
328
|
}
|
|
297
329
|
}
|
|
@@ -309,7 +341,10 @@ function augmentCollectionWithMissingFields(collection, table, usePlural, modelK
|
|
|
309
341
|
// Return augmented collection
|
|
310
342
|
return {
|
|
311
343
|
...collection,
|
|
312
|
-
fields: [
|
|
344
|
+
fields: [
|
|
345
|
+
...collection.fields,
|
|
346
|
+
...missingFields
|
|
347
|
+
]
|
|
313
348
|
};
|
|
314
349
|
}
|
|
315
350
|
/**
|
|
@@ -344,26 +379,25 @@ function augmentCollectionWithMissingFields(collection, table, usePlural, modelK
|
|
|
344
379
|
* },
|
|
345
380
|
* })
|
|
346
381
|
* ```
|
|
347
|
-
*/
|
|
348
|
-
|
|
349
|
-
|
|
382
|
+
*/ export function betterAuthCollections(options = {}) {
|
|
383
|
+
const { betterAuthOptions = {}, skipCollections = [
|
|
384
|
+
'user'
|
|
385
|
+
], adminGroup = 'Auth', access, usePlural = true, configureSaveToJWT = true, firstUserAdmin, customizeCollection } = options;
|
|
350
386
|
// Parse firstUserAdmin option (defaults to true)
|
|
351
|
-
const firstUserAdminOptions = firstUserAdmin === false
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
387
|
+
const firstUserAdminOptions = firstUserAdmin === false ? null : typeof firstUserAdmin === 'object' ? firstUserAdmin : {} // true or undefined = enabled with defaults
|
|
388
|
+
;
|
|
389
|
+
return (incomingConfig)=>{
|
|
390
|
+
const existingCollections = new Map((incomingConfig.collections ?? []).map((c)=>[
|
|
391
|
+
c.slug,
|
|
392
|
+
c
|
|
393
|
+
]));
|
|
358
394
|
const tables = getAuthTables(betterAuthOptions);
|
|
359
395
|
const generatedCollections = [];
|
|
360
396
|
const augmentedCollections = [];
|
|
361
397
|
// Calculate users collection slug for firstUserAdmin hook
|
|
362
398
|
const userTable = tables['user'];
|
|
363
|
-
const usersSlug = usePlural
|
|
364
|
-
|
|
365
|
-
: (userTable?.modelName ?? 'user');
|
|
366
|
-
for (const [modelKey, table] of Object.entries(tables)) {
|
|
399
|
+
const usersSlug = usePlural ? pluralize(userTable?.modelName ?? 'user') : userTable?.modelName ?? 'user';
|
|
400
|
+
for (const [modelKey, table] of Object.entries(tables)){
|
|
367
401
|
// Calculate slug
|
|
368
402
|
const baseName = table.modelName ?? modelKey;
|
|
369
403
|
const slug = usePlural ? pluralize(baseName) : baseName;
|
|
@@ -398,14 +432,18 @@ export function betterAuthCollections(options = {}) {
|
|
|
398
432
|
generatedCollections.push(collection);
|
|
399
433
|
}
|
|
400
434
|
// Merge: replace augmented collections, add new ones
|
|
401
|
-
const finalCollections = (incomingConfig.collections ?? []).map((c)
|
|
402
|
-
const augmented = augmentedCollections.find((a)
|
|
435
|
+
const finalCollections = (incomingConfig.collections ?? []).map((c)=>{
|
|
436
|
+
const augmented = augmentedCollections.find((a)=>a.slug === c.slug);
|
|
403
437
|
return augmented ?? c;
|
|
404
438
|
});
|
|
405
439
|
return {
|
|
406
440
|
...incomingConfig,
|
|
407
|
-
collections: [
|
|
441
|
+
collections: [
|
|
442
|
+
...finalCollections,
|
|
443
|
+
...generatedCollections
|
|
444
|
+
]
|
|
408
445
|
};
|
|
409
446
|
};
|
|
410
447
|
}
|
|
448
|
+
|
|
411
449
|
//# sourceMappingURL=collections.js.map
|