From 4c433e9e75c838cbda7af280ec79ae614b1853b0 Mon Sep 17 00:00:00 2001 From: "DHENRY - Mytinydc.com" Date: Mon, 19 Feb 2024 17:10:50 +0100 Subject: [PATCH] Version 1.4.0 (#17) * implementation & compatibility with existing systems * change model && prettier * bypass authentication with development mode * add route getGroups * swagger with development mode * new external component multiselect * changing model * users/groups management * fix loop when not connected * fix overflow * UI - users/groups/controls management * wip reviewf of routes * review of routes * test improvements * user context useless * item of db is named control from now * search control implementation * bug fix to call ci/cd * fix when ci/cd fetch returned an error * bug fix to call ci/cd entrypoint with wrong curl command * user information called up when the component is mounted * remove comments relating to swagger, which are too difficult to maintain * review of the token authentication process * search component * remove swaggerjsdoc & update storybook@7.6.15 * change password improvements * bug fix changebearertoken * autocomplete off for search input * prepa update version & database patch * search auto focus on load * loadership during compare process * add or edit control only via UI * bug UI: After comparison display immediate execution date * version 1.4.0 --- README-fr.md | 9 +- README.md | 9 +- client/package-lock.json | 889 ++++++++---------- client/package.json | 17 +- client/src/App.tsx | 25 +- client/src/api/mytinydcUPDONApi.ts | 37 +- client/src/app/Router.tsx | 33 +- client/src/app/contextSlice.ts | 20 +- client/src/app/css/loadership.scss | 90 ++ client/src/components/Block.scss | 3 +- client/src/components/Control.scss | 1 + client/src/components/Control.tsx | 22 +- client/src/components/Dialog.tsx | 6 +- .../src/components/FieldSetApiEntrypoint.tsx | 6 +- client/src/components/Header.tsx | 54 +- .../src/components/ResultCompare.stories.tsx | 2 +- client/src/components/ResultCompare.tsx | 44 +- client/src/components/ScrapProduction.scss | 15 +- .../components/ScrapProduction.stories.tsx | 8 +- client/src/components/ScrapProduction.tsx | 82 +- client/src/components/Search.scss | 28 + client/src/components/Search.stories.tsx | 50 + client/src/components/Search.tsx | 44 + client/src/components/Summary.tsx | 11 +- .../changepassword/ChangePassword.tsx | 5 +- .../controlmanagement/ControlManager.tsx | 15 +- .../features/curlcommands/CurlCommands.tsx | 7 +- .../displaycontrols/DisplayControls.tsx | 37 +- client/src/features/login/PageLogin.tsx | 7 +- .../src/features/usermanager/UserManager.scss | 30 +- .../src/features/usermanager/UserManager.tsx | 193 +++- client/src/helpers/UiMiscHelper.ts | 9 + doc/GROUPS.md | 31 + doc/INSTALL.md | 2 +- doc/assets/Screenshot_addGroup.png | Bin 0 -> 12784 bytes doc/assets/Screenshot_setGroupsControl.png | Bin 0 -> 31130 bytes doc/assets/Screenshot_usersgroupsmanager.png | Bin 0 -> 19225 bytes doc/en/GROUPS.md | 31 + doc/en/INSTALL.md | 2 +- locales/fr.json | 14 +- openapi.yaml | 459 +-------- package-lock.json | 203 +--- package.json | 5 +- src/Constants-dev.ts | 1 + src/Constants.ts | 5 +- src/Global.types.ts | 33 +- src/genSwaggerJson.ts | 42 - src/lib/Authentification.ts | 382 ++++++-- src/lib/Database.ts | 46 +- src/lib/Features.ts | 2 +- src/lib/PatchVersion.ts | 15 + src/main.ts | 45 +- src/routes/routerActions.ts | 157 +--- src/routes/routerAuth.ts | 590 ++++-------- src/routes/routerControls.ts | 153 ++- src/routes/routerCore.ts | 233 +---- test/Authentification.test.ts | 339 +++++-- test/Database.test.ts | 180 +++- test/Features.test.ts | 1 + test/Groups.test.ts | 346 +++++++ 60 files changed, 2764 insertions(+), 2361 deletions(-) create mode 100644 client/src/app/css/loadership.scss create mode 100644 client/src/components/Search.scss create mode 100644 client/src/components/Search.stories.tsx create mode 100644 client/src/components/Search.tsx create mode 100644 doc/GROUPS.md create mode 100644 doc/assets/Screenshot_addGroup.png create mode 100644 doc/assets/Screenshot_setGroupsControl.png create mode 100644 doc/assets/Screenshot_usersgroupsmanager.png create mode 100644 doc/en/GROUPS.md delete mode 100644 src/genSwaggerJson.ts create mode 100644 src/lib/PatchVersion.ts create mode 100644 test/Groups.test.ts diff --git a/README-fr.md b/README-fr.md index 03c5633..5a7bc76 100644 --- a/README-fr.md +++ b/README-fr.md @@ -37,6 +37,7 @@ Les versions "Release Candidats" seront définies comme suit : "[\d+]\.[\d+]\.[\ - http://[Addresse IP]:[port]/ - login/mot de passe par défaut: admin/admin - Vous changez le mot de passe. +- [Creation des utilisateurs & groupes](./doc/GROUPS.md) - [Création du premier "contrôle" (qui peut être votre nouveau service UTDON... pour vérifier que tout fonctionne)](./doc/CONTROL.md) - Vous exécuter la comparaison. - Chaque contrôle indique son dernier état de "comparaison". @@ -102,9 +103,7 @@ Les sessions sont gérées en RAM, un simple redémarrage du service réinitiali - Authentification Github pour supprimer la barrière "rate-limit". - Dupliquer un contrôle. -- Filtres pour l'affichage. -- Classement des contrôles par groupes. -- Plusieurs "Auth Token" par contrôle pour éviter de fournir le jeton d'authentification de l'admin. +- Ajout d'un token "readonly" par utilisateur pour utilisation à partir d'une chaîne CI/CD - Stockage S3. - Entrypoint API metrics. - Authentification LDAP. @@ -115,6 +114,10 @@ Les sessions sont gérées en RAM, un simple redémarrage du service réinitiali - Radioactive button : +- Multiselect component https://github.com/hc-oss/react-multi-select-component?tab=readme-ov-file + +- LoaderShip: https://www.loadership.com/ + - Logo: ## Si vous appréciez cette application diff --git a/README.md b/README.md index aaf8339..12e1749 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,7 @@ Release Candidates will be defined as follows: "[\d+]\.[\d+]\.[\d+]-rc-[\d+]". - http://[IP address]:[port]/ - default login/password: admin/admin - Change the password. +- [Create users & groups](./doc/en/GROUPS.md) - [Create the first "control" (which may be your new UTDON service... to check that everything is working)](./doc/en/CONTROL.md) - You run the comparison. - Each control indicates its last "comparison" status. @@ -102,9 +103,7 @@ Sessions are managed in RAM, so a simple service restart resets all sessions. - Github authentication to remove rate-limit barrier. - Duplicate a control. -- Display filters. -- Controls sorted into groups. -- Multiple "Auth Token" per control to avoid providing admin authentication token. +- Addition of a "readonly" token per user for use with a CI/CD chain - S3 storage. - Entrypoint API metrics. - LDAP authentication. @@ -115,6 +114,10 @@ Sessions are managed in RAM, so a simple service restart resets all sessions. - Radioactive button: +- Multiselect component https://github.com/hc-oss/react-multi-select-component?tab=readme-ov-file + +- LoaderShip: https://www.loadership.com/ + - Logo: ## If you like this application diff --git a/client/package-lock.json b/client/package-lock.json index f3f8214..ad96f78 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -1,12 +1,12 @@ { "name": "mytinydc-utdon-client", - "version": "1.3.0", + "version": "1.4.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mytinydc-utdon-client", - "version": "1.3.0", + "version": "1.4.0", "license": "AGPL-3.0", "dependencies": { "@reduxjs/toolkit": "^1.9.7", @@ -14,16 +14,17 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-intl": "^6.5.2", + "react-multi-select-component": "^4.3.4", "react-redux": "^8.1.3", "react-router-dom": "^6.18.0" }, "devDependencies": { - "@storybook/addon-essentials": "^7.6.4", - "@storybook/addon-interactions": "^7.6.4", - "@storybook/addon-links": "^7.6.4", - "@storybook/blocks": "^7.6.4", - "@storybook/react": "^7.6.4", - "@storybook/react-vite": "^7.6.4", + "@storybook/addon-essentials": "^7.6.15", + "@storybook/addon-interactions": "^7.6.15", + "@storybook/addon-links": "^7.6.15", + "@storybook/blocks": "^7.6.15", + "@storybook/react": "^7.6.15", + "@storybook/react-vite": "^7.6.15", "@storybook/testing-library": "^0.2.2", "@types/react": "^18.2.15", "@types/react-dom": "^18.2.7", @@ -35,7 +36,7 @@ "eslint-plugin-react-refresh": "^0.4.3", "eslint-plugin-storybook": "^0.6.15", "sass": "^1.69.5", - "storybook": "^7.6.4", + "storybook": "^7.6.15", "storybook-addon-react-router-v6": "^2.0.10", "typescript": "^5.0.2", "vite": "^4.4.5" @@ -254,9 +255,9 @@ } }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.23.6.tgz", - "integrity": "sha512-cBXU1vZni/CpGF29iTu4YRbOZt3Wat6zCoMDxRF1MayiEc4URxOj31tT65HUM0CRpMowA3HCJaAOVOUnMf96cw==", + "version": "7.23.10", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.23.10.tgz", + "integrity": "sha512-2XpP2XhkXzgxecPNEEK8Vz8Asj9aRxt08oKOqtiZoqV2UGZ5T+EkyP9sXQ9nwMxBIG34a7jmasVqoMop7VdPUw==", "dev": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", @@ -294,9 +295,9 @@ } }, "node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.4.4.tgz", - "integrity": "sha512-QcJMILQCu2jm5TFPGA3lCpJJTeEP+mqeXooG/NZbg/h5FTFi6V0+99ahlRsW8/kRLyb24LZVCCiclDedhLKcBA==", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.5.0.tgz", + "integrity": "sha512-NovQquuQLAQ5HuyjCz7WQP9MjRj7dx++yspwiyUiGl9ZyadHRSql1HZh5ogRd8W8w6YM6EQ/NTB8rgjLt5W65Q==", "dev": true, "dependencies": { "@babel/helper-compilation-targets": "^7.22.6", @@ -662,9 +663,9 @@ } }, "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.23.3.tgz", - "integrity": "sha512-XaJak1qcityzrX0/IU5nKHb34VaibwP3saKqG6a/tppelgllOH13LUann4ZCIBcVOeE6H18K4Vx9QKkVww3z/w==", + "version": "7.23.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.23.7.tgz", + "integrity": "sha512-LlRT7HgaifEpQA1ZgLVOIJZZFVPWN5iReq/7/JixwBtwcoeVGDBD53ZV28rrsLYOZs1Y/EHhA8N/Z6aazHR8cw==", "dev": true, "dependencies": { "@babel/helper-environment-visitor": "^7.22.20", @@ -985,9 +986,9 @@ } }, "node_modules/@babel/plugin-transform-async-generator-functions": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.23.4.tgz", - "integrity": "sha512-efdkfPhHYTtn0G6n2ddrESE91fgXxjlqLsnUtPWnJs4a4mZIbUaK7ffqKIIUKXSHwcDvaCVX6GXkaJJFqtX7jw==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.23.9.tgz", + "integrity": "sha512-8Q3veQEDGe14dTYuwagbRtwxQDnytyg1JFu4/HwEMETeofocrB0U0ejBJIXoeG/t2oXZ8kzCyI0ZZfbT80VFNQ==", "dev": true, "dependencies": { "@babel/helper-environment-visitor": "^7.22.20", @@ -1083,16 +1084,15 @@ } }, "node_modules/@babel/plugin-transform-classes": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.23.5.tgz", - "integrity": "sha512-jvOTR4nicqYC9yzOHIhXG5emiFEOpappSJAl73SDSEDcybD+Puuze8Tnpb9p9qEyYup24tq891gkaygIFvWDqg==", + "version": "7.23.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.23.8.tgz", + "integrity": "sha512-yAYslGsY1bX6Knmg46RjiCiNSwJKv2IUC8qOdYKqMMr0491SXFhcHqOdRDeCRohOOIzwN/90C6mQ9qAKgrP7dg==", "dev": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-compilation-targets": "^7.22.15", + "@babel/helper-compilation-targets": "^7.23.6", "@babel/helper-environment-visitor": "^7.22.20", "@babel/helper-function-name": "^7.23.0", - "@babel/helper-optimise-call-expression": "^7.22.5", "@babel/helper-plugin-utils": "^7.22.5", "@babel/helper-replace-supers": "^7.22.20", "@babel/helper-split-export-declaration": "^7.22.6", @@ -1360,9 +1360,9 @@ } }, "node_modules/@babel/plugin-transform-modules-systemjs": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.23.3.tgz", - "integrity": "sha512-ZxyKGTkF9xT9YJuKQRo19ewf3pXpopuYQd8cDXqNzc3mUNbOME0RKMoZxviQk74hwzfQsEe66dE92MaZbdHKNQ==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.23.9.tgz", + "integrity": "sha512-KDlPRM6sLo4o1FkiSlXoAa8edLXFsKKIda779fbLrvmeuc3itnjCtaO6RrtoaANsIJANj+Vk1zqbZIMhkCAHVw==", "dev": true, "dependencies": { "@babel/helper-hoist-variables": "^7.22.5", @@ -1807,9 +1807,9 @@ } }, "node_modules/@babel/preset-env": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.23.6.tgz", - "integrity": "sha512-2XPn/BqKkZCpzYhUUNZ1ssXw7DcXfKQEjv/uXZUXgaebCMYmkEsfZ2yY+vv+xtXv50WmL5SGhyB6/xsWxIvvOQ==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.23.9.tgz", + "integrity": "sha512-3kBGTNBBk9DQiPoXYS0g0BYlwTQYUTifqgKTjxUwEUkduRT2QOa0FPGBJ+NROQhGyYO5BuTJwGvBnqKDykac6A==", "dev": true, "dependencies": { "@babel/compat-data": "^7.23.5", @@ -1818,7 +1818,7 @@ "@babel/helper-validator-option": "^7.23.5", "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.23.3", "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.23.3", - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.23.3", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.23.7", "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", "@babel/plugin-syntax-async-generators": "^7.8.4", "@babel/plugin-syntax-class-properties": "^7.12.13", @@ -1839,13 +1839,13 @@ "@babel/plugin-syntax-top-level-await": "^7.14.5", "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", "@babel/plugin-transform-arrow-functions": "^7.23.3", - "@babel/plugin-transform-async-generator-functions": "^7.23.4", + "@babel/plugin-transform-async-generator-functions": "^7.23.9", "@babel/plugin-transform-async-to-generator": "^7.23.3", "@babel/plugin-transform-block-scoped-functions": "^7.23.3", "@babel/plugin-transform-block-scoping": "^7.23.4", "@babel/plugin-transform-class-properties": "^7.23.3", "@babel/plugin-transform-class-static-block": "^7.23.4", - "@babel/plugin-transform-classes": "^7.23.5", + "@babel/plugin-transform-classes": "^7.23.8", "@babel/plugin-transform-computed-properties": "^7.23.3", "@babel/plugin-transform-destructuring": "^7.23.3", "@babel/plugin-transform-dotall-regex": "^7.23.3", @@ -1861,7 +1861,7 @@ "@babel/plugin-transform-member-expression-literals": "^7.23.3", "@babel/plugin-transform-modules-amd": "^7.23.3", "@babel/plugin-transform-modules-commonjs": "^7.23.3", - "@babel/plugin-transform-modules-systemjs": "^7.23.3", + "@babel/plugin-transform-modules-systemjs": "^7.23.9", "@babel/plugin-transform-modules-umd": "^7.23.3", "@babel/plugin-transform-named-capturing-groups-regex": "^7.22.5", "@babel/plugin-transform-new-target": "^7.23.3", @@ -1887,9 +1887,9 @@ "@babel/plugin-transform-unicode-regex": "^7.23.3", "@babel/plugin-transform-unicode-sets-regex": "^7.23.3", "@babel/preset-modules": "0.1.6-no-external-plugins", - "babel-plugin-polyfill-corejs2": "^0.4.6", - "babel-plugin-polyfill-corejs3": "^0.8.5", - "babel-plugin-polyfill-regenerator": "^0.5.3", + "babel-plugin-polyfill-corejs2": "^0.4.8", + "babel-plugin-polyfill-corejs3": "^0.9.0", + "babel-plugin-polyfill-regenerator": "^0.5.5", "core-js-compat": "^3.31.0", "semver": "^6.3.1" }, @@ -1951,15 +1951,15 @@ } }, "node_modules/@babel/register": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/register/-/register-7.22.15.tgz", - "integrity": "sha512-V3Q3EqoQdn65RCgTLwauZaTfd1ShhwPmbBv+1dkZV/HpCGMKVyn6oFcRlI7RaKqiDQjX2Qd3AuoEguBgdjIKlg==", + "version": "7.23.7", + "resolved": "https://registry.npmjs.org/@babel/register/-/register-7.23.7.tgz", + "integrity": "sha512-EjJeB6+kvpk+Y5DAkEAmbOBEFkh9OASx0huoEkqYTFxAZHzOAX2Oh5uwAUuL2rUddqfM0SA+KPXV2TbzoZ2kvQ==", "dev": true, "dependencies": { "clone-deep": "^4.0.1", "find-cache-dir": "^2.0.0", "make-dir": "^2.1.0", - "pirates": "^4.0.5", + "pirates": "^4.0.6", "source-map-support": "^0.5.16" }, "engines": { @@ -3562,12 +3562,12 @@ "dev": true }, "node_modules/@storybook/addon-actions": { - "version": "7.6.6", - "resolved": "https://registry.npmjs.org/@storybook/addon-actions/-/addon-actions-7.6.6.tgz", - "integrity": "sha512-mLJip9Evb2Chj7ymKbpaybe5NgDy3Du7oSWeURPy/0qXJ2cBqHWnhZ8CTK2DasrstsUhQSJaZVXHhaENT+fn+g==", + "version": "7.6.15", + "resolved": "https://registry.npmjs.org/@storybook/addon-actions/-/addon-actions-7.6.15.tgz", + "integrity": "sha512-2Jfvbahe/tmq1iNnNxmcP0JnX0rqCuijjXXai9yMDV3koIMawn6t88MPVrdcso5ch/fxE45522nZqA3SZJbM4g==", "dev": true, "dependencies": { - "@storybook/core-events": "7.6.6", + "@storybook/core-events": "7.6.15", "@storybook/global": "^5.0.0", "@types/uuid": "^9.0.1", "dequal": "^2.0.2", @@ -3580,9 +3580,9 @@ } }, "node_modules/@storybook/addon-backgrounds": { - "version": "7.6.6", - "resolved": "https://registry.npmjs.org/@storybook/addon-backgrounds/-/addon-backgrounds-7.6.6.tgz", - "integrity": "sha512-w5dZ/0cOe55M2G/Lx9f+Ptk4txUPb+Ng+KqEvTaTNqHoh0Xw4QxEn/ciJwmh1u1g3aMZsOgOvwPwug7ykmLgsA==", + "version": "7.6.15", + "resolved": "https://registry.npmjs.org/@storybook/addon-backgrounds/-/addon-backgrounds-7.6.15.tgz", + "integrity": "sha512-t0wWZiLHUoxP1GqSR44Zt+mI6cq17dAtpX/aC9I1xGl4xKUizmZjjX9GcH2EjcIiuKBER0ouQtQcDNyV939VvA==", "dev": true, "dependencies": { "@storybook/global": "^5.0.0", @@ -3595,12 +3595,12 @@ } }, "node_modules/@storybook/addon-controls": { - "version": "7.6.6", - "resolved": "https://registry.npmjs.org/@storybook/addon-controls/-/addon-controls-7.6.6.tgz", - "integrity": "sha512-VAXXfPLi1M3RXhBf3uIBZ2hrD9UPDe7yvXHIlCzgj1HIJELODCFyUc+RtvN0mPc/nnlEfzhGfJtenZou5LYwIw==", + "version": "7.6.15", + "resolved": "https://registry.npmjs.org/@storybook/addon-controls/-/addon-controls-7.6.15.tgz", + "integrity": "sha512-HXcG/Lr4ri7WUFz14Y5lEBTA1XmKy0E/DepW88XVy6YNsTpERVWEBcvjKoLAU1smKrfhVto96hK2AVFL3A8EBQ==", "dev": true, "dependencies": { - "@storybook/blocks": "7.6.6", + "@storybook/blocks": "7.6.15", "lodash": "^4.17.21", "ts-dedent": "^2.0.0" }, @@ -3610,26 +3610,26 @@ } }, "node_modules/@storybook/addon-docs": { - "version": "7.6.6", - "resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-7.6.6.tgz", - "integrity": "sha512-l4gtoNTn1wHE11x44te1cDkqfm+/w+eNonHe56bwgSqETclS5z18wvM9bQZF32G6C9fpSefaJW3cxVvcuJL1fg==", + "version": "7.6.15", + "resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-7.6.15.tgz", + "integrity": "sha512-UPODqO+mrYaKyTSAtfRslxOFgSP/v/5vfDx896pbNTC4Sf8xLytoudw4I14hzkHmRdXiOnd21FqXJfmF/Onsvw==", "dev": true, "dependencies": { "@jest/transform": "^29.3.1", "@mdx-js/react": "^2.1.5", - "@storybook/blocks": "7.6.6", - "@storybook/client-logger": "7.6.6", - "@storybook/components": "7.6.6", - "@storybook/csf-plugin": "7.6.6", - "@storybook/csf-tools": "7.6.6", + "@storybook/blocks": "7.6.15", + "@storybook/client-logger": "7.6.15", + "@storybook/components": "7.6.15", + "@storybook/csf-plugin": "7.6.15", + "@storybook/csf-tools": "7.6.15", "@storybook/global": "^5.0.0", "@storybook/mdx2-csf": "^1.0.0", - "@storybook/node-logger": "7.6.6", - "@storybook/postinstall": "7.6.6", - "@storybook/preview-api": "7.6.6", - "@storybook/react-dom-shim": "7.6.6", - "@storybook/theming": "7.6.6", - "@storybook/types": "7.6.6", + "@storybook/node-logger": "7.6.15", + "@storybook/postinstall": "7.6.15", + "@storybook/preview-api": "7.6.15", + "@storybook/react-dom-shim": "7.6.15", + "@storybook/theming": "7.6.15", + "@storybook/types": "7.6.15", "fs-extra": "^11.1.0", "remark-external-links": "^8.0.0", "remark-slug": "^6.0.0", @@ -3645,24 +3645,24 @@ } }, "node_modules/@storybook/addon-essentials": { - "version": "7.6.6", - "resolved": "https://registry.npmjs.org/@storybook/addon-essentials/-/addon-essentials-7.6.6.tgz", - "integrity": "sha512-OQ8A6r06mg/HvyIk/j2Gt9DK5Qtqgtwq2Ydm5IgVW6gZsuRnv1FAeUG6okf8oXowAzpYoHdsDmCVwNOAGWGO7w==", - "dev": true, - "dependencies": { - "@storybook/addon-actions": "7.6.6", - "@storybook/addon-backgrounds": "7.6.6", - "@storybook/addon-controls": "7.6.6", - "@storybook/addon-docs": "7.6.6", - "@storybook/addon-highlight": "7.6.6", - "@storybook/addon-measure": "7.6.6", - "@storybook/addon-outline": "7.6.6", - "@storybook/addon-toolbars": "7.6.6", - "@storybook/addon-viewport": "7.6.6", - "@storybook/core-common": "7.6.6", - "@storybook/manager-api": "7.6.6", - "@storybook/node-logger": "7.6.6", - "@storybook/preview-api": "7.6.6", + "version": "7.6.15", + "resolved": "https://registry.npmjs.org/@storybook/addon-essentials/-/addon-essentials-7.6.15.tgz", + "integrity": "sha512-m8OJtRG1/DEbFCQ1S6y/yKN3uWl9bsEn2ZsX5WcYmEt501BUbTPwpGOPyP57Q7nYYXKmWT2375Uq1qauwcD6NA==", + "dev": true, + "dependencies": { + "@storybook/addon-actions": "7.6.15", + "@storybook/addon-backgrounds": "7.6.15", + "@storybook/addon-controls": "7.6.15", + "@storybook/addon-docs": "7.6.15", + "@storybook/addon-highlight": "7.6.15", + "@storybook/addon-measure": "7.6.15", + "@storybook/addon-outline": "7.6.15", + "@storybook/addon-toolbars": "7.6.15", + "@storybook/addon-viewport": "7.6.15", + "@storybook/core-common": "7.6.15", + "@storybook/manager-api": "7.6.15", + "@storybook/node-logger": "7.6.15", + "@storybook/preview-api": "7.6.15", "ts-dedent": "^2.0.0" }, "funding": { @@ -3675,9 +3675,9 @@ } }, "node_modules/@storybook/addon-highlight": { - "version": "7.6.6", - "resolved": "https://registry.npmjs.org/@storybook/addon-highlight/-/addon-highlight-7.6.6.tgz", - "integrity": "sha512-B85UaCts2uMpa0yHBSnupzy2WCdW4vfB+lfaBug9beyOyZQdel07BumblE0KwSJftYgdCNPUZ5MRlqEDzMLTWQ==", + "version": "7.6.15", + "resolved": "https://registry.npmjs.org/@storybook/addon-highlight/-/addon-highlight-7.6.15.tgz", + "integrity": "sha512-ptidWZJJcEM83YsxCjf+m1q8Rr9sN8piJ4PJlM2vyc4MLZY4q6htb1JJFeq3ov1Iz6SY9KjKc/zOkWo4L54nxw==", "dev": true, "dependencies": { "@storybook/global": "^5.0.0" @@ -3688,13 +3688,13 @@ } }, "node_modules/@storybook/addon-interactions": { - "version": "7.6.6", - "resolved": "https://registry.npmjs.org/@storybook/addon-interactions/-/addon-interactions-7.6.6.tgz", - "integrity": "sha512-EJWx6ciJPgv1c75tB/M4smWDpPDGM/L24v4DZxGpl1eV3oQOSQCKImG5btwoy6QcIi68ozroUHdUti/kzCKS1w==", + "version": "7.6.15", + "resolved": "https://registry.npmjs.org/@storybook/addon-interactions/-/addon-interactions-7.6.15.tgz", + "integrity": "sha512-wg8daQcxVjfC+OtZdgWE6YVnySzYhpA7SWf+rkUugkX/fwMmsxmJ1WwAr7zW5KYY4W6uhszCVPjgwvFgpd2MTg==", "dev": true, "dependencies": { "@storybook/global": "^5.0.0", - "@storybook/types": "7.6.6", + "@storybook/types": "7.6.15", "jest-mock": "^27.0.6", "polished": "^4.2.2", "ts-dedent": "^2.2.0" @@ -3705,9 +3705,9 @@ } }, "node_modules/@storybook/addon-links": { - "version": "7.6.6", - "resolved": "https://registry.npmjs.org/@storybook/addon-links/-/addon-links-7.6.6.tgz", - "integrity": "sha512-NEcqOz6zZ1dJnCcVmYdaQTAMAGIb8NFAZGnr9DU0q+t4B1fTaWUgqLtBM5V6YqIrXGSC/oKLpjWUkS5UpswlHA==", + "version": "7.6.15", + "resolved": "https://registry.npmjs.org/@storybook/addon-links/-/addon-links-7.6.15.tgz", + "integrity": "sha512-DEBlut3ofpggbm8N7n3f/Xdi6KkjKps2hnL5blz5aQ7iSJJPT683GDP2CKjhtrlrL6+uJyEHWDLoECVq2kveaQ==", "dev": true, "dependencies": { "@storybook/csf": "^0.1.2", @@ -3728,9 +3728,9 @@ } }, "node_modules/@storybook/addon-measure": { - "version": "7.6.6", - "resolved": "https://registry.npmjs.org/@storybook/addon-measure/-/addon-measure-7.6.6.tgz", - "integrity": "sha512-b4hyCudlhsbYN1We8pfZHZJ0i0sfC8+GJvrqZQqdSqGicUmA00mggY1GE+gEoHziQ5/4auxFRS3HfUgaQWUNjg==", + "version": "7.6.15", + "resolved": "https://registry.npmjs.org/@storybook/addon-measure/-/addon-measure-7.6.15.tgz", + "integrity": "sha512-3csc8Vu/wDkgpuHprl9fbKKym/+nR8HBvcALPLlH2MWnlU3DEURrj/ykRKWlp7G3F5eqDIcaIEjq6xiBZyWg7Q==", "dev": true, "dependencies": { "@storybook/global": "^5.0.0", @@ -3742,9 +3742,9 @@ } }, "node_modules/@storybook/addon-outline": { - "version": "7.6.6", - "resolved": "https://registry.npmjs.org/@storybook/addon-outline/-/addon-outline-7.6.6.tgz", - "integrity": "sha512-BMjpjzNEnN8LC7JK92WCXyWgmJwAaEQjRDinr7eD4cBt4Uas5kbciw1g8PtTnh0GbYUsImKao0nzakSVObAdzg==", + "version": "7.6.15", + "resolved": "https://registry.npmjs.org/@storybook/addon-outline/-/addon-outline-7.6.15.tgz", + "integrity": "sha512-5zYDWO0OIlFchYqSjRDmQv2mPMwAwIDTocc00FMiQAaNqPZ+3ZP9L6kOng8YgwYWpPBecoHdLvSW6rTmcufHtw==", "dev": true, "dependencies": { "@storybook/global": "^5.0.0", @@ -3756,9 +3756,9 @@ } }, "node_modules/@storybook/addon-toolbars": { - "version": "7.6.6", - "resolved": "https://registry.npmjs.org/@storybook/addon-toolbars/-/addon-toolbars-7.6.6.tgz", - "integrity": "sha512-sQm5+FcoSMSGn1ioXHoukO6OhUlcNZil0/fonAY50uvp6Z4DyI0FTU7BKIm/NoMqAExQk3sZRfAC/nZZ9Epb0Q==", + "version": "7.6.15", + "resolved": "https://registry.npmjs.org/@storybook/addon-toolbars/-/addon-toolbars-7.6.15.tgz", + "integrity": "sha512-QougKS2eABB5Jd332i9tBpKgh2lN4aaqXkvmVC5egT5dOuJ9IeuZbGwiALef/uf1f3IuyUP41So9l2dI4u19aw==", "dev": true, "funding": { "type": "opencollective", @@ -3766,9 +3766,9 @@ } }, "node_modules/@storybook/addon-viewport": { - "version": "7.6.6", - "resolved": "https://registry.npmjs.org/@storybook/addon-viewport/-/addon-viewport-7.6.6.tgz", - "integrity": "sha512-/ijbzDf1Iq30LvZW2NE8cO4TeHusw0N+jTDUK1+vhxGNMFo9DUIgRkAi6VpFEfS0aQ5d82523WSWzVso7b/Hmg==", + "version": "7.6.15", + "resolved": "https://registry.npmjs.org/@storybook/addon-viewport/-/addon-viewport-7.6.15.tgz", + "integrity": "sha512-0esg0+onJftU2prD3n/sbxBTrTOIGQnZhbrKPP+/S26dVHuYaR/65XdwpRgXNY5PHK2yjU78HxiJP+Kyu75ntw==", "dev": true, "dependencies": { "memoizerific": "^1.11.3" @@ -3779,22 +3779,22 @@ } }, "node_modules/@storybook/blocks": { - "version": "7.6.6", - "resolved": "https://registry.npmjs.org/@storybook/blocks/-/blocks-7.6.6.tgz", - "integrity": "sha512-QLqkiSNrtGnh8RK9ipD63jVAUenkRu+72xR31DViZWRV9V8G2hzky5E/RoZWPEx+DfmBIUJ7Tcef6cCRcxEj9A==", + "version": "7.6.15", + "resolved": "https://registry.npmjs.org/@storybook/blocks/-/blocks-7.6.15.tgz", + "integrity": "sha512-ODP7AVh2iIGblI5WKGokWSHbp9YQHc+Uce7JCGcnDbNavoy64Z6R6G+wXzF5jfl7xQlbhQ8yQCuSSL4GNdYTeA==", "dev": true, "dependencies": { - "@storybook/channels": "7.6.6", - "@storybook/client-logger": "7.6.6", - "@storybook/components": "7.6.6", - "@storybook/core-events": "7.6.6", + "@storybook/channels": "7.6.15", + "@storybook/client-logger": "7.6.15", + "@storybook/components": "7.6.15", + "@storybook/core-events": "7.6.15", "@storybook/csf": "^0.1.2", - "@storybook/docs-tools": "7.6.6", + "@storybook/docs-tools": "7.6.15", "@storybook/global": "^5.0.0", - "@storybook/manager-api": "7.6.6", - "@storybook/preview-api": "7.6.6", - "@storybook/theming": "7.6.6", - "@storybook/types": "7.6.6", + "@storybook/manager-api": "7.6.15", + "@storybook/preview-api": "7.6.15", + "@storybook/theming": "7.6.15", + "@storybook/types": "7.6.15", "@types/lodash": "^4.14.167", "color-convert": "^2.0.1", "dequal": "^2.0.2", @@ -3818,15 +3818,15 @@ } }, "node_modules/@storybook/builder-manager": { - "version": "7.6.6", - "resolved": "https://registry.npmjs.org/@storybook/builder-manager/-/builder-manager-7.6.6.tgz", - "integrity": "sha512-96vmtUqh016H2n80xhvBZU2w5flTOzY7S0nW9nfxbY4UY4b39WajgwJ5wpg8l0YmCwQTrxCwY9/VE2Pd6CCqPA==", + "version": "7.6.15", + "resolved": "https://registry.npmjs.org/@storybook/builder-manager/-/builder-manager-7.6.15.tgz", + "integrity": "sha512-vfpfCywiasyP7vtbgLJhjssBEwUjZhBsRsubDAzumgOochPiKKPNwsSc5NU/4ZIGaC5zRO26kUaUqFIbJdTEUQ==", "dev": true, "dependencies": { "@fal-works/esbuild-plugin-global-externals": "^2.1.2", - "@storybook/core-common": "7.6.6", - "@storybook/manager": "7.6.6", - "@storybook/node-logger": "7.6.6", + "@storybook/core-common": "7.6.15", + "@storybook/manager": "7.6.15", + "@storybook/node-logger": "7.6.15", "@types/ejs": "^3.1.1", "@types/find-cache-dir": "^3.2.1", "@yarnpkg/esbuild-plugin-pnp": "^3.0.0-rc.10", @@ -3846,19 +3846,19 @@ } }, "node_modules/@storybook/builder-vite": { - "version": "7.6.6", - "resolved": "https://registry.npmjs.org/@storybook/builder-vite/-/builder-vite-7.6.6.tgz", - "integrity": "sha512-vDBHjsswnVScVgGHeIZ22R/LoRt5T1F62p5czusydBSxKGzma5Va4JHQJp4/IKXwiCZbXcua/Cs7VKtBLO+50A==", - "dev": true, - "dependencies": { - "@storybook/channels": "7.6.6", - "@storybook/client-logger": "7.6.6", - "@storybook/core-common": "7.6.6", - "@storybook/csf-plugin": "7.6.6", - "@storybook/node-logger": "7.6.6", - "@storybook/preview": "7.6.6", - "@storybook/preview-api": "7.6.6", - "@storybook/types": "7.6.6", + "version": "7.6.15", + "resolved": "https://registry.npmjs.org/@storybook/builder-vite/-/builder-vite-7.6.15.tgz", + "integrity": "sha512-ZqmWoty+AsxArvwGCg1F/1dpZUWDYfiZe0Ag1S9hdqNj6geM1IqO0wLB6Y5c4gl3BKEFmOLA36yRVlP5KIkx8w==", + "dev": true, + "dependencies": { + "@storybook/channels": "7.6.15", + "@storybook/client-logger": "7.6.15", + "@storybook/core-common": "7.6.15", + "@storybook/csf-plugin": "7.6.15", + "@storybook/node-logger": "7.6.15", + "@storybook/preview": "7.6.15", + "@storybook/preview-api": "7.6.15", + "@storybook/types": "7.6.15", "@types/find-cache-dir": "^3.2.1", "browser-assert": "^1.2.1", "es-module-lexer": "^0.9.3", @@ -3891,13 +3891,13 @@ } }, "node_modules/@storybook/channels": { - "version": "7.6.6", - "resolved": "https://registry.npmjs.org/@storybook/channels/-/channels-7.6.6.tgz", - "integrity": "sha512-vvo7fBe2WffPonNNOA7Xx7jcHAto8qJYlq+VMysfheXrsRRbhHl3WQOA18Vm8hV9txtqdqk0hwQiXOWvhYVpeQ==", + "version": "7.6.15", + "resolved": "https://registry.npmjs.org/@storybook/channels/-/channels-7.6.15.tgz", + "integrity": "sha512-UPDYRzGkygYFa8QUpEiumWrvZm4u4RKVzgiBt9C4RmHORqkkZzL9LXhaZJp2SmIz1ND5gx6KR5ze8ZnAdwxxoQ==", "dev": true, "dependencies": { - "@storybook/client-logger": "7.6.6", - "@storybook/core-events": "7.6.6", + "@storybook/client-logger": "7.6.15", + "@storybook/core-events": "7.6.15", "@storybook/global": "^5.0.0", "qs": "^6.10.0", "telejson": "^7.2.0", @@ -3909,23 +3909,23 @@ } }, "node_modules/@storybook/cli": { - "version": "7.6.6", - "resolved": "https://registry.npmjs.org/@storybook/cli/-/cli-7.6.6.tgz", - "integrity": "sha512-FLmWrbmGOqe1VYwqyIWxU2lJcYPssORmSbSVVPw6OqQIXx3NrNBrmZDLncMwbVCDQ8eU54J1zb+MyDmSqMbVFg==", + "version": "7.6.15", + "resolved": "https://registry.npmjs.org/@storybook/cli/-/cli-7.6.15.tgz", + "integrity": "sha512-2QRqCyVGDSkraHxX2JPYkkFccbu5Uo+JYFaFJo4vmMXzDurjWON+Ga2B8FCTd4A8P4C02Ca/79jgQoyBB3xoew==", "dev": true, "dependencies": { "@babel/core": "^7.23.2", "@babel/preset-env": "^7.23.2", "@babel/types": "^7.23.0", "@ndelangen/get-tarball": "^3.0.7", - "@storybook/codemod": "7.6.6", - "@storybook/core-common": "7.6.6", - "@storybook/core-events": "7.6.6", - "@storybook/core-server": "7.6.6", - "@storybook/csf-tools": "7.6.6", - "@storybook/node-logger": "7.6.6", - "@storybook/telemetry": "7.6.6", - "@storybook/types": "7.6.6", + "@storybook/codemod": "7.6.15", + "@storybook/core-common": "7.6.15", + "@storybook/core-events": "7.6.15", + "@storybook/core-server": "7.6.15", + "@storybook/csf-tools": "7.6.15", + "@storybook/node-logger": "7.6.15", + "@storybook/telemetry": "7.6.15", + "@storybook/types": "7.6.15", "@types/semver": "^7.3.4", "@yarnpkg/fslib": "2.10.3", "@yarnpkg/libzip": "2.3.0", @@ -3950,7 +3950,6 @@ "puppeteer-core": "^2.1.1", "read-pkg-up": "^7.0.1", "semver": "^7.3.7", - "simple-update-notifier": "^2.0.0", "strip-json-comments": "^3.0.1", "tempy": "^1.0.1", "ts-dedent": "^2.0.0", @@ -3978,9 +3977,9 @@ } }, "node_modules/@storybook/cli/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -3999,9 +3998,9 @@ "dev": true }, "node_modules/@storybook/client-logger": { - "version": "7.6.6", - "resolved": "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-7.6.6.tgz", - "integrity": "sha512-WEvVyuQR5oNF8jcMmGA13zDjxP/l46kOBBvB6JSc8toUdtLZ/kZWSnU0ioNM8+ECpFqXHjBcF2K6uSJOEb6YEg==", + "version": "7.6.15", + "resolved": "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-7.6.15.tgz", + "integrity": "sha512-n+K8IqnombqiQNnywVovS+lK61tvv/XSfgPt0cgvoF/hJZB0VDOMRjWsV+v9qQpj1TQEl1lLWeJwZMthTWupJA==", "dev": true, "dependencies": { "@storybook/global": "^5.0.0" @@ -4012,18 +4011,18 @@ } }, "node_modules/@storybook/codemod": { - "version": "7.6.6", - "resolved": "https://registry.npmjs.org/@storybook/codemod/-/codemod-7.6.6.tgz", - "integrity": "sha512-6QwW6T6ZgwwbTkEoZ7CAoX7lUUob7Sy7bRkMHhSjJe2++wEVFOYLvzHcLUJCupK59+WhmsJU9PpUMlXEKi40TQ==", + "version": "7.6.15", + "resolved": "https://registry.npmjs.org/@storybook/codemod/-/codemod-7.6.15.tgz", + "integrity": "sha512-NiEbTLCdacj6TMxC7G49IImXeMzkG8wpPr8Ayxm9HeG6q5UkiF5/DiZdqbJm2zaosOsOKWwvXg1t6Pq6Nivytg==", "dev": true, "dependencies": { "@babel/core": "^7.23.2", "@babel/preset-env": "^7.23.2", "@babel/types": "^7.23.0", "@storybook/csf": "^0.1.2", - "@storybook/csf-tools": "7.6.6", - "@storybook/node-logger": "7.6.6", - "@storybook/types": "7.6.6", + "@storybook/csf-tools": "7.6.15", + "@storybook/node-logger": "7.6.15", + "@storybook/types": "7.6.15", "@types/cross-spawn": "^6.0.2", "cross-spawn": "^7.0.3", "globby": "^11.0.2", @@ -4038,18 +4037,18 @@ } }, "node_modules/@storybook/components": { - "version": "7.6.6", - "resolved": "https://registry.npmjs.org/@storybook/components/-/components-7.6.6.tgz", - "integrity": "sha512-FSfcRxdmV4+LJHjMk0eodGVnZdb2qrKKmbtsn0O/434z586zPA287/wJJsm4JS/Xr1WS9oTvU6mYMDChkcxgeQ==", + "version": "7.6.15", + "resolved": "https://registry.npmjs.org/@storybook/components/-/components-7.6.15.tgz", + "integrity": "sha512-xD+maP7+C9HeZXi2vJ+uK9hXN4S4spP4uDj9pyZ9yViKb+ztEO6WpovUMT8WRQ0mMegWyLXkx3zqu43hZvXM1g==", "dev": true, "dependencies": { "@radix-ui/react-select": "^1.2.2", "@radix-ui/react-toolbar": "^1.0.4", - "@storybook/client-logger": "7.6.6", + "@storybook/client-logger": "7.6.15", "@storybook/csf": "^0.1.2", "@storybook/global": "^5.0.0", - "@storybook/theming": "7.6.6", - "@storybook/types": "7.6.6", + "@storybook/theming": "7.6.15", + "@storybook/types": "7.6.15", "memoizerific": "^1.11.3", "use-resize-observer": "^9.1.0", "util-deprecate": "^1.0.2" @@ -4064,13 +4063,13 @@ } }, "node_modules/@storybook/core-client": { - "version": "7.6.6", - "resolved": "https://registry.npmjs.org/@storybook/core-client/-/core-client-7.6.6.tgz", - "integrity": "sha512-P100aNf+WpvzlfULZp1NPd60/nxsppLmft2DdIyAx1j4QPMZvUJyJB+hdBMzTFiPEhIUncIMoIVf2R3UXC5DfA==", + "version": "7.6.15", + "resolved": "https://registry.npmjs.org/@storybook/core-client/-/core-client-7.6.15.tgz", + "integrity": "sha512-jwWol+zo+ItKBzPm9i80bEL6seHMsV0wKSaViVMQ4TqHtEbNeFE8sFEc2NTr18VNBnQOdlQPnEWmdboXBUrGcA==", "dev": true, "dependencies": { - "@storybook/client-logger": "7.6.6", - "@storybook/preview-api": "7.6.6" + "@storybook/client-logger": "7.6.15", + "@storybook/preview-api": "7.6.15" }, "funding": { "type": "opencollective", @@ -4078,14 +4077,14 @@ } }, "node_modules/@storybook/core-common": { - "version": "7.6.6", - "resolved": "https://registry.npmjs.org/@storybook/core-common/-/core-common-7.6.6.tgz", - "integrity": "sha512-DpbFSYw8LHuwpeU2ec5uWryxrSqslFJnWTfNA7AvpzCviWXkz4kq+YYrDee9XExF6OozNwILmG6m52SnraysBA==", + "version": "7.6.15", + "resolved": "https://registry.npmjs.org/@storybook/core-common/-/core-common-7.6.15.tgz", + "integrity": "sha512-VGmcLJ5U1r1s8/YnLbKcyB4GnNL+/sZIPqwlcSKzDXO76HoVFv1kywf7PbASote7P3gdhLSxBdg95LH2bdIbmw==", "dev": true, "dependencies": { - "@storybook/core-events": "7.6.6", - "@storybook/node-logger": "7.6.6", - "@storybook/types": "7.6.6", + "@storybook/core-events": "7.6.15", + "@storybook/node-logger": "7.6.15", + "@storybook/types": "7.6.15", "@types/find-cache-dir": "^3.2.1", "@types/node": "^18.0.0", "@types/node-fetch": "^2.6.4", @@ -4113,18 +4112,18 @@ } }, "node_modules/@storybook/core-common/node_modules/@types/node": { - "version": "18.19.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.3.tgz", - "integrity": "sha512-k5fggr14DwAytoA/t8rPrIz++lXK7/DqckthCmoZOKNsEbJkId4Z//BqgApXBUGrGddrigYa1oqheo/7YmW4rg==", + "version": "18.19.16", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.16.tgz", + "integrity": "sha512-mjtrR7Wco9ZwcGBc1zre6fENlj9z42/+0W26lBGtGBTPiR3Zm9iZAaiPhxreG6magwGCILLVYwlQ48GjAaqM6w==", "dev": true, "dependencies": { "undici-types": "~5.26.4" } }, "node_modules/@storybook/core-events": { - "version": "7.6.6", - "resolved": "https://registry.npmjs.org/@storybook/core-events/-/core-events-7.6.6.tgz", - "integrity": "sha512-7+q9HiZiLxaQcwpaSLQrLdjHNHBoOoUY9ZcZXI9iNFSopOgb/ItDnzzlpv08NC7CbKae1hVKJM/t5aSTl7tCMw==", + "version": "7.6.15", + "resolved": "https://registry.npmjs.org/@storybook/core-events/-/core-events-7.6.15.tgz", + "integrity": "sha512-i4YnjGecbpGyrFe0340sPhQ9QjZZEBqvMy6kF4XWt6DYLHxZmsTj1HEdvxVl4Ej7V49Vw0Dm8MepJ1d4Y8MKrQ==", "dev": true, "dependencies": { "ts-dedent": "^2.0.0" @@ -4135,26 +4134,26 @@ } }, "node_modules/@storybook/core-server": { - "version": "7.6.6", - "resolved": "https://registry.npmjs.org/@storybook/core-server/-/core-server-7.6.6.tgz", - "integrity": "sha512-QFVahaExgGtq9swBXgQAMUiCqpCcyVXOiKTIy1j+1uAhPVqhpCxBkkFoXruih5hbIMZyohE4mLPCAr/ivicoDg==", + "version": "7.6.15", + "resolved": "https://registry.npmjs.org/@storybook/core-server/-/core-server-7.6.15.tgz", + "integrity": "sha512-iIlxEAkrmKTSA3iGNqt/4QG7hf5suxBGYIB3DZAOfBo8EdZogMYaEmuCm5dbuaJr0mcVwlqwdhQiWb1VsR/NhA==", "dev": true, "dependencies": { "@aw-web-design/x-default-browser": "1.4.126", "@discoveryjs/json-ext": "^0.5.3", - "@storybook/builder-manager": "7.6.6", - "@storybook/channels": "7.6.6", - "@storybook/core-common": "7.6.6", - "@storybook/core-events": "7.6.6", + "@storybook/builder-manager": "7.6.15", + "@storybook/channels": "7.6.15", + "@storybook/core-common": "7.6.15", + "@storybook/core-events": "7.6.15", "@storybook/csf": "^0.1.2", - "@storybook/csf-tools": "7.6.6", + "@storybook/csf-tools": "7.6.15", "@storybook/docs-mdx": "^0.1.0", "@storybook/global": "^5.0.0", - "@storybook/manager": "7.6.6", - "@storybook/node-logger": "7.6.6", - "@storybook/preview-api": "7.6.6", - "@storybook/telemetry": "7.6.6", - "@storybook/types": "7.6.6", + "@storybook/manager": "7.6.15", + "@storybook/node-logger": "7.6.15", + "@storybook/preview-api": "7.6.15", + "@storybook/telemetry": "7.6.15", + "@storybook/types": "7.6.15", "@types/detect-port": "^1.3.0", "@types/node": "^18.0.0", "@types/pretty-hrtime": "^1.0.0", @@ -4188,9 +4187,9 @@ } }, "node_modules/@storybook/core-server/node_modules/@types/node": { - "version": "18.19.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.3.tgz", - "integrity": "sha512-k5fggr14DwAytoA/t8rPrIz++lXK7/DqckthCmoZOKNsEbJkId4Z//BqgApXBUGrGddrigYa1oqheo/7YmW4rg==", + "version": "18.19.16", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.16.tgz", + "integrity": "sha512-mjtrR7Wco9ZwcGBc1zre6fENlj9z42/+0W26lBGtGBTPiR3Zm9iZAaiPhxreG6magwGCILLVYwlQ48GjAaqM6w==", "dev": true, "dependencies": { "undici-types": "~5.26.4" @@ -4209,9 +4208,9 @@ } }, "node_modules/@storybook/core-server/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -4239,12 +4238,12 @@ } }, "node_modules/@storybook/csf-plugin": { - "version": "7.6.6", - "resolved": "https://registry.npmjs.org/@storybook/csf-plugin/-/csf-plugin-7.6.6.tgz", - "integrity": "sha512-SqdffT14+XNpf+7vA29Elur28VArXtFv4cXMlsCbswbRuY+a0A8vYNwVIfCUy9u4WHTcQX1/tUkDAMh80lrVRQ==", + "version": "7.6.15", + "resolved": "https://registry.npmjs.org/@storybook/csf-plugin/-/csf-plugin-7.6.15.tgz", + "integrity": "sha512-5Pm2B8XKNdG3fHyItWKbWnXHSRDFSvetlML+sMWGWYIjwOsnvPqt+gAvLksWhv/uJgDujGxNcPEh+/Y5C8ZAjQ==", "dev": true, "dependencies": { - "@storybook/csf-tools": "7.6.6", + "@storybook/csf-tools": "7.6.15", "unplugin": "^1.3.1" }, "funding": { @@ -4253,9 +4252,9 @@ } }, "node_modules/@storybook/csf-tools": { - "version": "7.6.6", - "resolved": "https://registry.npmjs.org/@storybook/csf-tools/-/csf-tools-7.6.6.tgz", - "integrity": "sha512-VXOZCzfSVJL832u17pPhFu1x3PPaAN9d8VXNFX+t/2raga7tK3T7Qhe7lWfP7EZcrVvSCEEp0aMRz2EzzDGVtw==", + "version": "7.6.15", + "resolved": "https://registry.npmjs.org/@storybook/csf-tools/-/csf-tools-7.6.15.tgz", + "integrity": "sha512-8iKgg2cmbFTpVhRRJOqouhPcEh0c8ywabG4S8ICZvnJooSXUI9mD9p3tYCS7MYuSiHj0epa1Kkn9DtXJRo9o6g==", "dev": true, "dependencies": { "@babel/generator": "^7.23.0", @@ -4263,7 +4262,7 @@ "@babel/traverse": "^7.23.2", "@babel/types": "^7.23.0", "@storybook/csf": "^0.1.2", - "@storybook/types": "7.6.6", + "@storybook/types": "7.6.15", "fs-extra": "^11.1.0", "recast": "^0.23.1", "ts-dedent": "^2.0.0" @@ -4280,14 +4279,14 @@ "dev": true }, "node_modules/@storybook/docs-tools": { - "version": "7.6.6", - "resolved": "https://registry.npmjs.org/@storybook/docs-tools/-/docs-tools-7.6.6.tgz", - "integrity": "sha512-nc5ZjN2s8SC2PtsZoFf9Wm6gD8TcSlkYbF/mjtyLCGN+Fi+k5B5iudqoa65H19hwiLlzBdcnpQ8C89AiK33J9Q==", + "version": "7.6.15", + "resolved": "https://registry.npmjs.org/@storybook/docs-tools/-/docs-tools-7.6.15.tgz", + "integrity": "sha512-npZEaI9Wpn9uJcRXFElqyiRw8bSxt95mLywPiEEGMT2kE5FfXM8d5Uj5O64kzoXdRI9IhRPEEZZidOtA/UInfQ==", "dev": true, "dependencies": { - "@storybook/core-common": "7.6.6", - "@storybook/preview-api": "7.6.6", - "@storybook/types": "7.6.6", + "@storybook/core-common": "7.6.15", + "@storybook/preview-api": "7.6.15", + "@storybook/types": "7.6.15", "@types/doctrine": "^0.0.3", "assert": "^2.1.0", "doctrine": "^3.0.0", @@ -4305,9 +4304,9 @@ "dev": true }, "node_modules/@storybook/manager": { - "version": "7.6.6", - "resolved": "https://registry.npmjs.org/@storybook/manager/-/manager-7.6.6.tgz", - "integrity": "sha512-Ga3LcSu/xxSyg+cLlO9AS8QjW+D667V+c9qQPmsFyU6qfFc6m6mVqcRLSmFVD5e7P/o0FL7STOf9jAKkDcW8xw==", + "version": "7.6.15", + "resolved": "https://registry.npmjs.org/@storybook/manager/-/manager-7.6.15.tgz", + "integrity": "sha512-GGV2ElV5AOIApy/FSDzoSlLUbyd2VhQVD3TdNGRxNauYRjEO8ulXHw2tNbT6ludtpYpDTAILzI6zT/iag8hmPQ==", "dev": true, "funding": { "type": "opencollective", @@ -4315,23 +4314,22 @@ } }, "node_modules/@storybook/manager-api": { - "version": "7.6.6", - "resolved": "https://registry.npmjs.org/@storybook/manager-api/-/manager-api-7.6.6.tgz", - "integrity": "sha512-euRAbSZAUzHDt6z1Pq/g45N/RNqta9RaQAym18zt/oLWiYOIrkLmdf7kCuFYsmuA5XQBytiJqwkAD7uF1aLe0g==", + "version": "7.6.15", + "resolved": "https://registry.npmjs.org/@storybook/manager-api/-/manager-api-7.6.15.tgz", + "integrity": "sha512-cPBsXcnJiaO3QyaEum2JgdihYea3cI03FeV35JdrBYLIelT4oqbYFnzjznsFg9+Ia9iAbz7aOBNyyRsWnC/UKw==", "dev": true, "dependencies": { - "@storybook/channels": "7.6.6", - "@storybook/client-logger": "7.6.6", - "@storybook/core-events": "7.6.6", + "@storybook/channels": "7.6.15", + "@storybook/client-logger": "7.6.15", + "@storybook/core-events": "7.6.15", "@storybook/csf": "^0.1.2", "@storybook/global": "^5.0.0", - "@storybook/router": "7.6.6", - "@storybook/theming": "7.6.6", - "@storybook/types": "7.6.6", + "@storybook/router": "7.6.15", + "@storybook/theming": "7.6.15", + "@storybook/types": "7.6.15", "dequal": "^2.0.2", "lodash": "^4.17.21", "memoizerific": "^1.11.3", - "semver": "^7.3.7", "store2": "^2.14.2", "telejson": "^7.2.0", "ts-dedent": "^2.0.0" @@ -4341,39 +4339,6 @@ "url": "https://opencollective.com/storybook" } }, - "node_modules/@storybook/manager-api/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@storybook/manager-api/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@storybook/manager-api/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, "node_modules/@storybook/mdx2-csf": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@storybook/mdx2-csf/-/mdx2-csf-1.1.0.tgz", @@ -4381,9 +4346,9 @@ "dev": true }, "node_modules/@storybook/node-logger": { - "version": "7.6.6", - "resolved": "https://registry.npmjs.org/@storybook/node-logger/-/node-logger-7.6.6.tgz", - "integrity": "sha512-b2OF9GRNI01MlBlnDGS8S6/yOpBNl8eH/0ONafuMPzFEZs5PouHGsFflJvQwwcdVTknMjF5uVS2eSmnLZ8spvA==", + "version": "7.6.15", + "resolved": "https://registry.npmjs.org/@storybook/node-logger/-/node-logger-7.6.15.tgz", + "integrity": "sha512-C+sCvRjR+5uVU3VTrfyv7/RlPBxesAjIucUAK0keGyIZ7sFQYCPdkm4m/C4s+TcubgAzVvuoUHlRrSppdA7WzQ==", "dev": true, "funding": { "type": "opencollective", @@ -4391,9 +4356,9 @@ } }, "node_modules/@storybook/postinstall": { - "version": "7.6.6", - "resolved": "https://registry.npmjs.org/@storybook/postinstall/-/postinstall-7.6.6.tgz", - "integrity": "sha512-jamn7QNTJPZiu22nu25LqfSTJohugFhCu4b48yqP+pdMdkQ3qVd3NdDYhBlgkH/Btar+kppiJP1gRtoiJF761w==", + "version": "7.6.15", + "resolved": "https://registry.npmjs.org/@storybook/postinstall/-/postinstall-7.6.15.tgz", + "integrity": "sha512-DXQQ4kjAbQ7BSd9M4lDI/12vEEciYMP8uYFDlrPFjwD9LezsxtRiORkazjNRRX4730faO5zZsnWhXxCVkxck0g==", "dev": true, "funding": { "type": "opencollective", @@ -4401,9 +4366,9 @@ } }, "node_modules/@storybook/preview": { - "version": "7.6.6", - "resolved": "https://registry.npmjs.org/@storybook/preview/-/preview-7.6.6.tgz", - "integrity": "sha512-Rl+Pom2bNwzc0MdlbFALmvxsbCkbIwlpTZlRZZTh5Ah8JViV9htQgP9e8uwo3NZA2BhjbDLkmnZeloWxQnI5Ig==", + "version": "7.6.15", + "resolved": "https://registry.npmjs.org/@storybook/preview/-/preview-7.6.15.tgz", + "integrity": "sha512-q8d9v0+Bo/DHLV68OyV3Klep4knf2GAbrlHhLW1X4jlPccuEDUojIfqfK7m48ayeIxJzO48fcO0JdKM1XABx7g==", "dev": true, "funding": { "type": "opencollective", @@ -4411,17 +4376,17 @@ } }, "node_modules/@storybook/preview-api": { - "version": "7.6.6", - "resolved": "https://registry.npmjs.org/@storybook/preview-api/-/preview-api-7.6.6.tgz", - "integrity": "sha512-Bt6xIAR5yZ/JWc90X4BbLOA97iL65glZ1SOBgFFv2mHrdZ1lcdKhAlQr2aeJAf1mLvBtalPjvKzi9EuVY3FZ4w==", + "version": "7.6.15", + "resolved": "https://registry.npmjs.org/@storybook/preview-api/-/preview-api-7.6.15.tgz", + "integrity": "sha512-2KN9vlizF6sFlYsJEGnFqcQaJXs4TTdawC1VazVdtaMSHANDxxDu8F1cP+u7lpPH3DkNZUmTGQDBYfYY9xR0eQ==", "dev": true, "dependencies": { - "@storybook/channels": "7.6.6", - "@storybook/client-logger": "7.6.6", - "@storybook/core-events": "7.6.6", + "@storybook/channels": "7.6.15", + "@storybook/client-logger": "7.6.15", + "@storybook/core-events": "7.6.15", "@storybook/csf": "^0.1.2", "@storybook/global": "^5.0.0", - "@storybook/types": "7.6.6", + "@storybook/types": "7.6.15", "@types/qs": "^6.9.5", "dequal": "^2.0.2", "lodash": "^4.17.21", @@ -4437,18 +4402,18 @@ } }, "node_modules/@storybook/react": { - "version": "7.6.6", - "resolved": "https://registry.npmjs.org/@storybook/react/-/react-7.6.6.tgz", - "integrity": "sha512-pE6GJ4hPGJIsX6AREjW6HibshwZE6rFhWRtjeX5MV0eKMmQgoRWRgiRfg9/YB6Z0tRtuptI83Uaszimmif1BKg==", + "version": "7.6.15", + "resolved": "https://registry.npmjs.org/@storybook/react/-/react-7.6.15.tgz", + "integrity": "sha512-oJMSh4iTGu6OqCmj0LhkuPyMkxGMTCoohN4HcDpXd96jCSyWotVebRsg9xm5ddB7f54e6DY4XDoGH0WnVoR23g==", "dev": true, "dependencies": { - "@storybook/client-logger": "7.6.6", - "@storybook/core-client": "7.6.6", - "@storybook/docs-tools": "7.6.6", + "@storybook/client-logger": "7.6.15", + "@storybook/core-client": "7.6.15", + "@storybook/docs-tools": "7.6.15", "@storybook/global": "^5.0.0", - "@storybook/preview-api": "7.6.6", - "@storybook/react-dom-shim": "7.6.6", - "@storybook/types": "7.6.6", + "@storybook/preview-api": "7.6.15", + "@storybook/react-dom-shim": "7.6.15", + "@storybook/types": "7.6.15", "@types/escodegen": "^0.0.6", "@types/estree": "^0.0.51", "@types/node": "^18.0.0", @@ -4483,9 +4448,9 @@ } }, "node_modules/@storybook/react-dom-shim": { - "version": "7.6.6", - "resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-7.6.6.tgz", - "integrity": "sha512-WWNlXtCVoBWXX/kLNulUeMgzmlAEHi2aBrdIv2jz0DScPf0YxeWAkWmgK7F0zMot9mdwYncr+pk1AILbTBJSyg==", + "version": "7.6.15", + "resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-7.6.15.tgz", + "integrity": "sha512-2+X0HIxIyvjfSKVyGGjSJJLEFJ2ox7Rr8FjlMiRo5QfoOJhohZuWH7p4Lw7JMwm5PotnjrwlfsZI3cCilYJeYA==", "dev": true, "funding": { "type": "opencollective", @@ -4497,15 +4462,15 @@ } }, "node_modules/@storybook/react-vite": { - "version": "7.6.6", - "resolved": "https://registry.npmjs.org/@storybook/react-vite/-/react-vite-7.6.6.tgz", - "integrity": "sha512-76jH+rX0OhEwGraA2BphSu+19nKaSUnNw1Gp1zQ/UUX2FefZuI+6DI34LEzJNfq7T2kbGFzZgf1xDkL6RSwrXA==", + "version": "7.6.15", + "resolved": "https://registry.npmjs.org/@storybook/react-vite/-/react-vite-7.6.15.tgz", + "integrity": "sha512-/d+M1G4VkpPvpyU/FHvtsnXiurlyx0se03z1b2rjvxxEcf69mvvIgqzSsgxAWyFEXKmz0QWGVGD/llYjTycS5g==", "dev": true, "dependencies": { "@joshwooding/vite-plugin-react-docgen-typescript": "0.3.0", "@rollup/pluginutils": "^5.0.2", - "@storybook/builder-vite": "7.6.6", - "@storybook/react": "7.6.6", + "@storybook/builder-vite": "7.6.15", + "@storybook/react": "7.6.15", "@vitejs/plugin-react": "^3.0.1", "magic-string": "^0.30.0", "react-docgen": "^7.0.0" @@ -4564,12 +4529,12 @@ } }, "node_modules/@storybook/router": { - "version": "7.6.6", - "resolved": "https://registry.npmjs.org/@storybook/router/-/router-7.6.6.tgz", - "integrity": "sha512-dkn81MtxrG7JMDbOHEcVZkTDVKsneg72CyqJ8ELZfC81iKQcDMQkV9mdmnMl45aKn6UrscudI4K23OxQmsevkw==", + "version": "7.6.15", + "resolved": "https://registry.npmjs.org/@storybook/router/-/router-7.6.15.tgz", + "integrity": "sha512-5yhXXoVZ1iKUgeZoO8PGqBclrLgoJisxIYVK/Y1iJMXZ2ZvwUiTswLALT6lu97tSrcoBVxmqSghg0+U0YEU4Fg==", "dev": true, "dependencies": { - "@storybook/client-logger": "7.6.6", + "@storybook/client-logger": "7.6.15", "memoizerific": "^1.11.3", "qs": "^6.10.0" }, @@ -4579,14 +4544,14 @@ } }, "node_modules/@storybook/telemetry": { - "version": "7.6.6", - "resolved": "https://registry.npmjs.org/@storybook/telemetry/-/telemetry-7.6.6.tgz", - "integrity": "sha512-2WdDcrMrt1bPVgdMVO0tFmVxT6YIjiPRfKbH/7wwYMOGmV75m4mJ9Ha2gzZc/oXTSK1M4/fiK12IgW+S3ErcMg==", + "version": "7.6.15", + "resolved": "https://registry.npmjs.org/@storybook/telemetry/-/telemetry-7.6.15.tgz", + "integrity": "sha512-klhKXLUS3OXozGEtMbbhKZLDfm+m3nNk2jvGwD6kkBenzFUzb0P2m8awxU7h1pBcKZKH/27U9t3KVzNFzWoWPw==", "dev": true, "dependencies": { - "@storybook/client-logger": "7.6.6", - "@storybook/core-common": "7.6.6", - "@storybook/csf-tools": "7.6.6", + "@storybook/client-logger": "7.6.15", + "@storybook/core-common": "7.6.15", + "@storybook/csf-tools": "7.6.15", "chalk": "^4.1.0", "detect-package-manager": "^2.0.1", "fetch-retry": "^5.0.2", @@ -4610,13 +4575,13 @@ } }, "node_modules/@storybook/theming": { - "version": "7.6.6", - "resolved": "https://registry.npmjs.org/@storybook/theming/-/theming-7.6.6.tgz", - "integrity": "sha512-hNZOOxaF55iAGUEM0dvAIP6LfGMgPKCJQIk/qyotFk+SKkg3PBqzph89XfFl9yCD3KiX5cryqarULgVuNawLJg==", + "version": "7.6.15", + "resolved": "https://registry.npmjs.org/@storybook/theming/-/theming-7.6.15.tgz", + "integrity": "sha512-9PpsHAbUf6o0w33/P3mnb7QheTmfGlTYCismj5HMM1O2/zY0kQK9XcG9W+Cyvu56D/lFC19fz9YHQY8W4AbfnQ==", "dev": true, "dependencies": { "@emotion/use-insertion-effect-with-fallbacks": "^1.0.0", - "@storybook/client-logger": "7.6.6", + "@storybook/client-logger": "7.6.15", "@storybook/global": "^5.0.0", "memoizerific": "^1.11.3" }, @@ -4630,12 +4595,12 @@ } }, "node_modules/@storybook/types": { - "version": "7.6.6", - "resolved": "https://registry.npmjs.org/@storybook/types/-/types-7.6.6.tgz", - "integrity": "sha512-77vbQp3GX93OD8UzFkY4a0fAmkZrqLe61XVo6yABrwbVDY0EcAwaCF5gcXRhOHldlH7KYbLfEQkDkkKTBjX7ow==", + "version": "7.6.15", + "resolved": "https://registry.npmjs.org/@storybook/types/-/types-7.6.15.tgz", + "integrity": "sha512-tLH0lK6SXECSfMpKin9bge+7XiHZII17n6jc9ZI1TfSBZJyq3M6VzWh2r1C2lC97FlkcKXjIwM3n8h1xNjnI+A==", "dev": true, "dependencies": { - "@storybook/channels": "7.6.6", + "@storybook/channels": "7.6.15", "@types/babel__core": "^7.0.0", "@types/express": "^4.7.0", "file-system-cache": "2.3.0" @@ -4822,9 +4787,9 @@ } }, "node_modules/@types/express-serve-static-core": { - "version": "4.17.41", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.41.tgz", - "integrity": "sha512-OaJ7XLaelTgrvlZD8/aa0vvvxZdUmlCn6MtWeB7TkiKW70BQLc9XEPpDLPdbo52ZhXUCrznlWdCHWxJWtdyajA==", + "version": "4.17.43", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.43.tgz", + "integrity": "sha512-oaYtiBirUOPQGSWNGPWnzyAFJ0BP3cwvN4oWZQY+zUBwpVIGsKUkpBpSztp74drYcjavs7SKFZ4DX1V2QeN8rg==", "dev": true, "dependencies": { "@types/node": "*", @@ -4910,9 +4875,9 @@ "dev": true }, "node_modules/@types/mdx": { - "version": "2.0.10", - "resolved": "https://registry.npmjs.org/@types/mdx/-/mdx-2.0.10.tgz", - "integrity": "sha512-Rllzc5KHk0Al5/WANwgSPl1/CwjqCy+AZrGd78zuK+jO9aDM6ffblZ+zIjgPNAaEBmlO0RYDvLNh7wD0zKVgEg==", + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/mdx/-/mdx-2.0.11.tgz", + "integrity": "sha512-HM5bwOaIQJIQbAYfax35HCKxx7a3KrK3nBtIqJgSOitivTD1y3oW9P3rxY9RkXYPUk7y/AjAohfHKmFpGE79zw==", "dev": true }, "node_modules/@types/mime": { @@ -4943,9 +4908,9 @@ } }, "node_modules/@types/node-fetch": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.9.tgz", - "integrity": "sha512-bQVlnMLFJ2d35DkPNjEPmd9ueO/rh5EiaZt2bhqiSarPjZIuIV6bPQVqcrEyvNo+AfTrRGVazle1tl597w3gfA==", + "version": "2.6.11", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.11.tgz", + "integrity": "sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==", "dev": true, "dependencies": { "@types/node": "*", @@ -5050,9 +5015,9 @@ "integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==" }, "node_modules/@types/uuid": { - "version": "9.0.7", - "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.7.tgz", - "integrity": "sha512-WUtIVRUZ9i5dYXefDEAI7sh9/O7jGvHg7Df/5O/gtH3Yabe5odI3UWopVR1qbPXQtvOxWu3mM4XxlYeZtMWF4g==", + "version": "9.0.8", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz", + "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==", "dev": true }, "node_modules/@types/yargs": { @@ -5708,13 +5673,13 @@ } }, "node_modules/babel-plugin-polyfill-corejs2": { - "version": "0.4.7", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.7.tgz", - "integrity": "sha512-LidDk/tEGDfuHW2DWh/Hgo4rmnw3cduK6ZkOI1NPFceSK3n/yAGeOsNT7FLnSGHkXj3RHGSEVkN3FsCTY6w2CQ==", + "version": "0.4.8", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.8.tgz", + "integrity": "sha512-OtIuQfafSzpo/LhnJaykc0R/MMnuLSSVjVYy9mHArIZ9qTCSZ6TpWCuEKZYVoN//t8HqBNScHrOtCrIK5IaGLg==", "dev": true, "dependencies": { "@babel/compat-data": "^7.22.6", - "@babel/helper-define-polyfill-provider": "^0.4.4", + "@babel/helper-define-polyfill-provider": "^0.5.0", "semver": "^6.3.1" }, "peerDependencies": { @@ -5722,25 +5687,25 @@ } }, "node_modules/babel-plugin-polyfill-corejs3": { - "version": "0.8.7", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.8.7.tgz", - "integrity": "sha512-KyDvZYxAzkC0Aj2dAPyDzi2Ym15e5JKZSK+maI7NAwSqofvuFglbSsxE7wUOvTg9oFVnHMzVzBKcqEb4PJgtOA==", + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.9.0.tgz", + "integrity": "sha512-7nZPG1uzK2Ymhy/NbaOWTg3uibM2BmGASS4vHS4szRZAIR8R6GwA/xAujpdrXU5iyklrimWnLWU+BLF9suPTqg==", "dev": true, "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.4.4", - "core-js-compat": "^3.33.1" + "@babel/helper-define-polyfill-provider": "^0.5.0", + "core-js-compat": "^3.34.0" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "node_modules/babel-plugin-polyfill-regenerator": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.4.tgz", - "integrity": "sha512-S/x2iOCvDaCASLYsOOgWOq4bCfKYVqvO/uxjkaYyZ3rVsVE3CeAI/c84NpyuBBymEgNvHgjEot3a9/Z/kXvqsg==", + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.5.tgz", + "integrity": "sha512-OJGYZlhLqBh2DDHeqAxWB1XIvr49CxiJ2gIt61/PU55CQK4Z58OzMqjDe1zwQdQk+rBYsRc+1rJmdajM3gimHg==", "dev": true, "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.4.4" + "@babel/helper-define-polyfill-provider": "^0.5.0" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" @@ -5917,9 +5882,9 @@ } }, "node_modules/browserslist": { - "version": "4.22.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.2.tgz", - "integrity": "sha512-0UgcrvQmBDvZHFGdYUehrCNIazki7/lUP3kkoi/r3YB2amZbFM9J43ZRkJTXBUZK4gmx56+Sqk9+Vs9mwZx9+A==", + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", + "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==", "dev": true, "funding": [ { @@ -5936,8 +5901,8 @@ } ], "dependencies": { - "caniuse-lite": "^1.0.30001565", - "electron-to-chromium": "^1.4.601", + "caniuse-lite": "^1.0.30001587", + "electron-to-chromium": "^1.4.668", "node-releases": "^2.0.14", "update-browserslist-db": "^1.0.13" }, @@ -6038,9 +6003,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001571", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001571.tgz", - "integrity": "sha512-tYq/6MoXhdezDLFZuCO/TKboTzuQ/xR5cFdgXPfDtM7/kchBO3b4VWghE/OAi/DV7tTdhmLjZiZBZi1fA/GheQ==", + "version": "1.0.30001587", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001587.tgz", + "integrity": "sha512-HMFNotUmLXn71BQxg8cijvqxnIAofforZOwGsxyXJ0qugTdspUF4sPSJ2vhgprHCB996tIDzEq1ubumPDV8ULA==", "dev": true, "funding": [ { @@ -6137,9 +6102,9 @@ } }, "node_modules/citty": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.5.tgz", - "integrity": "sha512-AS7n5NSc0OQVMV9v6wt3ByujNIrne0/cTjiC2MYqhvao57VNfiuVksTSr2p17nVOhEr2KtqiAkGwHcgMC/qUuQ==", + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", + "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==", "dev": true, "dependencies": { "consola": "^3.2.3" @@ -6468,12 +6433,12 @@ "dev": true }, "node_modules/core-js-compat": { - "version": "3.34.0", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.34.0.tgz", - "integrity": "sha512-4ZIyeNbW/Cn1wkMMDy+mvrRUxrwFNjKwbhCfQpDd+eLgYipDqp8oGFGtLmhh18EDPKA0g3VUBYOxQGGwvWLVpA==", + "version": "3.36.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.36.0.tgz", + "integrity": "sha512-iV9Pd/PsgjNWBXeq8XRtWVSgz2tKAfhfvBs7qxYty+RlRd+OCksaWmOnc4JKrTc1cToXL1N0s3l/vwlxPtdElw==", "dev": true, "dependencies": { - "browserslist": "^4.22.2" + "browserslist": "^4.22.3" }, "funding": { "type": "opencollective", @@ -6638,9 +6603,9 @@ } }, "node_modules/defu": { - "version": "6.1.3", - "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.3.tgz", - "integrity": "sha512-Vy2wmG3NTkmHNg/kzpuvHhkqeIx3ODWqasgCRbKtbXEN0G+HpEEv9BtJLp7ZG1CZloFaC41Ah3ZFbq7aqCqMeQ==", + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", + "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", "dev": true }, "node_modules/del": { @@ -6774,15 +6739,15 @@ "dev": true }, "node_modules/dotenv": { - "version": "16.3.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz", - "integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==", + "version": "16.4.4", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.4.tgz", + "integrity": "sha512-XvPXc8XAQThSjAbY6cQ/9PcBXmFoWuw1sQ3b8HqUCR6ziGXjkTi//kB9SWa2UwqlgdAIuRqAa/9hVljzPehbYg==", "dev": true, "engines": { "node": ">=12" }, "funding": { - "url": "https://github.com/motdotla/dotenv?sponsor=1" + "url": "https://dotenvx.com" } }, "node_modules/dotenv-expand": { @@ -6870,9 +6835,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.4.616", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.616.tgz", - "integrity": "sha512-1n7zWYh8eS0L9Uy+GskE0lkBUNK83cXTVJI0pU3mGprFsbfSdAc15VTFbo+A+Bq4pwstmL30AVcEU3Fo463lNg==", + "version": "1.4.670", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.670.tgz", + "integrity": "sha512-hcijYOWjOtjKrKPtNA6tuLlA/bTLO3heFG8pQA6mLpq7dRydSWicXova5lyxDzp1iVJaYhK7J2OQlGE52KYn7A==", "dev": true }, "node_modules/emoji-regex": { @@ -6900,9 +6865,9 @@ } }, "node_modules/envinfo": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.11.0.tgz", - "integrity": "sha512-G9/6xF1FPbIw0TtalAMaVPpiq2aDEuKLXM314jPVAO9r2fo2a4BLqMNkmRS7O/xPPZ+COAhGIz3ETvHEV3eUcg==", + "version": "7.11.1", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.11.1.tgz", + "integrity": "sha512-8PiZgZNIB4q/Lw4AhOvAfB/ityHAd2bli3lESSWmWSzSsl5dKpy5N1d1Rfkd2teq/g9xN90lc6o98DOjMeYHpg==", "dev": true, "bin": { "envinfo": "dist/cli.js" @@ -7924,9 +7889,9 @@ "dev": true }, "node_modules/flow-parser": { - "version": "0.225.1", - "resolved": "https://registry.npmjs.org/flow-parser/-/flow-parser-0.225.1.tgz", - "integrity": "sha512-50fjR6zbLQcpq5IFNkheUSY/AFPxVeeLiBM5B3NQBSKId2G0cUuExOlDDOguxc49dl9lnh8hI1xcYlPJWNp4KQ==", + "version": "0.229.0", + "resolved": "https://registry.npmjs.org/flow-parser/-/flow-parser-0.229.0.tgz", + "integrity": "sha512-mOYmMuvJwAo/CvnMFEq4SHftq7E5188hYMTTxJyQOXk2nh+sgslRdYMw3wTthH+FMcFaZLtmBPuMu6IwztdoUQ==", "dev": true, "engines": { "node": ">=0.4.0" @@ -8045,6 +8010,20 @@ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "dev": true }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -8139,9 +8118,9 @@ } }, "node_modules/giget": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/giget/-/giget-1.2.0.tgz", - "integrity": "sha512-cIILJ8cBGW34vaH0IuZWeZj/pqy5s3Jkkps0m+g+nWqpsdwLjSoHaeECWjVeJWPUm7uhlS8tJSrrzSBaQXeM1A==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/giget/-/giget-1.2.1.tgz", + "integrity": "sha512-4VG22mopWtIeHwogGSy1FViXVo0YT+m6BrqZfz0JJFwbSsePsCdOzdLIIli5BtMp7Xe8f/o2OmBpQX2NBOC24g==", "dev": true, "dependencies": { "citty": "^0.1.5", @@ -9539,9 +9518,9 @@ } }, "node_modules/magic-string": { - "version": "0.30.5", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz", - "integrity": "sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==", + "version": "0.30.7", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.7.tgz", + "integrity": "sha512-8vBuFF/I/+OSLRmdf2wwFCJCz+nSn0m6DPvGH1fS/KiQoSaR+sETbov0eIk9KhEKy8CYqIkIAnbohxT/4H0kuA==", "dev": true, "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" @@ -9884,9 +9863,9 @@ } }, "node_modules/node-fetch-native": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.1.tgz", - "integrity": "sha512-bW9T/uJDPAJB2YNYEpWzE54U5O3MQidXsOyTfnbKYtTtFexRvGzb1waphBN4ZwP6EcIvYYEOwW0b72BpAqydTw==", + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.2.tgz", + "integrity": "sha512-69mtXOFZ6hSkYiXAVB5SqaRvrbITC/NPyqv7yuu/qw0nmgPyYbIMYYNIDhNtwPrzk0ptrimrLz/hhjvm4w5Z+w==", "dev": true }, "node_modules/node-int64": { @@ -9944,15 +9923,15 @@ } }, "node_modules/nypm": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.3.3.tgz", - "integrity": "sha512-FHoxtTscAE723e80d2M9cJRb4YVjL82Ra+ZV+YqC6rfNZUWahi+ZhPF+krnR+bdMvibsfHCtgKXnZf5R6kmEPA==", + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.3.6.tgz", + "integrity": "sha512-2CATJh3pd6CyNfU5VZM7qSwFu0ieyabkEdnogE30Obn1czrmOYiZ8DOZLe1yBdLKWoyD3Mcy2maUs+0MR3yVjQ==", "dev": true, "dependencies": { - "citty": "^0.1.4", + "citty": "^0.1.5", "execa": "^8.0.1", - "pathe": "^1.1.1", - "ufo": "^1.3.0" + "pathe": "^1.1.2", + "ufo": "^1.3.2" }, "bin": { "nypm": "dist/cli.mjs" @@ -10401,9 +10380,9 @@ } }, "node_modules/path-scurry/node_modules/lru-cache": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.1.0.tgz", - "integrity": "sha512-/1clY/ui8CzjKFyjdvwPWJUYKiFVXG2I2cY0ssG7h4+hwk+XOIX7ZSG9Q7TW8TW3Kp3BUSqgFWBLgL4PJ+Blag==", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz", + "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==", "dev": true, "engines": { "node": "14 || >=16.14" @@ -10425,9 +10404,9 @@ } }, "node_modules/pathe": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.1.tgz", - "integrity": "sha512-d+RQGp0MAYTIaDBIMmOfMwz3E+LOZnxx1HZd5R18mmCZY0QBlK0LDZfPc8FW8Ed2DlvsuE6PRjroDY+wg4+j/Q==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", "dev": true }, "node_modules/peek-stream": { @@ -10981,6 +10960,15 @@ "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true }, + "node_modules/react-multi-select-component": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/react-multi-select-component/-/react-multi-select-component-4.3.4.tgz", + "integrity": "sha512-Ui/bzCbROF4WfKq3OKWyQJHmy/bd1mW7CQM+L83TfiltuVvHElhKEyPM3JzO9urIcWplBUKv+kyxqmEnd9jPcA==", + "peerDependencies": { + "react": "^16 || ^17 || ^18", + "react-dom": "^16 || ^17 || ^18" + } + }, "node_modules/react-redux": { "version": "8.1.3", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-8.1.3.tgz", @@ -11769,51 +11757,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/simple-update-notifier": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", - "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", - "dev": true, - "dependencies": { - "semver": "^7.5.3" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/simple-update-notifier/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/simple-update-notifier/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/simple-update-notifier/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, "node_modules/sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", @@ -11878,9 +11821,9 @@ } }, "node_modules/spdx-exceptions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", - "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", "dev": true }, "node_modules/spdx-expression-parse": { @@ -11894,9 +11837,9 @@ } }, "node_modules/spdx-license-ids": { - "version": "3.0.16", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.16.tgz", - "integrity": "sha512-eWN+LnM3GR6gPu35WxNgbGl8rmY1AEmoMDvL/QD6zYmPWgywxWqJWNdLGT+ke8dKNWrcYgYjPpG5gbTfghP8rw==", + "version": "3.0.17", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.17.tgz", + "integrity": "sha512-sh8PWc/ftMqAAdFiBu6Fy6JUOYjqDJBJvIhpfDMyHrr0Rbp5liZqd4TjtQ/RgfLjKFZb+LMx5hpml5qOWy0qvg==", "dev": true }, "node_modules/sprintf-js": { @@ -11933,12 +11876,12 @@ "dev": true }, "node_modules/storybook": { - "version": "7.6.6", - "resolved": "https://registry.npmjs.org/storybook/-/storybook-7.6.6.tgz", - "integrity": "sha512-PmJxpjGdLvDOHaRzqLOvcJ3ALQPaNeW6D5Lv7rPPVbuO24wdDzd/75dPRP7gJKYcGE0NnDZ6cLQq3UlCfbkIBA==", + "version": "7.6.15", + "resolved": "https://registry.npmjs.org/storybook/-/storybook-7.6.15.tgz", + "integrity": "sha512-Ybezq9JRk5CBhzjgzZ/oT7mnU45UwhyVSGKW+PUKZGGUG9VH2hCrTEES9f/zEF82kj/5COVPyqR/5vlXuuS39A==", "dev": true, "dependencies": { - "@storybook/cli": "7.6.6" + "@storybook/cli": "7.6.15" }, "bin": { "sb": "index.js", @@ -11980,9 +11923,9 @@ } }, "node_modules/stream-shift": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", - "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", + "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==", "dev": true }, "node_modules/string_decoder": { @@ -12565,9 +12508,9 @@ } }, "node_modules/ufo": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.3.2.tgz", - "integrity": "sha512-o+ORpgGwaYQXgqGDwd+hkS4PuZ3QnmqMMxRuajK/a38L6fTpcE5GPIfrf+L/KemFzfUpeUQc1rRS1iDBozvnFA==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.4.0.tgz", + "integrity": "sha512-Hhy+BhRBleFjpJ2vchUNN40qgkh0366FWJGqVLYBHev0vpHTrXSA0ryT+74UiW6KWsldNurQMKGqCm1M2zBciQ==", "dev": true }, "node_modules/uglify-js": { @@ -12699,21 +12642,21 @@ } }, "node_modules/unplugin": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.5.1.tgz", - "integrity": "sha512-0QkvG13z6RD+1L1FoibQqnvTwVBXvS4XSPwAyinVgoOCl2jAgwzdUKmEj05o4Lt8xwQI85Hb6mSyYkcAGwZPew==", + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.7.1.tgz", + "integrity": "sha512-JqzORDAPxxs8ErLV4x+LL7bk5pk3YlcWqpSNsIkAZj972KzFZLClc/ekppahKkOczGkwIG6ElFgdOgOlK4tXZw==", "dev": true, "dependencies": { - "acorn": "^8.11.2", + "acorn": "^8.11.3", "chokidar": "^3.5.3", "webpack-sources": "^3.2.3", - "webpack-virtual-modules": "^0.6.0" + "webpack-virtual-modules": "^0.6.1" } }, "node_modules/unplugin/node_modules/acorn": { - "version": "8.11.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz", - "integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==", + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", "dev": true, "bin": { "acorn": "bin/acorn" @@ -12895,9 +12838,9 @@ } }, "node_modules/vite": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.1.tgz", - "integrity": "sha512-AXXFaAJ8yebyqzoNB9fu2pHoo/nWX+xZlaRwoeYUxEqBO+Zj4msE5G+BhGBll9lYEKv9Hfks52PAF2X7qDYXQA==", + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.2.tgz", + "integrity": "sha512-tBCZBNSBbHQkaGyhGCDUGqeo2ph8Fstyp6FMSvTtsXeZSPpSMGlviAOav2hxVTqFcx8Hj/twtWKsMJXNY0xI8w==", "dev": true, "dependencies": { "esbuild": "^0.18.10", @@ -13202,9 +13145,9 @@ "dev": true }, "node_modules/ws": { - "version": "8.15.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.15.1.tgz", - "integrity": "sha512-W5OZiCjXEmk0yZ66ZN82beM5Sz7l7coYxpRkzS+p9PP+ToQry8szKh+61eNktr7EA9DOwvFGhfC605jDHbP6QQ==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz", + "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==", "dev": true, "engines": { "node": ">=10.0.0" diff --git a/client/package.json b/client/package.json index 8f1ef75..db970fb 100644 --- a/client/package.json +++ b/client/package.json @@ -1,7 +1,7 @@ { "name": "mytinydc-utdon-client", "private": true, - "version": "1.3.0", + "version": "1.4.0", "description": "Application for tracking obsolete FOSS applications - UI", "type": "module", "scripts": { @@ -21,16 +21,17 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-intl": "^6.5.2", + "react-multi-select-component": "^4.3.4", "react-redux": "^8.1.3", "react-router-dom": "^6.18.0" }, "devDependencies": { - "@storybook/addon-essentials": "^7.6.4", - "@storybook/addon-interactions": "^7.6.4", - "@storybook/addon-links": "^7.6.4", - "@storybook/blocks": "^7.6.4", - "@storybook/react": "^7.6.4", - "@storybook/react-vite": "^7.6.4", + "@storybook/addon-essentials": "^7.6.15", + "@storybook/addon-interactions": "^7.6.15", + "@storybook/addon-links": "^7.6.15", + "@storybook/blocks": "^7.6.15", + "@storybook/react": "^7.6.15", + "@storybook/react-vite": "^7.6.15", "@storybook/testing-library": "^0.2.2", "@types/react": "^18.2.15", "@types/react-dom": "^18.2.7", @@ -42,7 +43,7 @@ "eslint-plugin-react-refresh": "^0.4.3", "eslint-plugin-storybook": "^0.6.15", "sass": "^1.69.5", - "storybook": "^7.6.4", + "storybook": "^7.6.15", "storybook-addon-react-router-v6": "^2.0.10", "typescript": "^5.0.2", "vite": "^4.4.5" diff --git a/client/src/App.tsx b/client/src/App.tsx index 5d44e0f..1cee6d2 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -12,12 +12,17 @@ import ServiceMessage from "./components/ServiceMessage"; import { RouterProvider } from "react-router-dom"; import { Router } from "./app/Router"; import { useEffect } from "react"; -import { setLanguage } from "./app/contextSlice"; +import { setIsLoaderShip, setLanguage } from "./app/contextSlice"; +import { Dialog } from "./components/Dialog"; + +import "./app/css/loadership.scss"; export const App = () => { const dispatch = useAppDispatch(); const contextLanguage = useAppSelector((state) => state.context.language); + const isDialogVisible = useAppSelector((state) => state.context.isLoaderShip); + useEffect(() => { // Browser language detection const navigatorLocale = @@ -37,6 +42,24 @@ export const App = () => { {/* Global Service Messenger */} + dispatch(setIsLoaderShip(false))} + sticky={true} + > +
+
+
+
+
+
+
+
+
+
+
+
); diff --git a/client/src/api/mytinydcUPDONApi.ts b/client/src/api/mytinydcUPDONApi.ts index c16db0d..b339373 100644 --- a/client/src/api/mytinydcUPDONApi.ts +++ b/client/src/api/mytinydcUPDONApi.ts @@ -16,7 +16,7 @@ export const mytinydcUPDONApi = createApi({ // Query service name reducerPath: "api", // tag types - tagTypes: ["User", "Users"], + tagTypes: ["User", "Users", "Groups", "Controls"], // Url Base API baseQuery: fetchBaseQuery({ baseUrl: "/api/v1" }), endpoints: (builder) => ({ @@ -57,10 +57,11 @@ export const mytinydcUPDONApi = createApi({ url: `/action/compare/${encodeURIComponent(uuid)}/0`, }), }), - getCheck: builder.query({ + getControl: builder.query({ query: (uuidOrAll: string) => ({ url: `/control/${uuidOrAll}`, }), + providesTags: ["Controls"], }), deleteCheck: builder.mutation({ query: (uuid: string) => ({ @@ -109,9 +110,12 @@ export const mytinydcUPDONApi = createApi({ }), providesTags: ["Users"], }), - getUserInfo: builder.query({ + // needed to keep context when use press F5 + // login method return info needed but if user press F5 once connected + // login method is not recalled + getUserLogin: builder.query({ query: () => ({ - url: `/user/`, + url: `/userlogin/`, }), providesTags: ["User"], }), @@ -121,7 +125,7 @@ export const mytinydcUPDONApi = createApi({ url: `/users/`, body: data, }), - invalidatesTags: ["Users"], + invalidatesTags: ["Users", "Groups"], }), putUser: builder.mutation({ query: (data: NewUserType) => ({ @@ -129,7 +133,7 @@ export const mytinydcUPDONApi = createApi({ url: `/users/`, body: data, }), - invalidatesTags: ["Users"], + invalidatesTags: ["Users", "Groups"], }), deleteUser: builder.mutation({ query: (login: string) => ({ @@ -139,12 +143,29 @@ export const mytinydcUPDONApi = createApi({ }), invalidatesTags: ["Users"], }), + getGroups: builder.query({ + query: () => ({ + url: `/groups/`, + }), + providesTags: ["Groups"], + }), + isAdmin: builder.query({ + query: () => ({ + url: `/isadmin/`, + }), + }), + getUserGroups: builder.query({ + query: () => ({ + url: `/userGroups/`, + }), + }), }), }); export const { usePostUserLoginMutation, - useGetCheckQuery, + useGetControlQuery, useGetUsersQuery, - useGetUserInfoQuery, + useGetUserLoginQuery, + useGetGroupsQuery, } = mytinydcUPDONApi; diff --git a/client/src/app/Router.tsx b/client/src/app/Router.tsx index 8644ac5..fd28d92 100644 --- a/client/src/app/Router.tsx +++ b/client/src/app/Router.tsx @@ -3,16 +3,16 @@ * @license AGPL3 */ -import { createBrowserRouter, redirect } from "react-router-dom"; +import { createBrowserRouter } from "react-router-dom"; import { ErrorInRouter } from "../features/errors/ErrorInRouter"; import { PageLogin } from "../features/login/PageLogin"; import { useAppDispatch } from "../app/hook"; import { mytinydcUPDONApi } from "../api/mytinydcUPDONApi"; import { showServiceMessage } from "./serviceMessageSlice"; import { PageHome } from "../features/homepage/PageHome"; -import { ApiResponseType } from "../../../src/Global.types"; import { DisplayControls } from "../features/displaycontrols/DisplayControls"; import { ControlManager } from "../features/controlmanagement/ControlManager"; +import { FetchBaseQueryError } from "@reduxjs/toolkit/query"; /** * Logic : @@ -31,23 +31,20 @@ export const Router = () => { return await dispatch( mytinydcUPDONApi.endpoints.getUserIsAuthenticated.initiate(null) ) - .then((response: unknown) => { - const convResponse: ApiResponseType = response as ApiResponseType; - // My api returns ... nothing only status 200 - Redux analyse like error !!! - if ( - convResponse.error && - convResponse.error.originalStatus !== 200 - ) { - return redirect("/login"); + .unwrap() + .catch((error: FetchBaseQueryError) => { + if (error.status === 401) { + return ; + } else { + dispatch( + showServiceMessage({ + detail: + error && error.data + ? error.data.toString() + : "Unknown check server logs", + }) + ); } - return null; - }) - .catch((error: unknown) => { - dispatch( - showServiceMessage({ - detail: error, - }) - ); }); }, children: [ diff --git a/client/src/app/contextSlice.ts b/client/src/app/contextSlice.ts index d9f7306..6d35f36 100644 --- a/client/src/app/contextSlice.ts +++ b/client/src/app/contextSlice.ts @@ -11,7 +11,6 @@ import { contextSliceType } from "../../../src/Global.types"; const initialState: contextSliceType = { // French is default language language: { locale: "fr", lang: languageFr }, - user: { login: "", bearer: "" }, application: { name: "UTdOn", applicationtitle: "UtDon", @@ -20,15 +19,15 @@ const initialState: contextSliceType = { }, uptodateForm: INITIALIZED_UPTODATEFORM, refetchuptodateForm: false, + isAdmin: false, + search: "", + isLoaderShip: false, }; export const contextSlice = createSlice({ name: "context", initialState, reducers: { - setUser: (state, value) => { - state.user = value.payload; - }, setLanguage: (state, value) => { if (value.payload === "fr") { state.language.locale = value.payload; @@ -55,16 +54,27 @@ export const contextSlice = createSlice({ setRefetchuptodateForm(state, value) { state.refetchuptodateForm = value.payload; }, + setIsAdmin(state, value) { + state.isAdmin = value.payload || false; + }, + setSearch(state, value) { + state.search = value.payload; + }, + setIsLoaderShip(state, value) { + state.isLoaderShip = value.payload; + }, }, }); // Exportable actions export const { setLanguage, - setUser, updateKeyUptodateFrom, resetUpdateForm, setUpdateForm, setRefetchuptodateForm, + setIsAdmin, + setSearch, + setIsLoaderShip, } = contextSlice.actions; export default contextSlice.reducer; diff --git a/client/src/app/css/loadership.scss b/client/src/app/css/loadership.scss new file mode 100644 index 0000000..9f81f4a --- /dev/null +++ b/client/src/app/css/loadership.scss @@ -0,0 +1,90 @@ +.loadership_Dialog { + .modal-content { + background-color: transparent !important; + } + + .loadership_ILQMG { + display: flex; + position: relative; + width: 54px; + height: 54px; + } + + .loadership_ILQMG div { + position: absolute; + width: 18px; + height: 18px; + background: #653799; + animation: loadership_ILQMG_scale 1.9s infinite, + loadership_ILQMG_fade 1.9s infinite; + animation-timing-function: ease-in-out; + } + + .loadership_ILQMG div:nth-child(1) { + animation-delay: 0s; + top: 0px; + left: 0px; + } + .loadership_ILQMG div:nth-child(2) { + animation-delay: 0.15s; + top: 0px; + left: 18px; + } + .loadership_ILQMG div:nth-child(3) { + animation-delay: 0.3s; + top: 0px; + left: 36px; + } + .loadership_ILQMG div:nth-child(4) { + animation-delay: 0.15s; + top: 18px; + left: 0px; + } + .loadership_ILQMG div:nth-child(5) { + animation-delay: 0.3s; + top: 18px; + left: 18px; + } + .loadership_ILQMG div:nth-child(6) { + animation-delay: 0.45s; + top: 18px; + left: 36px; + } + .loadership_ILQMG div:nth-child(7) { + animation-delay: 0.3s; + top: 36px; + left: 0px; + } + .loadership_ILQMG div:nth-child(8) { + animation-delay: 0.45s; + top: 36px; + left: 18px; + } + .loadership_ILQMG div:nth-child(9) { + animation-delay: 0.6s; + top: 36px; + left: 36px; + } + + @keyframes loadership_ILQMG_scale { + 0%, + 47.36842105263158%, + 100% { + transform: scale(1); + } + 23.68421052631579% { + transform: scale(0); + } + } + + @keyframes loadership_ILQMG_fade { + 0%, + 47.36842105263158%, + 100% { + opacity: 1; + } + 23.68421052631579% { + opacity: 1; + } + } +} diff --git a/client/src/components/Block.scss b/client/src/components/Block.scss index c36b8a8..147da51 100644 --- a/client/src/components/Block.scss +++ b/client/src/components/Block.scss @@ -2,6 +2,7 @@ .Block { min-width: 256px; display: flex; + flex-direction: row; box-shadow: $boxshadow-default; padding: $padding-default; margin: $margin-default; @@ -12,4 +13,4 @@ .Block { box-shadow: $boxshadow-default-dark; } -} \ No newline at end of file +} diff --git a/client/src/components/Control.scss b/client/src/components/Control.scss index a6bdc89..65ef764 100644 --- a/client/src/components/Control.scss +++ b/client/src/components/Control.scss @@ -27,6 +27,7 @@ flex-grow: 1; display: flex; flex-direction: column; + overflow: hidden; .name { flex-grow: 1; font-weight: bold; diff --git a/client/src/components/Control.tsx b/client/src/components/Control.tsx index 659af57..007c3e7 100644 --- a/client/src/components/Control.tsx +++ b/client/src/components/Control.tsx @@ -21,12 +21,13 @@ import { Badge } from "./Badge"; import { ResultCompare } from "./ResultCompare"; import { getRelativeTime } from "../helpers/DateHelper"; import { INPROGRESS_UPTODATEORNOTSTATE } from "../../../src/Constants"; +import { useAppSelector } from "../app/hook"; interface ControlProps { data: UptodateForm; handleOnDelete: (uuid: string) => void; - handleOnCompare: (check: UptodateForm) => void; - handleOnPause: (check: ChangeEvent, uuid: string) => void; + handleOnCompare: (control: UptodateForm) => void; + handleOnPause: (control: ChangeEvent, uuid: string) => void; userAuthBearer: string; } export const Control = ({ @@ -55,6 +56,8 @@ export const Control = ({ ); const [isDialogCompareVisible, setIsDialogCompareVisible] = useState(false); + const isAdmin = useAppSelector((state) => state.context.isAdmin); + /** * To update the badge's relative time without having to update the entire content */ @@ -79,7 +82,7 @@ export const Control = ({ alt={`logo app ${data.name}`} /> ) : ( - "" + <> )}
@@ -94,6 +97,17 @@ export const Control = ({
+ {isAdmin ? ( +
+
{data.groups && data.groups.join(",")}
+
+ ) : ( + <> + )} + diff --git a/client/src/components/Dialog.tsx b/client/src/components/Dialog.tsx index 4842a1a..065fc4e 100644 --- a/client/src/components/Dialog.tsx +++ b/client/src/components/Dialog.tsx @@ -15,6 +15,7 @@ interface DialogInterface { header?: string; footerClose?: boolean; className?: string; + sticky?: boolean; } export const Dialog = ({ @@ -25,6 +26,7 @@ export const Dialog = ({ header = "", footerClose = false, className, + sticky = false, }: DialogInterface) => { const intl = useIntl(); @@ -33,7 +35,9 @@ export const Dialog = ({
onHide()} + onClick={() => { + if (!sticky) onHide(); + }} className={`modal-common modal-container ${ className ? className : "" }`} diff --git a/client/src/components/FieldSetApiEntrypoint.tsx b/client/src/components/FieldSetApiEntrypoint.tsx index 6dc36c3..82de55d 100644 --- a/client/src/components/FieldSetApiEntrypoint.tsx +++ b/client/src/components/FieldSetApiEntrypoint.tsx @@ -81,8 +81,10 @@ export const FieldSetApiEntrypoint = ({
{`curl -s ${kParameter ? "-k" : ""} ${ - userAuthBearer ? `-H "Authorization: ${userAuthBearer}"` : "" - } ${body ? `-d "${body}"` : ""} ${url}`} + method && method !== "GET" ? `-X ${method}` : "" + } ${userAuthBearer ? `-H "Authorization: ${userAuthBearer}"` : ""} ${ + body ? `-H "Content-Type: application/json" --data '${body}'` : "" + } ${url}`}
{ const intl = useIntl(); @@ -31,13 +35,17 @@ export const Header = () => { const [dialogHeader, setDialogHeader] = useState(""); + const isAdmin = useAppSelector((state) => state.context.isAdmin); + + const searchString = useAppSelector((state) => state.context.search); + /** * Used for server errors (api entrypoint call) * @param error * @returns */ const dispatchServerError = (error: FetchBaseQueryError) => { - if (error) { + if (error && error.data) { const servererror = error.data as ErrorServer; if (servererror.error) { dispatch( @@ -59,6 +67,9 @@ export const Header = () => { forceRefetch: true, }) ).then(() => { + // cache behavior uneexpected !!! + // dispatch(mytinydcUPDONApi.util.resetApiState()); + // dispatch(mytinydcUPDONApi.util.invalidateTags(["Controls"])); return navigate("/login"); }); }; @@ -95,7 +106,7 @@ export const Header = () => { ); setIsDialogVisible(true); }) - .catch((error) => { + .catch((error: FetchBaseQueryError) => { dispatchServerError(error); }); }; @@ -112,10 +123,24 @@ export const Header = () => { const [dialogContent, setDialogContent] = useState(<>); - const { data: userInfo, isSuccess } = useGetUserInfoQuery(null, { + const { data: userInfo, isSuccess } = useGetUserLoginQuery(null, { skip: false, }); + useEffect(() => { + dispatch( + mytinydcUPDONApi.endpoints.isAdmin.initiate(null, { forceRefetch: true }) + ) + .unwrap() + .then(() => { + dispatch(setIsAdmin(true)); + }) + .catch((error: FetchBaseQueryError) => { + dispatch(setIsAdmin(false)); + if (error && error.status !== 401) dispatchServerError(error); + }); + }, []); + return (
@@ -166,13 +191,18 @@ export const Header = () => { onClick={displayDialogCurlCommands} />
+ {location.pathname === "/" ? ( + + ) : null}
- + {isAdmin ? ( + + ) : null} ; export const Primary: Story = { args: { - check: STORYBOOK_UPTODATEFORM, + control: STORYBOOK_UPTODATEFORM, result: STORYBOOK_UPDATEORNOTSTATE, }, }; diff --git a/client/src/components/ResultCompare.tsx b/client/src/components/ResultCompare.tsx index 5400638..0c8cbb0 100644 --- a/client/src/components/ResultCompare.tsx +++ b/client/src/components/ResultCompare.tsx @@ -28,11 +28,11 @@ import { INITIALIZED_TOAST } from "../../../src/Constants"; import { getRelativeTime } from "../helpers/DateHelper"; interface ResultCompareProps { - check: UptodateForm; + control: UptodateForm; result: UptoDateOrNotState; } -export const ResultCompare = ({ result, check }: ResultCompareProps) => { +export const ResultCompare = ({ result, control }: ResultCompareProps) => { const intl = useIntl(); const dispatch = useAppDispatch(); @@ -42,13 +42,15 @@ export const ResultCompare = ({ result, check }: ResultCompareProps) => { const dispatchServerError = (error: FetchBaseQueryError) => { if (error) { - const servererror = error.data as ErrorServer; + const servererror = error.data + ? (JSON.parse(error.data as string) as ErrorServer) + : { error: "Unknown error" }; dispatch( showServiceMessage({ ...INITIALIZED_TOAST, severity: "error", sticky: true, - detail: intl.formatMessage({ id: JSON.parse(servererror.error) }), + detail: servererror.error, }) ); if (error.status === 401) return navigate("/login"); @@ -89,7 +91,8 @@ export const ResultCompare = ({ result, check }: ResultCompareProps) => { }) ); }) - .catch((error) => { + .catch((error: FetchBaseQueryError) => { + console.log(error); dispatchServerError(error); }); }; @@ -133,19 +136,24 @@ export const ResultCompare = ({ result, check }: ResultCompareProps) => { <>
-
{check.name}
+
{control.name}
- {check.compareResult && check.compareResult.ts ? ( + {control.compareResult && control.compareResult.ts ? (
- <>{getRelativeTime(check.compareResult.ts, intl)} + <> + {getRelativeTime( + result.ts ? result.ts : control.compareResult.ts, + intl + )} +
) : ( <> @@ -212,18 +220,18 @@ export const ResultCompare = ({ result, check }: ResultCompareProps) => { id: "Send status to monitoring service", })} onClick={() => { - if (check.uuid) { + if (control.uuid) { handleSendStateExternalMonitoring( - check.uuid, + control.uuid, result.state, result.productionVersion, result.githubLatestRelease ); } }} - disabled={!check.urlCronJobMonitoring || check.isPause} + disabled={!control.urlCronJobMonitoring || control.isPause} title={ - !check.urlCronJobMonitoring || check.isPause + !control.urlCronJobMonitoring || control.isPause ? intl.formatMessage({ id: "Disabled because the monitoring service url is not defined or actions are disabled", }) @@ -236,18 +244,18 @@ export const ResultCompare = ({ result, check }: ResultCompareProps) => { id: "Start the action on the CI/CD chain", })} onClick={() => { - if (check.uuid) { + if (control.uuid) { setIsConfirmDialogVisible(true); } }} title={ - !check.urlCICD || check.isPause + !control.urlCICD || control.isPause ? intl.formatMessage({ id: "Disabled because the CI/CD service url is not defined or actions are disabled", }) : "" } - disabled={!check.urlCICD || check.isPause} + disabled={!control.urlCICD || control.isPause} />
@@ -261,7 +269,7 @@ export const ResultCompare = ({ result, check }: ResultCompareProps) => { } onConfirm={() => { setIsConfirmDialogVisible(false); - handleStartCiCd(check.uuid as string); + handleStartCiCd(control.uuid as string); }} onCancel={() => setIsConfirmDialogVisible(false)} /> diff --git a/client/src/components/ScrapProduction.scss b/client/src/components/ScrapProduction.scss index 727a6cc..5094532 100644 --- a/client/src/components/ScrapProduction.scss +++ b/client/src/components/ScrapProduction.scss @@ -7,10 +7,10 @@ padding: $padding-default; width: 100%; .Block { - align-items: flex-start; + flex-wrap: wrap; + align-items: stretch; &.filter { flex-direction: row; - align-items: flex-start; } .expression { flex-grow: 1; @@ -18,6 +18,7 @@ .ImageUploader { display: flex; justify-content: center; + width: 25%; } .name { width: 25%; @@ -30,6 +31,16 @@ .inputgeneric { width: 90%; } + width: 25%; + } + .getcontent { + display: flex; + flex-direction: row; + width: max-content; + align-items: flex-start; + } + .groups { + min-width: 15rem; } .content { width: 100%; diff --git a/client/src/components/ScrapProduction.stories.tsx b/client/src/components/ScrapProduction.stories.tsx index 684056d..7262633 100644 --- a/client/src/components/ScrapProduction.stories.tsx +++ b/client/src/components/ScrapProduction.stories.tsx @@ -34,16 +34,16 @@ const activeUptodateForm: UptodateForm = { }; const Component = (args: ScrapProductionProps) => { - const [check, setCheck] = useState(args.activeUptodateForm); + const [control, setControl] = useState(args.activeUptodateForm); args = { ...args, - handleOnChange: (key: UptodateFormFields, value: string) => { + handleOnChange: (key: UptodateFormFields, value: string | string[]) => { // defined by stories if (key === "scrapTypeProduction") return; - setCheck({ ...check, [key]: value }); + setControl({ ...control, [key]: value }); }, }; - return ; + return ; }; export const ScrapAsText: Story = { diff --git a/client/src/components/ScrapProduction.tsx b/client/src/components/ScrapProduction.tsx index fdc9832..88d20cf 100644 --- a/client/src/components/ScrapProduction.tsx +++ b/client/src/components/ScrapProduction.tsx @@ -28,13 +28,20 @@ import { Block } from "./Block"; import { FieldSet } from "./FieldSet"; import { ImageUploader } from "./ImageUploader"; import { + INITIALIZED_TOAST, SCRAPTYPEOPTIONJSON, SCRAPTYPEOPTIONTEXT, } from "../../../src/Constants"; +import { mytinydcUPDONApi, useGetGroupsQuery } from "../api/mytinydcUPDONApi"; +import { MultiSelect, Option } from "react-multi-select-component"; +import { buidMultiSelectGroups } from "../helpers/UiMiscHelper"; +import { useAppDispatch, useAppSelector } from "../app/hook"; +import { FetchBaseQueryError } from "@reduxjs/toolkit/query"; +import { showServiceMessage } from "../app/serviceMessageSlice"; export interface ScrapProductionProps { activeUptodateForm: UptodateForm; - handleOnChange: (key: UptodateFormFields, value: string) => void; + handleOnChange: (key: UptodateFormFields, value: string | string[]) => void; scrapUrl: (url: string) => Promise; onDone: (changeDoneState: boolean) => void; displayError: (message: string) => void; @@ -48,6 +55,7 @@ export const ScrapProduction = ({ displayError, }: ScrapProductionProps) => { const intl = useIntl(); + const dispatch = useAppDispatch(); const [scrapContent, setScrapContent] = useState(""); useState("json"); @@ -57,6 +65,18 @@ export const ScrapProduction = ({ { value: "text", label: SCRAPTYPEOPTIONTEXT }, ]; + const isAdmin = useAppSelector((state) => state.context.isAdmin); + + const { + data: groupsFromServer, + isError, + error, + isUninitialized, + refetch, + } = useGetGroupsQuery(null, { + skip: false, + }); + /** * server return ALWAYS string */ @@ -108,6 +128,14 @@ export const ScrapProduction = ({ handleOnChange("exprProduction", value); }; + const handleOnChangeGroups = (value: Option[]) => { + const controlGroups: string[] = []; + for (const item of value) { + controlGroups.push(item.value); + } + handleOnChange("groups", controlGroups); + }; + useEffect(() => { setProductionVersion(""); handleApplyProductionContentRegExp(); @@ -118,6 +146,29 @@ export const ScrapProduction = ({ setScrapContent(""); }, [activeUptodateForm.urlProduction]); + useEffect(() => { + if (!isUninitialized) refetch(); + // only normal users set own groups + if (!isAdmin) + dispatch(mytinydcUPDONApi.endpoints.getUserGroups.initiate(null)) + .unwrap() + .then((response) => { + if (response.groups) { + handleOnChange("groups", response.groups); + } + }) + .catch((error: FetchBaseQueryError) => { + dispatch( + showServiceMessage({ + ...INITIALIZED_TOAST, + severity: "error", + sticky: true, + detail: error.data ? error.data : "unknown error", + }) + ); + }); + }, []); + return (
@@ -130,7 +181,7 @@ export const ScrapProduction = ({
+
+ {!isError ? ( + 0 + ? buidMultiSelectGroups(activeUptodateForm.groups) + : [] + } + onChange={(values: Option[]) => handleOnChangeGroups(values)} + labelledBy={intl.formatMessage({ id: "Includes in group(s)" })} + /> + ) : ( +
{error.toString()}
+ )} +
diff --git a/client/src/components/Search.scss b/client/src/components/Search.scss new file mode 100644 index 0000000..7a8f7f3 --- /dev/null +++ b/client/src/components/Search.scss @@ -0,0 +1,28 @@ +@import "../app/css/root.scss"; +$searchbuttonsize: 2rem; +.Search { + margin-left: 2rem; + display: flex; + flex-direction: row; + column-gap: 0.2rem; + + border: 1px solid #8c6cb1; + border-radius: 6px; + padding: 0 0.2rem; + .inputgeneric { + border: none; + } + .inputgeneric:focus { + outline: none; + } + .ButtonGeneric { + width: $searchbuttonsize; + height: $searchbuttonsize; + align-self: center; + background-color: transparent; + i { + font-size: 1rem; + color: #f2598b; + } + } +} diff --git a/client/src/components/Search.stories.tsx b/client/src/components/Search.stories.tsx new file mode 100644 index 0000000..ade218a --- /dev/null +++ b/client/src/components/Search.stories.tsx @@ -0,0 +1,50 @@ +import type { Meta, StoryObj } from "@storybook/react"; +// import { useArgs } from "@storybook/preview-api"; + +import { Search } from "./Search"; +import { + reactRouterParameters, + withRouter, +} from "storybook-addon-react-router-v6"; + +// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction#default-export +const meta = { + title: "Components/Input/Search", + component: Search, + decorators: [withRouter], + parameters: { + // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/react/configure/story-layout + layout: "fullscreen", + reactRouter: reactRouterParameters({ + location: { path: "/" }, + }), + }, + // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/react/writing-docs/autodocs + tags: ["autodocs"], + // More on argTypes: https://storybook.js.org/docs/react/api/argtypes + argTypes: {}, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// If you need to keep state in storybook, you also could use the app redux store +// const Component = ({ ...args }) => { +// const [, setArgs] = useArgs(); +// const onChange = (value: string) => { +// // Call the provided callback +// // This is used for the Actions tab +// args.onChange?.(value); +// +// // Update the arg in Storybook +// setArgs({ value }); +// }; +// return ; +// }; +export const Primary: Story = { + args: { + searchString: "", + }, + // if you need to get a specific render see SelectArs component... + // render: (args) => Component(args), +}; diff --git a/client/src/components/Search.tsx b/client/src/components/Search.tsx new file mode 100644 index 0000000..2cc63fc --- /dev/null +++ b/client/src/components/Search.tsx @@ -0,0 +1,44 @@ +/** + * @author DHENRY for mytinydc.com + * @license AGPL3 + */ + +import { useIntl } from "react-intl"; +import { useAppDispatch } from "../app/hook"; + +import "./Search.scss"; +import InputGeneric from "./InputGeneric"; +import { setSearch } from "../app/contextSlice"; +import ButtonGeneric from "./ButtonGeneric"; + +interface SearchProps { + searchString: string; +} +export const Search = ({ searchString }: SearchProps) => { + const intl = useIntl(); + const dispatch = useAppDispatch(); + + return ( +
+ { + dispatch(setSearch(value)); + }} + autoComplete="off" + className="inputsearch" + autoFocus + /> + {searchString ? ( + { + dispatch(setSearch("")); + }} + title={intl.formatMessage({ id: "Reset" })} + /> + ) : null} +
+ ); +}; diff --git a/client/src/components/Summary.tsx b/client/src/components/Summary.tsx index 004b107..a5d5787 100644 --- a/client/src/components/Summary.tsx +++ b/client/src/components/Summary.tsx @@ -81,6 +81,15 @@ export const Summary = ({
{uptodateForm.name}
+
+
+ {uptodateForm && + uptodateForm.groups && + uptodateForm.groups.join(",")} +
+
{uptodateForm.uuid ? ( <>
@@ -174,7 +183,7 @@ export const Summary = ({ >
diff --git a/client/src/features/changepassword/ChangePassword.tsx b/client/src/features/changepassword/ChangePassword.tsx index 73d3b8c..2469cc6 100644 --- a/client/src/features/changepassword/ChangePassword.tsx +++ b/client/src/features/changepassword/ChangePassword.tsx @@ -4,7 +4,7 @@ */ import { useIntl } from "react-intl"; -import { useAppDispatch, useAppSelector } from "../../app/hook"; +import { useAppDispatch } from "../../app/hook"; import { useNavigate } from "react-router-dom"; import "./ChangePassword.scss"; @@ -31,8 +31,6 @@ export const ChangePassword = ({ onHide }: ChangePasswordProps) => { const dispatch = useAppDispatch(); const navigate = useNavigate(); - const login = useAppSelector((state) => state.context.user.login); - const [formData, setFormData] = useState( INITIALIZED_CHANGEPASSWORD ); @@ -73,7 +71,6 @@ export const ChangePassword = ({ onHide }: ChangePasswordProps) => { dispatch( mytinydcUPDONApi.endpoints.putChangePassword.initiate({ ...formData, - login, }) ) .unwrap() diff --git a/client/src/features/controlmanagement/ControlManager.tsx b/client/src/features/controlmanagement/ControlManager.tsx index f5df5d4..8168d01 100644 --- a/client/src/features/controlmanagement/ControlManager.tsx +++ b/client/src/features/controlmanagement/ControlManager.tsx @@ -106,6 +106,9 @@ export const ControlManager = () => { break; } } + if (activeUptodateForm.groups && activeUptodateForm.groups.length === 0) { + recordableState = false; + } setIsRecordable(recordableState); }; @@ -148,7 +151,7 @@ export const ControlManager = () => { */ const handleOnChangeUptodateForm = ( key: UptodateFormFields, - value: string + value: string | string[] ) => { dispatch(updateKeyUptodateFrom({ key: key, value: value })); // each time the model change to indicate user have to save @@ -186,12 +189,12 @@ export const ControlManager = () => { dispatch(mytinydcUPDONApi.endpoints.postCheck.initiate(uptodateForm)) .unwrap() .then((response) => { - const check = response?.check as UptodateForm; + const control = response?.control as UptodateForm; //update uuid - if (check.uuid) { - handleOnChangeUptodateForm("uuid", check.uuid); + if (control.uuid) { + handleOnChangeUptodateForm("uuid", control.uuid); setIsChangesOnModel(false); - resolv(check); + resolv(control); } else { const message = "the uuid is missing from the server response"; dispatch( @@ -246,7 +249,7 @@ export const ControlManager = () => { const uuid = location.pathname.replace(/\/ui\/editcontrol\//, ""); if (uuid) { dispatch( - mytinydcUPDONApi.endpoints.getCheck.initiate(uuid, { + mytinydcUPDONApi.endpoints.getControl.initiate(uuid, { forceRefetch: true, }) ) diff --git a/client/src/features/curlcommands/CurlCommands.tsx b/client/src/features/curlcommands/CurlCommands.tsx index 7c50056..757218e 100644 --- a/client/src/features/curlcommands/CurlCommands.tsx +++ b/client/src/features/curlcommands/CurlCommands.tsx @@ -70,7 +70,7 @@ export const CurlCommands = ({ id: "API entry point for the github version of the latest comparison", })} userAuthBearer={auth} - apiEntrypoint={`/api/v1/action/lastcomparegitrealase/${uptodateForm.uuid}`} + apiEntrypoint={`/api/v1/action/lastcomparegitrelease/${uptodateForm.uuid}`} method={"GET"} /> { const intl = useIntl(); @@ -58,13 +64,15 @@ export const DisplayControls = () => { const [userAuthBearer, setuserAuthBearer] = useState(""); + const searchString = useAppSelector((state) => state.context.search); + const { data, isSuccess, isError, refetch, error: ErrorOnFetch, - } = useGetCheckQuery("all", { + } = useGetControlQuery("all", { skip: false, }); @@ -158,24 +166,30 @@ export const DisplayControls = () => { }); }; - const handleOnCompare = async (check: UptodateForm) => { - if (check.uuid) { + const handleOnCompare = async (control: UptodateForm) => { + if (control.uuid) { + dispatch(setIsLoaderShip(true)); await dispatch( - mytinydcUPDONApi.endpoints.getCompare.initiate(check.uuid, { + mytinydcUPDONApi.endpoints.getCompare.initiate(control.uuid, { forceRefetch: true, }) ) .unwrap() .then((response) => { if (response) { - refetch(); setIsDialogVisible(true); setResultCompare(response); - setcheckInProgress(check); + setcheckInProgress(control); + refetch(); } }) .catch((error: FetchBaseQueryError) => { dispatchServerError(error); + }) + .finally(() => { + setTimeout(() => { + dispatch(setIsLoaderShip(false)); + }, 500); }); } else { dispatch( @@ -238,6 +252,11 @@ export const DisplayControls = () => { {/*
Filter on :
*/}
{data.map((item: UptodateForm) => { + if ( + searchString && + !item.name.match(new RegExp(searchString, "i")) + ) + return null; return ( { > { const handleOnLogin = async (jsonloginpassword: PostAuthent) => { return await userLogin(jsonloginpassword) .unwrap() - .then(async (response) => { - // set token to settings - dispatch(setUser(response)); + .then(async () => { + // user info will be set by header. Header is calle even user press F5 + // if already connected, info will be redispatch on global state // reset Toast await dispatch(clearToast()); return navigate("/"); diff --git a/client/src/features/usermanager/UserManager.scss b/client/src/features/usermanager/UserManager.scss index 3eeb2c4..c9e4569 100644 --- a/client/src/features/usermanager/UserManager.scss +++ b/client/src/features/usermanager/UserManager.scss @@ -1,10 +1,31 @@ @import "../../app/css/root.scss"; + +$usermanagerbuttonsize: 2rem; .UserManager { $color-border: #303030; @media (prefers-color-scheme: dark) { $color-border: #e0e0e0; } + .title { + display: flex; + flex-direction: row; + align-items: center; + column-gap: 1rem; + padding-top: 1rem; + h2 { + margin-top: 0; + padding-top: 0; + } + .ButtonGeneric { + border-radius: 100%; + width: $usermanagerbuttonsize; + height: $usermanagerbuttonsize; + i { + font-size: 1rem; + } + } + } .new-user-label { margin-bottom: 0; } @@ -15,6 +36,9 @@ .form { display: flex; + .groups { + min-width: 15rem; + } } .userslist { @@ -32,7 +56,7 @@ } .flex-table { display: grid; - grid-template-columns: 2rem auto 30%; + grid-template-columns: 2rem auto 30% 30%; grid-template-rows: 100% auto; transition: 0.5s; &:first-of-type .flex-row { @@ -55,6 +79,10 @@ color: $color-text-primary; } } + .groups { + text-overflow: ellipsis; + overflow: hidden; + } } } diff --git a/client/src/features/usermanager/UserManager.tsx b/client/src/features/usermanager/UserManager.tsx index e285872..f9aa6bd 100644 --- a/client/src/features/usermanager/UserManager.tsx +++ b/client/src/features/usermanager/UserManager.tsx @@ -4,7 +4,7 @@ */ import { useIntl } from "react-intl"; -import { useAppDispatch, useAppSelector } from "../../app/hook"; +import { useAppDispatch } from "../../app/hook"; import { useNavigate } from "react-router-dom"; import "./UserManager.scss"; @@ -14,14 +14,25 @@ import { INITIALIZED_NEWUSER, INITIALIZED_TOAST, } from "../../../../src/Constants"; -import { ErrorServer, NewUserType } from "../../../../src/Global.types"; +import { + ErrorServer, + NewUserType, + UserDescriptionType, +} from "../../../../src/Global.types"; import { FieldSet } from "../../components/FieldSet"; import ButtonGeneric from "../../components/ButtonGeneric"; import { Block } from "../../components/Block"; -import { mytinydcUPDONApi, useGetUsersQuery } from "../../api/mytinydcUPDONApi"; +import { + mytinydcUPDONApi, + useGetGroupsQuery, + useGetUserLoginQuery, + useGetUsersQuery, +} from "../../api/mytinydcUPDONApi"; import { FetchBaseQueryError } from "@reduxjs/toolkit/query"; import { showServiceMessage } from "../../app/serviceMessageSlice"; import { ConfirmDialog } from "../../components/ConfirmDialog"; +import { MultiSelect, Option } from "react-multi-select-component"; +import { buidMultiSelectGroups } from "../../helpers/UiMiscHelper"; export const UserManager = () => { const intl = useIntl(); @@ -30,17 +41,42 @@ export const UserManager = () => { const [formData, setFormData] = useState(INITIALIZED_NEWUSER); const [httpMethod, setHttpMethod] = useState<"POST" | "PUT">("POST"); + const [editMode, setEditMode] = useState(false); + + const [userGroups, setUserGroups] = useState([]); - const { data: users, isSuccess } = useGetUsersQuery(null, { + const { + data: users, + isSuccess, + refetch, + isUninitialized, + } = useGetUsersQuery(null, { skip: false, }); - const userLogger = useAppSelector((state) => state.context.user); + const { + data: groupsFromServer, + isSuccess: isSuccessGroups, + refetch: refetchGroups, + isUninitialized: isUninitializedGroups, + } = useGetGroupsQuery(null, { + skip: false, + }); + + const { data: userInfo, isSuccess: isSuccessUserInfo } = useGetUserLoginQuery( + null, + { + skip: false, + } + ); const [isButtonDisabled, setIsButtonDisabled] = useState(true); - const [userToDelete, setUserToDelete] = useState(null); + const [userToDelete, setUserToDelete] = useState( + null + ); const [confirmDeleteIsVisible, setConfirmDeleteIsVisible] = useState(false); + /** * Used for server errors (api entrypoint call) * @param error @@ -63,15 +99,22 @@ export const UserManager = () => { } }; + const setContextAsNewUser = () => { + setEditMode(false); + setFormData({ ...INITIALIZED_NEWUSER }); + setHttpMethod("POST"); + }; + const handleOnPost = async (e: React.FormEvent | null) => { e?.preventDefault(); - if (formData.login && formData.password) { + if (isFormComplete()) { (httpMethod === "POST" ? dispatch(mytinydcUPDONApi.endpoints.postUser.initiate(formData)) : dispatch(mytinydcUPDONApi.endpoints.putUser.initiate(formData)) ) .unwrap() .then(() => { + setContextAsNewUser(); // onHide(); dispatch( showServiceMessage({ @@ -80,25 +123,24 @@ export const UserManager = () => { sticky: false, detail: `${ httpMethod === "POST" - ? intl.formatMessage({ id: `User created` }) - : intl.formatMessage({ id: `User updated` }) + ? intl.formatMessage({ id: `The user has been created` }) + : intl.formatMessage({ id: `The user has been updated` }) }: ${formData.login}`, }) ); setFormData(INITIALIZED_NEWUSER); }) - .catch((error) => { + .catch((error: FetchBaseQueryError) => { dispatchServerError(error); - }) - .finally(() => { - setHttpMethod("POST"); }); } }; const deleteUser = () => { if (userToDelete) { - dispatch(mytinydcUPDONApi.endpoints.deleteUser.initiate(userToDelete)) + dispatch( + mytinydcUPDONApi.endpoints.deleteUser.initiate(userToDelete.uuid) + ) .unwrap() .then(() => { dispatch( @@ -120,14 +162,21 @@ export const UserManager = () => { }); } }; - const handleOnDelete = async (user: string) => { + const handleOnDelete = async (user: UserDescriptionType) => { + setContextAsNewUser(); setUserToDelete(user); setConfirmDeleteIsVisible(true); - return; }; - const handleOnEdit = async (user: string) => { - setFormData({ login: user, password: "" }); + const handleOnEdit = async (user: UserDescriptionType) => { + const userDesc = { + ...user, + groups: user.groups, + password: "", + uuid: user.uuid, + } as NewUserType; + setEditMode(true); + setFormData(userDesc); setHttpMethod("PUT"); dispatch( showServiceMessage({ @@ -135,34 +184,85 @@ export const UserManager = () => { severity: "info", sticky: false, detail: `${intl.formatMessage({ - id: `You can assign a new password for the selected user`, - })}: ${user}`, + id: `You can assign a new password, or groups to the selected user`, + })}: ${user.login}`, + life: 8000, }) ); }; - const handleOnChange = (key: string, value: string) => { + const handleOnChange = (key: string, value: string | string[] | Option[]) => { + // Convert Option[] to string[] + if (key === "groups" && Array.isArray(value)) { + const updateGroups: string[] = []; + const newGroups: string[] = []; + for (const option of value as Option[]) { + const test = { ...option } as any; //__isNew__ not set as normal attribut ??? + if (test.__isNew__) updateGroups.push(option.label); + newGroups.push(option.label); + } + value = [...newGroups]; + if (updateGroups.length > 0) { + setUserGroups(userGroups.concat(updateGroups)); + } + } setFormData({ ...formData, [key]: value }); }; + const isFormComplete = () => { + // mandatory password if new user + if (!formData.uuid && !formData.password) return false; + // mandatory login and group + if (formData.login && formData.groups.length > 0) return true; + return false; + }; + useEffect(() => { - if (formData.password && formData.login) { + if (isFormComplete()) { setIsButtonDisabled(false); } else { setIsButtonDisabled(true); } }, [formData]); + useEffect(() => { + if (groupsFromServer) setUserGroups(groupsFromServer); + }, [groupsFromServer]); + + useEffect(() => { + if (!isUninitialized) refetch(); + if (!isUninitializedGroups) refetchGroups(); + }, []); + return (
-

- {intl.formatMessage({ id: "Add a new user" })} -

+ {!editMode ? ( +
+

+ {intl.formatMessage({ id: "Add a new user" })} +

+
+ ) : ( +
+

+ {intl.formatMessage({ id: "Edit user" })} +

+ { + setFormData(INITIALIZED_NEWUSER); + setEditMode(false); + setHttpMethod("POST"); + }} + title={intl.formatMessage({ id: "Add a new user" })} + /> +
+ )}
{ autoComplete="new-password" />
+
+ 0 + ? buidMultiSelectGroups(formData.groups) + : [] + } + onChange={(values: Option[]) => handleOnChange("groups", values)} + labelledBy={intl.formatMessage({ id: "Includes in group(s)" })} + isCreatable={true} + disabled={!formData.login} + /> +
{
{intl.formatMessage({ id: "Username" })}
+
+ {intl.formatMessage({ id: "Groups" })} +
{intl.formatMessage({ id: "Actions" })}
{isSuccess && - (users as string[]).map((user) => { + isSuccessGroups && + isSuccessUserInfo && + (users as UserDescriptionType[]).map((user) => { return ( -
+
{""}
- {user} + {user.login} +
+
+ {user.groups.join(",")}
- {user !== "admin" && user !== userLogger.login ? ( + {user.login !== "admin" && user.login !== userInfo.login ? (
handleOnDelete(user)} @@ -244,9 +373,9 @@ export const UserManager = () => { { setConfirmDeleteIsVisible(false); deleteUser(); diff --git a/client/src/helpers/UiMiscHelper.ts b/client/src/helpers/UiMiscHelper.ts index d3ac01b..a29fafe 100644 --- a/client/src/helpers/UiMiscHelper.ts +++ b/client/src/helpers/UiMiscHelper.ts @@ -1,3 +1,4 @@ +import { Option } from "react-multi-select-component"; /** * generic method to copy div content to clipboard * @param divRef @@ -23,3 +24,11 @@ export const copyToClipboard = (divRef: React.MutableRefObject) => { } }); }; + +export const buidMultiSelectGroups = (groups: string[]): Option[] => { + const options: Option[] = []; + for (const group of groups) { + options.push({ label: group, value: group }); + } + return options; +}; diff --git a/doc/GROUPS.md b/doc/GROUPS.md new file mode 100644 index 0000000..baa24a2 --- /dev/null +++ b/doc/GROUPS.md @@ -0,0 +1,31 @@ +# Gestion des utilisateurs/groupes + +Introduit dans la version 1.4, les utilisateurs peuvent être gérés par groupes, et chaque contrôles peut-être affecté **à un ou plusieurs groupes**. + +## Création des groupes + +Se connecter en administrateur, puis ouvrir le module de "gestion des utilisateurs". + +![utilisateurs et groupes](./assets/Screenshot_usersgroupsmanager.png) + +Selectionner ou ajouter un utilisateur, dans la zone de recherche des "Groupes", taper le nom du nouveau groupe , le composant vous proposera de créer le groupe. + +![Ajout de groupes](./assets/Screenshot_addGroup.png) + +Envoyer les modifications, **le ou les groupes** sont créés. + +## Affectation d'un contrôle aux groupes + +**Pour réduire la compléxité**, la gestion "fine" des contrôles n'a pas été implémentée (read/write/execute). + +Quand un contrôle est affecté à un groupe, chaque utilisateur du groupe devient "gestionnaire" de ce contrôle. Il peut par conséquent le modifier ou le supprimer. + +![Affectation de groupes à un contrôle](./assets/Screenshot_setGroupsControl.png) + +## Affectation automatique des groupes + +Lorsque qu'un **administrateur** crée un contrôle, **il doit affecter** un ou plusieurs groupes ayant autorité sur ce dernier. + +Lorsque qu'un utilisateur **non administrateur** crée un contrôle, il est automatiquement affecté à tous ses groupes, libre à lui de réduire cette sélection. + +**Un contrôle ne peut être enregistré que** s'il dispose d'au moins un groupe. diff --git a/doc/INSTALL.md b/doc/INSTALL.md index 30db5c2..02d73bd 100644 --- a/doc/INSTALL.md +++ b/doc/INSTALL.md @@ -13,7 +13,7 @@ L'appplication ne supporte pas "https", vous devrez utiliser ce produit derrièr ``` # tag version -tag="1.3.0" +tag="1.4.0" # Which port do you expose the service on? port=3015 # Generate secrets diff --git a/doc/assets/Screenshot_addGroup.png b/doc/assets/Screenshot_addGroup.png new file mode 100644 index 0000000000000000000000000000000000000000..595277181cf06404e19ef7ee15c2369f9d2e662f GIT binary patch literal 12784 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 literal 0 HcmV?d00001 diff --git a/doc/assets/Screenshot_setGroupsControl.png b/doc/assets/Screenshot_setGroupsControl.png new file mode 100644 index 0000000000000000000000000000000000000000..b20471374a08819f0297517f0be0f3283d69aca8 GIT binary patch literal 31130 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

x;? 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 Zo?!;qGndB!mn?%o@8s3ws$|W-{6CM+V4DB{ literal 0 HcmV?d00001 diff --git a/doc/assets/Screenshot_usersgroupsmanager.png b/doc/assets/Screenshot_usersgroupsmanager.png new file mode 100644 index 0000000000000000000000000000000000000000..bed3e99b1fdad85997d019b3008ec3b797a88553 GIT binary patch literal 19225 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 monitoring status schema: type: string enum: @@ -176,25 +150,26 @@ paths: schema: type: string "401": - $ref: '#/components/responses/UnauthorizedError' + $ref: "#/components/responses/UnauthorizedError" "404": description: control uuid not found content: application/json: schema: - $ref: '#/components/schemas/Error' + $ref: "#/components/schemas/Error" "500": description: Internal error content: application/json: schema: - $ref: '#/components/schemas/Error' + $ref: "#/components/schemas/Error" /action/cicd/: put: - summary: Call CI/CD - description: Call url CI/CD for control uuid provided + summary: Call up the url CI/CD for control uuid provided + description: Call up the url CI/CD for control uuid provided security: - ApiKeyAuth: [] + - cookieAuth: [] tags: - Actions requestBody: @@ -215,25 +190,26 @@ paths: schema: type: string "401": - $ref: '#/components/responses/UnauthorizedError' + $ref: "#/components/responses/UnauthorizedError" "404": description: control uuid not found content: application/json: schema: - $ref: '#/components/schemas/Error' + $ref: "#/components/schemas/Error" "500": description: Internal error content: application/json: schema: - $ref: '#/components/schemas/Error' - /action/lastcomparegitrealase/{controlUuid}/: + $ref: "#/components/schemas/Error" + /action/lastcomparegitrelease/{controlUuid}/: get: - summary: Get the github release - description: Get the github release of the latest comparison (history) for one control uuid + summary: Get the latest github version registered on the control + description: Get the latest github version registered on the control security: - ApiKeyAuth: [] + - cookieAuth: [] tags: - Actions parameters: @@ -251,316 +227,23 @@ paths: schema: type: string "401": - $ref: '#/components/responses/UnauthorizedError' + $ref: "#/components/responses/UnauthorizedError" "404": description: control uuid not found or github release is empty content: application/json: schema: - $ref: '#/components/schemas/Error' - "500": - description: Internal error - content: - application/json: - schema: - $ref: '#/components/schemas/Error' - /userlogin: - post: - summary: login to the system with UI - description: UI login method - security: - - ApiKeyAuth: [] - tags: - - Authentication - requestBody: - description: login properties - required: true - content: - application/json: - schema: - type: object - properties: - login: - type: string - password: - type: string - responses: - "200": - description: Login OK - content: - application/text: - schema: - $ref: '#/components/schemas/InfoIuType' - "401": - $ref: '#/components/responses/UnauthorizedError' - "500": - description: Internal error - content: - application/json: - schema: - $ref: '#/components/schemas/Error' - /users: - get: - summary: get users list - description: Used by the User Management UI to get users list - security: - - ApiKeyAuth: [] - tags: - - Authentication - responses: - "200": - description: Users list - content: - application/json: - schema: - type: array - items: - type: string - "401": - $ref: '#/components/responses/UnauthorizedError' - "500": - description: Internal error - content: - application/json: - schema: - $ref: '#/components/schemas/Error' - post: - summary: create user in the database - description: UI create user method - security: - - ApiKeyAuth: [] - tags: - - Authentication - requestBody: - description: login and password - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/NewUserType' - responses: - "200": - description: created user - content: - application/text: - schema: - type: object - properties: - login: - type: string - "400": - description: User already exists - "401": - $ref: '#/components/responses/UnauthorizedError' - "500": - description: Internal error - content: - application/json: - schema: - $ref: '#/components/schemas/Error' - put: - summary: modify user in the database - description: UI modify user method - security: - - ApiKeyAuth: [] - tags: - - Authentication - requestBody: - description: login and password - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/NewUserType' - responses: - "204": - description: modified user - "401": - $ref: '#/components/responses/UnauthorizedError' - "404": - description: User not found - "500": - description: Internal error - content: - application/json: - schema: - $ref: '#/components/schemas/Error' - /users/{login}: - delete: - summary: delete user from the database - description: UI delete user method - security: - - ApiKeyAuth: [] - tags: - - Authentication - parameters: - - in: path - name: login - required: true - description: login of the user to delete - schema: - type: string - example: user - responses: - "200": - description: deleted - content: - application/text: - schema: - type: object - properties: - login: - type: string - "401": - $ref: '#/components/responses/UnauthorizedError' - "404": - description: User not found - "500": - description: Internal error - content: - application/json: - schema: - $ref: '#/components/schemas/Error' - /isauthenticated: - get: - summary: is user logged - description: Used by UI to verify user is logged - security: - - ApiKeyAuth: [] - tags: - - Authentication - responses: - "200": - description: User is logged - content: - application/json: - schema: - type: string - "401": - $ref: '#/components/responses/UnauthorizedError' - "500": - description: Internal error - content: - application/json: - schema: - $ref: '#/components/schemas/Error' - /userlogout: - get: - summary: user logout method - description: Used by UI to logout user - security: - - ApiKeyAuth: [] - tags: - - Authentication - responses: - "204": - description: User is logged out - "500": - description: Internal error - content: - application/json: - schema: - $ref: '#/components/schemas/Error' - /changepassword: - put: - summary: user change password - description: Used by UI to change user password - security: - - ApiKeyAuth: [] - tags: - - Authentication - requestBody: - description: passwords list - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/ChangePasswordType' - responses: - "204": - description: Password has been changed - "401": - $ref: '#/components/responses/UnauthorizedError' - "500": - description: Internal error - content: - application/json: - schema: - $ref: '#/components/schemas/Error' - /bearer: - get: - summary: get user user auth Token - description: Used by UI to get user auth Token - security: - - ApiKeyAuth: [] - tags: - - Authentication - responses: - "200": - description: User auth Token - content: - application/json: - schema: - type: object - properties: - bearer: - type: string - "401": - $ref: '#/components/responses/UnauthorizedError' - "500": - description: Internal error - content: - application/json: - schema: - $ref: '#/components/schemas/Error' - put: - summary: change user auth Token - description: Used by UI to get new user auth Token - security: - - ApiKeyAuth: [] - tags: - - Authentication - responses: - "204": - description: User auth Token has been changed - "401": - $ref: '#/components/responses/UnauthorizedError' + $ref: "#/components/schemas/Error" "500": description: Internal error content: application/json: schema: - $ref: '#/components/schemas/Error' - /user: - get: - summary: get user's login - description: Used by UI to show the user's login in the header - security: - - ApiKeyAuth: [] - tags: - - Authentication - responses: - "200": - description: User's login - content: - application/json: - schema: - type: object - properties: - login: - type: string - "401": - $ref: '#/components/responses/UnauthorizedError' - "500": - description: Internal error - content: - application/json: - schema: - $ref: '#/components/schemas/Error' + $ref: "#/components/schemas/Error" /control/{uuid}: get: - summary: Get control values - description: Get all control data per uuid or all controls (all) + summary: To get all the parameters of a control + description: To get all the parameters of a control; uuid could be 'all' for all controls security: - ApiKeyAuth: [] tags: @@ -578,80 +261,15 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/UptodateForm' - "401": - $ref: '#/components/responses/UnauthorizedError' - "500": - description: Internal error - content: - application/json: - schema: - $ref: '#/components/schemas/Error' - delete: - summary: Delete one control - description: Delete control per uuid - security: - - ApiKeyAuth: [] - tags: - - Control - parameters: - - in: path - name: uuid - required: true - description: control uuid - schema: - type: string - responses: - "200": - description: control uuid as JSON - content: - application/json: - schema: - $ref: '#/components/schemas/DeletedRecord' - "401": - $ref: '#/components/responses/UnauthorizedError' - "404": - description: control uuid not found - content: - application/json: - schema: - $ref: '#/components/schemas/Error' - "500": - description: Internal error - content: - application/json: - schema: - $ref: '#/components/schemas/Error' - /scrap/{url}: - get: - summary: Get provided url content - description: Used to retrieve url content from the production server, github API tags... - security: - - ApiKeyAuth: [] - tags: - - Core - parameters: - - in: path - name: url - required: true - description: url to scrap http||https supported - schema: - type: string - responses: - "200": - description: content as text - content: - application/text: - schema: - type: string + $ref: "#/components/schemas/UptodateForm" "401": - $ref: '#/components/responses/UnauthorizedError' + $ref: "#/components/responses/UnauthorizedError" "500": description: Internal error content: application/json: schema: - $ref: '#/components/schemas/Error' + $ref: "#/components/schemas/Error" /version: get: summary: To get the application version @@ -675,7 +293,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/Error' + $ref: "#/components/schemas/Error" /healthz: get: summary: Is service Healthy ? @@ -691,7 +309,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/Error' + $ref: "#/components/schemas/Error" /metrics: get: summary: not implemented @@ -702,13 +320,12 @@ paths: - Core responses: "401": - $ref: '#/components/responses/UnauthorizedError' + $ref: "#/components/responses/UnauthorizedError" "500": description: Internal error content: application/json: schema: - $ref: '#/components/schemas/Error' + $ref: "#/components/schemas/Error" "503": description: not implemented -tags: [] diff --git a/package-lock.json b/package-lock.json index 4f84eba..071dacf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "mytinydc-utdon", - "version": "1.3.0", + "version": "1.4.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mytinydc-utdon", - "version": "1.3.0", + "version": "1.4.0", "license": "AGPL-3.0", "dependencies": { "@metrichor/jmespath": "^0.3.1", @@ -35,7 +35,6 @@ "eslint": "^8.52.0", "jest": "^29.7.0", "nodemon": "^3.0.2", - "swagger-jsdoc": "^6.2.8", "ts-jest": "^29.1.1", "ts-node": "^10.9.2", "typescript": "^5.3.3", @@ -64,46 +63,6 @@ "node": ">=6.0.0" } }, - "node_modules/@apidevtools/json-schema-ref-parser": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.1.2.tgz", - "integrity": "sha512-r1w81DpR+KyRWd3f+rk6TNqMgedmAxZP5v5KWlXQWlgMUUtyEJch0DKEci1SorPMiSeM8XPl7MZ3miJ60JIpQg==", - "dependencies": { - "@jsdevtools/ono": "^7.1.3", - "@types/json-schema": "^7.0.6", - "call-me-maybe": "^1.0.1", - "js-yaml": "^4.1.0" - } - }, - "node_modules/@apidevtools/openapi-schemas": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz", - "integrity": "sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==", - "engines": { - "node": ">=10" - } - }, - "node_modules/@apidevtools/swagger-methods": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz", - "integrity": "sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==" - }, - "node_modules/@apidevtools/swagger-parser": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-10.0.3.tgz", - "integrity": "sha512-sNiLY51vZOmSPFZA5TF35KZ2HbgYklQnTSDnkghamzLb3EkNtcQnrBQEj5AOCxHpTtXpqMCRM1CrmV2rG6nw4g==", - "dependencies": { - "@apidevtools/json-schema-ref-parser": "^9.0.6", - "@apidevtools/openapi-schemas": "^2.0.4", - "@apidevtools/swagger-methods": "^3.0.2", - "@jsdevtools/ono": "^7.1.3", - "call-me-maybe": "^1.0.1", - "z-schema": "^5.0.1" - }, - "peerDependencies": { - "openapi-types": ">=7" - } - }, "node_modules/@babel/code-frame": { "version": "7.23.5", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", @@ -1316,11 +1275,6 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@jsdevtools/ono": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", - "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==" - }, "node_modules/@metrichor/jmespath": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/@metrichor/jmespath/-/jmespath-0.3.1.tgz", @@ -1566,7 +1520,8 @@ "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==" + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true }, "node_modules/@types/mime": { "version": "1.3.5", @@ -2002,7 +1957,8 @@ "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true }, "node_modules/array-flatten": { "version": "1.1.1", @@ -2142,7 +2098,8 @@ "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true }, "node_modules/binary-extensions": { "version": "2.2.0", @@ -2193,6 +2150,7 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -2290,11 +2248,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/call-me-maybe": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz", - "integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==" - }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -2505,18 +2458,11 @@ "text-hex": "1.0.x" } }, - "node_modules/commander": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.0.tgz", - "integrity": "sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q==", - "engines": { - "node": ">= 6" - } - }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true }, "node_modules/content-disposition": { "version": "0.5.4", @@ -2716,6 +2662,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, "dependencies": { "esutils": "^2.0.2" }, @@ -2950,6 +2897,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, "engines": { "node": ">=0.10.0" } @@ -3323,7 +3271,8 @@ "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true }, "node_modules/function-bind": { "version": "1.1.2", @@ -3641,6 +3590,7 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, "dependencies": { "once": "^1.3.0", "wrappy": "1" @@ -4394,6 +4344,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, "dependencies": { "argparse": "^2.0.1" }, @@ -4515,16 +4466,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lodash.get": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", - "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==" - }, - "node_modules/lodash.isequal": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", - "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==" - }, "node_modules/lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -4537,11 +4478,6 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, - "node_modules/lodash.mergewith": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz", - "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==" - }, "node_modules/logform": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/logform/-/logform-2.6.0.tgz", @@ -4689,6 +4625,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -4843,6 +4780,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, "dependencies": { "wrappy": "1" } @@ -4870,12 +4808,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/openapi-types": { - "version": "12.1.3", - "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz", - "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==", - "peer": true - }, "node_modules/optionator": { "version": "0.9.3", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", @@ -4983,6 +4915,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, "engines": { "node": ">=0.10.0" } @@ -5788,63 +5721,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/swagger-jsdoc": { - "version": "6.2.8", - "resolved": "https://registry.npmjs.org/swagger-jsdoc/-/swagger-jsdoc-6.2.8.tgz", - "integrity": "sha512-VPvil1+JRpmJ55CgAtn8DIcpBs0bL5L3q5bVQvF4tAW/k/9JYSj7dCpaYCAv5rufe0vcCbBRQXGvzpkWjvLklQ==", - "dependencies": { - "commander": "6.2.0", - "doctrine": "3.0.0", - "glob": "7.1.6", - "lodash.mergewith": "^4.6.2", - "swagger-parser": "^10.0.3", - "yaml": "2.0.0-1" - }, - "bin": { - "swagger-jsdoc": "bin/swagger-jsdoc.js" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/swagger-jsdoc/node_modules/glob": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/swagger-jsdoc/node_modules/yaml": { - "version": "2.0.0-1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.0.0-1.tgz", - "integrity": "sha512-W7h5dEhywMKenDJh2iX/LABkbFnBxasD27oyXWDS/feDsxiw0dD5ncXdYXgkvAsXIY2MpW/ZKkr9IU30DBdMNQ==", - "engines": { - "node": ">= 6" - } - }, - "node_modules/swagger-parser": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/swagger-parser/-/swagger-parser-10.0.3.tgz", - "integrity": "sha512-nF7oMeL4KypldrQhac8RyHerJeGPD1p2xDh900GPvc+Nk7nWP6jX2FcC7WmkinMoAmoO774+AFXcWsW8gMWEIg==", - "dependencies": { - "@apidevtools/swagger-parser": "10.0.3" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/swagger-ui-dist": { "version": "5.10.5", "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.10.5.tgz", @@ -6215,14 +6091,6 @@ "node": ">=10.12.0" } }, - "node_modules/validator": { - "version": "13.11.0", - "resolved": "https://registry.npmjs.org/validator/-/validator-13.11.0.tgz", - "integrity": "sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ==", - "engines": { - "node": ">= 0.10" - } - }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -6309,7 +6177,8 @@ "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true }, "node_modules/write-file-atomic": { "version": "4.0.2", @@ -6394,34 +6263,6 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } - }, - "node_modules/z-schema": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-5.0.5.tgz", - "integrity": "sha512-D7eujBWkLa3p2sIpJA0d1pr7es+a7m0vFAnZLlCEKq/Ij2k0MLi9Br2UPxoxdYystm5K1yeBGzub0FlYUEWj2Q==", - "dependencies": { - "lodash.get": "^4.4.2", - "lodash.isequal": "^4.5.0", - "validator": "^13.7.0" - }, - "bin": { - "z-schema": "bin/z-schema" - }, - "engines": { - "node": ">=8.0.0" - }, - "optionalDependencies": { - "commander": "^9.4.1" - } - }, - "node_modules/z-schema/node_modules/commander": { - "version": "9.5.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", - "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", - "optional": true, - "engines": { - "node": "^12.20.0 || >=14" - } } } } diff --git a/package.json b/package.json index af40562..f96c60f 100644 --- a/package.json +++ b/package.json @@ -1,17 +1,17 @@ { "name": "mytinydc-utdon", - "version": "1.3.0", + "version": "1.4.0", "description": "Application for tracking obsolete FOSS applications - Server", "main": "main.ts", "scripts": { "startServer": "USER_ENCRYPT_SECRET='7252c26afd532e75510e8d7bcf37bb56' DATABASE_ENCRYPT_SECRET=test environment=development nodemon -w src/ -e ts -L ./src/main.ts", + "startServerProduction": "USER_ENCRYPT_SECRET='7252c26afd532e75510e8d7bcf37bb56' DATABASE_ENCRYPT_SECRET=test nodemon -w src/ -e ts -L ./src/main.ts", "build": "environment=production tsc", "preview": "node dist/main.js", "lint": "npx eslint ./src", "updateVersionMajor": "./updateVersion.sh major", "updateVersionMinor": "./updateVersion.sh minor", "updateVersionPatch": "./updateVersion.sh patch", - "genSwaggerYamlFile": "ts-node ./src/genSwaggerJson.ts && yq -p json -o yaml openapi.json > openapi.yaml && rm -f openapi.json", "test": "jest --testPathIgnorePatterns='devtest*'" }, "repository": { @@ -32,7 +32,6 @@ "yaml": "^2.3.4" }, "devDependencies": { - "swagger-jsdoc": "^6.2.8", "@jest/globals": "^29.7.0", "@types/body-parser": "^1.19.5", "@types/cors": "^2.8.17", diff --git a/src/Constants-dev.ts b/src/Constants-dev.ts index 6fa605b..142dfd8 100644 --- a/src/Constants-dev.ts +++ b/src/Constants-dev.ts @@ -34,4 +34,5 @@ export const STORYBOOK_UPTODATEFORM: UptodateForm = { compareResult: null, logo: "data:image/jpeg;base64,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", uuid: "xxx-xxx-xxx-xxx", + groups: [], }; diff --git a/src/Constants.ts b/src/Constants.ts index 01b69e2..0680cb6 100644 --- a/src/Constants.ts +++ b/src/Constants.ts @@ -25,7 +25,7 @@ export const INPROGRESS_UPTODATEORNOTSTATE: UptoDateOrNotState = { urlProduction: "", }; -export const APPLICATION_VERSION = "1.3.0"; +export const APPLICATION_VERSION = "1.4.0"; // routes which dont need authentication to be served // firsts are UI routes @@ -80,7 +80,6 @@ export const SCRAPTYPEOPTIONJSON = "Json"; export const SCRAPTYPEOPTIONTEXT = "Text / HTML / XML"; export const INITIALIZED_CHANGEPASSWORD: ChangePasswordType = { - login: "", password: "", newPassword: "", newConfirmPassword: "", @@ -89,6 +88,7 @@ export const INITIALIZED_CHANGEPASSWORD: ChangePasswordType = { export const INITIALIZED_NEWUSER: NewUserType = { login: "", password: "", + groups: [], }; export const INITIALIZED_UPTODATEFORM: UptodateForm = { @@ -108,6 +108,7 @@ export const INITIALIZED_UPTODATEFORM: UptodateForm = { uuid: "", isPause: false, compareResult: null, + groups: [], }; export const HTTP_METHOD_ENUM: SelectOptionType[] = [ diff --git a/src/Global.types.ts b/src/Global.types.ts index a428075..4e1e15f 100644 --- a/src/Global.types.ts +++ b/src/Global.types.ts @@ -31,6 +31,7 @@ export type UptodateForm = { urlCICDAuth: string; isPause: boolean; compareResult: UptoDateOrNotState | null; + groups: GroupMembersType; }; export type UptodateFormFields = @@ -48,7 +49,8 @@ export type UptodateFormFields = | "urlCICD" | "httpMethodCICD" | "urlCICDAuth" - | "isPause"; + | "isPause" + | "groups"; export type ApiResponseType = { data?: JSON; @@ -58,6 +60,8 @@ export type ApiResponseType = { export type InfoIuType = { login: string; bearer: string; + uuid: string; + groups: string[]; }; export type ActionStatusType = { @@ -74,7 +78,6 @@ export type ActionCiCdType = { export type contextSliceType = { // French is default language language: { locale: string; lang: JSONLang }; - user: InfoIuType; application: { name: string; applicationtitle: string; @@ -83,6 +86,9 @@ export type contextSliceType = { }; uptodateForm: UptodateForm; refetchuptodateForm: boolean; + isAdmin: boolean; + search: string; + isLoaderShip: boolean; }; export type ToastSeverityType = "info" | "error" | "warn" | "success"; @@ -162,7 +168,6 @@ export type UIError = { }; export type ChangePasswordType = { - login: string; password: string; newPassword: string; newConfirmPassword: string; @@ -171,9 +176,31 @@ export type ChangePasswordType = { export type NewUserType = { login: string; password: string; + groups: string[]; + uuid?: string; }; export type ControlToPause = { uuid: string; state: boolean; }; + +export type GroupMembersType = string[]; + +/** + * {"group name" : [ members...] } + */ +export type GroupsType = { + [key: string]: GroupMembersType; +}; + +export type UsersGroupsType = { + users: UserType[]; + groups: GroupsType; +}; + +export type UserDescriptionType = { + login: string; + uuid: string; + groups: string[]; +}; diff --git a/src/genSwaggerJson.ts b/src/genSwaggerJson.ts deleted file mode 100644 index e566a8e..0000000 --- a/src/genSwaggerJson.ts +++ /dev/null @@ -1,42 +0,0 @@ -/** - * @author DHENRY for mytinydc.com - * @license AGPL3 - */ - -import { writeFileSync } from "fs"; - -import { APPLICATION_VERSION, OPENAPIFILEJSON } from "./Constants"; - -// Swagger Documentation -import swaggerJsdoc from "swagger-jsdoc"; - -//Swagger -const swaggerDefinition = { - definition: { - openapi: "3.0.0", - info: { - title: "Utdon API Documentation", - version: APPLICATION_VERSION, - }, - components: { - securitySchemes: { - ApiKeyAuth: { - type: "apiKey", - in: "header", - name: "Authorization", - }, - }, - }, - security: [ - { - ApiKeyAuth: [], - }, - ], - servers: [{ url: "/api/v1" }], - }, - apis: ["./src/routes/*"], -}; -const swaggerSpec = swaggerJsdoc(swaggerDefinition); -writeFileSync(OPENAPIFILEJSON, JSON.stringify(swaggerSpec), "utf-8"); -console.log("Swagger JSON file has been flushed", OPENAPIFILEJSON); -process.exit(0); diff --git a/src/lib/Authentification.ts b/src/lib/Authentification.ts index 8f49927..3e1a889 100644 --- a/src/lib/Authentification.ts +++ b/src/lib/Authentification.ts @@ -6,8 +6,10 @@ import { readFileSync, existsSync, writeFileSync, copyFileSync } from "fs"; import { ChangePasswordType, + GroupsType, InfoIuType, - UsersType, + UserDescriptionType, + UsersGroupsType, UserType, } from "../Global.types"; import crypto from "crypto"; @@ -24,31 +26,46 @@ import { SessionExt } from "../ServerTypes"; const userDatabaseDefault = `${__dirname}/../../data/user.json`; export class Authentification { - users: UsersType; + usersgroups: UsersGroupsType; database: string; constructor(databasePath: string) { // needed for tests this.database = userDatabaseDefault; if (databasePath) this.database = databasePath; - if (!existsSync(this.database)) this.initDatabase(); - this.users = this.loadUsersFromDatabase(); + if (!existsSync(this.database)) this.initDatabaseUsers(); + this.usersgroups = this.loadUsersFromDatabase(); } - loadUsersFromDatabase = (): UsersType => { + loadUsersFromDatabase = (): UsersGroupsType => { // PR#15 : Modification of the user database schema: multi-administrators let data = JSON.parse(readFileSync(this.database, "utf-8")); - if (!data.users) { - copyFileSync( - this.database, - this.database.replace(/\.json$/, "Before-PR#15-backup.json") - ); + const targetBackup = this.database.replace( + /\.json$/, + "Before-PR#15-backup.json" + ); + // Before-PR#15 + if (data && !data.users) { + copyFileSync(this.database, targetBackup); // save - this.initDatabase(data); - data = this.loadUsersFromDatabase(); + this.initDatabaseUsers(data); + data = { users: [{ ...data }] }; + // data = this.loadUsersFromDatabase(); console.log( "[INFO] PR#15: The user database has been converted into a multi-administrator database." ); + //security + this.writeDB(JSON.stringify(data)); + } + // Final model + if (data.users && !data.groups) { + data.groups = { admin: [] }; + for (const item of data.users) { + const user = { ...item } as UserType; + data.groups.admin.push(user.uuid); + } + //security + this.writeDB(JSON.stringify(data)); } return data; }; @@ -90,34 +107,30 @@ export class Authentification { }; }; - // test function - store = (user: UserType): UsersType => { - if (user && user.login && user.password && user.uuid && user.bearer) { - this.initDatabase(user); - //reset user ??? - this.users = { users: [user] }; - return this.users; - } else { - throw new Error("User is not well defined"); + initDatabaseUsers = (usersGroups?: UsersGroupsType) => { + let usersGroupsToWrite = { + users: [], + groups: { admin: [] }, + } as UsersGroupsType; + if (usersGroups) { + usersGroupsToWrite = usersGroups; } - }; - - initDatabase = (user?: UserType) => { - const users = { users: [] } as UsersType; - if (user) { - users.users.push(user); - } - - this.writeDB(JSON.stringify(users)); + this.writeDB(JSON.stringify(usersGroupsToWrite)); }; verifyPassword = ( login: string, clearPassword: string ): [number, string | InfoIuType] => { - if (this.users && clearPassword && process.env.USER_ENCRYPT_SECRET) { + if ( + this.usersgroups.users && + clearPassword && + process.env.USER_ENCRYPT_SECRET + ) { // find user in database - const currentUser = this.users.users.find((user) => user.login === login); + const currentUser = this.usersgroups.users.find( + (user) => user.login === login + ); if (!currentUser) return [401, LOGIN_FAILED]; const newHash = crypto .pbkdf2Sync( @@ -139,12 +152,35 @@ export class Authentification { } }; + getUserMemberGroupsName = (userUuid: string): string[] => { + const groups: string[] = []; + Object.getOwnPropertyNames(this.usersgroups.groups).forEach((groupName) => { + if (this.usersgroups.groups[groupName].includes(userUuid)) + groups.push(groupName); + }); + return groups; + }; + /* - * return all users logins + * return all users logins - must be used on by admin * @returns string[] */ - getUsersLogins = (): string[] => { - return this.users.users.map((user) => user.login); + getUsersForUi = (isAdmin: boolean): UserDescriptionType[] => { + const usersDescription: UserDescriptionType[] = []; + if (isAdmin) { + for (const user of this.usersgroups.users) { + usersDescription.push({ + login: user.login, + groups: this.getUserMemberGroupsName(user.uuid), + uuid: user.uuid, + }); + } + } + return usersDescription; + }; + + getUsers = (): UserType[] => { + return this.usersgroups.users; }; /* @@ -154,9 +190,9 @@ export class Authentification { */ addUser = (user: UserType) => { // check if user already exists - if (this.users.users.find((u) => u.login === user.login)) + if (this.usersgroups.users.find((u) => u.login === user.login)) throw new Error("User already exists"); - this.users.users.push(user); + this.usersgroups.users.push(user); this.writeDB(); }; @@ -165,12 +201,12 @@ export class Authentification { * @param user string * @returns void */ - deleteUser = (user: string) => { + deleteUser = (userUuid: string) => { // admin user could not be deleted - if (user === "admin") throw new Error("Admin user can't be deleted"); - const uid = this.users.users.findIndex((u) => u.login === user); + if (userUuid === "admin") throw new Error("Admin user can't be deleted"); + const uid = this.usersgroups.users.findIndex((u) => u.uuid === userUuid); if (uid !== -1) { - this.users.users.splice(uid, 1); + this.usersgroups.users.splice(uid, 1); this.writeDB(); } }; @@ -181,10 +217,10 @@ export class Authentification { */ getUserBearer = (user: string): string => { // return this.users.users.fil .map((user) => Authentification.dataDecrypt(user.bearer, process.env.USER_ENCRYPT_SECRET || "")); - const uid = this.users.users.findIndex((u) => u.login === user); + const uid = this.usersgroups.users.findIndex((u) => u.login === user); if (uid === -1) return ""; return Authentification.dataDecrypt( - this.users.users[uid].bearer, + this.usersgroups.users[uid].bearer, process.env.USER_ENCRYPT_SECRET || "" ); }; @@ -194,7 +230,7 @@ export class Authentification { * @returns string[] * */ getUsersBearers = (): string[] => { - return this.users.users.map((user) => + return this.usersgroups.users.map((user) => Authentification.dataDecrypt( user.bearer, process.env.USER_ENCRYPT_SECRET || "" @@ -203,41 +239,101 @@ export class Authentification { }; getInfoForUi(login: string): InfoIuType { - const user = this.users.users.find((user) => user.login === login); - if (!user) return { login: "", bearer: "" }; - return { login: user.login, bearer: user.bearer }; + const user = this.usersgroups.users.find((user) => user.login === login); + if (!user) return { login: "", bearer: "", uuid: "", groups: [] }; + return { + login: user.login, + bearer: user.bearer, + uuid: user.uuid, + groups: this.getUserMemberGroupsName(user.uuid), + }; } isAuthenticated = (req: Request) => { return this.isAuthBearer(req) || this.isAuthSession(req); }; + /** + * is user set in session ? + * @param req + * @returns + */ isAuthSession = (req: Request) => { const session = req.session as SessionExt; - return ( - session && - session.user && - session.user.login && - !!this.getUsersLogins().find((login) => login === session.user.login) - ); + //By pass auth in development mode + if (process.env.environment === "development") { + req.app + .get("LOGGER") + .error( + 'WARNING: process.env.environment === "development" Auth bypassed' + ); + if (!session || !session.user) { + const adminUser = this.usersgroups.users.find( + (item) => item.login === "admin" + ); + if (adminUser) session.user = this.getInfoForUi(adminUser.login); + } + return true; + } else { + return ( + session && + session.user && + session.user.login && + !!this.getUsers().find((item) => item.login === session.user.login) + ); + } }; + /** + * user connect with token + * if token found, load user in session + * @param req + * @returns + */ isAuthBearer = (req: Request) => { - if (req.headers) { + if (req.headers && req.headers["authorization"]) { const bearers = this.getUsersBearers(); const bearer = bearers.find( (bearer) => bearer === req.headers["authorization"] ); - return !!bearer; + if (bearer) { + const session = req.session as SessionExt; + for (const user of this.usersgroups.users) { + const bearer = Authentification.dataDecrypt( + user.bearer, + process.env.USER_ENCRYPT_SECRET || "" + ); + if (bearer === req.headers["authorization"]) { + // as there is no cookie, the session is completed programmatically. + session.user = { + login: user.login, + bearer: user.bearer, + uuid: user.uuid, + groups: this.getUserGroups(user.uuid), + }; + + break; + } + } + if (session && session.user && session.user.uuid) return true; + } } return false; }; + /** + * change user bearer token, token is encrypted + * @param user + * @returns + */ changeBearer = (user: string) => { try { - const uid = this.users.users.findIndex((u) => u.login === user); - if (uid !== -1) { - this.users.users[uid].bearer = this.generateBearerKey(); + const idx = this.usersgroups.users.findIndex((u) => u.login === user); + if (idx !== -1) { + this.usersgroups.users[idx].bearer = Authentification.dataEncrypt( + this.generateBearerKey(), + process.env.USER_ENCRYPT_SECRET + ); this.writeDB(); return [200, "User bearer has been changed"]; } else { @@ -253,32 +349,27 @@ export class Authentification { * @param changepassword * @returns */ - changePassword = (changepassword: ChangePasswordType) => { + changePassword = (changepassword: ChangePasswordType, userLogin: string) => { // check if all parameters are provided if ( - changepassword.login && + userLogin && changepassword.password && changepassword.newPassword && changepassword.newConfirmPassword ) { // check if current password is correct - if ( - this.verifyPassword( - changepassword.login, - changepassword.password - )[0] === 200 - ) { + if (this.verifyPassword(userLogin, changepassword.password)[0] === 200) { // if new password and its confirmation match if (changepassword.newPassword === changepassword.newConfirmPassword) { // if so, find the uid of the user we want to change the password // console.log(changepassword.login); - const uid = this.users.users.findIndex( - (u) => u.login === changepassword.login + const uid = this.usersgroups.users.findIndex( + (u) => u.login === userLogin ); if (uid === -1) return [500, "User not found"]; - this.users.users[uid].password = this.encryptPassword( + this.usersgroups.users[uid].password = this.encryptPassword( changepassword.newPassword ); this.writeDB(); @@ -348,10 +439,155 @@ export class Authentification { return msg.join(""); } - writeDB = (users?: string) => { - writeFileSync(this.database, users ? users : JSON.stringify(this.users), { - encoding: "utf-8", - mode: 0o600, + writeDB = (usersGroups?: string) => { + writeFileSync( + this.database, + usersGroups ? usersGroups : JSON.stringify(this.usersgroups), + { + encoding: "utf-8", + mode: 0o600, + } + ); + }; + + /** + * is user member of group ? + * @param group + * @param userUuid + * @returns + */ + isMemberOfGroup = (group: string, userUuid: string): boolean => { + //is user exists + if (!this.usersgroups.users.find((u) => u.uuid === userUuid)) return false; + + if (!this.usersgroups.groups[group]) return false; + + return this.usersgroups.groups[group].includes(userUuid); + }; + + /** + * simplicity : if non-existent group, creating group and add user inside + * addGroupMember could be restricted by controllers + * @param group + * @param userUuid + * @returns + */ + addGroupMember = (group: string, userUuid: string): boolean => { + // user is not set + if (!userUuid) return false; + // is user exists ? + if (!this.usersgroups.users.find((u) => u.uuid === userUuid)) return false; + // non-existent group, creating group + if (!this.usersgroups.groups[group]) this.usersgroups.groups[group] = []; + // add user to group if not already present + if (!this.usersgroups.groups[group].includes(userUuid)) { + this.usersgroups.groups[group].push(userUuid); + this.writeDB(); + } + return true; + }; + + /** + * autocleaning groups without users + */ + cleanGroups = () => { + const newGroups: GroupsType = {}; + Object.getOwnPropertyNames(this.usersgroups.groups).forEach((group) => { + // keep only groups with member(s) + if (this.usersgroups.groups[group].length > 0) + newGroups[group] = this.usersgroups.groups[group]; }); + this.usersgroups.groups = newGroups; + this.writeDB(); + }; + + removeUserFromGroups = (userUuid: string) => { + const newUsersGroups = { ...this.usersgroups.groups }; + const dbGroups = Object.getOwnPropertyNames(this.usersgroups.groups); + for (const group of dbGroups) { + const newSet = this.usersgroups.groups[group].filter( + (item) => item !== userUuid + ); + newUsersGroups[group] = newSet; + } + this.usersgroups.groups = newUsersGroups; + this.writeDB(); + }; + + /** + * Only available when session is set, non accesible via API + * @param req + * @returns + */ + isAdmin = (req?: Request) => { + //is user exists in db + if (req) { + const session = req.session as SessionExt; + if ( + session && + session.user && + session.user.uuid && + this.isMemberOfGroup("admin", session.user.uuid) + ) + return true; + } + return false; + }; + + /** + * return groups list + * @returns + */ + getGroups = (req: Request): string[] => { + const session = req.session as SessionExt; + if (req.app.get("AUTH").isAdmin(req)) { + return Object.getOwnPropertyNames(this.usersgroups.groups); + } else { + return this.getUserGroups(session.user.uuid); + } + }; + + /** + * get user groups + * @param userUuid + * @returns + */ + getUserGroups = (userUuid: string): string[] => { + const groups: string[] = []; + if (userUuid) { + const dbGroups = Object.getOwnPropertyNames(this.usersgroups.groups); + for (const group of dbGroups) { + if (this.usersgroups.groups[group].includes(userUuid)) + groups.push(group); + } + } + return groups; + }; + + /** + * object has a groups attribut (string[]) which are name of groups + * authorized to manipulate it + * @param req + * @param objectGroups + * @returns + */ + isAllowedForObject = (req: Request, objectGroups: string[]) => { + // user is admin + if (req.app.get("AUTH").isAdmin(req)) { + return true; + } else { + //check groups + const session = req.session as SessionExt; + const groupsAuthorized = req.app + .get("AUTH") + .getUserGroups(session.user.uuid); + if (groupsAuthorized.length > 0) { + return groupsAuthorized.some( + (v: string) => objectGroups.indexOf(v) !== -1 + ); + } else { + return false; + } + } }; } diff --git a/src/lib/Database.ts b/src/lib/Database.ts index 61ecac5..7572c5d 100644 --- a/src/lib/Database.ts +++ b/src/lib/Database.ts @@ -83,27 +83,27 @@ export const dbGetData = (file: string): Promise => { /** * This method must be called in try catch block * @param db - * @param check + * @param control * @returns */ export const dbInsert = ( db: UptodateForm[], - check: UptodateForm + control: UptodateForm ): Promise => { return new Promise((resolv, reject) => { - if (check.uuid) { + if (control.uuid) { reject(new Error("Impossible to add object with an uuid")); } const uuid = uuidv4(); db.push({ - ...check, + ...control, uuid: uuid, urlCICDAuth: Authentification.dataEncrypt( - check.urlCICDAuth, + control.urlCICDAuth, process.env.DATABASE_ENCRYPT_SECRET ), urlCronJobMonitoringAuth: Authentification.dataEncrypt( - check.urlCronJobMonitoringAuth, + control.urlCronJobMonitoringAuth, process.env.DATABASE_ENCRYPT_SECRET ), }); @@ -158,22 +158,46 @@ const decryptDb = (records: UptodateForm[], logger: Logger) => { return decryptedDb; }; +export const isRecordInUserGroups = ( + record: UptodateForm, + userGroups: string[] +): boolean => { + // could be remove "|| []" - for compatibility between 1.3 & 1.4 release + // with 1.3 data groups attribut was not defined + const groupsControl = record.groups || []; + for (const groupControl of groupsControl) { + if (userGroups.includes(groupControl)) return true; + } + return false; +}; + export const dbGetRecord = ( db: UptodateForm[], uuid: string, + userGroups: string[], + isAdmin: boolean, logger: Logger ): UptodateForm[] | UptodateForm | null => { if (!uuid) return null; if (uuid !== "all") { const control = db.filter((item) => item.uuid === uuid); - if (control.length === 1) { - // do not throw error, just log - const recordDecrypted = decryptRecord(control[0], logger); - return recordDecrypted; + // ok if admin or record groups includes in groups + if (isAdmin || isRecordInUserGroups(control[0], userGroups)) + return decryptRecord(control[0], logger); + // user is not authorized to get this control + return null; } } else { - return decryptDb(db, logger); + // All controls are asked + const controlsToReturn: UptodateForm[] = []; + const dbDecrypted = decryptDb(db, logger); + // is control has group which matchs with userGroups + for (const control of dbDecrypted) { + if (isAdmin || isRecordInUserGroups(control, userGroups)) + controlsToReturn.push(control); + } + return controlsToReturn; } return null; }; diff --git a/src/lib/Features.ts b/src/lib/Features.ts index f443966..dbb68ec 100644 --- a/src/lib/Features.ts +++ b/src/lib/Features.ts @@ -50,7 +50,7 @@ export const compareVersion = ( sourceCodeVersion: string, productionVersion: string, urlGitHub: string, - urlProduction: string, + urlProduction: string ): UptoDateOrNotState => { let uptodateState = false, githubLatestReleaseIncludesProductionVersion = false, diff --git a/src/lib/PatchVersion.ts b/src/lib/PatchVersion.ts new file mode 100644 index 0000000..65183f1 --- /dev/null +++ b/src/lib/PatchVersion.ts @@ -0,0 +1,15 @@ +import { UptodateForm } from "../Global.types"; + +// V1.3.0 -> V1.4.0 +// Add groups attribut to controls +export const patchV1_3_0To1_4_0 = async (db: UptodateForm[]) => { + const newDb: UptodateForm[] = []; + for (const record of db) { + if (record.groups === undefined) { + newDb.push({ ...record, groups: ["admin"] }); + } else { + newDb.push({ ...record }); + } + } + return newDb; +}; diff --git a/src/main.ts b/src/main.ts index 5daee71..e51c95a 100644 --- a/src/main.ts +++ b/src/main.ts @@ -24,6 +24,7 @@ import { ADMINPASSWORDDEFAULT, ADMINUSERLOGINDEFAULT, API_ENTRY_POINTS_NO_NEED_AUTHENTICATION, + APPLICATION_VERSION, JSON_POST_MAX_SIZE, OPENAPIFILEYAML, SERVER_ERROR_IMPOSSIBLE_TO_CREATE_DB, @@ -33,6 +34,7 @@ import routerControl from "./routes/routerControls"; import routerActions from "./routes/routerActions"; import routerCore from "./routes/routerCore"; import routerAuth from "./routes/routerAuth"; +import { patchV1_3_0To1_4_0 as patchDbTo1_4_0 } from "./lib/PatchVersion"; // Swagger Documentation import swaggerUi from "swagger-ui-express"; @@ -55,6 +57,9 @@ if (!process.env.USER_ENCRYPT_SECRET || !process.env.DATABASE_ENCRYPT_SECRET) { ); process.exit(1); } + +const app = express(); + // Database let dbfile = dbConnect(getDbInitJsonFileName()); if (!dbfile) { @@ -70,28 +75,42 @@ dbCreate(dbfile).catch((error: Error) => { } }); dbGetData(dbfile) - .then((res) => { - app.set("DB", res); + .then(async (res) => { + // Update data for version + if (APPLICATION_VERSION === "1.4.0") { + const newDbContent = await patchDbTo1_4_0(res); + await dbCommit(dbfile || "", newDbContent) + .then(() => { + app.set("DB", newDbContent); + }) + .catch((error) => { + throw error; + }); + } else { + app.set("DB", res); + } }) .catch((error) => { logger.error(error); process.exit(1); }); -// Checking user database -const userDbPathDev = `${__dirname}/../data/user.json`; -const userDbPath = `${__dirname}/data/user.json`; +// Checking users/groups databases - need to be renamed +const usersDbPathDev = `${__dirname}/../data/user.json`; +const usersDbPath = `${__dirname}/data/user.json`; + const auth = new Authentification( - process.env.environment === "development" ? userDbPathDev : userDbPath + process.env.environment === "development" ? usersDbPathDev : usersDbPath ); const data = auth.loadUsersFromDatabase(); if (data.users && data.users.length === 0) { logger.info({ action: "Creating user database, with admin/admin" }); - auth.store(auth.makeUser(ADMINUSERLOGINDEFAULT, ADMINPASSWORDDEFAULT)); + const newUser = auth.makeUser(ADMINUSERLOGINDEFAULT, ADMINPASSWORDDEFAULT); + auth.addUser(newUser); + // first user is admin + auth.addGroupMember("admin", newUser.uuid); } -const app = express(); - // shared objects app.set("LOGGER", logger); app.set("DBFILE", dbfile); @@ -115,7 +134,7 @@ app.use(express.json({ limit: JSON_POST_MAX_SIZE })); const sessionOpts = { secret: crypto.randomBytes(16).toString("hex"), resave: false, - saveUninitialized: true, // create non initialized session + saveUninitialized: false, }; const serverSession = session(sessionOpts); app.use(serverSession); @@ -233,9 +252,9 @@ app.use((error: Error, req: Request, res: Response, next: NextFunction) => { const httpServer = http.createServer(app); httpServer.listen(app.get("PORT"), app.get("IPADDRESS"), function () { logger.info( - `[Httpserver] listening at http://${app.get("IPADDRESS")}:${app.get( - "PORT" - )}` + `[Httpserver - ${ + process.env.environment !== "development" ? "production" : "development" + }] listening at http://${app.get("IPADDRESS")}:${app.get("PORT")}` ); }); diff --git a/src/routes/routerActions.ts b/src/routes/routerActions.ts index b303a16..080d266 100644 --- a/src/routes/routerActions.ts +++ b/src/routes/routerActions.ts @@ -12,6 +12,7 @@ import { import { dbCommit, dbGetRecord, dbUpdateRecord } from "../lib/Database"; import { scrapUrl, getUpToDateOrNotState } from "../lib/Features"; import { UUIDNOTFOUND, UUIDNOTPROVIDED } from "../Constants"; +import { SessionExt } from "../ServerTypes"; const routerActions = express.Router(); const updateExternalStatus = ( @@ -34,59 +35,15 @@ const updateExternalStatus = ( }; /** - * Call compare entrypoint for control uuid, control Uuid value could be "all" - * - * @swagger - * /action/compare/{controlUuid}/{setStatus}: - * get: - * summary: Call compare method - * description: Call compare method for one or all controls, and call url monitoring - * security: - * - ApiKeyAuth: [] - * tags: - * - Actions - * parameters: - * - in: path - * name: controlUuid - * required: true - * description: control uuid, could be 'all' - * schema: - * type: string - * - in: path - * name: setStatus - * required: true - * description: control uuid - * schema: - * type: string - * enum: - * - 0 - * - 1 - * responses: - * 200: - * description: The response varies according to the parameters provided - * content: - * application/text: - * schema: - * type: string - * 401: - * $ref: '#/components/responses/UnauthorizedError' - * 404: - * description: control uuid not found - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Error' - * 500: - * description: Internal error - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Error' + * Call up the comparison method and can update the monitoring status: 'all' is accepted */ routerActions.get( "/action/compare/:controlUuid/:setStatus", async (req: Request, res: Response, next: NextFunction) => { try { + // controlUuid could be an uuid or 'all' + const session = req.session as SessionExt; + const userGroups = req.app.get("AUTH").getUserGroups(session.user.uuid); // in regards the value of controlUuid, response could be UptodateForm | UptodateForm[] // data will processed as UptodateForm[] let finalRecords: UptodateForm[] = []; @@ -95,6 +52,8 @@ routerActions.get( const record = dbGetRecord( req.app.get("DB"), req.params.controlUuid, + userGroups, + req.app.get("AUTH").isAdmin(req), req.app.get("LOGGER") ); let errorFound = false; @@ -108,7 +67,7 @@ routerActions.get( for (const item of finalRecords) { // get state await getUpToDateOrNotState(item) - .then((compareResult) => { + .then(async (compareResult) => { // Update dbRecord dbUpdateRecord(req.app.get("DB"), { ...item, @@ -121,7 +80,7 @@ routerActions.get( // 0 : uptodate 1:toupdate state is true when uptodate const payload = compareResult.state ? "0" : "1"; // call and set external status - updateExternalStatus(item, payload) + await updateExternalStatus(item, payload) .then((response) => { const finalResponse: UptoDateOrNotStateResponseMonitoring = { @@ -208,63 +167,27 @@ routerActions.get( res.status(404).json({ error: UUIDNOTFOUND }); } } catch (error: unknown) { + console.log(error); next(error); } } ); /** - * Call ci/cd for uuid provided - * - * @swagger - * /action/cicd/: - * put: - * summary: Call CI/CD - * description: Call url CI/CD for control uuid provided - * security: - * - ApiKeyAuth: [] - * tags: - * - Actions - * requestBody: - * description: uuid - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * uuid: - * type: string - * responses: - * 200: - * description: CI/CD response body - * content: - * application/text: - * schema: - * type: string - * 401: - * $ref: '#/components/responses/UnauthorizedError' - * 404: - * description: control uuid not found - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Error' - * 500: - * description: Internal error - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Error' + * Call up the url CI/CD for control uuid provided: 'all' is not accepted */ routerActions.put( "/action/cicd/", async (req: Request, res: Response, next: NextFunction) => { try { if (req.body && req.body.uuid) { + const session = req.session as SessionExt; + const userGroups = req.app.get("AUTH").getUserGroups(session.user.uuid); const record = dbGetRecord( req.app.get("DB"), req.body.uuid, + userGroups, + req.app.get("AUTH").isAdmin(req), req.app.get("LOGGER") ); if (record && !Array.isArray(record) && record.urlCICD) { @@ -305,9 +228,13 @@ routerActions.put( async (req: Request, res: Response, next: NextFunction) => { try { if (req.body && req.body.uuid) { + const session = req.session as SessionExt; + const userGroups = req.app.get("AUTH").getUserGroups(session.user.uuid); const record = (await dbGetRecord( req.app.get("DB"), req.body.uuid, + userGroups, + req.app.get("AUTH").isAdmin(req), req.app.get("LOGGER") )) as UptodateForm; if (record && !Array.isArray(record) && record.urlCronJobMonitoring) { @@ -347,57 +274,23 @@ routerActions.put( /** * return the git release of lastcompare * usefull to be called by CI/CD - * - * @swagger - * /action/lastcomparegitrealase/{controlUuid}/: - * get: - * summary: Get the github release - * description: Get the github release of the latest comparison (history) for one control uuid - * security: - * - ApiKeyAuth: [] - * tags: - * - Actions - * parameters: - * - in: path - * name: controlUuid - * required: true - * description: control uuid - * schema: - * type: string - * responses: - * 200: - * description: Value of the github release - * content: - * application/text: - * schema: - * type: string - * 401: - * $ref: '#/components/responses/UnauthorizedError' - * 404: - * description: control uuid not found or github release is empty - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Error' - * 500: - * description: Internal error - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Error' */ routerActions.get( - "/action/lastcomparegitrealase/:controlUuid/", + "/action/lastcomparegitrelease/:controlUuid/", async (req: Request, res: Response, next: NextFunction) => { try { + const session = req.session as SessionExt; + const userGroups = req.app.get("AUTH").getUserGroups(session.user.uuid); const record = (await dbGetRecord( req.app.get("DB"), req.params.controlUuid, + userGroups, + req.app.get("AUTH").isAdmin(req), req.app.get("LOGGER") )) as UptodateForm; if (record) { if (record.compareResult?.githubLatestRelease) { - res.status(200).send(record.compareResult?.githubLatestRelease); + res.status(200).json(record.compareResult?.githubLatestRelease); } else { req.app.get("LOGGER").error("githubLatestRelease is empty"); res.status(404).json({ error: "githubLatestRelease is empty" }); diff --git a/src/routes/routerAuth.ts b/src/routes/routerAuth.ts index 3dddaee..64ec6fa 100644 --- a/src/routes/routerAuth.ts +++ b/src/routes/routerAuth.ts @@ -9,49 +9,13 @@ import { ChangePasswordType, InfoIuType, NewUserType, - UsersType, + UserType, } from "../Global.types"; import { SessionExt } from "../ServerTypes"; const routerAuth = express.Router(); /** - * - * @swagger - * /userlogin: - * post: - * summary: login to the system with UI - * description: UI login method - * security: - * - ApiKeyAuth: [] - * tags: - * - Authentication - * requestBody: - * description: login properties - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * login: - * type: string - * password: - * type: string - * responses: - * 200: - * description: Login OK - * content: - * application/text: - * schema: - * $ref: '#/components/schemas/InfoIuType' - * 401: - * $ref: '#/components/responses/UnauthorizedError' - * 500: - * description: Internal error - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Error' + * login to the system with UI */ routerAuth.post( "/userlogin", @@ -71,7 +35,9 @@ routerAuth.post( session.user = req.app.get("AUTH").getInfoForUi(req.body.login); } } - res.status(verif[0]).json(verif[1]); + // no need to send info, login page is isolated, if user press F5 + // UI loose user infos + res.status(verif[0]).send(); } catch (error) { next(error); } @@ -79,40 +45,19 @@ routerAuth.post( ); /** - * - * @swagger - * /users: - * get: - * summary: get users list - * description: Used by the User Management UI to get users list - * security: - * - ApiKeyAuth: [] - * tags: - * - Authentication - * responses: - * 200: - * description: Users list - * content: - * application/json: - * schema: - * type: array - * items: - * type: string - * 401: - * $ref: '#/components/responses/UnauthorizedError' - * 500: - * description: Internal error - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Error' + * get users list - only admin */ routerAuth.get( "/users", async (req: Request, res: Response, next: NextFunction) => { - //next step: for admin users only try { - res.status(200).json(req.app.get("AUTH").getUsersLogins()); + const session = req.session as SessionExt; + const isAdmin = req.app.get("AUTH").isAdmin(req); + if (session.user && session.user.login && isAdmin) { + res.status(200).json(req.app.get("AUTH").getUsersForUi(isAdmin)); + } else { + res.status(401).send(); + } } catch (error: unknown) { next(error); } @@ -120,68 +65,43 @@ routerAuth.get( ); /** - * - * @swagger - * /users: - * post: - * summary: create user in the database - * description: UI create user method - * security: - * - ApiKeyAuth: [] - * tags: - * - Authentication - * requestBody: - * description: login and password - * required: true - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/NewUserType' - * - * responses: - * 200: - * description: created user - * content: - * application/text: - * schema: - * type: object - * properties: - * login: - * type: string - * 400: - * description: User already exists - * 401: - * $ref: '#/components/responses/UnauthorizedError' - * 500: - * description: Internal error - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Error' + * create user in the database - only admin */ routerAuth.post( "/users", async (req: Request, res: Response, next: NextFunction) => { //next step: for admin users only try { - // get the login and password - const newUser = req.body as NewUserType; - // get the list of current users, to check if user already exists - const users: string[] = req.app.get("AUTH").getUsersLogins(); + const session = req.session as SessionExt; + if ( + session.user && + session.user.login && + req.app.get("AUTH").isAdmin(req) + ) { + // get the login and password + const newUser = req.body as NewUserType; + // get the list of current users, to check if user already exists + const users: UserType[] = req.app.get("AUTH").getUsers(); - // check if user already exists - if (!users.find((user) => user === newUser.login)) { - const user = req.app - .get("AUTH") - .makeUser(newUser.login, newUser.password); - req.app.get("AUTH").addUser(user); // add user to the list - req.app.get("LOGGER").info({ - action: "create user", - user: newUser.login, - }); - res.status(200).json({ login: newUser.login }); + // check if user already exists + if (!users.find((user) => user.login === newUser.login)) { + const user = req.app + .get("AUTH") + .makeUser(newUser.login, newUser.password); + req.app.get("AUTH").addUser(user); // add user to the list + for (const group of newUser.groups) { + req.app.get("AUTH").addGroupMember(group, user.uuid); + } + req.app.get("LOGGER").info({ + action: "create user", + user: newUser.login, + }); + res.status(200).json({ login: newUser.login }); + } else { + res.status(400).json({ error: "User already exists" }); + } } else { - res.status(400).json({ error: "User already exists" }); + res.status(401).send(); } } catch (error: unknown) { next(error); @@ -190,60 +110,56 @@ routerAuth.post( ); /** - * - * @swagger - * /users: - * put: - * summary: modify user in the database - * description: UI modify user method - * security: - * - ApiKeyAuth: [] - * tags: - * - Authentication - * requestBody: - * description: login and password - * required: true - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/NewUserType' - * - * responses: - * 204: - * description: modified user - * 404: - * description: User not found - * 401: - * $ref: '#/components/responses/UnauthorizedError' - * 500: - * description: Internal error - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Error' + * modify user in the database - only admin */ routerAuth.put( "/users", async (req: Request, res: Response, next: NextFunction) => { //next step: for admin users only try { - // get the login and password - const newUser = req.body as NewUserType; - // get the list of current users, to check if user already exists - const users: UsersType = req.app.get("AUTH").users; - const idx = users.users.findIndex((u) => u.login === newUser.login); - if (idx !== -1) { - users.users[idx].password = req.app - .get("AUTH") - .encryptPassword(newUser.password); - req.app.get("AUTH").writeDB(JSON.stringify(users)); - req.app.get("LOGGER").info({ - action: "modify user", - user: newUser.login, - }); - res.status(204).send(); + const session = req.session as SessionExt; + if ( + session.user && + session.user.login && + req.app.get("AUTH").isAdmin(req) + ) { + // get the login and password + const userToUpdate = req.body as NewUserType; + if (userToUpdate.uuid) { + // get the list of current users, to check if user already exists + // get the list of current users, to check if user already exists + const users: UserType[] = req.app.get("AUTH").getUsers(); + + const idx = users.findIndex((u) => u.uuid === userToUpdate.uuid); + if (idx !== -1) { + //change password only if provided + if (userToUpdate.password) { + users[idx].password = req.app + .get("AUTH") + .encryptPassword(userToUpdate.password); + req.app.get("AUTH").writeDB(JSON.stringify(users)); + } + + // update groups + req.app.get("AUTH").removeUserFromGroups(userToUpdate.uuid); + for (const group of userToUpdate.groups) { + req.app.get("AUTH").addGroupMember(group, userToUpdate.uuid); + } + // groups cleaning + req.app.get("AUTH").cleanGroups(); + req.app.get("LOGGER").info({ + action: "modify user", + user: userToUpdate.login, + }); + res.status(204).send(); + } else { + res.status(400).json({ error: "User not found" }); + } + } else { + next(new Error("User uuid not set, mandatory for update")); + } } else { - res.status(400).json({ error: "User not found" }); + res.status(401).send(); } } catch (error: unknown) { next(error); @@ -252,71 +168,44 @@ routerAuth.put( ); /** - * - * @swagger - * /users/{login}: - * delete: - * summary: delete user from the database - * description: UI delete user method - * security: - * - ApiKeyAuth: [] - * tags: - * - Authentication - * parameters: - * - in: path - * name: login - * required: true - * description: login of the user to delete - * schema: - * type: string - * example: "user" - * - * responses: - * 200: - * description: deleted - * content: - * application/text: - * schema: - * type: object - * properties: - * login: - * type: string - * 404: - * description: User not found - * 401: - * $ref: '#/components/responses/UnauthorizedError' - * 500: - * description: Internal error - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Error' + * delete user from the database - only admin */ routerAuth.delete( - "/users/:login", + "/users/:uuid", async (req: Request, res: Response, next: NextFunction) => { //next step: for admin users only try { - // get the login - const userToDelete = req.params.login; - // get the list of current users, to check if user exists before deleting it - const users: string[] = req.app.get("AUTH").getUsersLogins(); + const session = req.session as SessionExt; + if ( + session.user && + session.user.uuid && + req.app.get("AUTH").isAdmin(req) + ) { + // get the login + const userToDelete = req.params.uuid; + // get the list of current users, to check if user exists before deleting it + const users: UserType[] = req.app.get("AUTH").getUsers(); - const user = users.find((user) => user === userToDelete); + const user = users.find((user) => user.uuid === userToDelete); - const session = req.session as SessionExt; + const session = req.session as SessionExt; - // check if user exists - user could not delete their account - if (user && session.user.login !== req.params.login) { - // if so, delete it - req.app.get("AUTH").deleteUser(user); // add user to the list - req.app.get("LOGGER").info({ - action: "delete user", - user: userToDelete, - }); - res.status(200).json({ login: userToDelete }); + // check if user exists - user could not delete their account + if (user && session.user.uuid !== req.params.uuid) { + // if so, delete it + req.app.get("AUTH").deleteUser(user.uuid); // add user to the list + req.app.get("LOGGER").info({ + action: "delete user", + user: `${user.uuid} - ${user.login}`, + }); + // groups cleaning + req.app.get("AUTH").cleanGroups(); + res.status(200).json({ login: user.login, uuid: user.uuid }); + } else { + res.status(404).json({ error: "User not found" }); + } } else { - res.status(404).json({ error: "User not found" }); + res.status(401).send(); } } catch (error: unknown) { next(error); @@ -325,38 +214,14 @@ routerAuth.delete( ); /** - * - * @swagger - * /isauthenticated: - * get: - * summary: is user logged - * description: Used by UI to verify user is logged - * security: - * - ApiKeyAuth: [] - * tags: - * - Authentication - * responses: - * 200: - * description: User is logged - * content: - * application/json: - * schema: - * type: string - * 401: - * $ref: '#/components/responses/UnauthorizedError' - * 500: - * description: Internal error - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Error' + * is the user logged in ? */ routerAuth.get( "/isauthenticated", async (req: Request, res: Response, next: NextFunction) => { try { const verif = req.app.get("AUTH").isAuthenticated(req); - res.status(verif ? 200 : 401).json(); + res.status(verif ? 204 : 401).json(); } catch (error) { next(error); } @@ -364,26 +229,23 @@ routerAuth.get( ); /** - * @swagger - * /userlogout: - * get: - * summary: user logout method - * description: Used by UI to logout user - * security: - * - ApiKeyAuth: [] - * tags: - * - Authentication - * responses: - * 204: - * description: User is logged out - * 500: - * description: Internal error - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Error' + * does the user have the administrator role ? */ +routerAuth.get( + "/isadmin", + async (req: Request, res: Response, next: NextFunction) => { + try { + const verif = req.app.get("AUTH").isAdmin(req); + res.status(verif ? 204 : 401).send(); + } catch (error) { + next(error); + } + } +); +/** + * user logout method + */ routerAuth.get( "/userlogout", async (req: Request, res: Response, next: NextFunction) => { @@ -408,45 +270,25 @@ routerAuth.get( ); /** - * @swagger - * /changepassword: - * put: - * summary: user change password - * description: Used by UI to change user password - * security: - * - ApiKeyAuth: [] - * tags: - * - Authentication - * requestBody: - * description: passwords list - * required: true - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/ChangePasswordType' - * responses: - * 204: - * description: Password has been changed - * 401: - * $ref: '#/components/responses/UnauthorizedError' - * 500: - * description: Internal error - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Error' + * user change password */ - routerAuth.put( "/changepassword", async (req: Request, res: Response, next: NextFunction) => { try { - const changepassword = req.body as ChangePasswordType; - const verified = req.app.get("AUTH").changePassword(changepassword); - if (verified[0] === 200) { - res.status(204).send(); + const session = req.session as SessionExt; + if (session.user && session.user.login) { + const changepassword = req.body as ChangePasswordType; + const verified = req.app + .get("AUTH") + .changePassword(changepassword, session.user.login); + if (verified[0] === 200) { + res.status(204).send(); + } else { + res.status(500).json({ error: verified[1] }); + } } else { - res.status(500).json({ error: verified[1] }); + res.status(500).json({ error: "User is not logged with session" }); } } catch (error: unknown) { next(error); @@ -455,45 +297,14 @@ routerAuth.put( ); /** - * @swagger - * /bearer: - * get: - * summary: get user user auth Token - * description: Used by UI to get user auth Token - * security: - * - ApiKeyAuth: [] - * tags: - * - Authentication - * responses: - * 200: - * description: User auth Token - * content: - * application/json: - * schema: - * type: object - * properties: - * bearer: - * type: string - * 401: - * $ref: '#/components/responses/UnauthorizedError' - * 500: - * description: Internal error - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Error' + * get user user auth Token */ - routerAuth.get( "/bearer", async (req: Request, res: Response, next: NextFunction) => { try { const session = req.session as SessionExt; - if ( - session.user && - session.user.login // && - // session.user.login === "admin" - ) { + if (session.user && session.user.login) { res.status(200).json({ bearer: req.app.get("AUTH").getUserBearer(session.user.login), }); @@ -507,44 +318,14 @@ routerAuth.get( ); /** - * @swagger - * /user: - * get: - * summary: get user's login - * description: Used by UI to show the user's login in the header - * security: - * - ApiKeyAuth: [] - * tags: - * - Authentication - * responses: - * 200: - * description: User's login - * content: - * application/json: - * schema: - * type: object - * properties: - * login: - * type: string - * 401: - * $ref: '#/components/responses/UnauthorizedError' - * 500: - * description: Internal error - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Error' + * Used by UI to show the user's login in the header */ routerAuth.get( - "/user", + "/userlogin", async (req: Request, res: Response, next: NextFunction) => { try { const session = req.session as SessionExt; - if ( - session.user && - session.user.login // && - // session.user.login === "admin" - ) { + if (session.user && session.user.login) { res.status(200).json({ login: session.user.login }); } else { res.status(401).json({ error: "User is not logged with session" }); @@ -556,39 +337,15 @@ routerAuth.get( ); /** - * @swagger - * /bearer: - * put: - * summary: change user auth Token - * description: Used by UI to get new user auth Token - * security: - * - ApiKeyAuth: [] - * tags: - * - Authentication - * responses: - * 204: - * description: User auth Token has been changed - * 401: - * $ref: '#/components/responses/UnauthorizedError' - * 500: - * description: Internal error - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Error' + * Used by UI to get new user auth Token */ - routerAuth.put( "/bearer", async (req: Request, res: Response, next: NextFunction) => { try { const session = req.session as SessionExt; - if ( - session.user && - session.user.login // && - // session.user.login === "admin" - ) { - const result = req.app.get("AUTH").changeBearer(); + if (session.user && session.user.login) { + const result = req.app.get("AUTH").changeBearer(session.user.login); if (result[0] === 200) { res.status(204).json(); } else { @@ -603,4 +360,47 @@ routerAuth.put( } ); +/** + * Used by UI to display user's groups + */ +routerAuth.get( + "/groups", + async (req: Request, res: Response, next: NextFunction) => { + try { + const session = req.session as SessionExt; + if (session.user && session.user.login) { + let groups: string[] = []; + groups = req.app.get("AUTH").getGroups(req); + res.status(200).json(groups); + } else { + res + .status(401) + .json({ error: "User is not logged with session or not admin" }); + } + } catch (error: unknown) { + next(error); + } + } +); + +/** + * Used by UI to get the user's groups + */ +routerAuth.get( + "/userGroups", + async (req: Request, res: Response, next: NextFunction) => { + try { + const session = req.session as SessionExt; + if (session.user && session.user.login) { + const user = req.app.get("AUTH").getInfoForUi(session.user.login); + res.status(200).json({ groups: user.groups }); + } else { + res.status(401).json({ error: "User is not logged with session" }); + } + } catch (error: unknown) { + next(error); + } + } +); + export default routerAuth; diff --git a/src/routes/routerControls.ts b/src/routes/routerControls.ts index 7090c27..125d867 100644 --- a/src/routes/routerControls.ts +++ b/src/routes/routerControls.ts @@ -13,12 +13,20 @@ import { dbUpdateRecord, } from "../lib/Database"; import { recordsOrder } from "../lib/Features"; +import { SessionExt } from "../ServerTypes"; const routerControl = express.Router(); routerControl.post( "/control", async (req: Request, res: Response, next: NextFunction) => { try { + //access only from the user interface, because adding a control requires going through several steps + if (req.app.get("AUTH").isAuthBearer(req)) { + res.status(403).json({ + error: "This operation can only be performed via the user interface", + }); + return; + } const mfields = [ "name", "urlProduction", @@ -26,24 +34,32 @@ routerControl.post( "exprProduction", "urlGitHub", "exprGithub", + "groups", ]; let validate = true; for (const attr of Object.getOwnPropertyNames(req.body as UptodateForm)) { - if (mfields.includes(attr) && !req.body[attr]) { + if ( + (mfields.includes(attr) && !req.body[attr]) || + (attr === "groups" && req.body[attr].length === 0) + ) { validate = false; break; } } + // finally, is user allowed to manipulate object ? + validate = req.app.get("AUTH").isAllowedForObject(req, req.body.groups); if (!validate) { - res.status(503).json("check is not valid"); + res.status(503).json("control is not valid"); } else { if (req.body.uuid) { // uuid update const rupd = dbUpdateRecord(req.app.get("DB"), req.body); if (rupd) { dbCommit(req.app.get("DBFILE") || "", req.app.get("DB")); - req.app.get("LOGGER").info({ action: "check updated", uuid: rupd }); - res.status(200).json({ check: { ...req.body, uuid: rupd } }); + req.app + .get("LOGGER") + .info({ action: "control updated", uuid: rupd }); + res.status(200).json({ control: { ...req.body, uuid: rupd } }); } else { next(new Error("update return empty")); } @@ -52,8 +68,10 @@ routerControl.post( dbInsert(req.app.get("DB"), { ...req.body }) .then((uuid) => { dbCommit(req.app.get("DBFILE") || "", req.app.get("DB")); - req.app.get("LOGGER").info({ action: "check added", uuid: uuid }); - res.status(200).json({ check: { ...req.body, uuid: uuid } }); + req.app + .get("LOGGER") + .info({ action: "control added", uuid: uuid }); + res.status(200).json({ control: { ...req.body, uuid: uuid } }); }) .catch((error: Error) => { next(error); @@ -67,48 +85,19 @@ routerControl.post( ); /** - * uuid value could be "all" - * - * @swagger - * /control/{uuid}: - * get: - * summary: Get control values - * description: Get all control data per uuid or all controls (all) - * security: - * - ApiKeyAuth: [] - * tags: - * - Control - * parameters: - * - in: path - * name: uuid - * required: true - * description: control uuid||all - * schema: - * type: string - * responses: - * 200: - * description: control data or Array of controls data - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/UptodateForm' - * 401: - * $ref: '#/components/responses/UnauthorizedError' - * 500: - * description: Internal error - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Error' + * Get control - uuid value could be "all" */ - routerControl.get( "/control/:uuid", async (req: Request, res: Response, next: NextFunction) => { try { + const session = req.session as SessionExt; + const userGroups = req.app.get("AUTH").getUserGroups(session.user.uuid); let rec = dbGetRecord( req.app.get("DB"), req.params.uuid, + userGroups, + req.app.get("AUTH").isAdmin(req), req.app.get("LOGGER") ); if (Array.isArray(rec) && rec.length > 0) { @@ -122,67 +111,43 @@ routerControl.get( ); /** - * Delete controle per uuid - * - * @swagger - * /control/{uuid}: - * delete: - * summary: Delete one control - * description: Delete control per uuid - * security: - * - ApiKeyAuth: [] - * tags: - * - Control - * parameters: - * - in: path - * name: uuid - * required: true - * description: control uuid - * schema: - * type: string - * responses: - * 200: - * description: control uuid as JSON - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/DeletedRecord' - * 401: - * $ref: '#/components/responses/UnauthorizedError' - * 404: - * description: control uuid not found - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Error' - * 500: - * description: Internal error - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Error' + * Delete control per uuid */ routerControl.delete( "/control/:uuid", async (req: Request, res: Response, next: NextFunction) => { try { - const rec = dbDeleteRecord(req.app.get("DB"), req.params.uuid); - if (rec === req.params.uuid) { - //commit - dbCommit(req.app.get("DBFILE") as string, req.app.get("DB")) - .then(() => { - req.app - .get("LOGGER") - .info({ action: "check deleted", uuid: req.params.uuid }); - res.status(200).json({ uuid: rec }); - }) - .catch((error: Error) => { - next(error); + const session = req.session as SessionExt; + const userGroups = req.app.get("AUTH").getUserGroups(session.user.uuid); + // get record filtered on authorized groups + const rec = dbGetRecord( + req.app.get("DB"), + req.params.uuid, + userGroups, + req.app.get("AUTH").isAdmin(req), + req.app.get("LOGGER") + ); + if (rec && !Array.isArray(rec)) { + const rec = dbDeleteRecord(req.app.get("DB"), req.params.uuid); + if (rec === req.params.uuid) { + //commit + dbCommit(req.app.get("DBFILE") as string, req.app.get("DB")) + .then(() => { + req.app + .get("LOGGER") + .info({ action: "control deleted", uuid: req.params.uuid }); + res.status(200).json({ uuid: rec }); + }) + .catch((error: Error) => { + next(error); + }); + } else { + res.status(404).json({ + message: `Something went wrong during control deletion process, non-existent uuid: ${req.params.uuid}`, }); + } } else { - res.status(404).json({ - message: `Try to delete a non-existent uuid: ${req.params.uuid}`, - }); + res.status(404).send(); } } catch (error: unknown) { next(error); diff --git a/src/routes/routerCore.ts b/src/routes/routerCore.ts index a9a4ac3..fbc5f95 100644 --- a/src/routes/routerCore.ts +++ b/src/routes/routerCore.ts @@ -9,173 +9,7 @@ import { APPLICATION_VERSION } from "../Constants"; const routerCore = express.Router(); /** - * @swagger - * components: - * responses: - * UnauthorizedError: - * description: Access token,user/password are missing, invalid, or session is not set - * schemas: - * Error: - * type: string - * description: Catched Error as string - * HTTPMethods: - * type: string - * enum: - * - "GET" - * - "POST" - * - "PUT" - * - "DELETE" - * ChangePasswordType: - * type: object - * properties: - * password: - * type: string - * newPassword: - * type: string - * newConfirmPassword: - * type: string - * NewUserType: - * type: object - * properties: - * login: - * type: string - * description: login of the new user - * example: AzureDiamond - * password: - * type: string - * description: password of the new user - * example: hunter2 - * InfoIuType: - * type: object - * properties: - * login: - * type: string - * bearer: - * type: string - * DeletedRecord: - * type: object - * properties: - * uuid: - * type: string - * UptoDateOrNotState: - * type: object - * properties: - * name: - * type: string - * description: name of control - * githubLatestRelease: - * type: string - * description: latest github release after expression applied - * productionVersion: - * type: string - * description: production version after expression applied - * state: - * type: boolean - * description: is Uptodate or Not ? - * strictlyEqual: - * type: boolean - * description: is production strictly equal latest github release ? - * githubLatestReleaseIncludesProductionVersion: - * description: Is github latest release included in Production version? - * type: boolean - * productionVersionIncludesGithubLatestRelease: - * description: Is Production version included in github latest release? - * type: boolean - * urlGitHub: - * type: string - * urlProduction: - * type: string - * ts: - * type: number - * description: Execution timestamp - * UptodateForm: - * type: object - * description: control record - * properties: - * uuid: - * type: string - * description: control uniq id - * name: - * type: string - * description: name of control - * logo: - * type: string - * description: base64 html logo src - * urlProduction: - * type: string - * description: url of the production application to be verified - * scrapTypeProduction: - * type: string - * description: type of content - * exprProduction: - * type: string - * description: Expression to apply to get the version - * urlGitHub: - * type: string - * description: url of the github repository - * exprGithub: - * type: string - * description: Expression to apply to get the version - * urlCronJobMonitoring: - * type: string - * description: url of the cronJob monitoring - * httpMethodCronJobMonitoring: - * $ref: '#/components/schemas/HTTPMethods' - * description: Http method to call url of the cronJob monitoring - * urlCronJobMonitoringAuth: - * type: string - * description: Api Key to provide to call url of the cronJob monitoring - * urlCICD: - * type: string - * description: url of the CI/CD - * httpMethodCICD: - * $ref: '#/components/schemas/HTTPMethods' - * description: Http method to call url of the CI/CD - * urlCICDAuth: - * type: string - * description: Api Key to provide to call url of the CI/CD - * isPause: - * type: boolean - * description: When calling compare API if paused, this control will not be included in the process - * compareResult: - * description: latest compare result - * oneOf: - * - $ref: '#/components/schemas/UptoDateOrNotState' - * - nullable: true - */ - -/** - * @swagger - * /scrap/{url}: - * get: - * summary: Get provided url content - * description: Used to retrieve url content from the production server, github API tags... - * security: - * - ApiKeyAuth: [] - * tags: - * - Core - * parameters: - * - in: path - * name: url - * required: true - * description: url to scrap http||https supported - * schema: - * type: string - * responses: - * 200: - * description: content as text - * content: - * application/text: - * schema: - * type: string - * 401: - * $ref: '#/components/responses/UnauthorizedError' - * 500: - * description: Internal error - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Error' + * Used to retrieve url content from the production server, github API tags... */ routerCore.get( "/scrap/:url", @@ -198,31 +32,7 @@ routerCore.get( ); /** - * @swagger - * /version: - * get: - * summary: To get the application version - * description: Get the version in JSON format - * security: [] - * tags: - * - Core - * responses: - * 200: - * description: Version as JSON - * content: - * application/json: - * schema: - * type: object - * properties: - * version: - * type: string - * description: 'Major.Minor.Patch' - * 500: - * description: Internal error - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Error' + * To get the application version */ routerCore.get( "/version", @@ -236,23 +46,7 @@ routerCore.get( ); /** - * @swagger - * /healthz: - * get: - * summary: Is service Healthy ? - * description: Could be used to find out if the service is healthy - * security: [] - * tags: - * - Core - * responses: - * 204: - * description: service is healthy - * 500: - * description: Internal error - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Error' + * Could be used to find out if the service is healthy */ routerCore.get( "/healthz", @@ -266,26 +60,7 @@ routerCore.get( ); /** - * @swagger - * /metrics: - * get: - * summary: not implemented - * description: In the Roadmap - * security: - * - ApiKeyAuth: [] - * tags: - * - Core - * responses: - * 503: - * description: not implemented - * 401: - * $ref: '#/components/responses/UnauthorizedError' - * 500: - * description: Internal error - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Error' + * metrics not implemented */ routerCore.get( "/metrics", diff --git a/test/Authentification.test.ts b/test/Authentification.test.ts index e2b00c7..feea71f 100644 --- a/test/Authentification.test.ts +++ b/test/Authentification.test.ts @@ -9,10 +9,43 @@ import { LOGIN_FAILED, PASSWORD_OR_USER_UNDEFINED } from "../src/Constants"; import { Request } from "express"; import { ChangePasswordType, InfoIuType } from "../src/Global.types"; import { SessionExt } from "../src/ServerTypes"; + +import winston from "winston"; +import Transport from "winston-transport"; +const { combine, timestamp, json } = winston.format; +interface LastErrorTransportOptions { + level?: string; +} + +// Special transport - For testing: error has been flushed by winston +class LastErrorTransport extends Transport { + lastError: Error | null; + constructor(options: LastErrorTransportOptions = {}) { + super(options); + this.lastError = null; + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + log(info: any, callback: () => void) { + if (info.level === "error") { + this.lastError = info; + } + callback(); + } +} + +const logger = winston.createLogger({ + level: "info", + defaultMeta: { + service: "utdon", + }, + format: combine(timestamp(), json()), + transports: [new LastErrorTransport()], +}); + const userDatabase = `${__dirname}/data/userDatabase.json`; describe("Authentification", () => { - afterEach(() => { + beforeEach(() => { // delete userdatabase if (existsSync(userDatabase)) rmSync(userDatabase); process.env.DATABASE_ENCRYPT_SECRET = "mysecret"; @@ -27,22 +60,23 @@ describe("Authentification", () => { } }); - test("loadUserFromDatabase no user", () => { + test("loadUserFromDatabase no user, admin group must be defined", () => { try { const auth = new Authentification(userDatabase); const data = auth.loadUsersFromDatabase(); expect(data.users[0]).not.toBeDefined(); + expect(data.groups.admin).toBeDefined(); } catch (error: unknown) { // unexpected error expect(error).not.toBeDefined(); } }); - test("schema migration PR#15", () => { + test("schema migration from PR#15 to now...", () => { // old scheme copyFileSync("./test/samples/users-before-PR#15.json", userDatabase); try { - // get user values + // user data is older than PR#15 const oldData = JSON.parse(readFileSync(userDatabase, "utf-8")); const auth = new Authentification(userDatabase); const data = auth.loadUsersFromDatabase(); @@ -57,9 +91,12 @@ describe("Authentification", () => { expect(data.users[0].uuid).toEqual(oldData.uuid); expect(data.users[0].password).toEqual(oldData.password); expect(data.users[0].bearer).toEqual(oldData.bearer); + // automatically added in admin group + expect(data.groups.admin[0]).toEqual(oldData.uuid); //cleaning unlinkSync(fileCopy); } catch (error: unknown) { + console.log(error); // unexpected error expect(error).not.toBeDefined(); } @@ -72,6 +109,7 @@ describe("Authentification", () => { const bearer = auth.generateBearerKey(); expect(bearer).not.toEqual(""); } catch (error: unknown) { + console.log(error); // unexpected error expect(error).not.toBeDefined(); } @@ -121,7 +159,8 @@ describe("Authentification", () => { let data = auth.loadUsersFromDatabase(); expect(data.users[0]).not.toBeDefined(); const user = auth.makeUser("admin", "admin"); - auth.store(user); + auth.addUser(user); + auth.writeDB(); //reload from disk data = auth.loadUsersFromDatabase(); expect(data.users[0].login).toEqual("admin"); @@ -197,8 +236,10 @@ describe("Authentification", () => { expect(data.users.length).toEqual(3); + const userToDelete = data.users.find((user) => user.login === "user1"); + expect(userToDelete).toBeDefined(); // delete user1 - auth.deleteUser("user1"); + if (userToDelete) auth.deleteUser(userToDelete.uuid); //reload from disk data = auth.loadUsersFromDatabase(); @@ -206,8 +247,9 @@ describe("Authentification", () => { const user = data.users.find((user) => user.login === "user1"); expect(user).not.toBeDefined(); - expect(data.users.length).toEqual(2); + // expect(data.users.length).toEqual(2); } catch (error: unknown) { + console.log(error); // unexpected error expect(error).not.toBeDefined(); } @@ -248,7 +290,7 @@ describe("Authentification", () => { test("make & store User - user malformed - login empty", () => { try { const auth = new Authentification(userDatabase); - auth.store({ login: "", password: "xxxx", uuid: "xxx", bearer: "xxx" }); + auth.addUser({ login: "", password: "xxxx", uuid: "xxx", bearer: "xxx" }); //unexpected expect(true).toBeFalsy(); } catch (error: unknown) { @@ -260,7 +302,7 @@ describe("Authentification", () => { try { process.env.USER_ENCRYPT_SECRET = "test"; const auth = new Authentification(userDatabase); - auth.store({ login: "xxxx", password: "", uuid: "xxx", bearer: "xxx" }); + auth.addUser({ login: "xxxx", password: "", uuid: "xxx", bearer: "xxx" }); //unexpected expect(true).toBeFalsy(); } catch (error: unknown) { @@ -272,7 +314,7 @@ describe("Authentification", () => { try { process.env.USER_ENCRYPT_SECRET = "test"; const auth = new Authentification(userDatabase); - auth.store({ login: "xxxx", password: "xxx", uuid: "", bearer: "xxx" }); + auth.addUser({ login: "xxxx", password: "xxx", uuid: "", bearer: "xxx" }); //unexpected expect(true).toBeFalsy(); } catch (error: unknown) { @@ -284,7 +326,7 @@ describe("Authentification", () => { try { process.env.USER_ENCRYPT_SECRET = "test"; const auth = new Authentification(userDatabase); - auth.store({ login: "xxxx", password: "xxx", uuid: "xxx", bearer: "" }); + auth.addUser({ login: "xxxx", password: "xxx", uuid: "xxx", bearer: "" }); //unexpected expect(true).toBeFalsy(); } catch (error: unknown) { @@ -292,11 +334,43 @@ describe("Authentification", () => { } }); + test("getUsers - existent users", () => { + try { + const auth = new Authentification(userDatabase); + auth.addUser({ + login: "xxxx", + password: "xxxx", + uuid: "xxx", + bearer: "xxx", + }); + auth.addUser({ + login: "xxxx1", + password: "xxxx", + uuid: "xxx1", + bearer: "xxx", + }); + const users = auth.getUsers(); + expect(users.length).toEqual(2); + } catch (error: unknown) { + expect(error).toBeDefined(); + } + }); + + test("getUsers - no users", () => { + try { + const auth = new Authentification(userDatabase); + const users = auth.getUsers(); + expect(users.length).toEqual(0); + } catch (error: unknown) { + expect(error).toBeDefined(); + } + }); + test("verifyPassword correct password", () => { try { process.env.USER_ENCRYPT_SECRET = "test"; const auth = new Authentification(userDatabase); - auth.store(auth.makeUser("admin", "admin")); + auth.addUser(auth.makeUser("admin", "admin")); const verify = auth.verifyPassword("admin", "admin"); expect(verify[0]).toEqual(200); const user = verify[1] as InfoIuType; @@ -312,7 +386,7 @@ describe("Authentification", () => { try { process.env.USER_ENCRYPT_SECRET = "test"; const auth = new Authentification(userDatabase); - auth.store(auth.makeUser("admin", "admin")); + auth.addUser(auth.makeUser("admin", "admin")); const verify = auth.verifyPassword("admin", "adminxx"); expect(verify[0]).toEqual(401); expect(verify[1]).toEqual(LOGIN_FAILED); @@ -326,7 +400,7 @@ describe("Authentification", () => { try { process.env.USER_ENCRYPT_SECRET = "test"; const auth = new Authentification(userDatabase); - auth.store(auth.makeUser("admin", "admin")); + auth.addUser(auth.makeUser("admin", "admin")); const verify = auth.verifyPassword("admin", ""); expect(verify[0]).toEqual(500); expect(verify[1]).toEqual(PASSWORD_OR_USER_UNDEFINED); @@ -340,7 +414,7 @@ describe("Authentification", () => { try { process.env.USER_ENCRYPT_SECRET = "test"; const auth = new Authentification(userDatabase); - auth.store(auth.makeUser("admin", "admin")); + auth.addUser(auth.makeUser("admin", "admin")); const verify = auth.verifyPassword("adminxx", "admin"); expect(verify[0]).toEqual(401); expect(verify[1]).toEqual(LOGIN_FAILED); @@ -364,11 +438,39 @@ describe("Authentification", () => { } }); + test("getUsersForUi - admin", () => { + process.env.USER_ENCRYPT_SECRET = "test"; + const auth = new Authentification(userDatabase); + const admin = auth.makeUser("admin", "admin"); + auth.addUser(admin); + auth.addGroupMember("admin", admin.uuid); + + const test = auth.makeUser("test", "test"); + auth.addUser(test); + auth.addGroupMember("test", test.uuid); + + expect(auth.getUsersForUi(true).length).toEqual(2); + }); + + test("getUsersForUi - Non admin", () => { + process.env.USER_ENCRYPT_SECRET = "test"; + const auth = new Authentification(userDatabase); + const admin = auth.makeUser("admin", "admin"); + auth.addUser(admin); + auth.addGroupMember("admin", admin.uuid); + + const test = auth.makeUser("test", "test"); + auth.addUser(test); + auth.addGroupMember("test", test.uuid); + + expect(auth.getUsersForUi(false).length).toEqual(0); + }); + test("isAuthSession - true", () => { try { process.env.USER_ENCRYPT_SECRET = "test"; const auth = new Authentification(userDatabase); - auth.store(auth.makeUser("admin", "admin")); + auth.addUser(auth.makeUser("admin", "admin")); const req = { body: {} } as Request; req.session = { user: { login: "admin" } } as SessionExt; const isAuth = auth.isAuthSession(req); @@ -383,7 +485,7 @@ describe("Authentification", () => { try { process.env.USER_ENCRYPT_SECRET = "test"; const auth = new Authentification(userDatabase); - auth.store(auth.makeUser("admin", "admin")); + auth.addUser(auth.makeUser("admin", "admin")); const req = { body: {} } as Request; req.session = { user: { login: "adminx" } } as SessionExt; const isAuth = auth.isAuthSession(req); @@ -398,7 +500,7 @@ describe("Authentification", () => { try { process.env.USER_ENCRYPT_SECRET = "test"; const auth = new Authentification(userDatabase); - auth.store(auth.makeUser("admin", "admin")); + auth.addUser(auth.makeUser("admin", "admin")); const req = { body: {} } as Request; req.session = { user: { login: "" } } as SessionExt; const isAuth = auth.isAuthSession(req); @@ -413,7 +515,7 @@ describe("Authentification", () => { try { process.env.USER_ENCRYPT_SECRET = "test"; const auth = new Authentification(userDatabase); - auth.store(auth.makeUser("admin", "admin")); + auth.addUser(auth.makeUser("admin", "admin")); const req = { body: {} } as Request; req.session = { user: {} } as SessionExt; const isAuth = auth.isAuthSession(req); @@ -428,7 +530,7 @@ describe("Authentification", () => { try { process.env.USER_ENCRYPT_SECRET = "test"; const auth = new Authentification(userDatabase); - auth.store(auth.makeUser("admin", "admin")); + auth.addUser(auth.makeUser("admin", "admin")); const req = { body: {} } as Request; req.session = {} as SessionExt; const isAuth = auth.isAuthSession(req); @@ -443,7 +545,7 @@ describe("Authentification", () => { try { process.env.USER_ENCRYPT_SECRET = "test"; const auth = new Authentification(userDatabase); - auth.store(auth.makeUser("admin", "admin")); + auth.addUser(auth.makeUser("admin", "admin")); const req = { body: {} } as Request; const isAuth = auth.isAuthSession(req); expect(isAuth).toBeFalsy(); @@ -453,33 +555,67 @@ describe("Authentification", () => { } }); + test("isAuthSession - development mode", () => { + try { + process.env.USER_ENCRYPT_SECRET = "test"; + process.env.environment = "development"; + const auth = new Authentification(userDatabase); + auth.addUser(auth.makeUser("admin", "admin")); + const req = { + body: {}, + app: { + get: (key: string) => { + if (key === "LOGGER") return logger; + }, + }, + session: {}, + } as Request; + const isAuth = auth.isAuthSession(req); + expect(isAuth).toBeTruthy(); + const lastErrorWinston = (logger.transports[0] as LastErrorTransport) + .lastError; + expect(lastErrorWinston).toBeDefined(); + expect(lastErrorWinston?.message).toMatch( + /WARNING: process.env.environment/ + ); + } catch (error: unknown) { + console.log(error); + // unexpected error + expect(error).not.toBeDefined(); + } + }); + test("isAuthBearer - authorization provided is ok", () => { try { process.env.USER_ENCRYPT_SECRET = "test"; const auth = new Authentification(userDatabase); - auth.store(auth.makeUser("admin", "admin")); + auth.addUser(auth.makeUser("admin", "admin")); const req = { headers: { authorization: `${auth.getUsersBearers()[0]}` }, + session: {}, } as Request; const isAuth = auth.isAuthBearer(req); expect(isAuth).toBeTruthy(); } catch (error: unknown) { + console.log(error); // unexpected error expect(error).not.toBeDefined(); } }); - test("isAuthBearer - authorization provided is false", () => { + test("isAuthBearer - authorization provided is wrong", () => { try { process.env.USER_ENCRYPT_SECRET = "test"; const auth = new Authentification(userDatabase); - auth.store(auth.makeUser("admin", "admin")); + auth.addUser(auth.makeUser("admin", "admin")); const req = { headers: { authorization: `xxxx` }, + session: {}, } as Request; const isAuth = auth.isAuthBearer(req); expect(isAuth).toBeFalsy(); } catch (error: unknown) { + console.log(error); // unexpected error expect(error).not.toBeDefined(); } @@ -489,9 +625,10 @@ describe("Authentification", () => { try { process.env.USER_ENCRYPT_SECRET = "test"; const auth = new Authentification(userDatabase); - auth.store(auth.makeUser("admin", "admin")); + auth.addUser(auth.makeUser("admin", "admin")); const req = { headers: {}, + session: {}, } as Request; const isAuth = auth.isAuthBearer(req); expect(isAuth).toBeFalsy(); @@ -505,7 +642,7 @@ describe("Authentification", () => { try { process.env.USER_ENCRYPT_SECRET = "test"; const auth = new Authentification(userDatabase); - auth.store(auth.makeUser("admin", "admin")); + auth.addUser(auth.makeUser("admin", "admin")); const req = {} as Request; const isAuth = auth.isAuthBearer(req); expect(isAuth).toBeFalsy(); @@ -519,7 +656,7 @@ describe("Authentification", () => { try { process.env.USER_ENCRYPT_SECRET = "test"; const auth = new Authentification(userDatabase); - auth.store(auth.makeUser("admin", "admin")); + auth.addUser(auth.makeUser("admin", "admin")); const authBearer = auth.getUserBearer("test"); expect(authBearer).toEqual(""); } catch (error: unknown) { @@ -532,7 +669,7 @@ describe("Authentification", () => { try { process.env.USER_ENCRYPT_SECRET = "test"; const auth = new Authentification(userDatabase); - auth.store(auth.makeUser("admin", "admin")); + auth.addUser(auth.makeUser("admin", "admin")); const authBearer = auth.getUserBearer("admin"); expect(authBearer).not.toEqual(""); } catch (error: unknown) { @@ -545,7 +682,7 @@ describe("Authentification", () => { try { process.env.USER_ENCRYPT_SECRET = "test"; const auth = new Authentification(userDatabase); - auth.store(auth.makeUser("admin", "admin")); + auth.addUser(auth.makeUser("admin", "admin")); const authBearer = auth.changeBearer("test"); expect(authBearer[0]).toEqual(500); } catch (error: unknown) { @@ -554,12 +691,56 @@ describe("Authentification", () => { } }); - test("isAuthenticated - with session", () => { + test("isAdmin - session is needed - OK ", () => { try { process.env.USER_ENCRYPT_SECRET = "test"; const auth = new Authentification(userDatabase); - auth.store(auth.makeUser("admin", "admin")); + const user = auth.makeUser("admin", "admin"); + auth.addUser(user); + auth.addGroupMember("admin", user.uuid); const req = { body: {} } as Request; + req.session = { + user: { login: user.login, bearer: user.bearer, uuid: user.uuid }, + } as SessionExt; + const isAuth = auth.isAdmin(req); + expect(isAuth).toBeTruthy(); + } catch (error: unknown) { + console.log(error); + // unexpected error + expect(error).not.toBeDefined(); + } + }); + + test("isAdmin - session is not set", () => { + try { + process.env.USER_ENCRYPT_SECRET = "test"; + const auth = new Authentification(userDatabase); + const user = auth.makeUser("admin", "admin"); + auth.addUser(user); + auth.addGroupMember("admin", user.uuid); + const isAuth = auth.isAdmin(); + expect(isAuth).toBeFalsy(); + } catch (error: unknown) { + console.log(error); + // unexpected error + expect(error).not.toBeDefined(); + } + }); + + test("isAuthenticated - with session", () => { + try { + process.env.USER_ENCRYPT_SECRET = "test"; + const auth = new Authentification(userDatabase); + auth.addUser(auth.makeUser("admin", "admin")); + const req = { + body: {}, + app: { + get: (key: string) => { + if (key === "LOGGER") return logger; + }, + }, + session: {}, + } as Request; req.session = { user: { login: "admin" } } as SessionExt; const isAuth = auth.isAuthenticated(req); expect(isAuth).toBeTruthy(); @@ -573,9 +754,10 @@ describe("Authentification", () => { try { process.env.USER_ENCRYPT_SECRET = "test"; const auth = new Authentification(userDatabase); - auth.store(auth.makeUser("admin", "admin")); + auth.addUser(auth.makeUser("admin", "admin")); const req = { headers: { authorization: `${auth.getUsersBearers()[0]}` }, + session: {}, } as Request; const isAuth = auth.isAuthenticated(req); expect(isAuth).toBeTruthy(); @@ -589,19 +771,19 @@ describe("Authentification", () => { try { process.env.USER_ENCRYPT_SECRET = "test"; const auth = new Authentification(userDatabase); - const user = auth.store(auth.makeUser("admin", "admin")); + auth.addUser(auth.makeUser("admin", "admin")); + const changepassword: ChangePasswordType = { - login: "admin", password: "admin", newPassword: "newpassword", newConfirmPassword: "newpassword", }; - const oldbearer = user.users[0].bearer; - const verify = auth.changePassword(changepassword); + const oldbearer = auth.usersgroups.users[0].bearer; + const verify = auth.changePassword(changepassword, "admin"); expect(verify[0]).toEqual(200); - expect(user.users[0].login).toEqual("admin"); - expect(user.users[0].bearer).toEqual(oldbearer); + expect(auth.usersgroups.users[0].login).toEqual("admin"); + expect(auth.usersgroups.users[0].bearer).toEqual(oldbearer); const verifyPassword = auth.verifyPassword( "admin", changepassword.newPassword @@ -617,18 +799,17 @@ describe("Authentification", () => { try { process.env.USER_ENCRYPT_SECRET = "test"; const auth = new Authentification(userDatabase); - const user = auth.store(auth.makeUser("admin", "admin")); + auth.addUser(auth.makeUser("admin", "admin")); const changepassword: ChangePasswordType = { - login: "admin", password: "adminxxxx", newPassword: "newpassword", newConfirmPassword: "newpassword", }; - const oldbearer = user.users[0].bearer; - const verify = auth.changePassword(changepassword); + const oldbearer = auth.usersgroups.users[0].bearer; + const verify = auth.changePassword(changepassword, "admin"); expect(verify[0]).toEqual(500); - expect(user.users[0].login).toEqual("admin"); - expect(user.users[0].bearer).toEqual(oldbearer); + expect(auth.usersgroups.users[0].login).toEqual("admin"); + expect(auth.usersgroups.users[0].bearer).toEqual(oldbearer); const verifyPassword = auth.verifyPassword("admin", "admin"); expect(verifyPassword[0]).toEqual(200); } catch (error: unknown) { @@ -641,18 +822,17 @@ describe("Authentification", () => { try { process.env.USER_ENCRYPT_SECRET = "test"; const auth = new Authentification(userDatabase); - const user = auth.store(auth.makeUser("admin", "admin")); + auth.addUser(auth.makeUser("admin", "admin")); const changepassword: ChangePasswordType = { - login: "admin", password: "", newPassword: "newpassword", newConfirmPassword: "newpassword", }; - const oldbearer = user.users[0].bearer; - const verify = auth.changePassword(changepassword); + const oldbearer = auth.usersgroups.users[0].bearer; + const verify = auth.changePassword(changepassword, "admin"); expect(verify[0]).toEqual(500); - expect(user.users[0].login).toEqual("admin"); - expect(user.users[0].bearer).toEqual(oldbearer); + expect(auth.usersgroups.users[0].login).toEqual("admin"); + expect(auth.usersgroups.users[0].bearer).toEqual(oldbearer); const verifyPassword = auth.verifyPassword("admin", "admin"); expect(verifyPassword[0]).toEqual(200); } catch (error: unknown) { @@ -665,18 +845,17 @@ describe("Authentification", () => { try { process.env.USER_ENCRYPT_SECRET = "test"; const auth = new Authentification(userDatabase); - const user = auth.store(auth.makeUser("admin", "admin")); + auth.addUser(auth.makeUser("admin", "admin")); const changepassword: ChangePasswordType = { - login: "admin", password: "admin", newPassword: "", newConfirmPassword: "newpassword", }; - const oldbearer = user.users[0].bearer; - const verify = auth.changePassword(changepassword); + const oldbearer = auth.usersgroups.users[0].bearer; + const verify = auth.changePassword(changepassword, "admin"); expect(verify[0]).toEqual(500); - expect(user.users[0].login).toEqual("admin"); - expect(user.users[0].bearer).toEqual(oldbearer); + expect(auth.usersgroups.users[0].login).toEqual("admin"); + expect(auth.usersgroups.users[0].bearer).toEqual(oldbearer); const verifyPassword = auth.verifyPassword("admin", "admin"); expect(verifyPassword[0]).toEqual(200); } catch (error: unknown) { @@ -689,18 +868,17 @@ describe("Authentification", () => { try { process.env.USER_ENCRYPT_SECRET = "test"; const auth = new Authentification(userDatabase); - const user = auth.store(auth.makeUser("admin", "admin")); + auth.addUser(auth.makeUser("admin", "admin")); const changepassword: ChangePasswordType = { - login: "admin", password: "admin", newPassword: "newpassword", newConfirmPassword: "", }; - const oldbearer = user.users[0].bearer; - const verify = auth.changePassword(changepassword); + const oldbearer = auth.usersgroups.users[0].bearer; + const verify = auth.changePassword(changepassword, "admin"); expect(verify[0]).toEqual(500); - expect(user.users[0].login).toEqual("admin"); - expect(user.users[0].bearer).toEqual(oldbearer); + expect(auth.usersgroups.users[0].login).toEqual("admin"); + expect(auth.usersgroups.users[0].bearer).toEqual(oldbearer); const verifyPassword = auth.verifyPassword("admin", "admin"); expect(verifyPassword[0]).toEqual(200); } catch (error: unknown) { @@ -713,18 +891,17 @@ describe("Authentification", () => { try { process.env.USER_ENCRYPT_SECRET = "test"; const auth = new Authentification(userDatabase); - const user = auth.store(auth.makeUser("admin", "admin")); + auth.addUser(auth.makeUser("admin", "admin")); const changepassword: ChangePasswordType = { - login: "admin", password: "admin", newPassword: "newpassword", newConfirmPassword: "xxxxxx", }; - const oldbearer = user.users[0].bearer; - const verify = auth.changePassword(changepassword); + const oldbearer = auth.usersgroups.users[0].bearer; + const verify = auth.changePassword(changepassword, "admin"); expect(verify[0]).toEqual(500); - expect(user.users[0].login).toEqual("admin"); - expect(user.users[0].bearer).toEqual(oldbearer); + expect(auth.usersgroups.users[0].login).toEqual("admin"); + expect(auth.usersgroups.users[0].bearer).toEqual(oldbearer); const verifyPassword = auth.verifyPassword("admin", "admin"); expect(verifyPassword[0]).toEqual(200); } catch (error: unknown) { @@ -737,21 +914,20 @@ describe("Authentification", () => { try { process.env.USER_ENCRYPT_SECRET = "test"; const auth = new Authentification(userDatabase); - let user = auth.store(auth.makeUser("admin", "admin")); - const oldbearer = user.users[0].bearer; - const oldpasswd = user.users[0].password; - const oldlogin = user.users[0].login; - const olduuid = user.users[0].uuid; - const verify = auth.changeBearer(user.users[0].login); + auth.addUser(auth.makeUser("admin", "admin")); + const oldbearer = auth.usersgroups.users[0].bearer; + const oldpasswd = auth.usersgroups.users[0].password; + const oldlogin = auth.usersgroups.users[0].login; + const olduuid = auth.usersgroups.users[0].uuid; + const verify = auth.changeBearer(auth.usersgroups.users[0].login); expect(verify[0]).toEqual(200); - //reload user - user = auth.users; - expect(user.users[0].bearer).not.toEqual(""); - expect(user.users[0].bearer).not.toEqual(oldbearer); + + expect(auth.usersgroups.users[0].bearer).not.toEqual(""); + expect(auth.usersgroups.users[0].bearer).not.toEqual(oldbearer); //check no changes elsewhere - expect(user.users[0].password).toEqual(oldpasswd); - expect(user.users[0].login).toEqual(oldlogin); - expect(user.users[0].uuid).toEqual(olduuid); + expect(auth.usersgroups.users[0].password).toEqual(oldpasswd); + expect(auth.usersgroups.users[0].login).toEqual(oldlogin); + expect(auth.usersgroups.users[0].uuid).toEqual(olduuid); } catch (error: unknown) { // unexpected error expect(error).not.toBeDefined(); @@ -818,6 +994,7 @@ describe("Authentification", () => { if (error) expect(error.toString()).toMatch(/DATABASE_ENCRYPT_SECRET/); } }); + test("dataDecrypt - decrypt data from database - secret not defined", () => { try { process.env.DATABASE_ENCRYPT_SECRET = ""; diff --git a/test/Database.test.ts b/test/Database.test.ts index d65bbeb..c5f4344 100644 --- a/test/Database.test.ts +++ b/test/Database.test.ts @@ -15,6 +15,7 @@ import { dbInsert, dbUpdateRecord, getDbInitJsonFileName, + isRecordInUserGroups, } from "../src/lib/Database"; import winston from "winston"; import Transport from "winston-transport"; @@ -24,7 +25,7 @@ interface LastErrorTransportOptions { level?: string; } -const check: UptodateForm = { +const control: UptodateForm = { name: "xxxxxx", urlProduction: "https://xxxxxxxx", scrapTypeProduction: "json", @@ -40,6 +41,7 @@ const check: UptodateForm = { isPause: false, compareResult: null, uuid: "", + groups: ["admin"], }; // Special transport - For testing: error has been flushed by winston @@ -68,9 +70,8 @@ const logger = winston.createLogger({ }); describe("Database", () => { - afterEach(() => { + beforeEach(() => { const file = "./test/data/database.json"; - process.env.DATABASE_ENCRYPT_SECRET = "mysecret"; if (existsSync(file)) unlinkSync(file); writeFileSync(file, JSON.stringify([]), "utf-8"); }); @@ -103,6 +104,8 @@ describe("Database", () => { test("dbCreate - db not exists", () => { const mytpath = "./test/data/database.json"; if (existsSync(mytpath)) unlinkSync(mytpath); + process.env.DATABASE_ENCRYPT_SECRET = "mysecret"; + process.env.USER_ENCRYPT_SECRET = "test"; dbCreate(mytpath) .then((res) => { expect(res).not.toEqual(""); @@ -116,6 +119,8 @@ describe("Database", () => { test("dbCreate - db allready exists", () => { const mytpath = "./test/data/database.json"; if (existsSync(mytpath)) unlinkSync(mytpath); + process.env.DATABASE_ENCRYPT_SECRET = "mysecret"; + process.env.USER_ENCRYPT_SECRET = "test"; dbCreate(mytpath) .then(() => { // try create twice @@ -131,6 +136,8 @@ describe("Database", () => { test("dbGetData - with path & no records", () => { const mytpath = "./test/samples/database-empty.json"; + process.env.DATABASE_ENCRYPT_SECRET = "mysecret"; + process.env.USER_ENCRYPT_SECRET = "test"; dbGetData(mytpath) .then((res) => { expect(res).toEqual([]); @@ -142,6 +149,8 @@ describe("Database", () => { test("dbGetData - malformed object", () => { const mytpath = "./test/samples/database-malformed-object.json"; + process.env.DATABASE_ENCRYPT_SECRET = "mysecret"; + process.env.USER_ENCRYPT_SECRET = "test"; dbGetData(mytpath) .then(() => { //unexpected @@ -155,6 +164,8 @@ describe("Database", () => { test("dbGetData - malformed empty file non json", () => { const mytpath = "./test/samples/database-nocontent.json"; + process.env.DATABASE_ENCRYPT_SECRET = "mysecret"; + process.env.USER_ENCRYPT_SECRET = "test"; dbGetData(mytpath) .then((res) => { // unexpected @@ -168,8 +179,9 @@ describe("Database", () => { test("dbInsert - without uuid", () => { const db: UptodateForm[] = []; - - dbInsert(db, check) + process.env.DATABASE_ENCRYPT_SECRET = "mysecret"; + process.env.USER_ENCRYPT_SECRET = "test"; + dbInsert(db, control) .then((res) => { expect(res).toBeDefined(); expect(typeof res).toEqual("string"); @@ -182,7 +194,7 @@ describe("Database", () => { test("dbInsert - with uuid", () => { const db: UptodateForm[] = []; - dbInsert(db, { ...check, uuid: "xxxx" }) + dbInsert(db, { ...control, uuid: "xxxx" }) .then((res) => { expect(res).not.toBeDefined(); }) @@ -194,7 +206,7 @@ describe("Database", () => { test("dbCommit", () => { const db: UptodateForm[] = []; const file = "./test/data/database.json"; - dbInsert(db, check).then((uuid: string) => { + dbInsert(db, control).then((uuid: string) => { dbCommit(file, db) .then((res) => { expect(res).toBeNull(); @@ -211,29 +223,32 @@ describe("Database", () => { }); }); - test("dbCommit - db is read only", () => { + test("dbCommit - db is read only", async () => { const db: UptodateForm[] = []; const file = "./test/samples/database-readonly.json"; // to be sure chmodSync(file, "400"); - dbInsert(db, check).then(() => { + await dbInsert(db, control).then(() => { dbCommit(file, db) .then(() => { // unexpected - file is read only expect(false).toBeTruthy(); }) .catch((error: Error) => { - expect(error).toBeDefined(); + //expect(error).toBeDefined(); + // console.log(error); expect(error.toString()).toMatch(/EACCES: permission denied/); }); }); }); - test("dbGetRecord - uuid exists", () => { + test("dbGetRecord - uuid exists - admin", () => { const db: UptodateForm[] = []; - dbInsert(db, check) + process.env.DATABASE_ENCRYPT_SECRET = "mysecret"; + process.env.USER_ENCRYPT_SECRET = "test"; + dbInsert(db, control) .then((res) => { - const rec = dbGetRecord(db, res, logger); + const rec = dbGetRecord(db, res, [], true, logger); if (!Array.isArray(rec)) { expect(rec?.uuid).toEqual(res); } else { @@ -245,12 +260,14 @@ describe("Database", () => { }); }); - test("dbGetRecord - uuid exists impossible to decrypt secrets", () => { + test("dbGetRecord - uuid exists impossible to decrypt secrets - admin", () => { const db: UptodateForm[] = []; - dbInsert(db, check) + process.env.DATABASE_ENCRYPT_SECRET = "mysecret"; + process.env.USER_ENCRYPT_SECRET = "test"; + dbInsert(db, control) .then((res) => { - const newdb = [{ ...check, uuid: res }]; - const rec = dbGetRecord(newdb, res, logger); + const newdb = [{ ...control, uuid: res }]; + const rec = dbGetRecord(newdb, res, [], true, logger); const lastErrorWinston = (logger.transports[0] as LastErrorTransport) .lastError; expect(lastErrorWinston).toBeDefined(); @@ -269,11 +286,13 @@ describe("Database", () => { }); }); - test("dbGetRecord - uuid not exists", () => { + test("dbGetRecord - uuid not exists - admin", () => { const db: UptodateForm[] = []; - dbInsert(db, check) + process.env.DATABASE_ENCRYPT_SECRET = "mysecret"; + process.env.USER_ENCRYPT_SECRET = "test"; + dbInsert(db, control) .then(() => { - const rec = dbGetRecord(db, "xxxx", logger); + const rec = dbGetRecord(db, "xxxx", [], true, logger); expect(rec).toBeNull(); }) .catch((error) => { @@ -281,11 +300,13 @@ describe("Database", () => { }); }); - test("dbGetRecord - uuid not provided", () => { + test("dbGetRecord - uuid not provided - admin", () => { const db: UptodateForm[] = []; - dbInsert(db, check) + process.env.DATABASE_ENCRYPT_SECRET = "mysecret"; + process.env.USER_ENCRYPT_SECRET = "test"; + dbInsert(db, control) .then(() => { - const rec = dbGetRecord(db, "", logger); + const rec = dbGetRecord(db, "", [], true, logger); expect(rec).toBeNull(); }) .catch((error) => { @@ -293,11 +314,13 @@ describe("Database", () => { }); }); - test("dbGetRecord - get all records", () => { + test("dbGetRecord - get all records - admin", () => { const db: UptodateForm[] = []; - dbInsert(db, check) + process.env.DATABASE_ENCRYPT_SECRET = "mysecret"; + process.env.USER_ENCRYPT_SECRET = "test"; + dbInsert(db, control) .then(() => { - const rec = dbGetRecord(db, "all", logger); + const rec = dbGetRecord(db, "all", [], true, logger); expect(Array.isArray(rec)).toBeTruthy(); expect(Array.isArray(rec) && rec.length).toEqual(1); }) @@ -306,11 +329,46 @@ describe("Database", () => { }); }); + test("dbGetRecord - user is not admin and member of group which is not authorized to get this control", () => { + const db: UptodateForm[] = []; + process.env.DATABASE_ENCRYPT_SECRET = "mysecret"; + process.env.USER_ENCRYPT_SECRET = "test"; + // check groups is 'admin' + dbInsert(db, control) + .then((uuid: string) => { + //user wants all and member of xxxx + const rec = dbGetRecord(db, uuid, ["xxxx"], false, logger); + expect(rec).toBeNull(); + }) + .catch((error) => { + expect(error).not.toBeDefined(); + }); + }); + + test("dbGetRecord - user is not admin and member of group which is not authorized to get ALL controls", () => { + const db: UptodateForm[] = []; + process.env.DATABASE_ENCRYPT_SECRET = "mysecret"; + process.env.USER_ENCRYPT_SECRET = "test"; + // check groups is 'admin' + dbInsert(db, control) + .then(() => { + //user wants all and member of xxxx + const rec = dbGetRecord(db, "all", ["xxxx"], false, logger); + expect(Array.isArray(rec)).toBeTruthy(); + if (Array.isArray(rec)) expect(rec.length).toEqual(0); + }) + .catch((error) => { + expect(error).not.toBeDefined(); + }); + }); + test("dbDeleteRecord - existing record", () => { const db: UptodateForm[] = []; - dbInsert(db, check) + process.env.DATABASE_ENCRYPT_SECRET = "mysecret"; + process.env.USER_ENCRYPT_SECRET = "test"; + dbInsert(db, control) .then(() => { - dbInsert(db, check).then((uuid) => { + dbInsert(db, control).then((uuid) => { const rdel = dbDeleteRecord(db, uuid); expect(rdel).toEqual(uuid); expect(Array.isArray(db) && db.length).toEqual(1); @@ -323,9 +381,11 @@ describe("Database", () => { test("dbDeleteRecord - non existing record", () => { const db: UptodateForm[] = []; - dbInsert(db, check) + process.env.DATABASE_ENCRYPT_SECRET = "mysecret"; + process.env.USER_ENCRYPT_SECRET = "test"; + dbInsert(db, control) .then(() => { - dbInsert(db, check).then((uuid) => { + dbInsert(db, control).then((uuid) => { const rdel = dbDeleteRecord(db, `${uuid}xxx`); expect(rdel).toEqual(""); expect(Array.isArray(db) && db.length).toEqual(2); @@ -338,9 +398,11 @@ describe("Database", () => { test("dbDeleteRecord - empty uuid", () => { const db: UptodateForm[] = []; - dbInsert(db, check) + process.env.DATABASE_ENCRYPT_SECRET = "mysecret"; + process.env.USER_ENCRYPT_SECRET = "test"; + dbInsert(db, control) .then(() => { - dbInsert(db, check).then(() => { + dbInsert(db, control).then(() => { const rdel = dbDeleteRecord(db, ""); expect(rdel).toEqual(""); expect(Array.isArray(db) && db.length).toEqual(2); @@ -351,18 +413,19 @@ describe("Database", () => { }); }); - test("dbUpdateRecord - with uuid", () => { + test("dbUpdateRecord - with uuid and admin", () => { const db: UptodateForm[] = []; - dbInsert(db, check) + process.env.DATABASE_ENCRYPT_SECRET = "mysecret"; + process.env.USER_ENCRYPT_SECRET = "test"; + dbInsert(db, control) .then(() => { - dbInsert(db, check).then((uuid) => { - // const upd = { ...check, uuid: uuid }; - const upd = { ...check, uuid }; + dbInsert(db, control).then((uuid) => { + const upd = { ...control, uuid }; upd.urlProduction = "test"; const rupd = dbUpdateRecord(db, upd); expect(rupd).toEqual(uuid); expect(Array.isArray(db) && db.length).toEqual(2); - const verif = dbGetRecord(db, uuid, logger); + const verif = dbGetRecord(db, uuid, [], true, logger); expect(verif).not.toBeNull(); expect(verif && !Array.isArray(verif) && verif.urlProduction).toEqual( "test" @@ -376,11 +439,12 @@ describe("Database", () => { test("dbUpdateRecord - uuid not found", () => { const db: UptodateForm[] = []; - dbInsert(db, check) + process.env.DATABASE_ENCRYPT_SECRET = "mysecret"; + process.env.USER_ENCRYPT_SECRET = "test"; + dbInsert(db, control) .then(() => { - dbInsert(db, check).then(() => { - // const upd = { ...check, uuid: uuid }; - const upd = { ...check, uuid: "xxxx" }; + dbInsert(db, control).then(() => { + const upd = { ...control, uuid: "xxxx" }; upd.urlProduction = "test"; const rupd = dbUpdateRecord(db, upd); expect(rupd).toEqual(""); @@ -394,11 +458,12 @@ describe("Database", () => { test("dbUpdateRecord - uuid is not set", () => { const db: UptodateForm[] = []; - dbInsert(db, check) + process.env.DATABASE_ENCRYPT_SECRET = "mysecret"; + process.env.USER_ENCRYPT_SECRET = "test"; + dbInsert(db, control) .then(() => { - dbInsert(db, check).then(() => { - // const upd = { ...check, uuid: uuid }; - const upd = { ...check }; + dbInsert(db, control).then(() => { + const upd = { ...control }; upd.urlProduction = "test"; const rupd = dbUpdateRecord(db, upd); expect(rupd).toEqual(""); @@ -409,4 +474,27 @@ describe("Database", () => { expect(error).not.toBeDefined(); }); }); + + test("isRecordInUserGroups", () => { + const db: UptodateForm[] = []; + process.env.DATABASE_ENCRYPT_SECRET = "mysecret"; + process.env.USER_ENCRYPT_SECRET = "test"; + dbInsert(db, control) + .then(() => { + expect(isRecordInUserGroups(control, ["admin"])).toBeTruthy(); + expect(isRecordInUserGroups(control, ["test"])).toBeFalsy(); + expect(isRecordInUserGroups(control, [])).toBeFalsy(); + const upd = { ...control }; + upd.urlProduction = "test"; + upd.groups = ["admin", "test"]; + dbInsert(db, control).then(() => { + expect(isRecordInUserGroups(upd, ["admin", "test"])).toBeTruthy(); + expect(isRecordInUserGroups(upd, ["xxx", "yyy"])).toBeFalsy(); + expect(isRecordInUserGroups(upd, [])).toBeFalsy(); + }); + }) + .catch((error) => { + expect(error).not.toBeDefined(); + }); + }); }); diff --git a/test/Features.test.ts b/test/Features.test.ts index 0d71dd4..b864382 100644 --- a/test/Features.test.ts +++ b/test/Features.test.ts @@ -111,6 +111,7 @@ describe("Features", () => { httpMethodCronJobMonitoring: "GET", isPause: true, compareResult: null, + groups: [], }) .then((result) => { return result; diff --git a/test/Groups.test.ts b/test/Groups.test.ts new file mode 100644 index 0000000..c1a82a0 --- /dev/null +++ b/test/Groups.test.ts @@ -0,0 +1,346 @@ +import { existsSync, rmSync } from "fs"; +import { Authentification } from "../src/lib/Authentification"; +import { SessionExt } from "../src/ServerTypes"; +import { Request } from "express"; + +const userDatabase = `${__dirname}/data/userDatabase.json`; + +describe("Groups", () => { + beforeEach(() => { + // delete userdatabase + if (existsSync(userDatabase)) rmSync(userDatabase); + process.env.DATABASE_ENCRYPT_SECRET = "mysecret"; + process.env.USER_ENCRYPT_SECRET = "test"; + }); + + test("Add user to Group - admin", () => { + const auth = new Authentification(userDatabase); + const user = auth.makeUser("admin", "admin"); + auth.addUser(user); + expect(auth.addGroupMember("admin", user.uuid)).toBeTruthy(); + expect(auth.usersgroups.groups.admin.includes(user.uuid)).toBeTruthy(); + }); + + test("Add user to non-existent group", () => { + const auth = new Authentification(userDatabase); + const user = auth.makeUser("admin", "admin"); + auth.addUser(user); + expect(auth.addGroupMember("test", user.uuid)).toBeTruthy(); + expect(auth.usersgroups.groups.test.includes(user.uuid)).toBeTruthy(); + }); + + test("Add empty user to admin group", () => { + const auth = new Authentification(userDatabase); + const user = auth.makeUser("admin", "admin"); + auth.addUser(user); + expect(auth.addGroupMember("admin", "")).toBeFalsy(); + expect(auth.usersgroups.groups.admin.includes(user.uuid)).toBeFalsy(); + }); + + test("isMemberOfGroup - admin", () => { + const auth = new Authentification(userDatabase); + const user = auth.makeUser("admin", "admin"); + auth.addUser(user); + expect(auth.addGroupMember("admin", user.uuid)).toBeTruthy(); + expect(auth.isMemberOfGroup("admin", user.uuid)).toBeTruthy(); + }); + + test("isMemberOfGroup - group doesn't exist", () => { + const auth = new Authentification(userDatabase); + const user = auth.makeUser("admin", "admin"); + auth.addUser(user); + auth.addGroupMember("admin", user.uuid); + expect(auth.addGroupMember("admin", user.uuid)).toBeTruthy(); + expect(auth.isMemberOfGroup("test", user.uuid)).toBeFalsy(); + }); + + test("isMemberOfGroup - user is not admin", () => { + const auth = new Authentification(userDatabase); + let user = auth.makeUser("admin", "admin"); + auth.addUser(user); + expect(auth.addGroupMember("admin", user.uuid)).toBeTruthy(); + user = auth.makeUser("test", "test"); + auth.addUser(user); + expect(auth.addGroupMember("test", user.uuid)).toBeTruthy(); + expect(auth.isMemberOfGroup("admin", user.uuid)).toBeFalsy(); + }); + + test("getGroups - with groups set and admin user", () => { + const auth = new Authentification(userDatabase); + const adminuser = auth.makeUser("admin", "admin"); + auth.addUser(adminuser); + auth.addGroupMember("admin", adminuser.uuid); + const user = auth.makeUser("test", "test"); + auth.addUser(user); + auth.addGroupMember("test", user.uuid); + auth.isMemberOfGroup("admin", user.uuid); + const req = { + body: {}, + app: { + get: (key: string) => { + if (key === "AUTH") return auth; + }, + }, + } as Request; + req.session = { + user: { + login: adminuser.login, + bearer: adminuser.bearer, + uuid: adminuser.uuid, + }, + } as SessionExt; + const groups = auth.getGroups(req); + expect(groups.length).toEqual(2); + expect(groups.includes("admin")).toBeTruthy(); + expect(groups.includes("test")).toBeTruthy(); + }); + + test("getGroups - with groups set and normal user", () => { + const auth = new Authentification(userDatabase); + const adminuser = auth.makeUser("admin", "admin"); + auth.addUser(adminuser); + auth.addGroupMember("admin", adminuser.uuid); + const user = auth.makeUser("test", "test"); + auth.addUser(user); + auth.addGroupMember("test", user.uuid); + auth.isMemberOfGroup("admin", user.uuid); + const req = { + body: {}, + app: { + get: (key: string) => { + if (key === "AUTH") return auth; + }, + }, + } as Request; + req.session = { + user: { + login: user.login, + bearer: user.bearer, + uuid: user.uuid, + }, + } as SessionExt; + const groups = auth.getGroups(req); + expect(groups.length).toEqual(1); + expect(groups.includes("test")).toBeTruthy(); + }); + + test("removeUserFromGroups - user set in multi group then removed from all", () => { + const auth = new Authentification(userDatabase); + let user = auth.makeUser("admin", "admin"); + auth.addUser(user); + auth.addGroupMember("admin", user.uuid); + user = auth.makeUser("test", "test"); + auth.addUser(user); + auth.addGroupMember("test", user.uuid); + auth.addGroupMember("admin", user.uuid); + expect(auth.usersgroups.groups.admin.includes(user.uuid)).toBeTruthy(); + expect(auth.usersgroups.groups.test.includes(user.uuid)).toBeTruthy(); + auth.removeUserFromGroups(user.uuid); + expect(auth.usersgroups.groups.admin.includes(user.uuid)).toBeFalsy(); + expect(auth.usersgroups.groups.test.includes(user.uuid)).toBeFalsy(); + }); + + test("removeUserFromGroups - user not found", () => { + const auth = new Authentification(userDatabase); + let user = auth.makeUser("admin", "admin"); + auth.addUser(user); + auth.addGroupMember("admin", user.uuid); + user = auth.makeUser("test", "test"); + auth.addUser(user); + auth.addGroupMember("test", user.uuid); + auth.addGroupMember("admin", user.uuid); + expect(auth.usersgroups.groups.admin.includes(user.uuid)).toBeTruthy(); + expect(auth.usersgroups.groups.test.includes(user.uuid)).toBeTruthy(); + auth.removeUserFromGroups("xxxx"); + expect(auth.usersgroups.groups.admin.includes(user.uuid)).toBeTruthy(); + expect(auth.usersgroups.groups.test.includes(user.uuid)).toBeTruthy(); + }); + + test("getUserGroups - existent user", () => { + const auth = new Authentification(userDatabase); + let user = auth.makeUser("admin", "admin"); + auth.addUser(user); + auth.addGroupMember("admin", user.uuid); + user = auth.makeUser("test", "test"); + auth.addUser(user); + auth.addGroupMember("test", user.uuid); + auth.addGroupMember("admin", user.uuid); + expect(auth.getUserGroups(user.uuid).length).toEqual(2); + expect(auth.getUserGroups(user.uuid).includes("test")).toBeTruthy(); + expect(auth.getUserGroups(user.uuid).includes("admin")).toBeTruthy(); + }); + + test("getUserGroups - non-existent user", () => { + const auth = new Authentification(userDatabase); + let user = auth.makeUser("admin", "admin"); + auth.addUser(user); + auth.addGroupMember("admin", user.uuid); + user = auth.makeUser("test", "test"); + auth.addUser(user); + auth.addGroupMember("test", user.uuid); + auth.addGroupMember("admin", user.uuid); + expect(auth.getUserGroups("xxx").length).toEqual(0); + }); + + test("cleanGroups - cleaning, without empty groups", () => { + // adding users with groups + const auth = new Authentification(userDatabase); + + const admin = auth.makeUser("admin", "admin"); + auth.addUser(admin); + auth.addGroupMember("admin", admin.uuid); + + const test = auth.makeUser("test", "test"); + auth.addUser(test); + auth.addGroupMember("test", test.uuid); + + const test1 = auth.makeUser("test1", "test"); + auth.addUser(test1); + auth.addGroupMember("test1", test1.uuid); + + expect(auth.getUserGroups(admin.uuid).length).toEqual(1); + expect(auth.getUserGroups(test.uuid).length).toEqual(1); + expect(auth.getUserGroups(test1.uuid).length).toEqual(1); + // 3 groups + expect(Object.getOwnPropertyNames(auth.usersgroups.groups).length).toEqual( + 3 + ); + auth.cleanGroups(); + expect(Object.getOwnPropertyNames(auth.usersgroups.groups).length).toEqual( + 3 + ); + expect(auth.usersgroups.groups.admin).toBeDefined(); + expect(auth.usersgroups.groups.test).toBeDefined(); + expect(auth.usersgroups.groups.test1).toBeDefined(); + }); + + test("cleanGroups - I empty 1 group and clean", () => { + // adding users with groups + const auth = new Authentification(userDatabase); + + const admin = auth.makeUser("admin", "admin"); + auth.addUser(admin); + auth.addGroupMember("admin", admin.uuid); + + const test = auth.makeUser("test", "test"); + auth.addUser(test); + auth.addGroupMember("test", test.uuid); + + const test1 = auth.makeUser("test1", "test"); + auth.addUser(test1); + auth.addGroupMember("test1", test1.uuid); + + expect(auth.getUserGroups(admin.uuid).length).toEqual(1); + expect(auth.getUserGroups(test.uuid).length).toEqual(1); + expect(auth.getUserGroups(test1.uuid).length).toEqual(1); + // remove test from all groups + auth.removeUserFromGroups(test.uuid); + expect(auth.getUserGroups(test.uuid).length).toEqual(0); + // 3 groups + expect(Object.getOwnPropertyNames(auth.usersgroups.groups).length).toEqual( + 3 + ); + auth.cleanGroups(); + expect(Object.getOwnPropertyNames(auth.usersgroups.groups).length).toEqual( + 2 + ); + expect(auth.usersgroups.groups.admin).toBeDefined(); + expect(auth.usersgroups.groups.test).not.toBeDefined(); + expect(auth.usersgroups.groups.test1).toBeDefined(); + }); + + test("isAllowedForObject - admin user", () => { + const auth = new Authentification(userDatabase); + + const admin = auth.makeUser("admin", "admin"); + auth.addUser(admin); + auth.addGroupMember("admin", admin.uuid); + + const test = auth.makeUser("test", "test"); + auth.addUser(test); + auth.addGroupMember("test", test.uuid); + + const req = { + body: {}, + app: { + get: (key: string) => { + if (key === "AUTH") return auth; + }, + }, + } as Request; + req.session = { + user: { + login: admin.login, + bearer: admin.bearer, + uuid: admin.uuid, + }, + } as SessionExt; + + expect(auth.isAllowedForObject(req, ["xxx", "yyy"])).toBeTruthy(); + }); + + test("isAllowedForObject - NON admin user", () => { + const auth = new Authentification(userDatabase); + + const admin = auth.makeUser("admin", "admin"); + auth.addUser(admin); + auth.addGroupMember("admin", admin.uuid); + + const test = auth.makeUser("test", "test"); + auth.addUser(test); + auth.addGroupMember("test", test.uuid); + + const req = { + body: {}, + app: { + get: (key: string) => { + if (key === "AUTH") return auth; + }, + }, + } as Request; + req.session = { + user: { + login: test.login, + bearer: test.bearer, + uuid: test.uuid, + }, + } as SessionExt; + + // test is not member of xxx + expect(auth.isAllowedForObject(req, ["xxx"])).toBeFalsy(); + // test is member of test + expect(auth.isAllowedForObject(req, ["test"])).toBeTruthy(); + }); + + test("isAllowedForObject - user without group", () => { + const auth = new Authentification(userDatabase); + + const admin = auth.makeUser("admin", "admin"); + auth.addUser(admin); + auth.addGroupMember("admin", admin.uuid); + + const test = auth.makeUser("test", "test"); + auth.addUser(test); + + const req = { + body: {}, + app: { + get: (key: string) => { + if (key === "AUTH") return auth; + }, + }, + } as Request; + req.session = { + user: { + login: test.login, + bearer: test.bearer, + uuid: test.uuid, + }, + } as SessionExt; + + // test is not member of xxx + expect(auth.isAllowedForObject(req, ["xxx"])).toBeFalsy(); + // test is member of test + expect(auth.isAllowedForObject(req, ["test"])).toBeFalsy(); + }); +});