roles-privileges-payload-plugin 1.0.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/README.md +650 -0
- package/package.json +130 -0
package/README.md
ADDED
|
@@ -0,0 +1,650 @@
|
|
|
1
|
+
# Roles & Privileges Payload Plugin
|
|
2
|
+
|
|
3
|
+
A powerful Payload CMS plugin that automatically generates role-based access control (RBAC) with granular CRUD privileges for all your collections.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- 🔐 **Automatic Privilege Generation**: Automatically creates CRUD privileges for collections and read/update privileges for globals
|
|
8
|
+
- 🎯 **Smart Access Control Wrapping**: Seamlessly wraps existing collection and global access controls with privilege checks
|
|
9
|
+
- 🌐 **Full Global Support**: Generates privileges for Payload globals (read/update operations)
|
|
10
|
+
- 👑 **Super Admin Role**: Auto-seeds a Super Admin role with all privileges
|
|
11
|
+
- 🎨 **Beautiful UI**: Custom privilege selector component with collapsible interface showing both collections and globals
|
|
12
|
+
- ⭐ **Custom Privileges**: Register custom privileges that appear in the admin UI alongside auto-generated ones
|
|
13
|
+
- 🗣️ **Multilingual**: Full support for any language with fallback chains (\_default → en → first available)
|
|
14
|
+
- ⚙️ **Highly Configurable**: Exclude collections/globals, disable features, or customize behavior
|
|
15
|
+
- 🔄 **Zero Configuration**: Works out of the box with sensible defaults
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install roles-privileges-payload-plugin
|
|
21
|
+
# or
|
|
22
|
+
pnpm add roles-privileges-payload-plugin
|
|
23
|
+
# or
|
|
24
|
+
yarn add roles-privileges-payload-plugin
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Basic Usage
|
|
28
|
+
|
|
29
|
+
Add the plugin to your Payload configuration:
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
import { buildConfig } from 'payload'
|
|
33
|
+
import { rolesPrivilegesPayloadPlugin } from 'roles-privileges-payload-plugin'
|
|
34
|
+
|
|
35
|
+
export default buildConfig({
|
|
36
|
+
collections: [
|
|
37
|
+
// Your collections here
|
|
38
|
+
],
|
|
39
|
+
plugins: [rolesPrivilegesPayloadPlugin()],
|
|
40
|
+
})
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
That's it! The plugin will:
|
|
44
|
+
|
|
45
|
+
1. Scan all your collections and globals
|
|
46
|
+
2. Generate CRUD privileges for each collection (create, read, update, delete)
|
|
47
|
+
3. Generate read/update privileges for each global
|
|
48
|
+
4. Add a `roles` collection with a privilege selector UI
|
|
49
|
+
5. Wrap all collection and global access controls with privilege checks
|
|
50
|
+
6. Seed a Super Admin role with all privileges
|
|
51
|
+
|
|
52
|
+
## Configuration Options
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
rolesPrivilegesPayloadPlugin({
|
|
56
|
+
// Enable the plugin (defaults to true). When false, the plugin does nothing.
|
|
57
|
+
enable: true,
|
|
58
|
+
|
|
59
|
+
// Disable the plugin (roles collection will still be added for schema consistency)
|
|
60
|
+
disabled: false,
|
|
61
|
+
|
|
62
|
+
// Collections to exclude from automatic privilege generation
|
|
63
|
+
excludeCollections: ['media', 'payload-preferences'],
|
|
64
|
+
|
|
65
|
+
// Globals to exclude from automatic privilege generation
|
|
66
|
+
excludeGlobals: [],
|
|
67
|
+
|
|
68
|
+
// Automatically wrap collection access controls with privilege checks
|
|
69
|
+
wrapCollectionAccess: true,
|
|
70
|
+
|
|
71
|
+
// Automatically wrap global access controls with privilege checks
|
|
72
|
+
wrapGlobalAccess: true,
|
|
73
|
+
|
|
74
|
+
// Seed a Super Admin role with all privileges on init
|
|
75
|
+
seedSuperAdmin: true,
|
|
76
|
+
|
|
77
|
+
// Provide a custom roles collection configuration (optional)
|
|
78
|
+
// Use createRolesCollection() helper to create a base and customize it
|
|
79
|
+
customRolesCollection: undefined,
|
|
80
|
+
})
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## How It Works
|
|
84
|
+
|
|
85
|
+
### 1. Privilege Generation
|
|
86
|
+
|
|
87
|
+
**For Collections:**
|
|
88
|
+
|
|
89
|
+
The plugin automatically generates seven privileges for each collection:
|
|
90
|
+
|
|
91
|
+
- `{collection-slug}-admin`: Permission to access the collection's admin UI
|
|
92
|
+
- `{collection-slug}-create`: Permission to create new documents
|
|
93
|
+
- `{collection-slug}-read`: Permission to read/view documents
|
|
94
|
+
- `{collection-slug}-readVersions`: Permission to view document version history
|
|
95
|
+
- `{collection-slug}-update`: Permission to update existing documents
|
|
96
|
+
- `{collection-slug}-delete`: Permission to delete documents
|
|
97
|
+
- `{collection-slug}-unlock`: Permission to unlock documents being edited by others
|
|
98
|
+
|
|
99
|
+
Example for a `posts` collection:
|
|
100
|
+
|
|
101
|
+
- `posts-admin`
|
|
102
|
+
- `posts-create`
|
|
103
|
+
- `posts-read`
|
|
104
|
+
- `posts-readVersions`
|
|
105
|
+
- `posts-update`
|
|
106
|
+
- `posts-delete`
|
|
107
|
+
- `posts-unlock`
|
|
108
|
+
|
|
109
|
+
**For Globals:**
|
|
110
|
+
|
|
111
|
+
The plugin generates four privileges for each global:
|
|
112
|
+
|
|
113
|
+
- `{global-slug}-read`: Permission to read/view the global
|
|
114
|
+
- `{global-slug}-readDrafts`: Permission to view draft versions
|
|
115
|
+
- `{global-slug}-readVersions`: Permission to view version history
|
|
116
|
+
- `{global-slug}-update`: Permission to update the global
|
|
117
|
+
|
|
118
|
+
Example for a `site-settings` global:
|
|
119
|
+
|
|
120
|
+
- `site-settings-read`
|
|
121
|
+
- `site-settings-update`
|
|
122
|
+
|
|
123
|
+
### 2. Access Control Wrapping
|
|
124
|
+
|
|
125
|
+
The plugin wraps your existing collection access controls. For example:
|
|
126
|
+
|
|
127
|
+
**Before:**
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
{
|
|
131
|
+
slug: 'posts',
|
|
132
|
+
access: {
|
|
133
|
+
read: () => true,
|
|
134
|
+
create: ({ req: { user } }) => !!user,
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
**After (automatically wrapped):**
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
{
|
|
143
|
+
slug: 'posts',
|
|
144
|
+
access: {
|
|
145
|
+
read: async (args) => {
|
|
146
|
+
const hasOriginalAccess = true // from original function
|
|
147
|
+
if (!hasOriginalAccess) return false
|
|
148
|
+
return hasPrivilege('posts-read')(args)
|
|
149
|
+
},
|
|
150
|
+
create: async (args) => {
|
|
151
|
+
const hasOriginalAccess = !!args.req.user // from original function
|
|
152
|
+
if (!hasOriginalAccess) return false
|
|
153
|
+
return hasPrivilege('posts-create')(args)
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### 3. The Roles Collection
|
|
160
|
+
|
|
161
|
+
The plugin adds a `roles` collection with:
|
|
162
|
+
|
|
163
|
+
- `title`: The role name
|
|
164
|
+
- `slug`: Unique identifier (auto-generated from title)
|
|
165
|
+
- `privileges`: Array of privilege keys
|
|
166
|
+
- `description`: Optional role description
|
|
167
|
+
|
|
168
|
+
The privileges field uses a custom UI component that provides:
|
|
169
|
+
|
|
170
|
+
- Collapsible interface organized by collections and globals
|
|
171
|
+
- Visual distinction between collections (📦) and globals (🌐)
|
|
172
|
+
- Star icon (⭐) for custom privileges
|
|
173
|
+
- Checkbox selection for easy privilege management
|
|
174
|
+
- Real-time privilege descriptions with multilingual support
|
|
175
|
+
- Badge counter showing selected privileges per collection/global
|
|
176
|
+
|
|
177
|
+
### 4. User Integration
|
|
178
|
+
|
|
179
|
+
To use roles in your users collection, add a relationship field:
|
|
180
|
+
|
|
181
|
+
```ts
|
|
182
|
+
{
|
|
183
|
+
slug: 'users',
|
|
184
|
+
fields: [
|
|
185
|
+
{
|
|
186
|
+
name: 'roles',
|
|
187
|
+
type: 'relationship',
|
|
188
|
+
relationTo: 'roles',
|
|
189
|
+
hasMany: true,
|
|
190
|
+
required: true,
|
|
191
|
+
},
|
|
192
|
+
// other fields...
|
|
193
|
+
]
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
## Advanced Usage
|
|
198
|
+
|
|
199
|
+
### Creating Custom Privileges
|
|
200
|
+
|
|
201
|
+
You can create custom privileges beyond the auto-generated CRUD operations. Custom privileges registered with `registerCustomPrivilege` will appear in the admin UI with a star icon (⭐) to differentiate them from auto-generated privileges:
|
|
202
|
+
|
|
203
|
+
```ts
|
|
204
|
+
import { registerCustomPrivilege, hasPrivilege } from 'roles-privileges-payload-plugin'
|
|
205
|
+
|
|
206
|
+
// Register a custom privilege (will appear in admin UI)
|
|
207
|
+
const publishPrivilege = registerCustomPrivilege('posts', {
|
|
208
|
+
privilegeKey: 'posts-publish',
|
|
209
|
+
label: {
|
|
210
|
+
en: 'Publish Posts',
|
|
211
|
+
fr: 'Publier les articles',
|
|
212
|
+
},
|
|
213
|
+
description: {
|
|
214
|
+
en: 'Ability to publish posts to make them publicly visible',
|
|
215
|
+
fr: 'Capacité de publier des articles pour les rendre publiquement visibles',
|
|
216
|
+
},
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
// Use it in your collection
|
|
220
|
+
export const Posts = {
|
|
221
|
+
slug: 'posts',
|
|
222
|
+
fields: [
|
|
223
|
+
{
|
|
224
|
+
name: 'status',
|
|
225
|
+
type: 'select',
|
|
226
|
+
options: ['draft', 'published'],
|
|
227
|
+
access: {
|
|
228
|
+
update: async ({ req, data }) => {
|
|
229
|
+
if (data?.status === 'published') {
|
|
230
|
+
return hasPrivilege(publishPrivilege.privilegeKey)({ req })
|
|
231
|
+
}
|
|
232
|
+
return true
|
|
233
|
+
},
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
],
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Register multiple custom privileges at once
|
|
240
|
+
import { registerCustomPrivileges } from 'roles-privileges-payload-plugin'
|
|
241
|
+
|
|
242
|
+
const customPrivileges = registerCustomPrivileges('posts', [
|
|
243
|
+
{
|
|
244
|
+
privilegeKey: 'posts-publish',
|
|
245
|
+
label: { en: 'Publish Posts' },
|
|
246
|
+
description: { en: 'Publish posts to make them visible' },
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
privilegeKey: 'posts-feature',
|
|
250
|
+
label: { en: 'Feature Posts' },
|
|
251
|
+
description: { en: 'Feature posts on the homepage' },
|
|
252
|
+
},
|
|
253
|
+
])
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
Custom privileges are visually distinguished in the admin UI with:
|
|
257
|
+
|
|
258
|
+
- ⭐ Star icon next to the privilege label
|
|
259
|
+
- Yellow/warning background for the privilege key badge
|
|
260
|
+
- Clear separation from auto-generated CRUD privileges
|
|
261
|
+
|
|
262
|
+
For more details, see [CUSTOM_PRIVILEGES.md](./CUSTOM_PRIVILEGES.md).
|
|
263
|
+
|
|
264
|
+
### Custom Privilege Checks
|
|
265
|
+
|
|
266
|
+
You can use the privilege access functions in your own access controls:
|
|
267
|
+
|
|
268
|
+
```ts
|
|
269
|
+
import {
|
|
270
|
+
hasPrivilege,
|
|
271
|
+
hasAnyPrivilege,
|
|
272
|
+
hasAllPrivileges,
|
|
273
|
+
checkPrivilege,
|
|
274
|
+
} from 'roles-privileges-payload-plugin'
|
|
275
|
+
|
|
276
|
+
// Single privilege check (for collection/global access)
|
|
277
|
+
{
|
|
278
|
+
access: {
|
|
279
|
+
read: hasPrivilege('posts-read')
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// For field-level access, use checkPrivilege (synchronous)
|
|
284
|
+
{
|
|
285
|
+
fields: [
|
|
286
|
+
{
|
|
287
|
+
name: 'sensitiveField',
|
|
288
|
+
type: 'text',
|
|
289
|
+
access: {
|
|
290
|
+
read: ({ req }) => checkPrivilege('posts-admin', req.user),
|
|
291
|
+
update: ({ req }) => checkPrivilege('posts-admin', req.user),
|
|
292
|
+
},
|
|
293
|
+
},
|
|
294
|
+
]
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Field-level access with ANY privilege (OR logic)
|
|
298
|
+
{
|
|
299
|
+
fields: [
|
|
300
|
+
{
|
|
301
|
+
name: 'status',
|
|
302
|
+
access: {
|
|
303
|
+
update: ({ req }) => checkAnyPrivilege(req.user, 'posts-update', 'posts-admin'),
|
|
304
|
+
},
|
|
305
|
+
},
|
|
306
|
+
]
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Field-level access with ALL privileges (AND logic)
|
|
310
|
+
{
|
|
311
|
+
fields: [
|
|
312
|
+
{
|
|
313
|
+
name: 'publishedDate',
|
|
314
|
+
access: {
|
|
315
|
+
update: ({ req }) => checkAllPrivileges(req.user, 'posts-update', 'posts-publish'),
|
|
316
|
+
},
|
|
317
|
+
},
|
|
318
|
+
]
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Complex field-level privilege logic
|
|
322
|
+
{
|
|
323
|
+
fields: [
|
|
324
|
+
{
|
|
325
|
+
name: 'featured',
|
|
326
|
+
access: {
|
|
327
|
+
// User needs (posts-update AND posts-feature) OR (posts-admin)
|
|
328
|
+
update: ({ req }) =>
|
|
329
|
+
checkPrivileges([['posts-update', 'posts-feature'], ['posts-admin']], req.user),
|
|
330
|
+
},
|
|
331
|
+
},
|
|
332
|
+
]
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// User needs ANY of these privileges (OR logic)
|
|
336
|
+
{
|
|
337
|
+
access: {
|
|
338
|
+
read: hasAnyPrivilege('posts-read', 'posts-update')
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// User needs ALL of these privileges (AND logic)
|
|
343
|
+
{
|
|
344
|
+
access: {
|
|
345
|
+
read: hasAllPrivileges('posts-read', 'posts-update')
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Complex privilege logic
|
|
350
|
+
import { privilegesAccess } from 'roles-privileges-payload-plugin'
|
|
351
|
+
|
|
352
|
+
{
|
|
353
|
+
access: {
|
|
354
|
+
// User needs (posts-create AND posts-read) OR (pages-create AND pages-read)
|
|
355
|
+
read: privilegesAccess([
|
|
356
|
+
['posts-create', 'posts-read'],
|
|
357
|
+
['pages-create', 'pages-read'],
|
|
358
|
+
])
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
### Excluding Collections and Globals
|
|
364
|
+
|
|
365
|
+
Some collections or globals don't need privilege-based access control:
|
|
366
|
+
|
|
367
|
+
```ts
|
|
368
|
+
rolesPrivilegesPayloadPlugin({
|
|
369
|
+
excludeCollections: [
|
|
370
|
+
'media', // Public media access
|
|
371
|
+
'payload-preferences', // User preferences
|
|
372
|
+
'payload-migrations', // System migrations
|
|
373
|
+
],
|
|
374
|
+
excludeGlobals: [
|
|
375
|
+
'site-settings', // Public site settings
|
|
376
|
+
],
|
|
377
|
+
})
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
### Accessing Generated Privileges
|
|
381
|
+
|
|
382
|
+
You can access all generated privileges programmatically:
|
|
383
|
+
|
|
384
|
+
```ts
|
|
385
|
+
import {
|
|
386
|
+
allPrivilegesMap,
|
|
387
|
+
getAllPrivileges,
|
|
388
|
+
getAllPrivilegeKeys,
|
|
389
|
+
allGlobalPrivilegesMap,
|
|
390
|
+
getAllGlobalPrivileges,
|
|
391
|
+
getAllGlobalPrivilegeKeys,
|
|
392
|
+
} from 'roles-privileges-payload-plugin'
|
|
393
|
+
|
|
394
|
+
// Get all collection privileges as a Map
|
|
395
|
+
const privilegesMap = allPrivilegesMap
|
|
396
|
+
|
|
397
|
+
// Get all collection privileges as a flat array
|
|
398
|
+
const allPrivileges = getAllPrivileges()
|
|
399
|
+
|
|
400
|
+
// Get just the collection privilege keys
|
|
401
|
+
const privilegeKeys = getAllPrivilegeKeys()
|
|
402
|
+
|
|
403
|
+
// Get all global privileges
|
|
404
|
+
const globalPrivilegesMap = allGlobalPrivilegesMap
|
|
405
|
+
const allGlobalPrivileges = getAllGlobalPrivileges()
|
|
406
|
+
const globalPrivilegeKeys = getAllGlobalPrivilegeKeys()
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
### Custom Roles Collection
|
|
410
|
+
|
|
411
|
+
By default, the plugin creates a standard `roles` collection. However, you can provide your own custom roles collection configuration if you need to:
|
|
412
|
+
|
|
413
|
+
- Add additional fields to the roles collection
|
|
414
|
+
- Customize the collection's access control
|
|
415
|
+
- Add custom hooks or endpoints
|
|
416
|
+
- Modify the admin UI
|
|
417
|
+
|
|
418
|
+
```ts
|
|
419
|
+
import {
|
|
420
|
+
rolesPrivilegesPayloadPlugin,
|
|
421
|
+
createRolesCollection,
|
|
422
|
+
ensureSuperAdminDontGetDeleted,
|
|
423
|
+
ensureSuperAdminDontGetUpdated,
|
|
424
|
+
} from 'roles-privileges-payload-plugin'
|
|
425
|
+
|
|
426
|
+
// Create a custom roles collection based on the default
|
|
427
|
+
const customRolesCollection = createRolesCollection()
|
|
428
|
+
|
|
429
|
+
// Customize it by adding additional fields
|
|
430
|
+
customRolesCollection.fields.push({
|
|
431
|
+
name: 'department',
|
|
432
|
+
type: 'select',
|
|
433
|
+
options: ['Engineering', 'Marketing', 'Sales'],
|
|
434
|
+
admin: {
|
|
435
|
+
position: 'sidebar',
|
|
436
|
+
},
|
|
437
|
+
})
|
|
438
|
+
|
|
439
|
+
// Add custom hooks
|
|
440
|
+
customRolesCollection.hooks = {
|
|
441
|
+
...customRolesCollection.hooks,
|
|
442
|
+
afterChange: [
|
|
443
|
+
async ({ doc, req }) => {
|
|
444
|
+
// Send notification when role is changed
|
|
445
|
+
console.log(`Role ${doc.title} was modified`)
|
|
446
|
+
},
|
|
447
|
+
],
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Use the custom collection in the plugin
|
|
451
|
+
export default buildConfig({
|
|
452
|
+
collections: [
|
|
453
|
+
// Your other collections
|
|
454
|
+
],
|
|
455
|
+
plugins: [
|
|
456
|
+
rolesPrivilegesPayloadPlugin({
|
|
457
|
+
customRolesCollection,
|
|
458
|
+
}),
|
|
459
|
+
],
|
|
460
|
+
})
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
**Important Notes:**
|
|
464
|
+
|
|
465
|
+
- The custom roles collection **must** have the slug `'roles'`
|
|
466
|
+
- The plugin provides helper functions to maintain Super Admin protection:
|
|
467
|
+
- `createRolesCollection()`: Base factory function to create the roles collection
|
|
468
|
+
- `ensureSuperAdminDontGetDeleted`: Hook to prevent Super Admin role deletion
|
|
469
|
+
- `ensureSuperAdminDontGetUpdated`: Hook to prevent Super Admin slug modification
|
|
470
|
+
- You can start with `createRolesCollection()` and customize from there, or build entirely from scratch
|
|
471
|
+
- The `privileges` field must remain for the privilege system to work
|
|
472
|
+
|
|
473
|
+
**Available Exports for Custom Roles:**
|
|
474
|
+
|
|
475
|
+
```ts
|
|
476
|
+
import {
|
|
477
|
+
// Collection creation
|
|
478
|
+
createRolesCollection,
|
|
479
|
+
|
|
480
|
+
// Types
|
|
481
|
+
CollectionData,
|
|
482
|
+
GlobalData,
|
|
483
|
+
|
|
484
|
+
// Hooks
|
|
485
|
+
ensureSuperAdminDontGetDeleted,
|
|
486
|
+
ensureSuperAdminDontGetUpdated,
|
|
487
|
+
|
|
488
|
+
// Utilities
|
|
489
|
+
seedSuperAdminRole,
|
|
490
|
+
} from 'roles-privileges-payload-plugin'
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
## Super Admin Role
|
|
494
|
+
|
|
495
|
+
The plugin automatically creates/updates a Super Admin role with:
|
|
496
|
+
|
|
497
|
+
- Slug: `super-admin`
|
|
498
|
+
- All available privileges
|
|
499
|
+
- Protection against deletion and slug modification
|
|
500
|
+
|
|
501
|
+
To assign the Super Admin role to a user:
|
|
502
|
+
|
|
503
|
+
```ts
|
|
504
|
+
await payload.update({
|
|
505
|
+
collection: 'users',
|
|
506
|
+
id: userId,
|
|
507
|
+
data: {
|
|
508
|
+
roles: ['super-admin-role-id'],
|
|
509
|
+
},
|
|
510
|
+
})
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
## Privilege Label Customization
|
|
514
|
+
|
|
515
|
+
Privilege labels are automatically generated based on collection labels:
|
|
516
|
+
|
|
517
|
+
```ts
|
|
518
|
+
// If your collection has labels:
|
|
519
|
+
{
|
|
520
|
+
slug: 'blog-posts',
|
|
521
|
+
labels: {
|
|
522
|
+
singular: { en: 'Blog Post', fr: 'Article de blog' },
|
|
523
|
+
plural: { en: 'Blog Posts', fr: 'Articles de blog' }
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// Generated privilege labels:
|
|
528
|
+
// - Create Blog Post / Créer un article de blog
|
|
529
|
+
// - Read Blog Post / Lire un article de blog
|
|
530
|
+
// - Update Blog Post / Modifier un article de blog
|
|
531
|
+
// - Delete Blog Post / Supprimer un article de blog
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
If no labels are provided, the plugin capitalizes the slug:
|
|
535
|
+
|
|
536
|
+
- `blog-posts` → "Blog Posts"
|
|
537
|
+
|
|
538
|
+
## TypeScript Support
|
|
539
|
+
|
|
540
|
+
The plugin is fully typed. All exports include TypeScript definitions:
|
|
541
|
+
|
|
542
|
+
```ts
|
|
543
|
+
import type {
|
|
544
|
+
RolesPrivilegesPayloadPluginConfig,
|
|
545
|
+
Privilege,
|
|
546
|
+
CollectionPrivileges,
|
|
547
|
+
PrivilegeType,
|
|
548
|
+
GlobalPrivilege,
|
|
549
|
+
GlobalPrivileges,
|
|
550
|
+
GlobalPrivilegeType,
|
|
551
|
+
} from 'roles-privileges-payload-plugin'
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
## API Reference
|
|
555
|
+
|
|
556
|
+
### Plugin Configuration
|
|
557
|
+
|
|
558
|
+
```ts
|
|
559
|
+
type RolesPrivilegesPayloadPluginConfig = {
|
|
560
|
+
enable?: boolean
|
|
561
|
+
disabled?: boolean
|
|
562
|
+
excludeCollections?: string[]
|
|
563
|
+
excludeGlobals?: string[]
|
|
564
|
+
wrapCollectionAccess?: boolean
|
|
565
|
+
wrapGlobalAccess?: boolean
|
|
566
|
+
seedSuperAdmin?: boolean
|
|
567
|
+
}
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
### Exported Functions
|
|
571
|
+
|
|
572
|
+
**Main Plugin:**
|
|
573
|
+
|
|
574
|
+
- `rolesPrivilegesPayloadPlugin(config?)`: Main plugin function
|
|
575
|
+
|
|
576
|
+
**Access Control (Collection/Global Level - Async):**
|
|
577
|
+
|
|
578
|
+
- `hasPrivilege(key: string)`: Check for a single privilege
|
|
579
|
+
- `hasAnyPrivilege(...keys: string[])`: Check for any privilege (OR logic)
|
|
580
|
+
- `hasAllPrivileges(...keys: string[])`: Check for all privileges (AND logic)
|
|
581
|
+
- `privilegesAccess(arrays: string[][])`: Complex privilege logic
|
|
582
|
+
|
|
583
|
+
**Access Control (Field Level - Synchronous):**
|
|
584
|
+
|
|
585
|
+
- `checkPrivilege(key: string, user: any)`: Check for a single privilege
|
|
586
|
+
- `checkAnyPrivilege(user: any, ...keys: string[])`: Check for any privilege (OR logic)
|
|
587
|
+
- `checkAllPrivileges(user: any, ...keys: string[])`: Check for all privileges (AND logic)
|
|
588
|
+
- `checkPrivileges(arrays: string[][], user: any)`: Complex privilege logic
|
|
589
|
+
|
|
590
|
+
**Collection Privileges:**
|
|
591
|
+
|
|
592
|
+
- `generateCollectionPrivileges(collection)`: Generate privileges for a collection
|
|
593
|
+
- `getAllPrivileges()`: Get all collection privileges
|
|
594
|
+
- `getAllPrivilegeKeys()`: Get all collection privilege keys
|
|
595
|
+
- `allPrivilegesMap`: Map of all collection privileges
|
|
596
|
+
|
|
597
|
+
**Global Privileges:**
|
|
598
|
+
|
|
599
|
+
- `generateGlobalPrivileges(global)`: Generate privileges for a global
|
|
600
|
+
- `getAllGlobalPrivileges()`: Get all global privileges
|
|
601
|
+
- `getAllGlobalPrivilegeKeys()`: Get all global privilege keys
|
|
602
|
+
- `allGlobalPrivilegesMap`: Map of all global privileges
|
|
603
|
+
|
|
604
|
+
**Custom Privileges:**
|
|
605
|
+
|
|
606
|
+
- `registerCustomPrivilege(slug, config, options?)`: Register a single custom privilege
|
|
607
|
+
- `registerCustomPrivileges(slug, configs, options?)`: Register multiple custom privileges
|
|
608
|
+
- `customPrivilegesRegistry`: Map of all registered custom privileges
|
|
609
|
+
|
|
610
|
+
## Best Practices
|
|
611
|
+
|
|
612
|
+
1. **Always use the roles relationship**: Connect users to roles via a relationship field
|
|
613
|
+
2. **Start with Super Admin**: Create your first user with the Super Admin role
|
|
614
|
+
3. **Exclude system collections**: Exclude collections like migrations and preferences
|
|
615
|
+
4. **Consider global permissions**: Remember that globals only have read/update (no create/delete)
|
|
616
|
+
5. **Register custom privileges early**: Register custom privileges before the plugin initializes
|
|
617
|
+
6. **Use multilingual labels**: Provide translations for all languages your app supports
|
|
618
|
+
7. **Test privilege combinations**: Test different role configurations thoroughly
|
|
619
|
+
8. **Document custom roles**: Maintain documentation for custom roles you create
|
|
620
|
+
|
|
621
|
+
## Troubleshooting
|
|
622
|
+
|
|
623
|
+
### Privileges not working
|
|
624
|
+
|
|
625
|
+
Ensure your users collection has the roles relationship:
|
|
626
|
+
|
|
627
|
+
```ts
|
|
628
|
+
describe('Plugin tests', () => {
|
|
629
|
+
// Create tests to ensure expected behavior from the plugin
|
|
630
|
+
it('some condition that must be met', () => {
|
|
631
|
+
// Write your test logic here
|
|
632
|
+
expect(...)
|
|
633
|
+
})
|
|
634
|
+
})
|
|
635
|
+
```
|
|
636
|
+
|
|
637
|
+
## Best practices
|
|
638
|
+
|
|
639
|
+
With this tutorial and the plugin template, you should have everything you need to start building your own plugin.
|
|
640
|
+
In addition to the setup, here are other best practices aim we follow:
|
|
641
|
+
|
|
642
|
+
- **Providing an enable / disable option:** For a better user experience, provide a way to disable the plugin without uninstalling it. This is especially important if your plugin adds additional webpack aliases, this will allow you to still let the webpack run to prevent errors.
|
|
643
|
+
- **Include tests in your GitHub CI workflow**: If you’ve configured tests for your package, integrate them into your workflow to run the tests each time you commit to the plugin repository. Learn more about [how to configure tests into your GitHub CI workflow.](https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs)
|
|
644
|
+
- **Publish your finished plugin to NPM**: The best way to share and allow others to use your plugin once it is complete is to publish an NPM package. This process is straightforward and well documented, find out more [creating and publishing a NPM package here.](https://docs.npmjs.com/creating-and-publishing-scoped-public-packages/).
|
|
645
|
+
- **Add payload-plugin topic tag**: Apply the tag **payload-plugin **to your GitHub repository. This will boost the visibility of your plugin and ensure it gets listed with [existing payload plugins](https://github.com/topics/payload-plugin).
|
|
646
|
+
- **Use [Semantic Versioning](https://semver.org/) (SemVar)** - With the SemVar system you release version numbers that reflect the nature of changes (major, minor, patch). Ensure all major versions reference their Payload compatibility.
|
|
647
|
+
|
|
648
|
+
# Questions
|
|
649
|
+
|
|
650
|
+
Please contact [Payload](mailto:dev@payloadcms.com) with any questions about using this plugin template.
|
package/package.json
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "roles-privileges-payload-plugin",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Automatic role-based access control (RBAC) plugin for Payload CMS that generates granular CRUD privileges for all collections with beautiful UI and zero configuration",
|
|
5
|
+
"author": "Hassine",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./src/index.ts",
|
|
11
|
+
"types": "./src/index.ts",
|
|
12
|
+
"default": "./src/index.ts"
|
|
13
|
+
},
|
|
14
|
+
"./client": {
|
|
15
|
+
"import": "./src/exports/client.ts",
|
|
16
|
+
"types": "./src/exports/client.ts",
|
|
17
|
+
"default": "./src/exports/client.ts"
|
|
18
|
+
},
|
|
19
|
+
"./utilities": {
|
|
20
|
+
"import": "./src/exports/utilities.ts",
|
|
21
|
+
"types": "./src/exports/utilities.ts",
|
|
22
|
+
"default": "./src/exports/utilities.ts"
|
|
23
|
+
},
|
|
24
|
+
"./types": {
|
|
25
|
+
"import": "./src/exports/types.ts",
|
|
26
|
+
"types": "./src/exports/types.ts",
|
|
27
|
+
"default": "./src/exports/types.ts"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"main": "./src/index.ts",
|
|
31
|
+
"types": "./src/index.ts",
|
|
32
|
+
"files": [
|
|
33
|
+
"dist"
|
|
34
|
+
],
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "npm run copyfiles && npm run build:types && npm run build:swc",
|
|
37
|
+
"build:swc": "swc ./src -d ./dist --config-file .swcrc --strip-leading-paths",
|
|
38
|
+
"build:types": "tsc --outDir dist --rootDir ./src",
|
|
39
|
+
"clean": "rimraf {dist,*.tsbuildinfo}",
|
|
40
|
+
"copyfiles": "copyfiles -u 1 \"src/**/*.{html,css,scss,ttf,woff,woff2,eot,svg,jpg,png,json}\" dist/",
|
|
41
|
+
"dev": "next dev dev --turbo",
|
|
42
|
+
"dev:generate-importmap": "npm run dev:payload generate:importmap",
|
|
43
|
+
"dev:generate-types": "npm run dev:payload generate:types",
|
|
44
|
+
"dev:payload": "cross-env PAYLOAD_CONFIG_PATH=./dev/payload.config.ts payload",
|
|
45
|
+
"generate:importmap": "npm run dev:generate-importmap",
|
|
46
|
+
"generate:types": "npm run dev:generate-types",
|
|
47
|
+
"lint": "eslint",
|
|
48
|
+
"lint:fix": "eslint ./src --fix",
|
|
49
|
+
"test": "npm run test:int && npm run test:e2e",
|
|
50
|
+
"test:e2e": "playwright test",
|
|
51
|
+
"test:int": "vitest"
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@eslint/eslintrc": "^3.2.0",
|
|
55
|
+
"@payloadcms/db-mongodb": "3.37.0",
|
|
56
|
+
"@payloadcms/db-postgres": "3.37.0",
|
|
57
|
+
"@payloadcms/db-sqlite": "3.37.0",
|
|
58
|
+
"@payloadcms/eslint-config": "3.9.0",
|
|
59
|
+
"@payloadcms/next": "3.37.0",
|
|
60
|
+
"@payloadcms/richtext-lexical": "3.37.0",
|
|
61
|
+
"@payloadcms/ui": "3.37.0",
|
|
62
|
+
"@playwright/test": "1.56.1",
|
|
63
|
+
"@swc-node/register": "1.10.9",
|
|
64
|
+
"@swc/cli": "0.6.0",
|
|
65
|
+
"@types/node": "^22.5.4",
|
|
66
|
+
"@types/react": "19.2.1",
|
|
67
|
+
"@types/react-dom": "19.2.1",
|
|
68
|
+
"copyfiles": "2.4.1",
|
|
69
|
+
"cross-env": "^7.0.3",
|
|
70
|
+
"eslint": "^9.23.0",
|
|
71
|
+
"eslint-config-next": "15.4.7",
|
|
72
|
+
"graphql": "^16.8.1",
|
|
73
|
+
"mongodb-memory-server": "10.1.4",
|
|
74
|
+
"next": "15.4.10",
|
|
75
|
+
"open": "^10.1.0",
|
|
76
|
+
"payload": "3.37.0",
|
|
77
|
+
"prettier": "^3.4.2",
|
|
78
|
+
"qs-esm": "7.0.2",
|
|
79
|
+
"react": "19.2.1",
|
|
80
|
+
"react-dom": "19.2.1",
|
|
81
|
+
"rimraf": "3.0.2",
|
|
82
|
+
"sharp": "0.34.2",
|
|
83
|
+
"sort-package-json": "^2.10.0",
|
|
84
|
+
"typescript": "5.7.3",
|
|
85
|
+
"vite-tsconfig-paths": "^5.1.4",
|
|
86
|
+
"vitest": "^3.1.2"
|
|
87
|
+
},
|
|
88
|
+
"peerDependencies": {
|
|
89
|
+
"payload": "^3.37.0"
|
|
90
|
+
},
|
|
91
|
+
"engines": {
|
|
92
|
+
"node": "^18.20.2 || >=20.9.0",
|
|
93
|
+
"pnpm": "^9 || ^10"
|
|
94
|
+
},
|
|
95
|
+
"publishConfig": {
|
|
96
|
+
"exports": {
|
|
97
|
+
".": {
|
|
98
|
+
"import": "./dist/index.js",
|
|
99
|
+
"types": "./dist/index.d.ts",
|
|
100
|
+
"default": "./dist/index.js"
|
|
101
|
+
},
|
|
102
|
+
"./client": {
|
|
103
|
+
"import": "./dist/exports/client.js",
|
|
104
|
+
"types": "./dist/exports/client.d.ts",
|
|
105
|
+
"default": "./dist/exports/client.js"
|
|
106
|
+
},
|
|
107
|
+
"./utilities": {
|
|
108
|
+
"import": "./dist/exports/utilities.js",
|
|
109
|
+
"types": "./dist/exports/utilities.d.ts",
|
|
110
|
+
"default": "./dist/exports/utilities.js"
|
|
111
|
+
},
|
|
112
|
+
"./types": {
|
|
113
|
+
"import": "./dist/exports/types.js",
|
|
114
|
+
"types": "./dist/exports/types.d.ts",
|
|
115
|
+
"default": "./dist/exports/types.js"
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
"main": "./dist/index.js",
|
|
119
|
+
"types": "./dist/index.d.ts"
|
|
120
|
+
},
|
|
121
|
+
"pnpm": {
|
|
122
|
+
"onlyBuiltDependencies": [
|
|
123
|
+
"sharp",
|
|
124
|
+
"esbuild",
|
|
125
|
+
"unrs-resolver"
|
|
126
|
+
]
|
|
127
|
+
},
|
|
128
|
+
"registry": "https://registry.npmjs.org/",
|
|
129
|
+
"dependencies": {}
|
|
130
|
+
}
|