diff --git a/package-lock.json b/package-lock.json index 94729f0..1038f4d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,9 @@ "multer": "^1.4.5-lts.1", "nodemon": "^3.1.7", "path": "^0.12.7", - "pg": "^8.13.1" + "pg": "^8.13.1", + "websocket": "^1.0.35", + "ws": "^8.18.0" } }, "node_modules/@mapbox/node-pre-gyp": { @@ -228,6 +230,19 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "license": "MIT" }, + "node_modules/bufferutil": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.8.tgz", + "integrity": "sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-gyp-build": "^4.3.0" + }, + "engines": { + "node": ">=6.14.2" + } + }, "node_modules/busboy": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", @@ -408,6 +423,19 @@ "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", "license": "MIT" }, + "node_modules/d": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.2.tgz", + "integrity": "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==", + "license": "ISC", + "dependencies": { + "es5-ext": "^0.10.64", + "type": "^2.7.2" + }, + "engines": { + "node": ">=0.12" + } + }, "node_modules/debug": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", @@ -527,12 +555,67 @@ "node": ">= 0.4" } }, + "node_modules/es5-ext": { + "version": "0.10.64", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz", + "integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==", + "hasInstallScript": true, + "license": "ISC", + "dependencies": { + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.3", + "esniff": "^2.0.1", + "next-tick": "^1.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/es6-iterator": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", + "integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==", + "license": "MIT", + "dependencies": { + "d": "1", + "es5-ext": "^0.10.35", + "es6-symbol": "^3.1.1" + } + }, + "node_modules/es6-symbol": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.4.tgz", + "integrity": "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==", + "license": "ISC", + "dependencies": { + "d": "^1.0.2", + "ext": "^1.7.0" + }, + "engines": { + "node": ">=0.12" + } + }, "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", "license": "MIT" }, + "node_modules/esniff": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz", + "integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==", + "license": "ISC", + "dependencies": { + "d": "^1.0.1", + "es5-ext": "^0.10.62", + "event-emitter": "^0.3.5", + "type": "^2.7.2" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", @@ -542,6 +625,16 @@ "node": ">= 0.6" } }, + "node_modules/event-emitter": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", + "integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==", + "license": "MIT", + "dependencies": { + "d": "1", + "es5-ext": "~0.10.14" + } + }, "node_modules/express": { "version": "4.21.1", "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", @@ -599,6 +692,15 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, + "node_modules/ext": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz", + "integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==", + "license": "ISC", + "dependencies": { + "type": "^2.7.2" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -987,6 +1089,12 @@ "node": ">=0.12.0" } }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "license": "MIT" + }, "node_modules/isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", @@ -1274,6 +1382,12 @@ "node": ">= 0.6" } }, + "node_modules/next-tick": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", + "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==", + "license": "ISC" + }, "node_modules/node-addon-api": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", @@ -1300,6 +1414,17 @@ } } }, + "node_modules/node-gyp-build": { + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.3.tgz", + "integrity": "sha512-EMS95CMJzdoSKoIiXo8pxKoL8DYxwIZXYlLmgPb8KUv794abpnLK6ynsCAWNliOjREKruYKdzbh76HHYUHX7nw==", + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, "node_modules/nodemon": { "version": "3.1.7", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.7.tgz", @@ -1988,6 +2113,12 @@ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", "license": "MIT" }, + "node_modules/type": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz", + "integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==", + "license": "ISC" + }, "node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -2007,6 +2138,15 @@ "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", "license": "MIT" }, + "node_modules/typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "license": "MIT", + "dependencies": { + "is-typedarray": "^1.0.0" + } + }, "node_modules/undefsafe": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", @@ -2022,6 +2162,19 @@ "node": ">= 0.8" } }, + "node_modules/utf-8-validate": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz", + "integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-gyp-build": "^4.3.0" + }, + "engines": { + "node": ">=6.14.2" + } + }, "node_modules/util": { "version": "0.10.4", "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz", @@ -2067,6 +2220,38 @@ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", "license": "BSD-2-Clause" }, + "node_modules/websocket": { + "version": "1.0.35", + "resolved": "https://registry.npmjs.org/websocket/-/websocket-1.0.35.tgz", + "integrity": "sha512-/REy6amwPZl44DDzvRCkaI1q1bIiQB0mEFQLUrhz3z2EK91cp3n72rAjUlrTP0zV22HJIUOVHQGPxhFRjxjt+Q==", + "license": "Apache-2.0", + "dependencies": { + "bufferutil": "^4.0.1", + "debug": "^2.2.0", + "es5-ext": "^0.10.63", + "typedarray-to-buffer": "^3.1.5", + "utf-8-validate": "^5.0.2", + "yaeti": "^0.0.6" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/websocket/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/websocket/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, "node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", @@ -2092,6 +2277,27 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, + "node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", @@ -2101,6 +2307,15 @@ "node": ">=0.4" } }, + "node_modules/yaeti": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/yaeti/-/yaeti-0.0.6.tgz", + "integrity": "sha512-MvQa//+KcZCUkBTIC9blM+CU9J2GzuTytsOUwf2lidtvkx/6gnEp1QvJv34t9vdjhFmha/mUiNDbN0D0mJWdug==", + "license": "MIT", + "engines": { + "node": ">=0.10.32" + } + }, "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", diff --git a/package.json b/package.json index c58c150..476f8da 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,9 @@ "multer": "^1.4.5-lts.1", "nodemon": "^3.1.7", "path": "^0.12.7", - "pg": "^8.13.1" + "pg": "^8.13.1", + "websocket": "^1.0.35", + "ws": "^8.18.0" }, "type": "module" } diff --git a/sample.config.json b/sample.config.json index 753c0a1..ef23984 100644 --- a/sample.config.json +++ b/sample.config.json @@ -1,10 +1,11 @@ { "debug": true, "port": "8081", + "wsport": "8082", "dbuser": "bsfe", "dbhost": "localhost", "dbport": "5432", "dbpassword": "Ch@NgEME!", "dbname": "bsfe", "secret": "SECRET!11!1!" -} +} \ No newline at end of file diff --git a/src/controllers/abstractproduct.js b/src/controllers/abstractproduct.js index cfd6161..5b1d06a 100644 --- a/src/controllers/abstractproduct.js +++ b/src/controllers/abstractproduct.js @@ -6,6 +6,7 @@ import customError from '../response/customError.js'; import responseCodes from '../response/responseCodes.js'; import translate from '../utils/translate.js'; import { createHash } from 'crypto'; +import notify from '../utils/notify.js'; const TAG = "/controllers/abstractproduct.js"; @@ -29,6 +30,11 @@ class AbstractProductController { fs.rmSync(tempPath); await AbstractProductService.create(groupId, localId, barcode, name, net_weight, image_filename, category, unit); + + notify(req.headers.authorization.split(' ')[1], groupId, 'create', 'abstractproduct', { + localId, barcode, name, net_weight, image_filename, category, unit + }); + return res.status(200).send(translate(req.headers["accept-language"], responseCodes.responses.general.ok)); } @@ -58,6 +64,15 @@ class AbstractProductController { if (unit) await AbstractProductService.updateUnit(groupId, localId, unit); + let data = { localId } + if (barcode) data.barcode = barcode + if (name) data.name = name + if (net_weight) data.net_weight = net_weight + if (category) data.category = category + if (unit) data.unit = unit + + notify(req.headers.authorization.split(' ')[1], groupId, 'update', 'abstractproduct', data); + return res.status(200).send(translate(req.headers["accept-language"], responseCodes.responses.general.ok)); } @@ -85,6 +100,8 @@ class AbstractProductController { await AbstractProductService.delete(groupId, localId) + notify(req.headers.authorization.split(' ')[1], groupId, 'delete', 'abstractproduct', { localId }); + return res.status(200).send(translate(req.headers["accept-language"], responseCodes.responses.general.ok)) } }; diff --git a/src/controllers/category.js b/src/controllers/category.js index 5f51e44..3396348 100644 --- a/src/controllers/category.js +++ b/src/controllers/category.js @@ -1,8 +1,7 @@ import CategoryService from "../services/category.js"; -import AbstractProductService from "../services/abstractproduct.js"; -import ProductService from "../services/product.js"; import translate from "../utils/translate.js"; import responseCodes from "../response/responseCodes.js"; +import notify from "../utils/notify.js"; const TAG = "controllers/category.js"; @@ -12,6 +11,10 @@ class CategoryController { await CategoryService.create(groupId, localId, categoryName); + notify(req.headers.authorization.split(' ')[1], groupId, 'create', 'category', { + localId, categoryName + }); + return res.status(200).send(translate(req.headers["accept-language"], responseCodes.responses.general.ok)); } @@ -20,6 +23,11 @@ class CategoryController { await CategoryService.update(groupId, localId, categoryName); + let data = { localId } + if (categoryName) data.categoryName = categoryName + + notify(req.headers.authorization.split(' ')[1], groupId, 'update', 'category', data); + return res.status(200).send(translate(req.headers["accept-language"], responseCodes.responses.general.ok)); } @@ -36,6 +44,8 @@ class CategoryController { await CategoryService.delete(groupId, localId); + notify(req.headers.authorization.split(' ')[1], groupId, 'delete', 'category', { localId }); + return res.status(200).send(translate(req.headers["accept-language"], responseCodes.responses.general.ok)) } }; diff --git a/src/controllers/product.js b/src/controllers/product.js index 2f4b257..06afdc7 100644 --- a/src/controllers/product.js +++ b/src/controllers/product.js @@ -10,10 +10,13 @@ class AbstractProductController { await ProductService.create(groupId, localId, abstract_product_id, amount, date_of_production, expiry_date); + notify(req.headers.authorization.split(' ')[1], groupId, 'create', 'product', { + localId, abstract_product_id, amount, date_of_production, expiry_date + }); + return res.status(200).send(translate(req.headers["accept-language"], responseCodes.responses.general.ok)); } - async update(req, res) { let { groupId, localId, abstract_product_id, amount, date_of_production, expiry_date } = req.body; @@ -25,6 +28,14 @@ class AbstractProductController { if (expiry_date) await ProductService.updateExpiryDate(groupId, localId, expiry_date); + let data = { localId }; + if (abstract_product_id) data.abstract_product_id = abstract_product_id + if (amount) data.amount = amount + if (date_of_production) data.date_of_production = date_of_production + if (expiry_date) data.expiry_date = expiry_date + + notify(req.headers.authorization.split(' ')[1], groupId, 'update', 'product', data); + return res.status(200).send(translate(req.headers["accept-language"], responseCodes.responses.general.ok)); } @@ -43,6 +54,8 @@ class AbstractProductController { await ProductService.delete(id) + notify(req.headers.authorization.split(' ')[1], groupId, 'delete', 'product', { localId }); + return res.status(200).send(translate(req.headers["accept-language"], responseCodes.responses.general.ok)); } }; diff --git a/src/index.js b/src/index.js index 736ae40..e82e6b6 100644 --- a/src/index.js +++ b/src/index.js @@ -1,5 +1,6 @@ import express from 'express'; import UserRouter from './routers/user.js'; +import UserService from './services/user.js'; import GroupRouter from './routers/group.js'; import AbstractProductRouter from './routers/abstractproduct.js'; import log from './utils/log.js'; @@ -8,7 +9,15 @@ import config from '../config.json' with {type: "json"}; import ProductRouter from './routers/product.js'; import CategoryRouter from './routers/category.js'; +import { WebSocketServer } from 'ws'; +import http from 'http' +import jwt from 'jsonwebtoken'; + const app = express(); +const server = http.createServer(app); +const wss = new WebSocketServer({ port: config.wsport }); + +const clients = [] app.use(express.urlencoded({ extended: false, limit: "200mb", parameterLimit: 100000 })); app.use(express.json({ limit: "200mb", parameterLimit: 1000000 })); @@ -23,6 +32,42 @@ app.get('/status', (req, res) => { return res.status(200).send("All OK"); }); +wss.on('connection', (client) => { + client.on('message', async (message) => { + let parsed = JSON.parse(message) + let token = parsed.token + let currentGroup = parsed.currentGroup + + if (!jwt.verify(token, config.secret)) { + client.send("Invalid token"); + return + } + + if (!await UserService.isInGroup(jwt.decode(token, config.secret).login.id, currentGroup)) { + client.send("Not a member of specified group") + return + } + + clients.push({ + socket: client, + token, + currentGroup + }); + }); + + client.on('close', () => { + for (let i = 0; i < clients.length; i++) { + if (clients[i].socket == client) { + clients.splice(i, 1); + break; + } + } + }) +}); + + app.listen(config.port, () => { log.info(`Application has started on port ${config.port}`) -}); \ No newline at end of file +}); + +export default clients; \ No newline at end of file diff --git a/src/utils/notify.js b/src/utils/notify.js new file mode 100644 index 0000000..f6621c3 --- /dev/null +++ b/src/utils/notify.js @@ -0,0 +1,26 @@ +import clients from '../index.js'; +import jwt from 'jsonwebtoken'; + +import config from '../../config.json' with {type: "json"}; + +const notify = (token, groupId, action, item, data) => { + let login = jwt.decode(token, config.secret).login + let userIdCurrent = login.id + + let payload = { + action, + item, + groupId, + data + } + + clients.forEach(client => { + if (client.currentGroup == groupId) { + let userIdFromToken = jwt.decode(client.token, config.secret).login.id + if (userIdCurrent == userIdFromToken) return; + client.socket.send(payload) + } + }); +} + +export default notify; \ No newline at end of file