Files
SkyArtShop/backend/server.js

443 lines
12 KiB
JavaScript
Raw Normal View History

const express = require("express");
const session = require("express-session");
const pgSession = require("connect-pg-simple")(session);
const path = require("path");
2025-12-19 20:44:46 -06:00
const fs = require("fs");
const helmet = require("helmet");
const cors = require("cors");
2026-01-01 22:24:30 -06:00
const compressionMiddleware = require("./middleware/compression");
2026-01-04 17:52:37 -06:00
const { imageOptimization } = require("./middleware/imageOptimization");
2025-12-19 20:44:46 -06:00
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();
2026-01-04 17:52:37 -06:00
// SAFEGUARD: Register global process error handlers FIRST
require("./middleware/processHandlers");
const app = express();
const PORT = process.env.PORT || 5000;
2025-12-19 20:44:46 -06:00
const baseDir = getBaseDir();
2025-12-19 20:44:46 -06:00
logger.info(`📁 Serving from: ${baseDir}`);
2025-12-14 01:54:40 -06:00
2026-01-01 22:24:30 -06:00
// Start cache cleanup scheduler
const { startCleanup, stopCleanup } = require("./middleware/cache");
startCleanup();
// Compression middleware - should be early in the chain
app.use(compressionMiddleware);
2025-12-19 20:44:46 -06:00
// Security middleware
app.use(
helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
2025-12-24 00:13:23 -06:00
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'"],
2025-12-19 20:44:46 -06:00
imgSrc: ["'self'", "data:", "blob:"],
2025-12-24 00:13:23 -06:00
fontSrc: [
"'self'",
"https://cdn.jsdelivr.net",
"https://fonts.gstatic.com",
],
connectSrc: ["'self'", "https://cdn.jsdelivr.net"],
2026-01-04 17:52:37 -06:00
objectSrc: ["'none'"],
2026-01-19 01:17:43 -06:00
frameSrc: [
"'self'",
"https://www.google.com",
"https://maps.google.com",
"https://www.openstreetmap.org",
],
2026-01-04 17:52:37 -06:00
upgradeInsecureRequests: !isDevelopment() ? [] : null,
2025-12-19 20:44:46 -06:00
},
},
hsts: {
maxAge: 31536000,
includeSubDomains: true,
preload: true,
},
2026-01-19 01:17:43 -06:00
frameguard: { action: "sameorigin" },
2026-01-04 17:52:37 -06:00
xssFilter: true,
noSniff: true,
referrerPolicy: { policy: "strict-origin-when-cross-origin" },
2026-01-19 01:17:43 -06:00
}),
2025-12-19 20:44:46 -06:00
);
// CORS configuration
if (process.env.CORS_ORIGIN) {
app.use(
cors({
origin: process.env.CORS_ORIGIN.split(","),
credentials: true,
2026-01-19 01:17:43 -06:00
}),
2025-12-19 20:44:46 -06:00
);
}
// 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(
2026-01-19 01:17:43 -06:00
express.urlencoded({ extended: true, limit: BODY_PARSER_LIMITS.URLENCODED }),
2025-12-19 20:44:46 -06:00
);
// Fallback middleware for missing product images
const productImageFallback = (req, res, next) => {
const imagePath = path.join(
baseDir,
"assets",
"images",
"products",
2026-01-19 01:17:43 -06:00
req.path,
2025-12-19 20:44:46 -06:00
);
if (fs.existsSync(imagePath)) {
return next();
}
const placeholderPath = path.join(
baseDir,
"assets",
"images",
"products",
2026-01-19 01:17:43 -06:00
"placeholder.jpg",
2025-12-19 20:44:46 -06:00
);
logger.debug("Serving placeholder image", { requested: req.path });
res.sendFile(placeholderPath);
};
app.use("/assets/images/products", productImageFallback);
2025-12-14 01:54:40 -06:00
2026-01-01 22:24:30 -06:00
// 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,
etag: true,
lastModified: true,
2026-01-04 17:52:37 -06:00
setHeaders: (res, filepath) => {
2026-01-18 02:22:05 -06:00
// Short cache for CSS/JS files (use cache busting for updates)
if (filepath.endsWith(".css") || filepath.endsWith(".js")) {
res.setHeader("Cache-Control", "public, max-age=300"); // 5 minutes
} else if (filepath.endsWith(".html")) {
res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
} else {
res.setHeader("Cache-Control", "public, max-age=86400"); // 1 day default
2026-01-04 17:52:37 -06:00
}
},
2026-01-19 01:17:43 -06:00
}),
2026-01-01 22:24:30 -06:00
);
app.use(
"/assets",
2026-01-18 02:22:05 -06:00
express.static(path.join(baseDir, "public", "assets"), {
2026-01-01 22:24:30 -06:00
etag: true,
lastModified: true,
2026-01-04 17:52:37 -06:00
setHeaders: (res, filepath) => {
2026-01-18 02:22:05 -06:00
// Very short cache for CSS/JS to see changes quickly (with cache busting)
if (filepath.endsWith(".css") || filepath.endsWith(".js")) {
res.setHeader("Cache-Control", "public, max-age=300"); // 5 minutes
} else if (
filepath.endsWith(".woff2") ||
filepath.endsWith(".woff") ||
filepath.endsWith(".ttf")
) {
res.setHeader("Cache-Control", "public, max-age=31536000, immutable");
2026-01-04 17:52:37 -06:00
res.setHeader("Access-Control-Allow-Origin", "*");
2026-01-18 02:22:05 -06:00
} else {
res.setHeader("Cache-Control", "public, max-age=86400"); // 1 day for images
2026-01-04 17:52:37 -06:00
}
},
2026-01-19 01:17:43 -06:00
}),
2026-01-01 22:24:30 -06:00
);
2026-01-04 17:52:37 -06:00
// Optimized image serving with aggressive caching
app.use("/uploads", imageOptimization(path.join(baseDir, "uploads")));
2026-01-01 22:24:30 -06:00
app.use(
"/uploads",
express.static(path.join(baseDir, "uploads"), {
2026-01-04 17:52:37 -06:00
maxAge: "365d", // Cache uploads for 1 year
2026-01-01 22:24:30 -06:00
etag: true,
lastModified: true,
2026-01-04 17:52:37 -06:00
immutable: true,
2026-01-19 01:17:43 -06:00
}),
2026-01-01 22:24:30 -06:00
);
2025-12-19 20:44:46 -06:00
// Session middleware
2026-01-18 02:22:05 -06:00
// SECURITY: Ensure SESSION_SECRET is set - fail fast if missing
if (
!process.env.SESSION_SECRET ||
process.env.SESSION_SECRET === "change-this-secret"
) {
if (!isDevelopment()) {
logger.error(
2026-01-19 01:17:43 -06:00
"CRITICAL: SESSION_SECRET environment variable must be set in production!",
2026-01-18 02:22:05 -06:00
);
process.exit(1);
}
logger.warn(
2026-01-19 01:17:43 -06:00
"WARNING: Using insecure session secret. Set SESSION_SECRET in production!",
2026-01-18 02:22:05 -06:00
);
}
app.use(
session({
store: new pgSession({
pool: pool,
tableName: "session",
createTableIfMissing: true,
}),
2026-01-18 02:22:05 -06:00
secret:
process.env.SESSION_SECRET ||
(isDevelopment() ? "dev-secret-change-in-production" : ""),
resave: false,
saveUninitialized: false,
cookie: {
2025-12-19 20:44:46 -06:00
secure: !isDevelopment(),
httpOnly: true,
2025-12-19 20:44:46 -06:00
maxAge: SESSION_CONFIG.COOKIE_MAX_AGE,
2026-01-04 17:52:37 -06:00
sameSite: isDevelopment() ? "lax" : "strict",
},
2025-12-19 20:44:46 -06:00
proxy: !isDevelopment(),
name: SESSION_CONFIG.SESSION_NAME,
2026-01-04 17:52:37 -06:00
rolling: true, // Reset session expiration on each request
2026-01-19 01:17:43 -06:00
}),
);
2025-12-19 20:44:46 -06:00
// 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");
2025-12-14 01:54:40 -06:00
const uploadRoutes = require("./routes/upload");
2026-01-18 02:22:05 -06:00
const customerAuthRoutes = require("./routes/customer-auth");
const customerCartRoutes = require("./routes/customer-cart");
// Admin redirect - handle /admin to redirect to login (must be before static files)
app.get("/admin", (req, res) => {
2026-01-01 22:24:30 -06:00
res.redirect("/admin/login");
});
app.get("/admin/", (req, res) => {
2026-01-01 22:24:30 -06:00
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);
}
}
2026-01-18 02:22:05 -06:00
// Handle dynamic product pages: /product/:slug -> product.html
if (req.path.startsWith("/product/")) {
const productHtmlPath = path.join(baseDir, "public", "product.html");
if (fs.existsSync(productHtmlPath)) {
return res.sendFile(productHtmlPath);
}
}
2026-01-01 22:24:30 -06:00
// 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();
});
2025-12-19 20:44:46 -06:00
// Apply rate limiting to API routes
app.use("/api/admin/login", authLimiter);
app.use("/api/admin/logout", authLimiter);
2026-01-18 02:22:05 -06:00
app.use("/api/customers/login", authLimiter);
app.use("/api/customers/signup", authLimiter);
2025-12-19 20:44:46 -06:00
app.use("/api", apiLimiter);
// API Routes
app.use("/api/admin", authRoutes);
app.use("/api/admin", adminRoutes);
app.use("/api/admin/users", usersRoutes);
2025-12-14 01:54:40 -06:00
app.use("/api/admin", uploadRoutes);
2026-01-18 02:22:05 -06:00
app.use("/api/customers", customerAuthRoutes);
app.use("/api/customers", customerCartRoutes);
app.use("/api", publicRoutes);
2026-01-01 22:24:30 -06:00
// Admin static files (must be after URL rewriting)
app.use(
"/admin",
express.static(path.join(baseDir, "admin"), {
maxAge: "1d",
etag: true,
lastModified: true,
2026-01-19 01:17:43 -06:00
}),
2026-01-01 22:24:30 -06:00
);
2025-12-24 00:13:23 -06:00
// Favicon route
app.get("/favicon.ico", (req, res) => {
res.sendFile(path.join(baseDir, "public", "favicon.svg"));
});
2026-01-01 22:24:30 -06:00
// Old site (if needed for reference)
app.get("/old", (req, res) => {
2025-12-14 01:54:40 -06:00
res.sendFile(path.join(baseDir, "public", "index.html"));
});
2025-12-19 20:44:46 -06:00
// 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(
2026-01-19 01:17:43 -06:00
(img) => !fs.existsSync(path.join(baseDir, img)),
2025-12-19 20:44:46 -06:00
);
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",
});
}
});
2025-12-19 20:44:46 -06:00
// 404 handler
app.use(notFoundHandler);
2025-12-19 20:44:46 -06:00
// Global error handler
app.use(errorHandler);
2025-12-19 20:44:46 -06:00
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("========================================");
});
2025-12-19 20:44:46 -06:00
// Graceful shutdown
const gracefulShutdown = (signal) => {
logger.info(`${signal} received, shutting down gracefully...`);
2026-01-01 22:24:30 -06:00
// Stop cache cleanup
stopCleanup();
2025-12-19 20:44:46 -06:00
server.close(() => {
logger.info("HTTP server closed");
pool.end(() => {
logger.info("Database pool closed");
process.exit(0);
});
});
2025-12-19 20:44:46 -06:00
// 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");
});