@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.
@@ -5,6 +5,7 @@
5
5
  */ import { detectAuthConfig } from '../utils/detectAuthConfig.js';
6
6
  import { detectEnabledPlugins } from '../utils/detectEnabledPlugins.js';
7
7
  import { buildAvailableScopes, scopesToPermissions } from '../utils/generateScopes.js';
8
+ import { hasAnyRole, normalizeRoles } from '../utils/access.js';
8
9
  // Track auth instance for HMR
9
10
  let authInstance = null;
10
11
  // Store API key scopes config for access by management views
@@ -115,7 +116,7 @@ let apiKeyScopesConfig = undefined;
115
116
  }
116
117
  /**
117
118
  * Creates the auth endpoint handler that proxies requests to Better Auth.
118
- */ function createAuthEndpointHandler() {
119
+ */ function createAuthEndpointHandler(adminOptions) {
119
120
  return async (req)=>{
120
121
  const payloadWithAuth = req.payload;
121
122
  const auth = payloadWithAuth.betterAuth;
@@ -170,6 +171,45 @@ let apiKeyScopesConfig = undefined;
170
171
  }
171
172
  }
172
173
  }
174
+ // Guard API key mutation endpoints — require admin role
175
+ const isApiKeyMutation = req.method === 'POST' && (pathname.endsWith('/api-key/create') || pathname.endsWith('/api-key/update') || pathname.endsWith('/api-key/delete'));
176
+ if (isApiKeyMutation) {
177
+ const session = await auth.api.getSession({
178
+ headers: req.headers
179
+ });
180
+ if (!session?.user?.id) {
181
+ return new Response(JSON.stringify({
182
+ error: 'Unauthorized'
183
+ }), {
184
+ status: 401,
185
+ headers: {
186
+ 'Content-Type': 'application/json'
187
+ }
188
+ });
189
+ }
190
+ // Resolve required role: apiKey config > login config > default 'admin'
191
+ const requiredRole = apiKeyScopesConfig?.requiredRole ?? adminOptions?.login?.requiredRole ?? 'admin';
192
+ if (requiredRole !== null) {
193
+ // Find the auth collection slug from Payload's config
194
+ const authSlug = req.payload.config.collections.find((c)=>typeof c.auth === 'object' || c.auth === true)?.slug ?? 'users';
195
+ const user = await req.payload.findByID({
196
+ collection: authSlug,
197
+ id: session.user.id,
198
+ depth: 0,
199
+ overrideAccess: true
200
+ });
201
+ if (!hasAnyRole(user, normalizeRoles(requiredRole))) {
202
+ return new Response(JSON.stringify({
203
+ error: 'Forbidden: insufficient permissions to manage API keys'
204
+ }), {
205
+ status: 403,
206
+ headers: {
207
+ 'Content-Type': 'application/json'
208
+ }
209
+ });
210
+ }
211
+ }
212
+ }
173
213
  // Intercept API key creation requests with scopes
174
214
  // Better Auth's API key create endpoint is POST /api-key/create
175
215
  const isApiKeyCreate = req.method === 'POST' && pathname.endsWith('/api-key/create') && parsedBody?.scopes && Array.isArray(parsedBody.scopes);
@@ -199,8 +239,8 @@ let apiKeyScopesConfig = undefined;
199
239
  }
