@firecms/user_management 3.0.0-canary.12 → 3.0.0-canary.120
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/LICENSE +114 -21
- package/dist/hooks/index.d.ts +1 -1
- package/dist/hooks/{useBuildFirestoreUserManagement.d.ts → useBuildUserManagement.d.ts} +14 -7
- package/dist/index.es.js +1170 -817
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +1639 -1
- package/dist/index.umd.js.map +1 -1
- package/dist/types/user_management.d.ts +14 -3
- package/dist/useUserManagementPlugin.d.ts +6 -1
- package/package.json +12 -28
- package/src/components/roles/RolesDetailsForm.tsx +69 -24
- package/src/components/roles/RolesTable.tsx +4 -2
- package/src/components/roles/RolesView.tsx +3 -1
- package/src/components/users/UsersTable.tsx +4 -2
- package/src/hooks/index.ts +1 -1
- package/src/hooks/useBuildUserManagement.tsx +314 -0
- package/src/types/user_management.tsx +17 -3
- package/src/useUserManagementPlugin.tsx +88 -2
- package/src/utils/permissions.ts +3 -2
- package/src/hooks/useBuildFirestoreUserManagement.tsx +0 -240
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.
|
4
|
+
"version": "3.0.0-canary.120",
|
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.
|
34
|
-
"@firecms/formex": "^3.0.0-canary.
|
35
|
-
"@firecms/ui": "^3.0.0-canary.
|
32
|
+
"@firecms/core": "^3.0.0-canary.120",
|
33
|
+
"@firecms/formex": "^3.0.0-canary.120",
|
34
|
+
"@firecms/ui": "^3.0.0-canary.120",
|
36
35
|
"date-fns": "^3.6.0"
|
37
36
|
},
|
38
37
|
"peerDependencies": {
|
39
|
-
"
|
40
|
-
"react": "^18.
|
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.
|
45
|
-
"@types/react": "^18.
|
46
|
-
"@types/react-dom": "^18.
|
47
|
-
"
|
48
|
-
"
|
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.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
|
-
"
|
69
|
-
"extends": [
|
70
|
-
"react-app",
|
71
|
-
"react-app/jest"
|
72
|
-
]
|
73
|
-
},
|
74
|
-
"gitHead": "8bbe4916c2901f1cd63e6f507b844fc527c573c9"
|
58
|
+
"gitHead": "aa817730cb582b5724774a50ac8a512bf0643ef3"
|
75
59
|
}
|
@@ -1,7 +1,7 @@
|
|
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,
|
@@ -30,6 +30,15 @@ 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
|
+
if (!loggedUserIsAdmin) {
|
36
|
+
throw new Error("Only admins can edit roles");
|
37
|
+
}
|
38
|
+
|
39
|
+
return true;
|
40
|
+
}
|
41
|
+
|
33
42
|
export function RolesDetailsForm({
|
34
43
|
open,
|
35
44
|
role,
|
@@ -46,27 +55,39 @@ export function RolesDetailsForm({
|
|
46
55
|
|
47
56
|
const { saveRole } = useUserManagement();
|
48
57
|
const isNewRole = !role;
|
58
|
+
const {
|
59
|
+
user: loggedInUser
|
60
|
+
} = useAuthController();
|
49
61
|
|
50
62
|
const [savingError, setSavingError] = useState<Error | undefined>();
|
51
63
|
|
52
64
|
const onRoleUpdated = useCallback((role: Role) => {
|
53
65
|
setSavingError(undefined);
|
66
|
+
if (!loggedInUser) throw new Error("User not found");
|
67
|
+
canRoleBeEdited(loggedInUser);
|
54
68
|
return saveRole(role);
|
55
|
-
}, [saveRole]);
|
69
|
+
}, [saveRole, loggedInUser]);
|
56
70
|
|
57
71
|
const formex = useCreateFormex({
|
58
72
|
initialValues: role ?? {
|
59
73
|
name: ""
|
60
74
|
} as Role,
|
61
75
|
onSubmit: (role: Role, formexController) => {
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
76
|
+
try {
|
77
|
+
return onRoleUpdated(role)
|
78
|
+
.then(() => {
|
79
|
+
formexController.resetForm({
|
80
|
+
values: role
|
81
|
+
});
|
82
|
+
handleClose();
|
83
|
+
})
|
84
|
+
.catch(e => {
|
85
|
+
setSavingError(e);
|
66
86
|
});
|
67
|
-
|
68
|
-
|
69
|
-
.
|
87
|
+
} catch (e: any) {
|
88
|
+
setSavingError(e);
|
89
|
+
return Promise.resolve();
|
90
|
+
}
|
70
91
|
},
|
71
92
|
validation: (values) => {
|
72
93
|
return RoleYupSchema.validate(values, { abortEarly: false })
|
@@ -168,11 +189,9 @@ export function RolesDetailsForm({
|
|
168
189
|
</div>
|
169
190
|
|
170
191
|
<div className={"col-span-12"}>
|
171
|
-
<Paper
|
172
|
-
|
173
|
-
|
174
|
-
<Table>
|
175
|
-
<TableHeader>
|
192
|
+
<Paper className="bg-inherit overflow-hidden">
|
193
|
+
<Table className={"w-full rounded-md"}>
|
194
|
+
<TableHeader className={"rounded-md"}>
|
176
195
|
<TableCell></TableCell>
|
177
196
|
<TableCell
|
178
197
|
align="center">Create
|
@@ -190,6 +209,9 @@ export function RolesDetailsForm({
|
|
190
209
|
align="center">Delete
|
191
210
|
entities
|
192
211
|
</TableCell>
|
212
|
+
<TableCell
|
213
|
+
align="center">
|
214
|
+
</TableCell>
|
193
215
|
</TableHeader>
|
194
216
|
|
195
217
|
<TableBody>
|
@@ -245,6 +267,9 @@ export function RolesDetailsForm({
|
|
245
267
|
|
246
268
|
</Tooltip>
|
247
269
|
</TableCell>
|
270
|
+
<TableCell
|
271
|
+
align="center">
|
272
|
+
</TableCell>
|
248
273
|
</TableRow>
|
249
274
|
{collections && collections.map((col) => (
|
250
275
|
<TableRow key={col.name}>
|
@@ -256,29 +281,49 @@ export function RolesDetailsForm({
|
|
256
281
|
align="center">
|
257
282
|
<Checkbox
|
258
283
|
disabled={isAdmin || defaultCreate || !editable}
|
259
|
-
checked={(isAdmin || defaultCreate || getIn(values, `collectionPermissions.${col.
|
260
|
-
onCheckedChange={(checked) => setFieldValue(`collectionPermissions.${col.
|
284
|
+
checked={(isAdmin || defaultCreate || getIn(values, `collectionPermissions.${col.id}.create`)) ?? false}
|
285
|
+
onCheckedChange={(checked) => setFieldValue(`collectionPermissions.${col.id}.create`, checked)}/>
|
261
286
|
</TableCell>
|
262
287
|
<TableCell
|
263
288
|
align="center">
|
264
289
|
<Checkbox
|
265
290
|
disabled={isAdmin || defaultRead || !editable}
|
266
|
-
checked={(isAdmin || defaultRead || getIn(values, `collectionPermissions.${col.
|
267
|
-
onCheckedChange={(checked) => setFieldValue(`collectionPermissions.${col.
|
291
|
+
checked={(isAdmin || defaultRead || getIn(values, `collectionPermissions.${col.id}.read`)) ?? false}
|
292
|
+
onCheckedChange={(checked) => setFieldValue(`collectionPermissions.${col.id}.read`, checked)}/>
|
268
293
|
</TableCell>
|
269
294
|
<TableCell
|
270
295
|
align="center">
|
271
296
|
<Checkbox
|
272
297
|
disabled={isAdmin || defaultEdit || !editable}
|
273
|
-
checked={(isAdmin || defaultEdit || getIn(values, `collectionPermissions.${col.
|
274
|
-
onCheckedChange={(checked) => setFieldValue(`collectionPermissions.${col.
|
298
|
+
checked={(isAdmin || defaultEdit || getIn(values, `collectionPermissions.${col.id}.edit`)) ?? false}
|
299
|
+
onCheckedChange={(checked) => setFieldValue(`collectionPermissions.${col.id}.edit`, checked)}/>
|
275
300
|
</TableCell>
|
276
301
|
<TableCell
|
277
302
|
align="center">
|
278
303
|
<Checkbox
|
279
304
|
disabled={isAdmin || defaultDelete || !editable}
|
280
|
-
checked={(isAdmin || defaultDelete || getIn(values, `collectionPermissions.${col.
|
281
|
-
onCheckedChange={(checked) => setFieldValue(`collectionPermissions.${col.
|
305
|
+
checked={(isAdmin || defaultDelete || getIn(values, `collectionPermissions.${col.id}.delete`)) ?? false}
|
306
|
+
onCheckedChange={(checked) => setFieldValue(`collectionPermissions.${col.id}.delete`, checked)}/>
|
307
|
+
</TableCell>
|
308
|
+
|
309
|
+
<TableCell
|
310
|
+
align="center">
|
311
|
+
<Tooltip
|
312
|
+
title="Allow all permissions in this collections">
|
313
|
+
<Button
|
314
|
+
className={"color-inherit"}
|
315
|
+
onClick={() => {
|
316
|
+
setFieldValue(`collectionPermissions.${col.id}.create`, true);
|
317
|
+
setFieldValue(`collectionPermissions.${col.id}.read`, true);
|
318
|
+
setFieldValue(`collectionPermissions.${col.id}.edit`, true);
|
319
|
+
setFieldValue(`collectionPermissions.${col.id}.delete`, true);
|
320
|
+
}}
|
321
|
+
disabled={isAdmin || !editable}
|
322
|
+
variant={"text"}>
|
323
|
+
All
|
324
|
+
</Button>
|
325
|
+
|
326
|
+
</Tooltip>
|
282
327
|
</TableCell>
|
283
328
|
</TableRow>
|
284
329
|
))}
|
@@ -373,8 +418,8 @@ export function RolesDetailsForm({
|
|
373
418
|
</DialogContent>
|
374
419
|
|
375
420
|
<DialogActions position={"sticky"}>
|
376
|
-
{savingError && <Typography className={"text-red-500"}>
|
377
|
-
There was an error saving this role
|
421
|
+
{savingError && <Typography className={"text-red-500 dark:text-red-500"}>
|
422
|
+
{savingError.message ?? "There was an error saving this role"}
|
378
423
|
</Typography>}
|
379
424
|
<Button variant={"text"}
|
380
425
|
onClick={() => {
|
@@ -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
|
64
|
+
<Tooltip
|
65
|
+
asChild={true}
|
66
|
+
title={"Delete this role"}>
|
65
67
|
<IconButton
|
66
68
|
size={"small"}
|
67
69
|
disabled={!editable}
|
@@ -36,7 +36,9 @@ export const RolesView = React.memo(
|
|
36
36
|
component="h4">
|
37
37
|
Roles
|
38
38
|
</Typography>
|
39
|
-
<Tooltip
|
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}
|
@@ -50,7 +50,7 @@ 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>
|
@@ -75,7 +75,9 @@ export function UsersTable({ onUserClicked }: {
|
|
75
75
|
}}
|
76
76
|
>
|
77
77
|
<TableCell className={"w-10"}>
|
78
|
-
<Tooltip
|
78
|
+
<Tooltip
|
79
|
+
asChild={true}
|
80
|
+
title={"Delete this user"}>
|
79
81
|
<IconButton
|
80
82
|
size={"small"}
|
81
83
|
onClick={(event) => {
|
package/src/hooks/index.ts
CHANGED
@@ -1,2 +1,2 @@
|
|
1
|
-
export * from "./
|
1
|
+
export * from "./useBuildUserManagement";
|
2
2
|
export * from "./useUserManagement";
|
@@ -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
|
+
}
|
@@ -1,4 +1,4 @@
|
|
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
|
|
@@ -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
|
*/
|
@@ -39,9 +44,18 @@ export type UserManagement<USER extends User = User> = {
|
|
39
44
|
collectionPermissions: PermissionsBuilder;
|
40
45
|
|
41
46
|
/**
|
42
|
-
* Define the roles for a given user.
|
47
|
+
* Define the roles for a given user. You will typically want to plug this into your auth controller.
|
43
48
|
* @param user
|
44
49
|
*/
|
45
|
-
defineRolesFor: (user: User) => Promise<Role[]> | Role[] | undefined;
|
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;
|
46
60
|
|
47
61
|
};
|