Skip to content

levart/ngx-signal-permissions

ngx-signal-permissions

A modern, signal-based Angular library for managing permissions and roles with full TypeScript support.

npm version License: MIT CI Angular

Features

  • 🚦 Signal-based reactivity - Built on Angular Signals for optimal performance
  • 🎯 Clean Control Flow - Works seamlessly with @if, @for, @switch
  • 🛡️ Route Guards - Functional guards with async loading support
  • 📦 Standalone - No modules required, tree-shakable
  • 🔌 Policies - Define complex permission rules as reusable policies
  • 🌳 Wildcards - Hierarchical permission matching (admin:*)
  • 🧪 Testing Utilities - Easy mocking for unit tests
  • 📱 Angular 17+ - Modern Angular features support

Installation

npm install ngx-signal-permissions

Quick Start

1. Provide the library

// app.config.ts
import { ApplicationConfig } from '@angular/core';
import {
  providePermissions,
  withPermissions,
  withRoles,
} from 'ngx-signal-permissions';

export const appConfig: ApplicationConfig = {
  providers: [
    providePermissions(
      withPermissions(['posts:read', 'posts:write']),
      withRoles({
        admin: ['users:manage', 'settings:edit'],
        editor: ['posts:publish'],
      })
    ),
  ],
};

2. Use in components

import { Component } from '@angular/core';
import { hasPermission, hasRole } from 'ngx-signal-permissions';

@Component({
  selector: 'app-dashboard',
  template: `
    @if (canEdit()) {
      <button>Edit Post</button>
    }

    @if (isAdmin()) {
      <admin-panel />
    }
  `,
})
export class DashboardComponent {
  canEdit = hasPermission('posts:write');
  isAdmin = hasRole('admin');
}

Core Concepts

The Permissions Store

The PermissionsStore is the central service that manages all permissions and roles:

import { inject } from '@angular/core';
import { PermissionsStore } from 'ngx-signal-permissions';

export class MyService {
  private store = inject(PermissionsStore);

  loadUserPermissions(user: User) {
    this.store.loadPermissions(user.permissions);
    this.store.loadRoles(user.roles);
  }

  checkAccess() {
    // Immediate value
    if (this.store.hasPermission('admin')) {
      // ...
    }

    // Reactive signal
    const canEdit = this.store.hasPermissionSignal('posts:edit');
  }
}

Functional Helpers

The cleanest way to use permissions with Angular's control flow:

import {
  hasPermission,
  hasAnyPermission,
  hasAllPermissions,
  hasRole,
  hasAnyRole,
  authorize,
  withPermissions,
} from 'ngx-signal-permissions';

@Component({
  template: `
    @if (canRead()) { <content /> }
    @if (canManage()) { <admin-tools /> }
    @if (isPrivileged()) { <premium-content /> }
  `,
})
export class MyComponent {
  // Single permission
  canRead = hasPermission('posts:read');

  // Multiple permissions (any)
  canManage = hasAnyPermission(['posts:edit', 'posts:delete']);

  // Multiple permissions (all required)
  needsAll = hasAllPermissions(['read', 'write', 'delete']);

  // Role check
  isAdmin = hasRole('admin');

  // Combined check
  isPrivileged = authorize({
    permissions: ['premium:access'],
    roles: ['subscriber', 'admin'],
    strategy: 'any',
  });

  // Composable pattern
  protected perms = withPermissions();
  // Then use: this.perms.can('edit'), this.perms.isRole('admin'), etc.
}

Directives

For template-based permission checks:

import {
  HasPermissionDirective,
  HasRoleDirective,
  ExceptPermissionDirective,
} from 'ngx-signal-permissions';

@Component({
  imports: [HasPermissionDirective, HasRoleDirective, ExceptPermissionDirective],
  template: `
    <!-- Single permission -->
    <button *hasPermission="'delete'">Delete</button>

    <!-- Multiple permissions -->
    <nav *hasPermission="['admin', 'moderator']; strategy: 'any'">
      Admin Nav
    </nav>

    <!-- With else template -->
    <div *hasPermission="'premium'; else freeTpl">
      Premium Content
    </div>
    <ng-template #freeTpl>
      <button>Upgrade</button>
    </ng-template>

    <!-- Except directive -->
    <div *exceptPermission="'guest'">
      Logged in content
    </div>

    <!-- Role directive -->
    <admin-panel *hasRole="'admin'" />
  `,
})
export class MyComponent {}

Components

Declarative permission blocks:

import { WhenPermittedComponent } from 'ngx-signal-permissions';

@Component({
  imports: [WhenPermittedComponent],
  template: `
    <when-permitted only="admin">
      <admin-dashboard />
    </when-permitted>

    <when-permitted [only]="['edit', 'delete']" strategy="all">
      <editor-toolbar />
      <ng-template fallback>
        <read-only-view />
      </ng-template>
    </when-permitted>

    <when-permitted role="premium">
      <premium-features />
    </when-permitted>
  `,
})
export class MyComponent {}

Route Guards

Protect routes with functional guards:

import {
  permissionGuard,
  permissionMatcher,
  routePermissionGuard,
} from 'ngx-signal-permissions';

export const routes: Routes = [
  // Inline configuration
  {
    path: 'admin',
    loadComponent: () => import('./admin/admin.component'),
    canActivate: [
      permissionGuard({
        only: 'admin',
        redirectTo: '/unauthorized',
      }),
    ],
  },

  // Multiple permissions
  {
    path: 'editor',
    loadComponent: () => import('./editor/editor.component'),
    canActivate: [
      permissionGuard({
        only: ['posts:write', 'posts:publish'],
        strategy: 'all',
        redirectTo: '/dashboard',
      }),
    ],
  },

  // Prevent lazy loading if no permission
  {
    path: 'premium',
    loadChildren: () => import('./premium/routes'),
    canMatch: [permissionMatcher({ only: 'premium' })],
  },

  // Route data based
  {
    path: 'settings',
    loadComponent: () => import('./settings/settings.component'),
    canActivate: [routePermissionGuard],
    data: {
      permissions: {
        only: ['settings:view'],
        except: ['banned'],
        redirectTo: '/home',
      },
    },
  },
];

