docs: Organize documentation into structured folders
Some checks failed
CI/CD Pipeline / Backend Tests (push) Has been cancelled
CI/CD Pipeline / Frontend Tests (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (push) Has been cancelled
CI/CD Pipeline / Security Scan (push) Has been cancelled
CI/CD Pipeline / Deploy to Staging (push) Has been cancelled
CI/CD Pipeline / Deploy to Production (push) Has been cancelled
Some checks failed
CI/CD Pipeline / Backend Tests (push) Has been cancelled
CI/CD Pipeline / Frontend Tests (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (push) Has been cancelled
CI/CD Pipeline / Security Scan (push) Has been cancelled
CI/CD Pipeline / Deploy to Staging (push) Has been cancelled
CI/CD Pipeline / Deploy to Production (push) Has been cancelled
Organized documentation into cleaner structure: Root directory (user-facing): - README.md - Main documentation - CLAUDE.md - AI context (referenced by system) - QUICKSTART.md - Quick start guide docs/ (technical documentation): - CASL_AUTHORIZATION.md - Authorization guide - ERROR_HANDLING.md - Error handling patterns - REQUIREMENTS.md - Project requirements docs/deployment/ (production deployment): - HTTPS_SETUP.md - SSL/TLS setup - PRODUCTION_ENVIRONMENT_TEMPLATE.md - Env vars template - PRODUCTION_VERIFICATION_CHECKLIST.md - Deployment checklist Removed: - DOCKER_TROUBLESHOOTING.md - Outdated (referenced Google OAuth, old domain) Updated references: - Fixed links to moved files in CASL_AUTHORIZATION.md - Fixed links to moved files in ERROR_HANDLING.md - Removed reference to deleted BUILD_STATUS.md in QUICKSTART.md Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
650
docs/CASL_AUTHORIZATION.md
Normal file
650
docs/CASL_AUTHORIZATION.md
Normal file
@@ -0,0 +1,650 @@
|
||||
# CASL Authorization System
|
||||
|
||||
This document describes the CASL-based authorization system implemented in the VIP Coordinator application.
|
||||
|
||||
## Overview
|
||||
|
||||
CASL (pronounced "castle") is an isomorphic authorization library that makes it easy to manage permissions in both frontend and backend code. It allows us to define abilities once and reuse them across the entire application.
|
||||
|
||||
**Key Benefits:**
|
||||
- ✅ Type-safe permissions with TypeScript
|
||||
- ✅ Consistent authorization logic between frontend and backend
|
||||
- ✅ Declarative permission checks
|
||||
- ✅ Easy to extend and maintain
|
||||
- ✅ Supports complex conditional permissions
|
||||
|
||||
## Architecture
|
||||
|
||||
### Permissions Model
|
||||
|
||||
**Actions:** What can be done
|
||||
- `manage` - Special action that allows everything
|
||||
- `create` - Create new resources
|
||||
- `read` - View resources
|
||||
- `update` - Modify existing resources
|
||||
- `delete` - Remove resources
|
||||
- `approve` - Special: Approve user accounts
|
||||
- `update-status` - Special: Update event status (for drivers)
|
||||
|
||||
**Subjects:** What resources can be acted upon
|
||||
- `User` - User accounts
|
||||
- `VIP` - VIP profiles
|
||||
- `Driver` - Driver resources
|
||||
- `ScheduleEvent` - Schedule events
|
||||
- `Flight` - Flight information
|
||||
- `Vehicle` - Vehicle management
|
||||
- `all` - Special subject representing all resources
|
||||
|
||||
### Role-Based Permissions
|
||||
|
||||
| Action | Administrator | Coordinator | Driver |
|
||||
|--------|--------------|-------------|--------|
|
||||
| **Users** | | | |
|
||||
| Create | ✅ | ❌ | ❌ |
|
||||
| Read | ✅ | ❌ | ❌ |
|
||||
| Update | ✅ | ❌ | ❌ |
|
||||
| Delete | ✅ | ❌ | ❌ |
|
||||
| Approve | ✅ | ❌ | ❌ |
|
||||
| **VIPs** | | | |
|
||||
| Create | ✅ | ✅ | ❌ |
|
||||
| Read | ✅ | ✅ | ✅ |
|
||||
| Update | ✅ | ✅ | ❌ |
|
||||
| Delete | ✅ | ✅ | ❌ |
|
||||
| **Drivers** | | | |
|
||||
| Create | ✅ | ✅ | ❌ |
|
||||
| Read | ✅ | ✅ | ✅ |
|
||||
| Update | ✅ | ✅ | ❌ |
|
||||
| Delete | ✅ | ✅ | ❌ |
|
||||
| **Vehicles** | | | |
|
||||
| Create | ✅ | ✅ | ❌ |
|
||||
| Read | ✅ | ✅ | ✅ |
|
||||
| Update | ✅ | ✅ | ❌ |
|
||||
| Delete | ✅ | ✅ | ❌ |
|
||||
| **Schedule Events** | | | |
|
||||
| Create | ✅ | ✅ | ❌ |
|
||||
| Read | ✅ | ✅ | ✅ |
|
||||
| Update | ✅ | ✅ | ❌ |
|
||||
| Delete | ✅ | ✅ | ❌ |
|
||||
| UpdateStatus | ✅ | ✅ | ✅ (own events) |
|
||||
| **Flights** | | | |
|
||||
| Read/Manage | ✅ | ✅ | ❌ |
|
||||
|
||||
## Backend Implementation
|
||||
|
||||
### 1. Ability Factory
|
||||
|
||||
**Location:** `backend/src/auth/abilities/ability.factory.ts`
|
||||
|
||||
Defines all permissions based on user roles.
|
||||
|
||||
```typescript
|
||||
import { AbilityFactory, Action } from '../auth/abilities/ability.factory';
|
||||
|
||||
@Injectable()
|
||||
export class MyService {
|
||||
constructor(private abilityFactory: AbilityFactory) {}
|
||||
|
||||
async doSomething(user: User) {
|
||||
const ability = this.abilityFactory.defineAbilitiesFor(user);
|
||||
|
||||
if (ability.can(Action.Create, 'VIP')) {
|
||||
// User can create VIPs
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Abilities Guard
|
||||
|
||||
**Location:** `backend/src/auth/guards/abilities.guard.ts`
|
||||
|
||||
Guard that checks CASL abilities on routes.
|
||||
|
||||
```typescript
|
||||
import { UseGuards } from '@nestjs/common';
|
||||
import { AbilitiesGuard } from '../auth/guards/abilities.guard';
|
||||
|
||||
@Controller('vips')
|
||||
@UseGuards(JwtAuthGuard, AbilitiesGuard)
|
||||
export class VipsController {
|
||||
// Routes protected by AbilitiesGuard
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Permission Decorators
|
||||
|
||||
**Location:** `backend/src/auth/decorators/check-ability.decorator.ts`
|
||||
|
||||
Decorators to specify required permissions on routes.
|
||||
|
||||
#### Using Helper Decorators (Recommended)
|
||||
|
||||
```typescript
|
||||
import { CanCreate, CanRead, CanUpdate, CanDelete } from '../auth/decorators/check-ability.decorator';
|
||||
|
||||
@Post()
|
||||
@CanCreate('VIP')
|
||||
create(@Body() dto: CreateVIPDto) {
|
||||
return this.service.create(dto);
|
||||
}
|
||||
|
||||
@Get()
|
||||
@CanRead('VIP')
|
||||
findAll() {
|
||||
return this.service.findAll();
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
@CanUpdate('VIP')
|
||||
update(@Param('id') id: string, @Body() dto: UpdateVIPDto) {
|
||||
return this.service.update(id, dto);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@CanDelete('VIP')
|
||||
remove(@Param('id') id: string) {
|
||||
return this.service.remove(id);
|
||||
}
|
||||
```
|
||||
|
||||
#### Using CheckAbilities Decorator (For Custom Actions)
|
||||
|
||||
```typescript
|
||||
import { CheckAbilities } from '../auth/decorators/check-ability.decorator';
|
||||
import { Action } from '../auth/abilities/ability.factory';
|
||||
|
||||
@Patch(':id/approve')
|
||||
@CheckAbilities({ action: Action.Approve, subject: 'User' })
|
||||
approve(@Param('id') id: string, @Body() dto: ApproveUserDto) {
|
||||
return this.service.approve(id, dto);
|
||||
}
|
||||
```
|
||||
|
||||
#### Multiple Permissions (All Must Be Satisfied)
|
||||
|
||||
```typescript
|
||||
@Post('complex')
|
||||
@CheckAbilities(
|
||||
{ action: Action.Read, subject: 'VIP' },
|
||||
{ action: Action.Create, subject: 'ScheduleEvent' }
|
||||
)
|
||||
complexOperation() {
|
||||
// User must have BOTH permissions
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Controller Examples
|
||||
|
||||
#### VIPsController
|
||||
|
||||
```typescript
|
||||
import { Controller, UseGuards } from '@nestjs/common';
|
||||
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
|
||||
import { AbilitiesGuard } from '../auth/guards/abilities.guard';
|
||||
import { CanCreate, CanRead, CanUpdate, CanDelete } from '../auth/decorators/check-ability.decorator';
|
||||
|
||||
@Controller('vips')
|
||||
@UseGuards(JwtAuthGuard, AbilitiesGuard)
|
||||
export class VipsController {
|
||||
@Post()
|
||||
@CanCreate('VIP')
|
||||
create(@Body() dto: CreateVipDto) { }
|
||||
|
||||
@Get()
|
||||
@CanRead('VIP')
|
||||
findAll() { }
|
||||
|
||||
@Patch(':id')
|
||||
@CanUpdate('VIP')
|
||||
update(@Param('id') id: string, @Body() dto: UpdateVipDto) { }
|
||||
|
||||
@Delete(':id')
|
||||
@CanDelete('VIP')
|
||||
remove(@Param('id') id: string) { }
|
||||
}
|
||||
```
|
||||
|
||||
#### UsersController (Admin Only)
|
||||
|
||||
```typescript
|
||||
@Controller('users')
|
||||
@UseGuards(JwtAuthGuard, AbilitiesGuard)
|
||||
export class UsersController {
|
||||
@Get()
|
||||
@CanRead('User') // Only admins can read users
|
||||
findAll() { }
|
||||
|
||||
@Patch(':id/approve')
|
||||
@CheckAbilities({ action: Action.Approve, subject: 'User' })
|
||||
approve(@Param('id') id: string) { }
|
||||
}
|
||||
```
|
||||
|
||||
## Frontend Implementation
|
||||
|
||||
### 1. Ability Definitions
|
||||
|
||||
**Location:** `frontend/src/lib/abilities.ts`
|
||||
|
||||
Mirrors backend ability definitions for consistent permissions.
|
||||
|
||||
```typescript
|
||||
import { defineAbilitiesFor, Action } from '@/lib/abilities';
|
||||
|
||||
const user = { id: '1', role: 'COORDINATOR', isApproved: true };
|
||||
const ability = defineAbilitiesFor(user);
|
||||
|
||||
if (ability.can(Action.Create, 'VIP')) {
|
||||
// User can create VIPs
|
||||
}
|
||||
```
|
||||
|
||||
### 2. AbilityContext & Hooks
|
||||
|
||||
**Location:** `frontend/src/contexts/AbilityContext.tsx`
|
||||
|
||||
Provides CASL abilities throughout the React component tree.
|
||||
|
||||
#### useAbility Hook
|
||||
|
||||
```typescript
|
||||
import { useAbility } from '@/contexts/AbilityContext';
|
||||
import { Action } from '@/lib/abilities';
|
||||
|
||||
function MyComponent() {
|
||||
const ability = useAbility();
|
||||
|
||||
const canCreateVIP = ability.can(Action.Create, 'VIP');
|
||||
const canDeleteDriver = ability.can(Action.Delete, 'Driver');
|
||||
|
||||
return (
|
||||
<div>
|
||||
{canCreateVIP && <button>Add VIP</button>}
|
||||
{canDeleteDriver && <button>Delete Driver</button>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
#### Can Component (Declarative)
|
||||
|
||||
```typescript
|
||||
import { Can } from '@/contexts/AbilityContext';
|
||||
|
||||
function MyComponent() {
|
||||
return (
|
||||
<div>
|
||||
<Can I="create" a="VIP">
|
||||
<button>Add VIP</button>
|
||||
</Can>
|
||||
|
||||
<Can I="update" a="ScheduleEvent">
|
||||
<button>Edit Event</button>
|
||||
</Can>
|
||||
|
||||
<Can I="delete" a="Driver">
|
||||
<button>Delete Driver</button>
|
||||
</Can>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Layout Component Example
|
||||
|
||||
**Location:** `frontend/src/components/Layout.tsx`
|
||||
|
||||
Navigation filtered by permissions:
|
||||
|
||||
```typescript
|
||||
import { useAbility } from '@/contexts/AbilityContext';
|
||||
import { Action } from '@/lib/abilities';
|
||||
|
||||
export function Layout() {
|
||||
const ability = useAbility();
|
||||
|
||||
const allNavigation = [
|
||||
{ name: 'Dashboard', href: '/dashboard', alwaysShow: true },
|
||||
{ name: 'VIPs', href: '/vips', requireRead: 'VIP' as const },
|
||||
{ name: 'Users', href: '/users', requireRead: 'User' as const },
|
||||
];
|
||||
|
||||
const navigation = allNavigation.filter((item) => {
|
||||
if (item.alwaysShow) return true;
|
||||
if (item.requireRead) {
|
||||
return ability.can(Action.Read, item.requireRead);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
return (
|
||||
<nav>
|
||||
{navigation.map(item => (
|
||||
<Link key={item.name} to={item.href}>{item.name}</Link>
|
||||
))}
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Component Examples
|
||||
|
||||
#### Conditional Button Rendering
|
||||
|
||||
```typescript
|
||||
function VIPList() {
|
||||
const ability = useAbility();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>VIPs</h1>
|
||||
|
||||
{ability.can(Action.Create, 'VIP') && (
|
||||
<button onClick={handleCreate}>Add VIP</button>
|
||||
)}
|
||||
|
||||
{vips.map(vip => (
|
||||
<div key={vip.id}>
|
||||
{vip.name}
|
||||
|
||||
{ability.can(Action.Update, 'VIP') && (
|
||||
<button onClick={() => handleEdit(vip.id)}>Edit</button>
|
||||
)}
|
||||
|
||||
{ability.can(Action.Delete, 'VIP') && (
|
||||
<button onClick={() => handleDelete(vip.id)}>Delete</button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
#### Using Can Component
|
||||
|
||||
```typescript
|
||||
import { Can } from '@/contexts/AbilityContext';
|
||||
|
||||
function DriverDashboard() {
|
||||
return (
|
||||
<div>
|
||||
<h1>Driver Dashboard</h1>
|
||||
|
||||
<Can I="read" a="ScheduleEvent">
|
||||
<section>
|
||||
<h2>My Schedule</h2>
|
||||
<EventList />
|
||||
</section>
|
||||
</Can>
|
||||
|
||||
<Can I="update-status" a="ScheduleEvent">
|
||||
<button>Update Event Status</button>
|
||||
</Can>
|
||||
|
||||
<Can not I="read" a="Flight">
|
||||
<p>You don't have access to flight information.</p>
|
||||
</Can>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Migration Guide
|
||||
|
||||
### Backend Migration
|
||||
|
||||
#### Before (Old RolesGuard Pattern)
|
||||
|
||||
```typescript
|
||||
import { UseGuards } from '@nestjs/common';
|
||||
import { RolesGuard } from '../auth/guards/roles.guard';
|
||||
import { Roles } from '../auth/decorators/roles.decorator';
|
||||
import { Role } from '@prisma/client';
|
||||
|
||||
@Controller('vips')
|
||||
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||
export class VipsController {
|
||||
@Post()
|
||||
@Roles(Role.ADMINISTRATOR, Role.COORDINATOR)
|
||||
create(@Body() dto: CreateVIPDto) { }
|
||||
|
||||
@Get()
|
||||
@Roles(Role.ADMINISTRATOR, Role.COORDINATOR, Role.DRIVER)
|
||||
findAll() { }
|
||||
}
|
||||
```
|
||||
|
||||
#### After (New CASL Pattern)
|
||||
|
||||
```typescript
|
||||
import { UseGuards } from '@nestjs/common';
|
||||
import { AbilitiesGuard } from '../auth/guards/abilities.guard';
|
||||
import { CanCreate, CanRead } from '../auth/decorators/check-ability.decorator';
|
||||
|
||||
@Controller('vips')
|
||||
@UseGuards(JwtAuthGuard, AbilitiesGuard)
|
||||
export class VipsController {
|
||||
@Post()
|
||||
@CanCreate('VIP')
|
||||
create(@Body() dto: CreateVIPDto) { }
|
||||
|
||||
@Get()
|
||||
@CanRead('VIP')
|
||||
findAll() { }
|
||||
}
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- ✅ More semantic (describes WHAT, not WHO)
|
||||
- ✅ Type-safe with autocomplete
|
||||
- ✅ Easier to understand intent
|
||||
- ✅ Supports complex conditions
|
||||
|
||||
### Frontend Migration
|
||||
|
||||
#### Before (Direct Role Checks)
|
||||
|
||||
```typescript
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
|
||||
function MyComponent() {
|
||||
const { backendUser } = useAuth();
|
||||
const isAdmin = backendUser?.role === 'ADMINISTRATOR';
|
||||
const canManageVIPs = isAdmin || backendUser?.role === 'COORDINATOR';
|
||||
|
||||
return (
|
||||
<div>
|
||||
{canManageVIPs && <button>Add VIP</button>}
|
||||
{isAdmin && <Link to="/users">Users</Link>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
#### After (CASL Abilities)
|
||||
|
||||
```typescript
|
||||
import { useAbility } from '@/contexts/AbilityContext';
|
||||
import { Action } from '@/lib/abilities';
|
||||
|
||||
function MyComponent() {
|
||||
const ability = useAbility();
|
||||
|
||||
return (
|
||||
<div>
|
||||
{ability.can(Action.Create, 'VIP') && <button>Add VIP</button>}
|
||||
{ability.can(Action.Read, 'User') && <Link to="/users">Users</Link>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Or using Can component:**
|
||||
|
||||
```typescript
|
||||
import { Can } from '@/contexts/AbilityContext';
|
||||
|
||||
function MyComponent() {
|
||||
return (
|
||||
<div>
|
||||
<Can I="create" a="VIP">
|
||||
<button>Add VIP</button>
|
||||
</Can>
|
||||
|
||||
<Can I="read" a="User">
|
||||
<Link to="/users">Users</Link>
|
||||
</Can>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Adding New Permissions
|
||||
|
||||
### 1. Define New Action (If Needed)
|
||||
|
||||
**Backend:** `backend/src/auth/abilities/ability.factory.ts`
|
||||
**Frontend:** `frontend/src/lib/abilities.ts`
|
||||
|
||||
```typescript
|
||||
export enum Action {
|
||||
// ... existing actions
|
||||
Export = 'export', // New action
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Update Ability Definitions
|
||||
|
||||
**Backend:** `backend/src/auth/abilities/ability.factory.ts`
|
||||
|
||||
```typescript
|
||||
defineAbilitiesFor(user: User): AppAbility {
|
||||
const { can, cannot, build } = new AbilityBuilder<AppAbility>(/* ... */);
|
||||
|
||||
if (user.role === Role.ADMINISTRATOR) {
|
||||
can(Action.Manage, 'all');
|
||||
can(Action.Export, 'all'); // Admins can export anything
|
||||
} else if (user.role === Role.COORDINATOR) {
|
||||
can(Action.Export, 'VIP'); // Coordinators can only export VIPs
|
||||
}
|
||||
|
||||
return build();
|
||||
}
|
||||
```
|
||||
|
||||
**Frontend:** `frontend/src/lib/abilities.ts` (same pattern)
|
||||
|
||||
### 3. Use in Controllers
|
||||
|
||||
```typescript
|
||||
import { CheckAbilities } from '../auth/decorators/check-ability.decorator';
|
||||
import { Action } from '../auth/abilities/ability.factory';
|
||||
|
||||
@Get('export')
|
||||
@CheckAbilities({ action: Action.Export, subject: 'VIP' })
|
||||
export() {
|
||||
return this.service.exportToCSV();
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Use in Components
|
||||
|
||||
```typescript
|
||||
import { Can } from '@/contexts/AbilityContext';
|
||||
|
||||
function VIPList() {
|
||||
return (
|
||||
<div>
|
||||
<Can I="export" a="VIP">
|
||||
<button onClick={handleExport}>Export to CSV</button>
|
||||
</Can>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### ✅ DO
|
||||
|
||||
```typescript
|
||||
// Use semantic ability checks
|
||||
if (ability.can(Action.Create, 'VIP')) { }
|
||||
|
||||
// Use Can component for declarative rendering
|
||||
<Can I="update" a="Driver">
|
||||
<EditButton />
|
||||
</Can>
|
||||
|
||||
// Group related permissions in decorators
|
||||
@CheckAbilities(
|
||||
{ action: Action.Read, subject: 'VIP' },
|
||||
{ action: Action.Read, subject: 'Driver' }
|
||||
)
|
||||
|
||||
// Define abilities based on resources, not roles
|
||||
can(Action.Update, 'VIP') // Good
|
||||
can(Action.Manage, 'all') // For admins only
|
||||
```
|
||||
|
||||
### ❌ DON'T
|
||||
|
||||
```typescript
|
||||
// Don't check roles directly (use abilities instead)
|
||||
if (user.role === 'ADMINISTRATOR') { } // Bad
|
||||
|
||||
// Don't mix role checks and ability checks
|
||||
if (isAdmin || ability.can(Action.Create, 'VIP')) { } // Confusing
|
||||
|
||||
// Don't create overly specific actions
|
||||
Action.CreateVIPForJamboree // Too specific
|
||||
Action.Create // Better
|
||||
|
||||
// Don't forget to check both frontend and backend
|
||||
// Backend enforces security, frontend improves UX
|
||||
```
|
||||
|
||||
## Debugging
|
||||
|
||||
### Check User Abilities
|
||||
|
||||
**Backend:**
|
||||
```typescript
|
||||
const ability = this.abilityFactory.defineAbilitiesFor(user);
|
||||
console.log('Can create VIP?', ability.can(Action.Create, 'VIP'));
|
||||
console.log('Can manage all?', ability.can(Action.Manage, 'all'));
|
||||
```
|
||||
|
||||
**Frontend:**
|
||||
```typescript
|
||||
const ability = useAbility();
|
||||
console.log('Can create VIP?', ability.can(Action.Create, 'VIP'));
|
||||
console.log('User abilities:', ability.rules);
|
||||
```
|
||||
|
||||
### Common Issues
|
||||
|
||||
**"User does not have required permissions" error:**
|
||||
1. Check user role in database
|
||||
2. Verify ability definitions match frontend/backend
|
||||
3. Ensure AbilitiesGuard is applied to controller
|
||||
4. Check if decorator is correctly specified
|
||||
|
||||
**Navigation items not showing:**
|
||||
1. Verify AbilityProvider wraps the app
|
||||
2. Check ability.can() returns true for expected permissions
|
||||
3. Ensure user is authenticated and role is set
|
||||
|
||||
**Tests failing:**
|
||||
1. Mock AbilityFactory in tests
|
||||
2. Provide test abilities in test setup
|
||||
3. Use `@casl/ability` test utilities
|
||||
|
||||
---
|
||||
|
||||
**Last Updated:** 2026-01-25
|
||||
**See also:**
|
||||
- [CLAUDE.md](../CLAUDE.md) - General project documentation
|
||||
- [ERROR_HANDLING.md](./ERROR_HANDLING.md) - Error handling guide
|
||||
- [CASL Documentation](https://casl.js.org/) - Official CASL docs
|
||||
Reference in New Issue
Block a user