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.

This commit is contained in:
Tom Butcher 2025-06-29 22:39:23 +01:00
parent 15cb1a03b6
commit bf56234c4b
24 changed files with 1931 additions and 483 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

13
.eslintignore Normal file
View File

@ -0,0 +1,13 @@
node_modules/
dist/
build/
coverage/
*.min.js
*.bundle.js
package-lock.json
yarn.lock
pnpm-lock.yaml
.env
.env.*
logs/
*.log

100
.eslintrc.json Normal file
View File

@ -0,0 +1,100 @@
{
"env": {
"node": true,
"es2022": true
},
"extends": ["eslint:recommended", "prettier"],
"plugins": ["prettier"],
"parserOptions": {
"ecmaVersion": 2022,
"sourceType": "module"
},
"rules": {
"prettier/prettier": "error",
"indent": ["error", 2],
"linebreak-style": ["error", "unix"],
"quotes": ["error", "single"],
"semi": ["error", "always"],
"no-unused-vars": ["error", { "argsIgnorePattern": "^_" }],
"no-console": "warn",
"prefer-const": "error",
"no-var": "error",
"object-shorthand": "error",
"prefer-template": "error",
"template-curly-spacing": ["error", "never"],
"arrow-spacing": "error",
"no-duplicate-imports": "error",
"no-useless-rename": "error",
"prefer-destructuring": [
"error",
{
"array": true,
"object": true
}
],
"prefer-rest-params": "error",
"prefer-spread": "error",
"no-useless-constructor": "error",
"no-useless-computed-key": "error",
"no-useless-escape": "error",
"no-useless-return": "error",
"no-constant-condition": ["error", { "checkLoops": false }],
"no-empty": ["error", { "allowEmptyCatch": true }],
"no-extra-boolean-cast": "error",
"no-extra-semi": "error",
"no-irregular-whitespace": "error",
"no-multiple-empty-lines": ["error", { "max": 2 }],
"no-trailing-spaces": "error",
"eol-last": "error",
"comma-dangle": ["error", "never"],
"comma-spacing": ["error", { "before": false, "after": true }],
"comma-style": ["error", "last"],
"key-spacing": ["error", { "beforeColon": false, "afterColon": true }],
"keyword-spacing": ["error", { "before": true, "after": true }],
"object-curly-spacing": ["error", "always"],
"array-bracket-spacing": ["error", "never"],
"space-before-blocks": "error",
"space-before-function-paren": [
"error",
{
"anonymous": "always",
"named": "never",
"asyncArrow": "always"
}
],
"space-in-parens": ["error", "never"],
"space-infix-ops": "error",
"space-unary-ops": [
"error",
{
"words": true,
"nonwords": false
}
],
"spaced-comment": ["error", "always"],
"brace-style": ["error", "1tbs", { "allowSingleLine": true }],
"camelcase": ["error", { "properties": "never" }],
"new-cap": ["error", { "newIsCap": true, "capIsNew": false }],
"new-parens": "error",
"no-array-constructor": "error",
"no-new-object": "error",
"no-new-require": "error",
"no-path-concat": "error",
"no-process-exit": "error",
"no-return-assign": "error",
"no-self-compare": "error",
"no-sequences": "error",
"no-throw-literal": "error",
"no-unmodified-loop-condition": "error",
"no-unused-expressions": "error",
"no-useless-call": "error",
"no-useless-concat": "error",
"no-useless-escape": "error",
"no-void": "error",
"no-warning-comments": "warn",
"prefer-promise-reject-errors": "error",
"require-await": "error",
"yoda": "error"
},
"ignorePatterns": ["node_modules/", "dist/", "build/", "*.min.js"]
}

7
.prettierignore Normal file
View File

@ -0,0 +1,7 @@
node_modules/
dist/
build/
*.min.js
package-lock.json
.git/
.vscode/

15
.prettierrc Normal file
View File

