diff --git a/server.js b/server.js index 1f784daf1..3f6fc28b6 100644 --- a/server.js +++ b/server.js @@ -1,66 +1,58 @@ -const main = async () => { - // Import core modules - const express = require("express"); - const cors = require("cors"); - const bodyParser = require("body-parser"); - const path = require("path"); - const compression = require("compression"); - const cookieParser = require("cookie-parser"); - const http = require("http"); - const { Server } = require("socket.io"); - const { createClient } = require("redis"); - const { createAdapter } = require("@socket.io/redis-adapter"); +const express = require("express"); +const cors = require("cors"); +const bodyParser = require("body-parser"); +const path = require("path"); +const compression = require("compression"); +const cookieParser = require("cookie-parser"); +const http = require("http"); +const { Server } = require("socket.io"); +const { createClient } = require("redis"); +const { createAdapter } = require("@socket.io/redis-adapter"); +const logger = require("./server/utils/logger"); - // Load environment variables - require("dotenv").config({ - path: path.resolve(process.cwd(), `.env.${process.env.NODE_ENV || "development"}`) - }); +// This file offers the following exports +// redisClient: Redis client for external use +// io: Socket.IO server for external use - // Import custom utilities and handlers - const logger = require("./server/utils/logger"); +// Load environment variables +require("dotenv").config({ + path: path.resolve(process.cwd(), `.env.${process.env.NODE_ENV || "development"}`) +}); - // Express app and server setup - const app = express(); - const port = process.env.PORT || 5000; - const server = http.createServer(app); +/** + * CORS Origin for Socket.IO + * @type {string[][]} + */ +const SOCKETIO_CORS_ORIGIN = [ + [ + "https://test.imex.online", + "https://www.test.imex.online", + "http://localhost:3000", + "https://imex.online", + "https://www.imex.online", + "https://romeonline.io", + "https://www.romeonline.io", + "https://beta.test.romeonline.io", + "https://www.beta.test.romeonline.io", + "https://beta.romeonline.io", + "https://www.beta.romeonline.io", + "https://beta.test.imex.online", + "https://www.beta.test.imex.online", + "https://beta.imex.online", + "https://www.beta.imex.online", + "https://www.test.promanager.web-est.com", + "https://test.promanager.web-est.com", + "https://www.promanager.web-est.com", + "https://www.promanager.web-est.com", + "http://localhost:3000" + ] +]; - // Redis client setup for Pub/Sub and Key-Value Store - const pubClient = createClient({ url: process.env.REDIS_URL || "redis://localhost:6379" }); - const subClient = pubClient.duplicate(); - - // Clean up on exit - process.on("SIGINT", async () => { - await Promise.all([pubClient.disconnect(), subClient.disconnect()]); - process.exit(0); - }); - - // Connect Redis clients - await Promise.all([pubClient.connect(), subClient.connect()]); - - // Redis Pub/Sub adapter for Socket.IO - const io = new Server(server, { - path: "/ws", - adapter: createAdapter(pubClient, subClient), - cors: { - origin: [ - "https://test.imex.online", - "https://www.test.imex.online", - "http://localhost" - // Other allowed origins - ], - methods: ["GET", "POST"], - credentials: true, - exposedHeaders: ["set-cookie"] - } - }); - - // Export io and Redis client for external use - exports.io = io; - exports.redisClient = pubClient; - - require("./server/web-sockets/web-socket"); - - // Middleware +/** + * Middleware for Express app + * @param app + */ +const applyMiddleware = (app) => { app.use(compression()); app.use(cookieParser()); app.use(bodyParser.json({ limit: "50mb" })); @@ -72,15 +64,93 @@ const main = async () => { req.logger = logger; next(); }); +}; - // Route groupings +/** + * Route groupings for Express app + * @param app + */ +const applyRoutes = (app) => { app.use("/", require("./server/routes/miscellaneousRoutes")); - // Other routes... + app.use("/notifications", require("./server/routes/notificationsRoutes")); + app.use("/render", require("./server/routes/renderRoutes")); + app.use("/mixdata", require("./server/routes/mixDataRoutes")); + app.use("/accounting", require("./server/routes/accountingRoutes")); + app.use("/qbo", require("./server/routes/qboRoutes")); + app.use("/media", require("./server/routes/mediaRoutes")); + app.use("/sms", require("./server/routes/smsRoutes")); + app.use("/job", require("./server/routes/jobRoutes")); + app.use("/scheduling", require("./server/routes/schedulingRoutes")); + app.use("/utils", require("./server/routes/utilRoutes")); + app.use("/data", require("./server/routes/dataRoutes")); + app.use("/adm", require("./server/routes/adminRoutes")); + app.use("/tech", require("./server/routes/techRoutes")); + app.use("/intellipay", require("./server/routes/intellipayRoutes")); + app.use("/cdk", require("./server/routes/cdkRoutes")); + app.use("/csi", require("./server/routes/csiRoutes")); + app.use("/payroll", require("./server/routes/payrollRoutes")); // Default route for forbidden access app.get("/", (req, res) => { res.status(200).send("Access Forbidden."); }); +}; + +/** + * Main function to start the server + * @returns {Promise} + */ +const main = async () => { + // Express app and server setup + const app = express(); + const port = process.env.PORT || 5000; + const server = http.createServer(app); + + // Redis client setup for Pub/Sub and Key-Value Store + const pubClient = createClient({ url: process.env.REDIS_URL || "redis://localhost:6379" }); + const subClient = pubClient.duplicate(); + + // Add Error listeners to Redis clients + pubClient.on("error", (err) => logger.log(`Redis pubClient error: ${err}`, "ERROR", "redis")); + subClient.on("error", (err) => logger.log(`Redis subClient error: ${err}`, "ERROR", "redis")); + + // Connect Redis clients + try { + await Promise.all([pubClient.connect(), subClient.connect()]); + } catch (redisError) { + logger.log(`Failed to connect to Redis`, "ERROR", "redis", redisError); + } + + // Clean up on exit + process.on("SIGINT", async () => { + await Promise.all([pubClient.disconnect(), subClient.disconnect()]); + process.exit(0); + }); + + // Redis Pub/Sub adapter for Socket.IO + // Export io and Redis client for external use + exports.io = new Server(server, { + path: "/ws", + adapter: createAdapter(pubClient, subClient), + cors: { + origin: SOCKETIO_CORS_ORIGIN, + methods: ["GET", "POST"], + credentials: true, + exposedHeaders: ["set-cookie"] + } + }); + + // Export Redis client for external use + exports.redisClient = pubClient; + + // Include the Socket IO references + require("./server/web-sockets/web-socket"); + + // Middleware + applyMiddleware(app); + + // Route groupings + applyRoutes(app); try { await server.listen(port); @@ -96,4 +166,5 @@ const main = async () => { }; // Start server +// noinspection JSIgnoredPromiseFromCall main(); diff --git a/server/web-sockets/web-socket.js b/server/web-sockets/web-socket.js index a90d32aef..c27e96374 100644 --- a/server/web-sockets/web-socket.js +++ b/server/web-sockets/web-socket.js @@ -5,61 +5,60 @@ require("dotenv").config({ const { io } = require("../../server"); const { admin } = require("../firebase/firebase-handler"); +const { isArray } = require("lodash"); +const logger = require("../utils/logger"); const { default: CdkJobExport, CdkSelectedCustomer } = require("../cdk/cdk-job-export"); const CdkGetMakes = require("../cdk/cdk-get-makes").default; const CdkCalculateAllocations = require("../cdk/cdk-calculate-allocations").default; -const { isArray } = require("lodash"); -const logger = require("../utils/logger"); const { default: PbsExportJob, PbsSelectedCustomer } = require("../accounting/pbs/pbs-job-export"); - const { PbsCalculateAllocationsAp, PbsExportAp } = require("../accounting/pbs/pbs-ap-allocations"); -io.use(function (socket, next) { - try { - if (socket.handshake.auth.token) { - admin - .auth() - .verifyIdToken(socket.handshake.auth.token) - .then((user) => { - socket.user = user; - next(); - }) - .catch((error) => { - next(new Error("Authentication error", JSON.stringify(error))); - }); - } else { - next(new Error("Authentication error - no authorization token.")); - } - } catch (error) { - console.log("Uncaught connection error:::", error); - logger.log("websocket-connection-error", "error", null, null, { - token: socket.handshake.auth.token, - ...error - }); - next(new Error(`Authentication error ${error}`)); - } -}); +// Middleware for verifying tokens via Firebase authentication +function socketAuthMiddleware(socket, next) { + const token = socket.handshake.auth.token; -io.on("connection", (socket) => { + if (!token) return next(new Error("Authentication error - no authorization token.")); + + admin + .auth() + .verifyIdToken(token) + .then((user) => { + socket.user = user; + next(); + }) + .catch((error) => { + next(new Error("Authentication error", JSON.stringify(error))); + }); +} + +// Register all socket events for a given socket connection +function registerSocketEvents(socket) { socket.log_level = "TRACE"; createLogEvent(socket, "DEBUG", `Connected and Authenticated.`); - socket.on("set-log-level", (level) => { - socket.log_level = level; - socket.emit("log-event", { - timestamp: new Date(), - level: "INFO", - message: `Updated log level to ${level}` - }); - }); + // Register CDK-related socket events + registerCdkEvents(socket); - ///CDK - socket.on("cdk-export-job", (jobid) => { - CdkJobExport(socket, jobid); + // Register PBS AR-related socket events + registerPbsArEvents(socket); + + // Register PBS AP-related socket events + registerPbsApEvents(socket); + + // Register room and broadcasting events + registerRoomAndBroadcastEvents(socket); + + // Handle socket disconnection + socket.on("disconnect", () => { + createLogEvent(socket, "DEBUG", `User disconnected.`); }); +} + +// CDK-specific socket events +function registerCdkEvents(socket) { + socket.on("cdk-export-job", (jobid) => CdkJobExport(socket, jobid)); socket.on("cdk-selected-customer", (selectedCustomerId) => { - createLogEvent(socket, "DEBUG", `User selected customer ID ${selectedCustomerId}`); - socket.selectedCustomerId = selectedCustomerId; + createLogEvent(socket, "DEBUG", `User selected customer ID ${selectedCustomerId}`); CdkSelectedCustomer(socket, selectedCustomerId); }); @@ -68,44 +67,40 @@ io.on("connection", (socket) => { const makes = await CdkGetMakes(socket, cdk_dealerid); callback(makes); } catch (error) { - createLogEvent(socket, "ERROR", `Error in cdk-get-makes WS call. ${JSON.stringify(error, null, 2)}`); + createLogEvent(socket, "ERROR", `Error in cdk-get-makes WS call. ${JSON.stringify(error)}`); } }); socket.on("cdk-calculate-allocations", async (jobid, callback) => { const allocations = await CdkCalculateAllocations(socket, jobid); createLogEvent(socket, "DEBUG", `Allocations calculated.`); - createLogEvent(socket, "TRACE", `Allocations calculated. ${JSON.stringify(allocations, null, 2)}`); - + createLogEvent(socket, "TRACE", `Allocations details: ${JSON.stringify(allocations)}`); callback(allocations); }); - //END CDK +} - //PBS AR +// PBS AR-specific socket events +function registerPbsArEvents(socket) { socket.on("pbs-calculate-allocations", async (jobid, callback) => { const allocations = await CdkCalculateAllocations(socket, jobid); - createLogEvent(socket, "DEBUG", `Allocations calculated.`); - createLogEvent(socket, "TRACE", `Allocations calculated. ${JSON.stringify(allocations, null, 2)}`); - + createLogEvent(socket, "DEBUG", `PBS AR allocations calculated.`); + createLogEvent(socket, "TRACE", `Allocations details: ${JSON.stringify(allocations)}`); callback(allocations); }); - socket.on("pbs-export-job", (jobid) => { - PbsExportJob(socket, jobid); - }); - + socket.on("pbs-export-job", (jobid) => PbsExportJob(socket, jobid)); socket.on("pbs-selected-customer", (selectedCustomerId) => { - createLogEvent(socket, "DEBUG", `User selected customer ID ${selectedCustomerId}`); - socket.selectedCustomerId = selectedCustomerId; + createLogEvent(socket, "DEBUG", `PBS AR selected customer ID ${selectedCustomerId}`); PbsSelectedCustomer(socket, selectedCustomerId); }); - //End PBS AR +} - //PBS AP +// PBS AP-specific socket events +function registerPbsApEvents(socket) { socket.on("pbs-calculate-allocations-ap", async (billids, callback) => { const allocations = await PbsCalculateAllocationsAp(socket, billids); - createLogEvent(socket, "DEBUG", `AP Allocations calculated.`); - createLogEvent(socket, "TRACE", `Allocations calculated. ${JSON.stringify(allocations, null, 2)}`); + createLogEvent(socket, "DEBUG", `PBS AP allocations calculated.`); + createLogEvent(socket, "TRACE", `Allocations details: ${JSON.stringify(allocations)}`); socket.apAllocations = allocations; callback(allocations); }); @@ -114,81 +109,48 @@ io.on("connection", (socket) => { socket.txEnvelope = txEnvelope; PbsExportAp(socket, { billids, txEnvelope }); }); +} - //END PBS AP - - socket.on("disconnect", () => { - createLogEvent(socket, "DEBUG", `User disconnected.`); - }); - +// Room management and broadcasting events +function registerRoomAndBroadcastEvents(socket) { socket.on("join-bodyshop-room", (bodyshopUUID) => { - socket.join(bodyshopUUID); // Join the room identified by the bodyshop UUID + socket.join(bodyshopUUID); createLogEvent(socket, "DEBUG", `Client joined bodyshop room: ${bodyshopUUID}`); }); - // Optionally handle leaving the room socket.on("leave-bodyshop-room", (bodyshopUUID) => { - socket.leave(bodyshopUUID); // Leave the room identified by the bodyshop UUID + socket.leave(bodyshopUUID); createLogEvent(socket, "DEBUG", `Client left bodyshop room: ${bodyshopUUID}`); }); - // Broadcast to specific bodyshop rooms socket.on("broadcast-to-bodyshop", (bodyshopUUID, message) => { io.to(bodyshopUUID).emit("bodyshop-message", message); createLogEvent(socket, "INFO", `Broadcasted message to bodyshop ${bodyshopUUID}`); }); -}); +} +// Logging helper functions function createLogEvent(socket, level, message) { if (LogLevelHierarchy(socket.log_level) >= LogLevelHierarchy(level)) { console.log(`[WS LOG EVENT] ${level} - ${new Date()} - ${socket.user.email} - ${socket.id} - ${message}`); - socket.emit("log-event", { - timestamp: new Date(), - level, - message - }); - - logger.log("ws-log-event", level, socket.user.email, socket.recordid, { - wsmessage: message - }); + socket.emit("log-event", { timestamp: new Date(), level, message }); + logger.log("ws-log-event", level, socket.user.email, socket.recordid, { wsmessage: message }); if (socket.logEvents && isArray(socket.logEvents)) { - socket.logEvents.push({ - timestamp: new Date(), - level, - message - }); + socket.logEvents.push({ timestamp: new Date(), level, message }); } - // if (level === "ERROR") { - // throw new Error(message); - // } } } function createJsonEvent(socket, level, message, json) { if (LogLevelHierarchy(socket.log_level) >= LogLevelHierarchy(level)) { - console.log(`[WS LOG EVENT] ${level} - ${new Date()} - ${socket.user.email} - ${socket.id} - ${message}`); - socket.emit("log-event", { - timestamp: new Date(), - level, - message - }); + socket.emit("log-event", { timestamp: new Date(), level, message }); } - logger.log("ws-log-event-json", level, socket.user.email, socket.recordid, { - wsmessage: message, - json - }); + logger.log("ws-log-event-json", level, socket.user.email, socket.recordid, { wsmessage: message, json }); if (socket.logEvents && isArray(socket.logEvents)) { - socket.logEvents.push({ - timestamp: new Date(), - level, - message - }); + socket.logEvents.push({ timestamp: new Date(), level, message }); } - // if (level === "ERROR") { - // throw new Error(message); - // } } function createXmlEvent(socket, xml, message, isError = false) { @@ -205,41 +167,25 @@ function createXmlEvent(socket, xml, message, isError = false) { isError ? "ERROR" : "TRACE", socket.user.email, socket.recordid, - { - wsmessage: message, - xml - } + { wsmessage: message, xml } ); if (socket.logEvents && isArray(socket.logEvents)) { - socket.logEvents.push({ - timestamp: new Date(), - level: isError ? "ERROR" : "TRACE", - message, - xml - }); + socket.logEvents.push({ timestamp: new Date(), level: isError ? "ERROR" : "TRACE", message, xml }); } } +// Log level hierarchy function LogLevelHierarchy(level) { - switch (level) { - case "XML": - return 5; - case "TRACE": - return 5; - case "DEBUG": - return 4; - case "INFO": - return 3; - case "WARNING": - return 2; - case "ERROR": - return 1; - default: - return 3; - } + const levels = { XML: 5, TRACE: 5, DEBUG: 4, INFO: 3, WARNING: 2, ERROR: 1 }; + return levels[level] || 3; } +// Socket.IO Middleware and Connection +io.use(socketAuthMiddleware); +io.on("connection", registerSocketEvents); + +// Export logging helpers exports.createLogEvent = createLogEvent; exports.createXmlEvent = createXmlEvent; exports.createJsonEvent = createJsonEvent;