Files
crm-server/src/app.js
richardtekula d26e537244 fix: Harden security - CORS, XSS, file uploads, error handling
- Restrict no-origin CORS bypass to development only
- Activate xss-clean middleware for input sanitization
- Add MIME type whitelist and filename sanitization to file uploads
- Reduce project upload limit from 50MB to 20MB
- Stop leaking stack traces in error responses

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-26 15:21:44 +01:00

163 lines
5.2 KiB
JavaScript

import express from 'express';
import morgan from 'morgan';
import helmet from 'helmet';
import cors from 'cors';
import cookieParser from 'cookie-parser';
import xssClean from 'xss-clean';
import { validateBody } from './middlewares/global/validateBody.js';
import { notFound } from './middlewares/global/notFound.js';
import { errorHandler } from './middlewares/global/errorHandler.js';
import { apiRateLimiter } from './middlewares/security/rateLimiter.js';
import { logger } from './utils/logger.js';
// Import routes
import authRoutes from './routes/auth.routes.js';
import adminRoutes from './routes/admin.routes.js';
import contactRoutes from './routes/contact.routes.js';
import personalContactRoutes from './routes/personal-contact.routes.js';
import crmEmailRoutes from './routes/crm-email.routes.js';
import emailAccountRoutes from './routes/email-account.routes.js';
import timesheetRoutes from './routes/timesheet.routes.js';
import companyRoutes from './routes/company.routes.js';
import projectRoutes from './routes/project.routes.js';
import todoRoutes from './routes/todo.routes.js';
import timeTrackingRoutes from './routes/time-tracking.routes.js';
import noteRoutes from './routes/note.routes.js';
import auditRoutes from './routes/audit.routes.js';
import eventRoutes from './routes/event.routes.js';
import messageRoutes from './routes/message.routes.js';
import groupRoutes from './routes/group.routes.js';
import pushRoutes from './routes/push.routes.js';
import userRoutes from './routes/user.routes.js';
import serviceRoutes from './routes/service.routes.js';
import emailSignatureRoutes from './routes/email-signature.routes.js';
import aiKurzyRoutes from './routes/ai-kurzy.routes.js';
const app = express();
// HTTP request logging - only errors by default (LOG_LEVEL=debug shows all)
app.use(morgan((tokens, req, res) => {
const status = parseInt(tokens.status(req, res)) || 0;
const message = `${tokens.method(req, res)} ${tokens.url(req, res)} ${status} ${tokens['response-time'](req, res)} ms`;
logger.http(message, status);
return null; // Don't write to stdout, logger handles it
}));
app.use(
helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
},
},
hsts: {
maxAge: 31536000,
includeSubDomains: true,
preload: true,
},
})
);
// CORS configuration
const isProduction = process.env.NODE_ENV === 'production';
const corsOptions = {
origin: (origin, callback) => {
// Requests with no origin (curl, Postman, server-to-server)
// Only allow in development - in production this is a CORS bypass vector
if (!origin) {
if (isProduction) {
return callback(new Error('Not allowed by CORS'));
}
return callback(null, true);
}
// Check CORS_ORIGIN env variable first
const corsOrigin = process.env.CORS_ORIGIN;
if (corsOrigin && origin === corsOrigin) {
return callback(null, true);
}
// In development, allow localhost and local network IPs
if (!isProduction) {
const allowedPatterns = [
/^https?:\/\/localhost(:\d+)?$/,
/^https?:\/\/127\.0\.0\.1(:\d+)?$/,
/^https?:\/\/192\.168\.\d{1,3}\.\d{1,3}(:\d+)?$/,
/^https?:\/\/10\.\d{1,3}\.\d{1,3}\.\d{1,3}(:\d+)?$/,
/^https?:\/\/172\.(1[6-9]|2\d|3[01])\.\d{1,3}\.\d{1,3}(:\d+)?$/,
];
if (allowedPatterns.some(pattern => pattern.test(origin))) {
return callback(null, true);
}
}
callback(new Error('Not allowed by CORS'));
},
credentials: true,
optionsSuccessStatus: 200,
};
app.use(cors(corsOptions));
// Body parsing middleware
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
app.use(cookieParser());
// XSS sanitization - clean user input in body, params, and query
app.use(xssClean());
// Custom body validation middleware
app.use(validateBody);
// Rate limiting for all API routes
app.use('/api', apiRateLimiter);
// Health check endpoint
app.get('/health', (req, res) => {
res.status(200).json({
success: true,
message: 'CRM API is running',
timestamp: new Date().toISOString(),
});
});
// API Routes
app.use('/api/auth', authRoutes);
app.use('/api/admin', adminRoutes);
app.use('/api/contacts', contactRoutes);
app.use('/api/personal-contacts', personalContactRoutes);
app.use('/api/emails', crmEmailRoutes);
app.use('/api/email-accounts', emailAccountRoutes);
app.use('/api/timesheets', timesheetRoutes);
app.use('/api/companies', companyRoutes);
app.use('/api/projects', projectRoutes);
app.use('/api/todos', todoRoutes);
app.use('/api/time-tracking', timeTrackingRoutes);
app.use('/api/notes', noteRoutes);
app.use('/api/audit-logs', auditRoutes);
app.use('/api/events', eventRoutes);
app.use('/api/messages', messageRoutes);
app.use('/api/groups', groupRoutes);
app.use('/api/push', pushRoutes);
app.use('/api/users', userRoutes);
app.use('/api/services', serviceRoutes);
app.use('/api/email-signature', emailSignatureRoutes);
app.use('/api/ai-kurzy', aiKurzyRoutes);
// Basic route
app.get('/', (req, res) => {
res.json({
success: true,
message: 'CRM API Server',
version: '1.0.0',
});
});
// Global Middlewares (must be last)
app.use(notFound);
app.use(errorHandler);
export default app;