Major Enhancement: NestJS Migration + CASL Authorization + Error Handling
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

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>
This commit is contained in:
2026-01-31 08:50:25 +01:00
parent 8ace1ab2c1
commit 868f7efc23
351 changed files with 44997 additions and 6276 deletions

View File

@@ -0,0 +1,137 @@
-- CreateEnum
CREATE TYPE "Role" AS ENUM ('ADMINISTRATOR', 'COORDINATOR', 'DRIVER');
-- CreateEnum
CREATE TYPE "Department" AS ENUM ('OFFICE_OF_DEVELOPMENT', 'ADMIN');
-- CreateEnum
CREATE TYPE "ArrivalMode" AS ENUM ('FLIGHT', 'SELF_DRIVING');
-- CreateEnum
CREATE TYPE "EventType" AS ENUM ('TRANSPORT', 'MEETING', 'EVENT', 'MEAL', 'ACCOMMODATION');
-- CreateEnum
CREATE TYPE "EventStatus" AS ENUM ('SCHEDULED', 'IN_PROGRESS', 'COMPLETED', 'CANCELLED');
-- CreateTable
CREATE TABLE "users" (
"id" TEXT NOT NULL,
"auth0Id" TEXT NOT NULL,
"email" TEXT NOT NULL,
"name" TEXT,
"picture" TEXT,
"role" "Role" NOT NULL DEFAULT 'COORDINATOR',
"isApproved" BOOLEAN NOT NULL DEFAULT false,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"deletedAt" TIMESTAMP(3),
CONSTRAINT "users_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "vips" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"organization" TEXT,
"department" "Department" NOT NULL,
"arrivalMode" "ArrivalMode" NOT NULL,
"expectedArrival" TIMESTAMP(3),
"airportPickup" BOOLEAN NOT NULL DEFAULT false,
"venueTransport" BOOLEAN NOT NULL DEFAULT false,
"notes" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"deletedAt" TIMESTAMP(3),
CONSTRAINT "vips_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "flights" (
"id" TEXT NOT NULL,
"vipId" TEXT NOT NULL,
"flightNumber" TEXT NOT NULL,
"flightDate" TIMESTAMP(3) NOT NULL,
"segment" INTEGER NOT NULL DEFAULT 1,
"departureAirport" TEXT NOT NULL,
"arrivalAirport" TEXT NOT NULL,
"scheduledDeparture" TIMESTAMP(3),
"scheduledArrival" TIMESTAMP(3),
"actualDeparture" TIMESTAMP(3),
"actualArrival" TIMESTAMP(3),
"status" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "flights_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "drivers" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"phone" TEXT NOT NULL,
"department" "Department",
"userId" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"deletedAt" TIMESTAMP(3),
CONSTRAINT "drivers_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "schedule_events" (
"id" TEXT NOT NULL,
"vipId" TEXT NOT NULL,
"title" TEXT NOT NULL,
"location" TEXT,
"startTime" TIMESTAMP(3) NOT NULL,
"endTime" TIMESTAMP(3) NOT NULL,
"description" TEXT,
"type" "EventType" NOT NULL DEFAULT 'TRANSPORT',
"status" "EventStatus" NOT NULL DEFAULT 'SCHEDULED',
"driverId" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"deletedAt" TIMESTAMP(3),
CONSTRAINT "schedule_events_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "users_auth0Id_key" ON "users"("auth0Id");
-- CreateIndex
CREATE UNIQUE INDEX "users_email_key" ON "users"("email");
-- CreateIndex
CREATE INDEX "flights_vipId_idx" ON "flights"("vipId");
-- CreateIndex
CREATE INDEX "flights_flightNumber_flightDate_idx" ON "flights"("flightNumber", "flightDate");
-- CreateIndex
CREATE UNIQUE INDEX "drivers_userId_key" ON "drivers"("userId");
-- CreateIndex
CREATE INDEX "schedule_events_vipId_idx" ON "schedule_events"("vipId");
-- CreateIndex
CREATE INDEX "schedule_events_driverId_idx" ON "schedule_events"("driverId");
-- CreateIndex
CREATE INDEX "schedule_events_startTime_endTime_idx" ON "schedule_events"("startTime", "endTime");
-- AddForeignKey
ALTER TABLE "flights" ADD CONSTRAINT "flights_vipId_fkey" FOREIGN KEY ("vipId") REFERENCES "vips"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "drivers" ADD CONSTRAINT "drivers_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "schedule_events" ADD CONSTRAINT "schedule_events_vipId_fkey" FOREIGN KEY ("vipId") REFERENCES "vips"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "schedule_events" ADD CONSTRAINT "schedule_events_driverId_fkey" FOREIGN KEY ("driverId") REFERENCES "drivers"("id") ON DELETE SET NULL ON UPDATE CASCADE;

View File

@@ -0,0 +1,50 @@
-- CreateEnum
CREATE TYPE "VehicleType" AS ENUM ('VAN', 'SUV', 'SEDAN', 'BUS', 'GOLF_CART', 'TRUCK');
-- CreateEnum
CREATE TYPE "VehicleStatus" AS ENUM ('AVAILABLE', 'IN_USE', 'MAINTENANCE', 'RESERVED');
-- AlterTable
ALTER TABLE "drivers" ADD COLUMN "isAvailable" BOOLEAN NOT NULL DEFAULT true,
ADD COLUMN "shiftEndTime" TIMESTAMP(3),
ADD COLUMN "shiftStartTime" TIMESTAMP(3);
-- AlterTable
ALTER TABLE "schedule_events" ADD COLUMN "actualEndTime" TIMESTAMP(3),
ADD COLUMN "actualStartTime" TIMESTAMP(3),
ADD COLUMN "dropoffLocation" TEXT,
ADD COLUMN "notes" TEXT,
ADD COLUMN "pickupLocation" TEXT,
ADD COLUMN "vehicleId" TEXT;
-- CreateTable
CREATE TABLE "vehicles" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"type" "VehicleType" NOT NULL DEFAULT 'VAN',
"licensePlate" TEXT,
"seatCapacity" INTEGER NOT NULL,
"status" "VehicleStatus" NOT NULL DEFAULT 'AVAILABLE',
"currentDriverId" TEXT,
"notes" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"deletedAt" TIMESTAMP(3),
CONSTRAINT "vehicles_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "vehicles_currentDriverId_key" ON "vehicles"("currentDriverId");
-- CreateIndex
CREATE INDEX "schedule_events_vehicleId_idx" ON "schedule_events"("vehicleId");
-- CreateIndex
CREATE INDEX "schedule_events_status_idx" ON "schedule_events"("status");
-- AddForeignKey
ALTER TABLE "vehicles" ADD CONSTRAINT "vehicles_currentDriverId_fkey" FOREIGN KEY ("currentDriverId") REFERENCES "drivers"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "schedule_events" ADD CONSTRAINT "schedule_events_vehicleId_fkey" FOREIGN KEY ("vehicleId") REFERENCES "vehicles"("id") ON DELETE SET NULL ON UPDATE CASCADE;

View File

@@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
provider = "postgresql"

View File

@@ -0,0 +1,227 @@
// VIP Coordinator - Prisma Schema
// This is your database schema (source of truth)
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
// ============================================
// User Management
// ============================================
model User {
id String @id @default(uuid())
auth0Id String @unique // Auth0 sub claim
email String @unique
name String?
picture String?
role Role @default(COORDINATOR)
isApproved Boolean @default(false)
driver Driver? // Optional linked driver account
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime? // Soft delete
@@map("users")
}
enum Role {
ADMINISTRATOR
COORDINATOR
DRIVER
}
// ============================================
// VIP Management
// ============================================
model VIP {
id String @id @default(uuid())
name String
organization String?
department Department
arrivalMode ArrivalMode
expectedArrival DateTime? // For self-driving arrivals
airportPickup Boolean @default(false)
venueTransport Boolean @default(false)
notes String? @db.Text
flights Flight[]
events ScheduleEvent[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime? // Soft delete
@@map("vips")
}
enum Department {
OFFICE_OF_DEVELOPMENT
ADMIN
}
enum ArrivalMode {
FLIGHT
SELF_DRIVING
}
// ============================================
// Flight Tracking
// ============================================
model Flight {
id String @id @default(uuid())
vipId String
vip VIP @relation(fields: [vipId], references: [id], onDelete: Cascade)
flightNumber String
flightDate DateTime
segment Int @default(1) // For multi-segment itineraries
departureAirport String // IATA code (e.g., "JFK")
arrivalAirport String // IATA code (e.g., "LAX")
scheduledDeparture DateTime?
scheduledArrival DateTime?
actualDeparture DateTime?
actualArrival DateTime?
status String? // scheduled, delayed, landed, etc.
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("flights")
@@index([vipId])
@@index([flightNumber, flightDate])
}
// ============================================
// Driver Management
// ============================================
model Driver {
id String @id @default(uuid())
name String
phone String
department Department?
userId String? @unique
user User? @relation(fields: [userId], references: [id])
// Shift/Availability
shiftStartTime DateTime? // When driver's shift starts
shiftEndTime DateTime? // When driver's shift ends
isAvailable Boolean @default(true)
events ScheduleEvent[]
assignedVehicle Vehicle? @relation("AssignedDriver")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime? // Soft delete
@@map("drivers")
}
// ============================================
// Vehicle Management
// ============================================
model Vehicle {
id String @id @default(uuid())
name String // "Blue Van", "Suburban #3"
type VehicleType @default(VAN)
licensePlate String?
seatCapacity Int // Total seats (e.g., 8)
status VehicleStatus @default(AVAILABLE)
// Current assignment
currentDriverId String? @unique
currentDriver Driver? @relation("AssignedDriver", fields: [currentDriverId], references: [id])
// Relationships
events ScheduleEvent[]
notes String? @db.Text
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime? // Soft delete
@@map("vehicles")
}
enum VehicleType {
VAN // 7-15 seats
SUV // 5-8 seats
SEDAN // 4-5 seats
BUS // 15+ seats
GOLF_CART // 2-6 seats
TRUCK // For equipment/supplies
}
enum VehicleStatus {
AVAILABLE // Ready to use
IN_USE // Currently on a trip
MAINTENANCE // Out of service
RESERVED // Scheduled for upcoming trip
}
// ============================================
// Schedule & Event Management
// ============================================
model ScheduleEvent {
id String @id @default(uuid())
vipId String
vip VIP @relation(fields: [vipId], references: [id], onDelete: Cascade)
title String
// Location details
pickupLocation String?
dropoffLocation String?
location String? // For non-transport events
// Timing
startTime DateTime
endTime DateTime
actualStartTime DateTime?
actualEndTime DateTime?
description String? @db.Text
type EventType @default(TRANSPORT)
status EventStatus @default(SCHEDULED)
// Assignments
driverId String?
driver Driver? @relation(fields: [driverId], references: [id], onDelete: SetNull)
vehicleId String?
vehicle Vehicle? @relation(fields: [vehicleId], references: [id], onDelete: SetNull)
// Metadata
notes String? @db.Text
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime? // Soft delete
@@map("schedule_events")
@@index([vipId])
@@index([driverId])
@@index([vehicleId])
@@index([startTime, endTime])
@@index([status])
}
enum EventType {
TRANSPORT
MEETING
EVENT
MEAL
ACCOMMODATION
}
enum EventStatus {
SCHEDULED
IN_PROGRESS
COMPLETED
CANCELLED
}

165
backend/prisma/seed.ts Normal file
View File

@@ -0,0 +1,165 @@
import { PrismaClient, Role, Department, ArrivalMode, EventType, EventStatus } from '@prisma/client';
const prisma = new PrismaClient();
async function main() {
console.log('🌱 Seeding database...');
// Clean up existing data (careful in production!)
await prisma.scheduleEvent.deleteMany({});
await prisma.flight.deleteMany({});
await prisma.driver.deleteMany({});
await prisma.vIP.deleteMany({});
await prisma.user.deleteMany({});
console.log('✅ Cleared existing data');
// Create sample users
const admin = await prisma.user.create({
data: {
auth0Id: 'auth0|admin-sample-id',
email: 'admin@example.com',
name: 'Admin User',
role: Role.ADMINISTRATOR,
isApproved: true,
},
});
const coordinator = await prisma.user.create({
data: {
auth0Id: 'auth0|coordinator-sample-id',
email: 'coordinator@example.com',
name: 'Coordinator User',
role: Role.COORDINATOR,
isApproved: true,
},
});
console.log('✅ Created sample users');
// Create sample drivers
const driver1 = await prisma.driver.create({
data: {
name: 'John Smith',
phone: '+1 (555) 123-4567',
department: Department.OFFICE_OF_DEVELOPMENT,
},
});
const driver2 = await prisma.driver.create({
data: {
name: 'Jane Doe',
phone: '+1 (555) 987-6543',
department: Department.ADMIN,
},
});
console.log('✅ Created sample drivers');
// Create sample VIPs
const vip1 = await prisma.vIP.create({
data: {
name: 'Dr. Robert Johnson',
organization: 'Tech Corporation',
department: Department.OFFICE_OF_DEVELOPMENT,
arrivalMode: ArrivalMode.FLIGHT,
airportPickup: true,
venueTransport: true,
notes: 'Prefers window seat, dietary restriction: vegetarian',
flights: {
create: [
{
flightNumber: 'AA123',
flightDate: new Date('2026-02-15'),
segment: 1,
departureAirport: 'JFK',
arrivalAirport: 'LAX',
scheduledDeparture: new Date('2026-02-15T08:00:00'),
scheduledArrival: new Date('2026-02-15T11:30:00'),
status: 'scheduled',
},
],
},
},
});
const vip2 = await prisma.vIP.create({
data: {
name: 'Ms. Sarah Williams',
organization: 'Global Foundation',
department: Department.ADMIN,
arrivalMode: ArrivalMode.SELF_DRIVING,
expectedArrival: new Date('2026-02-16T14:00:00'),
airportPickup: false,
venueTransport: true,
notes: 'Bringing assistant',
},
});
console.log('✅ Created sample VIPs');
// Create sample events
await prisma.scheduleEvent.create({
data: {
vipId: vip1.id,
title: 'Airport Pickup',
location: 'LAX Terminal 4',
startTime: new Date('2026-02-15T11:30:00'),
endTime: new Date('2026-02-15T12:30:00'),
description: 'Pick up Dr. Johnson from LAX',
type: EventType.TRANSPORT,
status: EventStatus.SCHEDULED,
driverId: driver1.id,
},
});
await prisma.scheduleEvent.create({
data: {
vipId: vip1.id,
title: 'Welcome Dinner',
location: 'Grand Hotel Restaurant',
startTime: new Date('2026-02-15T19:00:00'),
endTime: new Date('2026-02-15T21:00:00'),
description: 'Welcome dinner with board members',
type: EventType.MEAL,
status: EventStatus.SCHEDULED,
driverId: driver2.id,
},
});
await prisma.scheduleEvent.create({
data: {
vipId: vip2.id,
title: 'Conference Transport',
location: 'Convention Center',
startTime: new Date('2026-02-16T14:30:00'),
endTime: new Date('2026-02-16T15:00:00'),
description: 'Transport to conference venue',
type: EventType.TRANSPORT,
status: EventStatus.SCHEDULED,
driverId: driver1.id,
},
});
console.log('✅ Created sample events');
console.log('\n🎉 Database seeded successfully!');
console.log('\nSample Users:');
console.log('- Admin: admin@example.com');
console.log('- Coordinator: coordinator@example.com');
console.log('\nSample VIPs:');
console.log('- Dr. Robert Johnson (Flight arrival)');
console.log('- Ms. Sarah Williams (Self-driving)');
console.log('\nSample Drivers:');
console.log('- John Smith');
console.log('- Jane Doe');
}
main()
.catch((e) => {
console.error('❌ Error seeding database:', e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});