@nestbolt/permissions
Quick Start
Set up the permissions module, implement the user repository, seed roles and permissions, and protect your first route.
This guide walks you through every step needed to get role-based access control working in a NestJS application: implementing the user repository, registering the module, seeding data, and protecting routes.
Step 1: Implement the PermissionUserRepository
The PermissionUserRepository interface is the bridge between the permissions package and your application's user model. You must provide a class that implements this interface so the package knows how to look up and modify user-role and user-permission associations.
Create the file src/repositories/permission-user.repository.ts:
import { Injectable } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from "typeorm";
import {
PermissionUserRepository,
UserHasRolesEntity,
UserHasPermissionsEntity,
} from "@nestbolt/permissions";
import { User } from "../entities/user.entity";
@Injectable()
export class TypeOrmPermissionUserRepository
implements PermissionUserRepository
{
constructor(
@InjectRepository(UserHasRolesEntity)
private readonly userRolesRepo: Repository<UserHasRolesEntity>,
@InjectRepository(UserHasPermissionsEntity)
private readonly userPermissionsRepo: Repository<UserHasPermissionsEntity>,
@InjectRepository(User)
private readonly userRepo: Repository<User>,
) {}
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> {
const count = await this.userRepo.count({ where: { id: userId } });
return count > 0;
}
}Step 2: Register the Module
Import PermissionsModule in your root AppModule. The module registers as global, so you only need to import it once.
import { Module } from "@nestjs/common";
import { TypeOrmModule } from "@nestjs/typeorm";
import { PermissionsModule } from "@nestbolt/permissions";
import { TypeOrmPermissionUserRepository } from "./repositories/permission-user.repository";
import { User } from "./entities/user.entity";
@Module({
imports: [
TypeOrmModule.forRoot({
type: "postgres",
host: "localhost",
port: 5432,
username: "app",
password: "secret",
database: "mydb",
autoLoadEntities: true,
synchronize: true,
}),
TypeOrmModule.forFeature([User]),
PermissionsModule.forRoot({
userRepository: TypeOrmPermissionUserRepository,
}),
],
})
export class AppModule {}Step 3: Seed Roles and Permissions
Create a service to seed your initial roles and permissions. This is typically run once during application bootstrap or as a CLI command.
import { Injectable, OnModuleInit, Logger } from "@nestjs/common";
import {
PermissionService,
RoleService,
} from "@nestbolt/permissions";
@Injectable()
export class SeederService implements OnModuleInit {
private readonly logger = new Logger(SeederService.name);
constructor(
private readonly permissionService: PermissionService,
private readonly roleService: RoleService,
) {}
async onModuleInit() {
await this.seed();
}
private async seed() {
// Create permissions
const createPost = await this.permissionService.findOrCreate("posts.create");
const editPost = await this.permissionService.findOrCreate("posts.edit");
const deletePost = await this.permissionService.findOrCreate("posts.delete");
const viewAnalytics = await this.permissionService.findOrCreate("analytics.view");
this.logger.log("Permissions seeded.");
// Create roles
const admin = await this.roleService.findOrCreate("admin");
const editor = await this.roleService.findOrCreate("editor");
const viewer = await this.roleService.findOrCreate("viewer");
// Assign permissions to roles
await this.roleService.givePermissionTo("admin", "posts.create", "posts.edit", "posts.delete", "analytics.view");
await this.roleService.givePermissionTo("editor", "posts.create", "posts.edit");
this.logger.log("Roles seeded and permissions assigned.");
}
}Register the SeederService in a module so it runs on startup:
import { Module } from "@nestjs/common";
import { SeederService } from "./seeder.service";
@Module({
providers: [SeederService],
})
export class SeederModule {}Step 4: Assign Roles to Users
Use PermissionRegistrarService to assign roles to users after they register or when an administrator changes their access level:
import { Injectable } from "@nestjs/common";
import { PermissionRegistrarService } from "@nestbolt/permissions";
@Injectable()
export class UserOnboardingService {
constructor(
private readonly registrar: PermissionRegistrarService,
) {}
async onUserRegistered(userId: string) {
// Every new user gets the "viewer" role
await this.registrar.assignRole(userId, "viewer");
}
async promoteToEditor(userId: string) {
await this.registrar.assignRole(userId, "editor");
}
async promoteToAdmin(userId: string) {
// syncRoles replaces all existing roles with the provided ones
await this.registrar.syncRoles(userId, "admin");
}
}Step 5: Protect Routes with Guards and Decorators
Apply the guards and decorators to your controller routes. Always place your authentication guard (e.g., JwtAuthGuard) before the permission/role guard so that request.user is populated.
import { Controller, Get, Post, Body, UseGuards } from "@nestjs/common";
import {
RequireRoles,
RequirePermissions,
RequireRolesOrPermissions,
RolesGuard,
PermissionsGuard,
RolesOrPermissionsGuard,
} from "@nestbolt/permissions";
import { JwtAuthGuard } from "./auth/jwt-auth.guard";
@Controller("posts")
@UseGuards(JwtAuthGuard)
export class PostsController {
// Only users with the "admin" or "editor" role can access this route
@UseGuards(RolesGuard)
@RequireRoles("admin", "editor")
@Post()
createPost(@Body() dto: CreatePostDto) {
return this.postsService.create(dto);
}
// Only users with BOTH "posts.edit" AND "posts.delete" permissions
@UseGuards(PermissionsGuard)
@RequirePermissions("posts.edit", "posts.delete")
@Post("bulk-update")
bulkUpdate(@Body() dto: BulkUpdateDto) {
return this.postsService.bulkUpdate(dto);
}
// Users who have the "admin" role OR the "analytics.view" permission
@UseGuards(RolesOrPermissionsGuard)
@RequireRolesOrPermissions("admin", "analytics.view")
@Get("analytics")
viewAnalytics() {
return this.analyticsService.getPostStats();
}
}Step 6: Check Permissions Programmatically
You can also check permissions in your service logic without relying on guards:
import { Injectable, ForbiddenException } from "@nestjs/common";
import { PermissionRegistrarService } from "@nestbolt/permissions";
@Injectable()
export class PostsService {
constructor(
private readonly registrar: PermissionRegistrarService,
) {}
async deletePost(userId: string, postId: string) {
const canDelete = await this.registrar.userHasPermissionTo(
userId,
"posts.delete",
);
if (!canDelete) {
throw new ForbiddenException("You do not have permission to delete posts.");
}
// proceed with deletion
}
async getUserCapabilities(userId: string) {
const roles = await this.registrar.getUserRoleNames(userId);
const permissions = await this.registrar.getUserAllPermissions(userId);
return {
roles,
permissions: permissions.map((p) => p.name),
};
}
}Complete Working Example
Here is a condensed, self-contained example showing all the pieces together:
// app.module.ts
import { Module } from "@nestjs/common";
import { TypeOrmModule } from "@nestjs/typeorm";
import { PermissionsModule } from "@nestbolt/permissions";
import { TypeOrmPermissionUserRepository } from "./repositories/permission-user.repository";
import { User } from "./entities/user.entity";
import { AdminController } from "./admin.controller";
import { SeederService } from "./seeder.service";
@Module({
imports: [
TypeOrmModule.forRoot({
type: "postgres",
host: "localhost",
port: 5432,
username: "app",
password: "secret",
database: "mydb",
autoLoadEntities: true,
synchronize: true,
}),
TypeOrmModule.forFeature([User]),
PermissionsModule.forRoot({
userRepository: TypeOrmPermissionUserRepository,
}),
],
controllers: [AdminController],
providers: [SeederService],
})
export class AppModule {}// admin.controller.ts
import { Controller, Get, UseGuards } from "@nestjs/common";
import { RequireRoles, RolesGuard } from "@nestbolt/permissions";
import { JwtAuthGuard } from "./auth/jwt-auth.guard";
@Controller("admin")
@UseGuards(JwtAuthGuard, RolesGuard)
export class AdminController {
@RequireRoles("admin")
@Get("dashboard")
dashboard() {
return { message: "Welcome, admin!" };
}
}Next Steps
- Configuration -- Explore all module options including cache settings, wildcard permissions, and exception display.
- Decorators and Guards -- Detailed reference for each decorator and guard.
- Role Management -- Full API reference for managing roles.