exguard-decorator
v1.1.36
Published
NestJS decorators for role and permission management using ExGuard /guard/me endpoint
Maintainers
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 redisQuick 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
- Authentication: Extracts JWT token from
Authorization: Bearer <token>header - Validation: Calls ExGuard
/guard/meendpoint to validate token and get user data - Caching: Caches user data in Redis with configurable TTL
- 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=trueDebug 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:token123If Redis fails, you'll see:
[REDIS FALLBACK] Falling back to memory cacheCommon Issues
- No Redis Activity:
- Must set
EXGUARD_CACHE_TYPE=redis(defaults to 'memory') - Check logs for
[GUARD CONSTRUCTOR] Cache type: redis
- Must set
- Cache Not Working:
- Check
EXGUARD_CACHE_ENABLED=true - Verify Redis server is running and accessible
- Check
- Token Issues:
- Verify
Authorization: Bearer <token>header - Check token is valid and not expired
- Verify
API Reference
ExGuardGuard
Main guard for route protection:
@UseGuards(ExGuardGuard)CurrentUser Decorator
Inject authenticated user:
@CurrentUser() user: ExGuardUserExGuardService
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
