@delmaredigital/payload-better-auth 0.3.8 → 0.3.10

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 (206) hide show
  1. package/dist/adapter/collections.d.ts +0 -1
  2. package/dist/adapter/collections.js +0 -2
  3. package/dist/adapter/index.d.ts +0 -1
  4. package/dist/adapter/index.js +0 -2
  5. package/dist/components/BeforeLogin.d.ts +0 -1
  6. package/dist/components/BeforeLogin.js +0 -2
  7. package/dist/components/LoginView.d.ts +0 -1
  8. package/dist/components/LoginView.js +0 -2
  9. package/dist/components/LoginViewWrapper.d.ts +0 -1
  10. package/dist/components/LoginViewWrapper.js +0 -2
  11. package/dist/components/LogoutButton.d.ts +0 -1
  12. package/dist/components/LogoutButton.js +0 -2
  13. package/dist/components/PasskeyRegisterButton.d.ts +0 -1
  14. package/dist/components/PasskeyRegisterButton.js +0 -2
  15. package/dist/components/PasskeySignInButton.d.ts +0 -1
  16. package/dist/components/PasskeySignInButton.js +0 -2
  17. package/dist/components/auth/ForgotPasswordView.d.ts +0 -1
  18. package/dist/components/auth/ForgotPasswordView.js +0 -2
  19. package/dist/components/auth/ResetPasswordView.d.ts +0 -1
  20. package/dist/components/auth/ResetPasswordView.js +0 -2
  21. package/dist/components/auth/index.d.ts +0 -1
  22. package/dist/components/auth/index.js +0 -2
  23. package/dist/components/management/ApiKeysManagementClient.d.ts +0 -1
  24. package/dist/components/management/ApiKeysManagementClient.js +0 -2
  25. package/dist/components/management/PasskeysManagementClient.d.ts +0 -1
  26. package/dist/components/management/PasskeysManagementClient.js +0 -2
  27. package/dist/components/management/SecurityNavLinks.d.ts +0 -1
  28. package/dist/components/management/SecurityNavLinks.js +0 -2
  29. package/dist/components/management/TwoFactorManagementClient.d.ts +0 -1
  30. package/dist/components/management/TwoFactorManagementClient.js +0 -2
  31. package/dist/components/management/index.d.ts +0 -1
  32. package/dist/components/management/index.js +0 -2
  33. package/dist/components/management/views/ApiKeysView.d.ts +0 -1
  34. package/dist/components/management/views/ApiKeysView.js +0 -2
  35. package/dist/components/management/views/PasskeysView.d.ts +0 -1
  36. package/dist/components/management/views/PasskeysView.js +0 -2
  37. package/dist/components/management/views/TwoFactorView.d.ts +0 -1
  38. package/dist/components/management/views/TwoFactorView.js +0 -2
  39. package/dist/components/management/views/index.d.ts +0 -1
  40. package/dist/components/management/views/index.js +0 -2
  41. package/dist/components/twoFactor/TwoFactorSetupView.d.ts +0 -1
  42. package/dist/components/twoFactor/TwoFactorSetupView.js +0 -2
  43. package/dist/components/twoFactor/TwoFactorVerifyView.d.ts +0 -1
  44. package/dist/components/twoFactor/TwoFactorVerifyView.js +0 -2
  45. package/dist/components/twoFactor/index.d.ts +0 -1
  46. package/dist/components/twoFactor/index.js +0 -2
  47. package/dist/exports/client.d.ts +0 -1
  48. package/dist/exports/client.js +0 -2
  49. package/dist/exports/components.d.ts +0 -1
  50. package/dist/exports/components.js +0 -2
  51. package/dist/exports/management.d.ts +0 -1
  52. package/dist/exports/management.js +0 -2
  53. package/dist/exports/rsc.d.ts +0 -1
  54. package/dist/exports/rsc.js +0 -2
  55. package/dist/generated-types.d.ts +0 -1
  56. package/dist/generated-types.js +0 -2
  57. package/dist/index.d.ts +0 -1
  58. package/dist/index.js +0 -2
  59. package/dist/plugin/index.d.ts +0 -1
  60. package/dist/plugin/index.js +0 -2
  61. package/dist/scripts/generate-types.d.ts +0 -1
  62. package/dist/scripts/generate-types.js +0 -2
  63. package/dist/types/apiKey.d.ts +0 -1
  64. package/dist/types/apiKey.js +0 -2
  65. package/dist/types/betterAuth.d.ts +0 -1
  66. package/dist/types/betterAuth.js +0 -2
  67. package/dist/utils/access.d.ts +0 -1
  68. package/dist/utils/access.js +0 -2
  69. package/dist/utils/apiKeyAccess.d.ts +0 -1
  70. package/dist/utils/apiKeyAccess.js +0 -2
  71. package/dist/utils/betterAuthDefaults.d.ts +0 -1
  72. package/dist/utils/betterAuthDefaults.js +0 -2
  73. package/dist/utils/detectAuthConfig.d.ts +0 -1
  74. package/dist/utils/detectAuthConfig.js +0 -2
  75. package/dist/utils/detectEnabledPlugins.d.ts +0 -1
  76. package/dist/utils/detectEnabledPlugins.js +0 -2
  77. package/dist/utils/firstUserAdmin.d.ts +0 -1
  78. package/dist/utils/firstUserAdmin.js +0 -2
  79. package/dist/utils/generateScopes.d.ts +0 -1
  80. package/dist/utils/generateScopes.js +0 -2
  81. package/dist/utils/session.d.ts +0 -1
  82. package/dist/utils/session.js +0 -2
  83. package/package.json +34 -91
  84. package/dist/adapter/collections.d.ts.map +0 -1
  85. package/dist/adapter/collections.js.map +0 -1
  86. package/dist/adapter/index.d.ts.map +0 -1
  87. package/dist/adapter/index.js.map +0 -1
  88. package/dist/components/BeforeLogin.d.ts.map +0 -1
  89. package/dist/components/BeforeLogin.js.map +0 -1
  90. package/dist/components/LoginView.d.ts.map +0 -1
  91. package/dist/components/LoginView.js.map +0 -1
  92. package/dist/components/LoginViewWrapper.d.ts.map +0 -1
  93. package/dist/components/LoginViewWrapper.js.map +0 -1
  94. package/dist/components/LogoutButton.d.ts.map +0 -1
  95. package/dist/components/LogoutButton.js.map +0 -1
  96. package/dist/components/PasskeyRegisterButton.d.ts.map +0 -1
  97. package/dist/components/PasskeyRegisterButton.js.map +0 -1
  98. package/dist/components/PasskeySignInButton.d.ts.map +0 -1
  99. package/dist/components/PasskeySignInButton.js.map +0 -1
  100. package/dist/components/auth/ForgotPasswordView.d.ts.map +0 -1
  101. package/dist/components/auth/ForgotPasswordView.js.map +0 -1
  102. package/dist/components/auth/ResetPasswordView.d.ts.map +0 -1
  103. package/dist/components/auth/ResetPasswordView.js.map +0 -1
  104. package/dist/components/auth/index.d.ts.map +0 -1
  105. package/dist/components/auth/index.js.map +0 -1
  106. package/dist/components/management/ApiKeysManagementClient.d.ts.map +0 -1
  107. package/dist/components/management/ApiKeysManagementClient.js.map +0 -1
  108. package/dist/components/management/PasskeysManagementClient.d.ts.map +0 -1
  109. package/dist/components/management/PasskeysManagementClient.js.map +0 -1
  110. package/dist/components/management/SecurityNavLinks.d.ts.map +0 -1
  111. package/dist/components/management/SecurityNavLinks.js.map +0 -1
  112. package/dist/components/management/TwoFactorManagementClient.d.ts.map +0 -1
  113. package/dist/components/management/TwoFactorManagementClient.js.map +0 -1
  114. package/dist/components/management/index.d.ts.map +0 -1
  115. package/dist/components/management/index.js.map +0 -1
  116. package/dist/components/management/views/ApiKeysView.d.ts.map +0 -1
  117. package/dist/components/management/views/ApiKeysView.js.map +0 -1
  118. package/dist/components/management/views/PasskeysView.d.ts.map +0 -1
  119. package/dist/components/management/views/PasskeysView.js.map +0 -1
  120. package/dist/components/management/views/TwoFactorView.d.ts.map +0 -1
  121. package/dist/components/management/views/TwoFactorView.js.map +0 -1
  122. package/dist/components/management/views/index.d.ts.map +0 -1
  123. package/dist/components/management/views/index.js.map +0 -1
  124. package/dist/components/twoFactor/TwoFactorSetupView.d.ts.map +0 -1
  125. package/dist/components/twoFactor/TwoFactorSetupView.js.map +0 -1
  126. package/dist/components/twoFactor/TwoFactorVerifyView.d.ts.map +0 -1
  127. package/dist/components/twoFactor/TwoFactorVerifyView.js.map +0 -1
  128. package/dist/components/twoFactor/index.d.ts.map +0 -1
  129. package/dist/components/twoFactor/index.js.map +0 -1
  130. package/dist/exports/client.d.ts.map +0 -1
  131. package/dist/exports/client.js.map +0 -1
  132. package/dist/exports/components.d.ts.map +0 -1
  133. package/dist/exports/components.js.map +0 -1
  134. package/dist/exports/management.d.ts.map +0 -1
  135. package/dist/exports/management.js.map +0 -1
  136. package/dist/exports/rsc.d.ts.map +0 -1
  137. package/dist/exports/rsc.js.map +0 -1
  138. package/dist/generated-types.d.ts.map +0 -1
  139. package/dist/generated-types.js.map +0 -1
  140. package/dist/index.d.ts.map +0 -1
  141. package/dist/index.js.map +0 -1
  142. package/dist/plugin/index.d.ts.map +0 -1
  143. package/dist/plugin/index.js.map +0 -1
  144. package/dist/scripts/generate-types.d.ts.map +0 -1
  145. package/dist/scripts/generate-types.js.map +0 -1
  146. package/dist/types/apiKey.d.ts.map +0 -1
  147. package/dist/types/apiKey.js.map +0 -1
  148. package/dist/types/betterAuth.d.ts.map +0 -1
  149. package/dist/types/betterAuth.js.map +0 -1
  150. package/dist/utils/access.d.ts.map +0 -1
  151. package/dist/utils/access.js.map +0 -1
  152. package/dist/utils/apiKeyAccess.d.ts.map +0 -1
  153. package/dist/utils/apiKeyAccess.js.map +0 -1
  154. package/dist/utils/betterAuthDefaults.d.ts.map +0 -1
  155. package/dist/utils/betterAuthDefaults.js.map +0 -1
  156. package/dist/utils/detectAuthConfig.d.ts.map +0 -1
  157. package/dist/utils/detectAuthConfig.js.map +0 -1
  158. package/dist/utils/detectEnabledPlugins.d.ts.map +0 -1
  159. package/dist/utils/detectEnabledPlugins.js.map +0 -1
  160. package/dist/utils/firstUserAdmin.d.ts.map +0 -1
  161. package/dist/utils/firstUserAdmin.js.map +0 -1
  162. package/dist/utils/generateScopes.d.ts.map +0 -1
  163. package/dist/utils/generateScopes.js.map +0 -1
  164. package/dist/utils/session.d.ts.map +0 -1
  165. package/dist/utils/session.js.map +0 -1
  166. package/src/adapter/collections.ts +0 -621
  167. package/src/adapter/index.ts +0 -712
  168. package/src/components/BeforeLogin.tsx +0 -39
  169. package/src/components/LoginView.tsx +0 -1516
  170. package/src/components/LoginViewWrapper.tsx +0 -35
  171. package/src/components/LogoutButton.tsx +0 -58
  172. package/src/components/PasskeyRegisterButton.tsx +0 -105
  173. package/src/components/PasskeySignInButton.tsx +0 -96
  174. package/src/components/auth/ForgotPasswordView.tsx +0 -274
  175. package/src/components/auth/ResetPasswordView.tsx +0 -331
  176. package/src/components/auth/index.ts +0 -8
  177. package/src/components/management/ApiKeysManagementClient.tsx +0 -988
  178. package/src/components/management/PasskeysManagementClient.tsx +0 -409
  179. package/src/components/management/SecurityNavLinks.tsx +0 -117
  180. package/src/components/management/TwoFactorManagementClient.tsx +0 -560
  181. package/src/components/management/index.ts +0 -20
  182. package/src/components/management/views/ApiKeysView.tsx +0 -57
  183. package/src/components/management/views/PasskeysView.tsx +0 -42
  184. package/src/components/management/views/TwoFactorView.tsx +0 -42
  185. package/src/components/management/views/index.ts +0 -10
  186. package/src/components/twoFactor/TwoFactorSetupView.tsx +0 -515
  187. package/src/components/twoFactor/TwoFactorVerifyView.tsx +0 -238
  188. package/src/components/twoFactor/index.ts +0 -8
  189. package/src/exports/client.ts +0 -77
  190. package/src/exports/components.ts +0 -30
  191. package/src/exports/management.ts +0 -25
  192. package/src/exports/rsc.ts +0 -11
  193. package/src/generated-types.ts +0 -269
  194. package/src/index.ts +0 -135
  195. package/src/plugin/index.ts +0 -834
  196. package/src/scripts/generate-types.ts +0 -269
  197. package/src/types/apiKey.ts +0 -63
  198. package/src/types/betterAuth.ts +0 -253
  199. package/src/utils/access.ts +0 -410
  200. package/src/utils/apiKeyAccess.ts +0 -443
  201. package/src/utils/betterAuthDefaults.ts +0 -102
  202. package/src/utils/detectAuthConfig.ts +0 -47
  203. package/src/utils/detectEnabledPlugins.ts +0 -69
  204. package/src/utils/firstUserAdmin.ts +0 -164
  205. package/src/utils/generateScopes.ts +0 -150
  206. 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&apos;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&apos;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&apos;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&apos;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