Complete rewrite from Express to NestJS with enterprise-grade features: ## Backend Improvements - Migrated from Express to NestJS 11.0.1 with TypeScript - Implemented Prisma ORM 7.3.0 for type-safe database access - Added CASL authorization system replacing role-based guards - Created global exception filters with structured logging - Implemented Auth0 JWT authentication with Passport.js - Added vehicle management with conflict detection - Enhanced event scheduling with driver/vehicle assignment - Comprehensive error handling and logging ## Frontend Improvements - Upgraded to React 19.2.0 with Vite 7.2.4 - Implemented CASL-based permission system - Added AbilityContext for declarative permissions - Created ErrorHandler utility for consistent error messages - Enhanced API client with request/response logging - Added War Room (Command Center) dashboard - Created VIP Schedule view with complete itineraries - Implemented Vehicle Management UI - Added mock data generators for testing (288 events across 20 VIPs) ## New Features - Vehicle fleet management (types, capacity, status tracking) - Complete 3-day Jamboree schedule generation - Individual VIP schedule pages with PDF export (planned) - Real-time War Room dashboard with auto-refresh - Permission-based navigation filtering - First user auto-approval as administrator ## Documentation - Created CASL_AUTHORIZATION.md (comprehensive guide) - Created ERROR_HANDLING.md (error handling patterns) - Updated CLAUDE.md with new architecture - Added migration guides and best practices ## Technical Debt Resolved - Removed custom authentication in favor of Auth0 - Replaced role checks with CASL abilities - Standardized error responses across API - Implemented proper TypeScript typing - Added comprehensive logging Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
15 KiB
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 everythingcreate- Create new resourcesread- View resourcesupdate- Modify existing resourcesdelete- Remove resourcesapprove- Special: Approve user accountsupdate-status- Special: Update event status (for drivers)
Subjects: What resources can be acted upon
User- User accountsVIP- VIP profilesDriver- Driver resourcesScheduleEvent- Schedule eventsFlight- Flight informationVehicle- Vehicle managementall- 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.
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.
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)
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)
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)
@Post('complex')
@CheckAbilities(
{ action: Action.Read, subject: 'VIP' },
{ action: Action.Create, subject: 'ScheduleEvent' }
)
complexOperation() {
// User must have BOTH permissions
}
4. Controller Examples
VIPsController
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)
@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.
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
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)
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:
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
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
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)
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)
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)
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)
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:
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
export enum Action {
// ... existing actions
Export = 'export', // New action
}
2. Update Ability Definitions
Backend: backend/src/auth/abilities/ability.factory.ts
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
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
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
// 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
// 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:
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:
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:
- Check user role in database
- Verify ability definitions match frontend/backend
- Ensure AbilitiesGuard is applied to controller
- Check if decorator is correctly specified
Navigation items not showing:
- Verify AbilityProvider wraps the app
- Check ability.can() returns true for expected permissions
- Ensure user is authenticated and role is set
Tests failing:
- Mock AbilityFactory in tests
- Provide test abilities in test setup
- Use
@casl/abilitytest utilities
Last Updated: 2026-01-25 See also:
- CLAUDE.md - General project documentation
- ERROR_HANDLING.md - Error handling guide
- CASL Documentation - Official CASL docs