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>
765 lines
32 KiB
JavaScript
765 lines
32 KiB
JavaScript
"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
|