@firecms/user_management 3.0.0-canary.9 → 3.0.0-canary.91

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/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.9",
4
+ "version": "3.0.0-canary.91",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -24,36 +24,26 @@
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.9",
34
- "@firecms/formex": "^3.0.0-canary.9",
35
- "@firecms/ui": "^3.0.0-canary.9",
32
+ "@firecms/core": "^3.0.0-canary.91",
33
+ "@firecms/formex": "^3.0.0-canary.91",
34
+ "@firecms/ui": "^3.0.0-canary.91",
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.1.6"
42
+ "@types/node": "^20.14.12",
43
+ "@types/react": "^18.3.3",
44
+ "@types/react-dom": "^18.3.0",
45
+ "typescript": "^5.5.4",
46
+ "vite": "^5.3.4"
57
47
  },
58
48
  "scripts": {
59
49
  "dev": "vite",
@@ -65,11 +55,5 @@
65
55
  "src",
66
56
  "bin"
67
57
  ],
68
- "eslintConfig": {
69
- "extends": [
70
- "react-app",
71
- "react-app/jest"
72
- ]
73
- },
74
- "gitHead": "5bd7f971be5956d225835af3b6cbe9f71d3e7e50"
58
+ "gitHead": "fc773194d8166c27d04bc69fec41b171a5b3e2df"
75
59
  }
@@ -12,7 +12,7 @@ import {
12
12
  TextField,
13
13
  Typography,
14
14
  } from "@firecms/ui";
15
- import { FieldCaption, Role, User, useSnackbarController } from "@firecms/core";
15
+ import { FieldCaption, Role, useAuthController, User, useSnackbarController } from "@firecms/core";
16
16
  import { Formex, useCreateFormex } from "@firecms/formex";
17
17
 
18
18
  import { areRolesEqual } from "../../utils";
