@rebasepro/auth 0.4.0 → 0.6.0
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/README.md +76 -0
- package/dist/index.d.ts +0 -2
- package/dist/index.es.js +765 -1009
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +822 -1061
- package/dist/index.umd.js.map +1 -1
- package/dist/types.d.ts +1 -1
- package/package.json +13 -13
- package/src/hooks/useRebaseAuthController.ts +5 -3
- package/src/index.ts +0 -3
- package/src/types.ts +1 -1
- package/dist/hooks/useBackendUserManagement.d.ts +0 -70
- package/src/hooks/useBackendUserManagement.ts +0 -504
package/dist/index.es.js
CHANGED
|
@@ -1,1059 +1,815 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
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
|
-
|
|
11
|
+
baseApiUrl = url;
|
|
5
12
|
}
|
|
13
|
+
/**
|
|
14
|
+
* Get the current API URL
|
|
15
|
+
*/
|
|
6
16
|
function getApiUrl() {
|
|
7
|
-
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
-
|
|
194
|
-
|
|
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
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
-
|
|
218
|
-
|
|
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
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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
|
-
|
|
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
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
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
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
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
|