@delmaredigital/payload-better-auth 0.3.7 → 0.3.8

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.
Files changed (160) hide show
  1. package/README.md +12 -1
  2. package/dist/adapter/collections.d.ts.map +1 -1
  3. package/dist/adapter/collections.js +126 -88
  4. package/dist/adapter/collections.js.map +1 -1
  5. package/dist/adapter/index.js +197 -150
  6. package/dist/adapter/index.js.map +1 -1
  7. package/dist/components/BeforeLogin.d.ts +1 -1
  8. package/dist/components/BeforeLogin.d.ts.map +1 -1
  9. package/dist/components/BeforeLogin.js +15 -7
  10. package/dist/components/BeforeLogin.js.map +1 -1
  11. package/dist/components/LoginView.d.ts +2 -2
  12. package/dist/components/LoginView.d.ts.map +1 -1
  13. package/dist/components/LoginView.js +660 -218
  14. package/dist/components/LoginView.js.map +1 -1
  15. package/dist/components/LoginViewWrapper.d.ts +1 -1
  16. package/dist/components/LoginViewWrapper.d.ts.map +1 -1
  17. package/dist/components/LoginViewWrapper.js +14 -4
  18. package/dist/components/LoginViewWrapper.js.map +1 -1
  19. package/dist/components/LogoutButton.d.ts +1 -1
  20. package/dist/components/LogoutButton.d.ts.map +1 -1
  21. package/dist/components/LogoutButton.js +19 -11
  22. package/dist/components/LogoutButton.js.map +1 -1
  23. package/dist/components/PasskeyRegisterButton.d.ts +2 -2
  24. package/dist/components/PasskeyRegisterButton.d.ts.map +1 -1
  25. package/dist/components/PasskeyRegisterButton.js +20 -16
  26. package/dist/components/PasskeyRegisterButton.js.map +1 -1
  27. package/dist/components/PasskeySignInButton.d.ts +2 -2
  28. package/dist/components/PasskeySignInButton.d.ts.map +1 -1
  29. package/dist/components/PasskeySignInButton.js +14 -12
  30. package/dist/components/PasskeySignInButton.js.map +1 -1
  31. package/dist/components/auth/ForgotPasswordView.d.ts +1 -1
  32. package/dist/components/auth/ForgotPasswordView.d.ts.map +1 -1
  33. package/dist/components/auth/ForgotPasswordView.js +133 -43
  34. package/dist/components/auth/ForgotPasswordView.js.map +1 -1
  35. package/dist/components/auth/ResetPasswordView.d.ts +1 -1
  36. package/dist/components/auth/ResetPasswordView.d.ts.map +1 -1
  37. package/dist/components/auth/ResetPasswordView.js +154 -50
  38. package/dist/components/auth/ResetPasswordView.js.map +1 -1
  39. package/dist/components/auth/index.js +2 -2
  40. package/dist/components/auth/index.js.map +1 -1
  41. package/dist/components/management/ApiKeysManagementClient.d.ts +2 -2
  42. package/dist/components/management/ApiKeysManagementClient.d.ts.map +1 -1
  43. package/dist/components/management/ApiKeysManagementClient.js +539 -222
  44. package/dist/components/management/ApiKeysManagementClient.js.map +1 -1
  45. package/dist/components/management/PasskeysManagementClient.d.ts +2 -2
  46. package/dist/components/management/PasskeysManagementClient.d.ts.map +1 -1
  47. package/dist/components/management/PasskeysManagementClient.js +215 -92
  48. package/dist/components/management/PasskeysManagementClient.js.map +1 -1
  49. package/dist/components/management/SecurityNavLinks.d.ts +1 -1
  50. package/dist/components/management/SecurityNavLinks.d.ts.map +1 -1
  51. package/dist/components/management/SecurityNavLinks.js +51 -24
  52. package/dist/components/management/SecurityNavLinks.js.map +1 -1
  53. package/dist/components/management/TwoFactorManagementClient.d.ts +2 -2
  54. package/dist/components/management/TwoFactorManagementClient.d.ts.map +1 -1
  55. package/dist/components/management/TwoFactorManagementClient.js +270 -111
  56. package/dist/components/management/TwoFactorManagementClient.js.map +1 -1
  57. package/dist/components/management/index.js +2 -2
  58. package/dist/components/management/index.js.map +1 -1
  59. package/dist/components/management/views/ApiKeysView.d.ts +1 -1
  60. package/dist/components/management/views/ApiKeysView.d.ts.map +1 -1
  61. package/dist/components/management/views/ApiKeysView.js +19 -4
  62. package/dist/components/management/views/ApiKeysView.js.map +1 -1
  63. package/dist/components/management/views/PasskeysView.d.ts +1 -1
  64. package/dist/components/management/views/PasskeysView.d.ts.map +1 -1
  65. package/dist/components/management/views/PasskeysView.js +16 -4
  66. package/dist/components/management/views/PasskeysView.js.map +1 -1
  67. package/dist/components/management/views/TwoFactorView.d.ts +1 -1
  68. package/dist/components/management/views/TwoFactorView.d.ts.map +1 -1
  69. package/dist/components/management/views/TwoFactorView.js +16 -4
  70. package/dist/components/management/views/TwoFactorView.js.map +1 -1
  71. package/dist/components/management/views/index.js +2 -2
  72. package/dist/components/management/views/index.js.map +1 -1
  73. package/dist/components/twoFactor/TwoFactorSetupView.d.ts +1 -1
  74. package/dist/components/twoFactor/TwoFactorSetupView.d.ts.map +1 -1
  75. package/dist/components/twoFactor/TwoFactorSetupView.js +240 -87
  76. package/dist/components/twoFactor/TwoFactorSetupView.js.map +1 -1
  77. package/dist/components/twoFactor/TwoFactorVerifyView.d.ts +1 -1
  78. package/dist/components/twoFactor/TwoFactorVerifyView.d.ts.map +1 -1
  79. package/dist/components/twoFactor/TwoFactorVerifyView.js +108 -45
  80. package/dist/components/twoFactor/TwoFactorVerifyView.js.map +1 -1
  81. package/dist/components/twoFactor/index.js +2 -2
  82. package/dist/components/twoFactor/index.js.map +1 -1
  83. package/dist/exports/client.js +9 -10
  84. package/dist/exports/client.js.map +1 -1
  85. package/dist/exports/components.js +2 -2
  86. package/dist/exports/components.js.map +1 -1
  87. package/dist/exports/management.js +3 -3
  88. package/dist/exports/management.js.map +1 -1
  89. package/dist/exports/rsc.js +2 -2
  90. package/dist/exports/rsc.js.map +1 -1
  91. package/dist/generated-types.js +4 -2
  92. package/dist/generated-types.js.map +1 -1
  93. package/dist/index.js +6 -6
  94. package/dist/index.js.map +1 -1
  95. package/dist/plugin/index.js +198 -162
  96. package/dist/plugin/index.js.map +1 -1
  97. package/dist/scripts/generate-types.js +66 -50
  98. package/dist/scripts/generate-types.js.map +1 -1
  99. package/dist/types/apiKey.js +7 -2
  100. package/dist/types/apiKey.js.map +1 -1
  101. package/dist/types/betterAuth.js +23 -2
  102. package/dist/types/betterAuth.js.map +1 -1
  103. package/dist/utils/access.js +78 -81
  104. package/dist/utils/access.js.map +1 -1
  105. package/dist/utils/apiKeyAccess.js +65 -72
  106. package/dist/utils/apiKeyAccess.js.map +1 -1
  107. package/dist/utils/betterAuthDefaults.js +8 -8
  108. package/dist/utils/betterAuthDefaults.js.map +1 -1
  109. package/dist/utils/detectAuthConfig.js +8 -11
  110. package/dist/utils/detectAuthConfig.js.map +1 -1
  111. package/dist/utils/detectEnabledPlugins.js +6 -7
  112. package/dist/utils/detectEnabledPlugins.js.map +1 -1
  113. package/dist/utils/firstUserAdmin.js +18 -20
  114. package/dist/utils/firstUserAdmin.js.map +1 -1
  115. package/dist/utils/generateScopes.js +40 -41
  116. package/dist/utils/generateScopes.js.map +1 -1
  117. package/dist/utils/session.js +8 -9
  118. package/dist/utils/session.js.map +1 -1
  119. package/package.json +97 -26
  120. package/src/adapter/collections.ts +621 -0
  121. package/src/adapter/index.ts +712 -0
  122. package/src/components/BeforeLogin.tsx +39 -0
  123. package/src/components/LoginView.tsx +1516 -0
  124. package/src/components/LoginViewWrapper.tsx +35 -0
  125. package/src/components/LogoutButton.tsx +58 -0
  126. package/src/components/PasskeyRegisterButton.tsx +105 -0
  127. package/src/components/PasskeySignInButton.tsx +96 -0
  128. package/src/components/auth/ForgotPasswordView.tsx +274 -0
  129. package/src/components/auth/ResetPasswordView.tsx +331 -0
  130. package/src/components/auth/index.ts +8 -0
  131. package/src/components/management/ApiKeysManagementClient.tsx +988 -0
  132. package/src/components/management/PasskeysManagementClient.tsx +409 -0
  133. package/src/components/management/SecurityNavLinks.tsx +117 -0
  134. package/src/components/management/TwoFactorManagementClient.tsx +560 -0
  135. package/src/components/management/index.ts +20 -0
  136. package/src/components/management/views/ApiKeysView.tsx +57 -0
  137. package/src/components/management/views/PasskeysView.tsx +42 -0
  138. package/src/components/management/views/TwoFactorView.tsx +42 -0
  139. package/src/components/management/views/index.ts +10 -0
  140. package/src/components/twoFactor/TwoFactorSetupView.tsx +515 -0
  141. package/src/components/twoFactor/TwoFactorVerifyView.tsx +238 -0
  142. package/src/components/twoFactor/index.ts +8 -0
  143. package/src/exports/client.ts +77 -0
  144. package/src/exports/components.ts +30 -0
  145. package/src/exports/management.ts +25 -0
  146. package/src/exports/rsc.ts +11 -0
  147. package/src/generated-types.ts +269 -0
  148. package/src/index.ts +135 -0
  149. package/src/plugin/index.ts +834 -0
  150. package/src/scripts/generate-types.ts +269 -0
  151. package/src/types/apiKey.ts +63 -0
  152. package/src/types/betterAuth.ts +253 -0
  153. package/src/utils/access.ts +410 -0
  154. package/src/utils/apiKeyAccess.ts +443 -0
  155. package/src/utils/betterAuthDefaults.ts +102 -0
  156. package/src/utils/detectAuthConfig.ts +47 -0
  157. package/src/utils/detectEnabledPlugins.ts +69 -0
  158. package/src/utils/firstUserAdmin.ts +164 -0
  159. package/src/utils/generateScopes.ts +150 -0
  160. package/src/utils/session.ts +91 -0
