@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.
- package/dist/api.d.ts +5 -1
- package/dist/hooks/useBackendUserManagement.d.ts +5 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.es.js +96 -74
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +96 -74
- package/dist/index.umd.js.map +1 -1
- package/dist/types.d.ts +10 -1
- package/package.json +35 -4
- package/src/api.ts +5 -1
- package/src/hooks/useBackendUserManagement.ts +121 -86
- package/src/hooks/useRebaseAuthController.ts +14 -10
- package/src/index.ts +1 -1
- package/src/types.ts +8 -1
|
@@ -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?:
|
|
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
|
-
//
|
|
131
|
-
//
|
|
132
|
-
const [
|
|
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
|
|
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<
|
|
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
|
-
*
|
|
280
|
+
* Lightweight admin-existence check: fetch a single admin user.
|
|
281
|
+
* Used by the BootstrapAdminBanner to decide whether to show.
|
|
257
282
|
*/
|
|
258
|
-
const
|
|
283
|
+
const checkAdminExists = useCallback(async (signal?: AbortSignal) => {
|
|
259
284
|
try {
|
|
260
|
-
|
|
261
|
-
const
|
|
262
|
-
|
|
263
|
-
|
|
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
|
|
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
|
|
320
|
-
//
|
|
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
|
-
//
|
|
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
|
|
335
|
-
|
|
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
|
|
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:
|
|
415
|
+
users: converted,
|
|
381
416
|
total: data.total
|
|
382
417
|
};
|
|
383
|
-
}, [apiRequest]);
|
|
418
|
+
}, [apiRequest, mergeIntoCache]);
|
|
384
419
|
|
|
385
420
|
/**
|
|
386
|
-
* Save user (
|
|
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
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
511
|
-
}, [
|
|
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
|
-
//
|
|
518
|
-
|
|
519
|
-
|
|
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
|
-
}, [
|
|
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
|
|
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
|
|
573
|
+
await checkAdminExists();
|
|
543
574
|
} catch (error) {
|
|
544
575
|
console.error("Failed to bootstrap admin:", error);
|
|
545
576
|
throw error;
|
|
546
577
|
}
|
|
547
|
-
}, [apiRequest,
|
|
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 = "
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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:
|
|
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,
|
|
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?:
|
|
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
|
/**
|