285 lines
9.4 KiB
TypeScript
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; |