npm package: @nestbolt/permissions
Description: Roles & permissions (RBAC) for NestJS applications with TypeORM support.
Latest version: 0.1.0

<p align="center">
    <h1 align="center">@nestbolt/permissions</h1>
    <p align="center">Roles & permissions (RBAC) for NestJS applications with TypeORM support.</p>
</p>

<p align="center">
    <a href="https://www.npmjs.com/package/@nestbolt/permissions"><img src="https://img.shields.io/npm/v/@nestbolt/permissions.svg?style=flat-square" alt="npm version"></a>
    <a href="https://www.npmjs.com/package/@nestbolt/permissions"><img src="https://img.shields.io/npm/dt/@nestbolt/permissions.svg?style=flat-square" alt="npm downloads"></a>
    <a href="https://github.com/nestbolt/permissions/actions"><img src="https://img.shields.io/github/actions/workflow/status/nestbolt/permissions/tests.yml?branch=main&style=flat-square&label=tests" alt="tests"></a>
    <a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square" alt="license"></a>
</p>

<hr>

This package provides a **role-based access control (RBAC) system** for [NestJS](https://nestjs.com) that lets you manage roles and permissions with TypeORM, protect routes with guards, and check permissions programmatically.

Once installed, using it is as simple as:

```typescript
@UseGuards(JwtAuthGuard, RolesGuard)
@RequireRoles('admin')
@Get('admin/dashboard')
dashboard() { return { message: 'Welcome, admin!' }; }
```

## Table of Contents

- [Installation](#installation)
- [Quick Start](#quick-start)
- [Module Configuration](#module-configuration)
  - [Static Configuration (forRoot)](#static-configuration-forroot)
  - [Async Configuration (forRootAsync)](#async-configuration-forrootasync)
- [Using the Guards & Decorators](#using-the-guards--decorators)
- [Using the Service Directly](#using-the-service-directly)
- [Implementing PermissionUserRepository](#implementing-permissionuserrepository)
- [Configuration Options](#configuration-options)
- [Features](#features)
  - [Wildcard Permissions](#wildcard-permissions)
  - [Events](#events)
  - [Caching](#caching)
- [Testing](#testing)
- [Changelog](#changelog)
- [Contributing](#contributing)
- [Security](#security)
- [Credits](#credits)
- [License](#license)

## Installation

Install the package via npm:

```bash
npm install @nestbolt/permissions
```

Or via yarn:

```bash
yarn add @nestbolt/permissions
```

Or via pnpm:

```bash
pnpm add @nestbolt/permissions
```

### Peer Dependencies

This package requires the following peer dependencies, which you likely already have in a NestJS project:

```
@nestjs/common    ^10.0.0 || ^11.0.0
@nestjs/core      ^10.0.0 || ^11.0.0
@nestjs/typeorm   ^10.0.0 || ^11.0.0
typeorm           ^0.3.0
class-validator   ^0.14.0
class-transformer ^0.5.0
reflect-metadata  ^0.1.13 || ^0.2.0
```

Optional:

```
@nestjs/event-emitter ^2.0.0 || ^3.0.0   # For permission/role lifecycle events
```

## Quick Start

### 1. Implement the PermissionUserRepository

Create a repository that bridges your User entity with the permission system:

```typescript
import { Injectable } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from "typeorm";
import {
  PermissionUserRepository,
  UserHasRolesEntity,
  UserHasPermissionsEntity,
} from "@nestbolt/permissions";

@Injectable()
export class TypeOrmPermissionUserRepository implements PermissionUserRepository {
  constructor(
    @InjectRepository(UserHasRolesEntity)
    private readonly userRolesRepo: Repository<UserHasRolesEntity>,
    @InjectRepository(UserHasPermissionsEntity)
    private readonly userPermissionsRepo: Repository<UserHasPermissionsEntity>,
  ) {}

  async getRoleIds(userId: string): Promise<string[]> {
    const rows = await this.userRolesRepo.find({ where: { userId } });
    return rows.map((r) => r.roleId);
  }

  async getDirectPermissionIds(userId: string): Promise<string[]> {
    const rows = await this.userPermissionsRepo.find({ where: { userId } });
    return rows.map((r) => r.permissionId);
  }

  async attachRoles(userId: string, roleIds: string[]): Promise<void> {
    const entities = roleIds.map((roleId) =>
      this.userRolesRepo.create({ userId, roleId }),
    );
    await this.userRolesRepo.save(entities);
  }

  async detachRoles(userId: string, roleIds: string[]): Promise<void> {
    for (const roleId of roleIds) {
      await this.userRolesRepo.delete({ userId, roleId });
    }
  }

  async detachAllRoles(userId: string): Promise<void> {
    await this.userRolesRepo.delete({ userId });
  }

  async attachPermissions(
    userId: string,
    permissionIds: string[],
  ): Promise<void> {
    const entities = permissionIds.map((permissionId) =>
      this.userPermissionsRepo.create({ userId, permissionId }),
    );
    await this.userPermissionsRepo.save(entities);
  }

  async detachPermissions(
    userId: string,
    permissionIds: string[],
  ): Promise<void> {
    for (const permissionId of permissionIds) {
      await this.userPermissionsRepo.delete({ userId, permissionId });
    }
  }

  async detachAllPermissions(userId: string): Promise<void> {
    await this.userPermissionsRepo.delete({ userId });
  }

  async userExists(userId: string): Promise<boolean> {
    // Implement based on your User entity
    return true;
  }
}
```

### 2. Register the module in AppModule

```typescript
import { Module } from "@nestjs/common";
import { TypeOrmModule } from "@nestjs/typeorm";
import { PermissionsModule } from "@nestbolt/permissions";
import { TypeOrmPermissionUserRepository } from "./repositories/permission-user.repository";

@Module({
  imports: [
    TypeOrmModule.forRoot({
      /* your config */
    }),
    PermissionsModule.forRoot({
      userRepository: TypeOrmPermissionUserRepository,
    }),
  ],
})
export class AppModule {}
```

### 3. Use guards and decorators on your routes

```typescript
import { Controller, Get, UseGuards } from "@nestjs/common";
import { RequireRoles, RolesGuard } from "@nestbolt/permissions";
import { JwtAuthGuard } from "@nestbolt/authentication";

@Controller("admin")
@UseGuards(JwtAuthGuard, RolesGuard)
export class AdminController {
  @RequireRoles("admin")
  @Get("dashboard")
  dashboard() {
    return { message: "Welcome, admin!" };
  }
}
```

## Module Configuration

### Static Configuration (forRoot)

```typescript
PermissionsModule.forRoot({
  userRepository: TypeOrmPermissionUserRepository,
  defaultGuardName: "default",
  enableWildcardPermissions: false,
  cache: { enabled: true, ttl: 86400000 },
  displayPermissionInException: false,
  displayRoleInException: false,
});
```

### Async Configuration (forRootAsync)

```typescript
PermissionsModule.forRootAsync({
  imports: [ConfigModule],
  inject: [ConfigService],
  useFactory: (config: ConfigService) => ({
    userRepository: TypeOrmPermissionUserRepository,
    enableWildcardPermissions: config.get("ENABLE_WILDCARD_PERMISSIONS", false),
  }),
});
```

> The module is registered as **global** — you only need to import it once.

## Using the Guards & Decorators

### Require specific permissions

```typescript
@UseGuards(JwtAuthGuard, PermissionsGuard)
@RequirePermissions('posts.create', 'posts.edit')
@Post('posts')
createPost() { /* user must have ALL listed permissions */ }
```

### Require specific roles

```typescript
@UseGuards(JwtAuthGuard, RolesGuard)
@RequireRoles('admin', 'editor')
@Get('manage')
manage() { /* user must have ANY of the listed roles */ }
```

### Require roles OR permissions

```typescript
@UseGuards(JwtAuthGuard, RolesOrPermissionsGuard)
@RequireRolesOrPermissions('admin', 'posts.create')
@Post('posts')
createPost() { /* user must have the role 'admin' OR the permission 'posts.create' */ }
```

> **Important:** Always put `JwtAuthGuard` (or your auth guard) **before** the permission/role guard so `request.user` is populated.

## Using the Service Directly

Inject `PermissionRegistrarService` for programmatic access:

```typescript
import {
  PermissionRegistrarService,
  PermissionService,
  RoleService,
} from "@nestbolt/permissions";

@Injectable()
export class SetupService {
  constructor(
    private readonly registrar: PermissionRegistrarService,
    private readonly permissionService: PermissionService,
    private readonly roleService: RoleService,
  ) {}

  async seedRolesAndPermissions() {
    // Create permissions
    await this.permissionService.create("posts.create");
    await this.permissionService.create("posts.edit");
    await this.permissionService.create("posts.delete");

    // Create roles and assign permissions
    await this.roleService.create("admin");
    await this.roleService.givePermissionTo(
      "admin",
      "posts.create",
      "posts.edit",
      "posts.delete",
    );

    await this.roleService.create("editor");
    await this.roleService.givePermissionTo(
      "editor",
      "posts.create",
      "posts.edit",
    );

    // Assign roles to users
    await this.registrar.assignRole("user-uuid", "admin");

    // Check permissions
    await this.registrar.userHasPermissionTo("user-uuid", "posts.create"); // true
    await this.registrar.userHasRole("user-uuid", "admin"); // true
  }
}
```

### PermissionRegistrarService Methods

| Method                                  | Returns                       | Description                                     |
| --------------------------------------- | ----------------------------- | ----------------------------------------------- |
| `assignRole(userId, ...roles)`          | `Promise<void>`               | Assign roles to a user                          |
| `removeRole(userId, ...roles)`          | `Promise<void>`               | Remove roles from a user                        |
| `syncRoles(userId, ...roles)`           | `Promise<void>`               | Replace all user roles with given ones          |
| `givePermissionTo(userId, ...perms)`    | `Promise<void>`               | Give direct permissions to a user               |
| `revokePermissionTo(userId, ...perms)`  | `Promise<void>`               | Revoke direct permissions from a user           |
| `syncPermissions(userId, ...perms)`     | `Promise<void>`               | Replace all direct user permissions             |
| `userHasRole(userId, role)`             | `Promise<boolean>`            | Check if user has a specific role               |
| `userHasAnyRole(userId, roles)`         | `Promise<boolean>`            | Check if user has any of the roles              |
| `userHasAllRoles(userId, roles)`        | `Promise<boolean>`            | Check if user has all roles                     |
| `userHasPermissionTo(userId, perm)`     | `Promise<boolean>`            | Check permission (direct + via role + wildcard) |
| `userHasAnyPermission(userId, perms)`   | `Promise<boolean>`            | Check if user has any permission                |
| `userHasAllPermissions(userId, perms)`  | `Promise<boolean>`            | Check if user has all permissions               |
| `userHasDirectPermission(userId, perm)` | `Promise<boolean>`            | Check only direct permission                    |
| `getUserRoleNames(userId)`              | `Promise<string[]>`           | Get all role names for a user                   |
| `getUserAllPermissions(userId)`         | `Promise<PermissionEntity[]>` | Get all permissions (direct + via roles)        |
| `getUserPermissionsViaRoles(userId)`    | `Promise<PermissionEntity[]>` | Get permissions inherited from roles            |
| `getUserDirectPermissions(userId)`      | `Promise<PermissionEntity[]>` | Get directly assigned permissions               |
| `flushCache()`                          | `void`                        | Clear the permission cache                      |

### PermissionService Methods

| Method                           | Returns                       | Description                        |
| -------------------------------- | ----------------------------- | ---------------------------------- |
| `create(name, guardName?)`       | `Promise<PermissionEntity>`   | Create a permission                |
| `findByName(name, guardName?)`   | `Promise<PermissionEntity>`   | Find by name (throws if not found) |
| `findById(id, guardName?)`       | `Promise<PermissionEntity>`   | Find by ID (throws if not found)   |
| `findOrCreate(name, guardName?)` | `Promise<PermissionEntity>`   | Find or create a permission        |
| `findAll(guardName?)`            | `Promise<PermissionEntity[]>` | List all permissions               |
| `findByNames(names, guardName?)` | `Promise<PermissionEntity[]>` | Find multiple by names             |
| `delete(nameOrId, guardName?)`   | `Promise<void>`               | Delete a permission                |

### RoleService Methods

| Method                               | Returns                 | Description                        |
| ------------------------------------ | ----------------------- | ---------------------------------- |
| `create(name, guardName?)`           | `Promise<RoleEntity>`   | Create a role                      |
| `findByName(name, guardName?)`       | `Promise<RoleEntity>`   | Find by name (throws if not found) |
| `findById(id, guardName?)`           | `Promise<RoleEntity>`   | Find by ID (throws if not found)   |
| `findOrCreate(name, guardName?)`     | `Promise<RoleEntity>`   | Find or create a role              |
| `findAll(guardName?)`                | `Promise<RoleEntity[]>` | List all roles                     |
| `delete(nameOrId, guardName?)`       | `Promise<void>`         | Delete a role                      |
| `givePermissionTo(role, ...perms)`   | `Promise<RoleEntity>`   | Attach permissions to a role       |
| `revokePermissionTo(role, ...perms)` | `Promise<RoleEntity>`   | Detach permissions from a role     |
| `syncPermissions(role, ...perms)`    | `Promise<RoleEntity>`   | Replace all role permissions       |
| `hasPermissionTo(role, perm)`        | `Promise<boolean>`      | Check if role has a permission     |

## Implementing PermissionUserRepository

The `PermissionUserRepository` interface is the bridge between this package and your User entity. You must implement it and pass it to the module configuration.

| Method                                     | Description                                 |
| ------------------------------------------ | ------------------------------------------- |
| `getDirectPermissionIds(userId)`           | Return IDs of directly assigned permissions |
| `getRoleIds(userId)`                       | Return IDs of assigned roles                |
| `attachRoles(userId, roleIds)`             | Insert user-role associations               |
| `detachRoles(userId, roleIds)`             | Remove user-role associations               |
| `detachAllRoles(userId)`                   | Remove all roles from user                  |
| `attachPermissions(userId, permissionIds)` | Insert user-permission associations         |
| `detachPermissions(userId, permissionIds)` | Remove user-permission associations         |
| `detachAllPermissions(userId)`             | Remove all direct permissions from user     |
| `userExists(userId)`                       | Check if user exists                        |

See [Quick Start](#quick-start) for a full TypeORM implementation example.

## Configuration Options

| Option                         | Type                             | Default        | Description                                    |
| ------------------------------ | -------------------------------- | -------------- | ---------------------------------------------- |
| `userRepository`               | `Type<PermissionUserRepository>` | **(required)** | Class implementing PermissionUserRepository    |
| `defaultGuardName`             | `string`                         | `"default"`    | Default guard name for roles/permissions       |
| `enableWildcardPermissions`    | `boolean`                        | `false`        | Enable wildcard permission matching            |
| `cache.enabled`                | `boolean`                        | `true`         | Enable in-memory permission cache              |
| `cache.ttl`                    | `number`                         | `86400000`     | Cache TTL in milliseconds (default: 24 hours)  |
| `displayPermissionInException` | `boolean`                        | `false`        | Include permission names in 403 error messages |
| `displayRoleInException`       | `boolean`                        | `false`        | Include role names in 403 error messages       |

## Features

### Wildcard Permissions

Enable wildcard matching to define broad permissions:

```typescript
PermissionsModule.forRoot({
  userRepository: TypeOrmPermissionUserRepository,
  enableWildcardPermissions: true,
});
```

```typescript
// Give a user the wildcard permission
await permissionService.create("articles.*");
await registrar.givePermissionTo(userId, "articles.*");

// Now these checks all return true:
await registrar.userHasPermissionTo(userId, "articles.create"); // true
await registrar.userHasPermissionTo(userId, "articles.edit"); // true
await registrar.userHasPermissionTo(userId, "articles.delete"); // true

// Super-admin wildcard
await permissionService.create("*");
await registrar.givePermissionTo(userId, "*");
await registrar.userHasPermissionTo(userId, "anything.at.all"); // true
```

### Events

If `@nestjs/event-emitter` is installed, the package emits events on role/permission changes:

| Event                             | Payload                     | When                            |
| --------------------------------- | --------------------------- | ------------------------------- |
| `permissions.role-attached`       | `{ userId, roleIds }`       | Role(s) assigned to user        |
| `permissions.role-detached`       | `{ userId, roleIds }`       | Role(s) removed from user       |
| `permissions.permission-attached` | `{ userId, permissionIds }` | Permission(s) given to user     |
| `permissions.permission-detached` | `{ userId, permissionIds }` | Permission(s) revoked from user |
| `permissions.role-created`        | `{ role }`                  | New role created                |
| `permissions.role-deleted`        | `{ role }`                  | Role deleted                    |
| `permissions.permission-created`  | `{ permission }`            | New permission created          |
| `permissions.permission-deleted`  | `{ permission }`            | Permission deleted              |
| `permissions.cache-flushed`       | `{}`                        | Permission cache cleared        |

### Caching

The in-memory cache stores permission data to avoid repeated database queries. It is automatically flushed when roles or permissions are modified. You can also flush it manually:

```typescript
registrar.flushCache();
```

## Testing

```bash
npm test
```

Run tests in watch mode:

```bash
npm run test:watch
```

Generate coverage report:

```bash
npm run test:cov
```

## Changelog

Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently.

## Contributing

Please see [CONTRIBUTING](CONTRIBUTING.md) for details.

## Security

If you discover any security-related issues, please report them via [GitHub Issues](https://github.com/nestbolt/permissions/issues) with the **security** label instead of using the public issue tracker.

## License

The MIT License (MIT). Please see [License File](LICENSE.md) for more information.