@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 +21 -0
- package/dist/components/management/PasskeysManagementClient.js +100 -197
- package/dist/components/management/SecurityNavLinks.d.ts +3 -6
- package/dist/components/management/SecurityNavLinks.js +12 -30
- package/dist/components/management/TwoFactorManagementClient.d.ts +3 -1
- package/dist/components/management/TwoFactorManagementClient.js +115 -248
- package/dist/components/management/fields/PasskeysField.d.ts +5 -0
- package/dist/components/management/fields/PasskeysField.js +9 -0
- package/dist/components/management/fields/TwoFactorField.d.ts +10 -0
- package/dist/components/management/fields/TwoFactorField.js +24 -0
- package/dist/exports/components.d.ts +2 -0
- package/dist/exports/components.js +3 -0
- package/dist/plugin/index.js +91 -31
- package/dist/types/apiKey.d.ts +8 -0
- package/package.json +1 -1
package/dist/plugin/index.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
//
|
|
312
|
-
|
|
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
|
-
|
|
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
|
package/dist/types/apiKey.d.ts
CHANGED
|
@@ -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.
|