@delmaredigital/payload-better-auth 0.4.1 → 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/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 +47 -27
- package/package.json +1 -1
|
@@ -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(
|
|
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
|
-
|
|
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: '
|
|
116
|
+
marginBottom: 'var(--base)'
|
|
111
117
|
},
|
|
112
118
|
children: [
|
|
113
|
-
/*#__PURE__*/
|
|
114
|
-
|
|
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
|
-
|
|
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: '
|
|
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__*/
|
|
140
|
+
/*#__PURE__*/ _jsxs("div", {
|
|
184
141
|
style: {
|
|
185
|
-
|
|
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__*/
|
|
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__*/
|
|
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
|
-
|
|
238
|
-
gap: 'calc(var(--base) * 0.5)'
|
|
178
|
+
marginTop: 'calc(var(--base) * 0.25)'
|
|
239
179
|
},
|
|
240
|
-
children:
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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("
|
|
277
|
-
|
|
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("
|
|
285
|
-
|
|
286
|
-
|
|
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
|
-
|
|
295
|
-
borderRadius: 'var(--style-radius-
|
|
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: '
|
|
305
|
-
borderBottom: index < passkeys.length - 1 ? '1px solid var(--theme-
|
|
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("
|
|
237
|
+
/*#__PURE__*/ _jsxs("p", {
|
|
238
|
+
className: "field-description",
|
|
319
239
|
style: {
|
|
320
|
-
|
|
321
|
-
fontSize: 'var(--font-size-small)'
|
|
240
|
+
margin: 'calc(var(--base) * 0.25) 0 0 0'
|
|
322
241
|
},
|
|
323
242
|
children: [
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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(
|
|
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
|
-
*
|
|
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,
|
|
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
|
-
*
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
if (
|
|
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:
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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;
|
|
@@ -1,11 +1,13 @@
|
|
|
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 { CopyIcon } from '@payloadcms/ui/icons/Copy';
|
|
4
6
|
import { createPayloadAuthClient } from '../../exports/client.js';
|
|
5
7
|
/**
|
|
6
8
|
* Client component for two-factor authentication management.
|
|
7
9
|
* Shows 2FA status and allows enabling/disabling.
|
|
8
|
-
*/ export function TwoFactorManagementClient({ authClient: providedClient, title = 'Two-Factor Authentication' } = {}) {
|
|
10
|
+
*/ export function TwoFactorManagementClient({ authClient: providedClient, title = 'Two-Factor Authentication', onComplete } = {}) {
|
|
9
11
|
const [isEnabled, setIsEnabled] = useState(false);
|
|
10
12
|
const [loading, setLoading] = useState(true);
|
|
11
13
|
const [error, setError] = useState(null);
|
|
@@ -43,8 +45,7 @@ import { createPayloadAuthClient } from '../../exports/client.js';
|
|
|
43
45
|
setPassword('');
|
|
44
46
|
setError(null);
|
|
45
47
|
}
|
|
46
|
-
async function handleEnableWithPassword(
|
|
47
|
-
e.preventDefault();
|
|
48
|
+
async function handleEnableWithPassword() {
|
|
48
49
|
setActionLoading(true);
|
|
49
50
|
setError(null);
|
|
50
51
|
try {
|
|
@@ -69,8 +70,7 @@ import { createPayloadAuthClient } from '../../exports/client.js';
|
|
|
69
70
|
setActionLoading(false);
|
|
70
71
|
}
|
|
71
72
|
}
|
|
72
|
-
async function handleVerify(
|
|
73
|
-
e.preventDefault();
|
|
73
|
+
async function handleVerify() {
|
|
74
74
|
setActionLoading(true);
|
|
75
75
|
setError(null);
|
|
76
76
|
try {
|
|
@@ -86,6 +86,7 @@ import { createPayloadAuthClient } from '../../exports/client.js';
|
|
|
86
86
|
} else {
|
|
87
87
|
setIsEnabled(true);
|
|
88
88
|
setStep('status');
|
|
89
|
+
onComplete?.();
|
|
89
90
|
}
|
|
90
91
|
}
|
|
91
92
|
} catch {
|
|
@@ -109,6 +110,7 @@ import { createPayloadAuthClient } from '../../exports/client.js';
|
|
|
109
110
|
setError(result.error.message ?? 'Failed to disable 2FA');
|
|
110
111
|
} else {
|
|
111
112
|
setIsEnabled(false);
|
|
113
|
+
onComplete?.();
|
|
112
114
|
}
|
|
113
115
|
} catch {
|
|
114
116
|
setError('Failed to disable 2FA');
|
|
@@ -119,186 +121,92 @@ import { createPayloadAuthClient } from '../../exports/client.js';
|
|
|
119
121
|
function handleBackupContinue() {
|
|
120
122
|
setIsEnabled(true);
|
|
121
123
|
setStep('status');
|
|
124
|
+
onComplete?.();
|
|
122
125
|
}
|
|
123
126
|
if (loading) {
|
|
124
|
-
return /*#__PURE__*/ _jsx("
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
alignItems: 'center',
|
|
128
|
-
justifyContent: 'center',
|
|
129
|
-
padding: 'calc(var(--base) * 3)'
|
|
130
|
-
},
|
|
131
|
-
children: /*#__PURE__*/ _jsx("div", {
|
|
132
|
-
style: {
|
|
133
|
-
color: 'var(--theme-text)',
|
|
134
|
-
opacity: 0.7
|
|
135
|
-
},
|
|
136
|
-
children: "Loading..."
|
|
137
|
-
})
|
|
127
|
+
return /*#__PURE__*/ _jsx("p", {
|
|
128
|
+
className: "field-description",
|
|
129
|
+
children: "Loading..."
|
|
138
130
|
});
|
|
139
131
|
}
|
|
140
132
|
return /*#__PURE__*/ _jsxs("div", {
|
|
141
|
-
|
|
142
|
-
maxWidth: '600px',
|
|
143
|
-
margin: '0 auto',
|
|
144
|
-
padding: 'calc(var(--base) * 2)'
|
|
145
|
-
},
|
|
133
|
+
className: "field-type two-factor-management",
|
|
146
134
|
children: [
|
|
147
|
-
/*#__PURE__*/ _jsx(
|
|
148
|
-
|
|
149
|
-
color: 'var(--theme-text)',
|
|
150
|
-
fontSize: 'var(--font-size-h2)',
|
|
151
|
-
fontWeight: 600,
|
|
152
|
-
margin: '0 0 calc(var(--base) * 2) 0'
|
|
153
|
-
},
|
|
154
|
-
children: title
|
|
155
|
-
}),
|
|
156
|
-
error && /*#__PURE__*/ _jsx("div", {
|
|
157
|
-
style: {
|
|
158
|
-
color: 'var(--theme-error-500)',
|
|
159
|
-
marginBottom: 'var(--base)',
|
|
160
|
-
fontSize: 'var(--font-size-small)',
|
|
161
|
-
padding: 'calc(var(--base) * 0.75)',
|
|
162
|
-
background: 'var(--theme-error-50)',
|
|
163
|
-
borderRadius: 'var(--style-radius-s)',
|
|
164
|
-
border: '1px solid var(--theme-error-200)'
|
|
165
|
-
},
|
|
135
|
+
error && /*#__PURE__*/ _jsx(Banner, {
|
|
136
|
+
type: "error",
|
|
166
137
|
children: error
|
|
167
138
|
}),
|
|
168
|
-
step === 'status' && /*#__PURE__*/
|
|
139
|
+
step === 'status' && /*#__PURE__*/ _jsxs("div", {
|
|
169
140
|
style: {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
border: '1px solid var(--theme-elevation-100)'
|
|
174
|
-
},
|
|
175
|
-
children: /*#__PURE__*/ _jsxs("div", {
|
|
176
|
-
style: {
|
|
177
|
-
display: 'flex',
|
|
178
|
-
justifyContent: 'space-between',
|
|
179
|
-
alignItems: 'center'
|
|
180
|
-
},
|
|
181
|
-
children: [
|
|
182
|
-
/*#__PURE__*/ _jsxs("div", {
|
|
183
|
-
children: [
|
|
184
|
-
/*#__PURE__*/ _jsx("div", {
|
|
185
|
-
style: {
|
|
186
|
-
color: 'var(--theme-text)',
|
|
187
|
-
fontWeight: 500,
|
|
188
|
-
marginBottom: 'calc(var(--base) * 0.25)'
|
|
189
|
-
},
|
|
190
|
-
children: "Status"
|
|
191
|
-
}),
|
|
192
|
-
/*#__PURE__*/ _jsx("div", {
|
|
193
|
-
style: {
|
|
194
|
-
color: isEnabled ? 'var(--theme-success-500)' : 'var(--theme-elevation-600)',
|
|
195
|
-
fontSize: 'var(--font-size-small)'
|
|
196
|
-
},
|
|
197
|
-
children: isEnabled ? '✓ Enabled' : 'Not enabled'
|
|
198
|
-
})
|
|
199
|
-
]
|
|
200
|
-
}),
|
|
201
|
-
/*#__PURE__*/ _jsx("button", {
|
|
202
|
-
onClick: isEnabled ? handleDisable : handleEnableClick,
|
|
203
|
-
disabled: actionLoading,
|
|
204
|
-
style: {
|
|
205
|
-
padding: 'calc(var(--base) * 0.5) calc(var(--base) * 1)',
|
|
206
|
-
background: isEnabled ? 'var(--theme-error-500)' : 'var(--theme-elevation-800)',
|
|
207
|
-
border: 'none',
|
|
208
|
-
borderRadius: 'var(--style-radius-s)',
|
|
209
|
-
color: 'var(--theme-elevation-50)',
|
|
210
|
-
fontSize: 'var(--font-size-small)',
|
|
211
|
-
cursor: actionLoading ? 'not-allowed' : 'pointer',
|
|
212
|
-
opacity: actionLoading ? 0.7 : 1
|
|
213
|
-
},
|
|
214
|
-
children: actionLoading ? 'Loading...' : isEnabled ? 'Disable' : 'Enable'
|
|
215
|
-
})
|
|
216
|
-
]
|
|
217
|
-
})
|
|
218
|
-
}),
|
|
219
|
-
step === 'password' && /*#__PURE__*/ _jsxs("div", {
|
|
220
|
-
style: {
|
|
221
|
-
background: 'var(--theme-elevation-50)',
|
|
222
|
-
padding: 'calc(var(--base) * 2)',
|
|
223
|
-
borderRadius: 'var(--style-radius-m)',
|
|
224
|
-
border: '1px solid var(--theme-elevation-100)'
|
|
141
|
+
display: 'flex',
|
|
142
|
+
justifyContent: 'space-between',
|
|
143
|
+
alignItems: 'center'
|
|
225
144
|
},
|
|
226
145
|
children: [
|
|
227
|
-
/*#__PURE__*/ _jsx("
|
|
146
|
+
/*#__PURE__*/ _jsx("p", {
|
|
147
|
+
className: "field-description",
|
|
228
148
|
style: {
|
|
229
|
-
|
|
230
|
-
fontSize: 'var(--font-size-h4)',
|
|
231
|
-
fontWeight: 500,
|
|
232
|
-
margin: '0 0 var(--base) 0'
|
|
149
|
+
margin: 0
|
|
233
150
|
},
|
|
234
|
-
children:
|
|
151
|
+
children: isEnabled ? 'Two-factor authentication is enabled.' : 'Two-factor authentication is not enabled.'
|
|
235
152
|
}),
|
|
153
|
+
/*#__PURE__*/ _jsx(Button, {
|
|
154
|
+
buttonStyle: isEnabled ? 'error' : 'secondary',
|
|
155
|
+
size: "small",
|
|
156
|
+
onClick: isEnabled ? handleDisable : handleEnableClick,
|
|
157
|
+
disabled: actionLoading,
|
|
158
|
+
children: actionLoading ? 'Loading...' : isEnabled ? 'Disable' : 'Enable'
|
|
159
|
+
})
|
|
160
|
+
]
|
|
161
|
+
}),
|
|
162
|
+
step === 'password' && /*#__PURE__*/ _jsxs("div", {
|
|
163
|
+
children: [
|
|
236
164
|
/*#__PURE__*/ _jsx("p", {
|
|
165
|
+
className: "field-description",
|
|
166
|
+
children: "Enter your password to enable two-factor authentication."
|
|
167
|
+
}),
|
|
168
|
+
/*#__PURE__*/ _jsx("input", {
|
|
169
|
+
type: "password",
|
|
170
|
+
value: password,
|
|
171
|
+
onChange: (e)=>setPassword(e.target.value),
|
|
172
|
+
onKeyDown: (e)=>{
|
|
173
|
+
if (e.key === 'Enter' && password) {
|
|
174
|
+
e.preventDefault();
|
|
175
|
+
handleEnableWithPassword();
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
placeholder: "Enter your password",
|
|
179
|
+
className: "field-type__wrap",
|
|
237
180
|
style: {
|
|
181
|
+
width: '100%',
|
|
182
|
+
padding: 'var(--base)',
|
|
183
|
+
background: 'var(--theme-input-bg)',
|
|
184
|
+
border: '1px solid var(--theme-border-color)',
|
|
185
|
+
borderRadius: 'var(--style-radius-s)',
|
|
238
186
|
color: 'var(--theme-text)',
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
}
|
|
243
|
-
children: "Enter your password to enable two-factor authentication."
|
|
187
|
+
fontSize: 'var(--base-body-size)',
|
|
188
|
+
marginBottom: 'var(--base)',
|
|
189
|
+
boxSizing: 'border-box'
|
|
190
|
+
}
|
|
244
191
|
}),
|
|
245
|
-
/*#__PURE__*/ _jsxs("
|
|
246
|
-
|
|
192
|
+
/*#__PURE__*/ _jsxs("div", {
|
|
193
|
+
style: {
|
|
194
|
+
display: 'flex',
|
|
195
|
+
gap: 'calc(var(--base) * 0.5)'
|
|
196
|
+
},
|
|
247
197
|
children: [
|
|
248
|
-
/*#__PURE__*/ _jsx(
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
style: {
|
|
255
|
-
width: '100%',
|
|
256
|
-
padding: 'calc(var(--base) * 0.75)',
|
|
257
|
-
background: 'var(--theme-input-bg)',
|
|
258
|
-
border: '1px solid var(--theme-elevation-150)',
|
|
259
|
-
borderRadius: 'var(--style-radius-s)',
|
|
260
|
-
color: 'var(--theme-text)',
|
|
261
|
-
fontSize: 'var(--font-size-base)',
|
|
262
|
-
marginBottom: 'var(--base)',
|
|
263
|
-
boxSizing: 'border-box'
|
|
264
|
-
}
|
|
198
|
+
/*#__PURE__*/ _jsx(Button, {
|
|
199
|
+
buttonStyle: "primary",
|
|
200
|
+
size: "small",
|
|
201
|
+
onClick: handleEnableWithPassword,
|
|
202
|
+
disabled: actionLoading || !password,
|
|
203
|
+
children: actionLoading ? 'Enabling...' : 'Continue'
|
|
265
204
|
}),
|
|
266
|
-
/*#__PURE__*/
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
children: [
|
|
272
|
-
/*#__PURE__*/ _jsx("button", {
|
|
273
|
-
type: "submit",
|
|
274
|
-
disabled: actionLoading || !password,
|
|
275
|
-
style: {
|
|
276
|
-
padding: 'calc(var(--base) * 0.75) calc(var(--base) * 1.5)',
|
|
277
|
-
background: 'var(--theme-elevation-800)',
|
|
278
|
-
border: 'none',
|
|
279
|
-
borderRadius: 'var(--style-radius-s)',
|
|
280
|
-
color: 'var(--theme-elevation-50)',
|
|
281
|
-
fontSize: 'var(--font-size-base)',
|
|
282
|
-
cursor: actionLoading || !password ? 'not-allowed' : 'pointer',
|
|
283
|
-
opacity: actionLoading || !password ? 0.7 : 1
|
|
284
|
-
},
|
|
285
|
-
children: actionLoading ? 'Enabling...' : 'Continue'
|
|
286
|
-
}),
|
|
287
|
-
/*#__PURE__*/ _jsx("button", {
|
|
288
|
-
type: "button",
|
|
289
|
-
onClick: ()=>setStep('status'),
|
|
290
|
-
style: {
|
|
291
|
-
padding: 'calc(var(--base) * 0.75) calc(var(--base) * 1.5)',
|
|
292
|
-
background: 'transparent',
|
|
293
|
-
border: '1px solid var(--theme-elevation-200)',
|
|
294
|
-
borderRadius: 'var(--style-radius-s)',
|
|
295
|
-
color: 'var(--theme-text)',
|
|
296
|
-
fontSize: 'var(--font-size-base)',
|
|
297
|
-
cursor: 'pointer'
|
|
298
|
-
},
|
|
299
|
-
children: "Cancel"
|
|
300
|
-
})
|
|
301
|
-
]
|
|
205
|
+
/*#__PURE__*/ _jsx(Button, {
|
|
206
|
+
buttonStyle: "secondary",
|
|
207
|
+
size: "small",
|
|
208
|
+
onClick: ()=>setStep('status'),
|
|
209
|
+
children: "Cancel"
|
|
302
210
|
})
|
|
303
211
|
]
|
|
304
212
|
})
|
|
@@ -306,18 +214,11 @@ import { createPayloadAuthClient } from '../../exports/client.js';
|
|
|
306
214
|
}),
|
|
307
215
|
step === 'setup' && totpUri && /*#__PURE__*/ _jsxs("div", {
|
|
308
216
|
style: {
|
|
309
|
-
background: 'var(--theme-elevation-50)',
|
|
310
|
-
padding: 'calc(var(--base) * 2)',
|
|
311
|
-
borderRadius: 'var(--style-radius-m)',
|
|
312
217
|
textAlign: 'center'
|
|
313
218
|
},
|
|
314
219
|
children: [
|
|
315
220
|
/*#__PURE__*/ _jsx("p", {
|
|
316
|
-
|
|
317
|
-
color: 'var(--theme-text)',
|
|
318
|
-
opacity: 0.7,
|
|
319
|
-
marginBottom: 'calc(var(--base) * 1.5)'
|
|
320
|
-
},
|
|
221
|
+
className: "field-description",
|
|
321
222
|
children: "Scan this QR code with your authenticator app:"
|
|
322
223
|
}),
|
|
323
224
|
/*#__PURE__*/ _jsx("img", {
|
|
@@ -326,7 +227,7 @@ import { createPayloadAuthClient } from '../../exports/client.js';
|
|
|
326
227
|
style: {
|
|
327
228
|
width: '200px',
|
|
328
229
|
height: '200px',
|
|
329
|
-
border: '1px solid var(--theme-
|
|
230
|
+
border: '1px solid var(--theme-border-color)',
|
|
330
231
|
borderRadius: 'var(--style-radius-s)',
|
|
331
232
|
marginBottom: 'var(--base)'
|
|
332
233
|
}
|
|
@@ -337,10 +238,8 @@ import { createPayloadAuthClient } from '../../exports/client.js';
|
|
|
337
238
|
},
|
|
338
239
|
children: [
|
|
339
240
|
/*#__PURE__*/ _jsx("p", {
|
|
241
|
+
className: "field-description",
|
|
340
242
|
style: {
|
|
341
|
-
color: 'var(--theme-text)',
|
|
342
|
-
opacity: 0.7,
|
|
343
|
-
fontSize: 'var(--font-size-small)',
|
|
344
243
|
marginBottom: 'calc(var(--base) * 0.5)'
|
|
345
244
|
},
|
|
346
245
|
children: "Or enter manually:"
|
|
@@ -352,15 +251,14 @@ import { createPayloadAuthClient } from '../../exports/client.js';
|
|
|
352
251
|
background: 'var(--theme-elevation-100)',
|
|
353
252
|
borderRadius: 'var(--style-radius-s)',
|
|
354
253
|
fontFamily: 'monospace',
|
|
355
|
-
fontSize: 'var(--
|
|
254
|
+
fontSize: 'var(--base-body-size)',
|
|
356
255
|
color: 'var(--theme-text)'
|
|
357
256
|
},
|
|
358
257
|
children: secret
|
|
359
258
|
})
|
|
360
259
|
]
|
|
361
260
|
}),
|
|
362
|
-
/*#__PURE__*/ _jsxs("
|
|
363
|
-
onSubmit: handleVerify,
|
|
261
|
+
/*#__PURE__*/ _jsxs("div", {
|
|
364
262
|
children: [
|
|
365
263
|
/*#__PURE__*/ _jsx("input", {
|
|
366
264
|
type: "text",
|
|
@@ -368,16 +266,21 @@ import { createPayloadAuthClient } from '../../exports/client.js';
|
|
|
368
266
|
pattern: "[0-9]*",
|
|
369
267
|
value: verificationCode,
|
|
370
268
|
onChange: (e)=>setVerificationCode(e.target.value.replace(/\D/g, '').slice(0, 6)),
|
|
371
|
-
|
|
269
|
+
onKeyDown: (e)=>{
|
|
270
|
+
if (e.key === 'Enter' && verificationCode.length === 6) {
|
|
271
|
+
e.preventDefault();
|
|
272
|
+
handleVerify();
|
|
273
|
+
}
|
|
274
|
+
},
|
|
275
|
+
placeholder: "000000",
|
|
372
276
|
style: {
|
|
373
|
-
width: '
|
|
374
|
-
|
|
375
|
-
padding: 'calc(var(--base) * 0.75)',
|
|
277
|
+
width: '200px',
|
|
278
|
+
padding: 'var(--base)',
|
|
376
279
|
background: 'var(--theme-input-bg)',
|
|
377
|
-
border: '1px solid var(--theme-
|
|
280
|
+
border: '1px solid var(--theme-border-color)',
|
|
378
281
|
borderRadius: 'var(--style-radius-s)',
|
|
379
282
|
color: 'var(--theme-text)',
|
|
380
|
-
fontSize: '
|
|
283
|
+
fontSize: '1.5rem',
|
|
381
284
|
fontFamily: 'monospace',
|
|
382
285
|
textAlign: 'center',
|
|
383
286
|
letterSpacing: '0.5em',
|
|
@@ -386,19 +289,11 @@ import { createPayloadAuthClient } from '../../exports/client.js';
|
|
|
386
289
|
}
|
|
387
290
|
}),
|
|
388
291
|
/*#__PURE__*/ _jsx("br", {}),
|
|
389
|
-
/*#__PURE__*/ _jsx(
|
|
390
|
-
|
|
292
|
+
/*#__PURE__*/ _jsx(Button, {
|
|
293
|
+
buttonStyle: "primary",
|
|
294
|
+
size: "small",
|
|
295
|
+
onClick: handleVerify,
|
|
391
296
|
disabled: actionLoading || verificationCode.length !== 6,
|
|
392
|
-
style: {
|
|
393
|
-
padding: 'calc(var(--base) * 0.75) calc(var(--base) * 2)',
|
|
394
|
-
background: 'var(--theme-elevation-800)',
|
|
395
|
-
border: 'none',
|
|
396
|
-
borderRadius: 'var(--style-radius-s)',
|
|
397
|
-
color: 'var(--theme-elevation-50)',
|
|
398
|
-
fontSize: 'var(--font-size-base)',
|
|
399
|
-
cursor: actionLoading || verificationCode.length !== 6 ? 'not-allowed' : 'pointer',
|
|
400
|
-
opacity: actionLoading || verificationCode.length !== 6 ? 0.7 : 1
|
|
401
|
-
},
|
|
402
297
|
children: actionLoading ? 'Verifying...' : 'Verify'
|
|
403
298
|
})
|
|
404
299
|
]
|
|
@@ -406,37 +301,17 @@ import { createPayloadAuthClient } from '../../exports/client.js';
|
|
|
406
301
|
]
|
|
407
302
|
}),
|
|
408
303
|
step === 'backup' && /*#__PURE__*/ _jsxs("div", {
|
|
409
|
-
style: {
|
|
410
|
-
background: 'var(--theme-elevation-50)',
|
|
411
|
-
padding: 'calc(var(--base) * 2)',
|
|
412
|
-
borderRadius: 'var(--style-radius-m)'
|
|
413
|
-
},
|
|
414
304
|
children: [
|
|
415
|
-
/*#__PURE__*/ _jsx(
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
fontSize: 'var(--font-size-h3)',
|
|
419
|
-
fontWeight: 600,
|
|
420
|
-
margin: '0 0 var(--base) 0',
|
|
421
|
-
textAlign: 'center'
|
|
422
|
-
},
|
|
423
|
-
children: "Save Your Backup Codes"
|
|
424
|
-
}),
|
|
425
|
-
/*#__PURE__*/ _jsx("p", {
|
|
426
|
-
style: {
|
|
427
|
-
color: 'var(--theme-text)',
|
|
428
|
-
opacity: 0.7,
|
|
429
|
-
fontSize: 'var(--font-size-small)',
|
|
430
|
-
textAlign: 'center',
|
|
431
|
-
marginBottom: 'calc(var(--base) * 1.5)'
|
|
432
|
-
},
|
|
433
|
-
children: "Store these codes safely. Use them if you lose your authenticator."
|
|
305
|
+
/*#__PURE__*/ _jsx(Banner, {
|
|
306
|
+
type: "info",
|
|
307
|
+
children: "Save these backup codes in a safe place. You can use them to sign in if you lose access to your authenticator app."
|
|
434
308
|
}),
|
|
435
309
|
/*#__PURE__*/ _jsx("div", {
|
|
436
310
|
style: {
|
|
437
311
|
background: 'var(--theme-elevation-100)',
|
|
438
312
|
padding: 'var(--base)',
|
|
439
313
|
borderRadius: 'var(--style-radius-s)',
|
|
314
|
+
marginTop: 'var(--base)',
|
|
440
315
|
marginBottom: 'var(--base)',
|
|
441
316
|
fontFamily: 'monospace'
|
|
442
317
|
},
|
|
@@ -455,34 +330,26 @@ import { createPayloadAuthClient } from '../../exports/client.js';
|
|
|
455
330
|
}, index))
|
|
456
331
|
})
|
|
457
332
|
}),
|
|
458
|
-
/*#__PURE__*/
|
|
459
|
-
onClick: ()=>navigator.clipboard.writeText(backupCodes.join('\n')),
|
|
460
|
-
style: {
|
|
461
|
-
width: '100%',
|
|
462
|
-
padding: 'calc(var(--base) * 0.5)',
|
|
463
|
-
background: 'var(--theme-elevation-150)',
|
|
464
|
-
border: 'none',
|
|
465
|
-
borderRadius: 'var(--style-radius-s)',
|
|
466
|
-
color: 'var(--theme-text)',
|
|
467
|
-
fontSize: 'var(--font-size-small)',
|
|
468
|
-
cursor: 'pointer',
|
|
469
|
-
marginBottom: 'var(--base)'
|
|
470
|
-
},
|
|
471
|
-
children: "Copy to Clipboard"
|
|
472
|
-
}),
|
|
473
|
-
/*#__PURE__*/ _jsx("button", {
|
|
474
|
-
onClick: handleBackupContinue,
|
|
333
|
+
/*#__PURE__*/ _jsxs("div", {
|
|
475
334
|
style: {
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
background: 'var(--theme-elevation-800)',
|
|
479
|
-
border: 'none',
|
|
480
|
-
borderRadius: 'var(--style-radius-s)',
|
|
481
|
-
color: 'var(--theme-elevation-50)',
|
|
482
|
-
fontSize: 'var(--font-size-base)',
|
|
483
|
-
cursor: 'pointer'
|
|
335
|
+
display: 'flex',
|
|
336
|
+
gap: 'calc(var(--base) * 0.5)'
|
|
484
337
|
},
|
|
485
|
-
children:
|
|
338
|
+
children: [
|
|
339
|
+
/*#__PURE__*/ _jsx(Button, {
|
|
340
|
+
buttonStyle: "secondary",
|
|
341
|
+
size: "small",
|
|
342
|
+
icon: /*#__PURE__*/ _jsx(CopyIcon, {}),
|
|
343
|
+
onClick: ()=>navigator.clipboard.writeText(backupCodes.join('\n')),
|
|
344
|
+
children: "Copy to Clipboard"
|
|
345
|
+
}),
|
|
346
|
+
/*#__PURE__*/ _jsx(Button, {
|
|
347
|
+
buttonStyle: "primary",
|
|
348
|
+
size: "small",
|
|
349
|
+
onClick: handleBackupContinue,
|
|
350
|
+
children: "I've Saved My Codes"
|
|
351
|
+
})
|
|
352
|
+
]
|
|
486
353
|
})
|
|
487
354
|
]
|
|
488
355
|
})
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { PasskeysManagementClient } from '../PasskeysManagementClient.js';
|
|
4
|
+
/**
|
|
5
|
+
* Thin wrapper around PasskeysManagementClient for use as a Payload `ui` field.
|
|
6
|
+
*/ export function PasskeysField() {
|
|
7
|
+
return /*#__PURE__*/ _jsx(PasskeysManagementClient, {});
|
|
8
|
+
}
|
|
9
|
+
export default PasskeysField;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wrapper around TwoFactorManagementClient for use as a Payload `ui` field.
|
|
3
|
+
*
|
|
4
|
+
* After 2FA is enabled or disabled, triggers a Next.js router refresh so that
|
|
5
|
+
* the document form re-fetches from the DB and picks up the `twoFactorEnabled`
|
|
6
|
+
* value that Better Auth wrote. Without this, navigating away without clicking
|
|
7
|
+
* Save would overwrite the change.
|
|
8
|
+
*/
|
|
9
|
+
export declare function TwoFactorField(): import("react").JSX.Element;
|
|
10
|
+
export default TwoFactorField;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { useRouter } from 'next/navigation.js';
|
|
4
|
+
import { useCallback } from 'react';
|
|
5
|
+
import { TwoFactorManagementClient } from '../TwoFactorManagementClient.js';
|
|
6
|
+
/**
|
|
7
|
+
* Wrapper around TwoFactorManagementClient for use as a Payload `ui` field.
|
|
8
|
+
*
|
|
9
|
+
* After 2FA is enabled or disabled, triggers a Next.js router refresh so that
|
|
10
|
+
* the document form re-fetches from the DB and picks up the `twoFactorEnabled`
|
|
11
|
+
* value that Better Auth wrote. Without this, navigating away without clicking
|
|
12
|
+
* Save would overwrite the change.
|
|
13
|
+
*/ export function TwoFactorField() {
|
|
14
|
+
const router = useRouter();
|
|
15
|
+
const handleComplete = useCallback(()=>{
|
|
16
|
+
router.refresh();
|
|
17
|
+
}, [
|
|
18
|
+
router
|
|
19
|
+
]);
|
|
20
|
+
return /*#__PURE__*/ _jsx(TwoFactorManagementClient, {
|
|
21
|
+
onComplete: handleComplete
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
export default TwoFactorField;
|
|
@@ -21,3 +21,5 @@ export type { PasskeySignInButtonProps } from '../components/PasskeySignInButton
|
|
|
21
21
|
export { PasskeySignInButton } from '../components/PasskeySignInButton.js';
|
|
22
22
|
export type { PasskeyRegisterButtonProps } from '../components/PasskeyRegisterButton.js';
|
|
23
23
|
export { PasskeyRegisterButton } from '../components/PasskeyRegisterButton.js';
|
|
24
|
+
export { TwoFactorField } from '../components/management/fields/TwoFactorField.js';
|
|
25
|
+
export { PasskeysField } from '../components/management/fields/PasskeysField.js';
|
|
@@ -12,3 +12,6 @@ export { TwoFactorSetupView } from '../components/twoFactor/TwoFactorSetupView.j
|
|
|
12
12
|
export { TwoFactorVerifyView } from '../components/twoFactor/TwoFactorVerifyView.js';
|
|
13
13
|
export { PasskeySignInButton } from '../components/PasskeySignInButton.js';
|
|
14
14
|
export { PasskeyRegisterButton } from '../components/PasskeyRegisterButton.js';
|
|
15
|
+
// Management UI field wrappers (for Payload ui fields)
|
|
16
|
+
export { TwoFactorField } from '../components/management/fields/TwoFactorField.js';
|
|
17
|
+
export { PasskeysField } from '../components/management/fields/PasskeysField.js';
|
package/dist/plugin/index.js
CHANGED
|
@@ -314,6 +314,9 @@ let apiKeyScopesConfig = undefined;
|
|
|
314
314
|
}
|
|
315
315
|
/**
|
|
316
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)
|
|
317
320
|
*/ function injectManagementComponents(config, options) {
|
|
318
321
|
const adminOptions = options.admin ?? {};
|
|
319
322
|
// Skip if management UI is disabled
|
|
@@ -324,55 +327,72 @@ let apiKeyScopesConfig = undefined;
|
|
|
324
327
|
const enabledPlugins = detectEnabledPlugins(adminOptions.betterAuthOptions);
|
|
325
328
|
// Get custom paths or use defaults
|
|
326
329
|
const paths = {
|
|
327
|
-
|
|
328
|
-
apiKeys: adminOptions.managementPaths?.apiKeys ?? '/security/api-keys',
|
|
329
|
-
passkeys: adminOptions.managementPaths?.passkeys ?? '/security/passkeys'
|
|
330
|
+
apiKeys: adminOptions.managementPaths?.apiKeys ?? '/security/api-keys'
|
|
330
331
|
};
|
|
331
332
|
const existingComponents = config.admin?.components ?? {};
|
|
332
333
|
const existingViews = existingComponents.views ?? {};
|
|
333
334
|
const existingAfterNavLinks = existingComponents.afterNavLinks ?? [];
|
|
334
|
-
// Build management views
|
|
335
|
-
// Note: Sessions and passkeys use Payload's default collection views
|
|
335
|
+
// Build management views — only API Keys stays as a sidebar view
|
|
336
336
|
const managementViews = {};
|
|
337
|
-
// Two-factor (if enabled)
|
|
338
|
-
if (enabledPlugins.hasTwoFactor) {
|
|
339
|
-
managementViews.securityTwoFactor = {
|
|
340
|
-
Component: '@delmaredigital/payload-better-auth/rsc#TwoFactorView',
|
|
341
|
-
path: paths.twoFactor
|
|
342
|
-
};
|
|
343
|
-
}
|
|
344
|
-
// API keys (if enabled)
|
|
345
337
|
if (enabledPlugins.hasApiKey) {
|
|
346
338
|
managementViews.securityApiKeys = {
|
|
347
339
|
Component: '@delmaredigital/payload-better-auth/rsc#ApiKeysView',
|
|
348
340
|
path: paths.apiKeys
|
|
349
341
|
};
|
|
350
342
|
}
|
|
351
|
-
//
|
|
352
|
-
|
|
353
|
-
managementViews.securityPasskeys = {
|
|
354
|
-
Component: '@delmaredigital/payload-better-auth/rsc#PasskeysView',
|
|
355
|
-
path: paths.passkeys
|
|
356
|
-
};
|
|
357
|
-
}
|
|
358
|
-
// Only add nav links if at least one plugin is enabled
|
|
359
|
-
const hasAnyPlugin = enabledPlugins.hasTwoFactor || enabledPlugins.hasApiKey || enabledPlugins.hasPasskey;
|
|
360
|
-
// Add SecurityNavLinks to afterNavLinks with clientProps for enabled plugins
|
|
361
|
-
const afterNavLinks = hasAnyPlugin ? [
|
|
343
|
+
// Only add nav links if API Keys is enabled
|
|
344
|
+
const afterNavLinks = enabledPlugins.hasApiKey ? [
|
|
362
345
|
...Array.isArray(existingAfterNavLinks) ? existingAfterNavLinks : [
|
|
363
346
|
existingAfterNavLinks
|
|
364
347
|
],
|
|
365
348
|
{
|
|
366
349
|
path: '@delmaredigital/payload-better-auth/components/management#SecurityNavLinks',
|
|
367
350
|
clientProps: {
|
|
368
|
-
|
|
369
|
-
showApiKeys: enabledPlugins.hasApiKey,
|
|
370
|
-
showPasskeys: enabledPlugins.hasPasskey
|
|
351
|
+
showApiKeys: enabledPlugins.hasApiKey
|
|
371
352
|
}
|
|
372
353
|
}
|
|
373
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
|
+
});
|
|
374
393
|
return {
|
|
375
394
|
...config,
|
|
395
|
+
collections,
|
|
376
396
|
admin: {
|
|
377
397
|
...config.admin,
|
|
378
398
|
components: {
|