@ -0,0 +1,15 @@
{
"semi": true,
"trailingComma": "none",
"singleQuote": true,
"printWidth": 80,
"tabWidth": 2,
"useTabs": false,
"endOfLine": "lf",
"arrowParens": "avoid",
"bracketSpacing": true,
"commaSpacing": true,
"objectCurlySpacing": true,
"arrayBracketSpacing": false,
"spaceBeforeFunctionParen": true
}

17
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,17 @@
{
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
},
"eslint.validate": [
"javascript"
],
"eslint.workingDirectories": [
"."
],
"prettier.requireConfig": true,
"files.eol": "\n",
"files.insertFinalNewline": true,
"files.trimTrailingWhitespace": true
}

178
README.md Normal file
View File

@ -0,0 +1,178 @@
# FarmControl WebSocket Service
A WebSocket microservice for FarmControl that handles real-time communication and distributed locking using etcd.
## Features
- Real-time WebSocket communication
- Distributed locking using etcd
- Keycloak authentication integration
- MongoDB integration for user management
- Event streaming and notifications
## Prerequisites
- Node.js (v16 or higher)
- etcd server
- MongoDB server
- Keycloak server (for authentication)
## Installation
1. Clone the repository
2. Install dependencies:
```bash
npm install
```
## Configuration
The application uses `config.json` for configuration. Update the following sections:
### Etcd Configuration
```json
{
"database": {
"etcd": {
"host": "localhost",
"port": 2379
}
}
}
```
### MongoDB Configuration
```json
{
"database": {
"mongo": {
"url": "mongodb://localhost:27017/farmcontrol"
}
}
}
```
### Authentication Configuration
```json
{
"auth": {
"enabled": true,
"keycloak": {
"url": "https://your-keycloak-server",
"realm": "your-realm",
"clientId": "your-client-id",
"clientSecret": "your-client-secret"
}
}
}
```
## Running the Application
### Development
```bash
npm run dev
```
### Production
```bash
npm start
```
## Etcd Setup
### Installation
#### Using Docker
```bash
docker run -d --name etcd \
-p 2379:2379 \
-p 2380:2380 \
quay.io/coreos/etcd:v3.5.0 \
/usr/local/bin/etcd \
--advertise-client-urls http://0.0.0.0:2379 \
--listen-client-urls http://0.0.0.0:2379
```
#### Using Homebrew (macOS)
```bash
brew install etcd
etcd
```
#### Using apt (Ubuntu/Debian)
```bash
sudo apt-get install etcd
sudo systemctl start etcd
```
### Verification
Test that etcd is running:
```bash
curl http://localhost:2379/version
```
## Migration from Redis
This application was migrated from Redis to etcd. The main changes include:
1. **Stream-like functionality**: Redis streams are replaced with etcd key-value pairs using a prefix pattern
2. **Hash-like functionality**: Redis hashes are replaced with etcd key-value pairs using a prefix pattern
3. **Pub/Sub**: Redis pub/sub is replaced with etcd watchers
4. **Connection management**: Simplified connection handling with automatic reconnection
### Key Differences
- **Data structure**: etcd uses a flat key-value store, so we simulate Redis data structures using key prefixes
- **Streams**: Instead of Redis streams, we use etcd keys with timestamps and random suffixes
- **Watching**: etcd watchers provide real-time notifications for key changes
- **Transactions**: etcd supports atomic operations for distributed locking
## API Endpoints
The service exposes WebSocket endpoints for:
- **Lock management**: Lock/unlock objects with real-time notifications
- **Authentication**: Keycloak-based authentication
- **Real-time updates**: Stream-based event notifications
## Development
### Project Structure
```
src/
├── auth/ # Authentication logic
├── database/ # Database connections (etcd, mongo)
├── lock/ # Distributed locking
├── notification/ # Notification management
├── socket/ # WebSocket handling
└── index.js # Main application entry point
```
### Adding New Features
1. **Database operations**: Use the `etcdServer` instance for etcd operations
2. **WebSocket events**: Extend the `SocketClient` class
3. **Authentication**: Extend the `KeycloakAuth` class
## Troubleshooting
### Common Issues
1. **Etcd connection failed**: Ensure etcd is running on the configured host and port
2. **Authentication errors**: Verify Keycloak configuration and credentials
3. **MongoDB connection issues**: Check MongoDB server status and connection string
### Logging
The application uses log4js for logging. Set the log level in the configuration:
```json
{
"server": {
"logLevel": "debug"
}
}
```
Available log levels: `trace`, `debug`, `info`, `warn`, `error`

