@delmaredigital/payload-better-auth 0.4.0 → 0.4.2

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 CHANGED
@@ -494,6 +494,27 @@ API keys can have granular permission scopes. By default, scopes are auto-genera
494
494
  | `includeCollectionScopes` | `boolean` | `true` when no custom scopes, `false` when custom scopes provided | Include auto-generated collection scopes |
495
495
  | `excludeCollections` | `string[]` | `['sessions', 'verifications', 'accounts', 'twoFactors', 'apikeys']` | Collections to exclude from auto-generated scopes |
496
496
  | `defaultScopes` | `string[]` | `[]` | Default scopes pre-selected when creating a key |
497
+ | `requiredRole` | `string \| string[] \| null` | Inherits from `admin.login.requiredRole` or `'admin'` | Role(s) required to create/update/delete API keys. Set to `null` to allow any authenticated user (not recommended) |
498
+
499
+ **Restricting API key management to admins:**
500
+
501
+ If your `admin.login.requiredRole` includes non-admin roles (e.g., editors who need admin panel access but shouldn't manage API keys), set `requiredRole` explicitly:
502
+
503
+ ```typescript
504
+ createBetterAuthPlugin({
505
+ createAuth,
506
+ admin: {
507
+ login: {
508
+ requiredRole: ['admin', 'content_editor'], // both can access admin panel
509
+ },
510
+ apiKey: {
511
+ requiredRole: 'admin', // only admins can create/update/delete API keys
512
+ },
513
+ },
514
+ })
515
+ ```
516
+
517
+ > **Note:** API key **verification** is not affected by this setting — existing keys continue to work regardless of who created them. This only restricts key management (create, update, delete).
497
518
 
498
519
  **Zero Config (recommended):**
499
520
  ```typescript
@@ -1,6 +1,9 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useState, useEffect } from 'react';
4
+ import { Button, Banner } from '@payloadcms/ui';
5
+ import { PlusIcon } from '@payloadcms/ui/icons/Plus';
6
+ import { XIcon } from '@payloadcms/ui/icons/X';
4
7
  import { createPayloadAuthClient } from '../../exports/client.js';
5
8
  /**
6
9
  * Client component for passkey management.
@@ -36,8 +39,7 @@ import { createPayloadAuthClient } from '../../exports/client.js';
36
39
  setLoading(false);
37
40
  }
38
41
  }
39
- async function handleRegister(e) {
40
- e.preventDefault();
42
+ async function handleRegister() {
41
43
  setRegistering(true);
42
44
  setError(null);
43
45
  setSuccess(null);
@@ -96,213 +98,131 @@ import { createPayloadAuthClient } from '../../exports/client.js';
96
98
  return d.toLocaleString();
97
99
  }
98
100
  return /*#__PURE__*/ _jsxs("div", {
99
- style: {
100
- maxWidth: '900px',
101
- margin: '0 auto',
102
- padding: 'calc(var(--base) * 2)'
103
- },
101
+ className: "field-type passkeys-management",
104
102
  children: [
103
+ error && /*#__PURE__*/ _jsx(Banner, {
104
+ type: "error",
105
+ children: error
106
+ }),
107
+ success && /*#__PURE__*/ _jsx(Banner, {
108
+ type: "success",
109
+ children: success
110
+ }),
105
111
  /*#__PURE__*/ _jsxs("div", {
106
112
  style: {
107
113
  display: 'flex',
108
114
  justifyContent: 'space-between',
109
115
  alignItems: 'center',
110
- marginBottom: 'calc(var(--base) * 2)'
116
+ marginBottom: 'var(--base)'
111
117
  },
112
118
  children: [
113
- /*#__PURE__*/ _jsxs("div", {
114
- children: [
115
- /*#__PURE__*/ _jsx("h1", {
116
- style: {
117
- color: 'var(--theme-text)',
118
- fontSize: 'var(--font-size-h2)',
119
- fontWeight: 600,
120
- margin: 0
121
- },
122
- children: title
123
- }),
124
- /*#__PURE__*/ _jsx("p", {
125
- style: {
126
- color: 'var(--theme-text)',
127
- opacity: 0.7,
128
- fontSize: 'var(--font-size-small)',
129
- margin: 'calc(var(--base) * 0.5) 0 0 0'
130
- },
131
- children: "Passkeys provide secure, passwordless sign-in using your device's biometrics or security keys."
132
- })
133
- ]
134
- }),
135
- /*#__PURE__*/ _jsx("button", {
136
- onClick: ()=>setShowRegisterForm(true),
119
+ /*#__PURE__*/ _jsx("p", {
120
+ className: "field-description",
137
121
  style: {
138
- padding: 'calc(var(--base) * 0.5) calc(var(--base) * 1)',
139
- background: 'var(--theme-elevation-800)',
140
- border: 'none',
141
- borderRadius: 'var(--style-radius-s)',
142
- color: 'var(--theme-elevation-50)',
143
- fontSize: 'var(--font-size-small)',
144
- cursor: 'pointer'
122
+ margin: 0
145
123
  },
124
+ children: "Passkeys provide secure, passwordless sign-in using your device's biometrics or security keys."
125
+ }),
126
+ !showRegisterForm && /*#__PURE__*/ _jsx(Button, {
127
+ buttonStyle: "secondary",
128
+ size: "small",
129
+ icon: /*#__PURE__*/ _jsx(PlusIcon, {}),
130
+ onClick: ()=>setShowRegisterForm(true),
146
131
  children: "Add Passkey"
147
132
  })
148
133
  ]
149
134
  }),
150
- error && /*#__PURE__*/ _jsx("div", {
151
- style: {
152
- color: 'var(--theme-error-500)',
153
- marginBottom: 'var(--base)',
154
- fontSize: 'var(--font-size-small)',
155
- padding: 'calc(var(--base) * 0.75)',
156
- background: 'var(--theme-error-50)',
157
- borderRadius: 'var(--style-radius-s)',
158
- border: '1px solid var(--theme-error-200)'
159
- },
160
- children: error
161
- }),
162
- success && /*#__PURE__*/ _jsx("div", {
163
- style: {
164
- color: 'var(--theme-success-700)',
165
- marginBottom: 'var(--base)',
166
- fontSize: 'var(--font-size-small)',
167
- padding: 'calc(var(--base) * 0.75)',
168
- background: 'var(--theme-success-50)',
169
- borderRadius: 'var(--style-radius-s)',
170
- border: '1px solid var(--theme-success-200)'
171
- },
172
- children: success
173
- }),
174
135
  showRegisterForm && /*#__PURE__*/ _jsxs("div", {
175
136
  style: {
176
- marginBottom: 'calc(var(--base) * 1.5)',
177
- padding: 'calc(var(--base) * 1.5)',
178
- background: 'var(--theme-elevation-50)',
179
- borderRadius: 'var(--style-radius-m)',
180
- border: '1px solid var(--theme-elevation-100)'
137
+ marginBottom: 'var(--base)'
181
138
  },
182
139
  children: [
183
- /*#__PURE__*/ _jsx("h2", {
140
+ /*#__PURE__*/ _jsxs("div", {
184
141
  style: {
185
- color: 'var(--theme-text)',
186
- fontSize: 'var(--font-size-h4)',
187
- fontWeight: 500,
188
- margin: '0 0 var(--base) 0'
142
+ marginBottom: 'var(--base)'
189
143
  },
190
- children: "Register New Passkey"
191
- }),
192
- /*#__PURE__*/ _jsxs("form", {
193
- onSubmit: handleRegister,
194
144
  children: [
195
- /*#__PURE__*/ _jsxs("div", {
145
+ /*#__PURE__*/ _jsx("label", {
146
+ className: "field-label",
196
147
  style: {
197
- marginBottom: 'var(--base)'
148
+ marginBottom: 'calc(var(--base) * 0.5)',
149
+ display: 'block'
198
150
  },
199
- children: [
200
- /*#__PURE__*/ _jsx("label", {
201
- style: {
202
- display: 'block',
203
- color: 'var(--theme-text)',
204
- fontSize: 'var(--font-size-small)',
205
- marginBottom: 'calc(var(--base) * 0.25)'
206
- },
207
- children: "Name (optional)"
208
- }),
209
- /*#__PURE__*/ _jsx("input", {
210
- type: "text",
211
- value: passkeyName,
212
- onChange: (e)=>setPasskeyName(e.target.value),
213
- placeholder: "e.g., MacBook Pro, iPhone",
214
- style: {
215
- width: '100%',
216
- padding: 'calc(var(--base) * 0.5)',
217
- background: 'var(--theme-input-bg)',
218
- border: '1px solid var(--theme-elevation-150)',
219
- borderRadius: 'var(--style-radius-s)',
220
- color: 'var(--theme-text)',
221
- boxSizing: 'border-box'
222
- }
223
- }),
224
- /*#__PURE__*/ _jsx("p", {
225
- style: {
226
- color: 'var(--theme-text)',
227
- opacity: 0.6,
228
- fontSize: 'var(--font-size-small)',
229
- margin: 'calc(var(--base) * 0.25) 0 0 0'
230
- },
231
- children: "Your browser will prompt you to use your device's biometrics or security key."
232
- })
233
- ]
151
+ children: "Name (optional)"
234
152
  }),
235
- /*#__PURE__*/ _jsxs("div", {
153
+ /*#__PURE__*/ _jsx("input", {
154
+ type: "text",
155
+ value: passkeyName,
156
+ onChange: (e)=>setPasskeyName(e.target.value),
157
+ onKeyDown: (e)=>{
158
+ if (e.key === 'Enter') {
159
+ e.preventDefault();
160
+ handleRegister();
161
+ }
162
+ },
163
+ placeholder: "e.g., MacBook Pro, iPhone",
164
+ style: {
165
+ width: '100%',
166
+ padding: 'var(--base)',
167
+ background: 'var(--theme-input-bg)',
168
+ border: '1px solid var(--theme-border-color)',
169
+ borderRadius: 'var(--style-radius-s)',
170
+ color: 'var(--theme-text)',
171
+ fontSize: 'var(--base-body-size)',
172
+ boxSizing: 'border-box'
173
+ }
174
+ }),
175
+ /*#__PURE__*/ _jsx("p", {
176
+ className: "field-description",
236
177
  style: {
237
- display: 'flex',
238
- gap: 'calc(var(--base) * 0.5)'
178
+ marginTop: 'calc(var(--base) * 0.25)'
239
179
  },
240
- children: [
241
- /*#__PURE__*/ _jsx("button", {
242
- type: "submit",
243
- disabled: registering,
244
- style: {
245
- padding: 'calc(var(--base) * 0.5) calc(var(--base) * 1)',
246
- background: 'var(--theme-elevation-800)',
247
- border: 'none',
248
- borderRadius: 'var(--style-radius-s)',
249
- color: 'var(--theme-elevation-50)',
250
- fontSize: 'var(--font-size-small)',
251
- cursor: registering ? 'not-allowed' : 'pointer',
252
- opacity: registering ? 0.7 : 1
253
- },
254
- children: registering ? 'Registering...' : 'Register Passkey'
255
- }),
256
- /*#__PURE__*/ _jsx("button", {
257
- type: "button",
258
- onClick: ()=>setShowRegisterForm(false),
259
- style: {
260
- padding: 'calc(var(--base) * 0.5) calc(var(--base) * 1)',
261
- background: 'transparent',
262
- border: '1px solid var(--theme-elevation-200)',
263
- borderRadius: 'var(--style-radius-s)',
264
- color: 'var(--theme-text)',
265
- fontSize: 'var(--font-size-small)',
266
- cursor: 'pointer'
267
- },
268
- children: "Cancel"
269
- })
270
- ]
180
+ children: "Your browser will prompt you to use your device's biometrics or security key."
181
+ })
182
+ ]
183
+ }),
184
+ /*#__PURE__*/ _jsxs("div", {
185
+ style: {
186
+ display: 'flex',
187
+ gap: 'calc(var(--base) * 0.5)'
188
+ },
189
+ children: [
190
+ /*#__PURE__*/ _jsx(Button, {
191
+ buttonStyle: "primary",
192
+ size: "small",
193
+ onClick: handleRegister,
194
+ disabled: registering,
195
+ children: registering ? 'Registering...' : 'Register Passkey'
196
+ }),
197
+ /*#__PURE__*/ _jsx(Button, {
198
+ buttonStyle: "secondary",
199
+ size: "small",
200
+ onClick: ()=>setShowRegisterForm(false),
201
+ children: "Cancel"
271
202
  })
272
203
  ]
273
204
  })
274
205
  ]
275
206
  }),
276
- loading ? /*#__PURE__*/ _jsx("div", {
277
- style: {
278
- color: 'var(--theme-text)',
279
- opacity: 0.7,
280
- textAlign: 'center',
281
- padding: 'calc(var(--base) * 3)'
282
- },
207
+ loading ? /*#__PURE__*/ _jsx("p", {
208
+ className: "field-description",
283
209
  children: "Loading passkeys..."
284
- }) : passkeys.length === 0 ? /*#__PURE__*/ _jsx("div", {
285
- style: {
286
- color: 'var(--theme-text)',
287
- opacity: 0.7,
288
- textAlign: 'center',
289
- padding: 'calc(var(--base) * 3)'
290
- },
291
- children: "No passkeys registered. Add one to enable passwordless sign-in."
210
+ }) : passkeys.length === 0 ? /*#__PURE__*/ _jsx("p", {
211
+ className: "field-description",
212
+ children: "No passkeys registered."
292
213
  }) : /*#__PURE__*/ _jsx("div", {
293
214
  style: {
294
- background: 'var(--theme-elevation-50)',
295
- borderRadius: 'var(--style-radius-m)',
296
- overflow: 'hidden',
297
- border: '1px solid var(--theme-elevation-100)'
215
+ border: '1px solid var(--theme-border-color)',
216
+ borderRadius: 'var(--style-radius-s)',
217
+ overflow: 'hidden'
298
218
  },
299
219
  children: passkeys.map((pk, index)=>/*#__PURE__*/ _jsxs("div", {
300
220
  style: {
301
221
  display: 'flex',
302
222
  justifyContent: 'space-between',
303
223
  alignItems: 'center',
304
- padding: 'calc(var(--base) * 1)',
305
- borderBottom: index < passkeys.length - 1 ? '1px solid var(--theme-elevation-100)' : 'none'
224
+ padding: 'var(--base)',
225
+ borderBottom: index < passkeys.length - 1 ? '1px solid var(--theme-border-color)' : 'none'
306
226
  },
307
227
  children: [
308
228
  /*#__PURE__*/ _jsxs("div", {
@@ -310,46 +230,29 @@ import { createPayloadAuthClient } from '../../exports/client.js';
310
230
  /*#__PURE__*/ _jsx("div", {
311
231
  style: {
312
232
  color: 'var(--theme-text)',
313
- fontWeight: 500,
314
- marginBottom: 'calc(var(--base) * 0.25)'
233
+ fontWeight: 500
315
234
  },
316
235
  children: pk.name || 'Passkey'
317
236
  }),
318
- /*#__PURE__*/ _jsxs("div", {
237
+ /*#__PURE__*/ _jsxs("p", {
238
+ className: "field-description",
319
239
  style: {
320
- color: 'var(--theme-elevation-600)',
321
- fontSize: 'var(--font-size-small)'
240
+ margin: 'calc(var(--base) * 0.25) 0 0 0'
322
241
  },
323
242
  children: [
324
- /*#__PURE__*/ _jsxs("span", {
325
- children: [
326
- "Created: ",
327
- formatDate(pk.createdAt)
328
- ]
329
- }),
330
- pk.lastUsedAt && /*#__PURE__*/ _jsxs("span", {
331
- children: [
332
- " | Last used: ",
333
- formatDate(pk.lastUsedAt)
334
- ]
335
- })
243
+ "Created: ",
244
+ formatDate(pk.createdAt),
245
+ pk.lastUsedAt && ` | Last used: ${formatDate(pk.lastUsedAt)}`
336
246
  ]
337
247
  })
338
248
  ]
339
249
  }),
340
- /*#__PURE__*/ _jsx("button", {
250
+ /*#__PURE__*/ _jsx(Button, {
251
+ buttonStyle: "error",
252
+ size: "small",
253
+ icon: /*#__PURE__*/ _jsx(XIcon, {}),
341
254
  onClick: ()=>handleDelete(pk.id),
342
255
  disabled: deleting === pk.id,
343
- style: {
344
- padding: 'calc(var(--base) * 0.5) calc(var(--base) * 0.75)',
345
- background: 'transparent',
346
- border: '1px solid var(--theme-error-300)',
347
- borderRadius: 'var(--style-radius-s)',
348
- color: 'var(--theme-error-500)',
349
- fontSize: 'var(--font-size-small)',
350
- cursor: deleting === pk.id ? 'not-allowed' : 'pointer',
351
- opacity: deleting === pk.id ? 0.7 : 1
352
- },
353
256
  children: deleting === pk.id ? 'Deleting...' : 'Delete'
354
257
  })
355
258
  ]
@@ -1,19 +1,16 @@
1
1
  export type SecurityNavLinksProps = {
2
2
  /** Base path for security views. Default: '/admin/security' */
3
3
  basePath?: string;
4
- /** Show Two-Factor Auth link. Default: true */
5
- showTwoFactor?: boolean;
6
4
  /** Show API Keys link. Default: true */
7
5
  showApiKeys?: boolean;
8
- /** Show Passkeys link. Default: true */
9
- showPasskeys?: boolean;
10
6
  };
11
7
  /**
12
8
  * Navigation links for security management features.
13
9
  * Rendered in admin sidebar via afterNavLinks injection.
14
10
  * Uses Payload's NavGroup and nav CSS classes for native styling.
15
11
  *
16
- * Links are conditionally shown based on which Better Auth plugins are enabled.
12
+ * Currently only renders API Keys link 2FA and Passkeys
13
+ * are now embedded as ui fields on the user document.
17
14
  */
18
- export declare function SecurityNavLinks({ basePath, showTwoFactor, showApiKeys, showPasskeys, }?: SecurityNavLinksProps): import("react").JSX.Element | null;
15
+ export declare function SecurityNavLinks({ basePath, showApiKeys, }?: SecurityNavLinksProps): import("react").JSX.Element | null;
19
16
  export default SecurityNavLinks;
@@ -6,40 +6,22 @@ import { NavGroup } from '@payloadcms/ui';
6
6
  * Rendered in admin sidebar via afterNavLinks injection.
7
7
  * Uses Payload's NavGroup and nav CSS classes for native styling.
8
8
  *
9
- * Links are conditionally shown based on which Better Auth plugins are enabled.
10
- */ export function SecurityNavLinks({ basePath = '/admin/security', showTwoFactor = true, showApiKeys = true, showPasskeys = true } = {}) {
11
- const links = [];
12
- if (showTwoFactor) {
13
- links.push({
14
- href: `${basePath}/two-factor`,
15
- label: 'Two-Factor Auth'
16
- });
17
- }
18
- if (showApiKeys) {
19
- links.push({
20
- href: `${basePath}/api-keys`,
21
- label: 'API Keys'
22
- });
23
- }
24
- if (showPasskeys) {
25
- links.push({
26
- href: `${basePath}/passkeys`,
27
- label: 'Passkeys'
28
- });
29
- }
30
- if (links.length === 0) {
9
+ * Currently only renders API Keys link 2FA and Passkeys
10
+ * are now embedded as ui fields on the user document.
11
+ */ export function SecurityNavLinks({ basePath = '/admin/security', showApiKeys = true } = {}) {
12
+ if (!showApiKeys) {
31
13
  return null;
32
14
  }
33
15
  return /*#__PURE__*/ _jsx(NavGroup, {
34
16
  label: "Security",
35
- children: links.map((link)=>/*#__PURE__*/ _jsx("a", {
36
- href: link.href,
37
- className: "nav__link",
38
- children: /*#__PURE__*/ _jsx("span", {
39
- className: "nav__link-label",
40
- children: link.label
41
- })
42
- }, link.href))
17
+ children: /*#__PURE__*/ _jsx("a", {
18
+ href: `${basePath}/api-keys`,
19
+ className: "nav__link",
20
+ children: /*#__PURE__*/ _jsx("span", {
21
+ className: "nav__link-label",
22
+ children: "API Keys"
23
+ })
24
+ })
43
25
  });
44
26
  }
45
27
  export default SecurityNavLinks;
@@ -4,10 +4,12 @@ export type TwoFactorManagementClientProps = {
4
4
  authClient?: PayloadAuthClient;
5
5
  /** Page title. Default: 'Two-Factor Authentication' */
6
6
  title?: string;
7
+ /** Called after 2FA is enabled or disabled. Use to refresh form state. */
8
+ onComplete?: () => void | Promise<void>;
7
9
  };
8
10
  /**
9
11
  * Client component for two-factor authentication management.
10
12
  * Shows 2FA status and allows enabling/disabling.
11
13
  */
12
- export declare function TwoFactorManagementClient({ authClient: providedClient, title, }?: TwoFactorManagementClientProps): import("react").JSX.Element;
14
+ export declare function TwoFactorManagementClient({ authClient: providedClient, title, onComplete, }?: TwoFactorManagementClientProps): import("react").JSX.Element;
13
15
  export default TwoFactorManagementClient;