diff --git a/package-lock.json b/package-lock.json index 0637dbc..cf84e24 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1830,23 +1830,27 @@ "license": "MIT" }, "node_modules/body-parser": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", - "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.1.tgz", + "integrity": "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==", "license": "MIT", "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", - "debug": "^4.4.0", + "debug": "^4.4.3", "http-errors": "^2.0.0", - "iconv-lite": "^0.6.3", + "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.0", - "raw-body": "^3.0.0", - "type-is": "^2.0.0" + "raw-body": "^3.0.1", + "type-is": "^2.0.1" }, "engines": { "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/brace-expansion": { @@ -2981,15 +2985,6 @@ "node": ">= 8.0.0" } }, - "node_modules/express-validator/node_modules/validator": { - "version": "13.15.20", - "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.20.tgz", - "integrity": "sha512-KxPOq3V2LmfQPP4eqf3Mq/zrT0Dqp2Vmx2Bn285LwVahLc+CsxOM0crBHczm8ijlcjZ0Q5Xd6LW3z3odTPnlrw==", - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -3479,15 +3474,19 @@ } }, "node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.1.tgz", + "integrity": "sha512-2Tth85cXwGFHfvRgZWszZSvdo+0Xsqmw8k8ZwxScfcBneNUraK+dxRxRm24nszx80Y0TVio8kKLt5sLE7ZCLlw==", "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" }, "engines": { "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/ignore-by-default": { @@ -4604,12 +4603,12 @@ } }, "node_modules/jws": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", - "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.3.tgz", + "integrity": "sha512-byiJ0FLRdLdSVSReO/U4E7RoEyOCKnEnEPMjq3HxWtvzLsV08/i5RQKsFVNkCldrCaPr2vDNAOMsfs8T/Hze7g==", "license": "MIT", "dependencies": { - "jwa": "^1.4.1", + "jwa": "^1.4.2", "safe-buffer": "^5.0.1" } }, @@ -6156,15 +6155,6 @@ "node": ">=10" } }, - "node_modules/sequelize/node_modules/validator": { - "version": "13.15.20", - "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.20.tgz", - "integrity": "sha512-KxPOq3V2LmfQPP4eqf3Mq/zrT0Dqp2Vmx2Bn285LwVahLc+CsxOM0crBHczm8ijlcjZ0Q5Xd6LW3z3odTPnlrw==", - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, "node_modules/serve-static": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", @@ -7192,6 +7182,15 @@ "node": ">=10.12.0" } }, + "node_modules/validator": { + "version": "13.15.26", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.26.tgz", + "integrity": "sha512-spH26xU080ydGggxRyR1Yhcbgx+j3y5jbNXk/8L+iRvdIEQ4uTRH2Sgf2dokud6Q4oAtsbNvJ1Ft+9xmm6IZcA==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", diff --git a/package.json b/package.json index 3a9d1d8..7096a13 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "overrides": { "inflight": "npm:@isaacs/inflight@^1.0.6", "glob": "10.5.0", - "validator": "13.15.20", + "validator": "^13.15.22", "js-yaml": "3.14.2" } } \ No newline at end of file diff --git a/server.js b/server.js index 0e4dfb9..1588c22 100644 --- a/server.js +++ b/server.js @@ -16,10 +16,6 @@ import authRoutes from './src/routes/auth.js'; const app = express(); const server = http.createServer(app); - -//Socket.io -setupSocket(server); - // Middleware app.use(express.json()); app.use(express.urlencoded({ extended: true })); @@ -61,6 +57,9 @@ async function startServer() { console.log('Initializing MinIO storage...'); await initializeStorage(); + //Socket.io + await setupSocket(server); + // Start server server.listen(PORT, () => { console.log(`Server is running on port ${PORT}`); diff --git a/src/__tests__/socket.test.js b/src/__tests__/socket.test.js deleted file mode 100644 index d6c7f83..0000000 --- a/src/__tests__/socket.test.js +++ /dev/null @@ -1,137 +0,0 @@ -import { createServer } from "http"; -import { io as Client } from "socket.io-client"; -import { setupSocket, makeDynamicNamespace } from "../socket/socket.js"; -import { jest } from '@jest/globals'; - -describe("Socket.IO Basic Communication", () => { - let httpServer, socketServer, port; - let issueClient1, issueClient2; - - // Set a timeout for the entire test suite - jest.setTimeout(10000); - - beforeAll(() => { - // Suppress console logs during tests - jest.spyOn(console, "log").mockImplementation(() => { }); - jest.spyOn(console, "error").mockImplementation(() => { }); - }); - - beforeAll((done) => { - httpServer = createServer(); - socketServer = setupSocket(httpServer); - - httpServer.listen(() => { - port = httpServer.address().port; - - // Create a dynamic namespace first - const testIssueId = 123; - makeDynamicNamespace(testIssueId); - - // Connect clients to the dynamic namespace - issueClient1 = new Client(`http://localhost:${port}/issue-${testIssueId}`, { - forceNew: true, - timeout: 5000, - transports: ['websocket'], - query: { userId: 'user1' } - }); - issueClient2 = new Client(`http://localhost:${port}/issue-${testIssueId}`, { - forceNew: true, - timeout: 5000, - transports: ['websocket'], - query: { userId: 'user2' } - }); - - let connectedCount = 0; - const checkDone = () => { - connectedCount++; - if (connectedCount === 2) done(); - }; - - const handleError = (error) => { - done(error); - }; - - issueClient1.on("connect", checkDone); - issueClient2.on("connect", checkDone); - issueClient1.on("connect_error", handleError); - issueClient2.on("connect_error", handleError); - }); - }); - - afterEach(() => { - // Clean up event listeners after each test - if (issueClient1) { - issueClient1.removeAllListeners("receive_message"); - } - if (issueClient2) { - issueClient2.removeAllListeners("receive_message"); - } - }); - - afterAll(async () => { - // Close clients first - if (issueClient1?.connected) { - issueClient1.disconnect(); - } - if (issueClient2?.connected) { - issueClient2.disconnect(); - } - - // Wait a bit for disconnections to process - await new Promise(resolve => setTimeout(resolve, 100)); - - // Close socket server - if (socketServer) { - await new Promise(resolve => { - socketServer.close(() => { - resolve(); - }); - }); - } - - // Close HTTP server - if (httpServer && httpServer.listening) { - await new Promise(resolve => { - httpServer.close(() => { - resolve(); - }); - }); - } - }); - - test("should broadcast message to all clients in issue room", (done) => { - const testMessage = { text: "Hello world" }; - - issueClient2.once("receive_message", (msg) => { - expect(msg).toEqual(testMessage); - done(); - }); - - issueClient1.emit("send_message_to_all", testMessage); - }); - - test("sender should not receive its own message", (done) => { - const testMessage = { text: "Sender check" }; - let messageReceived = false; - - // If sender receives a message, fail - const messageHandler = () => { - messageReceived = true; - done(new Error("Sender received its own message!")); - }; - - issueClient2.once("receive_message", messageHandler); - - // Emit from client2 - issueClient2.emit("send_message_to_all", testMessage); - - // Wait a bit to ensure no message arrives, then clean up - setTimeout(() => { - issueClient2.removeListener("receive_message", messageHandler); - if (!messageReceived) { - done(); - } - }, 300); - }); - -}); diff --git a/src/routes/auth.js b/src/routes/auth.js index 452eada..9080dd6 100644 --- a/src/routes/auth.js +++ b/src/routes/auth.js @@ -12,6 +12,26 @@ if (!process.env.JWT_SECRET) { const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key-change-in-production'; const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || '7d'; +// Helper: get role-specific profile id (if present) from a user object +function getRoleSpecificId(user) { + if (!user || !user.role) return null; + + // Profiles are included by userService with these aliases + if (user.role === 'technician' && user.technicianProfile && user.technicianProfile.id) { + return user.technicianProfile.id; + } + + if (user.role === 'branch_manager' && user.branchManagerProfile && user.branchManagerProfile.id) { + return user.branchManagerProfile.id; + } + + if (user.role === 'maintenance_executive' && user.maintenanceExecutiveProfile && user.maintenanceExecutiveProfile.id) { + return user.maintenanceExecutiveProfile.id; + } + + return null; +} + /** * @route POST /api/v1/auth/register * @desc Register a new user @@ -82,11 +102,14 @@ router.post('/register', async (req, res) => { const newUser = await userService.createUser(userData, profileData); // Generate JWT token + // include the role-specific profile id in the token as `roleId` + const roleSpecificId = getRoleSpecificId(newUser); const token = jwt.sign( { id: newUser.id, email: newUser.email, - role: newUser.role + role: newUser.role, + roleSpecificId: roleSpecificId }, JWT_SECRET, { expiresIn: JWT_EXPIRES_IN } @@ -168,11 +191,14 @@ router.post('/login', async (req, res) => { } // Generate JWT token + // include the role-specific profile id in the token as `roleId` + const roleSpecificId = getRoleSpecificId(user); const token = jwt.sign( { id: user.id, email: user.email, - role: user.role + role: user.role, + roleSpecificId: roleSpecificId }, JWT_SECRET, { expiresIn: JWT_EXPIRES_IN } diff --git a/src/socket/socket.js b/src/socket/socket.js index 15dd122..064b972 100644 --- a/src/socket/socket.js +++ b/src/socket/socket.js @@ -2,17 +2,38 @@ import 'dotenv/config'; import { Server } from "socket.io"; import messageService from '../services/messageService.js'; import jwt from "jsonwebtoken"; +import Issue from '../models/issue.js'; +import { Op } from 'sequelize'; let ioInstance = null; let newIssue = null; let assign = null; -export const setupSocket = (server) => { +export const setupSocket = async (server) => { const io = new Server(server, { cors: { origin: "*" } }); ioInstance = io; + // Initialize dynamic namespaces for existing active issues + try { + const issues = await Issue.findAll({ + where: { + status: { + [Op.or]: ['Open', 'In Progress'] + } + } + }); + + console.log(`Found ${issues.length} active issues. Creating namespaces...`); + + issues.forEach(issue => { + makeDynamicNamespace(issue.id); + }); + } catch (error) { + console.error("Error initializing issue namespaces:", error); + } + newIssue = io.of("/new-issue"); // the "new-issue" namespace for creating new issues assign = io.of("/assign"); // the "assign" namespace for assigning issues @@ -159,6 +180,9 @@ export function removeDynamicNamespace(issueId) { const namespace = ioInstance.of(namespaceName); console.log(`Removing dynamic namespace: ${namespaceName}`); + + // Emit issue_update event before disconnecting + namespace.emit('issue_update', { status: 'closed', message: 'The issue has been closed.' }); // Get all sockets in the namespace and disconnect them namespace.fetchSockets().then((sockets) => {