21
app.js
View File

@ -1,21 +0,0 @@
const log4js = require("log4js");
const os = require("os");
const config = require("./config.json");
const WebSocketServer = require("./src/websockets.js").WebSocketServer;
const logger = log4js.getLogger("App");
logger.level = config.logLevel;
function showSystemInfo() {
logger.info("=== System Info ===")
logger.info("Hostname:", os.hostname());
logger.info("Memory:", os.totalmem() / 1024 / 1024 + "mb");
console.log("");
}
showSystemInfo();
logger.info("Web Socket Server Starting...");
var server = new WebSocketServer();
server.start(5050);

View File

@ -1,5 +1,52 @@
{ {
"logLevel": "trace", "development": {
"jwt_secret": "secret123", "server": {
"mongo_uri": "mongodb://localhost:27017/socketio" "port": 9090,
} "logLevel": "trace"
},
"auth": {
"enabled": true,
"keycloak": {
"url": "https://auth.tombutcher.work",
"realm": "master",
"clientId": "farmcontrol-client",
"clientSecret": "GPyh59xctRX83yfKWb83ShK6VEwHIvLF"
},
"requiredRoles": []
},
"database": {
"etcd": {
"host": "localhost",
"port": 2379
},
"mongo": {
"url": "mongodb://192.168.68.53:27017/farmcontrol"
}
}
},
"production": {
"server": {
"port": 8081,
"logLevel": "info"
},
"auth": {
"enabled": true,
"keycloak": {
"url": "https://auth.tombutcher.work",
"realm": "master",
"clientId": "farmcontrol-client",
"clientSecret": "GPyh59xctRX83yfKWb83ShK6VEwHIvLF"
},
"requiredRoles": []
},
"database": {
"etcd": {
"host": "localhost",
"port": 2379
},
"mongo": {
"url": "mongodb://farmcontrol.tombutcher.local:27017/farmcontrol"
}
}
}
}

625
package-lock.json generated
View File

