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