@rebasepro/auth 0.0.1-canary.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/LICENSE +6 -0
- package/dist/api.d.ts +119 -0
- package/dist/components/AdminViews.d.ts +20 -0
- package/dist/components/RebaseLoginView.d.ts +52 -0
- package/dist/hooks/useBackendUserManagement.d.ts +41 -0
- package/dist/hooks/useRebaseAuthController.d.ts +9 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.es.js +1883 -0
- package/dist/index.es.js.map +1 -0
- package/dist/index.umd.js +1883 -0
- package/dist/index.umd.js.map +1 -0
- package/dist/types.d.ts +95 -0
- package/package.json +48 -0
- package/src/api.ts +328 -0
- package/src/components/AdminViews.tsx +795 -0
- package/src/components/RebaseLoginView.tsx +570 -0
- package/src/hooks/useBackendUserManagement.ts +407 -0
- package/src/hooks/useRebaseAuthController.ts +692 -0
- package/src/index.ts +28 -0
- package/src/types.ts +102 -0
package/dist/index.es.js
ADDED
|
@@ -0,0 +1,1883 @@
|
|
|
1
|
+
import { useState, useRef, useEffect, useCallback } from "react";
|
|
2
|
+
import { jsx, jsxs, Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { MailIcon, IconButton, ArrowBackIcon, Typography, TextField, CircularProgress, Button, CenteredView, Container, AddIcon, Table, TableHeader, TableCell, TableBody, TableRow, Tooltip, DeleteIcon, Checkbox, getColorSchemeForSeed, Chip, Dialog, DialogTitle, DialogContent, DialogActions, LoadingButton, MultiSelect, MultiSelectItem } from "@rebasepro/ui";
|
|
4
|
+
import { useModeController, ErrorView, RebaseLogo, useSnackbarController, ConfirmationDialog, useAuthController, FieldCaption, useCollectionRegistryController } from "@rebasepro/core";
|
|
5
|
+
let baseApiUrl = "";
|
|
6
|
+
function setApiUrl(url) {
|
|
7
|
+
baseApiUrl = url;
|
|
8
|
+
}
|
|
9
|
+
function getApiUrl() {
|
|
10
|
+
return baseApiUrl;
|
|
11
|
+
}
|
|
12
|
+
class AuthApiError extends Error {
|
|
13
|
+
code;
|
|
14
|
+
constructor(message, code) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.code = code;
|
|
17
|
+
this.name = "AuthApiError";
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
async function handleResponse(response) {
|
|
21
|
+
let data;
|
|
22
|
+
try {
|
|
23
|
+
data = await response.json();
|
|
24
|
+
} catch (parseError) {
|
|
25
|
+
throw new AuthApiError(
|
|
26
|
+
`Server returned non-JSON response (status: ${response.status})`,
|
|
27
|
+
"PARSE_ERROR"
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
if (!response.ok) {
|
|
31
|
+
throw new AuthApiError(
|
|
32
|
+
data.error?.message || "Request failed",
|
|
33
|
+
data.error?.code || "UNKNOWN_ERROR"
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
return data;
|
|
37
|
+
}
|
|
38
|
+
async function fetchWithHandling(input, init) {
|
|
39
|
+
try {
|
|
40
|
+
return await fetch(input, init);
|
|
41
|
+
} catch (error) {
|
|
42
|
+
if (error instanceof TypeError && error.message.includes("Failed to fetch")) {
|
|
43
|
+
throw new AuthApiError(
|
|
44
|
+
"Failed to connect to the backend server. The backend might be down or failed to initialize (e.g., database connection timeout).",
|
|
45
|
+
"NETWORK_ERROR"
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
throw new AuthApiError("Network error: " + (error instanceof Error ? error.message : String(error)), "NETWORK_ERROR");
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
async function register(email, password, displayName) {
|
|
52
|
+
const response = await fetchWithHandling(`${baseApiUrl}/api/auth/register`, {
|
|
53
|
+
method: "POST",
|
|
54
|
+
headers: { "Content-Type": "application/json" },
|
|
55
|
+
body: JSON.stringify({ email, password, displayName })
|
|
56
|
+
});
|
|
57
|
+
return handleResponse(response);
|
|
58
|
+
}
|
|
59
|
+
async function login(email, password) {
|
|
60
|
+
const response = await fetchWithHandling(`${baseApiUrl}/api/auth/login`, {
|
|
61
|
+
method: "POST",
|
|
62
|
+
headers: { "Content-Type": "application/json" },
|
|
63
|
+
body: JSON.stringify({ email, password })
|
|
64
|
+
});
|
|
65
|
+
return handleResponse(response);
|
|
66
|
+
}
|
|
67
|
+
async function googleLogin(idToken) {
|
|
68
|
+
const response = await fetchWithHandling(`${baseApiUrl}/api/auth/google`, {
|
|
69
|
+
method: "POST",
|
|
70
|
+
headers: { "Content-Type": "application/json" },
|
|
71
|
+
body: JSON.stringify({ idToken })
|
|
72
|
+
});
|
|
73
|
+
return handleResponse(response);
|
|
74
|
+
}
|
|
75
|
+
async function refreshAccessToken(refreshToken) {
|
|
76
|
+
console.log("[AUTH-API] Calling refresh endpoint...");
|
|
77
|
+
const response = await fetchWithHandling(`${baseApiUrl}/api/auth/refresh`, {
|
|
78
|
+
method: "POST",
|
|
79
|
+
headers: { "Content-Type": "application/json" },
|
|
80
|
+
body: JSON.stringify({ refreshToken })
|
|
81
|
+
});
|
|
82
|
+
console.log("[AUTH-API] Refresh response status:", response.status);
|
|
83
|
+
return handleResponse(response);
|
|
84
|
+
}
|
|
85
|
+
async function logout(refreshToken) {
|
|
86
|
+
await fetchWithHandling(`${baseApiUrl}/api/auth/logout`, {
|
|
87
|
+
method: "POST",
|
|
88
|
+
headers: { "Content-Type": "application/json" },
|
|
89
|
+
body: JSON.stringify({ refreshToken })
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
async function getCurrentUser(accessToken) {
|
|
93
|
+
const response = await fetchWithHandling(`${baseApiUrl}/api/auth/me`, {
|
|
94
|
+
method: "GET",
|
|
95
|
+
headers: {
|
|
96
|
+
"Content-Type": "application/json",
|
|
97
|
+
"Authorization": `Bearer ${accessToken}`
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
return handleResponse(response);
|
|
101
|
+
}
|
|
102
|
+
async function forgotPassword(email) {
|
|
103
|
+
const response = await fetchWithHandling(`${baseApiUrl}/api/auth/forgot-password`, {
|
|
104
|
+
method: "POST",
|
|
105
|
+
headers: { "Content-Type": "application/json" },
|
|
106
|
+
body: JSON.stringify({ email })
|
|
107
|
+
});
|
|
108
|
+
return handleResponse(response);
|
|
109
|
+
}
|
|
110
|
+
async function resetPassword(token, password) {
|
|
111
|
+
const response = await fetchWithHandling(`${baseApiUrl}/api/auth/reset-password`, {
|
|
112
|
+
method: "POST",
|
|
113
|
+
headers: { "Content-Type": "application/json" },
|
|
114
|
+
body: JSON.stringify({ token, password })
|
|
115
|
+
});
|
|
116
|
+
return handleResponse(response);
|
|
117
|
+
}
|
|
118
|
+
async function changePassword(accessToken, oldPassword, newPassword) {
|
|
119
|
+
const response = await fetchWithHandling(`${baseApiUrl}/api/auth/change-password`, {
|
|
120
|
+
method: "POST",
|
|
121
|
+
headers: {
|
|
122
|
+
"Content-Type": "application/json",
|
|
123
|
+
"Authorization": `Bearer ${accessToken}`
|
|
124
|
+
},
|
|
125
|
+
body: JSON.stringify({ oldPassword, newPassword })
|
|
126
|
+
});
|
|
127
|
+
return handleResponse(response);
|
|
128
|
+
}
|
|
129
|
+
async function updateProfile(accessToken, displayName, photoURL) {
|
|
130
|
+
const response = await fetchWithHandling(`${baseApiUrl}/api/auth/me`, {
|
|
131
|
+
method: "PATCH",
|
|
132
|
+
headers: {
|
|
133
|
+
"Content-Type": "application/json",
|
|
134
|
+
"Authorization": `Bearer ${accessToken}`
|
|
135
|
+
},
|
|
136
|
+
body: JSON.stringify({ displayName, photoURL })
|
|
137
|
+
});
|
|
138
|
+
return handleResponse(response);
|
|
139
|
+
}
|
|
140
|
+
async function fetchSessions(accessToken, currentRefreshToken) {
|
|
141
|
+
const headers = {
|
|
142
|
+
"Content-Type": "application/json",
|
|
143
|
+
"Authorization": `Bearer ${accessToken}`
|
|
144
|
+
};
|
|
145
|
+
if (currentRefreshToken) {
|
|
146
|
+
headers["X-Refresh-Token"] = currentRefreshToken;
|
|
147
|
+
}
|
|
148
|
+
const response = await fetchWithHandling(`${baseApiUrl}/api/auth/sessions`, {
|
|
149
|
+
method: "GET",
|
|
150
|
+
headers
|
|
151
|
+
});
|
|
152
|
+
return handleResponse(response);
|
|
153
|
+
}
|
|
154
|
+
async function revokeSession(accessToken, sessionId) {
|
|
155
|
+
const response = await fetchWithHandling(`${baseApiUrl}/api/auth/sessions/${sessionId}`, {
|
|
156
|
+
method: "DELETE",
|
|
157
|
+
headers: {
|
|
158
|
+
"Content-Type": "application/json",
|
|
159
|
+
"Authorization": `Bearer ${accessToken}`
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
return handleResponse(response);
|
|
163
|
+
}
|
|
164
|
+
async function revokeAllSessions(accessToken) {
|
|
165
|
+
const response = await fetchWithHandling(`${baseApiUrl}/api/auth/sessions`, {
|
|
166
|
+
method: "DELETE",
|
|
167
|
+
headers: {
|
|
168
|
+
"Content-Type": "application/json",
|
|
169
|
+
"Authorization": `Bearer ${accessToken}`
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
return handleResponse(response);
|
|
173
|
+
}
|
|
174
|
+
async function fetchAuthConfig() {
|
|
175
|
+
const response = await fetchWithHandling(`${baseApiUrl}/api/auth/config`, {
|
|
176
|
+
method: "GET",
|
|
177
|
+
headers: { "Content-Type": "application/json" }
|
|
178
|
+
});
|
|
179
|
+
return handleResponse(response);
|
|
180
|
+
}
|
|
181
|
+
const STORAGE_KEY = "rebase_auth";
|
|
182
|
+
const TOKEN_REFRESH_BUFFER_MS = 2 * 60 * 1e3;
|
|
183
|
+
function convertToUser(userInfo) {
|
|
184
|
+
return {
|
|
185
|
+
uid: userInfo.uid,
|
|
186
|
+
email: userInfo.email,
|
|
187
|
+
displayName: userInfo.displayName || null,
|
|
188
|
+
photoURL: userInfo.photoURL || null,
|
|
189
|
+
providerId: "custom",
|
|
190
|
+
isAnonymous: false,
|
|
191
|
+
roles: userInfo.roles || []
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
function saveAuthToStorage(tokens, user) {
|
|
195
|
+
try {
|
|
196
|
+
const data = { tokens, user };
|
|
197
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
|
|
198
|
+
const expiryDate = new Date(tokens.accessTokenExpiresAt);
|
|
199
|
+
const expiryStr = Number.isFinite(tokens.accessTokenExpiresAt) ? expiryDate.toISOString() : "invalid";
|
|
200
|
+
} catch (e) {
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
function loadAuthFromStorage() {
|
|
204
|
+
try {
|
|
205
|
+
const data = localStorage.getItem(STORAGE_KEY);
|
|
206
|
+
if (data) {
|
|
207
|
+
const parsed = JSON.parse(data);
|
|
208
|
+
return parsed;
|
|
209
|
+
}
|
|
210
|
+
} catch (e) {
|
|
211
|
+
console.warn("Failed to load auth from storage:", e);
|
|
212
|
+
}
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
function clearAuthFromStorage() {
|
|
216
|
+
try {
|
|
217
|
+
localStorage.removeItem(STORAGE_KEY);
|
|
218
|
+
} catch (e) {
|
|
219
|
+
console.warn("Failed to clear auth from storage:", e);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
function isTokenExpiredOrNearExpiry(expiresAt, bufferMs = TOKEN_REFRESH_BUFFER_MS) {
|
|
223
|
+
return Date.now() + bufferMs >= expiresAt;
|
|
224
|
+
}
|
|
225
|
+
function useRebaseAuthController(props = {}) {
|
|
226
|
+
const { apiUrl, onSignOut, defineRolesFor } = props;
|
|
227
|
+
const [user, setUser] = useState(null);
|
|
228
|
+
const [authLoading, setAuthLoading] = useState(false);
|
|
229
|
+
const [initialLoading, setInitialLoading] = useState(true);
|
|
230
|
+
const [authError, setAuthError] = useState(null);
|
|
231
|
+
const [authProviderError, setAuthProviderError] = useState(null);
|
|
232
|
+
const [loginSkipped, setLoginSkipped] = useState(false);
|
|
233
|
+
const [extra, setExtra] = useState(null);
|
|
234
|
+
const [authConfig, setAuthConfig] = useState(null);
|
|
235
|
+
const tokensRef = useRef(null);
|
|
236
|
+
const refreshTimeoutRef = useRef(null);
|
|
237
|
+
const refreshPromiseRef = useRef(null);
|
|
238
|
+
const isMountedRef = useRef(true);
|
|
239
|
+
useEffect(() => {
|
|
240
|
+
if (apiUrl) {
|
|
241
|
+
setApiUrl(apiUrl);
|
|
242
|
+
}
|
|
243
|
+
}, [apiUrl]);
|
|
244
|
+
const clearSessionAndSignOut = useCallback(() => {
|
|
245
|
+
tokensRef.current = null;
|
|
246
|
+
clearAuthFromStorage();
|
|
247
|
+
if (refreshTimeoutRef.current) {
|
|
248
|
+
clearTimeout(refreshTimeoutRef.current);
|
|
249
|
+
refreshTimeoutRef.current = null;
|
|
250
|
+
}
|
|
251
|
+
setUser(null);
|
|
252
|
+
setLoginSkipped(false);
|
|
253
|
+
onSignOut?.();
|
|
254
|
+
}, [onSignOut]);
|
|
255
|
+
const refreshAccessToken$1 = useCallback(async () => {
|
|
256
|
+
if (refreshPromiseRef.current) {
|
|
257
|
+
return refreshPromiseRef.current;
|
|
258
|
+
}
|
|
259
|
+
const executeRefresh = async () => {
|
|
260
|
+
const storedData = loadAuthFromStorage();
|
|
261
|
+
if (storedData?.tokens?.accessTokenExpiresAt) {
|
|
262
|
+
const storedTokens = storedData.tokens;
|
|
263
|
+
if (!isTokenExpiredOrNearExpiry(storedTokens.accessTokenExpiresAt) && storedTokens.accessToken !== tokensRef.current?.accessToken) {
|
|
264
|
+
tokensRef.current = storedTokens;
|
|
265
|
+
return storedTokens;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
const currentTokens = tokensRef.current;
|
|
269
|
+
if (!currentTokens?.refreshToken) {
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
try {
|
|
273
|
+
const response = await refreshAccessToken(currentTokens.refreshToken);
|
|
274
|
+
const newTokens = response.tokens;
|
|
275
|
+
tokensRef.current = newTokens;
|
|
276
|
+
const latestStoredData = loadAuthFromStorage();
|
|
277
|
+
if (latestStoredData) {
|
|
278
|
+
saveAuthToStorage(newTokens, latestStoredData.user);
|
|
279
|
+
}
|
|
280
|
+
const newExpiryStr = Number.isFinite(newTokens.accessTokenExpiresAt) ? new Date(newTokens.accessTokenExpiresAt).toISOString() : "invalid";
|
|
281
|
+
return newTokens;
|
|
282
|
+
} catch (error) {
|
|
283
|
+
if (error instanceof Error && error.code === "NETWORK_ERROR") {
|
|
284
|
+
throw error;
|
|
285
|
+
}
|
|
286
|
+
return null;
|
|
287
|
+
} finally {
|
|
288
|
+
refreshPromiseRef.current = null;
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
refreshPromiseRef.current = executeRefresh();
|
|
292
|
+
return refreshPromiseRef.current;
|
|
293
|
+
}, []);
|
|
294
|
+
const scheduleTokenRefresh = useCallback((tokens) => {
|
|
295
|
+
if (refreshTimeoutRef.current) {
|
|
296
|
+
clearTimeout(refreshTimeoutRef.current);
|
|
297
|
+
}
|
|
298
|
+
const expiresAt = tokens.accessTokenExpiresAt;
|
|
299
|
+
const refreshAt = expiresAt - TOKEN_REFRESH_BUFFER_MS;
|
|
300
|
+
const timeUntilRefresh = refreshAt - Date.now();
|
|
301
|
+
if (timeUntilRefresh <= 0) {
|
|
302
|
+
refreshAccessToken$1().then((newTokens) => {
|
|
303
|
+
if (newTokens && isMountedRef.current) {
|
|
304
|
+
scheduleTokenRefresh(newTokens);
|
|
305
|
+
} else if (!newTokens && isMountedRef.current) {
|
|
306
|
+
clearSessionAndSignOut();
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
refreshTimeoutRef.current = setTimeout(async () => {
|
|
312
|
+
if (!isMountedRef.current) return;
|
|
313
|
+
try {
|
|
314
|
+
const newTokens = await refreshAccessToken$1();
|
|
315
|
+
if (newTokens && isMountedRef.current) {
|
|
316
|
+
scheduleTokenRefresh(newTokens);
|
|
317
|
+
} else if (!newTokens && isMountedRef.current) {
|
|
318
|
+
clearSessionAndSignOut();
|
|
319
|
+
}
|
|
320
|
+
} catch (error) {
|
|
321
|
+
if (isMountedRef.current) {
|
|
322
|
+
refreshTimeoutRef.current = setTimeout(() => {
|
|
323
|
+
scheduleTokenRefresh(tokens);
|
|
324
|
+
}, 1e4);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}, timeUntilRefresh);
|
|
328
|
+
}, [refreshAccessToken$1, clearSessionAndSignOut]);
|
|
329
|
+
const getAuthToken = useCallback(async () => {
|
|
330
|
+
if (initialLoading) {
|
|
331
|
+
throw new Error("Auth is still loading");
|
|
332
|
+
}
|
|
333
|
+
const currentTokens = tokensRef.current;
|
|
334
|
+
if (!currentTokens) {
|
|
335
|
+
throw new Error("User is not logged in");
|
|
336
|
+
}
|
|
337
|
+
if (isTokenExpiredOrNearExpiry(currentTokens.accessTokenExpiresAt)) {
|
|
338
|
+
try {
|
|
339
|
+
const newTokens = await refreshAccessToken$1();
|
|
340
|
+
if (!newTokens) {
|
|
341
|
+
clearSessionAndSignOut();
|
|
342
|
+
throw new Error("Session expired. Please login again.");
|
|
343
|
+
}
|
|
344
|
+
return newTokens.accessToken;
|
|
345
|
+
} catch (error) {
|
|
346
|
+
if (error instanceof Error && error.code === "NETWORK_ERROR") {
|
|
347
|
+
throw error;
|
|
348
|
+
}
|
|
349
|
+
clearSessionAndSignOut();
|
|
350
|
+
throw error;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
return currentTokens.accessToken;
|
|
354
|
+
}, [initialLoading, refreshAccessToken$1, clearSessionAndSignOut]);
|
|
355
|
+
const handleAuthSuccess = useCallback(async (userInfo, tokens) => {
|
|
356
|
+
tokensRef.current = tokens;
|
|
357
|
+
let convertedUser = convertToUser(userInfo);
|
|
358
|
+
if (defineRolesFor) {
|
|
359
|
+
const customRoles = await defineRolesFor(convertedUser);
|
|
360
|
+
if (customRoles) {
|
|
361
|
+
convertedUser = { ...convertedUser, roles: customRoles.map((r) => r.id) };
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
saveAuthToStorage(tokens, userInfo);
|
|
365
|
+
setUser(convertedUser);
|
|
366
|
+
setAuthError(null);
|
|
367
|
+
setAuthProviderError(null);
|
|
368
|
+
setLoginSkipped(false);
|
|
369
|
+
scheduleTokenRefresh(tokens);
|
|
370
|
+
}, [scheduleTokenRefresh, defineRolesFor]);
|
|
371
|
+
const emailPasswordLogin = useCallback(async (email, password) => {
|
|
372
|
+
setAuthLoading(true);
|
|
373
|
+
setAuthProviderError(null);
|
|
374
|
+
try {
|
|
375
|
+
const response = await login(email, password);
|
|
376
|
+
await handleAuthSuccess(response.user, response.tokens);
|
|
377
|
+
} catch (error) {
|
|
378
|
+
setAuthProviderError(error);
|
|
379
|
+
throw error;
|
|
380
|
+
} finally {
|
|
381
|
+
setAuthLoading(false);
|
|
382
|
+
}
|
|
383
|
+
}, [handleAuthSuccess]);
|
|
384
|
+
const register$1 = useCallback(async (email, password, displayName) => {
|
|
385
|
+
setAuthLoading(true);
|
|
386
|
+
setAuthProviderError(null);
|
|
387
|
+
try {
|
|
388
|
+
const response = await register(email, password, displayName);
|
|
389
|
+
await handleAuthSuccess(response.user, response.tokens);
|
|
390
|
+
} catch (error) {
|
|
391
|
+
setAuthProviderError(error);
|
|
392
|
+
throw error;
|
|
393
|
+
} finally {
|
|
394
|
+
setAuthLoading(false);
|
|
395
|
+
}
|
|
396
|
+
}, [handleAuthSuccess]);
|
|
397
|
+
const googleLogin$1 = useCallback(async (idToken) => {
|
|
398
|
+
setAuthLoading(true);
|
|
399
|
+
setAuthProviderError(null);
|
|
400
|
+
try {
|
|
401
|
+
const response = await googleLogin(idToken);
|
|
402
|
+
await handleAuthSuccess(response.user, response.tokens);
|
|
403
|
+
} catch (error) {
|
|
404
|
+
setAuthProviderError(error);
|
|
405
|
+
throw error;
|
|
406
|
+
} finally {
|
|
407
|
+
setAuthLoading(false);
|
|
408
|
+
}
|
|
409
|
+
}, [handleAuthSuccess]);
|
|
410
|
+
const signOut = useCallback(async () => {
|
|
411
|
+
try {
|
|
412
|
+
if (tokensRef.current) {
|
|
413
|
+
await logout(tokensRef.current.refreshToken);
|
|
414
|
+
}
|
|
415
|
+
} catch (error) {
|
|
416
|
+
console.error("Logout error:", error);
|
|
417
|
+
} finally {
|
|
418
|
+
clearSessionAndSignOut();
|
|
419
|
+
}
|
|
420
|
+
}, [clearSessionAndSignOut]);
|
|
421
|
+
const skipLogin = useCallback(() => {
|
|
422
|
+
setLoginSkipped(true);
|
|
423
|
+
setUser(null);
|
|
424
|
+
}, []);
|
|
425
|
+
const forgotPassword$1 = useCallback(async (email) => {
|
|
426
|
+
setAuthLoading(true);
|
|
427
|
+
setAuthProviderError(null);
|
|
428
|
+
try {
|
|
429
|
+
await forgotPassword(email);
|
|
430
|
+
} catch (error) {
|
|
431
|
+
setAuthProviderError(error);
|
|
432
|
+
throw error;
|
|
433
|
+
} finally {
|
|
434
|
+
setAuthLoading(false);
|
|
435
|
+
}
|
|
436
|
+
}, []);
|
|
437
|
+
const resetPassword$1 = useCallback(async (token, password) => {
|
|
438
|
+
setAuthLoading(true);
|
|
439
|
+
setAuthProviderError(null);
|
|
440
|
+
try {
|
|
441
|
+
await resetPassword(token, password);
|
|
442
|
+
} catch (error) {
|
|
443
|
+
setAuthProviderError(error);
|
|
444
|
+
throw error;
|
|
445
|
+
} finally {
|
|
446
|
+
setAuthLoading(false);
|
|
447
|
+
}
|
|
448
|
+
}, []);
|
|
449
|
+
const changePassword$1 = useCallback(async (oldPassword, newPassword) => {
|
|
450
|
+
setAuthLoading(true);
|
|
451
|
+
setAuthProviderError(null);
|
|
452
|
+
try {
|
|
453
|
+
if (!tokensRef.current) {
|
|
454
|
+
throw new Error("User is not logged in");
|
|
455
|
+
}
|
|
456
|
+
await changePassword(tokensRef.current.accessToken, oldPassword, newPassword);
|
|
457
|
+
clearSessionAndSignOut();
|
|
458
|
+
} catch (error) {
|
|
459
|
+
setAuthProviderError(error);
|
|
460
|
+
throw error;
|
|
461
|
+
} finally {
|
|
462
|
+
setAuthLoading(false);
|
|
463
|
+
}
|
|
464
|
+
}, [clearSessionAndSignOut]);
|
|
465
|
+
const updateProfile$1 = useCallback(async (displayName, photoURL) => {
|
|
466
|
+
setAuthLoading(true);
|
|
467
|
+
setAuthProviderError(null);
|
|
468
|
+
try {
|
|
469
|
+
if (!tokensRef.current) {
|
|
470
|
+
throw new Error("User is not logged in");
|
|
471
|
+
}
|
|
472
|
+
const response = await updateProfile(tokensRef.current.accessToken, displayName, photoURL);
|
|
473
|
+
let convertedUser = convertToUser(response.user);
|
|
474
|
+
if (defineRolesFor) {
|
|
475
|
+
const customRoles = await defineRolesFor(convertedUser);
|
|
476
|
+
if (customRoles) {
|
|
477
|
+
convertedUser = { ...convertedUser, roles: customRoles.map((r) => r.id) };
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
const storedData = loadAuthFromStorage();
|
|
481
|
+
if (storedData) {
|
|
482
|
+
saveAuthToStorage(storedData.tokens, response.user);
|
|
483
|
+
}
|
|
484
|
+
setUser(convertedUser);
|
|
485
|
+
return convertedUser;
|
|
486
|
+
} catch (error) {
|
|
487
|
+
setAuthProviderError(error);
|
|
488
|
+
throw error;
|
|
489
|
+
} finally {
|
|
490
|
+
setAuthLoading(false);
|
|
491
|
+
}
|
|
492
|
+
}, [defineRolesFor]);
|
|
493
|
+
const fetchSessions$1 = useCallback(async () => {
|
|
494
|
+
try {
|
|
495
|
+
if (!tokensRef.current) {
|
|
496
|
+
throw new Error("User is not logged in");
|
|
497
|
+
}
|
|
498
|
+
const response = await fetchSessions(tokensRef.current.accessToken, tokensRef.current.refreshToken);
|
|
499
|
+
return response.sessions;
|
|
500
|
+
} catch (error) {
|
|
501
|
+
setAuthProviderError(error);
|
|
502
|
+
throw error;
|
|
503
|
+
}
|
|
504
|
+
}, []);
|
|
505
|
+
const revokeSession$1 = useCallback(async (sessionId) => {
|
|
506
|
+
try {
|
|
507
|
+
if (!tokensRef.current) {
|
|
508
|
+
throw new Error("User is not logged in");
|
|
509
|
+
}
|
|
510
|
+
await revokeSession(tokensRef.current.accessToken, sessionId);
|
|
511
|
+
} catch (error) {
|
|
512
|
+
setAuthProviderError(error);
|
|
513
|
+
throw error;
|
|
514
|
+
}
|
|
515
|
+
}, []);
|
|
516
|
+
useEffect(() => {
|
|
517
|
+
isMountedRef.current = true;
|
|
518
|
+
const restoreAuth = async () => {
|
|
519
|
+
try {
|
|
520
|
+
const config = await fetchAuthConfig();
|
|
521
|
+
if (isMountedRef.current) {
|
|
522
|
+
setAuthConfig(config);
|
|
523
|
+
}
|
|
524
|
+
} catch (e) {
|
|
525
|
+
}
|
|
526
|
+
const stored = loadAuthFromStorage();
|
|
527
|
+
if (!stored) {
|
|
528
|
+
setInitialLoading(false);
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
if (!stored.tokens?.refreshToken) {
|
|
532
|
+
clearAuthFromStorage();
|
|
533
|
+
setInitialLoading(false);
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
const expiresAt = stored.tokens.accessTokenExpiresAt;
|
|
537
|
+
if (typeof expiresAt !== "number" || !Number.isFinite(expiresAt)) {
|
|
538
|
+
clearAuthFromStorage();
|
|
539
|
+
setInitialLoading(false);
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
if (!isTokenExpiredOrNearExpiry(stored.tokens.accessTokenExpiresAt)) {
|
|
543
|
+
tokensRef.current = stored.tokens;
|
|
544
|
+
let userToSet = convertToUser(stored.user);
|
|
545
|
+
if (defineRolesFor) {
|
|
546
|
+
const customRoles = await defineRolesFor(userToSet);
|
|
547
|
+
if (customRoles) {
|
|
548
|
+
userToSet = { ...userToSet, roles: customRoles.map((r) => r.id) };
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
setUser(userToSet);
|
|
552
|
+
scheduleTokenRefresh(stored.tokens);
|
|
553
|
+
setInitialLoading(false);
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
tokensRef.current = stored.tokens;
|
|
557
|
+
try {
|
|
558
|
+
const newTokens = await refreshAccessToken$1();
|
|
559
|
+
if (!newTokens) {
|
|
560
|
+
clearAuthFromStorage();
|
|
561
|
+
tokensRef.current = null;
|
|
562
|
+
setInitialLoading(false);
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
if (!isMountedRef.current) return;
|
|
566
|
+
let userToSet;
|
|
567
|
+
try {
|
|
568
|
+
const meResponse = await getCurrentUser(newTokens.accessToken);
|
|
569
|
+
if (!isMountedRef.current) return;
|
|
570
|
+
const freshUserInfo = meResponse.user;
|
|
571
|
+
saveAuthToStorage(newTokens, freshUserInfo);
|
|
572
|
+
userToSet = convertToUser(freshUserInfo);
|
|
573
|
+
if (defineRolesFor) {
|
|
574
|
+
const customRoles = await defineRolesFor(userToSet);
|
|
575
|
+
if (!isMountedRef.current) return;
|
|
576
|
+
if (customRoles) {
|
|
577
|
+
userToSet = { ...userToSet, roles: customRoles.map((r) => r.id) };
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
} catch (meError) {
|
|
581
|
+
if (!isMountedRef.current) return;
|
|
582
|
+
userToSet = convertToUser(stored.user);
|
|
583
|
+
}
|
|
584
|
+
if (!isMountedRef.current) return;
|
|
585
|
+
setUser(userToSet);
|
|
586
|
+
scheduleTokenRefresh(newTokens);
|
|
587
|
+
} catch (error) {
|
|
588
|
+
if (!isMountedRef.current) return;
|
|
589
|
+
if (!(error instanceof Error && error.code === "NETWORK_ERROR")) {
|
|
590
|
+
clearAuthFromStorage();
|
|
591
|
+
tokensRef.current = null;
|
|
592
|
+
}
|
|
593
|
+
} finally {
|
|
594
|
+
if (isMountedRef.current) {
|
|
595
|
+
setInitialLoading(false);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
};
|
|
599
|
+
restoreAuth();
|
|
600
|
+
return () => {
|
|
601
|
+
isMountedRef.current = false;
|
|
602
|
+
};
|
|
603
|
+
}, [scheduleTokenRefresh, defineRolesFor, refreshAccessToken$1]);
|
|
604
|
+
useEffect(() => {
|
|
605
|
+
const handleVisibilityChange = async () => {
|
|
606
|
+
if (initialLoading) return;
|
|
607
|
+
if (document.visibilityState === "visible" && tokensRef.current) {
|
|
608
|
+
if (isTokenExpiredOrNearExpiry(tokensRef.current.accessTokenExpiresAt)) {
|
|
609
|
+
try {
|
|
610
|
+
const newTokens = await refreshAccessToken$1();
|
|
611
|
+
if (newTokens && isMountedRef.current) {
|
|
612
|
+
scheduleTokenRefresh(newTokens);
|
|
613
|
+
} else if (!newTokens && isMountedRef.current) {
|
|
614
|
+
clearSessionAndSignOut();
|
|
615
|
+
}
|
|
616
|
+
} catch (error) {
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
};
|
|
621
|
+
document.addEventListener("visibilitychange", handleVisibilityChange);
|
|
622
|
+
return () => {
|
|
623
|
+
document.removeEventListener("visibilitychange", handleVisibilityChange);
|
|
624
|
+
};
|
|
625
|
+
}, [initialLoading, refreshAccessToken$1, scheduleTokenRefresh, clearSessionAndSignOut]);
|
|
626
|
+
const getApiUrl$1 = useCallback(() => {
|
|
627
|
+
return getApiUrl();
|
|
628
|
+
}, []);
|
|
629
|
+
useEffect(() => {
|
|
630
|
+
return () => {
|
|
631
|
+
isMountedRef.current = false;
|
|
632
|
+
if (refreshTimeoutRef.current) {
|
|
633
|
+
clearTimeout(refreshTimeoutRef.current);
|
|
634
|
+
}
|
|
635
|
+
};
|
|
636
|
+
}, []);
|
|
637
|
+
const revokeAllSessions$1 = useCallback(async () => {
|
|
638
|
+
try {
|
|
639
|
+
if (!tokensRef.current) {
|
|
640
|
+
throw new Error("User is not logged in");
|
|
641
|
+
}
|
|
642
|
+
await revokeAllSessions(tokensRef.current.accessToken);
|
|
643
|
+
clearSessionAndSignOut();
|
|
644
|
+
} catch (error) {
|
|
645
|
+
setAuthProviderError(error);
|
|
646
|
+
throw error;
|
|
647
|
+
}
|
|
648
|
+
}, [clearSessionAndSignOut]);
|
|
649
|
+
return {
|
|
650
|
+
user,
|
|
651
|
+
authLoading,
|
|
652
|
+
initialLoading,
|
|
653
|
+
authError,
|
|
654
|
+
authProviderError,
|
|
655
|
+
loginSkipped,
|
|
656
|
+
needsSetup: authConfig?.needsSetup ?? false,
|
|
657
|
+
registrationEnabled: authConfig?.registrationEnabled ?? false,
|
|
658
|
+
getAuthToken,
|
|
659
|
+
getApiUrl: getApiUrl$1,
|
|
660
|
+
signOut,
|
|
661
|
+
emailPasswordLogin,
|
|
662
|
+
register: register$1,
|
|
663
|
+
googleLogin: googleLogin$1,
|
|
664
|
+
skipLogin,
|
|
665
|
+
forgotPassword: forgotPassword$1,
|
|
666
|
+
resetPassword: resetPassword$1,
|
|
667
|
+
changePassword: changePassword$1,
|
|
668
|
+
updateProfile: updateProfile$1,
|
|
669
|
+
fetchSessions: fetchSessions$1,
|
|
670
|
+
revokeSession: revokeSession$1,
|
|
671
|
+
revokeAllSessions: revokeAllSessions$1,
|
|
672
|
+
extra,
|
|
673
|
+
setExtra
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
function convertUser(apiUser) {
|
|
677
|
+
return {
|
|
678
|
+
uid: apiUser.uid,
|
|
679
|
+
email: apiUser.email,
|
|
680
|
+
displayName: apiUser.displayName || null,
|
|
681
|
+
photoURL: apiUser.photoURL || null,
|
|
682
|
+
providerId: "custom",
|
|
683
|
+
isAnonymous: false,
|
|
684
|
+
roles: apiUser.roles
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
function convertRole(apiRole) {
|
|
688
|
+
return {
|
|
689
|
+
id: apiRole.id,
|
|
690
|
+
name: apiRole.name,
|
|
691
|
+
isAdmin: apiRole.isAdmin ?? false,
|
|
692
|
+
config: apiRole.config ?? void 0
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
function useBackendUserManagement(config) {
|
|
696
|
+
const { apiUrl, getAuthToken, currentUser } = config;
|
|
697
|
+
const [users, setUsers] = useState([]);
|
|
698
|
+
const [roles, setRoles] = useState([]);
|
|
699
|
+
const [loading, setLoading] = useState(true);
|
|
700
|
+
const [usersError, setUsersError] = useState();
|
|
701
|
+
const [rolesError, setRolesError] = useState();
|
|
702
|
+
const apiRequest = useCallback(async (endpoint, method = "GET", body, retryCount = 6, signal) => {
|
|
703
|
+
let lastError = null;
|
|
704
|
+
for (let attempt = 0; attempt < retryCount; attempt++) {
|
|
705
|
+
if (signal?.aborted) {
|
|
706
|
+
const error = new Error("Request aborted");
|
|
707
|
+
error.name = "AbortError";
|
|
708
|
+
throw error;
|
|
709
|
+
}
|
|
710
|
+
try {
|
|
711
|
+
const token = await getAuthToken();
|
|
712
|
+
const response = await fetch(`${apiUrl}/api/admin${endpoint}`, {
|
|
713
|
+
method,
|
|
714
|
+
headers: {
|
|
715
|
+
"Content-Type": "application/json",
|
|
716
|
+
"Authorization": `Bearer ${token}`
|
|
717
|
+
},
|
|
718
|
+
body: body ? JSON.stringify(body) : void 0,
|
|
719
|
+
signal
|
|
720
|
+
});
|
|
721
|
+
if (!response.ok) {
|
|
722
|
+
const errorText = await response.text();
|
|
723
|
+
let errorMessage = "API request failed";
|
|
724
|
+
try {
|
|
725
|
+
const errorJson = JSON.parse(errorText);
|
|
726
|
+
errorMessage = errorJson.error?.message || errorMessage;
|
|
727
|
+
} catch (e) {
|
|
728
|
+
errorMessage = errorText || `HTTP error ${response.status}`;
|
|
729
|
+
}
|
|
730
|
+
const error = Object.assign(new Error(errorMessage), { status: response.status });
|
|
731
|
+
throw error;
|
|
732
|
+
}
|
|
733
|
+
return await response.json();
|
|
734
|
+
} catch (error) {
|
|
735
|
+
if (error instanceof Error && error.name === "AbortError" || signal?.aborted) {
|
|
736
|
+
throw error;
|
|
737
|
+
}
|
|
738
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
739
|
+
const isNetworkError = error instanceof TypeError;
|
|
740
|
+
const isServerError = typeof error.status === "number" && error.status >= 500 && error.status < 600;
|
|
741
|
+
if (attempt < retryCount - 1 && (isNetworkError || isServerError)) {
|
|
742
|
+
const delay = Math.min(1e3 * Math.pow(2, attempt), 5e3);
|
|
743
|
+
console.warn(`Admin API request to ${endpoint} failed, retrying in ${delay}ms...`);
|
|
744
|
+
await new Promise((resolve, reject) => {
|
|
745
|
+
if (signal?.aborted) return reject(new Error("AbortError"));
|
|
746
|
+
const timer = setTimeout(resolve, delay);
|
|
747
|
+
if (signal) {
|
|
748
|
+
signal.addEventListener("abort", () => {
|
|
749
|
+
clearTimeout(timer);
|
|
750
|
+
reject(new Error("AbortError"));
|
|
751
|
+
}, { once: true });
|
|
752
|
+
}
|
|
753
|
+
}).catch(() => {
|
|
754
|
+
});
|
|
755
|
+
if (signal?.aborted) {
|
|
756
|
+
const abortError = new Error("Request aborted");
|
|
757
|
+
abortError.name = "AbortError";
|
|
758
|
+
throw abortError;
|
|
759
|
+
}
|
|
760
|
+
continue;
|
|
761
|
+
}
|
|
762
|
+
console.error("Admin API error after retries:", error);
|
|
763
|
+
throw error;
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
throw lastError;
|
|
767
|
+
}, [apiUrl, getAuthToken]);
|
|
768
|
+
const loadUsers = useCallback(async (signal) => {
|
|
769
|
+
try {
|
|
770
|
+
const data = await apiRequest("/users", "GET", void 0, 6, signal);
|
|
771
|
+
setUsers(data.users.map((u) => convertUser(u)));
|
|
772
|
+
setUsersError(void 0);
|
|
773
|
+
} catch (error) {
|
|
774
|
+
if (error instanceof Error && error.name === "AbortError") return;
|
|
775
|
+
console.error("Failed to load users:", error);
|
|
776
|
+
setUsersError(error instanceof Error ? error : new Error(String(error)));
|
|
777
|
+
}
|
|
778
|
+
}, [apiRequest]);
|
|
779
|
+
useCallback(async (signal) => {
|
|
780
|
+
try {
|
|
781
|
+
const data = await apiRequest("/roles", "GET", void 0, 6, signal);
|
|
782
|
+
setRoles(data.roles.map(convertRole));
|
|
783
|
+
setRolesError(void 0);
|
|
784
|
+
} catch (error) {
|
|
785
|
+
if (error instanceof Error && error.name === "AbortError") return;
|
|
786
|
+
console.error("Failed to load roles:", error);
|
|
787
|
+
setRolesError(error instanceof Error ? error : new Error(String(error)));
|
|
788
|
+
}
|
|
789
|
+
}, [apiRequest]);
|
|
790
|
+
useEffect(() => {
|
|
791
|
+
if (!currentUser) {
|
|
792
|
+
setLoading(false);
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
const abortController = new AbortController();
|
|
796
|
+
const load = async () => {
|
|
797
|
+
setLoading(true);
|
|
798
|
+
let loadedRoles = [];
|
|
799
|
+
try {
|
|
800
|
+
const data = await apiRequest("/roles", "GET", void 0, 6, abortController.signal);
|
|
801
|
+
loadedRoles = data.roles.map(convertRole);
|
|
802
|
+
setRoles(loadedRoles);
|
|
803
|
+
setRolesError(void 0);
|
|
804
|
+
} catch (error) {
|
|
805
|
+
if (error instanceof Error && error.name !== "AbortError") {
|
|
806
|
+
console.error("Failed to load roles:", error);
|
|
807
|
+
setRolesError(error);
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
if (!abortController.signal.aborted) {
|
|
811
|
+
await loadUsers(abortController.signal);
|
|
812
|
+
}
|
|
813
|
+
if (!abortController.signal.aborted) {
|
|
814
|
+
setLoading(false);
|
|
815
|
+
}
|
|
816
|
+
};
|
|
817
|
+
load();
|
|
818
|
+
return () => {
|
|
819
|
+
abortController.abort();
|
|
820
|
+
};
|
|
821
|
+
}, [currentUser, apiRequest, loadUsers]);
|
|
822
|
+
const saveUser = useCallback(async (user) => {
|
|
823
|
+
const roleIds = user.roles ?? [];
|
|
824
|
+
const existingUser = users.find((u) => u.uid === user.uid);
|
|
825
|
+
if (existingUser) {
|
|
826
|
+
const data = await apiRequest(`/users/${user.uid}`, "PUT", {
|
|
827
|
+
email: user.email,
|
|
828
|
+
displayName: user.displayName,
|
|
829
|
+
roles: roleIds
|
|
830
|
+
});
|
|
831
|
+
const updated = convertUser(data.user);
|
|
832
|
+
setUsers((prev) => prev.map((u) => u.uid === updated.uid ? updated : u));
|
|
833
|
+
return updated;
|
|
834
|
+
} else {
|
|
835
|
+
const data = await apiRequest("/users", "POST", {
|
|
836
|
+
email: user.email,
|
|
837
|
+
displayName: user.displayName,
|
|
838
|
+
roles: roleIds
|
|
839
|
+
});
|
|
840
|
+
const created = convertUser(data.user);
|
|
841
|
+
setUsers((prev) => [...prev, created]);
|
|
842
|
+
return created;
|
|
843
|
+
}
|
|
844
|
+
}, [apiRequest, users, roles]);
|
|
845
|
+
const deleteUser = useCallback(async (user) => {
|
|
846
|
+
await apiRequest(`/users/${user.uid}`, "DELETE");
|
|
847
|
+
setUsers((prev) => prev.filter((u) => u.uid !== user.uid));
|
|
848
|
+
}, [apiRequest]);
|
|
849
|
+
const saveRole = useCallback(async (role) => {
|
|
850
|
+
const existingRole = roles.find((r) => r.id === role.id);
|
|
851
|
+
if (existingRole) {
|
|
852
|
+
const data = await apiRequest(`/roles/${role.id}`, "PUT", {
|
|
853
|
+
name: role.name,
|
|
854
|
+
isAdmin: role.isAdmin,
|
|
855
|
+
config: role.config
|
|
856
|
+
});
|
|
857
|
+
const updated = convertRole(data.role);
|
|
858
|
+
setRoles((prev) => prev.map((r) => r.id === updated.id ? updated : r));
|
|
859
|
+
} else {
|
|
860
|
+
const data = await apiRequest("/roles", "POST", {
|
|
861
|
+
id: role.id,
|
|
862
|
+
name: role.name,
|
|
863
|
+
isAdmin: role.isAdmin ?? false,
|
|
864
|
+
config: role.config
|
|
865
|
+
});
|
|
866
|
+
const created = convertRole(data.role);
|
|
867
|
+
setRoles((prev) => [...prev, created]);
|
|
868
|
+
}
|
|
869
|
+
}, [apiRequest, roles]);
|
|
870
|
+
const deleteRole = useCallback(async (role) => {
|
|
871
|
+
await apiRequest(`/roles/${role.id}`, "DELETE");
|
|
872
|
+
setRoles((prev) => prev.filter((r) => r.id !== role.id));
|
|
873
|
+
}, [apiRequest]);
|
|
874
|
+
const getUser = useCallback((uid) => {
|
|
875
|
+
return users.find((u) => u.uid === uid) ?? null;
|
|
876
|
+
}, [users]);
|
|
877
|
+
const defineRolesFor = useCallback(async (user) => {
|
|
878
|
+
const existingUser = users.find((u) => u.uid === user.uid || u.email === user.email);
|
|
879
|
+
if (!existingUser) return void 0;
|
|
880
|
+
const userRoleIds = existingUser.roles ?? [];
|
|
881
|
+
return roles.filter((r) => userRoleIds.includes(r.id));
|
|
882
|
+
}, [users, roles]);
|
|
883
|
+
const isAdmin = currentUser?.roles?.includes("admin") ?? false;
|
|
884
|
+
const bootstrapAdmin = useCallback(async () => {
|
|
885
|
+
try {
|
|
886
|
+
await apiRequest("/bootstrap", "POST");
|
|
887
|
+
const data = await apiRequest("/roles");
|
|
888
|
+
const loadedRoles = data.roles.map(convertRole);
|
|
889
|
+
setRoles(loadedRoles);
|
|
890
|
+
await loadUsers();
|
|
891
|
+
} catch (error) {
|
|
892
|
+
console.error("Failed to bootstrap admin:", error);
|
|
893
|
+
throw error;
|
|
894
|
+
}
|
|
895
|
+
}, [apiRequest, loadUsers]);
|
|
896
|
+
return {
|
|
897
|
+
loading,
|
|
898
|
+
users,
|
|
899
|
+
saveUser,
|
|
900
|
+
deleteUser,
|
|
901
|
+
roles,
|
|
902
|
+
saveRole,
|
|
903
|
+
deleteRole,
|
|
904
|
+
isAdmin,
|
|
905
|
+
allowDefaultRolesCreation: true,
|
|
906
|
+
includeCollectionConfigPermissions: true,
|
|
907
|
+
defineRolesFor,
|
|
908
|
+
getUser,
|
|
909
|
+
usersError,
|
|
910
|
+
rolesError,
|
|
911
|
+
bootstrapAdmin
|
|
912
|
+
};
|
|
913
|
+
}
|
|
914
|
+
function RebaseLoginView({
|
|
915
|
+
logo,
|
|
916
|
+
authController,
|
|
917
|
+
noUserComponent,
|
|
918
|
+
disableSignupScreen = false,
|
|
919
|
+
disabled = false,
|
|
920
|
+
notAllowedError,
|
|
921
|
+
googleEnabled = false,
|
|
922
|
+
googleClientId
|
|
923
|
+
}) {
|
|
924
|
+
const modeState = useModeController();
|
|
925
|
+
const [registrationSelected, setRegistrationSelected] = useState(false);
|
|
926
|
+
const [passwordLoginSelected, setPasswordLoginSelected] = useState(false);
|
|
927
|
+
const [forgotPasswordSelected, setForgotPasswordSelected] = useState(false);
|
|
928
|
+
const isBootstrapMode = authController.needsSetup;
|
|
929
|
+
function buildErrorView() {
|
|
930
|
+
if (!authController.authProviderError) return null;
|
|
931
|
+
if (authController.user != null) return null;
|
|
932
|
+
return /* @__PURE__ */ jsx(ErrorView, { error: authController.authProviderError.message ?? authController.authProviderError });
|
|
933
|
+
}
|
|
934
|
+
let logoComponent;
|
|
935
|
+
if (logo) {
|
|
936
|
+
logoComponent = /* @__PURE__ */ jsx(
|
|
937
|
+
"img",
|
|
938
|
+
{
|
|
939
|
+
src: logo,
|
|
940
|
+
style: {
|
|
941
|
+
height: "100%",
|
|
942
|
+
width: "100%",
|
|
943
|
+
objectFit: "cover"
|
|
944
|
+
},
|
|
945
|
+
alt: "Logo"
|
|
946
|
+
}
|
|
947
|
+
);
|
|
948
|
+
} else {
|
|
949
|
+
logoComponent = /* @__PURE__ */ jsx(RebaseLogo, {});
|
|
950
|
+
}
|
|
951
|
+
let notAllowedMessage;
|
|
952
|
+
if (notAllowedError) {
|
|
953
|
+
if (typeof notAllowedError === "string") {
|
|
954
|
+
notAllowedMessage = notAllowedError;
|
|
955
|
+
} else if (notAllowedError instanceof Error) {
|
|
956
|
+
notAllowedMessage = notAllowedError.message;
|
|
957
|
+
} else {
|
|
958
|
+
notAllowedMessage = "It looks like you don't have access to the CMS, based on the specified Authenticator configuration";
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
return /* @__PURE__ */ jsx("div", { className: "flex flex-col justify-center items-center min-h-screen min-w-full p-2", children: /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center w-full max-w-md", children: [
|
|
962
|
+
/* @__PURE__ */ jsx("div", { className: `m-4 p-4 w-64 h-64`, children: logoComponent }),
|
|
963
|
+
notAllowedMessage && /* @__PURE__ */ jsx("div", { className: "p-4", children: /* @__PURE__ */ jsx(ErrorView, { error: notAllowedMessage }) }),
|
|
964
|
+
!forgotPasswordSelected && buildErrorView(),
|
|
965
|
+
isBootstrapMode && !authController.user && /* @__PURE__ */ jsx(
|
|
966
|
+
LoginForm,
|
|
967
|
+
{
|
|
968
|
+
authController,
|
|
969
|
+
registrationMode: true,
|
|
970
|
+
onClose: () => {
|
|
971
|
+
},
|
|
972
|
+
onForgotPassword: () => {
|
|
973
|
+
},
|
|
974
|
+
mode: modeState.mode,
|
|
975
|
+
noUserComponent,
|
|
976
|
+
disableSignupScreen: false,
|
|
977
|
+
bootstrapMode: true
|
|
978
|
+
}
|
|
979
|
+
),
|
|
980
|
+
!isBootstrapMode && !passwordLoginSelected && !registrationSelected && !forgotPasswordSelected && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
981
|
+
/* @__PURE__ */ jsx(
|
|
982
|
+
LoginButton,
|
|
983
|
+
{
|
|
984
|
+
disabled,
|
|
985
|
+
text: "Email/password",
|
|
986
|
+
icon: /* @__PURE__ */ jsx(MailIcon, {}),
|
|
987
|
+
onClick: () => {
|
|
988
|
+
setRegistrationSelected(false);
|
|
989
|
+
setPasswordLoginSelected(true);
|
|
990
|
+
setForgotPasswordSelected(false);
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
),
|
|
994
|
+
googleEnabled && googleClientId && /* @__PURE__ */ jsx(
|
|
995
|
+
GoogleLoginButton,
|
|
996
|
+
{
|
|
997
|
+
disabled,
|
|
998
|
+
googleClientId,
|
|
999
|
+
authController
|
|
1000
|
+
}
|
|
1001
|
+
),
|
|
1002
|
+
!disableSignupScreen && authController.registrationEnabled && /* @__PURE__ */ jsx(
|
|
1003
|
+
LoginButton,
|
|
1004
|
+
{
|
|
1005
|
+
disabled,
|
|
1006
|
+
text: "Create account",
|
|
1007
|
+
icon: /* @__PURE__ */ jsx(MailIcon, {}),
|
|
1008
|
+
onClick: () => {
|
|
1009
|
+
setRegistrationSelected(true);
|
|
1010
|
+
setPasswordLoginSelected(false);
|
|
1011
|
+
setForgotPasswordSelected(false);
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
)
|
|
1015
|
+
] }),
|
|
1016
|
+
!isBootstrapMode && (passwordLoginSelected || registrationSelected) && !forgotPasswordSelected && /* @__PURE__ */ jsx(
|
|
1017
|
+
LoginForm,
|
|
1018
|
+
{
|
|
1019
|
+
authController,
|
|
1020
|
+
registrationMode: registrationSelected,
|
|
1021
|
+
onClose: () => {
|
|
1022
|
+
setRegistrationSelected(false);
|
|
1023
|
+
setPasswordLoginSelected(false);
|
|
1024
|
+
},
|
|
1025
|
+
onForgotPassword: () => {
|
|
1026
|
+
setForgotPasswordSelected(true);
|
|
1027
|
+
setPasswordLoginSelected(false);
|
|
1028
|
+
},
|
|
1029
|
+
mode: modeState.mode,
|
|
1030
|
+
noUserComponent,
|
|
1031
|
+
disableSignupScreen
|
|
1032
|
+
}
|
|
1033
|
+
),
|
|
1034
|
+
forgotPasswordSelected && /* @__PURE__ */ jsx(
|
|
1035
|
+
ForgotPasswordForm,
|
|
1036
|
+
{
|
|
1037
|
+
authController,
|
|
1038
|
+
onClose: () => {
|
|
1039
|
+
setForgotPasswordSelected(false);
|
|
1040
|
+
setPasswordLoginSelected(true);
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
)
|
|
1044
|
+
] }) });
|
|
1045
|
+
}
|
|
1046
|
+
function LoginButton({
|
|
1047
|
+
icon,
|
|
1048
|
+
onClick,
|
|
1049
|
+
text,
|
|
1050
|
+
disabled
|
|
1051
|
+
}) {
|
|
1052
|
+
return /* @__PURE__ */ jsx("div", { className: "m-2 w-full", children: /* @__PURE__ */ jsx(
|
|
1053
|
+
Button,
|
|
1054
|
+
{
|
|
1055
|
+
disabled,
|
|
1056
|
+
className: `w-full`,
|
|
1057
|
+
onClick,
|
|
1058
|
+
children: /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-center p-2 w-full h-8", children: [
|
|
1059
|
+
/* @__PURE__ */ jsx("div", { className: "flex flex-col items-center justify-center w-8", children: icon }),
|
|
1060
|
+
/* @__PURE__ */ jsx("div", { className: "grow pl-2 text-center", children: text })
|
|
1061
|
+
] })
|
|
1062
|
+
}
|
|
1063
|
+
) });
|
|
1064
|
+
}
|
|
1065
|
+
function GoogleLoginButton({
|
|
1066
|
+
disabled,
|
|
1067
|
+
googleClientId,
|
|
1068
|
+
authController
|
|
1069
|
+
}) {
|
|
1070
|
+
const handleGoogleLogin = async () => {
|
|
1071
|
+
try {
|
|
1072
|
+
const google = window.google;
|
|
1073
|
+
if (!google) {
|
|
1074
|
+
console.error("Google Sign-In not loaded");
|
|
1075
|
+
return;
|
|
1076
|
+
}
|
|
1077
|
+
google.accounts.id.initialize({
|
|
1078
|
+
client_id: googleClientId,
|
|
1079
|
+
callback: async (response) => {
|
|
1080
|
+
try {
|
|
1081
|
+
await authController.googleLogin(response.credential);
|
|
1082
|
+
} catch (err) {
|
|
1083
|
+
console.error("Google login error:", err);
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
});
|
|
1087
|
+
google.accounts.id.prompt();
|
|
1088
|
+
} catch (err) {
|
|
1089
|
+
console.error("Google login error:", err);
|
|
1090
|
+
}
|
|
1091
|
+
};
|
|
1092
|
+
return /* @__PURE__ */ jsx("div", { className: "m-2 w-full", children: /* @__PURE__ */ jsx(
|
|
1093
|
+
Button,
|
|
1094
|
+
{
|
|
1095
|
+
disabled,
|
|
1096
|
+
className: `w-full`,
|
|
1097
|
+
onClick: handleGoogleLogin,
|
|
1098
|
+
children: /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-center p-2 w-full h-8", children: [
|
|
1099
|
+
/* @__PURE__ */ jsx("div", { className: "flex flex-col items-center justify-center w-8", children: /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 24 24", width: "24", height: "24", children: [
|
|
1100
|
+
/* @__PURE__ */ jsx(
|
|
1101
|
+
"path",
|
|
1102
|
+
{
|
|
1103
|
+
fill: "currentColor",
|
|
1104
|
+
d: "M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
|
|
1105
|
+
}
|
|
1106
|
+
),
|
|
1107
|
+
/* @__PURE__ */ jsx(
|
|
1108
|
+
"path",
|
|
1109
|
+
{
|
|
1110
|
+
fill: "currentColor",
|
|
1111
|
+
d: "M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
|
|
1112
|
+
}
|
|
1113
|
+
),
|
|
1114
|
+
/* @__PURE__ */ jsx(
|
|
1115
|
+
"path",
|
|
1116
|
+
{
|
|
1117
|
+
fill: "currentColor",
|
|
1118
|
+
d: "M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
|
|
1119
|
+
}
|
|
1120
|
+
),
|
|
1121
|
+
/* @__PURE__ */ jsx(
|
|
1122
|
+
"path",
|
|
1123
|
+
{
|
|
1124
|
+
fill: "currentColor",
|
|
1125
|
+
d: "M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
|
|
1126
|
+
}
|
|
1127
|
+
)
|
|
1128
|
+
] }) }),
|
|
1129
|
+
/* @__PURE__ */ jsx("div", { className: "grow pl-2 text-center", children: "Continue with Google" })
|
|
1130
|
+
] })
|
|
1131
|
+
}
|
|
1132
|
+
) });
|
|
1133
|
+
}
|
|
1134
|
+
function LoginForm({
|
|
1135
|
+
onClose,
|
|
1136
|
+
onForgotPassword,
|
|
1137
|
+
authController,
|
|
1138
|
+
mode,
|
|
1139
|
+
registrationMode,
|
|
1140
|
+
noUserComponent,
|
|
1141
|
+
disableSignupScreen,
|
|
1142
|
+
bootstrapMode = false
|
|
1143
|
+
}) {
|
|
1144
|
+
const passwordRef = useRef(null);
|
|
1145
|
+
const [email, setEmail] = useState();
|
|
1146
|
+
const [password, setPassword] = useState();
|
|
1147
|
+
const [displayName, setDisplayName] = useState();
|
|
1148
|
+
useEffect(() => {
|
|
1149
|
+
if (!document) return;
|
|
1150
|
+
const escFunction = (event) => {
|
|
1151
|
+
if (event.keyCode === 27) {
|
|
1152
|
+
onClose();
|
|
1153
|
+
}
|
|
1154
|
+
};
|
|
1155
|
+
document.addEventListener("keydown", escFunction, false);
|
|
1156
|
+
return () => {
|
|
1157
|
+
document.removeEventListener("keydown", escFunction, false);
|
|
1158
|
+
};
|
|
1159
|
+
}, [onClose]);
|
|
1160
|
+
function handleEnterPassword() {
|
|
1161
|
+
if (email && password) {
|
|
1162
|
+
authController.emailPasswordLogin(email, password);
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
function handleRegistration() {
|
|
1166
|
+
if (email && password) {
|
|
1167
|
+
authController.register(email, password, displayName);
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
const onBackPressed = () => {
|
|
1171
|
+
onClose();
|
|
1172
|
+
};
|
|
1173
|
+
const handleSubmit = (event) => {
|
|
1174
|
+
event.preventDefault();
|
|
1175
|
+
if (registrationMode)
|
|
1176
|
+
handleRegistration();
|
|
1177
|
+
else
|
|
1178
|
+
handleEnterPassword();
|
|
1179
|
+
};
|
|
1180
|
+
const label = bootstrapMode ? "Welcome! Create your admin account" : registrationMode ? "Create a new account" : "Enter your email and password";
|
|
1181
|
+
const button = registrationMode ? "Create account" : "Login";
|
|
1182
|
+
return /* @__PURE__ */ jsxs("form", { onSubmit: handleSubmit, className: "flex flex-col items-center w-full max-w-[500px] gap-2", children: [
|
|
1183
|
+
!bootstrapMode && /* @__PURE__ */ jsx("div", { className: "w-full", children: /* @__PURE__ */ jsx(IconButton, { onClick: onBackPressed, children: /* @__PURE__ */ jsx(ArrowBackIcon, {}) }) }),
|
|
1184
|
+
/* @__PURE__ */ jsx("div", { className: "flex justify-center w-full py-2", children: /* @__PURE__ */ jsx(Typography, { align: "center", variant: bootstrapMode ? "subtitle1" : "subtitle2", children: label }) }),
|
|
1185
|
+
bootstrapMode && /* @__PURE__ */ jsx(Typography, { variant: "body2", className: "text-gray-500 text-center mb-2", children: "No users found. Create the first account to get started. This account will have admin privileges." }),
|
|
1186
|
+
registrationMode && /* @__PURE__ */ jsx("div", { className: "w-full", children: /* @__PURE__ */ jsx(
|
|
1187
|
+
TextField,
|
|
1188
|
+
{
|
|
1189
|
+
placeholder: "Display Name (optional)",
|
|
1190
|
+
className: "w-full",
|
|
1191
|
+
value: displayName ?? "",
|
|
1192
|
+
disabled: authController.initialLoading,
|
|
1193
|
+
type: "text",
|
|
1194
|
+
onChange: (event) => setDisplayName(event.target.value)
|
|
1195
|
+
}
|
|
1196
|
+
) }),
|
|
1197
|
+
/* @__PURE__ */ jsx("div", { className: "w-full", children: /* @__PURE__ */ jsx(
|
|
1198
|
+
TextField,
|
|
1199
|
+
{
|
|
1200
|
+
placeholder: "Email",
|
|
1201
|
+
className: "w-full",
|
|
1202
|
+
autoFocus: true,
|
|
1203
|
+
value: email ?? "",
|
|
1204
|
+
disabled: authController.initialLoading,
|
|
1205
|
+
type: "email",
|
|
1206
|
+
onChange: (event) => setEmail(event.target.value)
|
|
1207
|
+
}
|
|
1208
|
+
) }),
|
|
1209
|
+
/* @__PURE__ */ jsx("div", { className: "w-full", children: registrationMode && noUserComponent }),
|
|
1210
|
+
/* @__PURE__ */ jsx("div", { className: "w-full", children: /* @__PURE__ */ jsx(
|
|
1211
|
+
TextField,
|
|
1212
|
+
{
|
|
1213
|
+
placeholder: "Password",
|
|
1214
|
+
className: "w-full",
|
|
1215
|
+
value: password ?? "",
|
|
1216
|
+
disabled: authController.initialLoading,
|
|
1217
|
+
inputRef: passwordRef,
|
|
1218
|
+
type: "password",
|
|
1219
|
+
onChange: (event) => setPassword(event.target.value)
|
|
1220
|
+
}
|
|
1221
|
+
) }),
|
|
1222
|
+
registrationMode && /* @__PURE__ */ jsx(Typography, { variant: "caption", className: "text-gray-500 text-sm", children: "Password: 8+ chars, uppercase, lowercase, number" }),
|
|
1223
|
+
!registrationMode && /* @__PURE__ */ jsx("div", { className: "w-full text-right", children: /* @__PURE__ */ jsx(
|
|
1224
|
+
"button",
|
|
1225
|
+
{
|
|
1226
|
+
type: "button",
|
|
1227
|
+
className: "text-sm text-blue-600 hover:text-blue-800 hover:underline",
|
|
1228
|
+
onClick: onForgotPassword,
|
|
1229
|
+
children: "Forgot password?"
|
|
1230
|
+
}
|
|
1231
|
+
) }),
|
|
1232
|
+
/* @__PURE__ */ jsxs("div", { className: "flex justify-end items-center w-full gap-2", children: [
|
|
1233
|
+
authController.authLoading && /* @__PURE__ */ jsx(CircularProgress, {}),
|
|
1234
|
+
/* @__PURE__ */ jsx(Button, { type: "submit", disabled: authController.authLoading, children: button })
|
|
1235
|
+
] })
|
|
1236
|
+
] });
|
|
1237
|
+
}
|
|
1238
|
+
function ForgotPasswordForm({
|
|
1239
|
+
onClose,
|
|
1240
|
+
authController
|
|
1241
|
+
}) {
|
|
1242
|
+
const [email, setEmail] = useState("");
|
|
1243
|
+
const [submitted, setSubmitted] = useState(false);
|
|
1244
|
+
const [error, setError] = useState(null);
|
|
1245
|
+
useEffect(() => {
|
|
1246
|
+
if (!document) return;
|
|
1247
|
+
const escFunction = (event) => {
|
|
1248
|
+
if (event.keyCode === 27) {
|
|
1249
|
+
onClose();
|
|
1250
|
+
}
|
|
1251
|
+
};
|
|
1252
|
+
document.addEventListener("keydown", escFunction, false);
|
|
1253
|
+
return () => {
|
|
1254
|
+
document.removeEventListener("keydown", escFunction, false);
|
|
1255
|
+
};
|
|
1256
|
+
}, [onClose]);
|
|
1257
|
+
const handleSubmit = async (event) => {
|
|
1258
|
+
event.preventDefault();
|
|
1259
|
+
setError(null);
|
|
1260
|
+
if (!email) {
|
|
1261
|
+
setError("Please enter your email address");
|
|
1262
|
+
return;
|
|
1263
|
+
}
|
|
1264
|
+
try {
|
|
1265
|
+
await authController.forgotPassword(email);
|
|
1266
|
+
setSubmitted(true);
|
|
1267
|
+
} catch (err) {
|
|
1268
|
+
if (err instanceof Error && err.code === "EMAIL_NOT_CONFIGURED") {
|
|
1269
|
+
setError("Password reset is not available. Please contact your administrator.");
|
|
1270
|
+
} else {
|
|
1271
|
+
setSubmitted(true);
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
};
|
|
1275
|
+
if (submitted) {
|
|
1276
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center w-full max-w-[500px] gap-4 p-4", children: [
|
|
1277
|
+
/* @__PURE__ */ jsx("div", { className: "w-full", children: /* @__PURE__ */ jsx(IconButton, { onClick: onClose, children: /* @__PURE__ */ jsx(ArrowBackIcon, {}) }) }),
|
|
1278
|
+
/* @__PURE__ */ jsxs("div", { className: "text-center", children: [
|
|
1279
|
+
/* @__PURE__ */ jsx(Typography, { variant: "subtitle1", className: "mb-4", children: "Check your email" }),
|
|
1280
|
+
/* @__PURE__ */ jsxs(Typography, { variant: "body2", className: "text-gray-600", children: [
|
|
1281
|
+
"If an account exists for ",
|
|
1282
|
+
/* @__PURE__ */ jsx("strong", { children: email }),
|
|
1283
|
+
", you'll receive a password reset link shortly."
|
|
1284
|
+
] })
|
|
1285
|
+
] }),
|
|
1286
|
+
/* @__PURE__ */ jsx(Button, { onClick: onClose, className: "mt-4", children: "Back to login" })
|
|
1287
|
+
] });
|
|
1288
|
+
}
|
|
1289
|
+
return /* @__PURE__ */ jsxs("form", { onSubmit: handleSubmit, className: "flex flex-col items-center w-full max-w-[500px] gap-2", children: [
|
|
1290
|
+
/* @__PURE__ */ jsx("div", { className: "w-full", children: /* @__PURE__ */ jsx(IconButton, { onClick: onClose, children: /* @__PURE__ */ jsx(ArrowBackIcon, {}) }) }),
|
|
1291
|
+
/* @__PURE__ */ jsx("div", { className: "flex justify-center w-full py-2", children: /* @__PURE__ */ jsx(Typography, { align: "center", variant: "subtitle2", children: "Reset your password" }) }),
|
|
1292
|
+
/* @__PURE__ */ jsx(Typography, { variant: "body2", className: "text-gray-600 text-center mb-2", children: "Enter your email address and we'll send you a link to reset your password." }),
|
|
1293
|
+
error && /* @__PURE__ */ jsx("div", { className: "w-full", children: /* @__PURE__ */ jsx(ErrorView, { error }) }),
|
|
1294
|
+
/* @__PURE__ */ jsx("div", { className: "w-full", children: /* @__PURE__ */ jsx(
|
|
1295
|
+
TextField,
|
|
1296
|
+
{
|
|
1297
|
+
placeholder: "Email",
|
|
1298
|
+
className: "w-full",
|
|
1299
|
+
autoFocus: true,
|
|
1300
|
+
value: email,
|
|
1301
|
+
type: "email",
|
|
1302
|
+
onChange: (event) => setEmail(event.target.value)
|
|
1303
|
+
}
|
|
1304
|
+
) }),
|
|
1305
|
+
/* @__PURE__ */ jsxs("div", { className: "flex justify-end items-center w-full gap-2 mt-2", children: [
|
|
1306
|
+
authController.authLoading && /* @__PURE__ */ jsx(CircularProgress, {}),
|
|
1307
|
+
/* @__PURE__ */ jsx(Button, { type: "submit", disabled: authController.authLoading || !email, children: "Send reset link" })
|
|
1308
|
+
] })
|
|
1309
|
+
] });
|
|
1310
|
+
}
|
|
1311
|
+
function createUserManagementAdminViews({ userManagement, apiUrl, getAuthToken }) {
|
|
1312
|
+
return [
|
|
1313
|
+
{
|
|
1314
|
+
slug: "dev/users",
|
|
1315
|
+
name: "CMS Users",
|
|
1316
|
+
group: "Admin",
|
|
1317
|
+
icon: "face",
|
|
1318
|
+
view: /* @__PURE__ */ jsx(UsersView, { userManagement, apiUrl, getAuthToken })
|
|
1319
|
+
},
|
|
1320
|
+
{
|
|
1321
|
+
slug: "dev/roles",
|
|
1322
|
+
name: "Roles",
|
|
1323
|
+
group: "Admin",
|
|
1324
|
+
icon: "gpp_good",
|
|
1325
|
+
view: /* @__PURE__ */ jsx(RolesView, { userManagement })
|
|
1326
|
+
}
|
|
1327
|
+
];
|
|
1328
|
+
}
|
|
1329
|
+
function RoleChip({ role }) {
|
|
1330
|
+
let colorScheme;
|
|
1331
|
+
if (role.isAdmin) {
|
|
1332
|
+
colorScheme = "blueDarker";
|
|
1333
|
+
} else if (role.id === "editor") {
|
|
1334
|
+
colorScheme = "yellowLight";
|
|
1335
|
+
} else if (role.id === "viewer") {
|
|
1336
|
+
colorScheme = "grayLight";
|
|
1337
|
+
} else {
|
|
1338
|
+
colorScheme = getColorSchemeForSeed(role.id);
|
|
1339
|
+
}
|
|
1340
|
+
return /* @__PURE__ */ jsx(Chip, { colorScheme, children: role.name }, role.id);
|
|
1341
|
+
}
|
|
1342
|
+
function UsersView({ userManagement, apiUrl, getAuthToken }) {
|
|
1343
|
+
const { users, roles, saveUser, deleteUser, loading } = userManagement;
|
|
1344
|
+
const snackbarController = useSnackbarController();
|
|
1345
|
+
const { user: loggedInUser } = useAuthController();
|
|
1346
|
+
const [dialogOpen, setDialogOpen] = useState(false);
|
|
1347
|
+
const [selectedUser, setSelectedUser] = useState();
|
|
1348
|
+
const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false);
|
|
1349
|
+
const [userToDelete, setUserToDelete] = useState();
|
|
1350
|
+
const [deleteInProgress, setDeleteInProgress] = useState(false);
|
|
1351
|
+
const [formKey, setFormKey] = useState(0);
|
|
1352
|
+
const [bootstrapping, setBootstrapping] = useState(false);
|
|
1353
|
+
const hasAdmin = users.some((u) => u.roles?.includes("admin"));
|
|
1354
|
+
const handleBootstrap = async () => {
|
|
1355
|
+
setBootstrapping(true);
|
|
1356
|
+
try {
|
|
1357
|
+
const token = await getAuthToken();
|
|
1358
|
+
const response = await fetch(`${apiUrl}/api/admin/bootstrap`, {
|
|
1359
|
+
method: "POST",
|
|
1360
|
+
headers: {
|
|
1361
|
+
"Content-Type": "application/json",
|
|
1362
|
+
"Authorization": `Bearer ${token}`
|
|
1363
|
+
}
|
|
1364
|
+
});
|
|
1365
|
+
const data = await response.json();
|
|
1366
|
+
if (!response.ok) {
|
|
1367
|
+
throw new Error(data.error?.message || "Bootstrap failed");
|
|
1368
|
+
}
|
|
1369
|
+
snackbarController.open({ type: "success", message: "You are now an admin! Refreshing..." });
|
|
1370
|
+
window.location.reload();
|
|
1371
|
+
} catch (error) {
|
|
1372
|
+
snackbarController.open({ type: "error", message: error instanceof Error ? error.message : "Failed to bootstrap admin" });
|
|
1373
|
+
} finally {
|
|
1374
|
+
setBootstrapping(false);
|
|
1375
|
+
}
|
|
1376
|
+
};
|
|
1377
|
+
const handleAddUser = () => {
|
|
1378
|
+
setSelectedUser(void 0);
|
|
1379
|
+
setFormKey((k) => k + 1);
|
|
1380
|
+
setDialogOpen(true);
|
|
1381
|
+
};
|
|
1382
|
+
const handleEditUser = (user) => {
|
|
1383
|
+
setSelectedUser(user);
|
|
1384
|
+
setDialogOpen(true);
|
|
1385
|
+
};
|
|
1386
|
+
const handleClose = () => {
|
|
1387
|
+
setDialogOpen(false);
|
|
1388
|
+
setSelectedUser(void 0);
|
|
1389
|
+
};
|
|
1390
|
+
const handleDelete = async () => {
|
|
1391
|
+
if (!userToDelete) return;
|
|
1392
|
+
setDeleteInProgress(true);
|
|
1393
|
+
try {
|
|
1394
|
+
await deleteUser(userToDelete);
|
|
1395
|
+
snackbarController.open({ type: "success", message: "User deleted successfully" });
|
|
1396
|
+
setDeleteConfirmOpen(false);
|
|
1397
|
+
setUserToDelete(void 0);
|
|
1398
|
+
} catch (error) {
|
|
1399
|
+
snackbarController.open({ type: "error", message: error instanceof Error ? error.message : "Error deleting user" });
|
|
1400
|
+
} finally {
|
|
1401
|
+
setDeleteInProgress(false);
|
|
1402
|
+
}
|
|
1403
|
+
};
|
|
1404
|
+
if (loading) {
|
|
1405
|
+
return /* @__PURE__ */ jsx(CenteredView, { children: /* @__PURE__ */ jsx(CircularProgress, {}) });
|
|
1406
|
+
}
|
|
1407
|
+
return /* @__PURE__ */ jsxs(Container, { className: "w-full flex flex-col py-4 gap-4", maxWidth: "6xl", children: [
|
|
1408
|
+
!hasAdmin && loggedInUser && /* @__PURE__ */ jsxs("div", { className: "bg-yellow-100 dark:bg-yellow-900 border border-yellow-400 dark:border-yellow-700 rounded p-4 flex items-center justify-between", children: [
|
|
1409
|
+
/* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsx(Typography, { variant: "label", className: "text-yellow-800 dark:text-yellow-200", children: "No admin users exist. You can make yourself an admin." }) }),
|
|
1410
|
+
/* @__PURE__ */ jsx(
|
|
1411
|
+
Button,
|
|
1412
|
+
{
|
|
1413
|
+
onClick: handleBootstrap,
|
|
1414
|
+
disabled: bootstrapping,
|
|
1415
|
+
children: bootstrapping ? /* @__PURE__ */ jsx(CircularProgress, { size: "small" }) : "Make me admin"
|
|
1416
|
+
}
|
|
1417
|
+
)
|
|
1418
|
+
] }),
|
|
1419
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center mt-12", children: [
|
|
1420
|
+
/* @__PURE__ */ jsx(Typography, { gutterBottom: true, variant: "h4", className: "grow", component: "h4", children: "Users" }),
|
|
1421
|
+
/* @__PURE__ */ jsx(Button, { startIcon: /* @__PURE__ */ jsx(AddIcon, {}), onClick: handleAddUser, children: "Add user" })
|
|
1422
|
+
] }),
|
|
1423
|
+
/* @__PURE__ */ jsx("div", { className: "overflow-auto", children: /* @__PURE__ */ jsxs(Table, { className: "w-full", children: [
|
|
1424
|
+
/* @__PURE__ */ jsxs(TableHeader, { children: [
|
|
1425
|
+
/* @__PURE__ */ jsx(TableCell, { header: true, className: "truncate w-16" }),
|
|
1426
|
+
/* @__PURE__ */ jsx(TableCell, { header: true, children: "Email" }),
|
|
1427
|
+
/* @__PURE__ */ jsx(TableCell, { header: true, children: "Name" }),
|
|
1428
|
+
/* @__PURE__ */ jsx(TableCell, { header: true, children: "Roles" })
|
|
1429
|
+
] }),
|
|
1430
|
+
/* @__PURE__ */ jsxs(TableBody, { children: [
|
|
1431
|
+
users.map((user) => /* @__PURE__ */ jsxs(TableRow, { onClick: () => handleEditUser(user), children: [
|
|
1432
|
+
/* @__PURE__ */ jsx(TableCell, { style: { width: "64px" }, children: /* @__PURE__ */ jsx(Tooltip, { asChild: true, title: "Delete this user", children: /* @__PURE__ */ jsx(
|
|
1433
|
+
IconButton,
|
|
1434
|
+
{
|
|
1435
|
+
size: "small",
|
|
1436
|
+
onClick: (e) => {
|
|
1437
|
+
e.stopPropagation();
|
|
1438
|
+
setUserToDelete(user);
|
|
1439
|
+
setDeleteConfirmOpen(true);
|
|
1440
|
+
},
|
|
1441
|
+
children: /* @__PURE__ */ jsx(DeleteIcon, {})
|
|
1442
|
+
}
|
|
1443
|
+
) }) }),
|
|
1444
|
+
/* @__PURE__ */ jsx(TableCell, { children: user.email }),
|
|
1445
|
+
/* @__PURE__ */ jsx(TableCell, { className: "font-medium", children: user.displayName }),
|
|
1446
|
+
/* @__PURE__ */ jsx(TableCell, { children: /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-2", children: user.roles?.map((roleId) => {
|
|
1447
|
+
const role = roles.find((r) => r.id === roleId);
|
|
1448
|
+
return role ? /* @__PURE__ */ jsx(RoleChip, { role }, roleId) : /* @__PURE__ */ jsx("span", { children: roleId }, roleId);
|
|
1449
|
+
}) }) })
|
|
1450
|
+
] }, user.uid)),
|
|
1451
|
+
users.length === 0 && /* @__PURE__ */ jsx(TableRow, { children: /* @__PURE__ */ jsx(TableCell, { colspan: 4, children: /* @__PURE__ */ jsx(CenteredView, { className: "flex flex-col gap-4 my-8 items-center", children: /* @__PURE__ */ jsx(Typography, { variant: "label", children: "There are no users yet" }) }) }) })
|
|
1452
|
+
] })
|
|
1453
|
+
] }) }),
|
|
1454
|
+
/* @__PURE__ */ jsx(
|
|
1455
|
+
UserDetailsForm,
|
|
1456
|
+
{
|
|
1457
|
+
open: dialogOpen,
|
|
1458
|
+
user: selectedUser,
|
|
1459
|
+
roles,
|
|
1460
|
+
saveUser,
|
|
1461
|
+
handleClose
|
|
1462
|
+
},
|
|
1463
|
+
selectedUser?.uid ?? `new-${formKey}`
|
|
1464
|
+
),
|
|
1465
|
+
/* @__PURE__ */ jsx(
|
|
1466
|
+
ConfirmationDialog,
|
|
1467
|
+
{
|
|
1468
|
+
open: deleteConfirmOpen,
|
|
1469
|
+
loading: deleteInProgress,
|
|
1470
|
+
onAccept: handleDelete,
|
|
1471
|
+
onCancel: () => {
|
|
1472
|
+
setDeleteConfirmOpen(false);
|
|
1473
|
+
setUserToDelete(void 0);
|
|
1474
|
+
},
|
|
1475
|
+
title: /* @__PURE__ */ jsx(Fragment, { children: "Delete?" }),
|
|
1476
|
+
body: /* @__PURE__ */ jsx(Fragment, { children: "Are you sure you want to delete this user?" })
|
|
1477
|
+
}
|
|
1478
|
+
)
|
|
1479
|
+
] });
|
|
1480
|
+
}
|
|
1481
|
+
function UserDetailsForm({
|
|
1482
|
+
open,
|
|
1483
|
+
user: userProp,
|
|
1484
|
+
roles,
|
|
1485
|
+
saveUser,
|
|
1486
|
+
handleClose
|
|
1487
|
+
}) {
|
|
1488
|
+
const snackbarController = useSnackbarController();
|
|
1489
|
+
const isNewUser = !userProp;
|
|
1490
|
+
const [displayName, setDisplayName] = useState(userProp?.displayName || "");
|
|
1491
|
+
const [email, setEmail] = useState(userProp?.email || "");
|
|
1492
|
+
const [selectedRoleIds, setSelectedRoleIds] = useState(
|
|
1493
|
+
userProp?.roles || ["editor"]
|
|
1494
|
+
);
|
|
1495
|
+
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
1496
|
+
const [errors, setErrors] = useState({});
|
|
1497
|
+
const [submitCount, setSubmitCount] = useState(0);
|
|
1498
|
+
const validate = () => {
|
|
1499
|
+
const newErrors = {};
|
|
1500
|
+
if (!displayName) newErrors.displayName = "Required";
|
|
1501
|
+
if (!email) newErrors.email = "Required";
|
|
1502
|
+
else if (!/\S+@\S+\.\S+/.test(email)) newErrors.email = "Invalid email";
|
|
1503
|
+
if (selectedRoleIds.length === 0) newErrors.roles = "At least one role is required";
|
|
1504
|
+
setErrors(newErrors);
|
|
1505
|
+
return Object.keys(newErrors).length === 0;
|
|
1506
|
+
};
|
|
1507
|
+
const handleSubmit = async (e) => {
|
|
1508
|
+
e.preventDefault();
|
|
1509
|
+
setSubmitCount((c) => c + 1);
|
|
1510
|
+
if (!validate()) return;
|
|
1511
|
+
setIsSubmitting(true);
|
|
1512
|
+
try {
|
|
1513
|
+
const userToSave = {
|
|
1514
|
+
uid: userProp?.uid || crypto.randomUUID(),
|
|
1515
|
+
email,
|
|
1516
|
+
displayName: displayName || null,
|
|
1517
|
+
photoURL: userProp?.photoURL || null,
|
|
1518
|
+
providerId: "custom",
|
|
1519
|
+
isAnonymous: false,
|
|
1520
|
+
roles: selectedRoleIds
|
|
1521
|
+
};
|
|
1522
|
+
await saveUser(userToSave);
|
|
1523
|
+
handleClose();
|
|
1524
|
+
} catch (error) {
|
|
1525
|
+
snackbarController.open({ type: "error", message: error instanceof Error ? error.message : "Failed to save user" });
|
|
1526
|
+
} finally {
|
|
1527
|
+
setIsSubmitting(false);
|
|
1528
|
+
}
|
|
1529
|
+
};
|
|
1530
|
+
const dirty = isNewUser || displayName !== (userProp?.displayName || "") || email !== (userProp?.email || "") || JSON.stringify(selectedRoleIds.sort()) !== JSON.stringify((userProp?.roles || []).sort());
|
|
1531
|
+
return /* @__PURE__ */ jsx(Dialog, { open, onOpenChange: (open2) => !open2 ? handleClose() : void 0, maxWidth: "4xl", children: /* @__PURE__ */ jsxs(
|
|
1532
|
+
"form",
|
|
1533
|
+
{
|
|
1534
|
+
onSubmit: handleSubmit,
|
|
1535
|
+
autoComplete: "off",
|
|
1536
|
+
noValidate: true,
|
|
1537
|
+
style: { display: "flex", flexDirection: "column", position: "relative", height: "100%" },
|
|
1538
|
+
children: [
|
|
1539
|
+
/* @__PURE__ */ jsx(DialogTitle, { variant: "h4", gutterBottom: false, children: "User" }),
|
|
1540
|
+
/* @__PURE__ */ jsx(DialogContent, { className: "h-full grow", children: /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-12 gap-4", children: [
|
|
1541
|
+
/* @__PURE__ */ jsxs("div", { className: "col-span-12", children: [
|
|
1542
|
+
/* @__PURE__ */ jsx(
|
|
1543
|
+
TextField,
|
|
1544
|
+
{
|
|
1545
|
+
name: "displayName",
|
|
1546
|
+
required: true,
|
|
1547
|
+
error: submitCount > 0 && Boolean(errors.displayName),
|
|
1548
|
+
value: displayName,
|
|
1549
|
+
onChange: (e) => setDisplayName(e.target.value),
|
|
1550
|
+
label: "Name"
|
|
1551
|
+
}
|
|
1552
|
+
),
|
|
1553
|
+
/* @__PURE__ */ jsx(FieldCaption, { children: submitCount > 0 && errors.displayName ? errors.displayName : "Name of this user" })
|
|
1554
|
+
] }),
|
|
1555
|
+
/* @__PURE__ */ jsxs("div", { className: "col-span-12", children: [
|
|
1556
|
+
/* @__PURE__ */ jsx(
|
|
1557
|
+
TextField,
|
|
1558
|
+
{
|
|
1559
|
+
required: true,
|
|
1560
|
+
error: submitCount > 0 && Boolean(errors.email),
|
|
1561
|
+
name: "email",
|
|
1562
|
+
value: email,
|
|
1563
|
+
onChange: (e) => setEmail(e.target.value),
|
|
1564
|
+
label: "Email",
|
|
1565
|
+
disabled: !isNewUser
|
|
1566
|
+
}
|
|
1567
|
+
),
|
|
1568
|
+
/* @__PURE__ */ jsx(FieldCaption, { children: submitCount > 0 && errors.email ? errors.email : "Email of this user" })
|
|
1569
|
+
] }),
|
|
1570
|
+
/* @__PURE__ */ jsx("div", { className: "col-span-12", children: /* @__PURE__ */ jsx(
|
|
1571
|
+
MultiSelect,
|
|
1572
|
+
{
|
|
1573
|
+
className: "w-full",
|
|
1574
|
+
label: "Roles",
|
|
1575
|
+
value: selectedRoleIds,
|
|
1576
|
+
onValueChange: (value) => setSelectedRoleIds(value),
|
|
1577
|
+
children: roles.map((role) => /* @__PURE__ */ jsx(MultiSelectItem, { value: role.id, children: /* @__PURE__ */ jsx(RoleChip, { role }) }, role.id))
|
|
1578
|
+
}
|
|
1579
|
+
) })
|
|
1580
|
+
] }) }),
|
|
1581
|
+
/* @__PURE__ */ jsxs(DialogActions, { children: [
|
|
1582
|
+
/* @__PURE__ */ jsx(Button, { variant: "text", onClick: handleClose, children: "Cancel" }),
|
|
1583
|
+
/* @__PURE__ */ jsx(
|
|
1584
|
+
LoadingButton,
|
|
1585
|
+
{
|
|
1586
|
+
variant: "filled",
|
|
1587
|
+
type: "submit",
|
|
1588
|
+
disabled: !dirty,
|
|
1589
|
+
loading: isSubmitting,
|
|
1590
|
+
children: isNewUser ? "Create user" : "Update"
|
|
1591
|
+
}
|
|
1592
|
+
)
|
|
1593
|
+
] })
|
|
1594
|
+
]
|
|
1595
|
+
}
|
|
1596
|
+
) });
|
|
1597
|
+
}
|
|
1598
|
+
function RolesView({ userManagement }) {
|
|
1599
|
+
const { roles, saveRole, deleteRole, loading, allowDefaultRolesCreation } = userManagement;
|
|
1600
|
+
const snackbarController = useSnackbarController();
|
|
1601
|
+
const [dialogOpen, setDialogOpen] = useState(false);
|
|
1602
|
+
const [selectedRole, setSelectedRole] = useState();
|
|
1603
|
+
const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false);
|
|
1604
|
+
const [roleToDelete, setRoleToDelete] = useState();
|
|
1605
|
+
const [deleteInProgress, setDeleteInProgress] = useState(false);
|
|
1606
|
+
const handleAddRole = () => {
|
|
1607
|
+
setSelectedRole(void 0);
|
|
1608
|
+
setDialogOpen(true);
|
|
1609
|
+
};
|
|
1610
|
+
const handleEditRole = (role) => {
|
|
1611
|
+
setSelectedRole(role);
|
|
1612
|
+
setDialogOpen(true);
|
|
1613
|
+
};
|
|
1614
|
+
const handleClose = () => {
|
|
1615
|
+
setDialogOpen(false);
|
|
1616
|
+
setSelectedRole(void 0);
|
|
1617
|
+
};
|
|
1618
|
+
const handleDelete = async () => {
|
|
1619
|
+
if (!roleToDelete) return;
|
|
1620
|
+
setDeleteInProgress(true);
|
|
1621
|
+
try {
|
|
1622
|
+
await deleteRole(roleToDelete);
|
|
1623
|
+
snackbarController.open({ type: "success", message: "Role deleted successfully" });
|
|
1624
|
+
setDeleteConfirmOpen(false);
|
|
1625
|
+
setRoleToDelete(void 0);
|
|
1626
|
+
} catch (error) {
|
|
1627
|
+
snackbarController.open({ type: "error", message: error instanceof Error ? error.message : "Error deleting role" });
|
|
1628
|
+
} finally {
|
|
1629
|
+
setDeleteInProgress(false);
|
|
1630
|
+
}
|
|
1631
|
+
};
|
|
1632
|
+
const createDefaultRoles = () => {
|
|
1633
|
+
const defaultRoles = [
|
|
1634
|
+
{ id: "admin", name: "Admin", isAdmin: true },
|
|
1635
|
+
{ id: "editor", name: "Editor", isAdmin: false },
|
|
1636
|
+
{ id: "viewer", name: "Viewer", isAdmin: false }
|
|
1637
|
+
];
|
|
1638
|
+
defaultRoles.forEach((role) => saveRole(role));
|
|
1639
|
+
};
|
|
1640
|
+
if (loading) {
|
|
1641
|
+
return /* @__PURE__ */ jsx(CenteredView, { children: /* @__PURE__ */ jsx(CircularProgress, {}) });
|
|
1642
|
+
}
|
|
1643
|
+
return /* @__PURE__ */ jsxs(Container, { className: "w-full flex flex-col py-4 gap-4", maxWidth: "6xl", children: [
|
|
1644
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center mt-12", children: [
|
|
1645
|
+
/* @__PURE__ */ jsx(Typography, { gutterBottom: true, variant: "h4", className: "grow", component: "h4", children: "Roles" }),
|
|
1646
|
+
/* @__PURE__ */ jsx(Button, { startIcon: /* @__PURE__ */ jsx(AddIcon, {}), onClick: handleAddRole, children: "Add role" })
|
|
1647
|
+
] }),
|
|
1648
|
+
/* @__PURE__ */ jsx("div", { className: "w-full overflow-auto", children: /* @__PURE__ */ jsxs(Table, { className: "w-full", children: [
|
|
1649
|
+
/* @__PURE__ */ jsxs(TableHeader, { children: [
|
|
1650
|
+
/* @__PURE__ */ jsx(TableCell, { header: true, className: "w-16" }),
|
|
1651
|
+
/* @__PURE__ */ jsx(TableCell, { header: true, children: "Role" }),
|
|
1652
|
+
/* @__PURE__ */ jsx(TableCell, { header: true, className: "items-center", children: "Is Admin" })
|
|
1653
|
+
] }),
|
|
1654
|
+
/* @__PURE__ */ jsxs(TableBody, { children: [
|
|
1655
|
+
roles.map((role) => {
|
|
1656
|
+
return /* @__PURE__ */ jsxs(TableRow, { onClick: () => handleEditRole(role), children: [
|
|
1657
|
+
/* @__PURE__ */ jsx(TableCell, { style: { width: "64px" }, children: !role.isAdmin && /* @__PURE__ */ jsx(Tooltip, { asChild: true, title: "Delete this role", children: /* @__PURE__ */ jsx(
|
|
1658
|
+
IconButton,
|
|
1659
|
+
{
|
|
1660
|
+
size: "small",
|
|
1661
|
+
onClick: (e) => {
|
|
1662
|
+
e.stopPropagation();
|
|
1663
|
+
setRoleToDelete(role);
|
|
1664
|
+
setDeleteConfirmOpen(true);
|
|
1665
|
+
},
|
|
1666
|
+
children: /* @__PURE__ */ jsx(DeleteIcon, {})
|
|
1667
|
+
}
|
|
1668
|
+
) }) }),
|
|
1669
|
+
/* @__PURE__ */ jsx(TableCell, { children: /* @__PURE__ */ jsx(RoleChip, { role }) }),
|
|
1670
|
+
/* @__PURE__ */ jsx(TableCell, { className: "items-center", children: /* @__PURE__ */ jsx(Checkbox, { checked: role.isAdmin ?? false }) })
|
|
1671
|
+
] }, role.id);
|
|
1672
|
+
}),
|
|
1673
|
+
roles.length === 0 && /* @__PURE__ */ jsx(TableRow, { children: /* @__PURE__ */ jsx(TableCell, { colspan: 4, children: /* @__PURE__ */ jsxs(CenteredView, { className: "flex flex-col gap-4 my-8 items-center", children: [
|
|
1674
|
+
/* @__PURE__ */ jsx(Typography, { variant: "label", children: "You don't have any roles yet." }),
|
|
1675
|
+
allowDefaultRolesCreation && /* @__PURE__ */ jsx(Button, { onClick: createDefaultRoles, children: "Create default roles" })
|
|
1676
|
+
] }) }) })
|
|
1677
|
+
] })
|
|
1678
|
+
] }) }),
|
|
1679
|
+
/* @__PURE__ */ jsx(
|
|
1680
|
+
RoleDetailsForm,
|
|
1681
|
+
{
|
|
1682
|
+
open: dialogOpen,
|
|
1683
|
+
role: selectedRole,
|
|
1684
|
+
saveRole,
|
|
1685
|
+
handleClose
|
|
1686
|
+
},
|
|
1687
|
+
selectedRole?.id ?? "new"
|
|
1688
|
+
),
|
|
1689
|
+
/* @__PURE__ */ jsx(
|
|
1690
|
+
ConfirmationDialog,
|
|
1691
|
+
{
|
|
1692
|
+
open: deleteConfirmOpen,
|
|
1693
|
+
loading: deleteInProgress,
|
|
1694
|
+
onAccept: handleDelete,
|
|
1695
|
+
onCancel: () => {
|
|
1696
|
+
setDeleteConfirmOpen(false);
|
|
1697
|
+
setRoleToDelete(void 0);
|
|
1698
|
+
},
|
|
1699
|
+
title: /* @__PURE__ */ jsx(Fragment, { children: "Delete?" }),
|
|
1700
|
+
body: /* @__PURE__ */ jsx(Fragment, { children: "Are you sure you want to delete this role?" })
|
|
1701
|
+
}
|
|
1702
|
+
)
|
|
1703
|
+
] });
|
|
1704
|
+
}
|
|
1705
|
+
function RoleDetailsForm({
|
|
1706
|
+
open,
|
|
1707
|
+
role: roleProp,
|
|
1708
|
+
saveRole,
|
|
1709
|
+
handleClose
|
|
1710
|
+
}) {
|
|
1711
|
+
const snackbarController = useSnackbarController();
|
|
1712
|
+
const isNewRole = !roleProp;
|
|
1713
|
+
const [roleId, setRoleId] = useState(roleProp?.id || "");
|
|
1714
|
+
const [roleName, setRoleName] = useState(roleProp?.name || "");
|
|
1715
|
+
const [isAdmin, setIsAdmin] = useState(roleProp?.isAdmin ?? false);
|
|
1716
|
+
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
1717
|
+
const [errors, setErrors] = useState({});
|
|
1718
|
+
const [submitCount, setSubmitCount] = useState(0);
|
|
1719
|
+
const validate = () => {
|
|
1720
|
+
const newErrors = {};
|
|
1721
|
+
if (!roleId) newErrors.id = "Required";
|
|
1722
|
+
if (!roleName) newErrors.name = "Required";
|
|
1723
|
+
setErrors(newErrors);
|
|
1724
|
+
return Object.keys(newErrors).length === 0;
|
|
1725
|
+
};
|
|
1726
|
+
const handleSubmit = async (e) => {
|
|
1727
|
+
e.preventDefault();
|
|
1728
|
+
setSubmitCount((c) => c + 1);
|
|
1729
|
+
if (!validate()) return;
|
|
1730
|
+
setIsSubmitting(true);
|
|
1731
|
+
try {
|
|
1732
|
+
await saveRole({
|
|
1733
|
+
id: roleId,
|
|
1734
|
+
name: roleName,
|
|
1735
|
+
isAdmin
|
|
1736
|
+
});
|
|
1737
|
+
handleClose();
|
|
1738
|
+
} catch (error) {
|
|
1739
|
+
snackbarController.open({ type: "error", message: error instanceof Error ? error.message : "Failed to save role" });
|
|
1740
|
+
} finally {
|
|
1741
|
+
setIsSubmitting(false);
|
|
1742
|
+
}
|
|
1743
|
+
};
|
|
1744
|
+
return /* @__PURE__ */ jsx(Dialog, { open, onOpenChange: (open2) => !open2 ? handleClose() : void 0, maxWidth: "6xl", children: /* @__PURE__ */ jsxs(
|
|
1745
|
+
"form",
|
|
1746
|
+
{
|
|
1747
|
+
onSubmit: handleSubmit,
|
|
1748
|
+
autoComplete: "off",
|
|
1749
|
+
noValidate: true,
|
|
1750
|
+
style: { display: "flex", flexDirection: "column", position: "relative", height: "100%" },
|
|
1751
|
+
children: [
|
|
1752
|
+
/* @__PURE__ */ jsx(DialogTitle, { variant: "h4", gutterBottom: false, children: "Role" }),
|
|
1753
|
+
/* @__PURE__ */ jsx(DialogContent, { className: "h-full grow overflow-y-auto", children: /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-12 gap-4", children: [
|
|
1754
|
+
/* @__PURE__ */ jsxs("div", { className: "col-span-12 sm:col-span-4", children: [
|
|
1755
|
+
/* @__PURE__ */ jsx(
|
|
1756
|
+
TextField,
|
|
1757
|
+
{
|
|
1758
|
+
name: "id",
|
|
1759
|
+
required: true,
|
|
1760
|
+
error: submitCount > 0 && Boolean(errors.id),
|
|
1761
|
+
value: roleId,
|
|
1762
|
+
onChange: (e) => setRoleId(e.target.value),
|
|
1763
|
+
label: "Role ID",
|
|
1764
|
+
disabled: !isNewRole
|
|
1765
|
+
}
|
|
1766
|
+
),
|
|
1767
|
+
/* @__PURE__ */ jsx(FieldCaption, { children: submitCount > 0 && errors.id ? errors.id : "Unique identifier for this role" })
|
|
1768
|
+
] }),
|
|
1769
|
+
/* @__PURE__ */ jsxs("div", { className: "col-span-12 sm:col-span-4", children: [
|
|
1770
|
+
/* @__PURE__ */ jsx(
|
|
1771
|
+
TextField,
|
|
1772
|
+
{
|
|
1773
|
+
name: "name",
|
|
1774
|
+
required: true,
|
|
1775
|
+
error: submitCount > 0 && Boolean(errors.name),
|
|
1776
|
+
value: roleName,
|
|
1777
|
+
onChange: (e) => setRoleName(e.target.value),
|
|
1778
|
+
label: "Role Name"
|
|
1779
|
+
}
|
|
1780
|
+
),
|
|
1781
|
+
/* @__PURE__ */ jsx(FieldCaption, { children: submitCount > 0 && errors.name ? errors.name : "Display name for this role" })
|
|
1782
|
+
] }),
|
|
1783
|
+
/* @__PURE__ */ jsx("div", { className: "col-span-12 sm:col-span-4 flex items-start pt-2", children: /* @__PURE__ */ jsxs("label", { className: "flex items-center gap-2 cursor-pointer mt-3", children: [
|
|
1784
|
+
/* @__PURE__ */ jsx(
|
|
1785
|
+
Checkbox,
|
|
1786
|
+
{
|
|
1787
|
+
checked: isAdmin,
|
|
1788
|
+
onCheckedChange: (checked) => setIsAdmin(Boolean(checked))
|
|
1789
|
+
}
|
|
1790
|
+
),
|
|
1791
|
+
/* @__PURE__ */ jsx("span", { className: "font-medium", children: "Is Admin" })
|
|
1792
|
+
] }) }),
|
|
1793
|
+
/* @__PURE__ */ jsx("div", { className: "col-span-12", children: /* @__PURE__ */ jsx(CollectionPermissionsMatrix, { roleId, isAdmin }) })
|
|
1794
|
+
] }) }),
|
|
1795
|
+
/* @__PURE__ */ jsxs(DialogActions, { children: [
|
|
1796
|
+
/* @__PURE__ */ jsx(Button, { variant: "text", onClick: handleClose, children: "Cancel" }),
|
|
1797
|
+
/* @__PURE__ */ jsx(
|
|
1798
|
+
LoadingButton,
|
|
1799
|
+
{
|
|
1800
|
+
variant: "filled",
|
|
1801
|
+
type: "submit",
|
|
1802
|
+
loading: isSubmitting,
|
|
1803
|
+
children: isNewRole ? "Create role" : "Update"
|
|
1804
|
+
}
|
|
1805
|
+
)
|
|
1806
|
+
] })
|
|
1807
|
+
]
|
|
1808
|
+
}
|
|
1809
|
+
) });
|
|
1810
|
+
}
|
|
1811
|
+
const CRUD_OPS = [
|
|
1812
|
+
{ op: "select", label: "Read" },
|
|
1813
|
+
{ op: "insert", label: "Create" },
|
|
1814
|
+
{ op: "update", label: "Edit" },
|
|
1815
|
+
{ op: "delete", label: "Delete" }
|
|
1816
|
+
];
|
|
1817
|
+
function hasRoleAccess(rules, roleId, op) {
|
|
1818
|
+
if (!rules || rules.length === 0) return true;
|
|
1819
|
+
const applicable = rules.filter(
|
|
1820
|
+
(r) => r.operation === op || r.operation === "all" || r.operations?.includes(op) || r.operations?.includes("all")
|
|
1821
|
+
);
|
|
1822
|
+
if (applicable.length === 0) return false;
|
|
1823
|
+
const forRole = applicable.filter(
|
|
1824
|
+
(r) => !r.roles || r.roles.length === 0 || r.roles.includes(roleId) || r.roles.includes("public")
|
|
1825
|
+
);
|
|
1826
|
+
if (forRole.length === 0) return false;
|
|
1827
|
+
for (const r of forRole) {
|
|
1828
|
+
if ((r.mode ?? "permissive") === "restrictive") return false;
|
|
1829
|
+
}
|
|
1830
|
+
return forRole.some((r) => (r.mode ?? "permissive") === "permissive");
|
|
1831
|
+
}
|
|
1832
|
+
function PermCell({ granted }) {
|
|
1833
|
+
return /* @__PURE__ */ jsx(
|
|
1834
|
+
"span",
|
|
1835
|
+
{
|
|
1836
|
+
className: granted ? "text-green-500 dark:text-green-400 text-base select-none" : "text-surface-300 dark:text-surface-600 text-base select-none",
|
|
1837
|
+
children: granted ? "✓" : "✗"
|
|
1838
|
+
}
|
|
1839
|
+
);
|
|
1840
|
+
}
|
|
1841
|
+
function CollectionPermissionsMatrix({ roleId, isAdmin }) {
|
|
1842
|
+
const { collections } = useCollectionRegistryController();
|
|
1843
|
+
if (!collections || collections.length === 0) {
|
|
1844
|
+
return /* @__PURE__ */ jsx("div", { className: "mt-4", children: /* @__PURE__ */ jsx(Typography, { variant: "label", className: "text-surface-400", children: "No collections configured" }) });
|
|
1845
|
+
}
|
|
1846
|
+
const topLevel = collections.filter((c) => !c.collectionGroup);
|
|
1847
|
+
return /* @__PURE__ */ jsxs("div", { className: "mt-6", children: [
|
|
1848
|
+
/* @__PURE__ */ jsx(Typography, { variant: "label", className: "mb-2 block text-surface-600 dark:text-surface-400 uppercase tracking-wide text-xs", children: "Collection permissions" }),
|
|
1849
|
+
/* @__PURE__ */ jsx("div", { className: "rounded-lg border border-surface-200 dark:border-surface-700 overflow-hidden", children: /* @__PURE__ */ jsxs(Table, { children: [
|
|
1850
|
+
/* @__PURE__ */ jsx(TableHeader, { children: /* @__PURE__ */ jsxs(TableRow, { children: [
|
|
1851
|
+
/* @__PURE__ */ jsx(TableCell, { header: true, children: "Collection" }),
|
|
1852
|
+
CRUD_OPS.map(({ op, label }) => /* @__PURE__ */ jsx(TableCell, { header: true, className: "text-center w-24", children: label }, op))
|
|
1853
|
+
] }) }),
|
|
1854
|
+
/* @__PURE__ */ jsx(TableBody, { children: topLevel.map((collection) => {
|
|
1855
|
+
const noRules = !collection.securityRules || collection.securityRules.length === 0;
|
|
1856
|
+
return /* @__PURE__ */ jsxs(TableRow, { children: [
|
|
1857
|
+
/* @__PURE__ */ jsxs(TableCell, { children: [
|
|
1858
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
1859
|
+
/* @__PURE__ */ jsx("span", { className: "font-medium", children: collection.name }),
|
|
1860
|
+
noRules && !isAdmin && /* @__PURE__ */ jsx(Tooltip, { title: "No security rules — unrestricted", children: /* @__PURE__ */ jsx(Chip, { className: "text-xs", colorScheme: "yellowLight", children: "No rules" }) })
|
|
1861
|
+
] }),
|
|
1862
|
+
/* @__PURE__ */ jsx("span", { className: "text-xs text-surface-400 font-mono", children: collection.slug })
|
|
1863
|
+
] }),
|
|
1864
|
+
CRUD_OPS.map(({ op }) => /* @__PURE__ */ jsx(TableCell, { className: "text-center", children: /* @__PURE__ */ jsx(PermCell, { granted: isAdmin || hasRoleAccess(collection.securityRules, roleId, op) }) }, op))
|
|
1865
|
+
] }, collection.slug);
|
|
1866
|
+
}) })
|
|
1867
|
+
] }) }),
|
|
1868
|
+
!roleId && /* @__PURE__ */ jsx(Typography, { variant: "caption", className: "mt-2 text-surface-400 italic", children: "Enter a role ID above to preview permissions" })
|
|
1869
|
+
] });
|
|
1870
|
+
}
|
|
1871
|
+
export {
|
|
1872
|
+
AuthApiError,
|
|
1873
|
+
RebaseLoginView,
|
|
1874
|
+
RolesView,
|
|
1875
|
+
UsersView,
|
|
1876
|
+
createUserManagementAdminViews,
|
|
1877
|
+
fetchAuthConfig,
|
|
1878
|
+
getApiUrl,
|
|
1879
|
+
setApiUrl,
|
|
1880
|
+
useBackendUserManagement,
|
|
1881
|
+
useRebaseAuthController
|
|
1882
|
+
};
|
|
1883
|
+
//# sourceMappingURL=index.es.js.map
|