require("dotenv").config(); const express = require("express"); const couchdbService = require("./services/couchdbService"); const cors = require("cors"); const http = require("http"); const socketio = require("socket.io"); const helmet = require("helmet"); const rateLimit = require("express-rate-limit"); const mongoSanitize = require("express-mongo-sanitize"); const xss = require("xss-clean"); const { errorHandler } = require("./middleware/errorHandler"); const socketAuth = require("./middleware/socketAuth"); const requestLogger = require("./middleware/requestLogger"); const logger = require("./utils/logger"); const { validateEnv, logEnvConfig } = require("./utils/validateEnv"); // Validate environment variables before starting server try { validateEnv(); logEnvConfig(); } catch (error) { logger.error('Environment validation failed:'); logger.error(error.message); process.exit(1); } const app = express(); const server = http.createServer(app); const io = socketio(server, { cors: { origin: process.env.FRONTEND_URL || "http://localhost:3000", methods: ["GET", "POST"], credentials: true, }, }); const port = process.env.PORT || 5000; // Trust proxy - required when behind ingress/reverse proxy app.set('trust proxy', 1); // Security Headers - Helmet app.use(helmet()); // CORS Configuration app.use( cors({ origin: process.env.FRONTEND_URL || "http://localhost:3000", credentials: true, }), ); // Body Parser app.use(express.json()); // Data Sanitization against NoSQL injection app.use(mongoSanitize()); // Data Sanitization against XSS app.use(xss()); // Request Logging app.use(requestLogger); // Rate Limiting for Auth Routes (5 requests per 15 minutes) const authLimiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 5, // 5 requests per windowMs message: { success: false, error: "Too many authentication attempts, please try again later", }, standardHeaders: true, legacyHeaders: false, // Trust proxy when behind ingress validate: { trustProxy: false }, }); // General API Rate Limiting (100 requests per 15 minutes) const apiLimiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 100, // 100 requests per windowMs message: { success: false, error: "Too many requests, please try again later", }, standardHeaders: true, legacyHeaders: false, // Trust proxy when behind ingress validate: { trustProxy: false }, }); // Database Connection // CouchDB (primary database) // Skip initialization during testing if (process.env.NODE_ENV !== 'test') { couchdbService.initialize() .then(() => logger.info("CouchDB initialized successfully")) .catch((err) => { logger.error("CouchDB initialization failed", err); process.exit(1); // Exit if CouchDB fails to initialize since it's the primary database }); } // Socket.IO Authentication Middleware io.use(socketAuth); // Socket.IO Setup with Authentication io.on("connection", (socket) => { logger.info(`Socket.IO client connected`, { userId: socket.user.id }); socket.on("joinEvent", (eventId) => { socket.join(`event_${eventId}`); logger.debug(`User joined event`, { userId: socket.user.id, eventId }); }); socket.on("joinPost", (postId) => { socket.join(`post_${postId}`); logger.debug(`User joined post`, { userId: socket.user.id, postId }); }); socket.on("eventUpdate", (data) => { io.to(`event_${data.eventId}`).emit("update", data.message); }); socket.on("disconnect", () => { logger.info(`Socket.IO client disconnected`, { userId: socket.user.id }); }); }); // Make io available to routes app.set("io", io); // Routes const authRoutes = require("./routes/auth"); const streetRoutes = require("./routes/streets"); const taskRoutes = require("./routes/tasks"); const postRoutes = require("./routes/posts"); const commentsRoutes = require("./routes/comments"); const eventRoutes = require("./routes/events"); const rewardRoutes = require("./routes/rewards"); const reportRoutes = require("./routes/reports"); const badgesRoutes = require("./routes/badges"); const aiRoutes = require("./routes/ai"); const paymentRoutes = require("./routes/payments"); const userRoutes = require("./routes/users"); const cacheRoutes = require("./routes/cache"); const profileRoutes = require("./routes/profile"); const analyticsRoutes = require("./routes/analytics"); const leaderboardRoutes = require("./routes/leaderboard"); // Apply rate limiters app.use("/api/auth/register", authLimiter); app.use("/api/auth/login", authLimiter); app.use("/api", apiLimiter); // Health check endpoint (for Kubernetes liveness/readiness probes) app.get("/api/health", async (req, res) => { try { const couchdbStatus = await couchdbService.checkConnection(); // Check Socket.IO status const socketIOStatus = { engine: io.engine ? "running" : "stopped", connectedClients: io.engine ? io.engine.clientsCount : 0, // Get number of connected sockets sockets: io.sockets ? io.sockets.sockets.size : 0 }; const isHealthy = couchdbStatus && io.engine; res.status(isHealthy ? 200 : 503).json({ status: isHealthy ? "healthy" : "degraded", timestamp: new Date().toISOString(), uptime: process.uptime(), services: { couchdb: couchdbStatus ? "connected" : "disconnected", socketIO: { status: socketIOStatus.engine, connectedClients: socketIOStatus.connectedClients, activeSockets: socketIOStatus.sockets } }, memory: { heapUsed: Math.round(process.memoryUsage().heapUsed / 1024 / 1024) + " MB", heapTotal: Math.round(process.memoryUsage().heapTotal / 1024 / 1024) + " MB", rss: Math.round(process.memoryUsage().rss / 1024 / 1024) + " MB" } }); } catch (error) { res.status(503).json({ status: "unhealthy", timestamp: new Date().toISOString(), uptime: process.uptime(), services: { couchdb: "disconnected", socketIO: "unknown" }, error: error.message, }); } }); // Detailed Socket.IO health check endpoint app.get("/api/health/socketio", (req, res) => { try { const socketIOInfo = { status: io.engine ? "running" : "stopped", connectedClients: io.engine ? io.engine.clientsCount : 0, activeSockets: io.sockets ? io.sockets.sockets.size : 0, rooms: [], timestamp: new Date().toISOString() }; // Get list of active rooms (excluding auto-generated socket ID rooms) if (io.sockets && io.sockets.adapter && io.sockets.adapter.rooms) { const rooms = Array.from(io.sockets.adapter.rooms.keys()).filter(room => { // Filter out socket ID rooms (they start with socket ID pattern) return room.startsWith('event_') || room.startsWith('post_'); }); socketIOInfo.rooms = rooms.map(room => { const roomSize = io.sockets.adapter.rooms.get(room)?.size || 0; return { name: room, members: roomSize }; }); } res.status(200).json(socketIOInfo); } catch (error) { res.status(500).json({ status: "error", error: error.message, timestamp: new Date().toISOString() }); } }); // Routes app.use("/api/auth", authRoutes); app.use("/api/streets", streetRoutes); app.use("/api/tasks", taskRoutes); app.use("/api/posts", postRoutes); app.use("/api/posts", commentsRoutes); // Comments are nested under posts app.use("/api/events", eventRoutes); app.use("/api/rewards", rewardRoutes); app.use("/api/reports", reportRoutes); app.use("/api/badges", badgesRoutes); app.use("/api/ai", aiRoutes); app.use("/api/payments", paymentRoutes); app.use("/api/users", userRoutes); app.use("/api/cache", cacheRoutes); app.use("/api/profile", profileRoutes); app.use("/api/analytics", analyticsRoutes); app.use("/api/leaderboard", leaderboardRoutes); app.get("/", (req, res) => { res.send("Street Adoption App Backend"); }); // Error Handler Middleware (must be last) app.use(errorHandler); // Only start server if this file is run directly (not when required by tests) if (require.main === module) { server.listen(port, () => { logger.info(`Server started`, { port, env: process.env.NODE_ENV || 'development' }); }); } // Export app and server for testing module.exports = { app, server, io }; // Graceful shutdown process.on("SIGTERM", async () => { logger.info("SIGTERM received, shutting down gracefully"); try { // Close CouchDB connection await couchdbService.shutdown(); logger.info("CouchDB connection closed"); // Close server server.close(() => { logger.info("Server closed"); process.exit(0); }); } catch (error) { logger.error("Error during shutdown", error); process.exit(1); } }); process.on("SIGINT", async () => { logger.info("SIGINT received, shutting down gracefully"); try { // Close CouchDB connection await couchdbService.shutdown(); logger.info("CouchDB connection closed"); // Close server server.close(() => { logger.info("Server closed"); process.exit(0); }); } catch (error) { logger.error("Error during shutdown", error); process.exit(1); } });