Skip to main content

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:

  1. Library (@xond/ui): Provides feature registry and access checking functions
  2. App: Initializes RBAC with API endpoints and syncs features to database
  3. Database: Stores features and access rules in config schema

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 FieldDatabase FieldNotes
featureKeycodeUnique identifier for the feature
featureNamenameHuman-readable name
metadataadditional_infoJSONB 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:

  • config schema
  • config.feature table with fields: feature_id, code, name, description, feature_type, additional_info
  • config.feature_access table 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:

  1. Default Access: If no rules exist for a feature, access is allowed (open by default)
  2. Deny Rules: If a deny rule exists for the user/role/org, access is denied
  3. Allow Rules: If an allow rule exists (and no deny rule), access is allowed
  4. 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

  1. Register features early: Register features when menus/components are initialized
  2. Sync periodically: Sync features to database on app startup or periodically
  3. Cache access checks: Consider caching access checks to reduce API calls
  4. Fail secure: On error, deny access (fail secure) rather than allow
  5. 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);