@rebasepro/core 0.2.3 → 0.2.5
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/dist/components/LoginView/LoginView.d.ts +25 -1
- package/dist/components/common/types.d.ts +10 -7
- package/dist/components/common/useDebouncedData.d.ts +1 -1
- package/dist/core/RebaseProps.d.ts +13 -2
- package/dist/core/RebaseRouter.d.ts +1 -1
- package/dist/hooks/data/useCollectionFetch.d.ts +12 -1
- package/dist/hooks/index.d.ts +0 -1
- package/dist/index.es.js +565 -454
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +565 -454
- package/dist/index.umd.js.map +1 -1
- package/dist/util/entity_cache.d.ts +0 -5
- package/dist/util/index.d.ts +0 -2
- package/dist/util/useStorageUploadController.d.ts +2 -2
- package/package.json +6 -6
- package/src/components/BootstrapAdminBanner.tsx +12 -3
- package/src/components/LoginView/LoginView.tsx +177 -10
- package/src/components/UserSettingsView.tsx +95 -2
- package/src/components/common/types.tsx +7 -7
- package/src/components/common/useDebouncedData.ts +2 -2
- package/src/core/Rebase.tsx +3 -2
- package/src/core/RebaseProps.tsx +15 -2
- package/src/core/RebaseRouter.tsx +1 -1
- package/src/hooks/data/useCollectionFetch.tsx +27 -4
- package/src/hooks/data/useUserSelector.tsx +1 -1
- package/src/hooks/index.tsx +0 -1
- package/src/hooks/useResolvedComponent.tsx +4 -3
- package/src/locales/en.ts +13 -0
- package/src/locales/es.ts +11 -1
- package/src/util/entity_cache.ts +1 -27
- package/src/util/icon_list.ts +2 -2
- package/src/util/index.ts +2 -2
- package/src/util/previews.ts +9 -1
- package/src/util/useStorageUploadController.tsx +4 -4
- package/dist/hooks/useValidateAuthenticator.d.ts +0 -21
- package/dist/util/icon_synonyms.d.ts +0 -1
- package/dist/util/useTraceUpdate.d.ts +0 -2
- package/src/hooks/useValidateAuthenticator.tsx +0 -116
- package/src/util/icon_synonyms.ts +0 -1
- package/src/util/useTraceUpdate.tsx +0 -24
|
@@ -7,7 +7,6 @@ export declare function saveEntityToCache(path: string, data: object): void;
|
|
|
7
7
|
export declare function removeEntityFromMemoryCache(path: string): void;
|
|
8
8
|
export declare function saveEntityToMemoryCache(path: string, data: object): void;
|
|
9
9
|
export declare function getEntityFromMemoryCache(path: string): object | undefined;
|
|
10
|
-
export declare function hasEntityInCache(path: string): boolean;
|
|
11
10
|
/**
|
|
12
11
|
* Retrieves an entity from the in-memory cache or `sessionStorage`.
|
|
13
12
|
* If the entity is not in the cache but exists in `sessionStorage`, it loads it into the cache.
|
|
@@ -20,8 +19,4 @@ export declare function getEntityFromCache(path: string): object | undefined;
|
|
|
20
19
|
* @param path - The unique path/key for the entity to remove.
|
|
21
20
|
*/
|
|
22
21
|
export declare function removeEntityFromCache(path: string): void;
|
|
23
|
-
/**
|
|
24
|
-
* Clears the entire in-memory cache and removes all related entities from `sessionStorage`.
|
|
25
|
-
*/
|
|
26
|
-
export declare function clearEntityCache(): void;
|
|
27
22
|
export declare function flattenKeys(obj: Record<string, unknown> | unknown[], prefix?: string, result?: string[]): string[];
|
package/dist/util/index.d.ts
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
export * from "./icon_list";
|
|
2
|
-
export * from "./icon_synonyms";
|
|
3
2
|
export * from "./icons";
|
|
4
3
|
export * from "./createFormexStub";
|
|
5
4
|
export * from "./entity_cache";
|
|
6
5
|
export * from "./useStorageUploadController";
|
|
7
|
-
export * from "./useTraceUpdate";
|
|
8
6
|
export * from "./previews";
|
|
9
7
|
export * from "./enums";
|
|
10
8
|
export * from "./constants";
|
|
@@ -12,7 +12,7 @@ export interface StorageFieldItem {
|
|
|
12
12
|
storagePathOrDownloadUrl?: string;
|
|
13
13
|
file?: File;
|
|
14
14
|
fileName?: string;
|
|
15
|
-
metadata?:
|
|
15
|
+
metadata?: Record<string, unknown>;
|
|
16
16
|
size: StorageFieldSize;
|
|
17
17
|
}
|
|
18
18
|
export declare function useStorageUploadController<M extends Record<string, unknown>>({ entityId, entityValues, path, value, property, propertyKey, storageSource, disabled, onChange }: {
|
|
@@ -31,7 +31,7 @@ export declare function useStorageUploadController<M extends Record<string, unkn
|
|
|
31
31
|
storage: StorageConfig;
|
|
32
32
|
fileNameBuilder: (file: File) => Promise<string>;
|
|
33
33
|
storagePathBuilder: (file: File) => string;
|
|
34
|
-
onFileUploadComplete: (uploadedPath: string, entry: StorageFieldItem, metadata?:
|
|
34
|
+
onFileUploadComplete: (uploadedPath: string, entry: StorageFieldItem, metadata?: Record<string, unknown>, uploadedUrl?: string) => Promise<void>;
|
|
35
35
|
onFileUploadError: (entry: StorageFieldItem) => void;
|
|
36
36
|
onFilesAdded: (acceptedFiles: File[]) => Promise<void>;
|
|
37
37
|
multipleFilesSupported: boolean;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rebasepro/core",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.2.
|
|
4
|
+
"version": "0.2.5",
|
|
5
5
|
"description": "Rebase core — framework-agnostic runtime for data-driven admin panels",
|
|
6
6
|
"funding": {
|
|
7
7
|
"url": "https://github.com/sponsors/rebaseco"
|
|
@@ -53,11 +53,11 @@
|
|
|
53
53
|
"notistack": "^3.0.2",
|
|
54
54
|
"react-compiler-runtime": "1.0.0",
|
|
55
55
|
"react-i18next": "^14.1.3",
|
|
56
|
-
"@rebasepro/
|
|
57
|
-
"@rebasepro/
|
|
58
|
-
"@rebasepro/
|
|
59
|
-
"@rebasepro/
|
|
60
|
-
"@rebasepro/utils": "0.2.
|
|
56
|
+
"@rebasepro/common": "0.2.5",
|
|
57
|
+
"@rebasepro/formex": "0.2.5",
|
|
58
|
+
"@rebasepro/types": "0.2.5",
|
|
59
|
+
"@rebasepro/ui": "0.2.5",
|
|
60
|
+
"@rebasepro/utils": "0.2.5"
|
|
61
61
|
},
|
|
62
62
|
"peerDependencies": {
|
|
63
63
|
"react": ">=19.0.0",
|
|
@@ -27,10 +27,19 @@ export function BootstrapAdminBanner({
|
|
|
27
27
|
return null;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
// Non-admin users don't load the users list (admin API is skipped to
|
|
31
|
+
// avoid 403s), so `users` would be empty and falsely trigger this banner.
|
|
32
|
+
// Only admin users (or users with no roles yet, during initial bootstrap)
|
|
33
|
+
// should ever see this prompt.
|
|
34
|
+
const loggedInUserRoles = loggedInUser.roles ?? [];
|
|
35
|
+
const isLoggedInUserAdmin = loggedInUserRoles.length === 0 || loggedInUserRoles.some(r => r === "admin");
|
|
36
|
+
if (!isLoggedInUserAdmin) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const { hasAdminUsers, loading: delegateLoading, bootstrapAdmin, usersError } = userManagement;
|
|
32
41
|
|
|
33
|
-
if (delegateLoading ||
|
|
42
|
+
if (delegateLoading || hasAdminUsers || usersError || !bootstrapAdmin) {
|
|
34
43
|
return null;
|
|
35
44
|
}
|
|
36
45
|
|
|
@@ -95,6 +95,26 @@ export interface LoginViewProps {
|
|
|
95
95
|
*/
|
|
96
96
|
googleClientId?: string;
|
|
97
97
|
|
|
98
|
+
/**
|
|
99
|
+
* GitHub client ID for GitHub OAuth.
|
|
100
|
+
*/
|
|
101
|
+
githubClientId?: string;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* LinkedIn client ID for LinkedIn OAuth.
|
|
105
|
+
*/
|
|
106
|
+
linkedinClientId?: string;
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Optional custom title shown above options
|
|
110
|
+
*/
|
|
111
|
+
title?: string;
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Optional custom subtitle shown above options
|
|
115
|
+
*/
|
|
116
|
+
subtitle?: string;
|
|
117
|
+
|
|
98
118
|
/**
|
|
99
119
|
* When true, shows bootstrap/setup UI (first-user creation).
|
|
100
120
|
* If not set, derived from `authController` if it exposes `needsSetup`.
|
|
@@ -106,6 +126,16 @@ export interface LoginViewProps {
|
|
|
106
126
|
* If not set, derived from `authController.capabilities.registration`.
|
|
107
127
|
*/
|
|
108
128
|
registrationEnabled?: boolean;
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Pre-fill the email field (e.g. for demo or testing environments).
|
|
132
|
+
*/
|
|
133
|
+
defaultEmail?: string;
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Pre-fill the password field (e.g. for demo or testing environments).
|
|
137
|
+
*/
|
|
138
|
+
defaultPassword?: string;
|
|
109
139
|
}
|
|
110
140
|
|
|
111
141
|
type AuthMode = "buttons" | "login" | "register" | "forgot";
|
|
@@ -123,8 +153,15 @@ export function LoginView({
|
|
|
123
153
|
disabled = false,
|
|
124
154
|
notAllowedError,
|
|
125
155
|
googleClientId,
|
|
156
|
+
githubClientId,
|
|
157
|
+
linkedinClientId,
|
|
158
|
+
title,
|
|
159
|
+
subtitle,
|
|
126
160
|
needsSetup,
|
|
127
|
-
registrationEnabled
|
|
161
|
+
registrationEnabled,
|
|
162
|
+
additionalComponent,
|
|
163
|
+
defaultEmail,
|
|
164
|
+
defaultPassword
|
|
128
165
|
}: LoginViewProps) {
|
|
129
166
|
|
|
130
167
|
const modeState = useModeController();
|
|
@@ -150,6 +187,8 @@ export function LoginView({
|
|
|
150
187
|
?? false;
|
|
151
188
|
const canRegister = registrationEnabled ?? caps.registration ?? false;
|
|
152
189
|
const hasGoogleLogin = googleClientId && (caps.enabledProviders?.includes("google") ?? caps.googleLogin ?? false);
|
|
190
|
+
const hasGitHubLogin = githubClientId && (caps.enabledProviders?.includes("github") ?? false);
|
|
191
|
+
const hasLinkedinLogin = linkedinClientId && (caps.enabledProviders?.includes("linkedin") ?? false);
|
|
153
192
|
const hasPasswordReset = caps.passwordReset ?? !!authController.forgotPassword;
|
|
154
193
|
|
|
155
194
|
const showRegistration = !disableSignupScreen && canRegister;
|
|
@@ -159,6 +198,28 @@ export function LoginView({
|
|
|
159
198
|
return () => clearTimeout(timer);
|
|
160
199
|
}, []);
|
|
161
200
|
|
|
201
|
+
// Effect to handle incoming redirect OAuth codes (GitHub, LinkedIn, etc.)
|
|
202
|
+
useEffect(() => {
|
|
203
|
+
const params = new URLSearchParams(window.location.search);
|
|
204
|
+
const code = params.get("code");
|
|
205
|
+
const provider = localStorage.getItem("rebase_oauth_provider");
|
|
206
|
+
if (code && provider) {
|
|
207
|
+
localStorage.removeItem("rebase_oauth_provider");
|
|
208
|
+
// Clear URL search params without page reload
|
|
209
|
+
const cleanUrl = window.location.origin + window.location.pathname;
|
|
210
|
+
window.history.replaceState({}, document.title, cleanUrl);
|
|
211
|
+
|
|
212
|
+
if (authController.oauthLogin) {
|
|
213
|
+
authController.oauthLogin(provider, {
|
|
214
|
+
code,
|
|
215
|
+
redirectUri: cleanUrl
|
|
216
|
+
}).catch((err) => {
|
|
217
|
+
console.error(`${provider} login failed:`, err);
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}, [authController]);
|
|
222
|
+
|
|
162
223
|
function buildErrorView() {
|
|
163
224
|
if (!authController.authProviderError) return null;
|
|
164
225
|
if (authController.user != null) return null;
|
|
@@ -192,17 +253,21 @@ export function LoginView({
|
|
|
192
253
|
} else if (notAllowedError instanceof Error) {
|
|
193
254
|
notAllowedMessage = notAllowedError.message;
|
|
194
255
|
} else {
|
|
195
|
-
notAllowedMessage = "It looks like you don't have access, based on the specified
|
|
256
|
+
notAllowedMessage = "It looks like you don't have access, based on the specified access configuration";
|
|
196
257
|
}
|
|
197
258
|
}
|
|
198
259
|
|
|
199
260
|
return (
|
|
200
261
|
<div
|
|
201
262
|
className={cls(
|
|
202
|
-
"relative flex items-center justify-center h-screen w-screen p-4 transition-opacity duration-500 bg-surface-50 dark:bg-surface-
|
|
263
|
+
"relative flex items-center justify-center h-screen w-screen p-4 transition-opacity duration-500 bg-surface-50 dark:bg-surface-950 overflow-hidden",
|
|
203
264
|
fadeIn ? "opacity-100" : "opacity-0"
|
|
204
265
|
)}>
|
|
205
266
|
|
|
267
|
+
{/* Glowing background blobs */}
|
|
268
|
+
<div className="absolute top-[-10%] left-[-10%] w-[50%] h-[50%] rounded-full bg-primary-500/10 blur-[120px] pointer-events-none" />
|
|
269
|
+
<div className="absolute bottom-[-10%] right-[-10%] w-[50%] h-[50%] rounded-full bg-indigo-500/10 blur-[120px] pointer-events-none" />
|
|
270
|
+
|
|
206
271
|
{/* Top-right controls */}
|
|
207
272
|
<div className="absolute top-4 right-4 flex items-center gap-1 z-10">
|
|
208
273
|
<LanguageToggle/>
|
|
@@ -220,9 +285,9 @@ export function LoginView({
|
|
|
220
285
|
</Menu>
|
|
221
286
|
</div>
|
|
222
287
|
|
|
223
|
-
<div className="flex flex-col items-center w-[
|
|
288
|
+
<div className="relative flex flex-col items-center w-[440px] max-w-full p-8 sm:p-10 bg-white/70 dark:bg-surface-900/60 backdrop-blur-xl border border-surface-200/50 dark:border-surface-800/50 rounded-2xl shadow-2xl z-10 transition-all duration-300 hover:shadow-primary-500/5">
|
|
224
289
|
{/* Logo */}
|
|
225
|
-
<div className="w-
|
|
290
|
+
<div className="w-24 h-24 m-2 mb-4 drop-shadow-md">
|
|
226
291
|
{logoComponent}
|
|
227
292
|
</div>
|
|
228
293
|
|
|
@@ -248,6 +313,8 @@ export function LoginView({
|
|
|
248
313
|
noUserComponent={noUserComponent}
|
|
249
314
|
disableSignupScreen={false}
|
|
250
315
|
bootstrapMode={true}
|
|
316
|
+
defaultEmail={defaultEmail}
|
|
317
|
+
defaultPassword={defaultPassword}
|
|
251
318
|
/>
|
|
252
319
|
)}
|
|
253
320
|
|
|
@@ -257,6 +324,20 @@ export function LoginView({
|
|
|
257
324
|
{/* Provider buttons screen */}
|
|
258
325
|
{mode === "buttons" && (
|
|
259
326
|
<div className="w-full flex flex-col gap-3 mt-2">
|
|
327
|
+
{(title || subtitle) && (
|
|
328
|
+
<div className="text-center mb-2">
|
|
329
|
+
{title && (
|
|
330
|
+
<Typography variant="h6" className="mb-0.5 font-bold">
|
|
331
|
+
{title}
|
|
332
|
+
</Typography>
|
|
333
|
+
)}
|
|
334
|
+
{subtitle && (
|
|
335
|
+
<Typography variant="body2" color="secondary" className="mb-4">
|
|
336
|
+
{subtitle}
|
|
337
|
+
</Typography>
|
|
338
|
+
)}
|
|
339
|
+
</div>
|
|
340
|
+
)}
|
|
260
341
|
<LoginButton
|
|
261
342
|
disabled={disabled}
|
|
262
343
|
text={"Sign in with email"}
|
|
@@ -270,6 +351,18 @@ export function LoginView({
|
|
|
270
351
|
authController={authController}
|
|
271
352
|
/>
|
|
272
353
|
)}
|
|
354
|
+
{hasGitHubLogin && githubClientId && (
|
|
355
|
+
<GitHubLoginButton
|
|
356
|
+
disabled={disabled}
|
|
357
|
+
githubClientId={githubClientId}
|
|
358
|
+
/>
|
|
359
|
+
)}
|
|
360
|
+
{hasLinkedinLogin && linkedinClientId && (
|
|
361
|
+
<LinkedInLoginButton
|
|
362
|
+
disabled={disabled}
|
|
363
|
+
linkedinClientId={linkedinClientId}
|
|
364
|
+
/>
|
|
365
|
+
)}
|
|
273
366
|
{showRegistration && (
|
|
274
367
|
<div className="mt-2 text-center">
|
|
275
368
|
<Typography variant="body2" color="secondary">
|
|
@@ -297,6 +390,8 @@ export function LoginView({
|
|
|
297
390
|
noUserComponent={noUserComponent}
|
|
298
391
|
disableSignupScreen={disableSignupScreen}
|
|
299
392
|
switchToRegister={showRegistration ? () => switchMode("register") : undefined}
|
|
393
|
+
defaultEmail={defaultEmail}
|
|
394
|
+
defaultPassword={defaultPassword}
|
|
300
395
|
/>
|
|
301
396
|
)}
|
|
302
397
|
|
|
@@ -310,6 +405,8 @@ export function LoginView({
|
|
|
310
405
|
noUserComponent={noUserComponent}
|
|
311
406
|
disableSignupScreen={disableSignupScreen}
|
|
312
407
|
switchToLogin={() => switchMode("login")}
|
|
408
|
+
defaultEmail={defaultEmail}
|
|
409
|
+
defaultPassword={defaultPassword}
|
|
313
410
|
/>
|
|
314
411
|
)}
|
|
315
412
|
|
|
@@ -323,6 +420,12 @@ export function LoginView({
|
|
|
323
420
|
</>
|
|
324
421
|
)}
|
|
325
422
|
</div>
|
|
423
|
+
|
|
424
|
+
{additionalComponent && (
|
|
425
|
+
<div className="w-full">
|
|
426
|
+
{additionalComponent}
|
|
427
|
+
</div>
|
|
428
|
+
)}
|
|
326
429
|
</div>
|
|
327
430
|
</div>
|
|
328
431
|
);
|
|
@@ -337,7 +440,7 @@ function LoginButton({
|
|
|
337
440
|
return (
|
|
338
441
|
<Button
|
|
339
442
|
disabled={disabled}
|
|
340
|
-
className="w-full"
|
|
443
|
+
className="w-full transition-transform duration-200 active:scale-[0.98]"
|
|
341
444
|
variant="outlined"
|
|
342
445
|
size="large"
|
|
343
446
|
onClick={onClick}>
|
|
@@ -422,6 +525,66 @@ function GoogleLoginButton({
|
|
|
422
525
|
);
|
|
423
526
|
}
|
|
424
527
|
|
|
528
|
+
const GitHubIcon = () => (
|
|
529
|
+
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor">
|
|
530
|
+
<path d="M12 2C6.477 2 2 6.477 2 12c0 4.42 2.865 8.166 6.839 9.489.5.092.682-.217.682-.482 0-.237-.008-.866-.013-1.7-2.782.603-3.369-1.34-3.369-1.34-.454-1.156-1.11-1.464-1.11-1.464-.908-.62.069-.608.069-.608 1.003.07 1.531 1.03 1.531 1.03.892 1.529 2.341 1.087 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.11-4.555-4.943 0-1.091.39-1.984 1.029-2.683-.103-.253-.446-1.27.098-2.647 0 0 .84-.269 2.75 1.025A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.294 2.747-1.025 2.747-1.025.546 1.377.203 2.394.1 2.647.64.699 1.028 1.592 1.028 2.683 0 3.842-2.339 4.687-4.566 4.935.359.309.678.919.678 1.852 0 1.336-.012 2.415-.012 2.743 0 .267.18.579.688.481C19.137 20.162 22 16.418 22 12c0-5.523-4.477-10-10-10z"/>
|
|
531
|
+
</svg>
|
|
532
|
+
);
|
|
533
|
+
|
|
534
|
+
function GitHubLoginButton({
|
|
535
|
+
disabled,
|
|
536
|
+
githubClientId
|
|
537
|
+
}: {
|
|
538
|
+
disabled?: boolean,
|
|
539
|
+
githubClientId: string
|
|
540
|
+
}) {
|
|
541
|
+
const handleClick = () => {
|
|
542
|
+
localStorage.setItem("rebase_oauth_provider", "github");
|
|
543
|
+
const redirectUri = encodeURIComponent(window.location.origin + window.location.pathname);
|
|
544
|
+
const scope = "read:user,user:email";
|
|
545
|
+
window.location.href = `https://github.com/login/oauth/authorize?client_id=${githubClientId}&redirect_uri=${redirectUri}&scope=${scope}`;
|
|
546
|
+
};
|
|
547
|
+
|
|
548
|
+
return (
|
|
549
|
+
<LoginButton
|
|
550
|
+
disabled={disabled}
|
|
551
|
+
text="Sign in with GitHub"
|
|
552
|
+
icon={<GitHubIcon/>}
|
|
553
|
+
onClick={handleClick}
|
|
554
|
+
/>
|
|
555
|
+
);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
const LinkedInIcon = () => (
|
|
559
|
+
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor">
|
|
560
|
+
<path d="M19 0h-14c-2.761 0-5 2.239-5 5v14c0 2.761 2.239 5 5 5h14c2.762 0 5-2.239 5-5v-14c0-2.761-2.238-5-5-5zm-11 19h-3v-11h3v11zm-1.5-12.268c-.966 0-1.75-.79-1.75-1.764s.784-1.764 1.75-1.764 1.75.79 1.75 1.764-.783 1.764-1.75 1.764zm13.5 12.268h-3v-5.604c0-3.368-4-3.113-4 0v5.604h-3v-11h3v1.765c1.396-2.586 7-2.777 7 2.476v6.759z"/>
|
|
561
|
+
</svg>
|
|
562
|
+
);
|
|
563
|
+
|
|
564
|
+
function LinkedInLoginButton({
|
|
565
|
+
disabled,
|
|
566
|
+
linkedinClientId
|
|
567
|
+
}: {
|
|
568
|
+
disabled?: boolean,
|
|
569
|
+
linkedinClientId: string
|
|
570
|
+
}) {
|
|
571
|
+
const handleClick = () => {
|
|
572
|
+
localStorage.setItem("rebase_oauth_provider", "linkedin");
|
|
573
|
+
const redirectUri = encodeURIComponent(window.location.origin + window.location.pathname);
|
|
574
|
+
const scope = "openid profile email";
|
|
575
|
+
window.location.href = `https://www.linkedin.com/oauth/v2/authorization?response_type=code&client_id=${linkedinClientId}&redirect_uri=${redirectUri}&scope=${scope}`;
|
|
576
|
+
};
|
|
577
|
+
|
|
578
|
+
return (
|
|
579
|
+
<LoginButton
|
|
580
|
+
disabled={disabled}
|
|
581
|
+
text="Sign in with LinkedIn"
|
|
582
|
+
icon={<LinkedInIcon/>}
|
|
583
|
+
onClick={handleClick}
|
|
584
|
+
/>
|
|
585
|
+
);
|
|
586
|
+
}
|
|
587
|
+
|
|
425
588
|
function LoginForm({
|
|
426
589
|
onClose,
|
|
427
590
|
onForgotPassword,
|
|
@@ -431,7 +594,9 @@ function LoginForm({
|
|
|
431
594
|
disableSignupScreen,
|
|
432
595
|
bootstrapMode = false,
|
|
433
596
|
switchToRegister,
|
|
434
|
-
switchToLogin
|
|
597
|
+
switchToLogin,
|
|
598
|
+
defaultEmail,
|
|
599
|
+
defaultPassword
|
|
435
600
|
}: {
|
|
436
601
|
onClose: () => void,
|
|
437
602
|
onForgotPassword?: () => void,
|
|
@@ -441,12 +606,14 @@ function LoginForm({
|
|
|
441
606
|
disableSignupScreen: boolean,
|
|
442
607
|
bootstrapMode?: boolean,
|
|
443
608
|
switchToRegister?: () => void,
|
|
444
|
-
switchToLogin?: () => void
|
|
609
|
+
switchToLogin?: () => void,
|
|
610
|
+
defaultEmail?: string,
|
|
611
|
+
defaultPassword?: string
|
|
445
612
|
}) {
|
|
446
613
|
const passwordRef = useRef<HTMLInputElement | null>(null);
|
|
447
614
|
|
|
448
|
-
const [email, setEmail] = useState<string>();
|
|
449
|
-
const [password, setPassword] = useState<string>();
|
|
615
|
+
const [email, setEmail] = useState<string | undefined>(defaultEmail);
|
|
616
|
+
const [password, setPassword] = useState<string | undefined>(defaultPassword);
|
|
450
617
|
const [displayName, setDisplayName] = useState<string>();
|
|
451
618
|
|
|
452
619
|
useEffect(() => {
|
|
@@ -24,18 +24,22 @@ interface SessionInfo {
|
|
|
24
24
|
interface ExtendedAuthController {
|
|
25
25
|
user: { displayName?: string | null; photoURL?: string | null; email?: string | null } | null;
|
|
26
26
|
updateProfile?: (displayName: string, photoURL: string) => Promise<void>;
|
|
27
|
+
changePassword?: (oldPassword: string, newPassword: string) => Promise<void>;
|
|
27
28
|
fetchSessions?: () => Promise<SessionInfo[]>;
|
|
28
29
|
revokeSession?: (id: string) => Promise<void>;
|
|
29
30
|
revokeAllSessions?: () => Promise<void>;
|
|
30
31
|
signOut: () => Promise<void>;
|
|
31
32
|
}
|
|
32
33
|
|
|
34
|
+
type ActiveTab = "profile" | "security" | "sessions";
|
|
35
|
+
|
|
33
36
|
export function UserSettingsView() {
|
|
34
37
|
const authController = useAuthController() as ExtendedAuthController;
|
|
35
38
|
const user = authController.user;
|
|
36
39
|
const { t } = useTranslation();
|
|
37
40
|
|
|
38
|
-
const
|
|
41
|
+
const hasPasswordChange = !!authController.changePassword;
|
|
42
|
+
const [activeTab, setActiveTab] = useState<ActiveTab>("profile");
|
|
39
43
|
|
|
40
44
|
// Profile state
|
|
41
45
|
const [displayName, setDisplayName] = useState(user?.displayName || "");
|
|
@@ -43,6 +47,14 @@ export function UserSettingsView() {
|
|
|
43
47
|
const [savingProfile, setSavingProfile] = useState(false);
|
|
44
48
|
const [profileError, setProfileError] = useState<string | null>(null);
|
|
45
49
|
|
|
50
|
+
// Password change state
|
|
51
|
+
const [currentPassword, setCurrentPassword] = useState("");
|
|
52
|
+
const [newPassword, setNewPassword] = useState("");
|
|
53
|
+
const [confirmPassword, setConfirmPassword] = useState("");
|
|
54
|
+
const [changingPassword, setChangingPassword] = useState(false);
|
|
55
|
+
const [passwordError, setPasswordError] = useState<string | null>(null);
|
|
56
|
+
const [passwordSuccess, setPasswordSuccess] = useState<string | null>(null);
|
|
57
|
+
|
|
46
58
|
// Sessions state
|
|
47
59
|
const [sessions, setSessions] = useState<SessionInfo[]>([]);
|
|
48
60
|
const [loadingSessions, setLoadingSessions] = useState(false);
|
|
@@ -74,6 +86,41 @@ export function UserSettingsView() {
|
|
|
74
86
|
}
|
|
75
87
|
};
|
|
76
88
|
|
|
89
|
+
const handleChangePassword = async () => {
|
|
90
|
+
setPasswordError(null);
|
|
91
|
+
setPasswordSuccess(null);
|
|
92
|
+
|
|
93
|
+
// Validate
|
|
94
|
+
if (newPassword.length < 8) {
|
|
95
|
+
setPasswordError(t("password_too_short"));
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (newPassword !== confirmPassword) {
|
|
99
|
+
setPasswordError(t("passwords_dont_match"));
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
setChangingPassword(true);
|
|
104
|
+
try {
|
|
105
|
+
if (authController.changePassword) {
|
|
106
|
+
await authController.changePassword(currentPassword, newPassword);
|
|
107
|
+
setPasswordSuccess(t("password_changed"));
|
|
108
|
+
setCurrentPassword("");
|
|
109
|
+
setNewPassword("");
|
|
110
|
+
setConfirmPassword("");
|
|
111
|
+
// Backend invalidates all sessions on password change,
|
|
112
|
+
// so the user will be logged out shortly
|
|
113
|
+
setTimeout(() => {
|
|
114
|
+
authController.signOut();
|
|
115
|
+
}, 2000);
|
|
116
|
+
}
|
|
117
|
+
} catch (e: unknown) {
|
|
118
|
+
setPasswordError(e instanceof Error ? e.message : String(e));
|
|
119
|
+
} finally {
|
|
120
|
+
setChangingPassword(false);
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
|
|
77
124
|
const loadSessions = async () => {
|
|
78
125
|
setLoadingSessions(true);
|
|
79
126
|
setSessionsError(null);
|
|
@@ -134,8 +181,9 @@ export function UserSettingsView() {
|
|
|
134
181
|
<div className="flex-grow max-w-4xl w-full mx-auto p-4 sm:p-6 md:p-12">
|
|
135
182
|
<Typography variant="h4" className="mb-8">{t("account_settings")}</Typography>
|
|
136
183
|
|
|
137
|
-
<Tabs value={activeTab} onValueChange={(v) => setActiveTab(v as
|
|
184
|
+
<Tabs value={activeTab} onValueChange={(v) => setActiveTab(v as ActiveTab)} className="mb-8">
|
|
138
185
|
<Tab value="profile">{t("profile")}</Tab>
|
|
186
|
+
{hasPasswordChange && <Tab value="security">{t("security")}</Tab>}
|
|
139
187
|
<Tab value="sessions">{t("sessions")}</Tab>
|
|
140
188
|
</Tabs>
|
|
141
189
|
|
|
@@ -165,6 +213,51 @@ export function UserSettingsView() {
|
|
|
165
213
|
</div>
|
|
166
214
|
)}
|
|
167
215
|
|
|
216
|
+
{activeTab === "security" && hasPasswordChange && (
|
|
217
|
+
<div className="flex flex-col gap-6 max-w-xl">
|
|
218
|
+
<Typography variant="h6" className="mb-2">{t("change_password")}</Typography>
|
|
219
|
+
|
|
220
|
+
<TextField
|
|
221
|
+
label={t("current_password")}
|
|
222
|
+
type="password"
|
|
223
|
+
value={currentPassword}
|
|
224
|
+
onChange={(e) => setCurrentPassword(e.target.value)}
|
|
225
|
+
autoComplete="current-password"
|
|
226
|
+
/>
|
|
227
|
+
<TextField
|
|
228
|
+
label={t("new_password")}
|
|
229
|
+
type="password"
|
|
230
|
+
value={newPassword}
|
|
231
|
+
onChange={(e) => setNewPassword(e.target.value)}
|
|
232
|
+
autoComplete="new-password"
|
|
233
|
+
/>
|
|
234
|
+
<TextField
|
|
235
|
+
label={t("confirm_password")}
|
|
236
|
+
type="password"
|
|
237
|
+
value={confirmPassword}
|
|
238
|
+
onChange={(e) => setConfirmPassword(e.target.value)}
|
|
239
|
+
autoComplete="new-password"
|
|
240
|
+
/>
|
|
241
|
+
|
|
242
|
+
{passwordError && (
|
|
243
|
+
<Typography color="error">{passwordError}</Typography>
|
|
244
|
+
)}
|
|
245
|
+
{passwordSuccess && (
|
|
246
|
+
<Typography className="text-emerald-600 dark:text-emerald-400">{passwordSuccess}</Typography>
|
|
247
|
+
)}
|
|
248
|
+
|
|
249
|
+
<div className="mt-4">
|
|
250
|
+
<Button
|
|
251
|
+
variant="filled"
|
|
252
|
+
onClick={handleChangePassword}
|
|
253
|
+
disabled={changingPassword || !currentPassword || !newPassword || !confirmPassword}
|
|
254
|
+
>
|
|
255
|
+
{changingPassword ? t("changing_password") : t("change_password")}
|
|
256
|
+
</Button>
|
|
257
|
+
</div>
|
|
258
|
+
</div>
|
|
259
|
+
)}
|
|
260
|
+
|
|
168
261
|
{activeTab === "sessions" && (
|
|
169
262
|
<div className="flex flex-col gap-4 max-w-3xl">
|
|
170
263
|
{loadingSessions ? (
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import type { Property } from "@rebasepro/types";
|
|
2
2
|
import { CollectionSize, SelectedCellProps } from "@rebasepro/types";
|
|
3
3
|
|
|
4
|
-
export type EntityCollectionTableController<M extends Record<string,
|
|
4
|
+
export type EntityCollectionTableController<M extends Record<string, unknown>> = {
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* This cell is displayed as selected
|
|
8
8
|
*/
|
|
9
|
-
selectedCell?: SelectedCellProps
|
|
9
|
+
selectedCell?: SelectedCellProps;
|
|
10
10
|
/**
|
|
11
11
|
* Store used to sync selection state across cells efficiently.
|
|
12
12
|
*/
|
|
13
|
-
selectionStore?:
|
|
13
|
+
selectionStore?: { getSnapshot: () => SelectedCellProps | undefined; subscribe: (cb: () => void) => () => void };
|
|
14
14
|
/**
|
|
15
15
|
* Select a table cell
|
|
16
16
|
* @param cell
|
|
@@ -25,7 +25,7 @@ export type EntityCollectionTableController<M extends Record<string, any>> = {
|
|
|
25
25
|
* Callback used when the value of a cell has changed.
|
|
26
26
|
* @param params
|
|
27
27
|
*/
|
|
28
|
-
onValueChange?: (params: OnCellValueChangeParams<
|
|
28
|
+
onValueChange?: (params: OnCellValueChangeParams<unknown, M>) => void;
|
|
29
29
|
/**
|
|
30
30
|
* Size of the elements in the collection
|
|
31
31
|
*/
|
|
@@ -36,7 +36,7 @@ export type EntityCollectionTableController<M extends Record<string, any>> = {
|
|
|
36
36
|
* Props passed in a callback when the content of a cell in a table has been edited
|
|
37
37
|
* @group Collection components
|
|
38
38
|
*/
|
|
39
|
-
export interface OnCellValueChangeParams<T =
|
|
39
|
+
export interface OnCellValueChangeParams<T = unknown, D = unknown> {
|
|
40
40
|
value: T,
|
|
41
41
|
propertyKey: string,
|
|
42
42
|
data?: D,
|
|
@@ -49,7 +49,7 @@ export interface OnCellValueChangeParams<T = any, D = any> {
|
|
|
49
49
|
*/
|
|
50
50
|
export type UniqueFieldValidator = (props: {
|
|
51
51
|
name: string,
|
|
52
|
-
value:
|
|
52
|
+
value: unknown,
|
|
53
53
|
property: Property,
|
|
54
54
|
entityId?: string | number
|
|
55
55
|
}) => Promise<boolean>;
|
|
@@ -58,7 +58,7 @@ export type UniqueFieldValidator = (props: {
|
|
|
58
58
|
* Callback when a cell has changed in a table
|
|
59
59
|
* @group Collection components
|
|
60
60
|
*/
|
|
61
|
-
export type OnCellValueChange<T, M extends Record<string,
|
|
61
|
+
export type OnCellValueChange<T, M extends Record<string, unknown>> = (params: OnCellValueChangeParams<T, M>) => Promise<void> | void;
|
|
62
62
|
|
|
63
63
|
/**
|
|
64
64
|
* @group Collection components
|
|
@@ -9,7 +9,7 @@ import { deepEqual as equal } from "fast-equals";
|
|
|
9
9
|
* @param deps
|
|
10
10
|
* @param timeoutMs
|
|
11
11
|
*/
|
|
12
|
-
export function useDebouncedData<T>(data: T[], deps:
|
|
12
|
+
export function useDebouncedData<T>(data: T[], deps: unknown, timeoutMs = 5000) {
|
|
13
13
|
|
|
14
14
|
const [deferredData, setDeferredData] = React.useState(data);
|
|
15
15
|
const dataLength = React.useRef(deferredData.length ?? 0);
|
|
@@ -32,7 +32,7 @@ export function useDebouncedData<T>(data: T[], deps: any, timeoutMs = 5000) {
|
|
|
32
32
|
|
|
33
33
|
pendingUpdate.current = true;
|
|
34
34
|
|
|
35
|
-
let handler:
|
|
35
|
+
let handler: ReturnType<typeof setTimeout> | undefined;
|
|
36
36
|
if (immediateUpdate)
|
|
37
37
|
performUpdate()
|
|
38
38
|
else
|
package/src/core/Rebase.tsx
CHANGED
|
@@ -64,7 +64,8 @@ export function Rebase<USER extends User>(props: RebaseProps<USER>) {
|
|
|
64
64
|
apiKey,
|
|
65
65
|
userManagement: _userManagement,
|
|
66
66
|
effectiveRoleController,
|
|
67
|
-
apiUrl
|
|
67
|
+
apiUrl,
|
|
68
|
+
translations
|
|
68
69
|
} = props;
|
|
69
70
|
|
|
70
71
|
const plugins = pluginsProp;
|
|
@@ -182,7 +183,7 @@ export function Rebase<USER extends User>(props: RebaseProps<USER>) {
|
|
|
182
183
|
}
|
|
183
184
|
|
|
184
185
|
const content = (
|
|
185
|
-
<RebaseI18nProvider locale={locale}>
|
|
186
|
+
<RebaseI18nProvider locale={locale} translations={translations}>
|
|
186
187
|
<SnackbarProvider>
|
|
187
188
|
<ModeControllerProvider value={modeController}>
|
|
188
189
|
<AdminModeControllerProvider value={adminModeController}>
|