Role-Based Access Control (RBAC)
DBS features a granular, database-backed Role-Based Access Control system. Roles are collections of permissions. Users are assigned roles. Every API route and UI element is gated by a specific permission string.
The Permission Model
Permissions are defined as constants in src/lib/rbac/permission.ts and stored in the database via seeding:
// src/lib/rbac/permission.ts
export const PERMISSIONS = {
DASHBOARD_ACCESS: 'dashboard.access',
USER_READ: 'user.read',
USER_WRITE: 'user.write',
USER_UPDATE: 'user.update',
USER_DELETE: 'user.delete',
ROLES_MANAGE: 'roles.manage',
LOG_READ: 'log.read',
SETTINGS_MANAGE: 'settings.manage',
TEAM_READ: 'team.read',
TEAM_WRITE: 'team.write',
} as const;
export type Permission = typeof PERMISSIONS[keyof typeof PERMISSIONS];Role Hierarchy
Roles follow a strict hierarchy from highest to lowest privilege:
super_admin → admin → manager → user → guestsuper_adminbypasses all permission checks automatically.- Each role can only manage roles below it in the hierarchy. This is enforced by
canManageRole()insrc/lib/role-hierarchy.ts.
// src/lib/role-hierarchy.ts
import { canManageRole } from '@/lib/role-hierarchy';
// Can an admin manage a manager? Yes.
canManageRole('admin', 'manager'); // true
// Can a manager manage an admin? No.
canManageRole('manager', 'admin'); // falseProtecting an API Route
Use apiGuard() from src/lib/api-guard.ts at the top of every route handler:
Session Only
// Any authenticated user
export async function GET() {
const guard = await apiGuard();
if (guard.error) return guard.error; // 401 if not logged in
const { session } = guard;
// session.user.id, session.user.email, etc.
}super_admin users automatically bypass all permission checks. They still go through session validation and rate limiting.
Protecting a Page (Server Component)
Use the RouteGuard server component to protect entire pages. If the user lacks the required permission, they are redirected or shown an error.
// src/app/dashboard/users/page.tsx
import { RouteGuard } from '@/lib/rbac/components/RouteGuard';
export default function UsersPage() {
return (
<RouteGuard permission="user.read">
<UsersPageContent />
</RouteGuard>
);
}Client-Side Permission Check
Use the usePermission hook to conditionally show/hide UI elements based on the current user’s permissions:
'use client';
import { usePermission } from '@/lib/rbac/hooks/usePermission';
import { PermissionAlert } from '@/components/common/PermissionAlert';
export function CreateUserButton() {
const { allowed, isLoading } = usePermission('user.write');
if (isLoading) return <Skeleton />;
if (!allowed) return <PermissionAlert />;
return <Button>Create User</Button>;
}Client-side permission checks are for UI convenience only — they can be bypassed. Always enforce permissions on the server with apiGuard(). The client-side check should never be the only line of defense.
RBAC API Endpoints
| Method | Endpoint | Permission Required | Description |
|---|---|---|---|
GET | /api/access/roles | roles.manage | List all roles with their permissions |
POST | /api/access/roles | roles.manage | Create a new role |
PUT | /api/access/roles/[id] | roles.manage | Update a role’s permissions |
DELETE | /api/access/roles/[id] | roles.manage | Delete a role |
GET | /api/access/permissions | roles.manage | List all available permissions |
POST | /api/access/sync | roles.manage | Sync permission constants to the database |
Adding a New Permission
Step 1: Define
Add your new permission key to src/lib/rbac/permission.ts:
export const PERMISSIONS = {
// ...existing...
REPORTS_READ: 'reports.read',
REPORTS_WRITE: 'reports.write',
} as const;