@rebasepro/auth 0.5.0 → 0.6.1

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/index.umd.js CHANGED
@@ -1,1062 +1,823 @@
1
1
  (function(global, factory) {
2
- typeof exports === "object" && typeof module !== "undefined" ? factory(exports, require("react")) : typeof define === "function" && define.amd ? define(["exports", "react"], factory) : (global = typeof globalThis !== "undefined" ? globalThis : global || self, factory(global["Rebase Auth"] = {}, global.react));
3
- })(this, (function(exports2, react) {
4
- "use strict";
5
- let baseApiUrl = "";
6
- function setApiUrl(url) {
7
- baseApiUrl = url;
8
- }
9
- function getApiUrl() {
10
- return baseApiUrl;
11
- }
12
- class AuthApiError extends Error {
13
- code;
14
- constructor(message, code) {
15
- super(message);
16
- this.code = code;
17
- this.name = "AuthApiError";
18
- }
19
- }
20
- async function handleResponse(response) {
21
- let data;
22
- try {
23
- data = await response.json();
24
- } catch (parseError) {
25
- throw new AuthApiError(
26
- `Server returned non-JSON response (status: ${response.status})`,
27
- "PARSE_ERROR"
28
- );
29
- }
30
- if (!response.ok) {
31
- throw new AuthApiError(
32
- data.error?.message || "Request failed",
33
- data.error?.code || "UNKNOWN_ERROR"
34
- );
35
- }
36
- return data;
37
- }
38
- async function fetchWithHandling(input, init) {
39
- try {
40
- return await fetch(input, init);
41
- } catch (error) {
42
- if (error instanceof TypeError && error.message.includes("Failed to fetch")) {
43
- throw new AuthApiError(
44
- "Failed to connect to the backend server. The backend might be down or failed to initialize (e.g., database connection timeout).",
45
- "NETWORK_ERROR"
46
- );
47
- }
48
- throw new AuthApiError("Network error: " + (error instanceof Error ? error.message : String(error)), "NETWORK_ERROR");
49
- }
50
- }
51
- async function register(email, password, displayName) {
52
- const response = await fetchWithHandling(`${baseApiUrl}/api/auth/register`, {
53
- method: "POST",
54
- headers: { "Content-Type": "application/json" },
55
- body: JSON.stringify({
56
- email,
57
- password,
58
- displayName
59
- })
60
- });
61
- return handleResponse(response);
62
- }
63
- async function login(email, password) {
64
- const response = await fetchWithHandling(`${baseApiUrl}/api/auth/login`, {
65
- method: "POST",
66
- headers: { "Content-Type": "application/json" },
67
- body: JSON.stringify({
68
- email,
69
- password
70
- })
71
- });
72
- return handleResponse(response);
73
- }
74
- async function googleLogin(payload) {
75
- const response = await fetchWithHandling(`${baseApiUrl}/api/auth/google`, {
76
- method: "POST",
77
- headers: { "Content-Type": "application/json" },
78
- body: JSON.stringify(payload)
79
- });
80
- return handleResponse(response);
81
- }
82
- async function oauthLogin(providerId, payload) {
83
- const response = await fetchWithHandling(`${baseApiUrl}/api/auth/${providerId}`, {
84
- method: "POST",
85
- headers: { "Content-Type": "application/json" },
86
- body: JSON.stringify(payload)
87
- });
88
- return handleResponse(response);
89
- }
90
- async function refreshAccessToken(refreshToken) {
91
- const response = await fetchWithHandling(`${baseApiUrl}/api/auth/refresh`, {
92
- method: "POST",
93
- headers: { "Content-Type": "application/json" },
94
- body: JSON.stringify({ refreshToken })
95
- });
96
- return handleResponse(response);
97
- }
98
- async function logout(refreshToken) {
99
- await fetchWithHandling(`${baseApiUrl}/api/auth/logout`, {
100
- method: "POST",
101
- headers: { "Content-Type": "application/json" },
102
- body: JSON.stringify({ refreshToken })
103
- });
104
- }
105
- async function getCurrentUser(accessToken) {
106
- const response = await fetchWithHandling(`${baseApiUrl}/api/auth/me`, {
107
- method: "GET",
108
- headers: {
109
- "Content-Type": "application/json",
110
- "Authorization": `Bearer ${accessToken}`
111
- }
112
- });
113
- return handleResponse(response);
114
- }
115
- async function forgotPassword(email) {
116
- const response = await fetchWithHandling(`${baseApiUrl}/api/auth/forgot-password`, {
117
- method: "POST",
118
- headers: { "Content-Type": "application/json" },
119
- body: JSON.stringify({ email })
120
- });
121
- return handleResponse(response);
122
- }
123
- async function resetPassword(token, password) {
124
- const response = await fetchWithHandling(`${baseApiUrl}/api/auth/reset-password`, {
125
- method: "POST",
126
- headers: { "Content-Type": "application/json" },
127
- body: JSON.stringify({
128
- token,
129
- password
130
- })
131
- });
132
- return handleResponse(response);
133
- }
134
- async function changePassword(accessToken, oldPassword, newPassword) {
135
- const response = await fetchWithHandling(`${baseApiUrl}/api/auth/change-password`, {
136
- method: "POST",
137
- headers: {
138
- "Content-Type": "application/json",
139
- "Authorization": `Bearer ${accessToken}`
140
- },
141
- body: JSON.stringify({
142
- oldPassword,
143
- newPassword
144
- })
145
- });
146
- return handleResponse(response);
147
- }
148
- async function updateProfile(accessToken, displayName, photoURL) {
149
- const response = await fetchWithHandling(`${baseApiUrl}/api/auth/me`, {
150
- method: "PATCH",
151
- headers: {
152
- "Content-Type": "application/json",
153
- "Authorization": `Bearer ${accessToken}`
154
- },
155
- body: JSON.stringify({
156
- displayName,
157
- photoURL
158
- })
159
- });
160
- return handleResponse(response);
161
- }
162
- async function fetchSessions(accessToken, currentRefreshToken) {
163
- const headers = {
164
- "Content-Type": "application/json",
165
- "Authorization": `Bearer ${accessToken}`
166
- };
167
- if (currentRefreshToken) {
168
- headers["X-Refresh-Token"] = currentRefreshToken;
169
- }
170
- const response = await fetchWithHandling(`${baseApiUrl}/api/auth/sessions`, {
171
- method: "GET",
172
- headers
173
- });
174
- return handleResponse(response);
175
- }
176
- async function revokeSession(accessToken, sessionId) {
177
- const response = await fetchWithHandling(`${baseApiUrl}/api/auth/sessions/${sessionId}`, {
178
- method: "DELETE",
179
- headers: {
180
- "Content-Type": "application/json",
181
- "Authorization": `Bearer ${accessToken}`
182
- }
183
- });
184
- return handleResponse(response);
185
- }
186
- async function revokeAllSessions(accessToken) {
187
- const response = await fetchWithHandling(`${baseApiUrl}/api/auth/sessions`, {
188
- method: "DELETE",
189
- headers: {
190
- "Content-Type": "application/json",
191
- "Authorization": `Bearer ${accessToken}`
192
- }
193
- });
194
- return handleResponse(response);
195
- }
196
- let authConfigInflight = null;
197
- let authConfigCached = null;
198
- async function fetchAuthConfig() {
199
- if (authConfigCached) {
200
- return authConfigCached;
201
- }
202
- if (authConfigInflight) {
203
- return authConfigInflight;
204
- }
205
- authConfigInflight = (async () => {
206
- const response = await fetchWithHandling(`${baseApiUrl}/api/auth/config`, {
207
- method: "GET",
208
- headers: { "Content-Type": "application/json" }
209
- });
210
- return handleResponse(response);
211
- })();
212
- try {
213
- const result = await authConfigInflight;
214
- authConfigCached = result;
215
- return result;
216
- } finally {
217
- authConfigInflight = null;
218
- }
219
- }
220
- const STORAGE_KEY = "rebase_react_auth";
221
- const TOKEN_REFRESH_BUFFER_MS = 2 * 60 * 1e3;
222
- function convertToUser(userInfo) {
223
- return {
224
- uid: userInfo.uid,
225
- email: userInfo.email,
226
- displayName: userInfo.displayName || null,
227
- photoURL: userInfo.photoURL || null,
228
- providerId: "custom",
229
- isAnonymous: false,
230
- roles: userInfo.roles || [],
231
- metadata: userInfo.metadata
232
- };
233
- }
234
- function saveAuthToStorage(tokens, user) {
235
- try {
236
- const data = { tokens, user };
237
- localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
238
- } catch (e) {
239
- }
240
- }
241
- function loadAuthFromStorage() {
242
- try {
243
- const data = localStorage.getItem(STORAGE_KEY);
244
- if (data) {
245
- const parsed = JSON.parse(data);
246
- return parsed;
247
- }
248
- } catch (e) {
249
- console.warn("Failed to load auth from storage:", e);
250
- }
251
- return null;
252
- }
253
- function clearAuthFromStorage() {
254
- try {
255
- localStorage.removeItem(STORAGE_KEY);
256
- } catch (e) {
257
- console.warn("Failed to clear auth from storage:", e);
258
- }
259
- }
260
- function isTokenExpiredOrNearExpiry(expiresAt, bufferMs = TOKEN_REFRESH_BUFFER_MS) {
261
- return Date.now() + bufferMs >= expiresAt;
262
- }
263
- function useRebaseAuthController(props = {}) {
264
- const { client, apiUrl, onSignOut, defineRolesFor } = props;
265
- const [user, setUser] = react.useState(null);
266
- const [authLoading, setAuthLoading] = react.useState(false);
267
- const [initialLoading, setInitialLoading] = react.useState(true);
268
- const [authError, setAuthError] = react.useState(null);
269
- const [authProviderError, setAuthProviderError] = react.useState(null);
270
- const [loginSkipped, setLoginSkipped] = react.useState(false);
271
- const [extra, setExtra] = react.useState(null);
272
- const [authConfig, setAuthConfig] = react.useState(null);
273
- const tokensRef = react.useRef(null);
274
- const refreshTimeoutRef = react.useRef(null);
275
- const refreshPromiseRef = react.useRef(null);
276
- const isMountedRef = react.useRef(true);
277
- react.useEffect(() => {
278
- const url = client?.baseUrl || apiUrl;
279
- if (url) {
280
- setApiUrl(url);
281
- }
282
- }, [client, client?.baseUrl, apiUrl]);
283
- const clearError = react.useCallback(() => {
284
- setAuthProviderError(null);
285
- }, []);
286
- const clearSessionAndSignOut = react.useCallback(() => {
287
- tokensRef.current = null;
288
- clearAuthFromStorage();
289
- if (refreshTimeoutRef.current) {
290
- clearTimeout(refreshTimeoutRef.current);
291
- refreshTimeoutRef.current = null;
292
- }
293
- setUser(null);
294
- setLoginSkipped(false);
295
- onSignOut?.();
296
- }, [onSignOut]);
297
- const refreshAccessToken$1 = react.useCallback(async () => {
298
- if (refreshPromiseRef.current) {
299
- return refreshPromiseRef.current;
300
- }
301
- const executeRefresh = async () => {
302
- const storedData = loadAuthFromStorage();
303
- if (storedData?.tokens?.accessTokenExpiresAt) {
304
- const storedTokens = storedData.tokens;
305
- if (!isTokenExpiredOrNearExpiry(storedTokens.accessTokenExpiresAt) && storedTokens.accessToken !== tokensRef.current?.accessToken) {
306
- tokensRef.current = storedTokens;
307
- return storedTokens;
308
- }
309
- }
310
- const currentTokens = tokensRef.current;
311
- if (!currentTokens?.refreshToken) {
312
- return null;
313
- }
314
- try {
315
- const response = await refreshAccessToken(currentTokens.refreshToken);
316
- const newTokens = response.tokens;
317
- tokensRef.current = newTokens;
318
- const latestStoredData = loadAuthFromStorage();
319
- if (latestStoredData) {
320
- saveAuthToStorage(newTokens, latestStoredData.user);
321
- }
322
- return newTokens;
323
- } catch (error) {
324
- if (error instanceof Error && error.code === "NETWORK_ERROR") {
325
- throw error;
326
- }
327
- return null;
328
- } finally {
329
- refreshPromiseRef.current = null;
330
- }
331
- };
332
- refreshPromiseRef.current = executeRefresh();
333
- return refreshPromiseRef.current;
334
- }, []);
335
- const scheduleTokenRefresh = react.useCallback((tokens) => {
336
- if (refreshTimeoutRef.current) {
337
- clearTimeout(refreshTimeoutRef.current);
338
- }
339
- const expiresAt = tokens.accessTokenExpiresAt;
340
- const refreshAt = expiresAt - TOKEN_REFRESH_BUFFER_MS;
341
- const timeUntilRefresh = refreshAt - Date.now();
342
- if (timeUntilRefresh <= 0) {
343
- refreshAccessToken$1().then((newTokens) => {
344
- if (newTokens && isMountedRef.current) {
345
- scheduleTokenRefresh(newTokens);
346
- } else if (!newTokens && isMountedRef.current) {
347
- clearSessionAndSignOut();
348
- }
349
- });
350
- return;
351
- }
352
- refreshTimeoutRef.current = setTimeout(async () => {
353
- if (!isMountedRef.current) return;
354
- try {
355
- const newTokens = await refreshAccessToken$1();
356
- if (newTokens && isMountedRef.current) {
357
- scheduleTokenRefresh(newTokens);
358
- } else if (!newTokens && isMountedRef.current) {
359
- clearSessionAndSignOut();
360
- }
361
- } catch (error) {
362
- if (isMountedRef.current) {
363
- refreshTimeoutRef.current = setTimeout(() => {
364
- scheduleTokenRefresh(tokens);
365
- }, 1e4);
366
- }
367
- }
368
- }, timeUntilRefresh);
369
- }, [refreshAccessToken$1, clearSessionAndSignOut]);
370
- const getAuthToken = react.useCallback(async () => {
371
- if (initialLoading) {
372
- throw new Error("Auth is still loading");
373
- }
374
- const currentTokens = tokensRef.current;
375
- if (!currentTokens) {
376
- throw new Error("User is not logged in");
377
- }
378
- if (isTokenExpiredOrNearExpiry(currentTokens.accessTokenExpiresAt)) {
379
- try {
380
- const newTokens = await refreshAccessToken$1();
381
- if (!newTokens) {
382
- clearSessionAndSignOut();
383
- throw new Error("Session expired. Please login again.");
384
- }
385
- return newTokens.accessToken;
386
- } catch (error) {
387
- if (error instanceof Error && error.code === "NETWORK_ERROR") {
388
- throw error;
389
- }
390
- clearSessionAndSignOut();
391
- throw error;
392
- }
393
- }
394
- return currentTokens.accessToken;
395
- }, [initialLoading, refreshAccessToken$1, clearSessionAndSignOut]);
396
- react.useEffect(() => {
397
- if (client) {
398
- client.setAuthTokenGetter?.(async () => {
399
- try {
400
- return await getAuthToken();
401
- } catch {
402
- return null;
403
- }
404
- });
405
- if (client.setOnUnauthorized) {
406
- client.setOnUnauthorized(async () => {
407
- try {
408
- const newTokens = await refreshAccessToken$1();
409
- if (newTokens) return true;
410
- clearSessionAndSignOut();
411
- return false;
412
- } catch (e) {
413
- clearSessionAndSignOut();
414
- return false;
415
- }
416
- });
417
- }
418
- if (client.ws) {
419
- client.ws.setAuthTokenGetter(async () => {
420
- return await getAuthToken();
421
- });
422
- }
423
- }
424
- }, [client, getAuthToken, refreshAccessToken$1, clearSessionAndSignOut]);
425
- const handleAuthSuccess = react.useCallback(async (userInfo, tokens) => {
426
- tokensRef.current = tokens;
427
- let convertedUser = convertToUser(userInfo);
428
- if (defineRolesFor) {
429
- const customRoles = await defineRolesFor(convertedUser);
430
- if (customRoles) {
431
- convertedUser = { ...convertedUser, roles: customRoles };
432
- }
433
- }
434
- saveAuthToStorage(tokens, userInfo);
435
- setUser(convertedUser);
436
- setAuthError(null);
437
- setAuthProviderError(null);
438
- setLoginSkipped(false);
439
- scheduleTokenRefresh(tokens);
440
- }, [scheduleTokenRefresh, defineRolesFor]);
441
- const emailPasswordLogin = react.useCallback(async (email, password) => {
442
- setAuthLoading(true);
443
- setAuthProviderError(null);
444
- try {
445
- const response = await login(email, password);
446
- await handleAuthSuccess(response.user, response.tokens);
447
- } catch (error) {
448
- setAuthProviderError(error);
449
- throw error;
450
- } finally {
451
- setAuthLoading(false);
452
- }
453
- }, [handleAuthSuccess]);
454
- const register$1 = react.useCallback(async (email, password, displayName) => {
455
- setAuthLoading(true);
456
- setAuthProviderError(null);
457
- try {
458
- const response = await register(email, password, displayName);
459
- await handleAuthSuccess(response.user, response.tokens);
460
- } catch (error) {
461
- setAuthProviderError(error);
462
- throw error;
463
- } finally {
464
- setAuthLoading(false);
465
- }
466
- }, [handleAuthSuccess]);
467
- const googleLogin$1 = react.useCallback(async (payload) => {
468
- setAuthLoading(true);
469
- setAuthProviderError(null);
470
- try {
471
- const response = await googleLogin(payload);
472
- await handleAuthSuccess(response.user, response.tokens);
473
- } catch (error) {
474
- setAuthProviderError(error);
475
- throw error;
476
- } finally {
477
- setAuthLoading(false);
478
- }
479
- }, [handleAuthSuccess]);
480
- const oauthLogin$1 = react.useCallback(async (providerId, payload) => {
481
- setAuthLoading(true);
482
- setAuthProviderError(null);
483
- try {
484
- const response = await oauthLogin(providerId, payload);
485
- await handleAuthSuccess(response.user, response.tokens);
486
- } catch (error) {
487
- setAuthProviderError(error);
488
- throw error;
489
- } finally {
490
- setAuthLoading(false);
491
- }
492
- }, [handleAuthSuccess]);
493
- const signOut = react.useCallback(async () => {
494
- try {
495
- if (tokensRef.current) {
496
- await logout(tokensRef.current.refreshToken);
497
- }
498
- } catch (error) {
499
- console.error("Logout error:", error);
500
- } finally {
501
- clearSessionAndSignOut();
502
- }
503
- }, [clearSessionAndSignOut]);
504
- const skipLogin = react.useCallback(() => {
505
- setLoginSkipped(true);
506
- setUser(null);
507
- }, []);
508
- const forgotPassword$1 = react.useCallback(async (email) => {
509
- setAuthLoading(true);
510
- setAuthProviderError(null);
511
- try {
512
- await forgotPassword(email);
513
- } catch (error) {
514
- setAuthProviderError(error);
515
- throw error;
516
- } finally {
517
- setAuthLoading(false);
518
- }
519
- }, []);
520
- const resetPassword$1 = react.useCallback(async (token, password) => {
521
- setAuthLoading(true);
522
- setAuthProviderError(null);
523
- try {
524
- await resetPassword(token, password);
525
- } catch (error) {
526
- setAuthProviderError(error);
527
- throw error;
528
- } finally {
529
- setAuthLoading(false);
530
- }
531
- }, []);
532
- const changePassword$1 = react.useCallback(async (oldPassword, newPassword) => {
533
- setAuthLoading(true);
534
- setAuthProviderError(null);
535
- try {
536
- if (!tokensRef.current) {
537
- throw new Error("User is not logged in");
538
- }
539
- await changePassword(tokensRef.current.accessToken, oldPassword, newPassword);
540
- clearSessionAndSignOut();
541
- } catch (error) {
542
- setAuthProviderError(error);
543
- throw error;
544
- } finally {
545
- setAuthLoading(false);
546
- }
547
- }, [clearSessionAndSignOut]);
548
- const updateProfile$1 = react.useCallback(async (displayName, photoURL) => {
549
- setAuthLoading(true);
550
- setAuthProviderError(null);
551
- try {
552
- if (!tokensRef.current) {
553
- throw new Error("User is not logged in");
554
- }
555
- const response = await updateProfile(tokensRef.current.accessToken, displayName, photoURL);
556
- let convertedUser = convertToUser(response.user);
557
- if (defineRolesFor) {
558
- const customRoles = await defineRolesFor(convertedUser);
559
- if (customRoles) {
560
- convertedUser = {
561
- ...convertedUser,
562
- roles: customRoles
563
- };
564
- }
565
- }
566
- const storedData = loadAuthFromStorage();
567
- if (storedData) {
568
- saveAuthToStorage(storedData.tokens, response.user);
569
- }
570
- setUser(convertedUser);
571
- return convertedUser;
572
- } catch (error) {
573
- setAuthProviderError(error);
574
- throw error;
575
- } finally {
576
- setAuthLoading(false);
577
- }
578
- }, [defineRolesFor]);
579
- const fetchSessions$1 = react.useCallback(async () => {
580
- try {
581
- if (!tokensRef.current) {
582
- throw new Error("User is not logged in");
583
- }
584
- const response = await fetchSessions(tokensRef.current.accessToken, tokensRef.current.refreshToken);
585
- return response.sessions;
586
- } catch (error) {
587
- setAuthProviderError(error);
588
- throw error;
589
- }
590
- }, []);
591
- const revokeSession$1 = react.useCallback(async (sessionId) => {
592
- try {
593
- if (!tokensRef.current) {
594
- throw new Error("User is not logged in");
595
- }
596
- await revokeSession(tokensRef.current.accessToken, sessionId);
597
- } catch (error) {
598
- setAuthProviderError(error);
599
- throw error;
600
- }
601
- }, []);
602
- react.useEffect(() => {
603
- isMountedRef.current = true;
604
- const restoreAuth = async () => {
605
- try {
606
- const config = await fetchAuthConfig();
607
- if (isMountedRef.current) {
608
- setAuthConfig(config);
609
- }
610
- } catch (e) {
611
- }
612
- const stored = loadAuthFromStorage();
613
- if (!stored) {
614
- setInitialLoading(false);
615
- return;
616
- }
617
- if (!stored.tokens?.refreshToken) {
618
- clearAuthFromStorage();
619
- setInitialLoading(false);
620
- return;
621
- }
622
- const expiresAt = stored.tokens.accessTokenExpiresAt;
623
- if (typeof expiresAt !== "number" || !Number.isFinite(expiresAt)) {
624
- clearAuthFromStorage();
625
- setInitialLoading(false);
626
- return;
627
- }
628
- if (!isTokenExpiredOrNearExpiry(stored.tokens.accessTokenExpiresAt)) {
629
- tokensRef.current = stored.tokens;
630
- let userToSet = convertToUser(stored.user);
631
- if (defineRolesFor) {
632
- const customRoles = await defineRolesFor(userToSet);
633
- if (customRoles) {
634
- userToSet = {
635
- ...userToSet,
636
- roles: customRoles
637
- };
638
- }
639
- }
640
- setUser(userToSet);
641
- scheduleTokenRefresh(stored.tokens);
642
- setInitialLoading(false);
643
- return;
644
- }
645
- tokensRef.current = stored.tokens;
646
- try {
647
- const newTokens = await refreshAccessToken$1();
648
- if (!newTokens) {
649
- clearAuthFromStorage();
650
- tokensRef.current = null;
651
- setInitialLoading(false);
652
- return;
653
- }
654
- if (!isMountedRef.current) return;
655
- let userToSet;
656
- try {
657
- const meResponse = await getCurrentUser(newTokens.accessToken);
658
- if (!isMountedRef.current) return;
659
- const freshUserInfo = meResponse.user;
660
- saveAuthToStorage(newTokens, freshUserInfo);
661
- userToSet = convertToUser(freshUserInfo);
662
- if (defineRolesFor) {
663
- const customRoles = await defineRolesFor(userToSet);
664
- if (!isMountedRef.current) return;
665
- if (customRoles) {
666
- userToSet = {
667
- ...userToSet,
668
- roles: customRoles
669
- };
670
- }
671
- }
672
- } catch (meError) {
673
- if (!isMountedRef.current) return;
674
- if (meError instanceof AuthApiError && (meError.code === "NOT_FOUND" || meError.code === "UNAUTHORIZED")) {
675
- clearSessionAndSignOut();
676
- return;
677
- }
678
- userToSet = convertToUser(stored.user);
679
- }
680
- if (!isMountedRef.current) return;
681
- setUser(userToSet);
682
- scheduleTokenRefresh(newTokens);
683
- } catch (error) {
684
- if (!isMountedRef.current) return;
685
- if (!(error instanceof Error && error.code === "NETWORK_ERROR")) {
686
- clearAuthFromStorage();
687
- tokensRef.current = null;
688
- }
689
- } finally {
690
- if (isMountedRef.current) {
691
- setInitialLoading(false);
692
- }
693
- }
694
- };
695
- restoreAuth();
696
- return () => {
697
- isMountedRef.current = false;
698
- };
699
- }, [scheduleTokenRefresh, defineRolesFor, refreshAccessToken$1]);
700
- react.useEffect(() => {
701
- const handleVisibilityChange = async () => {
702
- if (initialLoading) return;
703
- if (document.visibilityState === "visible" && tokensRef.current) {
704
- if (isTokenExpiredOrNearExpiry(tokensRef.current.accessTokenExpiresAt)) {
705
- try {
706
- const newTokens = await refreshAccessToken$1();
707
- if (newTokens && isMountedRef.current) {
708
- scheduleTokenRefresh(newTokens);
709
- } else if (!newTokens && isMountedRef.current) {
710
- clearSessionAndSignOut();
711
- }
712
- } catch (e) {
713
- }
714
- }
715
- }
716
- };
717
- document.addEventListener("visibilitychange", handleVisibilityChange);
718
- return () => {
719
- document.removeEventListener("visibilitychange", handleVisibilityChange);
720
- };
721
- }, [initialLoading, refreshAccessToken$1, scheduleTokenRefresh, clearSessionAndSignOut]);
722
- const getApiUrl$1 = react.useCallback(() => {
723
- return getApiUrl();
724
- }, []);
725
- react.useEffect(() => {
726
- return () => {
727
- isMountedRef.current = false;
728
- if (refreshTimeoutRef.current) {
729
- clearTimeout(refreshTimeoutRef.current);
730
- }
731
- };
732
- }, []);
733
- const revokeAllSessions$1 = react.useCallback(async () => {
734
- try {
735
- if (!tokensRef.current) {
736
- throw new Error("User is not logged in");
737
- }
738
- await revokeAllSessions(tokensRef.current.accessToken);
739
- clearSessionAndSignOut();
740
- } catch (error) {
741
- setAuthProviderError(error);
742
- throw error;
743
- }
744
- }, [clearSessionAndSignOut]);
745
- return {
746
- user,
747
- authLoading,
748
- initialLoading,
749
- authError,
750
- authProviderError,
751
- loginSkipped,
752
- needsSetup: authConfig?.needsSetup ?? false,
753
- registrationEnabled: authConfig?.registrationEnabled ?? false,
754
- getAuthToken,
755
- getApiUrl: getApiUrl$1,
756
- signOut,
757
- emailPasswordLogin,
758
- register: register$1,
759
- googleLogin: googleLogin$1,
760
- oauthLogin: oauthLogin$1,
761
- skipLogin,
762
- forgotPassword: forgotPassword$1,
763
- resetPassword: resetPassword$1,
764
- changePassword: changePassword$1,
765
- updateProfile: updateProfile$1,
766
- fetchSessions: fetchSessions$1,
767
- revokeSession: revokeSession$1,
768
- revokeAllSessions: revokeAllSessions$1,
769
- clearError,
770
- setAuthProviderError,
771
- extra,
772
- setExtra,
773
- capabilities: {
774
- emailPasswordLogin: true,
775
- googleLogin: !!props.googleClientId,
776
- registration: authConfig?.registrationEnabled ?? false,
777
- passwordReset: authConfig?.passwordReset ?? false,
778
- sessionManagement: true,
779
- profileUpdate: true,
780
- emailVerification: authConfig?.emailVerification ?? false,
781
- enabledProviders: authConfig?.enabledProviders ?? []
782
- }
783
- };
784
- }
785
- function convertUser(apiUser) {
786
- return {
787
- uid: apiUser.uid,
788
- email: apiUser.email,
789
- displayName: apiUser.displayName || null,
790
- photoURL: apiUser.photoURL || null,
791
- providerId: "custom",
792
- isAnonymous: false,
793
- roles: apiUser.roles,
794
- createdAt: apiUser.createdAt ? new Date(apiUser.createdAt) : null
795
- };
796
- }
797
- function useBackendUserManagement(config) {
798
- const { client, apiUrl, getAuthToken, currentUser } = config;
799
- const [userCache, setUserCache] = react.useState(/* @__PURE__ */ new Map());
800
- const [hasAdminUsers, setHasAdminUsers] = react.useState(false);
801
- const userRoles = currentUser?.roles ?? [];
802
- const isUserAdmin = userRoles.some((r) => r === "admin" || r === "schema-admin");
803
- const [loading, setLoading] = react.useState(() => {
804
- if (!currentUser) return false;
805
- if (!isUserAdmin) return false;
806
- return true;
807
- });
808
- const [usersError, setUsersError] = react.useState();
809
- const lastLoadedUidRef = react.useRef(null);
810
- const effectiveLoading = loading || !!(currentUser && isUserAdmin && lastLoadedUidRef.current !== currentUser.uid);
811
- const mergeIntoCache = react.useCallback((incoming) => {
812
- setUserCache((prev) => {
813
- const next = new Map(prev);
814
- for (const u of incoming) {
815
- next.set(u.uid, u);
816
- }
817
- return next;
818
- });
819
- }, []);
820
- const apiRequestRef = react.useRef(null);
821
- const apiRequest = react.useCallback(async (endpoint, method = "GET", body, retryCount = 6, signal) => {
822
- let lastError = null;
823
- for (let attempt = 0; attempt < retryCount; attempt++) {
824
- if (signal?.aborted) {
825
- const error = new Error("Request aborted");
826
- error.name = "AbortError";
827
- throw error;
828
- }
829
- try {
830
- const token = getAuthToken ? await getAuthToken() : client?.resolveToken ? await client.resolveToken() : null;
831
- const baseUrl = apiUrl || (client?.baseUrl ? client.baseUrl : "");
832
- const response = await fetch(`${baseUrl}/api/admin${endpoint}`, {
833
- method,
834
- headers: {
835
- "Content-Type": "application/json",
836
- ...token ? { "Authorization": `Bearer ${token}` } : {}
837
- },
838
- body: body ? JSON.stringify(body) : void 0,
839
- signal
840
- });
841
- if (!response.ok) {
842
- const errorText = await response.text();
843
- let errorMessage = "API request failed";
844
- try {
845
- const errorJson = JSON.parse(errorText);
846
- errorMessage = errorJson.error?.message || errorMessage;
847
- } catch (e) {
848
- errorMessage = errorText || `HTTP error ${response.status}`;
849
- }
850
- const error = Object.assign(new Error(errorMessage), { status: response.status });
851
- throw error;
852
- }
853
- return await response.json();
854
- } catch (error) {
855
- if (error instanceof Error && error.name === "AbortError" || signal?.aborted) {
856
- throw error;
857
- }
858
- lastError = error instanceof Error ? error : new Error(String(error));
859
- const isNetworkError = error instanceof TypeError;
860
- const isServerError = typeof error.status === "number" && error.status >= 500 && error.status < 600;
861
- if (attempt < retryCount - 1 && (isNetworkError || isServerError)) {
862
- const delay = Math.min(1e3 * Math.pow(2, attempt), 5e3);
863
- console.warn(`Admin API request to ${endpoint} failed, retrying in ${delay}ms...`);
864
- await new Promise((resolve, reject) => {
865
- if (signal?.aborted) return reject(new Error("AbortError"));
866
- const timer = setTimeout(resolve, delay);
867
- if (signal) {
868
- signal.addEventListener("abort", () => {
869
- clearTimeout(timer);
870
- reject(new Error("AbortError"));
871
- }, { once: true });
872
- }
873
- }).catch(() => {
874
- });
875
- if (signal?.aborted) {
876
- const abortError = new Error("Request aborted");
877
- abortError.name = "AbortError";
878
- throw abortError;
879
- }
880
- continue;
881
- }
882
- console.error("Admin API error after retries:", error);
883
- throw error;
884
- }
885
- }
886
- throw lastError;
887
- }, [apiUrl, getAuthToken]);
888
- apiRequestRef.current = apiRequest;
889
- const checkAdminExists = react.useCallback(async (signal) => {
890
- try {
891
- const data = await apiRequest("/users?role=admin&limit=1", "GET", void 0, 6, signal);
892
- const adminUsers = data.users.map((u) => convertUser(u));
893
- setHasAdminUsers(adminUsers.length > 0);
894
- if (adminUsers.length > 0) {
895
- mergeIntoCache(adminUsers);
896
- }
897
- setUsersError(void 0);
898
- } catch (error) {
899
- if (error instanceof Error && error.name === "AbortError") return;
900
- console.error("Failed to check admin users:", error);
901
- setUsersError(error instanceof Error ? error : new Error(String(error)));
902
- }
903
- }, [apiRequest, mergeIntoCache]);
904
- react.useEffect(() => {
905
- if (!currentUser) {
906
- setLoading(false);
907
- return;
908
- }
909
- const userRoles2 = currentUser.roles ?? [];
910
- const isUserAdmin2 = userRoles2.some((r) => r === "admin" || r === "schema-admin");
911
- if (!isUserAdmin2) {
912
- setLoading(false);
913
- return;
914
- }
915
- if (lastLoadedUidRef.current === currentUser.uid) {
916
- setLoading(false);
917
- return;
918
- }
919
- const abortController = new AbortController();
920
- const load = async () => {
921
- setLoading(true);
922
- const request = apiRequestRef.current;
923
- if (!abortController.signal.aborted) {
924
- try {
925
- const data = await request("/users?role=admin&limit=1", "GET", void 0, 6, abortController.signal);
926
- const adminUsers = data.users.map((u) => convertUser(u));
927
- setHasAdminUsers(adminUsers.length > 0);
928
- if (adminUsers.length > 0) {
929
- mergeIntoCache(adminUsers);
930
- }
931
- setUsersError(void 0);
932
- } catch (error) {
933
- if (error instanceof Error && error.name === "AbortError") return;
934
- console.error("Failed to check admin users:", error);
935
- setUsersError(error instanceof Error ? error : new Error(String(error)));
936
- }
937
- }
938
- if (!abortController.signal.aborted) {
939
- lastLoadedUidRef.current = currentUser.uid;
940
- setLoading(false);
941
- }
942
- };
943
- load();
944
- return () => {
945
- abortController.abort();
946
- };
947
- }, [currentUser?.uid]);
948
- const searchUsers = react.useCallback(async (options) => {
949
- const params = new URLSearchParams();
950
- if (options.limit !== void 0) params.set("limit", String(options.limit));
951
- if (options.offset !== void 0) params.set("offset", String(options.offset));
952
- if (options.search) params.set("search", options.search);
953
- if (options.orderBy) params.set("orderBy", options.orderBy);
954
- if (options.orderDir) params.set("orderDir", options.orderDir);
955
- if (options.roleId) params.set("role", options.roleId);
956
- const qs = params.toString();
957
- const data = await apiRequest("/users" + (qs ? "?" + qs : ""), "GET");
958
- const converted = data.users.map((u) => convertUser(u));
959
- mergeIntoCache(converted);
960
- return {
961
- users: converted,
962
- total: data.total
963
- };
964
- }, [apiRequest, mergeIntoCache]);
965
- const saveUser = react.useCallback(async (user) => {
966
- const roleIds = user.roles ?? [];
967
- const data = await apiRequest(`/users/${user.uid}`, "PUT", {
968
- email: user.email,
969
- displayName: user.displayName,
970
- roles: roleIds
971
- });
972
- const updated = convertUser(data.user);
973
- mergeIntoCache([updated]);
974
- return updated;
975
- }, [apiRequest, mergeIntoCache]);
976
- const createUser = react.useCallback(async (user) => {
977
- const roleIds = user.roles ?? [];
978
- const data = await apiRequest("/users", "POST", {
979
- email: user.email,
980
- displayName: user.displayName,
981
- roles: roleIds
982
- });
983
- const created = convertUser(data.user);
984
- mergeIntoCache([created]);
985
- return {
986
- user: created,
987
- invitationSent: data.invitationSent ?? false,
988
- temporaryPassword: data.temporaryPassword
989
- };
990
- }, [apiRequest, mergeIntoCache]);
991
- const resetPassword2 = react.useCallback(async (user) => {
992
- const data = await apiRequest(`/users/${user.uid}/reset-password`, "POST");
993
- const updatedUser = convertUser(data.user);
994
- mergeIntoCache([updatedUser]);
995
- return {
996
- user: updatedUser,
997
- invitationSent: data.invitationSent ?? false,
998
- temporaryPassword: data.temporaryPassword
999
- };
1000
- }, [apiRequest, mergeIntoCache]);
1001
- const deleteUser = react.useCallback(async (user) => {
1002
- await apiRequest(`/users/${user.uid}`, "DELETE");
1003
- setUserCache((prev) => {
1004
- const next = new Map(prev);
1005
- next.delete(user.uid);
1006
- return next;
1007
- });
1008
- }, [apiRequest]);
1009
- const getUser = react.useCallback((uid) => {
1010
- return userCache.get(uid) ?? null;
1011
- }, [userCache]);
1012
- const defineRolesFor = react.useCallback(async (user) => {
1013
- let existingUser = userCache.get(user.uid) ?? Array.from(userCache.values()).find((u) => u.email === user.email);
1014
- if (!existingUser) {
1015
- try {
1016
- const data = await apiRequest(`/users/${user.uid}`, "GET");
1017
- existingUser = convertUser(data.user);
1018
- mergeIntoCache([existingUser]);
1019
- } catch {
1020
- return void 0;
1021
- }
1022
- }
1023
- const userRoleIds = existingUser.roles ?? [];
1024
- return userRoleIds;
1025
- }, [userCache, apiRequest, mergeIntoCache]);
1026
- const isAdmin = currentUser?.roles?.includes("admin") ?? false;
1027
- const bootstrapAdmin = react.useCallback(async () => {
1028
- try {
1029
- await apiRequest("/bootstrap", "POST");
1030
- await checkAdminExists();
1031
- } catch (error) {
1032
- console.error("Failed to bootstrap admin:", error);
1033
- throw error;
1034
- }
1035
- }, [apiRequest, checkAdminExists]);
1036
- const users = Array.from(userCache.values());
1037
- return {
1038
- loading: effectiveLoading,
1039
- users,
1040
- hasAdminUsers,
1041
- saveUser,
1042
- createUser,
1043
- resetPassword: resetPassword2,
1044
- deleteUser,
1045
- isAdmin,
1046
- allowDefaultRolesCreation: isAdmin,
1047
- defineRolesFor,
1048
- getUser,
1049
- searchUsers,
1050
- usersError,
1051
- bootstrapAdmin
1052
- };
1053
- }
1054
- exports2.AuthApiError = AuthApiError;
1055
- exports2.fetchAuthConfig = fetchAuthConfig;
1056
- exports2.getApiUrl = getApiUrl;
1057
- exports2.setApiUrl = setApiUrl;
1058
- exports2.useBackendUserManagement = useBackendUserManagement;
1059
- exports2.useRebaseAuthController = useRebaseAuthController;
1060
- Object.defineProperty(exports2, Symbol.toStringTag, { value: "Module" });
1061
- }));
1062
- //# sourceMappingURL=index.umd.js.map
2
+ typeof exports === "object" && typeof module !== "undefined" ? factory(exports, require("react")) : typeof define === "function" && define.amd ? define(["exports", "react"], factory) : (global = typeof globalThis !== "undefined" ? globalThis : global || self, factory(global["Rebase Auth"] = {}, global.react));
3
+ })(this, function(exports, react) {
4
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
5
+ //#region src/api.ts
6
+ /**
7
+ * Default API URL - can be overridden in hook props
8
+ */
9
+ var baseApiUrl = "";
10
+ /**
11
+ * Configure the API base URL
12
+ */
13
+ function setApiUrl(url) {
14
+ baseApiUrl = url;
15
+ }
16
+ /**
17
+ * Get the current API URL
18
+ */
19
+ function getApiUrl() {
20
+ return baseApiUrl;
21
+ }
22
+ var AuthApiError = class extends Error {
23
+ code;
24
+ constructor(message, code) {
25
+ super(message);
26
+ this.code = code;
27
+ this.name = "AuthApiError";
28
+ }
29
+ };
30
+ async function handleResponse(response) {
31
+ let data;
32
+ try {
33
+ data = await response.json();
34
+ } catch (parseError) {
35
+ throw new AuthApiError(`Server returned non-JSON response (status: ${response.status})`, "PARSE_ERROR");
36
+ }
37
+ if (!response.ok) throw new AuthApiError(data.error?.message || "Request failed", data.error?.code || "UNKNOWN_ERROR");
38
+ return data;
39
+ }
40
+ /**
41
+ * Wrapper for fetch that catches generic network failures (like server down)
42
+ * and translates them to an AuthApiError.
43
+ */
44
+ async function fetchWithHandling(input, init) {
45
+ try {
46
+ return await fetch(input, init);
47
+ } catch (error) {
48
+ if (error instanceof TypeError && error.message.includes("Failed to fetch")) throw new AuthApiError("Failed to connect to the backend server. The backend might be down or failed to initialize (e.g., database connection timeout).", "NETWORK_ERROR");
49
+ throw new AuthApiError("Network error: " + (error instanceof Error ? error.message : String(error)), "NETWORK_ERROR");
50
+ }
51
+ }
52
+ /**
53
+ * Register a new user with email/password
54
+ */
55
+ async function register(email, password, displayName) {
56
+ return handleResponse(await fetchWithHandling(`${baseApiUrl}/api/auth/register`, {
57
+ method: "POST",
58
+ headers: { "Content-Type": "application/json" },
59
+ body: JSON.stringify({
60
+ email,
61
+ password,
62
+ displayName
63
+ })
64
+ }));
65
+ }
66
+ /**
67
+ * Login with email/password
68
+ */
69
+ async function login(email, password) {
70
+ return handleResponse(await fetchWithHandling(`${baseApiUrl}/api/auth/login`, {
71
+ method: "POST",
72
+ headers: { "Content-Type": "application/json" },
73
+ body: JSON.stringify({
74
+ email,
75
+ password
76
+ })
77
+ }));
78
+ }
79
+ /**
80
+ * Login with Google.
81
+ *
82
+ * Accepts one of:
83
+ * - `{ idToken }` — ID-token flow (One Tap / Sign In button)
84
+ * - `{ accessToken }` — Access-token flow (popup)
85
+ * - `{ code, redirectUri }` — Authorization code flow (most secure)
86
+ */
87
+ async function googleLogin(payload) {
88
+ return handleResponse(await fetchWithHandling(`${baseApiUrl}/api/auth/google`, {
89
+ method: "POST",
90
+ headers: { "Content-Type": "application/json" },
91
+ body: JSON.stringify(payload)
92
+ }));
93
+ }
94
+ /**
95
+ * Generic OAuth login — works with any provider registered on the backend.
96
+ * The `providerId` is used to build the endpoint: `/api/auth/{providerId}`.
97
+ */
98
+ async function oauthLogin(providerId, payload) {
99
+ return handleResponse(await fetchWithHandling(`${baseApiUrl}/api/auth/${providerId}`, {
100
+ method: "POST",
101
+ headers: { "Content-Type": "application/json" },
102
+ body: JSON.stringify(payload)
103
+ }));
104
+ }
105
+ /**
106
+ * Refresh access token using refresh token
107
+ */
108
+ async function refreshAccessToken(refreshToken) {
109
+ return handleResponse(await fetchWithHandling(`${baseApiUrl}/api/auth/refresh`, {
110
+ method: "POST",
111
+ headers: { "Content-Type": "application/json" },
112
+ body: JSON.stringify({ refreshToken })
113
+ }));
114
+ }
115
+ /**
116
+ * Logout and invalidate refresh token
117
+ */
118
+ async function logout(refreshToken) {
119
+ await fetchWithHandling(`${baseApiUrl}/api/auth/logout`, {
120
+ method: "POST",
121
+ headers: { "Content-Type": "application/json" },
122
+ body: JSON.stringify({ refreshToken })
123
+ });
124
+ }
125
+ /**
126
+ * Get current user info
127
+ */
128
+ async function getCurrentUser(accessToken) {
129
+ return handleResponse(await fetchWithHandling(`${baseApiUrl}/api/auth/me`, {
130
+ method: "GET",
131
+ headers: {
132
+ "Content-Type": "application/json",
133
+ "Authorization": `Bearer ${accessToken}`
134
+ }
135
+ }));
136
+ }
137
+ /**
138
+ * Request password reset email
139
+ */
140
+ async function forgotPassword(email) {
141
+ return handleResponse(await fetchWithHandling(`${baseApiUrl}/api/auth/forgot-password`, {
142
+ method: "POST",
143
+ headers: { "Content-Type": "application/json" },
144
+ body: JSON.stringify({ email })
145
+ }));
146
+ }
147
+ /**
148
+ * Reset password using token from email
149
+ */
150
+ async function resetPassword(token, password) {
151
+ return handleResponse(await fetchWithHandling(`${baseApiUrl}/api/auth/reset-password`, {
152
+ method: "POST",
153
+ headers: { "Content-Type": "application/json" },
154
+ body: JSON.stringify({
155
+ token,
156
+ password
157
+ })
158
+ }));
159
+ }
160
+ /**
161
+ * Change password for authenticated user
162
+ */
163
+ async function changePassword(accessToken, oldPassword, newPassword) {
164
+ return handleResponse(await fetchWithHandling(`${baseApiUrl}/api/auth/change-password`, {
165
+ method: "POST",
166
+ headers: {
167
+ "Content-Type": "application/json",
168
+ "Authorization": `Bearer ${accessToken}`
169
+ },
170
+ body: JSON.stringify({
171
+ oldPassword,
172
+ newPassword
173
+ })
174
+ }));
175
+ }
176
+ /**
177
+ * Update current user profile
178
+ */
179
+ async function updateProfile(accessToken, displayName, photoURL) {
180
+ return handleResponse(await fetchWithHandling(`${baseApiUrl}/api/auth/me`, {
181
+ method: "PATCH",
182
+ headers: {
183
+ "Content-Type": "application/json",
184
+ "Authorization": `Bearer ${accessToken}`
185
+ },
186
+ body: JSON.stringify({
187
+ displayName,
188
+ photoURL
189
+ })
190
+ }));
191
+ }
192
+ /**
193
+ * Fetch active sessions for current user
194
+ */
195
+ async function fetchSessions(accessToken, currentRefreshToken) {
196
+ const headers = {
197
+ "Content-Type": "application/json",
198
+ "Authorization": `Bearer ${accessToken}`
199
+ };
200
+ if (currentRefreshToken) headers["X-Refresh-Token"] = currentRefreshToken;
201
+ return handleResponse(await fetchWithHandling(`${baseApiUrl}/api/auth/sessions`, {
202
+ method: "GET",
203
+ headers
204
+ }));
205
+ }
206
+ /**
207
+ * Revoke a specific session
208
+ */
209
+ async function revokeSession(accessToken, sessionId) {
210
+ return handleResponse(await fetchWithHandling(`${baseApiUrl}/api/auth/sessions/${sessionId}`, {
211
+ method: "DELETE",
212
+ headers: {
213
+ "Content-Type": "application/json",
214
+ "Authorization": `Bearer ${accessToken}`
215
+ }
216
+ }));
217
+ }
218
+ /**
219
+ * Revoke all sessions for current user
220
+ */
221
+ async function revokeAllSessions(accessToken) {
222
+ return handleResponse(await fetchWithHandling(`${baseApiUrl}/api/auth/sessions`, {
223
+ method: "DELETE",
224
+ headers: {
225
+ "Content-Type": "application/json",
226
+ "Authorization": `Bearer ${accessToken}`
227
+ }
228
+ }));
229
+ }
230
+ /**
231
+ * Inflight promise for `fetchAuthConfig` — ensures concurrent callers
232
+ * (e.g. React StrictMode double-mount) reuse the same network request.
233
+ */
234
+ var authConfigInflight = null;
235
+ /**
236
+ * Cached result of the last successful `fetchAuthConfig` call.
237
+ * Auth config is static for the lifetime of the app session, so
238
+ * repeat calls (e.g. from effect re-runs) return instantly.
239
+ */
240
+ var authConfigCached = null;
241
+ /**
242
+ * Fetch auth configuration / status from the backend
243
+ * This is an unauthenticated endpoint used to detect bootstrap mode.
244
+ *
245
+ * Results are cached for the session lifetime.
246
+ * Concurrent calls are deduplicated: only one network request is made
247
+ * and all callers share the same promise.
248
+ */
249
+ async function fetchAuthConfig() {
250
+ if (authConfigCached) return authConfigCached;
251
+ if (authConfigInflight) return authConfigInflight;
252
+ authConfigInflight = (async () => {
253
+ return handleResponse(await fetchWithHandling(`${baseApiUrl}/api/auth/config`, {
254
+ method: "GET",
255
+ headers: { "Content-Type": "application/json" }
256
+ }));
257
+ })();
258
+ try {
259
+ const result = await authConfigInflight;
260
+ authConfigCached = result;
261
+ return result;
262
+ } finally {
263
+ authConfigInflight = null;
264
+ }
265
+ }
266
+ //#endregion
267
+ //#region src/hooks/useRebaseAuthController.ts
268
+ var STORAGE_KEY = "rebase_react_auth";
269
+ var TOKEN_REFRESH_BUFFER_MS = 120 * 1e3;
270
+ /**
271
+ * Convert UserInfo from API to Rebase User type
272
+ */
273
+ function convertToUser(userInfo) {
274
+ return {
275
+ uid: userInfo.uid,
276
+ email: userInfo.email,
277
+ displayName: userInfo.displayName || null,
278
+ photoURL: userInfo.photoURL || null,
279
+ providerId: "custom",
280
+ isAnonymous: false,
281
+ roles: userInfo.roles || [],
282
+ metadata: userInfo.metadata
283
+ };
284
+ }
285
+ /**
286
+ * Save auth data to localStorage
287
+ */
288
+ function saveAuthToStorage(tokens, user) {
289
+ try {
290
+ const data = {
291
+ tokens,
292
+ user
293
+ };
294
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
295
+ } catch (e) {}
296
+ }
297
+ /**
298
+ * Load auth data from localStorage
299
+ */
300
+ function loadAuthFromStorage() {
301
+ try {
302
+ const data = localStorage.getItem(STORAGE_KEY);
303
+ if (data) return JSON.parse(data);
304
+ } catch (e) {
305
+ console.warn("Failed to load auth from storage:", e);
306
+ }
307
+ return null;
308
+ }
309
+ /**
310
+ * Clear auth data from localStorage
311
+ */
312
+ function clearAuthFromStorage() {
313
+ try {
314
+ localStorage.removeItem(STORAGE_KEY);
315
+ } catch (e) {
316
+ console.warn("Failed to clear auth from storage:", e);
317
+ }
318
+ }
319
+ /**
320
+ * Check if token is expired or about to expire
321
+ */
322
+ function isTokenExpiredOrNearExpiry(expiresAt, bufferMs = TOKEN_REFRESH_BUFFER_MS) {
323
+ return Date.now() + bufferMs >= expiresAt;
324
+ }
325
+ /**
326
+ * Auth controller hook for JWT-based authentication
327
+ * with @rebasepro/server-core
328
+ *
329
+ * @param props Configuration options
330
+ * @returns RebaseAuthController instance
331
+ */
332
+ function useRebaseAuthController(props = {}) {
333
+ const { client, apiUrl, onSignOut, defineRolesFor } = props;
334
+ const [user, setUser] = (0, react.useState)(null);
335
+ const [authLoading, setAuthLoading] = (0, react.useState)(false);
336
+ const [initialLoading, setInitialLoading] = (0, react.useState)(true);
337
+ const [authError, setAuthError] = (0, react.useState)(null);
338
+ const [authProviderError, setAuthProviderError] = (0, react.useState)(null);
339
+ const [loginSkipped, setLoginSkipped] = (0, react.useState)(false);
340
+ const [extra, setExtra] = (0, react.useState)(null);
341
+ const [authConfig, setAuthConfig] = (0, react.useState)(null);
342
+ const tokensRef = (0, react.useRef)(null);
343
+ const refreshTimeoutRef = (0, react.useRef)(null);
344
+ const refreshPromiseRef = (0, react.useRef)(null);
345
+ const isMountedRef = (0, react.useRef)(true);
346
+ (0, react.useEffect)(() => {
347
+ const url = client?.baseUrl || apiUrl;
348
+ if (url) setApiUrl(url);
349
+ }, [
350
+ client,
351
+ client?.baseUrl,
352
+ apiUrl
353
+ ]);
354
+ const clearError = (0, react.useCallback)(() => {
355
+ setAuthProviderError(null);
356
+ }, []);
357
+ const clearSessionAndSignOut = (0, react.useCallback)(() => {
358
+ tokensRef.current = null;
359
+ clearAuthFromStorage();
360
+ if (refreshTimeoutRef.current) {
361
+ clearTimeout(refreshTimeoutRef.current);
362
+ refreshTimeoutRef.current = null;
363
+ }
364
+ setUser(null);
365
+ setLoginSkipped(false);
366
+ onSignOut?.();
367
+ }, [onSignOut]);
368
+ /**
369
+ * Refresh the access token using the stored refresh token.
370
+ * Returns the new tokens or null if refresh failed.
371
+ */
372
+ const refreshAccessToken$1 = (0, react.useCallback)(async () => {
373
+ if (refreshPromiseRef.current) return refreshPromiseRef.current;
374
+ const executeRefresh = async () => {
375
+ const storedData = loadAuthFromStorage();
376
+ if (storedData?.tokens?.accessTokenExpiresAt) {
377
+ const storedTokens = storedData.tokens;
378
+ if (!isTokenExpiredOrNearExpiry(storedTokens.accessTokenExpiresAt) && storedTokens.accessToken !== tokensRef.current?.accessToken) {
379
+ tokensRef.current = storedTokens;
380
+ return storedTokens;
381
+ }
382
+ }
383
+ const currentTokens = tokensRef.current;
384
+ if (!currentTokens?.refreshToken) return null;
385
+ try {
386
+ const newTokens = (await refreshAccessToken(currentTokens.refreshToken)).tokens;
387
+ tokensRef.current = newTokens;
388
+ const latestStoredData = loadAuthFromStorage();
389
+ if (latestStoredData) saveAuthToStorage(newTokens, latestStoredData.user);
390
+ return newTokens;
391
+ } catch (error) {
392
+ if (error instanceof Error && error.code === "NETWORK_ERROR") throw error;
393
+ return null;
394
+ } finally {
395
+ refreshPromiseRef.current = null;
396
+ }
397
+ };
398
+ refreshPromiseRef.current = executeRefresh();
399
+ return refreshPromiseRef.current;
400
+ }, []);
401
+ const scheduleTokenRefresh = (0, react.useCallback)((tokens) => {
402
+ if (refreshTimeoutRef.current) clearTimeout(refreshTimeoutRef.current);
403
+ const timeUntilRefresh = tokens.accessTokenExpiresAt - TOKEN_REFRESH_BUFFER_MS - Date.now();
404
+ if (timeUntilRefresh <= 0) {
405
+ refreshAccessToken$1().then((newTokens) => {
406
+ if (newTokens && isMountedRef.current) scheduleTokenRefresh(newTokens);
407
+ else if (!newTokens && isMountedRef.current) clearSessionAndSignOut();
408
+ });
409
+ return;
410
+ }
411
+ refreshTimeoutRef.current = setTimeout(async () => {
412
+ if (!isMountedRef.current) return;
413
+ try {
414
+ const newTokens = await refreshAccessToken$1();
415
+ if (newTokens && isMountedRef.current) scheduleTokenRefresh(newTokens);
416
+ else if (!newTokens && isMountedRef.current) clearSessionAndSignOut();
417
+ } catch (error) {
418
+ if (isMountedRef.current) refreshTimeoutRef.current = setTimeout(() => {
419
+ scheduleTokenRefresh(tokens);
420
+ }, 1e4);
421
+ }
422
+ }, timeUntilRefresh);
423
+ }, [refreshAccessToken$1, clearSessionAndSignOut]);
424
+ const getAuthToken = (0, react.useCallback)(async () => {
425
+ if (initialLoading) throw new Error("Auth is still loading");
426
+ const currentTokens = tokensRef.current;
427
+ if (!currentTokens) throw new Error("User is not logged in");
428
+ if (isTokenExpiredOrNearExpiry(currentTokens.accessTokenExpiresAt)) try {
429
+ const newTokens = await refreshAccessToken$1();
430
+ if (!newTokens) {
431
+ clearSessionAndSignOut();
432
+ throw new Error("Session expired. Please login again.");
433
+ }
434
+ return newTokens.accessToken;
435
+ } catch (error) {
436
+ if (error instanceof Error && error.code === "NETWORK_ERROR") throw error;
437
+ clearSessionAndSignOut();
438
+ throw error;
439
+ }
440
+ return currentTokens.accessToken;
441
+ }, [
442
+ initialLoading,
443
+ refreshAccessToken$1,
444
+ clearSessionAndSignOut
445
+ ]);
446
+ (0, react.useEffect)(() => {
447
+ if (client) {
448
+ client.setAuthTokenGetter?.(async () => {
449
+ try {
450
+ return await getAuthToken();
451
+ } catch {
452
+ return null;
453
+ }
454
+ });
455
+ if (client.setOnUnauthorized) client.setOnUnauthorized(async () => {
456
+ try {
457
+ if (await refreshAccessToken$1()) return true;
458
+ clearSessionAndSignOut();
459
+ return false;
460
+ } catch (e) {
461
+ clearSessionAndSignOut();
462
+ return false;
463
+ }
464
+ });
465
+ if (client.ws) client.ws.setAuthTokenGetter(async () => {
466
+ try {
467
+ return await getAuthToken();
468
+ } catch {
469
+ return null;
470
+ }
471
+ });
472
+ }
473
+ }, [
474
+ client,
475
+ getAuthToken,
476
+ refreshAccessToken$1,
477
+ clearSessionAndSignOut
478
+ ]);
479
+ const handleAuthSuccess = (0, react.useCallback)(async (userInfo, tokens) => {
480
+ tokensRef.current = tokens;
481
+ let convertedUser = convertToUser(userInfo);
482
+ if (defineRolesFor) {
483
+ const customRoles = await defineRolesFor(convertedUser);
484
+ if (customRoles) convertedUser = {
485
+ ...convertedUser,
486
+ roles: customRoles
487
+ };
488
+ }
489
+ saveAuthToStorage(tokens, userInfo);
490
+ setUser(convertedUser);
491
+ setAuthError(null);
492
+ setAuthProviderError(null);
493
+ setLoginSkipped(false);
494
+ scheduleTokenRefresh(tokens);
495
+ }, [scheduleTokenRefresh, defineRolesFor]);
496
+ const emailPasswordLogin = (0, react.useCallback)(async (email, password) => {
497
+ setAuthLoading(true);
498
+ setAuthProviderError(null);
499
+ try {
500
+ const response = await login(email, password);
501
+ await handleAuthSuccess(response.user, response.tokens);
502
+ } catch (error) {
503
+ setAuthProviderError(error);
504
+ throw error;
505
+ } finally {
506
+ setAuthLoading(false);
507
+ }
508
+ }, [handleAuthSuccess]);
509
+ const register$1 = (0, react.useCallback)(async (email, password, displayName) => {
510
+ setAuthLoading(true);
511
+ setAuthProviderError(null);
512
+ try {
513
+ const response = await register(email, password, displayName);
514
+ await handleAuthSuccess(response.user, response.tokens);
515
+ } catch (error) {
516
+ setAuthProviderError(error);
517
+ throw error;
518
+ } finally {
519
+ setAuthLoading(false);
520
+ }
521
+ }, [handleAuthSuccess]);
522
+ const googleLogin$1 = (0, react.useCallback)(async (payload) => {
523
+ setAuthLoading(true);
524
+ setAuthProviderError(null);
525
+ try {
526
+ const response = await googleLogin(payload);
527
+ await handleAuthSuccess(response.user, response.tokens);
528
+ } catch (error) {
529
+ setAuthProviderError(error);
530
+ throw error;
531
+ } finally {
532
+ setAuthLoading(false);
533
+ }
534
+ }, [handleAuthSuccess]);
535
+ const oauthLogin$1 = (0, react.useCallback)(async (providerId, payload) => {
536
+ setAuthLoading(true);
537
+ setAuthProviderError(null);
538
+ try {
539
+ const response = await oauthLogin(providerId, payload);
540
+ await handleAuthSuccess(response.user, response.tokens);
541
+ } catch (error) {
542
+ setAuthProviderError(error);
543
+ throw error;
544
+ } finally {
545
+ setAuthLoading(false);
546
+ }
547
+ }, [handleAuthSuccess]);
548
+ const signOut = (0, react.useCallback)(async () => {
549
+ try {
550
+ if (tokensRef.current) await logout(tokensRef.current.refreshToken);
551
+ } catch (error) {
552
+ console.error("Logout error:", error);
553
+ } finally {
554
+ clearSessionAndSignOut();
555
+ }
556
+ }, [clearSessionAndSignOut]);
557
+ const skipLogin = (0, react.useCallback)(() => {
558
+ setLoginSkipped(true);
559
+ setUser(null);
560
+ }, []);
561
+ const forgotPassword$1 = (0, react.useCallback)(async (email) => {
562
+ setAuthLoading(true);
563
+ setAuthProviderError(null);
564
+ try {
565
+ await forgotPassword(email);
566
+ } catch (error) {
567
+ setAuthProviderError(error);
568
+ throw error;
569
+ } finally {
570
+ setAuthLoading(false);
571
+ }
572
+ }, []);
573
+ const resetPassword$1 = (0, react.useCallback)(async (token, password) => {
574
+ setAuthLoading(true);
575
+ setAuthProviderError(null);
576
+ try {
577
+ await resetPassword(token, password);
578
+ } catch (error) {
579
+ setAuthProviderError(error);
580
+ throw error;
581
+ } finally {
582
+ setAuthLoading(false);
583
+ }
584
+ }, []);
585
+ const changePassword$1 = (0, react.useCallback)(async (oldPassword, newPassword) => {
586
+ setAuthLoading(true);
587
+ setAuthProviderError(null);
588
+ try {
589
+ if (!tokensRef.current) throw new Error("User is not logged in");
590
+ await changePassword(tokensRef.current.accessToken, oldPassword, newPassword);
591
+ clearSessionAndSignOut();
592
+ } catch (error) {
593
+ setAuthProviderError(error);
594
+ throw error;
595
+ } finally {
596
+ setAuthLoading(false);
597
+ }
598
+ }, [clearSessionAndSignOut]);
599
+ const updateProfile$1 = (0, react.useCallback)(async (displayName, photoURL) => {
600
+ setAuthLoading(true);
601
+ setAuthProviderError(null);
602
+ try {
603
+ if (!tokensRef.current) throw new Error("User is not logged in");
604
+ const response = await updateProfile(tokensRef.current.accessToken, displayName, photoURL);
605
+ let convertedUser = convertToUser(response.user);
606
+ if (defineRolesFor) {
607
+ const customRoles = await defineRolesFor(convertedUser);
608
+ if (customRoles) convertedUser = {
609
+ ...convertedUser,
610
+ roles: customRoles
611
+ };
612
+ }
613
+ const storedData = loadAuthFromStorage();
614
+ if (storedData) saveAuthToStorage(storedData.tokens, response.user);
615
+ setUser(convertedUser);
616
+ return convertedUser;
617
+ } catch (error) {
618
+ setAuthProviderError(error);
619
+ throw error;
620
+ } finally {
621
+ setAuthLoading(false);
622
+ }
623
+ }, [defineRolesFor]);
624
+ const fetchSessions$1 = (0, react.useCallback)(async () => {
625
+ try {
626
+ if (!tokensRef.current) throw new Error("User is not logged in");
627
+ return (await fetchSessions(tokensRef.current.accessToken, tokensRef.current.refreshToken)).sessions;
628
+ } catch (error) {
629
+ setAuthProviderError(error);
630
+ throw error;
631
+ }
632
+ }, []);
633
+ const revokeSession$1 = (0, react.useCallback)(async (sessionId) => {
634
+ try {
635
+ if (!tokensRef.current) throw new Error("User is not logged in");
636
+ await revokeSession(tokensRef.current.accessToken, sessionId);
637
+ } catch (error) {
638
+ setAuthProviderError(error);
639
+ throw error;
640
+ }
641
+ }, []);
642
+ (0, react.useEffect)(() => {
643
+ isMountedRef.current = true;
644
+ const restoreAuth = async () => {
645
+ try {
646
+ const config = await fetchAuthConfig();
647
+ if (isMountedRef.current) setAuthConfig(config);
648
+ } catch (e) {}
649
+ const stored = loadAuthFromStorage();
650
+ if (!stored) {
651
+ setInitialLoading(false);
652
+ return;
653
+ }
654
+ if (!stored.tokens?.refreshToken) {
655
+ clearAuthFromStorage();
656
+ setInitialLoading(false);
657
+ return;
658
+ }
659
+ const expiresAt = stored.tokens.accessTokenExpiresAt;
660
+ if (typeof expiresAt !== "number" || !Number.isFinite(expiresAt)) {
661
+ clearAuthFromStorage();
662
+ setInitialLoading(false);
663
+ return;
664
+ }
665
+ if (!isTokenExpiredOrNearExpiry(stored.tokens.accessTokenExpiresAt)) {
666
+ tokensRef.current = stored.tokens;
667
+ let userToSet = convertToUser(stored.user);
668
+ if (defineRolesFor) {
669
+ const customRoles = await defineRolesFor(userToSet);
670
+ if (customRoles) userToSet = {
671
+ ...userToSet,
672
+ roles: customRoles
673
+ };
674
+ }
675
+ setUser(userToSet);
676
+ scheduleTokenRefresh(stored.tokens);
677
+ setInitialLoading(false);
678
+ return;
679
+ }
680
+ tokensRef.current = stored.tokens;
681
+ try {
682
+ const newTokens = await refreshAccessToken$1();
683
+ if (!newTokens) {
684
+ clearAuthFromStorage();
685
+ tokensRef.current = null;
686
+ setInitialLoading(false);
687
+ return;
688
+ }
689
+ if (!isMountedRef.current) return;
690
+ let userToSet;
691
+ try {
692
+ const meResponse = await getCurrentUser(newTokens.accessToken);
693
+ if (!isMountedRef.current) return;
694
+ const freshUserInfo = meResponse.user;
695
+ saveAuthToStorage(newTokens, freshUserInfo);
696
+ userToSet = convertToUser(freshUserInfo);
697
+ if (defineRolesFor) {
698
+ const customRoles = await defineRolesFor(userToSet);
699
+ if (!isMountedRef.current) return;
700
+ if (customRoles) userToSet = {
701
+ ...userToSet,
702
+ roles: customRoles
703
+ };
704
+ }
705
+ } catch (meError) {
706
+ if (!isMountedRef.current) return;
707
+ if (meError instanceof AuthApiError && (meError.code === "NOT_FOUND" || meError.code === "UNAUTHORIZED")) {
708
+ clearSessionAndSignOut();
709
+ return;
710
+ }
711
+ userToSet = convertToUser(stored.user);
712
+ }
713
+ if (!isMountedRef.current) return;
714
+ setUser(userToSet);
715
+ scheduleTokenRefresh(newTokens);
716
+ } catch (error) {
717
+ if (!isMountedRef.current) return;
718
+ if (!(error instanceof Error && error.code === "NETWORK_ERROR")) {
719
+ clearAuthFromStorage();
720
+ tokensRef.current = null;
721
+ }
722
+ } finally {
723
+ if (isMountedRef.current) setInitialLoading(false);
724
+ }
725
+ };
726
+ restoreAuth();
727
+ return () => {
728
+ isMountedRef.current = false;
729
+ };
730
+ }, [
731
+ scheduleTokenRefresh,
732
+ defineRolesFor,
733
+ refreshAccessToken$1
734
+ ]);
735
+ (0, react.useEffect)(() => {
736
+ const handleVisibilityChange = async () => {
737
+ if (initialLoading) return;
738
+ if (document.visibilityState === "visible" && tokensRef.current) {
739
+ if (isTokenExpiredOrNearExpiry(tokensRef.current.accessTokenExpiresAt)) try {
740
+ const newTokens = await refreshAccessToken$1();
741
+ if (newTokens && isMountedRef.current) scheduleTokenRefresh(newTokens);
742
+ else if (!newTokens && isMountedRef.current) clearSessionAndSignOut();
743
+ } catch (e) {}
744
+ }
745
+ };
746
+ document.addEventListener("visibilitychange", handleVisibilityChange);
747
+ return () => {
748
+ document.removeEventListener("visibilitychange", handleVisibilityChange);
749
+ };
750
+ }, [
751
+ initialLoading,
752
+ refreshAccessToken$1,
753
+ scheduleTokenRefresh,
754
+ clearSessionAndSignOut
755
+ ]);
756
+ const getApiUrl$1 = (0, react.useCallback)(() => {
757
+ return getApiUrl();
758
+ }, []);
759
+ (0, react.useEffect)(() => {
760
+ return () => {
761
+ isMountedRef.current = false;
762
+ if (refreshTimeoutRef.current) clearTimeout(refreshTimeoutRef.current);
763
+ };
764
+ }, []);
765
+ const revokeAllSessions$1 = (0, react.useCallback)(async () => {
766
+ try {
767
+ if (!tokensRef.current) throw new Error("User is not logged in");
768
+ await revokeAllSessions(tokensRef.current.accessToken);
769
+ clearSessionAndSignOut();
770
+ } catch (error) {
771
+ setAuthProviderError(error);
772
+ throw error;
773
+ }
774
+ }, [clearSessionAndSignOut]);
775
+ return {
776
+ user,
777
+ authLoading,
778
+ initialLoading,
779
+ authError,
780
+ authProviderError,
781
+ loginSkipped,
782
+ needsSetup: authConfig?.needsSetup ?? false,
783
+ registrationEnabled: authConfig?.registrationEnabled ?? false,
784
+ getAuthToken,
785
+ getApiUrl: getApiUrl$1,
786
+ signOut,
787
+ emailPasswordLogin,
788
+ register: register$1,
789
+ googleLogin: googleLogin$1,
790
+ oauthLogin: oauthLogin$1,
791
+ skipLogin,
792
+ forgotPassword: forgotPassword$1,
793
+ resetPassword: resetPassword$1,
794
+ changePassword: changePassword$1,
795
+ updateProfile: updateProfile$1,
796
+ fetchSessions: fetchSessions$1,
797
+ revokeSession: revokeSession$1,
798
+ revokeAllSessions: revokeAllSessions$1,
799
+ clearError,
800
+ setAuthProviderError,
801
+ extra,
802
+ setExtra,
803
+ capabilities: {
804
+ emailPasswordLogin: true,
805
+ googleLogin: !!props.googleClientId,
806
+ registration: authConfig?.registrationEnabled ?? false,
807
+ passwordReset: authConfig?.passwordReset ?? false,
808
+ sessionManagement: true,
809
+ profileUpdate: true,
810
+ emailVerification: authConfig?.emailVerification ?? false,
811
+ enabledProviders: authConfig?.enabledProviders ?? []
812
+ }
813
+ };
814
+ }
815
+ //#endregion
816
+ exports.AuthApiError = AuthApiError;
817
+ exports.fetchAuthConfig = fetchAuthConfig;
818
+ exports.getApiUrl = getApiUrl;
819
+ exports.setApiUrl = setApiUrl;
820
+ exports.useRebaseAuthController = useRebaseAuthController;
821
+ });
822
+
823
+ //# sourceMappingURL=index.umd.js.map