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-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

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-cached

2. Auto-setup

npx exguard-cached

This 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 miss

4. Protect endpoints with permissions

Prerequisite

A JWT auth guard must run first and set request.user.cognitoSubId.

Permission format: The decorator joins module and action with : to form the permission string (e.g. RequireCachedUserPermission('users', 'view') checks for users:view in 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

  1. Reads @RequireCachedUserPermission('module', 'action') from the route
  2. Checks if user.permissions includes module:action
  3. Gets cognitoSubId from request.user (set by your JWT guard)
  4. Looks up the user in Redis by key guard:user:{cognitoSubId}
  5. Cache miss? If guardBaseUrl is configured, calls {guardBaseUrl}/guard/me with the auth header to refresh, then retries Redis
  6. Missing permission? Throws ForbiddenException('Insufficient permissions. Required: module:action')
  7. Authorized? Attaches the full CachedUserData to request.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 guardBaseUrl is 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/del and 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 API

Permission 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)           // delete

ExGuardCacheModule.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/core as peer dependencies)
  • Redis server