@firecms/user_management 3.0.0-canary.15 → 3.0.0-canary.150

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