@rebasepro/auth 0.1.2 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.es.js CHANGED
@@ -1,8 +1,4 @@
1
- import { useState, useRef, useEffect, useCallback, useLayoutEffect } from "react";
2
- import { jsxs, jsx, Fragment } from "react/jsx-runtime";
3
- import { Menu, MenuItem, iconSize, IconButton, Typography, cls, TextField, LoadingButton, Button, CenteredView, CircularProgress, Container, Table, TableHeader, TableCell, TableBody, TableRow, Tooltip, Checkbox, getColorSchemeForSeed, Chip, Dialog, DialogTitle, DialogContent, DialogActions, MultiSelect, MultiSelectItem } from "@rebasepro/ui";
4
- import { MoonIcon, SunIcon, SunMoonIcon, MailIcon, ArrowLeftIcon, PlusIcon, Trash2Icon } from "lucide-react";
5
- import { useModeController, useTranslation, LanguageToggle, ErrorView, RebaseLogo, useRebaseRegistryDispatch, useSnackbarController, ConfirmationDialog, useAuthController } from "@rebasepro/core";
1
+ import { useState, useRef, useEffect, useCallback } from "react";
6
2
  let baseApiUrl = "";
7
3
  function setApiUrl(url) {
8
4
  baseApiUrl = url;
@@ -72,12 +68,11 @@ async function login(email, password) {
72
68
  });
73
69
  return handleResponse(response);
74
70
  }
75
- async function googleLogin(tokenOrPayload, tokenType = "idToken") {
76
- const body = typeof tokenOrPayload === "string" ? { [tokenType]: tokenOrPayload } : tokenOrPayload;
71
+ async function googleLogin(payload) {
77
72
  const response = await fetchWithHandling(`${baseApiUrl}/api/auth/google`, {
78
73
  method: "POST",
79
74
  headers: { "Content-Type": "application/json" },
80
- body: JSON.stringify(body)
75
+ body: JSON.stringify(payload)
81
76
  });
82
77
  return handleResponse(response);
83
78
  }
@@ -90,13 +85,11 @@ async function oauthLogin(providerId, payload) {
90
85
  return handleResponse(response);
91
86
  }
92
87
  async function refreshAccessToken(refreshToken) {
93
- console.log("[AUTH-API] Calling refresh endpoint...");
94
88
  const response = await fetchWithHandling(`${baseApiUrl}/api/auth/refresh`, {
95
89
  method: "POST",
96
90
  headers: { "Content-Type": "application/json" },
97
91
  body: JSON.stringify({ refreshToken })
98
92
  });
99
- console.log("[AUTH-API] Refresh response status:", response.status);
100
93
  return handleResponse(response);
101
94
  }
102
95
  async function logout(refreshToken) {
@@ -198,7 +191,11 @@ async function revokeAllSessions(accessToken) {
198
191
  return handleResponse(response);
199
192
  }
200
193
  let authConfigInflight = null;
194
+ let authConfigCached = null;
201
195
  async function fetchAuthConfig() {
196
+ if (authConfigCached) {
197
+ return authConfigCached;
198
+ }
202
199
  if (authConfigInflight) {
203
200
  return authConfigInflight;
204
201
  }
@@ -210,11 +207,17 @@ async function fetchAuthConfig() {
210
207
  return handleResponse(response);
211
208
  })();
212
209
  try {
213
- return await authConfigInflight;
210
+ const result = await authConfigInflight;
211
+ authConfigCached = result;
212
+ return result;
214
213
  } finally {
215
214
  authConfigInflight = null;
216
215
  }
217
216
  }
217
+ function clearAuthConfigCache() {
218
+ authConfigCached = null;
219
+ authConfigInflight = null;
220
+ }
218
221
  const STORAGE_KEY = "rebase_auth";
219
222
  const TOKEN_REFRESH_BUFFER_MS = 2 * 60 * 1e3;
220
223
  function convertToUser(userInfo) {
@@ -225,18 +228,14 @@ function convertToUser(userInfo) {
225
228
  photoURL: userInfo.photoURL || null,
226
229
  providerId: "custom",
227
230
  isAnonymous: false,
228
- roles: userInfo.roles || []
231
+ roles: userInfo.roles || [],
232
+ metadata: userInfo.metadata
229
233
  };
230
234
  }
231
235
  function saveAuthToStorage(tokens, user) {
232
236
  try {
233
- const data = {
234
- tokens,
235
- user
236
- };
237
+ const data = { tokens, user };
237
238
  localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
238
- const expiryDate = new Date(tokens.accessTokenExpiresAt);
239
- const expiryStr = Number.isFinite(tokens.accessTokenExpiresAt) ? expiryDate.toISOString() : "invalid";
240
239
  } catch (e) {
241
240
  }
242
241
  }
@@ -283,6 +282,9 @@ function useRebaseAuthController(props = {}) {
283
282
  setApiUrl(apiUrl);
284
283
  }
285
284
  }, [client, apiUrl]);
