235 lines
8.0 KiB
TypeScript
235 lines
8.0 KiB
TypeScript
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<string>();
|
|
|
|
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}`);
|
|
});
|