rbac-permission-system
Purpose
Implements role-based access control (RBAC) with permission checking throughout the application. Ensures users can only access features and data they're authorized to use.
When to Use
- •Checking if a user can perform an action
- •Hiding/showing UI elements based on permissions
- •Protecting API routes with permission gates
- •Implementing role-based logic
- •Checking location access
Permission System Structure
User Object with RBAC
typescript
interface RBACUser {
id: string
permissions: string[] // Aggregated from roles + direct permissions
roles: string[] // Role names (e.g., "Super Admin", "Cashier")
businessId?: string // Multi-tenant isolation
locationIds?: number[] // Accessible locations
}
How Permissions Are Loaded
From auth.ts (lines 186-230):
- •Role Permissions - Inherited from assigned roles
- •Direct Permissions - Assigned directly to user
- •Super Admin Override - Super Admin gets ALL permissions automatically
typescript
// Permissions are aggregated during login:
const rolePermissions = user.roles.flatMap(ur =>
ur.role.permissions.map(rp => rp.permission.name)
)
const directPermissions = user.permissions.map(up => up.permission.name)
let allPermissions = [...new Set([...rolePermissions, ...directPermissions])]
// Super Admin gets everything
if (roleNames.includes('Super Admin') ||
roleNames.includes('System Administrator') ||
roleNames.includes('Super Admin (Legacy)')) {
allPermissions = Object.values(PERMISSIONS)
}
Core Permission Functions
Import
typescript
import {
hasPermission,
hasAnyPermission,
hasAllPermissions,
hasRole,
hasAnyRole,
isSuperAdmin,
PERMISSIONS
} from '@/lib/rbac'
1. hasPermission()
Check single permission:
typescript
import { hasPermission, PERMISSIONS } from '@/lib/rbac'
// In API route
const user = session.user as any
if (hasPermission(user, PERMISSIONS.PRODUCT_CREATE)) {
// User can create products
}
// Super Admin automatically passes
if (hasPermission(superAdminUser, PERMISSIONS.PRODUCT_CREATE)) {
// Returns true even if permission not explicitly assigned
}
2. hasAnyPermission()
User needs AT LEAST ONE of the permissions:
typescript
import { hasAnyPermission, PERMISSIONS } from '@/lib/rbac'
// Check if user can view products (own or all)
if (hasAnyPermission(user, [
PERMISSIONS.PRODUCT_VIEW,
PERMISSIONS.PRODUCT_VIEW_OWN
])) {
// User can view at least some products
}
3. hasAllPermissions()
User needs ALL listed permissions:
typescript
import { hasAllPermissions, PERMISSIONS } from '@/lib/rbac'
// Check if user can fully manage products
if (hasAllPermissions(user, [
PERMISSIONS.PRODUCT_VIEW,
PERMISSIONS.PRODUCT_CREATE,
PERMISSIONS.PRODUCT_UPDATE,
PERMISSIONS.PRODUCT_DELETE
])) {
// User has full product management access
}
4. hasRole()
Check specific role:
typescript
import { hasRole } from '@/lib/rbac'
if (hasRole(user, 'Branch Manager')) {
// User is a Branch Manager
}
5. hasAnyRole()
Check if user has any of the roles:
typescript
import { hasAnyRole } from '@/lib/rbac'
if (hasAnyRole(user, ['Super Admin', 'Branch Manager', 'Admin'])) {
// User is an admin-level user
}
6. isSuperAdmin()
Check if user is Super Admin:
typescript
import { isSuperAdmin } from '@/lib/rbac'
if (isSuperAdmin(user)) {
// User has platform-level access
// Automatically has ALL permissions
}
PERMISSIONS Constant
All available permissions are defined in PERMISSIONS object:
Dashboard
typescript
PERMISSIONS.DASHBOARD_VIEW
Users
typescript
PERMISSIONS.USER_VIEW PERMISSIONS.USER_CREATE PERMISSIONS.USER_UPDATE PERMISSIONS.USER_DELETE
Roles
typescript
PERMISSIONS.ROLE_VIEW PERMISSIONS.ROLE_CREATE PERMISSIONS.ROLE_UPDATE PERMISSIONS.ROLE_DELETE
Products - Basic CRUD
typescript
PERMISSIONS.PRODUCT_VIEW PERMISSIONS.PRODUCT_CREATE PERMISSIONS.PRODUCT_UPDATE PERMISSIONS.PRODUCT_DELETE
Products - Field-Level Security
typescript
PERMISSIONS.PRODUCT_VIEW_PURCHASE_PRICE // Can see cost price PERMISSIONS.PRODUCT_VIEW_PROFIT_MARGIN // Can see profit calculations PERMISSIONS.PRODUCT_VIEW_SUPPLIER // Can see supplier information PERMISSIONS.PRODUCT_VIEW_ALL_BRANCH_STOCK // Can see stock at other locations
Products - Stock Management
typescript
PERMISSIONS.PRODUCT_OPENING_STOCK PERMISSIONS.PRODUCT_LOCK_OPENING_STOCK PERMISSIONS.PRODUCT_UNLOCK_OPENING_STOCK PERMISSIONS.PRODUCT_MODIFY_LOCKED_STOCK
Products - Pricing
typescript
PERMISSIONS.PRODUCT_PRICE_EDIT // Edit own location prices PERMISSIONS.PRODUCT_PRICE_EDIT_ALL // Edit all locations prices PERMISSIONS.PRODUCT_PRICE_GLOBAL // Edit base/global prices PERMISSIONS.PRODUCT_PRICE_BULK_EDIT // Bulk price editing PERMISSIONS.PRODUCT_PRICE_IMPORT // Import prices from Excel PERMISSIONS.PRODUCT_PRICE_EXPORT // Export price lists PERMISSIONS.PRODUCT_COST_AUDIT_VIEW // View cost audit report PERMISSIONS.PRODUCT_PRICE_COMPARISON_VIEW // View price comparison report
Sales
typescript
PERMISSIONS.SALE_VIEW PERMISSIONS.SALE_CREATE PERMISSIONS.SALE_UPDATE PERMISSIONS.SALE_DELETE PERMISSIONS.SALE_VOID PERMISSIONS.SALE_REFUND PERMISSIONS.SALE_VIEW_OWN // Only see own sales
Purchases
typescript
PERMISSIONS.PURCHASE_VIEW PERMISSIONS.PURCHASE_CREATE PERMISSIONS.PURCHASE_UPDATE PERMISSIONS.PURCHASE_DELETE PERMISSIONS.PURCHASE_APPROVE
Inventory
typescript
PERMISSIONS.INVENTORY_VIEW PERMISSIONS.INVENTORY_ADJUSTMENT PERMISSIONS.INVENTORY_TRANSFER PERMISSIONS.STOCK_TRANSFER_VIEW PERMISSIONS.STOCK_TRANSFER_CREATE PERMISSIONS.STOCK_TRANSFER_SEND PERMISSIONS.STOCK_TRANSFER_RECEIVE
Reports
typescript
PERMISSIONS.REPORT_SALES PERMISSIONS.REPORT_PURCHASE PERMISSIONS.REPORT_INVENTORY PERMISSIONS.REPORT_PROFIT_LOSS PERMISSIONS.REPORT_STOCK_ALERT
Customers
typescript
PERMISSIONS.CUSTOMER_VIEW PERMISSIONS.CUSTOMER_CREATE PERMISSIONS.CUSTOMER_UPDATE PERMISSIONS.CUSTOMER_DELETE
Suppliers
typescript
PERMISSIONS.SUPPLIER_VIEW PERMISSIONS.SUPPLIER_CREATE PERMISSIONS.SUPPLIER_UPDATE PERMISSIONS.SUPPLIER_DELETE
Payments
typescript
PERMISSIONS.PAYMENT_VIEW PERMISSIONS.PAYMENT_CREATE PERMISSIONS.PAYMENT_UPDATE PERMISSIONS.PAYMENT_DELETE
Shifts
typescript
PERMISSIONS.SHIFT_VIEW PERMISSIONS.SHIFT_VIEW_ALL PERMISSIONS.SHIFT_OPEN PERMISSIONS.SHIFT_CLOSE
Settings
typescript
PERMISSIONS.SETTINGS_VIEW PERMISSIONS.SETTINGS_UPDATE PERMISSIONS.BUSINESS_SETTINGS_VIEW PERMISSIONS.BUSINESS_SETTINGS_UPDATE
Super Admin
typescript
PERMISSIONS.SUPERADMIN_ALL // Platform owner - has everything
Implementation Patterns
Pattern 1: API Route Protection
typescript
import { NextRequest, NextResponse } from 'next/server'
import { getServerSession } from 'next-auth'
import { authOptions } from '@/lib/auth'
import { hasPermission, PERMISSIONS } from '@/lib/rbac'
export async function POST(request: NextRequest) {
const session = await getServerSession(authOptions)
if (!session?.user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const user = session.user as any
// Check permission
if (!hasPermission(user, PERMISSIONS.PRODUCT_CREATE)) {
return NextResponse.json(
{ error: 'Forbidden - Insufficient permissions' },
{ status: 403 }
)
}
// User has permission - proceed with operation
// ...
}
Pattern 2: Client-Side Component Protection
typescript
'use client'
import { useSession } from 'next-auth/react'
import { hasPermission, PERMISSIONS } from '@/lib/rbac'
import { Button } from '@/components/ui/button'
export default function ProductManagement() {
const { data: session } = useSession()
const user = session?.user as any
const canCreate = hasPermission(user, PERMISSIONS.PRODUCT_CREATE)
const canDelete = hasPermission(user, PERMISSIONS.PRODUCT_DELETE)
const canViewCost = hasPermission(user, PERMISSIONS.PRODUCT_VIEW_PURCHASE_PRICE)
return (
<div>
{/* Show create button only if user has permission */}
{canCreate && (
<Button onClick={() => createProduct()}>
Create Product
</Button>
)}
{/* Show cost column only if user can view it */}
<table>
<thead>
<tr>
<th>Name</th>
<th>Price</th>
{canViewCost && <th>Cost</th>}
<th>Actions</th>
</tr>
</thead>
<tbody>
{products.map(product => (
<tr key={product.id}>
<td>{product.name}</td>
<td>{product.price}</td>
{canViewCost && <td>{product.cost}</td>}
<td>
{canDelete && (
<Button variant="destructive" onClick={() => deleteProduct(product.id)}>
Delete
</Button>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
)
}
Pattern 3: Custom Hook for Permissions
typescript
// hooks/usePermissions.ts
import { useSession } from 'next-auth/react'
import { hasPermission, hasAnyPermission, hasRole, isSuperAdmin, PERMISSIONS } from '@/lib/rbac'
export function usePermissions() {
const { data: session } = useSession()
const user = session?.user as any
return {
user,
can: (permission: string) => hasPermission(user, permission),
canAny: (permissions: string[]) => hasAnyPermission(user, permissions),
hasRole: (role: string) => hasRole(user, role),
isSuperAdmin: () => isSuperAdmin(user),
}
}
// Usage in components:
export default function MyComponent() {
const { can, hasRole, isSuperAdmin } = usePermissions()
if (isSuperAdmin()) {
return <AdminPanel />
}
return (
<div>
{can(PERMISSIONS.PRODUCT_CREATE) && <CreateButton />}
{hasRole('Manager') && <ManagerTools />}
</div>
)
}
Pattern 4: Multi-Level Permission Check
typescript
// Check different permission levels
const user = session.user as any
// Level 1: Can user view any sales?
const canViewSales = hasAnyPermission(user, [
PERMISSIONS.SALE_VIEW,
PERMISSIONS.SALE_VIEW_OWN
])
// Level 2: Can user view ALL sales or only their own?
const canViewAllSales = hasPermission(user, PERMISSIONS.SALE_VIEW)
const canViewOwnSales = hasPermission(user, PERMISSIONS.SALE_VIEW_OWN)
// Apply filter based on permission level
const where: any = {
businessId: parseInt(user.businessId),
deletedAt: null,
}
if (canViewOwnSales && !canViewAllSales) {
// Restrict to user's own sales
where.createdBy = parseInt(user.id)
}
const sales = await prisma.sale.findMany({ where })
Pattern 5: Role-Based Logic
typescript
import { hasAnyRole } from '@/lib/rbac'
const user = session.user as any
if (hasAnyRole(user, ['Super Admin', 'Branch Manager'])) {
// Allow access to sensitive reports
const financialReport = await generateFinancialReport()
}
// Check specific role
if (hasRole(user, 'Cashier')) {
// Cashier-specific logic
// e.g., auto-open shift on login
}
Field-Level Security
Control what data users can see:
typescript
const user = session.user as any
const canViewCost = hasPermission(user, PERMISSIONS.PRODUCT_VIEW_PURCHASE_PRICE)
const canViewProfit = hasPermission(user, PERMISSIONS.PRODUCT_VIEW_PROFIT_MARGIN)
const canViewSupplier = hasPermission(user, PERMISSIONS.PRODUCT_VIEW_SUPPLIER)
const product = await prisma.product.findUnique({
where: { id: productId },
select: {
id: true,
name: true,
sku: true,
sellingPrice: true,
// Conditionally include sensitive fields
purchasePrice: canViewCost,
profitMargin: canViewProfit,
supplier: canViewSupplier ? {
select: {
id: true,
name: true,
contactPerson: true,
}
} : false,
}
})
Location-Based Access Control
typescript
const user = session.user as any
// Get user's accessible locations
const accessibleLocations = user.locationIds || []
// Filter data by accessible locations
const products = await prisma.variationLocationDetails.findMany({
where: {
locationId: {
in: accessibleLocations // Only locations user can access
},
product: {
businessId: parseInt(user.businessId)
}
}
})
Super Admin Special Handling
Super Admin bypasses all permission checks:
typescript
import { isSuperAdmin } from '@/lib/rbac'
const user = session.user as any
// Super Admin can access everything
if (isSuperAdmin(user)) {
// Skip permission checks
// Return all data without filters
return await prisma.sale.findMany({
where: { businessId: parseInt(user.businessId) }
})
}
// Regular users need permission check
if (!hasPermission(user, PERMISSIONS.SALE_VIEW)) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
}
Best Practices
✅ DO:
- •Check permissions in API routes - Never trust client-side checks alone
- •Use appropriate permission level - Don't give more access than needed
- •Check permissions before showing UI - Hide buttons users can't use
- •Filter data by permission level - Show only what user can see
- •Use field-level security - Hide sensitive fields from unauthorized users
- •Respect location access - Filter by
locationIds - •Handle Super Admin - Give them full access
- •Use the helper functions - Don't write custom permission logic
❌ DON'T:
- •Don't skip permission checks - Even for "internal" APIs
- •Don't check permissions only on client - Always validate server-side
- •Don't hardcode role names - Use the role checking functions
- •Don't forget Super Admin - They should always pass checks
- •Don't mix permissions and roles - Check permissions, not roles when possible
- •Don't expose sensitive data - Check field-level permissions
Common Permission Patterns
Pattern: CRUD Permissions
typescript
// View - Read access PERMISSIONS.PRODUCT_VIEW // Create - Write new records PERMISSIONS.PRODUCT_CREATE // Update - Modify existing records PERMISSIONS.PRODUCT_UPDATE // Delete - Remove records PERMISSIONS.PRODUCT_DELETE
Pattern: Field-Level Permissions
typescript
// Base permission to see the entity PERMISSIONS.PRODUCT_VIEW // Additional permissions to see sensitive fields PERMISSIONS.PRODUCT_VIEW_PURCHASE_PRICE // See cost PERMISSIONS.PRODUCT_VIEW_PROFIT_MARGIN // See profit PERMISSIONS.PRODUCT_VIEW_SUPPLIER // See supplier
Pattern: Scope Permissions
typescript
// View all records PERMISSIONS.SALE_VIEW // View only own records PERMISSIONS.SALE_VIEW_OWN
Pattern: Action Permissions
typescript
// Workflow actions PERMISSIONS.PURCHASE_APPROVE PERMISSIONS.SALE_VOID PERMISSIONS.SALE_REFUND PERMISSIONS.SHIFT_CLOSE
Troubleshooting
User doesn't have expected permission
- •Check if user's role includes the permission
- •Check if user has the permission directly assigned
- •Verify Super Admin gets all permissions automatically
- •Check session is properly loading permissions (in
auth.ts)
Permission check not working
- •Verify import:
import { hasPermission, PERMISSIONS } from '@/lib/rbac' - •Check user object exists:
const user = session.user as any - •Verify permission constant is correct:
PERMISSIONS.PRODUCT_CREATE - •Check Super Admin special case is handled
UI shows but API blocks
Client-side and server-side permission checks are BOTH required:
- •Client-side: Hide UI elements (UX improvement)
- •Server-side: Enforce security (CRITICAL)
File Locations
- •RBAC Library:
/src/lib/rbac.ts(2600 lines of permissions) - •Auth Configuration:
/src/lib/auth.ts(permission loading) - •Permission Hook:
/src/hooks/usePermissions.ts(client-side)
Summary
This skill ensures:
- •✅ Proper permission checking throughout the app
- •✅ Role-based access control
- •✅ Field-level security for sensitive data
- •✅ Location-based access control
- •✅ Super Admin special handling
- •✅ Consistent permission patterns
Remember: Always check permissions server-side in API routes. Client-side checks are for UX only!