diff --git a/package-lock.json b/package-lock.json index 38c10513..97fb8703 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,7 @@ "@fastify/swagger-ui": "^5.2.4", "bson": "^6.10.4", "fastify-raw-body": "^5.0.0", + "flowerbase": "file:flowerbase-1.0.0.tgz", "jsonwebtoken": "^9.0.3", "node-cron": "^4.2.1", "undici": "^7.18.2" @@ -1256,6 +1257,7 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -7636,6 +7638,7 @@ "integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/core": "^7.21.3", "@svgr/babel-preset": "8.1.0", @@ -7867,6 +7870,7 @@ "dev": true, "hasInstallScript": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "@swc/counter": "^0.1.3", "@swc/types": "^0.1.25" @@ -8082,6 +8086,7 @@ "integrity": "sha512-QamiFeIK3txNjgUTNppE6MiG3p7TdninpZu0E0PbqVh1a9FNLT2FRhisaa4NcaX52XVhA5l7Pk58Ft7Sqi/2sA==", "dev": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "tslib": "^2.8.0" } @@ -8110,6 +8115,7 @@ "integrity": "sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g==", "dev": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "@swc/counter": "^0.1.3" } @@ -8208,6 +8214,7 @@ "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", @@ -8305,6 +8312,7 @@ "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "*", "@types/json-schema": "*" @@ -8770,6 +8778,7 @@ "integrity": "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "7.18.0", "@typescript-eslint/types": "7.18.0", @@ -9457,6 +9466,7 @@ "integrity": "sha512-nrUSn7hzt7J6JWgWGz78ZYI8wj+gdIJdk0Ynjpp8l+trkn58Uqsf6RYrYkEK+3X18EX+TNdtJI0WxAtc+L84SQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "argparse": "^2.0.1" }, @@ -9513,6 +9523,7 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -9595,6 +9606,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -10404,6 +10416,7 @@ "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==", "dev": true, "license": "Apache-2.0", + "peer": true, "peerDependencies": { "bare-abort-controller": "*" }, @@ -10495,7 +10508,7 @@ "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true, + "devOptional": true, "funding": [ { "type": "github", @@ -10626,7 +10639,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "buffer": "^5.5.0", @@ -10786,6 +10799,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -10836,7 +10850,7 @@ "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "dev": true, + "devOptional": true, "funding": [ { "type": "github", @@ -11066,7 +11080,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "dev": true, + "devOptional": true, "license": "ISC" }, "node_modules/chrome-trace-event": { @@ -12096,7 +12110,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "mimic-response": "^3.1.0" @@ -12112,7 +12126,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=10" @@ -12125,7 +12139,7 @@ "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=4.0.0" @@ -12272,7 +12286,7 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "engines": { "node": ">=8" @@ -12959,6 +12973,7 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -13749,7 +13764,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", - "dev": true, + "devOptional": true, "license": "(MIT OR WTFPL)", "engines": { "node": ">=6" @@ -14310,6 +14325,7 @@ "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -14627,6 +14643,37 @@ "dev": true, "license": "ISC" }, + "node_modules/flowerbase": { + "version": "1.0.0", + "resolved": "file:flowerbase-1.0.0.tgz", + "integrity": "sha512-cRj1+HWqTNR7uXeg6FBIB2Ue4Ej+kBn4ZmdB2KP6XRBpPS/IUu3GTsi1xMElkfPf35hAK4//LUAC8Ed1iCt92w==", + "license": "ISC", + "workspaces": [ + "packages/*" + ], + "dependencies": { + "@aws-sdk/client-lambda": "^3.974.0", + "@aws-sdk/client-s3": "^3.974.0", + "@aws-sdk/s3-request-presigner": "^3.975.0", + "@fastify/cors": "^11.2.0", + "@fastify/jwt": "^10.0.0", + "@fastify/mongodb": "^9.0.2", + "@fastify/swagger": "^9.6.1", + "@fastify/swagger-ui": "^5.2.4", + "bson": "^6.10.4", + "fastify-raw-body": "^5.0.0", + "jsonwebtoken": "^9.0.3", + "node-cron": "^4.2.1", + "undici": "^7.18.2" + }, + "optionalDependencies": { + "@nx/nx-darwin-arm64": "19.4.1", + "@nx/nx-darwin-x64": "19.4.1", + "@nx/nx-linux-x64-gnu": "19.4.1", + "@nx/nx-win32-x64-msvc": "19.4.1", + "@rollup/rollup-linux-x64-gnu": "4.18.0" + } + }, "node_modules/flowerbase-demo": { "resolved": "packages/demo", "link": true @@ -14739,6 +14786,7 @@ "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -15002,7 +15050,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/fs-extra": { @@ -15226,7 +15274,7 @@ "version": "0.0.0", "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/glob": { @@ -15933,7 +15981,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true, + "devOptional": true, "funding": [ { "type": "github", @@ -16105,7 +16153,7 @@ "version": "1.3.8", "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "dev": true, + "devOptional": true, "license": "ISC" }, "node_modules/inspect-with-kind": { @@ -16880,6 +16928,7 @@ "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -16948,6 +16997,24 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/jest-circus/node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/jest-circus/node_modules/dedent": { "version": "1.7.2", "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz", @@ -16963,6 +17030,17 @@ } } }, + "node_modules/jest-circus/node_modules/yaml": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz", + "integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==", + "dev": true, + "license": "ISC", + "optional": true, + "engines": { + "node": ">= 6" + } + }, "node_modules/jest-cli": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", @@ -18925,7 +19003,7 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, + "devOptional": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -18944,7 +19022,7 @@ "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/mnemonist": { @@ -19010,9 +19088,10 @@ "version": "6.5.0", "resolved": "https://registry.npmjs.org/mongodb-client-encryption/-/mongodb-client-encryption-6.5.0.tgz", "integrity": "sha512-Gj8EeyYKsssdko0NKhWRBGDif6uVFBbv+e+Nyn7E316UmRzApc4IP+p2NLm+av+fU+dFHVT5WqfzaQVDTh8i9w==", - "dev": true, + "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "node-addon-api": "^4.3.0", "prebuild-install": "^7.1.3" @@ -19099,7 +19178,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/natural-compare": { @@ -19159,7 +19238,7 @@ "version": "3.87.0", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.87.0.tgz", "integrity": "sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "semver": "^7.3.5" @@ -19172,7 +19251,7 @@ "version": "7.7.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, + "devOptional": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -19192,7 +19271,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz", "integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/node-cron": { @@ -19502,6 +19581,7 @@ "dev": true, "hasInstallScript": true, "license": "MIT", + "peer": true, "dependencies": { "@nrwl/tao": "19.4.1", "@yarnpkg/lockfile": "^1.1.0", @@ -20426,6 +20506,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -21137,7 +21218,7 @@ "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "detect-libc": "^2.0.0", @@ -21176,6 +21257,7 @@ "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -21363,7 +21445,7 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "end-of-stream": "^1.1.0", @@ -21523,7 +21605,7 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "dev": true, + "devOptional": true, "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", "dependencies": { "deep-extend": "^0.6.0", @@ -21539,7 +21621,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -21552,6 +21634,17 @@ "dev": true, "license": "MIT" }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -21935,6 +22028,7 @@ "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -23776,7 +23870,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", - "dev": true, + "devOptional": true, "funding": [ { "type": "github", @@ -23797,7 +23891,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", - "dev": true, + "devOptional": true, "funding": [ { "type": "github", @@ -24709,7 +24803,7 @@ "version": "2.1.4", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "chownr": "^1.1.1", @@ -24722,7 +24816,7 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "bl": "^4.0.3", @@ -25321,7 +25415,7 @@ "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "dependencies": { "safe-buffer": "^5.0.1" @@ -25359,6 +25453,7 @@ "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", "dev": true, "license": "(MIT OR CC0-1.0)", + "peer": true, "engines": { "node": ">=10" }, @@ -25470,6 +25565,7 @@ "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -25537,6 +25633,7 @@ "integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/types": "8.56.1", @@ -26134,6 +26231,7 @@ "integrity": "sha512-jTywjboN9aHxFlToqb0K0Zs9SbBoW4zRUlGzI2tYNxVYcEi/IPpn+Xi4ye5jTLvX2YeLuic/IvxNot+Q1jMoOw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", @@ -26183,6 +26281,7 @@ "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@discoveryjs/json-ext": "^0.5.0", "@webpack-cli/configtest": "^2.1.1", @@ -27148,6 +27247,7 @@ "version": "8.34.0", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.34.0", "@typescript-eslint/types": "8.34.0", @@ -27365,6 +27465,7 @@ "version": "9.28.0", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", @@ -27670,6 +27771,7 @@ "version": "5.8.3", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/package.json b/package.json index 0a7e2cd7..9eb6449e 100644 --- a/package.json +++ b/package.json @@ -101,8 +101,9 @@ "@fastify/swagger-ui": "^5.2.4", "bson": "^6.10.4", "fastify-raw-body": "^5.0.0", + "flowerbase": "file:flowerbase-1.0.0.tgz", "jsonwebtoken": "^9.0.3", "node-cron": "^4.2.1", "undici": "^7.18.2" } -} \ No newline at end of file +} diff --git a/packages/demo/packages/backend/functions/config.json b/packages/demo/packages/backend/functions/config.json index 3f1b7c50..33d15580 100644 --- a/packages/demo/packages/backend/functions/config.json +++ b/packages/demo/packages/backend/functions/config.json @@ -16,5 +16,11 @@ "private": false, "run_as_system": true, "disable_arg_logs": true + }, + { + "name": "onUserCreation", + "private": false, + "run_as_system": true, + "disable_arg_logs": true } -] +] \ No newline at end of file diff --git a/packages/demo/packages/backend/functions/onUserCreation.ts b/packages/demo/packages/backend/functions/onUserCreation.ts new file mode 100644 index 00000000..52a0f3d1 --- /dev/null +++ b/packages/demo/packages/backend/functions/onUserCreation.ts @@ -0,0 +1,8 @@ +module.exports = async function (payload) { + console.log("User created with payload:", payload) + // you can do whatever you want with the user object, for example: + // - send a welcome email + // - add the user to a mailing list + // - create a profile for the user in another collection + // - etc. +}; \ No newline at end of file diff --git a/packages/demo/packages/frontend/src/pages/Registration/index.tsx b/packages/demo/packages/frontend/src/pages/Registration/index.tsx index cc05a54b..bd651963 100644 --- a/packages/demo/packages/frontend/src/pages/Registration/index.tsx +++ b/packages/demo/packages/frontend/src/pages/Registration/index.tsx @@ -36,6 +36,9 @@ export const Registration = () => { await app.emailPasswordAuth.registerUser({ email: data.email, password: data.password, + payload: { + tryingToAddCustomData: true, + } }); } catch (err) { alert("Error during registration"); diff --git a/packages/flowerbase-client/src/__tests__/auth.test.ts b/packages/flowerbase-client/src/__tests__/auth.test.ts index db497ecf..1fa5cbf4 100644 --- a/packages/flowerbase-client/src/__tests__/auth.test.ts +++ b/packages/flowerbase-client/src/__tests__/auth.test.ts @@ -210,4 +210,37 @@ describe('flowerbase-client auth', () => { }) ) }) + + it('sends payload when registering a user', async () => { + global.fetch = jest.fn().mockResolvedValue({ + ok: true, + text: async () => JSON.stringify({ status: 'ok' }) + }) as unknown as typeof fetch + + const app = new App({ id: 'my-app', baseUrl: 'http://localhost:3000' }) + + await app.emailPasswordAuth.registerUser({ + email: 'john@doe.com', + password: 'secret123', + payload: { + tryingToAddCustomData: true, + role: 'student' + } + }) + + expect(global.fetch).toHaveBeenCalledWith( + 'http://localhost:3000/api/client/v2.0/app/my-app/auth/providers/local-userpass/register', + expect.objectContaining({ + method: 'POST', + body: JSON.stringify({ + email: 'john@doe.com', + password: 'secret123', + payload: { + tryingToAddCustomData: true, + role: 'student' + } + }) + }) + ) + }) }) diff --git a/packages/flowerbase-client/src/app.ts b/packages/flowerbase-client/src/app.ts index a1ed9f55..0f26ecb5 100644 --- a/packages/flowerbase-client/src/app.ts +++ b/packages/flowerbase-client/src/app.ts @@ -41,7 +41,7 @@ export class App { private readonly listeners = new Set<() => void>() emailPasswordAuth: { - registerUser: (input: { email: string; password: string }) => Promise + registerUser: (input: { email: string; password: string; payload?: Record }) => Promise confirmUser: (input: { token: string; tokenId: string }) => Promise resendConfirmationEmail: (input: { email: string }) => Promise retryCustomConfirmation: (input: { email: string }) => Promise @@ -69,8 +69,8 @@ export class App { this.sessionBootstrapPromise = this.bootstrapSessionOnLoad() this.emailPasswordAuth = { - registerUser: ({ email, password }) => - this.postProvider('/local-userpass/register', { email, password }), + registerUser: ({ email, password, payload }) => + this.postProvider('/local-userpass/register', { email, password, payload }), confirmUser: ({ token, tokenId }) => this.postProvider('/local-userpass/confirm', { token, tokenId }), resendConfirmationEmail: ({ email }) => diff --git a/packages/flowerbase/src/__test__/constants.customUserData.test.ts b/packages/flowerbase/src/__test__/constants.customUserData.test.ts new file mode 100644 index 00000000..6f4f746c --- /dev/null +++ b/packages/flowerbase/src/__test__/constants.customUserData.test.ts @@ -0,0 +1,51 @@ +const loadConstantsWithCustomUserData = async ( + customUserDataConfig: Record +) => { + jest.resetModules() + + jest.doMock('../auth/utils', () => ({ + loadAuthConfig: jest.fn(() => ({ + auth_collection: 'auth_users', + auth_database: 'auth-db', + 'local-userpass': { + disabled: false, + config: {} + } + })), + loadCustomUserData: jest.fn(() => customUserDataConfig) + })) + + return import('../constants') +} + +describe('AUTH_CONFIG custom user data config', () => { + it('uses custom user collection when custom_user_data.enabled is true', async () => { + const { AUTH_CONFIG, DB_NAME } = await loadConstantsWithCustomUserData({ + enabled: true, + database_name: 'main', + collection_name: 'users', + user_id_field: 'id', + on_user_creation_function_name: 'onCreateUser' + }) + + expect(DB_NAME).toBe('main') + expect(AUTH_CONFIG.userCollection).toBe('users') + expect(AUTH_CONFIG.user_id_field).toBe('id') + expect(AUTH_CONFIG.on_user_creation_function_name).toBe('onCreateUser') + }) + + it('disables custom user collection when custom_user_data.enabled is false', async () => { + const { AUTH_CONFIG, DB_NAME } = await loadConstantsWithCustomUserData({ + enabled: false, + database_name: 'main', + collection_name: 'users', + user_id_field: 'id', + on_user_creation_function_name: 'onCreateUser' + }) + + expect(DB_NAME).toBe('main') + expect(AUTH_CONFIG.userCollection).toBeUndefined() + expect(AUTH_CONFIG.user_id_field).toBeUndefined() + expect(AUTH_CONFIG.on_user_creation_function_name).toBeUndefined() + }) +}) \ No newline at end of file diff --git a/packages/flowerbase/src/auth/__tests__/controller.test.ts b/packages/flowerbase/src/auth/__tests__/controller.test.ts index e7c32107..abe14604 100644 --- a/packages/flowerbase/src/auth/__tests__/controller.test.ts +++ b/packages/flowerbase/src/auth/__tests__/controller.test.ts @@ -14,6 +14,102 @@ jest.mock('../../constants', () => ({ } })) +const loadAuthControllerWithConfig = async ( + authConfigOverrides: Record +) => { + jest.resetModules() + + jest.doMock('../../constants', () => ({ + AUTH_CONFIG: { + authCollection: 'auth_users', + refreshTokensCollection: 'refresh_tokens', + userCollection: 'users', + user_id_field: 'id', + ...authConfigOverrides + }, + AUTH_DB_NAME: 'test-auth-db', + DB_NAME: 'test-db', + DEFAULT_CONFIG: { + REFRESH_TOKEN_TTL_DAYS: 1 + } + })) + + jest.doMock('../../state', () => ({ + StateManager: { + select: jest.fn((key: string) => { + if (key === 'projectId') return 'test-project' + return undefined + }) + } + })) + + return import('../controller') +} + +const buildProfileTestApp = ({ + authUser, + customUser +}: { + authUser: Record + customUser?: Record | null +}) => { + let profileHandler: ((req: any) => Promise) | undefined + + const authCollection = { + createIndex: jest.fn().mockResolvedValue('ok'), + findOne: jest.fn().mockResolvedValue(authUser) + } + + const refreshCollection = { + createIndex: jest.fn().mockResolvedValue('ok') + } + + const usersCollection = { + findOne: jest.fn().mockResolvedValue(customUser ?? null) + } + + const db = { + collection: jest.fn((name: string) => { + if (name === 'auth_users') return authCollection + if (name === 'refresh_tokens') return refreshCollection + if (name === 'users') return usersCollection + return { + createIndex: jest.fn().mockResolvedValue('ok'), + findOne: jest.fn().mockResolvedValue(null) + } + }) + } + + const app = { + mongo: { + client: { + db: jest.fn().mockReturnValue(db) + } + }, + addHook: jest.fn(), + get: jest.fn( + ( + path: string, + _opts: unknown, + handler: (req: any) => Promise + ) => { + if (path === '/profile') { + profileHandler = handler + } + } + ), + post: jest.fn(), + delete: jest.fn(), + jwtAuthentication: jest.fn() + } + + return { + app, + usersCollection, + getProfileHandler: () => profileHandler + } +} + describe('authController', () => { it('creates a unique email index on the auth collection', async () => { const authCollection = { @@ -113,4 +209,117 @@ describe('authController', () => { } }) }) + + it('returns custom_data from linked custom user collection when custom user data is enabled', async () => { + const authUserId = '697349de5dc2c5850198cc06' + + const { authController } = await loadAuthControllerWithConfig({ + userCollection: 'users', + user_id_field: 'id' + }) + + const { app, usersCollection, getProfileHandler } = buildProfileTestApp({ + authUser: { + _id: { toString: () => authUserId }, + email: 'enabled@example.com', + identities: [{ provider_type: 'local-userpass' }], + custom_data: { + role: 'from-auth-users' + } + }, + customUser: { + _id: 'custom-user-document-id', + id: authUserId, + name: 'Mario Rossi', + role: 'from-users-collection' + } + }) + + await authController(app as never) + + const result = await getProfileHandler()?.({ + user: { + typ: 'access', + id: authUserId + }, + params: { + appId: 'flowerbase-e2e' + } + }) + + expect(usersCollection.findOne).toHaveBeenCalledWith({ + id: authUserId + }) + + expect(result).toEqual({ + user_id: authUserId, + domain_id: 'flowerbase-e2e', + identities: [{ provider_type: 'local-userpass' }], + custom_data: { + _id: 'custom-user-document-id', + id: authUserId, + name: 'Mario Rossi', + role: 'from-users-collection' + }, + type: 'normal', + data: { + email: 'enabled@example.com' + } + }) + }) + + it('returns auth_users.custom_data when custom user data is disabled', async () => { + const authUserId = '697349de5dc2c5850198cc06' + + const { authController } = await loadAuthControllerWithConfig({ + userCollection: undefined, + user_id_field: undefined + }) + + const { app, usersCollection, getProfileHandler } = buildProfileTestApp({ + authUser: { + _id: { toString: () => authUserId }, + email: 'disabled@example.com', + identities: [{ provider_type: 'local-userpass' }], + custom_data: { + role: 'student', + tenantId: 'tenant-1', + tryingToAddCustomData: true + } + }, + customUser: { + id: authUserId, + role: 'this-should-not-be-used' + } + }) + + await authController(app as never) + + const result = await getProfileHandler()?.({ + user: { + typ: 'access', + id: authUserId + }, + params: { + appId: 'flowerbase-e2e' + } + }) + + expect(usersCollection.findOne).not.toHaveBeenCalled() + + expect(result).toEqual({ + user_id: authUserId, + domain_id: 'flowerbase-e2e', + identities: [{ provider_type: 'local-userpass' }], + custom_data: { + role: 'student', + tenantId: 'tenant-1', + tryingToAddCustomData: true + }, + type: 'normal', + data: { + email: 'disabled@example.com' + } + }) + }) }) diff --git a/packages/flowerbase/src/auth/controller.ts b/packages/flowerbase/src/auth/controller.ts index b12d2394..51b16a81 100644 --- a/packages/flowerbase/src/auth/controller.ts +++ b/packages/flowerbase/src/auth/controller.ts @@ -72,7 +72,7 @@ export async function authController(app: FastifyInstance) { ? await customUserDb .collection>(userCollection) .findOne({ [AUTH_CONFIG.user_id_field]: req.user.id }) - : null + : authUser?.custom_data const params = (req as unknown as { params?: { appId?: string } }).params const stateProjectId = StateManager.select('projectId') @@ -139,7 +139,7 @@ export async function authController(app: FastifyInstance) { const user = userCollection && AUTH_CONFIG.user_id_field ? (await customUserDb.collection(userCollection).findOne({ [AUTH_CONFIG.user_id_field]: req.user.sub })) - : {} + : auth_user.custom_data ?? {} res.status(201) return { diff --git a/packages/flowerbase/src/auth/providers/custom-function/controller.ts b/packages/flowerbase/src/auth/providers/custom-function/controller.ts index 6c544d63..06b6432b 100644 --- a/packages/flowerbase/src/auth/providers/custom-function/controller.ts +++ b/packages/flowerbase/src/auth/providers/custom-function/controller.ts @@ -116,7 +116,7 @@ export async function customFunctionController(app: FastifyInstance) { ? await customUserDb .collection(userCollection) .findOne({ [user_id_field]: authUser._id.toString() }) - : {} + : authUser.custom_data ?? {} const currentUserData = { _id: authUser._id, diff --git a/packages/flowerbase/src/auth/providers/local-userpass/__tests__/controller.test.ts b/packages/flowerbase/src/auth/providers/local-userpass/__tests__/controller.test.ts index 83105b61..b6cbd232 100644 --- a/packages/flowerbase/src/auth/providers/local-userpass/__tests__/controller.test.ts +++ b/packages/flowerbase/src/auth/providers/local-userpass/__tests__/controller.test.ts @@ -32,6 +32,152 @@ jest.mock('../../../../constants', () => ({ } })) +const loadLocalUserPassControllerWithConfig = async ( + authConfigOverrides: Record +) => { + jest.resetModules() + + jest.doMock('../../../../constants', () => ({ + AUTH_CONFIG: { + authCollection: 'auth_users', + refreshTokensCollection: 'refresh_tokens', + resetPasswordCollection: 'reset_password_requests', + userCollection: 'users', + user_id_field: 'id', + authProviders: { + 'local-userpass': { + disabled: false + } + }, + resetPasswordConfig: { + runResetFunction: true, + resetFunctionName: 'customReset' + }, + ...authConfigOverrides + }, + AUTH_DB_NAME: 'test-auth-db', + DB_NAME: 'test-db', + DEFAULT_CONFIG: { + RESET_PASSWORD_TTL_SECONDS: 3600, + AUTH_RATE_LIMIT_WINDOW_MS: 60000, + AUTH_LOGIN_MAX_ATTEMPTS: 100, + AUTH_REGISTER_MAX_ATTEMPTS: 100, + AUTH_RESET_MAX_ATTEMPTS: 100, + REFRESH_TOKEN_TTL_DAYS: 1 + } + })) + + jest.doMock('../../../../state', () => ({ + StateManager: { + select: jest.fn((key: string) => { + if (key === 'functions') { + return { + customReset: { + name: 'customReset', + code: 'exports = async () => ({ status: "success" })' + } + } + } + + if (key === 'services') { + return {} + } + + return {} + }) + } + })) + + jest.doMock('../../../../utils/context', () => ({ + GenerateContext: jest.fn() + })) + + jest.doMock('../../../../utils/crypto', () => ({ + comparePassword: jest.fn(async () => true), + generateToken: jest.fn(() => 'generated-token'), + hashPassword: jest.fn(async (password: string) => `hashed:${password}`), + hashToken: jest.fn(() => 'hashed-token') + })) + + return import('../controller') +} + +const buildLoginTestApp = ({ + authUser, + customUser +}: { + authUser: Record + customUser?: Record | null +}) => { + let loginHandler: + | ((req: any, res: any) => Promise) + | undefined + + const authUsersCollection = { + findOne: jest.fn().mockResolvedValue(authUser), + updateOne: jest.fn().mockResolvedValue({ acknowledged: true }) + } + + const usersCollection = { + findOne: jest.fn().mockResolvedValue(customUser ?? null) + } + + const resetCollection = { + createIndex: jest.fn().mockResolvedValue('ok'), + updateOne: jest.fn(), + deleteOne: jest.fn(), + findOne: jest.fn() + } + + const refreshCollection = { + createIndex: jest.fn().mockResolvedValue('ok'), + insertOne: jest.fn().mockResolvedValue({ insertedId: 'refresh-token-id' }) + } + + const db = { + collection: jest.fn((name: string) => { + if (name === 'auth_users') return authUsersCollection + if (name === 'users') return usersCollection + if (name === 'reset_password_requests') return resetCollection + if (name === 'refresh_tokens') return refreshCollection + return {} + }) + } + + const app: any = { + mongo: { + client: { + db: jest.fn().mockReturnValue(db) + } + }, + createAccessToken: jest.fn((user) => ({ + tokenType: 'access', + user + })), + createRefreshToken: jest.fn(() => 'refresh-token') + } + + app.post = jest.fn( + ( + path: string, + _opts: unknown, + handler: (req: any, res: any) => Promise + ) => { + if (path === '/login') { + loginHandler = handler.bind(app) + } + } + ) + + return { + app, + usersCollection, + authUsersCollection, + refreshCollection, + getLoginHandler: () => loginHandler + } +} + jest.mock('../../../../state', () => ({ StateManager: { select: jest.fn((key: string) => { diff --git a/packages/flowerbase/src/auth/providers/local-userpass/controller.ts b/packages/flowerbase/src/auth/providers/local-userpass/controller.ts index a50903cc..13f3a6ba 100644 --- a/packages/flowerbase/src/auth/providers/local-userpass/controller.ts +++ b/packages/flowerbase/src/auth/providers/local-userpass/controller.ts @@ -196,7 +196,8 @@ export async function localUserPassController(app: FastifyInstance) { provider: PROVIDER.LOCAL_USERPASS })({ email: req.body.email.toLowerCase(), - password: req.body.password + password: req.body.password, + payload: req.body.payload }) } catch (error) { if ( @@ -336,7 +337,7 @@ export async function localUserPassController(app: FastifyInstance) { ? await customUserDb .collection(userCollection) .findOne({ [user_id_field]: authUser._id.toString() }) - : {} + : authUser.custom_data ?? {} delete authUser?.password const userWithCustomData = { diff --git a/packages/flowerbase/src/auth/providers/local-userpass/dtos.ts b/packages/flowerbase/src/auth/providers/local-userpass/dtos.ts index d28a4ae9..86cdce53 100644 --- a/packages/flowerbase/src/auth/providers/local-userpass/dtos.ts +++ b/packages/flowerbase/src/auth/providers/local-userpass/dtos.ts @@ -1,6 +1,7 @@ export type RegisterUserDto = { email: string password: string + payload?: Record } export type LoginUserDto = { diff --git a/packages/flowerbase/src/auth/utils.ts b/packages/flowerbase/src/auth/utils.ts index 3c337e4b..d2a05e1c 100644 --- a/packages/flowerbase/src/auth/utils.ts +++ b/packages/flowerbase/src/auth/utils.ts @@ -96,7 +96,8 @@ export const REGISTRATION_SCHEMA = { minLength: 5, maxLength: 254 }, - password: { type: 'string', ...PASSWORD_RULES } + password: { type: 'string', ...PASSWORD_RULES }, + payload: { type: 'object', additionalProperties: true } }, required: ['email', 'password'] } diff --git a/packages/flowerbase/src/constants.ts b/packages/flowerbase/src/constants.ts index 200a3628..e5a708f2 100644 --- a/packages/flowerbase/src/constants.ts +++ b/packages/flowerbase/src/constants.ts @@ -14,6 +14,7 @@ const monitEnabled = typeof monitEnabledEnv === 'string' : false const { + enabled: customUserDataEnabled = false, database_name = 'main', collection_name = 'users', user_id_field = 'id', @@ -73,14 +74,20 @@ type AuthProviders = Record // TODO spostare nell'oggetto providers anche le altre configurazioni export const AUTH_CONFIG = { authCollection: auth_collection, - userCollection: collection_name, + + customUserDataEnabled, + + userCollection: customUserDataEnabled ? collection_name : undefined, + user_id_field: customUserDataEnabled ? user_id_field : undefined, + on_user_creation_function_name: customUserDataEnabled + ? on_user_creation_function_name + : undefined, + resetPasswordCollection: 'reset_password_requests', refreshTokensCollection: 'auth_refresh_tokens', resetPasswordConfig: configuration['local-userpass']?.config, localUserpassConfig: configuration['local-userpass']?.config, authProviders: configuration as unknown as AuthProviders, - user_id_field, - on_user_creation_function_name, providers: { "custom-function": configuration['custom-function']?.config, "anon-user": configuration['anon-user'] diff --git a/packages/flowerbase/src/features/triggers/utils.ts b/packages/flowerbase/src/features/triggers/utils.ts index 1e8fd23d..f1a5ee57 100644 --- a/packages/flowerbase/src/features/triggers/utils.ts +++ b/packages/flowerbase/src/features/triggers/utils.ts @@ -338,12 +338,22 @@ const handleAuthenticationTrigger = async ({ const currentUser = { ...document } delete (currentUser as { password?: unknown }).password + const idString = (currentUser as { _id: { toString: () => string } })._id.toString() + + const authCustomData = + (currentUser as { custom_data?: Record }).custom_data ?? {} + return { ...currentUser, - id: (currentUser as { _id: { toString: () => string } })._id.toString(), + id: idString, + custom_data: authCustomData, + user_data: authCustomData, data: { - _id: (currentUser as { _id: { toString: () => string } })._id.toString(), - email: (currentUser as { email?: string }).email + _id: idString, + id: idString, + email: (currentUser as { email?: string }).email, + payload: authCustomData, + custom_data: authCustomData } } } diff --git a/packages/flowerbase/src/shared/__test__/handleUserRegistration.test.ts b/packages/flowerbase/src/shared/__test__/handleUserRegistration.test.ts new file mode 100644 index 00000000..1ea836ac --- /dev/null +++ b/packages/flowerbase/src/shared/__test__/handleUserRegistration.test.ts @@ -0,0 +1,124 @@ +jest.mock('../../constants', () => ({ + AUTH_CONFIG: { + authCollection: 'auth_users', + localUserpassConfig: { + autoConfirm: true + } + }, + AUTH_DB_NAME: 'test-auth-db' +})) + +jest.mock('../../services/monitoring', () => ({ + emitServiceEvent: jest.fn() +})) + +jest.mock('../../utils/crypto', () => ({ + hashPassword: jest.fn(async (password: string) => `hashed:${password}`), + generateToken: jest.fn(() => 'generated-token') +})) + +import handleUserRegistration from '../handleUserRegistration' +import { PROVIDER } from '../models/handleUserRegistration.model' + +describe('handleUserRegistration', () => { + it('saves registration payload into custom_data', async () => { + const insertOne = jest.fn().mockResolvedValue({ + insertedId: { + toString: () => 'user-1' + } + }) + + const updateOne = jest.fn().mockResolvedValue({ + acknowledged: true + }) + + const findOne = jest.fn().mockResolvedValue(null) + + const collection = jest.fn(() => ({ + findOne, + insertOne, + updateOne + })) + + const db = { + collection + } + + const app = { + mongo: { + client: { + db: jest.fn(() => db) + } + } + } + + const payload = { + tryingToAddCustomData: true, + role: 'student' + } + + await handleUserRegistration(app as never, { + run_as_system: true, + provider: PROVIDER.LOCAL_USERPASS + })({ + email: 'john@doe.com', + password: 'secret123', + payload + }) + + expect(insertOne).toHaveBeenCalledWith( + expect.objectContaining({ + email: 'john@doe.com', + password: 'hashed:secret123', + status: 'confirmed', + custom_data: payload + }) + ) + }) + + it('saves an empty custom_data object when payload is missing', async () => { + const insertOne = jest.fn().mockResolvedValue({ + insertedId: { + toString: () => 'user-1' + } + }) + + const updateOne = jest.fn().mockResolvedValue({ + acknowledged: true + }) + + const findOne = jest.fn().mockResolvedValue(null) + + const collection = jest.fn(() => ({ + findOne, + insertOne, + updateOne + })) + + const db = { + collection + } + + const app = { + mongo: { + client: { + db: jest.fn(() => db) + } + } + } + + await handleUserRegistration(app as never, { + run_as_system: true, + provider: PROVIDER.LOCAL_USERPASS + })({ + email: 'john@doe.com', + password: 'secret123' + }) + + expect(insertOne).toHaveBeenCalledWith( + expect.objectContaining({ + custom_data: {} + }) + ) + }) +}) \ No newline at end of file diff --git a/packages/flowerbase/src/shared/handleUserRegistration.ts b/packages/flowerbase/src/shared/handleUserRegistration.ts index 3a572254..6832367c 100644 --- a/packages/flowerbase/src/shared/handleUserRegistration.ts +++ b/packages/flowerbase/src/shared/handleUserRegistration.ts @@ -115,7 +115,7 @@ export const sendConfirmationRequest = async ( * @param {Object} [opt] The options from the context * @returns {Promise>} A promise resolving to the result of the insert operation. */ -const handleUserRegistration: HandleUserRegistration = (app, opt) => async ({ email, password }) => { +const handleUserRegistration: HandleUserRegistration = (app, opt) => async ({ email, password, payload }) => { const { run_as_system, skipUserCheck, provider } = opt ?? {} const origin = opt?.monitoring?.invokedFrom const meta = { action: 'registerUser', email, provider } @@ -143,15 +143,12 @@ const handleUserRegistration: HandleUserRegistration = (app, opt) => async ({ em if (existingUser && !skipUserCheck) { throw new Error('This email address is already used') } - const result = await db?.collection(authCollection!).insertOne({ email, password: hashedPassword, status: (skipUserCheck || autoConfirm) ? 'confirmed' : 'pending', createdAt: new Date(), - custom_data: { - // TODO: aggiungere dati personalizzati alla registrazione - }, + custom_data: payload ?? {}, identities: [ { provider_type: provider, diff --git a/packages/flowerbase/src/shared/models/handleUserRegistration.model.ts b/packages/flowerbase/src/shared/models/handleUserRegistration.model.ts index 1e073ada..ec3930f9 100644 --- a/packages/flowerbase/src/shared/models/handleUserRegistration.model.ts +++ b/packages/flowerbase/src/shared/models/handleUserRegistration.model.ts @@ -5,6 +5,7 @@ import { Rules } from "../../features/rules/interface" type RegistrationParams = { email: string password: string + payload?: Record } export type Options = { diff --git a/tests/e2e/app/functions/onCreateUser.js b/tests/e2e/app/functions/onCreateUser.js index e129caf8..38554c6d 100644 --- a/tests/e2e/app/functions/onCreateUser.js +++ b/tests/e2e/app/functions/onCreateUser.js @@ -3,11 +3,14 @@ module.exports = async function (payload) { const mongoService = context.services.get('mongodb-atlas') const collection = mongoService.db('flowerbase-e2e').collection('triggerEvents') const documentId = user?.id?.toString() ?? 'unknown' + await collection.insertOne({ documentId, type: 'on_user_creation', email: user?.email ?? user?.data?.email ?? null, + customData: user?.custom_data ?? user?.data?.payload ?? null, createdAt: new Date().toISOString() }) + return { recorded: true, documentId } -} +} \ No newline at end of file diff --git a/tests/e2e/app/functions/registerUser.js b/tests/e2e/app/functions/registerUser.js index e3c31a1b..cc4ef3a7 100644 --- a/tests/e2e/app/functions/registerUser.js +++ b/tests/e2e/app/functions/registerUser.js @@ -2,7 +2,8 @@ module.exports = async function (payload) { const authService = context.services.get('auth') const result = await authService.emailPasswordAuth.registerUser({ email: payload?.email, - password: payload?.password + password: payload?.password, + payload: payload?.payload }) return { diff --git a/tests/e2e/mongodb-atlas.rules.e2e.auth-security.test.ts b/tests/e2e/mongodb-atlas.rules.e2e.auth-security.test.ts index a63fc18e..683e8d35 100644 --- a/tests/e2e/mongodb-atlas.rules.e2e.auth-security.test.ts +++ b/tests/e2e/mongodb-atlas.rules.e2e.auth-security.test.ts @@ -162,16 +162,16 @@ const callServiceOperation = async ({ }: { collection: string method: - | 'find' - | 'findOne' - | 'count' - | 'countDocuments' - | 'findOneAndUpdate' - | 'deleteOne' - | 'deleteMany' - | 'insertOne' - | 'updateOne' - | 'aggregate' + | 'find' + | 'findOne' + | 'count' + | 'countDocuments' + | 'findOneAndUpdate' + | 'deleteOne' + | 'deleteMany' + | 'insertOne' + | 'updateOne' + | 'aggregate' user: TestUser | null query?: Document filter?: Document @@ -602,9 +602,9 @@ const withDisabledProvider = (providerName: string) => { const next = cloneAuthProviders() const existing = (next as Record)[providerName] if (existing && typeof existing === 'object') { - ;(existing as { disabled?: boolean }).disabled = true + (existing as { disabled?: boolean }).disabled = true } else { - ;(next as Record)[providerName] = { + (next as Record)[providerName] = { name: providerName, type: providerName, disabled: true @@ -926,6 +926,50 @@ describe('MongoDB Atlas rule enforcement (e2e)', () => { expect(parsedError.message).toBe('This email address is already used') }) + it('passes payload through auth service registerUser function', async () => { + const email = 'function-payload-user@example.com' + + const response = await appInstance!.inject({ + method: 'POST', + url: FUNCTION_CALL_URL, + headers: { + authorization: `Bearer ${getTokenFor(ownerUser)}` + }, + payload: { + name: 'registerUser', + arguments: [ + { + email, + password: 'function-payload-pass', + payload: { + tryingToAddCustomData: true, + source: 'function' + } + } + ] + } + }) + + expect(response.statusCode).toBe(200) + + const body = EJSON.deserialize(response.json()) as { + insertedId?: string | null + } + + expect(body.insertedId).toBeDefined() + expect(body.insertedId).not.toBeNull() + + const authUser = await client + .db(DB_NAME) + .collection(AUTH_USERS_COLLECTION) + .findOne({ email }) + + expect(authUser?.custom_data).toEqual({ + tryingToAddCustomData: true, + source: 'function' + }) + }) + it('deletes auth users via deleteUser function', async () => { const email = 'service-delete@example.com' const registration = await appInstance!.inject({ @@ -1543,8 +1587,8 @@ describe('MongoDB Atlas rule enforcement (e2e)', () => { const res = (await getTodosCollection(ownerUser) .aggregate(pipeline) .toArray()) as Array<{ - projects?: ProjectDoc[] - }> + projects?: ProjectDoc[] + }> const projects = res.flatMap((item) => item.projects ?? []) expect(projects.length).toBeGreaterThan(0) diff --git a/tests/e2e/mongodb-atlas.rules.e2e.custom-user-data.test.ts b/tests/e2e/mongodb-atlas.rules.e2e.custom-user-data.test.ts new file mode 100644 index 00000000..7d494d4b --- /dev/null +++ b/tests/e2e/mongodb-atlas.rules.e2e.custom-user-data.test.ts @@ -0,0 +1,275 @@ +import fs from 'node:fs' +import os from 'node:os' +import path from 'node:path' +import { MongoClient, ObjectId } from 'mongodb' +import { hashPassword } from '../../packages/flowerbase/src/utils/crypto' + +jest.setTimeout(120000) + +const API_VERSION = '/api/client/v2.0' +const PROJECT_ID = 'flowerbase-e2e-custom-user-data' +const AUTH_USERS_COLLECTION = 'auth_users' +const USER_COLLECTION = 'users' + +const SOURCE_APP_ROOT = path.join(__dirname, 'app') + +const resolveMongoUrl = () => { + const value = process.env.DB_CONNECTION_STRING?.trim() + return value && value.length > 0 ? value : 'mongodb://localhost:27017' +} + +const copyFixtureApp = (customUserData: Record) => { + const tempRoot = fs.mkdtempSync( + path.join(os.tmpdir(), 'flowerbase-e2e-custom-user-data-') + ) + + fs.cpSync(SOURCE_APP_ROOT, tempRoot, { + recursive: true + }) + + fs.writeFileSync( + path.join(tempRoot, 'auth', 'custom_user_data.json'), + JSON.stringify(customUserData, null, 2) + ) + + return tempRoot +} + +const startApp = async ({ + enabled, + dbName +}: { + enabled: boolean + dbName: string +}) => { + jest.resetModules() + + const appRoot = copyFixtureApp({ + enabled, + mongo_service_name: 'mongodb-atlas', + database_name: dbName, + collection_name: 'users', + user_id_field: 'id', + on_user_creation_function_name: enabled ? 'onCreateUser' : '' + }) + + process.env.FLOWERBASE_APP_PATH = appRoot + + const { initialize } = await import('../../packages/flowerbase/src') + const { StateManager } = await import('../../packages/flowerbase/src/state') + + await initialize({ + projectId: PROJECT_ID, + mongodbUrl: resolveMongoUrl(), + jwtSecret: 'e2e-secret', + port: 0, + host: '127.0.0.1', + basePath: appRoot + }) + + const appInstance = StateManager.select('app') + + return { + appRoot, + appInstance + } +} + +let client: MongoClient +let apps: Array<{ close: () => Promise }> = [] + +beforeAll(async () => { + client = new MongoClient(resolveMongoUrl(), { + serverSelectionTimeoutMS: 60000 + }) + + await client.connect() +}) + +afterEach(async () => { + for (const app of apps) { + await app.close() + } + + apps = [] + + delete process.env.FLOWERBASE_APP_PATH + jest.resetModules() +}) + +afterAll(async () => { + await client.close() +}) + +it('uses linked custom user collection when custom_user_data is enabled', async () => { + const dbName = 'flowerbase-e2e-cud-enabled' + const db = client.db(dbName) + + await db.collection(AUTH_USERS_COLLECTION).deleteMany({}) + await db.collection(USER_COLLECTION).deleteMany({}) + await db.collection('auth_refresh_tokens').deleteMany({}) + + const { appInstance } = await startApp({ + enabled: true, + dbName + }) + + apps.push(appInstance!) + + const authUserId = new ObjectId() + const email = 'enabled-custom-data@example.com' + const password = 'enabled-pass' + + await db.collection(AUTH_USERS_COLLECTION).insertOne({ + _id: authUserId, + email, + password: await hashPassword(password), + status: 'confirmed', + custom_data: { + role: 'from-auth-users', + source: 'auth_users' + }, + identities: [ + { + provider_type: 'local-userpass', + provider_data: { email } + } + ] + }) + + await db.collection(USER_COLLECTION).insertOne({ + _id: new ObjectId(), + id: authUserId.toString(), + role: 'from-users-collection', + source: 'users', + tenantId: 'tenant-enabled' + }) + + const login = await appInstance!.inject({ + method: 'POST', + url: `${API_VERSION}/app/${PROJECT_ID}/auth/providers/local-userpass/login`, + payload: { + username: email, + password + } + }) + + expect(login.statusCode).toBe(200) + + const loginBody = login.json() as { + access_token?: string + } + + expect(loginBody.access_token).toBeDefined() + + const profile = await appInstance!.inject({ + method: 'GET', + url: `${API_VERSION}/auth/profile`, + headers: { + authorization: `Bearer ${loginBody.access_token}` + } + }) + + expect(profile.statusCode).toBe(200) + + const profileBody = profile.json() as { + custom_data?: Record + } + + expect(profileBody.custom_data).toEqual( + expect.objectContaining({ + id: authUserId.toString(), + role: 'from-users-collection', + source: 'users', + tenantId: 'tenant-enabled' + }) + ) + + expect(profileBody.custom_data?.role).not.toBe('from-auth-users') +}) + +it('uses auth_users.custom_data when custom_user_data is disabled', async () => { + const dbName = 'flowerbase-e2e-cud-disabled' + const db = client.db(dbName) + + await db.collection(AUTH_USERS_COLLECTION).deleteMany({}) + await db.collection(USER_COLLECTION).deleteMany({}) + await db.collection('auth_refresh_tokens').deleteMany({}) + + const { appInstance } = await startApp({ + enabled: false, + dbName + }) + + apps.push(appInstance!) + + const authUserId = new ObjectId() + const email = 'disabled-custom-data@example.com' + const password = 'disabled-pass' + + await db.collection(AUTH_USERS_COLLECTION).insertOne({ + _id: authUserId, + email, + password: await hashPassword(password), + status: 'confirmed', + custom_data: { + role: 'from-auth-users', + source: 'auth_users', + tenantId: 'tenant-disabled', + tryingToAddCustomData: true + }, + identities: [ + { + provider_type: 'local-userpass', + provider_data: { email } + } + ] + }) + + await db.collection(USER_COLLECTION).insertOne({ + _id: new ObjectId(), + id: authUserId.toString(), + role: 'this-should-not-be-used', + source: 'users' + }) + + const login = await appInstance!.inject({ + method: 'POST', + url: `${API_VERSION}/app/${PROJECT_ID}/auth/providers/local-userpass/login`, + payload: { + username: email, + password + } + }) + + expect(login.statusCode).toBe(200) + + const loginBody = login.json() as { + access_token?: string + } + + expect(loginBody.access_token).toBeDefined() + + const profile = await appInstance!.inject({ + method: 'GET', + url: `${API_VERSION}/auth/profile`, + headers: { + authorization: `Bearer ${loginBody.access_token}` + } + }) + + expect(profile.statusCode).toBe(200) + + const profileBody = profile.json() as { + custom_data?: Record + } + + expect(profileBody.custom_data).toEqual({ + role: 'from-auth-users', + source: 'auth_users', + tenantId: 'tenant-disabled', + tryingToAddCustomData: true + }) + + expect(profileBody.custom_data?.role).not.toBe('this-should-not-be-used') +}) \ No newline at end of file diff --git a/tests/e2e/mongodb-atlas.rules.e2e.functions-and-auth-flow.test.ts b/tests/e2e/mongodb-atlas.rules.e2e.functions-and-auth-flow.test.ts index bc10664d..9f0b0abd 100644 --- a/tests/e2e/mongodb-atlas.rules.e2e.functions-and-auth-flow.test.ts +++ b/tests/e2e/mongodb-atlas.rules.e2e.functions-and-auth-flow.test.ts @@ -795,6 +795,39 @@ describe('MongoDB Atlas rule enforcement (e2e)', () => { expect(loginBody.user_id).toBe(authUserIds.owner.toString()) }) + it('persists registration payload as custom_data', async () => { + const email = 'payload-user@example.com' + + const registration = await appInstance!.inject({ + method: 'POST', + url: `${AUTH_BASE_URL}/register`, + payload: { + email, + password: 'payload-pass', + payload: { + tryingToAddCustomData: true, + role: 'student' + } + } + }) + + expect(registration.statusCode).toBe(201) + + const registrationBody = registration.json() as { userId?: string } + expect(registrationBody.userId).toBeDefined() + + const authUser = await client + .db(DB_NAME) + .collection(AUTH_USERS_COLLECTION) + .findOne({ _id: new ObjectId(registrationBody.userId) }) + + expect(authUser).toBeDefined() + expect(authUser?.custom_data).toEqual({ + tryingToAddCustomData: true, + role: 'student' + }) + }) + it('runs confirmation function when autoConfirm is false', async () => { const originalConfig = AUTH_CONFIG.localUserpassConfig AUTH_CONFIG.localUserpassConfig = { @@ -1138,6 +1171,40 @@ describe('MongoDB Atlas rule enforcement (e2e)', () => { expect(afterCount).toBe(beforeCount) }) + it('passes custom_data to on_user_creation function on auto-confirmed registrations', async () => { + const email = 'autoconfirm-trigger-payload@example.com' + + const registration = await appInstance!.inject({ + method: 'POST', + url: `${AUTH_BASE_URL}/register`, + payload: { + email, + password: 'auto-pass', + payload: { + tryingToAddCustomData: true, + role: 'student' + } + } + }) + + expect(registration.statusCode).toBe(201) + + const registrationBody = registration.json() as { userId?: string } + expect(registrationBody.userId).toBeDefined() + + const creationEvent = await waitForTriggerEventType( + registrationBody.userId!, + 'on_user_creation' + ) + + expect(creationEvent).toBeDefined() + expect(creationEvent?.email).toBe(email) + expect(creationEvent?.customData).toEqual({ + tryingToAddCustomData: true, + role: 'student' + }) + }) + it('fires delete trigger when auth user is removed', async () => { const registration = await appInstance!.inject({ method: 'POST',