@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.
Files changed (40) hide show
  1. package/dist/components/LoginView/LoginView.d.ts +25 -1
  2. package/dist/components/common/types.d.ts +10 -7
  3. package/dist/components/common/useDebouncedData.d.ts +1 -1
  4. package/dist/core/RebaseProps.d.ts +13 -2
  5. package/dist/core/RebaseRouter.d.ts +1 -1
  6. package/dist/hooks/data/useCollectionFetch.d.ts +12 -1
  7. package/dist/hooks/index.d.ts +0 -1
  8. package/dist/index.es.js +565 -454
  9. package/dist/index.es.js.map +1 -1
  10. package/dist/index.umd.js +565 -454
  11. package/dist/index.umd.js.map +1 -1
  12. package/dist/util/entity_cache.d.ts +0 -5
  13. package/dist/util/index.d.ts +0 -2
  14. package/dist/util/useStorageUploadController.d.ts +2 -2
  15. package/package.json +6 -6
  16. package/src/components/BootstrapAdminBanner.tsx +12 -3
  17. package/src/components/LoginView/LoginView.tsx +177 -10
  18. package/src/components/UserSettingsView.tsx +95 -2
  19. package/src/components/common/types.tsx +7 -7
  20. package/src/components/common/useDebouncedData.ts +2 -2
  21. package/src/core/Rebase.tsx +3 -2
  22. package/src/core/RebaseProps.tsx +15 -2
  23. package/src/core/RebaseRouter.tsx +1 -1
  24. package/src/hooks/data/useCollectionFetch.tsx +27 -4
  25. package/src/hooks/data/useUserSelector.tsx +1 -1
  26. package/src/hooks/index.tsx +0 -1
  27. package/src/hooks/useResolvedComponent.tsx +4 -3
  28. package/src/locales/en.ts +13 -0
  29. package/src/locales/es.ts +11 -1
  30. package/src/util/entity_cache.ts +1 -27
  31. package/src/util/icon_list.ts +2 -2
  32. package/src/util/index.ts +2 -2
  33. package/src/util/previews.ts +9 -1
  34. package/src/util/useStorageUploadController.tsx +4 -4
  35. package/dist/hooks/useValidateAuthenticator.d.ts +0 -21
  36. package/dist/util/icon_synonyms.d.ts +0 -1
  37. package/dist/util/useTraceUpdate.d.ts +0 -2
  38. package/src/hooks/useValidateAuthenticator.tsx +0 -116
  39. package/src/util/icon_synonyms.ts +0 -1
  40. 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[];
@@ -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?: any;
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?: any, uploadedUrl?: string) => Promise<void>;
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.3",
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/formex": "0.2.3",
57
- "@rebasepro/types": "0.2.3",
58
- "@rebasepro/ui": "0.2.3",
59
- "@rebasepro/common": "0.2.3",
60
- "@rebasepro/utils": "0.2.3"
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
- const { users, loading: delegateLoading, bootstrapAdmin, usersError } = userManagement;
31
- const hasAdmin = users.some(u => u.roles?.includes("admin"));
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 || hasAdmin || usersError || !bootstrapAdmin) {
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 Authenticator configuration";
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-800",
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-[480px] max-w-full p-8 sm:p-10">
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-32 h-32 m-2 mb-6">
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 [activeTab, setActiveTab] = useState<"profile" | "sessions">("profile");
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 "profile" | "sessions")} className="mb-8">
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, any>> = {
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<any>;
9
+ selectedCell?: SelectedCellProps;
10
10
  /**
11
11
  * Store used to sync selection state across cells efficiently.
12
12
  */
13
- selectionStore?: any;
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<any, M>) => void;
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 = any, D = any> {
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: any,
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, any>> = (params: OnCellValueChangeParams<T, M>) => Promise<void> | void;
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: any, timeoutMs = 5000) {
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: any;
35
+ let handler: ReturnType<typeof setTimeout> | undefined;
36
36
  if (immediateUpdate)
37
37
  performUpdate()
38
38
  else
@@ -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}>