@rebasepro/auth 0.2.3 → 0.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api.d.ts +5 -1
- package/dist/hooks/useBackendUserManagement.d.ts +7 -8
- package/dist/index.d.ts +1 -1
- package/dist/index.es.js +96 -140
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +96 -140
- package/dist/index.umd.js.map +1 -1
- package/dist/types.d.ts +11 -3
- package/package.json +30 -4
- package/src/api.ts +5 -1
- package/src/hooks/useBackendUserManagement.ts +113 -178
- package/src/hooks/useRebaseAuthController.ts +16 -13
- package/src/index.ts +1 -1
- package/src/types.ts +9 -3
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
|
-
import {
|
|
2
|
+
import type { User } from "@rebasepro/types";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* UserManagement interface - compatible with @rebasepro/user_management
|
|
@@ -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>;
|
|
@@ -22,14 +23,9 @@ export interface UserManagement<USER extends User = User> {
|
|
|
22
23
|
}>;
|
|
23
24
|
deleteUser: (user: USER) => Promise<void>;
|
|
24
25
|
|
|
25
|
-
roles: Role[];
|
|
26
|
-
saveRole: (role: Role) => Promise<void>;
|
|
27
|
-
deleteRole: (role: Role) => Promise<void>;
|
|
28
|
-
|
|
29
26
|
isAdmin?: boolean;
|
|
30
27
|
allowDefaultRolesCreation?: boolean;
|
|
31
|
-
|
|
32
|
-
defineRolesFor: (user: User) => Promise<Role[] | undefined> | Role[] | undefined;
|
|
28
|
+
defineRolesFor: (user: User) => Promise<string[] | undefined> | string[] | undefined;
|
|
33
29
|
getUser: (uid: string) => User | null;
|
|
34
30
|
|
|
35
31
|
/**
|
|
@@ -47,7 +43,6 @@ export interface UserManagement<USER extends User = User> {
|
|
|
47
43
|
}) => Promise<{ users: USER[]; total: number }>;
|
|
48
44
|
|
|
49
45
|
usersError?: Error;
|
|
50
|
-
rolesError?: Error;
|
|
51
46
|
bootstrapAdmin?: () => Promise<void>;
|
|
52
47
|
}
|
|
53
48
|
|
|
@@ -55,7 +50,7 @@ export interface BackendUserManagementConfig {
|
|
|
55
50
|
/**
|
|
56
51
|
* The Rebase Client instance
|
|
57
52
|
*/
|
|
58
|
-
client?:
|
|
53
|
+
client?: { baseUrl?: string; resolveToken?: () => Promise<string | null> };
|
|
59
54
|
|
|
60
55
|
/**
|
|
61
56
|
* Base API URL for the backend (optional, extracted from client if not provided)
|
|
@@ -83,17 +78,13 @@ interface ApiUser {
|
|
|
83
78
|
updatedAt?: string;
|
|
84
79
|
}
|
|
85
80
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
isAdmin?: boolean;
|
|
90
|
-
config?: Record<string, any>;
|
|
91
|
-
}
|
|
81
|
+
/** Response shapes from the admin API */
|
|
82
|
+
interface ApiUsersResponse { users: ApiUser[]; total: number }
|
|
83
|
+
interface ApiUserResponse { user: ApiUser; invitationSent?: boolean; temporaryPassword?: string }
|
|
92
84
|
|
|
93
85
|
/**
|
|
94
86
|
* Convert API user to Rebase User
|
|
95
87
|
* @param apiUser - The API user object
|
|
96
|
-
* @param availableRoles - Optional array of available roles to look up names
|
|
97
88
|
*/
|
|
98
89
|
function convertUser(apiUser: ApiUser): User {
|
|
99
90
|
return {
|
|
@@ -108,18 +99,6 @@ function convertUser(apiUser: ApiUser): User {
|
|
|
108
99
|
} as User;
|
|
109
100
|
}
|
|
110
101
|
|
|
111
|
-
/**
|
|
112
|
-
* Convert API role to Rebase Role
|
|
113
|
-
*/
|
|
114
|
-
function convertRole(apiRole: ApiRole): Role {
|
|
115
|
-
return {
|
|
116
|
-
id: apiRole.id,
|
|
117
|
-
name: apiRole.name,
|
|
118
|
-
isAdmin: apiRole.isAdmin ?? false,
|
|
119
|
-
config: apiRole.config ?? undefined
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
|
-
|
|
123
102
|
/**
|
|
124
103
|
* Hook to manage users and roles via backend API
|
|
125
104
|
* Compatible with Rebase UserManagement interface
|
|
@@ -127,18 +106,37 @@ function convertRole(apiRole: ApiRole): Role {
|
|
|
127
106
|
export function useBackendUserManagement(config: BackendUserManagementConfig): UserManagement {
|
|
128
107
|
const { client, apiUrl, getAuthToken, currentUser } = config;
|
|
129
108
|
|
|
130
|
-
//
|
|
131
|
-
//
|
|
132
|
-
const [
|
|
133
|
-
const [
|
|
134
|
-
const
|
|
109
|
+
// Lazy user cache — populated on demand from search results, saves, and
|
|
110
|
+
// individual API lookups. We never load ALL users into memory.
|
|
111
|
+
const [userCache, setUserCache] = useState<Map<string, User>>(new Map());
|
|
112
|
+
const [hasAdminUsers, setHasAdminUsers] = useState(false);
|
|
113
|
+
const userRoles = currentUser?.roles ?? [];
|
|
114
|
+
const isUserAdmin = userRoles.some(r => r === "admin" || r === "schema-admin");
|
|
115
|
+
|
|
116
|
+
const [loading, setLoading] = useState(() => {
|
|
117
|
+
if (!currentUser) return false;
|
|
118
|
+
if (!isUserAdmin) return false;
|
|
119
|
+
return true;
|
|
120
|
+
});
|
|
135
121
|
const [usersError, setUsersError] = useState<Error | undefined>();
|
|
136
|
-
const [rolesError, setRolesError] = useState<Error | undefined>();
|
|
137
122
|
|
|
138
123
|
// Tracks the UID for which roles+users were last successfully loaded.
|
|
139
124
|
// Prevents redundant refetches on React StrictMode double-mounts.
|
|
140
125
|
const lastLoadedUidRef = useRef<string | null>(null);
|
|
141
126
|
|
|
127
|
+
const effectiveLoading = loading || !!(currentUser && isUserAdmin && lastLoadedUidRef.current !== currentUser.uid);
|
|
128
|
+
|
|
129
|
+
/** Merge one or more users into the cache without replacing the whole Map. */
|
|
130
|
+
const mergeIntoCache = useCallback((incoming: User[]) => {
|
|
131
|
+
setUserCache(prev => {
|
|
132
|
+
const next = new Map(prev);
|
|
133
|
+
for (const u of incoming) {
|
|
134
|
+
next.set(u.uid, u);
|
|
135
|
+
}
|
|
136
|
+
return next;
|
|
137
|
+
});
|
|
138
|
+
}, []);
|
|
139
|
+
|
|
142
140
|
// Ref to hold the latest apiRequest so the initial-load effect doesn't
|
|
143
141
|
// re-trigger every time the callback identity changes.
|
|
144
142
|
const apiRequestRef = useRef<typeof apiRequest | null>(null);
|
|
@@ -146,13 +144,13 @@ export function useBackendUserManagement(config: BackendUserManagementConfig): U
|
|
|
146
144
|
/**
|
|
147
145
|
* Make authenticated API request
|
|
148
146
|
*/
|
|
149
|
-
const apiRequest = useCallback(async (
|
|
147
|
+
const apiRequest = useCallback(async <T = Record<string, unknown>>(
|
|
150
148
|
endpoint: string,
|
|
151
149
|
method = "GET",
|
|
152
150
|
body?: Record<string, unknown>,
|
|
153
151
|
retryCount = 6,
|
|
154
152
|
signal?: AbortSignal
|
|
155
|
-
): Promise<
|
|
153
|
+
): Promise<T> => {
|
|
156
154
|
let lastError: Error | null = null;
|
|
157
155
|
for (let attempt = 0; attempt < retryCount; attempt++) {
|
|
158
156
|
if (signal?.aborted) {
|
|
@@ -163,7 +161,7 @@ export function useBackendUserManagement(config: BackendUserManagementConfig): U
|
|
|
163
161
|
|
|
164
162
|
try {
|
|
165
163
|
// Determine token provider
|
|
166
|
-
const token = getAuthToken ? await getAuthToken() : (client ? await client.resolveToken() : null);
|
|
164
|
+
const token = getAuthToken ? await getAuthToken() : (client?.resolveToken ? await client.resolveToken() : null);
|
|
167
165
|
const baseUrl = apiUrl || (client?.baseUrl ? client.baseUrl : "");
|
|
168
166
|
|
|
169
167
|
// Use /api/admin prefix for admin endpoints
|
|
@@ -237,37 +235,28 @@ export function useBackendUserManagement(config: BackendUserManagementConfig): U
|
|
|
237
235
|
// Keep the ref in sync after every render.
|
|
238
236
|
apiRequestRef.current = apiRequest;
|
|
239
237
|
|
|
240
|
-
|
|
241
|
-
* Load roles from API
|
|
242
|
-
*/
|
|
243
|
-
const loadRoles = useCallback(async (signal?: AbortSignal) => {
|
|
244
|
-
try {
|
|
245
|
-
const data = await apiRequest("/roles", "GET", undefined, 6, signal);
|
|
246
|
-
setRoles(data.roles.map(convertRole));
|
|
247
|
-
setRolesError(undefined);
|
|
248
|
-
} catch (error: unknown) {
|
|
249
|
-
if (error instanceof Error && error.name === "AbortError") return;
|
|
250
|
-
console.error("Failed to load roles:", error);
|
|
251
|
-
setRolesError(error instanceof Error ? error : new Error(String(error)));
|
|
252
|
-
}
|
|
253
|
-
}, [apiRequest]);
|
|
238
|
+
|
|
254
239
|
|
|
255
240
|
/**
|
|
256
|
-
*
|
|
241
|
+
* Lightweight admin-existence check: fetch a single admin user.
|
|
242
|
+
* Used by the BootstrapAdminBanner to decide whether to show.
|
|
257
243
|
*/
|
|
258
|
-
const
|
|
244
|
+
const checkAdminExists = useCallback(async (signal?: AbortSignal) => {
|
|
259
245
|
try {
|
|
260
|
-
|
|
261
|
-
const
|
|
262
|
-
|
|
263
|
-
|
|
246
|
+
const data = await apiRequest<ApiUsersResponse>("/users?role=admin&limit=1", "GET", undefined, 6, signal);
|
|
247
|
+
const adminUsers: User[] = data.users.map((u: ApiUser) => convertUser(u));
|
|
248
|
+
setHasAdminUsers(adminUsers.length > 0);
|
|
249
|
+
// Also cache these admin users for getUser lookups
|
|
250
|
+
if (adminUsers.length > 0) {
|
|
251
|
+
mergeIntoCache(adminUsers);
|
|
252
|
+
}
|
|
264
253
|
setUsersError(undefined);
|
|
265
254
|
} catch (error: unknown) {
|
|
266
255
|
if (error instanceof Error && error.name === "AbortError") return;
|
|
267
|
-
console.error("Failed to
|
|
256
|
+
console.error("Failed to check admin users:", error);
|
|
268
257
|
setUsersError(error instanceof Error ? error : new Error(String(error)));
|
|
269
258
|
}
|
|
270
|
-
}, [apiRequest]);
|
|
259
|
+
}, [apiRequest, mergeIntoCache]);
|
|
271
260
|
|
|
272
261
|
/**
|
|
273
262
|
* Initial data load - only when user is logged in
|
|
@@ -306,37 +295,19 @@ export function useBackendUserManagement(config: BackendUserManagementConfig): U
|
|
|
306
295
|
setLoading(true);
|
|
307
296
|
const request = apiRequestRef.current!;
|
|
308
297
|
|
|
309
|
-
//
|
|
310
|
-
try {
|
|
311
|
-
const data = await request("/roles", "GET", undefined, 6, abortController.signal);
|
|
312
|
-
setRoles(data.roles.map(convertRole));
|
|
313
|
-
setRolesError(undefined);
|
|
314
|
-
} catch (error: unknown) {
|
|
315
|
-
if (error instanceof Error && error.name === "AbortError") return;
|
|
316
|
-
console.error("Failed to load roles:", error);
|
|
317
|
-
setRolesError(error instanceof Error ? error : new Error(String(error)));
|
|
318
|
-
|
|
319
|
-
// If the error is a permission issue (e.g. 403), skip loading
|
|
320
|
-
// users — they will fail with the same error and we'd show a
|
|
321
|
-
// duplicate snackbar / error message.
|
|
322
|
-
const status = (error as { status?: number }).status;
|
|
323
|
-
if (status === 403 || status === 401) {
|
|
324
|
-
setUsersError(error instanceof Error ? error : new Error(String(error)));
|
|
325
|
-
setLoading(false);
|
|
326
|
-
return;
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
// Then load all users if not aborted
|
|
298
|
+
// Lightweight admin-existence check (NOT loading all users)
|
|
331
299
|
if (!abortController.signal.aborted) {
|
|
332
300
|
try {
|
|
333
|
-
const data = await request("/users", "GET", undefined, 6, abortController.signal);
|
|
334
|
-
const
|
|
335
|
-
|
|
301
|
+
const data = await request<ApiUsersResponse>("/users?role=admin&limit=1", "GET", undefined, 6, abortController.signal);
|
|
302
|
+
const adminUsers: User[] = data.users.map((u: ApiUser) => convertUser(u));
|
|
303
|
+
setHasAdminUsers(adminUsers.length > 0);
|
|
304
|
+
if (adminUsers.length > 0) {
|
|
305
|
+
mergeIntoCache(adminUsers);
|
|
306
|
+
}
|
|
336
307
|
setUsersError(undefined);
|
|
337
308
|
} catch (error: unknown) {
|
|
338
309
|
if (error instanceof Error && error.name === "AbortError") return;
|
|
339
|
-
console.error("Failed to
|
|
310
|
+
console.error("Failed to check admin users:", error);
|
|
340
311
|
setUsersError(error instanceof Error ? error : new Error(String(error)));
|
|
341
312
|
}
|
|
342
313
|
}
|
|
@@ -357,6 +328,7 @@ export function useBackendUserManagement(config: BackendUserManagementConfig): U
|
|
|
357
328
|
/**
|
|
358
329
|
* Search users with server-side pagination.
|
|
359
330
|
* This is the primary method used by the UsersView table.
|
|
331
|
+
* Results are also merged into the cache for getUser lookups.
|
|
360
332
|
*/
|
|
361
333
|
const searchUsers = useCallback(async (options: {
|
|
362
334
|
search?: string;
|
|
@@ -375,44 +347,31 @@ export function useBackendUserManagement(config: BackendUserManagementConfig): U
|
|
|
375
347
|
if (options.roleId) params.set("role", options.roleId);
|
|
376
348
|
const qs = params.toString();
|
|
377
349
|
|
|
378
|
-
const data = await apiRequest("/users" + (qs ? "?" + qs : ""), "GET");
|
|
350
|
+
const data = await apiRequest<ApiUsersResponse>("/users" + (qs ? "?" + qs : ""), "GET");
|
|
351
|
+
const converted = data.users.map((u: ApiUser) => convertUser(u));
|
|
352
|
+
// Feed search results into cache for getUser/defineRolesFor
|
|
353
|
+
mergeIntoCache(converted);
|
|
379
354
|
return {
|
|
380
|
-
users:
|
|
355
|
+
users: converted,
|
|
381
356
|
total: data.total
|
|
382
357
|
};
|
|
383
|
-
}, [apiRequest]);
|
|
358
|
+
}, [apiRequest, mergeIntoCache]);
|
|
384
359
|
|
|
385
360
|
/**
|
|
386
|
-
* Save user (
|
|
361
|
+
* Save user (update existing user)
|
|
387
362
|
*/
|
|
388
363
|
const saveUser = useCallback(async (user: User): Promise<User> => {
|
|
389
364
|
const roleIds = user.roles ?? [];
|
|
390
365
|
|
|
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]);
|
|
366
|
+
const data = await apiRequest<ApiUserResponse>(`/users/${user.uid}`, "PUT", {
|
|
367
|
+
email: user.email,
|
|
368
|
+
displayName: user.displayName,
|
|
369
|
+
roles: roleIds
|
|
370
|
+
});
|
|
371
|
+
const updated = convertUser(data.user);
|
|
372
|
+
mergeIntoCache([updated]);
|
|
373
|
+
return updated;
|
|
374
|
+
}, [apiRequest, mergeIntoCache]);
|
|
416
375
|
|
|
417
376
|
/**
|
|
418
377
|
* Create a new user with invitation/password generation support.
|
|
@@ -425,20 +384,19 @@ export function useBackendUserManagement(config: BackendUserManagementConfig): U
|
|
|
425
384
|
}> => {
|
|
426
385
|
const roleIds = user.roles ?? [];
|
|
427
386
|
|
|
428
|
-
const data = await apiRequest("/users", "POST", {
|
|
387
|
+
const data = await apiRequest<ApiUserResponse>("/users", "POST", {
|
|
429
388
|
email: user.email,
|
|
430
389
|
displayName: user.displayName,
|
|
431
390
|
roles: roleIds
|
|
432
391
|
});
|
|
433
392
|
const created = convertUser(data.user);
|
|
434
|
-
|
|
435
|
-
setUsers(prev => [...prev, created]);
|
|
393
|
+
mergeIntoCache([created]);
|
|
436
394
|
return {
|
|
437
395
|
user: created,
|
|
438
396
|
invitationSent: data.invitationSent ?? false,
|
|
439
397
|
temporaryPassword: data.temporaryPassword
|
|
440
398
|
};
|
|
441
|
-
}, [apiRequest,
|
|
399
|
+
}, [apiRequest, mergeIntoCache]);
|
|
442
400
|
|
|
443
401
|
/**
|
|
444
402
|
* Reset the password for an existing user
|
|
@@ -448,80 +406,60 @@ export function useBackendUserManagement(config: BackendUserManagementConfig): U
|
|
|
448
406
|
invitationSent: boolean;
|
|
449
407
|
temporaryPassword?: string;
|
|
450
408
|
}> => {
|
|
451
|
-
const data = await apiRequest(`/users/${user.uid}/reset-password`, "POST");
|
|
409
|
+
const data = await apiRequest<ApiUserResponse>(`/users/${user.uid}/reset-password`, "POST");
|
|
452
410
|
const updatedUser = convertUser(data.user);
|
|
453
|
-
|
|
411
|
+
mergeIntoCache([updatedUser]);
|
|
454
412
|
return {
|
|
455
413
|
user: updatedUser,
|
|
456
414
|
invitationSent: data.invitationSent ?? false,
|
|
457
415
|
temporaryPassword: data.temporaryPassword
|
|
458
416
|
};
|
|
459
|
-
}, [apiRequest]);
|
|
417
|
+
}, [apiRequest, mergeIntoCache]);
|
|
460
418
|
|
|
461
419
|
/**
|
|
462
420
|
* Delete user
|
|
463
421
|
*/
|
|
464
422
|
const deleteUser = useCallback(async (user: User): Promise<void> => {
|
|
465
423
|
await apiRequest(`/users/${user.uid}`, "DELETE");
|
|
466
|
-
|
|
424
|
+
setUserCache(prev => {
|
|
425
|
+
const next = new Map(prev);
|
|
426
|
+
next.delete(user.uid);
|
|
427
|
+
return next;
|
|
428
|
+
});
|
|
467
429
|
}, [apiRequest]);
|
|
468
430
|
|
|
469
|
-
/**
|
|
470
|
-
* Save role (create or update)
|
|
471
|
-
*/
|
|
472
|
-
const saveRole = useCallback(async (role: Role): Promise<void> => {
|
|
473
|
-
// Check if role exists
|
|
474
|
-
const existingRole = roles.find(r => r.id === role.id);
|
|
475
|
-
|
|
476
|
-
if (existingRole) {
|
|
477
|
-
// Update
|
|
478
|
-
const data = await apiRequest(`/roles/${role.id}`, "PUT", {
|
|
479
|
-
name: role.name,
|
|
480
|
-
isAdmin: role.isAdmin,
|
|
481
|
-
config: role.config
|
|
482
|
-
});
|
|
483
|
-
const updated = convertRole(data.role);
|
|
484
|
-
setRoles(prev => prev.map(r => r.id === updated.id ? updated : r));
|
|
485
|
-
} else {
|
|
486
|
-
// Create
|
|
487
|
-
const data = await apiRequest("/roles", "POST", {
|
|
488
|
-
id: role.id,
|
|
489
|
-
name: role.name,
|
|
490
|
-
isAdmin: role.isAdmin ?? false,
|
|
491
|
-
config: role.config
|
|
492
|
-
});
|
|
493
|
-
const created = convertRole(data.role);
|
|
494
|
-
setRoles(prev => [...prev, created]);
|
|
495
|
-
}
|
|
496
|
-
}, [apiRequest, roles]);
|
|
497
431
|
|
|
498
|
-
/**
|
|
499
|
-
* Delete role
|
|
500
|
-
*/
|
|
501
|
-
const deleteRole = useCallback(async (role: Role): Promise<void> => {
|
|
502
|
-
await apiRequest(`/roles/${role.id}`, "DELETE");
|
|
503
|
-
setRoles(prev => prev.filter(r => r.id !== role.id));
|
|
504
|
-
}, [apiRequest]);
|
|
505
432
|
|
|
506
433
|
/**
|
|
507
434
|
* Get user by uid
|
|
508
435
|
*/
|
|
509
436
|
const getUser = useCallback((uid: string): User | null => {
|
|
510
|
-
return
|
|
511
|
-
}, [
|
|
437
|
+
return userCache.get(uid) ?? null;
|
|
438
|
+
}, [userCache]);
|
|
512
439
|
|
|
513
440
|
/**
|
|
514
441
|
* Define roles for a given user (for authController)
|
|
515
442
|
*/
|
|
516
|
-
const defineRolesFor = useCallback(async (user: User): Promise<
|
|
517
|
-
//
|
|
518
|
-
|
|
519
|
-
|
|
443
|
+
const defineRolesFor = useCallback(async (user: User): Promise<string[] | undefined> => {
|
|
444
|
+
// Check cache first
|
|
445
|
+
let existingUser = userCache.get(user.uid)
|
|
446
|
+
?? Array.from(userCache.values()).find(u => u.email === user.email);
|
|
447
|
+
|
|
448
|
+
// If not cached, fetch from API
|
|
449
|
+
if (!existingUser) {
|
|
450
|
+
try {
|
|
451
|
+
const data = await apiRequest<ApiUserResponse>(`/users/${user.uid}`, "GET");
|
|
452
|
+
existingUser = convertUser(data.user);
|
|
453
|
+
mergeIntoCache([existingUser]);
|
|
454
|
+
} catch {
|
|
455
|
+
return undefined;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
520
458
|
|
|
521
|
-
// Return
|
|
459
|
+
// Return role IDs as simple strings
|
|
522
460
|
const userRoleIds = existingUser.roles ?? [];
|
|
523
|
-
return
|
|
524
|
-
}, [
|
|
461
|
+
return userRoleIds;
|
|
462
|
+
}, [userCache, apiRequest, mergeIntoCache]);
|
|
525
463
|
|
|
526
464
|
/**
|
|
527
465
|
* Check if current user is admin
|
|
@@ -535,35 +473,32 @@ export function useBackendUserManagement(config: BackendUserManagementConfig): U
|
|
|
535
473
|
const bootstrapAdmin = useCallback(async (): Promise<void> => {
|
|
536
474
|
try {
|
|
537
475
|
await apiRequest("/bootstrap", "POST");
|
|
538
|
-
//
|
|
539
|
-
|
|
540
|
-
const loadedRoles = data.roles.map(convertRole);
|
|
541
|
-
setRoles(loadedRoles);
|
|
542
|
-
await loadUsers();
|
|
476
|
+
// Re-check admin existence after successful bootstrap
|
|
477
|
+
await checkAdminExists();
|
|
543
478
|
} catch (error) {
|
|
544
479
|
console.error("Failed to bootstrap admin:", error);
|
|
545
480
|
throw error;
|
|
546
481
|
}
|
|
547
|
-
}, [apiRequest,
|
|
482
|
+
}, [apiRequest, checkAdminExists]);
|
|
483
|
+
|
|
484
|
+
// Expose cached users as an array for backward compat (BootstrapAdminBanner,
|
|
485
|
+
// UsersView fallback). This is NOT the full user list — just the cache.
|
|
486
|
+
const users = Array.from(userCache.values());
|
|
548
487
|
|
|
549
488
|
return {
|
|
550
|
-
loading,
|
|
489
|
+
loading: effectiveLoading,
|
|
551
490
|
users,
|
|
491
|
+
hasAdminUsers,
|
|
552
492
|
saveUser,
|
|
553
493
|
createUser,
|
|
554
494
|
resetPassword,
|
|
555
495
|
deleteUser,
|
|
556
|
-
roles,
|
|
557
|
-
saveRole,
|
|
558
|
-
deleteRole,
|
|
559
496
|
isAdmin,
|
|
560
497
|
allowDefaultRolesCreation: isAdmin,
|
|
561
|
-
includeCollectionConfigPermissions: true,
|
|
562
498
|
defineRolesFor,
|
|
563
499
|
getUser,
|
|
564
500
|
searchUsers,
|
|
565
501
|
usersError,
|
|
566
|
-
rolesError,
|
|
567
502
|
bootstrapAdmin
|
|
568
503
|
};
|
|
569
504
|
}
|
|
@@ -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;
|
|
@@ -113,12 +113,11 @@ export function useRebaseAuthController(
|
|
|
113
113
|
|
|
114
114
|
// Configure API URL on mount
|
|
115
115
|
useEffect(() => {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
authApi.setApiUrl(apiUrl);
|
|
116
|
+
const url = client?.baseUrl || apiUrl;
|
|
117
|
+
if (url) {
|
|
118
|
+
authApi.setApiUrl(url);
|
|
120
119
|
}
|
|
121
|
-
}, [client, apiUrl]);
|
|
120
|
+
}, [client, client?.baseUrl, apiUrl]);
|
|
122
121
|
|
|
123
122
|
const clearError = useCallback(() => {
|
|
124
123
|
setAuthProviderError(null);
|
|
@@ -281,7 +280,7 @@ export function useRebaseAuthController(
|
|
|
281
280
|
// Install token getter onto client
|
|
282
281
|
useEffect(() => {
|
|
283
282
|
if (client) {
|
|
284
|
-
client.setAuthTokenGetter(async () => {
|
|
283
|
+
client.setAuthTokenGetter?.(async () => {
|
|
285
284
|
try { return await getAuthToken(); } catch { return null; }
|
|
286
285
|
});
|
|
287
286
|
if (client.setOnUnauthorized) {
|
|
@@ -314,7 +313,7 @@ export function useRebaseAuthController(
|
|
|
314
313
|
if (defineRolesFor) {
|
|
315
314
|
const customRoles = await defineRolesFor(convertedUser);
|
|
316
315
|
if (customRoles) {
|
|
317
|
-
convertedUser = { ...convertedUser, roles: customRoles
|
|
316
|
+
convertedUser = { ...convertedUser, roles: customRoles };
|
|
318
317
|
}
|
|
319
318
|
}
|
|
320
319
|
|
|
@@ -480,7 +479,7 @@ export function useRebaseAuthController(
|
|
|
480
479
|
const customRoles = await defineRolesFor(convertedUser);
|
|
481
480
|
if (customRoles) {
|
|
482
481
|
convertedUser = { ...convertedUser,
|
|
483
|
-
roles: customRoles
|
|
482
|
+
roles: customRoles };
|
|
484
483
|
}
|
|
485
484
|
}
|
|
486
485
|
|
|
@@ -576,7 +575,7 @@ roles: customRoles.map(r => r.id) };
|
|
|
576
575
|
const customRoles = await defineRolesFor(userToSet);
|
|
577
576
|
if (customRoles) {
|
|
578
577
|
userToSet = { ...userToSet,
|
|
579
|
-
roles: customRoles
|
|
578
|
+
roles: customRoles };
|
|
580
579
|
}
|
|
581
580
|
}
|
|
582
581
|
|
|
@@ -620,11 +619,15 @@ roles: customRoles.map(r => r.id) };
|
|
|
620
619
|
if (!isMountedRef.current) return;
|
|
621
620
|
if (customRoles) {
|
|
622
621
|
userToSet = { ...userToSet,
|
|
623
|
-
roles: customRoles
|
|
622
|
+
roles: customRoles };
|
|
624
623
|
}
|
|
625
624
|
}
|
|
626
625
|
} catch (meError: unknown) {
|
|
627
626
|
if (!isMountedRef.current) return;
|
|
627
|
+
if (meError instanceof authApi.AuthApiError && (meError.code === "NOT_FOUND" || meError.code === "UNAUTHORIZED")) {
|
|
628
|
+
clearSessionAndSignOut();
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
628
631
|
userToSet = convertToUser(stored.user);
|
|
629
632
|
}
|
|
630
633
|
|
|
@@ -744,10 +747,10 @@ roles: customRoles.map(r => r.id) };
|
|
|
744
747
|
emailPasswordLogin: true,
|
|
745
748
|
googleLogin: !!(props.googleClientId),
|
|
746
749
|
registration: authConfig?.registrationEnabled ?? false,
|
|
747
|
-
passwordReset:
|
|
750
|
+
passwordReset: authConfig?.passwordReset ?? false,
|
|
748
751
|
sessionManagement: true,
|
|
749
752
|
profileUpdate: true,
|
|
750
|
-
emailVerification: false,
|
|
753
|
+
emailVerification: authConfig?.emailVerification ?? false,
|
|
751
754
|
enabledProviders: authConfig?.enabledProviders ?? []
|
|
752
755
|
}
|
|
753
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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AuthController,
|
|
1
|
+
import { AuthController, User } from "@rebasepro/types";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Auth controller that extends the base AuthController
|
|
@@ -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) */
|
|
@@ -58,7 +64,7 @@ export interface RebaseAuthControllerProps {
|
|
|
58
64
|
/** Callback when user signs out */
|
|
59
65
|
onSignOut?: () => void;
|
|
60
66
|
/** Define roles for a user after login */
|
|
61
|
-
defineRolesFor?: (user: User) => Promise<
|
|
67
|
+
defineRolesFor?: (user: User) => Promise<string[] | undefined> | string[] | undefined;
|
|
62
68
|
}
|
|
63
69
|
|
|
64
70
|
/**
|