159 lines
4.3 KiB
JavaScript
159 lines
4.3 KiB
JavaScript
const express = require('express');
|
|
const cors = require('cors');
|
|
const helmet = require('helmet');
|
|
const cookieParser = require('cookie-parser');
|
|
const csrf = require('csurf');
|
|
const securityConfig = require('./config/security');
|
|
const { apiLimiter } = require('./middleware/rateLimiter');
|
|
|
|
const app = express();
|
|
|
|
// Security Headers (helmet)
|
|
app.use(helmet({
|
|
contentSecurityPolicy: {
|
|
directives: {
|
|
defaultSrc: ["'self'"],
|
|
styleSrc: ["'self'", "'unsafe-inline'", "https://ui-avatars.com"],
|
|
scriptSrc: ["'self'"],
|
|
imgSrc: ["'self'", "data:", "https:", "https://ui-avatars.com"],
|
|
connectSrc: ["'self'"],
|
|
fontSrc: ["'self'"],
|
|
objectSrc: ["'none'"],
|
|
mediaSrc: ["'self'"],
|
|
frameSrc: ["'none'"],
|
|
},
|
|
},
|
|
hsts: {
|
|
maxAge: 31536000,
|
|
includeSubDomains: true,
|
|
preload: true,
|
|
},
|
|
noSniff: true,
|
|
xssFilter: true,
|
|
hidePoweredBy: true,
|
|
}));
|
|
|
|
// CORS
|
|
app.use(cors({
|
|
origin: (origin, callback) => {
|
|
const allowedOrigins = securityConfig.cors.origin;
|
|
|
|
// Allow requests with no origin (mobile apps, curl, etc.)
|
|
if (!origin) return callback(null, true);
|
|
|
|
if (allowedOrigins.includes(origin)) {
|
|
callback(null, true);
|
|
} else {
|
|
callback(new Error('Not allowed by CORS'));
|
|
}
|
|
},
|
|
credentials: securityConfig.cors.credentials,
|
|
maxAge: 86400, // 24 hours
|
|
}));
|
|
|
|
// Body parsing with size limits
|
|
app.use(express.json({ limit: securityConfig.bodyLimit }));
|
|
app.use(express.urlencoded({ extended: true, limit: securityConfig.bodyLimit }));
|
|
|
|
// Cookie parser (required for CSRF protection)
|
|
app.use(cookieParser());
|
|
|
|
// CSRF Protection (Phase 3 - Security Hardening)
|
|
if (securityConfig.csrf.enabled) {
|
|
const csrfProtection = csrf({
|
|
cookie: {
|
|
httpOnly: true,
|
|
secure: process.env.NODE_ENV === 'production', // HTTPS only in production
|
|
sameSite: 'strict',
|
|
}
|
|
});
|
|
|
|
// Apply CSRF protection to all routes
|
|
app.use(csrfProtection);
|
|
|
|
// CSRF token endpoint - provides token to clients
|
|
app.get('/api/csrf-token', (req, res) => {
|
|
res.json({ csrfToken: req.csrfToken() });
|
|
});
|
|
|
|
// CSRF error handler
|
|
app.use((err, req, res, next) => {
|
|
if (err.code === 'EBADCSRFTOKEN') {
|
|
return res.status(403).json({
|
|
success: false,
|
|
error: 'Invalid CSRF token',
|
|
message: 'Form submission failed. Please refresh the page and try again.',
|
|
});
|
|
}
|
|
next(err);
|
|
});
|
|
} else {
|
|
// Provide a harmless CSRF endpoint in development when CSRF is disabled
|
|
app.get('/api/csrf-token', (req, res) => {
|
|
res.json({ csrfToken: null, disabled: true });
|
|
});
|
|
}
|
|
|
|
// Request logging middleware
|
|
app.use((req, res, next) => {
|
|
console.log(`${new Date().toISOString()} - ${req.method} ${req.path}`);
|
|
next();
|
|
});
|
|
|
|
// Health check endpoint
|
|
app.get('/api/health', (req, res) => {
|
|
res.status(200).json({
|
|
status: 'ok',
|
|
message: 'Backend is running',
|
|
timestamp: new Date().toISOString(),
|
|
environment: process.env.NODE_ENV || 'development'
|
|
});
|
|
});
|
|
|
|
// Apply rate limiting to all API routes
|
|
app.use('/api/', apiLimiter);
|
|
|
|
// API routes
|
|
app.use('/api/auth', require('./routes/auth'));
|
|
app.use('/api/users', require('./routes/users'));
|
|
app.use('/api/dashboard', require('./routes/dashboard'));
|
|
app.use('/api/events', require('./routes/events'));
|
|
app.use('/api/wsdc', require('./routes/wsdc'));
|
|
app.use('/api/divisions', require('./routes/divisions'));
|
|
app.use('/api/competition-types', require('./routes/competitionTypes'));
|
|
app.use('/api/matches', require('./routes/matches'));
|
|
app.use('/api/admin', require('./routes/admin'));
|
|
// app.use('/api/ratings', require('./routes/ratings'));
|
|
|
|
// 404 handler
|
|
app.use((req, res) => {
|
|
res.status(404).json({
|
|
error: 'Not Found',
|
|
message: `Cannot ${req.method} ${req.path}`
|
|
});
|
|
});
|
|
|
|
// Error handler
|
|
app.use((err, req, res, next) => {
|
|
// Log full error for debugging
|
|
console.error('Error:', err);
|
|
|
|
// Determine if we should show detailed errors
|
|
const isDevelopment = process.env.NODE_ENV === 'development';
|
|
|
|
// Generic error response
|
|
const errorResponse = {
|
|
success: false,
|
|
error: isDevelopment ? err.message : 'Internal Server Error',
|
|
};
|
|
|
|
// Add stack trace only in development
|
|
if (isDevelopment && err.stack) {
|
|
errorResponse.stack = err.stack;
|
|
}
|
|
|
|
res.status(err.status || 500).json(errorResponse);
|
|
});
|
|
|
|
module.exports = app;
|