import bodyParser from "body-parser"; import compression from "compression"; import cors from "cors"; import dotenv, { config } from "dotenv"; import express, { Express } from "express"; import helmet from "helmet"; import morgan from "morgan"; import nocache from "nocache"; import path, { resolve } from "path"; import responseTime from "response-time"; import winston from "winston"; import DailyRotateFile from "winston-daily-rotate-file"; import BillRequestValidator from "./bills/billRequestValidator.js"; import { BillsListMedia } from "./bills/billsListMedia.js"; import { BillsMediaUploadMulter, BillsUploadMedia } from "./bills/billsUploadMedia.js"; import validateJobRequest from "./jobs/jobRequestValidator.js"; import { JobsDeleteMedia } from "./jobs/jobsDeleteMedia.js"; import { jobsDownloadMedia } from "./jobs/jobsDownloadMedia.js"; import { JobsListMedia } from "./jobs/jobsListMedia.js"; import { JobsMoveMedia } from "./jobs/jobsMoveMedia.js"; import { JobMediaUploadMulter, jobsUploadMedia } from "./jobs/jobsUploadMedia.js"; import InitServer, { FolderPaths } from "./util/serverInit.js"; import ValidateImsToken from "./util/validateToken.js"; import { dailyS3Scheduler } from "./util/dailyS3Scheduler.js"; import { analyzeJobsDirectory } from "./util/s3Sync.js"; dotenv.config({ path: resolve(process.cwd(), `.env.${process.env.NODE_ENV || "development"}`) }); // Global error handlers process.on("uncaughtException", (error) => { console.error("Uncaught Exception:", error); process.exit(1); }); process.on("unhandledRejection", (reason, promise) => { console.error("Unhandled Rejection at:", promise, "reason:", reason); process.exit(1); }); // Logger setup const commonTransportConfig = { maxSize: "20m", maxFiles: 14, tailable: true, zippedArchive: true, format: winston.format.combine(winston.format.timestamp(), winston.format.json()), datePattern: "YYYY-MM-DD" }; const baseFormat = winston.format.combine( winston.format.timestamp(), winston.format.errors({ stack: true }), winston.format.json(), winston.format.prettyPrint() ); export const logger = winston.createLogger({ format: baseFormat, level: "http", levels: { ...winston.config.syslog.levels, http: 8 }, exceptionHandlers: [ new DailyRotateFile({ filename: path.join(FolderPaths.Root, "logs", "exceptions-%DATE%.log"), ...commonTransportConfig }) ], rejectionHandlers: [ new DailyRotateFile({ filename: path.join(FolderPaths.Root, "logs", "rejections-%DATE%.log"), ...commonTransportConfig }) ], transports: [ new DailyRotateFile({ filename: path.join(FolderPaths.Root, "logs", "errors-%DATE%.log"), level: "error", ...commonTransportConfig }), new DailyRotateFile({ filename: path.join(FolderPaths.Root, "logs", "debug-%DATE%.log"), level: "debug", ...commonTransportConfig }), new DailyRotateFile({ filename: path.join(FolderPaths.Root, "logs", "ALL-%DATE%.log"), ...commonTransportConfig }), new winston.transports.Console({ format: winston.format.combine(winston.format.colorize(), winston.format.timestamp(), winston.format.simple()) }) ] }); // App init const app: Express = express(); const port = process.env.PORT; app.disable("x-powered-by"); app.set("trust proxy", 1); // Middleware order: security, logging, parsing, etc. app.set("etag", false); app.use(helmet({ crossOriginResourcePolicy: { policy: "cross-origin" } })); app.use(nocache()); app.use(cors()); app.use(compression()); app.use(responseTime()); app.use(bodyParser.json({ limit: "1000mb" })); app.use(bodyParser.urlencoded({ limit: "1000mb", extended: true })); app.use((req, res, next) => { res.setHeader("Connection", "keep-alive"); next(); }); const morganMiddleware = morgan("combined", { stream: { write: (message) => logger.http(message.trim()) } }); app.use(morganMiddleware); //Asynchronously check the headers for a bodyshopid. If it exists, write it to a json file in the root directory. Only do this if the bodyshopid has not already been logged once since server start. const loggedBodyshopIds = new Set(); app.use((req, res, next) => { //Asynchronously check the headers for a bodyshopid. If it exists, write it to a json file in the root directory. Only do this if the bodyshopid has not already been logged once since server start. const bodyshopId = req.headers.bodyshopid as string; if (bodyshopId && !loggedBodyshopIds.has(bodyshopId)) { loggedBodyshopIds.add(bodyshopId); // Asynchronously write to file without blocking the request (async () => { try { const fs = await import("fs/promises"); let existingIds: string[] = []; try { const fileContent = await fs.readFile(FolderPaths.Config, "utf-8"); const configFile = JSON.parse(fileContent); existingIds = configFile.bodyshopIds || []; } catch { // File doesn't exist or is invalid, start with empty array } if (!existingIds.includes(bodyshopId)) { existingIds.push(bodyshopId); await fs.writeFile(FolderPaths.Config, JSON.stringify({ bodyshopIds: existingIds }, null, 2)); logger.info(`Logged new bodyshop ID: ${bodyshopId}`); } } catch (error) { logger.error("Failed to log bodyshop ID:", error); } })(); } next(); }); // Job endpoints app.post("/jobs/list", ValidateImsToken, validateJobRequest, JobsListMedia); app.post("/jobs/upload", ValidateImsToken, JobMediaUploadMulter.array("file"), validateJobRequest, jobsUploadMedia); app.post("/jobs/download", ValidateImsToken, validateJobRequest, jobsDownloadMedia); app.post("/jobs/move", ValidateImsToken, JobsMoveMedia); app.post("/jobs/delete", ValidateImsToken, JobsDeleteMedia); // Bill endpoints app.post("/bills/list", BillRequestValidator, BillsListMedia); app.post( "/bills/upload", ValidateImsToken, BillsMediaUploadMulter.array("file"), BillRequestValidator, BillsUploadMedia ); // Health and root app.get("/", ValidateImsToken, (req, res) => { res.send("IMS running."); }); app.get("/health", (req, res) => { res.status(200).send("OK"); }); // S3 sync status endpoint app.get("/sync/status", ValidateImsToken, async (req, res) => { try { const status = await dailyS3Scheduler.getStatus(); res.json(status); } catch (error) { logger.error("Failed to get sync status:", error); const errorMessage = error instanceof Error ? error.message : "Unknown error"; res.status(500).json({ error: errorMessage }); } }); // Manual S3 sync trigger endpoint (for testing) // app.post("/sync/trigger", ValidateImsToken, async (req, res) => { // try { // await dailyS3Scheduler.triggerManualSync(); // res.json({ success: true, message: "Manual sync triggered successfully" }); // } catch (error) { // logger.error("Manua--l sync failed:", error); // const errorMessage = error instanceof Error ? error.message : "Unknown error"; // res.status(500).json({ success: false, message: "Manual sync failed", error: errorMessage }); // } // }); // Jobs directory analysis endpoint app.get("/jobs/analysis", ValidateImsToken, async (req, res) => { try { const analysis = await analyzeJobsDirectory(); res.json(analysis); } catch (error) { logger.error("Failed to analyze jobs directory:", JSON.stringify(error, null, 2)); const errorMessage = error instanceof Error ? error.message : "Unknown error"; res.status(500).json({ success: false, message: "Jobs analysis failed", error: errorMessage }); } }); // Static files InitServer(); app.use(FolderPaths.StaticPath, express.static(FolderPaths.Root, { etag: false, maxAge: 30 * 1000 })); app.use("/assets", express.static("/assets", { etag: false, maxAge: 30 * 1000 })); // Start the daily S3 sync scheduler dailyS3Scheduler .start() .then(() => { logger.info("Sync scheduler started successfully."); }) .catch((error) => { logger.error("Failed to start sync scheduler:", error); }); app.listen(port, () => { logger.info(`ImEX Media Server is running at http://localhost:${port}`); });