@zealamic/payload-auth-rbac-plugin 1.0.0 → 1.0.1

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.
Files changed (52) hide show
  1. package/dist/components/role-permission-matrix-client/default-data.js +2 -2
  2. package/dist/components/role-permission-matrix-client/default-data.js.map +1 -1
  3. package/dist/components/role-permission-matrix-client/index.js +2 -2
  4. package/dist/components/role-permission-matrix-client/index.js.map +1 -1
  5. package/dist/components/role-permission-matrix-client/matrix.module.scss +1 -4
  6. package/dist/components/role-permission-matrix-client/types.d.ts +4 -6
  7. package/dist/components/role-permission-matrix-client/types.js.map +1 -1
  8. package/docs/TRANSLATIONS.md +12 -5
  9. package/package.json +4 -27
  10. package/src/collections/permission-actions/default-data.ts +36 -0
  11. package/src/collections/permission-actions/index.ts +144 -0
  12. package/src/collections/permission-actions/types.ts +56 -0
  13. package/src/collections/permission-features/default-data.ts +30 -0
  14. package/src/collections/permission-features/index.ts +122 -0
  15. package/src/collections/permission-features/types.ts +47 -0
  16. package/src/collections/permissions/default-data.ts +38 -0
  17. package/src/collections/permissions/index.ts +160 -0
  18. package/src/collections/permissions/types.ts +57 -0
  19. package/src/collections/roles/default-data.ts +44 -0
  20. package/src/collections/roles/hooks/sync-permission-matrix-draft.ts +73 -0
  21. package/src/collections/roles/index.ts +178 -0
  22. package/src/collections/roles/types.ts +56 -0
  23. package/src/collections/roles-permissions/default-data.ts +28 -0
  24. package/src/collections/roles-permissions/index.ts +107 -0
  25. package/src/collections/roles-permissions/types.ts +42 -0
  26. package/src/collections/users/default-data.ts +19 -0
  27. package/src/collections/users/index.ts +148 -0
  28. package/src/collections/users/parent-path.ts +310 -0
  29. package/src/collections/users/types.ts +25 -0
  30. package/src/components/role-permission-matrix-client/default-data.ts +25 -0
  31. package/src/components/role-permission-matrix-client/index.tsx +369 -0
  32. package/src/components/role-permission-matrix-client/matrix.module.scss +66 -0
  33. package/src/components/role-permission-matrix-client/types.ts +16 -0
  34. package/src/endpoints/customEndpointHandler.ts +5 -0
  35. package/src/exports/client.ts +1 -0
  36. package/src/exports/rsc.ts +0 -0
  37. package/src/general-types.d.ts +5 -0
  38. package/src/index.ts +249 -0
  39. package/src/lib/constants/general.ts +1 -0
  40. package/src/lib/constants/index.ts +15 -0
  41. package/src/lib/constants/permission-action.ts +9 -0
  42. package/src/lib/constants/permission-feature.ts +4 -0
  43. package/src/lib/constants/permission.ts +4 -0
  44. package/src/lib/constants/role.ts +10 -0
  45. package/src/lib/constants/user.ts +1 -0
  46. package/src/lib/utils/access.ts +611 -0
  47. package/src/lib/utils/data.ts +7 -0
  48. package/src/lib/utils/fields.ts +62 -0
  49. package/src/lib/utils/index.ts +4 -0
  50. package/src/lib/utils/localization.ts +106 -0
  51. package/src/styles/variables.scss +1 -0
  52. package/src/types.ts +64 -0
