RBAC (Role-Based Access Control) System
This RBAC system allows you to control access to features (menus, context menus, actions, pages) based on user roles, persons, and organizations.
Architecture
The RBAC system follows the same pattern as other @xond/ui initialization functions:
- Library (
@xond/ui): Provides feature registry and access checking functions - App: Initializes RBAC with API endpoints and syncs features to database
- Database: Stores features and access rules in
configschema
Database Schema
The RBAC system uses two tables in the config schema:
config.feature: Stores registered features (menus, context menus, actions, etc.)config.feature_access: Controls who has access to which features
Field Mapping
The library uses camelCase field names (featureKey, featureName, metadata), but the database follows Xond's standard naming convention (code, name, additional_info). The backend must map between these:
| Library Field | Database Field | Notes |
|---|---|---|
featureKey | code | Unique identifier for the feature |
featureName | name | Human-readable name |
metadata | additional_info | JSONB field for additional data |
category | - | Optional, not in ERD (can be ignored) |
The backend endpoints handle this mapping automatically.
Setup
1. Create Database Schema
Create the database schema based on your ERD. The RBAC system requires:
configschemaconfig.featuretable with fields:feature_id,code,name,description,feature_type,additional_infoconfig.feature_accesstable with fields:feature_access_id,feature_id,person_id,role_id,organization_id,access_type
Refer to your database ERD for the complete schema definition.
2. Initialize RBAC in Your App
In your app's main.tsx (or initialization file):
import { initRbac, syncFeaturesToDatabase } from "@xond/ui";
// Initialize RBAC with API endpoints
initRbac(
"/rbac/sync-features", // Endpoint to sync features to database
"/rbac/check-access" // Endpoint to check user access
);
// Sync registered features to database (call after app initialization)
// This can be done periodically or on app startup
syncFeaturesToDatabase();
3. Create Backend Endpoints
You need to create two endpoints in your API:
/rbac/sync-features (POST)
Receives features from the frontend and upserts them to the database:
// Example endpoint handler
app.post("/rbac/sync-features", async (req, res) => {
const { features } = req.body;
// Upsert features to config.feature table
// Map library interface to Xond standard database schema:
// featureKey → code
// featureName → name
// metadata → additional_info
// category → ignore (optional, not in ERD)
for (const feature of features) {
await prisma.feature.upsert({
where: { code: feature.featureKey }, // Map featureKey → code
update: {
name: feature.featureName, // Map featureName → name
feature_type: feature.featureType,
description: feature.description,
additional_info: feature.metadata, // Map metadata → additional_info
last_updated: new Date(),
},
create: {
code: feature.featureKey, // Map featureKey → code
name: feature.featureName, // Map featureName → name
feature_type: feature.featureType,
description: feature.description,
additional_info: feature.metadata, // Map metadata → additional_info
},
});
}
res.json({ success: true, synced: features.length });
});
/rbac/check-access (GET or POST)
Checks if the current user has access to a feature:
// GET /rbac/check-access?featureKey=menu.dashboard
// POST /rbac/check-access { featureKeys: ["menu.dashboard", "context.user.edit"] }
app.get("/rbac/check-access", async (req, res) => {
const { featureKey } = req.query; // Library sends featureKey
const userId = req.user?.id; // From JWT token
const roleId = req.user?.roleId;
const organizationId = req.user?.organizationId;
// First, find the feature by code (mapped from featureKey)
const feature = await prisma.feature.findUnique({
where: { code: featureKey }, // Map featureKey → code
});
if (!feature) {
// Feature doesn't exist, default to allow (open by default)
return res.json({ hasAccess: true });
}
// Query config.feature_access to check access
// Default: allow if no rules exist (open by default)
// Deny if explicit deny rule exists
// Allow if explicit allow rule exists
const hasAccess = await checkFeatureAccess(
feature.feature_id, // Use feature_id from database
userId,
roleId,
organizationId
);
res.json({ hasAccess });
});
Usage
Registering Features
Features are automatically registered when menus/context menus are created, or you can register them manually:
import { registerFeature, registerFeatures } from "@xond/ui";
// Register a single feature
registerFeature({
featureKey: "menu.dashboard",
featureName: "Dashboard",
featureType: "menu",
category: "navigation",
metadata: { href: "#/dashboard", icon: "HomeIcon" },
});
// Register multiple features
registerFeatures([
{
featureKey: "menu.users",
featureName: "Users",
featureType: "menu",
},
{
featureKey: "context.user.edit",
featureName: "Edit User",
featureType: "context_menu",
},
]);
Checking Access
import { checkAccess, checkMultipleAccess, filterMenuItemsByAccess } from "@xond/ui";
// Check single feature access
const canAccessDashboard = await checkAccess("menu.dashboard");
// Check multiple features at once
const accessMap = await checkMultipleAccess(["menu.dashboard", "menu.users", "context.user.edit"]);
// Filter menu items by access
const filteredItems = await filterMenuItemsByAccess(menuItems);
Using with Components
ContextMenu
import { ContextMenu, IMenuItem } from "@xond/ui";
import { registerFeature } from "@xond/ui";
const menuItems: IMenuItem[] = [
{
menuTitle: "Edit",
menuAction: () => handleEdit(),
featureKey: "context.user.edit", // Add featureKey
},
{
menuTitle: "Delete",
menuAction: () => handleDelete(),
featureKey: "context.user.delete",
},
];
// Register features when menu is created
menuItems.forEach((item) => {
if (item.featureKey) {
registerFeature({
featureKey: item.featureKey,
featureName: item.menuTitle,
featureType: "context_menu",
});
}
});
// Filter by access before rendering
const filteredItems = await filterMenuItemsByAccess(menuItems);
<ContextMenu id="user-menu" menuItems={filteredItems} />;
SimpleSideNavigation
import { SimpleSideNavigation, NavigationItem } from "@xond/ui";
import { registerFeature, filterMenuItemsByAccess } from "@xond/ui";
const navigation: NavigationItem[] = [
{
id: "dashboard",
name: "Dashboard",
href: "#/dashboard",
featureKey: "menu.dashboard", // Add featureKey
},
{
id: "users",
name: "Users",
href: "#/users",
featureKey: "menu.users",
},
];
// Register features
navigation.forEach((item) => {
if (item.featureKey) {
registerFeature({
featureKey: item.featureKey,
featureName: item.name || "",
featureType: "menu",
metadata: { href: item.href },
});
}
});
// Filter by access
const filteredNavigation = await filterMenuItemsByAccess(navigation);
<SimpleSideNavigation navigation={filteredNavigation} currentHash={currentHash} />;
Access Control Logic
The access control follows this logic:
- Default Access: If no rules exist for a feature, access is allowed (open by default)
- Deny Rules: If a deny rule exists for the user/role/org, access is denied
- Allow Rules: If an allow rule exists (and no deny rule), access is allowed
- Priority: Deny rules take precedence over allow rules
Feature Key Naming Convention
Use a hierarchical naming convention:
menu.{section}- Main navigation menus (e.g.,menu.dashboard,menu.users)context.{entity}.{action}- Context menu actions (e.g.,context.user.edit,context.user.delete)page.{page}- Page access (e.g.,page.reports,page.settings)action.{action}- General actions (e.g.,action.export,action.print)
Best Practices
- Register features early: Register features when menus/components are initialized
- Sync periodically: Sync features to database on app startup or periodically
- Cache access checks: Consider caching access checks to reduce API calls
- Fail secure: On error, deny access (fail secure) rather than allow
- Feature keys: Use consistent, hierarchical naming for feature keys
Example: Complete Integration
// main.tsx
import { initRbac, syncFeaturesToDatabase } from "@xond/ui";
// Initialize RBAC
initRbac("/rbac/sync-features", "/rbac/check-access");
// After app initialization, sync features
setTimeout(() => {
syncFeaturesToDatabase();
}, 1000);
// In your navigation component
import { registerFeature, filterMenuItemsByAccess } from "@xond/ui";
const navigation = [
{ id: "dashboard", name: "Dashboard", href: "#/dashboard", featureKey: "menu.dashboard" },
// ... more items
];
// Register and filter
navigation.forEach((item) => {
if (item.featureKey) {
registerFeature({
featureKey: item.featureKey,
featureName: item.name || "",
featureType: "menu",
});
}
});
const filteredNav = await filterMenuItemsByAccess(navigation);