JSPM

  • Created
  • Published
  • Downloads 1106
  • Score
    100M100P100Q98885F
  • License MIT

ExGuard RBAC client with cache-first Redis support for maximum performance in EmpowerX applications

Package Exports

  • exguard-client
  • exguard-client/setup

Readme

exguard-client

ExGuard RBAC (Role-Based Access Control) client library with cache-first Redis support for EmpowerX applications.

๐Ÿš€ Cache-First Performance: Prioritizes Redis cache over API calls for maximum speed and efficiency.

Features

  • ๐Ÿ” RBAC Authentication: Token verification and user access management
  • ๐Ÿ”„ Realtime Updates: WebSocket-based real-time RBAC changes
  • ๐ŸŽฏ Permission Guards: React components for route and feature protection
  • ๐Ÿช React Hooks: Easy-to-use hooks for permission checking
  • ๐Ÿ“ฆ TypeScript: Full type safety with TypeScript definitions
  • โšก Auto-Configuration: Automatically detects API URL based on environment
  • ๐Ÿš€ Zero Setup: Works out of the box with sensible defaults
  • ๐Ÿ’พ Cache-First Redis: Prioritizes Redis cache, minimizes API calls
  • ๐Ÿ”— Cross-Tab Sync: Synchronized data across browser tabs
  • โšก Backend Integration: Seamless integration with exguard-cached backend package

Cache-First Strategy

This package implements a pure cache-first approach:

  1. โœ… Always check Redis cache first
  2. ๐Ÿ“ก Call API ONLY if cache is completely empty
  3. ๐Ÿšซ Never call API for permission checks
  4. ๐Ÿ”„ Rely on backend cache invalidation for updates

Environment Variables

Required for the frontend to know your backend API URL (which proxies to Redis):

# Backend API URL (required - used as proxy to Redis)
VITE_GUARD_API_URL=http://localhost:3000

# OR alternatively:
VITE_GUARD_APP_URL=http://localhost:3000

How it works: The HttpRedisClient automatically reads these env vars and constructs:

http://localhost:3000/guard/redis/*

No direct Redis connection needed - the backend handles Redis communication!

Installation

pnpm add exguard-client

Quick Start

To integrate exguard-client into your React frontend, you need to edit 3 files:

1. Initialize Redis Client (Optional - Auto-configured)

File to edit: src/main.tsx or src/App.tsx

The package automatically detects your backend API URL - no Redis config needed!

import { initializeCacheFirstRedisClient } from 'exguard-client';

// Option 1: Auto-detect from environment (VITE_GUARD_API_URL or VITE_GUARD_APP_URL)
initializeCacheFirstRedisClient();

// Option 2: Explicit backend API URL (if env vars not set)
initializeCacheFirstRedisClient({
  host: 'localhost',
  port: 3000,  // Your BACKEND API port (not Redis port!)
});

How it works: HttpRedisClient uses your backend API as a proxy to Redis - no need to expose Redis directly to the browser!

