exguard-cached
v1.3.4
Published
NestJS backend package for caching user RBAC data (roles, groups, permissions) in Redis with guards, decorators, and CLI auto-setup
Downloads
92
Maintainers
Readme
exguard-cached
NestJS backend package that caches user RBAC data (roles, groups, permissions) in Redis and provides guards/decorators for permission checks.
1. Install
pnpm add exguard-cached2. Auto-setup
npx exguard-cachedThis generates 3 files and updates your existing files:
Generated files
| File | Purpose |
|------|---------|
| src/redis/redis.module.ts | Creates a Redis client connection using REDIS_HOST, REDIS_PORT from .env. Registers 'REDIS_CLIENT' as a global provider. |
| src/guard/guard.module.ts | Imports ExGuardCacheModule.forRoot(...) with your Redis config and optional Guard API URL. Registers the controller. |
| src/guard/guard.controller.ts | Exposes Redis proxy endpoints (/guard/redis/set, /guard/redis/get, /guard/redis/del, etc.) so your frontend can read/write the cache directly. |
Modified files
| File | Change |
|------|--------|
| .env | Appends REDIS_HOST, REDIS_PORT, REDIS_PASSWORD, REDIS_DB |
| src/app.module.ts | Adds RedisModule and GuardModule to imports |
Note: The script never overwrites existing files. To regenerate a file, delete it and re-run
npx exguard-cached.
3. Environment variables
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=
REDIS_DB=0
GUARD_API_URL= # optional — enables auto-refresh on cache miss4. Protect endpoints with permissions
Prerequisite
A JWT auth guard must run first and set request.user.cognitoSubId.
Permission format: The decorator joins
moduleandactionwith:to form the permission string (e.g.RequireCachedUserPermission('users', 'view')checks forusers:viewin the user's permission list).
Basic example
import { Controller, Get, UseGuards } from '@nestjs/common';
import {
RequireCachedUserPermission,
CachedUser,
CachedUserPermissionGuard,
} from 'exguard-cached';
import type { CachedUserData } from 'exguard-cached';
@Controller('users')
@UseGuards(CachedUserPermissionGuard)
export class UsersController {
@Get()
@RequireCachedUserPermission('users', 'view_all')
async getUsers(@CachedUser() user: CachedUserData) {
return {
roles: user.roles,
groups: user.groups,
permissions: user.permissions,
};
}
}How the guard works
- Reads
@RequireCachedUserPermission('module', 'action')from the route - Checks if
user.permissionsincludesmodule:action - Gets
cognitoSubIdfromrequest.user(set by your JWT guard) - Looks up the user in Redis by key
guard:user:{cognitoSubId} - Cache miss? If
guardBaseUrlis configured, calls{guardBaseUrl}/guard/mewith the auth header to refresh, then retries Redis - Missing permission? Throws
ForbiddenException('Insufficient permissions. Required: module:action') - Authorized? Attaches the full
CachedUserDatatorequest.cachedUser, which you access via@CachedUser()
Protecting an entire controller vs. specific routes
Guard on the controller class level protects all routes:
@Controller('admin')
@UseGuards(CachedUserPermissionGuard)
export class AdminController {
@Get('dashboard')
@RequireCachedUserPermission('admin', 'dashboard_view')
getDashboard(@CachedUser() user: CachedUserData) {
// user.roles, user.permissions, etc.
}
@Get('settings')
@RequireCachedUserPermission('admin', 'settings_manage')
getSettings(@CachedUser() user: CachedUserData) {
// ...
}
}Guard on a single route (controller-level guard still needed):
@Controller('reports')
export class ReportsController {
@Get()
@UseGuards(CachedUserPermissionGuard)
@RequireCachedUserPermission('reports', 'view_report')
async getReports(@CachedUser() user: CachedUserData) {
return { reports: ..., userRole: user.roles };
}
}Using CachedUserService directly in your own services
import { Injectable } from '@nestjs/common';
import { CachedUserService } from 'exguard-cached';
@Injectable()
export class AuthService {
constructor(private cache: CachedUserService) {}
// Store user data after login
async onLogin(cognitoSubId: string, userData: CachedUserData) {
await this.cache.setCachedUser(cognitoSubId, userData);
}
// Invalidate cache when permissions change
async onPermissionsChanged(cognitoSubId: string) {
await this.cache.invalidateCachedUser(cognitoSubId);
}
// Check permissions programmatically
async canUserAccess(cognitoSubId: string, token: string) {
return this.cache.hasPermission(cognitoSubId, 'users', 'view_all', token);
}
}Using CacheUtils for static checks
import { CacheUtils } from 'exguard-cached';
// Anywhere — no injection needed
const allowed = CacheUtils.hasPermission(cachedUser, 'users', 'view_all');
const anyMatch = CacheUtils.hasAnyPermission(cachedUser, [
{ module: 'users', action: 'view' },
{ module: 'admin', action: 'manage' },
]);
const isAdmin = CacheUtils.hasRole(cachedUser, 'superadmin');
const inGroup = CacheUtils.isInGroup(cachedUser, 'field-operations');Extracting user profile info from cached data
The @CachedUser() decorator gives you the full CachedUserData object, which includes a user field with personal details:
@Controller('profile')
@UseGuards(CachedUserPermissionGuard)
export class ProfileController {
@Get('me')
@RequireCachedUserPermission('profile', 'view_own')
async getProfile(@CachedUser() cached: CachedUserData) {
const profile = cached.user; // UserInfo | null
const fieldOffice = profile?.fieldOffice;
return {
fullName: `${profile?.givenName} ${profile?.familyName}`,
email: profile?.email,
username: profile?.username,
employeeNumber: profile?.employeeNumber,
regionId: profile?.regionId,
fieldOffice: fieldOffice
? `${fieldOffice.name} (${fieldOffice.code})`
: null,
};
}
}You can also extract user info via CachedUserService:
@Injectable()
export class ProfileService {
constructor(private cache: CachedUserService) {}
async getUserProfile(cognitoSubId: string, token?: string) {
const data = await this.cache.getCachedUser(cognitoSubId, token);
return {
user: data?.user ?? null,
groups: data?.groups ?? [],
roles: data?.roles ?? [],
modules: data?.modules ?? [],
};
}
}Available fields on UserInfo:
| Field | Type | Description |
|-------|------|-------------|
| id | string | User ID |
| cognitoSubId | string | Cognito sub identifier |
| username | string | Login username |
| email | string | Email address |
| emailVerified | boolean | Email verification status |
| givenName | string | First name |
| familyName | string | Last name |
| employeeNumber | string | Employee number |
| regionId | string \| null | Assigned region |
| fieldOffice | { id, code, name } \| null | Field office details |
| lastLoginAt | string \| null | Last login timestamp |
5. How real-time cache sync works
exguard-cached enables real-time permission updates by combining Redis caching with an optional Guard API fallback:
Frontend (exguard-client) Backend (NestJS) Guard API
│ │ │
│ POST /guard/redis/set │ │
│ { key, value } │ │
├──────────────────────────────────────►│ writes to Redis │
│ │ │
│ POST /guard/redis/del │ │
│ { key: "user:{sub}" } │ │
├──────────────────────────────────────►│ deletes from Redis │
│ │ │
│ │ GET /guard/me │
│ │ (on cache miss) │
│ ├─────────────────────────────►│
│ │◄─────────────────────────────┤
│ │ refreshes Redis │
│ │ │
│ GET /guard/redis/get │ │
│ ?key=user:{sub} │ │
├──────────────────────────────────────►│ reads from Redis │- Frontend writes/updates permissions directly to Redis via the proxy endpoints (
/guard/redis/set,/guard/redis/del) - Backend reads permission-checking requests from Redis on every guarded route
- Auto-refresh on cache miss: If a user isn't in Redis yet and
guardBaseUrlis set, the service automatically calls the Guard API to populate the cache and retries the read — no manual seeding needed - Immediate invalidation: The moment a permission changes, the frontend (or any service) calls
/guard/redis/deland the next backend request re-fetches fresh data
Full CachedUserService API
Read / Write / Invalidate
getCachedUser(cognitoSubId, token?) // Get user data (falls back to Guard API)
setCachedUser(cognitoSubId, data, ttl?) // Store user in Redis
invalidateCachedUser(cognitoSubId) // Delete user from Redis
refreshCache(token) // Manually refresh from Guard APIPermission queries
hasPermission(cognitoSubId, module, action, token?) // boolean
getUserPermissions(cognitoSubId, token?) // string[]
getUserRoles(cognitoSubId, token?) // string[]
getUserGroups(cognitoSubId, token?) // string[]
getUserModules(cognitoSubId, token?) // ModulePermission[]Raw Redis (prefixed with keyPrefix)
getRaw(key) // any value
setRaw(key, value, ttl?) // write
delRaw(key) // deleteExGuardCacheModule.forRoot() options
| Option | Default | Description |
|--------|---------|-------------|
| redisClient | 'REDIS_CLIENT' | DI token for your Redis client |
| keyPrefix | 'guard:' | Redis key prefix |
| defaultTtl | 300 | Default TTL in seconds |
| guardBaseUrl | '' | Guard API URL for cache-miss fallback |
Data structure
interface CachedUserData {
user: UserInfo | null;
groups: string[];
roles: string[];
modules: Array<{ key: string; name: string; permissions: string[] }>;
fieldOffices: string[];
permissions: string[]; // ["users:view_all", "admin:dashboard_view", ...]
cacheInfo: {
userId: string;
cognitoSubId: string;
cacheKeys: { access: string; permissions: string };
};
}Requirements
- Node.js >= 18
- NestJS 10.x or 11.x (
@nestjs/common,@nestjs/coreas peer dependencies) - Redis server
