@rebasepro/auth 0.2.1 → 0.2.4

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.
@@ -7,6 +7,7 @@ import { Role, User } from "@rebasepro/types";
7
7
  */
8
8
  export interface UserManagement<USER extends User = User> {
9
9
  loading: boolean;
10
+ hasAdminUsers?: boolean;
10
11
 
11
12
  users: USER[];
12
13
  saveUser: (user: USER) => Promise<USER>;
@@ -28,7 +29,6 @@ export interface UserManagement<USER extends User = User> {
28
29
 
29
30
  isAdmin?: boolean;
30
31
  allowDefaultRolesCreation?: boolean;
31
- includeCollectionConfigPermissions?: boolean;
32
32
  defineRolesFor: (user: User) => Promise<Role[] | undefined> | Role[] | undefined;
33
33
  getUser: (uid: string) => User | null;
34
34
 
@@ -55,7 +55,7 @@ export interface BackendUserManagementConfig {
55
55
  /**
56
56
  * The Rebase Client instance
57
57
  */
58
- client?: any;
58
+ client?: { baseUrl?: string; resolveToken?: () => Promise<string | null> };
59
59
 
60
60
  /**
61
61
  * Base API URL for the backend (optional, extracted from client if not provided)
@@ -87,13 +87,17 @@ interface ApiRole {
87
87
  id: string;
88
88
  name: string;
89
89
  isAdmin?: boolean;
90
- config?: Record<string, any>;
91
90
  }
92
91
 
92
+ /** Response shapes from the admin API */
93
+ interface ApiRolesResponse { roles: ApiRole[] }
94
+ interface ApiUsersResponse { users: ApiUser[]; total: number }
95
+ interface ApiUserResponse { user: ApiUser; invitationSent?: boolean; temporaryPassword?: string }
96
+ interface ApiRoleResponse { role: ApiRole }
97
+
93
98
  /**
94
99
  * Convert API user to Rebase User
95
100
  * @param apiUser - The API user object
96
- * @param availableRoles - Optional array of available roles to look up names
97
101
  */
98
102
  function convertUser(apiUser: ApiUser): User {
99
103
  return {
@@ -115,8 +119,7 @@ function convertRole(apiRole: ApiRole): Role {
115
119
  return {
116
120
  id: apiRole.id,
117
121
  name: apiRole.name,
118
- isAdmin: apiRole.isAdmin ?? false,
119
- config: apiRole.config ?? undefined
122
+ isAdmin: apiRole.isAdmin ?? false
120
123
  };
121
124
  }
122
125
 
@@ -127,11 +130,19 @@ function convertRole(apiRole: ApiRole): Role {
127
130
  export function useBackendUserManagement(config: BackendUserManagementConfig): UserManagement {
128
131
  const { client, apiUrl, getAuthToken, currentUser } = config;
129
132
 
130
- // We no longer load ALL users into memory.
131
- // `users` now only holds admin/role-bearing users for getUser/defineRolesFor lookups.
132
- const [users, setUsers] = useState<User[]>([]);
133
+ // Lazy user cache populated on demand from search results, saves, and
134
+ // individual API lookups. We never load ALL users into memory.
135
+ const [userCache, setUserCache] = useState<Map<string, User>>(new Map());
136
+ const [hasAdminUsers, setHasAdminUsers] = useState(false);
133
137
  const [roles, setRoles] = useState<Role[]>([]);
134
- const [loading, setLoading] = useState(true);
138
+ const userRoles = currentUser?.roles ?? [];
139
+ const isUserAdmin = userRoles.some(r => r === "admin" || r === "schema-admin");
140
+
141
+ const [loading, setLoading] = useState(() => {
142
+ if (!currentUser) return false;
143
+ if (!isUserAdmin) return false;
144
+ return true;
145
+ });
135
146
  const [usersError, setUsersError] = useState<Error | undefined>();
136
147
  const [rolesError, setRolesError] = useState<Error | undefined>();
137
148
 
@@ -139,6 +150,19 @@ export function useBackendUserManagement(config: BackendUserManagementConfig): U
139
150
  // Prevents redundant refetches on React StrictMode double-mounts.
140
151
  const lastLoadedUidRef = useRef<string | null>(null);
141
152
 
153
+ const effectiveLoading = loading || !!(currentUser && isUserAdmin && lastLoadedUidRef.current !== currentUser.uid);
154
+
155
+ /** Merge one or more users into the cache without replacing the whole Map. */
156
+ const mergeIntoCache = useCallback((incoming: User[]) => {
157
+ setUserCache(prev => {
158
+ const next = new Map(prev);
159
+ for (const u of incoming) {
160
+ next.set(u.uid, u);
161
+ }
162
+ return next;
163
+ });
164
+ }, []);
165
+
142
166
  // Ref to hold the latest apiRequest so the initial-load effect doesn't
143
167
  // re-trigger every time the callback identity changes.
144
168
  const apiRequestRef = useRef<typeof apiRequest | null>(null);
@@ -146,13 +170,13 @@ export function useBackendUserManagement(config: BackendUserManagementConfig): U
146
170
  /**
147
171
  * Make authenticated API request
148
172
  */
149
- const apiRequest = useCallback(async (
173
+ const apiRequest = useCallback(async <T = Record<string, unknown>>(
150
174
  endpoint: string,
151
175
  method = "GET",
152
176
  body?: Record<string, unknown>,
153
177
  retryCount = 6,
154
178
  signal?: AbortSignal
155
- ): Promise<any> => {
179
+ ): Promise<T> => {
156
180
  let lastError: Error | null = null;
157
181
  for (let attempt = 0; attempt < retryCount; attempt++) {
158
182
  if (signal?.aborted) {
@@ -163,7 +187,7 @@ export function useBackendUserManagement(config: BackendUserManagementConfig): U
163
187
 
164
188
  try {
165
189
  // Determine token provider
166
- const token = getAuthToken ? await getAuthToken() : (client ? await client.resolveToken() : null);
190
+ const token = getAuthToken ? await getAuthToken() : (client?.resolveToken ? await client.resolveToken() : null);
167
191
  const baseUrl = apiUrl || (client?.baseUrl ? client.baseUrl : "");
168
192
 
169
193
  // Use /api/admin prefix for admin endpoints
@@ -242,7 +266,7 @@ export function useBackendUserManagement(config: BackendUserManagementConfig): U
242
266
  */
243
267
  const loadRoles = useCallback(async (signal?: AbortSignal) => {
244
268
  try {
245
- const data = await apiRequest("/roles", "GET", undefined, 6, signal);
269
+ const data = await apiRequest<ApiRolesResponse>("/roles", "GET", undefined, 6, signal);
246
270
  setRoles(data.roles.map(convertRole));
247
271
  setRolesError(undefined);
248
272
  } catch (error: unknown) {
@@ -253,21 +277,25 @@ export function useBackendUserManagement(config: BackendUserManagementConfig): U
253
277
  }, [apiRequest]);
254
278
 
255
279
  /**
256
- * Load users for getUser/defineRolesFor lookups and for UserSelect dropdowns.
280
+ * Lightweight admin-existence check: fetch a single admin user.
281
+ * Used by the BootstrapAdminBanner to decide whether to show.
257
282
  */
258
- const loadUsers = useCallback(async (signal?: AbortSignal) => {
283
+ const checkAdminExists = useCallback(async (signal?: AbortSignal) => {
259
284
  try {
260
- // Load all users to satisfy Rebase CMS UserSelect field bindings
261
- const data = await apiRequest("/users", "GET", undefined, 6, signal);
262
- const allUsers: User[] = data.users.map((u: ApiUser) => convertUser(u));
263
- setUsers(allUsers);
285
+ const data = await apiRequest<ApiUsersResponse>("/users?role=admin&limit=1", "GET", undefined, 6, signal);
286
+ const adminUsers: User[] = data.users.map((u: ApiUser) => convertUser(u));
287
+ setHasAdminUsers(adminUsers.length > 0);
288
+ // Also cache these admin users for getUser lookups
289
+ if (adminUsers.length > 0) {
290
+ mergeIntoCache(adminUsers);
291
+ }
264
292
  setUsersError(undefined);
265
293
  } catch (error: unknown) {
266
294
  if (error instanceof Error && error.name === "AbortError") return;
267
- console.error("Failed to load users:", error);
295
+ console.error("Failed to check admin users:", error);
268
296
  setUsersError(error instanceof Error ? error : new Error(String(error)));
269
297
  }
270
- }, [apiRequest]);
298
+ }, [apiRequest, mergeIntoCache]);
271
299
 
272
300
  /**
273
301
  * Initial data load - only when user is logged in
@@ -308,7 +336,7 @@ export function useBackendUserManagement(config: BackendUserManagementConfig): U
308
336
 
309
337
  // Load roles first
310
338
  try {
311
- const data = await request("/roles", "GET", undefined, 6, abortController.signal);
339
+ const data = await request<ApiRolesResponse>("/roles", "GET", undefined, 6, abortController.signal);
312
340
  setRoles(data.roles.map(convertRole));
313
341
  setRolesError(undefined);
314
342
  } catch (error: unknown) {
@@ -316,9 +344,9 @@ export function useBackendUserManagement(config: BackendUserManagementConfig): U
316
344
  console.error("Failed to load roles:", error);
317
345
  setRolesError(error instanceof Error ? error : new Error(String(error)));
318
346
 
319
- // If the error is a permission issue (e.g. 403), skip loading
320
- // usersthey will fail with the same error and we'd show a
321
- // duplicate snackbar / error message.
347
+ // If the error is a permission issue (e.g. 403), skip the
348
+ // admin check it will fail with the same error and we'd
349
+ // show a duplicate snackbar / error message.
322
350
  const status = (error as { status?: number }).status;
323
351
  if (status === 403 || status === 401) {
324
352
  setUsersError(error instanceof Error ? error : new Error(String(error)));
@@ -327,16 +355,19 @@ export function useBackendUserManagement(config: BackendUserManagementConfig): U
327
355
  }
328
356
  }
329
357
 
330
- // Then load all users if not aborted
358
+ // Lightweight admin-existence check (NOT loading all users)
331
359
  if (!abortController.signal.aborted) {
332
360
  try {
333
- const data = await request("/users", "GET", undefined, 6, abortController.signal);
334
- const allUsers: User[] = data.users.map((u: ApiUser) => convertUser(u));
335
- setUsers(allUsers);
361
+ const data = await request<ApiUsersResponse>("/users?role=admin&limit=1", "GET", undefined, 6, abortController.signal);
362
+ const adminUsers: User[] = data.users.map((u: ApiUser) => convertUser(u));
363
+ setHasAdminUsers(adminUsers.length > 0);
364
+ if (adminUsers.length > 0) {
365
+ mergeIntoCache(adminUsers);
366
+ }
336
367
  setUsersError(undefined);
337
368
  } catch (error: unknown) {
338
369
  if (error instanceof Error && error.name === "AbortError") return;
339
- console.error("Failed to load users:", error);
370
+ console.error("Failed to check admin users:", error);
340
371
  setUsersError(error instanceof Error ? error : new Error(String(error)));
341
372
  }
342
373
  }
@@ -357,6 +388,7 @@ export function useBackendUserManagement(config: BackendUserManagementConfig): U
357
388
  /**
358
389
  * Search users with server-side pagination.
359
390
  * This is the primary method used by the UsersView table.
391
+ * Results are also merged into the cache for getUser lookups.
360
392
  */
361
393
  const searchUsers = useCallback(async (options: {
362
394
  search?: string;
@@ -375,44 +407,31 @@ export function useBackendUserManagement(config: BackendUserManagementConfig): U
375
407
  if (options.roleId) params.set("role", options.roleId);
376
408
  const qs = params.toString();
377
409
 
378
- const data = await apiRequest("/users" + (qs ? "?" + qs : ""), "GET");
410
+ const data = await apiRequest<ApiUsersResponse>("/users" + (qs ? "?" + qs : ""), "GET");
411
+ const converted = data.users.map((u: ApiUser) => convertUser(u));
412
+ // Feed search results into cache for getUser/defineRolesFor
413
+ mergeIntoCache(converted);
379
414
  return {
380
- users: data.users.map((u: ApiUser) => convertUser(u)),
415
+ users: converted,
381
416
  total: data.total
382
417
  };
383
- }, [apiRequest]);
418
+ }, [apiRequest, mergeIntoCache]);
384
419
 
385
420
  /**
386
- * Save user (create or update)
421
+ * Save user (update existing user)
387
422
  */
388
423
  const saveUser = useCallback(async (user: User): Promise<User> => {
389
424
  const roleIds = user.roles ?? [];
390
425
 
391
- // Check if user exists
392
- const existingUser = users.find(u => u.uid === user.uid);
393
-
394
- if (existingUser) {
395
- // Update
396
- const data = await apiRequest(`/users/${user.uid}`, "PUT", {
397
- email: user.email,
398
- displayName: user.displayName,
399
- roles: roleIds
400
- });
401
- const updated = convertUser(data.user);
402
- setUsers(prev => prev.map(u => u.uid === updated.uid ? updated : u));
403
- return updated;
404
- } else {
405
- // Create
406
- const data = await apiRequest("/users", "POST", {
407
- email: user.email,
408
- displayName: user.displayName,
409
- roles: roleIds
410
- });
411
- const created = convertUser(data.user);
412
- setUsers(prev => [...prev, created]);
413
- return created;
414
- }
415
- }, [apiRequest, users, roles]);
426
+ const data = await apiRequest<ApiUserResponse>(`/users/${user.uid}`, "PUT", {
427
+ email: user.email,
428
+ displayName: user.displayName,
429
+ roles: roleIds
430
+ });
431
+ const updated = convertUser(data.user);
432
+ mergeIntoCache([updated]);
433
+ return updated;
434
+ }, [apiRequest, mergeIntoCache]);
416
435
 
417
436
  /**
418
437
  * Create a new user with invitation/password generation support.
@@ -425,20 +444,19 @@ export function useBackendUserManagement(config: BackendUserManagementConfig): U
425
444
  }> => {
426
445
  const roleIds = user.roles ?? [];
427
446
 
428
- const data = await apiRequest("/users", "POST", {
447
+ const data = await apiRequest<ApiUserResponse>("/users", "POST", {
429
448
  email: user.email,
430
449
  displayName: user.displayName,
431
450
  roles: roleIds
432
451
  });
433
452
  const created = convertUser(data.user);
434
- // Add to users cache
435
- setUsers(prev => [...prev, created]);
453
+ mergeIntoCache([created]);
436
454
  return {
437
455
  user: created,
438
456
  invitationSent: data.invitationSent ?? false,
439
457
  temporaryPassword: data.temporaryPassword
440
458
  };
441
- }, [apiRequest, roles]);
459
+ }, [apiRequest, mergeIntoCache]);
442
460
 
443
461
  /**
444
462
  * Reset the password for an existing user
@@ -448,22 +466,26 @@ export function useBackendUserManagement(config: BackendUserManagementConfig): U
448
466
  invitationSent: boolean;
449
467
  temporaryPassword?: string;
450
468
  }> => {
451
- const data = await apiRequest(`/users/${user.uid}/reset-password`, "POST");
469
+ const data = await apiRequest<ApiUserResponse>(`/users/${user.uid}/reset-password`, "POST");
452
470
  const updatedUser = convertUser(data.user);
453
- setUsers(prev => prev.map(u => u.uid === updatedUser.uid ? updatedUser : u));
471
+ mergeIntoCache([updatedUser]);
454
472
  return {
455
473
  user: updatedUser,
456
474
  invitationSent: data.invitationSent ?? false,
457
475
  temporaryPassword: data.temporaryPassword
458
476
  };
459
- }, [apiRequest]);
477
+ }, [apiRequest, mergeIntoCache]);
460
478
 
461
479
  /**
462
480
  * Delete user
463
481
  */
464
482
  const deleteUser = useCallback(async (user: User): Promise<void> => {
465
483
  await apiRequest(`/users/${user.uid}`, "DELETE");
466
- setUsers(prev => prev.filter(u => u.uid !== user.uid));
484
+ setUserCache(prev => {
485
+ const next = new Map(prev);
486
+ next.delete(user.uid);
487
+ return next;
488
+ });
467
489
  }, [apiRequest]);
468
490
 
469
491
  /**
@@ -475,20 +497,18 @@ export function useBackendUserManagement(config: BackendUserManagementConfig): U
475
497
 
476
498
  if (existingRole) {
477
499
  // Update
478
- const data = await apiRequest(`/roles/${role.id}`, "PUT", {
500
+ const data = await apiRequest<ApiRoleResponse>(`/roles/${role.id}`, "PUT", {
479
501
  name: role.name,
480
- isAdmin: role.isAdmin,
481
- config: role.config
502
+ isAdmin: role.isAdmin
482
503
  });
483
504
  const updated = convertRole(data.role);
484
505
  setRoles(prev => prev.map(r => r.id === updated.id ? updated : r));
485
506
  } else {
486
507
  // Create
487
- const data = await apiRequest("/roles", "POST", {
508
+ const data = await apiRequest<ApiRoleResponse>("/roles", "POST", {
488
509
  id: role.id,
489
510
  name: role.name,
490
- isAdmin: role.isAdmin ?? false,
491
- config: role.config
511
+ isAdmin: role.isAdmin ?? false
492
512
  });
493
513
  const created = convertRole(data.role);
494
514
  setRoles(prev => [...prev, created]);
@@ -507,21 +527,32 @@ export function useBackendUserManagement(config: BackendUserManagementConfig): U
507
527
  * Get user by uid
508
528
  */
509
529
  const getUser = useCallback((uid: string): User | null => {
510
- return users.find(u => u.uid === uid) ?? null;
511
- }, [users]);
530
+ return userCache.get(uid) ?? null;
531
+ }, [userCache]);
512
532
 
513
533
  /**
514
534
  * Define roles for a given user (for authController)
515
535
  */
516
536
  const defineRolesFor = useCallback(async (user: User): Promise<Role[] | undefined> => {
517
- // Find the user in our list
518
- const existingUser = users.find(u => u.uid === user.uid || u.email === user.email);
519
- if (!existingUser) return undefined;
537
+ // Check cache first
538
+ let existingUser = userCache.get(user.uid)
539
+ ?? Array.from(userCache.values()).find(u => u.email === user.email);
540
+
541
+ // If not cached, fetch from API
542
+ if (!existingUser) {
543
+ try {
544
+ const data = await apiRequest<ApiUserResponse>(`/users/${user.uid}`, "GET");
545
+ existingUser = convertUser(data.user);
546
+ mergeIntoCache([existingUser]);
547
+ } catch {
548
+ return undefined;
549
+ }
550
+ }
520
551
 
521
552
  // Return roles from our cached role data (string IDs → full Role objects)
522
553
  const userRoleIds = existingUser.roles ?? [];
523
554
  return roles.filter(r => userRoleIds.includes(r.id));
524
- }, [users, roles]);
555
+ }, [userCache, roles, apiRequest, mergeIntoCache]);
525
556
 
526
557
  /**
527
558
  * Check if current user is admin
@@ -535,20 +566,25 @@ export function useBackendUserManagement(config: BackendUserManagementConfig): U
535
566
  const bootstrapAdmin = useCallback(async (): Promise<void> => {
536
567
  try {
537
568
  await apiRequest("/bootstrap", "POST");
538
- // Reload users and roles after successful bootstrap
539
- const data = await apiRequest("/roles");
569
+ // Reload roles and re-check admin existence after successful bootstrap
570
+ const data = await apiRequest<ApiRolesResponse>("/roles");
540
571
  const loadedRoles = data.roles.map(convertRole);
541
572
  setRoles(loadedRoles);
542
- await loadUsers();
573
+ await checkAdminExists();
543
574
  } catch (error) {
544
575
  console.error("Failed to bootstrap admin:", error);
545
576
  throw error;
546
577
  }
547
- }, [apiRequest, loadUsers]);
578
+ }, [apiRequest, checkAdminExists]);
579
+
580
+ // Expose cached users as an array for backward compat (BootstrapAdminBanner,
581
+ // UsersView fallback). This is NOT the full user list — just the cache.
582
+ const users = Array.from(userCache.values());
548
583
 
549
584
  return {
550
- loading,
585
+ loading: effectiveLoading,
551
586
  users,
587
+ hasAdminUsers,
552
588
  saveUser,
553
589
  createUser,
554
590
  resetPassword,
@@ -558,7 +594,6 @@ export function useBackendUserManagement(config: BackendUserManagementConfig): U
558
594
  deleteRole,
559
595
  isAdmin,
560
596
  allowDefaultRolesCreation: isAdmin,
561
- includeCollectionConfigPermissions: true,
562
597
  defineRolesFor,
563
598
  getUser,
564
599
  searchUsers,
@@ -9,7 +9,7 @@ import {
9
9
  UserInfo
10
10
  } from "../types";
11
11
 
12
- const STORAGE_KEY = "rebase_auth";
12
+ const STORAGE_KEY = "rebase_react_auth";
13
13
 
14
14
  // Buffer time before expiry to trigger refresh (2 minutes)
15
15
  const TOKEN_REFRESH_BUFFER_MS = 2 * 60 * 1000;
@@ -25,7 +25,8 @@ function convertToUser(userInfo: UserInfo): User {
25
25
  photoURL: userInfo.photoURL || null,
26
26
  providerId: "custom",
27
27
  isAnonymous: false,
28
- roles: userInfo.roles || []
28
+ roles: userInfo.roles || [],
29
+ metadata: userInfo.metadata
29
30
  };
30
31
  }
31
32
 
@@ -112,12 +113,11 @@ export function useRebaseAuthController(
112
113
 
113
114
  // Configure API URL on mount
114
115
  useEffect(() => {
115
- if (client) {
116
- authApi.setApiUrl(client.baseUrl);
117
- } else if (apiUrl) {
118
- authApi.setApiUrl(apiUrl);
116
+ const url = client?.baseUrl || apiUrl;
117
+ if (url) {
118
+ authApi.setApiUrl(url);
119
119
  }
120
- }, [client, apiUrl]);
120
+ }, [client, client?.baseUrl, apiUrl]);
121
121
 
122
122
  const clearError = useCallback(() => {
123
123
  setAuthProviderError(null);
@@ -280,7 +280,7 @@ export function useRebaseAuthController(
280
280
  // Install token getter onto client
281
281
  useEffect(() => {
282
282
  if (client) {
283
- client.setAuthTokenGetter(async () => {
283
+ client.setAuthTokenGetter?.(async () => {
284
284
  try { return await getAuthToken(); } catch { return null; }
285
285
  });
286
286
  if (client.setOnUnauthorized) {
@@ -624,6 +624,10 @@ roles: customRoles.map(r => r.id) };
624
624
  }
625
625
  } catch (meError: unknown) {
626
626
  if (!isMountedRef.current) return;
627
+ if (meError instanceof authApi.AuthApiError && (meError.code === "NOT_FOUND" || meError.code === "UNAUTHORIZED")) {
628
+ clearSessionAndSignOut();
629
+ return;
630
+ }
627
631
  userToSet = convertToUser(stored.user);
628
632
  }
629
633
 
@@ -743,10 +747,10 @@ roles: customRoles.map(r => r.id) };
743
747
  emailPasswordLogin: true,
744
748
  googleLogin: !!(props.googleClientId),
745
749
  registration: authConfig?.registrationEnabled ?? false,
746
- passwordReset: true,
750
+ passwordReset: authConfig?.passwordReset ?? false,
747
751
  sessionManagement: true,
748
752
  profileUpdate: true,
749
- emailVerification: false,
753
+ emailVerification: authConfig?.emailVerification ?? false,
750
754
  enabledProviders: authConfig?.enabledProviders ?? []
751
755
  }
752
756
  };
package/src/index.ts CHANGED
@@ -22,5 +22,5 @@ export { useBackendUserManagement } from "./hooks/useBackendUserManagement";
22
22
  export type { BackendUserManagementConfig, UserManagement } from "./hooks/useBackendUserManagement";
23
23
 
24
24
  // API utilities
25
- export { setApiUrl, getApiUrl, fetchAuthConfig, clearAuthConfigCache, AuthApiError } from "./api";
25
+ export { setApiUrl, getApiUrl, fetchAuthConfig, AuthApiError } from "./api";
26
26
  export type { AuthConfigResponse } from "./api";
package/src/types.ts CHANGED
@@ -50,7 +50,13 @@ export type RebaseAuthController = AuthController & {
50
50
  */
51
51
  export interface RebaseAuthControllerProps {
52
52
  /** The Rebase Client instance */
53
- client?: any;
53
+ client?: {
54
+ baseUrl?: string;
55
+ resolveToken?: () => Promise<string | null>;
56
+ setAuthTokenGetter?: (getter: () => Promise<string | null>) => void;
57
+ setOnUnauthorized?: (handler: () => Promise<boolean>) => void;
58
+ ws?: { setAuthTokenGetter: (getter: () => Promise<string>) => void };
59
+ };
54
60
  /** Base URL of the backend API */
55
61
  apiUrl?: string;
56
62
  /** Google OAuth client ID (optional, enables Google login) */
@@ -81,6 +87,7 @@ export interface UserInfo {
81
87
  photoURL?: string | null;
82
88
  emailVerified?: boolean;
83
89
  roles?: string[];
90
+ metadata?: Record<string, unknown>;
84
91
  }
85
92
 
86
93
  /**