payload-rbac-plugin 1.0.0 → 1.0.2

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mahmoud Hassan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # Payload CMS Dynamic RBAC Plugin
2
2
 
3
+ [![NPM Version](https://img.shields.io/npm/v/payload-rbac-plugin.svg?style=flat-square)](https://www.npmjs.com/package/payload-rbac-plugin)
4
+ [![NPM Downloads](https://img.shields.io/npm/dm/payload-rbac-plugin.svg?style=flat-square)](https://www.npmjs.com/package/payload-rbac-plugin)
5
+ [![License](https://img.shields.io/npm/l/payload-rbac-plugin.svg?style=flat-square)](https://github.com/Mhmod-Hsn/Payload-RBAC-Plugin/blob/main/LICENSE)
6
+ [![CI Status](https://img.shields.io/github/actions/workflow/status/Mhmod-Hsn/Payload-RBAC-Plugin/ci.yml?branch=main&style=flat-square)](https://github.com/Mhmod-Hsn/Payload-RBAC-Plugin/actions)
7
+
3
8
  A professional, database-backed Role-Based Access Control (RBAC) system for [Payload CMS](https://payloadcms.com) (v3).
4
9
 
5
10
  This plugin emphasizes checking **permissions instead of roles** to avoid hard-coded authorization logic in your source code, making your access control highly scalable, modular, and dynamic.
@@ -1,27 +1,120 @@
1
+ import { createBeforePermissionChangeHook } from '../hooks/beforePermissionChange';
1
2
  export const createPermissionsCollection = (options)=>{
2
3
  const slug = options.permissionsCollectionSlug || 'permissions';
3
4
  const customFields = options.permissionsFields || [];
4
5
  return {
5
6
  slug,
6
- admin: {
7
- useAsTitle: 'name',
8
- group: 'Access Control'
9
- },
10
7
  access: {
11
8
  read: ()=>true
12
9
  },
10
+ admin: {
11
+ group: 'Access Control',
12
+ hidden: options.hidePermissions ?? false,
13
+ useAsTitle: 'name'
14
+ },
13
15
  fields: [
16
+ {
17
+ name: 'type',
18
+ type: 'select',
19
+ admin: {
20
+ condition: (data)=>!data?.id && !data?._id && !data?.createdAt,
21
+ description: 'Choose whether to create a single custom permission or generate multiple CRUD permissions at once.',
22
+ disableListColumn: true,
23
+ disableListFilter: true
24
+ },
25
+ defaultValue: 'single',
26
+ options: [
27
+ {
28
+ label: 'Single Permission',
29
+ value: 'single'
30
+ },
31
+ {
32
+ label: 'Bulk CRUD Generator',
33
+ value: 'bulk'
34
+ }
35
+ ]
36
+ },
14
37
  {
15
38
  name: 'name',
16
39
  type: 'text',
17
- required: true,
40
+ admin: {
41
+ condition: (data)=>data?.type === 'single',
42
+ description: 'The unique name of the permission (e.g., "access:admin", "posts:create").'
43
+ },
18
44
  unique: true,
45
+ validate: (value, { data })=>{
46
+ if (data?.type === 'single' && !value) {
47
+ return 'Name is required for single permissions.';
48
+ }
49
+ return true;
50
+ }
51
+ },
52
+ {
53
+ name: 'collectionName',
54
+ type: 'text',
19
55
  admin: {
20
- description: 'The unique name of the permission (e.g., "create:users", "read:posts").'
56
+ condition: (data)=>data?.type === 'bulk',
57
+ description: 'Enter a collection slug (e.g., "posts") to auto-generate permissions.',
58
+ disableListColumn: true,
59
+ disableListFilter: true
21
60
  }
22
61
  },
62
+ {
63
+ type: 'row',
64
+ fields: [
65
+ {
66
+ name: 'create',
67
+ type: 'checkbox',
68
+ admin: {
69
+ condition: (data)=>data?.type === 'bulk',
70
+ disableListColumn: true,
71
+ disableListFilter: true,
72
+ width: '25%'
73
+ },
74
+ defaultValue: false
75
+ },
76
+ {
77
+ name: 'read',
78
+ type: 'checkbox',
79
+ admin: {
80
+ condition: (data)=>data?.type === 'bulk',
81
+ disableListColumn: true,
82
+ disableListFilter: true,
83
+ width: '25%'
84
+ },
85
+ defaultValue: false
86
+ },
87
+ {
88
+ name: 'update',
89
+ type: 'checkbox',
90
+ admin: {
91
+ condition: (data)=>data?.type === 'bulk',
92
+ disableListColumn: true,
93
+ disableListFilter: true,
94
+ width: '25%'
95
+ },
96
+ defaultValue: false
97
+ },
98
+ {
99
+ name: 'delete',
100
+ type: 'checkbox',
101
+ admin: {
102
+ condition: (data)=>data?.type === 'bulk',
103
+ disableListColumn: true,
104
+ disableListFilter: true,
105
+ width: '25%'
106
+ },
107
+ defaultValue: false
108
+ }
109
+ ]
110
+ },
23
111
  ...customFields
24
- ]
112
+ ],
113
+ hooks: {
114
+ beforeChange: [
115
+ createBeforePermissionChangeHook(slug)
116
+ ]
117
+ }
25
118
  };
26
119
  };
27
120
 
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/collections/Permissions.ts"],"sourcesContent":["import type { CollectionConfig } from 'payload'\nimport type { PluginOptions } from '../types.js'\n\nexport const createPermissionsCollection = (options: PluginOptions): CollectionConfig => {\n const slug = options.permissionsCollectionSlug || 'permissions'\n const customFields = options.permissionsFields || []\n\n return {\n slug,\n admin: {\n useAsTitle: 'name',\n group: 'Access Control',\n },\n access: {\n read: () => true, // Access logic can be customized or injected later\n },\n fields: [\n {\n name: 'name',\n type: 'text',\n required: true,\n unique: true,\n admin: {\n description: 'The unique name of the permission (e.g., \"create:users\", \"read:posts\").',\n },\n },\n ...customFields,\n ],\n }\n}\n"],"names":["createPermissionsCollection","options","slug","permissionsCollectionSlug","customFields","permissionsFields","admin","useAsTitle","group","access","read","fields","name","type","required","unique","description"],"mappings":"AAGA,OAAO,MAAMA,8BAA8B,CAACC;IAC1C,MAAMC,OAAOD,QAAQE,yBAAyB,IAAI;IAClD,MAAMC,eAAeH,QAAQI,iBAAiB,IAAI,EAAE;IAEpD,OAAO;QACLH;QACAI,OAAO;YACLC,YAAY;YACZC,OAAO;QACT;QACAC,QAAQ;YACNC,MAAM,IAAM;QACd;QACAC,QAAQ;YACN;gBACEC,MAAM;gBACNC,MAAM;gBACNC,UAAU;gBACVC,QAAQ;gBACRT,OAAO;oBACLU,aAAa;gBACf;YACF;eACGZ;SACJ;IACH;AACF,EAAC"}
1
+ {"version":3,"sources":["../../src/collections/Permissions.ts"],"sourcesContent":["import type { CollectionConfig } from 'payload'\n\nimport type { PluginOptions } from '../types'\n\nimport { createBeforePermissionChangeHook } from '../hooks/beforePermissionChange'\n\nexport const createPermissionsCollection = (options: PluginOptions): CollectionConfig => {\n const slug = options.permissionsCollectionSlug || 'permissions'\n const customFields = options.permissionsFields || []\n\n return {\n slug,\n access: {\n read: () => true,\n },\n admin: {\n group: 'Access Control',\n hidden: options.hidePermissions ?? false,\n useAsTitle: 'name',\n },\n fields: [\n {\n name: 'type',\n type: 'select',\n admin: {\n condition: (data) => !data?.id && !data?._id && !data?.createdAt,\n description:\n 'Choose whether to create a single custom permission or generate multiple CRUD permissions at once.',\n disableListColumn: true,\n disableListFilter: true,\n },\n defaultValue: 'single',\n options: [\n { label: 'Single Permission', value: 'single' },\n { label: 'Bulk CRUD Generator', value: 'bulk' },\n ],\n },\n {\n name: 'name',\n type: 'text',\n admin: {\n condition: (data) => data?.type === 'single',\n description: 'The unique name of the permission (e.g., \"access:admin\", \"posts:create\").',\n },\n unique: true,\n validate: (value: null | string | undefined, { data }: { data?: any }) => {\n if (data?.type === 'single' && !value) {\n return 'Name is required for single permissions.'\n }\n return true\n },\n },\n {\n name: 'collectionName',\n type: 'text',\n admin: {\n condition: (data) => data?.type === 'bulk',\n description: 'Enter a collection slug (e.g., \"posts\") to auto-generate permissions.',\n disableListColumn: true,\n disableListFilter: true,\n },\n },\n {\n type: 'row',\n fields: [\n {\n name: 'create',\n type: 'checkbox',\n admin: {\n condition: (data) => data?.type === 'bulk',\n disableListColumn: true,\n disableListFilter: true,\n width: '25%',\n },\n defaultValue: false,\n },\n {\n name: 'read',\n type: 'checkbox',\n admin: {\n condition: (data) => data?.type === 'bulk',\n disableListColumn: true,\n disableListFilter: true,\n width: '25%',\n },\n defaultValue: false,\n },\n {\n name: 'update',\n type: 'checkbox',\n admin: {\n condition: (data) => data?.type === 'bulk',\n disableListColumn: true,\n disableListFilter: true,\n width: '25%',\n },\n defaultValue: false,\n },\n {\n name: 'delete',\n type: 'checkbox',\n admin: {\n condition: (data) => data?.type === 'bulk',\n disableListColumn: true,\n disableListFilter: true,\n width: '25%',\n },\n defaultValue: false,\n },\n ],\n },\n ...customFields,\n ],\n hooks: {\n beforeChange: [createBeforePermissionChangeHook(slug)],\n },\n }\n}\n"],"names":["createBeforePermissionChangeHook","createPermissionsCollection","options","slug","permissionsCollectionSlug","customFields","permissionsFields","access","read","admin","group","hidden","hidePermissions","useAsTitle","fields","name","type","condition","data","id","_id","createdAt","description","disableListColumn","disableListFilter","defaultValue","label","value","unique","validate","width","hooks","beforeChange"],"mappings":"AAIA,SAASA,gCAAgC,QAAQ,kCAAiC;AAElF,OAAO,MAAMC,8BAA8B,CAACC;IAC1C,MAAMC,OAAOD,QAAQE,yBAAyB,IAAI;IAClD,MAAMC,eAAeH,QAAQI,iBAAiB,IAAI,EAAE;IAEpD,OAAO;QACLH;QACAI,QAAQ;YACNC,MAAM,IAAM;QACd;QACAC,OAAO;YACLC,OAAO;YACPC,QAAQT,QAAQU,eAAe,IAAI;YACnCC,YAAY;QACd;QACAC,QAAQ;YACN;gBACEC,MAAM;gBACNC,MAAM;gBACNP,OAAO;oBACLQ,WAAW,CAACC,OAAS,CAACA,MAAMC,MAAM,CAACD,MAAME,OAAO,CAACF,MAAMG;oBACvDC,aACE;oBACFC,mBAAmB;oBACnBC,mBAAmB;gBACrB;gBACAC,cAAc;gBACdvB,SAAS;oBACP;wBAAEwB,OAAO;wBAAqBC,OAAO;oBAAS;oBAC9C;wBAAED,OAAO;wBAAuBC,OAAO;oBAAO;iBAC/C;YACH;YACA;gBACEZ,MAAM;gBACNC,MAAM;gBACNP,OAAO;oBACLQ,WAAW,CAACC,OAASA,MAAMF,SAAS;oBACpCM,aAAa;gBACf;gBACAM,QAAQ;gBACRC,UAAU,CAACF,OAAkC,EAAET,IAAI,EAAkB;oBACnE,IAAIA,MAAMF,SAAS,YAAY,CAACW,OAAO;wBACrC,OAAO;oBACT;oBACA,OAAO;gBACT;YACF;YACA;gBACEZ,MAAM;gBACNC,MAAM;gBACNP,OAAO;oBACLQ,WAAW,CAACC,OAASA,MAAMF,SAAS;oBACpCM,aAAa;oBACbC,mBAAmB;oBACnBC,mBAAmB;gBACrB;YACF;YACA;gBACER,MAAM;gBACNF,QAAQ;oBACN;wBACEC,MAAM;wBACNC,MAAM;wBACNP,OAAO;4BACLQ,WAAW,CAACC,OAASA,MAAMF,SAAS;4BACpCO,mBAAmB;4BACnBC,mBAAmB;4BACnBM,OAAO;wBACT;wBACAL,cAAc;oBAChB;oBACA;wBACEV,MAAM;wBACNC,MAAM;wBACNP,OAAO;4BACLQ,WAAW,CAACC,OAASA,MAAMF,SAAS;4BACpCO,mBAAmB;4BACnBC,mBAAmB;4BACnBM,OAAO;wBACT;wBACAL,cAAc;oBAChB;oBACA;wBACEV,MAAM;wBACNC,MAAM;wBACNP,OAAO;4BACLQ,WAAW,CAACC,OAASA,MAAMF,SAAS;4BACpCO,mBAAmB;4BACnBC,mBAAmB;4BACnBM,OAAO;wBACT;wBACAL,cAAc;oBAChB;oBACA;wBACEV,MAAM;wBACNC,MAAM;wBACNP,OAAO;4BACLQ,WAAW,CAACC,OAASA,MAAMF,SAAS;4BACpCO,mBAAmB;4BACnBC,mBAAmB;4BACnBM,OAAO;wBACT;wBACAL,cAAc;oBAChB;iBACD;YACH;eACGpB;SACJ;QACD0B,OAAO;YACLC,cAAc;gBAAChC,iCAAiCG;aAAM;QACxD;IACF;AACF,EAAC"}
@@ -6,7 +6,8 @@ export const createRolesCollection = (options)=>{
6
6
  slug,
7
7
  admin: {
8
8
  useAsTitle: 'name',
9
- group: 'Access Control'
9
+ group: 'Access Control',
10
+ hidden: options.hideRoles ?? false
10
11
  },
11
12
  access: {
12
13
  read: ()=>true
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/collections/Roles.ts"],"sourcesContent":["import type { CollectionConfig } from 'payload'\nimport type { PluginOptions } from '../types.js'\n\nexport const createRolesCollection = (options: PluginOptions): CollectionConfig => {\n const slug = options.rolesCollectionSlug || 'roles'\n const permissionsSlug = options.permissionsCollectionSlug || 'permissions'\n const customFields = options.rolesFields || []\n\n return {\n slug,\n admin: {\n useAsTitle: 'name',\n group: 'Access Control',\n },\n access: {\n read: () => true,\n },\n fields: [\n {\n name: 'name',\n type: 'text',\n required: true,\n unique: true,\n admin: {\n description: 'The unique name of the role (e.g., \"admin\", \"editor\").',\n },\n },\n {\n name: 'permissions',\n type: 'relationship',\n relationTo: permissionsSlug,\n hasMany: true,\n admin: {\n description: 'The permissions assigned to this role.',\n },\n },\n ...customFields,\n ],\n }\n}\n"],"names":["createRolesCollection","options","slug","rolesCollectionSlug","permissionsSlug","permissionsCollectionSlug","customFields","rolesFields","admin","useAsTitle","group","access","read","fields","name","type","required","unique","description","relationTo","hasMany"],"mappings":"AAGA,OAAO,MAAMA,wBAAwB,CAACC;IACpC,MAAMC,OAAOD,QAAQE,mBAAmB,IAAI;IAC5C,MAAMC,kBAAkBH,QAAQI,yBAAyB,IAAI;IAC7D,MAAMC,eAAeL,QAAQM,WAAW,IAAI,EAAE;IAE9C,OAAO;QACLL;QACAM,OAAO;YACLC,YAAY;YACZC,OAAO;QACT;QACAC,QAAQ;YACNC,MAAM,IAAM;QACd;QACAC,QAAQ;YACN;gBACEC,MAAM;gBACNC,MAAM;gBACNC,UAAU;gBACVC,QAAQ;gBACRT,OAAO;oBACLU,aAAa;gBACf;YACF;YACA;gBACEJ,MAAM;gBACNC,MAAM;gBACNI,YAAYf;gBACZgB,SAAS;gBACTZ,OAAO;oBACLU,aAAa;gBACf;YACF;eACGZ;SACJ;IACH;AACF,EAAC"}
1
+ {"version":3,"sources":["../../src/collections/Roles.ts"],"sourcesContent":["import type { CollectionConfig } from 'payload';\nimport type { PluginOptions } from '../types';\n\nexport const createRolesCollection = (options: PluginOptions): CollectionConfig => {\n const slug = options.rolesCollectionSlug || 'roles'\n const permissionsSlug = options.permissionsCollectionSlug || 'permissions'\n const customFields = options.rolesFields || []\n\n return {\n slug,\n admin: {\n useAsTitle: 'name',\n group: 'Access Control',\n hidden: options.hideRoles ?? false,\n },\n access: {\n read: () => true,\n },\n fields: [\n {\n name: 'name',\n type: 'text',\n required: true,\n unique: true,\n admin: {\n description: 'The unique name of the role (e.g., \"admin\", \"editor\").',\n },\n },\n {\n name: 'permissions',\n type: 'relationship',\n relationTo: permissionsSlug,\n hasMany: true,\n admin: {\n description: 'The permissions assigned to this role.',\n },\n },\n ...customFields,\n ],\n }\n}\n"],"names":["createRolesCollection","options","slug","rolesCollectionSlug","permissionsSlug","permissionsCollectionSlug","customFields","rolesFields","admin","useAsTitle","group","hidden","hideRoles","access","read","fields","name","type","required","unique","description","relationTo","hasMany"],"mappings":"AAGA,OAAO,MAAMA,wBAAwB,CAACC;IACpC,MAAMC,OAAOD,QAAQE,mBAAmB,IAAI;IAC5C,MAAMC,kBAAkBH,QAAQI,yBAAyB,IAAI;IAC7D,MAAMC,eAAeL,QAAQM,WAAW,IAAI,EAAE;IAE9C,OAAO;QACLL;QACAM,OAAO;YACLC,YAAY;YACZC,OAAO;YACPC,QAAQV,QAAQW,SAAS,IAAI;QAC/B;QACAC,QAAQ;YACNC,MAAM,IAAM;QACd;QACAC,QAAQ;YACN;gBACEC,MAAM;gBACNC,MAAM;gBACNC,UAAU;gBACVC,QAAQ;gBACRX,OAAO;oBACLY,aAAa;gBACf;YACF;YACA;gBACEJ,MAAM;gBACNC,MAAM;gBACNI,YAAYjB;gBACZkB,SAAS;gBACTd,OAAO;oBACLY,aAAa;gBACf;YACF;eACGd;SACJ;IACH;AACF,EAAC"}
@@ -1,3 +1,3 @@
1
- export { BeforeDashboardClient } from '../components/BeforeDashboardClient.js';
1
+ export { BeforeDashboardClient } from '../components/BeforeDashboardClient';
2
2
 
3
3
  //# sourceMappingURL=client.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/exports/client.ts"],"sourcesContent":["export { BeforeDashboardClient } from '../components/BeforeDashboardClient.js'\n"],"names":["BeforeDashboardClient"],"mappings":"AAAA,SAASA,qBAAqB,QAAQ,yCAAwC"}
1
+ {"version":3,"sources":["../../src/exports/client.ts"],"sourcesContent":["export { BeforeDashboardClient } from '../components/BeforeDashboardClient';\n\n"],"names":["BeforeDashboardClient"],"mappings":"AAAA,SAASA,qBAAqB,QAAQ,sCAAsC"}
@@ -1,3 +1,3 @@
1
- export { BeforeDashboardServer } from '../components/BeforeDashboardServer.js';
1
+ export { BeforeDashboardServer } from '../components/BeforeDashboardServer';
2
2
 
3
3
  //# sourceMappingURL=rsc.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/exports/rsc.ts"],"sourcesContent":["export { BeforeDashboardServer } from '../components/BeforeDashboardServer.js'\n"],"names":["BeforeDashboardServer"],"mappings":"AAAA,SAASA,qBAAqB,QAAQ,yCAAwC"}
1
+ {"version":3,"sources":["../../src/exports/rsc.ts"],"sourcesContent":["export { BeforeDashboardServer } from '../components/BeforeDashboardServer';\n\n"],"names":["BeforeDashboardServer"],"mappings":"AAAA,SAASA,qBAAqB,QAAQ,sCAAsC"}
@@ -0,0 +1,58 @@
1
+ export const createBeforePermissionChangeHook = (slug)=>{
2
+ return async ({ data, req })=>{
3
+ // Only run generator logic on create/update if type is bulk
4
+ if (data.type === 'bulk' && data.collectionName) {
5
+ const actions = [
6
+ 'create',
7
+ 'read',
8
+ 'update',
9
+ 'delete'
10
+ ].filter((action)=>data[action] === true);
11
+ if (actions.length === 0) {
12
+ throw new Error('You must select at least one CRUD operation when using the Bulk Generator.');
13
+ }
14
+ // e.g. ["posts:create", "posts:read"]
15
+ const generatedNames = actions.map((action)=>`${data.collectionName}:${action}`);
16
+ // Filter out names that already exist
17
+ const uniqueNames = [];
18
+ for (const name of generatedNames){
19
+ const existing = await req.payload.find({
20
+ collection: slug,
21
+ where: {
22
+ name: {
23
+ equals: name
24
+ }
25
+ }
26
+ });
27
+ if (existing.docs.length === 0) {
28
+ uniqueNames.push(name);
29
+ }
30
+ }
31
+ if (uniqueNames.length === 0) {
32
+ throw new Error('All selected CRUD permissions already exist.');
33
+ }
34
+ // Mutate current document to save the first unique generated name
35
+ data.name = uniqueNames[0];
36
+ // Create the remaining unique permissions in the background
37
+ const remainingNames = uniqueNames.slice(1);
38
+ for (const name of remainingNames){
39
+ await req.payload.create({
40
+ collection: slug,
41
+ data: {
42
+ name
43
+ }
44
+ });
45
+ }
46
+ }
47
+ // Ensure UI-only fields are never saved to the database
48
+ delete data.type;
49
+ delete data.collectionName;
50
+ delete data.create;
51
+ delete data.read;
52
+ delete data.update;
53
+ delete data.delete;
54
+ return data;
55
+ };
56
+ };
57
+
58
+ //# sourceMappingURL=beforePermissionChange.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/hooks/beforePermissionChange.ts"],"sourcesContent":["import type { CollectionBeforeChangeHook } from 'payload'\n\nexport const createBeforePermissionChangeHook = (slug: string): CollectionBeforeChangeHook => {\n return async ({ data, req }) => {\n // Only run generator logic on create/update if type is bulk\n if (data.type === 'bulk' && data.collectionName) {\n const actions = ['create', 'read', 'update', 'delete'].filter(\n (action) => data[action] === true,\n )\n\n if (actions.length === 0) {\n throw new Error('You must select at least one CRUD operation when using the Bulk Generator.')\n }\n\n // e.g. [\"posts:create\", \"posts:read\"]\n const generatedNames = actions.map((action) => `${data.collectionName}:${action}`)\n\n // Filter out names that already exist\n const uniqueNames: string[] = []\n for (const name of generatedNames) {\n const existing = await req.payload.find({\n collection: slug,\n where: {\n name: { equals: name },\n },\n })\n if (existing.docs.length === 0) {\n uniqueNames.push(name)\n }\n }\n\n if (uniqueNames.length === 0) {\n throw new Error('All selected CRUD permissions already exist.')\n }\n\n // Mutate current document to save the first unique generated name\n data.name = uniqueNames[0]\n\n // Create the remaining unique permissions in the background\n const remainingNames = uniqueNames.slice(1)\n for (const name of remainingNames) {\n await req.payload.create({\n collection: slug,\n data: {\n name,\n },\n })\n }\n }\n\n // Ensure UI-only fields are never saved to the database\n delete data.type\n delete data.collectionName\n delete data.create\n delete data.read\n delete data.update\n delete data.delete\n\n return data\n }\n}\n"],"names":["createBeforePermissionChangeHook","slug","data","req","type","collectionName","actions","filter","action","length","Error","generatedNames","map","uniqueNames","name","existing","payload","find","collection","where","equals","docs","push","remainingNames","slice","create","read","update","delete"],"mappings":"AAEA,OAAO,MAAMA,mCAAmC,CAACC;IAC/C,OAAO,OAAO,EAAEC,IAAI,EAAEC,GAAG,EAAE;QACzB,4DAA4D;QAC5D,IAAID,KAAKE,IAAI,KAAK,UAAUF,KAAKG,cAAc,EAAE;YAC/C,MAAMC,UAAU;gBAAC;gBAAU;gBAAQ;gBAAU;aAAS,CAACC,MAAM,CAC3D,CAACC,SAAWN,IAAI,CAACM,OAAO,KAAK;YAG/B,IAAIF,QAAQG,MAAM,KAAK,GAAG;gBACxB,MAAM,IAAIC,MAAM;YAClB;YAEA,sCAAsC;YACtC,MAAMC,iBAAiBL,QAAQM,GAAG,CAAC,CAACJ,SAAW,GAAGN,KAAKG,cAAc,CAAC,CAAC,EAAEG,QAAQ;YAEjF,sCAAsC;YACtC,MAAMK,cAAwB,EAAE;YAChC,KAAK,MAAMC,QAAQH,eAAgB;gBACjC,MAAMI,WAAW,MAAMZ,IAAIa,OAAO,CAACC,IAAI,CAAC;oBACtCC,YAAYjB;oBACZkB,OAAO;wBACLL,MAAM;4BAAEM,QAAQN;wBAAK;oBACvB;gBACF;gBACA,IAAIC,SAASM,IAAI,CAACZ,MAAM,KAAK,GAAG;oBAC9BI,YAAYS,IAAI,CAACR;gBACnB;YACF;YAEA,IAAID,YAAYJ,MAAM,KAAK,GAAG;gBAC5B,MAAM,IAAIC,MAAM;YAClB;YAEA,kEAAkE;YAClER,KAAKY,IAAI,GAAGD,WAAW,CAAC,EAAE;YAE1B,4DAA4D;YAC5D,MAAMU,iBAAiBV,YAAYW,KAAK,CAAC;YACzC,KAAK,MAAMV,QAAQS,eAAgB;gBACjC,MAAMpB,IAAIa,OAAO,CAACS,MAAM,CAAC;oBACvBP,YAAYjB;oBACZC,MAAM;wBACJY;oBACF;gBACF;YACF;QACF;QAEA,wDAAwD;QACxD,OAAOZ,KAAKE,IAAI;QAChB,OAAOF,KAAKG,cAAc;QAC1B,OAAOH,KAAKuB,MAAM;QAClB,OAAOvB,KAAKwB,IAAI;QAChB,OAAOxB,KAAKyB,MAAM;QAClB,OAAOzB,KAAK0B,MAAM;QAElB,OAAO1B;IACT;AACF,EAAC"}
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import { createPermissionsCollection } from './collections/Permissions.js';
2
- import { createRolesCollection } from './collections/Roles.js';
1
+ import { createPermissionsCollection } from './collections/Permissions';
2
+ import { createRolesCollection } from './collections/Roles';
3
3
  export const rbac = (pluginOptions)=>(config)=>{
4
4
  const options = pluginOptions || {};
5
5
  if (options.enabled === false) {
@@ -32,7 +32,7 @@ export const rbac = (pluginOptions)=>(config)=>{
32
32
  return config;
33
33
  };
34
34
  // Export utilities and types for consumers
35
- export * from './types.js';
36
- export * from './utilities/index.js';
35
+ export * from './types';
36
+ export * from './utilities/index';
37
37
 
38
38
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import type { Config } from 'payload'\nimport type { PluginOptions } from './types.js'\n\nimport { createPermissionsCollection } from './collections/Permissions.js'\nimport { createRolesCollection } from './collections/Roles.js'\n\nexport const rbac =\n (pluginOptions?: PluginOptions) =>\n (config: Config): Config => {\n const options = pluginOptions || {}\n \n if (options.enabled === false) {\n return config\n }\n\n if (!config.collections) {\n config.collections = []\n }\n\n // Add Roles and Permissions collections\n config.collections.push(createPermissionsCollection(options))\n config.collections.push(createRolesCollection(options))\n\n // Extend the target auth collection\n const authSlug = options.authCollectionSlug || 'users'\n const rolesSlug = options.rolesCollectionSlug || 'roles'\n\n const authCollection = config.collections.find(\n (collection) => collection.slug === authSlug,\n )\n\n if (authCollection) {\n authCollection.fields.push({\n name: 'roles',\n type: 'relationship',\n relationTo: rolesSlug,\n hasMany: true,\n saveToJWT: true,\n admin: {\n description: 'Roles assigned to this user.',\n },\n })\n } else {\n console.warn(`[rbac-plugin] Auth collection '${authSlug}' not found. Cannot inject roles field.`)\n }\n\n return config\n }\n\n// Export utilities and types for consumers\nexport * from './types.js'\nexport * from './utilities/index.js'\n"],"names":["createPermissionsCollection","createRolesCollection","rbac","pluginOptions","config","options","enabled","collections","push","authSlug","authCollectionSlug","rolesSlug","rolesCollectionSlug","authCollection","find","collection","slug","fields","name","type","relationTo","hasMany","saveToJWT","admin","description","console","warn"],"mappings":"AAGA,SAASA,2BAA2B,QAAQ,+BAA8B;AAC1E,SAASC,qBAAqB,QAAQ,yBAAwB;AAE9D,OAAO,MAAMC,OACX,CAACC,gBACD,CAACC;QACC,MAAMC,UAAUF,iBAAiB,CAAC;QAElC,IAAIE,QAAQC,OAAO,KAAK,OAAO;YAC7B,OAAOF;QACT;QAEA,IAAI,CAACA,OAAOG,WAAW,EAAE;YACvBH,OAAOG,WAAW,GAAG,EAAE;QACzB;QAEA,wCAAwC;QACxCH,OAAOG,WAAW,CAACC,IAAI,CAACR,4BAA4BK;QACpDD,OAAOG,WAAW,CAACC,IAAI,CAACP,sBAAsBI;QAE9C,oCAAoC;QACpC,MAAMI,WAAWJ,QAAQK,kBAAkB,IAAI;QAC/C,MAAMC,YAAYN,QAAQO,mBAAmB,IAAI;QAEjD,MAAMC,iBAAiBT,OAAOG,WAAW,CAACO,IAAI,CAC5C,CAACC,aAAeA,WAAWC,IAAI,KAAKP;QAGtC,IAAII,gBAAgB;YAClBA,eAAeI,MAAM,CAACT,IAAI,CAAC;gBACzBU,MAAM;gBACNC,MAAM;gBACNC,YAAYT;gBACZU,SAAS;gBACTC,WAAW;gBACXC,OAAO;oBACLC,aAAa;gBACf;YACF;QACF,OAAO;YACLC,QAAQC,IAAI,CAAC,CAAC,+BAA+B,EAAEjB,SAAS,uCAAuC,CAAC;QAClG;QAEA,OAAOL;IACT,EAAC;AAEH,2CAA2C;AAC3C,cAAc,aAAY;AAC1B,cAAc,uBAAsB"}
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import type { Config } from 'payload';\nimport type { PluginOptions } from './types';\n\nimport { createPermissionsCollection } from './collections/Permissions';\nimport { createRolesCollection } from './collections/Roles';\n\nexport const rbac =\n (pluginOptions?: PluginOptions) =>\n (config: Config): Config => {\n const options = pluginOptions || {}\n \n if (options.enabled === false) {\n return config\n }\n\n if (!config.collections) {\n config.collections = []\n }\n\n // Add Roles and Permissions collections\n config.collections.push(createPermissionsCollection(options))\n config.collections.push(createRolesCollection(options))\n\n // Extend the target auth collection\n const authSlug = options.authCollectionSlug || 'users'\n const rolesSlug = options.rolesCollectionSlug || 'roles'\n\n const authCollection = config.collections.find(\n (collection) => collection.slug === authSlug,\n )\n\n if (authCollection) {\n authCollection.fields.push({\n name: 'roles',\n type: 'relationship',\n relationTo: rolesSlug,\n hasMany: true,\n saveToJWT: true,\n admin: {\n description: 'Roles assigned to this user.',\n },\n })\n } else {\n console.warn(`[rbac-plugin] Auth collection '${authSlug}' not found. Cannot inject roles field.`)\n }\n\n return config\n }\n\n// Export utilities and types for consumers\nexport * from './types';\nexport * from './utilities/index';\n\n"],"names":["createPermissionsCollection","createRolesCollection","rbac","pluginOptions","config","options","enabled","collections","push","authSlug","authCollectionSlug","rolesSlug","rolesCollectionSlug","authCollection","find","collection","slug","fields","name","type","relationTo","hasMany","saveToJWT","admin","description","console","warn"],"mappings":"AAGA,SAASA,2BAA2B,QAAQ,4BAA4B;AACxE,SAASC,qBAAqB,QAAQ,sBAAsB;AAE5D,OAAO,MAAMC,OACX,CAACC,gBACD,CAACC;QACC,MAAMC,UAAUF,iBAAiB,CAAC;QAElC,IAAIE,QAAQC,OAAO,KAAK,OAAO;YAC7B,OAAOF;QACT;QAEA,IAAI,CAACA,OAAOG,WAAW,EAAE;YACvBH,OAAOG,WAAW,GAAG,EAAE;QACzB;QAEA,wCAAwC;QACxCH,OAAOG,WAAW,CAACC,IAAI,CAACR,4BAA4BK;QACpDD,OAAOG,WAAW,CAACC,IAAI,CAACP,sBAAsBI;QAE9C,oCAAoC;QACpC,MAAMI,WAAWJ,QAAQK,kBAAkB,IAAI;QAC/C,MAAMC,YAAYN,QAAQO,mBAAmB,IAAI;QAEjD,MAAMC,iBAAiBT,OAAOG,WAAW,CAACO,IAAI,CAC5C,CAACC,aAAeA,WAAWC,IAAI,KAAKP;QAGtC,IAAII,gBAAgB;YAClBA,eAAeI,MAAM,CAACT,IAAI,CAAC;gBACzBU,MAAM;gBACNC,MAAM;gBACNC,YAAYT;gBACZU,SAAS;gBACTC,WAAW;gBACXC,OAAO;oBACLC,aAAa;gBACf;YACF;QACF,OAAO;YACLC,QAAQC,IAAI,CAAC,CAAC,+BAA+B,EAAEjB,SAAS,uCAAuC,CAAC;QAClG;QAEA,OAAOL;IACT,EAAC;AAEH,2CAA2C;AAC3C,cAAc,UAAU;AACxB,cAAc,oBAAoB"}
package/dist/types.d.ts CHANGED
@@ -28,4 +28,20 @@ export interface PluginOptions {
28
28
  * Inject additional custom fields into the generated Permissions collection
29
29
  */
30
30
  permissionsFields?: Field[];
31
+ /**
32
+ * Hide the Roles collection from the Admin panel sidebar/dashboard
33
+ * Can be a boolean or a function based on the logged-in user
34
+ * @default false
35
+ */
36
+ hideRoles?: boolean | ((args: {
37
+ user: any;
38
+ }) => boolean);
39
+ /**
40
+ * Hide the Permissions collection from the Admin panel sidebar/dashboard
41
+ * Can be a boolean or a function based on the logged-in user
42
+ * @default false
43
+ */
44
+ hidePermissions?: boolean | ((args: {
45
+ user: any;
46
+ }) => boolean);
31
47
  }
package/dist/types.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/types.ts"],"sourcesContent":["import type { Field } from 'payload'\n\nexport interface PluginOptions {\n /**\n * Enable or disable plugin\n * @default false\n */\n enabled?: boolean\n\n /**\n * Override the default collection slug for Roles\n * @default 'roles'\n */\n rolesCollectionSlug?: string\n\n /**\n * Override the default collection slug for Permissions\n * @default 'permissions'\n */\n permissionsCollectionSlug?: string\n\n /**\n * The collection slug for the authentication collection (typically 'users')\n * @default 'users'\n */\n authCollectionSlug?: string\n\n /**\n * Inject additional custom fields into the generated Roles collection\n */\n rolesFields?: Field[]\n\n /**\n * Inject additional custom fields into the generated Permissions collection\n */\n permissionsFields?: Field[]\n}\n"],"names":[],"mappings":"AAEA,WAkCC"}
1
+ {"version":3,"sources":["../src/types.ts"],"sourcesContent":["import type { Field } from 'payload'\n\nexport interface PluginOptions {\n /**\n * Enable or disable plugin\n * @default false\n */\n enabled?: boolean\n\n /**\n * Override the default collection slug for Roles\n * @default 'roles'\n */\n rolesCollectionSlug?: string\n\n /**\n * Override the default collection slug for Permissions\n * @default 'permissions'\n */\n permissionsCollectionSlug?: string\n\n /**\n * The collection slug for the authentication collection (typically 'users')\n * @default 'users'\n */\n authCollectionSlug?: string\n\n /**\n * Inject additional custom fields into the generated Roles collection\n */\n rolesFields?: Field[]\n\n /**\n * Inject additional custom fields into the generated Permissions collection\n */\n permissionsFields?: Field[]\n\n /**\n * Hide the Roles collection from the Admin panel sidebar/dashboard\n * Can be a boolean or a function based on the logged-in user\n * @default false\n */\n hideRoles?: boolean | ((args: { user: any }) => boolean)\n\n /**\n * Hide the Permissions collection from the Admin panel sidebar/dashboard\n * Can be a boolean or a function based on the logged-in user\n * @default false\n */\n hidePermissions?: boolean | ((args: { user: any }) => boolean)\n}\n"],"names":[],"mappings":"AAEA,WAgDC"}
@@ -1,4 +1,4 @@
1
- import { hasPermission } from './hasPermission.js';
1
+ import { hasPermission } from './hasPermission';
2
2
  /**
3
3
  * A Higher-Order Function to drop into Payload Collection access control fields.
4
4
  *
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/utilities/checkPermission.ts"],"sourcesContent":["import type { Access } from 'payload'\nimport { hasPermission } from './hasPermission.js'\n\n/**\n * A Higher-Order Function to drop into Payload Collection access control fields.\n * \n * @param permissionName - The required permission name\n * @returns Access function\n */\nexport const checkPermission = (permissionName: string): Access => {\n return ({ req: { user } }) => {\n // We can just rely on the synchronous hasPermission check.\n // If relations are unpopulated, it will return false. Ensure your auth\n // collection is configured with adequate depth for saveToJWT or default depth.\n return hasPermission(user, permissionName)\n }\n}\n"],"names":["hasPermission","checkPermission","permissionName","req","user"],"mappings":"AACA,SAASA,aAAa,QAAQ,qBAAoB;AAElD;;;;;CAKC,GACD,OAAO,MAAMC,kBAAkB,CAACC;IAC9B,OAAO,CAAC,EAAEC,KAAK,EAAEC,IAAI,EAAE,EAAE;QACvB,2DAA2D;QAC3D,uEAAuE;QACvE,+EAA+E;QAC/E,OAAOJ,cAAcI,MAAMF;IAC7B;AACF,EAAC"}
1
+ {"version":3,"sources":["../../src/utilities/checkPermission.ts"],"sourcesContent":["import type { Access } from 'payload';\nimport { hasPermission } from './hasPermission';\n\n/**\n * A Higher-Order Function to drop into Payload Collection access control fields.\n * \n * @param permissionName - The required permission name\n * @returns Access function\n */\nexport const checkPermission = (permissionName: string): Access => {\n return ({ req: { user } }) => {\n // We can just rely on the synchronous hasPermission check.\n // If relations are unpopulated, it will return false. Ensure your auth\n // collection is configured with adequate depth for saveToJWT or default depth.\n return hasPermission(user, permissionName)\n }\n}\n"],"names":["hasPermission","checkPermission","permissionName","req","user"],"mappings":"AACA,SAASA,aAAa,QAAQ,kBAAkB;AAEhD;;;;;CAKC,GACD,OAAO,MAAMC,kBAAkB,CAACC;IAC9B,OAAO,CAAC,EAAEC,KAAK,EAAEC,IAAI,EAAE,EAAE;QACvB,2DAA2D;QAC3D,uEAAuE;QACvE,+EAA+E;QAC/E,OAAOJ,cAAcI,MAAMF;IAC7B;AACF,EAAC"}
@@ -19,16 +19,11 @@
19
19
  continue;
20
20
  }
21
21
  for (const permission of permissions){
22
- // If permission is an object (populated) and has a name
23
22
  if (typeof permission === 'object' && permission !== null) {
24
23
  if ('name' in permission && permission.name === requiredPermission) {
25
24
  return true;
26
25
  }
27
26
  }
28
- // Note: If permission is just an ID (unpopulated), we cannot synchronously
29
- // determine its name here. In a strictly synchronous check, we must return false
30
- // or ignore it. For a fully robust check with unpopulated data, an async version
31
- // requiring the payload instance would be necessary.
32
27
  }
33
28
  }
34
29
  return false;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/utilities/hasPermission.ts"],"sourcesContent":["import type { PayloadRequest } from 'payload'\n\n/**\n * Checks if a user has a specific permission.\n * Gracefully handles populated and unpopulated relationship states as far as possible synchronously.\n * \n * @param user - The Payload user object from req.user\n * @param requiredPermission - The name of the permission to check for\n * @returns boolean\n */\nexport const hasPermission = (\n user: PayloadRequest['user'] | null | undefined,\n requiredPermission: string,\n): boolean => {\n if (!user || !user.roles || !Array.isArray(user.roles)) {\n return false\n }\n\n for (const role of user.roles) {\n // If role is just an ID (unpopulated), we can't check its permissions synchronously\n if (typeof role !== 'object' || role === null) {\n continue\n }\n\n const permissions = role.permissions\n if (!permissions || !Array.isArray(permissions)) {\n continue\n }\n\n for (const permission of permissions) {\n // If permission is an object (populated) and has a name\n if (typeof permission === 'object' && permission !== null) {\n if ('name' in permission && permission.name === requiredPermission) {\n return true\n }\n } \n // Note: If permission is just an ID (unpopulated), we cannot synchronously\n // determine its name here. In a strictly synchronous check, we must return false \n // or ignore it. For a fully robust check with unpopulated data, an async version \n // requiring the payload instance would be necessary.\n }\n }\n\n return false\n}\n"],"names":["hasPermission","user","requiredPermission","roles","Array","isArray","role","permissions","permission","name"],"mappings":"AAEA;;;;;;;CAOC,GACD,OAAO,MAAMA,gBAAgB,CAC3BC,MACAC;IAEA,IAAI,CAACD,QAAQ,CAACA,KAAKE,KAAK,IAAI,CAACC,MAAMC,OAAO,CAACJ,KAAKE,KAAK,GAAG;QACtD,OAAO;IACT;IAEA,KAAK,MAAMG,QAAQL,KAAKE,KAAK,CAAE;QAC7B,oFAAoF;QACpF,IAAI,OAAOG,SAAS,YAAYA,SAAS,MAAM;YAC7C;QACF;QAEA,MAAMC,cAAcD,KAAKC,WAAW;QACpC,IAAI,CAACA,eAAe,CAACH,MAAMC,OAAO,CAACE,cAAc;YAC/C;QACF;QAEA,KAAK,MAAMC,cAAcD,YAAa;YACpC,wDAAwD;YACxD,IAAI,OAAOC,eAAe,YAAYA,eAAe,MAAM;gBACzD,IAAI,UAAUA,cAAcA,WAAWC,IAAI,KAAKP,oBAAoB;oBAClE,OAAO;gBACT;YACF;QACA,2EAA2E;QAC3E,kFAAkF;QAClF,kFAAkF;QAClF,qDAAqD;QACvD;IACF;IAEA,OAAO;AACT,EAAC"}
1
+ {"version":3,"sources":["../../src/utilities/hasPermission.ts"],"sourcesContent":["import type { PayloadRequest } from 'payload'\n\n/**\n * Checks if a user has a specific permission.\n * Gracefully handles populated and unpopulated relationship states as far as possible synchronously.\n * \n * @param user - The Payload user object from req.user\n * @param requiredPermission - The name of the permission to check for\n * @returns boolean\n */\nexport const hasPermission = (\n user: PayloadRequest['user'] | null | undefined,\n requiredPermission: string,\n): boolean => {\n if (!user || !user.roles || !Array.isArray(user.roles)) {\n return false\n }\n\n for (const role of user.roles) {\n // If role is just an ID (unpopulated), we can't check its permissions synchronously\n if (typeof role !== 'object' || role === null) {\n continue\n }\n\n const permissions = role.permissions\n if (!permissions || !Array.isArray(permissions)) {\n continue\n }\n\n for (const permission of permissions) {\n if (typeof permission === 'object' && permission !== null) {\n if ('name' in permission && permission.name === requiredPermission) {\n return true\n }\n } \n }\n }\n\n return false\n}\n"],"names":["hasPermission","user","requiredPermission","roles","Array","isArray","role","permissions","permission","name"],"mappings":"AAEA;;;;;;;CAOC,GACD,OAAO,MAAMA,gBAAgB,CAC3BC,MACAC;IAEA,IAAI,CAACD,QAAQ,CAACA,KAAKE,KAAK,IAAI,CAACC,MAAMC,OAAO,CAACJ,KAAKE,KAAK,GAAG;QACtD,OAAO;IACT;IAEA,KAAK,MAAMG,QAAQL,KAAKE,KAAK,CAAE;QAC7B,oFAAoF;QACpF,IAAI,OAAOG,SAAS,YAAYA,SAAS,MAAM;YAC7C;QACF;QAEA,MAAMC,cAAcD,KAAKC,WAAW;QACpC,IAAI,CAACA,eAAe,CAACH,MAAMC,OAAO,CAACE,cAAc;YAC/C;QACF;QAEA,KAAK,MAAMC,cAAcD,YAAa;YACpC,IAAI,OAAOC,eAAe,YAAYA,eAAe,MAAM;gBACzD,IAAI,UAAUA,cAAcA,WAAWC,IAAI,KAAKP,oBAAoB;oBAClE,OAAO;gBACT;YACF;QACF;IACF;IAEA,OAAO;AACT,EAAC"}
@@ -1,4 +1,5 @@
1
- import { hasPermission } from './hasPermission.js';
1
+ import { describe, expect, it } from 'vitest';
2
+ import { hasPermission } from './hasPermission';
2
3
  describe('hasPermission', ()=>{
3
4
  it('should return false if user is null', ()=>{
4
5
  expect(hasPermission(null, 'read:posts')).toBe(false);
@@ -112,6 +113,7 @@ describe('hasPermission', ()=>{
112
113
  ]
113
114
  };
114
115
  expect(hasPermission(user, 'delete:posts')).toBe(true);
116
+ expect(hasPermission(user, 'delete:posts')).toBe(true);
115
117
  expect(hasPermission(user, 'read:posts')).toBe(true);
116
118
  expect(hasPermission(user, 'create:users')).toBe(false);
117
119
  });
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/utilities/hasPermission.spec.ts"],"sourcesContent":["import { hasPermission } from './hasPermission.js'\nimport type { PayloadRequest } from 'payload'\n\ndescribe('hasPermission', () => {\n it('should return false if user is null', () => {\n expect(hasPermission(null, 'read:posts')).toBe(false)\n })\n\n it('should return false if user has no roles array', () => {\n const user = { id: '1' } as PayloadRequest['user']\n expect(hasPermission(user, 'read:posts')).toBe(false)\n })\n\n it('should return false if user roles is unpopulated (array of strings)', () => {\n const user = {\n id: '1',\n roles: ['role-id-1'],\n } as unknown as PayloadRequest['user']\n expect(hasPermission(user, 'read:posts')).toBe(false)\n })\n\n it('should return false if role has no permissions', () => {\n const user = {\n id: '1',\n roles: [{ id: 'role-1', name: 'admin' }],\n } as unknown as PayloadRequest['user']\n expect(hasPermission(user, 'read:posts')).toBe(false)\n })\n\n it('should return false if role permissions are unpopulated (array of strings)', () => {\n const user = {\n id: '1',\n roles: [{ id: 'role-1', name: 'admin', permissions: ['perm-id-1'] }],\n } as unknown as PayloadRequest['user']\n expect(hasPermission(user, 'read:posts')).toBe(false)\n })\n\n it('should return true if permission is found in populated roles', () => {\n const user = {\n id: '1',\n roles: [\n {\n id: 'role-1',\n name: 'admin',\n permissions: [\n { id: 'perm-1', name: 'read:posts' },\n { id: 'perm-2', name: 'write:posts' },\n ],\n },\n ],\n } as unknown as PayloadRequest['user']\n expect(hasPermission(user, 'write:posts')).toBe(true)\n })\n\n it('should return false if permission is not found in populated roles', () => {\n const user = {\n id: '1',\n roles: [\n {\n id: 'role-1',\n name: 'editor',\n permissions: [{ id: 'perm-1', name: 'read:posts' }],\n },\n ],\n } as unknown as PayloadRequest['user']\n expect(hasPermission(user, 'delete:posts')).toBe(false)\n })\n\n it('should correctly search through multiple roles', () => {\n const user = {\n id: '1',\n roles: [\n {\n id: 'role-1',\n name: 'editor',\n permissions: [{ id: 'perm-1', name: 'read:posts' }],\n },\n {\n id: 'role-2',\n name: 'manager',\n permissions: [{ id: 'perm-2', name: 'delete:posts' }],\n },\n ],\n } as unknown as PayloadRequest['user']\n expect(hasPermission(user, 'delete:posts')).toBe(true)\n expect(hasPermission(user, 'read:posts')).toBe(true)\n expect(hasPermission(user, 'create:users')).toBe(false)\n })\n})\n"],"names":["hasPermission","describe","it","expect","toBe","user","id","roles","name","permissions"],"mappings":"AAAA,SAASA,aAAa,QAAQ,qBAAoB;AAGlDC,SAAS,iBAAiB;IACxBC,GAAG,uCAAuC;QACxCC,OAAOH,cAAc,MAAM,eAAeI,IAAI,CAAC;IACjD;IAEAF,GAAG,kDAAkD;QACnD,MAAMG,OAAO;YAAEC,IAAI;QAAI;QACvBH,OAAOH,cAAcK,MAAM,eAAeD,IAAI,CAAC;IACjD;IAEAF,GAAG,uEAAuE;QACxE,MAAMG,OAAO;YACXC,IAAI;YACJC,OAAO;gBAAC;aAAY;QACtB;QACAJ,OAAOH,cAAcK,MAAM,eAAeD,IAAI,CAAC;IACjD;IAEAF,GAAG,kDAAkD;QACnD,MAAMG,OAAO;YACXC,IAAI;YACJC,OAAO;gBAAC;oBAAED,IAAI;oBAAUE,MAAM;gBAAQ;aAAE;QAC1C;QACAL,OAAOH,cAAcK,MAAM,eAAeD,IAAI,CAAC;IACjD;IAEAF,GAAG,8EAA8E;QAC/E,MAAMG,OAAO;YACXC,IAAI;YACJC,OAAO;gBAAC;oBAAED,IAAI;oBAAUE,MAAM;oBAASC,aAAa;wBAAC;qBAAY;gBAAC;aAAE;QACtE;QACAN,OAAOH,cAAcK,MAAM,eAAeD,IAAI,CAAC;IACjD;IAEAF,GAAG,gEAAgE;QACjE,MAAMG,OAAO;YACXC,IAAI;YACJC,OAAO;gBACL;oBACED,IAAI;oBACJE,MAAM;oBACNC,aAAa;wBACX;4BAAEH,IAAI;4BAAUE,MAAM;wBAAa;wBACnC;4BAAEF,IAAI;4BAAUE,MAAM;wBAAc;qBACrC;gBACH;aACD;QACH;QACAL,OAAOH,cAAcK,MAAM,gBAAgBD,IAAI,CAAC;IAClD;IAEAF,GAAG,qEAAqE;QACtE,MAAMG,OAAO;YACXC,IAAI;YACJC,OAAO;gBACL;oBACED,IAAI;oBACJE,MAAM;oBACNC,aAAa;wBAAC;4BAAEH,IAAI;4BAAUE,MAAM;wBAAa;qBAAE;gBACrD;aACD;QACH;QACAL,OAAOH,cAAcK,MAAM,iBAAiBD,IAAI,CAAC;IACnD;IAEAF,GAAG,kDAAkD;QACnD,MAAMG,OAAO;YACXC,IAAI;YACJC,OAAO;gBACL;oBACED,IAAI;oBACJE,MAAM;oBACNC,aAAa;wBAAC;4BAAEH,IAAI;4BAAUE,MAAM;wBAAa;qBAAE;gBACrD;gBACA;oBACEF,IAAI;oBACJE,MAAM;oBACNC,aAAa;wBAAC;4BAAEH,IAAI;4BAAUE,MAAM;wBAAe;qBAAE;gBACvD;aACD;QACH;QACAL,OAAOH,cAAcK,MAAM,iBAAiBD,IAAI,CAAC;QACjDD,OAAOH,cAAcK,MAAM,eAAeD,IAAI,CAAC;QAC/CD,OAAOH,cAAcK,MAAM,iBAAiBD,IAAI,CAAC;IACnD;AACF"}
1
+ {"version":3,"sources":["../../src/utilities/hasPermission.spec.ts"],"sourcesContent":["import type { PayloadRequest } from 'payload';\nimport { describe, expect, it } from 'vitest';\nimport { hasPermission } from './hasPermission';\n\ndescribe('hasPermission', () => {\n it('should return false if user is null', () => {\n expect(hasPermission(null, 'read:posts')).toBe(false)\n })\n\n it('should return false if user has no roles array', () => {\n const user = { id: '1' } as PayloadRequest['user']\n expect(hasPermission(user, 'read:posts')).toBe(false)\n })\n\n it('should return false if user roles is unpopulated (array of strings)', () => {\n const user = {\n id: '1',\n roles: ['role-id-1'],\n } as unknown as PayloadRequest['user']\n expect(hasPermission(user, 'read:posts')).toBe(false)\n })\n\n it('should return false if role has no permissions', () => {\n const user = {\n id: '1',\n roles: [{ id: 'role-1', name: 'admin' }],\n } as unknown as PayloadRequest['user']\n expect(hasPermission(user, 'read:posts')).toBe(false)\n })\n\n it('should return false if role permissions are unpopulated (array of strings)', () => {\n const user = {\n id: '1',\n roles: [{ id: 'role-1', name: 'admin', permissions: ['perm-id-1'] }],\n } as unknown as PayloadRequest['user']\n expect(hasPermission(user, 'read:posts')).toBe(false)\n })\n\n it('should return true if permission is found in populated roles', () => {\n const user = {\n id: '1',\n roles: [\n {\n id: 'role-1',\n name: 'admin',\n permissions: [\n { id: 'perm-1', name: 'read:posts' },\n { id: 'perm-2', name: 'write:posts' },\n ],\n },\n ],\n } as unknown as PayloadRequest['user']\n expect(hasPermission(user, 'write:posts')).toBe(true)\n })\n\n it('should return false if permission is not found in populated roles', () => {\n const user = {\n id: '1',\n roles: [\n {\n id: 'role-1',\n name: 'editor',\n permissions: [{ id: 'perm-1', name: 'read:posts' }],\n },\n ],\n } as unknown as PayloadRequest['user']\n expect(hasPermission(user, 'delete:posts')).toBe(false)\n })\n\n it('should correctly search through multiple roles', () => {\n const user = {\n id: '1',\n roles: [\n {\n id: 'role-1',\n name: 'editor',\n permissions: [{ id: 'perm-1', name: 'read:posts' }],\n },\n {\n id: 'role-2',\n name: 'manager',\n permissions: [{ id: 'perm-2', name: 'delete:posts' }],\n },\n ],\n } as unknown as PayloadRequest['user']\n expect(hasPermission(user, 'delete:posts')).toBe(true)\n expect(hasPermission(user, 'delete:posts')).toBe(true)\n expect(hasPermission(user, 'read:posts')).toBe(true)\n expect(hasPermission(user, 'create:users')).toBe(false)\n })\n})\n"],"names":["describe","expect","it","hasPermission","toBe","user","id","roles","name","permissions"],"mappings":"AACA,SAASA,QAAQ,EAAEC,MAAM,EAAEC,EAAE,QAAQ,SAAS;AAC9C,SAASC,aAAa,QAAQ,kBAAkB;AAEhDH,SAAS,iBAAiB;IACxBE,GAAG,uCAAuC;QACxCD,OAAOE,cAAc,MAAM,eAAeC,IAAI,CAAC;IACjD;IAEAF,GAAG,kDAAkD;QACnD,MAAMG,OAAO;YAAEC,IAAI;QAAI;QACvBL,OAAOE,cAAcE,MAAM,eAAeD,IAAI,CAAC;IACjD;IAEAF,GAAG,uEAAuE;QACxE,MAAMG,OAAO;YACXC,IAAI;YACJC,OAAO;gBAAC;aAAY;QACtB;QACAN,OAAOE,cAAcE,MAAM,eAAeD,IAAI,CAAC;IACjD;IAEAF,GAAG,kDAAkD;QACnD,MAAMG,OAAO;YACXC,IAAI;YACJC,OAAO;gBAAC;oBAAED,IAAI;oBAAUE,MAAM;gBAAQ;aAAE;QAC1C;QACAP,OAAOE,cAAcE,MAAM,eAAeD,IAAI,CAAC;IACjD;IAEAF,GAAG,8EAA8E;QAC/E,MAAMG,OAAO;YACXC,IAAI;YACJC,OAAO;gBAAC;oBAAED,IAAI;oBAAUE,MAAM;oBAASC,aAAa;wBAAC;qBAAY;gBAAC;aAAE;QACtE;QACAR,OAAOE,cAAcE,MAAM,eAAeD,IAAI,CAAC;IACjD;IAEAF,GAAG,gEAAgE;QACjE,MAAMG,OAAO;YACXC,IAAI;YACJC,OAAO;gBACL;oBACED,IAAI;oBACJE,MAAM;oBACNC,aAAa;wBACX;4BAAEH,IAAI;4BAAUE,MAAM;wBAAa;wBACnC;4BAAEF,IAAI;4BAAUE,MAAM;wBAAc;qBACrC;gBACH;aACD;QACH;QACAP,OAAOE,cAAcE,MAAM,gBAAgBD,IAAI,CAAC;IAClD;IAEAF,GAAG,qEAAqE;QACtE,MAAMG,OAAO;YACXC,IAAI;YACJC,OAAO;gBACL;oBACED,IAAI;oBACJE,MAAM;oBACNC,aAAa;wBAAC;4BAAEH,IAAI;4BAAUE,MAAM;wBAAa;qBAAE;gBACrD;aACD;QACH;QACAP,OAAOE,cAAcE,MAAM,iBAAiBD,IAAI,CAAC;IACnD;IAEAF,GAAG,kDAAkD;QACnD,MAAMG,OAAO;YACXC,IAAI;YACJC,OAAO;gBACL;oBACED,IAAI;oBACJE,MAAM;oBACNC,aAAa;wBAAC;4BAAEH,IAAI;4BAAUE,MAAM;wBAAa;qBAAE;gBACrD;gBACA;oBACEF,IAAI;oBACJE,MAAM;oBACNC,aAAa;wBAAC;4BAAEH,IAAI;4BAAUE,MAAM;wBAAe;qBAAE;gBACvD;aACD;QACH;QACAP,OAAOE,cAAcE,MAAM,iBAAiBD,IAAI,CAAC;QACjDH,OAAOE,cAAcE,MAAM,iBAAiBD,IAAI,CAAC;QACjDH,OAAOE,cAAcE,MAAM,eAAeD,IAAI,CAAC;QAC/CH,OAAOE,cAAcE,MAAM,iBAAiBD,IAAI,CAAC;IACnD;AACF"}
@@ -1,4 +1,4 @@
1
- export { hasPermission } from './hasPermission.js';
2
- export { checkPermission } from './checkPermission.js';
1
+ export { checkPermission } from './checkPermission';
2
+ export { hasPermission } from './hasPermission';
3
3
 
4
4
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/utilities/index.ts"],"sourcesContent":["export { hasPermission } from './hasPermission.js'\nexport { checkPermission } from './checkPermission.js'\n"],"names":["hasPermission","checkPermission"],"mappings":"AAAA,SAASA,aAAa,QAAQ,qBAAoB;AAClD,SAASC,eAAe,QAAQ,uBAAsB"}
1
+ {"version":3,"sources":["../../src/utilities/index.ts"],"sourcesContent":["export { checkPermission } from './checkPermission';\nexport { hasPermission } from './hasPermission';\n\n"],"names":["checkPermission","hasPermission"],"mappings":"AAAA,SAASA,eAAe,QAAQ,oBAAoB;AACpD,SAASC,aAAa,QAAQ,kBAAkB"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "payload-rbac-plugin",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "RBAC plugin for payloadcms",
5
5
  "repository": {
6
6
  "type": "git",
@@ -11,47 +11,45 @@
11
11
  },
12
12
  "homepage": "https://github.com/Mhmod-Hsn/Payload-RBAC-Plugin#readme",
13
13
  "license": "MIT",
14
+ "author": "Mahmoud Hassan <hsn.mhmod.96@gmail.com> (https://github.com/Mhmod-Hsn)",
15
+ "keywords": [
16
+ "payload",
17
+ "payloadcms",
18
+ "payload-plugin",
19
+ "plugin",
20
+ "rbac",
21
+ "role-based-access-control",
22
+ "access-control",
23
+ "permissions",
24
+ "roles",
25
+ "security",
26
+ "authorization",
27
+ "auth",
28
+ "typescript"
29
+ ],
14
30
  "type": "module",
15
31
  "exports": {
16
32
  ".": {
17
- "import": "./src/index.ts",
18
- "types": "./src/index.ts",
19
- "default": "./src/index.ts"
33
+ "import": "./dist/index.js",
34
+ "types": "./dist/index.d.ts",
35
+ "default": "./dist/index.js"
20
36
  },
21
37
  "./client": {
22
- "import": "./src/exports/client.ts",
23
- "types": "./src/exports/client.ts",
24
- "default": "./src/exports/client.ts"
38
+ "import": "./dist/exports/client.js",
39
+ "types": "./dist/exports/client.d.ts",
40
+ "default": "./dist/exports/client.js"
25
41
  },
26
42
  "./rsc": {
27
- "import": "./src/exports/rsc.ts",
28
- "types": "./src/exports/rsc.ts",
29
- "default": "./src/exports/rsc.ts"
43
+ "import": "./dist/exports/rsc.js",
44
+ "types": "./dist/exports/rsc.d.ts",
45
+ "default": "./dist/exports/rsc.js"
30
46
  }
31
47
  },
32
- "main": "./src/index.ts",
33
- "types": "./src/index.ts",
48
+ "main": "./dist/index.js",
49
+ "types": "./dist/index.d.ts",
34
50
  "files": [
35
51
  "dist"
36
52
  ],
37
- "scripts": {
38
- "build": "pnpm copyfiles && pnpm build:types && pnpm build:swc",
39
- "build:swc": "swc ./src -d ./dist --config-file .swcrc --strip-leading-paths",
40
- "build:types": "tsc --outDir dist --rootDir ./src",
41
- "clean": "rimraf {dist,*.tsbuildinfo}",
42
- "copyfiles": "copyfiles -u 1 \"src/**/*.{html,css,scss,ttf,woff,woff2,eot,svg,jpg,png,json}\" dist/",
43
- "dev": "next dev dev --turbo",
44
- "dev:generate-importmap": "pnpm dev:payload generate:importmap",
45
- "dev:generate-types": "pnpm dev:payload generate:types",
46
- "dev:payload": "cross-env PAYLOAD_CONFIG_PATH=./dev/payload.config.ts payload",
47
- "generate:importmap": "pnpm dev:generate-importmap",
48
- "generate:types": "pnpm dev:generate-types",
49
- "lint": "eslint",
50
- "lint:fix": "eslint ./src --fix",
51
- "test": "pnpm test:int && pnpm test:e2e",
52
- "test:e2e": "playwright test",
53
- "test:int": "vitest"
54
- },
55
53
  "devDependencies": {
56
54
  "@eslint/eslintrc": "^3.2.0",
57
55
  "@payloadcms/db-mongodb": "3.84.1",
@@ -94,33 +92,23 @@
94
92
  "node": "^18.20.2 || >=20.9.0",
95
93
  "pnpm": "^9 || ^10"
96
94
  },
97
- "publishConfig": {
98
- "exports": {
99
- ".": {
100
- "import": "./dist/index.js",
101
- "types": "./dist/index.d.ts",
102
- "default": "./dist/index.js"
103
- },
104
- "./client": {
105
- "import": "./dist/exports/client.js",
106
- "types": "./dist/exports/client.d.ts",
107
- "default": "./dist/exports/client.js"
108
- },
109
- "./rsc": {
110
- "import": "./dist/exports/rsc.js",
111
- "types": "./dist/exports/rsc.d.ts",
112
- "default": "./dist/exports/rsc.js"
113
- }
114
- },
115
- "main": "./dist/index.js",
116
- "types": "./dist/index.d.ts"
117
- },
118
- "pnpm": {
119
- "onlyBuiltDependencies": [
120
- "sharp",
121
- "esbuild",
122
- "unrs-resolver"
123
- ]
124
- },
125
- "registry": "https://registry.npmjs.org/"
126
- }
95
+ "registry": "https://registry.npmjs.org/",
96
+ "scripts": {
97
+ "build": "pnpm copyfiles && pnpm build:types && pnpm build:swc",
98
+ "build:swc": "swc ./src -d ./dist --config-file .swcrc --strip-leading-paths",
99
+ "build:types": "tsc --outDir dist --rootDir ./src",
100
+ "clean": "rimraf {dist,*.tsbuildinfo}",
101
+ "copyfiles": "copyfiles -u 1 \"src/**/*.{html,css,scss,ttf,woff,woff2,eot,svg,jpg,png,json}\" dist/",
102
+ "dev": "next dev dev --turbo",
103
+ "dev:generate-importmap": "pnpm dev:payload generate:importmap",
104
+ "dev:generate-types": "pnpm dev:payload generate:types",
105
+ "dev:payload": "cross-env PAYLOAD_CONFIG_PATH=./dev/payload.config.ts payload",
106
+ "generate:importmap": "pnpm dev:generate-importmap",
107
+ "generate:types": "pnpm dev:generate-types",
108
+ "lint": "eslint",
109
+ "lint:fix": "eslint ./src --fix",
110
+ "test": "pnpm test:int && pnpm test:e2e",
111
+ "test:e2e": "playwright test",
112
+ "test:int": "vitest"
113
+ }
114
+ }