Skip to content

Commit 0055aea

Browse files
authored
Merge pull request #1 from sSeewald/feature/public-access
Add public role feature
2 parents 37f202b + 03e5661 commit 0055aea

34 files changed

+1906
-468
lines changed

README.md

Lines changed: 156 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,22 @@ A comprehensive, production-ready permissions system with role-based access cont
3030
-**Zero Config Start** - Works out of the box with sensible defaults
3131
- 🔧 **Fully Typed** - Complete TypeScript support
3232

33+
## UI Overview
34+
35+
Payload Gatekeeper provides a clean and intuitive interface for managing roles and permissions:
36+
37+
### Roles Management
38+
![Roles Collection](docs/roles-collection-with-default-roles.png)
39+
*The Roles collection showing system default roles - fully manageable through the UI*
40+
41+
### User Role Assignment
42+
![User Creation with Role Selection](docs/users-collection-create-new-user-select-roles.png)
43+
*Assigning roles when creating new users - with searchable dropdown*
44+
45+
### Users with Roles
46+
![Users Collection with Roles](docs/users-collection-with-roles.png)
47+
*Users overview showing their assigned roles*
48+
3349
## Installation
3450

3551
```bash
@@ -55,13 +71,22 @@ export default buildConfig({
5571
collections: {
5672
'users': {
5773
enhance: true,
74+
autoAssignFirstUser: true,
5875
}
59-
}
76+
},
77+
// Exclude collections from permission system entirely
78+
excludeCollections: ['special-config'] // These use their own access control
6079
})
6180
]
6281
})
6382
```
6483

84+
### First User Setup
85+
When `autoAssignFirstUser` is enabled, the first user automatically receives the super_admin role:
86+
87+
![First User Creation](docs/create-first-user.png)
88+
*The first user gets super admin privileges automatically*
89+
6590
## Configuration
6691

