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,200 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
class ScheduleValidationService {
// Validate a single schedule event
validateEvent(event, isEdit = false) {
const errors = [];
const now = new Date();
const startTime = new Date(event.startTime);
const endTime = new Date(event.endTime);
// 1. Check if dates are valid
if (isNaN(startTime.getTime())) {
errors.push({
field: 'startTime',
message: 'Start time is not a valid date',
code: 'INVALID_START_DATE'
});
}
if (isNaN(endTime.getTime())) {
errors.push({
field: 'endTime',
message: 'End time is not a valid date',
code: 'INVALID_END_DATE'
});
}
// If dates are invalid, return early
if (errors.length > 0) {
return errors;
}
// 2. Check if start time is in the future (with 5-minute grace period for edits)
const graceMinutes = isEdit ? 5 : 0;
const minimumStartTime = new Date(now.getTime() + (graceMinutes * 60 * 1000));
if (startTime < minimumStartTime) {
errors.push({
field: 'startTime',
message: isEdit
? 'Start time must be at least 5 minutes in the future for edits'
: 'Start time must be in the future',
code: 'START_TIME_IN_PAST'
});
}
// 3. Check if end time is after start time
if (endTime <= startTime) {
errors.push({
field: 'endTime',
message: 'End time must be after start time',
code: 'END_BEFORE_START'
});
}
// 4. Check minimum event duration (5 minutes)
const durationMinutes = (endTime.getTime() - startTime.getTime()) / (1000 * 60);
if (durationMinutes < 5) {
errors.push({
field: 'endTime',
message: 'Event must be at least 5 minutes long',
code: 'DURATION_TOO_SHORT'
});
}
// 5. Check maximum event duration (24 hours)
if (durationMinutes > (24 * 60)) {
errors.push({
field: 'endTime',
message: 'Event cannot be longer than 24 hours',
code: 'DURATION_TOO_LONG'
});
}
// 6. Check if end time is in the future
if (endTime < now) {
errors.push({
field: 'endTime',
message: 'End time must be in the future',
code: 'END_TIME_IN_PAST'
});
}
// 7. Validate required fields
if (!event.title || event.title.trim().length === 0) {
errors.push({
field: 'title',
message: 'Event title is required',
code: 'TITLE_REQUIRED'
});
}
if (!event.location || event.location.trim().length === 0) {
errors.push({
field: 'location',
message: 'Event location is required',
code: 'LOCATION_REQUIRED'
});
}
if (!event.type || event.type.trim().length === 0) {
errors.push({
field: 'type',
message: 'Event type is required',
code: 'TYPE_REQUIRED'
});
}
// 8. Validate title length
if (event.title && event.title.length > 100) {
errors.push({
field: 'title',
message: 'Event title cannot exceed 100 characters',
code: 'TITLE_TOO_LONG'
});
}
// 9. Validate location length
if (event.location && event.location.length > 200) {
errors.push({
field: 'location',
message: 'Event location cannot exceed 200 characters',
code: 'LOCATION_TOO_LONG'
});
}
// 10. Check for reasonable scheduling (not more than 2 years in the future)
const twoYearsFromNow = new Date();
twoYearsFromNow.setFullYear(twoYearsFromNow.getFullYear() + 2);
if (startTime > twoYearsFromNow) {
errors.push({
field: 'startTime',
message: 'Event cannot be scheduled more than 2 years in the future',
code: 'START_TIME_TOO_FAR'
});
}
// 11. Check for business hours validation (optional warning)
const startHour = startTime.getHours();
const endHour = endTime.getHours();
if (startHour < 6 || startHour > 23) {
// This is a warning, not an error - we'll add it but with a different severity
errors.push({
field: 'startTime',
message: 'Event starts outside typical business hours (6 AM - 11 PM)',
code: 'OUTSIDE_BUSINESS_HOURS'
});
}
return errors;
}
// Validate multiple events for conflicts and logical sequencing
validateEventSequence(events) {
const errors = [];
// Sort events by start time
const sortedEvents = events
.map((event, index) => ({ ...event, originalIndex: index }))
.sort((a, b) => new Date(a.startTime).getTime() - new Date(b.startTime).getTime());
// Check for overlapping events
for (let i = 0; i < sortedEvents.length - 1; i++) {
const currentEvent = sortedEvents[i];
const nextEvent = sortedEvents[i + 1];
const currentEnd = new Date(currentEvent.endTime);
const nextStart = new Date(nextEvent.startTime);
if (currentEnd > nextStart) {
errors.push({
field: 'schedule',
message: `Event "${currentEvent.title}" overlaps with "${nextEvent.title}"`,
code: 'EVENTS_OVERLAP'
});
}
}
return errors;
}
// Get user-friendly error messages
getErrorSummary(errors) {
if (errors.length === 0)
return '';
const errorMessages = errors.map(error => error.message);
if (errors.length === 1) {
return errorMessages[0];
}
return `Multiple validation errors:\n${errorMessages.join('\n• ')}`;
}
// Check if errors are warnings vs critical errors
isCriticalError(error) {
const warningCodes = ['OUTSIDE_BUSINESS_HOURS'];
return !warningCodes.includes(error.code);
}
// Separate critical errors from warnings
categorizeErrors(errors) {
const critical = [];
const warnings = [];
errors.forEach(error => {
if (this.isCriticalError(error)) {
critical.push(error);
}
else {
warnings.push(error);
}
});
return { critical, warnings };
}
// Validate time format and suggest corrections
validateTimeFormat(timeString) {
const date = new Date(timeString);
if (isNaN(date.getTime())) {
return {
isValid: false,
suggestion: 'Please use format: YYYY-MM-DDTHH:MM (e.g., 2025-07-01T14:30)'
};
}
return { isValid: true };
}
}
exports.default = new ScheduleValidationService();
//# sourceMappingURL=scheduleValidationService.js.map