Re_Backend/src/app.ts

285 lines
9.4 KiB
TypeScript

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<void> => {
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<void> => {
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;