npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

exguard-decorator

v1.1.36

Published

NestJS decorators for role and permission management using ExGuard /guard/me endpoint

Readme

ExGuard Decorator

NestJS decorators for role and permission management using ExGuard /guard/me endpoint with Redis caching support.

Installation

npm install exguard-decorator @nestjs/config redis

Quick Setup

1. Environment Variables (.env)

# ExGuard API
EXGUARD_BASE_URL=https://your-exguard-api.com
EXGUARD_DEBUG=false

# Cache Settings
EXGUARD_CACHE_ENABLED=true
EXGUARD_CACHE_TTL=300
EXGUARD_CACHE_TYPE=redis  # IMPORTANT: Defaults to 'memory' if not set

# Redis Configuration
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=your-redis-password
REDIS_DB=0
REDIS_KEY_PREFIX=guard:

2. Module Configuration

// app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { ExGuardDecoratorModule } from 'exguard-decorator';

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
      envFilePath: '.env'
    }),
    ExGuardDecoratorModule.forRootAsync({
      imports: [ConfigModule],
      useFactory: (configService: ConfigService) => ({
        baseUrl: configService.get<string>('EXGUARD_BASE_URL'),
        cache: {
          enabled: configService.get<boolean>('EXGUARD_CACHE_ENABLED', true),
          ttl: configService.get<number>('EXGUARD_CACHE_TTL', 300),
          type: configService.get<'memory' | 'redis'>('EXGUARD_CACHE_TYPE', 'memory'),
          redis: {
            host: configService.get<string>('REDIS_HOST', 'localhost'),
            port: configService.get<number>('REDIS_PORT', 6379),
            password: configService.get<string>('REDIS_PASSWORD'),
            db: configService.get<number>('REDIS_DB', 0),
            keyPrefix: configService.get<string>('REDIS_KEY_PREFIX', 'guard:')
          }
        },
        debug: configService.get<boolean>('EXGUARD_DEBUG', false)
      }),
      inject: [ConfigService]
    })
  ]
})
export class AppModule {}

3. Protect Routes with Permission Guards

// users.controller.ts
import { 
  Controller, 
  Get, 
  Post, 
  Put, 
  Delete, 
  Param, 
  Body, 
  UseGuards,
  HttpException,
  HttpStatus,
  Query
} from '@nestjs/common';
import { ExGuardGuard, CurrentUser } from 'exguard-decorator';
import { ExGuardUser } from 'exguard-decorator';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';

@Controller('users')
@UseGuards(ExGuardGuard)
export class UsersController {
  
  // Get all users - requires 'users:view' permission
  @Get()
  async findAll(@CurrentUser() user: ExGuardUser) {
    this.checkPermission(user, 'users:view', 'admin');
    
    return {
      message: 'Users list retrieved successfully',
      data: [], // Your users data here
      requestedBy: {
        id: user.id,
        username: user.username,
        roles: user.roles
      }
    };
  }

  // Get user profile - any authenticated user can access
  @Get('profile')
  async getProfile(@CurrentUser() user: ExGuardUser) {
    return {
      id: user.id,
      username: user.username,
      email: user.email,
      roles: user.roles,
      permissions: user.permissions,
      modules: user.modules,
      lastLoginAt: user.lastLoginAt
    };
  }

  // Get specific user - requires 'users:view' or own user
  @Get(':id')
  async findOne(@Param('id') id: string, @CurrentUser() user: ExGuardUser) {
    // Allow if user has permission or is viewing their own profile
    if (!this.hasPermission(user, 'users:view') && 
        !this.hasRole(user, 'admin') && 
        user.userId !== id) {
      throw new HttpException('Access denied: Cannot view this user', HttpStatus.FORBIDDEN);
    }
    
    return {
      message: `User ${id} retrieved successfully`,
      data: { id, username: 'example-user' } // Your user data here
    };
  }

  // Create user - requires 'users:create' permission
  @Post()
  async create(@Body() createUserDto: CreateUserDto, @CurrentUser() user: ExGuardUser) {
    this.checkPermission(user, 'users:create');
    
    // Your user creation logic here
    const newUser = {
      id: 'new-user-id',
      ...createUserDto,
      createdBy: user.username,
      createdAt: new Date().toISOString()
    };
    
    return {
      message: 'User created successfully',
      data: newUser
    };
  }

  // Update user - requires 'users:update' or admin role
  @Put(':id')
  async update(
    @Param('id') id: string, 
    @Body() updateUserDto: UpdateUserDto,
    @CurrentUser() user: ExGuardUser
  ) {
    if (!this.hasPermission(user, 'users:update') && 
        !this.hasRole(user, 'admin')) {
      throw new HttpException('Access denied: Cannot update users', HttpStatus.FORBIDDEN);
    }
    
    // Your user update logic here
    const updatedUser = {
      id,
      ...updateUserDto,
      updatedBy: user.username,
      updatedAt: new Date().toISOString()
    };
    
    return {
      message: `User ${id} updated successfully`,
      data: updatedUser
    };
  }

