@firecms/user_management 3.0.0-3.0.0-canary.44.0 → 3.0.0-beta.10

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.
@@ -0,0 +1,314 @@
1
+ import React, { useCallback, useEffect } from "react";
2
+ import equal from "react-fast-compare"
3
+
4
+ import { UserManagement } from "../types";
5
+ import {
6
+ Authenticator,
7
+ DataSourceDelegate,
8
+ Entity,
9
+ PermissionsBuilder,
10
+ removeUndefined,
11
+ Role,
12
+ User
13
+ } from "@firecms/core";
14
+ import { resolveUserRolePermissions } from "../utils";
15
+
16
+ type UserWithRoleIds = Omit<User, "roles"> & { roles: string[] };
17
+
18
+ export interface UserManagementParams {
19
+
20
+ /**
21
+ * The delegate in charge of persisting the data.
22
+ */
23
+ dataSourceDelegate?: DataSourceDelegate;
24
+
25
+ /**
26
+ * Path where the plugin users configuration is stored.
27
+ * Default: __FIRECMS/config/users
28
+ * You can specify a different path if you want to store the user management configuration in a different place.
29
+ * Please keep in mind that the FireCMS users are not necessarily the same as the Firebase users (but they can be).
30
+ * The path should be relative to the root of the database, and should always have an odd number of segments.
31
+ */
32
+ usersPath?: string;
33
+
34
+ /**
35
+ * Path where the plugin roles configuration is stored.
36
+ * Default: __FIRECMS/config/roles
37
+ */
38
+ rolesPath?: string;
39
+
40
+ /**
41
+ * Maximum number of users that can be created.
42
+ */
43
+ usersLimit?: number;
44
+
45
+ /**
46
+ * Can the logged user edit roles
47
+ */
48
+ canEditRoles?: boolean;
49
+
50
+ /**
51
+ * If there are no roles in the database, provide a button to create the default roles.
52
+ */
53
+ allowDefaultRolesCreation?: boolean;
54
+
55
+ /**
56
+ * Include the collection config permissions in the user management system.
57
+ */
58
+ includeCollectionConfigPermissions?: boolean;
59
+
60
+ }
61
+
62
+ /**
63
+ * This hook is used to build a user management object that can be used to
64
+ * manage users and roles in a Firestore backend.
65
+ * @param dataSourceDelegate
66
+ * @param usersPath
67
+ * @param rolesPath
68
+ * @param usersLimit
69
+ * @param canEditRoles
70
+ * @param allowDefaultRolesCreation
71
+ * @param includeCollectionConfigPermissions
72
+ */
73
+ export function useBuildUserManagement({
74
+ dataSourceDelegate,
75
+ usersPath = "__FIRECMS/config/users",
76
+ rolesPath = "__FIRECMS/config/roles",
77
+ usersLimit,
78
+ canEditRoles = true,
79
+ allowDefaultRolesCreation,
80
+ includeCollectionConfigPermissions
81
+ }: UserManagementParams): UserManagement {
82
+
83
+ const [rolesLoading, setRolesLoading] = React.useState<boolean>(true);
84
+ const [usersLoading, setUsersLoading] = React.useState<boolean>(true);
85
+ const [roles, setRoles] = React.useState<Role[]>([]);
86
+ const [usersWithRoleIds, setUsersWithRoleIds] = React.useState<UserWithRoleIds[]>([]);
87
+
88
+ const users = usersWithRoleIds.map(u => ({
89
+ ...u,
90
+ roles: roles.filter(r => u.roles?.includes(r.id))
91
+ }) as User);
92
+
93
+ const [rolesError, setRolesError] = React.useState<Error | undefined>();
94
+ const [usersError, setUsersError] = React.useState<Error | undefined>();
95
+
96
+ const loading = rolesLoading || usersLoading;
97
+
98
+ useEffect(() => {
99
+ if (!dataSourceDelegate || !rolesPath) return;
100
+ if (dataSourceDelegate.initialised !== undefined && !dataSourceDelegate.initialised) return;
101
+ if (dataSourceDelegate.authenticated !== undefined && !dataSourceDelegate.authenticated) {
102
+ setRolesLoading(false);
103
+ return;
104
+ }
105
+
106
+ setRolesLoading(true);
107
+ return dataSourceDelegate.listenCollection?.({
108
+ path: rolesPath,
109
+ onUpdate(entities: Entity<any>[]): void {
110
+ setRolesError(undefined);
111
+ try {
112
+ const newRoles = entityToRoles(entities);
113
+ if (!equal(newRoles, roles))
114
+ setRoles(newRoles);
115
+ } catch (e) {
116
+ setRoles([]);
117
+ console.error("Error loading roles", e);
118
+ setRolesError(e as Error);
119
+ }
120
+ setRolesLoading(false);
121
+ },
122
+ onError(e: any): void {
123
+ setRoles([]);
124
+ console.error("Error loading roles", e);
125
+ setRolesError(e);
126
+ setRolesLoading(false);
127
+ }
128
+ });
129
+
130
+ }, [dataSourceDelegate?.initialised, dataSourceDelegate?.authenticated, rolesPath]);
131
+
132
+ useEffect(() => {
133
+ if (!dataSourceDelegate || !usersPath) return;
134
+ if (dataSourceDelegate.initialised !== undefined && !dataSourceDelegate.initialised) return;
135
+ if (dataSourceDelegate.authenticated !== undefined && !dataSourceDelegate.authenticated) {
136
+ setUsersLoading(false);
137
+ return;
138
+ }
139
+
140
+ setUsersLoading(true);
141
+ return dataSourceDelegate.listenCollection?.({
142
+ path: usersPath,
143
+ onUpdate(entities: Entity<any>[]): void {
144
+ setUsersError(undefined);
145
+ try {
146
+ const newUsers = entitiesToUsers(entities);
147
+ if (!equal(newUsers, usersWithRoleIds))
148
+ setUsersWithRoleIds(newUsers);
149
+ } catch (e) {
150
+ setUsersWithRoleIds([]);
151
+ console.error("Error loading users", e);
152
+ setUsersError(e as Error);
153
+ }
154
+ setUsersLoading(false);
155
+ },
156
+ onError(e: any): void {
157
+ setUsersWithRoleIds([]);
158
+ console.error("Error loading users", e);
159
+ setUsersError(e);
160
+ setUsersLoading(false);
161
+ }
162
+ });
163
+
164
+ }, [dataSourceDelegate?.initialised, dataSourceDelegate?.authenticated, usersPath]);
165
+
166
+ const saveUser = useCallback(async (user: User): Promise<User> => {
167
+ if (!dataSourceDelegate) throw Error("useBuildUserManagement Firebase not initialised");
168
+ if (!usersPath) throw Error("useBuildUserManagement Firestore not initialised");
169
+
170
+ console.debug("Persisting user", user);
171
+
172
+ const roleIds = user.roles?.map(r => r.id);
173
+ const email = user.email?.toLowerCase().trim();
174
+ if (!email) throw Error("Email is required");
175
+
176
+ const userExists = users.find(u => u.email?.toLowerCase() === email);
177
+ const data = {
178
+ ...user,
179
+ roles: roleIds ?? []
180
+ };
181
+ if (!userExists) {
182
+ // @ts-ignore
183
+ data.created_on = new Date();
184
+ }
185
+
186
+ return dataSourceDelegate.saveEntity({
187
+ status: "existing",
188
+ path: usersPath,
189
+ entityId: email,
190
+ values: removeUndefined(data)
191
+ }).then(() => user);
192
+ }, [usersPath, dataSourceDelegate?.initialised]);
193
+
194
+ const saveRole = useCallback((role: Role): Promise<void> => {
195
+ if (!dataSourceDelegate) throw Error("useBuildUserManagement Firebase not initialised");
196
+ if (!rolesPath) throw Error("useBuildUserManagement Firestore not initialised");
197
+ console.debug("Persisting role", role);
198
+ const {
199
+ id,
200
+ ...roleData
201
+ } = role;
202
+ return dataSourceDelegate.saveEntity({
203
+ status: "existing",
204
+ path: rolesPath,
205
+ entityId: id,
206
+ values: removeUndefined(roleData)
207
+ }).then(() => {
208
+ return;
209
+ });
210
+ }, [rolesPath, dataSourceDelegate?.initialised]);
211
+
212
+ const deleteUser = useCallback(async (user: User): Promise<void> => {
213
+ if (!dataSourceDelegate) throw Error("useBuildUserManagement Firebase not initialised");
214
+ if (!usersPath) throw Error("useBuildUserManagement Firestore not initialised");
215
+ console.debug("Deleting", user);
216
+ const { uid } = user;
217
+ const entity: Entity<any> = {
218
+ path: usersPath,
219
+ id: uid,
220
+ values: {}
221
+ };
222
+ await dataSourceDelegate.deleteEntity({ entity })
223
+ }, [usersPath, dataSourceDelegate?.initialised]);
224
+
225
+ const deleteRole = useCallback(async (role: Role): Promise<void> => {
226
+ if (!dataSourceDelegate) throw Error("useBuildUserManagement Firebase not initialised");
227
+ if (!rolesPath) throw Error("useBuildUserManagement Firestore not initialised");
228
+ console.debug("Deleting", role);
229
+ const { id } = role;
230
+ const entity: Entity<any> = {
231
+ path: rolesPath,
232
+ id: id,
233
+ values: {}
234
+ };
235
+ await dataSourceDelegate.deleteEntity({ entity })
236
+ }, [rolesPath, dataSourceDelegate?.initialised]);
237
+
238
+ const collectionPermissions: PermissionsBuilder = useCallback(({
239
+ collection,
240
+ user
241
+ }) => resolveUserRolePermissions({
242
+ collection,
243
+ user
244
+ }), []);
245
+
246
+ const defineRolesFor: ((user: User) => Role[] | undefined) = useCallback((user) => {
247
+ if (!users) throw Error("Users not loaded");
248
+ const mgmtUser = users.find(u => u.email?.toLowerCase() === user?.email?.toLowerCase());
249
+ return mgmtUser?.roles;
250
+ }, [users]);
251
+
252
+ const authenticator: Authenticator = useCallback(({ user }) => {
253
+ console.debug("Authenticating user", user);
254
+
255
+ if (loading) {
256
+ console.warn("User management is still loading");
257
+ return false;
258
+ }
259
+
260
+ // This is an example of how you can link the access system to the user management plugin
261
+ if (users.length === 0) {
262
+ return true; // If there are no users created yet, we allow access to every user
263
+ }
264
+
265
+ const mgmtUser = users.find(u => u.email?.toLowerCase() === user?.email?.toLowerCase());
266
+ if (mgmtUser) {
267
+ return true;
268
+ }
269
+
270
+ throw Error("Could not find a user with the provided email in the user management system.");
271
+ }, [loading, users, usersError, rolesError]);
272
+
273
+ const isAdmin = roles.some(r => r.id === "admin");
274
+
275
+ return {
276
+ loading,
277
+ roles,
278
+ users,
279
+ saveUser,
280
+ saveRole,
281
+ rolesError,
282
+ deleteUser,
283
+ deleteRole,
284
+ usersLimit,
285
+ usersError,
286
+ isAdmin,
287
+ canEditRoles: canEditRoles === undefined ? true : canEditRoles,
288
+ allowDefaultRolesCreation: allowDefaultRolesCreation === undefined ? true : allowDefaultRolesCreation,
289
+ includeCollectionConfigPermissions: Boolean(includeCollectionConfigPermissions),
290
+ collectionPermissions,
291
+ defineRolesFor,
292
+ authenticator
293
+ }
294
+ }
295
+
296
+ const entitiesToUsers = (docs: Entity<Omit<UserWithRoleIds, "id">>[]): (UserWithRoleIds)[] => {
297
+ return docs.map((doc) => {
298
+ const data = doc.values as any;
299
+ const newVar = {
300
+ uid: doc.id,
301
+ ...data,
302
+ created_on: data?.created_on,
303
+ updated_on: data?.updated_on
304
+ };
305
+ return newVar as (UserWithRoleIds);
306
+ });
307
+ }
308
+
309
+ const entityToRoles = (entities: Entity<Omit<Role, "id">>[]): Role[] => {
310
+ return entities.map((doc) => ({
311
+ id: doc.id,
312
+ ...doc.values
313
+ } as Role));
314
+ }
@@ -22,6 +22,11 @@ export type UserManagement<USER extends User = User> = {
22
22
  */
23
23
  canEditRoles?: boolean;
24
24
 
25
+ /**
26
+ * Is the logged user Admin?
27
+ */
28
+ isAdmin?: boolean;
29
+
25
30
  /**
26
31
  * Include a button to create default roles, in case there are no roles in the system.
27
32
  */
@@ -45,13 +45,14 @@ export function resolveUserRolePermissions<UserType extends User>
45
45
  function resolveCollectionRole(role: Role, id: string): Permissions {
46
46
 
47
47
  const basePermissions = {
48
- read: role.isAdmin || role.defaultPermissions?.read,
49
- create: role.isAdmin || role.defaultPermissions?.create,
50
- edit: role.isAdmin || role.defaultPermissions?.edit,
51
- delete: role.isAdmin || role.defaultPermissions?.delete
48
+ read: (role.isAdmin || role.defaultPermissions?.read) ?? false,
49
+ create: (role.isAdmin || role.defaultPermissions?.create) ?? false,
50
+ edit: (role.isAdmin || role.defaultPermissions?.edit) ?? false,
51
+ delete: (role.isAdmin || role.defaultPermissions?.delete) ?? false
52
52
  };
53
- if (role.collectionPermissions && role.collectionPermissions[id]) {
54
- return mergePermissions(role.collectionPermissions[id], basePermissions);
53
+ const thisCollectionPermissions = role.collectionPermissions?.[id];
54
+ if (thisCollectionPermissions) {
55
+ return mergePermissions(thisCollectionPermissions, basePermissions);
55
56
  } else if (role.defaultPermissions) {
56
57
  return mergePermissions(role.defaultPermissions, basePermissions);
57
58
  } else {
@@ -1,274 +0,0 @@
1
- import React, { useCallback, useEffect } from "react";
2
- import {
3
- addDoc,
4
- collection,
5
- deleteDoc,
6
- doc,
7
- DocumentSnapshot,
8
- getFirestore,
9
- onSnapshot,
10
- setDoc
11
- } from "firebase/firestore";
12
- import { FirebaseApp } from "firebase/app";
13
- import { UserManagement } from "../types";
14
- import { Authenticator, PermissionsBuilder, Role, User } from "@firecms/core";
15
- import { resolveUserRolePermissions } from "../utils";
16
-
17
- type UserWithRoleIds = User & { roles: string[] };
18
-
19
- export interface UserManagementParams {
20
- /**
21
- * The Firebase app to use for the user management. The config will be saved in the Firestore
22
- * collection indicated by `configPath`.
23
- */
24
- firebaseApp?: FirebaseApp;
25
- /**
26
- * Path where the plugin users configuration is stored.
27
- * Default: __FIRECMS/config/users
28
- * You can specify a different path if you want to store the user management configuration in a different place.
29
- * Please keep in mind that the FireCMS users are not necessarily the same as the Firebase users (but they can be).
30
- * The path should be relative to the root of the Firestore database, and should always have an odd number of segments.
31
- */
32
- usersPath?: string;
33
-
34
- /**
35
- * Path where the plugin roles configuration is stored.
36
- * Default: __FIRECMS/config/roles
37
- */
38
- rolesPath?: string;
39
-
40
- /**
41
- * Maximum number of users that can be created.
42
- */
43
- usersLimit?: number;
44
-
45
- /**
46
- * Can the logged user edit roles
47
- */
48
- canEditRoles?: boolean;
49
-
50
- /**
51
- * If there are no roles in the database, provide a button to create the default roles.
52
- */
53
- allowDefaultRolesCreation?: boolean;
54
-
55
- /**
56
- * Include the collection config permissions in the user management system.
57
- */
58
- includeCollectionConfigPermissions?: boolean;
59
-
60
- }
61
-
62
- /**
63
- * This hook is used to build a user management object that can be used to
64
- * manage users and roles in a Firestore backend.
65
- * @param backendFirebaseApp
66
- * @param usersPath
67
- * @param rolesPath
68
- * @param usersLimit
69
- * @param canEditRoles
70
- */
71
- export function useFirestoreUserManagement({
72
- firebaseApp,
73
- usersPath = "__FIRECMS/config/users",
74
- rolesPath = "__FIRECMS/config/roles",
75
- usersLimit,
76
- canEditRoles = true,
77
- allowDefaultRolesCreation,
78
- includeCollectionConfigPermissions
79
- }: UserManagementParams): UserManagement {
80
-
81
- const [rolesLoading, setRolesLoading] = React.useState<boolean>(true);
82
- const [usersLoading, setUsersLoading] = React.useState<boolean>(true);
83
- const [roles, setRoles] = React.useState<Role[]>([]);
84
- const [usersWithRoleIds, setUsersWithRoleIds] = React.useState<UserWithRoleIds[]>([]);
85
-
86
- const users = usersWithRoleIds.map(u => ({
87
- ...u,
88
- roles: roles.filter(r => u.roles?.includes(r.id))
89
- }) as User);
90
-
91
- const [rolesError, setRolesError] = React.useState<Error | undefined>();
92
- const [usersError, setUsersError] = React.useState<Error | undefined>();
93
-
94
- const loading = rolesLoading || usersLoading;
95
-
96
- useEffect(() => {
97
- if (!firebaseApp || !rolesPath) return;
98
- const firestore = getFirestore(firebaseApp);
99
-
100
- return onSnapshot(collection(firestore, rolesPath),
101
- {
102
- next: (snapshot) => {
103
- setRolesError(undefined);
104
- try {
105
- const newRoles = docsToRoles(snapshot.docs);
106
- setRoles(newRoles);
107
- } catch (e) {
108
- console.error("Error loading roles", e);
109
- setRolesError(e as Error);
110
- }
111
- setRolesLoading(false);
112
- },
113
- error: (e) => {
114
- console.error("Error loading roles", e);
115
- setRolesError(e);
116
- setRolesLoading(false);
117
- }
118
- }
119
- );
120
- }, [firebaseApp, rolesPath]);
121
-
122
- useEffect(() => {
123
- if (!firebaseApp || !usersPath) return;
124
- const firestore = getFirestore(firebaseApp);
125
-
126
- return onSnapshot(collection(firestore, usersPath),
127
- {
128
- next: (snapshot) => {
129
- setUsersError(undefined);
130
- try {
131
- const newUsers = docsToUsers(snapshot.docs);
132
- setUsersWithRoleIds(newUsers);
133
- } catch (e) {
134
- console.error("Error loading users", e);
135
- setUsersError(e as Error);
136
- }
137
- setUsersLoading(false);
138
- },
139
- error: (e) => {
140
- console.error("Error loading users", e);
141
- setUsersError(e);
142
- setUsersLoading(false);
143
- }
144
- }
145
- );
146
- }, [firebaseApp, usersPath]);
147
-
148
- const saveUser = useCallback(async (user: User): Promise<User> => {
149
- if (!firebaseApp) throw Error("useFirestoreUserManagement Firebase not initialised");
150
- const firestore = getFirestore(firebaseApp);
151
- if (!firestore || !usersPath) throw Error("useFirestoreUserManagement Firestore not initialised");
152
- console.debug("Persisting user", user);
153
- const roleIds = user.roles?.map(r => r.id);
154
- const {
155
- uid,
156
- ...userData
157
- } = user;
158
- const data = {
159
- ...userData,
160
- roles: roleIds
161
- };
162
- if (uid) {
163
- return setDoc(doc(firestore, usersPath, uid), data, { merge: true }).then(() => user);
164
- } else {
165
- return addDoc(collection(firestore, usersPath), data).then(() => user);
166
- }
167
- }, [usersPath, firebaseApp]);
168
-
169
- const saveRole = useCallback((role: Role): Promise<void> => {
170
- if (!firebaseApp) throw Error("useFirestoreUserManagement Firebase not initialised");
171
- const firestore = getFirestore(firebaseApp);
172
- if (!firestore || !rolesPath) throw Error("useFirestoreUserManagement Firestore not initialised");
173
- console.debug("Persisting role", role);
174
- const {
175
- id,
176
- ...roleData
177
- } = role;
178
- const ref = doc(firestore, rolesPath, id);
179
- return setDoc(ref, roleData, { merge: true });
180
- }, [rolesPath, firebaseApp]);
181
-
182
- const deleteUser = useCallback(async (user: User): Promise<void> => {
183
- if (!firebaseApp) throw Error("useFirestoreUserManagement Firebase not initialised");
184
- const firestore = getFirestore(firebaseApp);
185
- if (!firestore || !usersPath) throw Error("useFirestoreUserManagement Firestore not initialised");
186
- console.debug("Deleting", user);
187
- const { uid } = user;
188
- return deleteDoc(doc(firestore, usersPath, uid));
189
- }, [usersPath, firebaseApp]);
190
-
191
- const deleteRole = useCallback((role: Role): Promise<void> => {
192
- if (!firebaseApp) throw Error("useFirestoreUserManagement Firebase not initialised");
193
- const firestore = getFirestore(firebaseApp);
194
- if (!firestore || !rolesPath) throw Error("useFirestoreUserManagement Firestore not initialised");
195
- console.debug("Deleting", role);
196
- const { id } = role;
197
- const ref = doc(firestore, rolesPath, id);
198
- return deleteDoc(ref);
199
- }, [rolesPath, firebaseApp]);
200
-
201
- const collectionPermissions: PermissionsBuilder = useCallback(({
202
- collection,
203
- user
204
- }) => resolveUserRolePermissions({
205
- collection,
206
- user
207
- }), []);
208
-
209
- const defineRolesFor: ((user: User) => Role[] | undefined) = useCallback((user) => {
210
- if (!users) throw Error("Users not loaded");
211
- const mgmtUser = users.find(u => u.email?.toLowerCase() === user?.email?.toLowerCase());
212
- return mgmtUser?.roles;
213
- }, [users]);
214
-
215
- const authenticator: Authenticator = useCallback(({ user }) => {
216
- console.debug("Authenticating user", user);
217
-
218
- if (loading) {
219
- console.warn("User management is still loading");
220
- return false;
221
- }
222
-
223
- // This is an example of how you can link the access system to the user management plugin
224
- if (users.length === 0) {
225
- return true; // If there are no users created yet, we allow access to every user
226
- }
227
-
228
- const mgmtUser = users.find(u => u.email?.toLowerCase() === user?.email?.toLowerCase());
229
- if (mgmtUser) {
230
- return true;
231
- }
232
-
233
- throw Error("Could not find a user with the provided email in the user management system.");
234
- }, [loading, users])
235
-
236
- return {
237
- loading,
238
- roles,
239
- users,
240
- saveUser,
241
- saveRole,
242
- rolesError,
243
- deleteUser,
244
- deleteRole,
245
- usersLimit,
246
- usersError,
247
- canEditRoles: canEditRoles === undefined ? true : canEditRoles,
248
- allowDefaultRolesCreation: allowDefaultRolesCreation === undefined ? true : allowDefaultRolesCreation,
249
- includeCollectionConfigPermissions: Boolean(includeCollectionConfigPermissions),
250
- collectionPermissions,
251
- defineRolesFor,
252
- authenticator
253
- }
254
- }
255
-
256
- const docsToUsers = (docs: DocumentSnapshot[]): (UserWithRoleIds)[] => {
257
- return docs.map((doc) => {
258
- const data = doc.data() as any;
259
- const newVar = {
260
- uid: doc.id,
261
- ...data,
262
- created_on: data?.created_on?.toDate(),
263
- updated_on: data?.updated_on?.toDate()
264
- };
265
- return newVar as (UserWithRoleIds);
266
- });
267
- }
268
-
269
- const docsToRoles = (docs: DocumentSnapshot[]): Role[] => {
270
- return docs.map((doc) => ({
271
- id: doc.id,
272
- ...doc.data()
273
- } as Role));
274
- }