commit bcfce32adbb8515bf311eed45aa65ef21032b44e Author: Giuseppe Raffa <77052701+sesee3@users.noreply.github.com> Date: Sat Mar 28 15:29:34 2026 +0100 feat: initialize microservice architecture with auth, api, realtime, copernicus, ml, and console modules diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..e69de29 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6ed48a9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.env +node_modules diff --git a/api/.dockerignore b/api/.dockerignore new file mode 100644 index 0000000..5171c54 --- /dev/null +++ b/api/.dockerignore @@ -0,0 +1,2 @@ +node_modules +npm-debug.log \ No newline at end of file diff --git a/api/.env.example b/api/.env.example new file mode 100644 index 0000000..765950e --- /dev/null +++ b/api/.env.example @@ -0,0 +1,14 @@ +PORT=3003 + +VERSION=2.0.0 +VERSION_BUILD=1.0 +VERSION_STATE=pre-release + +INFLX_URL= +INFLX_TOKEN= +INFLX_ORG= + +MINIO_ENDPOINT= +MINIO_PORT= +MINIO_ACCESS_KEY= +MINIO_SECRET_KEY= \ No newline at end of file diff --git a/api/Dockerfile b/api/Dockerfile new file mode 100644 index 0000000..a4a8991 --- /dev/null +++ b/api/Dockerfile @@ -0,0 +1,13 @@ +FROM node:20-alpine + +WORKDIR /app + +COPY package*.json ./ +RUN npm install + + +COPY src ./src + +EXPOSE 3003 + +CMD ["node", "src/index.js"] \ No newline at end of file diff --git a/api/package-lock.json b/api/package-lock.json new file mode 100644 index 0000000..6bc1a97 --- /dev/null +++ b/api/package-lock.json @@ -0,0 +1,1541 @@ +{ + "name": "meb-api-service", + "version": "2.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "meb-api-service", + "version": "2.0.0", + "dependencies": { + "@influxdata/influxdb-client": "^1.35.0", + "@influxdata/influxdb-client-apis": "^1.35.0", + "cookie-parser": "^1.4.7", + "dotenv": "^17.3.1", + "express": "^5.2.1", + "ioredis": "^5.10.0", + "jsonwebtoken": "^9.0.3", + "minio": "^8.0.7", + "pg": "^8.20.0" + } + }, + "node_modules/@influxdata/influxdb-client": { + "version": "1.35.0", + "resolved": "https://registry.npmjs.org/@influxdata/influxdb-client/-/influxdb-client-1.35.0.tgz", + "integrity": "sha512-woWMi8PDpPQpvTsRaUw4Ig+nOGS/CWwAwS66Fa1Vr/EkW+NEwxI8YfPBsdBMn33jK2Y86/qMiiuX/ROHIkJLTw==", + "license": "MIT" + }, + "node_modules/@influxdata/influxdb-client-apis": { + "version": "1.35.0", + "resolved": "https://registry.npmjs.org/@influxdata/influxdb-client-apis/-/influxdb-client-apis-1.35.0.tgz", + "integrity": "sha512-+7h6smVPHYBge2rNKgYh/5k+SriYvPMsoJDfbUiQt1vJtpWnElwgBDLDl7Cr6d9XPC+FCI9GP4GQEMv7y8WxdA==", + "license": "MIT", + "peerDependencies": { + "@influxdata/influxdb-client": "*" + } + }, + "node_modules/@ioredis/commands": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.5.1.tgz", + "integrity": "sha512-JH8ZL/ywcJyR9MmJ5BNqZllXNZQqQbnVZOqpPQqE1vHiFgAw4NHbvE0FOduNU8IX9babitBT46571OnPTT0Zcw==", + "license": "MIT" + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, + "node_modules/block-stream2": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/block-stream2/-/block-stream2-2.1.0.tgz", + "integrity": "sha512-suhjmLI57Ewpmq00qaygS8UgEq2ly2PCItenIyhMqVjo4t4pGzqMvfgJuX8iWTeSDdfSSqS6j38fL4ToNL7Pfg==", + "license": "MIT", + "dependencies": { + "readable-stream": "^3.4.0" + } + }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/browser-or-node": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/browser-or-node/-/browser-or-node-2.1.1.tgz", + "integrity": "sha512-8CVjaLJGuSKMVTxJ2DpBl5XnlNDiT4cQFeuCJJrvJmts9YrTZDizTX7PjC2s6W4x+MBGZeEY6dGMrF04/6Hgqg==", + "license": "MIT" + }, + "node_modules/buffer-crc32": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz", + "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/content-disposition": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-parser": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", + "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==", + "license": "MIT", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.6" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/cookie-parser/node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decode-uri-component": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", + "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dotenv": { + "version": "17.3.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz", + "integrity": "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==", + "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", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "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", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "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" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "license": "MIT" + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/fast-xml-builder": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.2.tgz", + "integrity": "sha512-NJAmiuVaJEjVa7TjLZKlYd7RqmzOC91EtPFXHvlTcqBVo50Qh7XV5IwvXi1c7NRz2Q/majGX9YLcwJtWgHjtkA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "path-expression-matcher": "^1.1.3" + } + }, + "node_modules/fast-xml-parser": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.5.3.tgz", + "integrity": "sha512-Ymnuefk6VzAhT3SxLzVUw+nMio/wB1NGypHkgetwtXcK1JfryaHk4DWQFGVwQ9XgzyS5iRZ7C2ZGI4AMsdMZ6A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "fast-xml-builder": "^1.1.2", + "path-expression-matcher": "^1.1.3", + "strnum": "^2.1.2" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/filter-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz", + "integrity": "sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "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", + "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" + }, + "funding": { + "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/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "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" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ioredis": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.10.0.tgz", + "integrity": "sha512-HVBe9OFuqs+Z6n64q09PQvP1/R4Bm+30PAyyD4wIEqssh3v9L21QjCVk4kRLucMBcDokJTcLjsGeVRlq/nH6DA==", + "license": "MIT", + "dependencies": { + "@ioredis/commands": "1.5.1", + "cluster-key-slot": "^1.1.0", + "debug": "^4.3.4", + "denque": "^2.1.0", + "lodash.defaults": "^4.2.0", + "lodash.isarguments": "^3.1.0", + "redis-errors": "^1.2.0", + "redis-parser": "^3.0.0", + "standard-as-callback": "^2.1.0" + }, + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ioredis" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "license": "MIT" + }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "license": "MIT" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "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": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/minio": { + "version": "8.0.7", + "resolved": "https://registry.npmjs.org/minio/-/minio-8.0.7.tgz", + "integrity": "sha512-E737MgufW8CeQAsTAtnEMrxZ9scMSf29kkhZoXzDTKj/Jszzo2SfeZUH9wbDQH2Rsq6TCtl/yQL0+XdVKZansQ==", + "license": "Apache-2.0", + "dependencies": { + "async": "^3.2.4", + "block-stream2": "^2.1.0", + "browser-or-node": "^2.1.1", + "buffer-crc32": "^1.0.0", + "eventemitter3": "^5.0.1", + "fast-xml-parser": "^5.3.4", + "ipaddr.js": "^2.0.1", + "lodash": "^4.17.21", + "mime-types": "^2.1.35", + "query-string": "^7.1.3", + "stream-json": "^1.8.0", + "through2": "^4.0.2", + "xml2js": "^0.5.0 || ^0.6.2" + }, + "engines": { + "node": "^16 || ^18 || >=20" + } + }, + "node_modules/minio/node_modules/ipaddr.js": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.3.0.tgz", + "integrity": "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/minio/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minio/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-expression-matcher": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.1.3.tgz", + "integrity": "sha512-qdVgY8KXmVdJZRSS1JdEPOKPdTiEK/pi0RkcT2sw1RhXxohdujUlJFPuS1TSkevZ9vzd3ZlL7ULl1MHGTApKzQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/pg": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.20.0.tgz", + "integrity": "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.12.0", + "pg-pool": "^3.13.0", + "pg-protocol": "^1.13.0", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.3.0" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.3.0.tgz", + "integrity": "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.12.0.tgz", + "integrity": "sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.13.0.tgz", + "integrity": "sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.13.0.tgz", + "integrity": "sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", + "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/query-string": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.3.tgz", + "integrity": "sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==", + "license": "MIT", + "dependencies": { + "decode-uri-component": "^0.2.2", + "filter-obj": "^1.1.0", + "split-on-first": "^1.0.0", + "strict-uri-encode": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/redis-errors": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", + "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/redis-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", + "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", + "license": "MIT", + "dependencies": { + "redis-errors": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/sax": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.5.0.tgz", + "integrity": "sha512-21IYA3Q5cQf089Z6tgaUTr7lDAyzoTPx5HRtbhsME8Udispad8dC/+sziTNugOEx54ilvatQ9YCzl4KQLPcRHA==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/split-on-first": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz", + "integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/standard-as-callback": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", + "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==", + "license": "MIT" + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/stream-chain": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/stream-chain/-/stream-chain-2.2.5.tgz", + "integrity": "sha512-1TJmBx6aSWqZ4tx7aTpBDXK0/e2hhcNSTV8+CbFJtDjbb+I1mZ8lHit0Grw9GRT+6JbIrrDd8esncgBi8aBXGA==", + "license": "BSD-3-Clause" + }, + "node_modules/stream-json": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/stream-json/-/stream-json-1.9.1.tgz", + "integrity": "sha512-uWkjJ+2Nt/LO9Z/JyKZbMusL8Dkh97uUBTv3AJQ74y07lVahLY4eEFsPsE97pxYBwr8nnjMAIch5eqI0gPShyw==", + "license": "BSD-3-Clause", + "dependencies": { + "stream-chain": "^2.2.5" + } + }, + "node_modules/strict-uri-encode": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz", + "integrity": "sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/strnum": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.0.tgz", + "integrity": "sha512-Y7Bj8XyJxnPAORMZj/xltsfo55uOiyHcU2tnAVzHUnSJR/KsEX+9RoDeXEnsXtl/CX4fAcrt64gZ13aGaWPeBg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, + "node_modules/through2": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz", + "integrity": "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==", + "license": "MIT", + "dependencies": { + "readable-stream": "3" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/xml2js": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", + "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + } + } +} diff --git a/api/package.json b/api/package.json new file mode 100644 index 0000000..465261d --- /dev/null +++ b/api/package.json @@ -0,0 +1,22 @@ +{ + "name": "meb-api-service", + "version": "2.0.0", + "description": "API microservice for MEB console - Node.js", + "main": "src/index.js", + "scripts": { + "start": "node src/index.js", + "dev": "node --watch src/index.js", + "generate-docs": "node -e \"console.log(JSON.stringify(require('./swagger-specs-logic')))\" > openapi.json" + }, + "dependencies": { + "@influxdata/influxdb-client": "^1.35.0", + "@influxdata/influxdb-client-apis": "^1.35.0", + "cookie-parser": "^1.4.7", + "dotenv": "^17.3.1", + "express": "^5.2.1", + "ioredis": "^5.10.0", + "jsonwebtoken": "^9.0.3", + "minio": "^8.0.7", + "pg": "^8.20.0" + } +} diff --git a/api/src/index.js b/api/src/index.js new file mode 100644 index 0000000..20604ff --- /dev/null +++ b/api/src/index.js @@ -0,0 +1,73 @@ +const express = require('express'); +const parser = require('cookie-parser'); +const jwt = require('jsonwebtoken'); + +const app = express(); +const PORT = process.env.PORT; + +const version = process.env.VERSION; +const vBuild = process.env.VERSION_BUILD; +const vState = process.env.VERSION_STATE; + +app.use(express.json()); +app.use(parser()); + +app.get('/', (req, res) => { + res.redirect('/health'); +}); + +app.get('/health', (req, res) => { + res.json({ + status: "ok", + service: "api", + version: version, + build_number: vBuild, + version_state: vState + }); +}); + +// Route pubblica: autenticazione tramite SENSOR_CODE (per il plugin) +const paramsSensorRoutes = require('./routes/params.sensor'); +app.use('/params/sensor', paramsSensorRoutes); + +// Middleware di autenticazione per le API +app.use((req, res, next) => { + if (req.path === '/health' || req.path === '/') return next(); + + // 1. Service-to-service: x-api-key header + const apiKey = req.headers['x-api-key']; + if (apiKey && apiKey === process.env.INTERNAL_API_KEY) { + req.internal = true; + return next(); + } + + // 2. User auth: cookie o Authorization header + const token = req.cookies?.auth_token + || (req.headers.authorization?.startsWith('Bearer ') && req.headers.authorization.slice(7)); + + if (!token) { + return res.status(401).json({ error: 'Unauthorized: Nessun token di autenticazione fornito' }); + } + + try { + const payload = jwt.verify(token, process.env.JWT_SECRET, { algorithms: ['HS256'] }); + req.user = payload; + next(); + } catch (err) { + return res.status(401).json({ error: 'Unauthorized: Token non valido o scaduto' }); + } +}); + +const dataRoutes = require('./routes/data'); +app.use('/data', dataRoutes); + +const storageRoutes = require('./routes/storage') +app.use('/storage', storageRoutes) + +const paramsRoutes = require('./routes/params') +app.use('/params', paramsRoutes) + +// Avvio del server +app.listen(PORT, () => { + console.log(`Started on port ${PORT}`); +}); diff --git a/api/src/routes/data.js b/api/src/routes/data.js new file mode 100644 index 0000000..669784d --- /dev/null +++ b/api/src/routes/data.js @@ -0,0 +1,27 @@ +// Collezione di tutte le api che usando influxdb e la telemetria della barca + +//api.mebboat.it/data + +const router = require('express').Router(); + +const { write, query, writeBatch } = require('../storage/influx'); + +router.post('/write', async (req, res) => { + const { measurement, sensor, data } = req.body; + await write(measurement, sensor, data); + res.json({ status: "ok" }); +}); + +router.post('/write/batch', async (req, res) => { + const { points } = req.body; + await writeBatch(points); + res.json({ result: "added" }); +}); + +router.get('/query', async (req, res) => { + const { collection, timeFromNow, measurement, sensor, field } = req.query; + const result = await query(collection, timeFromNow, measurement, sensor, field); + res.json(result); +}); + +module.exports = router; \ No newline at end of file diff --git a/api/src/routes/params.js b/api/src/routes/params.js new file mode 100644 index 0000000..b489bb7 --- /dev/null +++ b/api/src/routes/params.js @@ -0,0 +1,216 @@ +const router = require('express').Router(); +const { query } = require('../storage/postgres'); + +const sets = ['forecasts', 'marine', 'sensors', 'telemetry']; + +/* ─── READS ─── */ + +/** + * GET /params/active?set=sensors + * Restituisce il set attivo per il tipo richiesto + */ +router.get('/active', async (req, res) => { + const { set } = req.query; + if (!set || !sets.includes(set)) + return res.status(400).json({ error: 'SET parameter invalid' }); + + try { + const result = await query(`SELECT * FROM ${set} WHERE active = true LIMIT 1`, [], 'references'); + res.json(result.rows[0] || null); + } catch (err) { + console.error('[PARAMS] Error reading active set:', err.message); + res.status(500).json({ error: 'Internal server error' }); + } +}); + +/** + * GET /params/sets?type=sensors + * Lista tutti i set di un tipo (con paginazione) + */ +router.get('/sets', async (req, res) => { + const { type } = req.query; + if (!type || !sets.includes(type)) + return res.status(400).json({ error: 'type parameter invalid' }); + + try { + const result = await query( + `SELECT id, name, description, mayor, minor, patch, state, active, tags, created_at, updated_at + FROM ${type} ORDER BY updated_at DESC`, + [], 'references' + ); + res.json(result.rows); + } catch (err) { + console.error('[PARAMS] Error listing sets:', err.message); + res.status(500).json({ error: 'Internal server error' }); + } +}); + +/** + * GET /params/sets/:id?type=sensors + * Restituisce un singolo set completo (con content) + */ +router.get('/sets/:id', async (req, res) => { + const { type } = req.query; + const { id } = req.params; + if (!type || !sets.includes(type)) + return res.status(400).json({ error: 'type parameter invalid' }); + + try { + const result = await query(`SELECT * FROM ${type} WHERE id = $1`, [id], 'references'); + if (!result.rows[0]) return res.status(404).json({ error: 'Set not found' }); + res.json(result.rows[0]); + } catch (err) { + console.error('[PARAMS] Error reading set:', err.message); + res.status(500).json({ error: 'Internal server error' }); + } +}); + + +/* ─── WRITES ─── */ + +/** + * POST /params/sets?type=sensors + * Crea un nuovo set di regole + */ +router.post('/sets', async (req, res) => { + const { type } = req.query; + if (!type || !sets.includes(type)) + return res.status(400).json({ error: 'type parameter invalid' }); + + const { name, description, content, tags } = req.body; + if (!name || !content) return res.status(400).json({ error: 'name and content required' }); + + try { + const result = await query( + `INSERT INTO ${type} (name, description, mayor, minor, patch, state, active, tags, content) + VALUES ($1, $2, 1, 0, 0, 'draft', false, $3, $4) RETURNING *`, + [name, description || '', tags || [], JSON.stringify(content)], + 'references' + ); + res.status(201).json(result.rows[0]); + } catch (err) { + console.error('[PARAMS] Error creating set:', err.message); + res.status(500).json({ error: 'Internal server error' }); + } +}); + +/** + * PUT /params/sets/:id?type=sensors + * Aggiorna un set esistente + */ +router.put('/sets/:id', async (req, res) => { + const { type } = req.query; + const { id } = req.params; + if (!type || !sets.includes(type)) + return res.status(400).json({ error: 'type parameter invalid' }); + + const { name, description, content, tags, state } = req.body; + + try { + const fields = []; + const values = []; + let idx = 1; + + if (name !== undefined) { fields.push(`name = $${idx++}`); values.push(name); } + if (description !== undefined) { fields.push(`description = $${idx++}`); values.push(description); } + if (content !== undefined) { fields.push(`content = $${idx++}`); values.push(JSON.stringify(content)); } + if (tags !== undefined) { fields.push(`tags = $${idx++}`); values.push(tags); } + if (state !== undefined) { fields.push(`state = $${idx++}`); values.push(state); } + + if (fields.length === 0) return res.status(400).json({ error: 'No fields to update' }); + + fields.push(`updated_at = NOW()`); + values.push(id); + + const result = await query( + `UPDATE ${type} SET ${fields.join(', ')} WHERE id = $${idx} RETURNING *`, + values, 'references' + ); + + if (!result.rows[0]) return res.status(404).json({ error: 'Set not found' }); + res.json(result.rows[0]); + } catch (err) { + console.error('[PARAMS] Error updating set:', err.message); + res.status(500).json({ error: 'Internal server error' }); + } +}); + +/** + * POST /params/sets/:id/activate?type=sensors + * Attiva un set (disattiva tutti gli altri dello stesso tipo) + */ +router.post('/sets/:id/activate', async (req, res) => { + const { type } = req.query; + const { id } = req.params; + if (!type || !sets.includes(type)) + return res.status(400).json({ error: 'type parameter invalid' }); + + try { + // Disattiva tutti + await query(`UPDATE ${type} SET active = false WHERE active = true`, [], 'references'); + // Attiva quello richiesto + const result = await query( + `UPDATE ${type} SET active = true, state = 'active', updated_at = NOW() WHERE id = $1 RETURNING *`, + [id], 'references' + ); + + if (!result.rows[0]) return res.status(404).json({ error: 'Set not found' }); + res.json(result.rows[0]); + } catch (err) { + console.error('[PARAMS] Error activating set:', err.message); + res.status(500).json({ error: 'Internal server error' }); + } +}); + +/** + * DELETE /params/sets/:id?type=sensors + * Elimina un set (solo se non attivo) + */ +router.delete('/sets/:id', async (req, res) => { + const { type } = req.query; + const { id } = req.params; + if (!type || !sets.includes(type)) + return res.status(400).json({ error: 'type parameter invalid' }); + + try { + // Controlla che non sia attivo + const check = await query(`SELECT active FROM ${type} WHERE id = $1`, [id], 'references'); + if (!check.rows[0]) return res.status(404).json({ error: 'Set not found' }); + if (check.rows[0].active) return res.status(409).json({ error: 'Cannot delete an active set' }); + + await query(`DELETE FROM ${type} WHERE id = $1`, [id], 'references'); + res.json({ deleted: true }); + } catch (err) { + console.error('[PARAMS] Error deleting set:', err.message); + res.status(500).json({ error: 'Internal server error' }); + } +}); + +/** + * PUT /params/sets/:id/version?type=sensors + * Incrementa la versione (bump) + */ +router.put('/sets/:id/version', async (req, res) => { + const { type } = req.query; + const { id } = req.params; + const { bump } = req.body; // 'major', 'minor', 'patch' + if (!type || !sets.includes(type)) + return res.status(400).json({ error: 'type parameter invalid' }); + + const field = bump === 'major' ? 'mayor' : bump === 'minor' ? 'minor' : 'patch'; + const resets = bump === 'major' ? ', minor = 0, patch = 0' : bump === 'minor' ? ', patch = 0' : ''; + + try { + const result = await query( + `UPDATE ${type} SET ${field} = ${field} + 1 ${resets}, updated_at = NOW() WHERE id = $1 RETURNING *`, + [id], 'references' + ); + if (!result.rows[0]) return res.status(404).json({ error: 'Set not found' }); + res.json(result.rows[0]); + } catch (err) { + console.error('[PARAMS] Error bumping version:', err.message); + res.status(500).json({ error: 'Internal server error' }); + } +}); + +module.exports = router; diff --git a/api/src/routes/params.sensor.js b/api/src/routes/params.sensor.js new file mode 100644 index 0000000..17337fe --- /dev/null +++ b/api/src/routes/params.sensor.js @@ -0,0 +1,51 @@ +const router = require('express').Router(); +const crypto = require('crypto'); +const { query } = require('../storage/postgres'); + +const sets = ['forecasts', 'sensors']; + +function hashSensorCode(code) { + return crypto.createHash('sha256').update(code).digest('hex'); +} + +/** + * GET /params/sensor/:sensorCode/active?set=sensors + * Autenticazione tramite SENSOR_CODE (stesso meccanismo di realtime) + */ +router.get('/:sensorCode/active', async (req, res) => { + const { sensorCode } = req.params; + const { set } = req.query; + + if (!set || !sets.includes(set)) + return res.status(400).json({ error: 'SET parameter invalid' }); + + try { + const hashed = hashSensorCode(sensorCode); + const sensor = await query( + 'SELECT id, is_active FROM sensors WHERE code_hash = $1', + [hashed], + 'sensors' + ); + + if (!sensor.rows[0]) { + return res.status(401).json({ error: 'Sensor code not valid' }); + } + + if (!sensor.rows[0].is_active) { + return res.status(403).json({ error: 'Sensor is not active' }); + } + + const result = await query( + `SELECT * FROM ${set} WHERE active = true LIMIT 1`, + [], + 'references' + ); + + res.json(result.rows[0] || null); + } catch (err) { + console.error('[PARAMS/SENSOR] Error:', err.message); + res.status(500).json({ error: 'Internal server error' }); + } +}); + +module.exports = router; diff --git a/api/src/routes/storage.js b/api/src/routes/storage.js new file mode 100644 index 0000000..3831ed4 --- /dev/null +++ b/api/src/routes/storage.js @@ -0,0 +1,59 @@ +// Collezzione di tutte le api che prendono i dati da minio +//api.mebboat.it/storage + +const express = require('express'); +const { getBuckets, getBucket, getObject, getObjects, getFileStream } = require('../storage/minio'); +const router = express.Router(); + +/** + * Restituisce una lista con tutti i bucket del database, + * altrimenti restituisce i file del bucket passato come parametro + * + * @param {string} bucket - il bucket + */ +router.get('/', async (req, res) => { + const { bucket } = req.query; + + if (bucket == undefined) { + const buckets = await getBuckets(); + res.status(200).json(buckets); + } else { + const returnBucket = await getBucket(bucket); + res.status(200).json(returnBucket); + } +}) + +router.get('/files', async (req, res) => { + const { bucket, fileID } = req.query; + + if (bucket == undefined) { + res.status(400).json({ error: "No bucket name in the request" }); + } else { + if (fileID == undefined) { + const files = await getObjects(bucket); + res.status(200).json(files); + } else { + const file = await getObject(bucket, fileID); + res.status(200).json(file); + } + } +}) + +router.get('/file', async (req, res) => { + const { bucket, fileID } = req.query; + const stream = await getFileStream(bucket, fileID); + res.setHeader('Content-Type', 'application/octet-stream'); + stream.pipe(res); +}) + +router.post('/upload', async (req, res) => { + const { bucket } = req.query; + const files = await getObjects(bucket); + res.status(200).json(files); +}) + +router.get('/download', async (req, res) => { + +}) + +module.exports = router; \ No newline at end of file diff --git a/api/src/storage/influx.js b/api/src/storage/influx.js new file mode 100644 index 0000000..eca99d8 --- /dev/null +++ b/api/src/storage/influx.js @@ -0,0 +1,67 @@ +const { InfluxDB, Point } = require('@influxdata/influxdb-client'); + +const url = process.env.INFLX_URL; +const token = process.env.INFLX_TOKEN; +const org = process.env.INFLX_ORG; + +const boatTelemetry = "boat" + +const client = new InfluxDB({ url, token }) +const write = client.getWriteApi(org, boatTelemetry); +const querying = client.getQueryApi(org); + + +async function append(measurement, sensor, data) { + const point = new Point(measurement) + .tag("sensor", sensor) + .floatField('temperature', data.temperature) + .floatField('humidity', data.humidity) + + write.writePoint(point); + await write.flush(); + +} + +async function writeBatch(datas) { + datas.forEach(data => { + append(data.measurement, data.sensor, data.data); + }) +} + +async function query(bucket, relativeTime, measurement, sensor, field) { + + const fluxTimeRange = relativeTime || "-1h"; + let fluxQuery = ` + from(bucket: "${bucket}") + |> range(start: ${fluxTimeRange}) + |> filter(fn: (r) => r._measurement == "${measurement}")`; + + if (sensor) { + fluxQuery += `\n |> filter(fn: (r) => r.sensor == "${sensor}")`; + } + + if (field) { + fluxQuery += `\n |> filter(fn: (r) => r._field == "${field}")`; + } + + fluxQuery += `\n |> yield(name: "data")`; + + try { + const data = []; + for await (const { values, tableMeta } of querying.iterateRows(fluxQuery)) { + data.push(tableMeta.toObject(values)); + } + return data; + } catch (error) { + console.error("Error in query:", error); + return []; + } + +} + +module.exports = { + write:append, + writeBatch, + query +} + diff --git a/api/src/storage/minio.js b/api/src/storage/minio.js new file mode 100644 index 0000000..75c012b --- /dev/null +++ b/api/src/storage/minio.js @@ -0,0 +1,136 @@ +const Minio = require('minio'); + +const client = new Minio.Client({ + endPoint: process.env.MINIO_ENDPOINT || 'localhost', + port: parseInt(process.env.MINIO_PORT) || 9000, + useSSL: process.env.MINIO_USE_SSL === 'true', + accessKey: process.env.MINIO_ACCESS_KEY, + secretKey: process.env.MINIO_SECRET_KEY +}) + + +// Buckets + +/** + * + * @param {String} bucket - Il nome del bucket + * @returns {String} - restituisce il nome del bucket creato + */ +async function bucketExists(bucket) { + const exists = await client.bucketExists(bucket); + if(!exists) { + await client.makeBucket(bucket); + } + return bucket +} + +/** + * Restituisce un array con tutti i bucket sul server + * @returns {Array} - i diversi bucket presenti sul server + */ +async function getBuckets() { + const buckets = await client.listBuckets(); + return buckets; +} + +/** + * Restituisce i metadata del bucket passato come parametro + * @param {String} bucket - il nome del bucket + * @returns {Object} - i metadata del bucket + */ +async function getBucket(bucket) { + const buckets = await client.listBuckets(); + return buckets.filter(b => b.name === bucket); +} + + +/** + * Restituisce un array con tutti gli oggetti presenti nel bucket passato come parametro + * @param {String} bucket - il nome del bucket + * @returns {Promise} - i file del bucket + */ +async function getObjects(bucket) { + return new Promise((resolve, reject) => { + const objects = []; + const stream = client.listObjects(bucket, '', true); + stream.on('data', obj => objects.push(obj)); + stream.on('error', err => reject(err)); + stream.on('end', () => resolve(objects)); + }); +} + +/** + * Restituisce i metadata del file con id passato come parametro presente nel bucket + * @param {String} bucket - il nome del bucket + * @param {String} objectName - il nome dell'oggetto + * @returns {Object} - i metadata del file + */ +async function getObject(bucket, objectName) { + const item = await client.statObject(bucket, objectName); + return item; +} + +/** + * Elimina il file con l'id passato a parametro dal bucket + * @param {String} bucket - il nome del bucket + * @param {String} objectName - il nome dell'oggetto + */ +async function removeObject(bucket, objectName) { + await client.removeObject(bucket, objectName); +} + + +//Upload - Download +/** + * Carica un file nel bucket come buffer + * @param {String} bucket - il nome del bucket + * @param {String} objectName - il nome che avrà il file in Minio + * @param {Buffer} fileBuffer - il file caricato + * @param {Number} size - dimensione del file + * @param {String} contentType - mimetype (es. 'image/png') + */ +async function upload(bucket, objectName, fileBuffer, size, contentType) { + await bucketExists(bucket); + + const metaData = { + 'Content-Type': contentType || 'application/octet-stream', + } + + const result = await client.putObject(bucket, objectName, fileBuffer, size, metaData); + return result; +} + +/** + * Genera un URL temporaneo di download + * @param {String} bucket - il nome del bucket + * @param {String} objectName - il nome del file + * @param {Number} expiry - quanto dura il link in secondi (default: 24h = 86400) + * + * @returns {String} url - url di download del file + */ +async function download(bucket, objectName, expiry = 86400) { + const url = await client.presignedGetObject(bucket, objectName, expiry); + return url; +} + +/** + * Recupera e ritorna lo stream dei dati dal server Minio (per leggere il contenuto via API) + * @param {String} bucket - il nome del bucket + * @param {String} objectName - il nome dell'oggetto + */ +async function getFileStream(bucket, objectName) { + const dataStream = await client.getObject(bucket, objectName); + return dataStream; +} + +module.exports = { + bucketExists, + getBuckets, + getBucket, + getObjects, + getObject, + removeObject, + upload, + download, + getFileStream +} \ No newline at end of file diff --git a/api/src/storage/postgres.js b/api/src/storage/postgres.js new file mode 100644 index 0000000..04cc596 --- /dev/null +++ b/api/src/storage/postgres.js @@ -0,0 +1,79 @@ +const { Pool } = require('pg'); + +const config = { + user: process.env.PG_USER, + password: process.env.PG_PASSWORD, + host: process.env.PG_HOST, + port: process.env.PG_PORT, + max: 10, + idleTimeoutMillis: 30000, + connectionTimeoutMillis: 5000 +} + +const pools = { + users: new Pool({ ...config, database: process.env.DATA_DB }), + references: new Pool({ ...config, database: process.env.REFERENCES_DB }), + sensors: new Pool({ ...config, database: process.env.SENSOR_DB || 'users' }) +} + +Object.entries(pools).forEach(([name, pool]) => { + pool.on('error', (err) => { + console.error(`Error in ${name} pool`, err); + }) +}); + +/** + * + * @param {'users' | 'references'} db - the name of the database + * @returns {Promise} + */ +async function getClient(db) { + const pool = pools[db]; + if (!pool) throw new Error(`Database pool type ${db} does not exist`); + return await pool.connect(); +} + +/** + * Esegue una query sul database specificato + * @param {string} text - Query SQL + * @param {any[]} params - Parametri + * @param {'users' | 'references'} name - Quale DB usare + */ +async function query(text, params, name = 'users') { + const client = await getClient(name); + try { + return await client.query(text, params); + } catch (error) { + console.error(`[DB Query Error on ${name}]`, error.message); + throw error; + } finally { + client.release(); + } +} + +/** + * Inserisce una riga in una tabella + */ +async function append(table, data, type = 'users') { + const keys = Object.keys(data); + const values = Object.values(data); + const placeholders = keys.map((_, i) => `$${i + 1}`).join(', '); + const columns = keys.join(', '); + const sql = `INSERT INTO ${table} (${columns}) VALUES (${placeholders}) RETURNING *`; + return await query(sql, values, type); +} + +/** + * Rimuove una riga + */ +async function remove(table, condition, params, type = 'users') { + const sql = `DELETE FROM ${table} WHERE ${condition}`; + return await query(sql, params, type); +} +module.exports = { + query, + append, + remove, + getClient, + pools +}; \ No newline at end of file diff --git a/auth/Dockerfile b/auth/Dockerfile new file mode 100644 index 0000000..ad9d768 --- /dev/null +++ b/auth/Dockerfile @@ -0,0 +1,13 @@ +FROM node:20-alpine + +WORKDIR /app + +COPY package*.json ./ +RUN npm install + + +COPY src ./src + +EXPOSE 3006 + +CMD ["node", "src/index.js"] \ No newline at end of file diff --git a/auth/package-lock.json b/auth/package-lock.json new file mode 100644 index 0000000..d4d2b06 --- /dev/null +++ b/auth/package-lock.json @@ -0,0 +1,1402 @@ +{ + "name": "meb-auth-service", + "version": "1.2.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "meb-auth-service", + "version": "1.2.0", + "dependencies": { + "bcrypt": "^6.0.0", + "cookie-parser": "^1.4.7", + "express": "^5.2.1", + "ioredis": "^5.10.0", + "jsonwebtoken": "^9.0.3", + "nunjucks": "^3.2.4", + "pg": "^8.20.0", + "ua-parser-js": "^2.0.9", + "uuid": "^13.0.0" + } + }, + "node_modules/@ioredis/commands": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.5.1.tgz", + "integrity": "sha512-JH8ZL/ywcJyR9MmJ5BNqZllXNZQqQbnVZOqpPQqE1vHiFgAw4NHbvE0FOduNU8IX9babitBT46571OnPTT0Zcw==", + "license": "MIT" + }, + "node_modules/a-sync-waterfall": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/a-sync-waterfall/-/a-sync-waterfall-1.0.1.tgz", + "integrity": "sha512-RYTOHHdWipFUliRFMCS4X2Yn2X8M87V/OpSqWzKKOGhzqyUxzyVmhHDH9sAvG+ZuQf/TAOFsLCpMw09I1ufUnA==", + "license": "MIT" + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "license": "MIT" + }, + "node_modules/bcrypt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz", + "integrity": "sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^8.3.0", + "node-gyp-build": "^4.8.4" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/commander": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/content-disposition": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-parser": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", + "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==", + "license": "MIT", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.6" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/cookie-parser/node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/detect-europe-js": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/detect-europe-js/-/detect-europe-js-0.1.2.tgz", + "integrity": "sha512-lgdERlL3u0aUdHocoouzT10d9I89VVhk0qNRmll7mXdGfJT1/wqZ2ZLA4oJAjeACPY5fT1wsbq2AT+GkuInsow==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + } + ], + "license": "MIT" + }, + "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", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "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", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "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" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "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", + "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" + }, + "funding": { + "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/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "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" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ioredis": { + "version": "5.10.1", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.10.1.tgz", + "integrity": "sha512-HuEDBTI70aYdx1v6U97SbNx9F1+svQKBDo30o0b9fw055LMepzpOOd0Ccg9Q6tbqmBSJaMuY0fB7yw9/vjBYCA==", + "license": "MIT", + "dependencies": { + "@ioredis/commands": "1.5.1", + "cluster-key-slot": "^1.1.0", + "debug": "^4.3.4", + "denque": "^2.1.0", + "lodash.defaults": "^4.2.0", + "lodash.isarguments": "^3.1.0", + "redis-errors": "^1.2.0", + "redis-parser": "^3.0.0", + "standard-as-callback": "^2.1.0" + }, + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ioredis" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/is-standalone-pwa": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-standalone-pwa/-/is-standalone-pwa-0.1.1.tgz", + "integrity": "sha512-9Cbovsa52vNQCjdXOzeQq5CnCbAcRk05aU62K20WO372NrTv0NxibLFCK6lQ4/iZEFdEA3p3t2VNOn8AJ53F5g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + } + ], + "license": "MIT" + }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "license": "MIT" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "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": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-addon-api": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.6.0.tgz", + "integrity": "sha512-gBVjCaqDlRUk0EwoPNKzIr9KkS9041G/q31IBShPs1Xz6UTA+EXdZADbzqAJQrpDRq71CIMnOP5VMut3SL0z5Q==", + "license": "MIT", + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/nunjucks": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/nunjucks/-/nunjucks-3.2.4.tgz", + "integrity": "sha512-26XRV6BhkgK0VOxfbU5cQI+ICFUtMLixv1noZn1tGU38kQH5A5nmmbk/O45xdyBhD1esk47nKrY0mvQpZIhRjQ==", + "license": "BSD-2-Clause", + "dependencies": { + "a-sync-waterfall": "^1.0.0", + "asap": "^2.0.3", + "commander": "^5.1.0" + }, + "bin": { + "nunjucks-precompile": "bin/precompile" + }, + "engines": { + "node": ">= 6.9.0" + }, + "peerDependencies": { + "chokidar": "^3.3.0" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/pg": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.20.0.tgz", + "integrity": "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.12.0", + "pg-pool": "^3.13.0", + "pg-protocol": "^1.13.0", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.3.0" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.3.0.tgz", + "integrity": "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.12.0.tgz", + "integrity": "sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.13.0.tgz", + "integrity": "sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.13.0.tgz", + "integrity": "sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", + "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/redis-errors": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", + "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/redis-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", + "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", + "license": "MIT", + "dependencies": { + "redis-errors": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/standard-as-callback": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", + "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==", + "license": "MIT" + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ua-is-frozen": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ua-is-frozen/-/ua-is-frozen-0.1.2.tgz", + "integrity": "sha512-RwKDW2p3iyWn4UbaxpP2+VxwqXh0jpvdxsYpZ5j/MLLiQOfbsV5shpgQiw93+KMYQPcteeMQ289MaAFzs3G9pw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + } + ], + "license": "MIT" + }, + "node_modules/ua-parser-js": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-2.0.9.tgz", + "integrity": "sha512-OsqGhxyo/wGdLSXMSJxuMGN6H4gDnKz6Fb3IBm4bxZFMnyy0sdf6MN96Ie8tC6z/btdO+Bsy8guxlvLdwT076w==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + }, + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" + } + ], + "license": "AGPL-3.0-or-later", + "dependencies": { + "detect-europe-js": "^0.1.2", + "is-standalone-pwa": "^0.1.1", + "ua-is-frozen": "^0.1.2" + }, + "bin": { + "ua-parser-js": "script/cli.js" + }, + "engines": { + "node": "*" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/uuid": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz", + "integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist-node/bin/uuid" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + } + } +} diff --git a/auth/package.json b/auth/package.json new file mode 100644 index 0000000..37c6692 --- /dev/null +++ b/auth/package.json @@ -0,0 +1,22 @@ +{ + "name": "meb-auth-service", + "version": "1.2.0", + "description": "Servizio di Sicurezza e Autenticazione per il server MEB", + "main": "src/index.js", + "scripts": { + "start": "node src/index.js", + "dev": "node --watch src/index.js", + "generate-docs": "node -e \"console.log(JSON.stringify(require('./swagger-specs-logic')))\" > openapi.json" + }, + "dependencies": { + "bcrypt": "^6.0.0", + "cookie-parser": "^1.4.7", + "express": "^5.2.1", + "ioredis": "^5.10.0", + "jsonwebtoken": "^9.0.3", + "nunjucks": "^3.2.4", + "pg": "^8.20.0", + "ua-parser-js": "^2.0.9", + "uuid": "^13.0.0" + } +} diff --git a/auth/src/core/auth.core.js b/auth/src/core/auth.core.js new file mode 100644 index 0000000..e746f23 --- /dev/null +++ b/auth/src/core/auth.core.js @@ -0,0 +1,120 @@ +const query = require('../storage/database').query; +const track = require('../tools/tracking') +const { v4: uuid } = require('uuid'); +const security = require('../tools/security') + + +/** + * Registra un nuovo utente + */ +async function register(username, password) { + const userExists = await query('SELECT id FROM users WHERE username = $1', [username]); + + if (userExists.rows.length > 0) { + throw new Error('User already exists'); + } + + const hashedPassword = security.hashPassword(password); + const id = uuid(); + + await query('INSERT INTO users (id, username, password_hash) VALUES ($1, $2, $3)', [id, username, hashedPassword]); + + return { + success: true, + user: { + id, + username + } + }; +} + + +/** + * Esegue il login di un utente + */ +async function login(username, password) { + const result = await query('SELECT id, username, password_hash, is_active, created_at FROM users WHERE username = $1', [username]); + if (result.rows.length === 0) { + throw new Error('No user matched') + } + + const user = result.rows[0]; + const isValid = await security.verifyPassword(password, user.password_hash); + + if (!isValid) { + throw new Error('Password mismatch') + } + + return { + id: user.id, + username: user.username, + created: user.created_at + } +} + +/** + * Esegue il logout di un utente + * + */ +async function logout(sessionID) { + if (!sessionID) { + throw new Error('no sessio id passed'); + } + + const result = await query('UPDATE sessions SET is_revoked = TRUE WHERE id = $1', [sessionID]); + return result.rowCount > 0; +} + +/** + * Crea una nuova sessione per un utente che ha appaena eseguito il login + */ +async function newSession(userId, userAgent, ip) { + const id = uuid(); + const sessionCode = security.generateSessionCode(); + const metadata = track.getBasicMetadata(userAgent); + + await query( + `INSERT INTO sessions (id, user_id, session_code, encoded_username, ip_address, user_agent, browser, os, device_type) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`, + [id, userId, sessionCode, '', ip, userAgent, metadata.browser, metadata.os, metadata.device_type] + ); + + return { id, sessionCode }; +} + +/** + * Valida una sessione + */ +async function validateSession(token) { + const parsed = security.parseSessionToken(token); + + if (!parsed) { + throw new Error('Invalid token format'); + } + + const { code, username } = parsed; + + const result = await query('SELECT s.id as session_id, s.user_id, u.username, u.is_active, u.created_at FROM sessions s JOIN users u ON s.user_id = u.id WHERE s.session_code = $1 AND s.is_revoked = FALSE', [code]); + if (result.rows.length === 0) { + throw new Error('Session not found or revoked') + } + + const session = result.rows[0]; + + if (session.username !== username) { + throw new Error('Session user mismatch'); + } + + if (!session.is_active) { + throw new Error('Session is not active'); + } +} + + +module.exports = { + register, + login, + logout, + newSession, + validateSession +} \ No newline at end of file diff --git a/auth/src/core/session.core.js b/auth/src/core/session.core.js new file mode 100644 index 0000000..88a6b9b --- /dev/null +++ b/auth/src/core/session.core.js @@ -0,0 +1,56 @@ +const query = require('../storage/database').query; +const { parseSessionToken } = require('../tools/security'); + +async function getSessions(username) { + const result = await query('SELECT s.id, s.session_code, s.browser, s.os, s.device_type, s.created_at, s.last_active, s.is_revoked FROM sessions s JOIN users u ON s.user_id = u.id WHERE u.username = $1 AND s.is_revoked = FALSE ORDER BY s.last_active DESC', [username]); + + return result.rows.map(s => ({ + id: s.id, + code: s.session_code, + browser: s.browser, + os: s.os, + deviceType: s.device_type, + createdAt: s.created_at?.toLocaleDateString('it-IT', { + month: 'short', day: 'numeric', year: 'numeric', hour: '2-digit', minute: '2-digit' + }), + lastActive: s.last_active?.toLocaleDateString('it-IT', { + month: 'short', day: 'numeric', year: 'numeric', hour: '2-digit', minute: '2-digit' + }), + isRevoked: s.is_revoked, + isCurrent: false + })); +}; + +async function getCurrentSessionID(token) { + const parsed = parseSessionToken(token); + if (!parsed) { + throw new Error('Invalid token'); + } + + const result = await query('SELECT id FROM sessions WHERE session_code = $1', [parsed.code]); + return result.rows[0]?.id || null; +} + +async function revoke(id, username) { + const result = await query('UPDATE sessions s SET is_revoked = TRUE FROM users u WHERE s.id = $1 AND s.user_id = u.id AND u.username = $2', [id, username]); + return result.rowCount > 0; +} + +async function revokeOthers(username, current) { + const result = await query('UPDATE sessions s SET is_revoked = TRUE FROM users u WHERE s.user_id = u.id AND u.username = $1 AND s.id != $2 AND s.is_revoked = FALSE', [username, current]); + return result.rowCount; +} + +async function getCount(username) { + const result = await query('SELECT COUNT(*) as count FROM sessions s JOIN users u ON s.user_id = u.id WHERE u.username = $1 AND s.is_revoked = FALSE', [username]); + return parseInt(result.rows[0].count, 10); +} + +module.exports = { + getSessions, + getCurrentSessionID, + revoke, + revokeOthers, + getCount +}; + diff --git a/auth/src/index.js b/auth/src/index.js new file mode 100644 index 0000000..91c4bc6 --- /dev/null +++ b/auth/src/index.js @@ -0,0 +1,49 @@ +const express = require('express'); +const nunjucks = require('nunjucks'); +const path = require('path'); +const parser = require('cookie-parser'); + +const database = require('./storage/database'); + +const app = express(); +const PORT = process.env.PORT || 3006; + +const version = process.env.VERSION; +const vBuild = process.env.VERSION_BUILD; +const vState = process.env.VERSION_STATE; + +app.use(express.json()); +app.use(express.urlencoded({ extended: true })); +app.use(parser()); + +// Static files +const staticFolder = path.join(__dirname, 'static'); +app.use('/static', express.static(staticFolder)); + +// Nunjucks templates +const templatesFolder = path.join(__dirname, 'templates'); +nunjucks.configure(templatesFolder, { + autoescape: true, + express: app, + noCache: true, + watch: false +}); +app.set('views', templatesFolder); +app.set('view engine', 'html'); + +// Routes +const authRoutes = require('./routes/auth'); +app.use('/', authRoutes); + +// Startup +async function start() { + await database.initDb(); + app.listen(PORT, () => { + console.log(`[AUTH] Started on port ${PORT}`); + }); +} + +start().catch(err => { + console.error('[AUTH] Failed to start:', err); + process.exit(1); +}); diff --git a/auth/src/routes/auth.js b/auth/src/routes/auth.js new file mode 100644 index 0000000..11662ae --- /dev/null +++ b/auth/src/routes/auth.js @@ -0,0 +1,97 @@ +const router = require('express').Router(); +const auth = require('../core/auth.core'); +const jwt = require('../tools/jwt'); + +const version = process.env.VERSION; +const vBuild = process.env.VERSION_BUILD; +const vState = process.env.VERSION_STATE; + +const CONSOLE_URL = process.env.CONSOLE_URL || 'http://localhost:3004'; +const COOKIE_DOMAIN = process.env.COOKIE_DOMAIN || undefined; + +router.get('/health', (req, res) => { + res.json({ + status: 'ok', + service: 'auth', + version: version, + build_number: vBuild, + version_state: vState + }); +}); + +router.post('/register', async (req, res) => { + const { username, password } = req.body; + + if (!username || !password) { + return res.status(400).json({ error: 'Username e password richiesti' }); + } + + try { + await auth.register(username, password); + res.status(201).end(); + } catch (err) { + console.error('[AUTH] Register failed:', err.message); + const status = err.message === 'User already exists' ? 409 : 500; + res.status(status).json({ error: err.message }); + } +}); + +router.get('/login', (req, res) => { + const redirect = req.query.redirect || ''; + res.render('loginpage', { error: null, redirect }); +}); + +router.post('/login', async (req, res) => { + const { username, password, redirect } = req.body; + + try { + const user = await auth.login(username, password); + const session = await auth.newSession(user.id, req.headers['user-agent'], req.ip); + const token = jwt.generateToken(user, session.id); + + const cookieOptions = { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + sameSite: 'lax', + maxAge: 7 * 24 * 60 * 60 * 1000 // 7 giorni + }; + + if (COOKIE_DOMAIN) { + cookieOptions.domain = COOKIE_DOMAIN; + } + + res.cookie('auth_token', token, cookieOptions); + + // Redirect alla pagina da cui l'utente e' arrivato, o alla console + const destination = redirect || CONSOLE_URL; + res.redirect(destination); + } catch (err) { + console.error('[AUTH] Login failed:', err.message, err.stack); + res.render('loginpage', { error: 'Credenziali non valide', redirect: redirect || '' }); + } +}); + +router.post('/logout', async (req, res) => { + const token = req.cookies && req.cookies.auth_token; + + if (token) { + try { + const verified = jwt.verifyToken(token); + if (verified.valid) { + await auth.logout(verified.payload.session_id); + } + } catch (err) { + console.error('[AUTH] Logout error:', err.message); + } + } + + const clearOptions = { httpOnly: true, sameSite: 'lax' }; + if (COOKIE_DOMAIN) { + clearOptions.domain = COOKIE_DOMAIN; + } + + res.clearCookie('auth_token', clearOptions); + res.redirect('/login'); +}); + +module.exports = router; diff --git a/auth/src/routes/sessions.js b/auth/src/routes/sessions.js new file mode 100644 index 0000000..e69de29 diff --git a/auth/src/routes/users.js b/auth/src/routes/users.js new file mode 100644 index 0000000..0651f20 --- /dev/null +++ b/auth/src/routes/users.js @@ -0,0 +1,2 @@ +const router = require('express').Router(); + diff --git a/auth/src/static/icon.png b/auth/src/static/icon.png new file mode 100644 index 0000000..652cfe3 Binary files /dev/null and b/auth/src/static/icon.png differ diff --git a/auth/src/static/style/login.css b/auth/src/static/style/login.css new file mode 100644 index 0000000..1ad937a --- /dev/null +++ b/auth/src/static/style/login.css @@ -0,0 +1,99 @@ +.container { + + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + user-select: none; + +} + +/* Fix size even if the title gets bigger when hovered*/ +.login { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + border-radius: 30px; + padding: 50px; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); +} + +span.prominent-title { + color: var(--accent-color); + font-weight: 700; + transition: all 0.3s ease; +} + +span.prominent-title:hover { + font-size: 26px; + /* Animated color gradient transitioning with all the colors of the rainbow, animated when hovere*/ + background: linear-gradient(90deg, #002bff, #7a00ff, #ff00c8); + background-size: 200% 200%; + animation: gradient 5s ease infinite; + -webkit-background-clip: text; + background-clip: text; + -webkit-text-fill-color: transparent; + +} + +@keyframes gradient { + 0% { + background-position: 0% 50%; + } + + 50% { + background-position: 100% 50%; + } + + 100% { + background-position: 0% 50%; + } +} + +.login header { + padding: 40px; + font-size: 24px; + font-weight: 700; +} + +.login form { + padding: 20px; + align-items: center; + align-self: center; + align-content: center; + +} + +.login form .group { + display: flex; + flex-direction: column; + justify-content: left; + align-items: left; + margin-bottom: 20px; +} + +.login form .group label { + font-size: 10px; + font-weight: 600; + align-items: left; + color: var(--text-secondary); + margin-bottom: 5px; +} + +.login form .group input { + padding: 10px; + border-radius: 15px; + border: 1px solid #ccc; + font-size: 14px; + font-weight: 600; + margin-bottom: 5px; + width: 100%; + transition: all 0.3s ease; +} + +.login form .group input:focus { + outline: none; + border-color: var(--accent-color); + box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); +} \ No newline at end of file diff --git a/auth/src/static/style/style.css b/auth/src/static/style/style.css new file mode 100644 index 0000000..8b7defd --- /dev/null +++ b/auth/src/static/style/style.css @@ -0,0 +1,201 @@ +:root { + --accent-color: #2563eb; + --accent-hover: #1d4ed8; + --accent-light: #eff6ff; + --accent-border: #bfdbfe; + + --text-primary: #0f172a; + --text-secondary: #4755698f; + --text-tertiary: #94a3b8c0; + + --surface: #f8fafc; + + --header-bg: rgba(255, 255, 255, 0.85); + /* For Glassmorphism */ + --header-border: #e2e8f0; + + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05); + --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); + --radius-md: 8px; + --radius-lg: 12px; +} + +* { + margin: 0; + padding: 0; +} + +@font-face { + font-family: 'Normal'; + src: url('../font/Quicksand-VariableFont_wght.ttf'); + font-weight: 400; + font-style: normal; +} + +@font-face { + font-family: 'Bold'; + src: url('../font/Quicksand-VariableFont_wght.ttf'); + font-weight: 700; + font-style: normal; +} + +body { + font-family: 'Normal', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; + color: var(--text-primary); + -webkit-font-smoothing: antialiased; +} + +button { + padding: 10px 24px; + border-radius: var(--radius-lg); + border: 1px solid var(--header-border); + background-color: var(--bg-surface); + color: var(--text-primary); + font-size: 14px; + font-weight: 600; + font-family: 'Bold', inherit; + cursor: pointer; + transition: all 0.5s cubic-bezier(0.8, 0, 0.2, 1); + white-space: nowrap; + flex-shrink: 0; +} + +button:hover { + background-color: var(--accent-light); + color: var(--accent-color); + border-color: var(--accent-border); +} + +button.prominent { + background-color: var(--accent-color); + color: #ffffff; + border: 1px solid transparent; + box-shadow: var(--shadow-sm); +} + +button.prominent:hover { + background-color: var(--accent-hover); + transform: translateY(-1px); + box-shadow: var(--shadow-md); +} + + +button.prominent:active { + transform: translateY(1px); + box-shadow: var(--shadow-sm); +} + + +/* INFO PANEL */ + +.info-panel { + display: block; + text-align: center; + align-content: center; + padding: 60px 20px; + user-select: none; +} + +.info-panel h3 { + margin-bottom: 10px; +} + +.info-panel p { + margin-bottom: 25px; +} + +.info-panel .icon { + font-size: 48px; + margin-bottom: 20px; + align-self: center; + transition: transform 0.12s ease; +} + + + + +/* GRID & CARD ITEMS */ + +.grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 1.5rem; + margin-inline: 30px; +} + +.card { + display: flex; + gap: 0.75rem; + align-items: center; + padding: 1rem; + border: 1px solid var(--header-border); + border-radius: 20px; + text-decoration: none; + color: var(--text-primary); + transition: transform 0.12s ease, box-shadow 0.12s ease; +} + +.card h3 { + margin: 0; + font-size: 1rem; + text-align: left; +} + +.card p { + margin: 0.25rem 0 0; + color: var(--text-secondary); + opacity: 0.4; + font-size: 0.8rem; + text-align: left; +} + +.card:hover { + transform: translateY(-4px); + box-shadow: 0 8px 30px #bfdbfe30; +} + +.card.standalone { + grid-column: 1 / -1; +} + + + + + +/* HEADER */ + +.header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 16px 24px; + background-color: var(--header-bg); + border-bottom: 1px solid var(--header-border); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + position: sticky; + top: 0; + z-index: 10; + user-select: none; +} + + +.header h1 { + color: var(--text-primary); + font-size: 1.25rem; + font-weight: 700; + letter-spacing: -0.025em; +} + +.header .profile { + display: flex; + align-items: center; + gap: 16px; +} + +.header .profile p { + font-size: 0.875rem; + font-weight: 500; + color: var(--text-secondary); + padding-inline: 5px; +} \ No newline at end of file diff --git a/auth/src/storage/database.js b/auth/src/storage/database.js new file mode 100644 index 0000000..d7c155c --- /dev/null +++ b/auth/src/storage/database.js @@ -0,0 +1,105 @@ +const { Pool } = require('pg'); + +const config = { + user: process.env.DB_USER, + password: process.env.DB_PASSWORD, + host: process.env.DB_HOST, + port: process.env.DB_PORT, + max: 10, + idleTimeoutMillis: 30000, + connectionTimeoutMillis: 5000 +} + +const pool = new Pool({ ...config, database: process.env.DB_NAME }); + +pool.on('error', (err) => { + console.error('Error in database', err); +}); + +/** + * Execute a query with parameters + * @param {string} text - SQL query + * @param {Array} params - Query parameters + * @returns {Promise} Query result + */ +async function query(text, params) { + const start = Date.now(); + const result = await pool.query(text, params); + const duration = Date.now() - start; + + if (duration > 100) { + console.warn(`[DB] Slow query (${duration}ms):`, text.substring(0, 80)); + } + + return result; +} + +/** + * Get a client from pool for transactions + * @returns {Promise} Pool client + */ +async function getClient() { + return await pool.connect(); +} + +/** + * Initialize database and ensure tables exist + */ +async function initDb() { + // Test connection + await pool.query('SELECT NOW()'); + // Ensure pgcrypto extension (provides gen_random_uuid) + // Note: creating extensions requires proper DB permissions (usually superuser in PG) + try { + await pool.query(`CREATE EXTENSION IF NOT EXISTS pgcrypto;`); + } catch (err) { + console.warn('[DB] Could not create pgcrypto extension (may require superuser):', err.message); + } + + // Ensure tables exist (UUID default generated by DB) + await pool.query(` + CREATE TABLE IF NOT EXISTS users ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + username VARCHAR(50) UNIQUE NOT NULL, + password_hash VARCHAR(255) NOT NULL, + is_active BOOLEAN DEFAULT TRUE, + created_at TIMESTAMP DEFAULT NOW(), + telegram_id VARCHAR(50) UNIQUE + ); + + CREATE INDEX IF NOT EXISTS idx_users_username ON users(username); + CREATE INDEX IF NOT EXISTS idx_users_telegram_id ON users(telegram_id); + `); + + await pool.query(` + CREATE TABLE IF NOT EXISTS sessions ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + session_code VARCHAR(64) NOT NULL, + encoded_username TEXT NOT NULL, + ip_address INET, + user_agent TEXT, + browser VARCHAR(100), + os VARCHAR(100), + device_type VARCHAR(50), + location_country VARCHAR(100), + location_city VARCHAR(100), + created_at TIMESTAMP DEFAULT NOW(), + last_active TIMESTAMP DEFAULT NOW(), + is_revoked BOOLEAN DEFAULT FALSE + ); + + -- Altera colonna in base al nuovo standard token 32 byte - 64 url chars + ALTER TABLE sessions ALTER COLUMN session_code TYPE VARCHAR(64); + + CREATE INDEX IF NOT EXISTS idx_sessions_code ON sessions(session_code); + CREATE INDEX IF NOT EXISTS idx_sessions_user_id ON sessions(user_id); + `); +} + +module.exports = { + pool, + query, + getClient, + initDb +}; diff --git a/auth/src/storage/redis.js b/auth/src/storage/redis.js new file mode 100644 index 0000000..b0680b9 --- /dev/null +++ b/auth/src/storage/redis.js @@ -0,0 +1,36 @@ +const Redis = require('ioredis'); + +const redis = new Redis({ + host: process.env.REDIS_HOST, + port: parseInt(process.env.REDIS_PORT), + maxRetriesPerRequest: 3, + lazyConnect: true, + retryStrategy(times) { + const delay = Math.min(times * 50, 2000); + return delay; + } +}) + +redis.on('error', (error) => { + console.error('redis error', error); +}) + +redis.on('connect', () => {}) +redis.on('reconnecting', () => { + console.log('reconnecting to redis') +}) + +async function configure() { + try { + await redis.connect(); + await redis.ping(); + } catch (err) { + console.error('Redis error', err); + } +} + +function connected() { + return redis.status === 'ready'; +} + +module.exports = { redis, configure, connected }; \ No newline at end of file diff --git a/auth/src/templates/loginpage.html b/auth/src/templates/loginpage.html new file mode 100644 index 0000000..87ad4fe --- /dev/null +++ b/auth/src/templates/loginpage.html @@ -0,0 +1,39 @@ + + + + + + + + + + +
+ +
+ + + \ No newline at end of file diff --git a/auth/src/templates/sessions.html b/auth/src/templates/sessions.html new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/auth/src/templates/sessions.html @@ -0,0 +1 @@ + diff --git a/auth/src/templates/user.html b/auth/src/templates/user.html new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/auth/src/templates/user.html @@ -0,0 +1 @@ + diff --git a/auth/src/tools/jwt.js b/auth/src/tools/jwt.js new file mode 100644 index 0000000..a87fadb --- /dev/null +++ b/auth/src/tools/jwt.js @@ -0,0 +1,70 @@ +const jwt = require('jsonwebtoken'); + +const secret = process.env.JWT_SECRET; +const expires_in = process.env.JWT_EXPIRES_IN; + +/** + * Genera un JWT Token a partire dall'utente e crea una nuova sessione + * + * Uso dell'algoritmo HS256 per firmare il token con JWT_SECRET + * + * @param {Object} user - Utente + * @param {string} sessionID - ID della sessione + * @returns {string} - JWT Token + */ +function generateToken(user, sessionID) { + const payload = { + sub: user.id, + username: user.username, + session_id: sessionID, + iat: Math.floor(Date.now() / 1000) + }; + + return jwt.sign(payload, secret, { expiresIn: expires_in, algorithm: 'HS256' }); +} + +/** + * Verifica e decodifica il token + * @param {string} token - JWT Token + * @returns {{valid: boolean, payload?: Object, error?: string, reason?: string}} - Il risultato della verifica. Se fallisce restituisce errore e motivo, altrimenti restituisce una conferma e il payload completo + */ +function verifyToken(token) { + try { + const payload = jwt.verify(token, secret, { + algorithms: ['HS256'] + }); + return { + valid: true, + payload: { + user_id: payload.sub, + username: payload.username, + session_id: payload.session_id, + iat: payload.iat, + exp: payload.exp + } + }; + } catch (err) { + + const reason = err.name === 'TokenExpiredError' ? 'expired' : 'invalid'; + + return { + valid: false, + error: err.message, + reason: `token ${reason}` + }; + } +} + +function getToken(header) { + if (!header) return null; + + const parts = header.split(' '); + if (parts.length === 2 && parts[0].toLowerCase() === 'bearer') { + return parts[1]; + } + + //TODO: valutare se modificare in return null per accettare solo metodo Bearer Authorization Token, + return header; +} + +module.exports = { generateToken, verifyToken, getToken }; \ No newline at end of file diff --git a/auth/src/tools/security.js b/auth/src/tools/security.js new file mode 100644 index 0000000..13d4c3b --- /dev/null +++ b/auth/src/tools/security.js @@ -0,0 +1,54 @@ +const bcrypt = require('bcrypt'); +const crypto = require('crypto'); + +const saltRounds = 12; + +/** + * Genera un hash di una password + * @param {string} password - Password da hashare + * @returns {string} - Hash della password + */ +function hashPassword(password) { + return bcrypt.hashSync(password, saltRounds); +} + +/** + * Verifica una password + * @param {string} password - Password da verificare + * @param {string} hash - Hash della password + * @returns {boolean} - True se la password è corretta, false altrimenti + */ +function verifyPassword(password, hash) { + return bcrypt.compareSync(password, hash); +} + +/** + * Create a session token from code and username + * Format: XXXXXXXX-base64_username + * @param {string} sessionCode + * @param {string} username + * @returns {string} Session token + */ +function generateSessionCode() { + return crypto.randomBytes(32).toString('base64url'); +} + +/** + * Parse a session token + * @param {string} token + * @returns {string|null} The session token itself if valid + */ +function parseSessionToken(token) { + if (!token || typeof token !== 'string' || token.length < 32 || token.length > 64) { + return null; + } + + return token; +} + +module.exports = { + hashPassword, + verifyPassword, + generateSessionCode, + parseSessionToken +}; diff --git a/auth/src/tools/tracking.js b/auth/src/tools/tracking.js new file mode 100644 index 0000000..62287b9 --- /dev/null +++ b/auth/src/tools/tracking.js @@ -0,0 +1,24 @@ +//TODO: Verfica se serve davvero prendere le info come ip e browser + +const { UAParser: parser } = require('ua-parser-js'); + +/** + * Estrae le informazioni base su browser, sistema operativo e dispositivo per identificare meglio la sessione dell'utente + * @param {*} userAgent + * @returns + */ +function getBasicMetadata(userAgent) { + const parsed = parser(userAgent); + + return { + browser: parsed.browser.name, + os: parsed.os.name, + device_type: parsed.device.type, + + } +} + +module.exports = { + getBasicMetadata +} + diff --git a/console/.dockerignore b/console/.dockerignore new file mode 100644 index 0000000..5171c54 --- /dev/null +++ b/console/.dockerignore @@ -0,0 +1,2 @@ +node_modules +npm-debug.log \ No newline at end of file diff --git a/console/Dockerfile b/console/Dockerfile new file mode 100644 index 0000000..c9b6242 --- /dev/null +++ b/console/Dockerfile @@ -0,0 +1,13 @@ +FROM node:20-alpine + +WORKDIR /app + +COPY package*.json ./ +RUN npm install + + +COPY src ./src + +EXPOSE 3004 + +CMD ["node", "src/index.js"] \ No newline at end of file diff --git a/console/package-lock.json b/console/package-lock.json new file mode 100644 index 0000000..955ec20 --- /dev/null +++ b/console/package-lock.json @@ -0,0 +1,1127 @@ +{ + "name": "meb-console-service", + "version": "1.4.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "meb-console-service", + "version": "1.4.0", + "dependencies": { + "cookie-parser": "^1.4.7", + "dotenv": "^17.3.1", + "express": "^5.2.1", + "ioredis": "^5.10.0", + "jsonwebtoken": "^9.0.3", + "nunjucks": "^3.2.4" + } + }, + "node_modules/@ioredis/commands": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.5.1.tgz", + "integrity": "sha512-JH8ZL/ywcJyR9MmJ5BNqZllXNZQqQbnVZOqpPQqE1vHiFgAw4NHbvE0FOduNU8IX9babitBT46571OnPTT0Zcw==", + "license": "MIT" + }, + "node_modules/a-sync-waterfall": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/a-sync-waterfall/-/a-sync-waterfall-1.0.1.tgz", + "integrity": "sha512-RYTOHHdWipFUliRFMCS4X2Yn2X8M87V/OpSqWzKKOGhzqyUxzyVmhHDH9sAvG+ZuQf/TAOFsLCpMw09I1ufUnA==", + "license": "MIT" + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "license": "MIT" + }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/commander": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/content-disposition": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-parser": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", + "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==", + "license": "MIT", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.6" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/cookie-parser/node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dotenv": { + "version": "17.3.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz", + "integrity": "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==", + "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", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "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", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "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" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "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", + "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" + }, + "funding": { + "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/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "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" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ioredis": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.10.0.tgz", + "integrity": "sha512-HVBe9OFuqs+Z6n64q09PQvP1/R4Bm+30PAyyD4wIEqssh3v9L21QjCVk4kRLucMBcDokJTcLjsGeVRlq/nH6DA==", + "license": "MIT", + "dependencies": { + "@ioredis/commands": "1.5.1", + "cluster-key-slot": "^1.1.0", + "debug": "^4.3.4", + "denque": "^2.1.0", + "lodash.defaults": "^4.2.0", + "lodash.isarguments": "^3.1.0", + "redis-errors": "^1.2.0", + "redis-parser": "^3.0.0", + "standard-as-callback": "^2.1.0" + }, + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ioredis" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "license": "MIT" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "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": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/nunjucks": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/nunjucks/-/nunjucks-3.2.4.tgz", + "integrity": "sha512-26XRV6BhkgK0VOxfbU5cQI+ICFUtMLixv1noZn1tGU38kQH5A5nmmbk/O45xdyBhD1esk47nKrY0mvQpZIhRjQ==", + "license": "BSD-2-Clause", + "dependencies": { + "a-sync-waterfall": "^1.0.0", + "asap": "^2.0.3", + "commander": "^5.1.0" + }, + "bin": { + "nunjucks-precompile": "bin/precompile" + }, + "engines": { + "node": ">= 6.9.0" + }, + "peerDependencies": { + "chokidar": "^3.3.0" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/redis-errors": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", + "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/redis-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", + "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", + "license": "MIT", + "dependencies": { + "redis-errors": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/standard-as-callback": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", + "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==", + "license": "MIT" + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + } + } +} diff --git a/console/package.json b/console/package.json new file mode 100644 index 0000000..d9a5312 --- /dev/null +++ b/console/package.json @@ -0,0 +1,19 @@ +{ + "name": "meb-console-service", + "version": "1.4.0", + "description": "Console service for meb", + "main": "src/index.js", + "scripts": { + "start": "node src/index.js", + "dev": "node --watch src/index.js", + "generate-docs": "node -e \"console.log(JSON.stringify(require('./swagger-specs-logic')))\" > openapi.json" + }, + "dependencies": { + "cookie-parser": "^1.4.7", + "dotenv": "^17.3.1", + "express": "^5.2.1", + "ioredis": "^5.10.0", + "jsonwebtoken": "^9.0.3", + "nunjucks": "^3.2.4" + } +} diff --git a/console/src/index.js b/console/src/index.js new file mode 100644 index 0000000..10b2382 --- /dev/null +++ b/console/src/index.js @@ -0,0 +1,92 @@ +const express = require('express'); +const nunjucks = require('nunjucks'); +const path = require('path'); +const jwt = require('jsonwebtoken'); + +const parser = require('cookie-parser'); + +const app = express(); +const PORT = process.env.PORT; + +const version = process.env.VERSION; +const vBuild = process.env.VERSION_BUILD; +const vState = process.env.VERSION_STATE; + +app.use(express.json()); +app.use(parser()); + +// Set up static files serving +const staticFolder = path.join(__dirname, 'static'); +app.use('/static', express.static(staticFolder)); + +app.get('/', (req, res) => { + res.redirect(301, "/dashboard") +}); + +app.get('/health', (req, res) => { + res.json({ + status: "ok", + service: "console", + version: version, + build_number: vBuild, + version_state: vState + }); +}); + +const pagesFolder = path.join(__dirname, 'pages'); +nunjucks.configure(pagesFolder, { + autoescape: true, + express: app, + noCache: true, + watch: false +}) +app.set('views', pagesFolder); +app.set('view engine', 'html'); + +const renderPage = (page, extra = {}) => (req, res) => { + res.render(page, {current_path: req.path, ...extra}) +} + +// Middleware di autenticazione per le pagine +app.use((req, res, next) => { + if (req.path === '/health' || req.path.startsWith('/static')) { + return next(); + } + + const authBase = process.env.AUTH_LOGIN_URL || 'http://localhost:3006/login'; + + // Costruisci l'URL di redirect-back: protocollo + host + path originale + const proto = req.protocol; + const host = req.get('host'); + const redirectBack = `${proto}://${host}${req.originalUrl}`; + const loginUrl = `${authBase}?redirect=${encodeURIComponent(redirectBack)}`; + + const token = req.cookies && req.cookies.auth_token; + + if (!token) { + return res.redirect(loginUrl); + } + + try { + const payload = jwt.verify(token, process.env.JWT_SECRET, { algorithms: ['HS256'] }); + req.user = payload; + next(); + } catch (err) { + const clearOptions = { httpOnly: true, sameSite: 'lax' }; + if (process.env.COOKIE_DOMAIN) { + clearOptions.domain = process.env.COOKIE_DOMAIN; + } + res.clearCookie('auth_token', clearOptions); + return res.redirect(loginUrl); + } +}); + +app.get('/dashboard', renderPage('dashboard')); +app.get('/live', renderPage('live', { + realtimeUrl: process.env.REALTIME_URL || 'http://localhost:3002', + realtimeWsUrl: process.env.REALTIME_WS_URL || 'ws://localhost:3002' +})); + +app.listen(PORT, () => { + console.log(`Started on port ${PORT}`); +}); diff --git a/console/src/pages/dashboard.html b/console/src/pages/dashboard.html new file mode 100644 index 0000000..249530b --- /dev/null +++ b/console/src/pages/dashboard.html @@ -0,0 +1,55 @@ + + + + + + + + +
+
+

+ +
+

username

+ +
+
+ +
+
🚤
+

Benvenuto nella MEB Console

+

Usa uno dei tool a disposizione per visualizzare i dati dalla barca in tempo reale, creare nuovi dataset condivisi e

+
+ +
+ + +
+

Live

+

Visualizza i dati dei sensori sulla barca in tempo reale.

+
+
+ + +
+

Previsioni

+

Visualizza le condizioni meteo attuali e le previsioni future.

+
+
+ + +
+

Previsioni

+

Visualizza le condizioni meteo attuali e le previsioni future.

+
+
+ +
+ +
+ + + \ No newline at end of file diff --git a/console/src/pages/live.html b/console/src/pages/live.html new file mode 100644 index 0000000..e4eac83 --- /dev/null +++ b/console/src/pages/live.html @@ -0,0 +1,707 @@ + + + + + + + + + + + + +
+ + +
+
+

Sessioni Attive

+ +
+
Caricamento sessioni...
+
+ +
+
+ + + + + + + +
+
+ + + + +
+ +
+ +
+
+
+ HDG +
+
+
+ Vento +
+
+
+ Onde +
+
+
+
+ + + + diff --git a/console/src/static/styles/dashboard.css b/console/src/static/styles/dashboard.css new file mode 100644 index 0000000..4e0230a --- /dev/null +++ b/console/src/static/styles/dashboard.css @@ -0,0 +1,26 @@ +.content { + width: 100%; + height: 100%; +} + + +.card[title="Live"] { + position: relative; + z-index: 1; +} + +.card[title="Live"]::before { + content: ''; + position: absolute; + inset: 0; + z-index: -1; + background: linear-gradient(135deg, #ff00d0, #0026ff); + filter: blur(40px); + opacity: 0; + transition: opacity 0.7s ease; + border-radius: inherit; +} + +.card[title="Live"]:hover::before { + opacity: 0.2; +} \ No newline at end of file diff --git a/console/src/static/styles/live.css b/console/src/static/styles/live.css new file mode 100644 index 0000000..5ab0e46 --- /dev/null +++ b/console/src/static/styles/live.css @@ -0,0 +1,669 @@ +/* === Session Overlay Popup === */ +.session-overlay { + position: fixed; + inset: 0; + z-index: 2000; + display: flex; + align-items: center; + justify-content: center; + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); +} + +.session-popup { + background: var(--header-bg, #fff); + border: 1px solid var(--header-border); + border-radius: 24px; + padding: 32px; + width: 420px; + max-width: 90vw; + max-height: 70vh; + display: flex; + flex-direction: column; + box-shadow: var(--shadow-lg, 0 20px 60px rgba(0,0,0,0.3)); + user-select: none; +} + +.session-popup h2 { + font-size: 1.25rem; + font-weight: 600; + color: var(--text-primary); + margin: 0 0 4px; +} + +.popup-subtitle { + font-size: 0.85rem; + color: var(--text-secondary); + margin: 0 0 20px; +} + +.session-list { + flex: 1; + overflow-y: auto; + display: flex; + flex-direction: column; + gap: 8px; + margin-bottom: 16px; +} + +.session-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 14px 16px; + border-radius: 16px; + border: 1px solid var(--header-border); + background: var(--surface, #f8fafc); + cursor: pointer; + transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease; +} + +.session-item:hover { + border-color: var(--accent-light, #bfdbfe); + box-shadow: 0 0 0 2px var(--accent-light, #bfdbfe); + transform: translateY(-1px); +} + +.session-item-info { + display: flex; + flex-direction: column; + gap: 2px; +} + +.session-item-info strong { + font-size: 0.95rem; + color: var(--text-primary); +} + +.session-item-id { + font-size: 0.75rem; + color: var(--text-tertiary); + font-family: monospace; +} + +.session-item-meta { + display: flex; + align-items: center; + gap: 8px; +} + +.session-item-time { + font-size: 0.8rem; + color: var(--text-secondary); +} + +.session-item-dot { + width: 8px; + height: 8px; + border-radius: 50%; + background: #10b981; + box-shadow: 0 0 8px #10b981; + flex-shrink: 0; +} + +.session-loading, +.session-empty, +.session-error { + text-align: center; + padding: 32px 16px; + color: var(--text-secondary); + font-size: 0.9rem; +} + +.session-error { + color: #ef4444; +} + +/* === Top Info (Last Update) === */ +.last-update { + font-size: 0.8rem; + color: var(--text-secondary); + margin-top: 4px; +} + +/* === Layout Wrappers === */ +.dashboard-layout { + display: flex; + align-items: flex-start; + gap: 24px; + margin: 0 30px 20px; + padding-bottom: 140px; /* Evita che la bottom bar copra i dati */ +} + +.main-column { + flex: 1; + min-width: 0; +} + +/* === Sticky Map & Expanded Chart === */ +.sticky-area { + position: sticky; + top: 20px; + z-index: 100; + display: flex; + flex-direction: column; + gap: 16px; + margin-bottom: 20px; +} + +.map-container { + border-radius: 24px; + overflow: hidden; + border: 1px solid rgba(226, 232, 240, 0.6); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.04); + background: var(--surface); +} + +.map-container #liveMap { + width: 100%; + height: 300px; /* Ridotto un po' per far spazio */ +} + +.expanded-chart-container { + background: var(--surface, #f8fafc); + border: 1px solid rgba(226, 232, 240, 0.6); + border-radius: 24px; + padding: 20px; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.04); + position: relative; + display: flex; + flex-direction: column; + height: 300px; +} + +.expanded-chart-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 12px; +} + +.expanded-chart-header h3 { + margin: 0; + font-size: 1rem; + color: var(--text-primary); +} + +.close-expanded-btn { + background: transparent; + border: none; + color: var(--text-secondary); + cursor: pointer; + font-size: 1.2rem; + line-height: 1; +} + +.close-expanded-btn:hover { + color: var(--text-primary); +} + +.expanded-chart-body { + flex: 1; + min-height: 0; + position: relative; +} + +.expanded-chart-body canvas { + position: absolute; + inset: 0; + width: 100% !important; + height: 100% !important; +} + +/* === Card ibrida (Dati + Minigrafico) === */ +.grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 20px; +} + +.data-card { + background: var(--surface, #ffffff); + border: 1px solid rgba(226, 232, 240, 0.6); + border-radius: 24px; + padding: 20px; + display: flex; + flex-direction: column; + box-shadow: 0 4px 20px rgba(0,0,0,0.03); + position: relative; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +.data-card:hover { + box-shadow: 0 12px 32px rgba(0,0,0,0.06); + border-color: var(--accent-light); + transform: translateY(-2px); +} + +.card-top { + display: flex; + align-items: flex-start; + justify-content: space-between; + margin-bottom: 12px; +} + +.card-title-group { + display: flex; + align-items: center; + gap: 10px; +} + +.card-icon { + width: 32px; + height: 32px; + border-radius: 8px; + background: #f1f5f9; + color: #64748b; + display: flex; + align-items: center; + justify-content: center; +} + +.card-info { + margin-left: 6px; +} + +.card-info h4 { + margin: 0; + font-size: 0.9rem; + color: var(--text-primary); + font-weight: 600; +} + +.card-info span { + font-size: 0.75rem; + color: var(--text-tertiary); + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.card-actions { + display: flex; + gap: 6px; + opacity: 0; + transition: opacity 0.2s; +} + +.data-card:hover .card-actions { + opacity: 1; +} + +.card-action-btn { + background: transparent; + border: none; + color: var(--text-secondary); + cursor: pointer; + padding: 4px; + border-radius: 4px; +} + +.card-action-btn:hover { + background: #f1f5f9; + color: var(--text-primary); +} + +.card-body { + display: flex; + align-items: flex-end; + justify-content: space-between; + gap: 16px; + padding: 0 6px 6px; +} + +.card-values { + display: flex; + flex-direction: row; + align-items: flex-end; + gap: 4px; + min-width: 80px; +} + +.card-main-val { + font-size: 1.75rem; + font-weight: 700; + color: var(--text-primary); + line-height: 1; +} + +.card-unit { + font-size: 0.85rem; + color: var(--text-secondary); + font-weight: 500; + margin-bottom: 2px; +} + +.card-mini-chart { + flex: 1; + height: 50px; + position: relative; + min-width: 0; +} + +.card-mini-chart canvas { + position: absolute; + inset: 0; + width: 100% !important; + height: 100% !important; +} + +/* Modalità Confronto disabilita il minigrafico se selezionato, ma lascio a JS questo controllo visivo */ + +/* === Sidebar Confronto === */ +.comparison-sidebar { + width: 400px; + background: var(--surface); + border: 1px solid rgba(226, 232, 240, 0.6); + border-radius: 24px; + padding: 24px; + display: flex; + flex-direction: column; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.04); + position: sticky; + top: 20px; + height: calc(100vh - 120px); + overflow: hidden; +} + +.comp-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 16px; + border-bottom: 1px solid var(--header-border); + padding-bottom: 12px; +} + +.comp-header h3 { + margin: 0; + font-size: 1.1rem; +} + +.comp-close { + background: transparent; + border: none; + cursor: pointer; + font-size: 1.2rem; + color: var(--text-secondary); +} + +.comp-list { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-bottom: 16px; +} + +.comp-pill { + padding: 4px 10px; + border-radius: 12px; + font-size: 0.8rem; + display: flex; + align-items: center; + gap: 6px; + background: #f1f5f9; + border: 1px solid var(--header-border); +} + +.comp-pill .color-dot { + width: 8px; + height: 8px; + border-radius: 50%; +} + +.comp-pill button { + background: none; + border: none; + cursor: pointer; + line-height: 1; + font-size: 1rem; + color: var(--text-secondary); +} + +.comp-chart-area { + flex: 1; + min-height: 0; + position: relative; +} + +.comp-chart-area canvas { + position: absolute; + inset: 0; + width: 100% !important; + height: 100% !important; +} + +/* Quando la modalita confronto è attiva le card cambiano aspetto al click? Lo gestisce JS (es. classe .selected-for-comp) */ +.data-card.selected-for-comp { + border-color: var(--accent-color); + box-shadow: 0 0 0 1px var(--accent-color); +} +.data-card.selected-for-comp .card-mini-chart { + opacity: 0.2; /* Nasconde o sbiadisce il mini chart per far capire che è in confronto */ +} + +/* === Bottom Bar & secondary ... same as before mostly === */ +.bottom-bar { + position: fixed; + bottom: 32px; + left: 50%; + transform: translateX(-50%); + display: flex; + width: max-content; + max-width: 95vw; + height: 56px; + justify-content: center; + align-items: center; + gap: 12px; + padding: 0 16px; + font-size: 15px; + user-select: none; + background-color: rgba(255, 255, 255, 0.75); + backdrop-filter: blur(24px) saturate(1.5); + -webkit-backdrop-filter: blur(24px) saturate(1.5); + border: 1px solid rgba(255, 255, 255, 0.4); + border-radius: 28px; + z-index: 1000; + box-shadow: 0 16px 40px rgba(0, 0, 0, 0.08), 0 4px 12px rgba(0, 0, 0, 0.02); + overflow-x: auto; + scrollbar-width: none; + color: var(--text-primary); +} + +.bottom-bar::-webkit-scrollbar { + display: none; +} + +.bottom-bar .search-field { + height: 38px; + width: 200px; + padding-right: 10px; + border-radius: 14px; + border: 1px solid rgba(226, 232, 240, 0.8); + background: rgba(255, 255, 255, 0.6); + display: flex; + align-items: center; + transition: background 0.3s ease, border-color 0.3s ease, box-shadow 0.3s ease; + flex-shrink: 0; +} + +.bottom-bar .search-field:focus-within { + background: rgba(255, 255, 255, 0.9); + border-color: var(--accent-light, #bfdbfe); + box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.1); +} + +.bottom-bar .search-field input { + border: none; + outline: none; + background: transparent; + color: var(--text-primary); + font-size: 14px; + padding: 5px; + width: 100%; +} + +.bottom-bar .filter { + display: flex; + height: 38px; + padding: 0 4px; + align-items: center; + gap: 4px; + flex-shrink: 0; + border-radius: 14px; + border: 1px solid rgba(226, 232, 240, 0.8); + background: rgba(255, 255, 255, 0.6); +} + +.bottom-bar .filter.boxed button { + display: flex; + width: 30px; + height: 30px; + padding: 0; + justify-content: center; + align-items: center; +} + +.bottom-bar .filter button { + display: flex; + padding: 6px 12px; + justify-content: center; + align-items: center; + border-radius: 8px; + border: none; + background: transparent; + color: var(--text-secondary); + transition: color 0.15s ease, background 0.15s ease; + cursor: pointer; + white-space: nowrap; + font-size: 13px; + font-weight: 500; +} + +.bottom-bar .filter button.active { + background: var(--accent-color); + color: #ffffff; +} + +.bottom-bar > button#downloadBtn { + display: flex; + padding: 6px 16px; + justify-content: center; + align-items: center; + border-radius: 12px; + background: var(--surface, #f8fafc); + border: 1px solid var(--header-border); + color: var(--text-primary); + font-size: 13px; + font-weight: 600; + cursor: pointer; +} + +.bar-sep { + width: 1px; + height: 24px; + background-color: var(--header-border); + margin: 0 4px; + flex-shrink: 0; +} + +.map-bar { + position: fixed; + bottom: 6rem; + left: 50%; + transform: translateX(-50%) translateY(20px); + display: flex; + width: max-content; + height: 48px; + justify-content: center; + align-items: center; + gap: 12px; + padding: 0 16px; + background-color: rgba(255, 255, 255, 0.75); + backdrop-filter: blur(24px) saturate(1.5); + -webkit-backdrop-filter: blur(24px) saturate(1.5); + border: 1px solid rgba(255, 255, 255, 0.4); + border-radius: 24px; + z-index: 1000; + box-shadow: 0 12px 32px rgba(0, 0, 0, 0.06); + opacity: 0; + pointer-events: none; + transition: opacity 0.3s ease, transform 0.3s ease; +} + +.map-bar.visible { + opacity: 1; + pointer-events: all; + transform: translateX(-50%) translateY(0); +} + +.map-bar .filter { + display: flex; + height: 36px; + padding: 0 4px; + align-items: center; + gap: 4px; + flex-shrink: 0; + border-radius: 12px; + border: 1px solid var(--header-border); + background: var(--surface, #f8fafc); +} + +.map-bar .filter button { + display: flex; + padding: 6px 12px; + justify-content: center; + align-items: center; + border-radius: 8px; + border: none; + background: transparent; + color: var(--text-secondary); +} + +.map-bar .filter button.active { + background: var(--accent-color); + color: #ffffff; +} + +.map-toggles-group { + display: flex; + gap: 8px; + align-items: center; +} + +.bb-toggle { + display: flex; + align-items: center; + gap: 8px; + padding: 6px 12px; + background: var(--surface, #f8fafc); + border: 1px solid var(--header-border); + border-radius: 12px; + font-size: 13px; + font-weight: 500; + color: var(--text-secondary); + cursor: pointer; +} + +.bb-toggle.on { + border-color: var(--accent-border, #bfdbfe); + background: var(--accent-light, #eff6ff); + color: var(--accent-color, #2563eb); +} + +@media (max-width: 1100px) { + .dashboard-layout { + flex-direction: column; + } + .comparison-sidebar { + width: 100%; + height: 400px; + position: static; + } +} + +@media (max-width: 900px) { + .grid { + grid-template-columns: 1fr; + } +} + diff --git a/console/src/static/styles/style.css b/console/src/static/styles/style.css new file mode 100644 index 0000000..8b7defd --- /dev/null +++ b/console/src/static/styles/style.css @@ -0,0 +1,201 @@ +:root { + --accent-color: #2563eb; + --accent-hover: #1d4ed8; + --accent-light: #eff6ff; + --accent-border: #bfdbfe; + + --text-primary: #0f172a; + --text-secondary: #4755698f; + --text-tertiary: #94a3b8c0; + + --surface: #f8fafc; + + --header-bg: rgba(255, 255, 255, 0.85); + /* For Glassmorphism */ + --header-border: #e2e8f0; + + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05); + --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); + --radius-md: 8px; + --radius-lg: 12px; +} + +* { + margin: 0; + padding: 0; +} + +@font-face { + font-family: 'Normal'; + src: url('../font/Quicksand-VariableFont_wght.ttf'); + font-weight: 400; + font-style: normal; +} + +@font-face { + font-family: 'Bold'; + src: url('../font/Quicksand-VariableFont_wght.ttf'); + font-weight: 700; + font-style: normal; +} + +body { + font-family: 'Normal', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; + color: var(--text-primary); + -webkit-font-smoothing: antialiased; +} + +button { + padding: 10px 24px; + border-radius: var(--radius-lg); + border: 1px solid var(--header-border); + background-color: var(--bg-surface); + color: var(--text-primary); + font-size: 14px; + font-weight: 600; + font-family: 'Bold', inherit; + cursor: pointer; + transition: all 0.5s cubic-bezier(0.8, 0, 0.2, 1); + white-space: nowrap; + flex-shrink: 0; +} + +button:hover { + background-color: var(--accent-light); + color: var(--accent-color); + border-color: var(--accent-border); +} + +button.prominent { + background-color: var(--accent-color); + color: #ffffff; + border: 1px solid transparent; + box-shadow: var(--shadow-sm); +} + +button.prominent:hover { + background-color: var(--accent-hover); + transform: translateY(-1px); + box-shadow: var(--shadow-md); +} + + +button.prominent:active { + transform: translateY(1px); + box-shadow: var(--shadow-sm); +} + + +/* INFO PANEL */ + +.info-panel { + display: block; + text-align: center; + align-content: center; + padding: 60px 20px; + user-select: none; +} + +.info-panel h3 { + margin-bottom: 10px; +} + +.info-panel p { + margin-bottom: 25px; +} + +.info-panel .icon { + font-size: 48px; + margin-bottom: 20px; + align-self: center; + transition: transform 0.12s ease; +} + + + + +/* GRID & CARD ITEMS */ + +.grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 1.5rem; + margin-inline: 30px; +} + +.card { + display: flex; + gap: 0.75rem; + align-items: center; + padding: 1rem; + border: 1px solid var(--header-border); + border-radius: 20px; + text-decoration: none; + color: var(--text-primary); + transition: transform 0.12s ease, box-shadow 0.12s ease; +} + +.card h3 { + margin: 0; + font-size: 1rem; + text-align: left; +} + +.card p { + margin: 0.25rem 0 0; + color: var(--text-secondary); + opacity: 0.4; + font-size: 0.8rem; + text-align: left; +} + +.card:hover { + transform: translateY(-4px); + box-shadow: 0 8px 30px #bfdbfe30; +} + +.card.standalone { + grid-column: 1 / -1; +} + + + + + +/* HEADER */ + +.header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 16px 24px; + background-color: var(--header-bg); + border-bottom: 1px solid var(--header-border); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + position: sticky; + top: 0; + z-index: 10; + user-select: none; +} + + +.header h1 { + color: var(--text-primary); + font-size: 1.25rem; + font-weight: 700; + letter-spacing: -0.025em; +} + +.header .profile { + display: flex; + align-items: center; + gap: 16px; +} + +.header .profile p { + font-size: 0.875rem; + font-weight: 500; + color: var(--text-secondary); + padding-inline: 5px; +} \ No newline at end of file diff --git a/copernicus/Dockerfile b/copernicus/Dockerfile new file mode 100644 index 0000000..e69de29 diff --git a/copernicus/core/cache.py b/copernicus/core/cache.py new file mode 100644 index 0000000..09881f9 --- /dev/null +++ b/copernicus/core/cache.py @@ -0,0 +1,125 @@ +""" +Redis Keys: +- marine:catalog:full → lista dei dataset completo (TTL 1h) +- marine:catalog:search:{hash} → risultati ricerca (TTL 30min) +- marine:job:{session_id} → stato job download (TTL 48h) +""" + +import json +import os +import logging +from typing import Any, Optional + +import redis + +logger = logging.getLogger(__name__) + +# Configurazione Redis da variabili ambiente +REDIS_HOST = os.getenv("REDIS_HOST", "meb-redis") +REDIS_PORT = int(os.getenv("REDIS_PORT", "6379")) + +# Pool di connessioni condiviso (thread-safe, riutilizzabile) +_pool: Optional[redis.ConnectionPool] = None +_client: Optional[redis.Redis] = None + + +def _get_client() -> Optional[redis.Redis]: + """Restituisce il client Redis singleton con connection pool. + Ritorna None se Redis non è raggiungibile.""" + global _pool, _client + + if _client is not None: + return _client + + try: + _pool = redis.ConnectionPool( + host=REDIS_HOST, + port=REDIS_PORT, + # Decodifica automatica delle risposte in stringhe UTF-8 + decode_responses=True, + # Massimo 5 connessioni nel pool (VPS 1-core, non serve di più) + max_connections=5, + # Timeout connessione e socket per evitare blocchi + socket_connect_timeout=3, + socket_timeout=3, + # Riprova automaticamente se la connessione viene interrotta + retry_on_timeout=True, + ) + _client = redis.Redis(connection_pool=_pool) + # Test connessione + _client.ping() + logger.info("[Redis] Connessione stabilita per il servizio Marine") + return _client + except Exception as e: + logger.warning(f"[Redis] Non disponibile, la cache è disabilitata: {e}") + _client = None + return None + + +def cache_get(key: str) -> Optional[Any]: + """Legge un valore dalla cache Redis. + + Args: + key: Chiave Redis (es. 'marine:catalog:full') + + Returns: + Il valore deserializzato da JSON, oppure None se non trovato o errore + """ + try: + client = _get_client() + if client is None: + return None + + data = client.get(key) + if data is None: + return None + + return json.loads(data) + except Exception as e: + logger.warning(f"[Redis] Errore lettura chiave '{key}': {e}") + return None + + +def cache_set(key: str, value: Any, ttl: int = 3600) -> bool: + """Scrive un valore nella cache Redis con TTL. + + Args: + key: Chiave Redis + value: Valore da serializzare in JSON + ttl: Tempo di vita in secondi (default: 1 ora) + + Returns: + True se scritto con successo, False altrimenti + """ + try: + client = _get_client() + if client is None: + return False + + serialized = json.dumps(value) + client.setex(key, ttl, serialized) + return True + except Exception as e: + logger.warning(f"[Redis] Errore scrittura chiave '{key}': {e}") + return False + + +def cache_delete(key: str) -> bool: + """Elimina una chiave dalla cache Redis. + + Args: + key: Chiave Redis da eliminare + + Returns: + True se eliminata, False altrimenti + """ + try: + client = _get_client() + if client is None: + return False + + client.delete(key) + return True + except Exception as e: + logger.warning(f"[Redis] Errore eliminazione chiave '{key}': {e}") + return False diff --git a/copernicus/core/copernicus.py b/copernicus/core/copernicus.py new file mode 100644 index 0000000..1ab20d6 --- /dev/null +++ b/copernicus/core/copernicus.py @@ -0,0 +1,310 @@ +import hashlib +import io +import logging +import os +from datetime import datetime, timezone +from typing import Callable, List, Optional + +import pandas as pd + +from core.cache import cache_get, cache_set + +logger = logging.getLogger(__name__) + +# ── Chiavi Redis e TTL ──────────────────────────────────────────────── +# Chiave per il catalogo completo Copernicus +_CATALOG_KEY = "marine:catalog:full" +# TTL del catalogo: 1 ora (il catalogo Copernicus cambia raramente) +_CATALOG_TTL = 3600 +# TTL per i risultati di ricerca: 30 minuti +_SEARCH_TTL = 1800 + + +def _fmt_description(name: Optional[str]) -> Optional[str]: + """Formatta meglio il titolo del dataset""" + if not name: + return None + return name.replace("_", " ").title() + + +def _get_raw_catalog() -> dict: + """Interroga le API di Copernicus per ottenere la lista completa dei dataset. + + Strategia cache Redis: + 1. Cerca in Redis (chiave marine:catalog:full) + 2. Se non trovato → chiama Copernicus SDK → salva in Redis con TTL 1h + 3. Se Redis non disponibile → chiama sempre l'SDK (nessuna cache) + + Il catalogo in Redis sopravvive al restart del servizio grazie + alla persistenza RDB+AOF configurata in redis.conf. + """ + # Cerca in Redis prima di chiamare l'SDK Copernicus + cached = cache_get(_CATALOG_KEY) + if cached is not None: + logger.debug("[Catalogo] Servito da cache Redis") + return cached + + # Cache miss: interroga Copernicus SDK (operazione lenta, ~10-30s) + logger.info("[Catalogo] Cache miss, scaricamento da Copernicus SDK...") + import copernicusmarine + catalog = copernicusmarine.describe(disable_progress_bar=True) + + # Serializza la risposta SDK in un dizionario standard + if hasattr(catalog, "model_dump"): + result = catalog.model_dump() + elif hasattr(catalog, "__dict__"): + result = catalog.__dict__ + else: + result = catalog + + # Salva in Redis per le prossime richieste (TTL 1 ora) + cache_set(_CATALOG_KEY, result, _CATALOG_TTL) + logger.info("[Catalogo] Salvato in cache Redis") + + return result + + +def _get_dataset_reqs(ds: dict) -> tuple: + """ + Ottieni dalla risposta del dataset le variabili disponibili e le coordinate dell'area disponibile. + + Attualmente è implementato Copernicus SDK v2, le variabili sono in:: + dataset -> versions[-1] -> parts[] -> services[] -> variables[] + + Le coordinate sono disponibili in variable.bbox = [min_lon, min_lat, max_lon, max_lat]. + La finestra temporale disponibile è nel servizio "arco-time-series" + dove coordinate_id == 'time' (i valori sono in millisecondi, usando Unix epoch). + """ + variables = [] + seen: set = set() + bounds = { + "min_longitude": None, "max_longitude": None, + "min_latitude": None, "max_latitude": None, + "start_datetime": None, "end_datetime": None, + } + + versions = ds.get("versions", []) + if not versions: + return variables, bounds + + for part in versions[-1].get("parts", []): + for service in part.get("services", []): + service_name = service.get("service_name", "") + for var in service.get("variables", []): + short_name = var.get("short_name", "") + if not short_name or short_name in seen: + continue + seen.add(short_name) + std = var.get("standard_name") + variables.append({ + "short_name": short_name, + "standard_name": std, + "units": var.get("units"), + "description": _fmt_description(std), + }) + + # Ottieni la box delle coordinate + if bounds["min_longitude"] is None: + bbox = var.get("bbox") + if bbox and len(bbox) >= 4: + # [min_lon, min_lat, max_lon, max_lat] + bounds["min_longitude"] = bbox[0] + bounds["min_latitude"] = bbox[1] + bounds["max_longitude"] = bbox[2] + bounds["max_latitude"] = bbox[3] + + # Ottieni la finestra temporale del dataset dal servizio "arco-time-series" + if bounds["start_datetime"] is None and "arco-time" in service_name: + for coord in var.get("coordinates", []): + if coord.get("coordinate_id") == "time": + min_ms = coord.get("minimum_value") + max_ms = coord.get("maximum_value") + if min_ms is not None: + bounds["start_datetime"] = datetime.fromtimestamp( + min_ms / 1000, tz=timezone.utc + ).strftime("%Y-%m-%d") + if max_ms is not None: + bounds["end_datetime"] = datetime.fromtimestamp( + max_ms / 1000, tz=timezone.utc + ).strftime("%Y-%m-%d") + break + + return variables, bounds + + +def get_catalog(search: Optional[str] = None, limit: int = 50, offset: int = 0) -> dict: + """Ottieni dataset dal catalogo Copernicus Marine, filtrabili per nome o ID. + + Cache Redis per le ricerche: + - Chiave: marine:catalog:search:{md5(search|limit|offset)} + - TTL: 30 minuti + - La cache ricerca viene invalidata quando il catalogo scade (1h) + """ + # Genera chiave cache unica per questa combinazione di parametri + cache_key = None + if search: + query_hash = hashlib.md5(f"{search}|{limit}|{offset}".encode()).hexdigest()[:12] + cache_key = f"marine:catalog:search:{query_hash}" + + # Cerca risultato in cache Redis + cached_result = cache_get(cache_key) + if cached_result is not None: + logger.debug(f"[Catalogo] Ricerca '{search}' servita da cache Redis") + return cached_result + + raw = _get_raw_catalog() + # Gestisce formati diversi della risposta SDK (lista o dizionario) + if isinstance(raw, list): + products = raw + else: + products = raw.get("products", []) + + results = [] + for product in products: + title = product.get("title", "") + description = product.get("description", "") + + for ds in product.get("datasets", []): + dataset_id = ds.get("dataset_id", "") + + if search: + needle = search.lower() + if needle not in dataset_id.lower() and needle not in title.lower(): + continue + + variables, bounds = _get_dataset_reqs(ds) + results.append({ + "dataset_id": dataset_id, + "title": title, + "description": description[:200] if description else "", + "variables": variables, + **bounds, + }) + + total = len(results) + page = results[offset: offset + limit] + response = {"total": total, "offset": offset, "limit": limit, "datasets": page} + + # Salva risultato ricerca in cache Redis (solo se c'è un filtro di ricerca) + if cache_key: + cache_set(cache_key, response, _SEARCH_TTL) + + return response + + +def get_dataset_info(dataset_id: str) -> Optional[dict]: + """Return detailed info for a single dataset (variables, bounds, time range).""" + raw = _get_raw_catalog() + if isinstance(raw, list): + products = raw + else: + products = raw.get("products", []) + + for product in products: + for ds in product.get("datasets", []): + if ds.get("dataset_id") == dataset_id: + variables, bounds = _get_dataset_reqs(ds) + return { + "dataset_id": dataset_id, + "title": product.get("title", ""), + "description": product.get("description", ""), + "variables": variables, + **bounds, + } + return None + + +def download_dataset( + dataset_id: str, + variables: List[str], + min_longitude: float, + max_longitude: float, + min_latitude: float, + max_latitude: float, + start_datetime: str, + end_datetime: str, + progress_callback: Optional[Callable[[int, str], None]] = None +) -> pd.DataFrame: + """ + Scarica i dati di un dataset da Copernicus Marine. L'SDK ufficiale di Copernicus, + restituisce i dati del download sotto forma di pandas Dataframe. + """ + import tempfile + + import copernicusmarine + + if progress_callback: + progress_callback(5, "Avvio dowload...") + + # l'SDK di copernicus richiede l'autenticazione di un utente + if not os.getenv("COPERNICUS_USERNAME") or not os.getenv("COPERNICUS_PASSWORD"): + raise ValueError("non sono presenti username e password per copernicus.") + + with tempfile.TemporaryDirectory() as tmpdir: + try: + copernicusmarine.subset( + dataset_id=dataset_id, + variables=variables, + minimum_longitude=min_longitude, + maximum_longitude=max_longitude, + minimum_latitude=min_latitude, + maximum_latitude=max_latitude, + start_datetime=start_datetime, + end_datetime=end_datetime, + username=os.getenv("COPERNICUS_USERNAME"), + password=os.getenv("COPERNICUS_PASSWORD"), + output_directory=tmpdir, + output_filename="data.nc", + force_download=True, + overwrite_output_data=True, + disable_progress_bar=True, + ) + except TypeError: + # Fallback for older versions of copernicusmarine + copernicusmarine.subset( + dataset_id=dataset_id, + variables=variables, + minimum_longitude=min_longitude, + maximum_longitude=max_longitude, + minimum_latitude=min_latitude, + maximum_latitude=max_latitude, + start_datetime=start_datetime, + end_datetime=end_datetime, + username=os.getenv("COPERNICUS_USERNAME"), + password=os.getenv("COPERNICUS_PASSWORD"), + output_directory=tmpdir, + output_filename="data.nc", + overwrite=True, + disable_progress_bar=True, + ) + + if progress_callback: + progress_callback(50, "Download completato, elaboro i dati...") + + import xarray as xr + ds = xr.open_dataset(os.path.join(tmpdir, "data.nc")) + df = ds.to_dataframe().reset_index() + ds.close() + + if df is None or df.empty: + raise ValueError("Nessun dato disponibile. errore nel download") + + if progress_callback: + progress_callback(75, "Elaborazione completata, formatto i dati...") + + return df + + +def dataframe_to_bytes(df: pd.DataFrame, fmt: str, variable_renames: dict = None) -> tuple: + """ + Converte i dati in memorie sottoforma di DataFrame scaircati da Copernicus in byte per migliorarne l'elaborazione e la formattazione in file CSV o JSON.""" + if variable_renames: + df = df.rename(columns=variable_renames) + if fmt == "csv": + buf = io.StringIO() + df.to_csv(buf, index=True) + return buf.getvalue().encode("utf-8"), "text/csv" + else: + buf = io.StringIO() + df.to_json(buf, orient="records", date_format="iso", indent=2) + return buf.getvalue().encode("utf-8"), "application/json" diff --git a/copernicus/core/storage.py b/copernicus/core/storage.py new file mode 100644 index 0000000..0de0ec2 --- /dev/null +++ b/copernicus/core/storage.py @@ -0,0 +1,112 @@ +import io +import json +import os +from typing import Any, Optional + +from minio.error import S3Error + +from minio import Minio + +_minio_host = os.getenv("MINIO_ENDPOINT", "minio") +_minio_port = os.getenv("MINIO_PORT", "9000") +MINIO_ACCESS_KEY = os.getenv("MINIO_ACCESS_KEY", "meb-admin") +MINIO_SECRET_KEY = os.getenv("MINIO_SECRET_KEY", "meb-cloud") +MINIO_SECURE = os.getenv("MINIO_SECURE", "false").lower() == "true" + +DATASETS_BUCKET = "datasets" +METADATA_FILE = "metadata.json" + +_client: Optional[Minio] = None + + +def get_client() -> Minio: + + global _client + if _client is None: + _client = Minio( + f"{_minio_host}:{_minio_port}", + access_key=MINIO_ACCESS_KEY, + secret_key=MINIO_SECRET_KEY, + secure=MINIO_SECURE + ) + return _client + + +def bucket_exists(bucket: str = DATASETS_BUCKET) -> bool: + try: + client = get_client() + if not client.bucket_exists(bucket): + client.make_bucket(bucket) + return True + except Exception as e: + print(f"[Storage] Error in '{bucket}': {e}") + return False + + +def fetch_metadata() -> dict: + """Il bucket datasets contiene un file JSON di metadata valido per tutti i file dataset salvati, che questi siano JSON, csv o + un altro formato. I metadata per ogni file sono salvati come oggetti nel file metadata.json. """ + try: + client = get_client() + response = client.get_object(DATASETS_BUCKET, METADATA_FILE) + data = json.loads(response.read().decode("utf-8")) + response.close() + return data + except S3Error as e: + if e.code == "NoSuchKey": + return {"datasets": []} + raise + except Exception: + return {"datasets": []} + + +def write_metadata(data: dict) -> None: + """Aggiunge al file metadata.json un nuovo oggetto con l'id del nuovo file caricato dall'utente""" + client = get_client() + raw = json.dumps(data, ensure_ascii=False, indent=2).encode("utf-8") + client.put_object( + DATASETS_BUCKET, + METADATA_FILE, + io.BytesIO(raw), + length=len(raw), + content_type="application/json" + ) + + +def upload_file(data: bytes, filename: str, content_type: str) -> None: + """Carica un nuovo file di qualsiasi formato nel bucket dataset.""" + client = get_client() + client.put_object( + DATASETS_BUCKET, + filename, + io.BytesIO(data), + length=len(data), + content_type=content_type + ) + + +def delete_file(filename: str) -> None: + """Elimina un file dal bucket dataset.""" + client = get_client() + client.remove_object(DATASETS_BUCKET, filename) + + +def get_presigned_url(filename: str, expires_hours: int = 1) -> str: + """Genera un URL temporaneo per scaricare un file dal bucket dataset""" + from datetime import timedelta + client = get_client() + return client.presigned_get_object( + DATASETS_BUCKET, + filename, + expires=timedelta(hours=expires_hours) + ) + + +def file_exists(filename: str) -> bool: + """Verifica se un file esiste.""" + try: + client = get_client() + client.stat_object(DATASETS_BUCKET, filename) + return True + except S3Error: + return False diff --git a/copernicus/main.py b/copernicus/main.py new file mode 100644 index 0000000..3987b31 --- /dev/null +++ b/copernicus/main.py @@ -0,0 +1,53 @@ +""" +Servizio reindirizzato: api.{domain}/marine/* (Traefik strips /marine prefix) +""" + +import os +from contextlib import asynccontextmanager + +from dotenv import load_dotenv +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware + +load_dotenv() + +from routers import catalog, datasets, jobs + + +@asynccontextmanager +async def lifespan(app: FastAPI): + api_url = os.getenv("API_SERVICE_URL", "http://api:3003") + yield + + +app = FastAPI( + title="MEB Marine Service", + description="Copernicus Marine data download and dataset management for the MEB platform", + version="1.0.0", + docs_url="/docs", + redoc_url="/redoc", + lifespan=lifespan, + root_path="" +) + +app.add_middleware( + CORSMiddleware, + allow_origin_regex=r"https?://.*\.(localhost|mebboat\.it)(:\d+)?$", + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +app.include_router(catalog.router) +app.include_router(jobs.router) +app.include_router(datasets.router) + + +@app.get("/", tags=["health"]) +async def root(): + return {"service": "MEB Marine Service", "version": "1.0.0", "docs": "/docs"} + + +@app.get("/health", tags=["health"]) +async def health(): + return {"status": "healthy"} diff --git a/copernicus/requirements.txt b/copernicus/requirements.txt new file mode 100644 index 0000000..59c411d --- /dev/null +++ b/copernicus/requirements.txt @@ -0,0 +1,13 @@ +fastapi>=0.115.0 +uvicorn[standard]>=0.32.0 +httpx>=0.27.0 +python-dotenv>=1.0.0 +copernicusmarine>=2.0.0 +xarray>=2024.0.0 +pandas>=2.2.0 +numpy>=1.26.0 +pydantic>=2.6.0 +redis>=5.0.0 +python-multipart>=0.0.9 +h5py +h5netcdf diff --git a/copernicus/routers/catalog.py b/copernicus/routers/catalog.py new file mode 100644 index 0000000..e844d59 --- /dev/null +++ b/copernicus/routers/catalog.py @@ -0,0 +1,46 @@ +from fastapi import APIRouter, Depends, Query, HTTPException +from typing import Optional +from middleware.auth import require_auth +from core import copernicus + +""" +api.mebboat.it/marine/... +""" + +router = APIRouter(prefix="/catalog", tags=["Copernicus Marine Database"]) + +@router.get("") +async def list_catalog( + search: Optional[str] = Query(None, description="Cerca per nome o ID"), + limit: int = Query(50, ge=1, le=200), + offset: int = Query(0, ge=0), + user=Depends(require_auth) +): + """ + [API] Ottieni una lista di dataset corrispondeti alla query di ricerca, con paginazione. + Ogni dataset include ID, titolo, descrizione, variabili, posizione e finestra di tempo. + I risultati rimangono salvati nella cache del server per un ora. + """ + try: + return copernicus.get_catalog(search=search, limit=limit, offset=offset) + except Exception as e: + raise HTTPException(status_code=502, detail=f"Catalog unavailable: {str(e)}") + + +@router.get("/{dataset_id}") +async def get_dataset( + dataset_id: str, + user=Depends(require_auth) +): + """ + [API] Ottieni i dati di un dataset dal catalogo di Copernics Marine. + """ + try: + info = copernicus.get_dataset_info(dataset_id) + except Exception as e: + raise HTTPException(status_code=502, detail=f"Catalog unavailable: {str(e)}") + + if info is None: + raise HTTPException(status_code=404, detail=f"Dataset '{dataset_id}' not found in catalog") + + return info diff --git a/copernicus/routers/datasets.py b/copernicus/routers/datasets.py new file mode 100644 index 0000000..6c1f87d --- /dev/null +++ b/copernicus/routers/datasets.py @@ -0,0 +1,57 @@ +""" +api.mebboat.it/marine/datasets/* +""" +import os +from typing import Optional + +import httpx +from fastapi import APIRouter, Depends, HTTPException, Query +from middleware.auth import require_auth + +router = APIRouter(prefix="/datasets", tags=["datasets"]) + +API_URL = os.getenv("API_SERVICE_URL", "http://api-service:3003") + + +def _auth_headers(user: dict) -> dict: + return {"Authorization": f"Bearer {user['token']}"} + + +@router.get("") +async def list_datasets( + tags: Optional[str] = Query(None), + user=Depends(require_auth) +): + async with httpx.AsyncClient(timeout=10.0) as client: + r = await client.get( + f"{API_URL}/marine/datasets", + params={"tags": tags} if tags else {}, + headers=_auth_headers(user), + ) + if not r.is_success: + raise HTTPException(status_code=r.status_code, detail=r.json().get("error", "Upstream error")) + return r.json() + + +@router.get("/{dataset_id}/download") +async def download_dataset(dataset_id: str, user=Depends(require_auth)): + async with httpx.AsyncClient(timeout=10.0) as client: + r = await client.get( + f"{API_URL}/marine/datasets/{dataset_id}/download", + headers=_auth_headers(user), + ) + if not r.is_success: + raise HTTPException(status_code=r.status_code, detail=r.json().get("error", "Upstream error")) + return r.json() + + +@router.delete("/{dataset_id}") +async def delete_dataset(dataset_id: str, user=Depends(require_auth)): + async with httpx.AsyncClient(timeout=10.0) as client: + r = await client.delete( + f"{API_URL}/marine/datasets/{dataset_id}", + headers=_auth_headers(user), + ) + if not r.is_success: + raise HTTPException(status_code=r.status_code, detail=r.json().get("error", "Upstream error")) + return r.json() diff --git a/copernicus/routers/jobs.py b/copernicus/routers/jobs.py new file mode 100644 index 0000000..dbb7d66 --- /dev/null +++ b/copernicus/routers/jobs.py @@ -0,0 +1,145 @@ +""" +Flusso: +1. POST /jobs → crea job in Redis con stato "pending" +2. Background task: scarica dati → aggiorna stato in Redis +3. GET /jobs/{id} → legge stato da Redis +""" + +import json +import os +import uuid +from typing import Any, Dict + +import httpx +from core import copernicus +from core.cache import cache_get, cache_set, cache_delete +from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException +from middleware.auth import require_auth +from schemas import DownloadJobRequest, JobStatus + +router = APIRouter(prefix="/jobs", tags=["sessions"]) + +API_URL = os.getenv("API_SERVICE_URL", "http://api:3003") + +# TTL per lo stato dei job: 48 ore (i job completati vengono puliti automaticamente) +_JOB_TTL = 48 * 3600 + + +def _job_key(session_id: str) -> str: + """Genera la chiave Redis per un job.""" + return f"marine:job:{session_id}" + + +def _get_job(session_id: str) -> Dict[str, Any] | None: + """Legge lo stato di un job da Redis.""" + return cache_get(_job_key(session_id)) + + +def _set_job(session_id: str, **kwargs): + """Aggiorna lo stato di un job in Redis. + Legge lo stato corrente, applica le modifiche, e riscrive.""" + job = cache_get(_job_key(session_id)) + if job is None: + return + job.update(kwargs) + cache_set(_job_key(session_id), job, _JOB_TTL) + + +def _run_download(session_id: str, req: DownloadJobRequest, username: str, user_token: str): + """Download in background: Copernicus → conversione → upload via API service. + + Ad ogni cambio di fase, lo stato viene aggiornato in Redis + così il frontend può fare polling su GET /jobs/{id}. + """ + def progress(pct: int, msg: str): + _set_job(session_id, progress=pct, message=msg) + + try: + _set_job(session_id, status="downloading", progress=5, message="Scarico da Copernicus Marine...") + + # Scarica dati dal catalogo Copernicus + df = copernicus.download_dataset( + dataset_id=req.dataset_id, + variables=req.variables, + min_longitude=req.min_longitude, + max_longitude=req.max_longitude, + min_latitude=req.min_latitude, + max_latitude=req.max_latitude, + start_datetime=req.start_date, + end_datetime=req.end_date, + progress_callback=progress, + ) + + _set_job(session_id, status="converting", progress=80, message="Creo il file...") + + # Converte il DataFrame in bytes (CSV o JSON) + data_bytes, content_type = copernicus.dataframe_to_bytes(df, req.format, req.variable_renames) + filename = f"upload.{req.format}" + + _set_job(session_id, status="saving", progress=90, message="Carico su storage...") + + # Metadati del dataset per l'API service + metadata = { + "nome": req.nome, + "tags": req.tags, + "created_by": username, + "type": req.format, + "notes": req.notes, + "copernicus_dataset_id": req.dataset_id, + "variables": req.variables, + "variable_renames": req.variable_renames, + "bbox": [req.min_longitude, req.min_latitude, req.max_longitude, req.max_latitude], + "start_date": req.start_date, + "end_date": req.end_date, + } + + # Upload al servizio API che gestisce MinIO + with httpx.Client(timeout=None) as client: + r = client.post( + f"{API_URL}/marine/datasets/upload", + headers={"Authorization": f"Bearer {user_token}"}, + files={"file": (filename, data_bytes, content_type)}, + data={"metadata": json.dumps(metadata)}, + ) + + if not r.is_success: + raise RuntimeError(f"API upload failed ({r.status_code}): {r.text}") + + entry = r.json() + _set_job(session_id, status="done", progress=100, message="Dataset salvato.", dataset_id=entry["id"]) + + except Exception as e: + _set_job(session_id, status="error", progress=0, message=str(e)) + + +@router.post("", response_model=JobStatus, status_code=202) +async def new_download_session( + req: DownloadJobRequest, + background_tasks: BackgroundTasks, + user=Depends(require_auth) +): + """Crea un nuovo job di download e lo avvia in background.""" + session_id = str(uuid.uuid4()) + + # Stato iniziale del job salvato in Redis + initial_state = { + "job_id": session_id, + "status": "pending", + "progress": 0, + "message": "In coda", + "dataset_id": None, + } + cache_set(_job_key(session_id), initial_state, _JOB_TTL) + + # Avvia il download in background + background_tasks.add_task(_run_download, session_id, req, user["username"], user["token"]) + return initial_state + + +@router.get("/{session_id}", response_model=JobStatus) +async def get_download_session(session_id: str, user=Depends(require_auth)): + """Legge lo stato di un job di download da Redis.""" + session = _get_job(session_id) + if session is None: + raise HTTPException(status_code=404, detail="Job not found") + return session diff --git a/copernicus/schemas.py b/copernicus/schemas.py new file mode 100644 index 0000000..2a97b0b --- /dev/null +++ b/copernicus/schemas.py @@ -0,0 +1,77 @@ +from pydantic import BaseModel, Field +from typing import Dict, List, Optional, Any +from datetime import datetime + + +# ── Catalog ────────────────────────────────────────────────────────────────── + +class DatasetVariable(BaseModel): + short_name: str + standard_name: Optional[str] = None + units: Optional[str] = None + description: Optional[str] = None # human-readable label derived from standard_name + + +class CatalogDataset(BaseModel): + dataset_id: str + title: Optional[str] = None + description: Optional[str] = None + variables: List[DatasetVariable] = [] + min_longitude: Optional[float] = None + max_longitude: Optional[float] = None + min_latitude: Optional[float] = None + max_latitude: Optional[float] = None + start_datetime: Optional[str] = None + end_datetime: Optional[str] = None + + +# ── Jobs ───────────────────────────────────────────────────────────────────── + +class DownloadJobRequest(BaseModel): + dataset_id: str + variables: List[str] = Field(..., min_length=1) + min_longitude: float + max_longitude: float + min_latitude: float + max_latitude: float + start_date: str # YYYY-MM-DD + end_date: str # YYYY-MM-DD + format: str = Field("json", pattern="^(json|csv)$") + nome: str = Field(..., min_length=1) + tags: List[str] = Field(default_factory=lambda: ["marine"]) + notes: str = "" + variable_renames: Dict[str, str] = Field(default_factory=dict) # {original: custom} + + +class JobStatus(BaseModel): + job_id: str + status: str # pending | downloading | converting | saving | done | error + progress: int = 0 # 0-100 + message: str = "" + dataset_id: Optional[str] = None # filled on done + + +# ── Saved Datasets ──────────────────────────────────────────────────────────── + +class DatasetMeta(BaseModel): + id: str + nome: str + tags: List[str] = [] + created_date: str + created_by: str + used_last_date: Optional[str] = None + type: str # json | csv + size: int + notes: str = "" + version: int = 1 + filename: str + copernicus_dataset_id: str + variables: List[str] = [] + bbox: List[float] = [] # [min_lon, min_lat, max_lon, max_lat] + start_date: str + end_date: str + + +class DatasetListResponse(BaseModel): + datasets: List[DatasetMeta] + count: int diff --git a/copernicus/static/script.js b/copernicus/static/script.js new file mode 100644 index 0000000..01c6657 --- /dev/null +++ b/copernicus/static/script.js @@ -0,0 +1,581 @@ +const MARINE_API = API_URL + '/marine'; +const MAPBOX_TOKEN = 'pk.eyJ1Ijoic2VzZWUzIiwiYSI6ImNtZ2dydndkMDBsNjUya3NjeW91dW41MzcifQ.M2qxj0wL1W7plRzIataojQ'; + +// ── State ────────────────────────────────────────────────────────────────── +let selectedDatasetId = null; +let selectedVariables = new Set(); +let datasetDateRange = { min: null, max: null }; +let tags = ['marine']; +let currentBbox = null; +let currentStep = 0; +let map = null; +let isDrawMode = false; +let isDrawing = false; +let drawStart = null; +let pollInterval = null; +const TOTAL_STEPS = 6; + +// Variable renames: { originalName: customName } +let variableRenames = {}; +let _currentRenaming = null; + +// ── Init ─────────────────────────────────────────────────────────────────── +document.addEventListener('DOMContentLoaded', () => { + initMap(); + renderTags(); + setupTagInput(); + renderDots(); + showStep(0, false); + + document.getElementById('catalogSearch').addEventListener('keydown', e => { + if (e.key === 'Enter') searchCatalog(); + }); + + document.getElementById('renameInput').addEventListener('keydown', e => { + if (e.key === 'Enter') { e.preventDefault(); saveRename(); } + if (e.key === 'Escape') closeRenameModal(); + }); + + ['startDate','endDate','datasetName','outputFormat'].forEach(id => { + const el = document.getElementById(id); + if (el) el.addEventListener('change', () => { markDone(); updateSummary(); refreshNext(); }); + }); +}); + +// ── Stepper ──────────────────────────────────────────────────────────────── +function showStep(i, smooth = true) { + const steps = Array.from(document.querySelectorAll('.step')); + currentStep = Math.max(0, Math.min(i, TOTAL_STEPS - 1)); + + steps.forEach((el, idx) => { + const active = idx === currentStep; + el.classList.toggle('active', active); + if (active) el.removeAttribute('disabled'); + else el.setAttribute('disabled', ''); + }); + + document.getElementById('prevBtn').disabled = currentStep === 0; + document.getElementById('nextBtn').textContent = currentStep === TOTAL_STEPS - 1 ? 'Fine' : 'Prossimo'; + refreshNext(); + updateDots(); + updateSummary(); + markDone(); + + if (smooth) { + const active = steps[currentStep]; + if (active) setTimeout(() => active.scrollIntoView({ behavior: 'smooth', block: 'center' }), 60); + } +} + +function refreshNext() { + document.getElementById('nextBtn').disabled = !canAdvance(currentStep); +} + +function nextStep() { + if (!canAdvance(currentStep)) { showToast('Completa questo passo per continuare', 'error'); return; } + if (currentStep < TOTAL_STEPS - 1) showStep(currentStep + 1); +} + +function prevStep() { + if (currentStep > 0) showStep(currentStep - 1); +} + +function canAdvance(i) { + switch (i) { + case 0: return !!selectedDatasetId; + case 1: return selectedVariables.size > 0; + case 2: return !!currentBbox; + case 3: { + const s = document.getElementById('startDate').value; + const e = document.getElementById('endDate').value; + return !!(s && e && s <= e); + } + case 4: return !!document.getElementById('datasetName').value.trim(); + default: return true; + } +} + +// ── Dots ─────────────────────────────────────────────────────────────────── +function renderDots() { + const c = document.getElementById('progressDots'); + c.innerHTML = ''; + for (let i = 0; i < TOTAL_STEPS; i++) { + const d = document.createElement('button'); + d.className = 'progress-dot'; + d.setAttribute('aria-label', `Passo ${i + 1}`); + d.addEventListener('click', () => { if (i <= currentStep) showStep(i); }); + c.appendChild(d); + } + updateDots(); +} + +function updateDots() { + Array.from(document.getElementById('progressDots').children) + .forEach((d, i) => d.classList.toggle('active', i === currentStep)); +} + +// ── Done badges ──────────────────────────────────────────────────────────── +function markDone() { + const steps = document.querySelectorAll('.step'); + const checks = [ + !!selectedDatasetId, + selectedVariables.size > 0, + !!currentBbox, + canAdvance(3), + !!document.getElementById('datasetName').value.trim(), + false, + ]; + steps.forEach((el, i) => el.classList.toggle('done', checks[i] === true)); +} + +// ── Catalog ──────────────────────────────────────────────────────────────── +async function searchCatalog() { + const q = document.getElementById('catalogSearch').value.trim(); + const btn = document.getElementById('searchBtn'); + const box = document.getElementById('catalogResults'); + + box.innerHTML = '
Ricerca in corso...
'; + btn.disabled = true; + + try { + const params = q ? `?search=${encodeURIComponent(q)}&limit=30` : '?limit=30'; + const res = await MEB_AUTH.fetch(`${MARINE_API}/catalog${params}`); + const data = await res.json(); + + if (!res.ok) throw new Error(data.detail || 'Errore catalogo'); + + if (!data.datasets?.length) { + box.innerHTML = '
Nessun dataset trovato
'; + return; + } + + box.innerHTML = ''; + data.datasets.forEach(ds => { + const item = document.createElement('div'); + item.className = 'catalog-item'; + item.innerHTML = `
${ds.dataset_id}
${ds.title || ''}
`; + item.addEventListener('click', () => selectDataset(ds, item)); + box.appendChild(item); + }); + } catch (e) { + box.innerHTML = `
Errore: ${e.message}
`; + } finally { + btn.disabled = false; + } +} + +async function selectDataset(ds, itemEl) { + document.querySelectorAll('.catalog-item').forEach(i => i.classList.remove('selected')); + itemEl.classList.add('selected'); + selectedDatasetId = ds.dataset_id; + + const badge = document.getElementById('selectedDsBadge'); + badge.textContent = ds.dataset_id; + badge.style.display = 'block'; + + // Reset dependent steps + selectedVariables.clear(); + const vBox = document.getElementById('variablesContainer'); + vBox.innerHTML = 'Caricamento variabili...'; + + try { + const res = await MEB_AUTH.fetch(`${MARINE_API}/catalog/${encodeURIComponent(ds.dataset_id)}`); + const info = await res.json(); + + renderVariables(info.variables || ds.variables || []); + + if (info.min_longitude != null) { + setBboxAndFit(info.min_longitude, info.max_longitude, info.min_latitude, info.max_latitude); + } + + if (info.start_datetime) { + prefillDates(info.start_datetime, info.end_datetime); + } + } catch { + renderVariables(ds.variables || []); + } + + markDone(); + refreshNext(); + // Auto-advance to variables step + setTimeout(() => showStep(1), 700); +} + +// ── Variables ────────────────────────────────────────────────────────────── +function renderVariables(vars) { + const c = document.getElementById('variablesContainer'); + if (!vars?.length) { + c.innerHTML = 'Nessuna variabile disponibile'; + updateVarCount(); + return; + } + + c.innerHTML = ''; + vars.forEach(v => { + const name = typeof v === 'string' ? v : v.short_name; + const desc = typeof v === 'object' ? (v.description || v.standard_name || '') : ''; + const units = typeof v === 'object' && v.units ? v.units : ''; + + const chip = document.createElement('div'); + chip.className = 'var-chip'; + chip.dataset.name = name; + chip.innerHTML = ` + ${esc(name)} + ${desc ? `${esc(desc)}` : ''} + ${units ? `[${esc(units)}]` : ''} + + ${variableRenames[name] ? '→ ' + esc(variableRenames[name]) : ''} + `; + chip.addEventListener('click', () => toggleVar(chip, name)); + c.appendChild(chip); + }); + + updateVarCount(); +} + +function toggleVar(chip, name) { + if (selectedVariables.has(name)) { selectedVariables.delete(name); chip.classList.remove('selected'); } + else { selectedVariables.add(name); chip.classList.add('selected'); } + updateVarCount(); markDone(); refreshNext(); +} + +function updateVarCount() { + const n = selectedVariables.size; + document.getElementById('varCount').textContent = + n === 0 ? 'Nessuna selezionata' : `${n} selezionat${n === 1 ? 'a' : 'e'}`; +} + +function selectAllVars() { + document.querySelectorAll('.var-chip').forEach(chip => { + chip.classList.add('selected'); + selectedVariables.add(chip.dataset.name); + }); + updateVarCount(); markDone(); refreshNext(); +} + +function deselectAllVars() { + document.querySelectorAll('.var-chip').forEach(chip => chip.classList.remove('selected')); + selectedVariables.clear(); + updateVarCount(); markDone(); refreshNext(); +} + +// ── Dates ────────────────────────────────────────────────────────────────── +function prefillDates(minDate, maxDate) { + datasetDateRange = { min: minDate, max: maxDate }; + const s = document.getElementById('startDate'); + const e = document.getElementById('endDate'); + + if (minDate) { s.min = minDate; s.value = minDate; e.min = minDate; } + if (maxDate) { e.max = maxDate; e.value = maxDate; s.max = maxDate; } + + const hint = document.getElementById('dateRangeHint'); + if (minDate && maxDate) hint.textContent = `Dati disponibili: ${minDate} → ${maxDate}`; + + markDone(); updateSummary(); +} + +// ── Map ──────────────────────────────────────────────────────────────────── +function initMap() { + mapboxgl.accessToken = MAPBOX_TOKEN; + map = new mapboxgl.Map({ + container: 'mapContainer', + style: 'mapbox://styles/mapbox/dark-v11', + center: [14, 42], zoom: 3.5, + attributionControl: false, + }); + map.addControl(new mapboxgl.NavigationControl({ showCompass: false }), 'top-right'); + map.addControl(new mapboxgl.AttributionControl({ compact: true }), 'bottom-right'); + + map.on('load', () => { + map.addSource('bbox-rect', { type: 'geojson', data: { type: 'FeatureCollection', features: [] } }); + map.addLayer({ id: 'bbox-fill', type: 'fill', source: 'bbox-rect', paint: { 'fill-color': '#00a7f5', 'fill-opacity': 0.13 } }); + map.addLayer({ id: 'bbox-line', type: 'line', source: 'bbox-rect', paint: { 'line-color': '#00a7f5', 'line-width': 2, 'line-dasharray': [4, 2] } }); + }); + + map.getCanvas().addEventListener('mousedown', onCanvasMouseDown, true); + window.addEventListener('mousemove', onWindowMouseMove); + window.addEventListener('mouseup', onWindowMouseUp); +} + +function startDraw() { + if (!map) return; + isDrawMode = true; + map.dragPan.disable(); map.boxZoom.disable(); + document.getElementById('mapContainer').classList.add('draw-mode'); + const btn = document.getElementById('drawBtn'); + btn.textContent = 'Clicca e trascina…'; + btn.classList.replace('secondary', 'primary'); +} + +function exitDrawMode() { + isDrawMode = isDrawing = false; drawStart = null; + map.dragPan.enable(); map.boxZoom.enable(); + document.getElementById('mapContainer').classList.remove('draw-mode', 'drawing'); + const btn = document.getElementById('drawBtn'); + btn.textContent = 'Disegna area'; + btn.classList.replace('primary', 'secondary'); +} + +function onCanvasMouseDown(e) { + if (!isDrawMode) return; + e.preventDefault(); e.stopPropagation(); + isDrawing = true; + drawStart = map.unproject([e.offsetX, e.offsetY]); + document.getElementById('mapContainer').classList.add('drawing'); +} + +function onWindowMouseMove(e) { + if (!isDrawing || !drawStart) return; + const rc = map.getCanvas().getBoundingClientRect(); + _drawRect(drawStart, map.unproject([e.clientX - rc.left, e.clientY - rc.top])); +} + +function onWindowMouseUp(e) { + if (!isDrawing || !drawStart) return; + const rc = map.getCanvas().getBoundingClientRect(); + const end = map.unproject([e.clientX - rc.left, e.clientY - rc.top]); + setBbox( + Math.min(drawStart.lng, end.lng), Math.max(drawStart.lng, end.lng), + Math.min(drawStart.lat, end.lat), Math.max(drawStart.lat, end.lat) + ); + exitDrawMode(); +} + +function _drawRect(a, b) { + if (!map.getSource('bbox-rect')) return; + map.getSource('bbox-rect').setData({ + type: 'Feature', + geometry: { type: 'Polygon', coordinates: [[[a.lng,a.lat],[b.lng,a.lat],[b.lng,b.lat],[a.lng,b.lat],[a.lng,a.lat]]] }, + }); +} + +function setBbox(minLon, maxLon, minLat, maxLat) { + currentBbox = { minLon, maxLon, minLat, maxLat }; + document.getElementById('minLon').value = minLon.toFixed(4); + document.getElementById('maxLon').value = maxLon.toFixed(4); + document.getElementById('minLat').value = minLat.toFixed(4); + document.getElementById('maxLat').value = maxLat.toFixed(4); + document.getElementById('bboxReadout').textContent = + `${minLon.toFixed(2)}°/${minLat.toFixed(2)}° → ${maxLon.toFixed(2)}°/${maxLat.toFixed(2)}°`; + _drawRect({ lng: minLon, lat: minLat }, { lng: maxLon, lat: maxLat }); + markDone(); refreshNext(); +} + +function setBboxAndFit(minLon, maxLon, minLat, maxLat) { + const doIt = () => { + setBbox(minLon, maxLon, minLat, maxLat); + map.fitBounds([[minLon, minLat], [maxLon, maxLat]], { padding: 40, maxZoom: 10, duration: 600 }); + }; + if (!map) return; + if (map.isStyleLoaded()) doIt(); else map.once('load', doIt); +} + +function clearBbox() { + currentBbox = null; + ['minLon','maxLon','minLat','maxLat'].forEach(id => document.getElementById(id).value = ''); + document.getElementById('bboxReadout').textContent = ''; + if (map?.getSource('bbox-rect')) map.getSource('bbox-rect').setData({ type: 'FeatureCollection', features: [] }); + markDone(); refreshNext(); +} + +// ── Tags ─────────────────────────────────────────────────────────────────── +function setupTagInput() { + const inp = document.getElementById('tagInput'); + inp.addEventListener('keydown', e => { + if ((e.key === 'Enter' || e.key === ',') && inp.value.trim()) { + e.preventDefault(); + const t = inp.value.trim().replace(/,/g,'').toLowerCase(); + if (t && !tags.includes(t)) { tags.push(t); renderTags(); } + inp.value = ''; + } else if (e.key === 'Backspace' && !inp.value && tags.length) { + tags.pop(); renderTags(); + } + }); +} + +function renderTags() { + const wrap = document.getElementById('tagsWrap'); + const inp = document.getElementById('tagInput'); + wrap.innerHTML = ''; + tags.forEach(t => { + const chip = document.createElement('span'); + chip.className = 'tag-chip'; + chip.innerHTML = `${t} ×`; + wrap.appendChild(chip); + }); + wrap.appendChild(inp); +} + +function removeTag(t) { tags = tags.filter(x => x !== t); renderTags(); } + +// ── Download ─────────────────────────────────────────────────────────────── +async function startDownload() { + if (!selectedDatasetId) return showToast('Seleziona un dataset', 'error'); + if (!selectedVariables.size) return showToast('Seleziona almeno una variabile', 'error'); + if (!currentBbox) return showToast("Disegna un'area sulla mappa", 'error'); + if (!document.getElementById('startDate').value || + !document.getElementById('endDate').value) return showToast('Inserisci le date', 'error'); + if (!document.getElementById('datasetName').value.trim()) return showToast('Inserisci un nome', 'error'); + + const body = { + dataset_id: selectedDatasetId, + variables: Array.from(selectedVariables), + min_longitude: parseFloat(document.getElementById('minLon').value), + max_longitude: parseFloat(document.getElementById('maxLon').value), + min_latitude: parseFloat(document.getElementById('minLat').value), + max_latitude: parseFloat(document.getElementById('maxLat').value), + start_date: document.getElementById('startDate').value, + end_date: document.getElementById('endDate').value, + format: document.getElementById('outputFormat').value, + nome: document.getElementById('datasetName').value.trim(), + tags: [...tags], + notes: document.getElementById('datasetNotes').value.trim(), + variable_renames: { ...variableRenames }, + }; + + const btn = document.getElementById('downloadBtn'); + const prog = document.getElementById('downloadProgress'); + btn.disabled = true; + prog.style.display = 'block'; + setProgress(0, 'Avvio download...'); + + try { + const res = await MEB_AUTH.fetch(`${MARINE_API}/jobs`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }); + const data = await res.json(); + if (!res.ok) throw new Error(data.detail || 'Errore avvio job'); + pollJob(data.job_id, btn, prog); + } catch (e) { + showToast(`Errore: ${e.message}`, 'error'); + btn.disabled = false; + prog.style.display = 'none'; + } +} + +function pollJob(jobId, btn, prog) { + clearInterval(pollInterval); + pollInterval = setInterval(async () => { + try { + const res = await MEB_AUTH.fetch(`${MARINE_API}/jobs/${jobId}`); + const job = await res.json(); + setProgress(job.progress, job.message); + + if (job.status === 'done') { + clearInterval(pollInterval); + btn.disabled = false; + showToast('Dataset salvato con successo!', 'success'); + setTimeout(resetAll, 1800); + } else if (job.status === 'error') { + clearInterval(pollInterval); + btn.disabled = false; + showToast(`Errore: ${job.message}`, 'error'); + prog.style.display = 'none'; + } + } catch { /* transient */ } + }, 2000); +} + +function setProgress(pct, msg) { + document.getElementById('progressFill').style.width = pct + '%'; + document.getElementById('progressMsg').textContent = msg; +} + +// ── Reset ────────────────────────────────────────────────────────────────── +function resetAll() { + selectedDatasetId = null; + selectedVariables.clear(); + datasetDateRange = { min: null, max: null }; + variableRenames = {}; + tags = ['marine']; + + ['startDate','endDate','datasetName','datasetNotes'].forEach(id => { + const el = document.getElementById(id); + if (el) { el.value = ''; el.min = ''; el.max = ''; } + }); + + document.getElementById('catalogResults').innerHTML = + '
Cerca un dataset Copernicus per iniziare
'; + document.getElementById('selectedDsBadge').style.display = 'none'; + document.getElementById('variablesContainer').innerHTML = + 'Seleziona un dataset per vedere le variabili'; + document.getElementById('dateRangeHint').textContent = ''; + document.getElementById('downloadProgress').style.display = 'none'; + + clearBbox(); + renderTags(); + showStep(0); +} + +// ── Summary ──────────────────────────────────────────────────────────────── +function updateSummary() { + if (currentStep !== 5) return; + const s = document.getElementById('startDate').value || '-'; + const e = document.getElementById('endDate').value || '-'; + const name = document.getElementById('datasetName').value || '-'; + const fmt = document.getElementById('outputFormat').value || '-'; + const vars = Array.from(selectedVariables).join(', ') || '-'; + const bbox = currentBbox + ? `${currentBbox.minLon.toFixed(2)},${currentBbox.minLat.toFixed(2)} → ${currentBbox.maxLon.toFixed(2)},${currentBbox.maxLat.toFixed(2)}` + : '-'; + document.getElementById('summaryContent').innerHTML = ` +
Dataset: ${esc(selectedDatasetId || '-')}
+
Variabili (${selectedVariables.size}): ${esc(vars)}
+
Area: ${esc(bbox)}
+
Periodo: ${esc(s)} → ${esc(e)}
+
Formato: ${esc(fmt)}
+
Nome: ${esc(name)}
+
Tags: ${esc(tags.join(', '))}
+ `; +} + +// ── Rename modal ─────────────────────────────────────────────────────────── +function openRenameModal(varName) { + _currentRenaming = varName; + document.getElementById('renameVarLabel').textContent = varName; + document.getElementById('renameInput').value = variableRenames[varName] || ''; + document.getElementById('renameDeleteBtn').style.display = variableRenames[varName] ? 'inline-flex' : 'none'; + document.getElementById('renameModal').classList.add('visible'); + setTimeout(() => document.getElementById('renameInput').select(), 50); +} + +function saveRename() { + if (!_currentRenaming) return; + const val = document.getElementById('renameInput').value.trim(); + if (!val) { deleteRename(); return; } + variableRenames[_currentRenaming] = val; + _updateRenameBadge(_currentRenaming, val); + closeRenameModal(); +} + +function deleteRename() { + if (!_currentRenaming) return; + delete variableRenames[_currentRenaming]; + _updateRenameBadge(_currentRenaming, ''); + closeRenameModal(); +} + +function closeRenameModal() { + document.getElementById('renameModal').classList.remove('visible'); + _currentRenaming = null; +} + +function _updateRenameBadge(varName, text) { + const chip = [...document.querySelectorAll('.var-chip')].find(c => c.dataset.name === varName); + if (!chip) return; + chip.querySelector('.rename-badge').textContent = text ? '→ ' + text : ''; +} + +// ── Helpers ──────────────────────────────────────────────────────────────── +function esc(s) { + return String(s).replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"'); +} + +function showToast(msg, type = 'success') { + const t = document.getElementById('toast'); + t.className = `${type} show`; + t.textContent = msg; + setTimeout(() => t.classList.remove('show'), 3500); +} \ No newline at end of file diff --git a/copernicus/static/style.css b/copernicus/static/style.css new file mode 100644 index 0000000..e69de29 diff --git a/copernicus/templates/coprncs.html b/copernicus/templates/coprncs.html new file mode 100644 index 0000000..decbfa2 --- /dev/null +++ b/copernicus/templates/coprncs.html @@ -0,0 +1,163 @@ + + + + + + Copernicus Marine + + + + + + + + + +
+ + + + +
+
+ 1 +

Cerca un dataset

+ ✓ Selezionato +
+ +
+
+
+
+
+ + +
+
+ 2 +

Variabili

+ ✓ Selezionate +
+
+ Nessuna selezionata + +
+
+ +
+
+ + +
+
+ 3 +

Area

+ ✓ Impostata +
+
+
+ + + +
+ + + + +
+ + +
+
+ 4 +

Finestra temporale

+ +
+
+ + +
+
+
+ + +
+
+ 5 +

Dettagli

+ Completo +
+
+ +
+
+ +
+
+
+ +
+
+
+ +
+
+ + +
+
+ 6 +

Scarica

+
+
+
+ + +
+
+
+
+
+
+
+
+ +
+ + +
+
+
+ + +
+
+ +
+ + +
+
+
+

Rinomina variabile

+
+ +
+ + +
+
+
+ + + \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..b19a92c --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,164 @@ +services: + auth: + container_name: auth + build: + context: ./auth + dockerfile: Dockerfile + restart: unless-stopped + command: npm run dev + volumes: + - ./auth:/app + - /app/node_modules + env_file: + - ./auth/.env + healthcheck: + test: ["CMD", "node", "-e", "fetch('http://localhost:3006/health').then(r => r.ok ? process.exit(0) : process.exit(1))"] + interval: 30s + timeout: 5s + retries: 3 + networks: + - meb-proxy-net + - meb-internal + ports: + - "3006:3006" + labels: + - "traefik.enable=true" + - "traefik.http.routers.auth.rule=Host(`auth.${URL_DOMAIN}`)" + - "traefik.http.routers.auth.entrypoints=web" + - "traefik.http.services.auth.loadbalancer.server.port=3006" + - "traefik.docker.network=meb-proxy-net" + + api: + container_name: api-services + build: + context: ./api + dockerfile: Dockerfile + restart: unless-stopped + command: npm run dev + volumes: + - ./api/src:/app/src + - /app/node_modules + - ./ml:/ml-source + - /var/run/docker.sock:/var/run/docker.sock + env_file: + - ./api/.env + networks: + - meb-proxy-net + - meb-internal + ports: + - "3003:3003" + + console: + build: + context: ./console + dockerfile: Dockerfile + restart: unless-stopped + command: npm run dev + volumes: + - ./console:/app + - /app/node_modules + env_file: + - ./console/.env + networks: + - meb-proxy-net + - meb-internal + ports: + - "3004:3004" + + realtime: + build: + context: ./realtime + dockerfile: Dockerfile + restart: unless-stopped + command: npm run dev + ports: + - "3002:3002" + - "3102:3102" + volumes: + - ./realtime:/app + - /app/node_modules + env_file: + - ./realtime/.env + networks: + - meb-proxy-net + - meb-internal + + # ml: + # container_name: ml-service + # build: + # context: ./ml + # dockerfile: Dockerfile + # restart: unless-stopped + # volumes: + # - ./ml:/app + # env_file: + # - ./ml/.env + # ports: + # - "3005:3005" + # networks: + # - meb-proxy-net + # - meb-internal + +# marine: +# container_name: marine-service +# build: +# context: ./marine +# dockerfile: Dockerfile +# restart: unless-stopped +# volumes: +# - ./marine:/app +# env_file: +# - ./marine/.env +# environment: +# - REDIS_HOST=meb-redis +# - REDIS_PORT=6379 +# networks: +# - meb-proxy-net +# - meb-internal +# labels: +# - "traefik.enable=true" +# - "traefik.http.routers.marine.rule=Host(`api.${URL_DOMAIN}`) && PathPrefix(`/marine`)" +# - "traefik.http.routers.marine.entrypoints=web" +# - "traefik.http.services.marine.loadbalancer.server.port=8001" +# - "traefik.docker.network=meb-proxy-net" +# - "traefik.http.middlewares.marine-strip.stripprefix.prefixes=/marine" +# - "traefik.http.routers.marine.middlewares=marine-strip" + +# circuits: +# container_name: meb-circuits +# build: +# context: ./circuits +# dockerfile: Dockerfile +# restart: unless-stopped +# environment: +# - DATABASE_URL=postgresql://meb:meb@meb-postgres:5432/circuits +# - AUTH_SERVICE_URL=http://auth:3001 +# - AUTH_URL=http://auth.${URL_DOMAIN:-localhost} +# - API_URL=http://api.${URL_DOMAIN:-localhost} +# - NODE_ENV=${NODE_ENV:-development} +# volumes: +# - ./circuits/src:/app/src +# - /app/node_modules +# healthcheck: +# test: ["CMD", "node", "-e", "fetch('http://localhost:3005/health').then(r => r.ok ? process.exit(0) : process.exit(1))"] +# interval: 30s +# timeout: 5s +# retries: 3 +# depends_on: +# - auth +# networks: +# - meb-proxy-net +# - meb-internal +# labels: +# - "traefik.enable=true" +# - "traefik.http.routers.circuits.rule=Host(`circuits.${URL_DOMAIN}`)" +# - "traefik.http.routers.circuits.entrypoints=web" +# - "traefik.http.services.circuits.loadbalancer.server.port=3005" +# - "traefik.docker.network=meb-proxy-net" +# - "traefik.http.routers.circuits.middlewares=cors-ignore" + +networks: + meb-proxy-net: + external: true + meb-internal: + external: true diff --git a/ml/.dockerignore b/ml/.dockerignore new file mode 100644 index 0000000..bee8a64 --- /dev/null +++ b/ml/.dockerignore @@ -0,0 +1 @@ +__pycache__ diff --git a/ml/.env.example b/ml/.env.example new file mode 100644 index 0000000..e69de29 diff --git a/ml/Dockerfile b/ml/Dockerfile new file mode 100644 index 0000000..cc0f8cd --- /dev/null +++ b/ml/Dockerfile @@ -0,0 +1,13 @@ +FROM python:3.11-slim + +WORKDIR /app +ENV PYTHONUNBUFFERED=1 + +COPY ./requirements.txt . + +RUN pip install --no-cache-dir -r requirements.txt + +COPY . /app + +EXPOSE 3007 +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "3007"] diff --git a/ml/main.py b/ml/main.py new file mode 100644 index 0000000..51046b2 --- /dev/null +++ b/ml/main.py @@ -0,0 +1,19 @@ +from fastapi import FastAPI, Request, Response, Header +from fastapi.responses import HTMLResponse, JSONResponse +import time + +app = FastAPI() + +@app.get("/health") +def health(): + return { + "status": "ok", + "service": "ml", + "version": "1.0.0", + "build_number": "1", + "version_state": "dev" + } + +@app.get("/") +def root(): + return {"message": "ML Service"} \ No newline at end of file diff --git a/ml/requirements.txt b/ml/requirements.txt new file mode 100644 index 0000000..97dc7cd --- /dev/null +++ b/ml/requirements.txt @@ -0,0 +1,2 @@ +fastapi +uvicorn diff --git a/ml/static/font/quicksand.ttf b/ml/static/font/quicksand.ttf new file mode 100644 index 0000000..44ecad8 Binary files /dev/null and b/ml/static/font/quicksand.ttf differ diff --git a/ml/static/styles/style.css b/ml/static/styles/style.css new file mode 100644 index 0000000..8b7defd --- /dev/null +++ b/ml/static/styles/style.css @@ -0,0 +1,201 @@ +:root { + --accent-color: #2563eb; + --accent-hover: #1d4ed8; + --accent-light: #eff6ff; + --accent-border: #bfdbfe; + + --text-primary: #0f172a; + --text-secondary: #4755698f; + --text-tertiary: #94a3b8c0; + + --surface: #f8fafc; + + --header-bg: rgba(255, 255, 255, 0.85); + /* For Glassmorphism */ + --header-border: #e2e8f0; + + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05); + --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); + --radius-md: 8px; + --radius-lg: 12px; +} + +* { + margin: 0; + padding: 0; +} + +@font-face { + font-family: 'Normal'; + src: url('../font/Quicksand-VariableFont_wght.ttf'); + font-weight: 400; + font-style: normal; +} + +@font-face { + font-family: 'Bold'; + src: url('../font/Quicksand-VariableFont_wght.ttf'); + font-weight: 700; + font-style: normal; +} + +body { + font-family: 'Normal', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; + color: var(--text-primary); + -webkit-font-smoothing: antialiased; +} + +button { + padding: 10px 24px; + border-radius: var(--radius-lg); + border: 1px solid var(--header-border); + background-color: var(--bg-surface); + color: var(--text-primary); + font-size: 14px; + font-weight: 600; + font-family: 'Bold', inherit; + cursor: pointer; + transition: all 0.5s cubic-bezier(0.8, 0, 0.2, 1); + white-space: nowrap; + flex-shrink: 0; +} + +button:hover { + background-color: var(--accent-light); + color: var(--accent-color); + border-color: var(--accent-border); +} + +button.prominent { + background-color: var(--accent-color); + color: #ffffff; + border: 1px solid transparent; + box-shadow: var(--shadow-sm); +} + +button.prominent:hover { + background-color: var(--accent-hover); + transform: translateY(-1px); + box-shadow: var(--shadow-md); +} + + +button.prominent:active { + transform: translateY(1px); + box-shadow: var(--shadow-sm); +} + + +/* INFO PANEL */ + +.info-panel { + display: block; + text-align: center; + align-content: center; + padding: 60px 20px; + user-select: none; +} + +.info-panel h3 { + margin-bottom: 10px; +} + +.info-panel p { + margin-bottom: 25px; +} + +.info-panel .icon { + font-size: 48px; + margin-bottom: 20px; + align-self: center; + transition: transform 0.12s ease; +} + + + + +/* GRID & CARD ITEMS */ + +.grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 1.5rem; + margin-inline: 30px; +} + +.card { + display: flex; + gap: 0.75rem; + align-items: center; + padding: 1rem; + border: 1px solid var(--header-border); + border-radius: 20px; + text-decoration: none; + color: var(--text-primary); + transition: transform 0.12s ease, box-shadow 0.12s ease; +} + +.card h3 { + margin: 0; + font-size: 1rem; + text-align: left; +} + +.card p { + margin: 0.25rem 0 0; + color: var(--text-secondary); + opacity: 0.4; + font-size: 0.8rem; + text-align: left; +} + +.card:hover { + transform: translateY(-4px); + box-shadow: 0 8px 30px #bfdbfe30; +} + +.card.standalone { + grid-column: 1 / -1; +} + + + + + +/* HEADER */ + +.header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 16px 24px; + background-color: var(--header-bg); + border-bottom: 1px solid var(--header-border); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + position: sticky; + top: 0; + z-index: 10; + user-select: none; +} + + +.header h1 { + color: var(--text-primary); + font-size: 1.25rem; + font-weight: 700; + letter-spacing: -0.025em; +} + +.header .profile { + display: flex; + align-items: center; + gap: 16px; +} + +.header .profile p { + font-size: 0.875rem; + font-weight: 500; + color: var(--text-secondary); + padding-inline: 5px; +} \ No newline at end of file diff --git a/ml/templates/console.html b/ml/templates/console.html new file mode 100644 index 0000000..5968994 --- /dev/null +++ b/ml/templates/console.html @@ -0,0 +1,28 @@ + + + + + + ML + + + + +
+

Modelli ML

+
+

Utente

+ +
+
+ +
+ +
+ + + + + \ No newline at end of file diff --git a/ml/templates/datasets.html b/ml/templates/datasets.html new file mode 100644 index 0000000..e69de29 diff --git a/ml/templates/results.html b/ml/templates/results.html new file mode 100644 index 0000000..6c220f0 --- /dev/null +++ b/ml/templates/results.html @@ -0,0 +1,89 @@ + + + + + Risultati + + + + + + + +
+

Risultati

+
+

Utente

+ +
+
+ +
+ +
+ +
+

+ Seleziona +

+ +

+ una sessione di training eseguita per visualizzarne i risultati +

+
+ +
+ +
+

sessione 1

+
+

24/03/2026

+

12:00

+

dataset: d-1

+
+ +
+ +
+

sessione 2

+

24/03/2026

+ +
+ +
+ +
+ +
+ + + + + diff --git a/ml/templates/test.html b/ml/templates/test.html new file mode 100644 index 0000000..e69de29 diff --git a/ml/templates/train.html b/ml/templates/train.html new file mode 100644 index 0000000..e69de29 diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..1193580 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,179 @@ +{ + "name": "meb-custom-server", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "cookie-parser": "^1.4.7", + "jsonwebtoken": "^9.0.3" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-parser": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", + "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==", + "license": "MIT", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.6" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..fa13629 --- /dev/null +++ b/package.json @@ -0,0 +1,6 @@ +{ + "dependencies": { + "cookie-parser": "^1.4.7", + "jsonwebtoken": "^9.0.3" + } +} diff --git a/realtime/.dockerignore b/realtime/.dockerignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/realtime/.dockerignore @@ -0,0 +1 @@ +node_modules diff --git a/realtime/.env.example b/realtime/.env.example new file mode 100644 index 0000000..59fe0c6 --- /dev/null +++ b/realtime/.env.example @@ -0,0 +1,9 @@ +PORT=3004 + +VERSION=1.0.0 +VERSION_BUILD=1.0 +VERSION_STATE=beta + +INFLX_URL= +INFLX_TOKEN= +INFLX_ORG= diff --git a/realtime/Dockerfile b/realtime/Dockerfile new file mode 100644 index 0000000..ed2909e --- /dev/null +++ b/realtime/Dockerfile @@ -0,0 +1,12 @@ +FROM node:20-alpine + +WORKDIR /app + +COPY package*.json ./ +RUN npm install + +COPY src ./src + +EXPOSE 3002 + +CMD ["node", "src/index.js"] \ No newline at end of file diff --git a/realtime/package-lock.json b/realtime/package-lock.json new file mode 100644 index 0000000..ef635c9 --- /dev/null +++ b/realtime/package-lock.json @@ -0,0 +1,1110 @@ +{ + "name": "meb-realtime-service", + "version": "1.4.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "meb-realtime-service", + "version": "1.4.0", + "dependencies": { + "@influxdata/influxdb-client": "^1.35.0", + "@influxdata/influxdb-client-apis": "^1.35.0", + "@msgpack/msgpack": "^3.1.3", + "express": "^5.2.1", + "ioredis": "^5.10.0", + "pg": "^8.20.0", + "ws": "^8.19.0" + } + }, + "node_modules/@influxdata/influxdb-client": { + "version": "1.35.0", + "resolved": "https://registry.npmjs.org/@influxdata/influxdb-client/-/influxdb-client-1.35.0.tgz", + "integrity": "sha512-woWMi8PDpPQpvTsRaUw4Ig+nOGS/CWwAwS66Fa1Vr/EkW+NEwxI8YfPBsdBMn33jK2Y86/qMiiuX/ROHIkJLTw==", + "license": "MIT" + }, + "node_modules/@influxdata/influxdb-client-apis": { + "version": "1.35.0", + "resolved": "https://registry.npmjs.org/@influxdata/influxdb-client-apis/-/influxdb-client-apis-1.35.0.tgz", + "integrity": "sha512-+7h6smVPHYBge2rNKgYh/5k+SriYvPMsoJDfbUiQt1vJtpWnElwgBDLDl7Cr6d9XPC+FCI9GP4GQEMv7y8WxdA==", + "license": "MIT", + "peerDependencies": { + "@influxdata/influxdb-client": "*" + } + }, + "node_modules/@ioredis/commands": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.5.1.tgz", + "integrity": "sha512-JH8ZL/ywcJyR9MmJ5BNqZllXNZQqQbnVZOqpPQqE1vHiFgAw4NHbvE0FOduNU8IX9babitBT46571OnPTT0Zcw==", + "license": "MIT" + }, + "node_modules/@msgpack/msgpack": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@msgpack/msgpack/-/msgpack-3.1.3.tgz", + "integrity": "sha512-47XIizs9XZXvuJgoaJUIE2lFoID8ugvc0jzSHP+Ptfk8nTbnR8g788wv48N03Kx0UkAv559HWRQ3yzOgzlRNUA==", + "license": "ISC", + "engines": { + "node": ">= 18" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/content-disposition": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "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", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "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" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "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", + "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" + }, + "funding": { + "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/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "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" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ioredis": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.10.0.tgz", + "integrity": "sha512-HVBe9OFuqs+Z6n64q09PQvP1/R4Bm+30PAyyD4wIEqssh3v9L21QjCVk4kRLucMBcDokJTcLjsGeVRlq/nH6DA==", + "license": "MIT", + "dependencies": { + "@ioredis/commands": "1.5.1", + "cluster-key-slot": "^1.1.0", + "debug": "^4.3.4", + "denque": "^2.1.0", + "lodash.defaults": "^4.2.0", + "lodash.isarguments": "^3.1.0", + "redis-errors": "^1.2.0", + "redis-parser": "^3.0.0", + "standard-as-callback": "^2.1.0" + }, + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ioredis" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "license": "MIT" + }, + "node_modules/lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==", + "license": "MIT" + }, + "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": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/pg": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.20.0.tgz", + "integrity": "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.12.0", + "pg-pool": "^3.13.0", + "pg-protocol": "^1.13.0", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.3.0" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.3.0.tgz", + "integrity": "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.12.0.tgz", + "integrity": "sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.13.0.tgz", + "integrity": "sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.13.0.tgz", + "integrity": "sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", + "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/redis-errors": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", + "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/redis-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", + "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", + "license": "MIT", + "dependencies": { + "redis-errors": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/standard-as-callback": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", + "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==", + "license": "MIT" + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + } + } +} diff --git a/realtime/package.json b/realtime/package.json new file mode 100644 index 0000000..41b825f --- /dev/null +++ b/realtime/package.json @@ -0,0 +1,19 @@ +{ + "name": "meb-realtime-service", + "version": "1.4.0", + "description": "Realtime service for sensor connections - Node.js", + "main": "src/index.js", + "scripts": { + "start": "node src/index.js", + "dev": "node --watch src/index.js" + }, + "dependencies": { + "@influxdata/influxdb-client": "^1.35.0", + "@influxdata/influxdb-client-apis": "^1.35.0", + "@msgpack/msgpack": "^3.1.3", + "express": "^5.2.1", + "ioredis": "^5.10.0", + "pg": "^8.20.0", + "ws": "^8.19.0" + } +} diff --git a/realtime/src/helper/authdb.js b/realtime/src/helper/authdb.js new file mode 100644 index 0000000..382c505 --- /dev/null +++ b/realtime/src/helper/authdb.js @@ -0,0 +1,84 @@ +const { Pool } = require('pg'); +const { hash, generateShortId } = require('./cryptoUtils'); + +const pool = new Pool({ + user: process.env.DB_USER, + host: process.env.DB_HOST, + database: process.env.DB_NAME, + password: process.env.DB_PASSWORD, + port: process.env.DB_PORT, +}) + +async function checkDB() { + try { + await pool.query('SELECT NOW()'); + } catch (error) { + console.error('Database connection failed:', error); + } +} + +/** + * Restituisce i dati del sensore in base al token ricevuto. + * Il token viene hashato prima della comparazione con il database. + * @param {string} token - il codice segreto del sensore (raw) + */ +async function getSensor(token) { + const hashed = hash(token); + const result = await pool.query('SELECT id, is_active, name, last_seen, created_at FROM sensors WHERE code_hash = $1', [hashed]); + return result.rows[0]; +} + +async function createSensor(name, code) { + const hashedCode = hash(code); + + // Verifica se l'hash esiste già + const result = await pool.query('SELECT id FROM sensors WHERE code_hash = $1', [hashedCode]); + if (result.rows.length > 0) { + throw new Error('Sensor with this code already exists'); + } + + // Genera un ID casuale di 8 caratteri (ottimizzato per spazio, non solo alfanumerico) + const sensorId = generateShortId(8); + + await pool.query('INSERT INTO sensors (id, name, code_hash, is_active, last_seen, created_at) VALUES ($1, $2, $3, $4, $5, $6)', + [sensorId, name, hashedCode, true, new Date(), new Date()]); +} + +/** + * Aggiorna l'ultima attività del sensore. + * @param {*} id - l'id del sensore + * @returns {Promise} + */ +async function updateLastSeen(id) { + await pool.query('UPDATE sensors SET last_seen = NOW() WHERE id = $1', [id]); +} + +/** + * Modifica la disponibilità del sensore. + * @param {*} id - l'id del sensore + * @param {*} is_active - la disponibilità del sensore + * @returns {Promise} + */ +async function setSensorActivity(id, is_active) { + await pool.query('UPDATE sensors SET is_active = $1 WHERE id = $2', [is_active, id]); +} + +async function sensorsExists(id) { + const result = await pool.query('SELECT id FROM sensors WHERE id = $1', [id]); + return result.rows.length > 0; +} + +async function getSensors() { + const resutls = await pool.query('SELECT id, is_active, name, last_seen, created_at FROM sensors'); + return resutls.rows; +} + +module.exports = { + checkDB, + getSensor, + updateLastSeen, + setSensorActivity, + getSensors, + sensorsExists, + createSensor +} \ No newline at end of file diff --git a/realtime/src/helper/cryptoUtils.js b/realtime/src/helper/cryptoUtils.js new file mode 100644 index 0000000..ec2bdab --- /dev/null +++ b/realtime/src/helper/cryptoUtils.js @@ -0,0 +1,35 @@ +const crypto = require('crypto'); + +/** + * Genera un hash SHA256 in formato esadecimale da una stringa. + * Utilizzato per rendere compatibili authdb.js e tokenStore.js. + */ +function hash(text) { + if (!text) return null; + return crypto.createHash('sha256').update(text).digest('hex'); +} + +/** + * Genera una stringa casuale di lunghezza 'length'. + * Ottimizzata per risparmiare spazio (8 caratteri). + * Include lettere, numeri e simboli per massimizzare l'entropia (non solo alfanumerico). + */ +function generateShortId(length = 8) { + const charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*'; + let result = ''; + while (result.length < length) { + const bytes = crypto.randomBytes(length); + for (let i = 0; i < bytes.length && result.length < length; i++) { + // Selezioniamo solo i byte che rientrano nel range del charset per evitare bias + if (bytes[i] < 256 - (256 % charset.length)) { + result += charset[bytes[i] % charset.length]; + } + } + } + return result; +} + +module.exports = { + hash, + generateShortId +}; diff --git a/realtime/src/helper/influxReader.js b/realtime/src/helper/influxReader.js new file mode 100644 index 0000000..7e8b3e1 --- /dev/null +++ b/realtime/src/helper/influxReader.js @@ -0,0 +1,75 @@ +const { InfluxDB } = require('@influxdata/influxdb-client'); + +const url = process.env.INFLX_URL; +const token = process.env.INFLX_TOKEN; +const org = process.env.INFLX_ORG; +const bucket = 'boat'; + +const client = new InfluxDB({ url, token }); +const queryApi = client.getQueryApi(org); + +/** + * Query tutti i dati di una sessione sensore da un timestamp di inizio. + * Ritorna array di righe { _time, _measurement, _field, _value, sensor }. + */ +async function querySessionData(sensorId, fromTimestamp) { + const from = new Date(fromTimestamp).toISOString(); + + const query = ` + from(bucket: "${bucket}") + |> range(start: ${from}) + |> filter(fn: (r) => r["sensor"] == "${sensorId}") + |> pivot(rowKey:["_time"], columnKey: ["_field"], valueColumn: "_value") + `; + + const rows = []; + return new Promise((resolve, reject) => { + queryApi.queryRows(query, { + next(row, tableMeta) { + const obj = tableMeta.toObject(row); + rows.push(obj); + }, + error(err) { + console.error(`[INFLUX] Query error:`, err.message); + reject(err); + }, + complete() { + resolve(rows); + } + }); + }); +} + +/** + * Formatta i risultati della query in CSV. + */ +function formatCSV(rows) { + if (rows.length === 0) return ''; + + // Raccogli tutte le colonne uniche escludendo meta-campi InfluxDB + const excludeKeys = new Set(['result', 'table', '_start', '_stop', '']); + const allKeys = new Set(); + for (const row of rows) { + for (const key of Object.keys(row)) { + if (!excludeKeys.has(key)) allKeys.add(key); + } + } + + const columns = ['_time', '_measurement', 'sensor', + ...Array.from(allKeys).filter(k => !['_time', '_measurement', 'sensor'].includes(k)).sort() + ]; + + const header = columns.join(','); + const lines = rows.map(row => + columns.map(col => { + const val = row[col]; + if (val == null) return ''; + if (typeof val === 'string' && val.includes(',')) return `"${val}"`; + return val; + }).join(',') + ); + + return header + '\n' + lines.join('\n'); +} + +module.exports = { querySessionData, formatCSV }; diff --git a/realtime/src/helper/influxWriter.js b/realtime/src/helper/influxWriter.js new file mode 100644 index 0000000..a666ebd --- /dev/null +++ b/realtime/src/helper/influxWriter.js @@ -0,0 +1,156 @@ +const { InfluxDB, Point } = require('@influxdata/influxdb-client'); + +const url = process.env.INFLX_URL; +const token = process.env.INFLX_TOKEN; +const org = process.env.INFLX_ORG; +const boatTelemetry = 'boat'; + +const client = new InfluxDB({ url, token }); +const writeApi = client.getWriteApi(org, boatTelemetry); + +const FIELD_MAP = { + logs: { + lat: 'latitude', lon: 'longitude', hdg: 'heading', + sog: 'speed_over_ground', cog: 'course_over_ground', + depth: 'depth', engTemp: 'engine_temperature', + fTemp: 'forecast_temperature', fHum: 'forecast_humidity', fPres: 'forecast_pressure', + fWSpd: 'forecast_wind_speed', fWDir: 'forecast_wind_direction', + wvH: 'wave_height', wvP: 'wave_period', wvD: 'wave_direction', + curD: 'current_direction', curV: 'current_velocity' + }, + weather: { + temp: 'temperature', hum: 'humidity', pres: 'pressure', + wSpd: 'wind_speed', wDir: 'wind_direction', gust: 'wind_gusts', + rain: 'rain', prec: 'precipitation', + wvH: 'wave_height', wvP: 'wave_period', wvD: 'wave_direction', + wvPkP: 'wave_peak_period', curD: 'current_direction', curV: 'current_velocity' + }, + forecast: { + temp: 'temperature', hum: 'humidity', pres: 'pressure', + wSpd: 'wind_speed', wDir: 'wind_direction', + precProb: 'precipitation_probability', prec: 'precipitation', + rain: 'rain', cloud: 'cloud_cover', + wvH: 'wave_height', wvP: 'wave_period', wvD: 'wave_direction', + curD: 'current_direction', curV: 'current_velocity' + } +}; + +async function writePoint(sensorId, timestamp, measurement, fields) { + try { + const map = FIELD_MAP[measurement] || {}; + const point = new Point(measurement) + .tag('sensor', sensorId) + .timestamp(new Date(timestamp)); + + for (const [key, value] of Object.entries(fields)) { + if (value == null) continue; + const fieldName = map[key] || key; + + if (typeof value === 'number') { + point.floatField(fieldName, value); + } else if (typeof value === 'string') { + point.stringField(fieldName, value); + } else if (typeof value === 'boolean') { + point.booleanField(fieldName, value); + } + } + + writeApi.writePoint(point); + await writeApi.flush(); + } catch (error) { + console.error(`[INFLUX] Errore writePoint (${measurement}):`, error.message); + } +} + +async function writeForecastBatch(sensorId, points) { + try { + const map = FIELD_MAP.forecast || {}; + + for (const [ts, fields] of points) { + const point = new Point('forecast') + .tag('sensor', sensorId) + .timestamp(new Date(ts)); + + for (const [key, value] of Object.entries(fields)) { + if (value == null) continue; + const fieldName = map[key] || key; + + if (typeof value === 'number') { + point.floatField(fieldName, value); + } else if (typeof value === 'string') { + point.stringField(fieldName, value); + } + } + writeApi.writePoint(point); + } + + await writeApi.flush(); + console.log(`[INFLUX] Scritti ${points.length} punti forecast per sensore ${sensorId}`); + } catch (error) { + console.error(`[INFLUX] Errore writeForecastBatch:`, error.message); + } +} + +// --- Batch buffer per watchers --- +const BATCH_SIZE = 10; +const batchBuffers = new Map(); // sensorId → [{timestamp, measurement, fields}, ...] + +function bufferPoint(sensorId, timestamp, measurement, fields) { + if (!batchBuffers.has(sensorId)) { + batchBuffers.set(sensorId, []); + } + const buffer = batchBuffers.get(sensorId); + buffer.push({ timestamp, measurement, fields }); + + if (buffer.length >= BATCH_SIZE) { + const batch = buffer.splice(0, BATCH_SIZE); + writeBatch(sensorId, batch); + } +} + +async function writeBatch(sensorId, batch) { + try { + for (const { timestamp, measurement, fields } of batch) { + const map = FIELD_MAP[measurement] || {}; + const point = new Point(measurement) + .tag('sensor', sensorId) + .timestamp(new Date(timestamp)); + + for (const [key, value] of Object.entries(fields)) { + if (value == null) continue; + const fieldName = map[key] || key; + if (typeof value === 'number') { + point.floatField(fieldName, value); + } else if (typeof value === 'string') { + point.stringField(fieldName, value); + } else if (typeof value === 'boolean') { + point.booleanField(fieldName, value); + } + } + writeApi.writePoint(point); + } + await writeApi.flush(); + console.log(`[INFLUX] Batch scritto: ${batch.length} punti per sensore ${sensorId}`); + } catch (error) { + console.error(`[INFLUX] Errore writeBatch:`, error.message); + } +} + +async function flushBuffer(sensorId) { + const buffer = batchBuffers.get(sensorId); + if (!buffer || buffer.length === 0) return []; + + const remaining = buffer.splice(0); + await writeBatch(sensorId, remaining); + return remaining; +} + +function getBufferedPoints(sensorId) { + return batchBuffers.get(sensorId) || []; +} + +function clearBuffer(sensorId) { + batchBuffers.delete(sensorId); +} + +module.exports = { writePoint, writeForecastBatch, bufferPoint, flushBuffer, getBufferedPoints, clearBuffer, FIELD_MAP }; diff --git a/realtime/src/helper/redis.js b/realtime/src/helper/redis.js new file mode 100644 index 0000000..80fecbe --- /dev/null +++ b/realtime/src/helper/redis.js @@ -0,0 +1,79 @@ +const Redis = require('ioredis'); + +const redis = new Redis({ + host: process.env.REDIS_HOST, + port: process.env.REDIS_PORT +}); + +// Client dedicato per subscribe (ioredis richiede client separato) +const redisSub = new Redis({ + host: process.env.REDIS_HOST, + port: process.env.REDIS_PORT +}); + +redis.on('error', (error) => { + console.error('Redis error:', error); +}); + +redis.on('connect', () => { + console.log('Server connected to Redis DB'); +}); + +redisSub.on('error', (error) => { + console.error('Redis sub error:', error); +}); + +const sensors_hash_map = 'sensors:sessions'; + +async function setSession(sensorID, metadata) { + await redis.hset(sensors_hash_map, sensorID, JSON.stringify(metadata)); +} + +async function getSession(sensorID) { + return await redis.hget(sensors_hash_map, sensorID); +} + +async function deleteSession(sensorID) { + await redis.hdel(sensors_hash_map, sensorID); +} + +async function getSessions() { + return await redis.hgetall(sensors_hash_map); +} + +// --- Pub/Sub per live watchers --- + +async function publishSensorData(sensorId, data) { + await redis.publish(`sensor:data:${sensorId}`, JSON.stringify(data)); +} + +async function addWatcher(sensorId) { + return await redis.incr(`sensor:watchers:${sensorId}`); +} + +async function removeWatcher(sensorId) { + const count = await redis.decr(`sensor:watchers:${sensorId}`); + if (count <= 0) { + await redis.del(`sensor:watchers:${sensorId}`); + return 0; + } + return count; +} + +async function getWatcherCount(sensorId) { + const count = await redis.get(`sensor:watchers:${sensorId}`); + return parseInt(count) || 0; +} + +module.exports = { + setSession, + getSession, + deleteSession, + getSessions, + publishSensorData, + addWatcher, + removeWatcher, + getWatcherCount, + redis, + redisSub +}; \ No newline at end of file diff --git a/realtime/src/helper/tokenStore.js b/realtime/src/helper/tokenStore.js new file mode 100644 index 0000000..c987cc6 --- /dev/null +++ b/realtime/src/helper/tokenStore.js @@ -0,0 +1,52 @@ +const { redis } = require('./redis'); +const { generateShortId } = require('./cryptoUtils'); + +const TOKEN_PREFIX = 'token:pending:'; + +/** + * Genera un nuovo token effimero valido per i prossimi 5 secondi. + */ +async function setToken(sensorId, metadata = {}, duration = 5) { + const token = generateShortId(8); + const key = `${TOKEN_PREFIX}${token}`; + + const payload = JSON.stringify({ + sensorId, + metadata, + createdAt: Date.now() + }); + + await redis.set(key, payload, 'EX', duration); + + return token; +} + +/** + * Consuma (valida e rimuove) un token. + * @returns {Object|null} - I dati della sessione se valida, altrimenti null + */ +async function consumeToken(token) { + const key = `${TOKEN_PREFIX}${token}`; + + // Recupera il token + const rawData = await redis.get(key); + if (!rawData) { + return null; + } + + // Il token è monouso: lo cancelliamo subito dopo la lettura + await redis.del(key); + + try { + const data = JSON.parse(rawData); + return data; // Ritorna l'intero oggetto (sensorId, metadata, ecc.) + } catch (e) { + console.error('Error parsing token data:', e); + return null; + } +} + +module.exports = { + setToken, + consumeToken +}; diff --git a/realtime/src/index.js b/realtime/src/index.js new file mode 100644 index 0000000..f606bd0 --- /dev/null +++ b/realtime/src/index.js @@ -0,0 +1,149 @@ +const express = require('express'); +const WebSocket = require('ws'); +const Redis = require('ioredis'); +const redisHelper = require('./helper/redis'); +const influxWriter = require('./helper/influxWriter'); +const influxReader = require('./helper/influxReader'); + +const app = express(); +app.use(express.json()); +app.use((req, res, next) => { + res.header('Access-Control-Allow-Origin', req.headers.origin || '*'); + res.header('Access-Control-Allow-Credentials', 'true'); + res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization'); + res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); + if (req.method === 'OPTIONS') return res.sendStatus(204); + next(); +}); + +require('./socket'); + +app.get('/', (req, res) => { + res.redirect('/health'); +}); + +app.get('/health', (req, res) => { + res.status(200).send({ + status: 'OK', + service: 'realtime', + version: process.env.VERSION, + build: process.env.VERSION_BUILD, + state: process.env.VERSION_STATE, + port: process.env.PORT + }); +}); + +app.use('/sensors', require('./routes/sensors')); +app.use('/connect', require('./routes/connect')); +app.use('/sessions', require('./routes/sessions')); + +// --- Flush buffer e CSV export --- + +app.post('/sessions/:sensorId/flush', async (req, res) => { + try { + const { sensorId } = req.params; + const flushed = await influxWriter.flushBuffer(sensorId); + res.status(200).json({ flushed: flushed.length }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +app.get('/sessions/:sensorId/csv', async (req, res) => { + try { + const { sensorId } = req.params; + const { from } = req.query; + if (!from) return res.status(400).json({ error: 'from timestamp required' }); + + // Flusha prima il buffer residuo + await influxWriter.flushBuffer(sensorId); + + const rows = await influxReader.querySessionData(sensorId, parseInt(from)); + const csv = influxReader.formatCSV(rows); + + res.setHeader('Content-Type', 'text/csv'); + res.setHeader('Content-Disposition', `attachment; filename="session_${sensorId}.csv"`); + res.send(csv); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// --- HTTP server + WebSocket per watchers live --- + +const server = app.listen(process.env.PORT, () => { + console.log(`Realtime on port ${process.env.PORT}`); +}); + +const wss = new WebSocket.Server({ server, path: '/live' }); + +wss.on('connection', (client) => { + let watchedSensor = null; + let subscriber = null; + + client.on('message', async (raw) => { + try { + const msg = JSON.parse(raw); + + if (msg.action === 'watch' && msg.sensorId) { + // Rimuovi watch precedente se esiste + if (watchedSensor) { + await redisHelper.removeWatcher(watchedSensor); + if (subscriber) { + subscriber.unsubscribe(); + subscriber.quit(); + subscriber = null; + } + } + + watchedSensor = msg.sensorId; + await redisHelper.addWatcher(watchedSensor); + + // Subscriber Redis dedicato per questo client + subscriber = new Redis({ + host: process.env.REDIS_HOST, + port: process.env.REDIS_PORT + }); + + subscriber.subscribe(`sensor:data:${watchedSensor}`); + subscriber.on('message', (channel, message) => { + if (client.readyState === WebSocket.OPEN) { + client.send(message); // Dati gia' JSON + } + }); + + client.send(JSON.stringify({ type: 'watching', sensorId: watchedSensor })); + } + + if (msg.action === 'unwatch') { + if (watchedSensor) { + await redisHelper.removeWatcher(watchedSensor); + watchedSensor = null; + } + if (subscriber) { + subscriber.unsubscribe(); + subscriber.quit(); + subscriber = null; + } + client.send(JSON.stringify({ type: 'unwatched' })); + } + + } catch (err) { + console.error('[WS-LIVE] Message error:', err); + } + }); + + client.on('close', async () => { + if (watchedSensor) { + await redisHelper.removeWatcher(watchedSensor); + } + if (subscriber) { + subscriber.unsubscribe(); + subscriber.quit(); + } + }); + + client.on('error', (err) => { + console.error('[WS-LIVE] Client error:', err); + }); +}); \ No newline at end of file diff --git a/realtime/src/routes/connect.js b/realtime/src/routes/connect.js new file mode 100644 index 0000000..497e8b5 --- /dev/null +++ b/realtime/src/routes/connect.js @@ -0,0 +1,74 @@ +const express = require('express'); +const db = require('../helper/authdb'); +const tokenStore = require('../helper/tokenStore'); +const redis = require('../helper/redis'); +const router = express.Router(); + +/** + * POST /connect + * Il sensore invia il suo codice segreto (token) e metadati opzionali. + * Se autentica, riceve un token effimero per la connessione WebSocket. + */ +router.post('/', async (req, res) => { + try { + const { token, metadata } = req.body; + + if (!token) { + return res.status(400).send({ error: 'Token is required' }); + } + + const sensor = await db.getSensor(token); + + if (!sensor) { + return res.status(401).send({ error: 'token not valid' }); + } + if (!sensor.is_active) { + return res.status(403).send({ error: 'token not valid' }); + } + + // Genera il token effimero valido per max 5 secondi + const socketToken = await tokenStore.setToken(sensor.id, metadata, 5); + + return res.status(200).send({ + socketToken, + sensorId: sensor.id, + expiresIn: 5 + }); + } catch (error) { + return res.status(500).send({ error: `${error}` }); + } +}); + +/** + * DELETE /connect/:sensorId + * Disconnette forzatamente un sensore rimuovendo la sua sessione da Redis. + */ +router.delete('/:sensorId', async (req, res) => { + const { sensorId } = req.params; + + try { + await redis.deleteSession(sensorId); + return res.status(200).send({ result: 'disconnected' }); + } catch (error) { + return res.status(500).send({ error: `${error}` }); + } +}); + +/** + * POST /connect/new + * Crea un nuovo sensore nel database. + */ +router.post('/new', async (req, res) => { + const { name, code } = req.body; + if (!name || !code) { + return res.status(400).send({ error: 'Name and code are required' }); + } + try { + await db.createSensor(name, code); + return res.status(200).send({ result: 'created' }); + } catch (error) { + return res.status(500).send({ error: `${error}` }); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/realtime/src/routes/sensors.js b/realtime/src/routes/sensors.js new file mode 100644 index 0000000..730cb3d --- /dev/null +++ b/realtime/src/routes/sensors.js @@ -0,0 +1,36 @@ +const express = require('express'); +const db = require('../helper/authdb'); + +router = express.Router(); + +router.get('/', async (req, res) => { + const sensors = await db.getSensors(); + res.status(200).json(sensors); +}); + +router.post('/:id/:activity', async (req, res) => { + const { id, activity } = req.params; + + let isActive; + if (activity === 'active') { + isActive = true; + } else if (activity === 'inactive') { + isActive = false; + } else { + return res.status(400).json({ error: 'Invalid activity' }); + } + + try { + const exists = await db.sensorsExists(id); + if (!exists) { + return res.status(404).json({ error: `Sensor with id ${id} not found` }); + } + await db.setSensorActivity(id, isActive); + res.status(200).json({ status: `Sensor ${activity}` }); + } catch (error) { + console.error('Error updating sensor ID:', id, error); + res.status(500).json({ error: 'Database error' }); + } +}) + +module.exports = router \ No newline at end of file diff --git a/realtime/src/routes/sessions.js b/realtime/src/routes/sessions.js new file mode 100644 index 0000000..9a53626 --- /dev/null +++ b/realtime/src/routes/sessions.js @@ -0,0 +1,36 @@ +const express = require('express'); +const redis = require('../helper/redis'); + +const router = express.Router(); + +/** + * GET /sessions + * Ritorna tutti i sensori attualmente connessi con i loro metadati. + * Se viene passato un parametro ?sensor=ID, restituisce solo quello. + */ +router.get('/', async (req, res) => { + const { sensor } = req.query; + + // Se viene passato un parametro ?sensor=ID, restituiamo solo quello + if (sensor) { + try { + const session = await redis.getSession(sensor); + if (!session) { + return res.status(404).json({ error: 'Session not found' }); + } + return res.status(200).json(JSON.parse(session)); + } catch (error) { + return res.status(500).json({ error: `${error}` }); + } + } + + // Altrimenti restituiamo tutta la lista + try { + const sessions = await redis.getSessions(); + res.status(200).json(sessions); + } catch (error) { + res.status(500).json({ error: `${error}` }); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/realtime/src/socket.js b/realtime/src/socket.js new file mode 100644 index 0000000..0c29a19 --- /dev/null +++ b/realtime/src/socket.js @@ -0,0 +1,110 @@ +const WebSocket = require('ws'); +const { encode, decode } = require('@msgpack/msgpack'); +const url = require('url'); +const tokenStore = require('./helper/tokenStore'); +const redisHelper = require('./helper/redis'); +const influxWriter = require('./helper/influxWriter'); + +const ws = new WebSocket.Server({ + port: process.env.SOCKET_PORT, + perMessageDeflate: false, + verifyClient: async (info, callback) => { + const { query } = url.parse(info.req.url, true); + const token = query.token; + + if (!token) { + return callback(false, 401, 'token not passed'); + } + + try { + const sessionData = await tokenStore.consumeToken(token); + if (!sessionData) { + return callback(false, 401, 'token not valid or expired'); + } + + info.req.sensorSession = sessionData; + callback(true); + } catch (error) { + callback(false, 500, `internal server error: ${error}`); + } + } +}); + +ws.on('connection', async (client, req) => { + const session = req.sensorSession; + const sensorId = session.sensorId; + + client.sensorId = sensorId; + + // Registra la sessione su Redis + try { + await redisHelper.setSession(sensorId, { + ...session.metadata, + connectedAt: Math.floor(Date.now() / 1000) + }); + } catch (err) { + console.error(`[WS] Redis setSession error for ${sensorId}:`, err); + } + + /** + * Gestione messaggi in arrivo dal sensore. + * + * Il sensore invia dati codificati in MessagePack (binario). + * Il formato atteso è un array compatto (per risparmiare spazio): + * + * [timestamp, measurement, { field1: value1, field2: value2, ... }] + * + * Esempio pratico (dati meteo da barca): + * [1710681000, "weather", { t: 22.5, h: 65, p: 1013.2, w: 12.3 }] + * + * Dove le chiavi abbreviate sono: + * t = temperature, h = humidity, p = pressure, w = windSpeed ... + * + * Il server decodifica il messaggio e prepara il punto per InfluxDB. + */ + client.on('message', async (raw) => { + try { + const msg = decode(raw); + + // Rispondi con ACK minimale in msgpack: { a: 1 } = acknowledged + client.send(encode({ a: 1 })); + + const [timestamp, measurement, fields] = msg; + + // Pubblica su Redis per i watchers live + redisHelper.publishSensorData(sensorId, { timestamp, measurement, fields }); + + // Controlla se ci sono watchers attivi + const watchers = await redisHelper.getWatcherCount(sensorId); + + if (measurement === 'forecast_batch') { + influxWriter.writeForecastBatch(sensorId, fields); + } else if (watchers > 0) { + // Con watchers: accumula nel buffer, flusha ogni 10 punti + influxWriter.bufferPoint(sensorId, timestamp, measurement, fields); + } else { + // Senza watchers: scrivi immediatamente (comportamento originale) + influxWriter.writePoint(sensorId, timestamp, measurement, fields); + } + + } catch (err) { + console.error(`[WS|${sensorId}] decode error:`, err); + // Rispondi con errore in msgpack: { e: 1 } = error + client.send(encode({ e: 1 })); + } + }); + + client.on('error', (err) => { + console.error(`[WS|${sensorId}] error:`, err); + }); + + client.on('close', async () => { + try { + await redisHelper.deleteSession(sensorId); + } catch (err) { + console.error(`[WS] Redis deleteSession error for ${sensorId}:`, err); + } + }); +}); + +console.log(`[WS] Realtime websocket server on port: ${process.env.SOCKET_PORT}`); \ No newline at end of file