285
+ const clearError = useCallback(() => {
286
+ setAuthProviderError(null);
287
+ }, []);
286
288
  const clearSessionAndSignOut = useCallback(() => {
287
289
  tokensRef.current = null;
288
290
  clearAuthFromStorage();
@@ -319,7 +321,6 @@ function useRebaseAuthController(props = {}) {
319
321
  if (latestStoredData) {
320
322
  saveAuthToStorage(newTokens, latestStoredData.user);
321
323
  }
322
- const newExpiryStr = Number.isFinite(newTokens.accessTokenExpiresAt) ? new Date(newTokens.accessTokenExpiresAt).toISOString() : "invalid";
323
324
  return newTokens;
324
325
  } catch (error) {
325
326
  if (error instanceof Error && error.code === "NETWORK_ERROR") {
@@ -424,26 +425,20 @@ function useRebaseAuthController(props = {}) {
424
425
  }
425
426
  }, [client, getAuthToken, refreshAccessToken$1, clearSessionAndSignOut]);
426
427
  const handleAuthSuccess = useCallback(async (userInfo, tokens) => {
427
- console.log("[Auth] handleAuthSuccess called, user:", userInfo.email, "uid:", userInfo.uid);
428
428
  tokensRef.current = tokens;
429
429
  let convertedUser = convertToUser(userInfo);
430
430
  if (defineRolesFor) {
431
431
  const customRoles = await defineRolesFor(convertedUser);
432
432
  if (customRoles) {
433
- convertedUser = {
434
- ...convertedUser,
435
- roles: customRoles.map((r) => r.id)
436
- };
433
+ convertedUser = { ...convertedUser, roles: customRoles.map((r) => r.id) };
437
434
  }
438
435
  }
439
436
  saveAuthToStorage(tokens, userInfo);
440
- console.log("[Auth] Calling setUser, roles:", convertedUser.roles);
441
437
  setUser(convertedUser);
442
438
  setAuthError(null);
443
439
  setAuthProviderError(null);
444
440
  setLoginSkipped(false);
445
441
  scheduleTokenRefresh(tokens);
446
- console.log("[Auth] handleAuthSuccess completed");
447
442
  }, [scheduleTokenRefresh, defineRolesFor]);
448
443
  const emailPasswordLogin = useCallback(async (email, password) => {
449
444
  setAuthLoading(true);
@@ -471,11 +466,11 @@ function useRebaseAuthController(props = {}) {
471
466
  setAuthLoading(false);
472
467
  }
473
468
  }, [handleAuthSuccess]);
474
- const googleLogin$1 = useCallback(async (tokenOrPayload, tokenType) => {
469
+ const googleLogin$1 = useCallback(async (payload) => {
475
470
  setAuthLoading(true);
476
471
  setAuthProviderError(null);
477
472
  try {
478
- const response = typeof tokenOrPayload === "string" ? await googleLogin(tokenOrPayload, tokenType ?? "idToken") : await googleLogin(tokenOrPayload);
473
+ const response = await googleLogin(payload);
479
474
  await handleAuthSuccess(response.user, response.tokens);
480
475
  } catch (error) {
481
476
  setAuthProviderError(error);
@@ -769,6 +764,8 @@ function useRebaseAuthController(props = {}) {
769
764
  fetchSessions: fetchSessions$1,
770
765
  revokeSession: revokeSession$1,
771
766
  revokeAllSessions: revokeAllSessions$1,
767
+ clearError,
768
+ setAuthProviderError,
772
769
  extra,
773
770
  setExtra,
774
771
  capabilities: {
@@ -908,6 +905,12 @@ function useBackendUserManagement(config) {
908
905
  setLoading(false);
909
906
  return;
910
907
  }
908
+ const userRoles = currentUser.roles ?? [];
909
+ const isUserAdmin = userRoles.some((r) => r === "admin" || r === "schema-admin");
910
+ if (!isUserAdmin) {
911
+ setLoading(false);
912
+ return;
913
+ }
911
914
  if (lastLoadedUidRef.current === currentUser.uid) {
912
915
  setLoading(false);
913
916
  return;
@@ -1078,7 +1081,7 @@ function useBackendUserManagement(config) {
1078
1081
  saveRole,
1079
1082
  deleteRole,
1080
1083
  isAdmin,
1081
- allowDefaultRolesCreation: true,
1084
+ allowDefaultRolesCreation: isAdmin,
1082
1085
  includeCollectionConfigPermissions: true,
1083
1086
  defineRolesFor,
1084
1087
  getUser,
@@ -1088,1197 +1091,9 @@ function useBackendUserManagement(config) {
1088
1091
  bootstrapAdmin
1089
1092
  };
1090
1093
  }
1091
- function RebaseLoginView({
1092
- logo,
1093
- authController,
1094
- noUserComponent,
1095
- disableSignupScreen = false,
1096
- disabled = false,
1097
- notAllowedError,
1098
- googleEnabled = false,
1099
- googleClientId
1100
- }) {
1101
- const modeState = useModeController();
1102
- const { mode: colorMode, setMode: setColorMode } = modeState;
1103
- const { t } = useTranslation();
1104
- const [mode, setMode] = useState("buttons");
1105
- const [fadeIn, setFadeIn] = useState(false);
1106
- const [viewVisible, setViewVisible] = useState(true);
1107
- const switchMode = (newMode) => {
1108
- setViewVisible(false);
1109
- setTimeout(() => {
1110
- setMode(newMode);
1111
- setViewVisible(true);
1112
- }, 150);
1113
- };
1114
- const isBootstrapMode = authController.needsSetup;
1115
- useEffect(() => {
1116
- const timer = setTimeout(() => setFadeIn(true), 50);
1117
- return () => clearTimeout(timer);
1118
- }, []);
1119
- function buildErrorView() {
1120
- if (!authController.authProviderError) return null;
1121
- if (authController.user != null) return null;
1122
- return /* @__PURE__ */ jsx("div", { className: "w-full", children: /* @__PURE__ */ jsx(ErrorView, { error: authController.authProviderError.message ?? authController.authProviderError }) });
1123
- }
1124
- let logoComponent;
1125
- if (logo) {
1126
- logoComponent = /* @__PURE__ */ jsx(
1127
- "img",
1128
- {
1129
- src: logo,
1130
- style: {
1131
- height: "100%",
1132
- width: "100%",
1133
- objectFit: "cover"
1134
- },
1135
- alt: "Logo"
1136
- }
1137
- );
1138
- } else {
1139
- logoComponent = /* @__PURE__ */ jsx(RebaseLogo, {});
1140
- }
1141
- let notAllowedMessage;
1142
- if (notAllowedError) {
1143
- if (typeof notAllowedError === "string") {
1144
- notAllowedMessage = notAllowedError;
1145
- } else if (notAllowedError instanceof Error) {
1146
- notAllowedMessage = notAllowedError.message;
1147
- } else {
1148
- notAllowedMessage = "It looks like you don't have access to the CMS, based on the specified Authenticator configuration";
1149
- }
1150
- }
1151
- const showRegistration = !disableSignupScreen && authController.registrationEnabled;
1152
- return /* @__PURE__ */ jsxs(
1153
- "div",
1154
- {
1155
- className: cls(
1156
- "relative flex items-center justify-center h-screen w-screen p-4 transition-opacity duration-500 bg-white dark:bg-surface-900",
1157
- fadeIn ? "opacity-100" : "opacity-0"
1158
- ),
1159
- children: [
1160
- /* @__PURE__ */ jsxs("div", { className: "absolute top-4 right-4 flex items-center gap-1 z-10", children: [
1161
- /* @__PURE__ */ jsx(LanguageToggle, {}),
1162
- /* @__PURE__ */ jsxs(
1163
- Menu,
1164
- {
1165
- trigger: /* @__PURE__ */ jsx(
1166
- IconButton,
1167
- {
1168
- color: "inherit",
1169
- "aria-label": "Toggle theme",
1170
- children: colorMode === "dark" ? /* @__PURE__ */ jsx(MoonIcon, { size: iconSize.small }) : /* @__PURE__ */ jsx(SunIcon, { size: iconSize.small })
1171
- }
1172
- ),
1173
- children: [
1174
- /* @__PURE__ */ jsxs(MenuItem, { onClick: () => setColorMode("dark"), children: [
1175
- /* @__PURE__ */ jsx(MoonIcon, { size: iconSize.smallest }),
1176
- " ",
1177
- t("dark_mode")
1178
- ] }),
1179
- /* @__PURE__ */ jsxs(MenuItem, { onClick: () => setColorMode("light"), children: [
1180
- /* @__PURE__ */ jsx(SunIcon, { size: iconSize.smallest }),
1181
- " ",
1182
- t("light_mode")
1183
- ] }),
1184
- /* @__PURE__ */ jsxs(MenuItem, { onClick: () => setColorMode("system"), children: [
1185
- /* @__PURE__ */ jsx(SunMoonIcon, { size: iconSize.smallest }),
1186
- " ",
1187
- t("system_mode")
1188
- ] })
1189
- ]
1190
- }
1191
- )
1192
- ] }),
1193
- /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center w-[480px] max-w-full p-8 sm:p-10", children: [
1194
- /* @__PURE__ */ jsx("div", { className: "w-32 h-32 m-2 mb-6", children: logoComponent }),
1195
- notAllowedMessage && /* @__PURE__ */ jsx("div", { className: "p-4 w-full", children: /* @__PURE__ */ jsx(ErrorView, { error: notAllowedMessage }) }),
1196
- mode !== "forgot" && buildErrorView(),
1197
- /* @__PURE__ */ jsxs("div", { className: cls(
1198
- "w-full transition-opacity duration-150",
1199
- viewVisible ? "opacity-100" : "opacity-0"
1200
- ), children: [
1201
- isBootstrapMode && !authController.user && /* @__PURE__ */ jsx(
1202
- LoginForm,
1203
- {
1204
- authController,
1205
- registrationMode: true,
1206
- onClose: () => {
1207
- },
1208
- onForgotPassword: () => {
1209
- },
1210
- noUserComponent,
1211
- disableSignupScreen: false,
1212
- bootstrapMode: true
1213
- }
1214
- ),
1215
- !isBootstrapMode && /* @__PURE__ */ jsxs(Fragment, { children: [
1216
- mode === "buttons" && /* @__PURE__ */ jsxs("div", { className: "w-full flex flex-col gap-3 mt-2", children: [
1217
- /* @__PURE__ */ jsx(
1218
- LoginButton,
1219
- {
1220
- disabled,
1221
- text: "Sign in with email",
1222
- icon: /* @__PURE__ */ jsx(MailIcon, {}),
1223
- onClick: () => switchMode("login")
1224
- }
1225
- ),
1226
- googleEnabled && googleClientId && /* @__PURE__ */ jsx(
1227
- GoogleLoginButton,
1228
- {
1229
- disabled,
1230
- googleClientId,
1231
- authController
1232
- }
1233
- ),
1234
- showRegistration && /* @__PURE__ */ jsx("div", { className: "mt-2 text-center", children: /* @__PURE__ */ jsxs(Typography, { variant: "body2", color: "secondary", children: [
1235
- "Don't have an account?",
1236
- " ",
1237
- /* @__PURE__ */ jsx(
1238
- "button",
1239
- {
1240
- type: "button",
1241
- className: cls(
1242
- "font-semibold hover:underline cursor-pointer",
1243
- "text-primary-600 dark:text-primary-400"
1244
- ),
1245
- onClick: () => switchMode("register"),
1246
- children: "Create one"
1247
- }
1248
- )
1249
- ] }) })
1250
- ] }),
1251
- mode === "login" && /* @__PURE__ */ jsx(
1252
- LoginForm,
1253
- {
1254
- authController,
1255
- registrationMode: false,
1256
- onClose: () => switchMode("buttons"),
1257
- onForgotPassword: () => switchMode("forgot"),
1258
- noUserComponent,
1259
- disableSignupScreen,
1260
- switchToRegister: showRegistration ? () => switchMode("register") : void 0
1261
- }
1262
- ),
1263
- mode === "register" && /* @__PURE__ */ jsx(
1264
- LoginForm,
1265
- {
1266
- authController,
1267
- registrationMode: true,
1268
- onClose: () => switchMode("buttons"),
1269
- onForgotPassword: () => switchMode("forgot"),
1270
- noUserComponent,
1271
- disableSignupScreen,
1272
- switchToLogin: () => switchMode("login")
1273
- }
1274
- ),
1275
- mode === "forgot" && /* @__PURE__ */ jsx(
1276
- ForgotPasswordForm,
1277
- {
1278
- authController,
1279
- onClose: () => switchMode("login")
1280
- }
1281
- )
1282
- ] })
1283
- ] })
1284
- ] })
1285
- ]
1286
- }
1287
- );
1288
- }
1289
- function LoginButton({
1290
- icon,
1291
- onClick,
1292
- text,
1293
- disabled
1294
- }) {
1295
- return /* @__PURE__ */ jsx(
1296
- Button,
1297
- {
1298
- disabled,
1299
- className: "w-full",
1300
- variant: "outlined",
1301
- size: "large",
1302
- onClick,
1303
- children: /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-center w-full gap-3 py-1", children: [
1304
- /* @__PURE__ */ jsx("span", { className: "flex items-center justify-center w-5 h-5", children: icon }),
1305
- /* @__PURE__ */ jsx(Typography, { variant: "button", children: text })
1306
- ] })
1307
- }
1308
- );
1309
- }
1310
- const GoogleIcon = () => /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 24 24", width: "20", height: "20", children: [
1311
- /* @__PURE__ */ jsx(
1312
- "path",
1313
- {
1314
- fill: "#4285F4",
1315
- 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"
1316
- }
1317
- ),
1318
- /* @__PURE__ */ jsx(
1319
- "path",
1320
- {
1321
- fill: "#34A853",
1322
- 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"
1323
- }
1324
- ),
1325
- /* @__PURE__ */ jsx(
1326
- "path",
1327
- {
1328
- fill: "#FBBC05",
1329
- 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"
1330
- }
1331
- ),
1332
- /* @__PURE__ */ jsx(
1333
- "path",
1334
- {
1335
- fill: "#EA4335",
1336
- 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"
1337
- }
1338
- )
1339
- ] });
1340
- function GoogleLoginButton({
1341
- disabled,
1342
- googleClientId,
1343
- authController
1344
- }) {
1345
- const codeClientRef = useRef(null);
1346
- useEffect(() => {
1347
- const google = window.google;
1348
- if (!google || codeClientRef.current) return;
1349
- codeClientRef.current = google.accounts.oauth2.initCodeClient({
1350
- client_id: googleClientId,
1351
- scope: "openid email profile",
1352
- ux_mode: "popup",
1353
- callback: async (response) => {
1354
- if (response.error || !response.code) {
1355
- console.error("Google login error:", response.error);
1356
- return;
1357
- }
1358
- try {
1359
- await authController.googleLogin({
1360
- code: response.code,
1361
- redirectUri: "postmessage"
1362
- });
1363
- } catch (err) {
1364
- console.error("Google login error:", err);
1365
- }
1366
- }
1367
- });
1368
- }, [googleClientId, authController]);
1369
- const handleClick = () => {
1370
- if (!codeClientRef.current) {
1371
- console.error("Google Sign-In not loaded");
1372
- return;
1373
- }
1374
- codeClientRef.current.requestCode();
1375
- };
1376
- return /* @__PURE__ */ jsx(
1377
- LoginButton,
1378
- {
1379
- disabled,
1380
- text: "Sign in with Google",
1381
- icon: /* @__PURE__ */ jsx(GoogleIcon, {}),
1382
- onClick: handleClick
1383
- }
1384
- );
1385
- }
1386
- function LoginForm({
1387
- onClose,
1388
- onForgotPassword,
1389
- authController,
1390
- registrationMode,
1391
- noUserComponent,
1392
- disableSignupScreen,
1393
- bootstrapMode = false,
1394
- switchToRegister,
1395
- switchToLogin
1396
- }) {
1397
- const passwordRef = useRef(null);
1398
- const [email, setEmail] = useState();
1399
- const [password, setPassword] = useState();
1400
- const [displayName, setDisplayName] = useState();
1401
- useEffect(() => {
1402
- if (!document) return;
1403
- const escFunction = (event) => {
1404
- if (event.keyCode === 27) {
1405
- onClose();
1406
- }
1407
- };
1408
- document.addEventListener("keydown", escFunction, false);
1409
- return () => {
1410
- document.removeEventListener("keydown", escFunction, false);
1411
- };
1412
- }, [onClose]);
1413
- function handleEnterPassword() {
1414
- if (email && password) {
1415
- authController.emailPasswordLogin(email, password);
1416
- }
1417
- }
1418
- function handleRegistration() {
1419
- if (email && password) {
1420
- authController.register(email, password, displayName);
1421
- }
1422
- }
1423
- const handleSubmit = (event) => {
1424
- event.preventDefault();
1425
- if (registrationMode)
1426
- handleRegistration();
1427
- else
1428
- handleEnterPassword();
1429
- };
1430
- const title = bootstrapMode ? "Welcome!" : registrationMode ? "Create account" : "Sign in";
1431
- const subtitle = bootstrapMode ? "Create your admin account to get started. This account will have admin privileges." : registrationMode ? "Fill in your details to create a new account" : "Enter your credentials to continue";
1432
- const buttonLabel = registrationMode ? "Create account" : "Sign in";
1433
- return /* @__PURE__ */ jsxs("form", { onSubmit: handleSubmit, className: "flex flex-col w-full gap-1 mt-2", children: [
1434
- !bootstrapMode && /* @__PURE__ */ jsx("div", { className: "w-full mb-2 -ml-2.5", children: /* @__PURE__ */ jsx(IconButton, { onClick: onClose, children: /* @__PURE__ */ jsx(ArrowLeftIcon, {}) }) }),
1435
- /* @__PURE__ */ jsx(Typography, { variant: "h6", className: "mb-0.5", children: title }),
1436
- /* @__PURE__ */ jsx(Typography, { variant: "body2", color: "secondary", className: "mb-5", children: subtitle }),
1437
- registrationMode && noUserComponent && /* @__PURE__ */ jsx("div", { className: "w-full mb-2", children: noUserComponent }),
1438
- registrationMode && /* @__PURE__ */ jsxs("div", { className: "w-full mb-3", children: [
1439
- /* @__PURE__ */ jsx(Typography, { variant: "label", color: "secondary", className: "mb-1", children: "Display Name" }),
1440
- /* @__PURE__ */ jsx(
1441
- TextField,
1442
- {
1443
- placeholder: "Jane Doe (optional)",
1444
- className: "w-full",
1445
- value: displayName ?? "",
1446
- disabled: authController.initialLoading,
1447
- type: "text",
1448
- size: "medium",
1449
- onChange: (event) => setDisplayName(event.target.value)
1450
- }
1451
- )
1452
- ] }),
1453
- /* @__PURE__ */ jsxs("div", { className: "w-full mb-3", children: [
1454
- /* @__PURE__ */ jsx(Typography, { variant: "label", color: "secondary", className: "mb-1", children: "Email" }),
1455
- /* @__PURE__ */ jsx(
1456
- TextField,
1457
- {
1458
- placeholder: "you@example.com",
1459
- className: "w-full",
1460
- autoFocus: true,
1461
- value: email ?? "",
1462
- disabled: authController.initialLoading,
1463
- type: "email",
1464
- size: "medium",
1465
- onChange: (event) => setEmail(event.target.value)
1466
- }
1467
- )
1468
- ] }),
1469
- /* @__PURE__ */ jsxs("div", { className: "w-full mb-1", children: [
1470
- /* @__PURE__ */ jsx(Typography, { variant: "label", color: "secondary", className: "mb-1", children: "Password" }),
1471
- /* @__PURE__ */ jsx(
1472
- TextField,
1473
- {
1474
- placeholder: "••••••••",
1475
- className: "w-full",
1476
- value: password ?? "",
1477
- disabled: authController.initialLoading,
1478
- inputRef: passwordRef,
1479
- type: "password",
1480
- size: "medium",
1481
- onChange: (event) => setPassword(event.target.value)
1482
- }
1483
- )
1484
- ] }),
1485
- registrationMode && /* @__PURE__ */ jsx(Typography, { variant: "caption", color: "secondary", className: "mb-3", children: "Password must be 8+ characters with uppercase, lowercase, and a number" }),
1486
- !registrationMode && /* @__PURE__ */ jsx("div", { className: "w-full text-right mb-3", children: /* @__PURE__ */ jsx(
1487
- "button",
1488
- {
1489
- type: "button",
1490
- className: cls(
1491
- "text-xs font-medium hover:underline cursor-pointer",
1492
- "text-primary-600 dark:text-primary-400"
1493
- ),
1494
- onClick: onForgotPassword,
1495
- children: "Forgot password?"
1496
- }
1497
- ) }),
1498
- /* @__PURE__ */ jsx(
1499
- LoadingButton,
1500
- {
1501
- type: "submit",
1502
- variant: "filled",
1503
- color: "primary",
1504
- className: "w-full mt-1",
1505
- size: "large",
1506
- loading: authController.authLoading,
1507
- disabled: authController.authLoading || !email || !password,
1508
- children: buttonLabel
1509
- }
1510
- ),
1511
- switchToRegister && /* @__PURE__ */ jsx("div", { className: "mt-4 text-center", children: /* @__PURE__ */ jsxs(Typography, { variant: "body2", color: "secondary", children: [
1512
- "Don't have an account?",
1513
- " ",
1514
- /* @__PURE__ */ jsx(
1515
- "button",
1516
- {
1517
- type: "button",
1518
- className: cls(
1519
- "font-semibold hover:underline cursor-pointer",
1520
- "text-primary-600 dark:text-primary-400"
1521
- ),
1522
- onClick: switchToRegister,
1523
- children: "Create one"
1524
- }
1525
- )
1526
- ] }) }),
1527
- switchToLogin && /* @__PURE__ */ jsx("div", { className: "mt-4 text-center", children: /* @__PURE__ */ jsxs(Typography, { variant: "body2", color: "secondary", children: [
1528
- "Already have an account?",
1529
- " ",
1530
- /* @__PURE__ */ jsx(
1531
- "button",
1532
- {
1533
- type: "button",
1534
- className: cls(
1535
- "font-semibold hover:underline cursor-pointer",
1536
- "text-primary-600 dark:text-primary-400"
1537
- ),
1538
- onClick: switchToLogin,
1539
- children: "Sign in"
1540
- }
1541
- )
1542
- ] }) })
1543
- ] });
1544
- }
1545
- function ForgotPasswordForm({
1546
- onClose,
1547
- authController
1548
- }) {
1549
- const [email, setEmail] = useState("");
1550
- const [submitted, setSubmitted] = useState(false);
1551
- const [error, setError] = useState(null);
1552
- useEffect(() => {
1553
- if (!document) return;
1554
- const escFunction = (event) => {
1555
- if (event.keyCode === 27) {
1556
- onClose();
1557
- }
1558
- };
1559
- document.addEventListener("keydown", escFunction, false);
1560
- return () => {
1561
- document.removeEventListener("keydown", escFunction, false);
1562
- };
1563
- }, [onClose]);
1564
- const handleSubmit = async (event) => {
1565
- event.preventDefault();
1566
- setError(null);
1567
- if (!email) {
1568
- setError("Please enter your email address");
1569
- return;
1570
- }
1571
- try {
1572
- await authController.forgotPassword(email);
1573
- setSubmitted(true);
1574
- } catch (err) {
1575
- if (err instanceof Error && err.code === "EMAIL_NOT_CONFIGURED") {
1576
- setError("Password reset is not available. Please contact your administrator.");
1577
- } else {
1578
- setSubmitted(true);
1579
- }
1580
- }
1581
- };
1582
- if (submitted) {
1583
- return /* @__PURE__ */ jsxs("div", { className: "flex flex-col w-full gap-4 mt-2", children: [
1584
- /* @__PURE__ */ jsx("div", { className: "w-full -ml-2.5", children: /* @__PURE__ */ jsx(IconButton, { onClick: onClose, children: /* @__PURE__ */ jsx(ArrowLeftIcon, {}) }) }),
1585
- /* @__PURE__ */ jsxs("div", { className: cls(
1586
- "text-center rounded-xl p-6",
1587
- "bg-surface-50 dark:bg-surface-950"
1588
- ), children: [
1589
- /* @__PURE__ */ jsx("div", { className: "text-3xl mb-3", children: "📧" }),
1590
- /* @__PURE__ */ jsx(Typography, { variant: "subtitle1", className: "mb-2", children: "Check your email" }),
1591
- /* @__PURE__ */ jsxs(Typography, { variant: "body2", color: "secondary", children: [
1592
- "If an account exists for ",
1593
- /* @__PURE__ */ jsx("strong", { children: email }),
1594
- ", you'll receive a password reset link shortly."
1595
- ] })
1596
- ] }),
1597
- /* @__PURE__ */ jsx(Button, { onClick: onClose, variant: "text", className: "mt-2", children: "Back to sign in" })
1598
- ] });
1599
- }
1600
- return /* @__PURE__ */ jsxs("form", { onSubmit: handleSubmit, className: "flex flex-col w-full gap-1 mt-2", children: [
1601
- /* @__PURE__ */ jsx("div", { className: "w-full mb-2 -ml-2.5", children: /* @__PURE__ */ jsx(IconButton, { onClick: onClose, children: /* @__PURE__ */ jsx(ArrowLeftIcon, {}) }) }),
1602
- /* @__PURE__ */ jsx(Typography, { variant: "h6", className: "mb-0.5", children: "Reset password" }),
1603
- /* @__PURE__ */ jsx(Typography, { variant: "body2", color: "secondary", className: "mb-5", children: "Enter your email and we'll send you a reset link." }),
1604
- error && /* @__PURE__ */ jsx("div", { className: "w-full mb-3", children: /* @__PURE__ */ jsx(ErrorView, { error }) }),
1605
- /* @__PURE__ */ jsxs("div", { className: "w-full mb-3", children: [
1606
- /* @__PURE__ */ jsx(Typography, { variant: "label", color: "secondary", className: "mb-1", children: "Email" }),
1607
- /* @__PURE__ */ jsx(
1608
- TextField,
1609
- {
1610
- placeholder: "you@example.com",
1611
- className: "w-full",
1612
- autoFocus: true,
1613
- value: email,
1614
- type: "email",
1615
- size: "medium",
1616
- onChange: (event) => setEmail(event.target.value)
1617
- }
1618
- )
1619
- ] }),
1620
- /* @__PURE__ */ jsx(
1621
- LoadingButton,
1622
- {
1623
- type: "submit",
1624
- variant: "filled",
1625
- color: "primary",
1626
- className: "w-full",
1627
- size: "large",
1628
- loading: authController.authLoading,
1629
- disabled: authController.authLoading || !email,
1630
- children: "Send reset link"
1631
- }
1632
- )
1633
- ] });
1634
- }
1635
- function RebaseAuth({ loginView }) {
1636
- const dispatch = useRebaseRegistryDispatch();
1637
- const registeredRef = useRef(false);
1638
- useLayoutEffect(() => {
1639
- dispatch.registerAuth({ loginView });
1640
- registeredRef.current = true;
1641
- return () => {
1642
- registeredRef.current = false;
1643
- dispatch.unregisterAuth();
1644
- };
1645
- }, [dispatch, loginView]);
1646
- return null;
1647
- }
1648
- function createUserManagementAdminViews({ userManagement, apiUrl, getAuthToken, collections = [] }) {
1649
- return [
1650
- {
1651
- slug: "dev/users",
1652
- name: "CMS Users",
1653
- icon: "face",
1654
- view: /* @__PURE__ */ jsx(UsersView, { userManagement, apiUrl, getAuthToken })
1655
- },
1656
- {
1657
- slug: "dev/roles",
1658
- name: "Roles",
1659
- icon: "gpp_good",
1660
- view: /* @__PURE__ */ jsx(RolesView, { userManagement, collections })
1661
- }
1662
- ];
1663
- }
1664
- function RoleChip({ role }) {
1665
- let colorScheme;
1666
- if (role.isAdmin) {
1667
- colorScheme = "blue";
1668
- } else if (role.id === "editor") {
1669
- colorScheme = "yellow";
1670
- } else if (role.id === "viewer") {
1671
- colorScheme = "gray";
1672
- } else {
1673
- colorScheme = getColorSchemeForSeed(role.id);
1674
- }
1675
- return /* @__PURE__ */ jsx(Chip, { colorScheme, children: role.name }, role.id);
1676
- }
1677
- function UsersView({ userManagement, apiUrl, getAuthToken }) {
1678
- const { users, roles, saveUser, deleteUser, loading } = userManagement;
1679
- const usersError = "usersError" in userManagement ? userManagement.usersError : void 0;
1680
- const snackbarController = useSnackbarController();
1681
- const { user: loggedInUser } = useAuthController();
1682
- const [dialogOpen, setDialogOpen] = useState(false);
1683
- const [selectedUser, setSelectedUser] = useState();
1684
- const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false);
1685
- const [userToDelete, setUserToDelete] = useState();
1686
- const [deleteInProgress, setDeleteInProgress] = useState(false);
1687
- const [formKey, setFormKey] = useState(0);
1688
- const [bootstrapping, setBootstrapping] = useState(false);
1689
- const hasAdmin = users.some((u) => u.roles?.includes("admin"));
1690
- const handleBootstrap = async () => {
1691
- setBootstrapping(true);
1692
- try {
1693
- const token = await getAuthToken();
1694
- const response = await fetch(`${apiUrl}/api/admin/bootstrap`, {
1695
- method: "POST",
1696
- headers: {
1697
- "Content-Type": "application/json",
1698
- "Authorization": `Bearer ${token}`
1699
- }
1700
- });
1701
- const data = await response.json();
1702
- if (!response.ok) {
1703
- throw new Error(data.error?.message || "Bootstrap failed");
1704
- }
1705
- snackbarController.open({
1706
- type: "success",
1707
- message: "You are now an admin! Refreshing..."
1708
- });
1709
- window.location.reload();
1710
- } catch (error) {
1711
- snackbarController.open({
1712
- type: "error",
1713
- message: error instanceof Error ? error.message : "Failed to bootstrap admin"
1714
- });
1715
- } finally {
1716
- setBootstrapping(false);
1717
- }
1718
- };
1719
- const handleAddUser = () => {
1720
- setSelectedUser(void 0);
1721
- setFormKey((k) => k + 1);
1722
- setDialogOpen(true);
1723
- };
1724
- const handleEditUser = (user) => {
1725
- setSelectedUser(user);
1726
- setDialogOpen(true);
1727
- };
1728
- const handleClose = () => {
1729
- setDialogOpen(false);
1730
- setSelectedUser(void 0);
1731
- };
1732
- const handleDelete = async () => {
1733
- if (!userToDelete) return;
1734
- setDeleteInProgress(true);
1735
- try {
1736
- await deleteUser(userToDelete);
1737
- snackbarController.open({
1738
- type: "success",
1739
- message: "User deleted successfully"
1740
- });
1741
- setDeleteConfirmOpen(false);
1742
- setUserToDelete(void 0);
1743
- } catch (error) {
1744
- snackbarController.open({
1745
- type: "error",
1746
- message: error instanceof Error ? error.message : "Error deleting user"
1747
- });
1748
- } finally {
1749
- setDeleteInProgress(false);
1750
- }
1751
- };
1752
- if (loading) {
1753
- return /* @__PURE__ */ jsx(CenteredView, { children: /* @__PURE__ */ jsx(CircularProgress, {}) });
1754
- }
1755
- return /* @__PURE__ */ jsxs(Container, { className: "w-full flex flex-col py-4 gap-4", maxWidth: "6xl", children: [
1756
- !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: [
1757
- /* @__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." }) }),
1758
- /* @__PURE__ */ jsx(
1759
- Button,
1760
- {
1761
- onClick: handleBootstrap,
1762
- disabled: bootstrapping,
1763
- children: bootstrapping ? /* @__PURE__ */ jsx(CircularProgress, { size: "small" }) : "Make me admin"
1764
- }
1765
- )
1766
- ] }),
1767
- /* @__PURE__ */ jsxs("div", { className: "flex items-center mt-12", children: [
1768
- /* @__PURE__ */ jsx(Typography, { gutterBottom: true, variant: "h4", className: "grow", component: "h4", children: "Users" }),
1769
- /* @__PURE__ */ jsx(Button, { startIcon: /* @__PURE__ */ jsx(PlusIcon, {}), onClick: handleAddUser, children: "Add user" })
1770
- ] }),
1771
- /* @__PURE__ */ jsx("div", { className: "overflow-auto", children: /* @__PURE__ */ jsxs(Table, { className: "w-full", children: [
1772
- /* @__PURE__ */ jsxs(TableHeader, { children: [
1773
- /* @__PURE__ */ jsx(TableCell, { header: true, className: "truncate w-16" }),
1774
- /* @__PURE__ */ jsx(TableCell, { header: true, children: "Email" }),
1775
- /* @__PURE__ */ jsx(TableCell, { header: true, children: "Name" }),
1776
- /* @__PURE__ */ jsx(TableCell, { header: true, children: "Roles" })
1777
- ] }),
1778
- /* @__PURE__ */ jsxs(TableBody, { children: [
1779
- users.map((user) => /* @__PURE__ */ jsxs(TableRow, { onClick: () => handleEditUser(user), children: [
1780
- /* @__PURE__ */ jsx(TableCell, { style: { width: "64px" }, children: /* @__PURE__ */ jsx(Tooltip, { asChild: true, title: "Delete this user", children: /* @__PURE__ */ jsx(
1781
- IconButton,
1782
- {
1783
- size: "small",
1784
- onClick: (e) => {
1785
- e.stopPropagation();
1786
- setUserToDelete(user);
1787
- setDeleteConfirmOpen(true);
1788
- },
1789
- children: /* @__PURE__ */ jsx(Trash2Icon, {})
1790
- }
1791
- ) }) }),
1792
- /* @__PURE__ */ jsx(TableCell, { children: user.email }),
1793
- /* @__PURE__ */ jsx(TableCell, { className: "font-medium", children: user.displayName }),
1794
- /* @__PURE__ */ jsx(TableCell, { children: /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-2", children: user.roles?.map((roleId) => {
1795
- const role = roles.find((r) => r.id === roleId);
1796
- return role ? /* @__PURE__ */ jsx(RoleChip, { role }, roleId) : /* @__PURE__ */ jsx("span", { children: roleId }, roleId);
1797
- }) }) })
1798
- ] }, user.uid)),
1799
- users.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: [
1800
- /* @__PURE__ */ jsx(Typography, { variant: "label", children: usersError ? "You don't have permission to view users" : "There are no users yet" }),
1801
- usersError && /* @__PURE__ */ jsx(Typography, { variant: "caption", className: "text-surface-500", children: "Contact an administrator if you need access to this section." })
1802
- ] }) }) })
1803
- ] })
1804
- ] }) }),
1805
- /* @__PURE__ */ jsx(
1806
- UserDetailsForm,
1807
- {
1808
- open: dialogOpen,
1809
- user: selectedUser,
1810
- roles,
1811
- saveUser,
1812
- handleClose
1813
- },
1814
- selectedUser?.uid ?? `new-${formKey}`
1815
- ),
1816
- /* @__PURE__ */ jsx(
1817
- ConfirmationDialog,
1818
- {
1819
- open: deleteConfirmOpen,
1820
- loading: deleteInProgress,
1821
- onAccept: handleDelete,
1822
- onCancel: () => {
1823
- setDeleteConfirmOpen(false);
1824
- setUserToDelete(void 0);
1825
- },
1826
- title: /* @__PURE__ */ jsx(Fragment, { children: "Delete?" }),
1827
- body: /* @__PURE__ */ jsx(Fragment, { children: "Are you sure you want to delete this user?" })
1828
- }
1829
- )
1830
- ] });
1831
- }
1832
- function UserDetailsForm({
1833
- open,
1834
- user: userProp,
1835
- roles,
1836
- saveUser,
1837
- handleClose
1838
- }) {
1839
- const snackbarController = useSnackbarController();
1840
- const isNewUser = !userProp;
1841
- const [displayName, setDisplayName] = useState(userProp?.displayName || "");
1842
- const [email, setEmail] = useState(userProp?.email || "");
1843
- const [selectedRoleIds, setSelectedRoleIds] = useState(
1844
- userProp?.roles || ["editor"]
1845
- );
1846
- const [isSubmitting, setIsSubmitting] = useState(false);
1847
- const [errors, setErrors] = useState({});
1848
- const [submitCount, setSubmitCount] = useState(0);
1849
- const validate = () => {
1850
- const newErrors = {};
1851
- if (!displayName) newErrors.displayName = "Required";
1852
- if (!email) newErrors.email = "Required";
1853
- else if (!/\S+@\S+\.\S+/.test(email)) newErrors.email = "Invalid email";
1854
- if (selectedRoleIds.length === 0) newErrors.roles = "At least one role is required";
1855
- setErrors(newErrors);
1856
- return Object.keys(newErrors).length === 0;
1857
- };
1858
- const handleSubmit = async (e) => {
1859
- e.preventDefault();
1860
- setSubmitCount((c) => c + 1);
1861
- if (!validate()) return;
1862
- setIsSubmitting(true);
1863
- try {
1864
- const userToSave = {
1865
- uid: userProp?.uid || crypto.randomUUID(),
1866
- email,
1867
- displayName: displayName || null,
1868
- photoURL: userProp?.photoURL || null,
1869
- providerId: "custom",
1870
- isAnonymous: false,
1871
- roles: selectedRoleIds
1872
- };
1873
- await saveUser(userToSave);
1874
- handleClose();
1875
- } catch (error) {
1876
- snackbarController.open({
1877
- type: "error",
1878
- message: error instanceof Error ? error.message : "Failed to save user"
1879
- });
1880
- } finally {
1881
- setIsSubmitting(false);
1882
- }
1883
- };
1884
- const dirty = isNewUser || displayName !== (userProp?.displayName || "") || email !== (userProp?.email || "") || JSON.stringify(selectedRoleIds.sort()) !== JSON.stringify((userProp?.roles || []).sort());
1885
- return /* @__PURE__ */ jsx(Dialog, { open, onOpenChange: (open2) => !open2 ? handleClose() : void 0, maxWidth: "4xl", children: /* @__PURE__ */ jsxs(
1886
- "form",
1887
- {
1888
- onSubmit: handleSubmit,
1889
- autoComplete: "off",
1890
- noValidate: true,
1891
- style: {
1892
- display: "flex",
1893
- flexDirection: "column",
1894
- position: "relative",
1895
- height: "100%"
1896
- },
1897
- children: [
1898
- /* @__PURE__ */ jsx(DialogTitle, { variant: "h4", gutterBottom: false, children: "User" }),
1899
- /* @__PURE__ */ jsx(DialogContent, { className: "h-full grow", children: /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-12 gap-4", children: [
1900
- /* @__PURE__ */ jsxs("div", { className: "col-span-12", children: [
1901
- /* @__PURE__ */ jsx(
1902
- TextField,
1903
- {
1904
- name: "displayName",
1905
- required: true,
1906
- error: submitCount > 0 && Boolean(errors.displayName),
1907
- value: displayName,
1908
- onChange: (e) => setDisplayName(e.target.value),
1909
- label: "Name"
1910
- }
1911
- ),
1912
- /* @__PURE__ */ jsx(FieldCaption, { children: submitCount > 0 && errors.displayName ? errors.displayName : "Name of this user" })
1913
- ] }),
1914
- /* @__PURE__ */ jsxs("div", { className: "col-span-12", children: [
1915
- /* @__PURE__ */ jsx(
1916
- TextField,
1917
- {
1918
- required: true,
1919
- error: submitCount > 0 && Boolean(errors.email),
1920
- name: "email",
1921
- value: email,
1922
- onChange: (e) => setEmail(e.target.value),
1923
- label: "Email",
1924
- disabled: !isNewUser
1925
- }
1926
- ),
1927
- /* @__PURE__ */ jsx(FieldCaption, { children: submitCount > 0 && errors.email ? errors.email : "Email of this user" })
1928
- ] }),
1929
- /* @__PURE__ */ jsx("div", { className: "col-span-12", children: /* @__PURE__ */ jsx(
1930
- MultiSelect,
1931
- {
1932
- className: "w-full",
1933
- label: "Roles",
1934
- value: selectedRoleIds,
1935
- onValueChange: (value) => setSelectedRoleIds(value),
1936
- children: roles.map((role) => /* @__PURE__ */ jsx(MultiSelectItem, { value: role.id, children: /* @__PURE__ */ jsx(RoleChip, { role }) }, role.id))
1937
- }
1938
- ) })
1939
- ] }) }),
1940
- /* @__PURE__ */ jsxs(DialogActions, { children: [
1941
- /* @__PURE__ */ jsx(Button, { variant: "text", onClick: handleClose, children: "Cancel" }),
1942
- /* @__PURE__ */ jsx(
1943
- LoadingButton,
1944
- {
1945
- variant: "filled",
1946
- type: "submit",
1947
- disabled: !dirty,
1948
- loading: isSubmitting,
1949
- children: isNewUser ? "Create user" : "Update"
1950
- }
1951
- )
1952
- ] })
1953
- ]
1954
- }
1955
- ) });
1956
- }
1957
- function RolesView({ userManagement, collections = [] }) {
1958
- const { roles, saveRole, deleteRole, loading, allowDefaultRolesCreation } = userManagement;
1959
- const rolesError = "rolesError" in userManagement ? userManagement.rolesError : void 0;
1960
- const snackbarController = useSnackbarController();
1961
- const [dialogOpen, setDialogOpen] = useState(false);
1962
- const [selectedRole, setSelectedRole] = useState();
1963
- const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false);
1964
- const [roleToDelete, setRoleToDelete] = useState();
1965
- const [deleteInProgress, setDeleteInProgress] = useState(false);
1966
- const handleAddRole = () => {
1967
- setSelectedRole(void 0);
1968
- setDialogOpen(true);
1969
- };
1970
- const handleEditRole = (role) => {
1971
- setSelectedRole(role);
1972
- setDialogOpen(true);
1973
- };
1974
- const handleClose = () => {
1975
- setDialogOpen(false);
1976
- setSelectedRole(void 0);
1977
- };
1978
- const handleDelete = async () => {
1979
- if (!roleToDelete) return;
1980
- setDeleteInProgress(true);
1981
- try {
1982
- await deleteRole(roleToDelete);
1983
- snackbarController.open({
1984
- type: "success",
1985
- message: "Role deleted successfully"
1986
- });
1987
- setDeleteConfirmOpen(false);
1988
- setRoleToDelete(void 0);
1989
- } catch (error) {
1990
- snackbarController.open({
1991
- type: "error",
1992
- message: error instanceof Error ? error.message : "Error deleting role"
1993
- });
1994
- } finally {
1995
- setDeleteInProgress(false);
1996
- }
1997
- };
1998
- const createDefaultRoles = () => {
1999
- const defaultRoles = [
2000
- {
2001
- id: "admin",
2002
- name: "Admin",
2003
- isAdmin: true
2004
- },
2005
- {
2006
- id: "editor",
2007
- name: "Editor",
2008
- isAdmin: false
2009
- },
2010
- {
2011
- id: "viewer",
2012
- name: "Viewer",
2013
- isAdmin: false
2014
- }
2015
- ];
2016
- defaultRoles.forEach((role) => saveRole(role));
2017
- };
2018
- if (loading) {
2019
- return /* @__PURE__ */ jsx(CenteredView, { children: /* @__PURE__ */ jsx(CircularProgress, {}) });
2020
- }
2021
- return /* @__PURE__ */ jsxs(Container, { className: "w-full flex flex-col py-4 gap-4", maxWidth: "6xl", children: [
2022
- /* @__PURE__ */ jsxs("div", { className: "flex items-center mt-12", children: [
2023
- /* @__PURE__ */ jsx(Typography, { gutterBottom: true, variant: "h4", className: "grow", component: "h4", children: "Roles" }),
2024
- /* @__PURE__ */ jsx(Button, { startIcon: /* @__PURE__ */ jsx(PlusIcon, {}), onClick: handleAddRole, children: "Add role" })
2025
- ] }),
2026
- /* @__PURE__ */ jsx("div", { className: "w-full overflow-auto", children: /* @__PURE__ */ jsxs(Table, { className: "w-full", children: [
2027
- /* @__PURE__ */ jsxs(TableHeader, { children: [
2028
- /* @__PURE__ */ jsx(TableCell, { header: true, className: "w-16" }),
2029
- /* @__PURE__ */ jsx(TableCell, { header: true, children: "Role" }),
2030
- /* @__PURE__ */ jsx(TableCell, { header: true, className: "items-center", children: "Is Admin" })
2031
- ] }),
2032
- /* @__PURE__ */ jsxs(TableBody, { children: [
2033
- roles.map((role) => {
2034
- return /* @__PURE__ */ jsxs(TableRow, { onClick: () => handleEditRole(role), children: [
2035
- /* @__PURE__ */ jsx(TableCell, { style: { width: "64px" }, children: !role.isAdmin && /* @__PURE__ */ jsx(Tooltip, { asChild: true, title: "Delete this role", children: /* @__PURE__ */ jsx(
2036
- IconButton,
2037
- {
2038
- size: "small",
2039
- onClick: (e) => {
2040
- e.stopPropagation();
2041
- setRoleToDelete(role);
2042
- setDeleteConfirmOpen(true);
2043
- },
2044
- children: /* @__PURE__ */ jsx(Trash2Icon, {})
2045
- }
2046
- ) }) }),
2047
- /* @__PURE__ */ jsx(TableCell, { children: /* @__PURE__ */ jsx(RoleChip, { role }) }),
2048
- /* @__PURE__ */ jsx(TableCell, { className: "items-center", children: /* @__PURE__ */ jsx(Checkbox, { checked: role.isAdmin ?? false }) })
2049
- ] }, role.id);
2050
- }),
2051
- 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: [
2052
- /* @__PURE__ */ jsx(Typography, { variant: "label", children: rolesError ? "You don't have permission to view roles" : "You don't have any roles yet." }),
2053
- rolesError && /* @__PURE__ */ jsx(Typography, { variant: "caption", className: "text-surface-500", children: "Contact an administrator if you need access to this section." }),
2054
- !rolesError && allowDefaultRolesCreation && /* @__PURE__ */ jsx(Button, { onClick: createDefaultRoles, children: "Create default roles" })
2055
- ] }) }) })
2056
- ] })
2057
- ] }) }),
2058
- /* @__PURE__ */ jsx(
2059
- RoleDetailsForm,
2060
- {
2061
- open: dialogOpen,
2062
- role: selectedRole,
2063
- saveRole,
2064
- handleClose,
2065
- collections
2066
- },
2067
- selectedRole?.id ?? "new"
2068
- ),
2069
- /* @__PURE__ */ jsx(
2070
- ConfirmationDialog,
2071
- {
2072
- open: deleteConfirmOpen,
2073
- loading: deleteInProgress,
2074
- onAccept: handleDelete,
2075
- onCancel: () => {
2076
- setDeleteConfirmOpen(false);
2077
- setRoleToDelete(void 0);
2078
- },
2079
- title: /* @__PURE__ */ jsx(Fragment, { children: "Delete?" }),
2080
- body: /* @__PURE__ */ jsx(Fragment, { children: "Are you sure you want to delete this role?" })
2081
- }
2082
- )
2083
- ] });
2084
- }
2085
- function RoleDetailsForm({
2086
- open,
2087
- role: roleProp,
2088
- saveRole,
2089
- handleClose,
2090
- collections = []
2091
- }) {
2092
- const snackbarController = useSnackbarController();
2093
- const isNewRole = !roleProp;
2094
- const [roleId, setRoleId] = useState(roleProp?.id || "");
2095
- const [roleName, setRoleName] = useState(roleProp?.name || "");
2096
- const [isAdmin, setIsAdmin] = useState(roleProp?.isAdmin ?? false);
2097
- const [isSubmitting, setIsSubmitting] = useState(false);
2098
- const [errors, setErrors] = useState({});
2099
- const [submitCount, setSubmitCount] = useState(0);
2100
- const validate = () => {
2101
- const newErrors = {};
2102
- if (!roleId) newErrors.id = "Required";
2103
- if (!roleName) newErrors.name = "Required";
2104
- setErrors(newErrors);
2105
- return Object.keys(newErrors).length === 0;
2106
- };
2107
- const handleSubmit = async (e) => {
2108
- e.preventDefault();
2109
- setSubmitCount((c) => c + 1);
2110
- if (!validate()) return;
2111
- setIsSubmitting(true);
2112
- try {
2113
- await saveRole({
2114
- id: roleId,
2115
- name: roleName,
2116
- isAdmin
2117
- });
2118
- handleClose();
2119
- } catch (error) {
2120
- snackbarController.open({
2121
- type: "error",
2122
- message: error instanceof Error ? error.message : "Failed to save role"
2123
- });
2124
- } finally {
2125
- setIsSubmitting(false);
2126
- }
2127
- };
2128
- return /* @__PURE__ */ jsx(Dialog, { open, onOpenChange: (open2) => !open2 ? handleClose() : void 0, maxWidth: "6xl", children: /* @__PURE__ */ jsxs(
2129
- "form",
2130
- {
2131
- onSubmit: handleSubmit,
2132
- autoComplete: "off",
2133
- noValidate: true,
2134
- style: {
2135
- display: "flex",
2136
- flexDirection: "column",
2137
- position: "relative",
2138
- height: "100%"
2139
- },
2140
- children: [
2141
- /* @__PURE__ */ jsx(DialogTitle, { variant: "h4", gutterBottom: false, children: "Role" }),
2142
- /* @__PURE__ */ jsx(DialogContent, { className: "h-full grow overflow-y-auto", children: /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-12 gap-4", children: [
2143
- /* @__PURE__ */ jsxs("div", { className: "col-span-12 sm:col-span-4", children: [
2144
- /* @__PURE__ */ jsx(
2145
- TextField,
2146
- {
2147
- name: "id",
2148
- required: true,
2149
- error: submitCount > 0 && Boolean(errors.id),
2150
- value: roleId,
2151
- onChange: (e) => setRoleId(e.target.value),
2152
- label: "Role ID",
2153
- disabled: !isNewRole
2154
- }
2155
- ),
2156
- /* @__PURE__ */ jsx(FieldCaption, { children: submitCount > 0 && errors.id ? errors.id : "Unique identifier for this role" })
2157
- ] }),
2158
- /* @__PURE__ */ jsxs("div", { className: "col-span-12 sm:col-span-4", children: [
2159
- /* @__PURE__ */ jsx(
2160
- TextField,
2161
- {
2162
- name: "name",
2163
- required: true,
2164
- error: submitCount > 0 && Boolean(errors.name),
2165
- value: roleName,
2166
- onChange: (e) => setRoleName(e.target.value),
2167
- label: "Role Name"
2168
- }
2169
- ),
2170
- /* @__PURE__ */ jsx(FieldCaption, { children: submitCount > 0 && errors.name ? errors.name : "Display name for this role" })
2171
- ] }),
2172
- /* @__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: [
2173
- /* @__PURE__ */ jsx(
2174
- Checkbox,
2175
- {
2176
- checked: isAdmin,
2177
- onCheckedChange: (checked) => setIsAdmin(Boolean(checked))
2178
- }
2179
- ),
2180
- /* @__PURE__ */ jsx("span", { className: "font-medium", children: "Is Admin" })
2181
- ] }) }),
2182
- /* @__PURE__ */ jsx("div", { className: "col-span-12", children: /* @__PURE__ */ jsx(CollectionPermissionsMatrix, { roleId, isAdmin, collections }) })
2183
- ] }) }),
2184
- /* @__PURE__ */ jsxs(DialogActions, { children: [
2185
- /* @__PURE__ */ jsx(Button, { variant: "text", onClick: handleClose, children: "Cancel" }),
2186
- /* @__PURE__ */ jsx(
2187
- LoadingButton,
2188
- {
2189
- variant: "filled",
2190
- type: "submit",
2191
- loading: isSubmitting,
2192
- children: isNewRole ? "Create role" : "Update"
2193
- }
2194
- )
2195
- ] })
2196
- ]
2197
- }
2198
- ) });
2199
- }
2200
- const CRUD_OPS = [
2201
- {
2202
- op: "select",
2203
- label: "Read"
2204
- },
2205
- {
2206
- op: "insert",
2207
- label: "Create"
2208
- },
2209
- {
2210
- op: "update",
2211
- label: "Edit"
2212
- },
2213
- {
2214
- op: "delete",
2215
- label: "Delete"
2216
- }
2217
- ];
2218
- function hasRoleAccess(rules, roleId, op) {
2219
- if (!rules || rules.length === 0) return true;
2220
- const applicable = rules.filter(
2221
- (r) => r.operation === op || r.operation === "all" || r.operations?.includes(op) || r.operations?.includes("all")
2222
- );
2223
- if (applicable.length === 0) return false;
2224
- const forRole = applicable.filter(
2225
- (r) => !r.roles || r.roles.length === 0 || r.roles.includes(roleId) || r.roles.includes("public")
2226
- );
2227
- if (forRole.length === 0) return false;
2228
- for (const r of forRole) {
2229
- if ((r.mode ?? "permissive") === "restrictive") return false;
2230
- }
2231
- return forRole.some((r) => (r.mode ?? "permissive") === "permissive");
2232
- }
2233
- function PermCell({ granted }) {
2234
- return /* @__PURE__ */ jsx(
2235
- "span",
2236
- {
2237
- className: granted ? "text-green-500 dark:text-green-400 text-base select-none" : "text-surface-300 dark:text-surface-600 text-base select-none",
2238
- children: granted ? "✓" : "✗"
2239
- }
2240
- );
2241
- }
2242
- function CollectionPermissionsMatrix({ roleId, isAdmin, collections }) {
2243
- if (!collections || collections.length === 0) {
2244
- return /* @__PURE__ */ jsx("div", { className: "mt-4", children: /* @__PURE__ */ jsx(Typography, { variant: "label", className: "text-surface-400", children: "No collections configured" }) });
2245
- }
2246
- const topLevel = collections;
2247
- return /* @__PURE__ */ jsxs("div", { className: "mt-6", children: [
2248
- /* @__PURE__ */ jsx(Typography, { variant: "label", className: "mb-2 block text-surface-600 dark:text-surface-400 uppercase tracking-wide text-xs", children: "Collection permissions" }),
2249
- /* @__PURE__ */ jsx("div", { className: "rounded-lg border border-surface-200 dark:border-surface-700 overflow-hidden", children: /* @__PURE__ */ jsxs(Table, { children: [
2250
- /* @__PURE__ */ jsx(TableHeader, { children: /* @__PURE__ */ jsxs(TableRow, { children: [
2251
- /* @__PURE__ */ jsx(TableCell, { header: true, children: "Collection" }),
2252
- CRUD_OPS.map(({ op, label }) => /* @__PURE__ */ jsx(TableCell, { header: true, className: "text-center w-24", children: label }, op))
2253
- ] }) }),
2254
- /* @__PURE__ */ jsx(TableBody, { children: topLevel.map((collection) => {
2255
- const extCol = collection;
2256
- const noRules = !extCol.securityRules || extCol.securityRules.length === 0;
2257
- return /* @__PURE__ */ jsxs(TableRow, { children: [
2258
- /* @__PURE__ */ jsxs(TableCell, { children: [
2259
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
2260
- /* @__PURE__ */ jsx("span", { className: "font-medium", children: collection.name }),
2261
- noRules && !isAdmin && /* @__PURE__ */ jsx(Tooltip, { title: "No security rules — unrestricted", children: /* @__PURE__ */ jsx(Chip, { className: "text-xs", colorScheme: "yellow", children: "No rules" }) })
2262
- ] }),
2263
- /* @__PURE__ */ jsx("span", { className: "text-xs text-surface-400 font-mono", children: collection.slug })
2264
- ] }),
2265
- CRUD_OPS.map(({ op }) => /* @__PURE__ */ jsx(TableCell, { className: "text-center", children: /* @__PURE__ */ jsx(PermCell, { granted: isAdmin || hasRoleAccess(extCol.securityRules, roleId, op) }) }, op))
2266
- ] }, collection.slug);
2267
- }) })
2268
- ] }) }),
2269
- !roleId && /* @__PURE__ */ jsx(Typography, { variant: "caption", className: "mt-2 text-surface-400 italic", children: "Enter a role ID above to preview permissions" })
2270
- ] });
2271
- }
2272
- function FieldCaption({ children }) {
2273
- return /* @__PURE__ */ jsx(Typography, { variant: "caption", className: "text-surface-500 mt-1.5 ml-1 block", children });
2274
- }
2275
1094
  export {
2276
1095
  AuthApiError,
2277
- RebaseAuth,
2278
- RebaseLoginView,
2279
- RolesView,
2280
- UsersView,
2281
- createUserManagementAdminViews,
1096
+ clearAuthConfigCache,
2282
1097
  fetchAuthConfig,
2283
1098
  getApiUrl,
2284
1099
  setApiUrl,