@delmaredigital/payload-better-auth 0.3.8 → 0.3.9
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/package.json +34 -91
- package/src/adapter/collections.ts +0 -621
- package/src/adapter/index.ts +0 -712
- package/src/components/BeforeLogin.tsx +0 -39
- package/src/components/LoginView.tsx +0 -1516
- package/src/components/LoginViewWrapper.tsx +0 -35
- package/src/components/LogoutButton.tsx +0 -58
- package/src/components/PasskeyRegisterButton.tsx +0 -105
- package/src/components/PasskeySignInButton.tsx +0 -96
- package/src/components/auth/ForgotPasswordView.tsx +0 -274
- package/src/components/auth/ResetPasswordView.tsx +0 -331
- package/src/components/auth/index.ts +0 -8
- package/src/components/management/ApiKeysManagementClient.tsx +0 -988
- package/src/components/management/PasskeysManagementClient.tsx +0 -409
- package/src/components/management/SecurityNavLinks.tsx +0 -117
- package/src/components/management/TwoFactorManagementClient.tsx +0 -560
- package/src/components/management/index.ts +0 -20
- package/src/components/management/views/ApiKeysView.tsx +0 -57
- package/src/components/management/views/PasskeysView.tsx +0 -42
- package/src/components/management/views/TwoFactorView.tsx +0 -42
- package/src/components/management/views/index.ts +0 -10
- package/src/components/twoFactor/TwoFactorSetupView.tsx +0 -515
- package/src/components/twoFactor/TwoFactorVerifyView.tsx +0 -238
- package/src/components/twoFactor/index.ts +0 -8
- package/src/exports/client.ts +0 -77
- package/src/exports/components.ts +0 -30
- package/src/exports/management.ts +0 -25
- package/src/exports/rsc.ts +0 -11
- package/src/generated-types.ts +0 -269
- package/src/index.ts +0 -135
- package/src/plugin/index.ts +0 -834
- package/src/scripts/generate-types.ts +0 -269
- package/src/types/apiKey.ts +0 -63
- package/src/types/betterAuth.ts +0 -253
- package/src/utils/access.ts +0 -410
- package/src/utils/apiKeyAccess.ts +0 -443
- package/src/utils/betterAuthDefaults.ts +0 -102
- package/src/utils/detectAuthConfig.ts +0 -47
- package/src/utils/detectEnabledPlugins.ts +0 -69
- package/src/utils/firstUserAdmin.ts +0 -164
- package/src/utils/generateScopes.ts +0 -150
- package/src/utils/session.ts +0 -91
|
@@ -1,1516 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import { useState, useEffect, type FormEvent } from 'react'
|
|
4
|
-
import { useRouter } from 'next/navigation.js'
|
|
5
|
-
import {
|
|
6
|
-
createPayloadAuthClient,
|
|
7
|
-
type PayloadAuthClient,
|
|
8
|
-
} from '../exports/client.js'
|
|
9
|
-
import { hasAnyRole, hasAllRoles, normalizeRoles } from '../utils/access.js'
|
|
10
|
-
|
|
11
|
-
export type LoginViewProps = {
|
|
12
|
-
/** Optional pre-configured auth client */
|
|
13
|
-
authClient?: PayloadAuthClient
|
|
14
|
-
/** Custom logo element */
|
|
15
|
-
logo?: React.ReactNode
|
|
16
|
-
/** Login page title. Default: 'Login' */
|
|
17
|
-
title?: string
|
|
18
|
-
/** Path to redirect after successful login. Default: '/admin' */
|
|
19
|
-
afterLoginPath?: string
|
|
20
|
-
/**
|
|
21
|
-
* Required role(s) for admin access.
|
|
22
|
-
* - string: Single role required (default: 'admin')
|
|
23
|
-
* - string[]: Multiple roles (behavior depends on requireAllRoles)
|
|
24
|
-
* - null/undefined: Disable role checking
|
|
25
|
-
* For complex RBAC beyond these options, disable the login view and create your own.
|
|
26
|
-
*/
|
|
27
|
-
requiredRole?: string | string[] | null
|
|
28
|
-
/**
|
|
29
|
-
* When requiredRole is an array, require ALL roles (true) or ANY role (false).
|
|
30
|
-
* Default: false (any matching role grants access)
|
|
31
|
-
*/
|
|
32
|
-
requireAllRoles?: boolean
|
|
33
|
-
/**
|
|
34
|
-
* Enable passkey (WebAuthn) sign-in option.
|
|
35
|
-
* - true: Always show passkey button
|
|
36
|
-
* - false: Never show passkey button
|
|
37
|
-
* - 'auto' (default): Auto-detect if passkey plugin is available
|
|
38
|
-
*/
|
|
39
|
-
enablePasskey?: boolean | 'auto'
|
|
40
|
-
/**
|
|
41
|
-
* Enable user registration (sign up) option.
|
|
42
|
-
* - true: Always show "Create account" link
|
|
43
|
-
* - false: Never show registration option
|
|
44
|
-
* - 'auto' (default): Auto-detect if sign-up endpoint is available
|
|
45
|
-
*/
|
|
46
|
-
enableSignUp?: boolean | 'auto'
|
|
47
|
-
/**
|
|
48
|
-
* Default role to assign to new users during registration.
|
|
49
|
-
* Default: 'user'
|
|
50
|
-
*/
|
|
51
|
-
defaultSignUpRole?: string
|
|
52
|
-
/**
|
|
53
|
-
* Enable forgot password option.
|
|
54
|
-
* - true: Always show "Forgot password?" link
|
|
55
|
-
* - false: Never show forgot password option
|
|
56
|
-
* - 'auto' (default): Auto-detect if forget-password endpoint is available
|
|
57
|
-
*/
|
|
58
|
-
enableForgotPassword?: boolean | 'auto'
|
|
59
|
-
/**
|
|
60
|
-
* Custom URL for password reset page. If provided, users will be redirected here
|
|
61
|
-
* instead of showing the inline password reset form.
|
|
62
|
-
* The reset token will be appended as ?token=xxx
|
|
63
|
-
*/
|
|
64
|
-
resetPasswordUrl?: string
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Check if user has the required role(s)
|
|
69
|
-
*/
|
|
70
|
-
function checkUserRoles(
|
|
71
|
-
user: { role?: unknown } | null | undefined,
|
|
72
|
-
requiredRole: string | string[] | null | undefined,
|
|
73
|
-
requireAllRoles: boolean
|
|
74
|
-
): boolean {
|
|
75
|
-
// No role requirement = access granted
|
|
76
|
-
if (!requiredRole) return true
|
|
77
|
-
|
|
78
|
-
// No user = access denied
|
|
79
|
-
if (!user) return false
|
|
80
|
-
|
|
81
|
-
const roles = Array.isArray(requiredRole) ? requiredRole : [requiredRole]
|
|
82
|
-
|
|
83
|
-
if (requireAllRoles) {
|
|
84
|
-
return hasAllRoles(user, roles)
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
return hasAnyRole(user, roles)
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Full login page component matching Payload's admin theme.
|
|
92
|
-
* Registered as a custom admin view at /admin/login.
|
|
93
|
-
*/
|
|
94
|
-
type ViewMode = 'login' | 'register' | 'forgotPassword' | 'resetSent' | 'twoFactor'
|
|
95
|
-
|
|
96
|
-
export function LoginView({
|
|
97
|
-
authClient: providedClient,
|
|
98
|
-
logo,
|
|
99
|
-
title = 'Login',
|
|
100
|
-
afterLoginPath = '/admin',
|
|
101
|
-
requiredRole = 'admin',
|
|
102
|
-
requireAllRoles = false,
|
|
103
|
-
enablePasskey = 'auto',
|
|
104
|
-
enableSignUp = 'auto',
|
|
105
|
-
defaultSignUpRole = 'user',
|
|
106
|
-
enableForgotPassword = 'auto',
|
|
107
|
-
resetPasswordUrl,
|
|
108
|
-
}: LoginViewProps) {
|
|
109
|
-
const router = useRouter()
|
|
110
|
-
|
|
111
|
-
// View state
|
|
112
|
-
const [viewMode, setViewMode] = useState<ViewMode>('login')
|
|
113
|
-
|
|
114
|
-
// Form fields
|
|
115
|
-
const [email, setEmail] = useState('')
|
|
116
|
-
const [password, setPassword] = useState('')
|
|
117
|
-
const [name, setName] = useState('')
|
|
118
|
-
const [confirmPassword, setConfirmPassword] = useState('')
|
|
119
|
-
|
|
120
|
-
// UI state
|
|
121
|
-
const [error, setError] = useState<string | null>(null)
|
|
122
|
-
const [successMessage, setSuccessMessage] = useState<string | null>(null)
|
|
123
|
-
const [loading, setLoading] = useState(false)
|
|
124
|
-
const [passkeyLoading, setPasskeyLoading] = useState(false)
|
|
125
|
-
const [checkingSession, setCheckingSession] = useState(true)
|
|
126
|
-
const [accessDenied, setAccessDenied] = useState(false)
|
|
127
|
-
|
|
128
|
-
// Feature availability
|
|
129
|
-
const [passkeyAvailable, setPasskeyAvailable] = useState(enablePasskey === true)
|
|
130
|
-
const [signUpAvailable, setSignUpAvailable] = useState(enableSignUp === true)
|
|
131
|
-
const [forgotPasswordAvailable, setForgotPasswordAvailable] = useState(enableForgotPassword === true)
|
|
132
|
-
|
|
133
|
-
// Two-factor authentication state
|
|
134
|
-
const [totpCode, setTotpCode] = useState('')
|
|
135
|
-
const [totpLoading, setTotpLoading] = useState(false)
|
|
136
|
-
|
|
137
|
-
const getClient = () => providedClient ?? createPayloadAuthClient()
|
|
138
|
-
|
|
139
|
-
// Check if user is already logged in on mount
|
|
140
|
-
useEffect(() => {
|
|
141
|
-
async function checkSession() {
|
|
142
|
-
try {
|
|
143
|
-
const client = getClient()
|
|
144
|
-
const result = await client.getSession()
|
|
145
|
-
|
|
146
|
-
if (result.data?.user) {
|
|
147
|
-
const user = result.data.user as { role?: unknown }
|
|
148
|
-
// User is logged in, check role
|
|
149
|
-
if (checkUserRoles(user, requiredRole, requireAllRoles)) {
|
|
150
|
-
router.push(afterLoginPath)
|
|
151
|
-
return
|
|
152
|
-
} else {
|
|
153
|
-
setAccessDenied(true)
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
} catch {
|
|
157
|
-
// No session, show login form
|
|
158
|
-
}
|
|
159
|
-
setCheckingSession(false)
|
|
160
|
-
}
|
|
161
|
-
checkSession()
|
|
162
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
163
|
-
}, [afterLoginPath, requiredRole, requireAllRoles, router])
|
|
164
|
-
|
|
165
|
-
// Auto-detect passkey availability if set to 'auto'
|
|
166
|
-
useEffect(() => {
|
|
167
|
-
if (enablePasskey === 'auto') {
|
|
168
|
-
// Check if passkey endpoint exists (GET request)
|
|
169
|
-
// Better Auth passkey routes are at /passkey/* (singular)
|
|
170
|
-
fetch('/api/auth/passkey/generate-authenticate-options', {
|
|
171
|
-
method: 'GET',
|
|
172
|
-
credentials: 'include',
|
|
173
|
-
})
|
|
174
|
-
.then((res) => {
|
|
175
|
-
// If we get a response (even 400/401 for not authenticated), passkey is available
|
|
176
|
-
// 404 means passkey plugin is not installed
|
|
177
|
-
setPasskeyAvailable(res.status !== 404)
|
|
178
|
-
})
|
|
179
|
-
.catch(() => {
|
|
180
|
-
setPasskeyAvailable(false)
|
|
181
|
-
})
|
|
182
|
-
} else {
|
|
183
|
-
setPasskeyAvailable(enablePasskey === true)
|
|
184
|
-
}
|
|
185
|
-
}, [enablePasskey])
|
|
186
|
-
|
|
187
|
-
// Auto-detect sign up availability if set to 'auto'
|
|
188
|
-
useEffect(() => {
|
|
189
|
-
if (enableSignUp === 'auto') {
|
|
190
|
-
// Check if sign-up endpoint exists
|
|
191
|
-
fetch('/api/auth/sign-up/email', {
|
|
192
|
-
method: 'OPTIONS',
|
|
193
|
-
credentials: 'include',
|
|
194
|
-
})
|
|
195
|
-
.then((res) => {
|
|
196
|
-
// 404 means sign-up is not available
|
|
197
|
-
setSignUpAvailable(res.status !== 404)
|
|
198
|
-
})
|
|
199
|
-
.catch(() => {
|
|
200
|
-
// If OPTIONS fails, try a HEAD or just assume it's available since it's a core endpoint
|
|
201
|
-
setSignUpAvailable(true)
|
|
202
|
-
})
|
|
203
|
-
} else {
|
|
204
|
-
setSignUpAvailable(enableSignUp === true)
|
|
205
|
-
}
|
|
206
|
-
}, [enableSignUp])
|
|
207
|
-
|
|
208
|
-
// Auto-detect forgot password availability if set to 'auto'
|
|
209
|
-
useEffect(() => {
|
|
210
|
-
if (enableForgotPassword === 'auto') {
|
|
211
|
-
// Check if request-password-reset endpoint exists
|
|
212
|
-
fetch('/api/auth/request-password-reset', {
|
|
213
|
-
method: 'OPTIONS',
|
|
214
|
-
credentials: 'include',
|
|
215
|
-
})
|
|
216
|
-
.then((res) => {
|
|
217
|
-
// 404 means request-password-reset is not available
|
|
218
|
-
setForgotPasswordAvailable(res.status !== 404)
|
|
219
|
-
})
|
|
220
|
-
.catch(() => {
|
|
221
|
-
// If OPTIONS fails, assume it's available since it's a core endpoint
|
|
222
|
-
setForgotPasswordAvailable(true)
|
|
223
|
-
})
|
|
224
|
-
} else {
|
|
225
|
-
setForgotPasswordAvailable(enableForgotPassword === true)
|
|
226
|
-
}
|
|
227
|
-
}, [enableForgotPassword])
|
|
228
|
-
|
|
229
|
-
async function handleSubmit(e: FormEvent) {
|
|
230
|
-
e.preventDefault()
|
|
231
|
-
setLoading(true)
|
|
232
|
-
setError(null)
|
|
233
|
-
setSuccessMessage(null)
|
|
234
|
-
setAccessDenied(false)
|
|
235
|
-
|
|
236
|
-
try {
|
|
237
|
-
const client = getClient()
|
|
238
|
-
const result = await client.signIn.email({
|
|
239
|
-
email,
|
|
240
|
-
password,
|
|
241
|
-
})
|
|
242
|
-
|
|
243
|
-
// Check if 2FA is required (use 'in' operator for proper TypeScript inference)
|
|
244
|
-
if (result.data && 'twoFactorRedirect' in result.data && result.data.twoFactorRedirect) {
|
|
245
|
-
setViewMode('twoFactor')
|
|
246
|
-
setLoading(false)
|
|
247
|
-
return
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
if (result.error) {
|
|
251
|
-
setError(result.error.message ?? 'Invalid credentials')
|
|
252
|
-
setLoading(false)
|
|
253
|
-
return
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
if (result.data?.user) {
|
|
257
|
-
const user = result.data.user as { role?: unknown }
|
|
258
|
-
// Check role if required
|
|
259
|
-
if (!checkUserRoles(user, requiredRole, requireAllRoles)) {
|
|
260
|
-
setAccessDenied(true)
|
|
261
|
-
setLoading(false)
|
|
262
|
-
return
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
router.push(afterLoginPath)
|
|
266
|
-
router.refresh()
|
|
267
|
-
}
|
|
268
|
-
} catch {
|
|
269
|
-
setError('An error occurred. Please try again.')
|
|
270
|
-
setLoading(false)
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
async function handleSignUp(e: FormEvent) {
|
|
275
|
-
e.preventDefault()
|
|
276
|
-
setLoading(true)
|
|
277
|
-
setError(null)
|
|
278
|
-
setSuccessMessage(null)
|
|
279
|
-
|
|
280
|
-
// Validate passwords match
|
|
281
|
-
if (password !== confirmPassword) {
|
|
282
|
-
setError('Passwords do not match')
|
|
283
|
-
setLoading(false)
|
|
284
|
-
return
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
// Validate password strength (basic)
|
|
288
|
-
if (password.length < 8) {
|
|
289
|
-
setError('Password must be at least 8 characters')
|
|
290
|
-
setLoading(false)
|
|
291
|
-
return
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
try {
|
|
295
|
-
const client = getClient()
|
|
296
|
-
const result = await client.signUp.email({
|
|
297
|
-
email,
|
|
298
|
-
password,
|
|
299
|
-
name,
|
|
300
|
-
role: defaultSignUpRole,
|
|
301
|
-
} as Parameters<typeof client.signUp.email>[0])
|
|
302
|
-
|
|
303
|
-
if (result.error) {
|
|
304
|
-
setError(result.error.message ?? 'Registration failed')
|
|
305
|
-
setLoading(false)
|
|
306
|
-
return
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
// Registration successful - either auto-signed in or need to verify email
|
|
310
|
-
if (result.data?.user) {
|
|
311
|
-
// Re-fetch session to get updated user data (role may have been changed by hooks)
|
|
312
|
-
// This handles cases like firstUserAdmin where the role is set after creation
|
|
313
|
-
const sessionResult = await client.getSession()
|
|
314
|
-
|
|
315
|
-
if (sessionResult.data?.user) {
|
|
316
|
-
const user = sessionResult.data.user as { role?: unknown }
|
|
317
|
-
// Check role if required
|
|
318
|
-
if (!checkUserRoles(user, requiredRole, requireAllRoles)) {
|
|
319
|
-
setAccessDenied(true)
|
|
320
|
-
setLoading(false)
|
|
321
|
-
return
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
router.push(afterLoginPath)
|
|
326
|
-
router.refresh()
|
|
327
|
-
} else {
|
|
328
|
-
// Likely requires email verification - show success and switch to login
|
|
329
|
-
setSuccessMessage('Account created! Please check your email to verify your account.')
|
|
330
|
-
setViewMode('login')
|
|
331
|
-
setPassword('')
|
|
332
|
-
setConfirmPassword('')
|
|
333
|
-
setLoading(false)
|
|
334
|
-
}
|
|
335
|
-
} catch {
|
|
336
|
-
setError('An error occurred. Please try again.')
|
|
337
|
-
setLoading(false)
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
async function handleForgotPassword(e: FormEvent) {
|
|
342
|
-
e.preventDefault()
|
|
343
|
-
setLoading(true)
|
|
344
|
-
setError(null)
|
|
345
|
-
setSuccessMessage(null)
|
|
346
|
-
|
|
347
|
-
try {
|
|
348
|
-
const client = getClient()
|
|
349
|
-
const result = await client.requestPasswordReset({
|
|
350
|
-
email,
|
|
351
|
-
redirectTo: resetPasswordUrl ?? `${window.location.origin}/admin/reset-password`,
|
|
352
|
-
})
|
|
353
|
-
|
|
354
|
-
if (result.error) {
|
|
355
|
-
setError(result.error.message ?? 'Failed to send reset email')
|
|
356
|
-
setLoading(false)
|
|
357
|
-
return
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
// Success - show confirmation
|
|
361
|
-
setViewMode('resetSent')
|
|
362
|
-
setLoading(false)
|
|
363
|
-
} catch {
|
|
364
|
-
setError('An error occurred. Please try again.')
|
|
365
|
-
setLoading(false)
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
async function handleTotpVerify(e: FormEvent) {
|
|
370
|
-
e.preventDefault()
|
|
371
|
-
setTotpLoading(true)
|
|
372
|
-
setError(null)
|
|
373
|
-
|
|
374
|
-
try {
|
|
375
|
-
const client = getClient()
|
|
376
|
-
const result = await client.twoFactor.verifyTotp({ code: totpCode })
|
|
377
|
-
|
|
378
|
-
if (result.error) {
|
|
379
|
-
setError(result.error.message ?? 'Invalid verification code')
|
|
380
|
-
setTotpLoading(false)
|
|
381
|
-
return
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
// Verify-totp may not return all user fields (like custom 'role')
|
|
385
|
-
// Fetch the session to get complete user data for role check
|
|
386
|
-
if (requiredRole) {
|
|
387
|
-
const sessionResult = await client.getSession()
|
|
388
|
-
if (sessionResult.data?.user) {
|
|
389
|
-
const user = sessionResult.data.user as { role?: unknown }
|
|
390
|
-
if (!checkUserRoles(user, requiredRole, requireAllRoles)) {
|
|
391
|
-
setAccessDenied(true)
|
|
392
|
-
setTotpLoading(false)
|
|
393
|
-
return
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
router.push(afterLoginPath)
|
|
399
|
-
router.refresh()
|
|
400
|
-
} catch {
|
|
401
|
-
setError('An error occurred. Please try again.')
|
|
402
|
-
setTotpLoading(false)
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
function switchView(newView: ViewMode) {
|
|
407
|
-
setViewMode(newView)
|
|
408
|
-
setError(null)
|
|
409
|
-
setSuccessMessage(null)
|
|
410
|
-
// Reset form fields based on context
|
|
411
|
-
if (newView === 'login') {
|
|
412
|
-
setTotpCode('')
|
|
413
|
-
setConfirmPassword('')
|
|
414
|
-
} else if (newView === 'register') {
|
|
415
|
-
setPassword('')
|
|
416
|
-
setConfirmPassword('')
|
|
417
|
-
} else if (newView === 'forgotPassword') {
|
|
418
|
-
setPassword('')
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
function handleBackToLogin() {
|
|
423
|
-
switchView('login')
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
async function handlePasskeySignIn() {
|
|
427
|
-
if (!passkeyAvailable) return
|
|
428
|
-
|
|
429
|
-
setPasskeyLoading(true)
|
|
430
|
-
setError(null)
|
|
431
|
-
setAccessDenied(false)
|
|
432
|
-
|
|
433
|
-
try {
|
|
434
|
-
const client = getClient()
|
|
435
|
-
const result = await client.signIn.passkey()
|
|
436
|
-
|
|
437
|
-
if (result.error) {
|
|
438
|
-
setError(result.error.message ?? 'Passkey authentication failed')
|
|
439
|
-
setPasskeyLoading(false)
|
|
440
|
-
return
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
// Passkey sign-in succeeded - fetch session to get full user data (including role)
|
|
444
|
-
// This is more reliable than checking result.data.user which may vary by SDK version
|
|
445
|
-
const sessionResult = await client.getSession()
|
|
446
|
-
|
|
447
|
-
if (sessionResult.data?.user) {
|
|
448
|
-
const user = sessionResult.data.user as { role?: unknown }
|
|
449
|
-
// Check role if required
|
|
450
|
-
if (!checkUserRoles(user, requiredRole, requireAllRoles)) {
|
|
451
|
-
setAccessDenied(true)
|
|
452
|
-
setPasskeyLoading(false)
|
|
453
|
-
return
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
router.push(afterLoginPath)
|
|
457
|
-
router.refresh()
|
|
458
|
-
} else {
|
|
459
|
-
// Session fetch failed - shouldn't happen after successful passkey auth
|
|
460
|
-
setError('Authentication succeeded but session could not be verified')
|
|
461
|
-
setPasskeyLoading(false)
|
|
462
|
-
}
|
|
463
|
-
} catch (err) {
|
|
464
|
-
if (err instanceof Error && err.name === 'NotAllowedError') {
|
|
465
|
-
setError('Passkey authentication was cancelled or not allowed')
|
|
466
|
-
} else {
|
|
467
|
-
setError(err instanceof Error ? err.message : 'Passkey authentication failed')
|
|
468
|
-
}
|
|
469
|
-
setPasskeyLoading(false)
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
async function handleSignOut() {
|
|
474
|
-
const client = getClient()
|
|
475
|
-
await client.signOut()
|
|
476
|
-
setAccessDenied(false)
|
|
477
|
-
router.refresh()
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
// Loading state while checking session
|
|
481
|
-
if (checkingSession) {
|
|
482
|
-
return (
|
|
483
|
-
<div
|
|
484
|
-
style={{
|
|
485
|
-
minHeight: '100vh',
|
|
486
|
-
display: 'flex',
|
|
487
|
-
alignItems: 'center',
|
|
488
|
-
justifyContent: 'center',
|
|
489
|
-
background: 'var(--theme-bg)',
|
|
490
|
-
}}
|
|
491
|
-
>
|
|
492
|
-
<div style={{ color: 'var(--theme-text)', opacity: 0.7 }}>
|
|
493
|
-
Loading...
|
|
494
|
-
</div>
|
|
495
|
-
</div>
|
|
496
|
-
)
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
// Access denied state
|
|
500
|
-
if (accessDenied) {
|
|
501
|
-
return (
|
|
502
|
-
<div
|
|
503
|
-
style={{
|
|
504
|
-
minHeight: '100vh',
|
|
505
|
-
display: 'flex',
|
|
506
|
-
alignItems: 'center',
|
|
507
|
-
justifyContent: 'center',
|
|
508
|
-
background: 'var(--theme-bg)',
|
|
509
|
-
padding: 'var(--base)',
|
|
510
|
-
}}
|
|
511
|
-
>
|
|
512
|
-
<div
|
|
513
|
-
style={{
|
|
514
|
-
background: 'var(--theme-elevation-50)',
|
|
515
|
-
padding: 'calc(var(--base) * 2)',
|
|
516
|
-
borderRadius: 'var(--style-radius-m)',
|
|
517
|
-
boxShadow: '0 2px 20px rgba(0, 0, 0, 0.1)',
|
|
518
|
-
width: '100%',
|
|
519
|
-
maxWidth: '400px',
|
|
520
|
-
textAlign: 'center',
|
|
521
|
-
}}
|
|
522
|
-
>
|
|
523
|
-
<h1
|
|
524
|
-
style={{
|
|
525
|
-
color: 'var(--theme-error-500)',
|
|
526
|
-
fontSize: 'var(--font-size-h3)',
|
|
527
|
-
fontWeight: 600,
|
|
528
|
-
margin: '0 0 var(--base) 0',
|
|
529
|
-
}}
|
|
530
|
-
>
|
|
531
|
-
Access Denied
|
|
532
|
-
</h1>
|
|
533
|
-
<p
|
|
534
|
-
style={{
|
|
535
|
-
color: 'var(--theme-text)',
|
|
536
|
-
opacity: 0.8,
|
|
537
|
-
marginBottom: 'calc(var(--base) * 1.5)',
|
|
538
|
-
fontSize: 'var(--font-size-small)',
|
|
539
|
-
}}
|
|
540
|
-
>
|
|
541
|
-
You don't have permission to access the admin panel.
|
|
542
|
-
Please contact an administrator if you believe this is an error.
|
|
543
|
-
</p>
|
|
544
|
-
<button
|
|
545
|
-
onClick={handleSignOut}
|
|
546
|
-
style={{
|
|
547
|
-
padding: 'calc(var(--base) * 0.75) calc(var(--base) * 1.5)',
|
|
548
|
-
background: 'var(--theme-elevation-150)',
|
|
549
|
-
border: 'none',
|
|
550
|
-
borderRadius: 'var(--style-radius-s)',
|
|
551
|
-
color: 'var(--theme-text)',
|
|
552
|
-
fontSize: 'var(--font-size-base)',
|
|
553
|
-
cursor: 'pointer',
|
|
554
|
-
}}
|
|
555
|
-
>
|
|
556
|
-
Sign out and try again
|
|
557
|
-
</button>
|
|
558
|
-
</div>
|
|
559
|
-
</div>
|
|
560
|
-
)
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
// Two-factor verification view
|
|
564
|
-
if (viewMode === 'twoFactor') {
|
|
565
|
-
return (
|
|
566
|
-
<div
|
|
567
|
-
style={{
|
|
568
|
-
minHeight: '100vh',
|
|
569
|
-
display: 'flex',
|
|
570
|
-
alignItems: 'center',
|
|
571
|
-
justifyContent: 'center',
|
|
572
|
-
background: 'var(--theme-bg)',
|
|
573
|
-
padding: 'var(--base)',
|
|
574
|
-
}}
|
|
575
|
-
>
|
|
576
|
-
<div
|
|
577
|
-
style={{
|
|
578
|
-
background: 'var(--theme-elevation-50)',
|
|
579
|
-
padding: 'calc(var(--base) * 2)',
|
|
580
|
-
borderRadius: 'var(--style-radius-m)',
|
|
581
|
-
boxShadow: '0 2px 20px rgba(0, 0, 0, 0.1)',
|
|
582
|
-
width: '100%',
|
|
583
|
-
maxWidth: '400px',
|
|
584
|
-
}}
|
|
585
|
-
>
|
|
586
|
-
{logo && (
|
|
587
|
-
<div
|
|
588
|
-
style={{
|
|
589
|
-
textAlign: 'center',
|
|
590
|
-
marginBottom: 'calc(var(--base) * 1.5)',
|
|
591
|
-
}}
|
|
592
|
-
>
|
|
593
|
-
{logo}
|
|
594
|
-
</div>
|
|
595
|
-
)}
|
|
596
|
-
|
|
597
|
-
<h1
|
|
598
|
-
style={{
|
|
599
|
-
color: 'var(--theme-text)',
|
|
600
|
-
fontSize: 'var(--font-size-h3)',
|
|
601
|
-
fontWeight: 600,
|
|
602
|
-
margin: '0 0 calc(var(--base) * 0.5) 0',
|
|
603
|
-
textAlign: 'center',
|
|
604
|
-
}}
|
|
605
|
-
>
|
|
606
|
-
Two-Factor Authentication
|
|
607
|
-
</h1>
|
|
608
|
-
|
|
609
|
-
<p
|
|
610
|
-
style={{
|
|
611
|
-
color: 'var(--theme-text)',
|
|
612
|
-
opacity: 0.7,
|
|
613
|
-
fontSize: 'var(--font-size-small)',
|
|
614
|
-
textAlign: 'center',
|
|
615
|
-
marginBottom: 'calc(var(--base) * 1.5)',
|
|
616
|
-
}}
|
|
617
|
-
>
|
|
618
|
-
Enter the 6-digit code from your authenticator app
|
|
619
|
-
</p>
|
|
620
|
-
|
|
621
|
-
<form onSubmit={handleTotpVerify}>
|
|
622
|
-
<div style={{ marginBottom: 'calc(var(--base) * 1.5)' }}>
|
|
623
|
-
<label
|
|
624
|
-
htmlFor="totp-code"
|
|
625
|
-
style={{
|
|
626
|
-
display: 'block',
|
|
627
|
-
color: 'var(--theme-text)',
|
|
628
|
-
marginBottom: 'calc(var(--base) * 0.5)',
|
|
629
|
-
fontSize: 'var(--font-size-small)',
|
|
630
|
-
fontWeight: 500,
|
|
631
|
-
}}
|
|
632
|
-
>
|
|
633
|
-
Verification Code
|
|
634
|
-
</label>
|
|
635
|
-
<input
|
|
636
|
-
id="totp-code"
|
|
637
|
-
type="text"
|
|
638
|
-
inputMode="numeric"
|
|
639
|
-
pattern="[0-9]*"
|
|
640
|
-
autoComplete="one-time-code"
|
|
641
|
-
value={totpCode}
|
|
642
|
-
onChange={(e) => setTotpCode(e.target.value.replace(/\D/g, '').slice(0, 6))}
|
|
643
|
-
required
|
|
644
|
-
placeholder="000000"
|
|
645
|
-
style={{
|
|
646
|
-
width: '100%',
|
|
647
|
-
padding: 'calc(var(--base) * 0.75)',
|
|
648
|
-
background: 'var(--theme-input-bg)',
|
|
649
|
-
border: '1px solid var(--theme-elevation-150)',
|
|
650
|
-
borderRadius: 'var(--style-radius-s)',
|
|
651
|
-
color: 'var(--theme-text)',
|
|
652
|
-
fontSize: 'var(--font-size-h4)',
|
|
653
|
-
fontFamily: 'monospace',
|
|
654
|
-
textAlign: 'center',
|
|
655
|
-
letterSpacing: '0.5em',
|
|
656
|
-
outline: 'none',
|
|
657
|
-
boxSizing: 'border-box',
|
|
658
|
-
}}
|
|
659
|
-
/>
|
|
660
|
-
</div>
|
|
661
|
-
|
|
662
|
-
{error && (
|
|
663
|
-
<div
|
|
664
|
-
style={{
|
|
665
|
-
color: 'var(--theme-error-500)',
|
|
666
|
-
marginBottom: 'var(--base)',
|
|
667
|
-
fontSize: 'var(--font-size-small)',
|
|
668
|
-
padding: 'calc(var(--base) * 0.5)',
|
|
669
|
-
background: 'var(--theme-error-50)',
|
|
670
|
-
borderRadius: 'var(--style-radius-s)',
|
|
671
|
-
border: '1px solid var(--theme-error-200)',
|
|
672
|
-
}}
|
|
673
|
-
>
|
|
674
|
-
{error}
|
|
675
|
-
</div>
|
|
676
|
-
)}
|
|
677
|
-
|
|
678
|
-
<button
|
|
679
|
-
type="submit"
|
|
680
|
-
disabled={totpLoading || totpCode.length !== 6}
|
|
681
|
-
style={{
|
|
682
|
-
width: '100%',
|
|
683
|
-
padding: 'calc(var(--base) * 0.75)',
|
|
684
|
-
background: 'var(--theme-elevation-800)',
|
|
685
|
-
border: 'none',
|
|
686
|
-
borderRadius: 'var(--style-radius-s)',
|
|
687
|
-
color: 'var(--theme-elevation-50)',
|
|
688
|
-
fontSize: 'var(--font-size-base)',
|
|
689
|
-
fontWeight: 500,
|
|
690
|
-
cursor: totpLoading || totpCode.length !== 6 ? 'not-allowed' : 'pointer',
|
|
691
|
-
opacity: totpLoading || totpCode.length !== 6 ? 0.7 : 1,
|
|
692
|
-
transition: 'opacity 150ms ease',
|
|
693
|
-
}}
|
|
694
|
-
>
|
|
695
|
-
{totpLoading ? 'Verifying...' : 'Verify'}
|
|
696
|
-
</button>
|
|
697
|
-
</form>
|
|
698
|
-
|
|
699
|
-
<button
|
|
700
|
-
type="button"
|
|
701
|
-
onClick={handleBackToLogin}
|
|
702
|
-
style={{
|
|
703
|
-
width: '100%',
|
|
704
|
-
marginTop: 'var(--base)',
|
|
705
|
-
padding: 'calc(var(--base) * 0.5)',
|
|
706
|
-
background: 'transparent',
|
|
707
|
-
border: 'none',
|
|
708
|
-
color: 'var(--theme-text)',
|
|
709
|
-
opacity: 0.7,
|
|
710
|
-
fontSize: 'var(--font-size-small)',
|
|
711
|
-
cursor: 'pointer',
|
|
712
|
-
}}
|
|
713
|
-
>
|
|
714
|
-
← Back to login
|
|
715
|
-
</button>
|
|
716
|
-
</div>
|
|
717
|
-
</div>
|
|
718
|
-
)
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
// Registration view
|
|
722
|
-
if (viewMode === 'register') {
|
|
723
|
-
return (
|
|
724
|
-
<div
|
|
725
|
-
style={{
|
|
726
|
-
minHeight: '100vh',
|
|
727
|
-
display: 'flex',
|
|
728
|
-
alignItems: 'center',
|
|
729
|
-
justifyContent: 'center',
|
|
730
|
-
background: 'var(--theme-bg)',
|
|
731
|
-
padding: 'var(--base)',
|
|
732
|
-
}}
|
|
733
|
-
>
|
|
734
|
-
<div
|
|
735
|
-
style={{
|
|
736
|
-
background: 'var(--theme-elevation-50)',
|
|
737
|
-
padding: 'calc(var(--base) * 2)',
|
|
738
|
-
borderRadius: 'var(--style-radius-m)',
|
|
739
|
-
boxShadow: '0 2px 20px rgba(0, 0, 0, 0.1)',
|
|
740
|
-
width: '100%',
|
|
741
|
-
maxWidth: '400px',
|
|
742
|
-
}}
|
|
743
|
-
>
|
|
744
|
-
{logo && (
|
|
745
|
-
<div
|
|
746
|
-
style={{
|
|
747
|
-
textAlign: 'center',
|
|
748
|
-
marginBottom: 'calc(var(--base) * 1.5)',
|
|
749
|
-
}}
|
|
750
|
-
>
|
|
751
|
-
{logo}
|
|
752
|
-
</div>
|
|
753
|
-
)}
|
|
754
|
-
|
|
755
|
-
<h1
|
|
756
|
-
style={{
|
|
757
|
-
color: 'var(--theme-text)',
|
|
758
|
-
fontSize: 'var(--font-size-h3)',
|
|
759
|
-
fontWeight: 600,
|
|
760
|
-
margin: '0 0 calc(var(--base) * 1.5) 0',
|
|
761
|
-
textAlign: 'center',
|
|
762
|
-
}}
|
|
763
|
-
>
|
|
764
|
-
Create Account
|
|
765
|
-
</h1>
|
|
766
|
-
|
|
767
|
-
<form onSubmit={handleSignUp}>
|
|
768
|
-
<div style={{ marginBottom: 'var(--base)' }}>
|
|
769
|
-
<label
|
|
770
|
-
htmlFor="name"
|
|
771
|
-
style={{
|
|
772
|
-
display: 'block',
|
|
773
|
-
color: 'var(--theme-text)',
|
|
774
|
-
marginBottom: 'calc(var(--base) * 0.5)',
|
|
775
|
-
fontSize: 'var(--font-size-small)',
|
|
776
|
-
fontWeight: 500,
|
|
777
|
-
}}
|
|
778
|
-
>
|
|
779
|
-
Name
|
|
780
|
-
</label>
|
|
781
|
-
<input
|
|
782
|
-
id="name"
|
|
783
|
-
type="text"
|
|
784
|
-
value={name}
|
|
785
|
-
onChange={(e) => setName(e.target.value)}
|
|
786
|
-
required
|
|
787
|
-
autoComplete="name"
|
|
788
|
-
style={{
|
|
789
|
-
width: '100%',
|
|
790
|
-
padding: 'calc(var(--base) * 0.75)',
|
|
791
|
-
background: 'var(--theme-input-bg)',
|
|
792
|
-
border: '1px solid var(--theme-elevation-150)',
|
|
793
|
-
borderRadius: 'var(--style-radius-s)',
|
|
794
|
-
color: 'var(--theme-text)',
|
|
795
|
-
fontSize: 'var(--font-size-base)',
|
|
796
|
-
outline: 'none',
|
|
797
|
-
boxSizing: 'border-box',
|
|
798
|
-
}}
|
|
799
|
-
/>
|
|
800
|
-
</div>
|
|
801
|
-
|
|
802
|
-
<div style={{ marginBottom: 'var(--base)' }}>
|
|
803
|
-
<label
|
|
804
|
-
htmlFor="register-email"
|
|
805
|
-
style={{
|
|
806
|
-
display: 'block',
|
|
807
|
-
color: 'var(--theme-text)',
|
|
808
|
-
marginBottom: 'calc(var(--base) * 0.5)',
|
|
809
|
-
fontSize: 'var(--font-size-small)',
|
|
810
|
-
fontWeight: 500,
|
|
811
|
-
}}
|
|
812
|
-
>
|
|
813
|
-
Email
|
|
814
|
-
</label>
|
|
815
|
-
<input
|
|
816
|
-
id="register-email"
|
|
817
|
-
type="email"
|
|
818
|
-
value={email}
|
|
819
|
-
onChange={(e) => setEmail(e.target.value)}
|
|
820
|
-
required
|
|
821
|
-
autoComplete="email"
|
|
822
|
-
style={{
|
|
823
|
-
width: '100%',
|
|
824
|
-
padding: 'calc(var(--base) * 0.75)',
|
|
825
|
-
background: 'var(--theme-input-bg)',
|
|
826
|
-
border: '1px solid var(--theme-elevation-150)',
|
|
827
|
-
borderRadius: 'var(--style-radius-s)',
|
|
828
|
-
color: 'var(--theme-text)',
|
|
829
|
-
fontSize: 'var(--font-size-base)',
|
|
830
|
-
outline: 'none',
|
|
831
|
-
boxSizing: 'border-box',
|
|
832
|
-
}}
|
|
833
|
-
/>
|
|
834
|
-
</div>
|
|
835
|
-
|
|
836
|
-
<div style={{ marginBottom: 'var(--base)' }}>
|
|
837
|
-
<label
|
|
838
|
-
htmlFor="register-password"
|
|
839
|
-
style={{
|
|
840
|
-
display: 'block',
|
|
841
|
-
color: 'var(--theme-text)',
|
|
842
|
-
marginBottom: 'calc(var(--base) * 0.5)',
|
|
843
|
-
fontSize: 'var(--font-size-small)',
|
|
844
|
-
fontWeight: 500,
|
|
845
|
-
}}
|
|
846
|
-
>
|
|
847
|
-
Password
|
|
848
|
-
</label>
|
|
849
|
-
<input
|
|
850
|
-
id="register-password"
|
|
851
|
-
type="password"
|
|
852
|
-
value={password}
|
|
853
|
-
onChange={(e) => setPassword(e.target.value)}
|
|
854
|
-
required
|
|
855
|
-
autoComplete="new-password"
|
|
856
|
-
style={{
|
|
857
|
-
width: '100%',
|
|
858
|
-
padding: 'calc(var(--base) * 0.75)',
|
|
859
|
-
background: 'var(--theme-input-bg)',
|
|
860
|
-
border: '1px solid var(--theme-elevation-150)',
|
|
861
|
-
borderRadius: 'var(--style-radius-s)',
|
|
862
|
-
color: 'var(--theme-text)',
|
|
863
|
-
fontSize: 'var(--font-size-base)',
|
|
864
|
-
outline: 'none',
|
|
865
|
-
boxSizing: 'border-box',
|
|
866
|
-
}}
|
|
867
|
-
/>
|
|
868
|
-
</div>
|
|
869
|
-
|
|
870
|
-
<div style={{ marginBottom: 'calc(var(--base) * 1.5)' }}>
|
|
871
|
-
<label
|
|
872
|
-
htmlFor="confirm-password"
|
|
873
|
-
style={{
|
|
874
|
-
display: 'block',
|
|
875
|
-
color: 'var(--theme-text)',
|
|
876
|
-
marginBottom: 'calc(var(--base) * 0.5)',
|
|
877
|
-
fontSize: 'var(--font-size-small)',
|
|
878
|
-
fontWeight: 500,
|
|
879
|
-
}}
|
|
880
|
-
>
|
|
881
|
-
Confirm Password
|
|
882
|
-
</label>
|
|
883
|
-
<input
|
|
884
|
-
id="confirm-password"
|
|
885
|
-
type="password"
|
|
886
|
-
value={confirmPassword}
|
|
887
|
-
onChange={(e) => setConfirmPassword(e.target.value)}
|
|
888
|
-
required
|
|
889
|
-
autoComplete="new-password"
|
|
890
|
-
style={{
|
|
891
|
-
width: '100%',
|
|
892
|
-
padding: 'calc(var(--base) * 0.75)',
|
|
893
|
-
background: 'var(--theme-input-bg)',
|
|
894
|
-
border: '1px solid var(--theme-elevation-150)',
|
|
895
|
-
borderRadius: 'var(--style-radius-s)',
|
|
896
|
-
color: 'var(--theme-text)',
|
|
897
|
-
fontSize: 'var(--font-size-base)',
|
|
898
|
-
outline: 'none',
|
|
899
|
-
boxSizing: 'border-box',
|
|
900
|
-
}}
|
|
901
|
-
/>
|
|
902
|
-
</div>
|
|
903
|
-
|
|
904
|
-
{error && (
|
|
905
|
-
<div
|
|
906
|
-
style={{
|
|
907
|
-
color: 'var(--theme-error-500)',
|
|
908
|
-
marginBottom: 'var(--base)',
|
|
909
|
-
fontSize: 'var(--font-size-small)',
|
|
910
|
-
padding: 'calc(var(--base) * 0.5)',
|
|
911
|
-
background: 'var(--theme-error-50)',
|
|
912
|
-
borderRadius: 'var(--style-radius-s)',
|
|
913
|
-
border: '1px solid var(--theme-error-200)',
|
|
914
|
-
}}
|
|
915
|
-
>
|
|
916
|
-
{error}
|
|
917
|
-
</div>
|
|
918
|
-
)}
|
|
919
|
-
|
|
920
|
-
<button
|
|
921
|
-
type="submit"
|
|
922
|
-
disabled={loading}
|
|
923
|
-
style={{
|
|
924
|
-
width: '100%',
|
|
925
|
-
padding: 'calc(var(--base) * 0.75)',
|
|
926
|
-
background: 'var(--theme-elevation-800)',
|
|
927
|
-
border: 'none',
|
|
928
|
-
borderRadius: 'var(--style-radius-s)',
|
|
929
|
-
color: 'var(--theme-elevation-50)',
|
|
930
|
-
fontSize: 'var(--font-size-base)',
|
|
931
|
-
fontWeight: 500,
|
|
932
|
-
cursor: loading ? 'not-allowed' : 'pointer',
|
|
933
|
-
opacity: loading ? 0.7 : 1,
|
|
934
|
-
transition: 'opacity 150ms ease',
|
|
935
|
-
}}
|
|
936
|
-
>
|
|
937
|
-
{loading ? 'Creating account...' : 'Create Account'}
|
|
938
|
-
</button>
|
|
939
|
-
</form>
|
|
940
|
-
|
|
941
|
-
<div
|
|
942
|
-
style={{
|
|
943
|
-
marginTop: 'calc(var(--base) * 1.5)',
|
|
944
|
-
textAlign: 'center',
|
|
945
|
-
fontSize: 'var(--font-size-small)',
|
|
946
|
-
color: 'var(--theme-text)',
|
|
947
|
-
opacity: 0.8,
|
|
948
|
-
}}
|
|
949
|
-
>
|
|
950
|
-
Already have an account?{' '}
|
|
951
|
-
<button
|
|
952
|
-
type="button"
|
|
953
|
-
onClick={handleBackToLogin}
|
|
954
|
-
style={{
|
|
955
|
-
background: 'none',
|
|
956
|
-
border: 'none',
|
|
957
|
-
color: 'var(--theme-elevation-800)',
|
|
958
|
-
cursor: 'pointer',
|
|
959
|
-
fontSize: 'inherit',
|
|
960
|
-
textDecoration: 'underline',
|
|
961
|
-
padding: 0,
|
|
962
|
-
}}
|
|
963
|
-
>
|
|
964
|
-
Sign in
|
|
965
|
-
</button>
|
|
966
|
-
</div>
|
|
967
|
-
</div>
|
|
968
|
-
</div>
|
|
969
|
-
)
|
|
970
|
-
}
|
|
971
|
-
|
|
972
|
-
// Forgot password view
|
|
973
|
-
if (viewMode === 'forgotPassword') {
|
|
974
|
-
return (
|
|
975
|
-
<div
|
|
976
|
-
style={{
|
|
977
|
-
minHeight: '100vh',
|
|
978
|
-
display: 'flex',
|
|
979
|
-
alignItems: 'center',
|
|
980
|
-
justifyContent: 'center',
|
|
981
|
-
background: 'var(--theme-bg)',
|
|
982
|
-
padding: 'var(--base)',
|
|
983
|
-
}}
|
|
984
|
-
>
|
|
985
|
-
<div
|
|
986
|
-
style={{
|
|
987
|
-
background: 'var(--theme-elevation-50)',
|
|
988
|
-
padding: 'calc(var(--base) * 2)',
|
|
989
|
-
borderRadius: 'var(--style-radius-m)',
|
|
990
|
-
boxShadow: '0 2px 20px rgba(0, 0, 0, 0.1)',
|
|
991
|
-
width: '100%',
|
|
992
|
-
maxWidth: '400px',
|
|
993
|
-
}}
|
|
994
|
-
>
|
|
995
|
-
{logo && (
|
|
996
|
-
<div
|
|
997
|
-
style={{
|
|
998
|
-
textAlign: 'center',
|
|
999
|
-
marginBottom: 'calc(var(--base) * 1.5)',
|
|
1000
|
-
}}
|
|
1001
|
-
>
|
|
1002
|
-
{logo}
|
|
1003
|
-
</div>
|
|
1004
|
-
)}
|
|
1005
|
-
|
|
1006
|
-
<h1
|
|
1007
|
-
style={{
|
|
1008
|
-
color: 'var(--theme-text)',
|
|
1009
|
-
fontSize: 'var(--font-size-h3)',
|
|
1010
|
-
fontWeight: 600,
|
|
1011
|
-
margin: '0 0 calc(var(--base) * 0.5) 0',
|
|
1012
|
-
textAlign: 'center',
|
|
1013
|
-
}}
|
|
1014
|
-
>
|
|
1015
|
-
Reset Password
|
|
1016
|
-
</h1>
|
|
1017
|
-
|
|
1018
|
-
<p
|
|
1019
|
-
style={{
|
|
1020
|
-
color: 'var(--theme-text)',
|
|
1021
|
-
opacity: 0.7,
|
|
1022
|
-
fontSize: 'var(--font-size-small)',
|
|
1023
|
-
textAlign: 'center',
|
|
1024
|
-
marginBottom: 'calc(var(--base) * 1.5)',
|
|
1025
|
-
}}
|
|
1026
|
-
>
|
|
1027
|
-
Enter your email and we'll send you a link to reset your password
|
|
1028
|
-
</p>
|
|
1029
|
-
|
|
1030
|
-
<form onSubmit={handleForgotPassword}>
|
|
1031
|
-
<div style={{ marginBottom: 'calc(var(--base) * 1.5)' }}>
|
|
1032
|
-
<label
|
|
1033
|
-
htmlFor="forgot-email"
|
|
1034
|
-
style={{
|
|
1035
|
-
display: 'block',
|
|
1036
|
-
color: 'var(--theme-text)',
|
|
1037
|
-
marginBottom: 'calc(var(--base) * 0.5)',
|
|
1038
|
-
fontSize: 'var(--font-size-small)',
|
|
1039
|
-
fontWeight: 500,
|
|
1040
|
-
}}
|
|
1041
|
-
>
|
|
1042
|
-
Email
|
|
1043
|
-
</label>
|
|
1044
|
-
<input
|
|
1045
|
-
id="forgot-email"
|
|
1046
|
-
type="email"
|
|
1047
|
-
value={email}
|
|
1048
|
-
onChange={(e) => setEmail(e.target.value)}
|
|
1049
|
-
required
|
|
1050
|
-
autoComplete="email"
|
|
1051
|
-
style={{
|
|
1052
|
-
width: '100%',
|
|
1053
|
-
padding: 'calc(var(--base) * 0.75)',
|
|
1054
|
-
background: 'var(--theme-input-bg)',
|
|
1055
|
-
border: '1px solid var(--theme-elevation-150)',
|
|
1056
|
-
borderRadius: 'var(--style-radius-s)',
|
|
1057
|
-
color: 'var(--theme-text)',
|
|
1058
|
-
fontSize: 'var(--font-size-base)',
|
|
1059
|
-
outline: 'none',
|
|
1060
|
-
boxSizing: 'border-box',
|
|
1061
|
-
}}
|
|
1062
|
-
/>
|
|
1063
|
-
</div>
|
|
1064
|
-
|
|
1065
|
-
{error && (
|
|
1066
|
-
<div
|
|
1067
|
-
style={{
|
|
1068
|
-
color: 'var(--theme-error-500)',
|
|
1069
|
-
marginBottom: 'var(--base)',
|
|
1070
|
-
fontSize: 'var(--font-size-small)',
|
|
1071
|
-
padding: 'calc(var(--base) * 0.5)',
|
|
1072
|
-
background: 'var(--theme-error-50)',
|
|
1073
|
-
borderRadius: 'var(--style-radius-s)',
|
|
1074
|
-
border: '1px solid var(--theme-error-200)',
|
|
1075
|
-
}}
|
|
1076
|
-
>
|
|
1077
|
-
{error}
|
|
1078
|
-
</div>
|
|
1079
|
-
)}
|
|
1080
|
-
|
|
1081
|
-
<button
|
|
1082
|
-
type="submit"
|
|
1083
|
-
disabled={loading}
|
|
1084
|
-
style={{
|
|
1085
|
-
width: '100%',
|
|
1086
|
-
padding: 'calc(var(--base) * 0.75)',
|
|
1087
|
-
background: 'var(--theme-elevation-800)',
|
|
1088
|
-
border: 'none',
|
|
1089
|
-
borderRadius: 'var(--style-radius-s)',
|
|
1090
|
-
color: 'var(--theme-elevation-50)',
|
|
1091
|
-
fontSize: 'var(--font-size-base)',
|
|
1092
|
-
fontWeight: 500,
|
|
1093
|
-
cursor: loading ? 'not-allowed' : 'pointer',
|
|
1094
|
-
opacity: loading ? 0.7 : 1,
|
|
1095
|
-
transition: 'opacity 150ms ease',
|
|
1096
|
-
}}
|
|
1097
|
-
>
|
|
1098
|
-
{loading ? 'Sending...' : 'Send Reset Link'}
|
|
1099
|
-
</button>
|
|
1100
|
-
</form>
|
|
1101
|
-
|
|
1102
|
-
<button
|
|
1103
|
-
type="button"
|
|
1104
|
-
onClick={handleBackToLogin}
|
|
1105
|
-
style={{
|
|
1106
|
-
width: '100%',
|
|
1107
|
-
marginTop: 'var(--base)',
|
|
1108
|
-
padding: 'calc(var(--base) * 0.5)',
|
|
1109
|
-
background: 'transparent',
|
|
1110
|
-
border: 'none',
|
|
1111
|
-
color: 'var(--theme-text)',
|
|
1112
|
-
opacity: 0.7,
|
|
1113
|
-
fontSize: 'var(--font-size-small)',
|
|
1114
|
-
cursor: 'pointer',
|
|
1115
|
-
}}
|
|
1116
|
-
>
|
|
1117
|
-
← Back to login
|
|
1118
|
-
</button>
|
|
1119
|
-
</div>
|
|
1120
|
-
</div>
|
|
1121
|
-
)
|
|
1122
|
-
}
|
|
1123
|
-
|
|
1124
|
-
// Reset link sent confirmation view
|
|
1125
|
-
if (viewMode === 'resetSent') {
|
|
1126
|
-
return (
|
|
1127
|
-
<div
|
|
1128
|
-
style={{
|
|
1129
|
-
minHeight: '100vh',
|
|
1130
|
-
display: 'flex',
|
|
1131
|
-
alignItems: 'center',
|
|
1132
|
-
justifyContent: 'center',
|
|
1133
|
-
background: 'var(--theme-bg)',
|
|
1134
|
-
padding: 'var(--base)',
|
|
1135
|
-
}}
|
|
1136
|
-
>
|
|
1137
|
-
<div
|
|
1138
|
-
style={{
|
|
1139
|
-
background: 'var(--theme-elevation-50)',
|
|
1140
|
-
padding: 'calc(var(--base) * 2)',
|
|
1141
|
-
borderRadius: 'var(--style-radius-m)',
|
|
1142
|
-
boxShadow: '0 2px 20px rgba(0, 0, 0, 0.1)',
|
|
1143
|
-
width: '100%',
|
|
1144
|
-
maxWidth: '400px',
|
|
1145
|
-
textAlign: 'center',
|
|
1146
|
-
}}
|
|
1147
|
-
>
|
|
1148
|
-
{logo && (
|
|
1149
|
-
<div
|
|
1150
|
-
style={{
|
|
1151
|
-
marginBottom: 'calc(var(--base) * 1.5)',
|
|
1152
|
-
}}
|
|
1153
|
-
>
|
|
1154
|
-
{logo}
|
|
1155
|
-
</div>
|
|
1156
|
-
)}
|
|
1157
|
-
|
|
1158
|
-
<div
|
|
1159
|
-
style={{
|
|
1160
|
-
width: '64px',
|
|
1161
|
-
height: '64px',
|
|
1162
|
-
background: 'var(--theme-success-100)',
|
|
1163
|
-
borderRadius: '50%',
|
|
1164
|
-
display: 'flex',
|
|
1165
|
-
alignItems: 'center',
|
|
1166
|
-
justifyContent: 'center',
|
|
1167
|
-
margin: '0 auto calc(var(--base) * 1.5)',
|
|
1168
|
-
fontSize: '28px',
|
|
1169
|
-
}}
|
|
1170
|
-
>
|
|
1171
|
-
✓
|
|
1172
|
-
</div>
|
|
1173
|
-
|
|
1174
|
-
<h1
|
|
1175
|
-
style={{
|
|
1176
|
-
color: 'var(--theme-text)',
|
|
1177
|
-
fontSize: 'var(--font-size-h3)',
|
|
1178
|
-
fontWeight: 600,
|
|
1179
|
-
margin: '0 0 calc(var(--base) * 0.5) 0',
|
|
1180
|
-
}}
|
|
1181
|
-
>
|
|
1182
|
-
Check Your Email
|
|
1183
|
-
</h1>
|
|
1184
|
-
|
|
1185
|
-
<p
|
|
1186
|
-
style={{
|
|
1187
|
-
color: 'var(--theme-text)',
|
|
1188
|
-
opacity: 0.7,
|
|
1189
|
-
fontSize: 'var(--font-size-small)',
|
|
1190
|
-
marginBottom: 'calc(var(--base) * 1.5)',
|
|
1191
|
-
}}
|
|
1192
|
-
>
|
|
1193
|
-
We've sent a password reset link to <strong>{email}</strong>
|
|
1194
|
-
</p>
|
|
1195
|
-
|
|
1196
|
-
<p
|
|
1197
|
-
style={{
|
|
1198
|
-
color: 'var(--theme-text)',
|
|
1199
|
-
opacity: 0.6,
|
|
1200
|
-
fontSize: 'var(--font-size-small)',
|
|
1201
|
-
marginBottom: 'calc(var(--base) * 1.5)',
|
|
1202
|
-
}}
|
|
1203
|
-
>
|
|
1204
|
-
Didn't receive the email? Check your spam folder or try again.
|
|
1205
|
-
</p>
|
|
1206
|
-
|
|
1207
|
-
<button
|
|
1208
|
-
type="button"
|
|
1209
|
-
onClick={handleBackToLogin}
|
|
1210
|
-
style={{
|
|
1211
|
-
padding: 'calc(var(--base) * 0.75) calc(var(--base) * 1.5)',
|
|
1212
|
-
background: 'var(--theme-elevation-150)',
|
|
1213
|
-
border: 'none',
|
|
1214
|
-
borderRadius: 'var(--style-radius-s)',
|
|
1215
|
-
color: 'var(--theme-text)',
|
|
1216
|
-
fontSize: 'var(--font-size-base)',
|
|
1217
|
-
cursor: 'pointer',
|
|
1218
|
-
}}
|
|
1219
|
-
>
|
|
1220
|
-
Back to login
|
|
1221
|
-
</button>
|
|
1222
|
-
</div>
|
|
1223
|
-
</div>
|
|
1224
|
-
)
|
|
1225
|
-
}
|
|
1226
|
-
|
|
1227
|
-
// Main login view
|
|
1228
|
-
return (
|
|
1229
|
-
<div
|
|
1230
|
-
style={{
|
|
1231
|
-
minHeight: '100vh',
|
|
1232
|
-
display: 'flex',
|
|
1233
|
-
alignItems: 'center',
|
|
1234
|
-
justifyContent: 'center',
|
|
1235
|
-
background: 'var(--theme-bg)',
|
|
1236
|
-
padding: 'var(--base)',
|
|
1237
|
-
}}
|
|
1238
|
-
>
|
|
1239
|
-
<div
|
|
1240
|
-
style={{
|
|
1241
|
-
background: 'var(--theme-elevation-50)',
|
|
1242
|
-
padding: 'calc(var(--base) * 2)',
|
|
1243
|
-
borderRadius: 'var(--style-radius-m)',
|
|
1244
|
-
boxShadow: '0 2px 20px rgba(0, 0, 0, 0.1)',
|
|
1245
|
-
width: '100%',
|
|
1246
|
-
maxWidth: '400px',
|
|
1247
|
-
}}
|
|
1248
|
-
>
|
|
1249
|
-
{logo && (
|
|
1250
|
-
<div
|
|
1251
|
-
style={{
|
|
1252
|
-
textAlign: 'center',
|
|
1253
|
-
marginBottom: 'calc(var(--base) * 1.5)',
|
|
1254
|
-
}}
|
|
1255
|
-
>
|
|
1256
|
-
{logo}
|
|
1257
|
-
</div>
|
|
1258
|
-
)}
|
|
1259
|
-
|
|
1260
|
-
<h1
|
|
1261
|
-
style={{
|
|
1262
|
-
color: 'var(--theme-text)',
|
|
1263
|
-
fontSize: 'var(--font-size-h3)',
|
|
1264
|
-
fontWeight: 600,
|
|
1265
|
-
textAlign: 'center',
|
|
1266
|
-
margin: '0 0 calc(var(--base) * 1.5) 0',
|
|
1267
|
-
}}
|
|
1268
|
-
>
|
|
1269
|
-
{title}
|
|
1270
|
-
</h1>
|
|
1271
|
-
|
|
1272
|
-
{successMessage && (
|
|
1273
|
-
<div
|
|
1274
|
-
style={{
|
|
1275
|
-
color: 'var(--theme-success-500)',
|
|
1276
|
-
marginBottom: 'var(--base)',
|
|
1277
|
-
fontSize: 'var(--font-size-small)',
|
|
1278
|
-
padding: 'calc(var(--base) * 0.5)',
|
|
1279
|
-
background: 'var(--theme-success-50)',
|
|
1280
|
-
borderRadius: 'var(--style-radius-s)',
|
|
1281
|
-
border: '1px solid var(--theme-success-200)',
|
|
1282
|
-
}}
|
|
1283
|
-
>
|
|
1284
|
-
{successMessage}
|
|
1285
|
-
</div>
|
|
1286
|
-
)}
|
|
1287
|
-
|
|
1288
|
-
<form onSubmit={handleSubmit}>
|
|
1289
|
-
<div style={{ marginBottom: 'var(--base)' }}>
|
|
1290
|
-
<label
|
|
1291
|
-
htmlFor="email"
|
|
1292
|
-
style={{
|
|
1293
|
-
display: 'block',
|
|
1294
|
-
color: 'var(--theme-text)',
|
|
1295
|
-
marginBottom: 'calc(var(--base) * 0.5)',
|
|
1296
|
-
fontSize: 'var(--font-size-small)',
|
|
1297
|
-
fontWeight: 500,
|
|
1298
|
-
}}
|
|
1299
|
-
>
|
|
1300
|
-
Email
|
|
1301
|
-
</label>
|
|
1302
|
-
<input
|
|
1303
|
-
id="email"
|
|
1304
|
-
type="email"
|
|
1305
|
-
value={email}
|
|
1306
|
-
onChange={(e) => setEmail(e.target.value)}
|
|
1307
|
-
required
|
|
1308
|
-
autoComplete="email"
|
|
1309
|
-
style={{
|
|
1310
|
-
width: '100%',
|
|
1311
|
-
padding: 'calc(var(--base) * 0.75)',
|
|
1312
|
-
background: 'var(--theme-input-bg)',
|
|
1313
|
-
border: '1px solid var(--theme-elevation-150)',
|
|
1314
|
-
borderRadius: 'var(--style-radius-s)',
|
|
1315
|
-
color: 'var(--theme-text)',
|
|
1316
|
-
fontSize: 'var(--font-size-base)',
|
|
1317
|
-
outline: 'none',
|
|
1318
|
-
boxSizing: 'border-box',
|
|
1319
|
-
}}
|
|
1320
|
-
/>
|
|
1321
|
-
</div>
|
|
1322
|
-
|
|
1323
|
-
<div style={{ marginBottom: 'var(--base)' }}>
|
|
1324
|
-
<label
|
|
1325
|
-
htmlFor="password"
|
|
1326
|
-
style={{
|
|
1327
|
-
display: 'block',
|
|
1328
|
-
color: 'var(--theme-text)',
|
|
1329
|
-
marginBottom: 'calc(var(--base) * 0.5)',
|
|
1330
|
-
fontSize: 'var(--font-size-small)',
|
|
1331
|
-
fontWeight: 500,
|
|
1332
|
-
}}
|
|
1333
|
-
>
|
|
1334
|
-
Password
|
|
1335
|
-
</label>
|
|
1336
|
-
<input
|
|
1337
|
-
id="password"
|
|
1338
|
-
type="password"
|
|
1339
|
-
value={password}
|
|
1340
|
-
onChange={(e) => setPassword(e.target.value)}
|
|
1341
|
-
required
|
|
1342
|
-
autoComplete="current-password"
|
|
1343
|
-
style={{
|
|
1344
|
-
width: '100%',
|
|
1345
|
-
padding: 'calc(var(--base) * 0.75)',
|
|
1346
|
-
background: 'var(--theme-input-bg)',
|
|
1347
|
-
border: '1px solid var(--theme-elevation-150)',
|
|
1348
|
-
borderRadius: 'var(--style-radius-s)',
|
|
1349
|
-
color: 'var(--theme-text)',
|
|
1350
|
-
fontSize: 'var(--font-size-base)',
|
|
1351
|
-
outline: 'none',
|
|
1352
|
-
boxSizing: 'border-box',
|
|
1353
|
-
}}
|
|
1354
|
-
/>
|
|
1355
|
-
</div>
|
|
1356
|
-
|
|
1357
|
-
{forgotPasswordAvailable && (
|
|
1358
|
-
<div
|
|
1359
|
-
style={{
|
|
1360
|
-
marginBottom: 'calc(var(--base) * 1.5)',
|
|
1361
|
-
textAlign: 'right',
|
|
1362
|
-
}}
|
|
1363
|
-
>
|
|
1364
|
-
<button
|
|
1365
|
-
type="button"
|
|
1366
|
-
onClick={() => switchView('forgotPassword')}
|
|
1367
|
-
style={{
|
|
1368
|
-
background: 'none',
|
|
1369
|
-
border: 'none',
|
|
1370
|
-
color: 'var(--theme-text)',
|
|
1371
|
-
opacity: 0.7,
|
|
1372
|
-
cursor: 'pointer',
|
|
1373
|
-
fontSize: 'var(--font-size-small)',
|
|
1374
|
-
padding: 0,
|
|
1375
|
-
textDecoration: 'underline',
|
|
1376
|
-
}}
|
|
1377
|
-
>
|
|
1378
|
-
Forgot password?
|
|
1379
|
-
</button>
|
|
1380
|
-
</div>
|
|
1381
|
-
)}
|
|
1382
|
-
|
|
1383
|
-
{error && (
|
|
1384
|
-
<div
|
|
1385
|
-
style={{
|
|
1386
|
-
color: 'var(--theme-error-500)',
|
|
1387
|
-
marginBottom: 'var(--base)',
|
|
1388
|
-
fontSize: 'var(--font-size-small)',
|
|
1389
|
-
padding: 'calc(var(--base) * 0.5)',
|
|
1390
|
-
background: 'var(--theme-error-50)',
|
|
1391
|
-
borderRadius: 'var(--style-radius-s)',
|
|
1392
|
-
border: '1px solid var(--theme-error-200)',
|
|
1393
|
-
}}
|
|
1394
|
-
>
|
|
1395
|
-
{error}
|
|
1396
|
-
</div>
|
|
1397
|
-
)}
|
|
1398
|
-
|
|
1399
|
-
<button
|
|
1400
|
-
type="submit"
|
|
1401
|
-
disabled={loading || passkeyLoading}
|
|
1402
|
-
style={{
|
|
1403
|
-
width: '100%',
|
|
1404
|
-
padding: 'calc(var(--base) * 0.75)',
|
|
1405
|
-
background: 'var(--theme-elevation-800)',
|
|
1406
|
-
border: 'none',
|
|
1407
|
-
borderRadius: 'var(--style-radius-s)',
|
|
1408
|
-
color: 'var(--theme-elevation-50)',
|
|
1409
|
-
fontSize: 'var(--font-size-base)',
|
|
1410
|
-
fontWeight: 500,
|
|
1411
|
-
cursor: loading || passkeyLoading ? 'not-allowed' : 'pointer',
|
|
1412
|
-
opacity: loading || passkeyLoading ? 0.7 : 1,
|
|
1413
|
-
transition: 'opacity 150ms ease',
|
|
1414
|
-
}}
|
|
1415
|
-
>
|
|
1416
|
-
{loading ? 'Signing in...' : 'Sign In'}
|
|
1417
|
-
</button>
|
|
1418
|
-
</form>
|
|
1419
|
-
|
|
1420
|
-
{passkeyAvailable && (
|
|
1421
|
-
<>
|
|
1422
|
-
<div
|
|
1423
|
-
style={{
|
|
1424
|
-
display: 'flex',
|
|
1425
|
-
alignItems: 'center',
|
|
1426
|
-
margin: 'calc(var(--base) * 1.5) 0',
|
|
1427
|
-
gap: 'calc(var(--base) * 1)',
|
|
1428
|
-
}}
|
|
1429
|
-
>
|
|
1430
|
-
<div
|
|
1431
|
-
style={{
|
|
1432
|
-
flex: 1,
|
|
1433
|
-
height: '1px',
|
|
1434
|
-
background: 'var(--theme-elevation-150)',
|
|
1435
|
-
}}
|
|
1436
|
-
/>
|
|
1437
|
-
<span
|
|
1438
|
-
style={{
|
|
1439
|
-
color: 'var(--theme-text)',
|
|
1440
|
-
opacity: 0.6,
|
|
1441
|
-
fontSize: 'var(--font-size-small)',
|
|
1442
|
-
}}
|
|
1443
|
-
>
|
|
1444
|
-
or
|
|
1445
|
-
</span>
|
|
1446
|
-
<div
|
|
1447
|
-
style={{
|
|
1448
|
-
flex: 1,
|
|
1449
|
-
height: '1px',
|
|
1450
|
-
background: 'var(--theme-elevation-150)',
|
|
1451
|
-
}}
|
|
1452
|
-
/>
|
|
1453
|
-
</div>
|
|
1454
|
-
|
|
1455
|
-
<button
|
|
1456
|
-
type="button"
|
|
1457
|
-
onClick={handlePasskeySignIn}
|
|
1458
|
-
disabled={loading || passkeyLoading}
|
|
1459
|
-
style={{
|
|
1460
|
-
width: '100%',
|
|
1461
|
-
padding: 'calc(var(--base) * 0.75)',
|
|
1462
|
-
background: 'transparent',
|
|
1463
|
-
border: '1px solid var(--theme-elevation-300)',
|
|
1464
|
-
borderRadius: 'var(--style-radius-s)',
|
|
1465
|
-
color: 'var(--theme-text)',
|
|
1466
|
-
fontSize: 'var(--font-size-base)',
|
|
1467
|
-
fontWeight: 500,
|
|
1468
|
-
cursor: loading || passkeyLoading ? 'not-allowed' : 'pointer',
|
|
1469
|
-
opacity: loading || passkeyLoading ? 0.7 : 1,
|
|
1470
|
-
transition: 'opacity 150ms ease',
|
|
1471
|
-
display: 'flex',
|
|
1472
|
-
alignItems: 'center',
|
|
1473
|
-
justifyContent: 'center',
|
|
1474
|
-
gap: 'calc(var(--base) * 0.5)',
|
|
1475
|
-
}}
|
|
1476
|
-
>
|
|
1477
|
-
<span style={{ fontSize: '18px' }}>🔐</span>
|
|
1478
|
-
{passkeyLoading ? 'Authenticating...' : 'Sign in with Passkey'}
|
|
1479
|
-
</button>
|
|
1480
|
-
</>
|
|
1481
|
-
)}
|
|
1482
|
-
|
|
1483
|
-
{signUpAvailable && (
|
|
1484
|
-
<div
|
|
1485
|
-
style={{
|
|
1486
|
-
marginTop: 'calc(var(--base) * 1.5)',
|
|
1487
|
-
textAlign: 'center',
|
|
1488
|
-
fontSize: 'var(--font-size-small)',
|
|
1489
|
-
color: 'var(--theme-text)',
|
|
1490
|
-
opacity: 0.8,
|
|
1491
|
-
}}
|
|
1492
|
-
>
|
|
1493
|
-
Don't have an account?{' '}
|
|
1494
|
-
<button
|
|
1495
|
-
type="button"
|
|
1496
|
-
onClick={() => switchView('register')}
|
|
1497
|
-
style={{
|
|
1498
|
-
background: 'none',
|
|
1499
|
-
border: 'none',
|
|
1500
|
-
color: 'var(--theme-elevation-800)',
|
|
1501
|
-
cursor: 'pointer',
|
|
1502
|
-
fontSize: 'inherit',
|
|
1503
|
-
textDecoration: 'underline',
|
|
1504
|
-
padding: 0,
|
|
1505
|
-
}}
|
|
1506
|
-
>
|
|
1507
|
-
Create account
|
|
1508
|
-
</button>
|
|
1509
|
-
</div>
|
|
1510
|
-
)}
|
|
1511
|
-
</div>
|
|
1512
|
-
</div>
|
|
1513
|
-
)
|
|
1514
|
-
}
|
|
1515
|
-
|
|
1516
|
-
export default LoginView
|