@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,42 @@
1
+ import type { AdminViewProps, Locale } from 'payload'
2
+ import { DefaultTemplate } from '@payloadcms/next/templates'
3
+ import { getVisibleEntities } from '@payloadcms/ui/shared'
4
+ import { TwoFactorManagementClient } from '../TwoFactorManagementClient.js'
5
+
6
+ type TwoFactorViewProps = AdminViewProps
7
+
8
+ /**
9
+ * Two-factor management view for Payload admin panel.
10
+ * Server component that provides the admin layout.
11
+ */
12
+ export async function TwoFactorView({
13
+ initPageResult,
14
+ params,
15
+ searchParams,
16
+ }: TwoFactorViewProps) {
17
+ const { req } = initPageResult
18
+ const { payload } = req
19
+
20
+ // Await params/searchParams for Next.js 15+ compatibility
21
+ const resolvedParams = params ? await params : undefined
22
+ const resolvedSearchParams = searchParams ? await searchParams : undefined
23
+
24
+ const visibleEntities = getVisibleEntities({ req })
25
+
26
+ return (
27
+ <DefaultTemplate
28
+ i18n={req.i18n}
29
+ locale={req.locale as Locale | undefined}
30
+ params={resolvedParams}
31
+ payload={payload}
32
+ permissions={initPageResult.permissions}
33
+ searchParams={resolvedSearchParams}
34
+ user={req.user ?? undefined}
35
+ visibleEntities={visibleEntities}
36
+ >
37
+ <TwoFactorManagementClient />
38
+ </DefaultTemplate>
39
+ )
40
+ }
41
+
42
+ export default TwoFactorView
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Server Component Views for Payload Admin Panel
3
+ *
4
+ * These are async server components that use DefaultTemplate
5
+ * for proper integration with Payload's admin layout.
6
+ */
7
+
8
+ export { TwoFactorView } from './TwoFactorView.js'
9
+ export { ApiKeysView } from './ApiKeysView.js'
10
+ export { PasskeysView } from './PasskeysView.js'
@@ -0,0 +1,515 @@
1
+ 'use client'
2
+
3
+ import { useState, useEffect, type FormEvent } from 'react'
4
+
5
+ export type TwoFactorSetupViewProps = {
6
+ /** Custom logo element */
7
+ logo?: React.ReactNode
8
+ /** Page title. Default: 'Set Up Two-Factor Authentication' */
9
+ title?: string
10
+ /** Path to redirect after successful setup. Default: '/admin' */
11
+ afterSetupPath?: string
12
+ /** Callback after successful setup */
13
+ onSetupComplete?: () => void
14
+ }
15
+
16
+ /**
17
+ * Two-factor authentication setup component.
18
+ * Displays QR code for TOTP apps and allows verification.
19
+ * Uses Better Auth's twoFactor plugin endpoints.
20
+ */
21
+ export function TwoFactorSetupView({
22
+ logo,
23
+ title = 'Set Up Two-Factor Authentication',
24
+ afterSetupPath = '/admin',
25
+ onSetupComplete,
26
+ }: TwoFactorSetupViewProps) {
27
+ const [step, setStep] = useState<'loading' | 'qr' | 'verify' | 'backup' | 'complete'>('loading')
28
+ const [totpUri, setTotpUri] = useState<string | null>(null)
29
+ const [secret, setSecret] = useState<string | null>(null)
30
+ const [backupCodes, setBackupCodes] = useState<string[]>([])
31
+ const [verificationCode, setVerificationCode] = useState('')
32
+ const [error, setError] = useState<string | null>(null)
33
+ const [loading, setLoading] = useState(false)
34
+
35
+ useEffect(() => {
36
+ async function enableTwoFactor() {
37
+ try {
38
+ const response = await fetch('/api/auth/two-factor/enable', {
39
+ method: 'POST',
40
+ headers: { 'Content-Type': 'application/json' },
41
+ credentials: 'include',
42
+ body: JSON.stringify({}),
43
+ })
44
+
45
+ if (response.ok) {
46
+ const data = await response.json()
47
+ setTotpUri(data.totpURI)
48
+ setSecret(data.secret)
49
+ setBackupCodes(data.backupCodes || [])
50
+ setStep('qr')
51
+ } else {
52
+ const data = await response.json().catch(() => ({}))
53
+ setError(data.message || 'Failed to enable two-factor authentication.')
54
+ setStep('qr')
55
+ }
56
+ } catch {
57
+ setError('An error occurred. Please try again.')
58
+ setStep('qr')
59
+ }
60
+ }
61
+ enableTwoFactor()
62
+ }, [])
63
+
64
+ async function handleVerify(e: FormEvent) {
65
+ e.preventDefault()
66
+ setLoading(true)
67
+ setError(null)
68
+
69
+ try {
70
+ const response = await fetch('/api/auth/two-factor/verify-totp', {
71
+ method: 'POST',
72
+ headers: { 'Content-Type': 'application/json' },
73
+ credentials: 'include',
74
+ body: JSON.stringify({ code: verificationCode }),
75
+ })
76
+
77
+ if (response.ok) {
78
+ if (backupCodes.length > 0) {
79
+ setStep('backup')
80
+ } else {
81
+ setStep('complete')
82
+ onSetupComplete?.()
83
+ }
84
+ } else {
85
+ const data = await response.json().catch(() => ({}))
86
+ setError(data.message || 'Invalid verification code. Please try again.')
87
+ }
88
+ } catch {
89
+ setError('An error occurred. Please try again.')
90
+ } finally {
91
+ setLoading(false)
92
+ }
93
+ }
94
+
95
+ function handleBackupContinue() {
96
+ setStep('complete')
97
+ onSetupComplete?.()
98
+ }
99
+
100
+ // Loading state
101
+ if (step === 'loading') {
102
+ return (
103
+ <div
104
+ style={{
105
+ minHeight: '100vh',
106
+ display: 'flex',
107
+ alignItems: 'center',
108
+ justifyContent: 'center',
109
+ background: 'var(--theme-bg)',
110
+ }}
111
+ >
112
+ <div style={{ color: 'var(--theme-text)', opacity: 0.7 }}>
113
+ Setting up two-factor authentication...
114
+ </div>
115
+ </div>
116
+ )
117
+ }
118
+
119
+ // Complete state
120
+ if (step === 'complete') {
121
+ return (
122
+ <div
123
+ style={{
124
+ minHeight: '100vh',
125
+ display: 'flex',
126
+ alignItems: 'center',
127
+ justifyContent: 'center',
128
+ background: 'var(--theme-bg)',
129
+ padding: 'var(--base)',
130
+ }}
131
+ >
132
+ <div
133
+ style={{
134
+ background: 'var(--theme-elevation-50)',
135
+ padding: 'calc(var(--base) * 2)',
136
+ borderRadius: 'var(--style-radius-m)',
137
+ boxShadow: '0 2px 20px rgba(0, 0, 0, 0.1)',
138
+ width: '100%',
139
+ maxWidth: '400px',
140
+ textAlign: 'center',
141
+ }}
142
+ >
143
+ {logo && (
144
+ <div style={{ marginBottom: 'calc(var(--base) * 1.5)' }}>
145
+ {logo}
146
+ </div>
147
+ )}
148
+
149
+ <h1
150
+ style={{
151
+ color: 'var(--theme-success-500)',
152
+ fontSize: 'var(--font-size-h3)',
153
+ fontWeight: 600,
154
+ margin: '0 0 var(--base) 0',
155
+ }}
156
+ >
157
+ Two-Factor Enabled!
158
+ </h1>
159
+
160
+ <p
161
+ style={{
162
+ color: 'var(--theme-text)',
163
+ opacity: 0.8,
164
+ marginBottom: 'calc(var(--base) * 1.5)',
165
+ fontSize: 'var(--font-size-small)',
166
+ }}
167
+ >
168
+ Your account is now protected with two-factor authentication.
169
+ </p>
170
+
171
+ <a
172
+ href={afterSetupPath}
173
+ style={{
174
+ display: 'inline-block',
175
+ padding: 'calc(var(--base) * 0.75) calc(var(--base) * 1.5)',
176
+ background: 'var(--theme-elevation-800)',
177
+ border: 'none',
178
+ borderRadius: 'var(--style-radius-s)',
179
+ color: 'var(--theme-elevation-50)',
180
+ fontSize: 'var(--font-size-base)',
181
+ fontWeight: 500,
182
+ textDecoration: 'none',
183
+ }}
184
+ >
185
+ Continue
186
+ </a>
187
+ </div>
188
+ </div>
189
+ )
190
+ }
191
+
192
+ // Backup codes state
193
+ if (step === 'backup') {
194
+ return (
195
+ <div
196
+ style={{
197
+ minHeight: '100vh',
198
+ display: 'flex',
199
+ alignItems: 'center',
200
+ justifyContent: 'center',
201
+ background: 'var(--theme-bg)',
202
+ padding: 'var(--base)',
203
+ }}
204
+ >
205
+ <div
206
+ style={{
207
+ background: 'var(--theme-elevation-50)',
208
+ padding: 'calc(var(--base) * 2)',
209
+ borderRadius: 'var(--style-radius-m)',
210
+ boxShadow: '0 2px 20px rgba(0, 0, 0, 0.1)',
211
+ width: '100%',
212
+ maxWidth: '450px',
213
+ }}
214
+ >
215
+ {logo && (
216
+ <div
217
+ style={{
218
+ textAlign: 'center',
219
+ marginBottom: 'calc(var(--base) * 1.5)',
220
+ }}
221
+ >
222
+ {logo}
223
+ </div>
224
+ )}
225
+
226
+ <h1
227
+ style={{
228
+ color: 'var(--theme-text)',
229
+ fontSize: 'var(--font-size-h3)',
230
+ fontWeight: 600,
231
+ margin: '0 0 calc(var(--base) * 0.5) 0',
232
+ textAlign: 'center',
233
+ }}
234
+ >
235
+ Save Your Backup Codes
236
+ </h1>
237
+
238
+ <p
239
+ style={{
240
+ color: 'var(--theme-text)',
241
+ opacity: 0.7,
242
+ fontSize: 'var(--font-size-small)',
243
+ textAlign: 'center',
244
+ marginBottom: 'calc(var(--base) * 1.5)',
245
+ }}
246
+ >
247
+ Store these codes safely. You can use them to access your account if you lose your authenticator.
248
+ </p>
249
+
250
+ <div
251
+ style={{
252
+ background: 'var(--theme-elevation-100)',
253
+ padding: 'var(--base)',
254
+ borderRadius: 'var(--style-radius-s)',
255
+ marginBottom: 'calc(var(--base) * 1.5)',
256
+ fontFamily: 'monospace',
257
+ fontSize: 'var(--font-size-small)',
258
+ }}
259
+ >
260
+ <div
261
+ style={{
262
+ display: 'grid',
263
+ gridTemplateColumns: 'repeat(2, 1fr)',
264
+ gap: 'calc(var(--base) * 0.5)',
265
+ }}
266
+ >
267
+ {backupCodes.map((code, index) => (
268
+ <div
269
+ key={index}
270
+ style={{
271
+ color: 'var(--theme-text)',
272
+ padding: 'calc(var(--base) * 0.25)',
273
+ }}
274
+ >
275
+ {code}
276
+ </div>
277
+ ))}
278
+ </div>
279
+ </div>
280
+
281
+ <button
282
+ onClick={() => {
283
+ navigator.clipboard.writeText(backupCodes.join('\n'))
284
+ }}
285
+ style={{
286
+ width: '100%',
287
+ padding: 'calc(var(--base) * 0.5)',
288
+ background: 'var(--theme-elevation-150)',
289
+ border: 'none',
290
+ borderRadius: 'var(--style-radius-s)',
291
+ color: 'var(--theme-text)',
292
+ fontSize: 'var(--font-size-small)',
293
+ cursor: 'pointer',
294
+ marginBottom: 'var(--base)',
295
+ }}
296
+ >
297
+ Copy to Clipboard
298
+ </button>
299
+
300
+ <button
301
+ onClick={handleBackupContinue}
302
+ style={{
303
+ width: '100%',
304
+ padding: 'calc(var(--base) * 0.75)',
305
+ background: 'var(--theme-elevation-800)',
306
+ border: 'none',
307
+ borderRadius: 'var(--style-radius-s)',
308
+ color: 'var(--theme-elevation-50)',
309
+ fontSize: 'var(--font-size-base)',
310
+ fontWeight: 500,
311
+ cursor: 'pointer',
312
+ }}
313
+ >
314
+ I've Saved My Codes
315
+ </button>
316
+ </div>
317
+ </div>
318
+ )
319
+ }
320
+
321
+ // QR code and verify state
322
+ return (
323
+ <div
324
+ style={{
325
+ minHeight: '100vh',
326
+ display: 'flex',
327
+ alignItems: 'center',
328
+ justifyContent: 'center',
329
+ background: 'var(--theme-bg)',
330
+ padding: 'var(--base)',
331
+ }}
332
+ >
333
+ <div
334
+ style={{
335
+ background: 'var(--theme-elevation-50)',
336
+ padding: 'calc(var(--base) * 2)',
337
+ borderRadius: 'var(--style-radius-m)',
338
+ boxShadow: '0 2px 20px rgba(0, 0, 0, 0.1)',
339
+ width: '100%',
340
+ maxWidth: '400px',
341
+ }}
342
+ >
343
+ {logo && (
344
+ <div
345
+ style={{
346
+ textAlign: 'center',
347
+ marginBottom: 'calc(var(--base) * 1.5)',
348
+ }}
349
+ >
350
+ {logo}
351
+ </div>
352
+ )}
353
+
354
+ <h1
355
+ style={{
356
+ color: 'var(--theme-text)',
357
+ fontSize: 'var(--font-size-h3)',
358
+ fontWeight: 600,
359
+ margin: '0 0 calc(var(--base) * 0.5) 0',
360
+ textAlign: 'center',
361
+ }}
362
+ >
363
+ {title}
364
+ </h1>
365
+
366
+ <p
367
+ style={{
368
+ color: 'var(--theme-text)',
369
+ opacity: 0.7,
370
+ fontSize: 'var(--font-size-small)',
371
+ textAlign: 'center',
372
+ marginBottom: 'calc(var(--base) * 1.5)',
373
+ }}
374
+ >
375
+ Scan the QR code with your authenticator app, then enter the code below.
376
+ </p>
377
+
378
+ {totpUri && (
379
+ <div
380
+ style={{
381
+ textAlign: 'center',
382
+ marginBottom: 'calc(var(--base) * 1.5)',
383
+ }}
384
+ >
385
+ {/* QR code using QRServer.com API */}
386
+ <img
387
+ src={`https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=${encodeURIComponent(totpUri)}`}
388
+ alt="QR Code for authenticator app"
389
+ style={{
390
+ width: '200px',
391
+ height: '200px',
392
+ border: '1px solid var(--theme-elevation-150)',
393
+ borderRadius: 'var(--style-radius-s)',
394
+ }}
395
+ />
396
+ </div>
397
+ )}
398
+
399
+ {secret && (
400
+ <div
401
+ style={{
402
+ marginBottom: 'calc(var(--base) * 1.5)',
403
+ textAlign: 'center',
404
+ }}
405
+ >
406
+ <p
407
+ style={{
408
+ color: 'var(--theme-text)',
409
+ opacity: 0.7,
410
+ fontSize: 'var(--font-size-small)',
411
+ marginBottom: 'calc(var(--base) * 0.5)',
412
+ }}
413
+ >
414
+ Or enter this code manually:
415
+ </p>
416
+ <code
417
+ style={{
418
+ display: 'inline-block',
419
+ padding: 'calc(var(--base) * 0.5)',
420
+ background: 'var(--theme-elevation-100)',
421
+ borderRadius: 'var(--style-radius-s)',
422
+ fontFamily: 'monospace',
423
+ fontSize: 'var(--font-size-small)',
424
+ color: 'var(--theme-text)',
425
+ wordBreak: 'break-all',
426
+ }}
427
+ >
428
+ {secret}
429
+ </code>
430
+ </div>
431
+ )}
432
+
433
+ <form onSubmit={handleVerify}>
434
+ <div style={{ marginBottom: 'calc(var(--base) * 1.5)' }}>
435
+ <label
436
+ htmlFor="code"
437
+ style={{
438
+ display: 'block',
439
+ color: 'var(--theme-text)',
440
+ marginBottom: 'calc(var(--base) * 0.5)',
441
+ fontSize: 'var(--font-size-small)',
442
+ fontWeight: 500,
443
+ }}
444
+ >
445
+ Verification Code
446
+ </label>
447
+ <input
448
+ id="code"
449
+ type="text"
450
+ inputMode="numeric"
451
+ pattern="[0-9]*"
452
+ autoComplete="one-time-code"
453
+ value={verificationCode}
454
+ onChange={(e) => setVerificationCode(e.target.value.replace(/\D/g, '').slice(0, 6))}
455
+ required
456
+ placeholder="000000"
457
+ style={{
458
+ width: '100%',
459
+ padding: 'calc(var(--base) * 0.75)',
460
+ background: 'var(--theme-input-bg)',
461
+ border: '1px solid var(--theme-elevation-150)',
462
+ borderRadius: 'var(--style-radius-s)',
463
+ color: 'var(--theme-text)',
464
+ fontSize: 'var(--font-size-h4)',
465
+ fontFamily: 'monospace',
466
+ textAlign: 'center',
467
+ letterSpacing: '0.5em',
468
+ outline: 'none',
469
+ boxSizing: 'border-box',
470
+ }}
471
+ />
472
+ </div>
473
+
474
+ {error && (
475
+ <div
476
+ style={{
477
+ color: 'var(--theme-error-500)',
478
+ marginBottom: 'var(--base)',
479
+ fontSize: 'var(--font-size-small)',
480
+ padding: 'calc(var(--base) * 0.5)',
481
+ background: 'var(--theme-error-50)',
482
+ borderRadius: 'var(--style-radius-s)',
483
+ border: '1px solid var(--theme-error-200)',
484
+ }}
485
+ >
486
+ {error}
487
+ </div>
488
+ )}
489
+
490
+ <button
491
+ type="submit"
492
+ disabled={loading || verificationCode.length !== 6}
493
+ style={{
494
+ width: '100%',
495
+ padding: 'calc(var(--base) * 0.75)',
496
+ background: 'var(--theme-elevation-800)',
497
+ border: 'none',
498
+ borderRadius: 'var(--style-radius-s)',
499
+ color: 'var(--theme-elevation-50)',
500
+ fontSize: 'var(--font-size-base)',
501
+ fontWeight: 500,
502
+ cursor: loading || verificationCode.length !== 6 ? 'not-allowed' : 'pointer',
503
+ opacity: loading || verificationCode.length !== 6 ? 0.7 : 1,
504
+ transition: 'opacity 150ms ease',
505
+ }}
506
+ >
507
+ {loading ? 'Verifying...' : 'Verify and Enable'}
508
+ </button>
509
+ </form>
510
+ </div>
511
+ </div>
512
+ )
513
+ }
514
+
515
+ export default TwoFactorSetupView