exguard-decorator
v1.1.44
Published
NestJS decorators for role and permission management using ExGuard /guard/me endpoint
Maintainers
Readme
ExGuard Decorator
NestJS decorators for role and permission management via /guard/me with Redis caching and WebSocket-based realtime cache invalidation.
How it works
Request → ExGuardGuard → extract JWT from Authorization header
↓
ExGuardDecoratorService.getGuardInfo(token)
↓
┌─ Cache hit? ──→ return cached ExGuardUser
│
└─ Cache miss ──→ fetch GET /guard/me → cache result → return ExGuardUser
↓
ExGuardGuard checks required permissions/roles/modules from decorators
↓
Grant or deny accessRealtime cache invalidation (via WebSocket):
RBAC server emits WS event → ExGuardRealtimeProvider receives
↓
ExGuardDecoratorService.refetchUserCache(cognitoSubId)
↓
┌─ Shared Redis has data? ──→ parse & update local cache
│
└─ Shared Redis miss ──→ fallback GET /guard/me → update local cacheThe shared Redis key is {prefix}user:{cognitoSubId} (default prefix: guard:) — the same key where empowerx-guard-api writes fresh data after RBAC changes.
Dependencies
@nestjs/common^10.0.0 (peer)@nestjs/core^10.0.0 (peer)redis^4.6.0socket.io-client^4.7.0
Installation
npm install exguard-decoratorQuick Setup
npx exguard-decorator setupCreates src/cache-invalidation/cache-invalidation.service.ts, updates .env and app.module.ts.
Manual Setup
1. Register the Module
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { ExGuardDecoratorModule } from 'exguard-decorator';
@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true }),
ExGuardDecoratorModule.forRootAsync({
imports: [ConfigModule],
useFactory: (config: ConfigService) => ({
baseUrl: config.get<string>('EXGUARD_API_URL'),
cache: {
enabled: config.get<boolean>('EXGUARD_CACHE_ENABLED', true),
ttl: config.get<number>('EXGUARD_CACHE_TTL', 300),
type: config.get<'memory' | 'redis'>('EXGUARD_CACHE_TYPE', 'memory'),
redis: {
host: config.get<string>('REDIS_HOST', 'localhost'),
port: config.get<number>('REDIS_PORT', 6379),
password: config.get<string>('REDIS_PASSWORD'),
db: config.get<number>('REDIS_DB', 0),
keyPrefix: config.get<string>('REDIS_KEY_PREFIX', 'guard:'),
},
},
sharedRedis: {
host: config.get<string>('REDIS_HOST', 'localhost'),
port: config.get<number>('REDIS_PORT', 6379),
password: config.get<string>('REDIS_PASSWORD'),
db: config.get<number>('REDIS_DB', 0),
keyPrefix: config.get<string>('REDIS_KEY_PREFIX', 'guard:'),
},
debug: config.get<boolean>('EXGUARD_DEBUG', false),
}),
inject: [ConfigService],
}),
],
providers: [CacheInvalidationService],
})
export class AppModule {}The module registers ExGuardGuard as a global guard — all routes require a valid token by default.
2. Protect Routes with Decorators
RequirePermissions — ALL listed permissions required (AND)
import { Controller, Get, Post, Delete, UseGuards } from '@nestjs/common';
import { RequirePermissions, RequireRoles, RequireModules, GuardWithPermissions, GuardWithRoles, GuardWithModules, GuardWith } from 'exguard-decorator';
@Controller('orders')
@UseGuards(ExGuardGuard)
export class OrdersController {
@Get()
@RequirePermissions('orders:view')
async findAll() { return { data: [] }; }
@Post()
@RequirePermissions('orders:create', 'orders:admin')
async create() { return { success: true }; }
@Delete(':id')
@RequirePermissions('orders:delete')
@RequireRoles('admin')
async remove() { return { deleted: true }; }
}Combined decorators (guard + check in one line)
@Controller('inventory')
export class InventoryController {
@Get()
@GuardWithPermissions('inventory:view')
async findAll() { return { data: [] }; }
@Post()
@GuardWithRoles('admin', 'manager')
async create() { return { success: true }; }
@Get('reports')
@GuardWithModules('reports')
async reports() { return { data: [] }; }
@Delete(':id')
@GuardWith({ permissions: ['inventory:delete'], roles: ['admin'] })
async remove() { return { deleted: true }; }
}Role checks
@Get('admin-dashboard')
@RequireRoles('admin')
async adminDashboard() { return { data: [] }; }Module access checks
@Get('analytics')
@RequireModules('analytics')
async analytics() { return { data: [] }; }3. Programmatic Checks with ExGuardDecoratorService
Inject the service for flexible runtime checks:
import { Controller, Get, Post, Delete, Req } from '@nestjs/common';
import { ExGuardGuard, ExGuardDecoratorService, CurrentUser, ExGuardUser } from 'exguard-decorator';
@Controller('orders')
@UseGuards(ExGuardGuard)
export class OrdersController {
constructor(private readonly exGuard: ExGuardDecoratorService) {}
@Get()
async findAll(@CurrentUser() user: ExGuardUser) {
// user is already attached by the guard
return { user };
}
@Get('special')
async special(@Req() req: any) {
const token = extractToken(req); // your token extraction
// Single permission
if (!(await this.exGuard.hasPermission(token, 'orders:special'))) {
throw new ForbiddenException();
}
// ANY permission (OR)
const anyOk = await this.exGuard.hasAnyPermission(token, ['orders:view', 'orders:read']);
if (!anyOk) throw new ForbiddenException();
// ALL permissions (AND)
const allOk = await this.exGuard.hasAllPermissions(token, ['orders:view', 'orders:export']);
if (!allOk) throw new ForbiddenException();
// Single role
if (!(await this.exGuard.hasRole(token, 'admin'))) throw new ForbiddenException();
// ANY role (OR)
if (!(await this.exGuard.hasAnyRole(token, ['admin', 'supervisor']))) throw new ForbiddenException();
// ALL roles (AND)
if (!(await this.exGuard.hasAllRoles(token, ['admin', 'finance']))) throw new ForbiddenException();
// Module access
if (!(await this.exGuard.hasModuleAccess(token, 'reports'))) throw new ForbiddenException();
return { ok: true };
}
}4. Realtime Cache Invalidation Service
import { Injectable, OnModuleInit, OnModuleDestroy, Logger } from '@nestjs/common';
import { ExGuardRealtimeProvider, ExGuardDecoratorService } from 'exguard-decorator';
@Injectable()
export class CacheInvalidationService implements OnModuleInit, OnModuleDestroy {
private readonly logger = new Logger(CacheInvalidationService.name);
private realtimeProvider = new ExGuardRealtimeProvider();
constructor(private readonly exGuardService: ExGuardDecoratorService) {}
async onModuleInit() {
const url = process.env.EXGUARD_REALTIME_URL || process.env.EXGUARD_API_URL || 'http://localhost:3000';
await this.realtimeProvider.initialize(
{
baseUrl: url,
cache: {
enabled: process.env.EXGUARD_CACHE_ENABLED !== 'false',
ttl: parseInt(process.env.EXGUARD_CACHE_TTL || '300', 10),
type: (process.env.EXGUARD_CACHE_TYPE as 'memory' | 'redis') || 'redis',
},
sharedRedis: {
host: process.env.REDIS_HOST || 'localhost',
port: parseInt(process.env.REDIS_PORT || '6379'),
password: process.env.REDIS_PASSWORD,
db: parseInt(process.env.REDIS_DB || '0'),
keyPrefix: process.env.REDIS_KEY_PREFIX || 'guard:',
},
debug: process.env.EXGUARD_DEBUG === 'true',
},
this.exGuardService,
);
this.logger.log(`[CACHE] Connected to ${url}/realtime`);
}
onModuleDestroy() {
this.realtimeProvider.disconnect();
}
}Exceptions
| Exception | HTTP Status | When |
|-----------|-------------|------|
| ExGuardTokenMissingException | 401 | No Bearer token in request |
| ExGuardTokenInvalidException | 401 | Token is invalid/expired |
| ExGuardPermissionDeniedException | 403 | Missing required permissions |
| ExGuardRoleDeniedException | 403 | Missing required roles |
| ExGuardModuleDeniedException | 403 | Missing required module access |
| ExGuardApiException | varies | /guard/me API error |
Configuration
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| baseUrl | string | — | ExGuard API URL |
| cache.enabled | boolean | true | Enable local caching |
| cache.ttl | number | 300 | Cache TTL (seconds) |
| cache.type | string | 'memory' | 'memory' or 'redis' |
| cache.redis.* | object | — | Local Redis connection |
| sharedRedis.* | object | — | Shared Redis connection (reads {prefix}user:{cognitoSubId}) |
| debug | boolean | false | Debug logging |
Cache Logic
- First request:
getGuardInfo()checks local cache → miss → callsGET /guard/me→ stores result in local cache (noaccessTokenstored) - Subsequent requests: local cache hit → returns immediately
- WS event received (
user:access-changed,user:permissions-changed, etc.):refetchUserCache(cognitoSubId, token?):- Reads from shared Redis at
{sharedRedis.keyPrefix}user:{cognitoSubId}(e.g.guard:user:abc123) - If found → converts
SharedCachedUserData→ExGuardUser→ updates local cache - If not found → fallback: calls
GET /guard/mewith provided token → updates local cache
- Reads from shared Redis at
- No cache invalidation on WS events — data is refetched, stale data is overwritten
API Reference
ExGuardDecoratorService
getGuardInfo(accessToken: string): Promise<ExGuardUser>
hasPermission(token: string, permission: string): Promise<boolean>
hasAnyPermission(token: string, permissions: string[]): Promise<boolean>
hasAllPermissions(token: string, permissions: string[]): Promise<boolean>
hasRole(token: string, role: string): Promise<boolean>
hasAnyRole(token: string, roles: string[]): Promise<boolean>
hasAllRoles(token: string, roles: string[]): Promise<boolean>
hasModuleAccess(token: string, module: string): Promise<boolean>
refetchUserCache(cognitoSubId: string, accessToken?: string): Promise<void>
clearCache(accessToken: string): Promise<void>
clearAllCache(): Promise<void>Decorators
| Decorator | Description |
|-----------|-------------|
| @RequirePermissions('perm1', 'perm2') | ALL required (AND) — set metadata only, needs @UseGuards(ExGuardGuard) |
| @RequireRoles('role1', 'role2') | ALL required (AND) — set metadata only |
| @RequireModules('module') | ALL required (AND) — set metadata only |
| @GuardWithPermissions(...) | Combines UseGuards(ExGuardGuard) + RequirePermissions |
| @GuardWithRoles(...) | Combines UseGuards(ExGuardGuard) + RequireRoles |
| @GuardWithModules(...) | Combines UseGuards(ExGuardGuard) + RequireModules |
| @GuardWith({permissions, roles, modules}) | Combines guard + all three metadata types |
| @CurrentUser() | Parameter decorator to inject ExGuardUser |
ExGuardUser
interface ExGuardUser {
id: string;
username: string;
email: string;
roles: string[];
permissions: string[];
modules?: string[];
fieldOffices?: string[];
cognitoSubId?: string;
emailVerified?: boolean;
givenName?: string;
familyName?: string;
employeeNumber?: string;
regionId?: string;
createdAt?: string;
updatedAt?: string;
lastLoginAt?: string;
fieldOffice?: string;
[key: string]: any;
}