@ -9,6 +9,9 @@
"version": "1.0.0", "version": "1.0.0",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"axios": "^1.10.0",
"dotenv": "^16.4.5",
"etcd3": "^1.1.2",
"express": "^4.19.2", "express": "^4.19.2",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"log4js": "^6.9.1", "log4js": "^6.9.1",
@ -19,6 +22,10 @@
"socketio-jwt": "^4.6.2" "socketio-jwt": "^4.6.2"
}, },
"devDependencies": { "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" "standard": "^17.1.0"
} }
}, },
@ -107,6 +114,37 @@
"node": "^12.22.0 || ^14.17.0 || >=16.0.0" "node": "^12.22.0 || ^14.17.0 || >=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": { "node_modules/@humanwhocodes/config-array": {
"version": "0.11.14", "version": "0.11.14",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz",
@ -170,6 +208,16 @@
"dev": true, "dev": true,
"license": "BSD-3-Clause" "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": { "node_modules/@mongodb-js/saslprep": {
"version": "1.1.7", "version": "1.1.7",
"resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.1.7.tgz", "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.1.7.tgz",
@ -217,6 +265,83 @@
"node": ">= 8" "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": { "node_modules/@socket.io/component-emitter": {
"version": "3.1.2", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
@ -333,7 +458,6 @@
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=8" "node": ">=8"
@ -343,7 +467,6 @@
"version": "4.3.0", "version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"color-convert": "^2.0.1" "color-convert": "^2.0.1"
@ -526,6 +649,12 @@
"url": "https://github.com/sponsors/ljharb" "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": { "node_modules/available-typed-arrays": {
"version": "1.0.7", "version": "1.0.7",
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", "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" "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": { "node_modules/balanced-match": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@ -566,6 +706,15 @@
"node": "^4.5.0 || >= 5.9" "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": { "node_modules/body-parser": {
"version": "1.20.2", "version": "1.20.2",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz",
@ -670,6 +819,19 @@
"url": "https://github.com/sponsors/ljharb" "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": { "node_modules/callsites": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
@ -697,11 +859,33 @@
"url": "https://github.com/chalk/chalk?sponsor=1" "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": { "node_modules/color-convert": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"color-name": "~1.1.4" "color-name": "~1.1.4"
@ -714,9 +898,20 @@
"version": "1.1.4", "version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true,
"license": "MIT" "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": { "node_modules/concat-map": {
"version": "0.0.1", "version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@ -918,6 +1113,15 @@
"node": ">= 0.4" "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": { "node_modules/depd": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
@ -950,6 +1154,32 @@
"node": ">=6.0.0" "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": { "node_modules/ecdsa-sig-formatter": {
"version": "1.0.11", "version": "1.0.11",
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", "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==", "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
"license": "MIT" "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": { "node_modules/encodeurl": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
@ -1118,13 +1354,10 @@
} }
}, },
"node_modules/es-define-property": { "node_modules/es-define-property": {
"version": "1.0.0", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"license": "MIT", "license": "MIT",
"dependencies": {
"get-intrinsic": "^1.2.4"
},
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
} }
@ -1165,10 +1398,9 @@
} }
}, },
"node_modules/es-object-atoms": { "node_modules/es-object-atoms": {
"version": "1.0.0", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"es-errors": "^1.3.0" "es-errors": "^1.3.0"
@ -1178,15 +1410,15 @@
} }
}, },
"node_modules/es-set-tostringtag": { "node_modules/es-set-tostringtag": {
"version": "2.0.3", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==", "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"get-intrinsic": "^1.2.4", "es-errors": "^1.3.0",
"get-intrinsic": "^1.2.6",
"has-tostringtag": "^1.0.2", "has-tostringtag": "^1.0.2",
"hasown": "^2.0.1" "hasown": "^2.0.2"
}, },
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
@ -1226,6 +1458,15 @@
"integrity": "sha512-oj4jOSXvWglTsc3wrw86iom3LDPOx1nbipQk+jaG3dy+sMRM6ReSgVr/VlmBuF6lXUrflN9DCcQHeSbAwGUl4g==", "integrity": "sha512-oj4jOSXvWglTsc3wrw86iom3LDPOx1nbipQk+jaG3dy+sMRM6ReSgVr/VlmBuF6lXUrflN9DCcQHeSbAwGUl4g==",
"license": "MIT" "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": { "node_modules/escape-html": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
@ -1301,6 +1542,22 @@
"url": "https://opencollective.com/eslint" "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": { "node_modules/eslint-config-standard": {
"version": "17.1.0", "version": "17.1.0",
"resolved": "https://registry.npmjs.org/eslint-config-standard/-/eslint-config-standard-17.1.0.tgz", "resolved": "https://registry.npmjs.org/eslint-config-standard/-/eslint-config-standard-17.1.0.tgz",
@ -1564,6 +1821,37 @@
"eslint": ">=7.0.0" "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": { "node_modules/eslint-plugin-promise": {
"version": "6.6.0", "version": "6.6.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-6.6.0.tgz", "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-6.6.0.tgz",
@ -1811,6 +2099,21 @@
"node": ">= 0.6" "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": { "node_modules/express": {
"version": "4.19.2", "version": "4.19.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz",
@ -1860,6 +2163,13 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/fast-json-stable-stringify": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "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==", "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==",
"license": "ISC" "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": { "node_modules/for-each": {
"version": "0.3.3", "version": "0.3.3",
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
@ -1963,6 +2293,22 @@
"is-callable": "^1.1.3" "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": { "node_modules/forwarded": {
"version": "0.2.0", "version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@ -2040,17 +2386,31 @@
"url": "https://github.com/sponsors/ljharb" "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": { "node_modules/get-intrinsic": {
"version": "1.2.4", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0", "es-errors": "^1.3.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2", "function-bind": "^1.1.2",
"has-proto": "^1.0.1", "get-proto": "^1.0.1",
"has-symbols": "^1.0.3", "gopd": "^1.2.0",
"hasown": "^2.0.0" "has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.1.0"
}, },
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
@ -2059,6 +2419,19 @@
"url": "https://github.com/sponsors/ljharb" "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": { "node_modules/get-stdin": {
"version": "8.0.0", "version": "8.0.0",
"resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-8.0.0.tgz", "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-8.0.0.tgz",
@ -2159,12 +2532,12 @@
} }
}, },
"node_modules/gopd": { "node_modules/gopd": {
"version": "1.0.1", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"license": "MIT", "license": "MIT",
"dependencies": { "engines": {
"get-intrinsic": "^1.1.3" "node": ">= 0.4"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
@ -2219,6 +2592,7 @@
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz",
"integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
@ -2228,9 +2602,9 @@
} }
}, },
"node_modules/has-symbols": { "node_modules/has-symbols": {
"version": "1.0.3", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
@ -2243,7 +2617,6 @@
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"has-symbols": "^1.0.3" "has-symbols": "^1.0.3"
@ -2538,6 +2911,15 @@
"url": "https://github.com/sponsors/ljharb" "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": { "node_modules/is-generator-function": {
"version": "1.0.10", "version": "1.0.10",
"resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", "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" "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": { "node_modules/lodash.includes": {
"version": "4.3.0", "version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
@ -3082,6 +3470,12 @@
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"license": "MIT" "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": { "node_modules/loose-envify": {
"version": "1.4.0", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
@ -3095,6 +3489,15 @@
"loose-envify": "cli.js" "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": { "node_modules/media-typer": {
"version": "0.3.0", "version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
@ -3831,6 +4234,35 @@
"node": ">= 0.8.0" "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": { "node_modules/process-nextick-args": {
"version": "1.0.7", "version": "1.0.7",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz",
@ -3849,6 +4281,30 @@
"react-is": "^16.13.1" "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": { "node_modules/proxy-addr": {
"version": "2.0.7", "version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
@ -3862,6 +4318,12 @@
"node": ">= 0.10" "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": { "node_modules/punycode": {
"version": "2.3.1", "version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@ -4026,6 +4488,15 @@
"semver": "bin/semver" "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": { "node_modules/resolve": {
"version": "1.22.8", "version": "1.22.8",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
@ -4638,6 +5109,20 @@
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"license": "MIT" "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": { "node_modules/string.prototype.matchall": {
"version": "4.0.11", "version": "4.0.11",
"resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.11.tgz", "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.11.tgz",
@ -4732,7 +5217,6 @@
"version": "6.0.1", "version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"ansi-regex": "^5.0.1" "ansi-regex": "^5.0.1"
@ -4790,6 +5274,22 @@
"url": "https://github.com/sponsors/ljharb" "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": { "node_modules/text-table": {
"version": "0.2.0", "version": "0.2.0",
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
@ -5180,6 +5680,23 @@
"node": ">=0.10.0" "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": { "node_modules/wrappy": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
@ -5229,6 +5746,42 @@
"node": ">=0.4" "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": { "node_modules/yocto-queue": {
"version": "0.1.0", "version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",

View File

@ -3,14 +3,23 @@
"version": "1.0.0", "version": "1.0.0",
"description": "Farmcontrol Web Socket microservice", "description": "Farmcontrol Web Socket microservice",
"main": "app.js", "main": "app.js",
"type": "module",
"scripts": { "scripts": {
"test": "echo \"Error: no test specified\" && exit 1", "test": "echo \"Error: no test specified\" && exit 1",
"start": "node app.js", "start": "node src/index.js",
"dev": "nodemon app.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", "author": "Tom Butcher",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"axios": "^1.10.0",
"dotenv": "^16.4.5",
"etcd3": "^1.1.2",
"express": "^4.19.2", "express": "^4.19.2",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"log4js": "^6.9.1", "log4js": "^6.9.1",
@ -21,6 +30,10 @@
"socketio-jwt": "^4.6.2" "socketio-jwt": "^4.6.2"
}, },
"devDependencies": { "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" "standard": "^17.1.0"
} }
} }

BIN
src/.DS_Store vendored Normal file

Binary file not shown.

147
src/auth/auth.js Normal file
View File

@ -0,0 +1,147 @@
// auth.js - Keycloak authentication handler
import axios from 'axios';
import jwt from 'jsonwebtoken';
import log4js from 'log4js';
// Load configuration
import { loadConfig } from '../config.js';
const config = loadConfig();
const logger = log4js.getLogger('Auth');
logger.level = config.server.logLevel;
export class KeycloakAuth {
constructor(config) {
this.config = config.auth;
this.tokenCache = new Map(); // Cache for verified tokens
}
// Verify a token with Keycloak server
async verifyToken(token) {
// Check cache first
if (this.tokenCache.has(token)) {
const cachedInfo = this.tokenCache.get(token);
if (cachedInfo.expiresAt > 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'));
}
};
}

40
src/config.js Normal file
View File

@ -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;
}

256
src/database/etcd.js Normal file
View File

@ -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 };

51
src/database/mongo.js Normal file
View File

@ -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 };

View File

@ -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);

53
src/index.js Normal file
View File

@ -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();
});
})();

100
src/lock/lockmanager.js Normal file
View File

@ -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.');
}
}

View File

128
src/socket/socketclient.js Normal file
View File

@ -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);
}
}

View File

@ -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);
}
}
}
}

View File

@ -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.');
}
}

View File

@ -1,436 +1,34 @@
// WebSocketServer.js // WebSocketServer.js
const express = require("express"); import express from 'express';
const http = require("http"); import http from 'http';
const socketIo = require("socket.io"); import { Server as SocketIo } from 'socket.io';
const { MongoClient } = require("mongodb"); import log4js from 'log4js';
const socketioJwt = require("socketio-jwt"); import dotenv from 'dotenv';
const log4js = require("log4js"); import { KeycloakAuth, createAuthMiddleware } from './auth/auth.js';
const config = require("../config.json");
const { trace } = require("console");
const logger = log4js.getLogger("Web Sockets"); dotenv.config();
logger.level = config.logLevel;
class WebSocketServer { const logger = log4js.getLogger('Web Sockets');
logger.level = process.env.LOG_LEVEL;
export default class WebSocketServer {
constructor() { constructor() {
this.app = express(); this.app = express();
this.server = http.createServer(this.app); this.server = http.createServer(this.app);
this.io = socketIo(this.server, { this.io = new SocketIo(this.server, {
cors: { cors: {
origin: "*", origin: '*'
}, },
maxHttpBufferSize: 1e8 maxHttpBufferSize: 1e8
}); });
this.JWT_SECRET = process.env.JWT_SECRET || config.jwt_secret; this.hostServices = [];
this.mongoUri = process.env.MONGO_URI || config.mongo_uri; this.userServices = [];
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);
}
} }
configureMiddleware() { configureMiddleware() {
// Middleware for JWT authentication // Middleware for Keycloak authentication
this.io.use( const keycloakAuth = new KeycloakAuth();
socketioJwt.authorize({ this.io.use(createAuthMiddleware(keycloakAuth));
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}`);
});
} }
} }
module.exports = { WebSocketServer };