const { admin } = require("../firebase/firebase-handler"); const redisSocketEvents = ({ io, redisHelpers: { setSessionData, clearSessionData }, // Note: Used if we persist user to Redis ioHelpers: { getBodyshopRoom, getBodyshopConversationRoom }, logger }) => { // Logging helper functions const createLogEvent = (socket, level, message) => { logger.log("ioredis-log-event", level, socket?.user?.email, null, { wsmessage: message }); }; // Socket Auth Middleware const authMiddleware = (socket, next) => { try { if (socket.handshake.auth.token) { admin .auth() .verifyIdToken(socket.handshake.auth.token) .then((user) => { socket.user = user; // Note: if we ever want to capture user data across sockets // Uncomment the following line and then remove the next() to a second then() // return setSessionData(socket.id, "user", user); next(); }) .catch((error) => { next(new Error(`Authentication error: ${error.message}`)); }); } else { next(new Error("Authentication error - no authorization token.")); } } catch (error) { logger.log("websocket-connection-error", "error", null, null, { ...error }); next(new Error(`Authentication error ${error}`)); } }; // Register Socket Events const registerSocketEvents = (socket) => { // Uncomment for further testing // createLogEvent(socket, "debug", `Registering RedisIO Socket Events.`); // Token Update Events const registerUpdateEvents = (socket) => { let latestTokenTimestamp = 0; const updateToken = async (newToken) => { const currentTimestamp = Date.now(); latestTokenTimestamp = currentTimestamp; try { // Verify token with Firebase Admin SDK const user = await admin.auth().verifyIdToken(newToken, true); // Skip outdated token validations if (currentTimestamp < latestTokenTimestamp) { createLogEvent(socket, "warn", "Outdated token validation skipped."); return; } socket.user = user; createLogEvent(socket, "debug", `Token updated successfully for socket ID: ${socket.id}`); socket.emit("token-updated", { success: true }); } catch (error) { if (error.code === "auth/id-token-expired") { createLogEvent(socket, "warn", "Stale token received, waiting for new token"); socket.emit("token-updated", { success: false, error: "Stale token." }); return; // Avoid disconnecting for expired tokens } createLogEvent(socket, "error", `Token update failed for socket ID: ${socket.id}, Error: ${error.message}`); socket.emit("token-updated", { success: false, error: error.message }); // Optionally disconnect for invalid tokens or other errors socket.disconnect(); } }; socket.on("update-token", updateToken); }; // Room Broadcast Events const registerRoomAndBroadcastEvents = (socket) => { const joinBodyshopRoom = (bodyshopUUID) => { try { const room = getBodyshopRoom(bodyshopUUID); socket.join(room); // createLogEvent(socket, "debug", `Client joined bodyshop room: ${room}`); } catch (error) { createLogEvent(socket, "error", `Error joining room: ${error}`); } }; const leaveBodyshopRoom = (bodyshopUUID) => { try { const room = getBodyshopRoom(bodyshopUUID); socket.leave(room); createLogEvent(socket, "debug", `Client left bodyshop room: ${room}`); } catch (error) { createLogEvent(socket, "error", `Error joining room: ${error}`); } }; const broadcastToBodyshopRoom = (bodyshopUUID, message) => { try { const room = getBodyshopRoom(bodyshopUUID); io.to(room).emit("bodyshop-message", message); // We do not need this as these can be debugged live // createLogEvent(socket, "debug", `Broadcast message to bodyshop ${room}`); } catch (error) { createLogEvent(socket, "error", `Error getting room: ${error}`); } }; socket.on("join-bodyshop-room", joinBodyshopRoom); socket.on("leave-bodyshop-room", leaveBodyshopRoom); socket.on("broadcast-to-bodyshop", broadcastToBodyshopRoom); }; // Disconnect Events const registerDisconnectEvents = (socket) => { const disconnect = () => { // Uncomment for further testing // createLogEvent(socket, "debug", `User disconnected.`); const rooms = Array.from(socket.rooms).filter((room) => room !== socket.id); for (const room of rooms) { socket.leave(room); } // If we ever want to persist the user across workers // clearSessionData(socket.id); }; socket.on("disconnect", disconnect); }; // Messaging Events const registerMessagingEvents = (socket) => { const joinConversationRoom = async ({ bodyshopId, conversationId }) => { try { const room = getBodyshopConversationRoom({ bodyshopId, conversationId }); socket.join(room); } catch (error) { logger.log("Failed to Join Conversation Room", "error", "io-redis", null, { bodyshopId, conversationId, error: error.message, stack: error.stack }); } }; const leaveConversationRoom = ({ bodyshopId, conversationId }) => { try { const room = getBodyshopConversationRoom({ bodyshopId, conversationId }); socket.leave(room); } catch (error) { logger.log("Failed to Leave Conversation Room", "error", "io-redis", null, { bodyshopId, conversationId, error: error.message, stack: error.stack }); } }; const conversationModified = ({ bodyshopId, conversationId, ...fields }) => { try { // Retrieve the room name for the conversation const room = getBodyshopRoom(bodyshopId); // Emit the updated data to all clients in the room io.to(room).emit("conversation-changed", { conversationId, ...fields }); } catch (error) { logger.log("Failed to handle conversation modification", "error", "io-redis", null, { error: error.message, stack: error.stack }); } }; const messageAdded = ({ bodyshopId, conversationId, message }) => { try { const room = getBodyshopConversationRoom({ bodyshopId, conversationId }); io.to(room).emit("new-message", { message, conversationId }); } catch (error) { logger.log("Failed to handle new message", "error", "io-redis", null, { error: error.message, stack: error.stack }); } }; socket.on("message-added", messageAdded); socket.on("conversation-modified", conversationModified); socket.on("join-bodyshop-conversation", joinConversationRoom); socket.on("leave-bodyshop-conversation", leaveConversationRoom); }; // Call Handlers registerRoomAndBroadcastEvents(socket); registerUpdateEvents(socket); registerMessagingEvents(socket); registerDisconnectEvents(socket); }; // Associate Middleware and Handlers io.use(authMiddleware); io.on("connection", registerSocketEvents); }; module.exports = { redisSocketEvents };