@@ -0,0 +1,369 @@
1
+ "use client";
2
+
3
+ import {
4
+ useConfig,
5
+ useDocumentInfo,
6
+ useField,
7
+ useTranslation,
8
+ } from "@payloadcms/ui";
9
+ import { Fragment, useEffect, useId, useMemo, useRef, useState } from "react";
10
+ import type { PermissionAction } from "../../collections/permission-actions/types.js";
11
+ import type { PermissionFeature } from "../../collections/permission-features/types.js";
12
+ import type { Permission } from "../../collections/permissions/types.js";
13
+ import type { RolePermission } from "../../collections/roles-permissions/types.js";
14
+ import { STATUS as PERMISSION_STATUS } from "../../lib/constants/permission.js";
15
+ import {
16
+ STATUS as PERMISSION_ACTION_STATUS,
17
+ TYPE,
18
+ } from "../../lib/constants/permission-action.js";
19
+ import { STATUS as PERMISSION_FEATURE_STATUS } from "../../lib/constants/permission-feature.js";
20
+ import { toID } from "../../lib/utils/data.js";
21
+
22
+ import styles from "./matrix.module.scss";
23
+
24
+ const RBAC_PREFIX = "rbac";
25
+
26
+ type ApiListResponse<T> = {
27
+ docs?: T[];
28
+ };
29
+
30
+ export const RolePermissionMatrixClient = () => {
31
+ const checkboxId = useId();
32
+ const { config } = useConfig();
33
+ const { hasSavePermission, id } = useDocumentInfo();
34
+ const { setValue, value } = useField<Record<string, boolean> | null>({
35
+ path: "permissionMatrixDraft",
36
+ });
37
+ const isReadOnly = !hasSavePermission;
38
+
39
+ const [features, setFeatures] = useState<PermissionFeature[]>([]);
40
+ const [actions, setActions] = useState<PermissionAction[]>([]);
41
+ const [permissions, setPermissions] = useState<Permission[]>([]);
42
+ const [rolePermissions, setRolePermissions] = useState<RolePermission[]>([]);
43
+ const [loading, setLoading] = useState(true);
44
+ const { t } = useTranslation();
45
+ const seededForRoleIdRef = useRef<string | null>(null);
46
+
47
+ useEffect(() => {
48
+ const run = async () => {
49
+ setLoading(true);
50
+ try {
51
+ const base = config?.routes?.api || "/api";
52
+
53
+ const [featuresRes, actionsRes, permissionsRes, rolePermissionsRes] =
54
+ await Promise.all([
55
+ fetch(
56
+ `${base}/permission-features?limit=0&depth=0&where[status][equals]=${PERMISSION_FEATURE_STATUS.ACTIVE}`,
57
+ { credentials: "include" },
58
+ ),
59
+ fetch(
60
+ `${base}/permission-actions?limit=0&depth=0&where[status][equals]=${PERMISSION_ACTION_STATUS.ACTIVE}`,
61
+ { credentials: "include" },
62
+ ),
63
+ fetch(
64
+ `${base}/permissions?limit=0&depth=1&where[status][equals]=${PERMISSION_STATUS.ACTIVE}`,
65
+ { credentials: "include" },
66
+ ),
67
+ id
68
+ ? fetch(
69
+ `${base}/roles-permissions?limit=0&depth=0&where[role][equals]=${id}`,
70
+ {
71
+ credentials: "include",
72
+ },
73
+ )
74
+ : Promise.resolve(new Response(JSON.stringify({ docs: [] }))),
75
+ ]);
76
+
77
+ const featuresJson =
78
+ (await featuresRes.json()) as ApiListResponse<PermissionFeature>;
79
+ const actionsJson =
80
+ (await actionsRes.json()) as ApiListResponse<PermissionAction>;
81
+ const permissionsJson =
82
+ (await permissionsRes.json()) as ApiListResponse<Permission>;
83
+ const rolePermissionsJson =
84
+ (await rolePermissionsRes.json()) as ApiListResponse<RolePermission>;
85
+
86
+ setFeatures(
87
+ featuresJson.docs?.sort(
88
+ (a, b) => (a?.sortOrder ?? 0) - (b?.sortOrder ?? 0),
89
+ ) || [],
90
+ );
91
+ setActions(
92
+ actionsJson.docs?.sort(
93
+ (a, b) => (a?.sortOrder ?? 0) - (b?.sortOrder ?? 0),
94
+ ) || [],
95
+ );
96
+ setPermissions(permissionsJson.docs || []);
97
+ setRolePermissions(rolePermissionsJson.docs || []);
98
+ } finally {
99
+ setLoading(false);
100
+ }
101
+ };
102
+
103
+ void run();
104
+ }, [config?.routes?.api, id]);
105
+
106
+ const enabledByPermissionID = useMemo(() => {
107
+ const map = new Map<string, boolean>();
108
+ for (const row of rolePermissions) {
109
+ map.set(toID(row.permission), Boolean(row.enabled));
110
+ }
111
+ return map;
112
+ }, [rolePermissions]);
113
+
114
+ const draftValue = (
115
+ value && typeof value === "object" && !Array.isArray(value) ? value : {}
116
+ ) as Record<string, boolean>;
117
+
118
+ useEffect(() => {
119
+ if (!id || loading) {
120
+ return;
121
+ }
122
+
123
+ const roleId = String(id);
124
+ if (seededForRoleIdRef.current === roleId) {
125
+ return;
126
+ }
127
+
128
+ const fromRolesPermissions: Record<string, boolean> = {};
129
+ for (const [permissionID, enabled] of enabledByPermissionID.entries()) {
130
+ if (permissionID) {
131
+ fromRolesPermissions[permissionID] = enabled;
132
+ }
133
+ }
134
+
135
+ const fromDocument =
136
+ value && typeof value === "object" && !Array.isArray(value)
137
+ ? (value as Record<string, boolean>)
138
+ : {};
139
+
140
+ const hasRolesPermissions = Object.keys(fromRolesPermissions).length > 0;
141
+ const hasDocumentDraft = Object.keys(fromDocument).length > 0;
142
+
143
+ if (!hasRolesPermissions && !hasDocumentDraft) {
144
+ return;
145
+ }
146
+
147
+ seededForRoleIdRef.current = roleId;
148
+ setValue({
149
+ ...fromRolesPermissions,
150
+ ...fromDocument,
151
+ });
152
+ }, [enabledByPermissionID, id, loading, setValue, value]);
153
+
154
+ if (!id) {
155
+ return (
156
+ <div className={styles[`${RBAC_PREFIX}-component-placeholder`]}>
157
+ {t(
158
+ `components:rolePermissionMatrix:viewInUpdateScreenOnly:label` as Parameters<
159
+ typeof t
160
+ >[0],
161
+ )}
162
+ </div>
163
+ );
164
+ }
165
+
166
+ if (loading) {
167
+ return (
168
+ <div className={styles[`${RBAC_PREFIX}-component-placeholder`]}>
169
+ {t(
170
+ `components:rolePermissionMatrix:loading:placeholder` as Parameters<
171
+ typeof t
172
+ >[0],
173
+ )}
174
+ </div>
175
+ );
176
+ }
177
+
178
+ return (
179
+ <div>
180
+ <div className={styles[`${RBAC_PREFIX}-component-title`]}>
181
+ {t(`components:rolePermissionMatrix:title` as Parameters<typeof t>[0])}
182
+ </div>
183
+
184
+ <div className={styles[`${RBAC_PREFIX}-table-container`]}>
185
+ <table className={styles[`${RBAC_PREFIX}-table`]}>
186
+ <thead>
187
+ <tr>
188
+ <th className={styles[`${RBAC_PREFIX}-table-th-feature`]}>
189
+ {t(
190
+ `components:rolePermissionMatrix:featuresLabel` as Parameters<
191
+ typeof t
192
+ >[0],
193
+ )}
194
+ </th>
195
+ <th
196
+ className={styles[`${RBAC_PREFIX}-table-th-action`]}
197
+ colSpan={
198
+ actions.filter((action) => action.type === TYPE.MAIN).length
199
+ }
200
+ >
201
+ {t(
202
+ `components:rolePermissionMatrix:actionsLabel` as Parameters<
203
+ typeof t
204
+ >[0],
205
+ )}
206
+ </th>
207
+ </tr>
208
+ </thead>
209
+ <tbody>
210
+ {features.map((feature) => {
211
+ const mainActions = actions.filter(
212
+ (action) => action.type === TYPE.MAIN,
213
+ );
214
+ const subActions = actions.filter(
215
+ (action) =>
216
+ action.type === TYPE.SUB &&
217
+ permissions.some(
218
+ (permission) =>
219
+ toID(permission.permissionAction) === String(action.id) &&
220
+ toID(permission.permissionFeature) === String(feature.id),
221
+ ),
222
+ );
223
+ const isSubActionInPermission = subActions.length > 0;
224
+
225
+ return (
226
+ <Fragment key={String(feature.id)}>
227
+ <tr>
228
+ <td className={styles[`${RBAC_PREFIX}-table-td-feature`]}>
229
+ {t(
230
+ `components:rolePermissionMatrix:features:${feature.code}` as Parameters<
231
+ typeof t
232
+ >[0],
233
+ ) || feature.id}
234
+ </td>
235
+
236
+ {mainActions.map((action) => {
237
+ const matchedPermission = permissions.find(
238
+ (permission) =>
239
+ toID(permission.permissionFeature) ===
240
+ String(feature.id) &&
241
+ toID(permission.permissionAction) ===
242
+ String(action.id),
243
+ );
244
+
245
+ if (!matchedPermission) {
246
+ return (
247
+ <td
248
+ key={`${feature.id}-${action.id}`}
249
+ className={styles[`${RBAC_PREFIX}-table-td-action`]}
250
+ >
251
+ -
252
+ </td>
253
+ );
254
+ }
255
+
256
+ const permissionID = String(matchedPermission.id);
257
+ const checked =
258
+ typeof draftValue[permissionID] === "boolean"
259
+ ? draftValue[permissionID]
260
+ : (enabledByPermissionID.get(permissionID) ?? false);
261
+
262
+ return (
263
+ <td
264
+ key={`${feature.id}-${action.id}`}
265
+ className={styles[`${RBAC_PREFIX}-table-td-action`]}
266
+ >
267
+ <div
268
+ className={
269
+ styles[`${RBAC_PREFIX}-table-td-action-container`]
270
+ }
271
+ >
272
+ <input
273
+ type="checkbox"
274
+ id={`permission-matrix-checkbox-${checkboxId}-${feature.id}-${action.id}`}
275
+ name={`permission-matrix-checkbox-${checkboxId}-${feature.id}-${action.id}`}
276
+ checked={checked}
277
+ disabled={isReadOnly}
278
+ onChange={(event) => {
279
+ setValue({
280
+ ...draftValue,
281
+ [permissionID]: event.target.checked,
282
+ });
283
+ }}
284
+ className={
285
+ styles[`${RBAC_PREFIX}-table-td-action-input`]
286
+ }
287
+ />
288
+ <label
289
+ htmlFor={`permission-matrix-checkbox-${checkboxId}-${feature.id}-${action.id}`}
290
+ className={
291
+ styles[`${RBAC_PREFIX}-table-td-action-label`]
292
+ }
293
+ >
294
+ {t(
295
+ `components:rolePermissionMatrix:actions:${action.code}` as Parameters<
296
+ typeof t
297
+ >[0],
298
+ ) || action.id}
299
+ </label>
300
+ </div>
301
+ </td>
302
+ );
303
+ })}
304
+ </tr>
305
+
306
+ {isSubActionInPermission && (
307
+ <tr>
308
+ <td
309
+ className={styles[`${RBAC_PREFIX}-table-td-feature`]}
310
+ ></td>
311
+ <td
312
+ className={styles[`${RBAC_PREFIX}-table-td-action`]}
313
+ colSpan={mainActions.length}
314
+ >
315
+ {subActions.map((action) => {
316
+ const matchedPermission = permissions.find(
317
+ (permission) =>
318
+ toID(permission.permissionFeature) ===
319
+ String(feature.id) &&
320
+ toID(permission.permissionAction) ===
321
+ String(action.id),
322
+ );
323
+
324
+ if (!matchedPermission) {
325
+ return null;
326
+ }
327
+
328
+ const permissionID = String(matchedPermission.id);
329
+ const checked =
330
+ typeof draftValue[permissionID] === "boolean"
331
+ ? draftValue[permissionID]
332
+ : (enabledByPermissionID.get(permissionID) ??
333
+ false);
334
+
335
+ return (
336
+ <div key={`${feature.id}-${action.id}-sub`}>
337
+ <input
338
+ type="checkbox"
339
+ checked={checked}
340
+ disabled={isReadOnly}
341
+ onChange={(event) => {
342
+ setValue({
343
+ ...draftValue,
344
+ [permissionID]: event.target.checked,
345
+ });
346
+ }}
347
+ />{" "}
348
+ <span
349
+ style={{
350
+ display: "inline-block",
351
+ }}
352
+ >
353
+ {action.code || action.id}
354
+ </span>
355
+ </div>
356
+ );
357
+ })}
358
+ </td>
359
+ </tr>
360
+ )}
361
+ </Fragment>
362
+ );
363
+ })}
364
+ </tbody>
365
+ </table>
366
+ </div>
367
+ </div>
368
+ );
369
+ };
@@ -0,0 +1,66 @@
1
+ // Import Payload compile-time SCSS vars ($breakpoint-s-width, etc.).
2
+ // Note: @payloadcms/ui/scss/vars is not a published export — use dist/scss/vars.
3
+ // @use "../../../node_modules/@payloadcms/ui/dist/scss/vars" as payload-vars;
4
+ @use "../../styles/variables" as rbac-vars;
5
+
6
+ $local-prefix: rbac-vars.$rbac-prefix;
7
+
8
+ .#{$local-prefix}-component {
9
+ &-label {
10
+ opacity: 0.8;
11
+ padding: 0.5rem 0;
12
+ }
13
+ &-placeholder {
14
+ opacity: 0.8;
15
+ padding: 0.5rem 0;
16
+ }
17
+ &-title {
18
+ font-weight: 600;
19
+ margin-bottom: 0.5rem;
20
+ }
21
+ }
22
+
23
+ .#{$local-prefix}-table {
24
+ border-collapse: collapse;
25
+ width: 100%;
26
+ &-container {
27
+ border-radius: var(--style-radius-m);
28
+ border: 1px solid var(--theme-border-color);
29
+ }
30
+ &-th {
31
+ &-feature {
32
+ padding: 0.5rem;
33
+ text-align: left;
34
+ border-right: 1px solid var(--theme-border-color);
35
+ width: 25%;
36
+ }
37
+ &-action {
38
+ padding: 0.5rem;
39
+ }
40
+ }
41
+ &-td {
42
+ &-feature {
43
+ border-right: 1px solid var(--theme-border-color);
44
+ border-top: 1px solid var(--theme-border-color);
45
+ padding: 0.5rem;
46
+ }
47
+ &-action {
48
+ border-top: 1px solid var(--theme-border-color);
49
+ padding: 0.5rem;
50
+ &-container {
51
+ display: flex;
52
+ align-items: center;
53
+ }
54
+ &-input {
55
+ user-select: none;
56
+ cursor: pointer;
57
+ }
58
+ &-label {
59
+ display: inline-block;
60
+ padding-left: 0.25rem;
61
+ user-select: none;
62
+ cursor: pointer;
63
+ }
64
+ }
65
+ }
66
+ }
@@ -0,0 +1,16 @@
1
+ export type RolePermissionMatrixClientTranslations = {
2
+ [locale: string]: {
3
+ viewInUpdateScreenOnly?: {
4
+ label?: string;
5
+ placeholder?: string;
6
+ };
7
+ loading?: {
8
+ placeholder?: string;
9
+ };
10
+ title?: string;
11
+ featuresLabel?: string;
12
+ features?: Record<string, string>;
13
+ actionsLabel?: string;
14
+ actions?: Record<string, string>;
15
+ };
16
+ };
@@ -0,0 +1,5 @@
1
+ import type { PayloadHandler } from "payload"
2
+
3
+ export const customEndpointHandler: PayloadHandler = () => {
4
+ return Response.json({ message: "Hello from custom endpoint" })
5
+ }
@@ -0,0 +1 @@
1
+ export { RolePermissionMatrixClient } from "../components/role-permission-matrix-client/index.js"
File without changes
@@ -0,0 +1,5 @@
1
+ type ItemRef = number | string | { id?: number | string };
2
+
3
+ type ApiListResponse<T> = {
4
+ docs?: T[];
5
+ };