@rebasepro/auth 0.0.1-canary.0
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/LICENSE +6 -0
- package/dist/api.d.ts +119 -0
- package/dist/components/AdminViews.d.ts +20 -0
- package/dist/components/RebaseLoginView.d.ts +52 -0
- package/dist/hooks/useBackendUserManagement.d.ts +41 -0
- package/dist/hooks/useRebaseAuthController.d.ts +9 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.es.js +1883 -0
- package/dist/index.es.js.map +1 -0
- package/dist/index.umd.js +1883 -0
- package/dist/index.umd.js.map +1 -0
- package/dist/types.d.ts +95 -0
- package/package.json +48 -0
- package/src/api.ts +328 -0
- package/src/components/AdminViews.tsx +795 -0
- package/src/components/RebaseLoginView.tsx +570 -0
- package/src/hooks/useBackendUserManagement.ts +407 -0
- package/src/hooks/useRebaseAuthController.ts +692 -0
- package/src/index.ts +28 -0
- package/src/types.ts +102 -0
|
@@ -0,0 +1,570 @@
|
|
|
1
|
+
import React, { ReactNode, useEffect, useRef, useState } from "react";
|
|
2
|
+
|
|
3
|
+
import { ArrowBackIcon, Button, CircularProgress, IconButton, MailIcon, TextField, Typography } from "@rebasepro/ui";
|
|
4
|
+
import { ErrorView, RebaseLogo, useModeController } from "@rebasepro/core";
|
|
5
|
+
|
|
6
|
+
import { RebaseAuthController } from "../types";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Props for RebaseLoginView
|
|
10
|
+
*/
|
|
11
|
+
export interface RebaseLoginViewProps {
|
|
12
|
+
/**
|
|
13
|
+
* Auth controller from useRebaseAuthController
|
|
14
|
+
*/
|
|
15
|
+
authController: RebaseAuthController;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Path to the logo displayed in the login screen
|
|
19
|
+
*/
|
|
20
|
+
logo?: string;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Enable the skip login button
|
|
24
|
+
*/
|
|
25
|
+
allowSkipLogin?: boolean;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Disable the login buttons
|
|
29
|
+
*/
|
|
30
|
+
disabled?: boolean;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Prevent users from creating new accounts
|
|
34
|
+
*/
|
|
35
|
+
disableSignupScreen?: boolean;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Display this component when no user is found
|
|
39
|
+
*/
|
|
40
|
+
noUserComponent?: ReactNode;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Display this component below the sign-in buttons
|
|
44
|
+
*/
|
|
45
|
+
additionalComponent?: ReactNode;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Error message when user is not allowed access
|
|
49
|
+
*/
|
|
50
|
+
notAllowedError?: string | Error;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Enable Google login button (requires googleClientId in hook)
|
|
54
|
+
*/
|
|
55
|
+
googleEnabled?: boolean;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Google client ID for OAuth
|
|
59
|
+
*/
|
|
60
|
+
googleClientId?: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Login view component for custom JWT authentication
|
|
65
|
+
* Based on MongoLoginView pattern from @rebasepro/mongodb
|
|
66
|
+
*/
|
|
67
|
+
export function RebaseLoginView({
|
|
68
|
+
logo,
|
|
69
|
+
authController,
|
|
70
|
+
noUserComponent,
|
|
71
|
+
disableSignupScreen = false,
|
|
72
|
+
disabled = false,
|
|
73
|
+
notAllowedError,
|
|
74
|
+
googleEnabled = false,
|
|
75
|
+
googleClientId
|
|
76
|
+
}: RebaseLoginViewProps) {
|
|
77
|
+
|
|
78
|
+
const modeState = useModeController();
|
|
79
|
+
|
|
80
|
+
const [registrationSelected, setRegistrationSelected] = useState(false);
|
|
81
|
+
const [passwordLoginSelected, setPasswordLoginSelected] = useState(false);
|
|
82
|
+
const [forgotPasswordSelected, setForgotPasswordSelected] = useState(false);
|
|
83
|
+
|
|
84
|
+
// Auto-show setup form when no users exist (bootstrap mode)
|
|
85
|
+
const isBootstrapMode = authController.needsSetup;
|
|
86
|
+
|
|
87
|
+
function buildErrorView() {
|
|
88
|
+
if (!authController.authProviderError) return null;
|
|
89
|
+
if (authController.user != null) return null;
|
|
90
|
+
return <ErrorView error={authController.authProviderError.message ?? authController.authProviderError} />;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
let logoComponent;
|
|
94
|
+
if (logo) {
|
|
95
|
+
logoComponent = <img src={logo}
|
|
96
|
+
style={{
|
|
97
|
+
height: "100%",
|
|
98
|
+
width: "100%",
|
|
99
|
+
objectFit: "cover"
|
|
100
|
+
}}
|
|
101
|
+
alt={"Logo"} />;
|
|
102
|
+
} else {
|
|
103
|
+
logoComponent = <RebaseLogo />;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
let notAllowedMessage: string | undefined;
|
|
107
|
+
if (notAllowedError) {
|
|
108
|
+
if (typeof notAllowedError === "string") {
|
|
109
|
+
notAllowedMessage = notAllowedError;
|
|
110
|
+
} else if (notAllowedError instanceof Error) {
|
|
111
|
+
notAllowedMessage = notAllowedError.message;
|
|
112
|
+
} else {
|
|
113
|
+
notAllowedMessage = "It looks like you don't have access to the CMS, based on the specified Authenticator configuration";
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
<div className="flex flex-col justify-center items-center min-h-screen min-w-full p-2">
|
|
119
|
+
<div className="flex flex-col items-center w-full max-w-md">
|
|
120
|
+
<div className={`m-4 p-4 w-64 h-64`}>
|
|
121
|
+
{logoComponent}
|
|
122
|
+
</div>
|
|
123
|
+
{notAllowedMessage &&
|
|
124
|
+
<div className="p-4">
|
|
125
|
+
<ErrorView error={notAllowedMessage} />
|
|
126
|
+
</div>
|
|
127
|
+
}
|
|
128
|
+
{!forgotPasswordSelected && buildErrorView()}
|
|
129
|
+
|
|
130
|
+
{/* Bootstrap mode: show setup form directly */}
|
|
131
|
+
{isBootstrapMode && !authController.user && (
|
|
132
|
+
<LoginForm
|
|
133
|
+
authController={authController}
|
|
134
|
+
registrationMode={true}
|
|
135
|
+
onClose={() => {}}
|
|
136
|
+
onForgotPassword={() => {}}
|
|
137
|
+
mode={modeState.mode}
|
|
138
|
+
noUserComponent={noUserComponent}
|
|
139
|
+
disableSignupScreen={false}
|
|
140
|
+
bootstrapMode={true}
|
|
141
|
+
/>
|
|
142
|
+
)}
|
|
143
|
+
|
|
144
|
+
{/* Normal mode: show login/register buttons */}
|
|
145
|
+
{!isBootstrapMode && !passwordLoginSelected && !registrationSelected && !forgotPasswordSelected && (
|
|
146
|
+
<>
|
|
147
|
+
<LoginButton
|
|
148
|
+
disabled={disabled}
|
|
149
|
+
text={"Email/password"}
|
|
150
|
+
icon={<MailIcon />}
|
|
151
|
+
onClick={() => {
|
|
152
|
+
setRegistrationSelected(false);
|
|
153
|
+
setPasswordLoginSelected(true);
|
|
154
|
+
setForgotPasswordSelected(false);
|
|
155
|
+
}}
|
|
156
|
+
/>
|
|
157
|
+
{googleEnabled && googleClientId && (
|
|
158
|
+
<GoogleLoginButton
|
|
159
|
+
disabled={disabled}
|
|
160
|
+
googleClientId={googleClientId}
|
|
161
|
+
authController={authController}
|
|
162
|
+
/>
|
|
163
|
+
)}
|
|
164
|
+
{!disableSignupScreen && authController.registrationEnabled && (
|
|
165
|
+
<LoginButton
|
|
166
|
+
disabled={disabled}
|
|
167
|
+
text={"Create account"}
|
|
168
|
+
icon={<MailIcon />}
|
|
169
|
+
onClick={() => {
|
|
170
|
+
setRegistrationSelected(true);
|
|
171
|
+
setPasswordLoginSelected(false);
|
|
172
|
+
setForgotPasswordSelected(false);
|
|
173
|
+
}}
|
|
174
|
+
/>
|
|
175
|
+
)}
|
|
176
|
+
</>
|
|
177
|
+
)}
|
|
178
|
+
{!isBootstrapMode && (passwordLoginSelected || registrationSelected) && !forgotPasswordSelected && (
|
|
179
|
+
<LoginForm
|
|
180
|
+
authController={authController}
|
|
181
|
+
registrationMode={registrationSelected}
|
|
182
|
+
onClose={() => {
|
|
183
|
+
setRegistrationSelected(false);
|
|
184
|
+
setPasswordLoginSelected(false);
|
|
185
|
+
}}
|
|
186
|
+
onForgotPassword={() => {
|
|
187
|
+
setForgotPasswordSelected(true);
|
|
188
|
+
setPasswordLoginSelected(false);
|
|
189
|
+
}}
|
|
190
|
+
mode={modeState.mode}
|
|
191
|
+
noUserComponent={noUserComponent}
|
|
192
|
+
disableSignupScreen={disableSignupScreen}
|
|
193
|
+
/>
|
|
194
|
+
)}
|
|
195
|
+
{forgotPasswordSelected && (
|
|
196
|
+
<ForgotPasswordForm
|
|
197
|
+
authController={authController}
|
|
198
|
+
onClose={() => {
|
|
199
|
+
setForgotPasswordSelected(false);
|
|
200
|
+
setPasswordLoginSelected(true);
|
|
201
|
+
}}
|
|
202
|
+
/>
|
|
203
|
+
)}
|
|
204
|
+
</div>
|
|
205
|
+
</div>
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function LoginButton({
|
|
210
|
+
icon,
|
|
211
|
+
onClick,
|
|
212
|
+
text,
|
|
213
|
+
disabled
|
|
214
|
+
}: { icon: React.ReactNode, onClick: () => void, text: string, disabled?: boolean }) {
|
|
215
|
+
return (
|
|
216
|
+
<div className="m-2 w-full">
|
|
217
|
+
<Button
|
|
218
|
+
disabled={disabled}
|
|
219
|
+
className={`w-full`}
|
|
220
|
+
onClick={onClick}>
|
|
221
|
+
<div className="flex items-center justify-center p-2 w-full h-8">
|
|
222
|
+
<div className="flex flex-col items-center justify-center w-8">
|
|
223
|
+
{icon}
|
|
224
|
+
</div>
|
|
225
|
+
<div className="grow pl-2 text-center">
|
|
226
|
+
{text}
|
|
227
|
+
</div>
|
|
228
|
+
</div>
|
|
229
|
+
</Button>
|
|
230
|
+
</div>
|
|
231
|
+
)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function GoogleLoginButton({
|
|
235
|
+
disabled,
|
|
236
|
+
googleClientId,
|
|
237
|
+
authController
|
|
238
|
+
}: {
|
|
239
|
+
disabled?: boolean,
|
|
240
|
+
googleClientId: string,
|
|
241
|
+
authController: RebaseAuthController
|
|
242
|
+
}) {
|
|
243
|
+
const handleGoogleLogin = async () => {
|
|
244
|
+
try {
|
|
245
|
+
const google = (window as unknown as { google?: { accounts: { id: { initialize: (config: { client_id: string; callback: (response: { credential: string }) => void }) => void; prompt: () => void } } } }).google;
|
|
246
|
+
if (!google) {
|
|
247
|
+
console.error("Google Sign-In not loaded");
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
google.accounts.id.initialize({
|
|
252
|
+
client_id: googleClientId,
|
|
253
|
+
callback: async (response: { credential: string }) => {
|
|
254
|
+
try {
|
|
255
|
+
await authController.googleLogin(response.credential);
|
|
256
|
+
} catch (err: unknown) {
|
|
257
|
+
console.error("Google login error:", err);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
google.accounts.id.prompt();
|
|
263
|
+
} catch (err: unknown) {
|
|
264
|
+
console.error("Google login error:", err);
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
return (
|
|
269
|
+
<div className="m-2 w-full">
|
|
270
|
+
<Button
|
|
271
|
+
disabled={disabled}
|
|
272
|
+
className={`w-full`}
|
|
273
|
+
onClick={handleGoogleLogin}>
|
|
274
|
+
<div className="flex items-center justify-center p-2 w-full h-8">
|
|
275
|
+
<div className="flex flex-col items-center justify-center w-8">
|
|
276
|
+
<svg viewBox="0 0 24 24" width="24" height="24">
|
|
277
|
+
<path fill="currentColor"
|
|
278
|
+
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" />
|
|
279
|
+
<path fill="currentColor"
|
|
280
|
+
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" />
|
|
281
|
+
<path fill="currentColor"
|
|
282
|
+
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" />
|
|
283
|
+
<path fill="currentColor"
|
|
284
|
+
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" />
|
|
285
|
+
</svg>
|
|
286
|
+
</div>
|
|
287
|
+
<div className="grow pl-2 text-center">
|
|
288
|
+
Continue with Google
|
|
289
|
+
</div>
|
|
290
|
+
</div>
|
|
291
|
+
</Button>
|
|
292
|
+
</div>
|
|
293
|
+
)
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function LoginForm({
|
|
297
|
+
onClose,
|
|
298
|
+
onForgotPassword,
|
|
299
|
+
authController,
|
|
300
|
+
mode,
|
|
301
|
+
registrationMode,
|
|
302
|
+
noUserComponent,
|
|
303
|
+
disableSignupScreen,
|
|
304
|
+
bootstrapMode = false
|
|
305
|
+
}: {
|
|
306
|
+
onClose: () => void,
|
|
307
|
+
onForgotPassword: () => void,
|
|
308
|
+
authController: RebaseAuthController,
|
|
309
|
+
mode: "light" | "dark",
|
|
310
|
+
registrationMode: boolean,
|
|
311
|
+
noUserComponent?: ReactNode,
|
|
312
|
+
disableSignupScreen: boolean,
|
|
313
|
+
bootstrapMode?: boolean
|
|
314
|
+
}) {
|
|
315
|
+
|
|
316
|
+
const passwordRef = useRef<HTMLInputElement | null>(null);
|
|
317
|
+
|
|
318
|
+
const [email, setEmail] = useState<string>();
|
|
319
|
+
const [password, setPassword] = useState<string>();
|
|
320
|
+
const [displayName, setDisplayName] = useState<string>();
|
|
321
|
+
|
|
322
|
+
const loginMode = !registrationMode;
|
|
323
|
+
|
|
324
|
+
useEffect(() => {
|
|
325
|
+
if (!document) return;
|
|
326
|
+
const escFunction = (event: KeyboardEvent) => {
|
|
327
|
+
if (event.keyCode === 27) {
|
|
328
|
+
onClose();
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
document.addEventListener("keydown", escFunction, false);
|
|
332
|
+
return () => {
|
|
333
|
+
document.removeEventListener("keydown", escFunction, false);
|
|
334
|
+
};
|
|
335
|
+
}, [onClose]);
|
|
336
|
+
|
|
337
|
+
function handleEnterPassword() {
|
|
338
|
+
if (email && password) {
|
|
339
|
+
authController.emailPasswordLogin(email, password);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function handleRegistration() {
|
|
344
|
+
if (email && password) {
|
|
345
|
+
authController.register(email, password, displayName);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const onBackPressed = () => {
|
|
350
|
+
onClose();
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const handleSubmit = (event: React.FormEvent) => {
|
|
354
|
+
event.preventDefault();
|
|
355
|
+
if (registrationMode)
|
|
356
|
+
handleRegistration();
|
|
357
|
+
else
|
|
358
|
+
handleEnterPassword();
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const label = bootstrapMode
|
|
362
|
+
? "Welcome! Create your admin account"
|
|
363
|
+
: registrationMode
|
|
364
|
+
? "Create a new account"
|
|
365
|
+
: "Enter your email and password";
|
|
366
|
+
|
|
367
|
+
const button = registrationMode ? "Create account" : "Login";
|
|
368
|
+
|
|
369
|
+
return (
|
|
370
|
+
<form onSubmit={handleSubmit} className="flex flex-col items-center w-full max-w-[500px] gap-2">
|
|
371
|
+
{!bootstrapMode && (
|
|
372
|
+
<div className="w-full">
|
|
373
|
+
<IconButton onClick={onBackPressed}>
|
|
374
|
+
<ArrowBackIcon />
|
|
375
|
+
</IconButton>
|
|
376
|
+
</div>
|
|
377
|
+
)}
|
|
378
|
+
|
|
379
|
+
<div className="flex justify-center w-full py-2">
|
|
380
|
+
<Typography align={"center"} variant={bootstrapMode ? "subtitle1" : "subtitle2"}>{label}</Typography>
|
|
381
|
+
</div>
|
|
382
|
+
|
|
383
|
+
{bootstrapMode && (
|
|
384
|
+
<Typography variant="body2" className="text-gray-500 text-center mb-2">
|
|
385
|
+
No users found. Create the first account to get started. This account will have admin privileges.
|
|
386
|
+
</Typography>
|
|
387
|
+
)}
|
|
388
|
+
|
|
389
|
+
{registrationMode && (
|
|
390
|
+
<div className="w-full">
|
|
391
|
+
<TextField placeholder="Display Name (optional)"
|
|
392
|
+
className={"w-full"}
|
|
393
|
+
value={displayName ?? ""}
|
|
394
|
+
disabled={authController.initialLoading}
|
|
395
|
+
type="text"
|
|
396
|
+
onChange={(event) => setDisplayName(event.target.value)} />
|
|
397
|
+
</div>
|
|
398
|
+
)}
|
|
399
|
+
|
|
400
|
+
<div className="w-full">
|
|
401
|
+
<TextField placeholder="Email"
|
|
402
|
+
className={"w-full"}
|
|
403
|
+
autoFocus
|
|
404
|
+
value={email ?? ""}
|
|
405
|
+
disabled={authController.initialLoading}
|
|
406
|
+
type="email"
|
|
407
|
+
onChange={(event) => setEmail(event.target.value)} />
|
|
408
|
+
</div>
|
|
409
|
+
|
|
410
|
+
<div className="w-full">
|
|
411
|
+
{registrationMode && noUserComponent}
|
|
412
|
+
</div>
|
|
413
|
+
|
|
414
|
+
<div className="w-full">
|
|
415
|
+
<TextField placeholder="Password"
|
|
416
|
+
className={"w-full"}
|
|
417
|
+
value={password ?? ""}
|
|
418
|
+
disabled={authController.initialLoading}
|
|
419
|
+
inputRef={passwordRef}
|
|
420
|
+
type="password"
|
|
421
|
+
onChange={(event) => setPassword(event.target.value)} />
|
|
422
|
+
</div>
|
|
423
|
+
|
|
424
|
+
{registrationMode && (
|
|
425
|
+
<Typography variant="caption" className="text-gray-500 text-sm">
|
|
426
|
+
Password: 8+ chars, uppercase, lowercase, number
|
|
427
|
+
</Typography>
|
|
428
|
+
)}
|
|
429
|
+
|
|
430
|
+
{!registrationMode && (
|
|
431
|
+
<div className="w-full text-right">
|
|
432
|
+
<button
|
|
433
|
+
type="button"
|
|
434
|
+
className="text-sm text-blue-600 hover:text-blue-800 hover:underline"
|
|
435
|
+
onClick={onForgotPassword}
|
|
436
|
+
>
|
|
437
|
+
Forgot password?
|
|
438
|
+
</button>
|
|
439
|
+
</div>
|
|
440
|
+
)}
|
|
441
|
+
|
|
442
|
+
<div className="flex justify-end items-center w-full gap-2">
|
|
443
|
+
{authController.authLoading && (
|
|
444
|
+
<CircularProgress />
|
|
445
|
+
)}
|
|
446
|
+
<Button type="submit" disabled={authController.authLoading}>
|
|
447
|
+
{button}
|
|
448
|
+
</Button>
|
|
449
|
+
</div>
|
|
450
|
+
</form>
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function ForgotPasswordForm({
|
|
455
|
+
onClose,
|
|
456
|
+
authController
|
|
457
|
+
}: {
|
|
458
|
+
onClose: () => void,
|
|
459
|
+
authController: RebaseAuthController
|
|
460
|
+
}) {
|
|
461
|
+
const [email, setEmail] = useState<string>("");
|
|
462
|
+
const [submitted, setSubmitted] = useState(false);
|
|
463
|
+
const [error, setError] = useState<string | null>(null);
|
|
464
|
+
|
|
465
|
+
useEffect(() => {
|
|
466
|
+
if (!document) return;
|
|
467
|
+
const escFunction = (event: KeyboardEvent) => {
|
|
468
|
+
if (event.keyCode === 27) {
|
|
469
|
+
onClose();
|
|
470
|
+
}
|
|
471
|
+
};
|
|
472
|
+
document.addEventListener("keydown", escFunction, false);
|
|
473
|
+
return () => {
|
|
474
|
+
document.removeEventListener("keydown", escFunction, false);
|
|
475
|
+
};
|
|
476
|
+
}, [onClose]);
|
|
477
|
+
|
|
478
|
+
const handleSubmit = async (event: React.FormEvent) => {
|
|
479
|
+
event.preventDefault();
|
|
480
|
+
setError(null);
|
|
481
|
+
|
|
482
|
+
if (!email) {
|
|
483
|
+
setError("Please enter your email address");
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
try {
|
|
488
|
+
await authController.forgotPassword(email);
|
|
489
|
+
setSubmitted(true);
|
|
490
|
+
} catch (err: unknown) {
|
|
491
|
+
// Check for EMAIL_NOT_CONFIGURED error
|
|
492
|
+
if (err instanceof Error && (err as { code?: string }).code === "EMAIL_NOT_CONFIGURED") {
|
|
493
|
+
setError("Password reset is not available. Please contact your administrator.");
|
|
494
|
+
} else {
|
|
495
|
+
// Still show success (security: don't reveal if email exists)
|
|
496
|
+
setSubmitted(true);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
};
|
|
500
|
+
|
|
501
|
+
if (submitted) {
|
|
502
|
+
return (
|
|
503
|
+
<div className="flex flex-col items-center w-full max-w-[500px] gap-4 p-4">
|
|
504
|
+
<div className="w-full">
|
|
505
|
+
<IconButton onClick={onClose}>
|
|
506
|
+
<ArrowBackIcon />
|
|
507
|
+
</IconButton>
|
|
508
|
+
</div>
|
|
509
|
+
|
|
510
|
+
<div className="text-center">
|
|
511
|
+
<Typography variant="subtitle1" className="mb-4">
|
|
512
|
+
Check your email
|
|
513
|
+
</Typography>
|
|
514
|
+
<Typography variant="body2" className="text-gray-600">
|
|
515
|
+
If an account exists for <strong>{email}</strong>, you'll receive a password reset link shortly.
|
|
516
|
+
</Typography>
|
|
517
|
+
</div>
|
|
518
|
+
|
|
519
|
+
<Button onClick={onClose} className="mt-4">
|
|
520
|
+
Back to login
|
|
521
|
+
</Button>
|
|
522
|
+
</div>
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
return (
|
|
527
|
+
<form onSubmit={handleSubmit} className="flex flex-col items-center w-full max-w-[500px] gap-2">
|
|
528
|
+
<div className="w-full">
|
|
529
|
+
<IconButton onClick={onClose}>
|
|
530
|
+
<ArrowBackIcon />
|
|
531
|
+
</IconButton>
|
|
532
|
+
</div>
|
|
533
|
+
|
|
534
|
+
<div className="flex justify-center w-full py-2">
|
|
535
|
+
<Typography align={"center"} variant={"subtitle2"}>Reset your password</Typography>
|
|
536
|
+
</div>
|
|
537
|
+
|
|
538
|
+
<Typography variant="body2" className="text-gray-600 text-center mb-2">
|
|
539
|
+
Enter your email address and we'll send you a link to reset your password.
|
|
540
|
+
</Typography>
|
|
541
|
+
|
|
542
|
+
{error && (
|
|
543
|
+
<div className="w-full">
|
|
544
|
+
<ErrorView error={error} />
|
|
545
|
+
</div>
|
|
546
|
+
)}
|
|
547
|
+
|
|
548
|
+
<div className="w-full">
|
|
549
|
+
<TextField
|
|
550
|
+
placeholder="Email"
|
|
551
|
+
className={"w-full"}
|
|
552
|
+
autoFocus
|
|
553
|
+
value={email}
|
|
554
|
+
type="email"
|
|
555
|
+
onChange={(event) => setEmail(event.target.value)}
|
|
556
|
+
/>
|
|
557
|
+
</div>
|
|
558
|
+
|
|
559
|
+
<div className="flex justify-end items-center w-full gap-2 mt-2">
|
|
560
|
+
{authController.authLoading && (
|
|
561
|
+
<CircularProgress />
|
|
562
|
+
)}
|
|
563
|
+
<Button type="submit" disabled={authController.authLoading || !email}>
|
|
564
|
+
Send reset link
|
|
565
|
+
</Button>
|
|
566
|
+
</div>
|
|
567
|
+
</form>
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
|