@delmaredigital/payload-better-auth 0.4.2 → 0.4.4
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 +19 -1713
- package/dist/adapter/collections.js +9 -1
- package/dist/adapter/index.d.ts +19 -0
- package/dist/adapter/index.js +99 -33
- package/dist/components/LogoutButton.d.ts +3 -0
- package/dist/components/LogoutButton.js +20 -8
- package/dist/components/management/fields/PasskeysField.d.ts +8 -0
- package/dist/components/management/fields/PasskeysField.js +23 -0
- package/dist/components/management/fields/TwoFactorField.d.ts +9 -0
- package/dist/components/management/fields/TwoFactorField.js +24 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/package.json +6 -6
|
@@ -303,11 +303,19 @@ function generateCollection(modelKey, table, usePlural, adminGroup, customAccess
|
|
|
303
303
|
});
|
|
304
304
|
} else {
|
|
305
305
|
const saveToJWT = configureSaveToJWT ? getSaveToJWT(modelKey, payloadFieldName) : undefined;
|
|
306
|
+
// Fields managed exclusively by Better Auth should be read-only in the admin UI
|
|
307
|
+
const readOnlyFields = [
|
|
308
|
+
'twoFactorEnabled'
|
|
309
|
+
];
|
|
310
|
+
const isReadOnly = readOnlyFields.includes(payloadFieldName);
|
|
306
311
|
const field = {
|
|
307
312
|
name: payloadFieldName,
|
|
308
313
|
type: fieldType,
|
|
309
314
|
admin: {
|
|
310
|
-
description: `Auto-added by Better Auth (${fieldKey})
|
|
315
|
+
description: `Auto-added by Better Auth (${fieldKey})`,
|
|
316
|
+
...isReadOnly && {
|
|
317
|
+
readOnly: true
|
|
318
|
+
}
|
|
311
319
|
},
|
|
312
320
|
...saveToJWT !== undefined && {
|
|
313
321
|
saveToJWT
|
package/dist/adapter/index.d.ts
CHANGED
|
@@ -8,6 +8,20 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import type { Adapter, BetterAuthOptions } from 'better-auth';
|
|
10
10
|
import type { BasePayload } from 'payload';
|
|
11
|
+
/**
|
|
12
|
+
* Database types supported by Payload CMS.
|
|
13
|
+
*/
|
|
14
|
+
export type DbType = 'postgres' | 'mongodb' | 'sqlite';
|
|
15
|
+
/**
|
|
16
|
+
* Detect the database type from the Payload instance.
|
|
17
|
+
*/
|
|
18
|
+
export declare function detectDbType(payload: BasePayload): DbType;
|
|
19
|
+
/**
|
|
20
|
+
* Determine ID type based on database type and Better Auth config.
|
|
21
|
+
* MongoDB always uses text IDs (ObjectId strings).
|
|
22
|
+
* Postgres defaults to 'number' (SERIAL) unless generateId indicates otherwise.
|
|
23
|
+
*/
|
|
24
|
+
export declare function resolveIdType(dbType: DbType, options: BetterAuthOptions, explicitIdType?: 'number' | 'text'): 'number' | 'text';
|
|
11
25
|
export type PayloadAdapterConfig = {
|
|
12
26
|
/**
|
|
13
27
|
* The Payload instance or a function that returns it.
|
|
@@ -22,6 +36,11 @@ export type PayloadAdapterConfig = {
|
|
|
22
36
|
* Enable debug logging for troubleshooting
|
|
23
37
|
*/
|
|
24
38
|
enableDebugLogs?: boolean;
|
|
39
|
+
/**
|
|
40
|
+
* Database type. Auto-detected from Payload's database adapter if not set.
|
|
41
|
+
* Set explicitly if auto-detection doesn't work for your adapter.
|
|
42
|
+
*/
|
|
43
|
+
dbType?: DbType;
|
|
25
44
|
/**
|
|
26
45
|
* ID type used by Payload.
|
|
27
46
|
* If not specified, auto-detects from Better Auth's generateId setting.
|
package/dist/adapter/index.js
CHANGED
|
@@ -7,15 +7,26 @@
|
|
|
7
7
|
* @packageDocumentation
|
|
8
8
|
*/ import { createAdapterFactory } from 'better-auth/adapters';
|
|
9
9
|
/**
|
|
10
|
-
* Detect
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
* Detect the database type from the Payload instance.
|
|
11
|
+
*/ export function detectDbType(payload) {
|
|
12
|
+
const dbName = payload.db?.name;
|
|
13
|
+
if (typeof dbName === 'string') {
|
|
14
|
+
if (dbName.includes('mongo') || dbName.includes('mongoose')) return 'mongodb';
|
|
15
|
+
if (dbName.includes('sqlite')) return 'sqlite';
|
|
16
|
+
}
|
|
17
|
+
return 'postgres';
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Determine ID type based on database type and Better Auth config.
|
|
21
|
+
* MongoDB always uses text IDs (ObjectId strings).
|
|
22
|
+
* Postgres defaults to 'number' (SERIAL) unless generateId indicates otherwise.
|
|
23
|
+
*/ export function resolveIdType(dbType, options, explicitIdType) {
|
|
24
|
+
if (dbType === 'mongodb') return 'text';
|
|
25
|
+
if (explicitIdType) return explicitIdType;
|
|
13
26
|
const generateId = options.advanced?.database?.generateId;
|
|
14
|
-
// If explicitly set to something other than 'serial', use text (UUID)
|
|
15
27
|
if (generateId !== undefined && generateId !== 'serial') {
|
|
16
28
|
return 'text';
|
|
17
29
|
}
|
|
18
|
-
// Default to number (SERIAL) - Payload's default
|
|
19
30
|
return 'number';
|
|
20
31
|
}
|
|
21
32
|
/**
|
|
@@ -59,7 +70,7 @@
|
|
|
59
70
|
async function resolvePayloadClient() {
|
|
60
71
|
return typeof payloadClient === 'function' ? await payloadClient() : payloadClient;
|
|
61
72
|
}
|
|
62
|
-
function convertOperator(operator, value) {
|
|
73
|
+
function convertOperator(operator, value, dbType) {
|
|
63
74
|
switch(operator){
|
|
64
75
|
case 'eq':
|
|
65
76
|
return {
|
|
@@ -89,15 +100,25 @@
|
|
|
89
100
|
return {
|
|
90
101
|
in: value
|
|
91
102
|
};
|
|
103
|
+
case 'not_in':
|
|
104
|
+
return {
|
|
105
|
+
not_in: value
|
|
106
|
+
};
|
|
92
107
|
case 'contains':
|
|
93
108
|
return {
|
|
94
109
|
contains: value
|
|
95
110
|
};
|
|
96
111
|
case 'starts_with':
|
|
112
|
+
if (dbType === 'mongodb') return {
|
|
113
|
+
contains: value
|
|
114
|
+
};
|
|
97
115
|
return {
|
|
98
116
|
like: `${value}%`
|
|
99
117
|
};
|
|
100
118
|
case 'ends_with':
|
|
119
|
+
if (dbType === 'mongodb') return {
|
|
120
|
+
contains: value
|
|
121
|
+
};
|
|
101
122
|
return {
|
|
102
123
|
like: `%${value}`
|
|
103
124
|
};
|
|
@@ -122,9 +143,11 @@
|
|
|
122
143
|
}
|
|
123
144
|
// Return the adapter factory function
|
|
124
145
|
return (options)=>{
|
|
125
|
-
// Determine ID type
|
|
126
|
-
//
|
|
127
|
-
|
|
146
|
+
// Determine ID type based on database type
|
|
147
|
+
// If payloadClient is already resolved, detect dbType immediately
|
|
148
|
+
// Otherwise default to 'postgres' (will be updated on first operation)
|
|
149
|
+
const effectiveDbType = adapterConfig.dbType ?? (typeof payloadClient !== 'function' ? detectDbType(payloadClient) : 'postgres');
|
|
150
|
+
const idType = resolveIdType(effectiveDbType, options, adapterConfig.idType);
|
|
128
151
|
const generateId = options.advanced?.database?.generateId;
|
|
129
152
|
// Warn if using number IDs but Better Auth is explicitly configured to generate its own IDs
|
|
130
153
|
// This would cause Better Auth to generate UUIDs which won't work with SERIAL columns
|
|
@@ -153,11 +176,13 @@
|
|
|
153
176
|
// Payload collections are plural by default (users, sessions, etc.)
|
|
154
177
|
// Users can customize via BetterAuthOptions: user: { modelName: 'custom_users' }
|
|
155
178
|
usePlural: true,
|
|
156
|
-
//
|
|
157
|
-
disableIdGeneration:
|
|
158
|
-
//
|
|
159
|
-
supportsNumericIds:
|
|
160
|
-
|
|
179
|
+
// Payload always generates IDs (SERIAL for postgres/sqlite, ObjectId for mongodb)
|
|
180
|
+
disableIdGeneration: true,
|
|
181
|
+
// MongoDB uses ObjectId strings, not numeric IDs
|
|
182
|
+
supportsNumericIds: effectiveDbType !== 'mongodb',
|
|
183
|
+
// Payload returns dates as ISO strings via its Local API, not Date objects.
|
|
184
|
+
// Setting false tells the factory to convert string dates ↔ Date objects.
|
|
185
|
+
supportsDates: false,
|
|
161
186
|
supportsBooleans: true,
|
|
162
187
|
supportsJSON: true,
|
|
163
188
|
supportsArrays: false,
|
|
@@ -171,11 +196,18 @@
|
|
|
171
196
|
// so we'll resolve it lazily on first operation
|
|
172
197
|
let resolvedPayload = null;
|
|
173
198
|
let resolvePromise = null;
|
|
199
|
+
let resolvedDbType = adapterConfig.dbType ?? null;
|
|
174
200
|
const getPayload = async ()=>{
|
|
175
201
|
if (resolvedPayload) return resolvedPayload;
|
|
176
202
|
if (!resolvePromise) {
|
|
177
203
|
resolvePromise = resolvePayloadClient().then((p)=>{
|
|
178
204
|
resolvedPayload = p;
|
|
205
|
+
if (!resolvedDbType) {
|
|
206
|
+
resolvedDbType = detectDbType(p);
|
|
207
|
+
if (enableDebugLogs) {
|
|
208
|
+
console.log('[payload-adapter] Detected database type:', resolvedDbType);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
179
211
|
return p;
|
|
180
212
|
});
|
|
181
213
|
}
|
|
@@ -295,19 +327,6 @@
|
|
|
295
327
|
}
|
|
296
328
|
}
|
|
297
329
|
}
|
|
298
|
-
// Convert date strings to Date objects
|
|
299
|
-
// Better Auth expects Date objects for date field comparisons
|
|
300
|
-
for (const [key, value] of Object.entries(transformed)){
|
|
301
|
-
if (typeof value !== 'string') continue;
|
|
302
|
-
// Check if schema defines this field as a date type
|
|
303
|
-
const fieldDef = modelSchema.fields[key];
|
|
304
|
-
if (fieldDef?.type === 'date') {
|
|
305
|
-
const dateValue = new Date(value);
|
|
306
|
-
if (!isNaN(dateValue.getTime())) {
|
|
307
|
-
transformed[key] = dateValue;
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
330
|
// Convert semantic ID fields to numbers when using serial IDs
|
|
312
331
|
// Heuristic: fields ending in 'Id' or '_id' containing numeric strings
|
|
313
332
|
// Modified by allowlist (add) and blocklist (exclude)
|
|
@@ -337,7 +356,7 @@
|
|
|
337
356
|
if (where.length === 1) {
|
|
338
357
|
const w = where[0];
|
|
339
358
|
return {
|
|
340
|
-
[getPayloadFieldName(model, w.field)]: convertOperator(w.operator, w.value)
|
|
359
|
+
[getPayloadFieldName(model, w.field)]: convertOperator(w.operator, w.value, resolvedDbType ?? 'postgres')
|
|
341
360
|
};
|
|
342
361
|
}
|
|
343
362
|
const andConditions = where.filter((w)=>w.connector !== 'OR');
|
|
@@ -345,12 +364,12 @@
|
|
|
345
364
|
const result = {};
|
|
346
365
|
if (andConditions.length > 0) {
|
|
347
366
|
result.and = andConditions.map((w)=>({
|
|
348
|
-
[getPayloadFieldName(model, w.field)]: convertOperator(w.operator, w.value)
|
|
367
|
+
[getPayloadFieldName(model, w.field)]: convertOperator(w.operator, w.value, resolvedDbType ?? 'postgres')
|
|
349
368
|
}));
|
|
350
369
|
}
|
|
351
370
|
if (orConditions.length > 0) {
|
|
352
371
|
result.or = orConditions.map((w)=>({
|
|
353
|
-
[getPayloadFieldName(model, w.field)]: convertOperator(w.operator, w.value)
|
|
372
|
+
[getPayloadFieldName(model, w.field)]: convertOperator(w.operator, w.value, resolvedDbType ?? 'postgres')
|
|
354
373
|
}));
|
|
355
374
|
}
|
|
356
375
|
return result;
|
|
@@ -389,10 +408,19 @@
|
|
|
389
408
|
// Transform back and merge with input data for Better Auth
|
|
390
409
|
// Database result takes precedence (handles hooks that modify data like firstUserAdmin)
|
|
391
410
|
const transformed = transformDataFromPayload(model, result);
|
|
392
|
-
|
|
411
|
+
const merged = {
|
|
393
412
|
...data,
|
|
394
413
|
...transformed
|
|
395
414
|
};
|
|
415
|
+
if (enableDebugLogs) {
|
|
416
|
+
debugLog('create result', {
|
|
417
|
+
collection,
|
|
418
|
+
resultId: result.id,
|
|
419
|
+
transformedKeys: Object.keys(transformed),
|
|
420
|
+
mergedKeys: Object.keys(merged)
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
return merged;
|
|
396
424
|
} catch (error) {
|
|
397
425
|
console.error('[payload-adapter] create failed:', {
|
|
398
426
|
collection,
|
|
@@ -424,15 +452,39 @@
|
|
|
424
452
|
depth: join ? 1 : 0,
|
|
425
453
|
overrideAccess: true
|
|
426
454
|
});
|
|
427
|
-
|
|
455
|
+
const transformed = transformDataFromPayload(model, result);
|
|
456
|
+
if (enableDebugLogs) {
|
|
457
|
+
debugLog('findOne result (byID)', {
|
|
458
|
+
collection,
|
|
459
|
+
id: convertId(id),
|
|
460
|
+
found: true,
|
|
461
|
+
transformedKeys: Object.keys(transformed)
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
return transformed;
|
|
428
465
|
} catch (error) {
|
|
429
466
|
if (error instanceof Error && 'status' in error && error.status === 404) {
|
|
467
|
+
if (enableDebugLogs) {
|
|
468
|
+
debugLog('findOne result (byID)', {
|
|
469
|
+
collection,
|
|
470
|
+
id: convertId(id),
|
|
471
|
+
found: false
|
|
472
|
+
});
|
|
473
|
+
}
|
|
430
474
|
return null;
|
|
431
475
|
}
|
|
432
476
|
throw error;
|
|
433
477
|
}
|
|
434
478
|
}
|
|
435
479
|
const payloadWhere = convertWhereToPayload(model, where);
|
|
480
|
+
if (enableDebugLogs) {
|
|
481
|
+
debugLog('findOne query', {
|
|
482
|
+
collection,
|
|
483
|
+
payloadWhere: JSON.stringify(payloadWhere),
|
|
484
|
+
resolvedDbType,
|
|
485
|
+
idType
|
|
486
|
+
});
|
|
487
|
+
}
|
|
436
488
|
const result = await payload.find({
|
|
437
489
|
collection,
|
|
438
490
|
where: payloadWhere,
|
|
@@ -440,8 +492,22 @@
|
|
|
440
492
|
depth: join ? 1 : 0,
|
|
441
493
|
overrideAccess: true
|
|
442
494
|
});
|
|
495
|
+
if (enableDebugLogs) {
|
|
496
|
+
debugLog('findOne result', {
|
|
497
|
+
collection,
|
|
498
|
+
totalDocs: result.totalDocs,
|
|
499
|
+
found: result.docs.length > 0
|
|
500
|
+
});
|
|
501
|
+
}
|
|
443
502
|
if (!result.docs[0]) return null;
|
|
444
|
-
|
|
503
|
+
const transformed = transformDataFromPayload(model, result.docs[0]);
|
|
504
|
+
if (enableDebugLogs) {
|
|
505
|
+
debugLog('findOne transformed', {
|
|
506
|
+
collection,
|
|
507
|
+
transformedKeys: Object.keys(transformed)
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
return transformed;
|
|
445
511
|
} catch (error) {
|
|
446
512
|
console.error('[payload-adapter] findOne failed:', {
|
|
447
513
|
model,
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Logout button component styled to match Payload's admin nav.
|
|
3
3
|
* Uses Payload's CSS classes and variables for native theme integration.
|
|
4
|
+
*
|
|
5
|
+
* Clears both Better Auth session and Payload's JWT cookie to ensure
|
|
6
|
+
* clean state when switching between users.
|
|
4
7
|
*/
|
|
5
8
|
export declare function LogoutButton(): import("react").JSX.Element;
|
|
6
9
|
export default LogoutButton;
|
|
@@ -5,6 +5,9 @@ import { useRouter } from 'next/navigation.js';
|
|
|
5
5
|
/**
|
|
6
6
|
* Logout button component styled to match Payload's admin nav.
|
|
7
7
|
* Uses Payload's CSS classes and variables for native theme integration.
|
|
8
|
+
*
|
|
9
|
+
* Clears both Better Auth session and Payload's JWT cookie to ensure
|
|
10
|
+
* clean state when switching between users.
|
|
8
11
|
*/ export function LogoutButton() {
|
|
9
12
|
const router = useRouter();
|
|
10
13
|
const [isLoading, setIsLoading] = useState(false);
|
|
@@ -12,14 +15,23 @@ import { useRouter } from 'next/navigation.js';
|
|
|
12
15
|
if (isLoading) return;
|
|
13
16
|
setIsLoading(true);
|
|
14
17
|
try {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
18
|
+
// Clear both sessions simultaneously while cookies are still valid.
|
|
19
|
+
// - Better Auth: clears BA session cookie
|
|
20
|
+
// - Payload: clears JWT cookie (payload-token) so useAuth() resets
|
|
21
|
+
await Promise.allSettled([
|
|
22
|
+
fetch('/api/auth/sign-out', {
|
|
23
|
+
method: 'POST',
|
|
24
|
+
credentials: 'include',
|
|
25
|
+
headers: {
|
|
26
|
+
'Content-Type': 'application/json'
|
|
27
|
+
},
|
|
28
|
+
body: JSON.stringify({})
|
|
29
|
+
}),
|
|
30
|
+
fetch('/api/users/logout', {
|
|
31
|
+
method: 'POST',
|
|
32
|
+
credentials: 'include'
|
|
33
|
+
})
|
|
34
|
+
]);
|
|
23
35
|
router.push('/admin/login');
|
|
24
36
|
} catch (error) {
|
|
25
37
|
console.error('[better-auth] Logout error:', error);
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Thin wrapper around PasskeysManagementClient for use as a Payload `ui` field.
|
|
3
|
+
*
|
|
4
|
+
* Better Auth's passkey APIs are session-based (always operate on the logged-in user).
|
|
5
|
+
* When viewing another user's document, we show an info message instead of the
|
|
6
|
+
* management UI to avoid displaying the admin's own passkeys on someone else's page.
|
|
7
|
+
*
|
|
8
|
+
* While auth context is hydrating (user is null), we show the management UI since
|
|
9
|
+
* the API is session-based and will only ever return the logged-in user's passkeys —
|
|
10
|
+
* no other user's data can be leaked.
|
|
3
11
|
*/
|
|
4
12
|
export declare function PasskeysField(): import("react").JSX.Element;
|
|
5
13
|
export default PasskeysField;
|
|
@@ -1,9 +1,32 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { useAuth, useDocumentInfo } from '@payloadcms/ui';
|
|
3
4
|
import { PasskeysManagementClient } from '../PasskeysManagementClient.js';
|
|
4
5
|
/**
|
|
5
6
|
* Thin wrapper around PasskeysManagementClient for use as a Payload `ui` field.
|
|
7
|
+
*
|
|
8
|
+
* Better Auth's passkey APIs are session-based (always operate on the logged-in user).
|
|
9
|
+
* When viewing another user's document, we show an info message instead of the
|
|
10
|
+
* management UI to avoid displaying the admin's own passkeys on someone else's page.
|
|
11
|
+
*
|
|
12
|
+
* While auth context is hydrating (user is null), we show the management UI since
|
|
13
|
+
* the API is session-based and will only ever return the logged-in user's passkeys —
|
|
14
|
+
* no other user's data can be leaked.
|
|
6
15
|
*/ export function PasskeysField() {
|
|
16
|
+
const { id: documentId } = useDocumentInfo();
|
|
17
|
+
const { user } = useAuth();
|
|
18
|
+
// Only block when we KNOW it's a different user.
|
|
19
|
+
// While auth is loading (user is null), show the UI — it's session-based
|
|
20
|
+
// and only returns the logged-in user's own passkeys regardless.
|
|
21
|
+
if (user && String(documentId) !== String(user.id)) {
|
|
22
|
+
return /*#__PURE__*/ _jsx("div", {
|
|
23
|
+
className: "field-type",
|
|
24
|
+
children: /*#__PURE__*/ _jsx("p", {
|
|
25
|
+
className: "field-description",
|
|
26
|
+
children: "Passkeys can only be managed by the account owner."
|
|
27
|
+
})
|
|
28
|
+
});
|
|
29
|
+
}
|
|
7
30
|
return /*#__PURE__*/ _jsx(PasskeysManagementClient, {});
|
|
8
31
|
}
|
|
9
32
|
export default PasskeysField;
|
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Wrapper around TwoFactorManagementClient for use as a Payload `ui` field.
|
|
3
3
|
*
|
|
4
|
+
* Better Auth's 2FA APIs are session-based (always operate on the logged-in user).
|
|
5
|
+
* When viewing another user's document, we show an info message instead of the
|
|
6
|
+
* management UI to avoid the admin accidentally modifying their own 2FA settings
|
|
7
|
+
* while on someone else's page.
|
|
8
|
+
*
|
|
9
|
+
* While auth context is hydrating (user is null), we show the management UI since
|
|
10
|
+
* the API is session-based and will only ever operate on the logged-in user's own
|
|
11
|
+
* 2FA settings — no other user's data can be leaked or modified.
|
|
12
|
+
*
|
|
4
13
|
* After 2FA is enabled or disabled, triggers a Next.js router refresh so that
|
|
5
14
|
* the document form re-fetches from the DB and picks up the `twoFactorEnabled`
|
|
6
15
|
* value that Better Auth wrote. Without this, navigating away without clicking
|
|
@@ -2,21 +2,45 @@
|
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
3
|
import { useRouter } from 'next/navigation.js';
|
|
4
4
|
import { useCallback } from 'react';
|
|
5
|
+
import { useAuth, useDocumentInfo } from '@payloadcms/ui';
|
|
5
6
|
import { TwoFactorManagementClient } from '../TwoFactorManagementClient.js';
|
|
6
7
|
/**
|
|
7
8
|
* Wrapper around TwoFactorManagementClient for use as a Payload `ui` field.
|
|
8
9
|
*
|
|
10
|
+
* Better Auth's 2FA APIs are session-based (always operate on the logged-in user).
|
|
11
|
+
* When viewing another user's document, we show an info message instead of the
|
|
12
|
+
* management UI to avoid the admin accidentally modifying their own 2FA settings
|
|
13
|
+
* while on someone else's page.
|
|
14
|
+
*
|
|
15
|
+
* While auth context is hydrating (user is null), we show the management UI since
|
|
16
|
+
* the API is session-based and will only ever operate on the logged-in user's own
|
|
17
|
+
* 2FA settings — no other user's data can be leaked or modified.
|
|
18
|
+
*
|
|
9
19
|
* After 2FA is enabled or disabled, triggers a Next.js router refresh so that
|
|
10
20
|
* the document form re-fetches from the DB and picks up the `twoFactorEnabled`
|
|
11
21
|
* value that Better Auth wrote. Without this, navigating away without clicking
|
|
12
22
|
* Save would overwrite the change.
|
|
13
23
|
*/ export function TwoFactorField() {
|
|
14
24
|
const router = useRouter();
|
|
25
|
+
const { id: documentId } = useDocumentInfo();
|
|
26
|
+
const { user } = useAuth();
|
|
15
27
|
const handleComplete = useCallback(()=>{
|
|
16
28
|
router.refresh();
|
|
17
29
|
}, [
|
|
18
30
|
router
|
|
19
31
|
]);
|
|
32
|
+
// Only block when we KNOW it's a different user.
|
|
33
|
+
// While auth is loading (user is null), show the UI — it's session-based
|
|
34
|
+
// and only operates on the logged-in user's own 2FA regardless.
|
|
35
|
+
if (user && String(documentId) !== String(user.id)) {
|
|
36
|
+
return /*#__PURE__*/ _jsx("div", {
|
|
37
|
+
className: "field-type",
|
|
38
|
+
children: /*#__PURE__*/ _jsx("p", {
|
|
39
|
+
className: "field-description",
|
|
40
|
+
children: "Two-factor authentication can only be managed by the account owner."
|
|
41
|
+
})
|
|
42
|
+
});
|
|
43
|
+
}
|
|
20
44
|
return /*#__PURE__*/ _jsx(TwoFactorManagementClient, {
|
|
21
45
|
onComplete: handleComplete
|
|
22
46
|
});
|
package/dist/index.d.ts
CHANGED
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
*
|
|
7
7
|
* @packageDocumentation
|
|
8
8
|
*/
|
|
9
|
-
export { payloadAdapter } from './adapter/index.js';
|
|
10
|
-
export type { PayloadAdapterConfig } from './adapter/index.js';
|
|
9
|
+
export { payloadAdapter, detectDbType, resolveIdType } from './adapter/index.js';
|
|
10
|
+
export type { PayloadAdapterConfig, DbType } from './adapter/index.js';
|
|
11
11
|
export { betterAuthCollections } from './adapter/collections.js';
|
|
12
12
|
export type { BetterAuthCollectionsOptions } from './adapter/collections.js';
|
|
13
13
|
export { createBetterAuthPlugin, betterAuthStrategy, resetAuthInstance, getApiKeyScopesConfig, } from './plugin/index.js';
|
package/dist/index.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*
|
|
7
7
|
* @packageDocumentation
|
|
8
8
|
*/ // Adapter
|
|
9
|
-
export { payloadAdapter } from './adapter/index.js';
|
|
9
|
+
export { payloadAdapter, detectDbType, resolveIdType } from './adapter/index.js';
|
|
10
10
|
// Collection generator plugin
|
|
11
11
|
export { betterAuthCollections } from './adapter/collections.js';
|
|
12
12
|
// Payload plugin and strategy
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@delmaredigital/payload-better-auth",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.4",
|
|
4
4
|
"description": "Better Auth adapter and plugins for Payload CMS",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -89,17 +89,17 @@
|
|
|
89
89
|
}
|
|
90
90
|
},
|
|
91
91
|
"devDependencies": {
|
|
92
|
-
"@
|
|
93
|
-
"@payloadcms/
|
|
92
|
+
"@better-auth/passkey": "^1.4.18",
|
|
93
|
+
"@payloadcms/next": "^3.77.0",
|
|
94
|
+
"@payloadcms/ui": "^3.77.0",
|
|
94
95
|
"@swc/cli": "^0.6.0",
|
|
95
96
|
"@swc/core": "^1.15.11",
|
|
96
|
-
"@types/node": "^25.
|
|
97
|
+
"@types/node": "^25.3.0",
|
|
97
98
|
"@types/react": "^19.2.14",
|
|
98
99
|
"@vitest/coverage-v8": "^2.1.9",
|
|
99
|
-
"@better-auth/passkey": "^1.4.18",
|
|
100
100
|
"better-auth": "^1.4.18",
|
|
101
101
|
"next": "^16.1.6",
|
|
102
|
-
"payload": "^3.
|
|
102
|
+
"payload": "^3.77.0",
|
|
103
103
|
"react": "^19.2.4",
|
|
104
104
|
"tsx": "^4.21.0",
|
|
105
105
|
"typescript": "^5.9.3",
|