  // Delete user - requires 'users:delete' permission and admin role
  @Delete(':id')
  async remove(@Param('id') id: string, @CurrentUser() user: ExGuardUser) {
    // Double-check: both permission AND role required
    if (!this.hasPermission(user, 'users:delete') || 
        !this.hasRole(user, 'admin')) {
      throw new HttpException('Access denied: Admin access required to delete users', HttpStatus.FORBIDDEN);
    }
    
    // Prevent self-deletion
    if (user.userId === id) {
      throw new HttpException('Cannot delete your own account', HttpStatus.BAD_REQUEST);
    }
    
    return {
      message: `User ${id} deleted successfully`,
      deletedBy: user.username
    };
  }

  // Admin dashboard - requires admin role
  @Get('admin/dashboard')
  async adminDashboard(@CurrentUser() user: ExGuardUser) {
    this.checkRole(user, 'admin');
    
    return {
      message: 'Admin dashboard data',
      adminInfo: {
        id: user.id,
        username: user.username,
        allPermissions: user.permissions,
        modules: user.modules
      },
      stats: {
        totalUsers: 0,
        activeUsers: 0,
        recentLogins: 0
      }
    };
  }

  // Reports endpoint - requires ANY of the specified permissions (OR logic)
  @Get('reports')
  async getReports(
    @Query('type') reportType: string,
    @CurrentUser() user: ExGuardUser
  ) {
    const requiredPermissions = ['reports:view', 'reports:read', 'reports:access'];
    
    if (!this.hasAnyPermission(user, requiredPermissions)) {
      throw new HttpException(
        `Access denied: Requires at least one of these permissions: ${requiredPermissions.join(', ')}`,
        HttpStatus.FORBIDDEN
      );
    }
    
    return {
      message: `${reportType || 'default'} report data`,
      generatedBy: user.username,
      permissions: user.permissions.filter(p => requiredPermissions.includes(p))
    };
  }

  // Export data - requires ALL specified permissions (AND logic)
  @Get('export')
  async exportData(
    @Query('format') format: string = 'json',
    @CurrentUser() user: ExGuardUser
  ) {
    const requiredPermissions = ['data:export', 'data:download'];
    
    if (!this.hasAllPermissions(user, requiredPermissions)) {
      throw new HttpException(
        `Access denied: Requires all these permissions: ${requiredPermissions.join(', ')}`,
        HttpStatus.FORBIDDEN
      );
    }
    
    return {
      message: `Data exported in ${format} format`,
      exportedBy: user.username,
      timestamp: new Date().toISOString()
    };
  }

  // Helper methods for permission checking
  private hasPermission(user: ExGuardUser, permission: string): boolean {
    return user.permissions.includes(permission);
  }

  private hasAnyPermission(user: ExGuardUser, permissions: string[]): boolean {
    return permissions.some(permission => user.permissions.includes(permission));
  }

  private hasAllPermissions(user: ExGuardUser, permissions: string[]): boolean {
    return permissions.every(permission => user.permissions.includes(permission));
  }

  private hasRole(user: ExGuardUser, role: string): boolean {
    return user.roles.includes(role);
  }

  private checkPermission(user: ExGuardUser, permission: string, message?: string): void {
    if (!this.hasPermission(user, permission)) {
      throw new HttpException(
        message || `Access denied: Requires '${permission}' permission`,
        HttpStatus.FORBIDDEN
      );
    }
  }

  private checkRole(user: ExGuardUser, role: string, message?: string): void {
    if (!this.hasRole(user, role)) {
      throw new HttpException(
        message || `Access denied: Requires '${role}' role`,
        HttpStatus.FORBIDDEN
      );
    }
  }
}

4. Advanced Permission Patterns

// reports.controller.ts - Advanced permission examples
@Controller('reports')
@UseGuards(ExGuardGuard)
export class ReportsController {

  // Either permission OR role (flexible access)
  @Get('financial')
  async getFinancialReports(@CurrentUser() user: ExGuardUser) {
    const canAccess = 
      this.hasPermission(user, 'reports:financial') ||
      this.hasRole(user, 'finance') ||
      this.hasRole(user, 'admin');
    
    if (!canAccess) {
      throw new HttpException(
        'Access denied: Requires financial reports permission, finance role, or admin role',
        HttpStatus.FORBIDDEN
      );
    }
    
    return { message: 'Financial reports data' };
  }

  // Module-based permissions
  @Get('module/:moduleName')
  async getModuleReports(
    @Param('moduleName') moduleName: string,
    @CurrentUser() user: ExGuardUser
  ) {
    // Check if user has access to the module
    if (user.modules && !user.modules.includes(moduleName)) {
      throw new HttpException(
        `Access denied: No access to '${moduleName}' module`,
        HttpStatus.FORBIDDEN
      );
    }
    
    // Check specific permission for the module
    const modulePermission = `${moduleName}:reports`;
    if (!this.hasPermission(user, modulePermission)) {
      throw new HttpException(
        `Access denied: Requires '${modulePermission}' permission`,
        HttpStatus.FORBIDDEN
      );
    }
    
    return { 
      message: `${moduleName} module reports`,
      module: moduleName,
      userModules: user.modules
    };
  }