Backend Setup Required: Your backend must use exguard-cached package to provide Redis proxy endpoints (/guard/redis/*).

2. Wrap with Providers

File to edit: src/App.tsx or your root component

import { ExGuardRealtimeProvider } from 'exguard-client';

function App() {
  return (
    <ExGuardRealtimeProvider>
      <Router>
        <Routes>
          <Route path="/roles" element={<RolesPage />} />
          <Route path="/users" element={<UsersPage />} />
        </Routes>
      </Router>
    </ExGuardRealtimeProvider>
  );
}

Optional: If you want a global permission context, create a PermissionProvider in your app (see Advanced Usage below).

3. Use Cache-First Hook in Components

File to edit: Any component that needs user access or permission checks

import { useUserAccessCacheFirst } from 'exguard-client';

function UsersPage() {
  const { userAccess, isLoading, hasPermission, hasModulePermission } = useUserAccessCacheFirst();

  // Permission checking uses ONLY cached data - no API calls!
  // Supports both formats:
  const canViewUsers = hasPermission('user_groups:view');  // Format: 'resource:action'
  const canEditRoles = hasPermission('exGUARD', 'roles:edit');  // Format: module, 'resource:action'
  const hasUserGroupModule = hasModulePermission('user_groups');  // Check if user has ANY permission in module

  if (isLoading) return <div>Loading from cache...</div>;

  return (
    <div>
      <h1>Welcome {userAccess?.user?.username}</h1>
      {canViewUsers && <UserList />}
      {canEditRoles && <RoleEditor />}
    </div>
  );
}

Cache-First Hook API

useUserAccessCacheFirst()

Returns an object with the following properties:

interface UseUserAccessCacheFirstReturn {
  userAccess: UserAccessData | null;           // Cached user data
  isLoading: boolean;                          // Loading from cache
  isFetching: boolean;                        // Fetching from API (rare)
  error: Error | null;                        // Error state
  hasPermission: (permission: string, action?: string) => boolean;
  hasModuleAccess: (module: string) => boolean;
  hasModulePermission: (module: string) => boolean;  // Alias for hasModuleAccess
  getModulePermissions: (module: string) => string[];
  refetch: (force?: boolean) => Promise<void>; // Force API call
  refetchSilent: () => Promise<void>;         // Silent refresh
  invalidateCache: () => void;                // Invalidate cache
  clearCache: () => Promise<void>;            // Clear cache
}

Permission Checking

const { hasPermission, hasModulePermission, getModulePermissions } = useUserAccessCacheFirst();

// Format 1: hasPermission('resource:action') - RECOMMENDED
// Searches across ALL modules for the permission
const canViewUsers = hasPermission('user_groups:view');
const canUpdateGroup = hasPermission('user_groups:update_group');
const canDeleteUser = hasPermission('exGUARD:users:delete');

// Format 2: hasPermission('module', 'resource:action')
const canEditRoles = hasPermission('exGUARD', 'roles:edit');

// Check if user has ANY permission in a module
const hasUserGroupModule = hasModulePermission('user_groups');

// Get all permissions for a module
const permissions = getModulePermissions('exGUARD');

Getting User Data from Cache

import { useMemo } from 'react';
import { useUserAccessCacheFirst } from 'exguard-client';

const { userAccess } = useUserAccessCacheFirst();

// Get user roles (from cache - no API call!)
const userRoles = useMemo(() => userAccess?.roles ?? [], [userAccess?.roles]);
// Returns: ["system administrator", "regional admin", ...]

// Get user groups (from cache - no API call!)
const userGroups = useMemo(() => userAccess?.groups ?? [], [userAccess?.groups]);
// Returns: ["administrator", "national access", ...]

// Get user details (from cache - no API call!)
const user = userAccess?.user;
// Returns: { id, username, email, givenName, familyName, ... }

// Get module permissions (from cache - no API call!)
const modules = userAccess?.modules;
// Returns: [{ key: 'exGUARD', permissions: ['user_groups:view', ...] }, ...]

// Example: Check if user is admin
const isAdmin = userGroups.some((g: string) => g.toLowerCase() === 'administrator');
const isSystemAdmin = userRoles.some((r: string) => r.toLowerCase() === 'system administrator');

How It Works

  1. Redis First: Always checks Redis cache first (via backend HTTP proxy)
  2. API Fallback: Only calls /guard/verify-token + /guard/me if NO cache exists
  3. Auto-Cache: API results are automatically cached in Redis
  4. Auth Token: HttpRedisClient automatically sends auth token with Redis requests
// โšก INSTANT - Uses cached data (0ms)
const canView = hasPermission('user_groups:view');

// ๐Ÿ“ก SLOW - API call only when cache empty
// (happens once per session or on cache invalidation)
const { userAccess } = useUserAccessCacheFirst();

Cache Behavior

When API is Called ๐Ÿ“ก

The API endpoint is called ONLY in these rare cases:

  1. First visit with completely empty cache
  2. Cache expired and real-time update invalidates
  3. Manual refetch is explicitly called
  4. Cache corruption detected

When API is NOT Called ๐Ÿšซ

The API is NEVER called for:

  1. Permission checks - uses cached data only
  2. Module access checks - uses cached data only
  3. User roles/groups - uses cached data only
  4. Field office data - uses cached data only

Cache Performance

// โœ… INSTANT - Uses cached data (0ms)
const canView = hasPermission('exGUARD', 'users:view');

// ๐Ÿ“ก SLOW - API call only when cache empty
// (happens once per session or on cache invalidation)
const { userAccess } = useUserAccessCacheFirst();

Backend Integration

For optimal performance, ensure your backend uses the exguard-cached package:

Backend Setup

# Install backend package
pnpm add exguard-cached

# Auto-configure (recommended)
npx exguard-cached setup

Backend Endpoint

import { Controller, Get, UseGuards } from '@nestjs/common';
import { RequireCachedUserPermission, CachedUser } from 'exguard-cached';

@Controller('guard')
export class GuardController {
  
  @Get('me')
  @RequireCachedUserPermission('exGUARD', 'profile:view')
  async getMe(@CachedUser() cachedUser: CachedUserData) {
    return {
      success: true,
      data: {
        user: cachedUser.user,
        groups: cachedUser.groups,
        roles: cachedUser.roles,
        modules: cachedUser.modules,
        fieldOffices: cachedUser.fieldOffices,
      }
    };
  }
}

Cache Synchronization

The backend automatically invalidates cache when:

  • User permissions change
  • Roles are updated
  • Groups are modified
  • Real-time WebSocket updates are received

Advanced Usage

Direct Redis Client Access

import { getCacheFirstRedisClient } from 'exguard-client';

const redisClient = getCacheFirstRedisClient();

// Direct Redis operations
await redisClient.set('custom:key', data, { ttl: 300 });
const cached = await redisClient.get('custom:key');
await redisClient.del('custom:key');

Custom Cache Keys

import { RedisCacheClient } from 'exguard-client';

const customClient = new RedisCacheClient({
  host: 'localhost',
  port: 6379,
  keyPrefix: 'myapp:custom:',
});

await customClient.set('user:123', userData, { ttl: 600 });

Fallback to LocalStorage

If Redis is unavailable, the client gracefully falls back to localStorage:

// The hook automatically handles Redis failures
const { userAccess } = useUserAccessCacheFirst();
// Works even if Redis is down (uses localStorage fallback)

Migration Guide

From localStorage

// Before (localStorage)
import { useUserAccessSingleton } from 'exguard-client';
const { userAccess } = useUserAccessSingleton();

// After (Cache-First Redis)
import { useUserAccessCacheFirst, initializeCacheFirstRedisClient } from 'exguard-client';

// Initialize once
initializeCacheFirstRedisClient({
  host: 'localhost',
  port: 6379,
});

// Use in components - cache-first approach
const { userAccess, hasPermission } = useUserAccessCacheFirst();

From Basic Redis

// Before (basic Redis)
import { useUserAccessRedis, initializeRedisClient } from 'exguard-client';
initializeRedisClient(config);

// After (Cache-First Redis)
import { useUserAccessCacheFirst, initializeCacheFirstRedisClient } from 'exguard-client';
initializeCacheFirstRedisClient(config);

Performance Benefits

Feature localStorage Basic Redis Cache-First Redis
API Calls Frequent Moderate Minimal
Cross-tab Sync โŒ โœ… โœ…
Backend Sync โŒ โœ… โœ…
Permission Speed โšก Fast ๐Ÿš€ Fast โšกโšก Instant
Cache Hits 30% 70% 95%+

Troubleshooting

Common Issues

Permission Check Returns False

Your cached permissions might be in format exGUARD:user_groups:update_group but you're checking user_groups:update.

Solution: Use the full permission string:

// โœ… CORRECT - Searches across ALL modules
const canUpdate = hasPermission('user_groups:update_group');
const canUpdate = hasPermission('exGUARD:user_groups:update_group');

// Also works with module + permission
const canUpdate = hasPermission('exGUARD', 'user_groups:update_group');

// Debug: Check what permissions user has
const { userAccess } = useUserAccessCacheFirst();
console.log('User modules:', userAccess?.modules);

Redis Connection Failed (401 Unauthorized)

The HttpRedisClient now automatically sends the auth token. If you still get 401:

# Check if user is logged in
echo $localStorage.getItem('access_token')

# Check backend API is running
curl http://localhost:3000/guard/me

Cache Not Updating

// Force cache refresh
const { refetch } = useUserAccessCacheFirst();
await refetch(true); // Force API call

Debug Mode

Enable debug logging:

// Add to environment
EXGUARD_DEBUG=true

// Check browser console for cache logs
// [ExGuard Cache-First] โœ… Cache hit - using Redis data
// [ExGuard Cache-First] ๐Ÿ“ก No cache found, calling API as last resort

License

MIT License - see LICENSE file for details.

Support

For issues and questions:

  1. Check the troubleshooting section
  2. Verify Redis connectivity
  3. Ensure backend uses exguard-cached package
  4. Check environment variables

๐Ÿš€ Cache-First Performance: Built for speed, designed for scale.