const express = require("express"); const session = require("express-session"); const pgSession = require("connect-pg-simple")(session); const path = require("path"); const fs = require("fs"); const helmet = require("helmet"); const cors = require("cors"); const compressionMiddleware = require("./middleware/compression"); const { imageOptimization } = require("./middleware/imageOptimization"); const { pool, healthCheck } = require("./config/database"); const logger = require("./config/logger"); const { apiLimiter, authLimiter } = require("./config/rateLimiter"); const { errorHandler, notFoundHandler } = require("./middleware/errorHandler"); const { isDevelopment, getBaseDir, SESSION_CONFIG, BODY_PARSER_LIMITS, } = require("./config/constants"); require("dotenv").config(); // SAFEGUARD: Register global process error handlers FIRST require("./middleware/processHandlers"); const app = express(); const PORT = process.env.PORT || 5000; const baseDir = getBaseDir(); logger.info(`📁 Serving from: ${baseDir}`); // Start cache cleanup scheduler const { startCleanup, stopCleanup } = require("./middleware/cache"); startCleanup(); // Compression middleware - should be early in the chain app.use(compressionMiddleware); // Security middleware app.use( helmet({ contentSecurityPolicy: { directives: { defaultSrc: ["'self'"], styleSrc: [ "'self'", "'unsafe-inline'", "https://cdn.jsdelivr.net", "https://cdn.quilljs.com", "https://fonts.googleapis.com", ], scriptSrc: [ "'self'", "'unsafe-inline'", "'unsafe-eval'", "https://cdn.jsdelivr.net", "https://cdn.quilljs.com", ], scriptSrcAttr: ["'self'", "'unsafe-inline'"], imgSrc: ["'self'", "data:", "blob:"], fontSrc: [ "'self'", "https://cdn.jsdelivr.net", "https://fonts.gstatic.com", ], connectSrc: ["'self'", "https://cdn.jsdelivr.net"], objectSrc: ["'none'"], upgradeInsecureRequests: !isDevelopment() ? [] : null, }, }, hsts: { maxAge: 31536000, includeSubDomains: true, preload: true, }, frameguard: { action: "deny" }, xssFilter: true, noSniff: true, referrerPolicy: { policy: "strict-origin-when-cross-origin" }, }) ); // CORS configuration if (process.env.CORS_ORIGIN) { app.use( cors({ origin: process.env.CORS_ORIGIN.split(","), credentials: true, }) ); } // Trust proxy for rate limiting behind nginx app.set("trust proxy", 1); // Body parsers app.use(express.json({ limit: BODY_PARSER_LIMITS.JSON })); app.use( express.urlencoded({ extended: true, limit: BODY_PARSER_LIMITS.URLENCODED }) ); // Fallback middleware for missing product images const productImageFallback = (req, res, next) => { const imagePath = path.join( baseDir, "assets", "images", "products", req.path ); if (fs.existsSync(imagePath)) { return next(); } const placeholderPath = path.join( baseDir, "assets", "images", "products", "placeholder.jpg" ); logger.debug("Serving placeholder image", { requested: req.path }); res.sendFile(placeholderPath); }; app.use("/assets/images/products", productImageFallback); // Root redirect - serve the original HTML site app.get("/", (req, res) => { res.sendFile(path.join(baseDir, "public", "home.html")); }); // Redirect /index to /home app.get("/index", (req, res) => { res.redirect("/home"); }); app.use( express.static(path.join(baseDir, "public"), { index: false, maxAge: "30d", // Cache static files for 30 days etag: true, lastModified: true, setHeaders: (res, filepath) => { // Aggressive caching for versioned files if ( filepath.includes("?v=") || filepath.match(/\.(\w+)\.[a-f0-9]{8,}\./) ) { res.setHeader("Cache-Control", "public, max-age=31536000, immutable"); } }, }) ); app.use( "/assets", express.static(path.join(baseDir, "assets"), { maxAge: "365d", // Cache assets for 1 year etag: true, lastModified: true, immutable: true, setHeaders: (res, filepath) => { // Add immutable for all asset files res.setHeader("Cache-Control", "public, max-age=31536000, immutable"); // Add resource hints for fonts if (filepath.endsWith(".woff2") || filepath.endsWith(".woff")) { res.setHeader("Access-Control-Allow-Origin", "*"); } }, }) ); // Optimized image serving with aggressive caching app.use("/uploads", imageOptimization(path.join(baseDir, "uploads"))); app.use( "/uploads", express.static(path.join(baseDir, "uploads"), { maxAge: "365d", // Cache uploads for 1 year etag: true, lastModified: true, immutable: true, }) ); // Session middleware app.use( session({ store: new pgSession({ pool: pool, tableName: "session", createTableIfMissing: true, }), secret: process.env.SESSION_SECRET || "change-this-secret", resave: false, saveUninitialized: false, cookie: { secure: !isDevelopment(), httpOnly: true, maxAge: SESSION_CONFIG.COOKIE_MAX_AGE, sameSite: isDevelopment() ? "lax" : "strict", }, proxy: !isDevelopment(), name: SESSION_CONFIG.SESSION_NAME, rolling: true, // Reset session expiration on each request }) ); // Request logging app.use((req, res, next) => { logger.info("Request received", { method: req.method, path: req.path, ip: req.ip, }); next(); }); app.use((req, res, next) => { res.locals.session = req.session; res.locals.currentPath = req.path; next(); }); // API Routes const authRoutes = require("./routes/auth"); const adminRoutes = require("./routes/admin"); const publicRoutes = require("./routes/public"); const usersRoutes = require("./routes/users"); const uploadRoutes = require("./routes/upload"); // Admin redirect - handle /admin to redirect to login (must be before static files) app.get("/admin", (req, res) => { res.redirect("/admin/login"); }); app.get("/admin/", (req, res) => { res.redirect("/admin/login"); }); // URL Rewriting Middleware - Remove .html extension (must be before static files) app.use((req, res, next) => { // Skip API routes, static assets with extensions (except .html) if ( req.path.startsWith("/api/") || req.path.startsWith("/uploads/") || req.path.startsWith("/assets/") || (req.path.includes(".") && !req.path.endsWith(".html")) ) { return next(); } // Check if path is for admin area if (req.path.startsWith("/admin/")) { const cleanPath = req.path.replace(/\.html$/, "").replace(/^\/admin\//, ""); const htmlPath = path.join(baseDir, "admin", cleanPath + ".html"); if (fs.existsSync(htmlPath)) { return res.sendFile(htmlPath); } } // Check if path is for public pages (root level pages) if (!req.path.includes("/admin/")) { let cleanPath = req.path.replace(/^\//, "").replace(/\.html$/, ""); // Handle root path if (cleanPath === "" || cleanPath === "index") { cleanPath = "home"; } const htmlPath = path.join(baseDir, "public", cleanPath + ".html"); if (fs.existsSync(htmlPath)) { return res.sendFile(htmlPath); } } next(); }); // Apply rate limiting to API routes app.use("/api/admin/login", authLimiter); app.use("/api/admin/logout", authLimiter); app.use("/api", apiLimiter); // API Routes app.use("/api/admin", authRoutes); app.use("/api/admin", adminRoutes); app.use("/api/admin/users", usersRoutes); app.use("/api/admin", uploadRoutes); app.use("/api", publicRoutes); // Admin static files (must be after URL rewriting) app.use( "/admin", express.static(path.join(baseDir, "admin"), { maxAge: "1d", etag: true, lastModified: true, }) ); // Favicon route app.get("/favicon.ico", (req, res) => { res.sendFile(path.join(baseDir, "public", "favicon.svg")); }); // Old site (if needed for reference) app.get("/old", (req, res) => { res.sendFile(path.join(baseDir, "public", "index.html")); }); // Health check endpoint const { CRITICAL_IMAGES } = require("./config/constants"); app.get("/health", async (req, res) => { try { const dbHealth = await healthCheck(); const missingImages = CRITICAL_IMAGES.filter( (img) => !fs.existsSync(path.join(baseDir, img)) ); const assetsHealthy = missingImages.length === 0; const overallHealthy = dbHealth.healthy && assetsHealthy; const status = overallHealthy ? 200 : 503; res.status(status).json({ status: overallHealthy ? "ok" : "degraded", timestamp: new Date().toISOString(), uptime: process.uptime(), database: dbHealth, assets: { healthy: assetsHealthy, missingCritical: missingImages, }, memory: { used: Math.round(process.memoryUsage().heapUsed / 1024 / 1024), total: Math.round(process.memoryUsage().heapTotal / 1024 / 1024), }, }); } catch (error) { logger.error("Health check failed:", error); res.status(503).json({ status: "error", timestamp: new Date().toISOString(), error: "Health check failed", }); } }); // 404 handler app.use(notFoundHandler); // Global error handler app.use(errorHandler); const server = app.listen(PORT, "0.0.0.0", () => { logger.info("========================================"); logger.info(" SkyArtShop Backend Server"); logger.info("========================================"); logger.info(`🚀 Server running on http://localhost:${PORT}`); logger.info(`📦 Environment: ${process.env.NODE_ENV || "development"}`); logger.info(`🗄️ Database: PostgreSQL (${process.env.DB_NAME})`); logger.info("========================================"); }); // Graceful shutdown const gracefulShutdown = (signal) => { logger.info(`${signal} received, shutting down gracefully...`); // Stop cache cleanup stopCleanup(); server.close(() => { logger.info("HTTP server closed"); pool.end(() => { logger.info("Database pool closed"); process.exit(0); }); }); // Force close after 10 seconds setTimeout(() => { logger.error("Forced shutdown after timeout"); process.exit(1); }, 10000); }; process.on("SIGTERM", () => gracefulShutdown("SIGTERM")); process.on("SIGINT", () => gracefulShutdown("SIGINT")); process.on("unhandledRejection", (reason, promise) => { logger.error("Unhandled Rejection at:", { promise, reason }); }); process.on("uncaughtException", (error) => { logger.error("Uncaught Exception:", error); gracefulShutdown("UNCAUGHT_EXCEPTION"); });