diff --git a/client/package-lock.json b/client/package-lock.json
index 2784d0830..93b492582 100644
--- a/client/package-lock.json
+++ b/client/package-lock.json
@@ -9,7 +9,7 @@
"version": "0.2.1",
"hasInstallScript": true,
"dependencies": {
- "@ant-design/pro-layout": "^7.20.2",
+ "@ant-design/pro-layout": "^7.19.12",
"@apollo/client": "^3.11.8",
"@emotion/is-prop-valid": "^1.3.1",
"@fingerprintjs/fingerprintjs": "^4.5.0",
@@ -20,7 +20,7 @@
"@splitsoftware/splitio-react": "^1.13.0",
"@tanem/react-nprogress": "^5.0.51",
"@vitejs/plugin-react": "^4.3.1",
- "antd": "^5.21.0",
+ "antd": "^5.20.1",
"apollo-link-logger": "^2.0.1",
"apollo-link-sentry": "^3.3.0",
"autosize": "^6.0.1",
diff --git a/client/package.json b/client/package.json
index a3f5d7de8..38d148706 100644
--- a/client/package.json
+++ b/client/package.json
@@ -8,7 +8,7 @@
"private": true,
"proxy": "http://localhost:4000",
"dependencies": {
- "@ant-design/pro-layout": "^7.20.2",
+ "@ant-design/pro-layout": "^7.19.12",
"@apollo/client": "^3.11.8",
"@emotion/is-prop-valid": "^1.3.1",
"@fingerprintjs/fingerprintjs": "^4.5.0",
@@ -19,7 +19,7 @@
"@splitsoftware/splitio-react": "^1.13.0",
"@tanem/react-nprogress": "^5.0.51",
"@vitejs/plugin-react": "^4.3.1",
- "antd": "^5.21.0",
+ "antd": "^5.20.1",
"apollo-link-logger": "^2.0.1",
"apollo-link-sentry": "^3.3.0",
"autosize": "^6.0.1",
diff --git a/client/src/pages/manage/manage.page.component.jsx b/client/src/pages/manage/manage.page.component.jsx
index ce44d3641..e66f47398 100644
--- a/client/src/pages/manage/manage.page.component.jsx
+++ b/client/src/pages/manage/manage.page.component.jsx
@@ -604,7 +604,7 @@ export function Manage({ conflict, bodyshop }) {
}}
>
-
+
{`${InstanceRenderManager({
imex: t("titles.imexonline"),
rome: t("titles.romeonline"),
@@ -613,8 +613,6 @@ export function Manage({ conflict, bodyshop }) {
-
-
Disclaimer & Notices
diff --git a/package-lock.json b/package-lock.json
index acdc927e7..be917a0e0 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -13,6 +13,7 @@
"@aws-sdk/client-ses": "^3.654.0",
"@aws-sdk/credential-provider-node": "^3.654.0",
"@opensearch-project/opensearch": "^2.12.0",
+ "@socket.io/admin-ui": "^0.5.1",
"@socket.io/redis-adapter": "^8.3.0",
"aws4": "^1.13.2",
"axios": "^1.7.7",
@@ -2395,6 +2396,20 @@
"node": ">=16.0.0"
}
},
+ "node_modules/@socket.io/admin-ui": {
+ "version": "0.5.1",
+ "resolved": "https://registry.npmjs.org/@socket.io/admin-ui/-/admin-ui-0.5.1.tgz",
+ "integrity": "sha512-1dlGL2FGm6T+uL1e6iDvbo2eCINwvW7iVbjIblwh5kPPRM1SP8lmZrbFZf4QNJ/cqQ+JLcx49eXGM9WAB4TK7w==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/bcryptjs": "^2.4.2",
+ "bcryptjs": "^2.4.3",
+ "debug": "~4.3.1"
+ },
+ "peerDependencies": {
+ "socket.io": ">=3.1.0"
+ }
+ },
"node_modules/@socket.io/component-emitter": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz",
@@ -2448,6 +2463,12 @@
}
}
},
+ "node_modules/@types/bcryptjs": {
+ "version": "2.4.6",
+ "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz",
+ "integrity": "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==",
+ "license": "MIT"
+ },
"node_modules/@types/body-parser": {
"version": "1.19.5",
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz",
@@ -2870,6 +2891,12 @@
"tweetnacl": "^0.14.3"
}
},
+ "node_modules/bcryptjs": {
+ "version": "2.4.3",
+ "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz",
+ "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==",
+ "license": "MIT"
+ },
"node_modules/better-queue": {
"version": "3.8.12",
"resolved": "https://registry.npmjs.org/better-queue/-/better-queue-3.8.12.tgz",
diff --git a/package.json b/package.json
index f5c94b611..ef95ef36d 100644
--- a/package.json
+++ b/package.json
@@ -23,6 +23,7 @@
"@aws-sdk/client-ses": "^3.654.0",
"@aws-sdk/credential-provider-node": "^3.654.0",
"@opensearch-project/opensearch": "^2.12.0",
+ "@socket.io/admin-ui": "^0.5.1",
"@socket.io/redis-adapter": "^8.3.0",
"aws4": "^1.13.2",
"axios": "^1.7.7",
diff --git a/server.js b/server.js
index 9b5d984f1..dbc52b68f 100644
--- a/server.js
+++ b/server.js
@@ -10,6 +10,9 @@ const { createClient } = require("redis");
const { createAdapter } = require("@socket.io/redis-adapter");
const logger = require("./server/utils/logger");
const { redisSocketEvents } = require("./server/web-sockets/redisSocketEvents");
+const { instrument, RedisStore } = require("@socket.io/admin-ui");
+const { isString, isEmpty } = require("lodash");
+const applyRedisHelpers = require("./server/utils/redisHelpers");
// Load environment variables
require("dotenv").config({
@@ -41,7 +44,9 @@ const SOCKETIO_CORS_ORIGIN = [
"https://www.promanager.web-est.com",
"https://www.promanager.web-est.com",
"https://old.imex.online",
- "https://www.old.imex.online"
+ "https://www.old.imex.online",
+ "https://wsadmin.imex.online",
+ "https://www.wsadmin.imex.online"
];
/**
@@ -129,6 +134,19 @@ const applySocketIO = async (server, app) => {
}
});
+ if (isString(process.env.REDIS_ADMIN_PASS) && !isEmpty(process.env.REDIS_ADMIN_PASS)) {
+ logger.log(`[${process.env.NODE_ENV}] Initializing Redis Admin UI....`, "INFO", "redis", "api");
+ instrument(ioRedis, {
+ auth: {
+ type: "basic",
+ username: "admin",
+ password: process.env.REDIS_ADMIN_PASS
+ },
+ store: new RedisStore(pubClient),
+ mode: process.env.REDIS_ADMIN_MODE || "development"
+ });
+ }
+
const io = new Server(server, {
path: "/ws",
cors: {
@@ -154,164 +172,6 @@ const applySocketIO = async (server, app) => {
return { pubClient, io, ioRedis };
};
-/**
- * Apply Redis helper functions
- * @param pubClient
- * @param app
- */
-const applyRedisHelpers = (pubClient, app) => {
- // Store session data in Redis
- const setSessionData = async (socketId, key, value) => {
- await pubClient.hSet(`socket:${socketId}`, key, JSON.stringify(value)); // Use Redis pubClient
- };
-
- // Retrieve session data from Redis
- const getSessionData = async (socketId, key) => {
- const data = await pubClient.hGet(`socket:${socketId}`, key);
- return data ? JSON.parse(data) : null;
- };
-
- // Clear session data from Redis
- const clearSessionData = async (socketId) => {
- await pubClient.del(`socket:${socketId}`);
- };
-
- // Store multiple session data in Redis
- const setMultipleSessionData = async (socketId, keyValues) => {
- // keyValues is expected to be an object { key1: value1, key2: value2, ... }
- const entries = Object.entries(keyValues).map(([key, value]) => [key, JSON.stringify(value)]);
- await pubClient.hSet(`socket:${socketId}`, ...entries.flat());
- };
-
- // Retrieve multiple session data from Redis
- const getMultipleSessionData = async (socketId, keys) => {
- const data = await pubClient.hmGet(`socket:${socketId}`, keys);
- // Redis returns an object with null values for missing keys, so we parse the non-null ones
- return Object.fromEntries(keys.map((key, index) => [key, data[index] ? JSON.parse(data[index]) : null]));
- };
-
- const setMultipleFromArraySessionData = async (socketId, keyValueArray) => {
- // Use Redis multi/pipeline to batch the commands
- const multi = pubClient.multi();
-
- keyValueArray.forEach(([key, value]) => {
- multi.hSet(`socket:${socketId}`, key, JSON.stringify(value));
- });
-
- await multi.exec(); // Execute all queued commands
- };
-
- // Helper function to add an item to the end of the Redis list
- const addItemToEndOfList = async (socketId, key, newItem) => {
- try {
- await pubClient.rPush(`socket:${socketId}:${key}`, JSON.stringify(newItem));
- } catch (error) {
- console.error(`Error adding item to the end of the list for socket ${socketId}:`, error);
- }
- };
-
- // Helper function to add an item to the beginning of the Redis list
- const addItemToBeginningOfList = async (socketId, key, newItem) => {
- try {
- await pubClient.lPush(`socket:${socketId}:${key}`, JSON.stringify(newItem));
- } catch (error) {
- console.error(`Error adding item to the beginning of the list for socket ${socketId}:`, error);
- }
- };
-
- // Helper function to clear a list in Redis
- const clearList = async (socketId, key) => {
- try {
- await pubClient.del(`socket:${socketId}:${key}`);
- } catch (error) {
- console.error(`Error clearing list for socket ${socketId}:`, error);
- }
- };
-
- const api = {
- setSessionData,
- getSessionData,
- clearSessionData,
- setMultipleSessionData,
- getMultipleSessionData,
- setMultipleFromArraySessionData,
- addItemToEndOfList,
- addItemToBeginningOfList,
- clearList
- };
-
- Object.assign(module.exports, api);
-
- app.use((req, res, next) => {
- req.sessionUtils = api;
- next();
- });
-
- // // Demo to show how all the helper functions work
- // const demoSessionData = async () => {
- // const socketId = "testSocketId";
- //
- // // Store session data using setSessionData
- // await exports.setSessionData(socketId, "field1", "Hello, Redis!");
- //
- // // Retrieve session data using getSessionData
- // const field1Value = await exports.getSessionData(socketId, "field1");
- // console.log("Retrieved single field value:", field1Value);
- //
- // // Store multiple session data using setMultipleSessionData
- // await exports.setMultipleSessionData(socketId, { field2: "Second Value", field3: "Third Value" });
- //
- // // Retrieve multiple session data using getMultipleSessionData
- // const multipleFields = await exports.getMultipleSessionData(socketId, ["field2", "field3"]);
- // console.log("Retrieved multiple field values:", multipleFields);
- //
- // // Store multiple session data using setMultipleFromArraySessionData
- // await exports.setMultipleFromArraySessionData(socketId, [
- // ["field4", "Fourth Value"],
- // ["field5", "Fifth Value"]
- // ]);
- //
- // // Retrieve and log all fields
- // const allFields = await exports.getMultipleSessionData(socketId, [
- // "field1",
- // "field2",
- // "field3",
- // "field4",
- // "field5"
- // ]);
- // console.log("Retrieved all field values:", allFields);
- //
- // // Add item to the end of a Redis list
- // await exports.addItemToEndOfList(socketId, "logEvents", { event: "Log Event 1", timestamp: new Date() });
- // await exports.addItemToEndOfList(socketId, "logEvents", { event: "Log Event 2", timestamp: new Date() });
- //
- // // Add item to the beginning of a Redis list
- // await exports.addItemToBeginningOfList(socketId, "logEvents", { event: "First Log Event", timestamp: new Date() });
- //
- // // Retrieve the entire list (using lRange)
- // const logEvents = await pubClient.lRange(`socket:${socketId}:logEvents`, 0, -1);
- // console.log("Log Events List:", logEvents.map(JSON.parse));
- //
- // // **Add the new code below to test clearList**
- //
- // // Clear the list using clearList
- // await exports.clearList(socketId, "logEvents");
- // console.log("Log Events List cleared.");
- //
- // // Retrieve the list after clearing to confirm it's empty
- // const logEventsAfterClear = await pubClient.lRange(`socket:${socketId}:logEvents`, 0, -1);
- // console.log("Log Events List after clearing:", logEventsAfterClear); // Should be an empty array
- //
- // // Clear session data
- // await exports.clearSessionData(socketId);
- // console.log("Session data cleared.");
- // };
- //
- // if (process.env.NODE_ENV === "development") {
- // demoSessionData();
- // }
-};
-
/**
* Main function to start the server
* @returns {Promise
}
@@ -323,14 +183,14 @@ const main = async () => {
const server = http.createServer(app);
const { pubClient, ioRedis } = await applySocketIO(server, app);
- applyRedisHelpers(pubClient, app);
+ const api = applyRedisHelpers(pubClient, app);
// Legacy Socket Events
require("./server/web-sockets/web-socket");
applyMiddleware(app);
applyRoutes(app);
- redisSocketEvents(ioRedis);
+ redisSocketEvents(ioRedis, api);
try {
await server.listen(port);
diff --git a/server/utils/redisHelpers.js b/server/utils/redisHelpers.js
new file mode 100644
index 000000000..b4c4e1f0d
--- /dev/null
+++ b/server/utils/redisHelpers.js
@@ -0,0 +1,177 @@
+/**
+ * Apply Redis helper functions
+ * @param pubClient
+ * @param app
+ */
+const applyRedisHelpers = (pubClient, app) => {
+ // Store session data in Redis
+ const setSessionData = async (socketId, key, value) => {
+ await pubClient.hSet(`socket:${socketId}`, key, JSON.stringify(value)); // Use Redis pubClient
+ };
+
+ // Retrieve session data from Redis
+ const getSessionData = async (socketId, key) => {
+ const data = await pubClient.hGet(`socket:${socketId}`, key);
+ return data ? JSON.parse(data) : null;
+ };
+
+ // Clear session data from Redis
+ const clearSessionData = async (socketId) => {
+ await pubClient.del(`socket:${socketId}`);
+ };
+
+ // Store multiple session data in Redis
+ const setMultipleSessionData = async (socketId, keyValues) => {
+ // keyValues is expected to be an object { key1: value1, key2: value2, ... }
+ const entries = Object.entries(keyValues).map(([key, value]) => [key, JSON.stringify(value)]);
+ await pubClient.hSet(`socket:${socketId}`, ...entries.flat());
+ };
+
+ // Retrieve multiple session data from Redis
+ const getMultipleSessionData = async (socketId, keys) => {
+ const data = await pubClient.hmGet(`socket:${socketId}`, keys);
+ // Redis returns an object with null values for missing keys, so we parse the non-null ones
+ return Object.fromEntries(keys.map((key, index) => [key, data[index] ? JSON.parse(data[index]) : null]));
+ };
+
+ const setMultipleFromArraySessionData = async (socketId, keyValueArray) => {
+ // Use Redis multi/pipeline to batch the commands
+ const multi = pubClient.multi();
+
+ keyValueArray.forEach(([key, value]) => {
+ multi.hSet(`socket:${socketId}`, key, JSON.stringify(value));
+ });
+
+ await multi.exec(); // Execute all queued commands
+ };
+
+ // Helper function to add an item to the end of the Redis list
+ const addItemToEndOfList = async (socketId, key, newItem) => {
+ try {
+ await pubClient.rPush(`socket:${socketId}:${key}`, JSON.stringify(newItem));
+ } catch (error) {
+ console.error(`Error adding item to the end of the list for socket ${socketId}:`, error);
+ }
+ };
+
+ // Helper function to add an item to the beginning of the Redis list
+ const addItemToBeginningOfList = async (socketId, key, newItem) => {
+ try {
+ await pubClient.lPush(`socket:${socketId}:${key}`, JSON.stringify(newItem));
+ } catch (error) {
+ console.error(`Error adding item to the beginning of the list for socket ${socketId}:`, error);
+ }
+ };
+
+ // Helper function to clear a list in Redis
+ const clearList = async (socketId, key) => {
+ try {
+ await pubClient.del(`socket:${socketId}:${key}`);
+ } catch (error) {
+ console.error(`Error clearing list for socket ${socketId}:`, error);
+ }
+ };
+
+ // Add methods to manage room users
+ const addUserToRoom = async (bodyshopUUID, user) => {
+ await pubClient.sAdd(`bodyshopRoom:${bodyshopUUID}`, JSON.stringify(user));
+ };
+
+ const removeUserFromRoom = async (bodyshopUUID, user) => {
+ await pubClient.sRem(`bodyshopRoom:${bodyshopUUID}`, JSON.stringify(user));
+ };
+
+ const getUsersInRoom = async (bodyshopUUID) => {
+ const users = await pubClient.sMembers(`bodyshopRoom:${bodyshopUUID}`);
+ return users.map((user) => JSON.parse(user));
+ };
+
+ const api = {
+ setSessionData,
+ getSessionData,
+ clearSessionData,
+ setMultipleSessionData,
+ getMultipleSessionData,
+ setMultipleFromArraySessionData,
+ addItemToEndOfList,
+ addItemToBeginningOfList,
+ clearList,
+ addUserToRoom,
+ removeUserFromRoom,
+ getUsersInRoom
+ };
+
+ Object.assign(module.exports, api);
+
+ app.use((req, res, next) => {
+ req.sessionUtils = api;
+ next();
+ });
+
+ // // Demo to show how all the helper functions work
+ // const demoSessionData = async () => {
+ // const socketId = "testSocketId";
+ //
+ // // Store session data using setSessionData
+ // await exports.setSessionData(socketId, "field1", "Hello, Redis!");
+ //
+ // // Retrieve session data using getSessionData
+ // const field1Value = await exports.getSessionData(socketId, "field1");
+ // console.log("Retrieved single field value:", field1Value);
+ //
+ // // Store multiple session data using setMultipleSessionData
+ // await exports.setMultipleSessionData(socketId, { field2: "Second Value", field3: "Third Value" });
+ //
+ // // Retrieve multiple session data using getMultipleSessionData
+ // const multipleFields = await exports.getMultipleSessionData(socketId, ["field2", "field3"]);
+ // console.log("Retrieved multiple field values:", multipleFields);
+ //
+ // // Store multiple session data using setMultipleFromArraySessionData
+ // await exports.setMultipleFromArraySessionData(socketId, [
+ // ["field4", "Fourth Value"],
+ // ["field5", "Fifth Value"]
+ // ]);
+ //
+ // // Retrieve and log all fields
+ // const allFields = await exports.getMultipleSessionData(socketId, [
+ // "field1",
+ // "field2",
+ // "field3",
+ // "field4",
+ // "field5"
+ // ]);
+ // console.log("Retrieved all field values:", allFields);
+ //
+ // // Add item to the end of a Redis list
+ // await exports.addItemToEndOfList(socketId, "logEvents", { event: "Log Event 1", timestamp: new Date() });
+ // await exports.addItemToEndOfList(socketId, "logEvents", { event: "Log Event 2", timestamp: new Date() });
+ //
+ // // Add item to the beginning of a Redis list
+ // await exports.addItemToBeginningOfList(socketId, "logEvents", { event: "First Log Event", timestamp: new Date() });
+ //
+ // // Retrieve the entire list (using lRange)
+ // const logEvents = await pubClient.lRange(`socket:${socketId}:logEvents`, 0, -1);
+ // console.log("Log Events List:", logEvents.map(JSON.parse));
+ //
+ // // **Add the new code below to test clearList**
+ //
+ // // Clear the list using clearList
+ // await exports.clearList(socketId, "logEvents");
+ // console.log("Log Events List cleared.");
+ //
+ // // Retrieve the list after clearing to confirm it's empty
+ // const logEventsAfterClear = await pubClient.lRange(`socket:${socketId}:logEvents`, 0, -1);
+ // console.log("Log Events List after clearing:", logEventsAfterClear); // Should be an empty array
+ //
+ // // Clear session data
+ // await exports.clearSessionData(socketId);
+ // console.log("Session data cleared.");
+ // };
+ //
+ // if (process.env.NODE_ENV === "development") {
+ // demoSessionData();
+ // }
+
+ return api;
+};
+module.exports = applyRedisHelpers;
diff --git a/server/web-sockets/redisSocketEvents.js b/server/web-sockets/redisSocketEvents.js
index ad2394e8b..516be1fe4 100644
--- a/server/web-sockets/redisSocketEvents.js
+++ b/server/web-sockets/redisSocketEvents.js
@@ -11,12 +11,17 @@ function createLogEvent(socket, level, message) {
logger.log("ioredis-log-event", level, socket.user.email, null, { wsmessage: message });
}
-const redisSocketEvents = (io) => {
+const redisSocketEvents = (io, { addUserToRoom, getUsersInRoom, removeUserFromRoom }) => {
// Room management and broadcasting events
function registerRoomAndBroadcastEvents(socket) {
socket.on("join-bodyshop-room", async (bodyshopUUID) => {
socket.join(bodyshopUUID);
+ await addUserToRoom(bodyshopUUID, { uid: socket.user.uid, email: socket.user.email });
createLogEvent(socket, "DEBUG", `Client joined bodyshop room: ${bodyshopUUID}`);
+
+ // Notify all users in the room about the updated user list
+ const usersInRoom = await getUsersInRoom(bodyshopUUID);
+ io.to(bodyshopUUID).emit("room-users-updated", usersInRoom);
});
socket.on("leave-bodyshop-room", async (bodyshopUUID) => {
@@ -24,10 +29,30 @@ const redisSocketEvents = (io) => {
createLogEvent(socket, "DEBUG", `Client left bodyshop room: ${bodyshopUUID}`);
});
+ socket.on("get-room-users", async (bodyshopUUID, callback) => {
+ const usersInRoom = await getUsersInRoom(bodyshopUUID);
+ callback(usersInRoom);
+ });
+
socket.on("broadcast-to-bodyshop", async (bodyshopUUID, message) => {
io.to(bodyshopUUID).emit("bodyshop-message", message);
createLogEvent(socket, "INFO", `Broadcast message to bodyshop ${bodyshopUUID}`);
});
+
+ socket.on("disconnect", async () => {
+ createLogEvent(socket, "DEBUG", `User disconnected.`);
+
+ // Get all rooms the socket is part of
+ const rooms = Array.from(socket.rooms).filter((room) => room !== socket.id);
+
+ for (const bodyshopUUID of rooms) {
+ await removeUserFromRoom(bodyshopUUID, { uid: socket.user.uid, email: socket.user.email });
+
+ // Notify all users in the room about the updated user list
+ const usersInRoom = await getUsersInRoom(bodyshopUUID);
+ io.to(bodyshopUUID).emit("room-users-updated", usersInRoom);
+ }
+ });
}
// Register all socket events for a given socket connection