@rebasepro/core 0.2.3 → 0.3.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/dist/components/LoginView/LoginView.d.ts +17 -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/index.d.ts +0 -1
- package/dist/index.es.js +499 -418
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +499 -418
- 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 +151 -6
- 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/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/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.
|
|
4
|
+
"version": "0.3.0",
|
|
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/ui": "0.
|
|
59
|
-
"@rebasepro/
|
|
60
|
-
"@rebasepro/
|
|
56
|
+
"@rebasepro/common": "0.3.0",
|
|
57
|
+
"@rebasepro/formex": "0.3.0",
|
|
58
|
+
"@rebasepro/ui": "0.3.0",
|
|
59
|
+
"@rebasepro/utils": "0.3.0",
|
|
60
|
+
"@rebasepro/types": "0.3.0"
|
|
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`.
|
|
@@ -123,8 +143,13 @@ export function LoginView({
|
|
|
123
143
|
disabled = false,
|
|
124
144
|
notAllowedError,
|
|
125
145
|
googleClientId,
|
|
146
|
+
githubClientId,
|
|
147
|
+
linkedinClientId,
|
|
148
|
+
title,
|
|
149
|
+
subtitle,
|
|
126
150
|
needsSetup,
|
|
127
|
-
registrationEnabled
|
|
151
|
+
registrationEnabled,
|
|
152
|
+
additionalComponent
|
|
128
153
|
}: LoginViewProps) {
|
|
129
154
|
|
|
130
155
|
const modeState = useModeController();
|
|
@@ -150,6 +175,8 @@ export function LoginView({
|
|
|
150
175
|
?? false;
|
|
151
176
|
const canRegister = registrationEnabled ?? caps.registration ?? false;
|
|
152
177
|
const hasGoogleLogin = googleClientId && (caps.enabledProviders?.includes("google") ?? caps.googleLogin ?? false);
|
|
178
|
+
const hasGitHubLogin = githubClientId && (caps.enabledProviders?.includes("github") ?? false);
|
|
179
|
+
const hasLinkedinLogin = linkedinClientId && (caps.enabledProviders?.includes("linkedin") ?? false);
|
|
153
180
|
const hasPasswordReset = caps.passwordReset ?? !!authController.forgotPassword;
|
|
154
181
|
|
|
155
182
|
const showRegistration = !disableSignupScreen && canRegister;
|
|
@@ -159,6 +186,28 @@ export function LoginView({
|
|
|
159
186
|
return () => clearTimeout(timer);
|
|
160
187
|
}, []);
|
|
161
188
|
|
|
189
|
+
// Effect to handle incoming redirect OAuth codes (GitHub, LinkedIn, etc.)
|
|
190
|
+
useEffect(() => {
|
|
191
|
+
const params = new URLSearchParams(window.location.search);
|
|
192
|
+
const code = params.get("code");
|
|
193
|
+
const provider = localStorage.getItem("rebase_oauth_provider");
|
|
194
|
+
if (code && provider) {
|
|
195
|
+
localStorage.removeItem("rebase_oauth_provider");
|
|
196
|
+
// Clear URL search params without page reload
|
|
197
|
+
const cleanUrl = window.location.origin + window.location.pathname;
|
|
198
|
+
window.history.replaceState({}, document.title, cleanUrl);
|
|
199
|
+
|
|
200
|
+
if (authController.oauthLogin) {
|
|
201
|
+
authController.oauthLogin(provider, {
|
|
202
|
+
code,
|
|
203
|
+
redirectUri: cleanUrl
|
|
204
|
+
}).catch((err) => {
|
|
205
|
+
console.error(`${provider} login failed:`, err);
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}, [authController]);
|
|
210
|
+
|
|
162
211
|
function buildErrorView() {
|
|
163
212
|
if (!authController.authProviderError) return null;
|
|
164
213
|
if (authController.user != null) return null;
|
|
@@ -192,17 +241,21 @@ export function LoginView({
|
|
|
192
241
|
} else if (notAllowedError instanceof Error) {
|
|
193
242
|
notAllowedMessage = notAllowedError.message;
|
|
194
243
|
} else {
|
|
195
|
-
notAllowedMessage = "It looks like you don't have access, based on the specified
|
|
244
|
+
notAllowedMessage = "It looks like you don't have access, based on the specified access configuration";
|
|
196
245
|
}
|
|
197
246
|
}
|
|
198
247
|
|
|
199
248
|
return (
|
|
200
249
|
<div
|
|
201
250
|
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-
|
|
251
|
+
"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
252
|
fadeIn ? "opacity-100" : "opacity-0"
|
|
204
253
|
)}>
|
|
205
254
|
|
|
255
|
+
{/* Glowing background blobs */}
|
|
256
|
+
<div className="absolute top-[-10%] left-[-10%] w-[50%] h-[50%] rounded-full bg-primary-500/10 blur-[120px] pointer-events-none" />
|
|
257
|
+
<div className="absolute bottom-[-10%] right-[-10%] w-[50%] h-[50%] rounded-full bg-indigo-500/10 blur-[120px] pointer-events-none" />
|
|
258
|
+
|
|
206
259
|
{/* Top-right controls */}
|
|
207
260
|
<div className="absolute top-4 right-4 flex items-center gap-1 z-10">
|
|
208
261
|
<LanguageToggle/>
|
|
@@ -220,9 +273,9 @@ export function LoginView({
|
|
|
220
273
|
</Menu>
|
|
221
274
|
</div>
|
|
222
275
|
|
|
223
|
-
<div className="flex flex-col items-center w-[
|
|
276
|
+
<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
277
|
{/* Logo */}
|
|
225
|
-
<div className="w-
|
|
278
|
+
<div className="w-24 h-24 m-2 mb-4 drop-shadow-md">
|
|
226
279
|
{logoComponent}
|
|
227
280
|
</div>
|
|
228
281
|
|
|
@@ -257,6 +310,20 @@ export function LoginView({
|
|
|
257
310
|
{/* Provider buttons screen */}
|
|
258
311
|
{mode === "buttons" && (
|
|
259
312
|
<div className="w-full flex flex-col gap-3 mt-2">
|
|
313
|
+
{(title || subtitle) && (
|
|
314
|
+
<div className="text-center mb-2">
|
|
315
|
+
{title && (
|
|
316
|
+
<Typography variant="h6" className="mb-0.5 font-bold">
|
|
317
|
+
{title}
|
|
318
|
+
</Typography>
|
|
319
|
+
)}
|
|
320
|
+
{subtitle && (
|
|
321
|
+
<Typography variant="body2" color="secondary" className="mb-4">
|
|
322
|
+
{subtitle}
|
|
323
|
+
</Typography>
|
|
324
|
+
)}
|
|
325
|
+
</div>
|
|
326
|
+
)}
|
|
260
327
|
<LoginButton
|
|
261
328
|
disabled={disabled}
|
|
262
329
|
text={"Sign in with email"}
|
|
@@ -270,6 +337,18 @@ export function LoginView({
|
|
|
270
337
|
authController={authController}
|
|
271
338
|
/>
|
|
272
339
|
)}
|
|
340
|
+
{hasGitHubLogin && githubClientId && (
|
|
341
|
+
<GitHubLoginButton
|
|
342
|
+
disabled={disabled}
|
|
343
|
+
githubClientId={githubClientId}
|
|
344
|
+
/>
|
|
345
|
+
)}
|
|
346
|
+
{hasLinkedinLogin && linkedinClientId && (
|
|
347
|
+
<LinkedInLoginButton
|
|
348
|
+
disabled={disabled}
|
|
349
|
+
linkedinClientId={linkedinClientId}
|
|
350
|
+
/>
|
|
351
|
+
)}
|
|
273
352
|
{showRegistration && (
|
|
274
353
|
<div className="mt-2 text-center">
|
|
275
354
|
<Typography variant="body2" color="secondary">
|
|
@@ -323,6 +402,12 @@ export function LoginView({
|
|
|
323
402
|
</>
|
|
324
403
|
)}
|
|
325
404
|
</div>
|
|
405
|
+
|
|
406
|
+
{additionalComponent && (
|
|
407
|
+
<div className="w-full">
|
|
408
|
+
{additionalComponent}
|
|
409
|
+
</div>
|
|
410
|
+
)}
|
|
326
411
|
</div>
|
|
327
412
|
</div>
|
|
328
413
|
);
|
|
@@ -337,7 +422,7 @@ function LoginButton({
|
|
|
337
422
|
return (
|
|
338
423
|
<Button
|
|
339
424
|
disabled={disabled}
|
|
340
|
-
className="w-full"
|
|
425
|
+
className="w-full transition-transform duration-200 active:scale-[0.98]"
|
|
341
426
|
variant="outlined"
|
|
342
427
|
size="large"
|
|
343
428
|
onClick={onClick}>
|
|
@@ -422,6 +507,66 @@ function GoogleLoginButton({
|
|
|
422
507
|
);
|
|
423
508
|
}
|
|
424
509
|
|
|
510
|
+
const GitHubIcon = () => (
|
|
511
|
+
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor">
|
|
512
|
+
<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"/>
|
|
513
|
+
</svg>
|
|
514
|
+
);
|
|
515
|
+
|
|
516
|
+
function GitHubLoginButton({
|
|
517
|
+
disabled,
|
|
518
|
+
githubClientId
|
|
519
|
+
}: {
|
|
520
|
+
disabled?: boolean,
|
|
521
|
+
githubClientId: string
|
|
522
|
+
}) {
|
|
523
|
+
const handleClick = () => {
|
|
524
|
+
localStorage.setItem("rebase_oauth_provider", "github");
|
|
525
|
+
const redirectUri = encodeURIComponent(window.location.origin + window.location.pathname);
|
|
526
|
+
const scope = "read:user,user:email";
|
|
527
|
+
window.location.href = `https://github.com/login/oauth/authorize?client_id=${githubClientId}&redirect_uri=${redirectUri}&scope=${scope}`;
|
|
528
|
+
};
|
|
529
|
+
|
|
530
|
+
return (
|
|
531
|
+
<LoginButton
|
|
532
|
+
disabled={disabled}
|
|
533
|
+
text="Sign in with GitHub"
|
|
534
|
+
icon={<GitHubIcon/>}
|
|
535
|
+
onClick={handleClick}
|
|
536
|
+
/>
|
|
537
|
+
);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
const LinkedInIcon = () => (
|
|
541
|
+
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor">
|
|
542
|
+
<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"/>
|
|
543
|
+
</svg>
|
|
544
|
+
);
|
|
545
|
+
|
|
546
|
+
function LinkedInLoginButton({
|
|
547
|
+
disabled,
|
|
548
|
+
linkedinClientId
|
|
549
|
+
}: {
|
|
550
|
+
disabled?: boolean,
|
|
551
|
+
linkedinClientId: string
|
|
552
|
+
}) {
|
|
553
|
+
const handleClick = () => {
|
|
554
|
+
localStorage.setItem("rebase_oauth_provider", "linkedin");
|
|
555
|
+
const redirectUri = encodeURIComponent(window.location.origin + window.location.pathname);
|
|
556
|
+
const scope = "openid profile email";
|
|
557
|
+
window.location.href = `https://www.linkedin.com/oauth/v2/authorization?response_type=code&client_id=${linkedinClientId}&redirect_uri=${redirectUri}&scope=${scope}`;
|
|
558
|
+
};
|
|
559
|
+
|
|
560
|
+
return (
|
|
561
|
+
<LoginButton
|
|
562
|
+
disabled={disabled}
|
|
563
|
+
text="Sign in with LinkedIn"
|
|
564
|
+
icon={<LinkedInIcon/>}
|
|
565
|
+
onClick={handleClick}
|
|
566
|
+
/>
|
|
567
|
+
);
|
|
568
|
+
}
|
|
569
|
+
|
|
425
570
|
function LoginForm({
|
|
426
571
|
onClose,
|
|
427
572
|
onForgotPassword,
|
|
@@ -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}>
|
package/src/core/RebaseProps.tsx
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import { Locale, User, AuthController, AnalyticsEvent, DataDriver, StorageSource, UserConfigurationPersistence, CollectionRegistryController, DatabaseAdmin, UrlController, NavigationStateController, RebaseData, RebaseClient, RebaseContext, UserManagementDelegate, EntityLinkBuilder, RebasePlugin, SlotContribution, PropertyConfig, EntityCustomView, EntityAction } from "@rebasepro/types";
|
|
2
|
+
import { Locale, User, AuthController, AnalyticsEvent, DataDriver, StorageSource, UserConfigurationPersistence, CollectionRegistryController, DatabaseAdmin, UrlController, NavigationStateController, RebaseData, RebaseClient, RebaseContext, UserManagementDelegate, EntityLinkBuilder, RebasePlugin, SlotContribution, PropertyConfig, EntityCustomView, EntityAction, RebaseTranslations } from "@rebasepro/types";
|
|
3
|
+
|
|
4
|
+
/** DeepPartial helper — allows partial overrides at any nesting level */
|
|
5
|
+
type DeepPartial<T> = T extends object
|
|
6
|
+
? { [K in keyof T]?: DeepPartial<T[K]> }
|
|
7
|
+
: T;
|
|
3
8
|
|
|
4
9
|
/**
|
|
5
10
|
* Controller to simulate different roles when dev mode is active.
|
|
@@ -138,7 +143,7 @@ export type RebaseProps<USER extends User> = {
|
|
|
138
143
|
/**
|
|
139
144
|
* Entity Views
|
|
140
145
|
*/
|
|
141
|
-
entityViews?: EntityCustomView
|
|
146
|
+
entityViews?: EntityCustomView[];
|
|
142
147
|
|
|
143
148
|
/**
|
|
144
149
|
* Entity Actions
|
|
@@ -161,4 +166,12 @@ export type RebaseProps<USER extends User> = {
|
|
|
161
166
|
*/
|
|
162
167
|
effectiveRoleController?: EffectiveRoleController;
|
|
163
168
|
|
|
169
|
+
/**
|
|
170
|
+
* Override or extend any Rebase UI string, keyed by locale.
|
|
171
|
+
*/
|
|
172
|
+
translations?: {
|
|
173
|
+
[locale: string]: DeepPartial<RebaseTranslations>;
|
|
174
|
+
};
|
|
175
|
+
|
|
164
176
|
};
|
|
177
|
+
|
package/src/hooks/index.tsx
CHANGED
|
@@ -33,7 +33,6 @@ export * from "./useCustomizationController";
|
|
|
33
33
|
export * from "./useBuildLocalConfigurationPersistence";
|
|
34
34
|
export * from "./useBuildModeController";
|
|
35
35
|
|
|
36
|
-
export * from "./useValidateAuthenticator";
|
|
37
36
|
export * from "./useRebaseRegistry";
|
|
38
37
|
export * from "./useBackendStorageSource";
|
|
39
38
|
export * from "./usePermissions";
|
|
@@ -13,7 +13,8 @@ import { isLazyComponentRef } from "@rebasepro/types";
|
|
|
13
13
|
* to the `React.lazy()` wrapper it produced. Strings are keyed by a separate
|
|
14
14
|
* plain Map since they can't be WeakMap keys.
|
|
15
15
|
*/
|
|
16
|
-
|
|
16
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
17
|
+
const lazyCache = new WeakMap<object | Function, React.ComponentType<any>>();
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* Resolves a `ComponentRef` into a renderable `React.ComponentType`.
|
|
@@ -62,7 +63,7 @@ export function useResolvedComponent<P = unknown>(
|
|
|
62
63
|
* same loader always returns the same lazy component identity.
|
|
63
64
|
*/
|
|
64
65
|
function getOrCreateLazy<P>(
|
|
65
|
-
key: object |
|
|
66
|
+
key: object | Function,
|
|
66
67
|
loader: () => Promise<{ default: React.ComponentType<P> }>
|
|
67
68
|
): React.ComponentType<P> {
|
|
68
69
|
const cached = lazyCache.get(key);
|
|
@@ -108,7 +109,7 @@ export function resolveComponentRef<P = unknown>(
|
|
|
108
109
|
|
|
109
110
|
// 3. Function — either a React component or a lazy import loader.
|
|
110
111
|
if (typeof ref === "function") {
|
|
111
|
-
const fn = ref as
|
|
112
|
+
const fn = ref as Function;
|
|
112
113
|
|
|
113
114
|
// Class components (React.Component / PureComponent) have this flag
|
|
114
115
|
if (fn.prototype?.isReactComponent) {
|