6792
### Full Configuration Example
@@ -143,11 +168,8 @@ export default buildConfig({
143168
// Optional: Exclude collections from permission system
144169
excludeCollections: ['public-pages'],
145170

146-
// Optional: Enable audit logging
147-
enableAuditLog: false,
148-
149171
// Environment-based options
150-
seedingMode: false, // Set to true during seeding
172+
skipPermissionChecks: false, // Set to true during seeding/migration
151173
syncRolesOnInit: process.env.SYNC_ROLES === 'true',
152174

153175
// UI customization
@@ -160,6 +182,41 @@ export default buildConfig({
160182

161183
## Core Concepts
162184

185+
### Public Access
186+
187+
Non-authenticated users automatically have configurable public access:
188+
189+
```typescript
190+
// Default behavior - public can read all non-auth collections
191+
gatekeeperPlugin({})
192+
193+
// Custom public permissions
194+
gatekeeperPlugin({
195+
publicRolePermissions: [
196+
'*.read', // Read all collections
197+
'comments.create', // Can create comments
198+
'reactions.create' // Can add reactions
199+
]
200+
})
201+
202+
// Completely private system
203+
gatekeeperPlugin({
204+
disablePublicRole: true // No public access at all
205+
})
206+
```
207+
208+
**Important:** Auth-enabled collections (users, admins) are ALWAYS protected from public access, regardless of public permissions.
209+
210+
### Role Management
211+
212+
The plugin automatically creates a Roles collection where you can manage all roles through the UI:
213+
214+
![Creating Editor Role](docs/roles-collection-create-new-role-editor.png)
215+
*Creating a new editor role with specific permissions*
216+
217+
![Roles with Custom Editor](docs/roles-collection-with-custom-role-editor.png)
218+
*Roles collection showing both system and custom roles*
219+
163220
### Permission Patterns
164221

165222
The plugin supports various permission patterns:
@@ -175,13 +232,16 @@ The plugin supports various permission patterns:
175232
Protected roles cannot be deleted and have restricted field updates. Only users with `*` permission can modify protected roles.
176233

177234
```typescript
178-
{
235+
const superAdminRole = {
179236
name: 'super_admin',
180237
permissions: ['*'],
181238
protected: true, // Cannot be deleted, limited updates
182239
}
183240
```
184241

242+
![Super Admin Role Details](docs/role-details-super-admin.png)
243+
*Protected super admin role showing the lock indicator and full permissions*
244+
185245
### UI Visibility vs CRUD Operations
186246

187247
The plugin separates UI visibility from CRUD operations:
@@ -405,14 +465,18 @@ gatekeeperPlugin({
405465
})
406466
```
407467

408-
### Seeding First Admin User
468+
### Seeding Users
469+
470+
#### Simple: First Admin User (with autoAssignFirstUser)
471+
472+
When `autoAssignFirstUser: true` is configured, you don't need to search for roles - the first user automatically gets super_admin:
409473

410474
```typescript
411475
// seed-admin.ts
412476
import { getPayload } from 'payload'
413477
import config from './payload.config'
414478

415-
async function seedAdmin() {
479+
async function seedFirstAdmin() {
416480
const payload = await getPayload({ config })
417481

418482
try {
@@ -426,38 +490,81 @@ async function seedAdmin() {
426490
return
427491
}
428492

429-
// Find the super_admin role (created by plugin on first start)
430-
const superAdminRole = await payload.find({
431-
collection: 'roles',
432-
where: {
433-
name: { equals: 'super_admin' },
434-
},
435-
})
436-
437-
if (superAdminRole.docs.length === 0) {
438-
throw new Error('Super admin role not found! Start the application first.')
439-
}
440-
441-
// Create the first admin user
493+
// Create the first admin user - automatically gets super_admin role!
442494
await payload.create({
443495
collection: 'users',
444496
data: {
445497
email: 'admin@example.com',
446498
password: 'SecurePassword123!',
447-
role: superAdminRole.docs[0].id,
448-
// ... other fields
499+
// No need to set role - autoAssignFirstUser handles it
449500
},
450501
})
451502

452-
console.log('Admin user created successfully')
503+
console.log('First admin user created with super_admin role')
453504
} catch (error) {
454505
console.error('Error seeding admin:', error)
455506
}
456507

457508
process.exit(0)
458509
}
459510

460-
seedAdmin()
511+
seedFirstAdmin()
512+
```
513+
514+
#### Advanced: Multiple Users with Specific Roles
515+
516+
Only search for roles when you need to create additional users with specific roles:
517+
518+
```typescript
519+
// seed-users.ts
520+
async function seedUsers() {
521+
const payload = await getPayload({ config })
522+
523+
try {
524+
// Find specific roles for additional users
525+
const editorRole = await payload.find({
526+
collection: 'roles',
527+
where: { name: { equals: 'editor' } },
528+
limit: 1,
529+
})
530+
531+
const viewerRole = await payload.find({
532+
collection: 'roles',
533+
where: { name: { equals: 'viewer' } },
534+
limit: 1,
535+
})
536+
537+
// Create editor user
538+
if (editorRole.docs.length > 0) {
539+
await payload.create({
540+
collection: 'users',
541+
data: {
542+
email: 'editor@example.com',
543+
password: 'EditorPass123!',
544+
role: editorRole.docs[0].id,
545+
},
546+
})
547+
}
548+
549+
// Create viewer user
550+
if (viewerRole.docs.length > 0) {
551+
await payload.create({
552+
collection: 'users',
553+
data: {
554+
email: 'viewer@example.com',
555+
password: 'ViewerPass123!',
556+
role: viewerRole.docs[0].id,
557+
},
558+
})
559+
}
560+
561+
console.log('✅ Additional users created')
562+
} catch (error) {
563+
console.error('Error seeding users:', error)
564+
}
565+
}
566+
567+
seedUsers()
461568
```
462569

463570
### Custom Permission Checks in Your Code
@@ -494,8 +601,9 @@ async function myCustomEndpoint(req: PayloadRequest) {
494601
| `collections` | `object` | Collection-specific configuration | `{}` |
495602
| `systemRoles` | `array` | Roles to create/sync on init | `[]` |
496603
| `excludeCollections` | `string[]` | Collections to exclude from permission system | `[]` |
497-
| `enableAuditLog` | `boolean` | Enable audit logging | `false` |
498-
| `seedingMode` | `boolean` | Skip permission checks during seeding | `false` |
604+
| `disablePublicRole` | `boolean` | Disable public access for non-authenticated users | `false` |
605+
| `publicRolePermissions` | `string[]` | Custom permissions for public users | `['*.read']` |
606+
| `skipPermissionChecks` | `boolean \| (() => boolean)` | Skip permission checks (for seeding/migration) | `false` |
499607
| `syncRolesOnInit` | `boolean` | Force role sync on every init | `false` |
500608
| `rolesGroup` | `string` | UI group name for Roles collection | `'System'` |
501609
| `rolesSlug` | `string` | Custom slug for Roles collection | `'roles'` |
@@ -615,16 +723,32 @@ const Products: CollectionConfig = {
615723
// Both must pass for access to be granted
616724
```
617725

618-
### Environment Variables
726+
### Using Environment Variables
619727

620-
```bash
621-
# Skip permission checks during seeding (configure in plugin options)
622-
npm run seed
728+
The plugin doesn't read environment variables directly. You need to configure them in your plugin options:
729+
730+
```typescript
731+
// payload.config.ts
732+
gatekeeperPlugin({
733+
// Use environment variables in your config
734+
syncRolesOnInit: process.env.SYNC_ROLES === 'true',
735+
skipPermissionChecks: process.env.SKIP_PERMISSIONS === 'true',
736+
737+
// Or use a function for dynamic control
738+
skipPermissionChecks: () => process.env.NODE_ENV === 'seed',
739+
})
740+
```
623741

742+
Then run your application with environment variables:
743+
744+
```bash
624745
# Force role synchronization
625746
SYNC_ROLES=true npm run dev
626747

627-
# Development mode (auto-syncs roles)
748+
# Skip permissions during seeding
749+
NODE_ENV=seed npm run seed
750+
751+
# Development mode (auto-syncs roles when NODE_ENV=development)
628752
NODE_ENV=development npm run dev
629753
```
630754

@@ -668,8 +792,6 @@ const role: Role = {
668792

669793
## Testing
670794

671-
The plugin has comprehensive test coverage with a blackbox testing approach:
672-
673795
```bash
674796
# Run all tests
675797
npm test
@@ -782,16 +904,6 @@ Contributions are welcome! Please read our contributing guidelines before submit
782904

783905
For issues, questions, or suggestions, please open an issue on GitHub.
784906

785-
## Roadmap
786-
787-
- [ ] Global permissions UI component
788-
- [ ] Audit log persistence
789-
- [ ] Permission templates
790-
- [ ] Dynamic permission generation from custom fields
791-
- [ ] GraphQL support
792-
- [ ] Permission caching layer
793-
- [ ] Role hierarchy support
794-
795907
## Acknowledgments
796908

797909
Built with ❤️ for the [Payload CMS](https://payloadcms.com/) community.

docs/create-first-user.png

308 KB
Loading

docs/role-details-super-admin.png

505 KB
Loading
430 KB
Loading
412 KB
Loading
389 KB
Loading
391 KB
Loading
383 KB
Loading

jest.setup.js

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,29 @@ global.console = {
66
warn: jest.fn(),
77
error: jest.fn(),
88
debug: jest.fn(),
9-
}
9+
}
10+
11+
// Mock Payload module and its error classes
12+
jest.mock('payload', () => {
13+
// Create mock error classes that match Payload's error structure
14+
class ValidationError extends Error {
15+
constructor(options) {
16+
super(options.errors?.[0]?.message || 'Validation error')
17+
this.name = 'ValidationError'
18+
this.errors = options.errors || []
19+
this.collection = options.collection
20+
}
21+
}
22+
23+
class Forbidden extends Error {
24+
constructor(message) {
25+
super(message || 'Forbidden')
26+
this.name = 'Forbidden'
27+
}
28+
}
29+
30+
return {
31+
ValidationError,
32+
Forbidden,
33+
}
34+
})

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,18 @@
33
"version": "1.0.0",
44
"description": "The ultimate access control gatekeeper for Payload CMS v3 - Advanced RBAC with wildcard support, auto role assignment, and flexible configuration",
55
"type": "module",
6-
"main": "dist/cjs/index.cjs",
6+
"main": "dist/cjs/index.js",
77
"module": "dist/esm/index.js",
88
"types": "dist/esm/index.d.ts",
99
"exports": {
1010
".": {
1111
"types": "./dist/esm/index.d.ts",
1212
"import": "./dist/esm/index.js",
13-
"require": "./dist/cjs/index.cjs"
13+
"require": "./dist/cjs/index.js"
1414
},
1515
"./components/*": {
1616
"import": "./dist/esm/components/*.js",
17-
"require": "./dist/cjs/components/*.cjs"
17+
"require": "./dist/cjs/components/*.js"
1818
},
1919
"./package.json": "./package.json"
2020
},

0 commit comments

Comments
 (0)