require("dotenv").config(); const express = require("express"); const couchdbService = require("./services/couchdbService"); const sseService = require("./services/sseService"); const cors = require("cors"); const http = require("http"); 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 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 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 }); } // Make sse available to routes app.set("sse", sseService); // 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"); const sseRoutes = require("./routes/sse"); // 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(); // Get SSE stats const sseStats = sseService.getStats(); const isHealthy = couchdbStatus; res.status(isHealthy ? 200 : 503).json({ status: isHealthy ? "healthy" : "degraded", timestamp: new Date().toISOString(), uptime: process.uptime(), services: { couchdb: couchdbStatus ? "connected" : "disconnected", sse: { totalClients: sseStats.totalClients, totalTopics: sseStats.totalTopics } }, 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", sse: "unknown" }, error: error.message, }); } }); // 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.use("/api/sse", sseRoutes); 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 }; // 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); } });