@@ -56,7 +56,9 @@ export function UserDetailsForm({
56
56
 
57
57
  const snackbarController = useSnackbarController();
58
58
  const {
59
- loggedInUser,
59
+ user: loggedInUser
60
+ } = useAuthController();
61
+ const {
60
62
  saveUser,
61
63
  users,
62
64
  roles,
@@ -1,2 +1,2 @@
1
- export * from "./useBuildFirestoreUserManagement";
1
+ export * from "./useBuildUserManagement";
2
2
  export * from "./useUserManagement";
@@ -0,0 +1,295 @@
1
+ import React, { useCallback, useEffect } from "react";
2
+ import equal from "react-fast-compare"
3
+
4
+ import { UserManagement } from "../types";
5
+ import { Authenticator, DataSourceDelegate, Entity, PermissionsBuilder, Role, User } from "@firecms/core";
6
+ import { resolveUserRolePermissions } from "../utils";
7
+
8
+ type UserWithRoleIds = User & { roles: string[] };
9
+
10
+ export interface UserManagementParams {
11
+
12
+ /**
13
+ * The delegate in charge of persisting the data.
14
+ */
15
+ dataSourceDelegate?: DataSourceDelegate;
16
+
17
+ /**
18
+ * Path where the plugin users configuration is stored.
19
+ * Default: __FIRECMS/config/users
20
+ * You can specify a different path if you want to store the user management configuration in a different place.
21
+ * Please keep in mind that the FireCMS users are not necessarily the same as the Firebase users (but they can be).
22
+ * The path should be relative to the root of the database, and should always have an odd number of segments.
23
+ */
24
+ usersPath?: string;
25
+
26
+ /**
27
+ * Path where the plugin roles configuration is stored.
28
+ * Default: __FIRECMS/config/roles
29
+ */
30
+ rolesPath?: string;
31
+
32
+ /**
33
+ * Maximum number of users that can be created.
34
+ */
35
+ usersLimit?: number;
36
+
37
+ /**
38
+ * Can the logged user edit roles
39
+ */
40
+ canEditRoles?: boolean;
41
+
42
+ /**
43
+ * If there are no roles in the database, provide a button to create the default roles.
44
+ */
45
+ allowDefaultRolesCreation?: boolean;
46
+
47
+ /**
48
+ * Include the collection config permissions in the user management system.
49
+ */
50
+ includeCollectionConfigPermissions?: boolean;
51
+
52
+ }
53
+
54
+ /**
55
+ * This hook is used to build a user management object that can be used to
56
+ * manage users and roles in a Firestore backend.
57
+ * @param dataSourceDelegate
58
+ * @param usersPath
59
+ * @param rolesPath
60
+ * @param usersLimit
61
+ * @param canEditRoles
62
+ * @param allowDefaultRolesCreation
63
+ * @param includeCollectionConfigPermissions
64
+ */
65
+ export function useBuildUserManagement({
66
+ dataSourceDelegate,
67
+ usersPath = "__FIRECMS/config/users",
68
+ rolesPath = "__FIRECMS/config/roles",
69
+ usersLimit,
70
+ canEditRoles = true,
71
+ allowDefaultRolesCreation,
72
+ includeCollectionConfigPermissions
73
+ }: UserManagementParams): UserManagement {
74
+
75
+ const [rolesLoading, setRolesLoading] = React.useState<boolean>(true);
76
+ const [usersLoading, setUsersLoading] = React.useState<boolean>(true);
77
+ const [roles, setRoles] = React.useState<Role[]>([]);
78
+ const [usersWithRoleIds, setUsersWithRoleIds] = React.useState<UserWithRoleIds[]>([]);
79
+
80
+ const users = usersWithRoleIds.map(u => ({
81
+ ...u,
82
+ roles: roles.filter(r => u.roles?.includes(r.id))
83
+ }) as User);
84
+
85
+ const [rolesError, setRolesError] = React.useState<Error | undefined>();
86
+ const [usersError, setUsersError] = React.useState<Error | undefined>();
87
+
88
+ const loading = rolesLoading || usersLoading;
89
+
90
+ useEffect(() => {
91
+ if (!dataSourceDelegate || !rolesPath) return;
92
+ if (dataSourceDelegate.initialised !== undefined && !dataSourceDelegate.initialised) return;
93
+
94
+ return dataSourceDelegate.listenCollection?.({
95
+ path: rolesPath,
96
+ onUpdate(entities: Entity<any>[]): void {
97
+ setRolesError(undefined);
98
+ try {
99
+ const newRoles = entityToRoles(entities);
100
+ if (!equal(newRoles, roles))
101
+ setRoles(newRoles);
102
+ } catch (e) {
103
+ console.error("Error loading roles", e);
104
+ setRolesError(e as Error);
105
+ }
106
+ setRolesLoading(false);
107
+ },
108
+ onError(e: any): void {
109
+ console.error("Error loading roles", e);
110
+ setRolesError(e);
111
+ setRolesLoading(false);
112
+ }
113
+ });
114
+
115
+ }, [dataSourceDelegate, rolesPath]);
116
+
117
+ useEffect(() => {
118
+ if (!dataSourceDelegate || !usersPath) return;
119
+ if (dataSourceDelegate.initialised !== undefined && !dataSourceDelegate.initialised) return;
120
+
121
+ return dataSourceDelegate.listenCollection?.({
122
+ path: usersPath,
123
+ onUpdate(entities: Entity<any>[]): void {
124
+ setUsersError(undefined);
125
+ try {
126
+ const newUsers = entitiesToUsers(entities);
127
+ if (!equal(newUsers, usersWithRoleIds))
128
+ setUsersWithRoleIds(newUsers);
129
+ } catch (e) {
130
+ console.error("Error loading users", e);
131
+ setUsersError(e as Error);
132
+ }
133
+ setUsersLoading(false);
134
+ },
135
+ onError(e: any): void {
136
+ console.error("Error loading users", e);
137
+ setUsersError(e);
138
+ setUsersLoading(false);
139
+ }
140
+ });
141
+
142
+ }, [dataSourceDelegate, usersPath]);
143
+
144
+ const saveUser = useCallback(async (user: User): Promise<User> => {
145
+ if (!dataSourceDelegate) throw Error("useBuildUserManagement Firebase not initialised");
146
+ if (!usersPath) throw Error("useBuildUserManagement Firestore not initialised");
147
+
148
+ console.debug("Persisting user", user);
149
+
150
+ const roleIds = user.roles?.map(r => r.id);
151
+ const {
152
+ uid,
153
+ ...userData
154
+ } = user;
155
+ const data = {
156
+ ...userData,
157
+ roles: roleIds
158
+ };
159
+ if (uid) {
160
+ return dataSourceDelegate.saveEntity({
161
+ status: "existing",
162
+ path: usersPath,
163
+ entityId: uid,
164
+ values: data
165
+ }).then(() => user);
166
+ } else {
167
+ return dataSourceDelegate.saveEntity({
168
+ status: "new",
169
+ path: usersPath,
170
+ values: data
171
+ }).then(() => user);
172
+ }
173
+ }, [usersPath, dataSourceDelegate]);
174
+
175
+ const saveRole = useCallback((role: Role): Promise<void> => {
176
+ if (!dataSourceDelegate) throw Error("useBuildUserManagement Firebase not initialised");
177
+ if (!rolesPath) throw Error("useBuildUserManagement Firestore not initialised");
178
+ console.debug("Persisting role", role);
179
+ const {
180
+ id,
181
+ ...roleData
182
+ } = role;
183
+ return dataSourceDelegate.saveEntity({
184
+ status: "existing",
185
+ path: rolesPath,
186
+ entityId: id,
187
+ values: roleData
188
+ }).then(() => {
189
+ return;
190
+ });
191
+ }, [rolesPath, dataSourceDelegate]);
192
+
193
+ const deleteUser = useCallback(async (user: User): Promise<void> => {
194
+ if (!dataSourceDelegate) throw Error("useBuildUserManagement Firebase not initialised");
195
+ if (!usersPath) throw Error("useBuildUserManagement Firestore not initialised");
196
+ console.debug("Deleting", user);
197
+ const { uid } = user;
198
+ const entity: Entity<any> = {
199
+ path: usersPath,
200
+ id: uid,
201
+ values: {}
202
+ };
203
+ await dataSourceDelegate.deleteEntity({ entity })
204
+ }, [usersPath, dataSourceDelegate]);
205
+
206
+ const deleteRole = useCallback(async (role: Role): Promise<void> => {
207
+ if (!dataSourceDelegate) throw Error("useBuildUserManagement Firebase not initialised");
208
+ if (!rolesPath) throw Error("useBuildUserManagement Firestore not initialised");
209
+ console.debug("Deleting", role);
210
+ const { id } = role;
211
+ const entity: Entity<any> = {
212
+ path: usersPath,
213
+ id: id,
214
+ values: {}
215
+ };
216
+ await dataSourceDelegate.deleteEntity({ entity })
217
+ }, [rolesPath, dataSourceDelegate]);
218
+
219
+ const collectionPermissions: PermissionsBuilder = useCallback(({
220
+ collection,
221
+ user
222
+ }) => resolveUserRolePermissions({
223
+ collection,
224
+ user
225
+ }), []);
226
+
227
+ const defineRolesFor: ((user: User) => Role[] | undefined) = useCallback((user) => {
228
+ if (!users) throw Error("Users not loaded");
229
+ const mgmtUser = users.find(u => u.email?.toLowerCase() === user?.email?.toLowerCase());
230
+ return mgmtUser?.roles;
231
+ }, [users]);
232
+
233
+ const authenticator: Authenticator = useCallback(({ user }) => {
234
+ console.debug("Authenticating user", user);
235
+
236
+ if (loading) {
237
+ console.warn("User management is still loading");
238
+ return false;
239
+ }
240
+
241
+ // This is an example of how you can link the access system to the user management plugin
242
+ if (users.length === 0) {
243
+ return true; // If there are no users created yet, we allow access to every user
244
+ }
245
+
246
+ const mgmtUser = users.find(u => u.email?.toLowerCase() === user?.email?.toLowerCase());
247
+ if (mgmtUser) {
248
+ return true;
249
+ }
250
+
251
+ throw Error("Could not find a user with the provided email in the user management system.");
252
+ }, [loading, users]);
253
+
254
+ const isAdmin = roles.some(r => r.id === "admin");
255
+
256
+ return {
257
+ loading,
258
+ roles,
259
+ users,
260
+ saveUser,
261
+ saveRole,
262
+ rolesError,
263
+ deleteUser,
264
+ deleteRole,
265
+ usersLimit,
266
+ usersError,
267
+ isAdmin,
268
+ canEditRoles: canEditRoles === undefined ? true : canEditRoles,
269
+ allowDefaultRolesCreation: allowDefaultRolesCreation === undefined ? true : allowDefaultRolesCreation,
270
+ includeCollectionConfigPermissions: Boolean(includeCollectionConfigPermissions),
271
+ collectionPermissions,
272
+ defineRolesFor,
273
+ authenticator
274
+ }
275
+ }
276
+
277
+ const entitiesToUsers = (docs: Entity<Omit<UserWithRoleIds, "id">>[]): (UserWithRoleIds)[] => {
278
+ return docs.map((doc) => {
279
+ const data = doc.values as any;
280
+ const newVar = {
281
+ uid: doc.id,
282
+ ...data,
283
+ created_on: data?.created_on,
284
+ updated_on: data?.updated_on
285
+ };
286
+ return newVar as (UserWithRoleIds);
287
+ });
288
+ }
289
+
290
+ const entityToRoles = (entities: Entity<Omit<Role, "id">>[]): Role[] => {
291
+ return entities.map((doc) => ({
292
+ id: doc.id,
293
+ ...doc.values
294
+ } as Role));
295
+ }
@@ -1,16 +1,9 @@
1
- import { PermissionsBuilder, Role, User } from "@firecms/core";
1
+ import { Authenticator, PermissionsBuilder, Role, User } from "@firecms/core";
2
2
 
3
3
  export type UserManagement<USER extends User = User> = {
4
4
 
5
5
  loading: boolean;
6
6
 
7
- /**
8
- * The user currently logged in, in the user management system.
9
- * This is the same user that is logged in the Authenticator, but with the roles
10
- * and permissions loaded.
11
- */
12
- loggedInUser: USER | undefined;
13
-
14
7
  users: USER[];
15
8
  saveUser: (user: USER) => Promise<USER>;
16
9
  deleteUser: (user: USER) => Promise<void>;
@@ -29,6 +22,11 @@ export type UserManagement<USER extends User = User> = {
29
22
  */
30
23
  canEditRoles?: boolean;
31
24
 
25
+ /**
26
+ * Is the logged user Admin?
27
+ */
28
+ isAdmin?: boolean;
29
+
32
30
  /**
33
31
  * Include a button to create default roles, in case there are no roles in the system.
34
32
  */
@@ -45,4 +43,19 @@ export type UserManagement<USER extends User = User> = {
45
43
  */
46
44
  collectionPermissions: PermissionsBuilder;
47
45
 
46
+ /**
47
+ * Define the roles for a given user. You will typically want to plug this into your auth controller.
48
+ * @param user
49
+ */
50
+ defineRolesFor: (user: User) => Promise<Role[] | undefined> | Role[] | undefined;
51
+
52
+ /**
53
+ * You can build an authenticator callback from the current configuration of the user management.
54
+ * It will only allow access to users with the required roles.
55
+ */
56
+ authenticator?: Authenticator;
57
+
58
+ rolesError?: Error;
59
+ usersError?: Error;
60
+
48
61
  };
@@ -1,13 +1,28 @@
1
- import { FireCMSPlugin } from "@firecms/core";
1
+ import { FireCMSPlugin, useAuthController, useSnackbarController } from "@firecms/core";
2
2
  import { UserManagementProvider } from "./UserManagementProvider";
3
- import { UserManagement } from "./types";
3
+ import { PersistedUser, UserManagement } from "./types";
4
+ import { AddIcon, Button, Paper, Typography } from "@firecms/ui";
5
+ import { DEFAULT_ROLES } from "./components/roles/default_roles";
4
6
 
5
7
  export function useUserManagementPlugin({ userManagement }: {
6
8
  userManagement: UserManagement,
7
9
  }): FireCMSPlugin {
10
+
11
+ const noUsers = userManagement.users.length === 0;
12
+ const noRoles = userManagement.roles.length === 0;
13
+
8
14
  return {
9
- name: "User management plugin",
15
+ key: "user_management",
10
16
  loading: userManagement.loading,
17
+
18
+ homePage: {
19
+ additionalChildrenStart: noUsers || noRoles
20
+ ? <IntroWidget
21
+ noUsers={noUsers}
22
+ noRoles={noRoles}
23
+ userManagement={userManagement}/>
24
+ : undefined
25
+ },
11
26
  provider: {
12
27
  Component: UserManagementProvider,
13
28
  props: {
@@ -16,3 +31,74 @@ export function useUserManagementPlugin({ userManagement }: {
16
31
  }
17
32
  }
18
33
  }
34
+
35
+ export function IntroWidget({
36
+ noUsers,
37
+ noRoles,
38
+ userManagement
39
+ }: {
40
+ noUsers: boolean;
41
+ noRoles: boolean;
42
+ userManagement: UserManagement<PersistedUser>;
43
+ }) {
44
+
45
+ const authController = useAuthController();
46
+ const snackbarController = useSnackbarController();
47
+
48
+ const buttonLabel = noUsers && noRoles
49
+ ? "Create default roles and add current user as admin"
50
+ : noUsers
51
+ ? "Add current user as admin"
52
+ : noRoles ? "Create default roles" : undefined;
53
+
54
+ return (
55
+ <Paper
56
+ className={"my-4 flex flex-col px-4 py-6 bg-white dark:bg-slate-800 gap-2"}>
57
+ <Typography variant={"subtitle2"} className={"uppercase"}>Create your users and roles</Typography>
58
+ <Typography>
59
+ You have no users or roles defined. You can create default roles and add the current user as admin.
60
+ </Typography>
61
+ <Button onClick={() => {
62
+ if (!authController.user?.uid) {
63
+ throw Error("UsersTable, authController misconfiguration");
64
+ }
65
+ if (noUsers) {
66
+ userManagement.saveUser({
67
+ uid: authController.user?.uid,
68
+ email: authController.user?.email,
69
+ displayName: authController.user?.displayName,
70
+ photoURL: authController.user?.photoURL,
71
+ providerId: authController.user?.providerId,
72
+ isAnonymous: authController.user?.isAnonymous,
73
+ roles: [{
74
+ id: "admin",
75
+ name: "Admin"
76
+ }],
77
+ created_on: new Date()
78
+ })
79
+ .then(() => {
80
+ snackbarController.open({
81
+ type: "success",
82
+ message: "User added successfully"
83
+ })
84
+ })
85
+ .catch((error) => {
86
+ snackbarController.open({
87
+ type: "error",
88
+ message: "Error adding user: " + error.message
89
+ })
90
+ });
91
+ }
92
+ if (noRoles) {
93
+ DEFAULT_ROLES.forEach((role) => {
94
+ userManagement.saveRole(role);
95
+ });
96
+ }
97
+ }}>
98
+ <AddIcon/>
99
+ {buttonLabel}
100
+ </Button>
101
+ </Paper>
102
+ );
103
+
104
+ }
@@ -10,7 +10,10 @@ const DEFAULT_PERMISSIONS = {
10
10
  };
11
11
 
12
12
  export function resolveUserRolePermissions<UserType extends User>
13
- ({ collection, user }: {
13
+ ({
14
+ collection,
15
+ user
16
+ }: {
14
17
  collection: EntityCollection<any>,
15
18
  user: UserType | null
16
19
  }): Permissions {