Files
SkyArtShop/backend/server.js

278 lines
7.2 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");
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;
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
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"],
2025-12-19 20:44:46 -06:00
},
},
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);
2025-12-14 01:54:40 -06:00
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")));
2025-12-19 20:44:46 -06:00
// Session middleware
app.use(
session({
store: new pgSession({
pool: pool,
tableName: "session",
createTableIfMissing: true,
}),
2025-12-19 20:44:46 -06:00
secret: process.env.SESSION_SECRET || "change-this-secret",
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,
sameSite: "lax",
},
2025-12-19 20:44:46 -06:00
proxy: !isDevelopment(),
name: SESSION_CONFIG.SESSION_NAME,
})
);
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");
// 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");
});
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);
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);
app.use("/api", publicRoutes);
// Admin static files (must be after redirect routes)
2025-12-14 01:54:40 -06:00
app.use("/admin", express.static(path.join(baseDir, "admin")));
2025-12-24 00:13:23 -06:00
// Favicon route
app.get("/favicon.ico", (req, res) => {
res.sendFile(path.join(baseDir, "public", "favicon.svg"));
});
2025-12-14 01:54:40 -06:00
// Root redirect to home page
app.get("/", (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(
(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",
});
}
});
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...`);
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");
});