import express from 'express'; import helmet from 'helmet'; import morgan from 'morgan'; import dotenv from 'dotenv'; import cookieParser from 'cookie-parser'; import { UserService } from './services/user.service'; import { SSOUserData } from './types/auth.types'; import { sequelize } from './config/database'; import { corsMiddleware } from './middlewares/cors.middleware'; import { metricsMiddleware, createMetricsRouter } from './middlewares/metrics.middleware'; import routes from './routes/index'; import { ensureUploadDir, UPLOAD_DIR } from './config/storage'; import path from 'path'; // Load environment variables dotenv.config(); const app: express.Application = express(); const userService = new UserService(); // Initialize database connection const initializeDatabase = async () => { try { await sequelize.authenticate(); } catch (error) { console.error('❌ Database connection failed:', error); } }; // Initialize database initializeDatabase(); // Trust proxy - Enable this when behind a reverse proxy (nginx, load balancer, etc.) // This allows Express to read X-Forwarded-* headers correctly // Set to true in production, false in development if (process.env.TRUST_PROXY === 'true' || process.env.NODE_ENV === 'production') { app.set('trust proxy', true); } else { // In development, trust first proxy (useful for local testing with nginx) app.set('trust proxy', 1); } // CORS middleware - MUST be before other middleware app.use(corsMiddleware); // Security middleware - Configure Helmet to work with CORS // Get frontend URL for CSP - allow cross-origin connections in development const frontendUrl = process.env.FRONTEND_URL || 'http://localhost:3000'; const isDevelopment = process.env.NODE_ENV !== 'production'; // Build connect-src directive - allow backend API and blob URLs const connectSrc = ["'self'", "blob:", "data:"]; if (isDevelopment) { // In development, allow connections to common dev ports connectSrc.push("http://localhost:3000", "http://localhost:5000", "ws://localhost:3000", "ws://localhost:5000"); // Also allow the configured frontend URL if it's a localhost URL if (frontendUrl.includes('localhost')) { connectSrc.push(frontendUrl); } } else { // In production, only allow the configured frontend URL if (frontendUrl && frontendUrl !== '*') { const frontendOrigins = frontendUrl.split(',').map(url => url.trim()).filter(Boolean); connectSrc.push(...frontendOrigins); } } // Build CSP directives - conditionally include upgradeInsecureRequests const cspDirectives: any = { defaultSrc: ["'self'", "blob:"], styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com"], scriptSrc: ["'self'"], imgSrc: ["'self'", "data:", "https:", "blob:"], connectSrc: connectSrc, frameSrc: ["'self'", "blob:"], fontSrc: ["'self'", "https://fonts.gstatic.com", "data:"], objectSrc: ["'none'"], baseUri: ["'self'"], formAction: ["'self'"], }; // Only add upgradeInsecureRequests in production (it forces HTTPS) if (!isDevelopment) { cspDirectives.upgradeInsecureRequests = []; } app.use(helmet({ crossOriginEmbedderPolicy: false, crossOriginResourcePolicy: { policy: "cross-origin" }, contentSecurityPolicy: { directives: cspDirectives, }, })); // Cookie parser middleware - MUST be before routes app.use(cookieParser()); // Body parsing middleware app.use(express.json({ limit: '10mb' })); app.use(express.urlencoded({ extended: true, limit: '10mb' })); // Logging middleware app.use(morgan('combined')); // Prometheus metrics middleware - collect request metrics app.use(metricsMiddleware); // Prometheus metrics endpoint - expose metrics for scraping app.use(createMetricsRouter()); // Health check endpoint (before API routes) app.get('/health', (_req: express.Request, res: express.Response) => { res.status(200).json({ status: 'OK', timestamp: new Date(), uptime: process.uptime(), environment: process.env.NODE_ENV || 'development' }); }); // Mount API routes - MUST be before static file serving app.use('/api/v1', routes); // Serve uploaded files statically ensureUploadDir(); app.use('/uploads', express.static(UPLOAD_DIR)); // Legacy SSO Callback endpoint for user creation/update (kept for backward compatibility) app.post('/api/v1/auth/sso-callback', async (req: express.Request, res: express.Response): Promise => { try { const ssoData: SSOUserData = req.body; // Validate required fields - email and oktaSub are required if (!ssoData.email || !ssoData.oktaSub) { res.status(400).json({ success: false, message: 'Missing required fields: email and oktaSub are required', timestamp: new Date() }); return; } // Create or update user const user = await userService.createOrUpdateUser(ssoData); res.status(200).json({ success: true, message: 'User processed successfully', data: { user: { userId: user.userId, employeeId: user.employeeId || null, oktaSub: user.oktaSub, email: user.email, firstName: user.firstName || null, lastName: user.lastName || null, displayName: user.displayName || null, department: user.department || null, designation: user.designation || null, phone: user.phone || null, location: user.location || null, role: user.role, lastLogin: user.lastLogin }, isNewUser: user.createdAt.getTime() === user.updatedAt.getTime() }, timestamp: new Date() }); } catch (error) { console.error('❌ SSO Callback failed:', error); res.status(500).json({ success: false, message: 'Internal server error', timestamp: new Date() }); } }); // Get all users endpoint app.get('/api/v1/users', async (_req: express.Request, res: express.Response): Promise => { try { const users = await userService.getAllUsers(); res.status(200).json({ success: true, message: 'Users retrieved successfully', data: { users: users.map(user => ({ userId: user.userId, employeeId: user.employeeId || null, oktaSub: user.oktaSub, email: user.email, firstName: user.firstName || null, lastName: user.lastName || null, displayName: user.displayName || null, department: user.department || null, designation: user.designation || null, phone: user.phone || null, location: user.location || null, role: user.role, lastLogin: user.lastLogin, createdAt: user.createdAt })), total: users.length }, timestamp: new Date() }); } catch (error) { console.error('❌ Get Users failed:', error); res.status(500).json({ success: false, message: 'Internal server error', timestamp: new Date() }); } }); // Serve React build static files (only in production or when build folder exists) // Check for both 'build' (Create React App) and 'dist' (Vite) folders const buildPath = path.join(__dirname, "..", "build"); const distPath = path.join(__dirname, "..", "dist"); const fs = require('fs'); // Try to find React build directory let reactBuildPath: string | null = null; if (fs.existsSync(buildPath)) { reactBuildPath = buildPath; } else if (fs.existsSync(distPath)) { reactBuildPath = distPath; } // Serve static files if React build exists if (reactBuildPath && fs.existsSync(path.join(reactBuildPath, "index.html"))) { // Serve static assets (JS, CSS, images, etc.) - these will have CSP headers from Helmet app.use(express.static(reactBuildPath, { setHeaders: (res: express.Response, filePath: string) => { // Apply CSP headers to HTML files served as static files if (filePath.endsWith('.html')) { // CSP headers are already set by Helmet middleware, but ensure they're applied // The meta tag in index.html will also enforce CSP } } })); // Catch-all handler: serve React app for all non-API routes // This must be AFTER all API routes to avoid intercepting API requests app.get('*', (req: express.Request, res: express.Response): void => { // Don't serve React for API routes, uploads, or health check if (req.path.startsWith('/api/') || req.path.startsWith('/uploads/') || req.path === '/health') { res.status(404).json({ success: false, message: `Route ${req.originalUrl} not found`, timestamp: new Date(), }); return; } // Serve React app for all other routes (SPA routing) // This handles client-side routing in React Router // CSP headers from Helmet will be applied to this response res.sendFile(path.join(reactBuildPath!, "index.html")); }); } else { // No React build found - provide API info at root and use standard 404 handler app.get('/', (_req: express.Request, res: express.Response): void => { res.status(200).json({ message: 'Royal Enfield Workflow Management System API', version: '1.0.0', status: 'running', timestamp: new Date(), note: 'React build not found. API is available at /api/v1' }); }); // Standard 404 handler for non-existent routes app.use((req: express.Request, res: express.Response): void => { res.status(404).json({ success: false, message: `Route ${req.originalUrl} not found`, timestamp: new Date(), }); }); } export default app;