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,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