255 lines
6.7 KiB
JavaScript
255 lines
6.7 KiB
JavaScript
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"],
|
|
scriptSrc: ["'self'", "'unsafe-inline'", "https://cdn.jsdelivr.net"],
|
|
imgSrc: ["'self'", "data:", "blob:"],
|
|
fontSrc: ["'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")));
|
|
|
|
// 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");
|
|
});
|