@rebasepro/auth 0.1.2 → 0.2.3
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 +21 -6
- package/dist/api.d.ts +10 -8
- package/dist/index.d.ts +1 -5
- package/dist/index.es.js +33 -1218
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +34 -1215
- package/dist/index.umd.js.map +1 -1
- package/dist/types.d.ts +14 -8
- package/package.json +15 -9
- package/src/api.ts +31 -22
- package/src/hooks/useBackendUserManagement.ts +10 -1
- package/src/hooks/useRebaseAuthController.ts +13 -17
- package/src/index.ts +1 -8
- package/src/types.ts +7 -5
- package/dist/components/AdminViews.d.ts +0 -22
- package/dist/components/RebaseAuth.d.ts +0 -6
- package/dist/components/RebaseLoginView.d.ts +0 -73
- package/src/components/AdminViews.tsx +0 -825
- package/src/components/RebaseAuth.tsx +0 -23
- package/src/components/RebaseLoginView.tsx +0 -724
|
@@ -1,724 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
import React, { ReactNode, useEffect, useRef, useState } from "react";
|
|
3
|
-
|
|
4
|
-
import { Button, CircularProgress, cls, IconButton, LoadingButton, Menu, MenuItem, TextField, Typography, iconSize } from "@rebasepro/ui";
|
|
5
|
-
import { ArrowLeftIcon, MailIcon, MoonIcon, SunIcon, SunMoonIcon } from "lucide-react";
|
|
6
|
-
import { ErrorView, LanguageToggle, RebaseLogo, useModeController, useTranslation } from "@rebasepro/core";
|
|
7
|
-
|
|
8
|
-
import { RebaseAuthController } from "../types";
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Props for RebaseLoginView
|
|
12
|
-
*/
|
|
13
|
-
export interface RebaseLoginViewProps {
|
|
14
|
-
/**
|
|
15
|
-
* Auth controller from useRebaseAuthController
|
|
16
|
-
*/
|
|
17
|
-
authController: RebaseAuthController;
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Path to the logo displayed in the login screen
|
|
21
|
-
*/
|
|
22
|
-
logo?: string;
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Enable the skip login button
|
|
26
|
-
*/
|
|
27
|
-
allowSkipLogin?: boolean;
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Disable the login buttons
|
|
31
|
-
*/
|
|
32
|
-
disabled?: boolean;
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Prevent users from creating new accounts
|
|
36
|
-
*/
|
|
37
|
-
disableSignupScreen?: boolean;
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Display this component when no user is found
|
|
41
|
-
*/
|
|
42
|
-
noUserComponent?: ReactNode;
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Display this component below the sign-in buttons
|
|
46
|
-
*/
|
|
47
|
-
additionalComponent?: ReactNode;
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Error message when user is not allowed access
|
|
51
|
-
*/
|
|
52
|
-
notAllowedError?: string | Error;
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Enable Google login button (requires googleClientId in hook)
|
|
56
|
-
*/
|
|
57
|
-
googleEnabled?: boolean;
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Google client ID for OAuth
|
|
61
|
-
*/
|
|
62
|
-
googleClientId?: string;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
type AuthMode = "buttons" | "login" | "register" | "forgot";
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Login view component for custom JWT authentication
|
|
69
|
-
*/
|
|
70
|
-
export function RebaseLoginView({
|
|
71
|
-
logo,
|
|
72
|
-
authController,
|
|
73
|
-
noUserComponent,
|
|
74
|
-
disableSignupScreen = false,
|
|
75
|
-
disabled = false,
|
|
76
|
-
notAllowedError,
|
|
77
|
-
googleEnabled = false,
|
|
78
|
-
googleClientId
|
|
79
|
-
}: RebaseLoginViewProps) {
|
|
80
|
-
|
|
81
|
-
const modeState = useModeController();
|
|
82
|
-
const { mode: colorMode, setMode: setColorMode } = modeState;
|
|
83
|
-
const { t } = useTranslation();
|
|
84
|
-
|
|
85
|
-
const [mode, setMode] = useState<AuthMode>("buttons");
|
|
86
|
-
const [fadeIn, setFadeIn] = useState(false);
|
|
87
|
-
const [viewVisible, setViewVisible] = useState(true);
|
|
88
|
-
|
|
89
|
-
const switchMode = (newMode: AuthMode) => {
|
|
90
|
-
setViewVisible(false);
|
|
91
|
-
setTimeout(() => {
|
|
92
|
-
setMode(newMode);
|
|
93
|
-
setViewVisible(true);
|
|
94
|
-
}, 150);
|
|
95
|
-
};
|
|
96
|
-
|
|
97
|
-
// Auto-show setup form when no users exist (bootstrap mode)
|
|
98
|
-
const isBootstrapMode = authController.needsSetup;
|
|
99
|
-
|
|
100
|
-
useEffect(() => {
|
|
101
|
-
const timer = setTimeout(() => setFadeIn(true), 50);
|
|
102
|
-
return () => clearTimeout(timer);
|
|
103
|
-
}, []);
|
|
104
|
-
|
|
105
|
-
function buildErrorView() {
|
|
106
|
-
if (!authController.authProviderError) return null;
|
|
107
|
-
if (authController.user != null) return null;
|
|
108
|
-
return (
|
|
109
|
-
<div className="w-full">
|
|
110
|
-
<ErrorView error={authController.authProviderError.message ?? authController.authProviderError}/>
|
|
111
|
-
</div>
|
|
112
|
-
);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
let logoComponent;
|
|
116
|
-
if (logo) {
|
|
117
|
-
logoComponent = <img src={logo}
|
|
118
|
-
style={{
|
|
119
|
-
height: "100%",
|
|
120
|
-
width: "100%",
|
|
121
|
-
objectFit: "cover"
|
|
122
|
-
}}
|
|
123
|
-
alt={"Logo"}/>;
|
|
124
|
-
} else {
|
|
125
|
-
logoComponent = <RebaseLogo/>;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
let notAllowedMessage: string | undefined;
|
|
129
|
-
if (notAllowedError) {
|
|
130
|
-
if (typeof notAllowedError === "string") {
|
|
131
|
-
notAllowedMessage = notAllowedError;
|
|
132
|
-
} else if (notAllowedError instanceof Error) {
|
|
133
|
-
notAllowedMessage = notAllowedError.message;
|
|
134
|
-
} else {
|
|
135
|
-
notAllowedMessage = "It looks like you don't have access to the CMS, based on the specified Authenticator configuration";
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
const showRegistration = !disableSignupScreen && authController.registrationEnabled;
|
|
140
|
-
|
|
141
|
-
return (
|
|
142
|
-
<div
|
|
143
|
-
className={cls(
|
|
144
|
-
"relative flex items-center justify-center h-screen w-screen p-4 transition-opacity duration-500 bg-white dark:bg-surface-900",
|
|
145
|
-
fadeIn ? "opacity-100" : "opacity-0"
|
|
146
|
-
)}>
|
|
147
|
-
|
|
148
|
-
{/* Top-right controls */}
|
|
149
|
-
<div className="absolute top-4 right-4 flex items-center gap-1 z-10">
|
|
150
|
-
<LanguageToggle/>
|
|
151
|
-
<Menu
|
|
152
|
-
trigger={<IconButton
|
|
153
|
-
color="inherit"
|
|
154
|
-
aria-label="Toggle theme">
|
|
155
|
-
{colorMode === "dark"
|
|
156
|
-
? <MoonIcon size={iconSize.small}/>
|
|
157
|
-
: <SunIcon size={iconSize.small}/>}
|
|
158
|
-
</IconButton>}>
|
|
159
|
-
<MenuItem onClick={() => setColorMode("dark")}><MoonIcon size={iconSize.smallest}/> {t("dark_mode")}</MenuItem>
|
|
160
|
-
<MenuItem onClick={() => setColorMode("light")}><SunIcon size={iconSize.smallest}/> {t("light_mode")}</MenuItem>
|
|
161
|
-
<MenuItem onClick={() => setColorMode("system")}><SunMoonIcon size={iconSize.smallest}/> {t("system_mode")}</MenuItem>
|
|
162
|
-
</Menu>
|
|
163
|
-
</div>
|
|
164
|
-
|
|
165
|
-
<div className="flex flex-col items-center w-[480px] max-w-full p-8 sm:p-10">
|
|
166
|
-
{/* Logo */}
|
|
167
|
-
<div className="w-32 h-32 m-2 mb-6">
|
|
168
|
-
{logoComponent}
|
|
169
|
-
</div>
|
|
170
|
-
|
|
171
|
-
{notAllowedMessage && (
|
|
172
|
-
<div className="p-4 w-full">
|
|
173
|
-
<ErrorView error={notAllowedMessage}/>
|
|
174
|
-
</div>
|
|
175
|
-
)}
|
|
176
|
-
|
|
177
|
-
{mode !== "forgot" && buildErrorView()}
|
|
178
|
-
|
|
179
|
-
<div className={cls(
|
|
180
|
-
"w-full transition-opacity duration-150",
|
|
181
|
-
viewVisible ? "opacity-100" : "opacity-0"
|
|
182
|
-
)}>
|
|
183
|
-
{/* Bootstrap mode: show setup form directly */}
|
|
184
|
-
{isBootstrapMode && !authController.user && (
|
|
185
|
-
<LoginForm
|
|
186
|
-
authController={authController}
|
|
187
|
-
registrationMode={true}
|
|
188
|
-
onClose={() => {}}
|
|
189
|
-
onForgotPassword={() => {}}
|
|
190
|
-
noUserComponent={noUserComponent}
|
|
191
|
-
disableSignupScreen={false}
|
|
192
|
-
bootstrapMode={true}
|
|
193
|
-
/>
|
|
194
|
-
)}
|
|
195
|
-
|
|
196
|
-
{/* Normal mode */}
|
|
197
|
-
{!isBootstrapMode && (
|
|
198
|
-
<>
|
|
199
|
-
{/* Provider buttons screen */}
|
|
200
|
-
{mode === "buttons" && (
|
|
201
|
-
<div className="w-full flex flex-col gap-3 mt-2">
|
|
202
|
-
<LoginButton
|
|
203
|
-
disabled={disabled}
|
|
204
|
-
text={"Sign in with email"}
|
|
205
|
-
icon={<MailIcon/>}
|
|
206
|
-
onClick={() => switchMode("login")}
|
|
207
|
-
/>
|
|
208
|
-
{googleEnabled && googleClientId && (
|
|
209
|
-
<GoogleLoginButton
|
|
210
|
-
disabled={disabled}
|
|
211
|
-
googleClientId={googleClientId}
|
|
212
|
-
authController={authController}
|
|
213
|
-
/>
|
|
214
|
-
)}
|
|
215
|
-
{showRegistration && (
|
|
216
|
-
<div className="mt-2 text-center">
|
|
217
|
-
<Typography variant="body2" color="secondary">
|
|
218
|
-
Don't have an account?{" "}
|
|
219
|
-
<button
|
|
220
|
-
type="button"
|
|
221
|
-
className={cls(
|
|
222
|
-
"font-semibold hover:underline cursor-pointer",
|
|
223
|
-
"text-primary-600 dark:text-primary-400"
|
|
224
|
-
)}
|
|
225
|
-
onClick={() => switchMode("register")}
|
|
226
|
-
>
|
|
227
|
-
Create one
|
|
228
|
-
</button>
|
|
229
|
-
</Typography>
|
|
230
|
-
</div>
|
|
231
|
-
)}
|
|
232
|
-
</div>
|
|
233
|
-
)}
|
|
234
|
-
|
|
235
|
-
{/* Login form */}
|
|
236
|
-
{mode === "login" && (
|
|
237
|
-
<LoginForm
|
|
238
|
-
authController={authController}
|
|
239
|
-
registrationMode={false}
|
|
240
|
-
onClose={() => switchMode("buttons")}
|
|
241
|
-
onForgotPassword={() => switchMode("forgot")}
|
|
242
|
-
noUserComponent={noUserComponent}
|
|
243
|
-
disableSignupScreen={disableSignupScreen}
|
|
244
|
-
switchToRegister={showRegistration ? () => switchMode("register") : undefined}
|
|
245
|
-
/>
|
|
246
|
-
)}
|
|
247
|
-
|
|
248
|
-
{/* Registration form */}
|
|
249
|
-
{mode === "register" && (
|
|
250
|
-
<LoginForm
|
|
251
|
-
authController={authController}
|
|
252
|
-
registrationMode={true}
|
|
253
|
-
onClose={() => switchMode("buttons")}
|
|
254
|
-
onForgotPassword={() => switchMode("forgot")}
|
|
255
|
-
noUserComponent={noUserComponent}
|
|
256
|
-
disableSignupScreen={disableSignupScreen}
|
|
257
|
-
switchToLogin={() => switchMode("login")}
|
|
258
|
-
/>
|
|
259
|
-
)}
|
|
260
|
-
|
|
261
|
-
{/* Forgot password form */}
|
|
262
|
-
{mode === "forgot" && (
|
|
263
|
-
<ForgotPasswordForm
|
|
264
|
-
authController={authController}
|
|
265
|
-
onClose={() => switchMode("login")}
|
|
266
|
-
/>
|
|
267
|
-
)}
|
|
268
|
-
</>
|
|
269
|
-
)}
|
|
270
|
-
</div>
|
|
271
|
-
</div>
|
|
272
|
-
</div>
|
|
273
|
-
);
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
function LoginButton({
|
|
277
|
-
icon,
|
|
278
|
-
onClick,
|
|
279
|
-
text,
|
|
280
|
-
disabled
|
|
281
|
-
}: { icon: React.ReactNode, onClick: () => void, text: string, disabled?: boolean }) {
|
|
282
|
-
return (
|
|
283
|
-
<Button
|
|
284
|
-
disabled={disabled}
|
|
285
|
-
className="w-full"
|
|
286
|
-
variant="outlined"
|
|
287
|
-
size="large"
|
|
288
|
-
onClick={onClick}>
|
|
289
|
-
<div className="flex items-center justify-center w-full gap-3 py-1">
|
|
290
|
-
<span className="flex items-center justify-center w-5 h-5">
|
|
291
|
-
{icon}
|
|
292
|
-
</span>
|
|
293
|
-
<Typography variant="button">{text}</Typography>
|
|
294
|
-
</div>
|
|
295
|
-
</Button>
|
|
296
|
-
);
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
const GoogleIcon = () => (
|
|
300
|
-
<svg viewBox="0 0 24 24" width="20" height="20">
|
|
301
|
-
<path fill="#4285F4"
|
|
302
|
-
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"/>
|
|
303
|
-
<path fill="#34A853"
|
|
304
|
-
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"/>
|
|
305
|
-
<path fill="#FBBC05"
|
|
306
|
-
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"/>
|
|
307
|
-
<path fill="#EA4335"
|
|
308
|
-
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"/>
|
|
309
|
-
</svg>
|
|
310
|
-
);
|
|
311
|
-
|
|
312
|
-
/** Google Identity Services SDK — injected by the GIS <script> tag. */
|
|
313
|
-
declare global {
|
|
314
|
-
interface Window {
|
|
315
|
-
google?: {
|
|
316
|
-
accounts: {
|
|
317
|
-
oauth2: {
|
|
318
|
-
initCodeClient(config: {
|
|
319
|
-
client_id: string;
|
|
320
|
-
scope: string;
|
|
321
|
-
ux_mode: "popup" | "redirect";
|
|
322
|
-
callback: (response: { code?: string; error?: string }) => void;
|
|
323
|
-
}): { requestCode(): void };
|
|
324
|
-
};
|
|
325
|
-
};
|
|
326
|
-
};
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
function GoogleLoginButton({
|
|
331
|
-
disabled,
|
|
332
|
-
googleClientId,
|
|
333
|
-
authController
|
|
334
|
-
}: {
|
|
335
|
-
disabled?: boolean,
|
|
336
|
-
googleClientId: string,
|
|
337
|
-
authController: RebaseAuthController
|
|
338
|
-
}) {
|
|
339
|
-
const codeClientRef = useRef<{ requestCode(): void } | null>(null);
|
|
340
|
-
|
|
341
|
-
useEffect(() => {
|
|
342
|
-
const google = window.google;
|
|
343
|
-
if (!google || codeClientRef.current) return;
|
|
344
|
-
|
|
345
|
-
codeClientRef.current = google.accounts.oauth2.initCodeClient({
|
|
346
|
-
client_id: googleClientId,
|
|
347
|
-
scope: "openid email profile",
|
|
348
|
-
ux_mode: "popup",
|
|
349
|
-
callback: async (response: { code?: string; error?: string }) => {
|
|
350
|
-
if (response.error || !response.code) {
|
|
351
|
-
console.error("Google login error:", response.error);
|
|
352
|
-
return;
|
|
353
|
-
}
|
|
354
|
-
try {
|
|
355
|
-
// Send the authorization code to the backend.
|
|
356
|
-
// redirectUri "postmessage" is required when using popup ux_mode.
|
|
357
|
-
await authController.googleLogin({
|
|
358
|
-
code: response.code,
|
|
359
|
-
redirectUri: "postmessage"
|
|
360
|
-
});
|
|
361
|
-
} catch (err: unknown) {
|
|
362
|
-
console.error("Google login error:", err);
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
});
|
|
366
|
-
}, [googleClientId, authController]);
|
|
367
|
-
|
|
368
|
-
const handleClick = () => {
|
|
369
|
-
if (!codeClientRef.current) {
|
|
370
|
-
console.error("Google Sign-In not loaded");
|
|
371
|
-
return;
|
|
372
|
-
}
|
|
373
|
-
codeClientRef.current.requestCode();
|
|
374
|
-
};
|
|
375
|
-
|
|
376
|
-
return (
|
|
377
|
-
<LoginButton
|
|
378
|
-
disabled={disabled}
|
|
379
|
-
text="Sign in with Google"
|
|
380
|
-
icon={<GoogleIcon/>}
|
|
381
|
-
onClick={handleClick}
|
|
382
|
-
/>
|
|
383
|
-
);
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
function LoginForm({
|
|
387
|
-
onClose,
|
|
388
|
-
onForgotPassword,
|
|
389
|
-
authController,
|
|
390
|
-
registrationMode,
|
|
391
|
-
noUserComponent,
|
|
392
|
-
disableSignupScreen,
|
|
393
|
-
bootstrapMode = false,
|
|
394
|
-
switchToRegister,
|
|
395
|
-
switchToLogin
|
|
396
|
-
}: {
|
|
397
|
-
onClose: () => void,
|
|
398
|
-
onForgotPassword: () => void,
|
|
399
|
-
authController: RebaseAuthController,
|
|
400
|
-
registrationMode: boolean,
|
|
401
|
-
noUserComponent?: ReactNode,
|
|
402
|
-
disableSignupScreen: boolean,
|
|
403
|
-
bootstrapMode?: boolean,
|
|
404
|
-
switchToRegister?: () => void,
|
|
405
|
-
switchToLogin?: () => void
|
|
406
|
-
}) {
|
|
407
|
-
const passwordRef = useRef<HTMLInputElement | null>(null);
|
|
408
|
-
|
|
409
|
-
const [email, setEmail] = useState<string>();
|
|
410
|
-
const [password, setPassword] = useState<string>();
|
|
411
|
-
const [displayName, setDisplayName] = useState<string>();
|
|
412
|
-
|
|
413
|
-
useEffect(() => {
|
|
414
|
-
if (!document) return;
|
|
415
|
-
const escFunction = (event: KeyboardEvent) => {
|
|
416
|
-
if (event.keyCode === 27) {
|
|
417
|
-
onClose();
|
|
418
|
-
}
|
|
419
|
-
};
|
|
420
|
-
document.addEventListener("keydown", escFunction, false);
|
|
421
|
-
return () => {
|
|
422
|
-
document.removeEventListener("keydown", escFunction, false);
|
|
423
|
-
};
|
|
424
|
-
}, [onClose]);
|
|
425
|
-
|
|
426
|
-
function handleEnterPassword() {
|
|
427
|
-
if (email && password) {
|
|
428
|
-
authController.emailPasswordLogin(email, password);
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
function handleRegistration() {
|
|
433
|
-
if (email && password) {
|
|
434
|
-
authController.register(email, password, displayName);
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
const handleSubmit = (event: React.FormEvent) => {
|
|
439
|
-
event.preventDefault();
|
|
440
|
-
if (registrationMode)
|
|
441
|
-
handleRegistration();
|
|
442
|
-
else
|
|
443
|
-
handleEnterPassword();
|
|
444
|
-
};
|
|
445
|
-
|
|
446
|
-
const title = bootstrapMode
|
|
447
|
-
? "Welcome!"
|
|
448
|
-
: registrationMode
|
|
449
|
-
? "Create account"
|
|
450
|
-
: "Sign in";
|
|
451
|
-
|
|
452
|
-
const subtitle = bootstrapMode
|
|
453
|
-
? "Create your admin account to get started. This account will have admin privileges."
|
|
454
|
-
: registrationMode
|
|
455
|
-
? "Fill in your details to create a new account"
|
|
456
|
-
: "Enter your credentials to continue";
|
|
457
|
-
|
|
458
|
-
const buttonLabel = registrationMode ? "Create account" : "Sign in";
|
|
459
|
-
|
|
460
|
-
return (
|
|
461
|
-
<form onSubmit={handleSubmit} className="flex flex-col w-full gap-1 mt-2">
|
|
462
|
-
{!bootstrapMode && (
|
|
463
|
-
<div className="w-full mb-2 -ml-2.5">
|
|
464
|
-
<IconButton onClick={onClose}>
|
|
465
|
-
<ArrowLeftIcon/>
|
|
466
|
-
</IconButton>
|
|
467
|
-
</div>
|
|
468
|
-
)}
|
|
469
|
-
|
|
470
|
-
<Typography variant="h6" className="mb-0.5">
|
|
471
|
-
{title}
|
|
472
|
-
</Typography>
|
|
473
|
-
<Typography variant="body2" color="secondary" className="mb-5">
|
|
474
|
-
{subtitle}
|
|
475
|
-
</Typography>
|
|
476
|
-
|
|
477
|
-
{registrationMode && noUserComponent && (
|
|
478
|
-
<div className="w-full mb-2">
|
|
479
|
-
{noUserComponent}
|
|
480
|
-
</div>
|
|
481
|
-
)}
|
|
482
|
-
|
|
483
|
-
{registrationMode && (
|
|
484
|
-
<div className="w-full mb-3">
|
|
485
|
-
<Typography variant="label" color="secondary" className="mb-1">
|
|
486
|
-
Display Name
|
|
487
|
-
</Typography>
|
|
488
|
-
<TextField placeholder="Jane Doe (optional)"
|
|
489
|
-
className="w-full"
|
|
490
|
-
value={displayName ?? ""}
|
|
491
|
-
disabled={authController.initialLoading}
|
|
492
|
-
type="text"
|
|
493
|
-
size="medium"
|
|
494
|
-
onChange={(event) => setDisplayName(event.target.value)}/>
|
|
495
|
-
</div>
|
|
496
|
-
)}
|
|
497
|
-
|
|
498
|
-
<div className="w-full mb-3">
|
|
499
|
-
<Typography variant="label" color="secondary" className="mb-1">
|
|
500
|
-
Email
|
|
501
|
-
</Typography>
|
|
502
|
-
<TextField placeholder="you@example.com"
|
|
503
|
-
className="w-full"
|
|
504
|
-
autoFocus
|
|
505
|
-
value={email ?? ""}
|
|
506
|
-
disabled={authController.initialLoading}
|
|
507
|
-
type="email"
|
|
508
|
-
size="medium"
|
|
509
|
-
onChange={(event) => setEmail(event.target.value)}/>
|
|
510
|
-
</div>
|
|
511
|
-
|
|
512
|
-
<div className="w-full mb-1">
|
|
513
|
-
<Typography variant="label" color="secondary" className="mb-1">
|
|
514
|
-
Password
|
|
515
|
-
</Typography>
|
|
516
|
-
<TextField placeholder="••••••••"
|
|
517
|
-
className="w-full"
|
|
518
|
-
value={password ?? ""}
|
|
519
|
-
disabled={authController.initialLoading}
|
|
520
|
-
inputRef={passwordRef}
|
|
521
|
-
type="password"
|
|
522
|
-
size="medium"
|
|
523
|
-
onChange={(event) => setPassword(event.target.value)}/>
|
|
524
|
-
</div>
|
|
525
|
-
|
|
526
|
-
{registrationMode && (
|
|
527
|
-
<Typography variant="caption" color="secondary" className="mb-3">
|
|
528
|
-
Password must be 8+ characters with uppercase, lowercase, and a number
|
|
529
|
-
</Typography>
|
|
530
|
-
)}
|
|
531
|
-
|
|
532
|
-
{!registrationMode && (
|
|
533
|
-
<div className="w-full text-right mb-3">
|
|
534
|
-
<button
|
|
535
|
-
type="button"
|
|
536
|
-
className={cls(
|
|
537
|
-
"text-xs font-medium hover:underline cursor-pointer",
|
|
538
|
-
"text-primary-600 dark:text-primary-400"
|
|
539
|
-
)}
|
|
540
|
-
onClick={onForgotPassword}
|
|
541
|
-
>
|
|
542
|
-
Forgot password?
|
|
543
|
-
</button>
|
|
544
|
-
</div>
|
|
545
|
-
)}
|
|
546
|
-
|
|
547
|
-
<LoadingButton
|
|
548
|
-
type="submit"
|
|
549
|
-
variant="filled"
|
|
550
|
-
color="primary"
|
|
551
|
-
className="w-full mt-1"
|
|
552
|
-
size="large"
|
|
553
|
-
loading={authController.authLoading}
|
|
554
|
-
disabled={authController.authLoading || !email || !password}
|
|
555
|
-
>
|
|
556
|
-
{buttonLabel}
|
|
557
|
-
</LoadingButton>
|
|
558
|
-
|
|
559
|
-
{/* Switch between login/register */}
|
|
560
|
-
{switchToRegister && (
|
|
561
|
-
<div className="mt-4 text-center">
|
|
562
|
-
<Typography variant="body2" color="secondary">
|
|
563
|
-
Don't have an account?{" "}
|
|
564
|
-
<button
|
|
565
|
-
type="button"
|
|
566
|
-
className={cls(
|
|
567
|
-
"font-semibold hover:underline cursor-pointer",
|
|
568
|
-
"text-primary-600 dark:text-primary-400"
|
|
569
|
-
)}
|
|
570
|
-
onClick={switchToRegister}
|
|
571
|
-
>
|
|
572
|
-
Create one
|
|
573
|
-
</button>
|
|
574
|
-
</Typography>
|
|
575
|
-
</div>
|
|
576
|
-
)}
|
|
577
|
-
|
|
578
|
-
{switchToLogin && (
|
|
579
|
-
<div className="mt-4 text-center">
|
|
580
|
-
<Typography variant="body2" color="secondary">
|
|
581
|
-
Already have an account?{" "}
|
|
582
|
-
<button
|
|
583
|
-
type="button"
|
|
584
|
-
className={cls(
|
|
585
|
-
"font-semibold hover:underline cursor-pointer",
|
|
586
|
-
"text-primary-600 dark:text-primary-400"
|
|
587
|
-
)}
|
|
588
|
-
onClick={switchToLogin}
|
|
589
|
-
>
|
|
590
|
-
Sign in
|
|
591
|
-
</button>
|
|
592
|
-
</Typography>
|
|
593
|
-
</div>
|
|
594
|
-
)}
|
|
595
|
-
</form>
|
|
596
|
-
);
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
function ForgotPasswordForm({
|
|
600
|
-
onClose,
|
|
601
|
-
authController
|
|
602
|
-
}: {
|
|
603
|
-
onClose: () => void,
|
|
604
|
-
authController: RebaseAuthController
|
|
605
|
-
}) {
|
|
606
|
-
const [email, setEmail] = useState<string>("");
|
|
607
|
-
const [submitted, setSubmitted] = useState(false);
|
|
608
|
-
const [error, setError] = useState<string | null>(null);
|
|
609
|
-
|
|
610
|
-
useEffect(() => {
|
|
611
|
-
if (!document) return;
|
|
612
|
-
const escFunction = (event: KeyboardEvent) => {
|
|
613
|
-
if (event.keyCode === 27) {
|
|
614
|
-
onClose();
|
|
615
|
-
}
|
|
616
|
-
};
|
|
617
|
-
document.addEventListener("keydown", escFunction, false);
|
|
618
|
-
return () => {
|
|
619
|
-
document.removeEventListener("keydown", escFunction, false);
|
|
620
|
-
};
|
|
621
|
-
}, [onClose]);
|
|
622
|
-
|
|
623
|
-
const handleSubmit = async (event: React.FormEvent) => {
|
|
624
|
-
event.preventDefault();
|
|
625
|
-
setError(null);
|
|
626
|
-
|
|
627
|
-
if (!email) {
|
|
628
|
-
setError("Please enter your email address");
|
|
629
|
-
return;
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
try {
|
|
633
|
-
await authController.forgotPassword(email);
|
|
634
|
-
setSubmitted(true);
|
|
635
|
-
} catch (err: unknown) {
|
|
636
|
-
// Check for EMAIL_NOT_CONFIGURED error
|
|
637
|
-
if (err instanceof Error && (err as { code?: string }).code === "EMAIL_NOT_CONFIGURED") {
|
|
638
|
-
setError("Password reset is not available. Please contact your administrator.");
|
|
639
|
-
} else {
|
|
640
|
-
// Still show success (security: don't reveal if email exists)
|
|
641
|
-
setSubmitted(true);
|
|
642
|
-
}
|
|
643
|
-
}
|
|
644
|
-
};
|
|
645
|
-
|
|
646
|
-
if (submitted) {
|
|
647
|
-
return (
|
|
648
|
-
<div className="flex flex-col w-full gap-4 mt-2">
|
|
649
|
-
<div className="w-full -ml-2.5">
|
|
650
|
-
<IconButton onClick={onClose}>
|
|
651
|
-
<ArrowLeftIcon/>
|
|
652
|
-
</IconButton>
|
|
653
|
-
</div>
|
|
654
|
-
|
|
655
|
-
<div className={cls(
|
|
656
|
-
"text-center rounded-xl p-6",
|
|
657
|
-
"bg-surface-50 dark:bg-surface-950"
|
|
658
|
-
)}>
|
|
659
|
-
<div className="text-3xl mb-3">📧</div>
|
|
660
|
-
<Typography variant="subtitle1" className="mb-2">
|
|
661
|
-
Check your email
|
|
662
|
-
</Typography>
|
|
663
|
-
<Typography variant="body2" color="secondary">
|
|
664
|
-
If an account exists for <strong>{email}</strong>, you'll receive a password reset link shortly.
|
|
665
|
-
</Typography>
|
|
666
|
-
</div>
|
|
667
|
-
|
|
668
|
-
<Button onClick={onClose} variant="text" className="mt-2">
|
|
669
|
-
Back to sign in
|
|
670
|
-
</Button>
|
|
671
|
-
</div>
|
|
672
|
-
);
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
return (
|
|
676
|
-
<form onSubmit={handleSubmit} className="flex flex-col w-full gap-1 mt-2">
|
|
677
|
-
<div className="w-full mb-2 -ml-2.5">
|
|
678
|
-
<IconButton onClick={onClose}>
|
|
679
|
-
<ArrowLeftIcon/>
|
|
680
|
-
</IconButton>
|
|
681
|
-
</div>
|
|
682
|
-
|
|
683
|
-
<Typography variant="h6" className="mb-0.5">
|
|
684
|
-
Reset password
|
|
685
|
-
</Typography>
|
|
686
|
-
<Typography variant="body2" color="secondary" className="mb-5">
|
|
687
|
-
Enter your email and we'll send you a reset link.
|
|
688
|
-
</Typography>
|
|
689
|
-
|
|
690
|
-
{error && (
|
|
691
|
-
<div className="w-full mb-3">
|
|
692
|
-
<ErrorView error={error}/>
|
|
693
|
-
</div>
|
|
694
|
-
)}
|
|
695
|
-
|
|
696
|
-
<div className="w-full mb-3">
|
|
697
|
-
<Typography variant="label" color="secondary" className="mb-1">
|
|
698
|
-
Email
|
|
699
|
-
</Typography>
|
|
700
|
-
<TextField
|
|
701
|
-
placeholder="you@example.com"
|
|
702
|
-
className="w-full"
|
|
703
|
-
autoFocus
|
|
704
|
-
value={email}
|
|
705
|
-
type="email"
|
|
706
|
-
size="medium"
|
|
707
|
-
onChange={(event) => setEmail(event.target.value)}
|
|
708
|
-
/>
|
|
709
|
-
</div>
|
|
710
|
-
|
|
711
|
-
<LoadingButton
|
|
712
|
-
type="submit"
|
|
713
|
-
variant="filled"
|
|
714
|
-
color="primary"
|
|
715
|
-
className="w-full"
|
|
716
|
-
size="large"
|
|
717
|
-
loading={authController.authLoading}
|
|
718
|
-
disabled={authController.authLoading || !email}
|
|
719
|
-
>
|
|
720
|
-
Send reset link
|
|
721
|
-
</LoadingButton>
|
|
722
|
-
</form>
|
|
723
|
-
);
|
|
724
|
-
}
|