@admicaa/netpress-permissions
v0.2.3
Published
Laravel-style roles, permissions, policies, and auth guard package for NetPress.
Maintainers
Readme
NetPress Permissions
Laravel-style roles, permissions, morph assignments, and can() integration for NetPress.
What It Includes
RoleandPermissionmodels- Polymorphic model assignments for roles and direct permissions
- Trait-style mixins for authorizable models
can()support that works with both registered policies and direct permissions- Migration helpers for creating the package tables
- Guard-scoped roles and permissions (
guardName) — the same role/permission name can coexist across multiple auth guards (web,api,admin, …) AuthGuardroute middleware forauthenticated/can/role/permission/guestgates- A publishable config, migration, and service provider via
vendor:publish
Publish Into An App
From a NetPress application:
npm run artisan -- vendor:publish --provider=NetpressPermissionsServiceProviderThe publish flow will:
- ask which configured database connection should store the permissions data
- publish
config/permissions.js - publish
database/migrations/*_create_permission_tables.js - publish
app/Providers/PermissionsServiceProvider.js - register the provider in
bootstrap/providers.js
If you pick a Mongo connection, the published migration targets that Mongo connection automatically.
Quick Start
import { BaseModel, registerPolicy } from '@admicaa/netpress';
import {
Authorizable,
Role,
Permission,
createPermissionTables,
} from '@admicaa/netpress-permissions';
class User extends Authorizable(BaseModel) {
static table = 'users';
}
class Post extends BaseModel {
static table = 'posts';
}
class PostPolicy {
async update(authUser, post) {
return authUser.id === post.userId;
}
}
registerPolicy(Post, PostPolicy);Migrations
Create an application migration that delegates to the package helper:
import { createPermissionTables, dropPermissionTables } from '@admicaa/netpress-permissions';
export async function up(schema) {
await createPermissionTables(schema);
}
export async function down(schema) {
await dropPermissionTables(schema);
}Usage
const editor = await Role.findOrCreate('editor');
const publishPosts = await Permission.findOrCreate('posts.publish');
await editor.givePermissionTo(publishPosts);
const user = await User.create({ name: 'Amina' });
await user.assignRole(editor);
await user.can('posts.publish'); // true
await user.can('update', post); // policy-awareGuards (guardName)
Every role and permission has a guardName column, mirroring Laravel Spatie. It lets the
same name live in different auth guards (for example admin under web and admin under api).
The default guard is resolved in this order:
config.permissions.defaultGuard(if set)- The core
Auth.defaultGuard()(i.e. whatever you calledAuth.setDefaultGuard(...)with) 'web'as a final fallback
// Create role/permission scoped to a specific guard:
const webAdmin = await Role.findOrCreate('admin', 'web');
const apiAdmin = await Role.findOrCreate('admin', 'api');
// Second argument may be guard *or* values (Spatie-style convenience):
const perm = await Permission.findOrCreate('posts.publish', { group: 'posts' });Pin a user model to a specific guard by declaring it on the class — this is the hook the
traits use (getDefaultGuardName()), equivalent to Spatie's model method of the same name:
class ApiUser extends Authorizable(BaseModel) {
static table = 'users';
static guardName = 'api'; // roles/permissions resolved under the 'api' guard
}
const apiUser = await ApiUser.create({ name: 'Amina' });
await apiUser.assignRole('admin'); // resolves 'admin' in the 'api' guard
await apiUser.hasRole('admin'); // only matches api-scoped admin rows
await apiUser.hasPermissionTo('posts.publish');String-based lookups (assignRole('admin'), hasRole('admin'), hasPermissionTo('posts.publish'), …)
are all filtered by the user's resolved guard, so a role named admin under web will not
satisfy a check made by an api user.
AuthGuard middleware
Express-compatible middleware factories for the most common authorization gates. All
middleware resolves the current user via the core Auth layer and responds with the
conventional 401/403 status codes.
import { AuthGuard } from '@admicaa/netpress-permissions';
router.get('/dashboard', AuthGuard.authenticated(), showDashboard);
router.get('/posts/:id/edit', AuthGuard.can('posts.update', 'id'), editPost);
router.post('/posts', AuthGuard.permission('posts.publish'), createPost);
router.delete('/users/:id', AuthGuard.role('admin', 'owner'), deleteUser);
router.get('/billing', AuthGuard.roles('admin', 'billing'), billing);
router.get('/login', AuthGuard.guest(), loginPage);Available gates:
| Gate | Allows a request through when… |
| --- | --- |
| AuthGuard.authenticated() | a user is authenticated on the active guard (else 401) |
| AuthGuard.can(ability, resource?) | the user passes can(ability, resource) — integrates with registered policies (else 403) |
| AuthGuard.canAny(abilities, resource?) | any of the listed abilities succeed |
| AuthGuard.canAll(abilities, resource?) | every listed ability succeeds |
| AuthGuard.role(...names) | user has any of the given roles |
| AuthGuard.roles(...names) | user has all of the given roles |
| AuthGuard.permission(...names) | user has any of the given permissions |
| AuthGuard.permissions(...names) | user has all of the given permissions |
| AuthGuard.guest() | no authenticated user on the active guard (else 403) |
The resource argument of can/canAny/canAll may be:
- a string — read as
req.params[resource](e.g.'id'→req.params.id) - a function — called with
req, return the resource to authorize against - any value — passed through as-is
Because the role/permission checks flow through the same getDefaultGuardName() hook on the
user model, AuthGuard naturally respects guard scoping: an api user will only match
api-scoped roles and permissions.
