257 lines
7.7 KiB
JavaScript
257 lines
7.7 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 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 PayoutProcessor = require("./jobs/payoutProcessor");
|
|
const RentalStatusJob = require("./jobs/rentalStatusJob");
|
|
const ConditionCheckReminderJob = require("./jobs/conditionCheckReminder");
|
|
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 { 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
|
|
})
|
|
);
|
|
|
|
// Serve static files from uploads directory with CORS headers
|
|
app.use(
|
|
"/uploads",
|
|
helmet.crossOriginResourcePolicy({ policy: "cross-origin" }),
|
|
express.static(path.join(__dirname, "uploads"))
|
|
);
|
|
|
|
// Health check endpoints (no auth, no rate limiting)
|
|
app.use("/health", healthRoutes);
|
|
|
|
// 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);
|
|
}
|
|
|
|
// Start the payout processor
|
|
const payoutJobs = PayoutProcessor.startScheduledPayouts();
|
|
logger.info("Payout processor started");
|
|
|
|
// Start the rental status job
|
|
const rentalStatusJobs = RentalStatusJob.startScheduledStatusUpdates();
|
|
logger.info("Rental status job started");
|
|
|
|
// Start the condition check reminder job
|
|
const conditionCheckJobs =
|
|
ConditionCheckReminderJob.startScheduledReminders();
|
|
logger.info("Condition check reminder job started");
|
|
|
|
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);
|
|
});
|