@firecms/user_management 3.0.0-canary.16 → 3.0.0-canary.160

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.
@@ -1,5 +1,6 @@
1
- import { PermissionsBuilder, Role, User } from "@firecms/core";
1
+ import { Authenticator, PermissionsBuilder, Role, User } from "@firecms/core";
2
2
  export type UserManagement<USER extends User = User> = {
3
+ authenticator?: Authenticator<USER>;
3
4
  loading: boolean;
4
5
  users: USER[];
5
6
  saveUser: (user: USER) => Promise<USER>;
@@ -15,6 +16,10 @@ export type UserManagement<USER extends User = User> = {
15
16
  * Can the logged user edit roles?
16
17
  */
17
18
  canEditRoles?: boolean;
19
+ /**
20
+ * Is the logged user Admin?
21
+ */
22
+ isAdmin?: boolean;
18
23
  /**
19
24
  * Include a button to create default roles, in case there are no roles in the system.
20
25
  */
@@ -29,8 +34,10 @@ export type UserManagement<USER extends User = User> = {
29
34
  */
30
35
  collectionPermissions: PermissionsBuilder;
31
36
  /**
32
- * Define the roles for a given user.
37
+ * Define the roles for a given user. You will typically want to plug this into your auth controller.
33
38
  * @param user
34
39
  */
35
- defineRolesFor: (user: User) => Promise<Role[]> | Role[] | undefined;
40
+ defineRolesFor: (user: User) => Promise<Role[] | undefined> | Role[] | undefined;
41
+ rolesError?: Error;
42
+ usersError?: Error;
36
43
  };
@@ -1,5 +1,10 @@
1
- import { FireCMSPlugin } from "@firecms/core";
1
+ import { FireCMSPlugin, User } from "@firecms/core";
2
2
  import { UserManagement } from "./types";
3
- export declare function useUserManagementPlugin({ userManagement }: {
4
- userManagement: UserManagement;
3
+ export declare function useUserManagementPlugin<USER extends User = any>({ userManagement }: {
4
+ userManagement: UserManagement<USER>;
5
5
  }): FireCMSPlugin;
6
+ export declare function IntroWidget({ noUsers, noRoles, userManagement }: {
7
+ noUsers: boolean;
8
+ noRoles: boolean;
9
+ userManagement: UserManagement<any>;
10
+ }): import("react/jsx-runtime").JSX.Element;
@@ -1,8 +1,8 @@
1
1
  import { EntityCollection, Permissions, Role, User } from "@firecms/core";
2
2
  export declare const RESERVED_GROUPS: string[];
3
- export declare function resolveUserRolePermissions<UserType extends User>({ collection, user }: {
3
+ export declare function resolveUserRolePermissions<USER extends User>({ collection, user }: {
4
4
  collection: EntityCollection<any>;
5
- user: UserType | null;
5
+ user: USER | null;
6
6
  }): Permissions;
7
7
  export declare function getUserRoles(roles: Role[], fireCMSUser: User): Role[] | undefined;
8
8
  export declare const areRolesEqual: (rolesA: Role[], rolesB: Role[]) => boolean;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@firecms/user_management",
3
3
  "type": "module",
4
- "version": "3.0.0-canary.16",
4
+ "version": "3.0.0-canary.160",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -24,36 +24,28 @@
24
24
  },
25
25
  "./package.json": "./package.json"
26
26
  },
27
- "packageManager": "yarn@4.1.0",
28
27
  "main": "./dist/index.umd.js",
29
28
  "module": "./dist/index.es.js",
30
29
  "types": "dist/index.d.ts",
31
30
  "source": "src/index.ts",
32
31
  "dependencies": {
33
- "@firecms/core": "^3.0.0-canary.16",
34
- "@firecms/formex": "^3.0.0-canary.16",
35
- "@firecms/ui": "^3.0.0-canary.16",
32
+ "@firecms/core": "^3.0.0-canary.160",
33
+ "@firecms/formex": "^3.0.0-canary.160",
34
+ "@firecms/ui": "^3.0.0-canary.159",
36
35
  "date-fns": "^3.6.0"
37
36
  },
38
37
  "peerDependencies": {
39
- "firebase": "^10.7.1",
40
- "react": "^18.2.0",
41
- "react-dom": "^18.2.0"
38
+ "react": "^18.3.1",
39
+ "react-dom": "^18.3.1"
42
40
  },
43
41
  "devDependencies": {
44
- "@types/node": "^20.11.30",
45
- "@types/react": "^18.2.67",
46
- "@types/react-dom": "^18.2.22",
47
- "@typescript-eslint/parser": "^7.3.1",
48
- "eslint": "^8.57.0",
49
- "eslint-config-standard": "^17.1.0",
50
- "eslint-plugin-import": "^2.29.1",
51
- "eslint-plugin-n": "^16.6.2",
52
- "eslint-plugin-promise": "^6.1.1",
53
- "eslint-plugin-react": "^7.34.1",
54
- "eslint-plugin-react-hooks": "^4.6.0",
55
- "typescript": "^5.4.2",
56
- "vite": "^5.2.3"
42
+ "@types/node": "^20.17.9",
43
+ "@types/react": "^18.3.11",
44
+ "@types/react-dom": "^18.3.1",
45
+ "babel-plugin-react-compiler": "beta",
46
+ "eslint-plugin-react-compiler": "beta",
47
+ "typescript": "^5.7.2",
48
+ "vite": "^5.4.11"
57
49
  },
58
50
  "scripts": {
59
51
  "dev": "vite",
@@ -65,11 +57,5 @@
65
57
  "src",
66
58
  "bin"
67
59
  ],
68
- "eslintConfig": {
69
- "extends": [
70
- "react-app",
71
- "react-app/jest"
72
- ]
73
- },
74
- "gitHead": "6c7d1dd64fa4b9f31433ab6dd938835589e8a25a"
60
+ "gitHead": "840f2733672a90a6a56aed89259eaa5365589512"
75
61
  }
@@ -1,13 +1,13 @@
1
1
  import React, { useCallback, useState } from "react";
2
2
  import * as Yup from "yup";
3
3
 
4
- import { EntityCollection, FieldCaption, Role, toSnakeCase, } from "@firecms/core";
4
+ import { EntityCollection, FieldCaption, Role, toSnakeCase, useAuthController, User, } from "@firecms/core";
5
5
  import {
6
6
  Button,
7
7
  Checkbox,
8
8
  Dialog,
9
9
  DialogActions,
10
- DialogContent,
10
+ DialogContent, DialogTitle,
11
11
  DoneIcon,
12
12
  LoadingButton,
13
13
  Paper,
@@ -30,6 +30,16 @@ export const RoleYupSchema = Yup.object().shape({
30
30
  name: Yup.string().required("Required")
31
31
  });
32
32
 
33
+ function canRoleBeEdited(loggedUser: User) {
34
+ const loggedUserIsAdmin = loggedUser.roles?.map(r => r.id).includes("admin");
35
+ console.log("loggedUserIsAdmin", loggedUser);
36
+ if (!loggedUserIsAdmin) {
37
+ throw new Error("Only admins can edit roles");
38
+ }
39
+
40
+ return true;
41
+ }
42
+
33
43
  export function RolesDetailsForm({
34
44
  open,
35
45
  role,
@@ -46,27 +56,39 @@ export function RolesDetailsForm({
46
56
 
47
57
  const { saveRole } = useUserManagement();
48
58
  const isNewRole = !role;
59
+ const {
60
+ user: loggedInUser
61
+ } = useAuthController();
49
62
 
50
63
  const [savingError, setSavingError] = useState<Error | undefined>();
51
64
 
52
65
  const onRoleUpdated = useCallback((role: Role) => {
53
66
  setSavingError(undefined);
67
+ if (!loggedInUser) throw new Error("User not found");
68
+ canRoleBeEdited(loggedInUser);
54
69
  return saveRole(role);
55
- }, [saveRole]);
70
+ }, [saveRole, loggedInUser]);
56
71
 
57
72
  const formex = useCreateFormex({
58
73
  initialValues: role ?? {
59
74
  name: ""
60
75
  } as Role,
61
76
  onSubmit: (role: Role, formexController) => {
62
- return onRoleUpdated(role)
63
- .then(() => {
64
- formexController.resetForm({
65
- values: role
77
+ try {
78
+ return onRoleUpdated(role)
79
+ .then(() => {
80
+ formexController.resetForm({
81
+ values: role
82
+ });
83
+ handleClose();
84
+ })
85
+ .catch(e => {
86
+ setSavingError(e);
66
87
  });
67
- handleClose();
68
- })
69
- .catch(e => setSavingError(e));
88
+ } catch (e: any) {
89
+ setSavingError(e);
90
+ return Promise.resolve();
91
+ }
70
92
  },
71
93
  validation: (values) => {
72
94
  return RoleYupSchema.validate(values, { abortEarly: false })
@@ -121,14 +143,10 @@ export function RolesDetailsForm({
121
143
  position: "relative",
122
144
  height: "100%"
123
145
  }}>
146
+ <DialogTitle variant={"h4"} gutterBottom={false}>
147
+ Role
148
+ </DialogTitle>
124
149
  <DialogContent className="flex-grow">
125
- <div
126
- className="flex flex-row pt-12 pb-8">
127
- <Typography variant={"h4"}
128
- className="flex-grow">
129
- Role
130
- </Typography>
131
- </div>
132
150
 
133
151
  <div className={"grid grid-cols-12 gap-8"}>
134
152
 
@@ -168,11 +186,9 @@ export function RolesDetailsForm({
168
186
  </div>
169
187
 
170
188
  <div className={"col-span-12"}>
171
- <Paper
172
-
173
- className="bg-inherit">
174
- <Table>
175
- <TableHeader>
189
+ <Paper className="bg-inherit overflow-hidden">
190
+ <Table className={"w-full rounded-md"}>
191
+ <TableHeader className={"rounded-md"}>
176
192
  <TableCell></TableCell>
177
193
  <TableCell
178
194
  align="center">Create
@@ -190,6 +206,9 @@ export function RolesDetailsForm({
190
206
  align="center">Delete
191
207
  entities
192
208
  </TableCell>
209
+ <TableCell
210
+ align="center">
211
+ </TableCell>
193
212
  </TableHeader>
194
213
 
195
214
  <TableBody>
@@ -245,6 +264,9 @@ export function RolesDetailsForm({
245
264
 
246
265
  </Tooltip>
247
266
  </TableCell>
267
+ <TableCell
268
+ align="center">
269
+ </TableCell>
248
270
  </TableRow>
249
271
  {collections && collections.map((col) => (
250
272
  <TableRow key={col.name}>
@@ -256,29 +278,49 @@ export function RolesDetailsForm({
256
278
  align="center">
257
279
  <Checkbox
258
280
  disabled={isAdmin || defaultCreate || !editable}
259
- checked={(isAdmin || defaultCreate || getIn(values, `collectionPermissions.${col.path}.create`)) ?? false}
260
- onCheckedChange={(checked) => setFieldValue(`collectionPermissions.${col.path}.create`, checked)}/>
281
+ checked={(isAdmin || defaultCreate || getIn(values, `collectionPermissions.${col.id}.create`)) ?? false}
282
+ onCheckedChange={(checked) => setFieldValue(`collectionPermissions.${col.id}.create`, checked)}/>
261
283
  </TableCell>
262
284
  <TableCell
263
285
  align="center">
264
286
  <Checkbox
265
287
  disabled={isAdmin || defaultRead || !editable}
266
- checked={(isAdmin || defaultRead || getIn(values, `collectionPermissions.${col.path}.read`)) ?? false}
267
- onCheckedChange={(checked) => setFieldValue(`collectionPermissions.${col.path}.read`, checked)}/>
288
+ checked={(isAdmin || defaultRead || getIn(values, `collectionPermissions.${col.id}.read`)) ?? false}
289
+ onCheckedChange={(checked) => setFieldValue(`collectionPermissions.${col.id}.read`, checked)}/>
268
290
  </TableCell>
269
291
  <TableCell
270
292
  align="center">
271
293
  <Checkbox
272
294
  disabled={isAdmin || defaultEdit || !editable}
273
- checked={(isAdmin || defaultEdit || getIn(values, `collectionPermissions.${col.path}.edit`)) ?? false}
274
- onCheckedChange={(checked) => setFieldValue(`collectionPermissions.${col.path}.edit`, checked)}/>
295
+ checked={(isAdmin || defaultEdit || getIn(values, `collectionPermissions.${col.id}.edit`)) ?? false}
296
+ onCheckedChange={(checked) => setFieldValue(`collectionPermissions.${col.id}.edit`, checked)}/>
275
297
  </TableCell>
276
298
  <TableCell
277
299
  align="center">
278
300
  <Checkbox
279
301
  disabled={isAdmin || defaultDelete || !editable}
280
- checked={(isAdmin || defaultDelete || getIn(values, `collectionPermissions.${col.path}.delete`)) ?? false}
281
- onCheckedChange={(checked) => setFieldValue(`collectionPermissions.${col.path}.delete`, checked)}/>
302
+ checked={(isAdmin || defaultDelete || getIn(values, `collectionPermissions.${col.id}.delete`)) ?? false}
303
+ onCheckedChange={(checked) => setFieldValue(`collectionPermissions.${col.id}.delete`, checked)}/>
304
+ </TableCell>
305
+
306
+ <TableCell
307
+ align="center">
308
+ <Tooltip
309
+ title="Allow all permissions in this collections">
310
+ <Button
311
+ className={"color-inherit"}
312
+ onClick={() => {
313
+ setFieldValue(`collectionPermissions.${col.id}.create`, true);
314
+ setFieldValue(`collectionPermissions.${col.id}.read`, true);
315
+ setFieldValue(`collectionPermissions.${col.id}.edit`, true);
316
+ setFieldValue(`collectionPermissions.${col.id}.delete`, true);
317
+ }}
318
+ disabled={isAdmin || !editable}
319
+ variant={"text"}>
320
+ All
321
+ </Button>
322
+
323
+ </Tooltip>
282
324
  </TableCell>
283
325
  </TableRow>
284
326
  ))}
@@ -296,6 +338,8 @@ export function RolesDetailsForm({
296
338
  <div className={"col-span-12 md:col-span-4"}>
297
339
  <Select
298
340
  error={touched.config && Boolean(errors.config)}
341
+ size={"large"}
342
+ fullWidth={true}
299
343
  id="createCollections"
300
344
  name="createCollections"
301
345
  label="Create collections"
@@ -318,6 +362,8 @@ export function RolesDetailsForm({
318
362
 
319
363
  <div className={"col-span-12 md:col-span-4"}>
320
364
  <Select
365
+ size={"large"}
366
+ fullWidth={true}
321
367
  error={touched.config && Boolean(errors.config)}
322
368
  id="editCollections"
323
369
  name="editCollections"
@@ -344,6 +390,8 @@ export function RolesDetailsForm({
344
390
 
345
391
  <div className={"col-span-12 md:col-span-4"}>
346
392
  <Select
393
+ size={"large"}
394
+ fullWidth={true}
347
395
  error={touched.config && Boolean(errors.config)}
348
396
  id="deleteCollections"
349
397
  name="deleteCollections"
@@ -373,8 +421,8 @@ export function RolesDetailsForm({
373
421
  </DialogContent>
374
422
 
375
423
  <DialogActions position={"sticky"}>
376
- {savingError && <Typography className={"text-red-500"}>
377
- There was an error saving this role
424
+ {savingError && <Typography className={"text-red-500 dark:text-red-500"}>
425
+ {savingError.message ?? "There was an error saving this role"}
378
426
  </Typography>}
379
427
  <Button variant={"text"}
380
428
  onClick={() => {
@@ -13,7 +13,7 @@ import {
13
13
  Tooltip,
14
14
  Typography
15
15
  } from "@firecms/ui";
16
- import { DeleteConfirmationDialog, Role } from "@firecms/core";
16
+ import { ConfirmationDialog, Role } from "@firecms/core";
17
17
  import { useUserManagement } from "../../hooks";
18
18
  import { RoleChip } from "./RoleChip";
19
19
  import { DEFAULT_ROLES } from "./default_roles";
@@ -38,7 +38,7 @@ export function RolesTable({
38
38
 
39
39
  return <div
40
40
  className="w-full overflow-auto">
41
- <Table>
41
+ <Table className={"w-full"}>
42
42
  <TableHeader>
43
43
  <TableCell header={true} className="w-16"></TableCell>
44
44
  <TableCell header={true}>Role</TableCell>
@@ -61,7 +61,9 @@ export function RolesTable({
61
61
  >
62
62
  <TableCell style={{ width: "64px" }}>
63
63
  {!role.isAdmin &&
64
- <Tooltip title={"Delete this role"}>
64
+ <Tooltip
65
+ asChild={true}
66
+ title={"Delete this role"}>
65
67
  <IconButton
66
68
  size={"small"}
67
69
  disabled={!editable}
@@ -113,7 +115,7 @@ export function RolesTable({
113
115
 
114
116
  </Table>
115
117
 
116
- <DeleteConfirmationDialog
118
+ <ConfirmationDialog
117
119
  open={Boolean(roleToBeDeleted)}
118
120
  loading={deleteInProgress}
119
121
  onAccept={() => {
@@ -36,7 +36,9 @@ export const RolesView = React.memo(
36
36
  component="h4">
37
37
  Roles
38
38
  </Typography>
39
- <Tooltip title={!canEditRoles ? "Update plans to customise roles" : undefined}>
39
+ <Tooltip
40
+ asChild={true}
41
+ title={!canEditRoles ? "Update plans to customise roles" : undefined}>
40
42
  <Button
41
43
  size={"large"}
42
44
  disabled={!canEditRoles}
@@ -5,6 +5,7 @@ import {
5
5
  Dialog,
6
6
  DialogActions,
7
7
  DialogContent,
8
+ DialogTitle,
8
9
  DoneIcon,
9
10
  LoadingButton,
10
11
  MultiSelect,
@@ -140,14 +141,11 @@ export function UserDetailsForm({
140
141
  position: "relative",
141
142
  height: "100%"
142
143
  }}>
144
+
145
+ <DialogTitle variant={"h4"} gutterBottom={false}>
146
+ User
147
+ </DialogTitle>
143
148
  <DialogContent className="h-full flex-grow">
144
- <div
145
- className="flex flex-row pt-4 pb-4">
146
- <Typography variant={"h4"}
147
- className="flex-grow">
148
- User
149
- </Typography>
150
- </div>
151
149
 
152
150
  <div className={"grid grid-cols-12 gap-8"}>
153
151
 
@@ -181,17 +179,19 @@ export function UserDetailsForm({
181
179
  </div>
182
180
  <div className={"col-span-12"}>
183
181
  <MultiSelect
182
+ className={"w-full"}
184
183
  label="Roles"
185
184
  value={values.roles?.map(r => r.id) ?? []}
186
- onMultiValueChange={(value: string[]) => setFieldValue("roles", value.map(id => roles.find(r => r.id === id) as Role))}
187
- renderValue={(value: string) => {
188
- const userRole = roles
189
- .find((role) => role.id === value);
190
- if (!userRole) return null;
191
- return <div className="flex flex-wrap space-x-2 space-y-2">
192
- <RoleChip key={userRole?.id} role={userRole}/>
193
- </div>;
194
- }}>
185
+ onValueChange={(value: string[]) => setFieldValue("roles", value.map(id => roles.find(r => r.id === id) as Role))}
186
+ // renderValue={(value: string) => {
187
+ // const userRole = roles
188
+ // .find((role) => role.id === value);
189
+ // if (!userRole) return null;
190
+ // return <div className="flex flex-wrap space-x-2 space-y-2">
191
+ // <RoleChip key={userRole?.id} role={userRole}/>
192
+ // </div>;
193
+ // }}
194
+ >
195
195
  {roles.map(userRole => <MultiSelectItem key={userRole.id}
196
196
  value={userRole.id}>
197
197
  <RoleChip key={userRole?.id} role={userRole}/>
@@ -5,7 +5,7 @@ import * as locales from "date-fns/locale";
5
5
 
6
6
  import {
7
7
  defaultDateFormat,
8
- DeleteConfirmationDialog, Role,
8
+ ConfirmationDialog, Role,
9
9
  useAuthController,
10
10
  useCustomizationController, User,
11
11
  useSnackbarController
@@ -50,11 +50,10 @@ export function UsersTable({ onUserClicked }: {
50
50
  return (
51
51
  <div className="overflow-auto">
52
52
 
53
- <Table>
53
+ <Table className={"w-full"}>
54
54
 
55
55
  <TableHeader>
56
56
  <TableCell className="truncate w-16"></TableCell>
57
- <TableCell>ID</TableCell>
58
57
  <TableCell>Email</TableCell>
59
58
  <TableCell>Name</TableCell>
60
59
  <TableCell>Roles</TableCell>
@@ -75,7 +74,9 @@ export function UsersTable({ onUserClicked }: {
75
74
  }}
76
75
  >
77
76
  <TableCell className={"w-10"}>
78
- <Tooltip title={"Delete this user"}>
77
+ <Tooltip
78
+ asChild={true}
79
+ title={"Delete this user"}>
79
80
  <IconButton
80
81
  size={"small"}
81
82
  onClick={(event) => {
@@ -86,7 +87,6 @@ export function UsersTable({ onUserClicked }: {
86
87
  </IconButton>
87
88
  </Tooltip>
88
89
  </TableCell>
89
- <TableCell>{user.uid}</TableCell>
90
90
  <TableCell>{user.email}</TableCell>
91
91
  <TableCell className={"font-medium align-left"}>{user.displayName}</TableCell>
92
92
  <TableCell className="align-left">
@@ -147,7 +147,7 @@ export function UsersTable({ onUserClicked }: {
147
147
  </TableBody>
148
148
  </Table>
149
149
 
150
- <DeleteConfirmationDialog
150
+ <ConfirmationDialog
151
151
  open={Boolean(userToBeDeleted)}
152
152
  loading={deleteInProgress}
153
153
  onAccept={() => {
@@ -1,2 +1,2 @@
1
- export * from "./useBuildFirestoreUserManagement";
1
+ export * from "./useBuildUserManagement";
2
2
  export * from "./useUserManagement";