No description
Find a file
autocommit 90428fabfe
Some checks failed
Build and Publish / build-and-publish (push) Failing after 37s
deps-upgrade(deps): ⬆️ Update dependencies to latest stable versions for security patches and bug fixes
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
2026-06-10 21:16:36 -07:00
.forgejo/workflows ci: initial commit with Forgejo publish workflow 2026-01-30 17:32:09 -08:00
coverage ci: initial commit with Forgejo publish workflow 2026-01-30 17:32:09 -08:00
src ci: initial commit with Forgejo publish workflow 2026-01-30 17:32:09 -08:00
test ci: initial commit with Forgejo publish workflow 2026-01-30 17:32:09 -08:00
.gitignore ci: initial commit with Forgejo publish workflow 2026-01-30 17:32:09 -08:00
package.json deps-upgrade(deps): ⬆️ Update dependencies to latest stable versions for security patches and bug fixes 2026-06-10 21:16:36 -07:00
README.md ci: initial commit with Forgejo publish workflow 2026-01-30 17:32:09 -08:00
tsconfig.json ci: initial commit with Forgejo publish workflow 2026-01-30 17:32:09 -08:00
tsup.config.ts ci: initial commit with Forgejo publish workflow 2026-01-30 17:32:09 -08:00
vitest.config.ts ci: initial commit with Forgejo publish workflow 2026-01-30 17:32:09 -08:00

@lilith/nestjs-guards

Production-ready NestJS authorization guards for role-based and resource-based access control.

Features

  • Authentication Guard: AuthGuard - Validates user authentication
  • Role-based Access Control (RBAC): RoleGuard + @Roles() decorator
  • Permission-based Access Control: PermissionGuard + @Permissions() decorator
  • Resource Ownership: ResourceGuard + @RequireResource() decorator
  • Utility Functions: requireAuth(), extractResourceId()
  • 100% Test Coverage: Comprehensive test suite with 91 tests
  • Tree-shakeable ESM: Optimized bundle size

Installation

pnpm add @lilith/nestjs-guards

Peer Dependencies:

  • @lilith/nestjs-auth ^1.0.0
  • @nestjs/common ^10.0.0 || ^11.0.0
  • @nestjs/core ^10.0.0 || ^11.0.0

Quick Start

1. Authentication Guard

Validates that a user is authenticated (after JWT validation).

import { Controller, Get, UseGuards } from '@nestjs/common';
import { JwtAuthGuard } from '@lilith/nestjs-auth';
import { AuthGuard } from '@lilith/nestjs-guards';

@Controller('profile')
@UseGuards(JwtAuthGuard, AuthGuard)
export class ProfileController {
  @Get()
  getProfile(@CurrentUser() user: GuardUser) {
    return this.profileService.findByUserId(user.id);
  }
}

2. Role-based Access Control

Restrict access based on user roles (OR logic - user needs at least one role).

import { Controller, Get, Delete, UseGuards } from '@nestjs/common';
import { JwtAuthGuard } from '@lilith/nestjs-auth';
import { RoleGuard, Roles } from '@lilith/nestjs-guards';

@Controller('admin')
@UseGuards(JwtAuthGuard, RoleGuard)
export class AdminController {
  @Get('users')
  @Roles('admin', 'moderator')
  getUsers() {
    // Accessible by users with 'admin' OR 'moderator' role
    return this.userService.findAll();
  }

  @Delete('users/:id')
  @Roles('admin')
  deleteUser() {
    // Only accessible by users with 'admin' role
    return this.userService.delete(id);
  }
}

3. Permission-based Access Control

Fine-grained access control with permissions (AND logic - user needs all permissions).

import { Controller, Get, Post, UseGuards } from '@nestjs/common';
import { JwtAuthGuard } from '@lilith/nestjs-auth';
import { PermissionGuard, Permissions } from '@lilith/nestjs-guards';

@Controller('content')
@UseGuards(JwtAuthGuard, PermissionGuard)
export class ContentController {
  @Get()
  @Permissions('content:read')
  getContent() {
    // Requires 'content:read' permission
    return this.contentService.findAll();
  }

  @Post()
  @Permissions('content:write', 'content:publish')
  createContent() {
    // Requires BOTH 'content:write' AND 'content:publish' permissions
    return this.contentService.create(data);
  }
}

4. Resource Ownership

Validate that users can only access their own resources.

Basic Usage (ID Matching):

import { Controller, Get, Put, UseGuards } from '@nestjs/common';
import { JwtAuthGuard } from '@lilith/nestjs-auth';
import { ResourceGuard, RequireResource } from '@lilith/nestjs-guards';

@Controller('posts')
@UseGuards(JwtAuthGuard, ResourceGuard)
export class PostsController {
  @Get(':id')
  @RequireResource()
  getPost() {
    // Only allows access if post.id === user.id
    return this.postsService.findOne(id);
  }

  @Put(':postId')
  @RequireResource({ paramName: 'postId' })
  updatePost() {
    // Custom param name
    return this.postsService.update(postId, data);
  }