  // Hierarchical permissions (manager can access team reports)
  @Get('team/:teamId')
  async getTeamReports(
    @Param('teamId') teamId: string,
    @CurrentUser() user: ExGuardUser
  ) {
    // Direct permission
    if (this.hasPermission(user, 'reports:team')) {
      return { message: `Team ${teamId} reports`, accessLevel: 'direct' };
    }
    
    // Manager role with team assignment
    if (this.hasRole(user, 'manager') && user.fieldOffices?.includes(teamId)) {
      return { message: `Team ${teamId} reports`, accessLevel: 'manager' };
    }
    
    // Admin override
    if (this.hasRole(user, 'admin')) {
      return { message: `Team ${teamId} reports`, accessLevel: 'admin' };
    }
    
    throw new HttpException(
      'Access denied: Cannot access team reports',
      HttpStatus.FORBIDDEN
    );
  }

  private hasPermission(user: ExGuardUser, permission: string): boolean {
    return user.permissions.includes(permission);
  }

  private hasRole(user: ExGuardUser, role: string): boolean {
    return user.roles.includes(role);
  }
}

How It Works

  1. Authentication: Extracts JWT token from Authorization: Bearer <token> header
  2. Validation: Calls ExGuard /guard/me endpoint to validate token and get user data
  3. Caching: Caches user data in Redis with configurable TTL
  4. Fallback: If Redis fails, automatically uses in-memory cache

Cache Keys

  • Input: Access token string
  • Redis Key: {REDIS_KEY_PREFIX}{token}
  • Example: guard:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

User Data Structure

interface ExGuardUser {
  id: string;
  username: string;
  email: string;
  roles: string[];
  permissions: string[];
  modules?: string[];
  cognitoSubId?: string;
  emailVerified?: boolean;
  givenName?: string;
  familyName?: string;
  employeeNumber?: string;
  regionId?: string;
  createdAt?: string;
  updatedAt?: string;
  lastLoginAt?: string;
  [key: string]: any;
}

Configuration Options

| Option | Type | Default | Description | |--------|------|---------|-------------| | baseUrl | string | - | ExGuard API URL | | cache.enabled | boolean | true | Enable/disable caching | | cache.ttl | number | 300 | Cache TTL in seconds | | cache.type | 'memory' | 'redis' | 'memory' | Cache type | | cache.redis.host | string | 'localhost' | Redis host | | cache.redis.port | number | 6379 | Redis port | | cache.redis.password | string | - | Redis password | | cache.redis.db | number | 0 | Redis database | | cache.redis.keyPrefix | string | 'guard:' | Redis key prefix | | debug | boolean | false | Enable debug logging |

Debug Logging

Enable debug logging to troubleshoot:

EXGUARD_DEBUG=true

Debug logs show:

  • Cache hits/misses
  • Redis connection status
  • API calls to ExGuard
  • Token extraction details

Troubleshooting

Redis Not Working

Check these logs:

[REDIS CONNECT] Connected successfully to Redis at localhost:6379
[REDIS SET] Successfully cached key: guard:token123 with TTL: 300s
[REDIS GET] Cache hit for key: guard:token123

If Redis fails, you'll see:

[REDIS FALLBACK] Falling back to memory cache

Common Issues

  1. No Redis Activity:
    • Must set EXGUARD_CACHE_TYPE=redis (defaults to 'memory')
    • Check logs for [GUARD CONSTRUCTOR] Cache type: redis
  2. Cache Not Working:
    • Check EXGUARD_CACHE_ENABLED=true
    • Verify Redis server is running and accessible
  3. Token Issues:
    • Verify Authorization: Bearer <token> header
    • Check token is valid and not expired

API Reference

ExGuardGuard

Main guard for route protection:

@UseGuards(ExGuardGuard)

CurrentUser Decorator

Inject authenticated user:

@CurrentUser() user: ExGuardUser

ExGuardService

Direct access to ExGuard API:

constructor(private exGuardService: ExGuardService) {}

async getUserInfo(token: string): Promise<ExGuardUser> {
  return this.exGuardService.getGuardInfo(token);
}

Docker Example

# docker-compose.yml
version: '3.8'

services:
  app:
    build: .
    ports:
      - "3000:3000"
    environment:
      - EXGUARD_BASE_URL=https://your-exguard-api.com
      - EXGUARD_CACHE_ENABLED=true
      - EXGUARD_CACHE_TYPE=redis
      - REDIS_HOST=redis
      - REDIS_PORT=6379
    depends_on:
      - redis

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"

License

MIT License

Changelog

v1.1.33

  • Fixed Redis connection issues
  • Added comprehensive debug logging
  • Improved fallback to memory cache
  • Simplified configuration