@@ -0,0 +1,409 @@
1
+ 'use client'
2
+
3
+ import { useState, useEffect, type FormEvent } from 'react'
4
+ import {
5
+ createPayloadAuthClient,
6
+ type PayloadAuthClient,
7
+ } from '../../exports/client.js'
8
+
9
+ type PasskeyItem = {
10
+ id: string
11
+ name?: string | null
12
+ credentialID?: string
13
+ createdAt: Date
14
+ lastUsedAt?: Date | null
15
+ }
16
+
17
+ export type PasskeysManagementClientProps = {
18
+ /** Optional pre-configured auth client */
19
+ authClient?: PayloadAuthClient
20
+ /** Page title. Default: 'Passkeys' */
21
+ title?: string
22
+ }
23
+
24
+ /**
25
+ * Client component for passkey management.
26
+ * Lists, registers, and deletes passkeys.
27
+ */
28
+ export function PasskeysManagementClient({
29
+ authClient: providedClient,
30
+ title = 'Passkeys',
31
+ }: PasskeysManagementClientProps = {}) {
32
+ const [passkeys, setPasskeys] = useState<PasskeyItem[]>([])
33
+ const [loading, setLoading] = useState(true)
34
+ const [error, setError] = useState<string | null>(null)
35
+ const [success, setSuccess] = useState<string | null>(null)
36
+ const [registering, setRegistering] = useState(false)
37
+ const [deleting, setDeleting] = useState<string | null>(null)
38
+ const [showRegisterForm, setShowRegisterForm] = useState(false)
39
+ const [passkeyName, setPasskeyName] = useState('')
40
+
41
+ const getClient = () => providedClient ?? createPayloadAuthClient()
42
+
43
+ useEffect(() => {
44
+ fetchPasskeys()
45
+ // eslint-disable-next-line react-hooks/exhaustive-deps
46
+ }, [])
47
+
48
+ async function fetchPasskeys() {
49
+ setLoading(true)
50
+ setError(null)
51
+
52
+ try {
53
+ const client = getClient()
54
+ const result = await client.passkey.listUserPasskeys()
55
+
56
+ if (result.error) {
57
+ setError(result.error.message ?? 'Failed to load passkeys')
58
+ } else {
59
+ setPasskeys((result.data as PasskeyItem[]) ?? [])
60
+ }
61
+ } catch {
62
+ setError('Failed to load passkeys')
63
+ } finally {
64
+ setLoading(false)
65
+ }
66
+ }
67
+
68
+ async function handleRegister(e: FormEvent) {
69
+ e.preventDefault()
70
+ setRegistering(true)
71
+ setError(null)
72
+ setSuccess(null)
73
+
74
+ try {
75
+ const client = getClient()
76
+ const result = await client.passkey.addPasskey({
77
+ name: passkeyName || undefined,
78
+ })
79
+
80
+ if (result.error) {
81
+ setError(result.error.message ?? 'Failed to register passkey')
82
+ } else {
83
+ setSuccess('Passkey registered successfully!')
84
+ setShowRegisterForm(false)
85
+ setPasskeyName('')
86
+ fetchPasskeys()
87
+ }
88
+ } catch (err) {
89
+ if (err instanceof Error && err.name === 'NotAllowedError') {
90
+ setError('Passkey registration was cancelled or not allowed')
91
+ } else if (err instanceof Error && err.name === 'InvalidStateError') {
92
+ setError('This passkey is already registered')
93
+ } else {
94
+ setError(err instanceof Error ? err.message : 'Failed to register passkey')
95
+ }
96
+ } finally {
97
+ setRegistering(false)
98
+ }
99
+ }
100
+
101
+ async function handleDelete(passkeyId: string) {
102
+ if (!confirm('Are you sure you want to delete this passkey?')) {
103
+ return
104
+ }
105
+
106
+ setDeleting(passkeyId)
107
+ setError(null)
108
+ setSuccess(null)
109
+
110
+ try {
111
+ const client = getClient()
112
+ const result = await client.passkey.deletePasskey({ id: passkeyId })
113
+
114
+ if (result.error) {
115
+ setError(result.error.message ?? 'Failed to delete passkey')
116
+ } else {
117
+ setPasskeys((prev) => prev.filter((p) => p.id !== passkeyId))
118
+ setSuccess('Passkey deleted successfully')
119
+ }
120
+ } catch {
121
+ setError('Failed to delete passkey')
122
+ } finally {
123
+ setDeleting(null)
124
+ }
125
+ }
126
+
127
+ function formatDate(date?: Date | string | null) {
128
+ if (!date) return 'Never'
129
+ const d = date instanceof Date ? date : new Date(date)
130
+ return d.toLocaleString()
131
+ }
132
+
133
+ return (
134
+ <div
135
+ style={{
136
+ maxWidth: '900px',
137
+ margin: '0 auto',
138
+ padding: 'calc(var(--base) * 2)',
139
+ }}
140
+ >
141
+ <div
142
+ style={{
143
+ display: 'flex',
144
+ justifyContent: 'space-between',
145
+ alignItems: 'center',
146
+ marginBottom: 'calc(var(--base) * 2)',
147
+ }}
148
+ >
149
+ <div>
150
+ <h1
151
+ style={{
152
+ color: 'var(--theme-text)',
153
+ fontSize: 'var(--font-size-h2)',
154
+ fontWeight: 600,
155
+ margin: 0,
156
+ }}
157
+ >
158
+ {title}
159
+ </h1>
160
+ <p
161
+ style={{
162
+ color: 'var(--theme-text)',
163
+ opacity: 0.7,
164
+ fontSize: 'var(--font-size-small)',
165
+ margin: 'calc(var(--base) * 0.5) 0 0 0',
166
+ }}
167
+ >
168
+ Passkeys provide secure, passwordless sign-in using your device's
169
+ biometrics or security keys.
170
+ </p>
171
+ </div>
172
+
173
+ <button
174
+ onClick={() => setShowRegisterForm(true)}
175
+ style={{
176
+ padding: 'calc(var(--base) * 0.5) calc(var(--base) * 1)',
177
+ background: 'var(--theme-elevation-800)',
178
+ border: 'none',
179
+ borderRadius: 'var(--style-radius-s)',
180
+ color: 'var(--theme-elevation-50)',
181
+ fontSize: 'var(--font-size-small)',
182
+ cursor: 'pointer',
183
+ }}
184
+ >
185
+ Add Passkey
186
+ </button>
187
+ </div>
188
+
189
+ {error && (
190
+ <div
191
+ style={{
192
+ color: 'var(--theme-error-500)',
193
+ marginBottom: 'var(--base)',
194
+ fontSize: 'var(--font-size-small)',
195
+ padding: 'calc(var(--base) * 0.75)',
196
+ background: 'var(--theme-error-50)',
197
+ borderRadius: 'var(--style-radius-s)',
198
+ border: '1px solid var(--theme-error-200)',
199
+ }}
200
+ >
201
+ {error}
202
+ </div>
203
+ )}
204
+
205
+ {success && (
206
+ <div
207
+ style={{
208
+ color: 'var(--theme-success-700)',
209
+ marginBottom: 'var(--base)',
210
+ fontSize: 'var(--font-size-small)',
211
+ padding: 'calc(var(--base) * 0.75)',
212
+ background: 'var(--theme-success-50)',
213
+ borderRadius: 'var(--style-radius-s)',
214
+ border: '1px solid var(--theme-success-200)',
215
+ }}
216
+ >
217
+ {success}
218
+ </div>
219
+ )}
220
+
221
+ {showRegisterForm && (
222
+ <div
223
+ style={{
224
+ marginBottom: 'calc(var(--base) * 1.5)',
225
+ padding: 'calc(var(--base) * 1.5)',
226
+ background: 'var(--theme-elevation-50)',
227
+ borderRadius: 'var(--style-radius-m)',
228
+ border: '1px solid var(--theme-elevation-100)',
229
+ }}
230
+ >
231
+ <h2
232
+ style={{
233
+ color: 'var(--theme-text)',
234
+ fontSize: 'var(--font-size-h4)',
235
+ fontWeight: 500,
236
+ margin: '0 0 var(--base) 0',
237
+ }}
238
+ >
239
+ Register New Passkey
240
+ </h2>
241
+ <form onSubmit={handleRegister}>
242
+ <div style={{ marginBottom: 'var(--base)' }}>
243
+ <label
244
+ style={{
245
+ display: 'block',
246
+ color: 'var(--theme-text)',
247
+ fontSize: 'var(--font-size-small)',
248
+ marginBottom: 'calc(var(--base) * 0.25)',
249
+ }}
250
+ >
251
+ Name (optional)
252
+ </label>
253
+ <input
254
+ type="text"
255
+ value={passkeyName}
256
+ onChange={(e) => setPasskeyName(e.target.value)}
257
+ placeholder="e.g., MacBook Pro, iPhone"
258
+ style={{
259
+ width: '100%',
260
+ padding: 'calc(var(--base) * 0.5)',
261
+ background: 'var(--theme-input-bg)',
262
+ border: '1px solid var(--theme-elevation-150)',
263
+ borderRadius: 'var(--style-radius-s)',
264
+ color: 'var(--theme-text)',
265
+ boxSizing: 'border-box',
266
+ }}
267
+ />
268
+ <p
269
+ style={{
270
+ color: 'var(--theme-text)',
271
+ opacity: 0.6,
272
+ fontSize: 'var(--font-size-small)',
273
+ margin: 'calc(var(--base) * 0.25) 0 0 0',
274
+ }}
275
+ >
276
+ Your browser will prompt you to use your device's biometrics or
277
+ security key.
278
+ </p>
279
+ </div>
280
+ <div style={{ display: 'flex', gap: 'calc(var(--base) * 0.5)' }}>
281
+ <button
282
+ type="submit"
283
+ disabled={registering}
284
+ style={{
285
+ padding: 'calc(var(--base) * 0.5) calc(var(--base) * 1)',
286
+ background: 'var(--theme-elevation-800)',
287
+ border: 'none',
288
+ borderRadius: 'var(--style-radius-s)',
289
+ color: 'var(--theme-elevation-50)',
290
+ fontSize: 'var(--font-size-small)',
291
+ cursor: registering ? 'not-allowed' : 'pointer',
292
+ opacity: registering ? 0.7 : 1,
293
+ }}
294
+ >
295
+ {registering ? 'Registering...' : 'Register Passkey'}
296
+ </button>
297
+ <button
298
+ type="button"
299
+ onClick={() => setShowRegisterForm(false)}
300
+ style={{
301
+ padding: 'calc(var(--base) * 0.5) calc(var(--base) * 1)',
302
+ background: 'transparent',
303
+ border: '1px solid var(--theme-elevation-200)',
304
+ borderRadius: 'var(--style-radius-s)',
305
+ color: 'var(--theme-text)',
306
+ fontSize: 'var(--font-size-small)',
307
+ cursor: 'pointer',
308
+ }}
309
+ >
310
+ Cancel
311
+ </button>
312
+ </div>
313
+ </form>
314
+ </div>
315
+ )}
316
+
317
+ {loading ? (
318
+ <div
319
+ style={{
320
+ color: 'var(--theme-text)',
321
+ opacity: 0.7,
322
+ textAlign: 'center',
323
+ padding: 'calc(var(--base) * 3)',
324
+ }}
325
+ >
326
+ Loading passkeys...
327
+ </div>
328
+ ) : passkeys.length === 0 ? (
329
+ <div
330
+ style={{
331
+ color: 'var(--theme-text)',
332
+ opacity: 0.7,
333
+ textAlign: 'center',
334
+ padding: 'calc(var(--base) * 3)',
335
+ }}
336
+ >
337
+ No passkeys registered. Add one to enable passwordless sign-in.
338
+ </div>
339
+ ) : (
340
+ <div
341
+ style={{
342
+ background: 'var(--theme-elevation-50)',
343
+ borderRadius: 'var(--style-radius-m)',
344
+ overflow: 'hidden',
345
+ border: '1px solid var(--theme-elevation-100)',
346
+ }}
347
+ >
348
+ {passkeys.map((pk, index) => (
349
+ <div
350
+ key={pk.id}
351
+ style={{
352
+ display: 'flex',
353
+ justifyContent: 'space-between',
354
+ alignItems: 'center',
355
+ padding: 'calc(var(--base) * 1)',
356
+ borderBottom:
357
+ index < passkeys.length - 1
358
+ ? '1px solid var(--theme-elevation-100)'
359
+ : 'none',
360
+ }}
361
+ >
362
+ <div>
363
+ <div
364
+ style={{
365
+ color: 'var(--theme-text)',
366
+ fontWeight: 500,
367
+ marginBottom: 'calc(var(--base) * 0.25)',
368
+ }}
369
+ >
370
+ {pk.name || 'Passkey'}
371
+ </div>
372
+ <div
373
+ style={{
374
+ color: 'var(--theme-elevation-600)',
375
+ fontSize: 'var(--font-size-small)',
376
+ }}
377
+ >
378
+ <span>Created: {formatDate(pk.createdAt)}</span>
379
+ {pk.lastUsedAt && (
380
+ <span> | Last used: {formatDate(pk.lastUsedAt)}</span>
381
+ )}
382
+ </div>
383
+ </div>
384
+
385
+ <button
386
+ onClick={() => handleDelete(pk.id)}
387
+ disabled={deleting === pk.id}
388
+ style={{
389
+ padding: 'calc(var(--base) * 0.5) calc(var(--base) * 0.75)',
390
+ background: 'transparent',
391
+ border: '1px solid var(--theme-error-300)',
392
+ borderRadius: 'var(--style-radius-s)',
393
+ color: 'var(--theme-error-500)',
394
+ fontSize: 'var(--font-size-small)',
395
+ cursor: deleting === pk.id ? 'not-allowed' : 'pointer',
396
+ opacity: deleting === pk.id ? 0.7 : 1,
397
+ }}
398
+ >
399
+ {deleting === pk.id ? 'Deleting...' : 'Delete'}
400
+ </button>
401
+ </div>
402
+ ))}
403
+ </div>
404
+ )}
405
+ </div>
406
+ )
407
+ }
408
+
409
+ export default PasskeysManagementClient
@@ -0,0 +1,117 @@
1
+ 'use client'
2
+
3
+ export type SecurityNavLinksProps = {
4
+ /** Base path for security views. Default: '/admin/security' */
5
+ basePath?: string
6
+ /** Show Two-Factor Auth link. Default: true */
7
+ showTwoFactor?: boolean
8
+ /** Show API Keys link. Default: true */
9
+ showApiKeys?: boolean
10
+ /** Show Passkeys link. Default: true */
11
+ showPasskeys?: boolean
12
+ }
13
+
14
+ type NavLink = {
15
+ href: string
16
+ label: string
17
+ icon: string
18
+ }
19
+
20
+ /**
21
+ * Navigation links for security management features.
22
+ * Rendered in admin sidebar via afterNavLinks injection.
23
+ * Uses Payload's nav CSS classes for native styling.
24
+ *
25
+ * Links are conditionally shown based on which Better Auth plugins are enabled.
26
+ */
27
+ export function SecurityNavLinks({
28
+ basePath = '/admin/security',
29
+ showTwoFactor = true,
30
+ showApiKeys = true,
31
+ showPasskeys = true,
32
+ }: SecurityNavLinksProps = {}) {
33
+ // Build links based on enabled plugins
34
+ const links: NavLink[] = []
35
+
36
+ if (showTwoFactor) {
37
+ links.push({
38
+ href: `${basePath}/two-factor`,
39
+ label: 'Two-Factor Auth',
40
+ icon: '📱',
41
+ })
42
+ }
43
+
44
+ if (showApiKeys) {
45
+ links.push({
46
+ href: `${basePath}/api-keys`,
47
+ label: 'API Keys',
48
+ icon: '🔑',
49
+ })
50
+ }
51
+
52
+ if (showPasskeys) {
53
+ links.push({
54
+ href: `${basePath}/passkeys`,
55
+ label: 'Passkeys',
56
+ icon: '🔐',
57
+ })
58
+ }
59
+
60
+ // Don't render anything if no plugins are enabled
61
+ if (links.length === 0) {
62
+ return null
63
+ }
64
+
65
+ return (
66
+ <div
67
+ style={{
68
+ borderTop: '1px solid var(--theme-elevation-100)',
69
+ marginTop: 'var(--base)',
70
+ paddingTop: 'var(--base)',
71
+ }}
72
+ >
73
+ <div
74
+ style={{
75
+ fontSize: '11px',
76
+ fontWeight: 600,
77
+ color: 'var(--theme-elevation-500)',
78
+ padding: '0 calc(var(--base) * 0.75)',
79
+ marginBottom: 'calc(var(--base) * 0.5)',
80
+ textTransform: 'uppercase',
81
+ letterSpacing: '0.5px',
82
+ }}
83
+ >
84
+ Security
85
+ </div>
86
+ {links.map((link) => (
87
+ <a
88
+ key={link.href}
89
+ href={link.href}
90
+ className="nav__link"
91
+ style={{
92
+ display: 'flex',
93
+ alignItems: 'center',
94
+ gap: 'calc(var(--base) * 0.5)',
95
+ padding: 'calc(var(--base) * 0.5) calc(var(--base) * 0.75)',
96
+ color: 'var(--theme-elevation-800)',
97
+ textDecoration: 'none',
98
+ fontSize: 'var(--font-size-small)',
99
+ borderRadius: 'var(--style-radius-s)',
100
+ transition: 'background-color 150ms ease',
101
+ }}
102
+ onMouseEnter={(e) => {
103
+ e.currentTarget.style.backgroundColor = 'var(--theme-elevation-50)'
104
+ }}
105
+ onMouseLeave={(e) => {
106
+ e.currentTarget.style.backgroundColor = 'transparent'
107
+ }}
108
+ >
109
+ <span style={{ fontSize: '14px' }}>{link.icon}</span>
110
+ <span className="nav__link-label">{link.label}</span>
111
+ </a>
112
+ ))}
113
+ </div>
114
+ )
115
+ }
116
+
117
+ export default SecurityNavLinks