@firecms/user_management 3.0.0-3.0.0-beta.4.pre.1.0
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 +21 -0
- package/README.md +157 -0
- package/dist/UserManagementProvider.d.ts +7 -0
- package/dist/admin_views.d.ts +2 -0
- package/dist/components/index.d.ts +2 -0
- package/dist/components/roles/RoleChip.d.ts +5 -0
- package/dist/components/roles/RolesDetailsForm.d.ts +20 -0
- package/dist/components/roles/RolesTable.d.ts +5 -0
- package/dist/components/roles/RolesView.d.ts +4 -0
- package/dist/components/roles/default_roles.d.ts +2 -0
- package/dist/components/roles/index.d.ts +4 -0
- package/dist/components/users/UserDetailsForm.d.ts +20 -0
- package/dist/components/users/UsersTable.d.ts +4 -0
- package/dist/components/users/UsersView.d.ts +4 -0
- package/dist/components/users/index.d.ts +3 -0
- package/dist/hooks/index.d.ts +2 -0
- package/dist/hooks/useBuildFirestoreUserManagement.d.ts +44 -0
- package/dist/hooks/useUserManagement.d.ts +2 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.es.js +1263 -0
- package/dist/index.es.js.map +1 -0
- package/dist/index.umd.js +2 -0
- package/dist/index.umd.js.map +1 -0
- package/dist/types/firecms_user.d.ts +7 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/roles.d.ts +31 -0
- package/dist/types/user_management.d.ts +39 -0
- package/dist/useUserManagementPlugin.d.ts +5 -0
- package/dist/utils/colors.d.ts +2 -0
- package/dist/utils/index.d.ts +3 -0
- package/dist/utils/local_storage.d.ts +3 -0
- package/dist/utils/permissions.d.ts +9 -0
- package/dist/utils/useTraceUpdate.d.ts +1 -0
- package/package.json +76 -0
- package/src/UserManagementProvider.tsx +19 -0
- package/src/admin_views.tsx +19 -0
- package/src/components/index.ts +2 -0
- package/src/components/roles/RoleChip.tsx +28 -0
- package/src/components/roles/RolesDetailsForm.tsx +402 -0
- package/src/components/roles/RolesTable.tsx +139 -0
- package/src/components/roles/RolesView.tsx +63 -0
- package/src/components/roles/default_roles.tsx +36 -0
- package/src/components/roles/index.ts +4 -0
- package/src/components/users/UserDetailsForm.tsx +230 -0
- package/src/components/users/UsersTable.tsx +178 -0
- package/src/components/users/UsersView.tsx +59 -0
- package/src/components/users/index.ts +3 -0
- package/src/hooks/index.ts +2 -0
- package/src/hooks/useBuildFirestoreUserManagement.tsx +241 -0
- package/src/hooks/useUserManagement.tsx +5 -0
- package/src/index.ts +7 -0
- package/src/types/firecms_user.ts +8 -0
- package/src/types/index.ts +3 -0
- package/src/types/roles.ts +41 -0
- package/src/types/user_management.tsx +50 -0
- package/src/useUserManagementPlugin.tsx +18 -0
- package/src/utils/colors.ts +52 -0
- package/src/utils/index.ts +3 -0
- package/src/utils/local_storage.ts +53 -0
- package/src/utils/permissions.ts +83 -0
- package/src/utils/useTraceUpdate.tsx +23 -0
- package/tailwind.config.js +68 -0
@@ -0,0 +1,59 @@
|
|
1
|
+
import { AddIcon, Button, Container, Typography } from "@firecms/ui";
|
2
|
+
|
3
|
+
import { UsersTable } from "./UsersTable";
|
4
|
+
import { UserDetailsForm } from "./UserDetailsForm";
|
5
|
+
import React, { useCallback, useState } from "react";
|
6
|
+
import { UserWithRoles } from "../../types";
|
7
|
+
import { useUserManagement } from "../../hooks/useUserManagement";
|
8
|
+
|
9
|
+
export const UsersView = function UsersView({ children }: { children?: React.ReactNode }) {
|
10
|
+
|
11
|
+
const [dialogOpen, setDialogOpen] = useState<boolean>();
|
12
|
+
const [selectedUser, setSelectedUser] = useState<UserWithRoles | undefined>();
|
13
|
+
|
14
|
+
const { users, usersLimit } = useUserManagement();
|
15
|
+
|
16
|
+
const reachedUsersLimit = usersLimit !== undefined && (users && users.length >= usersLimit);
|
17
|
+
|
18
|
+
const onUserClicked = useCallback((user: UserWithRoles) => {
|
19
|
+
setSelectedUser(user);
|
20
|
+
setDialogOpen(true);
|
21
|
+
}, []);
|
22
|
+
|
23
|
+
const handleClose = useCallback(() => {
|
24
|
+
setDialogOpen(false);
|
25
|
+
setSelectedUser(undefined);
|
26
|
+
}, []);
|
27
|
+
|
28
|
+
return (
|
29
|
+
<Container className="w-full flex flex-col py-4 gap-4" maxWidth={"6xl"}>
|
30
|
+
|
31
|
+
{children}
|
32
|
+
|
33
|
+
<div
|
34
|
+
className="flex items-center mt-12">
|
35
|
+
<Typography gutterBottom variant="h4"
|
36
|
+
className="flex-grow"
|
37
|
+
component="h4">
|
38
|
+
Users
|
39
|
+
</Typography>
|
40
|
+
<Button
|
41
|
+
size={"large"}
|
42
|
+
disabled={reachedUsersLimit}
|
43
|
+
startIcon={<AddIcon/>}
|
44
|
+
onClick={() => setDialogOpen(true)}>
|
45
|
+
Add user
|
46
|
+
</Button>
|
47
|
+
</div>
|
48
|
+
|
49
|
+
<UsersTable onUserClicked={onUserClicked}/>
|
50
|
+
|
51
|
+
<UserDetailsForm
|
52
|
+
key={selectedUser?.uid ?? "new"}
|
53
|
+
open={dialogOpen ?? false}
|
54
|
+
user={selectedUser}
|
55
|
+
handleClose={handleClose}/>
|
56
|
+
|
57
|
+
</Container>
|
58
|
+
)
|
59
|
+
};
|
@@ -0,0 +1,241 @@
|
|
1
|
+
import React, { useCallback, useEffect, useRef } from "react";
|
2
|
+
import {
|
3
|
+
collection,
|
4
|
+
deleteDoc,
|
5
|
+
doc,
|
6
|
+
DocumentSnapshot,
|
7
|
+
Firestore,
|
8
|
+
getFirestore,
|
9
|
+
onSnapshot,
|
10
|
+
setDoc
|
11
|
+
} from "firebase/firestore";
|
12
|
+
import { FirebaseApp } from "firebase/app";
|
13
|
+
import { Role, UserManagement, UserWithRoles } from "../types";
|
14
|
+
import { AuthController, PermissionsBuilder, 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
|
+
usersLimit?: number;
|
41
|
+
|
42
|
+
canEditRoles?: boolean;
|
43
|
+
|
44
|
+
authController: AuthController;
|
45
|
+
|
46
|
+
/**
|
47
|
+
* If there are no roles in the database, provide a button to create the default roles.
|
48
|
+
*/
|
49
|
+
allowDefaultRolesCreation?: boolean;
|
50
|
+
|
51
|
+
/**
|
52
|
+
* Include the collection config permissions in the user management system.
|
53
|
+
*/
|
54
|
+
includeCollectionConfigPermissions?: boolean;
|
55
|
+
|
56
|
+
}
|
57
|
+
|
58
|
+
/**
|
59
|
+
* This hook is used to build a user management object that can be used to
|
60
|
+
* manage users and roles in a Firestore backend.
|
61
|
+
* @param backendFirebaseApp
|
62
|
+
* @param usersPath
|
63
|
+
* @param rolesPath
|
64
|
+
* @param usersLimit
|
65
|
+
* @param canEditRoles
|
66
|
+
*/
|
67
|
+
export function useBuildFirestoreUserManagement({
|
68
|
+
firebaseApp,
|
69
|
+
usersPath = "__FIRECMS/config/users",
|
70
|
+
rolesPath = "__FIRECMS/config/roles",
|
71
|
+
usersLimit,
|
72
|
+
canEditRoles = true,
|
73
|
+
authController,
|
74
|
+
allowDefaultRolesCreation,
|
75
|
+
includeCollectionConfigPermissions
|
76
|
+
}: UserManagementParams): UserManagement {
|
77
|
+
|
78
|
+
const firestoreRef = useRef<Firestore>();
|
79
|
+
|
80
|
+
const [rolesLoading, setRolesLoading] = React.useState<boolean>(true);
|
81
|
+
const [usersLoading, setUsersLoading] = React.useState<boolean>(true);
|
82
|
+
const [roles, setRoles] = React.useState<Role[]>([]);
|
83
|
+
const [usersWithRoleIds, setUsersWithRoleIds] = React.useState<UserWithRoleIds[]>([]);
|
84
|
+
|
85
|
+
const users = usersWithRoleIds.map(u => ({
|
86
|
+
...u,
|
87
|
+
roles: roles.filter(r => u.roles?.includes(r.id))
|
88
|
+
}) as UserWithRoles);
|
89
|
+
|
90
|
+
const [rolesError, setRolesError] = React.useState<Error | undefined>();
|
91
|
+
const [usersError, setUsersError] = React.useState<Error | undefined>();
|
92
|
+
|
93
|
+
const loading = rolesLoading || usersLoading;
|
94
|
+
|
95
|
+
const loggedInUser: UserWithRoles | undefined = users.find(u => u.email?.toLowerCase() === authController.user?.email?.toLowerCase());
|
96
|
+
// console.log("authController", authController);
|
97
|
+
// if (!loading && !authController.authLoading) {
|
98
|
+
// const user = authController.user;
|
99
|
+
// if (user) {
|
100
|
+
// loggedInUser = users.find(u => u.email?.toLowerCase() === user.email?.toLowerCase());
|
101
|
+
// }
|
102
|
+
// }
|
103
|
+
|
104
|
+
useEffect(() => {
|
105
|
+
if (!firebaseApp) return;
|
106
|
+
firestoreRef.current = getFirestore(firebaseApp);
|
107
|
+
}, [firebaseApp]);
|
108
|
+
|
109
|
+
useEffect(() => {
|
110
|
+
if (!firebaseApp || !rolesPath) return;
|
111
|
+
const firestore = getFirestore(firebaseApp);
|
112
|
+
|
113
|
+
return onSnapshot(collection(firestore, rolesPath),
|
114
|
+
{
|
115
|
+
next: (snapshot) => {
|
116
|
+
setRolesError(undefined);
|
117
|
+
try {
|
118
|
+
const newRoles = docsToRoles(snapshot.docs);
|
119
|
+
setRoles(newRoles);
|
120
|
+
} catch (e) {
|
121
|
+
// console.error(e);
|
122
|
+
setRolesError(e as Error);
|
123
|
+
}
|
124
|
+
setRolesLoading(false);
|
125
|
+
},
|
126
|
+
error: (e) => {
|
127
|
+
setRolesError(e);
|
128
|
+
setRolesLoading(false);
|
129
|
+
}
|
130
|
+
}
|
131
|
+
);
|
132
|
+
}, [firebaseApp, rolesPath]);
|
133
|
+
|
134
|
+
useEffect(() => {
|
135
|
+
if (!firebaseApp || !usersPath) return;
|
136
|
+
const firestore = getFirestore(firebaseApp);
|
137
|
+
|
138
|
+
return onSnapshot(collection(firestore, usersPath),
|
139
|
+
{
|
140
|
+
next: (snapshot) => {
|
141
|
+
setUsersError(undefined);
|
142
|
+
try {
|
143
|
+
const newUsers = docsToUsers(snapshot.docs);
|
144
|
+
setUsersWithRoleIds(newUsers);
|
145
|
+
} catch (e) {
|
146
|
+
setUsersError(e as Error);
|
147
|
+
}
|
148
|
+
setUsersLoading(false);
|
149
|
+
},
|
150
|
+
error: (e) => {
|
151
|
+
setUsersError(e);
|
152
|
+
setUsersLoading(false);
|
153
|
+
}
|
154
|
+
}
|
155
|
+
);
|
156
|
+
}, [firebaseApp, usersPath]);
|
157
|
+
|
158
|
+
const saveUser = useCallback(async (user: UserWithRoles): Promise<UserWithRoles> => {
|
159
|
+
const firestore = firestoreRef.current;
|
160
|
+
if (!firestore || !usersPath) throw Error("useFirestoreConfigurationPersistence Firestore not initialised");
|
161
|
+
console.debug("Persisting user", user);
|
162
|
+
const roleIds = user.roles.map(r => r.id);
|
163
|
+
const {
|
164
|
+
uid,
|
165
|
+
...userData
|
166
|
+
} = user;
|
167
|
+
return setDoc(doc(firestore, usersPath, uid), { ...userData, roles: roleIds }, { merge: true }).then(() => user);
|
168
|
+
}, [usersPath]);
|
169
|
+
|
170
|
+
const saveRole = useCallback((role: Role): Promise<void> => {
|
171
|
+
const firestore = firestoreRef.current;
|
172
|
+
if (!firestore || !rolesPath) throw Error("useFirestoreConfigurationPersistence 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]);
|
181
|
+
|
182
|
+
const deleteUser = useCallback(async (user: UserWithRoles): Promise<void> => {
|
183
|
+
const firestore = firestoreRef.current;
|
184
|
+
if (!firestore || !usersPath) throw Error("useFirestoreConfigurationPersistence Firestore not initialised");
|
185
|
+
console.debug("Deleting", user);
|
186
|
+
const { uid } = user;
|
187
|
+
return deleteDoc(doc(firestore, usersPath, uid));
|
188
|
+
}, [usersPath]);
|
189
|
+
|
190
|
+
const deleteRole = useCallback((role: Role): Promise<void> => {
|
191
|
+
const firestore = firestoreRef.current;
|
192
|
+
if (!firestore || !rolesPath) throw Error("useFirestoreConfigurationPersistence Firestore not initialised");
|
193
|
+
console.debug("Deleting", role);
|
194
|
+
const { id } = role;
|
195
|
+
const ref = doc(firestore, rolesPath, id);
|
196
|
+
return deleteDoc(ref);
|
197
|
+
}, [rolesPath]);
|
198
|
+
|
199
|
+
const collectionPermissions: PermissionsBuilder = useCallback(({
|
200
|
+
collection,
|
201
|
+
}) => resolveUserRolePermissions({
|
202
|
+
collection,
|
203
|
+
user: loggedInUser ?? null
|
204
|
+
}), [loggedInUser?.uid]);
|
205
|
+
|
206
|
+
return {
|
207
|
+
loading,
|
208
|
+
loggedInUser,
|
209
|
+
roles,
|
210
|
+
users,
|
211
|
+
saveUser,
|
212
|
+
saveRole,
|
213
|
+
deleteUser,
|
214
|
+
deleteRole,
|
215
|
+
usersLimit,
|
216
|
+
canEditRoles: canEditRoles === undefined ? true : canEditRoles,
|
217
|
+
allowDefaultRolesCreation: allowDefaultRolesCreation === undefined ? true : allowDefaultRolesCreation,
|
218
|
+
includeCollectionConfigPermissions: Boolean(includeCollectionConfigPermissions),
|
219
|
+
collectionPermissions
|
220
|
+
}
|
221
|
+
}
|
222
|
+
|
223
|
+
const docsToUsers = (docs: DocumentSnapshot[]): (UserWithRoleIds)[] => {
|
224
|
+
return docs.map((doc) => {
|
225
|
+
const data = doc.data() as any;
|
226
|
+
const newVar = {
|
227
|
+
uid: doc.id,
|
228
|
+
...data,
|
229
|
+
created_on: data?.created_on?.toDate(),
|
230
|
+
updated_on: data?.updated_on?.toDate()
|
231
|
+
};
|
232
|
+
return newVar as (UserWithRoleIds);
|
233
|
+
});
|
234
|
+
}
|
235
|
+
|
236
|
+
const docsToRoles = (docs: DocumentSnapshot[]): Role[] => {
|
237
|
+
return docs.map((doc) => ({
|
238
|
+
id: doc.id,
|
239
|
+
...doc.data()
|
240
|
+
} as Role));
|
241
|
+
}
|
package/src/index.ts
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
import { Permissions } from "@firecms/core";
|
2
|
+
|
3
|
+
export type Role = {
|
4
|
+
|
5
|
+
/**
|
6
|
+
* ID of the role
|
7
|
+
*/
|
8
|
+
id: string;
|
9
|
+
|
10
|
+
/**
|
11
|
+
* Name of the role
|
12
|
+
*/
|
13
|
+
name: string;
|
14
|
+
|
15
|
+
/**
|
16
|
+
* If this flag is true, the user can perform any action
|
17
|
+
*/
|
18
|
+
isAdmin?: boolean;
|
19
|
+
|
20
|
+
/**
|
21
|
+
* Default permissions for all collections for this role.
|
22
|
+
* You can override this values at the collection level using
|
23
|
+
* {@link collectionPermissions}
|
24
|
+
*/
|
25
|
+
defaultPermissions?: Permissions;
|
26
|
+
|
27
|
+
/**
|
28
|
+
* Record of stripped collection ids to their permissions.
|
29
|
+
* @see stripCollectionPath
|
30
|
+
*/
|
31
|
+
collectionPermissions?: Record<string, Permissions>;
|
32
|
+
|
33
|
+
config?: {
|
34
|
+
|
35
|
+
createCollections?: boolean;
|
36
|
+
|
37
|
+
editCollections?: boolean | "own";
|
38
|
+
|
39
|
+
deleteCollections?: boolean | "own";
|
40
|
+
}
|
41
|
+
}
|
@@ -0,0 +1,50 @@
|
|
1
|
+
import { UserWithRoles } from "./firecms_user";
|
2
|
+
import { Role } from "./roles";
|
3
|
+
import { PermissionsBuilder } from "@firecms/core";
|
4
|
+
|
5
|
+
export type UserManagement<USER extends UserWithRoles = UserWithRoles> = {
|
6
|
+
|
7
|
+
loading: boolean;
|
8
|
+
|
9
|
+
/**
|
10
|
+
* The user currently logged in, in the user management system.
|
11
|
+
* This is the same user that is logged in the Authenticator, but with the roles
|
12
|
+
* and permissions loaded.
|
13
|
+
*/
|
14
|
+
loggedInUser: USER | undefined;
|
15
|
+
|
16
|
+
users: USER[];
|
17
|
+
saveUser: (user: USER) => Promise<USER>;
|
18
|
+
deleteUser: (user: USER) => Promise<void>;
|
19
|
+
|
20
|
+
roles: Role[];
|
21
|
+
saveRole: (role: Role) => Promise<void>;
|
22
|
+
deleteRole: (role: Role) => Promise<void>;
|
23
|
+
|
24
|
+
/**
|
25
|
+
* Maximum number of users that can be created.
|
26
|
+
*/
|
27
|
+
usersLimit?: number;
|
28
|
+
|
29
|
+
/**
|
30
|
+
* Can the logged user edit roles?
|
31
|
+
*/
|
32
|
+
canEditRoles?: boolean;
|
33
|
+
|
34
|
+
/**
|
35
|
+
* Include a button to create default roles, in case there are no roles in the system.
|
36
|
+
*/
|
37
|
+
allowDefaultRolesCreation?: boolean;
|
38
|
+
|
39
|
+
/**
|
40
|
+
* Include the collection config permissions in the user management system.
|
41
|
+
*/
|
42
|
+
includeCollectionConfigPermissions?: boolean;
|
43
|
+
|
44
|
+
/**
|
45
|
+
* Get a permissions builder that defines which operations can be performed by a user in a collection.
|
46
|
+
* The permission builder generated should be based in the user roles and the collection config.
|
47
|
+
*/
|
48
|
+
collectionPermissions: PermissionsBuilder;
|
49
|
+
|
50
|
+
};
|
@@ -0,0 +1,18 @@
|
|
1
|
+
import { AuthController, FireCMSPlugin } from "@firecms/core";
|
2
|
+
import { UserManagementProvider } from "./UserManagementProvider";
|
3
|
+
import { UserManagement } from "./types";
|
4
|
+
|
5
|
+
export function useUserManagementPlugin({ userManagement }: {
|
6
|
+
userManagement: UserManagement,
|
7
|
+
}): FireCMSPlugin {
|
8
|
+
return {
|
9
|
+
name: "User management plugin",
|
10
|
+
loading: userManagement.loading,
|
11
|
+
provider: {
|
12
|
+
Component: UserManagementProvider,
|
13
|
+
props: {
|
14
|
+
userManagement
|
15
|
+
}
|
16
|
+
}
|
17
|
+
}
|
18
|
+
}
|
@@ -0,0 +1,52 @@
|
|
1
|
+
export function darkenColor(hexColor: string, darkenBy = 10): string {
|
2
|
+
// Check input validity
|
3
|
+
if (!/^#([0-9A-Fa-f]{3}){1,2}$/.test(hexColor)) {
|
4
|
+
throw new Error("Invalid color format");
|
5
|
+
}
|
6
|
+
|
7
|
+
// If shorthand form, convert to full form
|
8
|
+
let color = hexColor.substring(1).split("");
|
9
|
+
if (color.length === 3) {
|
10
|
+
color = [color[0], color[0], color[1], color[1], color[2], color[2]];
|
11
|
+
}
|
12
|
+
|
13
|
+
// Convert to RGB values
|
14
|
+
let r = parseInt(color[0] + color[1], 16);
|
15
|
+
let g = parseInt(color[2] + color[3], 16);
|
16
|
+
let b = parseInt(color[4] + color[5], 16);
|
17
|
+
|
18
|
+
// Reduce each color component by the specified percentage (darkenBy)
|
19
|
+
r = Math.floor(r * (1 - darkenBy / 100));
|
20
|
+
g = Math.floor(g * (1 - darkenBy / 100));
|
21
|
+
b = Math.floor(b * (1 - darkenBy / 100));
|
22
|
+
|
23
|
+
// Recombine into hex and return
|
24
|
+
return "#" +
|
25
|
+
(r < 16 ? "0" : "") + r.toString(16) +
|
26
|
+
(g < 16 ? "0" : "") + g.toString(16) +
|
27
|
+
(b < 16 ? "0" : "") + b.toString(16);
|
28
|
+
}
|
29
|
+
|
30
|
+
export function hexToRgbaWithOpacity(hexColor: string, opacity = 10): string {
|
31
|
+
// Check input validity
|
32
|
+
if (!/^#([0-9A-Fa-f]{3}){1,2}$/.test(hexColor)) {
|
33
|
+
throw new Error("Invalid color format");
|
34
|
+
}
|
35
|
+
|
36
|
+
// If shorthand form, convert to full form
|
37
|
+
let color = hexColor.substring(1).split("");
|
38
|
+
if (color.length === 3) {
|
39
|
+
color = [color[0], color[0], color[1], color[1], color[2], color[2]];
|
40
|
+
}
|
41
|
+
|
42
|
+
// Convert to RGB values
|
43
|
+
const r = parseInt(color[0] + color[1], 16);
|
44
|
+
const g = parseInt(color[2] + color[3], 16);
|
45
|
+
const b = parseInt(color[4] + color[5], 16);
|
46
|
+
|
47
|
+
// Convert opacity to a decimal for CSS
|
48
|
+
const alpha = opacity / 100;
|
49
|
+
|
50
|
+
// Construct and return the RGBA color
|
51
|
+
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
52
|
+
}
|
@@ -0,0 +1,53 @@
|
|
1
|
+
// const tokens = new Map<string, {
|
2
|
+
// token: string,
|
3
|
+
// expiry: Date
|
4
|
+
// }>();
|
5
|
+
|
6
|
+
export function cacheDelegatedLoginToken(projectId: string, delegatedToken?: string) {
|
7
|
+
if (!delegatedToken) {
|
8
|
+
return;
|
9
|
+
}
|
10
|
+
|
11
|
+
const data = parseJwt(delegatedToken);
|
12
|
+
// @ts-ignore
|
13
|
+
const expiry = new Date(data.exp * 1000);
|
14
|
+
localStorage.setItem(`auth_token::${projectId}`, JSON.stringify({
|
15
|
+
token: delegatedToken,
|
16
|
+
expiry
|
17
|
+
}));
|
18
|
+
|
19
|
+
}
|
20
|
+
|
21
|
+
export function getDelegatedLoginTokenFromCache(projectId: string) {
|
22
|
+
const entry = localStorage.getItem(`auth_token::${projectId}`);
|
23
|
+
if (entry) {
|
24
|
+
const data = JSON.parse(entry);
|
25
|
+
data.expiry = new Date(data.expiry);
|
26
|
+
if (data.expiry > new Date()) {
|
27
|
+
return data.token;
|
28
|
+
}
|
29
|
+
}
|
30
|
+
return undefined;
|
31
|
+
}
|
32
|
+
|
33
|
+
export function clearDelegatedLoginTokensCache() {
|
34
|
+
for (let i = 0; i < localStorage.length; i++) {
|
35
|
+
const key = localStorage.key(i);
|
36
|
+
if (key?.startsWith("auth_token::")) {
|
37
|
+
localStorage.removeItem(key);
|
38
|
+
}
|
39
|
+
}
|
40
|
+
}
|
41
|
+
|
42
|
+
function parseJwt(token?: string): object {
|
43
|
+
if (!token) {
|
44
|
+
throw new Error("No JWT token");
|
45
|
+
}
|
46
|
+
const base64Url = token.split(".")[1];
|
47
|
+
const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/");
|
48
|
+
const jsonPayload = decodeURIComponent(window.atob(base64).split("").map(function (c) {
|
49
|
+
return "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2);
|
50
|
+
}).join(""));
|
51
|
+
|
52
|
+
return JSON.parse(jsonPayload);
|
53
|
+
}
|
@@ -0,0 +1,83 @@
|
|
1
|
+
import { CMSType, EntityCollection, Permissions } from "@firecms/core";
|
2
|
+
import { Role, UserWithRoles } from "../types";
|
3
|
+
|
4
|
+
export const RESERVED_GROUPS = ["Admin"];
|
5
|
+
|
6
|
+
const DEFAULT_PERMISSIONS = {
|
7
|
+
read: false,
|
8
|
+
edit: false,
|
9
|
+
create: false,
|
10
|
+
delete: false
|
11
|
+
};
|
12
|
+
|
13
|
+
export function resolveUserRolePermissions<UserType extends UserWithRoles>
|
14
|
+
({ collection, user }: {
|
15
|
+
collection: EntityCollection<any>,
|
16
|
+
user: UserType | null
|
17
|
+
}): Permissions {
|
18
|
+
|
19
|
+
const roles = user?.roles;
|
20
|
+
if (!roles) {
|
21
|
+
return DEFAULT_PERMISSIONS;
|
22
|
+
} else if (collection.ownerId === user?.uid) {
|
23
|
+
return {
|
24
|
+
read: true,
|
25
|
+
create: true,
|
26
|
+
edit: true,
|
27
|
+
delete: true
|
28
|
+
};
|
29
|
+
} else {
|
30
|
+
const basePermissions = {
|
31
|
+
read: false,
|
32
|
+
create: false,
|
33
|
+
edit: false,
|
34
|
+
delete: false
|
35
|
+
};
|
36
|
+
|
37
|
+
return roles
|
38
|
+
.map(role => resolveCollectionRole(role, collection.id))
|
39
|
+
.reduce(mergePermissions, basePermissions);
|
40
|
+
}
|
41
|
+
}
|
42
|
+
|
43
|
+
function resolveCollectionRole(role: Role, id: string): Permissions {
|
44
|
+
|
45
|
+
const basePermissions = {
|
46
|
+
read: role.isAdmin || role.defaultPermissions?.read,
|
47
|
+
create: role.isAdmin || role.defaultPermissions?.create,
|
48
|
+
edit: role.isAdmin || role.defaultPermissions?.edit,
|
49
|
+
delete: role.isAdmin || role.defaultPermissions?.delete
|
50
|
+
};
|
51
|
+
if (role.collectionPermissions && role.collectionPermissions[id]) {
|
52
|
+
return mergePermissions(role.collectionPermissions[id], basePermissions);
|
53
|
+
} else if (role.defaultPermissions) {
|
54
|
+
return mergePermissions(role.defaultPermissions, basePermissions);
|
55
|
+
} else {
|
56
|
+
return basePermissions;
|
57
|
+
}
|
58
|
+
}
|
59
|
+
|
60
|
+
const mergePermissions = (permA: Permissions, permB: Permissions) => {
|
61
|
+
return {
|
62
|
+
read: permA.read || permB.read,
|
63
|
+
create: permA.create || permB.create,
|
64
|
+
edit: permA.edit || permB.edit,
|
65
|
+
delete: permA.delete || permB.delete
|
66
|
+
};
|
67
|
+
}
|
68
|
+
|
69
|
+
export function getUserRoles(roles: Role[], fireCMSUser: UserWithRoles): Role[] | undefined {
|
70
|
+
return !roles
|
71
|
+
? undefined
|
72
|
+
: (fireCMSUser.roles
|
73
|
+
? fireCMSUser.roles
|
74
|
+
.map(role => roles.find((r) => r.id === role.id))
|
75
|
+
.filter(Boolean) as Role[]
|
76
|
+
: []);
|
77
|
+
}
|
78
|
+
|
79
|
+
export const areRolesEqual = (rolesA: Role[], rolesB: Role[]) => {
|
80
|
+
const rolesAIds = rolesA.map(r => r.id);
|
81
|
+
const rolesBIds = rolesB.map(r => r.id);
|
82
|
+
return rolesAIds.length === rolesB.length && rolesAIds.every((role) => rolesBIds.includes(role));
|
83
|
+
}
|
@@ -0,0 +1,23 @@
|
|
1
|
+
import { useEffect, useRef } from "react";
|
2
|
+
|
3
|
+
function printChanged(props: any, prev: any, path = "", depth = 0) {
|
4
|
+
if (depth > 10) {
|
5
|
+
return;
|
6
|
+
}
|
7
|
+
if (props && prev && typeof props === "object" && typeof prev === "object") {
|
8
|
+
Object.keys(props).forEach((key) => {
|
9
|
+
printChanged(props[key], prev[key], path + "." + key, depth + 1);
|
10
|
+
});
|
11
|
+
} else if (props !== prev) {
|
12
|
+
console.log("Changed props:", path);
|
13
|
+
}
|
14
|
+
|
15
|
+
}
|
16
|
+
|
17
|
+
export function useTraceUpdate(props: any) {
|
18
|
+
const prev = useRef(props);
|
19
|
+
useEffect(() => {
|
20
|
+
printChanged(props, prev.current, "");
|
21
|
+
prev.current = props;
|
22
|
+
});
|
23
|
+
}
|