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 { 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(); const app = express(); const PORT = process.env.PORT || 5000; const baseDir = getBaseDir(); logger.info(`📁 Serving from: ${baseDir}`); // 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"], }, }, hsts: { maxAge: 31536000, includeSubDomains: true, preload: true, }, }) ); // 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); app.use(express.static(path.join(baseDir, "public"))); app.use("/assets", express.static(path.join(baseDir, "assets"))); app.use("/uploads", express.static(path.join(baseDir, "uploads"))); // 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: "lax", }, proxy: !isDevelopment(), name: SESSION_CONFIG.SESSION_NAME, }) ); // 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.html"); }); app.get("/admin/", (req, res) => { res.redirect("/admin/login.html"); }); // 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 redirect routes) app.use("/admin", express.static(path.join(baseDir, "admin"))); // Favicon route app.get("/favicon.ico", (req, res) => { res.sendFile(path.join(baseDir, "public", "favicon.svg")); }); // Root redirect to home page app.get("/", (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...`); 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"); });