From 47faa41eb9718b714c5ea55b0ab3d9161ee0d2a1 Mon Sep 17 00:00:00 2001 From: Giuseppe Raffa <77052701+sesee3@users.noreply.github.com> Date: Mon, 25 May 2026 23:14:50 +0200 Subject: [PATCH] Fixed some bugs in api and auth services, completed auth cores. --- api/Dockerfile | 9 +- api/package.json | 25 +- api/pnpm-lock.yaml | 298 +++++++++++++- api/src/core/cors.js | 16 + api/src/core/msgpackcore.js | 37 ++ api/src/core/rulesetscore.js | 111 +++++ api/src/data/db.js | 25 ++ api/src/data/influx.js | 26 ++ api/src/data/redis.js | 9 + api/src/index.js | 10 +- api/src/middlewares/internalware.js | 20 + api/src/middlewares/userware.js | 47 +++ api/src/routes/sensors.js | 38 ++ auth/Dockerfile | 2 +- auth/package.json | 2 +- auth/src/core/jwt.js | 9 +- auth/src/data/redis.js | 2 +- auth/src/index.js | 45 ++- auth/src/middleware/auth.js | 35 -- auth/src/middlewares/internalware.js | 9 + auth/src/middlewares/userware.js | 40 ++ auth/src/pages/login.html | 103 ++++- auth/src/pages/profile.html | 249 ++++++++++++ auth/src/routes/auth.js | 35 +- auth/src/routes/pages.js | 45 +++ auth/src/routes/sessions.js | 27 ++ auth/src/routes/users.js | 31 ++ auth/src/static/fonts/elmssans.ttf | Bin 0 -> 188136 bytes auth/src/static/styles/style.css | 585 +++++++++++++++++++++++++++ console/package.json | 12 + console/src/index.js | 1 + stream/Dockerfile | 8 + stream/package.json | 24 ++ stream/src/core/securitycore.js | 28 ++ stream/src/core/sessioncore.js | 26 ++ stream/src/data/db.js | 32 ++ stream/src/data/influx.js | 32 ++ stream/src/data/redis.js | 18 + stream/src/routes/connect.js | 44 ++ stream/src/ws/connection.js | 3 + stream/src/ws/upgrade.js | 44 ++ 41 files changed, 2061 insertions(+), 101 deletions(-) create mode 100644 api/src/core/cors.js create mode 100644 api/src/core/msgpackcore.js create mode 100644 api/src/core/rulesetscore.js create mode 100644 api/src/data/db.js create mode 100644 api/src/data/influx.js create mode 100644 api/src/data/redis.js create mode 100644 api/src/middlewares/internalware.js create mode 100644 api/src/middlewares/userware.js create mode 100644 api/src/routes/sensors.js delete mode 100644 auth/src/middleware/auth.js create mode 100644 auth/src/middlewares/internalware.js create mode 100644 auth/src/middlewares/userware.js create mode 100644 auth/src/pages/profile.html create mode 100644 auth/src/routes/pages.js create mode 100644 auth/src/routes/sessions.js create mode 100644 auth/src/routes/users.js create mode 100644 auth/src/static/fonts/elmssans.ttf create mode 100644 auth/src/static/styles/style.css create mode 100644 console/package.json create mode 100644 console/src/index.js create mode 100644 stream/Dockerfile create mode 100644 stream/package.json create mode 100644 stream/src/core/securitycore.js create mode 100644 stream/src/core/sessioncore.js create mode 100644 stream/src/data/db.js create mode 100644 stream/src/data/influx.js create mode 100644 stream/src/data/redis.js create mode 100644 stream/src/routes/connect.js create mode 100644 stream/src/ws/connection.js create mode 100644 stream/src/ws/upgrade.js diff --git a/api/Dockerfile b/api/Dockerfile index 0fafd47..4ccd361 100644 --- a/api/Dockerfile +++ b/api/Dockerfile @@ -1,11 +1,8 @@ -FROM node:20-slim -RUN corepack enable && corepack prepare pnpm@latest - +FROM node:20-alpine WORKDIR /app - +RUN corepack enable && corepack prepare pnpm@9.15.0 --activate COPY package.json pnpm-lock.yaml ./ RUN pnpm install --frozen-lockfile - COPY . . EXPOSE 3000 -CMD ["node", "src/index.js"] \ No newline at end of file +CMD ["pnpm", "exec", "nodemon", "src/index.js"] \ No newline at end of file diff --git a/api/package.json b/api/package.json index 6f63e92..f37f457 100644 --- a/api/package.json +++ b/api/package.json @@ -1,18 +1,25 @@ { "name": "api", + "type": "module", "version": "1.0.0", - "description": "", - "main": "index.js", + "private": true, + "packageManager": "pnpm@9.15.0", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "dev": "nodemon src/index.js", + "start": "node src/index.js" }, - "keywords": [], - "author": "", - "license": "ISC", - "packageManager": "pnpm@11.1.3", "dependencies": { + "@influxdata/influxdb-client": "^1.35.0", + "@msgpack/msgpack": "^3.1.2", + "cookie-parser": "^1.4.7", + "cors": "^2.8.5", "express": "^5.2.1", + "helmet": "^8.1.0", "ioredis": "^5.10.1", - "pg": "^8.21.0" + "pg": "^8.21.0", + "zod": "^4.4.3" + }, + "devDependencies": { + "nodemon": "^3.1.14" } -} +} \ No newline at end of file diff --git a/api/pnpm-lock.yaml b/api/pnpm-lock.yaml index 0419ab9..769e5cb 100644 --- a/api/pnpm-lock.yaml +++ b/api/pnpm-lock.yaml @@ -8,29 +8,78 @@ importers: .: dependencies: + '@influxdata/influxdb-client': + specifier: ^1.35.0 + version: 1.35.0 + '@msgpack/msgpack': + specifier: ^3.1.2 + version: 3.1.3 + cookie-parser: + specifier: ^1.4.7 + version: 1.4.7 + cors: + specifier: ^2.8.5 + version: 2.8.6 express: specifier: ^5.2.1 version: 5.2.1 + helmet: + specifier: ^8.1.0 + version: 8.1.0 ioredis: specifier: ^5.10.1 version: 5.10.1 pg: specifier: ^8.21.0 version: 8.21.0 + zod: + specifier: ^4.4.3 + version: 4.4.3 + devDependencies: + nodemon: + specifier: ^3.1.14 + version: 3.1.14 packages: + '@influxdata/influxdb-client@1.35.0': + resolution: {integrity: sha512-woWMi8PDpPQpvTsRaUw4Ig+nOGS/CWwAwS66Fa1Vr/EkW+NEwxI8YfPBsdBMn33jK2Y86/qMiiuX/ROHIkJLTw==} + '@ioredis/commands@1.5.1': resolution: {integrity: sha512-JH8ZL/ywcJyR9MmJ5BNqZllXNZQqQbnVZOqpPQqE1vHiFgAw4NHbvE0FOduNU8IX9babitBT46571OnPTT0Zcw==} + '@msgpack/msgpack@3.1.3': + resolution: {integrity: sha512-47XIizs9XZXvuJgoaJUIE2lFoID8ugvc0jzSHP+Ptfk8nTbnR8g788wv48N03Kx0UkAv559HWRQ3yzOgzlRNUA==} + engines: {node: '>= 18'} + accepts@2.0.0: resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} engines: {node: '>= 0.6'} + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + body-parser@2.2.2: resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} engines: {node: '>=18'} + brace-expansion@5.0.6: + resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==} + engines: {node: 18 || 20 || >=22} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + bytes@3.1.2: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} @@ -43,6 +92,10 @@ packages: resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} engines: {node: '>= 0.4'} + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + cluster-key-slot@1.1.2: resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==} engines: {node: '>=0.10.0'} @@ -59,6 +112,13 @@ packages: resolution: {integrity: sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==} engines: {node: '>=18'} + cookie-parser@1.4.7: + resolution: {integrity: sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==} + engines: {node: '>= 0.8.0'} + + cookie-signature@1.0.6: + resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} + cookie-signature@1.2.2: resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} engines: {node: '>=6.6.0'} @@ -67,6 +127,10 @@ packages: resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} engines: {node: '>= 0.6'} + cors@2.8.6: + resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} + engines: {node: '>= 0.10'} + debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -118,6 +182,10 @@ packages: resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} engines: {node: '>= 18'} + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + finalhandler@2.1.1: resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} engines: {node: '>= 18.0.0'} @@ -130,6 +198,11 @@ packages: resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} engines: {node: '>= 0.8'} + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} @@ -141,10 +214,18 @@ packages: resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} engines: {node: '>= 0.4'} + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} + has-flag@3.0.0: + resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} + engines: {node: '>=4'} + has-symbols@1.1.0: resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} engines: {node: '>= 0.4'} @@ -153,6 +234,10 @@ packages: resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==} engines: {node: '>= 0.4'} + helmet@8.1.0: + resolution: {integrity: sha512-jOiHyAZsmnr8LqoPGmCjYAaiuWwjAPLgY8ZX2XrmHawt99/u1y6RgrZMTeoPfpUbV96HOalYgz1qzkRbw54Pmg==} + engines: {node: '>=18.0.0'} + http-errors@2.0.1: resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} engines: {node: '>= 0.8'} @@ -161,6 +246,9 @@ packages: resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} engines: {node: '>=0.10.0'} + ignore-by-default@1.0.1: + resolution: {integrity: sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==} + inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} @@ -172,6 +260,22 @@ packages: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + is-promise@4.0.0: resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} @@ -201,6 +305,10 @@ packages: resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} engines: {node: '>=18'} + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + engines: {node: 18 || 20 || >=22} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -208,6 +316,19 @@ packages: resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} engines: {node: '>= 0.6'} + nodemon@3.1.14: + resolution: {integrity: sha512-jakjZi93UtB3jHMWsXL68FXSAosbLfY0In5gtKq3niLSkrWznrVBzXFNOEMJUfc9+Ke7SHWoAZsiMkNP3vq6Jw==} + engines: {node: '>=10'} + hasBin: true + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + object-inspect@1.13.4: resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} engines: {node: '>= 0.4'} @@ -260,6 +381,10 @@ packages: pgpass@1.0.5: resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==} + picomatch@2.3.2: + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} + engines: {node: '>=8.6'} + postgres-array@2.0.0: resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} engines: {node: '>=4'} @@ -280,6 +405,9 @@ packages: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} + pstree.remy@1.1.8: + resolution: {integrity: sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==} + qs@6.15.2: resolution: {integrity: sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==} engines: {node: '>=0.6'} @@ -292,6 +420,10 @@ packages: resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} engines: {node: '>= 0.10'} + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + redis-errors@1.2.0: resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} engines: {node: '>=4'} @@ -307,6 +439,11 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + semver@7.8.0: + resolution: {integrity: sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==} + engines: {node: '>=10'} + hasBin: true + send@1.2.1: resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} engines: {node: '>= 18'} @@ -334,6 +471,10 @@ packages: resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} engines: {node: '>= 0.4'} + simple-update-notifier@2.0.0: + resolution: {integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==} + engines: {node: '>=10'} + split2@4.2.0: resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} engines: {node: '>= 10.x'} @@ -345,14 +486,29 @@ packages: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} + supports-color@5.5.0: + resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} + engines: {node: '>=4'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + toidentifier@1.0.1: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} + touch@3.1.1: + resolution: {integrity: sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==} + hasBin: true + type-is@2.1.0: resolution: {integrity: sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==} engines: {node: '>= 18'} + undefsafe@2.0.5: + resolution: {integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==} + unpipe@1.0.0: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} engines: {node: '>= 0.8'} @@ -368,20 +524,36 @@ packages: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} + zod@4.4.3: + resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} + snapshots: + '@influxdata/influxdb-client@1.35.0': {} + '@ioredis/commands@1.5.1': {} + '@msgpack/msgpack@3.1.3': {} + accepts@2.0.0: dependencies: mime-types: 3.0.2 negotiator: 1.0.0 + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.2 + + balanced-match@4.0.4: {} + + binary-extensions@2.3.0: {} + body-parser@2.2.2: dependencies: bytes: 3.1.2 content-type: 1.0.5 - debug: 4.4.3 + debug: 4.4.3(supports-color@5.5.0) http-errors: 2.0.1 iconv-lite: 0.7.2 on-finished: 2.4.1 @@ -391,6 +563,14 @@ snapshots: transitivePeerDependencies: - supports-color + brace-expansion@5.0.6: + dependencies: + balanced-match: 4.0.4 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + bytes@3.1.2: {} call-bind-apply-helpers@1.0.2: @@ -403,6 +583,18 @@ snapshots: call-bind-apply-helpers: 1.0.2 get-intrinsic: 1.3.0 + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + cluster-key-slot@1.1.2: {} content-disposition@1.1.0: {} @@ -411,13 +603,27 @@ snapshots: content-type@2.0.0: {} + cookie-parser@1.4.7: + dependencies: + cookie: 0.7.2 + cookie-signature: 1.0.6 + + cookie-signature@1.0.6: {} + cookie-signature@1.2.2: {} cookie@0.7.2: {} - debug@4.4.3: + cors@2.8.6: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + + debug@4.4.3(supports-color@5.5.0): dependencies: ms: 2.1.3 + optionalDependencies: + supports-color: 5.5.0 denque@2.1.0: {} @@ -453,7 +659,7 @@ snapshots: content-type: 1.0.5 cookie: 0.7.2 cookie-signature: 1.2.2 - debug: 4.4.3 + debug: 4.4.3(supports-color@5.5.0) depd: 2.0.0 encodeurl: 2.0.0 escape-html: 1.0.3 @@ -478,9 +684,13 @@ snapshots: transitivePeerDependencies: - supports-color + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + finalhandler@2.1.1: dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@5.5.0) encodeurl: 2.0.0 escape-html: 1.0.3 on-finished: 2.4.1 @@ -493,6 +703,9 @@ snapshots: fresh@2.0.0: {} + fsevents@2.3.3: + optional: true + function-bind@1.1.2: {} get-intrinsic@1.3.0: @@ -513,14 +726,22 @@ snapshots: dunder-proto: 1.0.1 es-object-atoms: 1.1.1 + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + gopd@1.2.0: {} + has-flag@3.0.0: {} + has-symbols@1.1.0: {} hasown@2.0.3: dependencies: function-bind: 1.1.2 + helmet@8.1.0: {} + http-errors@2.0.1: dependencies: depd: 2.0.0 @@ -533,13 +754,15 @@ snapshots: dependencies: safer-buffer: 2.1.2 + ignore-by-default@1.0.1: {} + inherits@2.0.4: {} ioredis@5.10.1: dependencies: '@ioredis/commands': 1.5.1 cluster-key-slot: 1.1.2 - debug: 4.4.3 + debug: 4.4.3(supports-color@5.5.0) denque: 2.1.0 lodash.defaults: 4.2.0 lodash.isarguments: 3.1.0 @@ -551,6 +774,18 @@ snapshots: ipaddr.js@1.9.1: {} + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + + is-extglob@2.1.1: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-number@7.0.0: {} + is-promise@4.0.0: {} lodash.defaults@4.2.0: {} @@ -569,10 +804,31 @@ snapshots: dependencies: mime-db: 1.54.0 + minimatch@10.2.5: + dependencies: + brace-expansion: 5.0.6 + ms@2.1.3: {} negotiator@1.0.0: {} + nodemon@3.1.14: + dependencies: + chokidar: 3.6.0 + debug: 4.4.3(supports-color@5.5.0) + ignore-by-default: 1.0.1 + minimatch: 10.2.5 + pstree.remy: 1.1.8 + semver: 7.8.0 + simple-update-notifier: 2.0.0 + supports-color: 5.5.0 + touch: 3.1.1 + undefsafe: 2.0.5 + + normalize-path@3.0.0: {} + + object-assign@4.1.1: {} + object-inspect@1.13.4: {} on-finished@2.4.1: @@ -622,6 +878,8 @@ snapshots: dependencies: split2: 4.2.0 + picomatch@2.3.2: {} + postgres-array@2.0.0: {} postgres-bytea@1.0.1: {} @@ -637,6 +895,8 @@ snapshots: forwarded: 0.2.0 ipaddr.js: 1.9.1 + pstree.remy@1.1.8: {} + qs@6.15.2: dependencies: side-channel: 1.1.0 @@ -650,6 +910,10 @@ snapshots: iconv-lite: 0.7.2 unpipe: 1.0.0 + readdirp@3.6.0: + dependencies: + picomatch: 2.3.2 + redis-errors@1.2.0: {} redis-parser@3.0.0: @@ -658,7 +922,7 @@ snapshots: router@2.2.0: dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@5.5.0) depd: 2.0.0 is-promise: 4.0.0 parseurl: 1.3.3 @@ -668,9 +932,11 @@ snapshots: safer-buffer@2.1.2: {} + semver@7.8.0: {} + send@1.2.1: dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@5.5.0) encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 @@ -723,20 +989,36 @@ snapshots: side-channel-map: 1.0.1 side-channel-weakmap: 1.0.2 + simple-update-notifier@2.0.0: + dependencies: + semver: 7.8.0 + split2@4.2.0: {} standard-as-callback@2.1.0: {} statuses@2.0.2: {} + supports-color@5.5.0: + dependencies: + has-flag: 3.0.0 + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + toidentifier@1.0.1: {} + touch@3.1.1: {} + type-is@2.1.0: dependencies: content-type: 2.0.0 media-typer: 1.1.0 mime-types: 3.0.2 + undefsafe@2.0.5: {} + unpipe@1.0.0: {} vary@1.1.2: {} @@ -744,3 +1026,5 @@ snapshots: wrappy@1.0.2: {} xtend@4.0.2: {} + + zod@4.4.3: {} diff --git a/api/src/core/cors.js b/api/src/core/cors.js new file mode 100644 index 0000000..0f258b1 --- /dev/null +++ b/api/src/core/cors.js @@ -0,0 +1,16 @@ +import cors from 'cors'; + +const dev = { + origin: true, + credentials: true, +}; + +const prod = { + origin: (process.env.ALLOWED_ORIGINS ?? '').split(','), + credentials: true, + methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], + allowedHeaders: ['Content-Type', 'X-Internal-Token'], +}; + +export const corsMiddleware = cors(process.env.NODE_ENV === 'production' ? prod : dev); + diff --git a/api/src/core/msgpackcore.js b/api/src/core/msgpackcore.js new file mode 100644 index 0000000..b86333c --- /dev/null +++ b/api/src/core/msgpackcore.js @@ -0,0 +1,37 @@ +import { encode, decode } from '@msgpack/msgpack'; + +/* + Un middleware da interporre in una richiesta HTTP che converte il corpo della richiesta in un buffer MSGPack compatto. +*/ +export function bodyToMSGPack() { + return (req, _res, next) => { + if (!req.is('application/msgpack')) return next(); + const chunks = []; + req.on('data', chunk => chunks.push(chunk)); + req.on('end', () => { + try { + req.body = decode(Buffer.concat(chunks)); + next(); + } catch (e) { + next(new Error('bad_msgpack_error')); + } + }) + } +} + +/* + Invia un oggetto come risposta in formato JSON o MSGPack, a seconda delle preferenze del client, specificate nelle intestazioni Accept. +*/ +export function send(req, res, obj, status = 200) { + if (req.accepts(['json', 'application/msgpack']) === 'application/msgpack') { + return sendAsMsgpack(res, obj, status); + } + res.status(status).json(obj); +} + +/* + Invia un oggetto come risposta in formato MSGPack. +*/ +export function sendAsMsgpack(res, obj, status = 200) { + res.status(status).type('application/msgpack').send(Buffer.from(encode(obj))); +} diff --git a/api/src/core/rulesetscore.js b/api/src/core/rulesetscore.js new file mode 100644 index 0000000..5b197b2 --- /dev/null +++ b/api/src/core/rulesetscore.js @@ -0,0 +1,111 @@ +import { rulesets } from "../data/db.js"; +import { redis } from "../data/redis.js"; + +const kinds = new Set([ + 'telemetry', + 'forecasts' +]); + +/* + Esegue un controllo sul tipo di ruleset ricevuto per verificare che sia valido e evitare SQL injection +*/ +function checkKind(kind) { + if (!kinds.has(kind)) throw new Error(`Invalid kind: ${kind}`); +} + + +/* + Restituisce le versioni di un ruleset. +*/ +export async function getVersions(kind) { + checkKind(kind); + const { rows } = await rulesets.query(`select id, version_major, version_minor, version_patch, is_active, created_at, deprecated_at from ${kind} order by version_major desc, version_minor desc, version_patch desc`) + return rows; +} + +/* + Restituisce le versioni attive di un ruleset. +*/ +export async function getActive(kind) { + checkKind(kind); + const { rows } = await rulesets.query(`select id, version_major, version_minor, version_patch, content, created_at from ${kind} where is_active = true limit 1`); + return rows[0] ?? null; +} + +/* + Restituisce una versione di un ruleset dato il suo ID. +*/ +export async function getByID(kind, id) { + checkKind(kind); + const { rows } = await rulesets.query(`select * from ${kind} where id = $1`, [id]); + return rows[0] ?? null; +} + +/* + Crea una nuova versione di un ruleset. + @param {string} kind - Il tipo di ruleset (telemetry o forecasts). + @param {Object} content - Il contenuto della nuova versione in formaot JSON. + @param {Object} version - La versione della nuova versione con formato: { major: 1, minor: 0, patch: 0 }. + @returns {Promise} - Risultato della query. +*/ +export async function newVersion(kind, content, version) { + checkKind(kind); + let { major, minor, patch } = version; + + if (!version) { + const { rows } = await rulesets.query(`select version_major, version_minor, version_patch from ${kind} order by version_major desc, version_minor desc, version_patch desc limit 1`); + if (rows[0]) { + major = rows[0].version_major; + minor = rows[0].version_minor; + patch = rows[0].version_patch + 1; + } else { + major = 1; + minor = 0; + patch = 0; + } + } + + const { rows } = await rulesets.query(`insert into ${kind} (version_major, version_minor, version_patch, content) values ($1, $2, $3, $4) returning *`, [major, minor, patch, content]); + return rows[0]; +} + +export async function activate(kind, id) { + checkKind(kind); + const client = await rulesets.connect(); + try { + await client.query('begin'); + await client.query(`update ${kind} set is_active = false where is_active = true`); + const r = await client.query(`update ${kind} set is_active = true where id = $1 and deprecated_at is null returning *`, [id]); + if (!r.rows[0]) { + await client.query('rollback'); + return null; + } + + await client.query('commit'); + await redis.publish(`ruleset:update:${kind}`, JSON.stringify({ id, active: true })); + return r.rows[0]; + } catch (error) { + await client.query('rollback'); + throw error; + } finally { + client.release(); + } +} + + +export async function deprecate(kind, id) { + checkKind(kind); + const client = await rulesets.connect(); + try { + await client.query('begin'); + const r = await client.query(`update ${kind} set deprecated_at = now(), is_active = case when is_active then false else is_active end where id = $1`, [id]); + await client.query('commit'); + await redis.publish(`ruleset:update:${kind}`, JSON.stringify({ id, deprecated: true })); + return r.rows[0]; + } catch (error) { + await client.query('rollback'); + throw error; + } finally { + client.release(); + } +} \ No newline at end of file diff --git a/api/src/data/db.js b/api/src/data/db.js new file mode 100644 index 0000000..d9963e9 --- /dev/null +++ b/api/src/data/db.js @@ -0,0 +1,25 @@ +import pg from 'pg'; + +const client = { + host: process.env.DB_HOST, + port: Number(process.env.DB_PORT), + user: process.env.DB_USER, + password: process.env.DB_PASSWORD, + max: 10, + idleTimeoutMillis: 30_000 +}; + +/* The sensors database client */ +const sensors = new pg.Pool({ ...client, database: 'sensors' }); + +/* The rulesets database client */ +const rulesets = new pg.Pool({ ...client, database: 'rulesets' }); + +/* The data database client */ +const data = new pg.Pool({ ...client, database: 'data' }); + +export async function query(from, text, params) { + return await from.query(text, params); +}; + +export { sensors, rulesets, data }; \ No newline at end of file diff --git a/api/src/data/influx.js b/api/src/data/influx.js new file mode 100644 index 0000000..a3f4250 --- /dev/null +++ b/api/src/data/influx.js @@ -0,0 +1,26 @@ +import { InfluxDB, Point } from '@influxdata/influxdb-client'; + +const url = process.env.INFLX_URL; +const token = process.env.INFLX_TOKEN; +const org = process.env.INFLX_ORG; + +const boatTelemetry = 'boat_telemetry'; + +const client = new InfluxDB({ url, token }); + +const writeApi = client.getWriteApi(org, boatTelemetry, 'ns', { + batchSize: 1, + flushInterval: 0, + maxRetries: 0, + maxBufferLines: 1, +}); + +/* + * Aggiunge un punto al database + */ +export async function write(point) { + writeApi.writePoint(point); + await writeApi.flush(true); +} + +export { Point }; \ No newline at end of file diff --git a/api/src/data/redis.js b/api/src/data/redis.js new file mode 100644 index 0000000..20847e4 --- /dev/null +++ b/api/src/data/redis.js @@ -0,0 +1,9 @@ +import Redis from 'ioredis'; + +const client = new Redis({ + host: process.env.REDIS_HOST, + port: Number(process.env.REDIS_PORT), + password: process.env.REDIS_PASSWORD, +}); + +export { client as redis }; \ No newline at end of file diff --git a/api/src/index.js b/api/src/index.js index ffe03ff..ea1b503 100644 --- a/api/src/index.js +++ b/api/src/index.js @@ -1,7 +1,13 @@ import express from 'express'; - const app = express(); +import { sensorsAPIs } from '../src/routes/sensors.js'; +app.use('/sensors', sensorsAPIs) + +app.get('/', (req, res) => { + res.redirect('/health') +}) + app.get('/health', (req, res) => { res.json({ service: "api", @@ -14,6 +20,6 @@ app.get('/health', (req, res) => { }); -app.listen(3000, '0.0.0.0', () => { +app.listen('3000', '0.0.0.0', () => { console.log('API started') }) diff --git a/api/src/middlewares/internalware.js b/api/src/middlewares/internalware.js new file mode 100644 index 0000000..ac54256 --- /dev/null +++ b/api/src/middlewares/internalware.js @@ -0,0 +1,20 @@ +const interalToken = process.env.INTERNAL_API_TOKEN; + +export function internalware(req, res, next) { + if (req.headers['x-internal-token'] === interalToken) { + req.internal = true; // La richiesta è interna + return next(); + } + return res.status(403).json({error: 'not-internal'}); +} + +export function userOrInternal(userware) { + return (req, res, next) => { + if (req.headers['x-internal-token'] === interalToken) { + req.internal = true; + return next(); + } + return userware(req, res, next); + }; +} + diff --git a/api/src/middlewares/userware.js b/api/src/middlewares/userware.js new file mode 100644 index 0000000..1f7d50f --- /dev/null +++ b/api/src/middlewares/userware.js @@ -0,0 +1,47 @@ +import { redis } from "../data/redis.js"; + +const authURL = process.env.AUTH_INTERNAL_URL; +const cookieName = process.env.COOKIE_NAME; +const cacheTTL = 30; + +function hashCookie(cookie) { + return crypto.createHash('sha256').update(cookie).digest('hex').slice(0, 32); +} + +export async function userware(req, res, next) { + const token = req.cookies?.[cookieName]; + if (!token) return res.status(401).json({ message: 'not authenticated' }) + + const cacheKey = `auth:cookie:${hashCookie(token)}` + const cached = await redis.get(cacheKey).catch( ()=> null); + if (cached) { + req.user = JSON.parse(cached) + return next(); + } + + try { + const r = await fetch(`${authURL}/api/users/me`, { + headers: { + cookie: `${cookieName}=${token}` + }, + }); + if (!r.ok) throw new Error('unauthorized'); + const user = await r.json(); + req.user = { + id: body.user.id, + sessionId: body.thisSession?.id + }; + await redis.set(cacheKey, JSON.stringify(req.user), 'EX', cacheTTL).catch(() => { }); + return next(); + + } catch (error) { + console.error('Userware Middleware: errore in auth:', error.message); + return res.status(503).json({ message: 'Error in auth service', error: error}) + } + +} + +//TODO: Da finire + +//TODO: Capire perche le versioni del package manager pnpm sono diverse tra i vari servizi +// TODO: Aggiungere 'private' ai package.json per rendere privati i pacchetti \ No newline at end of file diff --git a/api/src/routes/sensors.js b/api/src/routes/sensors.js new file mode 100644 index 0000000..9ac89c7 --- /dev/null +++ b/api/src/routes/sensors.js @@ -0,0 +1,38 @@ +import { Router } from "express"; +import crypto from "crypto"; +import { sensors } from "../data/db.js"; +import { redis } from "../data/redis.js"; +import { userware } from "../middlewares/userware.js"; +const router = Router(); + +//TODO: Add sensors routes + +router.get('/', async (req, res) => { + const { rows } = await sensors.query('select * from sensors') + res.json(rows) +}) + +/* + Restituisce tutti i sensori attivi e attualmente connessi da redis. +*/ +router.get('/actives', async (req, res) => { + const sensors = redis.scanStream({ match: 'sensor:online:*', count: 100 }); + const ids = [] + for await (const sensor of sensors) { + for (const key of sensor) ids.push(key.slice('sensor:online'.length)); + } + res.json({ + sensors: ids, + count: ids.length, + }) +}) + +router.get('/:id', async (req, res) => { + const { id } = req.params + const { rows } = await sensors.query('select * from sensors where id = $1', [id]) + res.json(rows[0]) +}) + + + +export { router as sensorsAPIs } \ No newline at end of file diff --git a/auth/Dockerfile b/auth/Dockerfile index acd23cc..4ccd361 100644 --- a/auth/Dockerfile +++ b/auth/Dockerfile @@ -1,6 +1,6 @@ FROM node:20-alpine WORKDIR /app -RUN corepack enable && corepack prepare pnpm@10.26.2 --activate +RUN corepack enable && corepack prepare pnpm@9.15.0 --activate COPY package.json pnpm-lock.yaml ./ RUN pnpm install --frozen-lockfile COPY . . diff --git a/auth/package.json b/auth/package.json index 4f29c51..bdcbab9 100644 --- a/auth/package.json +++ b/auth/package.json @@ -10,7 +10,7 @@ "author": "", "type": "module", "license": "ISC", - "packageManager": "pnpm@10.26.2", + "packageManager": "pnpm@9.15.0", "dependencies": { "bcrypt": "^6.0.0", "cookie-parser": "^1.4.7", diff --git a/auth/src/core/jwt.js b/auth/src/core/jwt.js index 882a84b..8e6dda9 100644 --- a/auth/src/core/jwt.js +++ b/auth/src/core/jwt.js @@ -15,11 +15,18 @@ export async function verify(token) { return jwt.verify(token, secret); } +// In dev (localhost): nessun domain → il cookie è scopato al singolo host (localhost), +// ma viene comunque inviato a tutte le porte (4001 auth, 4003 console, 4000 api). +// In prod: COOKIE_DOMAIN=.server.com → cookie condiviso fra tutti i sottodomini +// (auth.server.com, console.server.com, api.server.com). +const cookieDomain = process.env.COOKIE_DOMAIN || undefined; + export const cookieOptions = { httpOnly: true, - secure: process.env.NODE_ENV, + secure: process.env.NODE_ENV === 'production', sameSite: 'lax', path: '/', maxAge: ttl_seconds * 1000, + ...(cookieDomain ? { domain: cookieDomain } : {}), }; diff --git a/auth/src/data/redis.js b/auth/src/data/redis.js index a46f281..20847e4 100644 --- a/auth/src/data/redis.js +++ b/auth/src/data/redis.js @@ -2,7 +2,7 @@ import Redis from 'ioredis'; const client = new Redis({ host: process.env.REDIS_HOST, - port: process.env.REDIS_PORT, + port: Number(process.env.REDIS_PORT), password: process.env.REDIS_PASSWORD, }); diff --git a/auth/src/index.js b/auth/src/index.js index 8366ba9..43c1351 100644 --- a/auth/src/index.js +++ b/auth/src/index.js @@ -1,35 +1,52 @@ import express from 'express'; import cookieParser from 'cookie-parser'; +import path from 'path'; +import { fileURLToPath } from 'url'; import { authRouter } from './routes/auth.js'; +import { userAPIs } from './routes/users.js'; +import { sessionsAPIs } from './routes/sessions.js'; +import { pagesAPIs } from './routes/pages.js'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); const app = express(); -app.use(express.json()); +app.use(express.json({ limit: '64kb' })); app.use(cookieParser()); -app.get('/health', (req, res) => { +// Asset statici (CSS, font, JS client). Servito a /static/* +app.use('/static', express.static(path.join(__dirname, 'static'), { + maxAge: process.env.NODE_ENV === 'production' ? '30d' : 0, + fallthrough: true, +})); + +app.get('/health', (_req, res) => { res.send({ status: 'ok', service: 'auth', version: { - 'major': process.env.V_MAJOR, - 'minor': process.env.V_MINOR, - 'patch': process.env.V_PATCH, + major: process.env.V_MAJOR, + minor: process.env.V_MINOR, + patch: process.env.V_PATCH, }, timestamp: new Date().toISOString(), }); }); -// Public web pages -// app.use('/login', authRouter); -// app.use('/profile', profileRouter); -// app.use('/profile/sessions', sessionRouter); +// Pagine web pubbliche (HTML) — /login, /profile, /config.js +app.use('/', pagesAPIs); +// API JSON app.use('/api', authRouter); -// app.use('/api/users', usersRouter); -// app.use('/api/sessions', sessionRouter); -// +app.use('/api/users', userAPIs); +app.use('/api/sessions', sessionsAPIs); -app.listen('3000', '0.0.0.0', () => { +// Error handler globale +app.use((err, _req, res, _next) => { + console.error('[auth] errore non gestito:', err); + res.status(500).json({ error: 'internal_error' }); +}); + +app.listen(Number(process.env.PORT ?? 3000), '0.0.0.0', () => { console.log('Auth started'); -}) \ No newline at end of file +}); diff --git a/auth/src/middleware/auth.js b/auth/src/middleware/auth.js deleted file mode 100644 index 89ba55b..0000000 --- a/auth/src/middleware/auth.js +++ /dev/null @@ -1,35 +0,0 @@ -import { verify } from "../core/jwt"; -import { query } from "../data/db"; -import { redis } from '../data/redis'; - -const cookieName = process.env.COOKIE_NAME; - -export async function requireUserAuth(req, res, next) { - const token = req.cookies?.[cookieName]; - if (!token) return res.status(401).json({ message: 'No token' }); - - let payload; - try { - payload = await verify(token); - } catch (error) { - return res.status(401).json({ message: 'Invalid token' }); - } - - // Session - const { rows } = await query( - 'select id, user_id, expires_at from sessions where id = $1', - [payload.sessionId] - ); - if (!rows[0]) return res.status(401).json({ message: 'Invalid session' }); - - await query('update sessions set last_activity = now() where id = $1', [payload.sessionId]).catch(() => { }); - redis.set(`onlineuser:${payload.sub}`, '1', 'EX', 60).catch(() => { }); - - req.user = { - id: payload.sub, - name: payload.sessionId, - } - next(); - - -} \ No newline at end of file diff --git a/auth/src/middlewares/internalware.js b/auth/src/middlewares/internalware.js new file mode 100644 index 0000000..69077e5 --- /dev/null +++ b/auth/src/middlewares/internalware.js @@ -0,0 +1,9 @@ +const token = process.env.INTERNAL_API_TOKEN; + +export function internalware(req, res, next) { + const header = req.get('X-Internal-Token'); + if (header !== token) { + return res.status(401).json({ message: 'Unauthorized' }) + } + next(); +} diff --git a/auth/src/middlewares/userware.js b/auth/src/middlewares/userware.js new file mode 100644 index 0000000..11af21d --- /dev/null +++ b/auth/src/middlewares/userware.js @@ -0,0 +1,40 @@ +import { verify } from "../core/jwt.js"; +import { query } from "../data/db.js"; +import { redis } from '../data/redis.js'; + +const cookieName = process.env.COOKIE_NAME; + +export async function requireUserAuth(req, res, next) { + const token = req.cookies?.[cookieName]; + if (!token) return res.status(401).json({ message: 'No token' }); + + let payload; + try { + payload = await verify(token); + } catch (error) { + return res.status(401).json({ message: 'Invalid token' }); + } + + // Session + const { rows } = await query( + 'select id, user_id, expires_at from sessions where id = $1 and expires_at > now()', + [payload.sessionId] + ); + if (!rows[0]) return res.status(401).json({ message: 'Invalid session' }); + + const writeKey = `user:lastonline:${payload.sessionId}`; + const acquired = await redis.set(writeKey, '1', 'EX', 30, 'NX').catch(() => null); + if (acquired === 'OK') { + await query('update sessions set last_activity = now() where id = $1', [payload.sessionId]).catch((err) => { + console.error('auth error in last_activity update', err.message); + }); + redis.set(`user:online:${payload.sub}`, '1', 'EX', 60).catch(() => { }); + } + + req.user = { + id: payload.sub, + sessionId: payload.sessionId, + } + next(); + +} \ No newline at end of file diff --git a/auth/src/pages/login.html b/auth/src/pages/login.html index 9ffddc9..97091e5 100644 --- a/auth/src/pages/login.html +++ b/auth/src/pages/login.html @@ -1,12 +1,93 @@ - - - - Ciao - - -

Ciao

-
- -
- + + + + + + + MEB — Accedi + + +
+ +

Bentornato

+

Inserisci le tue credenziali per accedere alla console.

+ + +
+ + + diff --git a/auth/src/pages/profile.html b/auth/src/pages/profile.html new file mode 100644 index 0000000..d942048 --- /dev/null +++ b/auth/src/pages/profile.html @@ -0,0 +1,249 @@ + + + + + + + + MEB — Profilo + + +
+
+
MEB
+ + +
+ +
+
+
?
+
+

+

Caricamento…

+
+
+ +
+
+

Sessioni attive

+ +
+
+
Caricamento sessioni…
+
+
+ +
+
+

Informazioni account

+
+
+
ID utente
+
+
Sessione corrente
+
+
Ambiente
+
+
+
+
+ +
+
+ + + + diff --git a/auth/src/routes/auth.js b/auth/src/routes/auth.js index 8761895..a059fc0 100644 --- a/auth/src/routes/auth.js +++ b/auth/src/routes/auth.js @@ -3,7 +3,8 @@ import { query } from "../data/db.js"; import { hash, verify } from "../core/securitycore.js"; import { sign, cookieOptions } from "../core/jwt.js"; import crypto from "crypto"; -import {redis} from "../data/redis.js"; +import { redis } from "../data/redis.js"; +import { requireUserAuth } from "../middlewares/userware.js"; const router = Router(); const cookieName = process.env.COOKIE_NAME @@ -35,7 +36,7 @@ router.post('/login', async (req, res) => { const user = rows[0]; const ok = user ? await verify(password, user.password_hash) : false; if (!ok) { - return res.status(400).json({ message: 'Invalid username or password' }); + return res.status(401).json({ message: 'Invalid username or password' }); } const ua = req.headers['user-agent']; @@ -43,28 +44,36 @@ router.post('/login', async (req, res) => { const sessionToken = crypto.randomUUID(); const ttlDays = 360; + const expiresAt = new Date(Date.now() + ttlDays * 86_400_000); //expires in 360 days - const { rows: srow } = await query('insert into sessions (user_id, session_token, device_name, device_os, ip_address, expires_at) values ($1, $2, $3, $4, $5, $6) returning id', [user.id, sessionToken, ua.slice(0, 100), '', ip?.slice(0, 45), ttlDays]); - const session_id = srow[0].id; - const jtoken = sign({ sub: user.id, session_id }); + const { rows: srow } = await query( + `insert into sessions + (user_id, session_token, device_name, device_os, ip_address, expires_at) + values ($1, $2, $3, $4, $5, $6) + returning id, expires_at`, + [user.id, sessionToken, ua?.slice(0, 100) ?? '', 'macos', ip?.slice(0, 45), expiresAt] + ); + const sessionId = srow[0].id; + const jtoken = sign({ sub: user.id, sessionId }); - await redis.set(`usersession:${session_id}`, user.id, 'EX', ttlDays * 24 * 3600); - await redis.set(`online:${user.id}`, '1', 'EX', 60); + await redis.set(`user:session:${sessionId}`, user.id, 'EX', ttlDays * 24 * 3600); + await redis.set(`user:online:${user.id}`, '1', 'EX', 60); res.cookie(cookieName, jtoken, cookieOptions); res.json({ ok: true, user: user.id, - session: session_id + session: sessionId }); }) -router.post('/logout', async (req, res) => { - await query('delete from sessions where id = $1', [req.user.sessionID]); - await redis.del(`online:${req.user.id}`); - res.clearCookie(cookieName); +router.post('/logout', requireUserAuth, async (req, res) => { + await query('delete from sessions where id = $1', [req.user.sessionId]); + await redis.del(`user:online:${req.user.id}`); + await redis.del(`user:session:${req.user.sessionId}`); + res.clearCookie(cookieName, { path: '/' }); res.json({ loggedOut: true }); }) -export { router as authRouter }; +export { router as authRouter }; \ No newline at end of file diff --git a/auth/src/routes/pages.js b/auth/src/routes/pages.js new file mode 100644 index 0000000..a3b7ee7 --- /dev/null +++ b/auth/src/routes/pages.js @@ -0,0 +1,45 @@ +import { Router } from "express"; +import path from "path"; +import { fileURLToPath } from "url"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const pagesDirectory = path.join(__dirname, "../pages"); + +const router = Router(); + +// Redirect intelligente sulla root: se non loggato → login, altrimenti profile. +// Il check vero è fatto client-side dalla pagina target tramite /api/users/me; +// qui ci basiamo solo sulla presenza del cookie per scegliere la destinazione. +router.get('/', (req, res) => { + const cookieName = process.env.COOKIE_NAME; + if (req.cookies?.[cookieName]) return res.redirect('/profile'); + return res.redirect('/login'); +}); + +router.get('/login', (_req, res) => { + res.sendFile(path.join(pagesDirectory, 'login.html')); +}); + +router.get('/profile', (_req, res) => { + // L'auth è verificata client-side: la pagina fetch-a /api/users/me + // e se 401 redirige a /login. Pattern semplice da SPA. + res.sendFile(path.join(pagesDirectory, 'profile.html')); +}); + +// Endpoint dinamico che espone la config runtime alle pagine HTML. +// Le pagine fanno e poi leggono window.MEB_CONFIG. +// In questo modo gli URL dei servizi (console, api) sono iniettati dal server e +// cambiano automaticamente fra dev e prod senza toccare l'HTML. +router.get('/config.js', (_req, res) => { + const config = { + env: process.env.NODE_ENV || 'development', + console: process.env.CONSOLE_PUBLIC_URL || 'http://localhost:4003', + api: process.env.API_PUBLIC_URL || 'http://localhost:4000', + auth: process.env.AUTH_PUBLIC_URL || '', // vuoto = same-origin (la pagina è servita da auth) + }; + res.type('application/javascript') + .set('Cache-Control', 'no-store') + .send(`window.MEB_CONFIG = Object.freeze(${JSON.stringify(config)});`); +}); + +export { router as pagesAPIs }; diff --git a/auth/src/routes/sessions.js b/auth/src/routes/sessions.js new file mode 100644 index 0000000..899d6c2 --- /dev/null +++ b/auth/src/routes/sessions.js @@ -0,0 +1,27 @@ +import { Router } from 'express'; +import { query } from '../data/db.js'; +import { redis } from '../data/redis.js'; +import { requireUserAuth} from '../middlewares/userware.js'; + +const router = Router(); + +router.get('/', requireUserAuth, async (req, res) => { + const { rows } = await query('select id, device_name, device_os, ip_address, created_at, last_activity, expires_at, (id = $2) as is_current from sessions where user_id = $1 and expires_at > now() order by created_at desc', [req.user.id, req.user.sessionId]); + res.json(rows); +}); + +router.delete('/:id', requireUserAuth, async (req, res) => { + + const sessionID = req.params.id; + + const { rows } = await query('select id from sessions where id = $1 and user_id = $2', [sessionID, req.user.id]); + if (!rows[0]) return res.status(404).json({ error: 'session not found' }); + + await query('delete from sessions where id = $1', [sessionID]); + await redis.del(`user:session:${sessionID}`); + await redis.publish(`user:session:revoked`, sessionID); + + res.sendStatus(200); +}); + +export { router as sessionsAPIs }; diff --git a/auth/src/routes/users.js b/auth/src/routes/users.js new file mode 100644 index 0000000..8a43844 --- /dev/null +++ b/auth/src/routes/users.js @@ -0,0 +1,31 @@ +import { Router } from 'express'; +import { query } from '../data/db.js'; +import { requireUserAuth } from '../middlewares/userware.js'; + +const router = Router(); + +router.get('/me', requireUserAuth, async (req, res) => { + const { rows } = await query('select id, username, created_at from users where id = $1', [req.user.id]); + if (!rows[0]) return res.status(404).json({ message: 'User not found' }); + res.json({ + user: rows[0], + thisSession: { id: req.user.sessionId } + }); +}); + + +//ADMIN ONLY +// TODO: require admin-only auth + +router.get('/', async (req, res) => { + const { rows } = await query('select id, username, created_at from users'); + res.json(rows); +}); + +router.get('/:id', async (req, res) => { + const { rows } = await query('select id, username, created_at from users where id = $1', [req.params.id]); + if (!rows[0]) return res.status(404).json({ message: 'User not found' }); + res.json(rows[0]); +}); + +export { router as userAPIs }; \ No newline at end of file diff --git a/auth/src/static/fonts/elmssans.ttf b/auth/src/static/fonts/elmssans.ttf new file mode 100644 index 0000000000000000000000000000000000000000..66d0dca80ae96d8a557fa3544912a19db7587cf7 GIT binary patch literal 188136 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 console.error('[redis] client error', e.message)); +sub.on('error', (e) => console.error('[redis] sub error', e.message)); + +export { client as redis, sub as redisSub }; diff --git a/stream/src/routes/connect.js b/stream/src/routes/connect.js new file mode 100644 index 0000000..f772a29 --- /dev/null +++ b/stream/src/routes/connect.js @@ -0,0 +1,44 @@ +import { Router } from 'express'; +import crypto from 'crypto'; +import { querySensors as sensors } from '../data/db.js'; +import { redis } from '../data/redis.js'; +import { verify } from '../core/securitycore.js'; + +const router = Router(); +const rateLimiter = 10; +const rateLimitWindow = 60; + +router.post('/connect', async (req, res) => { + const { sensorID, code } = req.body; + + const ip = (req.headers['x-forwarded-for']?.split(',')[0]?.trim()) || req.socket.remoteAddress || 'unknown'; + const tryKey = `streamconnect:fail:${ip}`; + const fails = Number(await redis.get(tryKey).catch(() => 0)); + if (fails >= rateLimiter) { + return res.status(429).json({ error: 'Too many failed attempts' }); + } + + if (!sensorID || !code) { + await redis.multi().incr(tryKey).expire(tryKey, rateLimitWindow).exec().catch(() => { }); + return res.status(400).json({ error: 'sensor and code are required' }); + } + + const { rows } = await sensors('select id, name, code_hash from sensors where id = $1', [sensorID]); + if (rows.length === 0) { + return res.status(404).json({ error: 'sensor not found' }); + } + if (!rows[0] || !verify(code, rows[0].code_hash)) { + await redis.multi().incr(tryKey).expire(tryKey, rateLimitWindow).exec().catch(() => { }); + return res.status(401).json({ error: 'invalid code' }); + } + + const token = crypto.randomUUID(); + await redis.set(`sensor:pending:${token}`, rows[0].id, 'EX', 5); + res.json({ + token, + expiresIn: 5 + }) + +}) + +export { router as connectsAPI } \ No newline at end of file diff --git a/stream/src/ws/connection.js b/stream/src/ws/connection.js new file mode 100644 index 0000000..cbcb985 --- /dev/null +++ b/stream/src/ws/connection.js @@ -0,0 +1,3 @@ +import { encode, decode } from '@msgpack/msgpack'; +import { queryData as datas } from '../data/db.js'; +import { write, point } from ''; diff --git a/stream/src/ws/upgrade.js b/stream/src/ws/upgrade.js new file mode 100644 index 0000000..ce0d8cb --- /dev/null +++ b/stream/src/ws/upgrade.js @@ -0,0 +1,44 @@ +import { URL } from 'url'; +import { redis } from '../data/redis'; +import { querySensors as sensors } from '../data/db'; + +export function buildUpgradeHandler(wss) { + return async function upgradeHandler(req, socket, head) { + try { + const url = new URL(req.url, 'http://localhost'); + const token = url.searchParams.get('token'); + + if (!token) { + socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n'); + return socket.destroy(); + } + + const pendingSensor = await redis.getdel(`sensors:pending:${token}`); + if (!pendingSensor) { + socket.write('HTTP/1.1 404 Not Found\r\n\r\n'); + return socket.destroy(); + } + + const { rows } = await sensors('select id, name from sensors where id = $1', [pendingSensor]); + const sensor = rows[0]; + if (!sensor) { + socket.write('HTTP/1.1 404 Not Found\r\n\r\n'); + return socket.destroy(); + } + + wss.handleUpgrade(req, socket, head, (ws) => { + ws._sensor = sensor; + wss.emit('connection', ws, req); + }); + + } catch (error) { + console.error('error in upgrading conenction with sensor to ws with error: ', error); + + try { + socket.destroy(); + } catch (destroyError) { + console.error('error destroying socket: ', destroyError); + } + } + }; +} \ No newline at end of file