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
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:
765
backend-old-20260125/dist/index.original.js
vendored
Normal file
765
backend-old-20260125/dist/index.original.js
vendored
Normal file
@@ -0,0 +1,765 @@
|
||||
"use strict";
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||
}
|
||||
Object.defineProperty(o, k2, desc);
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || (function () {
|
||||
var ownKeys = function(o) {
|
||||
ownKeys = Object.getOwnPropertyNames || function (o) {
|
||||
var ar = [];
|
||||
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
||||
return ar;
|
||||
};
|
||||
return ownKeys(o);
|
||||
};
|
||||
return function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const express_1 = __importDefault(require("express"));
|
||||
const dotenv_1 = __importDefault(require("dotenv"));
|
||||
const cors_1 = __importDefault(require("cors"));
|
||||
const simpleAuth_1 = __importStar(require("./routes/simpleAuth"));
|
||||
const flightService_1 = __importDefault(require("./services/flightService"));
|
||||
const driverConflictService_1 = __importDefault(require("./services/driverConflictService"));
|
||||
const scheduleValidationService_1 = __importDefault(require("./services/scheduleValidationService"));
|
||||
const flightTrackingScheduler_1 = __importDefault(require("./services/flightTrackingScheduler"));
|
||||
const enhancedDataService_1 = __importDefault(require("./services/enhancedDataService"));
|
||||
const databaseService_1 = __importDefault(require("./services/databaseService"));
|
||||
const jwtKeyManager_1 = __importDefault(require("./services/jwtKeyManager")); // Initialize JWT Key Manager
|
||||
const errorHandler_1 = require("./middleware/errorHandler");
|
||||
const logger_1 = require("./middleware/logger");
|
||||
const validation_1 = require("./middleware/validation");
|
||||
const schemas_1 = require("./types/schemas");
|
||||
dotenv_1.default.config();
|
||||
const app = (0, express_1.default)();
|
||||
const port = process.env.PORT ? parseInt(process.env.PORT) : 3000;
|
||||
// Middleware
|
||||
app.use((0, cors_1.default)({
|
||||
origin: [
|
||||
process.env.FRONTEND_URL || 'http://localhost:5173',
|
||||
'http://localhost:5173',
|
||||
'http://localhost:3000',
|
||||
'http://localhost', // Frontend Docker container (local testing)
|
||||
'https://bsa.madeamess.online' // Production frontend domain (where users access the site)
|
||||
],
|
||||
credentials: true
|
||||
}));
|
||||
app.use(express_1.default.json());
|
||||
app.use(express_1.default.urlencoded({ extended: true }));
|
||||
// Add request logging
|
||||
app.use(logger_1.requestLogger);
|
||||
// Simple JWT-based authentication - no passport needed
|
||||
// Authentication routes
|
||||
app.use('/auth', simpleAuth_1.default);
|
||||
// Temporary admin bypass route (remove after setup)
|
||||
app.get('/admin-bypass', (req, res) => {
|
||||
res.redirect(`${process.env.FRONTEND_URL || 'http://localhost:5173'}/admin?bypass=true`);
|
||||
});
|
||||
// Serve static files from public directory
|
||||
app.use(express_1.default.static('public'));
|
||||
// Enhanced health check endpoint with authentication system status
|
||||
app.get('/api/health', (0, errorHandler_1.asyncHandler)(async (req, res) => {
|
||||
const timestamp = new Date().toISOString();
|
||||
// Check JWT Key Manager status
|
||||
const jwtStatus = jwtKeyManager_1.default.getStatus();
|
||||
// Check environment variables
|
||||
const envCheck = {
|
||||
google_client_id: !!process.env.GOOGLE_CLIENT_ID,
|
||||
google_client_secret: !!process.env.GOOGLE_CLIENT_SECRET,
|
||||
google_redirect_uri: !!process.env.GOOGLE_REDIRECT_URI,
|
||||
frontend_url: !!process.env.FRONTEND_URL,
|
||||
database_url: !!process.env.DATABASE_URL,
|
||||
admin_password: !!process.env.ADMIN_PASSWORD
|
||||
};
|
||||
// Check database connectivity
|
||||
let databaseStatus = 'unknown';
|
||||
let userCount = 0;
|
||||
try {
|
||||
userCount = await databaseService_1.default.getUserCount();
|
||||
databaseStatus = 'connected';
|
||||
}
|
||||
catch (dbError) {
|
||||
databaseStatus = 'disconnected';
|
||||
console.error('Health check - Database error:', dbError);
|
||||
}
|
||||
// Overall system health
|
||||
const isHealthy = databaseStatus === 'connected' &&
|
||||
jwtStatus.hasCurrentKey &&
|
||||
envCheck.google_client_id &&
|
||||
envCheck.google_client_secret;
|
||||
const healthData = {
|
||||
status: isHealthy ? 'OK' : 'DEGRADED',
|
||||
timestamp,
|
||||
version: '1.0.0',
|
||||
environment: process.env.NODE_ENV || 'development',
|
||||
services: {
|
||||
database: {
|
||||
status: databaseStatus,
|
||||
user_count: databaseStatus === 'connected' ? userCount : null
|
||||
},
|
||||
authentication: {
|
||||
jwt_key_manager: jwtStatus,
|
||||
oauth_configured: envCheck.google_client_id && envCheck.google_client_secret,
|
||||
environment_variables: envCheck
|
||||
}
|
||||
},
|
||||
uptime: process.uptime(),
|
||||
memory: process.memoryUsage()
|
||||
};
|
||||
// Log health check for monitoring
|
||||
console.log(`🏥 Health Check [${timestamp}]:`, {
|
||||
status: healthData.status,
|
||||
database: databaseStatus,
|
||||
jwt_keys: jwtStatus.hasCurrentKey,
|
||||
oauth: envCheck.google_client_id && envCheck.google_client_secret
|
||||
});
|
||||
res.status(isHealthy ? 200 : 503).json(healthData);
|
||||
}));
|
||||
// Data is now persisted using dataService - no more in-memory storage!
|
||||
// Admin password - MUST be set via environment variable in production
|
||||
const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || 'CHANGE_ME_ADMIN_PASSWORD';
|
||||
// Initialize flight tracking scheduler
|
||||
const flightTracker = new flightTrackingScheduler_1.default(flightService_1.default);
|
||||
// VIP routes (protected)
|
||||
app.post('/api/vips', simpleAuth_1.requireAuth, (0, simpleAuth_1.requireRole)(['coordinator', 'administrator']), (0, validation_1.validate)(schemas_1.createVipSchema), (0, errorHandler_1.asyncHandler)(async (req, res) => {
|
||||
// Create a new VIP - data is already validated
|
||||
const { name, organization, department, // New: Office of Development or Admin
|
||||
transportMode, flightNumber, // Legacy single flight
|
||||
flights, // New: array of flights
|
||||
expectedArrival, needsAirportPickup, needsVenueTransport, notes } = req.body;
|
||||
const newVip = {
|
||||
id: Date.now().toString(), // Simple ID generation
|
||||
name,
|
||||
organization,
|
||||
department: department || 'Office of Development', // Default to Office of Development
|
||||
transportMode: transportMode || 'flight',
|
||||
// Support both legacy single flight and new multiple flights
|
||||
flightNumber: transportMode === 'flight' && !flights ? flightNumber : undefined,
|
||||
flights: transportMode === 'flight' && flights ? flights : undefined,
|
||||
expectedArrival: transportMode === 'self-driving' ? expectedArrival : undefined,
|
||||
arrivalTime: transportMode === 'flight' ? undefined : expectedArrival, // Legacy field for flight arrivals
|
||||
needsAirportPickup: transportMode === 'flight' ? (needsAirportPickup !== false) : false,
|
||||
needsVenueTransport: needsVenueTransport !== false, // Default to true
|
||||
assignedDriverIds: [],
|
||||
notes: notes || '',
|
||||
schedule: []
|
||||
};
|
||||
const savedVip = await enhancedDataService_1.default.addVip(newVip);
|
||||
// Add flights to tracking scheduler if applicable
|
||||
if (savedVip.transportMode === 'flight' && savedVip.flights && savedVip.flights.length > 0) {
|
||||
flightTracker.addVipFlights(savedVip.id, savedVip.name, savedVip.flights);
|
||||
}
|
||||
res.status(201).json(savedVip);
|
||||
}));
|
||||
app.get('/api/vips', simpleAuth_1.requireAuth, async (req, res) => {
|
||||
try {
|
||||
// Fetch all VIPs
|
||||
const vips = await enhancedDataService_1.default.getVips();
|
||||
res.json(vips);
|
||||
}
|
||||
catch (error) {
|
||||
res.status(500).json({ error: 'Failed to fetch VIPs' });
|
||||
}
|
||||
});
|
||||
app.put('/api/vips/:id', simpleAuth_1.requireAuth, (0, simpleAuth_1.requireRole)(['coordinator', 'administrator']), (0, validation_1.validate)(schemas_1.updateVipSchema), (0, errorHandler_1.asyncHandler)(async (req, res) => {
|
||||
// Update a VIP - data is already validated
|
||||
const { id } = req.params;
|
||||
const { name, organization, department, // New: Office of Development or Admin
|
||||
transportMode, flightNumber, // Legacy single flight
|
||||
flights, // New: array of flights
|
||||
expectedArrival, needsAirportPickup, needsVenueTransport, notes } = req.body;
|
||||
const updatedVip = {
|
||||
name,
|
||||
organization,
|
||||
department: department || 'Office of Development',
|
||||
transportMode: transportMode || 'flight',
|
||||
// Support both legacy single flight and new multiple flights
|
||||
flights: transportMode === 'flight' && flights ? flights : undefined,
|
||||
expectedArrival: transportMode === 'self-driving' ? expectedArrival : undefined,
|
||||
needsAirportPickup: transportMode === 'flight' ? (needsAirportPickup !== false) : false,
|
||||
needsVenueTransport: needsVenueTransport !== false,
|
||||
notes: notes || ''
|
||||
};
|
||||
const savedVip = await enhancedDataService_1.default.updateVip(id, updatedVip);
|
||||
if (!savedVip) {
|
||||
return res.status(404).json({ error: 'VIP not found' });
|
||||
}
|
||||
// Update flight tracking if needed
|
||||
if (savedVip.transportMode === 'flight') {
|
||||
// Remove old flights
|
||||
flightTracker.removeVipFlights(id);
|
||||
// Add new flights if any
|
||||
if (savedVip.flights && savedVip.flights.length > 0) {
|
||||
flightTracker.addVipFlights(savedVip.id, savedVip.name, savedVip.flights);
|
||||
}
|
||||
}
|
||||
res.json(savedVip);
|
||||
}));
|
||||
app.delete('/api/vips/:id', simpleAuth_1.requireAuth, (0, simpleAuth_1.requireRole)(['coordinator', 'administrator']), async (req, res) => {
|
||||
// Delete a VIP
|
||||
const { id } = req.params;
|
||||
try {
|
||||
const deletedVip = await enhancedDataService_1.default.deleteVip(id);
|
||||
if (!deletedVip) {
|
||||
return res.status(404).json({ error: 'VIP not found' });
|
||||
}
|
||||
// Remove from flight tracking
|
||||
flightTracker.removeVipFlights(id);
|
||||
res.json({ message: 'VIP deleted successfully', vip: deletedVip });
|
||||
}
|
||||
catch (error) {
|
||||
res.status(500).json({ error: 'Failed to delete VIP' });
|
||||
}
|
||||
});
|
||||
// Driver routes (protected)
|
||||
app.post('/api/drivers', simpleAuth_1.requireAuth, (0, simpleAuth_1.requireRole)(['coordinator', 'administrator']), (0, validation_1.validate)(schemas_1.createDriverSchema), (0, errorHandler_1.asyncHandler)(async (req, res) => {
|
||||
// Create a new driver - data is already validated
|
||||
const { name, phone, email, vehicleInfo, status } = req.body;
|
||||
const newDriver = {
|
||||
id: Date.now().toString(),
|
||||
name,
|
||||
phone,
|
||||
email,
|
||||
vehicleInfo,
|
||||
status: status || 'available',
|
||||
department: 'Office of Development', // Default to Office of Development
|
||||
currentLocation: { lat: 0, lng: 0 },
|
||||
assignedVipIds: []
|
||||
};
|
||||
const savedDriver = await enhancedDataService_1.default.addDriver(newDriver);
|
||||
res.status(201).json(savedDriver);
|
||||
}));
|
||||
app.get('/api/drivers', simpleAuth_1.requireAuth, async (req, res) => {
|
||||
try {
|
||||
// Fetch all drivers
|
||||
const drivers = await enhancedDataService_1.default.getDrivers();
|
||||
res.json(drivers);
|
||||
}
|
||||
catch (error) {
|
||||
res.status(500).json({ error: 'Failed to fetch drivers' });
|
||||
}
|
||||
});
|
||||
app.put('/api/drivers/:id', simpleAuth_1.requireAuth, (0, simpleAuth_1.requireRole)(['coordinator', 'administrator']), async (req, res) => {
|
||||
// Update a driver
|
||||
const { id } = req.params;
|
||||
const { name, phone, currentLocation, department } = req.body;
|
||||
try {
|
||||
const updatedDriver = {
|
||||
name,
|
||||
phone,
|
||||
department: department || 'Office of Development',
|
||||
currentLocation: currentLocation || { lat: 0, lng: 0 }
|
||||
};
|
||||
const savedDriver = await enhancedDataService_1.default.updateDriver(id, updatedDriver);
|
||||
if (!savedDriver) {
|
||||
return res.status(404).json({ error: 'Driver not found' });
|
||||
}
|
||||
res.json(savedDriver);
|
||||
}
|
||||
catch (error) {
|
||||
res.status(500).json({ error: 'Failed to update driver' });
|
||||
}
|
||||
});
|
||||
app.delete('/api/drivers/:id', simpleAuth_1.requireAuth, (0, simpleAuth_1.requireRole)(['coordinator', 'administrator']), async (req, res) => {
|
||||
// Delete a driver
|
||||
const { id } = req.params;
|
||||
try {
|
||||
const deletedDriver = await enhancedDataService_1.default.deleteDriver(id);
|
||||
if (!deletedDriver) {
|
||||
return res.status(404).json({ error: 'Driver not found' });
|
||||
}
|
||||
res.json({ message: 'Driver deleted successfully', driver: deletedDriver });
|
||||
}
|
||||
catch (error) {
|
||||
res.status(500).json({ error: 'Failed to delete driver' });
|
||||
}
|
||||
});
|
||||
// Enhanced flight tracking routes with date specificity
|
||||
app.get('/api/flights/:flightNumber', async (req, res) => {
|
||||
try {
|
||||
const { flightNumber } = req.params;
|
||||
const { date, departureAirport, arrivalAirport } = req.query;
|
||||
// Default to today if no date provided
|
||||
const flightDate = date || new Date().toISOString().split('T')[0];
|
||||
const flightData = await flightService_1.default.getFlightInfo({
|
||||
flightNumber,
|
||||
date: flightDate,
|
||||
departureAirport: departureAirport,
|
||||
arrivalAirport: arrivalAirport
|
||||
});
|
||||
if (flightData) {
|
||||
// Always return flight data for validation, even if date doesn't match
|
||||
res.json(flightData);
|
||||
}
|
||||
else {
|
||||
// Only return 404 if the flight number itself is invalid
|
||||
res.status(404).json({ error: 'Invalid flight number - this flight does not exist' });
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
res.status(500).json({ error: 'Failed to fetch flight data' });
|
||||
}
|
||||
});
|
||||
// Start periodic updates for a flight
|
||||
app.post('/api/flights/:flightNumber/track', async (req, res) => {
|
||||
try {
|
||||
const { flightNumber } = req.params;
|
||||
const { date, intervalMinutes = 5 } = req.body;
|
||||
if (!date) {
|
||||
return res.status(400).json({ error: 'Flight date is required' });
|
||||
}
|
||||
flightService_1.default.startPeriodicUpdates({
|
||||
flightNumber,
|
||||
date
|
||||
}, intervalMinutes);
|
||||
res.json({ message: `Started tracking ${flightNumber} on ${date}` });
|
||||
}
|
||||
catch (error) {
|
||||
res.status(500).json({ error: 'Failed to start flight tracking' });
|
||||
}
|
||||
});
|
||||
// Stop periodic updates for a flight
|
||||
app.delete('/api/flights/:flightNumber/track', async (req, res) => {
|
||||
try {
|
||||
const { flightNumber } = req.params;
|
||||
const { date } = req.query;
|
||||
if (!date) {
|
||||
return res.status(400).json({ error: 'Flight date is required' });
|
||||
}
|
||||
const key = `${flightNumber}_${date}`;
|
||||
flightService_1.default.stopPeriodicUpdates(key);
|
||||
res.json({ message: `Stopped tracking ${flightNumber} on ${date}` });
|
||||
}
|
||||
catch (error) {
|
||||
res.status(500).json({ error: 'Failed to stop flight tracking' });
|
||||
}
|
||||
});
|
||||
app.post('/api/flights/batch', async (req, res) => {
|
||||
try {
|
||||
const { flights } = req.body;
|
||||
if (!Array.isArray(flights)) {
|
||||
return res.status(400).json({ error: 'flights must be an array of {flightNumber, date} objects' });
|
||||
}
|
||||
// Validate flight objects
|
||||
for (const flight of flights) {
|
||||
if (!flight.flightNumber || !flight.date) {
|
||||
return res.status(400).json({ error: 'Each flight must have flightNumber and date' });
|
||||
}
|
||||
}
|
||||
const flightData = await flightService_1.default.getMultipleFlights(flights);
|
||||
res.json(flightData);
|
||||
}
|
||||
catch (error) {
|
||||
res.status(500).json({ error: 'Failed to fetch flight data' });
|
||||
}
|
||||
});
|
||||
// Get flight tracking status
|
||||
app.get('/api/flights/tracking/status', (req, res) => {
|
||||
const status = flightTracker.getTrackingStatus();
|
||||
res.json(status);
|
||||
});
|
||||
// Schedule management routes (protected)
|
||||
app.get('/api/vips/:vipId/schedule', simpleAuth_1.requireAuth, async (req, res) => {
|
||||
const { vipId } = req.params;
|
||||
try {
|
||||
const vipSchedule = await enhancedDataService_1.default.getSchedule(vipId);
|
||||
res.json(vipSchedule);
|
||||
}
|
||||
catch (error) {
|
||||
res.status(500).json({ error: 'Failed to fetch schedule' });
|
||||
}
|
||||
});
|
||||
app.post('/api/vips/:vipId/schedule', simpleAuth_1.requireAuth, (0, simpleAuth_1.requireRole)(['coordinator', 'administrator']), async (req, res) => {
|
||||
const { vipId } = req.params;
|
||||
const { title, location, startTime, endTime, description, type, assignedDriverId } = req.body;
|
||||
// Validate the event
|
||||
const validationErrors = scheduleValidationService_1.default.validateEvent({
|
||||
title: title || '',
|
||||
location: location || '',
|
||||
startTime: startTime || '',
|
||||
endTime: endTime || '',
|
||||
type: type || ''
|
||||
}, false);
|
||||
const { critical, warnings } = scheduleValidationService_1.default.categorizeErrors(validationErrors);
|
||||
// Return validation errors if any critical errors exist
|
||||
if (critical.length > 0) {
|
||||
return res.status(400).json({
|
||||
error: 'Validation failed',
|
||||
validationErrors: critical,
|
||||
warnings: warnings,
|
||||
message: scheduleValidationService_1.default.getErrorSummary(critical)
|
||||
});
|
||||
}
|
||||
const newEvent = {
|
||||
id: Date.now().toString(),
|
||||
title,
|
||||
location,
|
||||
startTime,
|
||||
endTime,
|
||||
description: description || '',
|
||||
assignedDriverId: assignedDriverId || '',
|
||||
status: 'scheduled',
|
||||
type
|
||||
};
|
||||
try {
|
||||
const savedEvent = await enhancedDataService_1.default.addScheduleEvent(vipId, newEvent);
|
||||
// Include warnings in the response if any
|
||||
const response = { ...savedEvent };
|
||||
if (warnings.length > 0) {
|
||||
response.warnings = warnings;
|
||||
}
|
||||
res.status(201).json(response);
|
||||
}
|
||||
catch (error) {
|
||||
res.status(500).json({ error: 'Failed to create schedule event' });
|
||||
}
|
||||
});
|
||||
app.put('/api/vips/:vipId/schedule/:eventId', simpleAuth_1.requireAuth, (0, simpleAuth_1.requireRole)(['coordinator', 'administrator']), async (req, res) => {
|
||||
const { vipId, eventId } = req.params;
|
||||
const { title, location, startTime, endTime, description, type, assignedDriverId, status } = req.body;
|
||||
// Validate the updated event (with edit flag for grace period)
|
||||
const validationErrors = scheduleValidationService_1.default.validateEvent({
|
||||
title: title || '',
|
||||
location: location || '',
|
||||
startTime: startTime || '',
|
||||
endTime: endTime || '',
|
||||
type: type || ''
|
||||
}, true);
|
||||
const { critical, warnings } = scheduleValidationService_1.default.categorizeErrors(validationErrors);
|
||||
// Return validation errors if any critical errors exist
|
||||
if (critical.length > 0) {
|
||||
return res.status(400).json({
|
||||
error: 'Validation failed',
|
||||
validationErrors: critical,
|
||||
warnings: warnings,
|
||||
message: scheduleValidationService_1.default.getErrorSummary(critical)
|
||||
});
|
||||
}
|
||||
const updatedEvent = {
|
||||
id: eventId,
|
||||
title,
|
||||
location,
|
||||
startTime,
|
||||
endTime,
|
||||
description: description || '',
|
||||
assignedDriverId: assignedDriverId || '',
|
||||
type,
|
||||
status: status || 'scheduled'
|
||||
};
|
||||
try {
|
||||
const savedEvent = await enhancedDataService_1.default.updateScheduleEvent(vipId, eventId, updatedEvent);
|
||||
if (!savedEvent) {
|
||||
return res.status(404).json({ error: 'Event not found' });
|
||||
}
|
||||
// Include warnings in the response if any
|
||||
const response = { ...savedEvent };
|
||||
if (warnings.length > 0) {
|
||||
response.warnings = warnings;
|
||||
}
|
||||
res.json(response);
|
||||
}
|
||||
catch (error) {
|
||||
res.status(500).json({ error: 'Failed to update schedule event' });
|
||||
}
|
||||
});
|
||||
app.patch('/api/vips/:vipId/schedule/:eventId/status', simpleAuth_1.requireAuth, async (req, res) => {
|
||||
const { vipId, eventId } = req.params;
|
||||
const { status } = req.body;
|
||||
try {
|
||||
const currentSchedule = await enhancedDataService_1.default.getSchedule(vipId);
|
||||
const currentEvent = currentSchedule.find((event) => event.id === eventId);
|
||||
if (!currentEvent) {
|
||||
return res.status(404).json({ error: 'Event not found' });
|
||||
}
|
||||
const updatedEvent = { ...currentEvent, status };
|
||||
const savedEvent = await enhancedDataService_1.default.updateScheduleEvent(vipId, eventId, updatedEvent);
|
||||
if (!savedEvent) {
|
||||
return res.status(404).json({ error: 'Event not found' });
|
||||
}
|
||||
res.json(savedEvent);
|
||||
}
|
||||
catch (error) {
|
||||
res.status(500).json({ error: 'Failed to update event status' });
|
||||
}
|
||||
});
|
||||
app.delete('/api/vips/:vipId/schedule/:eventId', simpleAuth_1.requireAuth, (0, simpleAuth_1.requireRole)(['coordinator', 'administrator']), async (req, res) => {
|
||||
const { vipId, eventId } = req.params;
|
||||
try {
|
||||
const deletedEvent = await enhancedDataService_1.default.deleteScheduleEvent(vipId, eventId);
|
||||
if (!deletedEvent) {
|
||||
return res.status(404).json({ error: 'Event not found' });
|
||||
}
|
||||
res.json({ message: 'Event deleted successfully', event: deletedEvent });
|
||||
}
|
||||
catch (error) {
|
||||
res.status(500).json({ error: 'Failed to delete schedule event' });
|
||||
}
|
||||
});
|
||||
// Driver availability and conflict checking (protected)
|
||||
app.post('/api/drivers/availability', simpleAuth_1.requireAuth, async (req, res) => {
|
||||
const { startTime, endTime, location } = req.body;
|
||||
if (!startTime || !endTime) {
|
||||
return res.status(400).json({ error: 'startTime and endTime are required' });
|
||||
}
|
||||
try {
|
||||
const allSchedules = await enhancedDataService_1.default.getAllSchedules();
|
||||
const drivers = await enhancedDataService_1.default.getDrivers();
|
||||
const availability = driverConflictService_1.default.getDriverAvailability({ startTime, endTime, location: location || '' }, allSchedules, drivers);
|
||||
res.json(availability);
|
||||
}
|
||||
catch (error) {
|
||||
res.status(500).json({ error: 'Failed to check driver availability' });
|
||||
}
|
||||
});
|
||||
// Check conflicts for specific driver assignment (protected)
|
||||
app.post('/api/drivers/:driverId/conflicts', simpleAuth_1.requireAuth, async (req, res) => {
|
||||
const { driverId } = req.params;
|
||||
const { startTime, endTime, location } = req.body;
|
||||
if (!startTime || !endTime) {
|
||||
return res.status(400).json({ error: 'startTime and endTime are required' });
|
||||
}
|
||||
try {
|
||||
const allSchedules = await enhancedDataService_1.default.getAllSchedules();
|
||||
const drivers = await enhancedDataService_1.default.getDrivers();
|
||||
const conflicts = driverConflictService_1.default.checkDriverConflicts(driverId, { startTime, endTime, location: location || '' }, allSchedules, drivers);
|
||||
res.json({ conflicts });
|
||||
}
|
||||
catch (error) {
|
||||
res.status(500).json({ error: 'Failed to check driver conflicts' });
|
||||
}
|
||||
});
|
||||
// Get driver's complete schedule (protected)
|
||||
app.get('/api/drivers/:driverId/schedule', simpleAuth_1.requireAuth, async (req, res) => {
|
||||
const { driverId } = req.params;
|
||||
try {
|
||||
const drivers = await enhancedDataService_1.default.getDrivers();
|
||||
const driver = drivers.find((d) => d.id === driverId);
|
||||
if (!driver) {
|
||||
return res.status(404).json({ error: 'Driver not found' });
|
||||
}
|
||||
// Get all events assigned to this driver across all VIPs
|
||||
const driverSchedule = [];
|
||||
const allSchedules = await enhancedDataService_1.default.getAllSchedules();
|
||||
const vips = await enhancedDataService_1.default.getVips();
|
||||
Object.entries(allSchedules).forEach(([vipId, events]) => {
|
||||
events.forEach((event) => {
|
||||
if (event.assignedDriverId === driverId) {
|
||||
// Get VIP name
|
||||
const vip = vips.find((v) => v.id === vipId);
|
||||
driverSchedule.push({
|
||||
...event,
|
||||
vipId,
|
||||
vipName: vip ? vip.name : 'Unknown VIP'
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
// Sort by start time
|
||||
driverSchedule.sort((a, b) => new Date(a.startTime).getTime() - new Date(b.startTime).getTime());
|
||||
res.json({
|
||||
driver: {
|
||||
id: driver.id,
|
||||
name: driver.name,
|
||||
phone: driver.phone,
|
||||
department: driver.department
|
||||
},
|
||||
schedule: driverSchedule
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
res.status(500).json({ error: 'Failed to fetch driver schedule' });
|
||||
}
|
||||
});
|
||||
// Admin routes
|
||||
app.post('/api/admin/authenticate', (req, res) => {
|
||||
const { password } = req.body;
|
||||
if (password === ADMIN_PASSWORD) {
|
||||
res.json({ success: true });
|
||||
}
|
||||
else {
|
||||
res.status(401).json({ error: 'Invalid password' });
|
||||
}
|
||||
});
|
||||
app.get('/api/admin/settings', async (req, res) => {
|
||||
const adminAuth = req.headers['admin-auth'];
|
||||
if (adminAuth !== 'true') {
|
||||
return res.status(401).json({ error: 'Unauthorized' });
|
||||
}
|
||||
try {
|
||||
const adminSettings = await enhancedDataService_1.default.getAdminSettings();
|
||||
// Return settings but mask API keys for display only
|
||||
// IMPORTANT: Don't return the actual keys, just indicate they exist
|
||||
const maskedSettings = {
|
||||
apiKeys: {
|
||||
aviationStackKey: adminSettings.apiKeys.aviationStackKey ? '***' + adminSettings.apiKeys.aviationStackKey.slice(-4) : '',
|
||||
googleMapsKey: adminSettings.apiKeys.googleMapsKey ? '***' + adminSettings.apiKeys.googleMapsKey.slice(-4) : '',
|
||||
twilioKey: adminSettings.apiKeys.twilioKey ? '***' + adminSettings.apiKeys.twilioKey.slice(-4) : '',
|
||||
googleClientId: adminSettings.apiKeys.googleClientId ? '***' + adminSettings.apiKeys.googleClientId.slice(-4) : '',
|
||||
googleClientSecret: adminSettings.apiKeys.googleClientSecret ? '***' + adminSettings.apiKeys.googleClientSecret.slice(-4) : ''
|
||||
},
|
||||
systemSettings: adminSettings.systemSettings
|
||||
};
|
||||
res.json(maskedSettings);
|
||||
}
|
||||
catch (error) {
|
||||
res.status(500).json({ error: 'Failed to fetch admin settings' });
|
||||
}
|
||||
});
|
||||
app.post('/api/admin/settings', async (req, res) => {
|
||||
const adminAuth = req.headers['admin-auth'];
|
||||
if (adminAuth !== 'true') {
|
||||
return res.status(401).json({ error: 'Unauthorized' });
|
||||
}
|
||||
try {
|
||||
const { apiKeys, systemSettings } = req.body;
|
||||
const currentSettings = await enhancedDataService_1.default.getAdminSettings();
|
||||
// Update API keys (only if provided and not masked)
|
||||
if (apiKeys) {
|
||||
if (apiKeys.aviationStackKey && !apiKeys.aviationStackKey.startsWith('***')) {
|
||||
currentSettings.apiKeys.aviationStackKey = apiKeys.aviationStackKey;
|
||||
// Update the environment variable for the flight service
|
||||
process.env.AVIATIONSTACK_API_KEY = apiKeys.aviationStackKey;
|
||||
}
|
||||
if (apiKeys.googleMapsKey && !apiKeys.googleMapsKey.startsWith('***')) {
|
||||
currentSettings.apiKeys.googleMapsKey = apiKeys.googleMapsKey;
|
||||
}
|
||||
if (apiKeys.twilioKey && !apiKeys.twilioKey.startsWith('***')) {
|
||||
currentSettings.apiKeys.twilioKey = apiKeys.twilioKey;
|
||||
}
|
||||
if (apiKeys.googleClientId && !apiKeys.googleClientId.startsWith('***')) {
|
||||
currentSettings.apiKeys.googleClientId = apiKeys.googleClientId;
|
||||
// Update the environment variable for Google OAuth
|
||||
process.env.GOOGLE_CLIENT_ID = apiKeys.googleClientId;
|
||||
}
|
||||
if (apiKeys.googleClientSecret && !apiKeys.googleClientSecret.startsWith('***')) {
|
||||
currentSettings.apiKeys.googleClientSecret = apiKeys.googleClientSecret;
|
||||
// Update the environment variable for Google OAuth
|
||||
process.env.GOOGLE_CLIENT_SECRET = apiKeys.googleClientSecret;
|
||||
}
|
||||
}
|
||||
// Update system settings
|
||||
if (systemSettings) {
|
||||
currentSettings.systemSettings = { ...currentSettings.systemSettings, ...systemSettings };
|
||||
}
|
||||
// Save the updated settings
|
||||
await enhancedDataService_1.default.updateAdminSettings(currentSettings);
|
||||
res.json({ success: true });
|
||||
}
|
||||
catch (error) {
|
||||
res.status(500).json({ error: 'Failed to update admin settings' });
|
||||
}
|
||||
});
|
||||
app.post('/api/admin/test-api/:apiType', async (req, res) => {
|
||||
const adminAuth = req.headers['admin-auth'];
|
||||
if (adminAuth !== 'true') {
|
||||
return res.status(401).json({ error: 'Unauthorized' });
|
||||
}
|
||||
const { apiType } = req.params;
|
||||
const { apiKey } = req.body;
|
||||
try {
|
||||
switch (apiType) {
|
||||
case 'aviationStackKey':
|
||||
// Test AviationStack API
|
||||
const testUrl = `http://api.aviationstack.com/v1/flights?access_key=${apiKey}&limit=1`;
|
||||
const response = await fetch(testUrl);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
if (data.error) {
|
||||
res.status(400).json({ error: data.error.message || 'Invalid API key' });
|
||||
}
|
||||
else {
|
||||
res.json({ success: true, message: 'API key is valid!' });
|
||||
}
|
||||
}
|
||||
else {
|
||||
res.status(400).json({ error: 'Failed to validate API key' });
|
||||
}
|
||||
break;
|
||||
case 'googleMapsKey':
|
||||
res.json({ success: true, message: 'Google Maps API testing not yet implemented' });
|
||||
break;
|
||||
case 'twilioKey':
|
||||
res.json({ success: true, message: 'Twilio API testing not yet implemented' });
|
||||
break;
|
||||
default:
|
||||
res.status(400).json({ error: 'Unknown API type' });
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
res.status(500).json({ error: 'Failed to test API connection' });
|
||||
}
|
||||
});
|
||||
// JWT Key Management endpoints (admin only)
|
||||
app.get('/api/admin/jwt-status', simpleAuth_1.requireAuth, (0, simpleAuth_1.requireRole)(['administrator']), (req, res) => {
|
||||
const jwtKeyManager = require('./services/jwtKeyManager').default;
|
||||
const status = jwtKeyManager.getStatus();
|
||||
res.json({
|
||||
keyRotationEnabled: true,
|
||||
rotationInterval: '24 hours',
|
||||
gracePeriod: '24 hours',
|
||||
...status,
|
||||
message: 'JWT keys are automatically rotated every 24 hours for enhanced security'
|
||||
});
|
||||
});
|
||||
app.post('/api/admin/jwt-rotate', simpleAuth_1.requireAuth, (0, simpleAuth_1.requireRole)(['administrator']), (req, res) => {
|
||||
const jwtKeyManager = require('./services/jwtKeyManager').default;
|
||||
try {
|
||||
jwtKeyManager.forceRotation();
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'JWT key rotation triggered successfully. New tokens will use the new key.'
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
res.status(500).json({ error: 'Failed to rotate JWT keys' });
|
||||
}
|
||||
});
|
||||
// Initialize database and start server
|
||||
// Add 404 handler for undefined routes
|
||||
app.use(errorHandler_1.notFoundHandler);
|
||||
// Add error logging middleware
|
||||
app.use(logger_1.errorLogger);
|
||||
// Add global error handler (must be last!)
|
||||
app.use(errorHandler_1.errorHandler);
|
||||
async function startServer() {
|
||||
try {
|
||||
// Initialize database schema and migrate data
|
||||
await databaseService_1.default.initializeDatabase();
|
||||
console.log('✅ Database initialization completed');
|
||||
// Start the server
|
||||
app.listen(port, () => {
|
||||
console.log(`🚀 Server is running on port ${port}`);
|
||||
console.log(`🔐 Admin password: ${ADMIN_PASSWORD}`);
|
||||
console.log(`📊 Admin dashboard: http://localhost:${port === 3000 ? 5173 : port}/admin`);
|
||||
console.log(`🏥 Health check: http://localhost:${port}/api/health`);
|
||||
console.log(`📚 API docs: http://localhost:${port}/api-docs.html`);
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
console.error('❌ Failed to start server:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
startServer();
|
||||
//# sourceMappingURL=index.original.js.map
|
||||
Reference in New Issue
Block a user