# Permission System Guide
This guide explains how to use the permission-driven UI system in the AP-Cash Frontend application.
## Overview
The permission system is built on top of Stytch B2B authentication and provides:
- Role-based access control (RBAC)
- Permission-based UI rendering
- Wildcard permission support (`resource:*`)
- Client and server-side guards
## Architecture
### 1. Stytch Integration
- Roles are stored in the Stytch member object at `member.roles[]`
- Each role is an object: `{ role_id: string, sources: [...] }`
- We extract `role_id` values (e.g., "member", "approver", "admin")
- Roles are mapped to permissions in our application
- Backend manages role definitions and assignments
### 2. Permission Flow
```
Stytch Session → Roles → Permissions → UI Components
```
## Files Structure
```
lib/auth/
├── permissions.ts # Permission constants & role mappings
├── permission-utils.ts # Permission check utilities
└── stytch/ # Stytch integration
lib/hooks/
└── use-permissions.ts # React hook for permissions
components/auth/
├── can.tsx # Inline permission wrapper
└── permission-gate.tsx # Page-level permission guard
middleware.ts # Route protection
```
## Usage Examples
### 1. Page-Level Protection
Protect entire pages using `PermissionGate`:
```tsx
// app/dashboard/approvals/page.tsx
import { PermissionGate } from "@/components/auth/permission-gate";
import { PERMISSIONS } from "@/lib/auth/permissions";
export default function ApprovalsPage() {
return (
);
}
```
### 2. Conditional UI Rendering
Show/hide UI elements using the `Can` component:
```tsx
import { Can } from "@/components/auth/can";
import { PERMISSIONS } from "@/lib/auth/permissions";
function InvoiceActions() {
return (
<>
{/* Single permission */}
{/* Multiple permissions (ANY - OR logic) */}
{/* Multiple permissions (ALL - AND logic) */}
{/* With fallback */}
}
>
>
);
}
```
### 3. Using the Hook
Access permissions programmatically:
```tsx
import { usePermissions } from "@/lib/hooks/use-permissions";
import { PERMISSIONS } from "@/lib/auth/permissions";
function MyComponent() {
const {
hasPermission,
hasAnyPermission,
roles,
permissions,
isAuthenticated
} = usePermissions();
// Check single permission
const canCreate = hasPermission(PERMISSIONS.INVOICE_CREATE);
// Check multiple permissions
const canViewOrEdit = hasAnyPermission([
PERMISSIONS.INVOICE_VIEW,
PERMISSIONS.INVOICE_CREATE
]);
// Use in logic
const handleAction = () => {
if (!hasPermission(PERMISSIONS.APPROVAL_APPROVE)) {
toast.error("You don't have permission to approve invoices");
return;
}
// ... perform action
};
return
User has {permissions.length} permissions
;
}
```
### 4. Navigation Filtering
Filter navigation items by permissions:
```tsx
import { usePermissions } from "@/lib/hooks/use-permissions";
import { PERMISSIONS } from "@/lib/auth/permissions";
const navigation = [
{
name: "Invoices",
href: "/dashboard/invoices",
permission: PERMISSIONS.INVOICE_VIEW,
},
{
name: "Approvals",
href: "/dashboard/approvals",
permission: PERMISSIONS.APPROVAL_VIEW,
},
];
function Sidebar() {
const { hasPermission } = usePermissions();
const visibleNav = navigation.filter(item =>
!item.permission || hasPermission(item.permission)
);
return (
);
}
```
## Available Permissions
### Invoice Management
- `invoice:create` — Upload and create new invoice records
- `invoice:view` — View invoice details
- `invoice:delete` — Delete invoices from the system (admin only)
### Duplicate Handling
- `duplicate:view` — View duplicate detection results
- `duplicate:resolve` — Resolve duplicate flags (admin only)
### Approval Workflow
- `approval:view` — View pending approvals and history
- `approval:approve` — Approve or reject invoices in the workflow
### Payment Optimization
- `payment_optimization:schedule` — Schedule payment runs (admin only)
- `payment_optimization:export` — Export payment files (admin only)
- `payment_optimization:execute` — Execute or reschedule payments (admin only)
### Audit Trail
- `audit:view` — View audit log timeline and summaries
### Organization Management
- `org:view` — View organization settings and roster (admin only)
- `org:manage` — Manage organization settings and members (admin only)
## Role Permissions
### Member
- `invoice:create`
- `invoice:view`
- `duplicate:view`
### Approver
- All Member permissions
- `approval:view`
- `approval:approve`
- `duplicate:view`
### Admin
- All permissions listed in this guide, including organization management, payment optimization, and duplicate resolution
## Wildcard Permissions
The system supports wildcard permissions using `*`:
```tsx
// Grant all actions for a resource
const permissions = ['invoice:*'];
// This matches:
// - invoice:view
// - invoice:create
// - invoice:delete
// Check wildcard permission
hasPermission('invoice:create'); // true if user has 'invoice:*'
```
## Server-Side Protection
### Middleware (Route Protection)
The middleware automatically protects dashboard routes:
```typescript
// middleware.ts
// Automatically protects:
// - /dashboard/*
// - /settings
// - /metrics
// - /audit
// Redirects to /auth if no session found
```
### API Client (401 Handling)
The API client automatically handles 401 responses:
```typescript
// On 401:
// 1. Clears session cookies
// 2. Redirects to /auth?returnTo={currentPath}
```
## Best Practices
### 1. Always Use Constants
```tsx
// ✅ Good
import { PERMISSIONS } from "@/lib/auth/permissions";
// ❌ Bad
```
### 2. Guard at Page Level
```tsx
// ✅ Good - Guard the entire page
export default function InvoicePage() {
return (
);
}
// ❌ Bad - Relying only on navigation filters
// Users can still access the URL directly
```
### 3. Show Helpful Fallbacks
```tsx
// ✅ Good
}
>
// ❌ Bad - Hiding without explanation
```
### 4. Backend Validation
**Always validate permissions on the backend!**
Client-side permission checks are for UX only. The backend must enforce permissions.
## Troubleshooting
### Permissions Not Working
1. Check Stytch session is initialized: `isInitialized === true`
2. Verify roles exist: `console.log(member?.roles)` - should be array of `{ role_id, sources }`
3. Check role_id values: `console.log(member?.roles?.map(r => r.role_id))`
4. Check role mapping in `lib/auth/permissions.ts` matches your role_id values
### Navigation Items Not Showing
1. Ensure permission constant is imported correctly
2. Check `usePermissions` hook is available (client component)
3. Verify navigation filter logic in sidebar
### 401 Redirect Loop
1. Check middleware excludes public routes
2. Verify session cookies are being set correctly
3. Check API endpoints don't return 401 for valid sessions
## Adding New Permissions
1. Add to `PERMISSIONS` constant:
```typescript
// lib/auth/permissions.ts
export const PERMISSIONS = {
// ... existing
NEW_RESOURCE_VIEW: 'new_resource:view',
NEW_RESOURCE_CREATE: 'new_resource:create',
} as const;
```
2. Update role mappings:
```typescript
export const ROLE_PERMISSIONS = {
admin: [
// ... existing admin permissions
PERMISSIONS.NEW_RESOURCE_VIEW,
PERMISSIONS.NEW_RESOURCE_CREATE,
],
approver: [
// ... approver permissions
PERMISSIONS.NEW_RESOURCE_VIEW,
],
member: [
// ... member permissions
PERMISSIONS.NEW_RESOURCE_VIEW,
],
};
```
3. Use in components:
```tsx
```
## Testing Permissions
```tsx
// Test with different roles
function PermissionDebug() {
const { roles, permissions, hasPermission } = usePermissions();
return (
Current Roles: {roles.join(', ')}
Permissions ({permissions.length}):
{permissions.map(p => - {p}
)}
Permission Checks:
Can create invoice: {hasPermission(PERMISSIONS.INVOICE_CREATE) ? '✅' : '❌'}
Can approve: {hasPermission(PERMISSIONS.APPROVAL_APPROVE) ? '✅' : '❌'}
);
}
```