From bf56234c4bb382c0b6d3a0b31da92d02ac734abb Mon Sep 17 00:00:00 2001 From: Tom Butcher Date: Sun, 29 Jun 2025 22:39:23 +0100 Subject: [PATCH] Refactor project structure: remove app.js, update config.json for environment-specific settings, enhance package.json with new scripts and dependencies, and convert websockets.js to ES module syntax with Keycloak authentication integration. --- .DS_Store | Bin 0 -> 6148 bytes .eslintignore | 13 + .eslintrc.json | 100 ++++ .prettierignore | 7 + .prettierrc | 15 + .vscode/settings.json | 17 + README.md | 178 +++++++ app.js | 21 - config.json | 55 ++- package-lock.json | 625 ++++++++++++++++++++++-- package.json | 17 +- src/.DS_Store | Bin 0 -> 6148 bytes src/auth/auth.js | 147 ++++++ src/config.js | 40 ++ src/database/etcd.js | 256 ++++++++++ src/database/mongo.js | 51 ++ src/database/user.schema.js | 20 + src/index.js | 53 ++ src/lock/lockmanager.js | 100 ++++ src/notification/notificationmanager.js | 0 src/socket/socketclient.js | 128 +++++ src/socket/socketmanager.js | 82 ++++ src/updates/updatemanager.js | 51 ++ src/websockets.js | 438 +---------------- 24 files changed, 1931 insertions(+), 483 deletions(-) create mode 100644 .DS_Store create mode 100644 .eslintignore create mode 100644 .eslintrc.json create mode 100644 .prettierignore create mode 100644 .prettierrc create mode 100644 .vscode/settings.json create mode 100644 README.md delete mode 100644 app.js create mode 100644 src/.DS_Store create mode 100644 src/auth/auth.js create mode 100644 src/config.js create mode 100644 src/database/etcd.js create mode 100644 src/database/mongo.js create mode 100644 src/database/user.schema.js create mode 100644 src/index.js create mode 100644 src/lock/lockmanager.js create mode 100644 src/notification/notificationmanager.js create mode 100644 src/socket/socketclient.js create mode 100644 src/socket/socketmanager.js create mode 100644 src/updates/updatemanager.js diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..428a811e03386930a95b30b27db0e0e2e2623e47 GIT binary patch literal 6148 zcmeHK&5qMB5FWRIby-&G0i-=z+GDFFT~_>v1EG``Ac!9U3eBe4jY#6EN!3GDDV&ht z!V4fSJOod$f*0Tkcmke)8QZ9?%C0!EE98;vZ|s?g$6p&y3;?V@O?m)r0N~gNy-V2r zLC8*RO&Vrq3z5+?vcq0@AoKB>r<-sLI0num1N82CunUKfLbdw-{^;1YTn+|tCS#oA zi{vRN8qoK6Z9+ofCcSyl%a@`|QIv zmDef$%RK+a-_G0H3NRIDLJ1Q%h-c;4F!CiHXK7ja{&&`BdKWHk@D0AnUxr6=7FNS* zTK0#N=QMgGWfuR|hwh6-?#rSYrp0KY0@7rJE-#*>MIvW?IW3Y>#fI9zTf7yu zx99VHztyJG+Pjm4tHukKvC^Dugy<%N7l>Ehvxr?V3JwQYHi1G5{O zbAV4@mlm7hx6$P1=Uv6HiOc1Z&vmQK9x&ft+v#V`fcaPO&9hF#t?w9c44gX#=zI{^ z2m^z)Ms;*xrwTcuhto<>r@I8j2!nyaS|eIem<~nMq0C${m=4EzVf+GvwMHEdOn*WE zGb=N5Lt$!le7;cbzygh~cMLcNRvBpOVVmmzgOl(7t3ht$7;p^yR}3(35DxmdB~x41 yHb=EqV0(p)g#2oaDg--Ij+H{CcpIA%v=16.0.0" } }, + "node_modules/@grpc/grpc-js": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.13.4.tgz", + "integrity": "sha512-GsFaMXCkMqkKIvwCQjCrwH+GHbPKBjhwo/8ZuUkWHqbI73Kky9I+pQltrlT0+MWpedCoosda53lgjYfyEPgxBg==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/proto-loader": "^0.7.13", + "@js-sdsl/ordered-map": "^4.4.2" + }, + "engines": { + "node": ">=12.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.7.15", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.15.tgz", + "integrity": "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==", + "license": "Apache-2.0", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.14", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", @@ -170,6 +208,16 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/@js-sdsl/ordered-map": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", + "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, "node_modules/@mongodb-js/saslprep": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.1.7.tgz", @@ -217,6 +265,83 @@ "node": ">= 8" } }, + "node_modules/@pkgr/core": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.7.tgz", + "integrity": "sha512-YLT9Zo3oNPJoBjBc4q8G2mjU4tqIbf5CEOORbUUr48dCD9q3umJ3IPlVqOqDakPfd2HuwccBaqlGhN4Gmr5OWg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "license": "BSD-3-Clause" + }, "node_modules/@socket.io/component-emitter": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", @@ -333,7 +458,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -343,7 +467,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -526,6 +649,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, "node_modules/available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", @@ -542,6 +671,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/axios": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.10.0.tgz", + "integrity": "sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -566,6 +706,15 @@ "node": "^4.5.0 || >= 5.9" } }, + "node_modules/bignumber.js": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.0.tgz", + "integrity": "sha512-EM7aMFTXbptt/wZdMlBv2t8IViwQL+h6SLHosp8Yf0dqJMTnY6iL32opnAB6kAdL0SZPuvcAzFr31o0c/R3/RA==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/body-parser": { "version": "1.20.2", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", @@ -670,6 +819,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -697,11 +859,33 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cockatiel": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/cockatiel/-/cockatiel-3.2.1.tgz", + "integrity": "sha512-gfrHV6ZPkquExvMh9IOkKsBzNDk6sDuZ6DdBGUBkvFnTCqCxzpuq48RySgP0AnaqQkw2zynOFj9yly6T1Q2G5Q==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -714,9 +898,20 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, "license": "MIT" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -918,6 +1113,15 @@ "node": ">= 0.4" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -950,6 +1154,32 @@ "node": ">=6.0.0" } }, + "node_modules/dotenv": { + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/ecdsa-sig-formatter": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", @@ -965,6 +1195,12 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "license": "MIT" }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, "node_modules/encodeurl": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", @@ -1118,13 +1354,10 @@ } }, "node_modules/es-define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", - "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.2.4" - }, "engines": { "node": ">= 0.4" } @@ -1165,10 +1398,9 @@ } }, "node_modules/es-object-atoms": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", - "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", - "dev": true, + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -1178,15 +1410,15 @@ } }, "node_modules/es-set-tostringtag": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz", - "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==", - "dev": true, + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", "license": "MIT", "dependencies": { - "get-intrinsic": "^1.2.4", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", - "hasown": "^2.0.1" + "hasown": "^2.0.2" }, "engines": { "node": ">= 0.4" @@ -1226,6 +1458,15 @@ "integrity": "sha512-oj4jOSXvWglTsc3wrw86iom3LDPOx1nbipQk+jaG3dy+sMRM6ReSgVr/VlmBuF6lXUrflN9DCcQHeSbAwGUl4g==", "license": "MIT" }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -1301,6 +1542,22 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint-config-prettier": { + "version": "10.1.5", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.5.tgz", + "integrity": "sha512-zc1UmCpNltmVY34vuLRV61r1K27sWuX39E+uyUnY8xS2Bex88VV9cugG+UZbRSRGtGyFboj+D8JODyme1plMpw==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, "node_modules/eslint-config-standard": { "version": "17.1.0", "resolved": "https://registry.npmjs.org/eslint-config-standard/-/eslint-config-standard-17.1.0.tgz", @@ -1564,6 +1821,37 @@ "eslint": ">=7.0.0" } }, + "node_modules/eslint-plugin-prettier": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.1.tgz", + "integrity": "sha512-dobTkHT6XaEVOo8IO90Q4DOSxnm3Y151QxPJlM/vKC0bVy+d6cVWQZLlFiuZPP0wS6vZwSKeJgKkcS+KfMBlRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "prettier-linter-helpers": "^1.0.0", + "synckit": "^0.11.7" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, "node_modules/eslint-plugin-promise": { "version": "6.6.0", "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-6.6.0.tgz", @@ -1811,6 +2099,21 @@ "node": ">= 0.6" } }, + "node_modules/etcd3": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/etcd3/-/etcd3-1.1.2.tgz", + "integrity": "sha512-YIampCz1/OmrVo/tR3QltAVUtYCQQOSFoqmHKKeoHbalm+WdXe3l4rhLIylklu8EzR/I3PBiOF4dC847dDskKg==", + "license": "MIT", + "dependencies": { + "@grpc/grpc-js": "^1.8.20", + "@grpc/proto-loader": "^0.7.8", + "bignumber.js": "^9.1.1", + "cockatiel": "^3.1.1" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/express": { "version": "4.19.2", "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", @@ -1860,6 +2163,13 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true, + "license": "Apache-2.0" + }, "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", @@ -1953,6 +2263,26 @@ "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", "license": "ISC" }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/for-each": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", @@ -1963,6 +2293,22 @@ "is-callable": "^1.1.3" } }, + "node_modules/form-data": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.3.tgz", + "integrity": "sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -2040,17 +2386,31 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/get-intrinsic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "license": "MIT", "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -2059,6 +2419,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/get-stdin": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-8.0.0.tgz", @@ -2159,12 +2532,12 @@ } }, "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.1.3" + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -2219,6 +2592,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -2228,9 +2602,9 @@ } }, "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -2243,7 +2617,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, "license": "MIT", "dependencies": { "has-symbols": "^1.0.3" @@ -2538,6 +2911,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-generator-function": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", @@ -2994,6 +3376,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "license": "MIT" + }, "node_modules/lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", @@ -3082,6 +3470,12 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "license": "MIT" }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -3095,6 +3489,15 @@ "loose-envify": "cli.js" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -3831,6 +4234,35 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/process-nextick-args": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", @@ -3849,6 +4281,30 @@ "react-is": "^16.13.1" } }, + "node_modules/protobufjs": { + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.3.tgz", + "integrity": "sha512-sildjKwVqOI2kmFDiXQ6aEB0fjYTafpEvIBs8tOR8qI4spuL9OPROLVu2qZqi/xgCfsHIwVqlaF8JBjWFHnKbw==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -3862,6 +4318,12 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -4026,6 +4488,15 @@ "semver": "bin/semver" } }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve": { "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", @@ -4638,6 +5109,20 @@ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "license": "MIT" }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/string.prototype.matchall": { "version": "4.0.11", "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.11.tgz", @@ -4732,7 +5217,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -4790,6 +5274,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/synckit": { + "version": "0.11.8", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.8.tgz", + "integrity": "sha512-+XZ+r1XGIJGeQk3VvXhT6xx/VpbHsRzsTkGgF6E5RX9TTXD0118l87puaEBZ566FhqblC6U0d4XnubznJDm30A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.2.4" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/synckit" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -5180,6 +5680,23 @@ "node": ">=0.10.0" } }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -5229,6 +5746,42 @@ "node": ">=0.4" } }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index 2c53086..c0cdbbf 100644 --- a/package.json +++ b/package.json @@ -3,14 +3,23 @@ "version": "1.0.0", "description": "Farmcontrol Web Socket microservice", "main": "app.js", + "type": "module", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", - "start": "node app.js", - "dev": "nodemon app.js" + "start": "node src/index.js", + "dev": "nodemon src/index.js", + "lint": "eslint src/", + "lint:fix": "eslint src/ --fix", + "format": "prettier --write \"src/**/*.{js,json}\"", + "format:check": "prettier --check \"src/**/*.{js,json}\"", + "fix": "npm run lint:fix && npm run format" }, "author": "Tom Butcher", "license": "ISC", "dependencies": { + "axios": "^1.10.0", + "dotenv": "^16.4.5", + "etcd3": "^1.1.2", "express": "^4.19.2", "jsonwebtoken": "^9.0.2", "log4js": "^6.9.1", @@ -21,6 +30,10 @@ "socketio-jwt": "^4.6.2" }, "devDependencies": { + "eslint": "^8.57.0", + "eslint-config-prettier": "^10.1.5", + "eslint-plugin-prettier": "^5.5.1", + "prettier": "^3.6.2", "standard": "^17.1.0" } } diff --git a/src/.DS_Store b/src/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..5008ddfcf53c02e82d7eee2e57c38e5672ef89f6 GIT binary patch literal 6148 zcmeH~Jr2S!425mzP>H1@V-^m;4Wg<&0T*E43hX&L&p$$qDprKhvt+--jT7}7np#A3 zem<@ulZcFPQ@L2!n>{z**++&mCkOWA81W14cNZlEfg7;MkzE(HCqgga^y>{tEnwC%0;vJ&^%eQ zLs35+`xjp>T0 Date.now()) { + return { valid: true, user: cachedInfo.user }; + } else { + // Token expired, remove from cache + this.tokenCache.delete(token); + } + } + + try { + // Verify token with Keycloak introspection endpoint + const response = await axios.post( + `${this.config.keycloak.url}/realms/${this.config.keycloak.realm}/protocol/openid-connect/token/introspect`, + new URLSearchParams({ + token, + client_id: this.config.keycloak.clientId, + client_secret: this.config.keycloak.clientSecret + }), + { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + } + } + ); + + const introspection = response.data; + + if (!introspection.active) { + logger.info('Token is not active'); + return { valid: false }; + } + + // Verify required roles if configured + if (this.config.requiredRoles && this.config.requiredRoles.length > 0) { + const hasRequiredRole = this.checkRoles( + introspection, + this.config.requiredRoles + ); + if (!hasRequiredRole) { + logger.info("User doesn't have required roles"); + return { valid: false }; + } + } + + // Parse token to extract user info + const decodedToken = jwt.decode(token); + const user = { + id: decodedToken.sub, + username: decodedToken.preferred_username, + email: decodedToken.email, + name: decodedToken.name, + roles: this.extractRoles(decodedToken) + }; + + // Cache the verified token + const expiresAt = introspection.exp * 1000; // Convert to milliseconds + this.tokenCache.set(token, { expiresAt, user }); + + return { valid: true, user }; + } catch (error) { + logger.error('Token verification error:', error.message); + return { valid: false }; + } + } + + // Extract roles from token + extractRoles(token) { + const roles = []; + + // Extract realm roles + if (token.realm_access && token.realm_access.roles) { + roles.push(...token.realm_access.roles); + } + + // Extract client roles + if (token.resource_access) { + for (const client in token.resource_access) { + if (token.resource_access[client].roles) { + roles.push( + ...token.resource_access[client].roles.map( + role => `${client}:${role}` + ) + ); + } + } + } + + return roles; + } + + // Check if user has required roles + checkRoles(tokenInfo, requiredRoles) { + // Extract roles from token + const userRoles = this.extractRoles(tokenInfo); + + // Check if user has any of the required roles + return requiredRoles.some(role => userRoles.includes(role)); + } +} + +// Socket.IO middleware for authentication +export function createAuthMiddleware(auth) { + return async (socket, next) => { + const { token } = socket.handshake.auth; + + if (!token) { + return next(new Error('Authentication token is required')); + } + + try { + const authResult = await auth.verifyToken(token); + + if (!authResult.valid) { + return next(new Error('Invalid authentication token')); + } + + // Attach user information to socket + socket.user = authResult.user; + next(); + } catch (err) { + logger.error('Authentication error:', err); + next(new Error('Authentication failed')); + } + }; +} diff --git a/src/config.js b/src/config.js new file mode 100644 index 0000000..688fe4e --- /dev/null +++ b/src/config.js @@ -0,0 +1,40 @@ +// config.js - Configuration handling +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +// Configure paths relative to this file +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const CONFIG_PATH = path.resolve(__dirname, '../config.json'); + +// Determine environment +const NODE_ENV = process.env.NODE_ENV || 'development'; + +// Load config file +export function loadConfig() { + try { + if (!fs.existsSync(CONFIG_PATH)) { + throw new Error(`Configuration file not found at ${CONFIG_PATH}`); + } + + const configData = fs.readFileSync(CONFIG_PATH, 'utf8'); + const config = JSON.parse(configData); + + if (!config[NODE_ENV]) { + throw new Error( + `Configuration for environment '${NODE_ENV}' not found in config.json` + ); + } + + return config[NODE_ENV]; + } catch (err) { + console.error('Error loading config:', err); + throw err; + } +} + +// Get current environment +export function getEnvironment() { + return NODE_ENV; +} diff --git a/src/database/etcd.js b/src/database/etcd.js new file mode 100644 index 0000000..9edfce7 --- /dev/null +++ b/src/database/etcd.js @@ -0,0 +1,256 @@ +import { Etcd3 } from 'etcd3'; +import log4js from 'log4js'; +import { loadConfig } from '../config.js'; + +const config = loadConfig(); +const logger = log4js.getLogger('Etcd'); +logger.level = config.server.logLevel; + +class EtcdServer { + constructor() { + this.client = null; + this.watchers = new Map(); + const etcdConfig = config.database?.etcd || config.database; // fallback for production config + const host = etcdConfig.host || 'localhost'; + const port = etcdConfig.port || 2379; + this.hosts = [`${host}:${port}`]; + logger.debug( + `EtcdServer constructor: hosts set to ${JSON.stringify(this.hosts)}` + ); + } + + async connect() { + if (!this.client) { + logger.info('Connecting to Etcd...'); + logger.debug( + `Creating Etcd client with hosts ${JSON.stringify(this.hosts)}` + ); + this.client = new Etcd3({ + hosts: this.hosts + }); + + // Test connection + try { + await this.client.get('test-connection').string(); + logger.debug('Etcd client connected successfully.'); + } catch (error) { + if (error.code === 'NOT_FOUND') { + logger.debug( + 'Etcd client connected successfully (test key not found as expected).' + ); + } else { + throw error; + } + } + } else { + logger.debug('Etcd client already exists, skipping connection.'); + } + return this.client; + } + + async getClient() { + logger.trace('Checking if Etcd client exists.'); + if (!this.client) { + logger.debug('No client found, calling connect().'); + await this.connect(); + } + logger.trace('Returning Etcd client.'); + return this.client; + } + + // Hash-like functionality using etcd + async set(key, value) { + const client = await this.getClient(); + const stringValue = + typeof value === 'string' ? value : JSON.stringify(value); + + await client.put(key).value(stringValue); + logger.debug(`Set key: ${key}, value: ${stringValue}`); + return true; + } + + async get(key) { + const client = await this.getClient(); + + try { + const value = await client.get(key).string(); + logger.debug(`Retrieved key: ${key}, value: ${value}`); + + // Try to parse as JSON, fallback to string + try { + return JSON.parse(value); + } catch { + return value; + } + } catch (error) { + if (error.code === 'NOT_FOUND') { + logger.debug(`Key not found: ${key}`); + return null; + } + throw error; + } + } + + async delete(key) { + const client = await this.getClient(); + + try { + await client.delete().key(key); + logger.debug(`Deleted key: ${key}`); + return true; + } catch (error) { + if (error.code === 'NOT_FOUND') { + logger.debug(`Key not found for deletion: ${key}`); + return false; + } + throw error; + } + } + + async onPrefixEvent(prefix, callback) { + const client = await this.getClient(); + logger.debug(`Setting up watcher for prefix events: ${prefix}`); + + client + .watch() + .prefix(prefix) + .create() + .then(watcher => { + // Handle put events + watcher.on('put', (kv, previous) => { + logger.debug(`Prefix put event detected: ${prefix}, key: ${kv.key}`); + try { + const value = kv.value.toString(); + let parsedValue; + try { + parsedValue = JSON.parse(value); + } catch { + parsedValue = value; + } + callback(kv.key.toString(), parsedValue, kv, previous); + } catch (error) { + logger.error( + `Error in onPrefixEvent put callback for prefix ${prefix}:`, + error + ); + } + }); + + // Handle delete events + watcher.on('delete', (kv, previous) => { + logger.debug( + `Prefix delete event detected: ${prefix}, key: ${kv.key}` + ); + try { + callback(kv.key.toString(), null, kv, previous); + } catch (error) { + logger.error( + `Error in onPrefixEvent delete callback for prefix ${prefix}:`, + error + ); + } + }); + + // Store watcher with a unique key + const watcherKey = `event:${prefix}`; + this.watchers.set(watcherKey, watcher); + }); + } + + async onPrefixPut(prefix, callback) { + const client = await this.getClient(); + logger.debug(`Setting up watcher for prefix put: ${prefix}`); + + client + .watch() + .prefix(prefix) + .create() + .then(watcher => { + watcher.on('put', (kv, previous) => { + logger.debug(`Prefix put event detected: ${prefix}, key: ${kv.key}`); + try { + const value = kv.value.toString(); + let parsedValue; + try { + parsedValue = JSON.parse(value); + } catch { + parsedValue = value; + } + callback(kv.key.toString(), parsedValue, kv, previous); + } catch (error) { + logger.error( + `Error in onPrefixPut callback for prefix ${prefix}:`, + error + ); + } + }); + + this.watchers.set(`put:${prefix}`, watcher); + }); + } + + async onPrefixDelete(prefix, callback) { + const client = await this.getClient(); + logger.debug(`Setting up watcher for prefix delete: ${prefix}`); + + client + .watch() + .prefix(prefix) + .create() + .then(watcher => { + watcher.on('delete', (kv, previous) => { + logger.debug( + `Prefix delete event detected: ${prefix}, key: ${kv.key}` + ); + try { + callback(kv.key.toString(), kv, previous); + } catch (error) { + logger.error( + `Error in onPrefixDelete callback for prefix ${prefix}:`, + error + ); + } + }); + + this.watchers.set(`delete:${prefix}`, watcher); + }); + } + + async removeWatcher(prefix, type = 'put') { + const watcherKey = `${type}:${prefix}`; + const watcher = this.watchers.get(watcherKey); + + if (watcher) { + logger.debug(`Removing watcher: ${watcherKey}`); + watcher.removeAllListeners(); + await watcher.close(); + this.watchers.delete(watcherKey); + return true; + } else { + logger.debug(`Watcher not found: ${watcherKey}`); + return false; + } + } + + async disconnect() { + logger.info('Disconnecting from Etcd...'); + + // Stop all watchers + for (const [key, watcher] of this.watchers) { + logger.debug(`Stopping watcher: ${key}`); + watcher.removeAllListeners(); + await watcher.close(); + } + this.watchers.clear(); + + if (this.client) { + await this.client.close(); + this.client = null; + logger.info('Disconnected from Etcd'); + } + } +} + +const etcdServer = new EtcdServer(); + +export { EtcdServer, etcdServer }; diff --git a/src/database/mongo.js b/src/database/mongo.js new file mode 100644 index 0000000..b28b932 --- /dev/null +++ b/src/database/mongo.js @@ -0,0 +1,51 @@ +import mongoose from 'mongoose'; +import { loadConfig } from '../config.js'; +import log4js from 'log4js'; + +const config = loadConfig(); +const logger = log4js.getLogger('Mongo DB'); +logger.level = config.server.logLevel; + +class MongoServer { + constructor() { + this.connected = false; + this.connecting = false; + this.connectionPromise = null; + this.url = config.database.mongo.url; + } + + connect() { + if (this.connected) return mongoose.connection; + if (this.connecting) return this.connectionPromise; + this.connecting = true; + logger.info('Connecting to MongoDB...'); + logger.debug('Connection URL:', this.url); + this.connectionPromise = mongoose + .connect(this.url, {}) + .then(conn => { + this.connected = true; + logger.info('Database connected.'); + return conn.connection; + }) + .catch(err => { + this.connected = false; + logger.error('MongoDB connection error:', err); + throw err; + }) + .finally(() => { + this.connecting = false; + }); + return this.connectionPromise; + } + + async getConnection() { + if (!this.connected) { + await this.connect(); + } + return mongoose.connection; + } +} + +const mongoServer = new MongoServer(); + +export { MongoServer, mongoServer }; diff --git a/src/database/user.schema.js b/src/database/user.schema.js new file mode 100644 index 0000000..3da6863 --- /dev/null +++ b/src/database/user.schema.js @@ -0,0 +1,20 @@ +import mongoose from 'mongoose'; + +const userSchema = new mongoose.Schema( + { + username: { required: true, type: String }, + name: { required: true, type: String }, + firstName: { required: false, type: String }, + lastName: { required: false, type: String }, + email: { required: true, type: String } + }, + { timestamps: true } +); + +userSchema.virtual('id').get(function () { + return this._id.toHexString(); +}); + +userSchema.set('toJSON', { virtuals: true }); + +export const userModel = mongoose.model('User', userSchema); diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..fa4e08e --- /dev/null +++ b/src/index.js @@ -0,0 +1,53 @@ +import { loadConfig } from './config.js'; +import { KeycloakAuth } from './auth/auth.js'; +import { SocketManager } from './socket/socketmanager.js'; +import { etcdServer } from './database/etcd.js'; +import express from 'express'; +import log4js from 'log4js'; +import http from 'http'; +import { mongoServer } from './database/mongo.js'; + +(async () => { + // Load configuration + const config = loadConfig(); + + // Setup logger + const logger = log4js.getLogger('FarmControl API WS'); + logger.level = config.server.logLevel; + + // Create Express app and HTTP server + const app = express(); + const server = http.createServer(app); + + // Setup Keycloak Integration + const keycloakAuth = new KeycloakAuth(config); + new SocketManager(keycloakAuth, server); + + // Connect to Etcd (await) + try { + await etcdServer.connect(); + logger.info('Connected to Etcd'); + } catch (err) { + logger.error('Failed to connect to Etcd:', err); + throw err; + } + + // Connect to Mongo DB (await) + try { + await mongoServer.connect(); + logger.info('Connected to Mongo DB'); + } catch (err) { + logger.error('Failed to connect to Mongo DB:', err); + throw err; + } + + // Start HTTP server + server.listen(config.server.port, () => { + logger.info(`Server listening on port ${config.server.port}`); + }); + + process.on('SIGINT', async () => { + logger.info('Shutting down...'); + await etcdServer.disconnect(); + }); +})(); diff --git a/src/lock/lockmanager.js b/src/lock/lockmanager.js new file mode 100644 index 0000000..18192be --- /dev/null +++ b/src/lock/lockmanager.js @@ -0,0 +1,100 @@ +import { etcdServer } from '../database/etcd.js'; +import log4js from 'log4js'; +import { loadConfig } from '../config.js'; +const config = loadConfig(); + +// Setup logger +const logger = log4js.getLogger('Lock Manager'); +logger.level = config.server.logLevel; + +/** + * LockManager handles distributed locking using Etcd and broadcasts lock events via websockets. + */ +export class LockManager { + constructor(socketManager) { + this.socketManager = socketManager; + this.setupLocksListeners(); + } + + async lockObject(object) { + // Add a 'lock' event to the 'locks' stream + logger.debug('Locking object:', object._id); + try { + await etcdServer.set(`/locks/${object.type}s/${object._id}`, object); + logger.info(`Lock event to id: ${object._id}`); + return true; + } catch (err) { + logger.error(`Error adding lock event to: ${object._id}:`, err); + throw err; + } + } + + async unlockObject(object) { + // Add an 'unlock' event to the 'locks' stream + const key = `/locks/${object.type}s/${object._id}`; + console.log('unlocking'); + try { + logger.debug('Checking user can unlock:', object._id); + + const lockEvent = await etcdServer.get(key); + + if (lockEvent?.user === object.user) { + logger.debug('Unlocking object:', object._id); + await etcdServer.delete(key); + logger.info(`Unlocked object: ${object._id}`); + return true; + } + } catch (err) { + logger.error(`Error unlocking object ${object._id}:`, err); + throw err; + } + } + + async getObjectLock(object) { + // Get the current lock status of an object and broadcast it + logger.info('Getting lock status for object:', object._id); + try { + const lockKey = `/locks/${object.type}s/${object._id}`; + const lockValue = await etcdServer.get(lockKey); + + if (lockValue) { + // Object is locked + logger.debug(`Object ${object._id} is locked`); + return { + ...lockValue, + locked: true + }; + } else { + // Object is not locked + logger.debug(`Object ${object._id} is not locked`); + return { + _id: object._id, + locked: false + }; + } + } catch (err) { + logger.error(`Error getting lock status for object ${object._id}:`, err); + throw err; + } + } + + setupLocksListeners() { + etcdServer.onPrefixPut('/locks', (key, value) => { + const id = key.split('/').pop(); + logger.debug('Lock object event:', id); + this.socketManager.broadcast('notify_lock_update', { + ...value, + locked: true + }); + }); + etcdServer.onPrefixDelete('/locks', key => { + const id = key.split('/').pop(); + logger.debug('Unlock object event:', id); + this.socketManager.broadcast('notify_lock_update', { + _id: id, + locked: false + }); + }); + logger.info('Subscribed to Etcd stream for lock changes.'); + } +} diff --git a/src/notification/notificationmanager.js b/src/notification/notificationmanager.js new file mode 100644 index 0000000..e69de29 diff --git a/src/socket/socketclient.js b/src/socket/socketclient.js new file mode 100644 index 0000000..9cf695f --- /dev/null +++ b/src/socket/socketclient.js @@ -0,0 +1,128 @@ +import log4js from 'log4js'; +// Load configuration +import { loadConfig } from '../config.js'; +import { userModel } from '../database/user.schema.js'; + +const config = loadConfig(); + +const logger = log4js.getLogger('Socket Client'); +logger.level = config.server.logLevel; + +export class SocketClient { + constructor(socket, socketManager) { + this.socket = socket; + this.user = null; + this.socketManager = socketManager; + this.lockManager = socketManager.lockManager; + this.updateManager = socketManager.updateManager; + } + + async initUser() { + if (this.socket?.user?.username) { + try { + const userDoc = await userModel + .findOne({ username: this.socket.user.username }) + .lean(); + this.user = userDoc; + logger.debug('ID:', this.user._id.toString()); + logger.debug('Name:', this.user.name); + logger.debug('Username:', this.user.username); + logger.debug('Email:', this.user.email); + this.setupSocketEventHandlers(); + } catch (err) { + logger.error('Error looking up user by username:', err); + this.user = null; + } + } + } + + setupSocketEventHandlers() { + this.socket.on('lock', this.handleLockEvent.bind(this)); + this.socket.on('unlock', this.handleUnlockEvent.bind(this)); + this.socket.on('getLock', this.handleGetLockEvent.bind(this)); + this.socket.on('update', this.handleUpdateEvent.bind(this)); + } + + async handleLockEvent(data) { + // data: { _id: string, params?: object } + if (!data || !data._id) { + this.socket.emit('lock_result', { + success: false, + error: 'Invalid lock event data' + }); + return; + } + data = { ...data, user: this.user._id.toString() }; + try { + await this.lockManager.lockObject(data); + } catch (err) { + logger.error('Lock event error:', err); + this.socket.emit('lock_result', { success: false, error: err.message }); + } + } + + async handleUnlockEvent(data) { + // data: { _id: string } + if (!data || !data._id) { + this.socket.emit('unlock_result', { + success: false, + error: 'Invalid unlock event data' + }); + return; + } + data = { ...data, user: this.user._id.toString() }; + try { + await this.lockManager.unlockObject(data); + } catch (err) { + logger.error('Unlock event error:', err); + this.socket.emit('unlock_result', { success: false, error: err.message }); + } + } + + async handleGetLockEvent(data, callback) { + // data: { _id: string } + if (!data || !data._id) { + callback({ + error: 'Invalid getLock event data' + }); + return; + } + try { + const lockEvent = await this.lockManager.getObjectLock(data); + callback(lockEvent); + } catch (err) { + logger.error('GetLock event error:', err); + callback({ + error: err.message + }); + } + } + + async handleUpdateEvent(data) { + // data: { _id: string, type: string, ...otherProperties } + if (!data || !data._id || !data.type) { + return; + } + + try { + // Add user information to the update data + const updateData = { + ...data, + updatedAt: new Date() + }; + + // Use the updateManager to handle the update + if (this.updateManager) { + await this.updateManager.updateObject(updateData); + } else { + throw new Error('UpdateManager not available'); + } + } catch (err) { + logger.error('Update event error:', err); + } + } + + handleDisconnect() { + logger.info('External client disconnected:', this.socket.user?.username); + } +} diff --git a/src/socket/socketmanager.js b/src/socket/socketmanager.js new file mode 100644 index 0000000..82258a4 --- /dev/null +++ b/src/socket/socketmanager.js @@ -0,0 +1,82 @@ +// server.js - HTTP and Socket.IO server setup +import { Server } from 'socket.io'; +import { createAuthMiddleware } from '../auth/auth.js'; +import log4js from 'log4js'; +// Load configuration +import { loadConfig } from '../config.js'; +import { SocketClient } from './socketclient.js'; +import { LockManager } from '../lock/lockmanager.js'; +import { UpdateManager } from '../updates/updatemanager.js'; + +const config = loadConfig(); + +const logger = log4js.getLogger('Socket Manager'); +logger.level = config.server.logLevel; + +export class SocketManager { + constructor(auth, server) { + this.socketClientConnections = new Map(); + this.lockManager = new LockManager(this); + this.updateManager = new UpdateManager(this); + + // Use the provided HTTP server + // Create Socket.IO server + const io = new Server(server, { + cors: { + origin: config.server.corsOrigins || '*', + methods: ['GET', 'POST'] + } + }); + + // Apply authentication middleware + io.use(createAuthMiddleware(auth)); + + // Handle client connections + io.on('connection', async socket => { + logger.info('External client connected:', socket.user?.username); + await this.addClient(socket); + }); + + this.io = io; + this.server = server; + } + + async addClient(socket) { + const client = new SocketClient(socket, this, this.lockManager); + await client.initUser(); + this.socketClientConnections.set(socket.id, client); + logger.info('External client connected:', socket.user?.username); + // Handle disconnection + socket.on('disconnect', () => { + logger.info('External client disconnected:', socket.user?.username); + this.removeClient(socket.id); + }); + } + + removeClient(socketClientId) { + const socketClient = this.socketClientConnections.get(socketClientId); + if (socketClient) { + this.socketClientConnections.delete(socketClientId); + logger.info( + 'External client disconnected:', + socketClient.socket.user?.username + ); + } + } + + getSocketClient(clientId) { + return this.socketClientConnections.get(clientId); + } + + getAllSocketClients() { + return Array.from(this.socketClientConnections.values()); + } + + broadcast(event, data, excludeClientId = null) { + for (const [clientId, socketClient] of this.socketClientConnections) { + if (excludeClientId !== clientId) { + socketClient.socket.emit(event, data); + } + } + } +} diff --git a/src/updates/updatemanager.js b/src/updates/updatemanager.js new file mode 100644 index 0000000..678d33d --- /dev/null +++ b/src/updates/updatemanager.js @@ -0,0 +1,51 @@ +import { etcdServer } from '../database/etcd.js'; + +import log4js from 'log4js'; +import { loadConfig } from '../config.js'; +const config = loadConfig(); + +// Setup logger +const logger = log4js.getLogger('Update Manager'); +logger.level = config.server.logLevel; + +/** + * UpdateManager handles tracking object updates using Etcd and broadcasts update events via websockets. + */ +export class UpdateManager { + constructor(socketManager) { + this.socketManager = socketManager; + this.setupUpdatesListeners(); + } + + async updateObject(object) { + // Add an 'update' event to the 'updates' stream + logger.debug('Updating object:', object._id); + try { + const updateData = { + _id: object._id, + type: object.type, + updatedAt: new Date().toISOString() + }; + + await etcdServer.set( + `/updates/${object.type}s/${object._id}`, + updateData + ); + logger.info(`Update event for id: ${object._id}`); + } catch (err) { + logger.error(`Error adding update event to: ${object._id}:`, err); + throw err; + } + } + + setupUpdatesListeners() { + etcdServer.onPrefixPut('/updates', (key, value) => { + const id = key.split('/').pop(); + logger.debug('Update object event:', id); + this.socketManager.broadcast('notify_object_update', { + ...value + }); + }); + logger.info('Subscribed to Etcd stream for update changes.'); + } +} diff --git a/src/websockets.js b/src/websockets.js index 34824ec..0824f1a 100644 --- a/src/websockets.js +++ b/src/websockets.js @@ -1,436 +1,34 @@ // WebSocketServer.js -const express = require("express"); -const http = require("http"); -const socketIo = require("socket.io"); -const { MongoClient } = require("mongodb"); -const socketioJwt = require("socketio-jwt"); -const log4js = require("log4js"); -const config = require("../config.json"); -const { trace } = require("console"); +import express from 'express'; +import http from 'http'; +import { Server as SocketIo } from 'socket.io'; +import log4js from 'log4js'; +import dotenv from 'dotenv'; +import { KeycloakAuth, createAuthMiddleware } from './auth/auth.js'; -const logger = log4js.getLogger("Web Sockets"); -logger.level = config.logLevel; +dotenv.config(); -class WebSocketServer { +const logger = log4js.getLogger('Web Sockets'); +logger.level = process.env.LOG_LEVEL; + +export default class WebSocketServer { constructor() { this.app = express(); this.server = http.createServer(this.app); - this.io = socketIo(this.server, { + this.io = new SocketIo(this.server, { cors: { - origin: "*", + origin: '*' }, maxHttpBufferSize: 1e8 }); - this.JWT_SECRET = process.env.JWT_SECRET || config.jwt_secret; - this.mongoUri = process.env.MONGO_URI || config.mongo_uri; - this.dbName = "farmcontrol"; // DB Name - - this.hostSockets = []; - this.userSockets = []; - } - - async connectToMongo() { - try { - const client = new MongoClient(this.mongoUri, {}); - await client.connect(); - this.db = client.db(this.dbName); - logger.info("Connected to MongoDB"); - this.clearHostsCollection(); - } catch (err) { - logger.error("MongoDB connection error:", err); - } + this.hostServices = []; + this.userServices = []; } configureMiddleware() { - // Middleware for JWT authentication - this.io.use( - socketioJwt.authorize({ - secret: this.JWT_SECRET, - handshake: true, - }) - ); - } - - setupSocketIO() { - this.io.on("connection", (socket) => { - var connectionType = "user"; - var hostId; - var id, email; - if (socket.decoded_token.hasOwnProperty("hostId")) { - var connectionType = "host"; - hostId = socket.decoded_token.hostId; - logger.info("Host connected:", hostId); - this.hostSockets.push(socket.id); - this.handleHostOnlineEvent({ hostId }); - } else { - id = socket.decoded_token.id; - email = socket.decoded_token.email; - logger.info("User connected:", id); - this.userSockets.push(socket.id); - } - - socket.on("disconnect", () => { - if (connectionType == "host") { - logger.info("Host " + socket.decoded_token.hostId + " disconnected."); - this.hostSockets = this.hostSockets.filter(id => id !== socket.id); - this.handleHostOfflineEvent({ hostId: hostId }); - } else { - logger.info("User " + socket.decoded_token.id + " disconnected.") - this.userSockets = this.userSockets.filter(id => id !== socket.id); - } - }); - - socket.on("status", (data) => { - logger.info("Setting", data.remoteAddress, "status to", data.status); - if (data.type == "printer") { - this.handlePrinterStatusEvent(data); - } - }); - - socket.on("online", (data) => { - logger.info("Setting", data.remoteAddress, "to online."); - if (data.type == "printer") { - this.handlePrinterOnlineEvent(data, socket); - } - }); - - socket.on("offline", (data) => { - logger.info("Setting", data.remoteAddress, "to offline."); - if (data.type == "printer") { - this.handlePrinterOfflineEvent(data); - } - }); - - socket.on("join", (data) => { - if (data.remoteAddress) { - logger.trace("Joining room", data.remoteAddress); - socket.join(data.remoteAddress); - } - }); - - socket.on("leave", (data) => { - if (data.remoteAddress) { - logger.trace("Leaving room", data.remoteAddress); - socket.leave(data.remoteAddress); - } - }); - - socket.on("temperature", (data) => { - if (connectionType == "host") { - socket.to(data.remoteAddress).emit("temperature", data); - } - }); - - socket.on("command", (data) => { - if (connectionType == "user") { - socket.to(data.remoteAddress).emit(data.type, data); - } - }); - }); - } - - async clearHostsCollection() { - try { - if (!this.db) { - await this.connectToMongo(); - } - - // Delete all documents from hosts collection - const deleteResult = await this.db.collection("hosts").deleteMany({}); - - logger.info( - `Deleted ${deleteResult.deletedCount} documents from hosts collection` - ); - } catch (error) { - logger.error("Error clearing hosts collection:", error); - } - } - - async handlePrinterOnlineEvent(data, socket) { - try { - if (!this.db) { - await this.connectToMongo(); - } - - // Check if data.remoteAddress exists in printers collection - const existingPrinter = await this.db - .collection("printers") - .findOne({ remoteAddress: data.remoteAddress }); - - if (existingPrinter) { - // If exists, update the document - const updateResult = await this.db.collection("printers").updateOne( - { remoteAddress: data.remoteAddress }, - { - $set: { - online: true, - status: { type: "Online" }, - connectedAt: new Date(), - hostId: data.hostId, // Assuming hostId is passed as a parameter to handleIdentifyEvent - }, - } - ); - - if (updateResult.modifiedCount > 0) { - logger.info(`Printer updated: ${data.remoteAddress}`); - } else { - logger.warn(`Printer not updated: ${data.remoteAddress}`); - } - } else { - // If not exists, insert a new document - const insertData = { - remoteAddress: data.remoteAddress, - hostId: data.hostId, - online: true, - status: { type: "Online" }, - connectedAt: new Date(), - friendlyName: "", - loadedFillament: null, - }; - const insertResult = await this.db - .collection("printers") - .insertOne(insertData); - - if (insertResult.insertedCount > 0) { - logger.info(`New printer added: ${data.remoteAddress}`); - } else { - logger.warn(`Failed to add new printer: ${data.remoteAddress}`); - } - } - - const onlineData = { - remoteAddress: data.remoteAddress, - hostId: data.hostId, - online: true, - status: { type: "Online" }, - connectedAt: new Date(), - }; - - logger.trace("Sending online data", onlineData) - this.sendStatusToUserSockets(onlineData); - - // Join socket room using remoteAddress as name - //logger.trace("Joining room", data.remoteAddress); - logger.trace("Joining room", data.remoteAddress); - socket.join(data.remoteAddress); - - } catch (error) { - logger.error("Error handling online event:", error); - } - } - - async handlePrinterStatusEvent(data) { - try { - if (!this.db) { - await this.connectToMongo(); - } - - // Check if data.remoteAddress exists in printers collection - const existingPrinter = await this.db - .collection("printers") - .findOne({ remoteAddress: data.remoteAddress }); - - if (existingPrinter) { - // If exists, update the document - const updateResult = await this.db.collection("printers").updateOne( - { remoteAddress: data.remoteAddress }, - { - $set: { - status: data.status, - }, - } - ); - - if (updateResult.modifiedCount > 0) { - logger.info(`Printer updated: ${data.remoteAddress}`); - } else { - logger.warn(`Printer not updated: ${data.remoteAddress}`); - } - } else { - - } - - const onlineData = { - remoteAddress: data.remoteAddress, - hostId: data.hostId, - online: true, - connectedAt: new Date(), - }; - - logger.trace("Sending status data", onlineData) - this.sendStatusToUserSockets(data); - - } catch (error) { - logger.error("Error handling status event:", error); - } - } - - async handlePrinterOfflineEvent(data) { - try { - if (!this.db) { - await this.connectToMongo(); - } - - // Check if data.remoteAddress exists in printers collection - const existingPrinter = await this.db - .collection("printers") - .findOne({ remoteAddress: data.remoteAddress }); - - if (existingPrinter) { - // If exists, update the document - const updateResult = await this.db.collection("printers").updateOne( - { remoteAddress: data.remoteAddress }, - { - $set: { - online: false, - status: { type: "Offline" }, - connectedAt: null, - hostId: data.hostId, // Assuming hostId is passed as a parameter to handleIdentifyEvent - }, - } - ); - - if (updateResult.modifiedCount > 0) { - logger.info(`Printer updated: ${data.remoteAddress}`); - } else { - logger.warn(`Printer not updated: ${data.remoteAddress}`); - } - } else { - // If not exists, insert a new document - const insertResult = await this.db.collection("printers").insertOne({ - remoteAddress: data.remoteAddress, - hostId: data.hostId, - online: false, - status: { type: "Offline" }, - connectedAt: null, - friendlyName: "", - loadedFillament: null, - }); - - if (insertResult.insertedCount > 0) { - logger.info(`New printer added: ${data.remoteAddress}`); - } else { - logger.warn(`Failed to add new printer: ${data.remoteAddress}`); - } - } - const offlineData = { - remoteAddress: data.remoteAddress, - hostId: data.hostId, - online: false, - status: { type: "Offline" }, - connectedAt: null, - }; - - logger.trace("Sending offline data", offlineData) - this.sendStatusToUserSockets(offlineData); - - } catch (error) { - logger.error("Error handling offline event:", error); - } - } - - async handleHostOnlineEvent(data) { - try { - if (!this.db) { - await this.connectToMongo(); - } - - // Add new entry to hosts collection. - const insertResult = await this.db.collection("hosts").insertOne({ - hostId: data.hostId, - online: true, - connectedAt: new Date(), - }); - - if (insertResult.insertedCount != 0) { - logger.info(`New host added: ${data.hostId}`); - } else { - logger.warn(`Failed to add new host: ${data.hostId}`); - } - } catch (error) { - logger.error("Error handling online event:", error); - } - } - - async handleHostOfflineEvent(data) { - //try { - if (!this.db) { - await this.connectToMongo(); - } - - // Delete user from hosts collection - const deleteUserResult = await this.db - .collection("hosts") - .deleteOne({ hostId: data.hostId }); - - if (deleteUserResult.deletedCount > 0) { - logger.info(`User deleted from hosts collection: ${data.hostId}`); - } else { - logger.warn(`User not found in hosts collection: ${data.hostId}`); - } - - // Update all printers with matching hostId - const updatePrintersResult = await this.db - .collection("printers") - .updateMany( - { hostId: data.hostId }, - { - $set: { - online: false, - status: { type: "Offline" }, - connectedAt: null, - }, - } - ); - - const listPrintersResult = await this.db - .collection("printers") - .find({ hostId: data.hostId }).toArray(); - - if (updatePrintersResult.modifiedCount > 0) { - logger.info( - `Updated ${updatePrintersResult.modifiedCount} printers for user: ${data.hostId}` - ); - for (let i = 0; i < listPrintersResult.length; i++) { - const printer = listPrintersResult[i]; - const offlineData = { - remoteAddress: printer.remoteAddress, - hostId: printer.hostId, - online: false, - status: { type: "Offline" }, - connectedAt: null, - }; - logger.info( - `Sending offline status for: ${printer.remoteAddress}` - ); - this.sendStatusToUserSockets(offlineData) - } - } else { - logger.warn(`No printers found for user: ${data.hostId}`); - } - - - //} catch (error) { - // logger.error('Error handling user offline event:', error); - //} - } - - sendStatusToUserSockets(data) { - this.userSockets.forEach(id => { - this.io.to(id).emit("status", data); - }); - } - - async start(port = 3000) { - await this.connectToMongo(); - this.configureMiddleware(); - this.setupSocketIO(); - - this.server.listen(port, () => { - logger.info(`Server is running on port ${port}`); - }); + // Middleware for Keycloak authentication + const keycloakAuth = new KeycloakAuth(); + this.io.use(createAuthMiddleware(keycloakAuth)); } } - -module.exports = { WebSocketServer };