242 lines
7.2 KiB
JavaScript
242 lines
7.2 KiB
JavaScript
// Load environment-specific config
|
|
const env = process.env.NODE_ENV || "dev";
|
|
const envFile = `.env.${env}`;
|
|
|
|
require("dotenv").config({
|
|
path: envFile,
|
|
});
|
|
const express = require("express");
|
|
const http = require("http");
|
|
const { Server } = require("socket.io");
|
|
const cors = require("cors");
|
|
const bodyParser = require("body-parser");
|
|
const path = require("path");
|
|
const helmet = require("helmet");
|
|
const { sequelize } = require("./models"); // Import from models/index.js to ensure associations are loaded
|
|
const { cookieParser } = require("./middleware/csrf");
|
|
const logger = require("./utils/logger");
|
|
const morgan = require("morgan");
|
|
|
|
const authRoutes = require("./routes/auth");
|
|
const { router: alphaRoutes } = require("./routes/alpha");
|
|
const userRoutes = require("./routes/users");
|
|
const itemRoutes = require("./routes/items");
|
|
const rentalRoutes = require("./routes/rentals");
|
|
const messageRoutes = require("./routes/messages");
|
|
const forumRoutes = require("./routes/forum");
|
|
const stripeRoutes = require("./routes/stripe");
|
|
const stripeWebhookRoutes = require("./routes/stripeWebhooks");
|
|
const mapsRoutes = require("./routes/maps");
|
|
const conditionCheckRoutes = require("./routes/conditionChecks");
|
|
const feedbackRoutes = require("./routes/feedback");
|
|
const uploadRoutes = require("./routes/upload");
|
|
const healthRoutes = require("./routes/health");
|
|
|
|
const emailServices = require("./services/email");
|
|
const s3Service = require("./services/s3Service");
|
|
|
|
// Socket.io setup
|
|
const { authenticateSocket } = require("./sockets/socketAuth");
|
|
const { initializeMessageSocket } = require("./sockets/messageSocket");
|
|
|
|
const app = express();
|
|
const server = http.createServer(app);
|
|
|
|
// Initialize Socket.io with CORS
|
|
const io = new Server(server, {
|
|
cors: {
|
|
origin: process.env.FRONTEND_URL || "http://localhost:3000",
|
|
credentials: true,
|
|
methods: ["GET", "POST"],
|
|
},
|
|
});
|
|
|
|
// Apply socket authentication middleware
|
|
io.use(authenticateSocket);
|
|
|
|
// Initialize message socket handlers
|
|
initializeMessageSocket(io);
|
|
|
|
// Store io instance in app for use in routes
|
|
app.set("io", io);
|
|
|
|
// Import security middleware
|
|
const {
|
|
enforceHTTPS,
|
|
securityHeaders,
|
|
addRequestId,
|
|
sanitizeError,
|
|
} = require("./middleware/security");
|
|
const { sanitizeInput } = require("./middleware/validation");
|
|
const { generalLimiter } = require("./middleware/rateLimiter");
|
|
const errorLogger = require("./middleware/errorLogger");
|
|
const apiLogger = require("./middleware/apiLogger");
|
|
const { requireAlphaAccess } = require("./middleware/alphaAccess");
|
|
|
|
// Apply security middleware
|
|
app.use(enforceHTTPS);
|
|
app.use(addRequestId);
|
|
app.use(securityHeaders);
|
|
|
|
// Security headers with Helmet
|
|
app.use(
|
|
helmet({
|
|
contentSecurityPolicy: {
|
|
directives: {
|
|
defaultSrc: ["'self'"],
|
|
styleSrc: ["'self'", "https://cdn.jsdelivr.net"],
|
|
fontSrc: ["'self'"],
|
|
scriptSrc: ["'self'", "https://accounts.google.com"],
|
|
imgSrc: ["'self'"],
|
|
connectSrc: ["'self'"],
|
|
frameSrc: ["'self'", "https://accounts.google.com"],
|
|
},
|
|
},
|
|
})
|
|
);
|
|
|
|
// Cookie parser for CSRF
|
|
app.use(cookieParser);
|
|
|
|
// HTTP request logging
|
|
app.use(morgan("combined", { stream: logger.stream }));
|
|
|
|
// API request/response logging
|
|
app.use("/api/", apiLogger);
|
|
|
|
// CORS with security settings (must come BEFORE rate limiter to ensure headers on all responses)
|
|
app.use(
|
|
cors({
|
|
origin: process.env.FRONTEND_URL || "http://localhost:3000",
|
|
credentials: true,
|
|
optionsSuccessStatus: 200,
|
|
exposedHeaders: ["X-CSRF-Token"],
|
|
})
|
|
);
|
|
|
|
// General rate limiting for all routes
|
|
app.use("/api/", generalLimiter);
|
|
|
|
// Body parsing with size limits
|
|
app.use(
|
|
bodyParser.json({
|
|
limit: "1mb",
|
|
verify: (req, res, buf) => {
|
|
// Store raw body for webhook verification
|
|
req.rawBody = buf;
|
|
},
|
|
})
|
|
);
|
|
app.use(
|
|
bodyParser.urlencoded({
|
|
extended: true,
|
|
limit: "1mb",
|
|
parameterLimit: 100, // Limit number of parameters
|
|
})
|
|
);
|
|
|
|
// Apply input sanitization to all API routes (XSS prevention)
|
|
app.use("/api/", sanitizeInput);
|
|
|
|
// Health check endpoints (no auth, no rate limiting)
|
|
app.use("/health", healthRoutes);
|
|
|
|
// Stripe webhooks (no auth, uses signature verification instead)
|
|
app.use("/api/stripe/webhooks", stripeWebhookRoutes);
|
|
|
|
// Root endpoint
|
|
app.get("/", (req, res) => {
|
|
res.json({ message: "Village Share API is running!" });
|
|
});
|
|
|
|
// Public routes (no alpha access required)
|
|
app.use("/api/alpha", alphaRoutes);
|
|
app.use("/api/auth", authRoutes); // Auth has its own alpha checks in registration
|
|
|
|
// Protected routes (require alpha access)
|
|
app.use("/api/users", requireAlphaAccess, userRoutes);
|
|
app.use("/api/items", requireAlphaAccess, itemRoutes);
|
|
app.use("/api/rentals", requireAlphaAccess, rentalRoutes);
|
|
app.use("/api/messages", requireAlphaAccess, messageRoutes);
|
|
app.use("/api/forum", requireAlphaAccess, forumRoutes);
|
|
app.use("/api/stripe", requireAlphaAccess, stripeRoutes);
|
|
app.use("/api/maps", requireAlphaAccess, mapsRoutes);
|
|
app.use("/api/condition-checks", requireAlphaAccess, conditionCheckRoutes);
|
|
app.use("/api/feedback", requireAlphaAccess, feedbackRoutes);
|
|
app.use("/api/upload", requireAlphaAccess, uploadRoutes);
|
|
|
|
// Error handling middleware (must be last)
|
|
app.use(errorLogger);
|
|
app.use(sanitizeError);
|
|
|
|
const PORT = process.env.PORT || 5000;
|
|
|
|
const { checkPendingMigrations } = require("./utils/checkMigrations");
|
|
|
|
sequelize
|
|
.authenticate()
|
|
.then(async () => {
|
|
logger.info("Database connection established successfully");
|
|
|
|
// Check for pending migrations
|
|
const pendingMigrations = await checkPendingMigrations(sequelize);
|
|
if (pendingMigrations.length > 0) {
|
|
logger.error(
|
|
`Found ${pendingMigrations.length} pending migration(s). Please run 'npm run db:migrate'`,
|
|
{ pendingMigrations }
|
|
);
|
|
process.exit(1);
|
|
}
|
|
logger.info("All migrations are up to date");
|
|
|
|
// Initialize email services and load templates
|
|
try {
|
|
await emailServices.initialize();
|
|
logger.info("Email services initialized successfully");
|
|
} catch (err) {
|
|
logger.error("Failed to initialize email services", {
|
|
error: err.message,
|
|
stack: err.stack,
|
|
});
|
|
// Fail fast - don't start server if email templates can't load
|
|
if (env === "prod" || env === "production") {
|
|
logger.error(
|
|
"Cannot start server without email services in production"
|
|
);
|
|
process.exit(1);
|
|
} else {
|
|
logger.warn(
|
|
"Email services failed to initialize - continuing in dev mode"
|
|
);
|
|
}
|
|
}
|
|
|
|
// Initialize S3 service for image uploads
|
|
try {
|
|
s3Service.initialize();
|
|
logger.info("S3 service initialized successfully");
|
|
} catch (err) {
|
|
logger.error("Failed to initialize S3 service", {
|
|
error: err.message,
|
|
stack: err.stack,
|
|
});
|
|
logger.error("Cannot start server without S3 service in production");
|
|
process.exit(1);
|
|
}
|
|
|
|
server.listen(PORT, () => {
|
|
logger.info(`Server is running on port ${PORT}`, {
|
|
port: PORT,
|
|
environment: env,
|
|
});
|
|
logger.info("Socket.io server initialized");
|
|
});
|
|
})
|
|
.catch((err) => {
|
|
logger.error("Unable to connect to database", {
|
|
error: err.message,
|
|
stack: err.stack,
|
|
});
|
|
process.exit(1);
|
|
});
|