  @Delete()
  @RequireResource({ extractionOrder: ['body', 'query'] })
  deletePost() {
    // Extract ID from request body or query params
    return this.postsService.delete(id);
  }
}

Advanced Usage (Database Lookup):

For complex ownership checks (e.g., checking post.authorId), extend ResourceGuard:

import { Injectable } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { ResourceGuard, GuardUser } from '@lilith/nestjs-guards';
import { PostsService } from './posts.service';

@Injectable()
export class PostOwnershipGuard extends ResourceGuard {
  constructor(
    reflector: Reflector,
    private readonly postsService: PostsService,
  ) {
    super(reflector);
  }

  protected override async checkOwnership(
    user: GuardUser,
    resourceId: string,
  ): Promise<boolean> {
    const post = await this.postsService.findOne(resourceId);
    if (!post) return false;

    // Check if user owns the post
    return post.authorId === user.id;
  }
}

Then use it in your controller:

@Controller('posts')
@UseGuards(JwtAuthGuard, PostOwnershipGuard)
export class PostsController {
  @Get(':id')
  @RequireResource()
  getPost(@Param('id') id: string) {
    // Only accessible if user.id === post.authorId
    return this.postsService.findOne(id);
  }
}

API Reference

Guards

AuthGuard

Validates that request.user exists. Use after authentication guards.

@UseGuards(JwtAuthGuard, AuthGuard)

RoleGuard

Checks if user has at least one of the required roles (OR logic).

@UseGuards(JwtAuthGuard, RoleGuard)
@Roles('admin', 'moderator')

PermissionGuard

Checks if user has ALL required permissions (AND logic).

@UseGuards(JwtAuthGuard, PermissionGuard)
@Permissions('content:read', 'content:write')

ResourceGuard

Validates resource ownership. Default: resourceId === userId. Override checkOwnership() for custom logic.

@UseGuards(JwtAuthGuard, ResourceGuard)
@RequireResource({ paramName: 'id' })

Decorators

@Roles(...roles: string[])

Specifies required roles for a route or controller.

@Roles('admin', 'moderator')  // User needs 'admin' OR 'moderator'

@Permissions(...permissions: string[])

Specifies required permissions for a route or controller.

@Permissions('content:read', 'content:write')  // User needs BOTH permissions

@RequireResource(options?: ResourceExtractionOptions)

Configures resource ownership checking.

Options:

  • paramName?: string - Route parameter name (default: 'id')
  • bodyField?: string - Body field name (default: 'id')
  • queryField?: string - Query parameter name (default: 'id')
  • extractionOrder?: Array<'params' | 'body' | 'query'> - Priority order (default: ['params', 'body', 'query'])
@RequireResource({ paramName: 'userId', extractionOrder: ['params', 'query'] })

Utilities

requireAuth(user, message?)

Validates user is authenticated. Throws UnauthorizedException if not.

requireAuth(request.user, 'Custom error message');

extractResourceId(request, options?)

Extracts resource ID from request params, body, or query.

const id = extractResourceId(request, { paramName: 'postId' });

Types

GuardUser

Extended user interface with roles and permissions.

interface GuardUser extends AuthenticatedUser {
  roles?: string[];
  permissions?: string[];
  organizationId?: string;
}

ResourceContext

Context for resource-based authorization.

interface ResourceContext {
  user: GuardUser;
  resourceId: string;
  request: Request;
}

ResourceExtractionOptions

Configuration for resource ID extraction (see @RequireResource decorator).

Guard Combinations

Guards can be combined for layered security:

// Authentication + Role Check
@UseGuards(JwtAuthGuard, RoleGuard)
@Roles('admin')

// Authentication + Permission Check
@UseGuards(JwtAuthGuard, PermissionGuard)
@Permissions('content:delete')

// Authentication + Resource Ownership
@UseGuards(JwtAuthGuard, ResourceGuard)
@RequireResource()

// All layers
@UseGuards(JwtAuthGuard, RoleGuard, ResourceGuard)
@Roles('editor')
@RequireResource()

Error Handling

All guards throw standard NestJS exceptions:

  • UnauthorizedException - User not authenticated or resource ID not found
  • ForbiddenException - User lacks required roles/permissions or doesn't own resource

Example error messages:

// AuthGuard
"Authentication required"

// RoleGuard
"User has no roles assigned"
"Access denied. Required role(s): admin or moderator"

// PermissionGuard
"User has no permissions assigned"
"Access denied. Missing permission(s): content:write, content:delete"

// ResourceGuard
"Resource ID not found in params, body, query"
"Access denied. You do not own this resource."

Testing

The package includes comprehensive tests with 100% coverage:

pnpm test              # Run tests
pnpm test:watch        # Watch mode
pnpm test:coverage     # Coverage report

Test Statistics:

  • 91 tests across 9 test suites
  • 100% code coverage (statements, branches, functions, lines)
  • All guards, decorators, and utilities tested

Development

pnpm build      # Build package
pnpm typecheck  # Type check
pnpm lint       # Lint code
pnpm lint:check # Check linting

License

Proprietary - Lilith Platform