Pipes

For inline template checks:

import { HasPermissionPipe, HasRolePipe } from 'ngx-signal-permissions';

@Component({
  imports: [HasPermissionPipe, HasRolePipe],
  template: `
    <button [disabled]="!('edit' | hasPermission)">Edit</button>
    <button [class.hidden]="!('admin' | hasRole)">Admin</button>
  `,
})
export class MyComponent {}

Policies

Define complex permission rules:

import { PolicyBuilder, PolicyService } from 'ngx-signal-permissions';

// Define policies
const canManageUsers = new PolicyBuilder()
  .hasPermission('users:read')
  .hasAnyPermission(['users:write', 'users:admin'])
  .hasAnyRole(['admin', 'hr'])
  .build('canManageUsers');

const canPublishPosts = new PolicyBuilder()
  .hasAllPermissions(['posts:write', 'posts:publish'])
  .lacksRole('suspended')
  .build('canPublishPosts');

// Register in config
providePermissions(
  withPolicies([canManageUsers, canPublishPosts])
);

// Use in component
@Component({
  template: `
    @if (canManage()) {
      <user-management />
    }
  `,
})
export class MyComponent {
  private policyService = inject(PolicyService);
  canManage = this.policyService.checkSignal('canManageUsers');
}

Wildcard Permissions

Support for hierarchical permission matching:

// Grant wildcard permission
store.loadPermissions(['admin:*']);

// These all return true
store.hasPermission('admin:read');
store.hasPermission('admin:write');
store.hasPermission('admin:users:delete');

// Configure separator
store.configure({ hierarchySeparator: '.' });
store.loadPermissions(['admin.*']);
store.hasPermission('admin.users.read'); // true

Async Loading

Load permissions from an API:

// Simple async loading
await store.loadPermissionsAsync(async () => {
  const response = await fetch('/api/user/permissions');
  return response.json();
});

// Full source configuration
await store.loadFromSource({
  loader: () => fetch('/api/user').then(r => r.json()),
  mapper: (user) => ({
    permissions: user.permissions,
    roles: user.roles.reduce((acc, role) => {
      acc[role.name] = role.permissions;
      return acc;
    }, {}),
  }),
});

// With provider
providePermissions(
  withAsyncSource({
    loader: () => fetch('/api/auth').then(r => r.json()),
    mapper: (data) => ({
      permissions: data.permissions,
      roles: data.roles,
    }),
  })
);

Testing

import { TestBed } from '@angular/core/testing';
import {
  providePermissionsTesting,
  createTestStore,
} from 'ngx-signal-permissions/testing';

describe('MyComponent', () => {
  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [MyComponent],
      providers: [
        providePermissionsTesting({
          permissions: ['read', 'write'],
          roles: { admin: ['delete'] },
        }),
      ],
    });
  });

  it('should show edit button for users with write permission', () => {
    const fixture = TestBed.createComponent(MyComponent);
    fixture.detectChanges();

    const button = fixture.nativeElement.querySelector('.edit-btn');
    expect(button).toBeTruthy();
  });
});

// Manual store for isolated tests
it('should check permissions', () => {
  const store = createTestStore({
    permissions: ['read'],
    roles: { user: [] },
  });

  expect(store.hasPermission('read')).toBeTrue();
  expect(store.hasPermission('write')).toBeFalse();
});

API Reference

PermissionsStore

Method Description
loadPermissions(permissions: string[]) Replace all permissions
addPermission(permission: string) Add single permission
removePermission(permission: string) Remove single permission
hasPermission(permission: string): boolean Check permission (immediate)
hasPermissionSignal(permission: string): Signal<boolean> Check permission (reactive)
hasAnyPermission(permissions: string[]): boolean Check any permission
hasAllPermissions(permissions: string[]): boolean Check all permissions
addRole(role: string, permissions?: string[]) Add role
removeRole(role: string) Remove role
hasRole(role: string): boolean Check role (immediate)
hasRoleSignal(role: string): Signal<boolean> Check role (reactive)
flush() Clear all data
loadFromSource(source: PermissionSource) Load from async source

Functional Helpers

Function Description
hasPermission(permission) Returns Signal<boolean>
hasAnyPermission(permissions) Returns Signal<boolean>
hasAllPermissions(permissions) Returns Signal<boolean>
hasRole(role) Returns Signal<boolean>
hasAnyRole(roles) Returns Signal<boolean>
authorize(config) Combined check, returns Signal<boolean>
withPermissions() Returns composable with all helpers

Guards

Guard Description
permissionGuard(config) Inline config guard
permissionMatcher(config) Route matcher
routePermissionGuard Route data based guard
roleGuard(config) Role-only guard

Migration from ngx-permissions

// Before (ngx-permissions)
<div *ngxPermissionsOnly="['admin']">Admin content</div>

// After (ngx-signal-permissions)
<div *hasPermission="'admin'">Admin content</div>

// Or with modern control flow (recommended)
@if (isAdmin()) {
  <div>Admin content</div>
}

// In component
isAdmin = hasRole('admin');

License

MIT

About

No description, website, or topics provided.

Resources

License

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Sponsor this project

Packages

 
 
 

Contributors

Languages