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:
- โ Always check Redis cache first
- ๐ก Call API ONLY if cache is completely empty
- ๐ซ Never call API for permission checks
- ๐ 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:3000How 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-clientQuick 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
- Redis First: Always checks Redis cache first (via backend HTTP proxy)
- API Fallback: Only calls
/guard/verify-token+/guard/meif NO cache exists - Auto-Cache: API results are automatically cached in Redis
- Auth Token:
HttpRedisClientautomatically 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:
- First visit with completely empty cache
- Cache expired and real-time update invalidates
- Manual refetch is explicitly called
- Cache corruption detected
When API is NOT Called ๐ซ
The API is NEVER called for:
- Permission checks - uses cached data only
- Module access checks - uses cached data only
- User roles/groups - uses cached data only
- 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 setupBackend 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/meCache Not Updating
// Force cache refresh
const { refetch } = useUserAccessCacheFirst();
await refetch(true); // Force API callDebug 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 resortLicense
MIT License - see LICENSE file for details.
Support
For issues and questions:
- Check the troubleshooting section
- Verify Redis connectivity
- Ensure backend uses
exguard-cachedpackage - Check environment variables
๐ Cache-First Performance: Built for speed, designed for scale.