200
240
  /**
201
241
  * Generates Payload endpoints for Better Auth.
202
- */ function generateAuthEndpoints(basePath) {
203
- const handler = createAuthEndpointHandler();
242
+ */ function generateAuthEndpoints(basePath, adminOptions) {
243
+ const handler = createAuthEndpointHandler(adminOptions);
204
244
  const methods = [
205
245
  'get',
206
246
  'post',
@@ -274,6 +314,9 @@ let apiKeyScopesConfig = undefined;
274
314
  }
275
315
  /**
276
316
  * Injects management UI components into the Payload config based on enabled plugins.
317
+ *
318
+ * - 2FA and Passkeys are injected as `ui` fields on the auth collection (per-user settings)
319
+ * - API Keys remain as a sidebar admin view (admin-level feature)
277
320
  */ function injectManagementComponents(config, options) {
278
321
  const adminOptions = options.admin ?? {};
279
322
  // Skip if management UI is disabled
@@ -284,55 +327,72 @@ let apiKeyScopesConfig = undefined;
284
327
  const enabledPlugins = detectEnabledPlugins(adminOptions.betterAuthOptions);
285
328
  // Get custom paths or use defaults
286
329
  const paths = {
287
- twoFactor: adminOptions.managementPaths?.twoFactor ?? '/security/two-factor',
288
- apiKeys: adminOptions.managementPaths?.apiKeys ?? '/security/api-keys',
289
- passkeys: adminOptions.managementPaths?.passkeys ?? '/security/passkeys'
330
+ apiKeys: adminOptions.managementPaths?.apiKeys ?? '/security/api-keys'
290
331
  };
291
332
  const existingComponents = config.admin?.components ?? {};
292
333
  const existingViews = existingComponents.views ?? {};
293
334
  const existingAfterNavLinks = existingComponents.afterNavLinks ?? [];
294
- // Build management views based on enabled plugins
295
- // Note: Sessions and passkeys use Payload's default collection views
335
+ // Build management views only API Keys stays as a sidebar view
296
336
  const managementViews = {};
297
- // Two-factor (if enabled)
298
- if (enabledPlugins.hasTwoFactor) {
299
- managementViews.securityTwoFactor = {
300
- Component: '@delmaredigital/payload-better-auth/rsc#TwoFactorView',
301
- path: paths.twoFactor
302
- };
303
- }
304
- // API keys (if enabled)
305
337
  if (enabledPlugins.hasApiKey) {
306
338
  managementViews.securityApiKeys = {
307
339
  Component: '@delmaredigital/payload-better-auth/rsc#ApiKeysView',
308
340
  path: paths.apiKeys
309
341
  };
310
342
  }
311
- // Passkeys (if enabled)
312
- if (enabledPlugins.hasPasskey) {
313
- managementViews.securityPasskeys = {
314
- Component: '@delmaredigital/payload-better-auth/rsc#PasskeysView',
315
- path: paths.passkeys
316
- };
317
- }
318
- // Only add nav links if at least one plugin is enabled
319
- const hasAnyPlugin = enabledPlugins.hasTwoFactor || enabledPlugins.hasApiKey || enabledPlugins.hasPasskey;
320
- // Add SecurityNavLinks to afterNavLinks with clientProps for enabled plugins
321
- const afterNavLinks = hasAnyPlugin ? [
343
+ // Only add nav links if API Keys is enabled
344
+ const afterNavLinks = enabledPlugins.hasApiKey ? [
322
345
  ...Array.isArray(existingAfterNavLinks) ? existingAfterNavLinks : [
323
346
  existingAfterNavLinks
324
347
  ],
325
348
  {
326
349
  path: '@delmaredigital/payload-better-auth/components/management#SecurityNavLinks',
327
350
  clientProps: {
328
- showTwoFactor: enabledPlugins.hasTwoFactor,
329
- showApiKeys: enabledPlugins.hasApiKey,
330
- showPasskeys: enabledPlugins.hasPasskey
351
+ showApiKeys: enabledPlugins.hasApiKey
331
352
  }
332
353
  }
333
354
  ] : existingAfterNavLinks;
355
+ // Inject 2FA and Passkeys as ui fields on the auth collection
356
+ const securityFields = [];
357
+ if (enabledPlugins.hasTwoFactor) {
358
+ securityFields.push({
359
+ type: 'ui',
360
+ name: 'twoFactorManagement',
361
+ label: 'Two-Factor Authentication',
362
+ admin: {
363
+ components: {
364
+ Field: '@delmaredigital/payload-better-auth/components#TwoFactorField'
365
+ }
366
+ }
367
+ });
368
+ }
369
+ if (enabledPlugins.hasPasskey) {
370
+ securityFields.push({
371
+ type: 'ui',
372
+ name: 'passkeysManagement',
373
+ label: 'Passkeys',
374
+ admin: {
375
+ components: {
376
+ Field: '@delmaredigital/payload-better-auth/components#PasskeysField'
377
+ }
378
+ }
379
+ });
380
+ }
381
+ // Add ui fields to the auth collection
382
+ const collections = (config.collections ?? []).map((collection)=>{
383
+ const isAuthCollection = collection.auth === true || typeof collection.auth === 'object' && collection.auth.disableLocalStrategy;
384
+ if (!isAuthCollection || securityFields.length === 0) return collection;
385
+ return {
386
+ ...collection,
387
+ fields: [
388
+ ...collection.fields ?? [],
389
+ ...securityFields
390
+ ]
391
+ };
392
+ });
334
393
  return {
335
394
  ...config,
395
+ collections,
336
396
  admin: {
337
397
  ...config.admin,
338
398
  components: {
@@ -384,7 +444,7 @@ let apiKeyScopesConfig = undefined;
384
444
  // Inject management UI components
385
445
  config = injectManagementComponents(config, options);
386
446
  // Generate auth endpoints if enabled
387
- const authEndpoints = autoRegisterEndpoints ? generateAuthEndpoints(authBasePath) : [];
447
+ const authEndpoints = autoRegisterEndpoints ? generateAuthEndpoints(authBasePath, options.admin) : [];
388
448
  // Merge endpoints
389
449
  const existingEndpoints = config.endpoints ?? [];
390
450
  // Get existing onInit
@@ -50,6 +50,14 @@ export type ApiKeyScopesConfig = {
50
50
  * If not provided, keys without scopes will have no permissions.
51
51
  */
52
52
  defaultScopes?: string[];
53
+ /**
54
+ * Role(s) required to create, update, and delete API keys.
55
+ * - string: Single role required (e.g., 'admin')
56
+ * - string[]: Any matching role grants access
57
+ * - null: Allow any authenticated user (not recommended)
58
+ * @default Inherits from admin.login.requiredRole, or 'admin' if unset
59
+ */
60
+ requiredRole?: string | string[] | null;
53
61
  };
54
62
  /**
55
63
  * Scope data passed to the API keys management client component.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@delmaredigital/payload-better-auth",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "description": "Better Auth adapter and plugins for Payload CMS",
5
5
  "type": "module",
6
6
  "license": "MIT",