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,459 @@
import React, { useState } from 'react';
interface Flight {
flightNumber: string;
flightDate: string;
segment: number;
validated?: boolean;
validationData?: any;
}
interface VipFormData {
name: string;
organization: string;
department: 'Office of Development' | 'Admin';
transportMode: 'flight' | 'self-driving';
flights?: Flight[];
expectedArrival?: string;
needsAirportPickup?: boolean;
needsVenueTransport: boolean;
notes: string;
}
interface VipFormProps {
onSubmit: (vipData: VipFormData) => void;
onCancel: () => void;
}
const VipForm: React.FC<VipFormProps> = ({ onSubmit, onCancel }) => {
const [formData, setFormData] = useState<VipFormData>({
name: '',
organization: '',
department: 'Office of Development',
transportMode: 'flight',
flights: [{ flightNumber: '', flightDate: '', segment: 1 }],
expectedArrival: '',
needsAirportPickup: true,
needsVenueTransport: true,
notes: ''
});
const [flightValidating, setFlightValidating] = useState<{ [key: number]: boolean }>({});
const [flightErrors, setFlightErrors] = useState<{ [key: number]: string }>({});
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
// Only include flights with flight numbers
const validFlights = formData.flights?.filter(f => f.flightNumber) || [];
onSubmit({
...formData,
flights: validFlights.length > 0 ? validFlights : undefined
});
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
const { name, value, type } = e.target;
if (type === 'checkbox') {
const checked = (e.target as HTMLInputElement).checked;
setFormData(prev => ({
...prev,
[name]: checked
}));
} else {
setFormData(prev => ({
...prev,
[name]: value
}));
}
};
const handleTransportModeChange = (mode: 'flight' | 'self-driving') => {
setFormData(prev => ({
...prev,
transportMode: mode,
flights: mode === 'flight' ? [{ flightNumber: '', flightDate: '', segment: 1 }] : undefined,
expectedArrival: mode === 'self-driving' ? prev.expectedArrival : '',
needsAirportPickup: mode === 'flight' ? true : false
}));
// Clear flight errors when switching away from flight mode
if (mode !== 'flight') {
setFlightErrors({});
}
};
const handleFlightChange = (index: number, field: 'flightNumber' | 'flightDate', value: string) => {
setFormData(prev => ({
...prev,
flights: prev.flights?.map((flight, i) =>
i === index ? { ...flight, [field]: value, validated: false } : flight
) || []
}));
// Clear validation for this flight when it changes
setFlightErrors(prev => ({ ...prev, [index]: '' }));
};
const addConnectingFlight = () => {
const currentFlights = formData.flights || [];
if (currentFlights.length < 3) {
setFormData(prev => ({
...prev,
flights: [...currentFlights, {
flightNumber: '',
flightDate: currentFlights[currentFlights.length - 1]?.flightDate || '',
segment: currentFlights.length + 1
}]
}));
}
};
const removeConnectingFlight = (index: number) => {
setFormData(prev => ({
...prev,
flights: prev.flights?.filter((_, i) => i !== index).map((flight, i) => ({
...flight,
segment: i + 1
})) || []
}));
// Clear errors for removed flight
setFlightErrors(prev => {
const newErrors = { ...prev };
delete newErrors[index];
return newErrors;
});
};
const validateFlight = async (index: number) => {
const flight = formData.flights?.[index];
if (!flight || !flight.flightNumber || !flight.flightDate) {
setFlightErrors(prev => ({ ...prev, [index]: 'Please enter flight number and date' }));
return;
}
setFlightValidating(prev => ({ ...prev, [index]: true }));
setFlightErrors(prev => ({ ...prev, [index]: '' }));
try {
const url = `/api/flights/${flight.flightNumber}?date=${flight.flightDate}`;
const response = await fetch(url);
if (response.ok) {
const data = await response.json();
// Update flight with validation data
setFormData(prev => ({
...prev,
flights: prev.flights?.map((f, i) =>
i === index ? { ...f, validated: true, validationData: data } : f
) || []
}));
setFlightErrors(prev => ({ ...prev, [index]: '' }));
} else {
const errorData = await response.json();
setFlightErrors(prev => ({
...prev,
[index]: errorData.error || 'Invalid flight number'
}));
}
} catch (error) {
setFlightErrors(prev => ({
...prev,
[index]: 'Error validating flight'
}));
} finally {
setFlightValidating(prev => ({ ...prev, [index]: false }));
}
};
return (
<div className="modal-overlay">
<div className="modal-content">
{/* Modal Header */}
<div className="modal-header">
<h2 className="text-2xl font-bold text-slate-800">
Add New VIP
</h2>
<p className="text-slate-600 mt-2">Enter VIP details and travel information</p>
</div>
{/* Modal Body */}
<div className="modal-body">
<form onSubmit={handleSubmit} className="space-y-8">
{/* Basic Information Section */}
<div className="form-section">
<div className="form-section-header">
<h3 className="form-section-title">Basic Information</h3>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="form-group">
<label htmlFor="name" className="form-label">Full Name *</label>
<input
type="text"
id="name"
name="name"
value={formData.name}
onChange={handleChange}
className="form-input"
placeholder="Enter full name"
required
/>
</div>
<div className="form-group">
<label htmlFor="organization" className="form-label">Organization *</label>
<input
type="text"
id="organization"
name="organization"
value={formData.organization}
onChange={handleChange}
className="form-input"
placeholder="Enter organization name"
required
/>
</div>
</div>
<div className="form-group">
<label htmlFor="department" className="form-label">Department *</label>
<select
id="department"
name="department"
value={formData.department}
onChange={handleChange}
className="form-select"
required
>
<option value="Office of Development">Office of Development</option>
<option value="Admin">Admin</option>
</select>
</div>
</div>
{/* Transportation Section */}
<div className="form-section">
<div className="form-section-header">
<h3 className="form-section-title">Transportation Details</h3>
</div>
<div className="form-group">
<label className="form-label">How are you arriving? *</label>
<div className="radio-group">
<div
className={`radio-option ${formData.transportMode === 'flight' ? 'selected' : ''}`}
onClick={() => handleTransportModeChange('flight')}
>
<input
type="radio"
name="transportMode"
value="flight"
checked={formData.transportMode === 'flight'}
onChange={() => handleTransportModeChange('flight')}
className="form-radio mr-3"
/>
<span className="font-medium">Arriving by Flight</span>
</div>
<div
className={`radio-option ${formData.transportMode === 'self-driving' ? 'selected' : ''}`}
onClick={() => handleTransportModeChange('self-driving')}
>
<input
type="radio"
name="transportMode"
value="self-driving"
checked={formData.transportMode === 'self-driving'}
onChange={() => handleTransportModeChange('self-driving')}
className="form-radio mr-3"
/>
<span className="font-medium">Self-Driving</span>
</div>
</div>
</div>
{/* Flight Mode Fields */}
{formData.transportMode === 'flight' && formData.flights && (
<div className="space-y-6">
{formData.flights.map((flight, index) => (
<div key={index} className="bg-white border-2 border-blue-200 rounded-xl p-6 shadow-sm">
<div className="flex justify-between items-center mb-4">
<h4 className="text-lg font-bold text-slate-800">
{index === 0 ? 'Primary Flight' : `Connecting Flight ${index}`}
</h4>
{index > 0 && (
<button
type="button"
onClick={() => removeConnectingFlight(index)}
className="text-red-500 hover:text-red-700 font-medium text-sm bg-red-50 hover:bg-red-100 px-3 py-1 rounded-lg transition-colors duration-200"
>
Remove
</button>
)}
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
<div className="form-group">
<label htmlFor={`flightNumber-${index}`} className="form-label">Flight Number *</label>
<input
type="text"
id={`flightNumber-${index}`}
value={flight.flightNumber}
onChange={(e) => handleFlightChange(index, 'flightNumber', e.target.value)}
className="form-input"
placeholder="e.g., AA123"
required={index === 0}
/>
</div>
<div className="form-group">
<label htmlFor={`flightDate-${index}`} className="form-label">Flight Date *</label>
<input
type="date"
id={`flightDate-${index}`}
value={flight.flightDate}
onChange={(e) => handleFlightChange(index, 'flightDate', e.target.value)}
className="form-input"
required={index === 0}
min={new Date().toISOString().split('T')[0]}
/>
</div>
</div>
<button
type="button"
className="btn btn-secondary w-full"
onClick={() => validateFlight(index)}
disabled={flightValidating[index] || !flight.flightNumber || !flight.flightDate}
>
{flightValidating[index] ? (
<>
<span className="animate-spin inline-block w-4 h-4 border-2 border-white border-t-transparent rounded-full mr-2"></span>
Validating Flight...
</>
) : (
<>Validate Flight</>
)}
</button>
{/* Flight Validation Results */}
{flightErrors[index] && (
<div className="mt-4 bg-red-50 border border-red-200 rounded-lg p-4">
<div className="text-red-700 font-medium">{flightErrors[index]}</div>
</div>
)}
{flight.validated && flight.validationData && (
<div className="mt-4 bg-green-50 border border-green-200 rounded-lg p-4">
<div className="text-green-700 font-medium mb-2">
Valid: {flight.validationData.airline || 'Flight'} - {flight.validationData.departure?.airport} {flight.validationData.arrival?.airport}
</div>
{flight.validationData.flightDate !== flight.flightDate && (
<div className="text-sm text-green-600">
Live tracking starts 4 hours before departure on {new Date(flight.flightDate).toLocaleDateString()}
</div>
)}
</div>
)}
</div>
))}
{formData.flights.length < 3 && (
<button
type="button"
className="btn btn-secondary w-full"
onClick={addConnectingFlight}
>
Add Connecting Flight
</button>
)}
<div className="checkbox-option checked">
<input
type="checkbox"
name="needsAirportPickup"
checked={formData.needsAirportPickup || false}
onChange={handleChange}
className="form-checkbox mr-3"
/>
<span className="font-medium">Needs Airport Pickup (from final destination)</span>
</div>
</div>
)}
{/* Self-Driving Mode Fields */}
{formData.transportMode === 'self-driving' && (
<div className="form-group">
<label htmlFor="expectedArrival" className="form-label">Expected Arrival *</label>
<input
type="datetime-local"
id="expectedArrival"
name="expectedArrival"
value={formData.expectedArrival}
onChange={handleChange}
className="form-input"
required
/>
</div>
)}
{/* Universal Transportation Option */}
<div className={`checkbox-option ${formData.needsVenueTransport ? 'checked' : ''}`}>
<input
type="checkbox"
name="needsVenueTransport"
checked={formData.needsVenueTransport}
onChange={handleChange}
className="form-checkbox mr-3"
/>
<div>
<span className="font-medium">Needs Transportation Between Venues</span>
<div className="text-sm text-slate-500 mt-1">
Check this if the VIP needs rides between different event locations
</div>
</div>
</div>
</div>
{/* Additional Information Section */}
<div className="form-section">
<div className="form-section-header">
<h3 className="form-section-title">Additional Information</h3>
</div>
<div className="form-group">
<label htmlFor="notes" className="form-label">Additional Notes</label>
<textarea
id="notes"
name="notes"
value={formData.notes}
onChange={handleChange}
rows={4}
className="form-textarea"
placeholder="Special requirements, dietary restrictions, accessibility needs, etc."
/>
</div>
</div>
{/* Form Actions */}
<div className="form-actions">
<button type="button" className="btn btn-secondary" onClick={onCancel}>
Cancel
</button>
<button type="submit" className="btn btn-primary">
Add VIP
</button>
</div>
</form>
</div>
</div>
</div>
);
};
export default VipForm;