From 3090130d9e7ef56e82db73342aa4e6278665dc45 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Dec 2024 23:10:52 +0000 Subject: [PATCH 01/25] build(deps): bump axios from 1.7.7 to 1.7.9 Bumps [axios](https://github.com/axios/axios) from 1.7.7 to 1.7.9. - [Release notes](https://github.com/axios/axios/releases) - [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md) - [Commits](https://github.com/axios/axios/compare/v1.7.7...v1.7.9) --- updated-dependencies: - dependency-name: axios dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- package-lock.json | 14 +++++++------- package.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0ac0c2d7..e600c277 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "@user-office-software/duo-logger": "^2.2.1", "@user-office-software/duo-message-broker": "^1.7.0", - "axios": "^1.7.7", + "axios": "^1.7.9", "dotenv": "^16.4.5", "envalid": "^8.0.0", "express": "^4.21.1", @@ -2309,9 +2309,9 @@ } }, "node_modules/axios": { - "version": "1.7.7", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", - "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", + "version": "1.7.9", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz", + "integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==", "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", @@ -10532,9 +10532,9 @@ } }, "axios": { - "version": "1.7.7", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", - "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", + "version": "1.7.9", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz", + "integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==", "requires": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", diff --git a/package.json b/package.json index f75a0a24..ef2dfc4f 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "dependencies": { "@user-office-software/duo-logger": "^2.2.1", "@user-office-software/duo-message-broker": "^1.7.0", - "axios": "^1.7.7", + "axios": "^1.7.9", "dotenv": "^16.4.5", "envalid": "^8.0.0", "express": "^4.21.1", From d89bb74be200056fbd9b17519125335b88c0071e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Dec 2024 23:16:36 +0000 Subject: [PATCH 02/25] build(deps): bump path-to-regexp and express Bumps [path-to-regexp](https://github.com/pillarjs/path-to-regexp) to 0.1.12 and updates ancestor dependency [express](https://github.com/expressjs/express). These dependencies need to be updated together. Updates `path-to-regexp` from 0.1.10 to 0.1.12 - [Release notes](https://github.com/pillarjs/path-to-regexp/releases) - [Changelog](https://github.com/pillarjs/path-to-regexp/blob/master/History.md) - [Commits](https://github.com/pillarjs/path-to-regexp/compare/v0.1.10...v0.1.12) Updates `express` from 4.21.1 to 4.21.2 - [Release notes](https://github.com/expressjs/express/releases) - [Changelog](https://github.com/expressjs/express/blob/4.21.2/History.md) - [Commits](https://github.com/expressjs/express/compare/4.21.1...4.21.2) --- updated-dependencies: - dependency-name: path-to-regexp dependency-type: indirect - dependency-name: express dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- package-lock.json | 34 +++++++++++++++++++--------------- package.json | 2 +- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/package-lock.json b/package-lock.json index e600c277..cb8305d7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,7 @@ "axios": "^1.7.9", "dotenv": "^16.4.5", "envalid": "^8.0.0", - "express": "^4.21.1", + "express": "^4.21.2", "kafkajs": "^2.2.3", "knex": "^3.1.0", "matrix-js-sdk": "24.1.0", @@ -3871,9 +3871,9 @@ } }, "node_modules/express": { - "version": "4.21.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", - "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", @@ -3894,7 +3894,7 @@ "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.10", + "path-to-regexp": "0.1.12", "proxy-addr": "~2.0.7", "qs": "6.13.0", "range-parser": "~1.2.1", @@ -3909,6 +3909,10 @@ }, "engines": { "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/express/node_modules/debug": { @@ -6817,9 +6821,9 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, "node_modules/path-to-regexp": { - "version": "0.1.10", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", - "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==" + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==" }, "node_modules/path-type": { "version": "4.0.0", @@ -11638,9 +11642,9 @@ } }, "express": { - "version": "4.21.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", - "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", "requires": { "accepts": "~1.3.8", "array-flatten": "1.1.1", @@ -11661,7 +11665,7 @@ "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.10", + "path-to-regexp": "0.1.12", "proxy-addr": "~2.0.7", "qs": "6.13.0", "range-parser": "~1.2.1", @@ -13719,9 +13723,9 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, "path-to-regexp": { - "version": "0.1.10", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", - "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==" + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==" }, "path-type": { "version": "4.0.0", diff --git a/package.json b/package.json index ef2dfc4f..686f9ba7 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ "axios": "^1.7.9", "dotenv": "^16.4.5", "envalid": "^8.0.0", - "express": "^4.21.1", + "express": "^4.21.2", "kafkajs": "^2.2.3", "knex": "^3.1.0", "matrix-js-sdk": "24.1.0", From e4d13f15f1d9b2dd3a1ebc9fb0341e699f1110bd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Dec 2024 23:20:36 +0000 Subject: [PATCH 03/25] build(deps-dev): bump @types/jest from 29.5.13 to 29.5.14 Bumps [@types/jest](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/jest) from 29.5.13 to 29.5.14. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/jest) --- updated-dependencies: - dependency-name: "@types/jest" dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- package-lock.json | 14 +++++++------- package.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index cb8305d7..58175882 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,7 +26,7 @@ }, "devDependencies": { "@types/express": "^4.17.21", - "@types/jest": "^29.5.13", + "@types/jest": "^29.5.14", "@types/knex": "^0.16.1", "@types/node": "^22.9.0", "@types/pg": "^8.11.10", @@ -1556,9 +1556,9 @@ } }, "node_modules/@types/jest": { - "version": "29.5.13", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.13.tgz", - "integrity": "sha512-wd+MVEZCHt23V0/L642O5APvspWply/rGY5BcW4SUETo2UzPU3Z26qr8jC2qxpimI2jjx9h7+2cj2FwIr01bXg==", + "version": "29.5.14", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz", + "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==", "dev": true, "dependencies": { "expect": "^29.0.0", @@ -10010,9 +10010,9 @@ } }, "@types/jest": { - "version": "29.5.13", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.13.tgz", - "integrity": "sha512-wd+MVEZCHt23V0/L642O5APvspWply/rGY5BcW4SUETo2UzPU3Z26qr8jC2qxpimI2jjx9h7+2cj2FwIr01bXg==", + "version": "29.5.14", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz", + "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==", "dev": true, "requires": { "expect": "^29.0.0", diff --git a/package.json b/package.json index 686f9ba7..369c344c 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "license": "MIT", "devDependencies": { "@types/express": "^4.17.21", - "@types/jest": "^29.5.13", + "@types/jest": "^29.5.14", "@types/knex": "^0.16.1", "@types/node": "^22.9.0", "@types/pg": "^8.11.10", From a109597654633d36514f4c8a6ec7768b7cae2e8d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 31 Dec 2024 00:00:17 +0000 Subject: [PATCH 04/25] build(deps-dev): bump typescript from 5.6.3 to 5.7.2 Bumps [typescript](https://github.com/microsoft/TypeScript) from 5.6.3 to 5.7.2. - [Release notes](https://github.com/microsoft/TypeScript/releases) - [Changelog](https://github.com/microsoft/TypeScript/blob/main/azure-pipelines.release.yml) - [Commits](https://github.com/microsoft/TypeScript/compare/v5.6.3...v5.7.2) --- updated-dependencies: - dependency-name: typescript dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- package-lock.json | 14 +++++++------- package.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index 58175882..ecac3c1d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -43,7 +43,7 @@ "lint-staged": "^15.2.10", "prettier": "3.3.3", "ts-jest": "^29.2.5", - "typescript": "^5.6.3" + "typescript": "^5.7.2" }, "engines": { "node": ">=18.0.0", @@ -8501,9 +8501,9 @@ } }, "node_modules/typescript": { - "version": "5.6.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", - "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", + "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -14904,9 +14904,9 @@ } }, "typescript": { - "version": "5.6.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", - "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==" + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", + "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==" }, "unbox-primitive": { "version": "1.0.2", diff --git a/package.json b/package.json index 369c344c..ba0df1ed 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "lint-staged": "^15.2.10", "prettier": "3.3.3", "ts-jest": "^29.2.5", - "typescript": "^5.6.3" + "typescript": "^5.7.2" }, "dependencies": { "@user-office-software/duo-logger": "^2.2.1", From e48cbca038c0c1ef71614f1c36a916609b7a4e32 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 20 Jan 2025 23:50:44 +0000 Subject: [PATCH 05/25] build(deps-dev): bump eslint-plugin-jest from 28.8.3 to 28.11.0 Bumps [eslint-plugin-jest](https://github.com/jest-community/eslint-plugin-jest) from 28.8.3 to 28.11.0. - [Release notes](https://github.com/jest-community/eslint-plugin-jest/releases) - [Changelog](https://github.com/jest-community/eslint-plugin-jest/blob/main/CHANGELOG.md) - [Commits](https://github.com/jest-community/eslint-plugin-jest/compare/v28.8.3...v28.11.0) --- updated-dependencies: - dependency-name: eslint-plugin-jest dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- package-lock.json | 15 ++++++++------- package.json | 2 +- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index ecac3c1d..01d55ba4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -35,7 +35,7 @@ "eslint": "^8.57.1", "eslint-config-prettier": "^9.1.0", "eslint-plugin-import": "^2.31.0", - "eslint-plugin-jest": "^28.8.3", + "eslint-plugin-jest": "^28.11.0", "eslint-plugin-prettier": "^5.2.1", "eslint-plugin-unused-imports": "^3.2.0", "husky": "^9.1.6", @@ -3608,10 +3608,11 @@ } }, "node_modules/eslint-plugin-jest": { - "version": "28.8.3", - "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-28.8.3.tgz", - "integrity": "sha512-HIQ3t9hASLKm2IhIOqnu+ifw7uLZkIlR7RYNv7fMcEi/p0CIiJmfriStQS2LDkgtY4nyLbIZAD+JL347Yc2ETQ==", + "version": "28.11.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-28.11.0.tgz", + "integrity": "sha512-QAfipLcNCWLVocVbZW8GimKn5p5iiMcgGbRzz8z/P5q7xw+cNEpYqyzFMtIF/ZgF2HLOyy+dYBut+DoYolvqig==", "dev": true, + "license": "MIT", "dependencies": { "@typescript-eslint/utils": "^6.0.0 || ^7.0.0 || ^8.0.0" }, @@ -11498,9 +11499,9 @@ } }, "eslint-plugin-jest": { - "version": "28.8.3", - "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-28.8.3.tgz", - "integrity": "sha512-HIQ3t9hASLKm2IhIOqnu+ifw7uLZkIlR7RYNv7fMcEi/p0CIiJmfriStQS2LDkgtY4nyLbIZAD+JL347Yc2ETQ==", + "version": "28.11.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-28.11.0.tgz", + "integrity": "sha512-QAfipLcNCWLVocVbZW8GimKn5p5iiMcgGbRzz8z/P5q7xw+cNEpYqyzFMtIF/ZgF2HLOyy+dYBut+DoYolvqig==", "dev": true, "requires": { "@typescript-eslint/utils": "^6.0.0 || ^7.0.0 || ^8.0.0" diff --git a/package.json b/package.json index ba0df1ed..b236487b 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "eslint": "^8.57.1", "eslint-config-prettier": "^9.1.0", "eslint-plugin-import": "^2.31.0", - "eslint-plugin-jest": "^28.8.3", + "eslint-plugin-jest": "^28.11.0", "eslint-plugin-prettier": "^5.2.1", "eslint-plugin-unused-imports": "^3.2.0", "husky": "^9.1.6", From 52ab7c6b16a84ddc8d5b96fbee0b25bd786c9f51 Mon Sep 17 00:00:00 2001 From: Jay Date: Thu, 27 Feb 2025 16:04:11 +0100 Subject: [PATCH 06/25] fix: improve error handling in checkProposalExists function (#505) --- .../proposalFoldersCreation.spec.ts | 2 +- .../consumerCallbacks/upsertProposalInScicat.ts | 13 ++++++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/queue/consumers/scicat/scicatProposal/consumerCallbacks/proposalFoldersCreation.spec.ts b/src/queue/consumers/scicat/scicatProposal/consumerCallbacks/proposalFoldersCreation.spec.ts index 24a1936f..28ade264 100644 --- a/src/queue/consumers/scicat/scicatProposal/consumerCallbacks/proposalFoldersCreation.spec.ts +++ b/src/queue/consumers/scicat/scicatProposal/consumerCallbacks/proposalFoldersCreation.spec.ts @@ -85,7 +85,7 @@ describe('proposalFoldersCreation', () => { expect(exec).toHaveBeenCalledTimes(1); expect(exec).toHaveBeenCalledWith( - 'command shortcode 2024 shortCode group_prefix_shortCode test.proposer@email.com test.member@email.com', + 'command shortcode 2025 shortCode group_prefix_shortCode test.proposer@email.com test.member@email.com', expect.any(Function) ); }); diff --git a/src/queue/consumers/scicat/scicatProposal/consumerCallbacks/upsertProposalInScicat.ts b/src/queue/consumers/scicat/scicatProposal/consumerCallbacks/upsertProposalInScicat.ts index d76c9de6..2c81043a 100644 --- a/src/queue/consumers/scicat/scicatProposal/consumerCallbacks/upsertProposalInScicat.ts +++ b/src/queue/consumers/scicat/scicatProposal/consumerCallbacks/upsertProposalInScicat.ts @@ -151,9 +151,16 @@ const checkProposalExists = async ( Authorization: `Bearer ${sciCatAccessToken}`, }, }).catch((error) => { - const parsedError = JSON.parse(error.message || '{}'); - if (parsedError.statusCode === 404) { - return false; + try { + const parsedError = JSON.parse(error.message); + if (parsedError.statusCode === 404) { + return false; + } + } catch (reason) { + logger.logError('Error parsing error message', { + error, + reason, + }); } throw error; }); From 176a4611020360079f6fa04490334c5db804bce3 Mon Sep 17 00:00:00 2001 From: janosbabik <143906591+janosbabik@users.noreply.github.com> Date: Fri, 28 Feb 2025 11:46:31 +0100 Subject: [PATCH 07/25] chore: upgrade Node.js version to 22 in Dockerfiles and update engine requirements (#506) Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> --- Dockerfile | 4 ++-- Dockerfile.connector.dev | 11 +++++++++++ README.md | 2 +- package-lock.json | 4 ++-- package.json | 4 ++-- 5 files changed, 18 insertions(+), 7 deletions(-) create mode 100644 Dockerfile.connector.dev diff --git a/Dockerfile b/Dockerfile index bd3ec80e..e8064497 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:18-alpine AS build-stage +FROM node:22-alpine AS build-stage USER node @@ -14,7 +14,7 @@ COPY --chown=node:node . . RUN npm run build -FROM node:18-alpine +FROM node:22-alpine USER node diff --git a/Dockerfile.connector.dev b/Dockerfile.connector.dev new file mode 100644 index 00000000..0abffcf5 --- /dev/null +++ b/Dockerfile.connector.dev @@ -0,0 +1,11 @@ +FROM node:22-alpine + +USER node + +RUN mkdir -p /home/node/app + +WORKDIR /home/node/app + +COPY --chown=node:node package*.json ./ + +RUN npm ci --loglevel error --no-fund diff --git a/README.md b/README.md index cf549ace..3c673db8 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ The connector connects other services through the message queue. # Requirements -This service requires node =>18.0.0 +This service requires node >=22.0.0 ## Getting started diff --git a/package-lock.json b/package-lock.json index ecac3c1d..e39a445c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -46,8 +46,8 @@ "typescript": "^5.7.2" }, "engines": { - "node": ">=18.0.0", - "npm": ">=9.0.0" + "node": ">=22.0.0", + "npm": ">=10.0.0" } }, "node_modules/@aashutoshrathi/word-wrap": { diff --git a/package.json b/package.json index ba0df1ed..291e0f52 100644 --- a/package.json +++ b/package.json @@ -18,8 +18,8 @@ "service" ], "engines": { - "npm": ">=9.0.0", - "node": ">=18.0.0" + "npm": ">=10.0.0", + "node": ">=22.0.0" }, "author": "SIMS", "license": "MIT", From 6c7f2ba54bd598682908bfa2f6ad414d23e8e30c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Mar 2025 23:42:39 +0000 Subject: [PATCH 08/25] build(deps-dev): bump lint-staged from 15.2.10 to 15.4.3 Bumps [lint-staged](https://github.com/lint-staged/lint-staged) from 15.2.10 to 15.4.3. - [Release notes](https://github.com/lint-staged/lint-staged/releases) - [Changelog](https://github.com/lint-staged/lint-staged/blob/main/CHANGELOG.md) - [Commits](https://github.com/lint-staged/lint-staged/compare/v15.2.10...v15.4.3) --- updated-dependencies: - dependency-name: lint-staged dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- package-lock.json | 284 +++++++++++++++++++++++++++------------------- package.json | 2 +- 2 files changed, 170 insertions(+), 116 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7e3b127e..0232274c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -40,7 +40,7 @@ "eslint-plugin-unused-imports": "^3.2.0", "husky": "^9.1.6", "jest": "^29.7.0", - "lint-staged": "^15.2.10", + "lint-staged": "^15.4.3", "prettier": "3.3.3", "ts-jest": "^29.2.5", "typescript": "^5.7.2" @@ -2711,6 +2711,7 @@ "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", "dev": true, + "license": "MIT", "dependencies": { "restore-cursor": "^5.0.0" }, @@ -2726,6 +2727,7 @@ "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz", "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==", "dev": true, + "license": "MIT", "dependencies": { "slice-ansi": "^5.0.0", "string-width": "^7.0.0" @@ -2738,10 +2740,11 @@ } }, "node_modules/cli-truncate/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -2750,16 +2753,18 @@ } }, "node_modules/cli-truncate/node_modules/emoji-regex": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz", - "integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==", - "dev": true + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "dev": true, + "license": "MIT" }, "node_modules/cli-truncate/node_modules/string-width": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", "dev": true, + "license": "MIT", "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", @@ -2777,6 +2782,7 @@ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", "dev": true, + "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" }, @@ -2839,7 +2845,8 @@ "version": "2.0.20", "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/combined-stream": { "version": "1.0.8", @@ -2853,10 +2860,11 @@ } }, "node_modules/commander": { - "version": "12.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", - "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", + "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", "dev": true, + "license": "MIT", "engines": { "node": ">=18" } @@ -3259,6 +3267,7 @@ "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=18" }, @@ -3813,7 +3822,8 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/events": { "version": "3.3.0", @@ -4278,10 +4288,11 @@ } }, "node_modules/get-east-asian-width": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.2.0.tgz", - "integrity": "sha512-2nk+7SIVb14QrgXFHcm84tD4bKQz0RxPuMT8Ag5KPOq7J5fEmAg0UbXdTOSHqNuHSU28k55qnceesxXRZGzKWA==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz", + "integrity": "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=18" }, @@ -5811,10 +5822,11 @@ } }, "node_modules/lilconfig": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.2.tgz", - "integrity": "sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", "dev": true, + "license": "MIT", "engines": { "node": ">=14" }, @@ -5829,21 +5841,22 @@ "dev": true }, "node_modules/lint-staged": { - "version": "15.2.10", - "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-15.2.10.tgz", - "integrity": "sha512-5dY5t743e1byO19P9I4b3x8HJwalIznL5E1FWYnU6OWw33KxNBSLAc6Cy7F2PsFEO8FKnLwjwm5hx7aMF0jzZg==", - "dev": true, - "dependencies": { - "chalk": "~5.3.0", - "commander": "~12.1.0", - "debug": "~4.3.6", - "execa": "~8.0.1", - "lilconfig": "~3.1.2", - "listr2": "~8.2.4", - "micromatch": "~4.0.8", - "pidtree": "~0.6.0", - "string-argv": "~0.3.2", - "yaml": "~2.5.0" + "version": "15.4.3", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-15.4.3.tgz", + "integrity": "sha512-FoH1vOeouNh1pw+90S+cnuoFwRfUD9ijY2GKy5h7HS3OR7JVir2N2xrsa0+Twc1B7cW72L+88geG5cW4wIhn7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.4.1", + "commander": "^13.1.0", + "debug": "^4.4.0", + "execa": "^8.0.1", + "lilconfig": "^3.1.3", + "listr2": "^8.2.5", + "micromatch": "^4.0.8", + "pidtree": "^0.6.0", + "string-argv": "^0.3.2", + "yaml": "^2.7.0" }, "bin": { "lint-staged": "bin/lint-staged.js" @@ -5856,10 +5869,11 @@ } }, "node_modules/lint-staged/node_modules/chalk": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", - "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", + "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", "dev": true, + "license": "MIT", "engines": { "node": "^12.17.0 || ^14.13 || >=16.0.0" }, @@ -5868,12 +5882,13 @@ } }, "node_modules/lint-staged/node_modules/debug": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", - "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", "dev": true, + "license": "MIT", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -5952,6 +5967,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lint-staged/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, "node_modules/lint-staged/node_modules/npm-run-path": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.1.0.tgz", @@ -6019,10 +6041,11 @@ } }, "node_modules/listr2": { - "version": "8.2.4", - "resolved": "https://registry.npmjs.org/listr2/-/listr2-8.2.4.tgz", - "integrity": "sha512-opevsywziHd3zHCVQGAj8zu+Z3yHNkkoYhWIGnq54RrCVwLz0MozotJEDnKsIBLvkfLGN6BLOyAeRrYI0pKA4g==", + "version": "8.2.5", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-8.2.5.tgz", + "integrity": "sha512-iyAZCeyD+c1gPyE9qpFu8af0Y+MRtmKOncdGoA2S5EY8iFq99dmmvkNnHiWo+pj0s7yH7l3KPIgee77tKpXPWQ==", "dev": true, + "license": "MIT", "dependencies": { "cli-truncate": "^4.0.0", "colorette": "^2.0.20", @@ -6036,10 +6059,11 @@ } }, "node_modules/listr2/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -6052,6 +6076,7 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -6060,16 +6085,18 @@ } }, "node_modules/listr2/node_modules/emoji-regex": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz", - "integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==", - "dev": true + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "dev": true, + "license": "MIT" }, "node_modules/listr2/node_modules/string-width": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", "dev": true, + "license": "MIT", "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", @@ -6087,6 +6114,7 @@ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", "dev": true, + "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" }, @@ -6102,6 +6130,7 @@ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", @@ -6151,6 +6180,7 @@ "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", "dev": true, + "license": "MIT", "dependencies": { "ansi-escapes": "^7.0.0", "cli-cursor": "^5.0.0", @@ -6170,6 +6200,7 @@ "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.0.0.tgz", "integrity": "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==", "dev": true, + "license": "MIT", "dependencies": { "environment": "^1.0.0" }, @@ -6181,10 +6212,11 @@ } }, "node_modules/log-update/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -6197,6 +6229,7 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -6205,16 +6238,18 @@ } }, "node_modules/log-update/node_modules/emoji-regex": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz", - "integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==", - "dev": true + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "dev": true, + "license": "MIT" }, "node_modules/log-update/node_modules/is-fullwidth-code-point": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.0.0.tgz", "integrity": "sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==", "dev": true, + "license": "MIT", "dependencies": { "get-east-asian-width": "^1.0.0" }, @@ -6230,6 +6265,7 @@ "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.0.tgz", "integrity": "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" @@ -6246,6 +6282,7 @@ "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", "dev": true, + "license": "MIT", "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", @@ -6263,6 +6300,7 @@ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", "dev": true, + "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" }, @@ -6278,6 +6316,7 @@ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", @@ -6472,6 +6511,7 @@ "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", "dev": true, + "license": "MIT", "engines": { "node": ">=18" }, @@ -7470,6 +7510,7 @@ "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", "dev": true, + "license": "MIT", "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" @@ -7486,6 +7527,7 @@ "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", "dev": true, + "license": "MIT", "dependencies": { "mimic-function": "^5.0.0" }, @@ -7501,6 +7543,7 @@ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", "dev": true, + "license": "ISC", "engines": { "node": ">=14" }, @@ -7530,7 +7573,8 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/rimraf": { "version": "3.0.2", @@ -7804,6 +7848,7 @@ "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^6.0.0", "is-fullwidth-code-point": "^4.0.0" @@ -7820,6 +7865,7 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -7832,6 +7878,7 @@ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -8761,10 +8808,11 @@ "dev": true }, "node_modules/yaml": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.0.tgz", - "integrity": "sha512-2wWLbGbYDiSqqIKoPjar3MPgB94ErzCtrNE1FdqGuaO0pi2JGjmE8aW8TDZwzU7vuxcGRdL/4gPQwQ7hD5AMSw==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz", + "integrity": "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==", "dev": true, + "license": "ISC", "bin": { "yaml": "bin.mjs" }, @@ -10843,15 +10891,15 @@ }, "dependencies": { "ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", "dev": true }, "emoji-regex": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz", - "integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", "dev": true }, "string-width": { @@ -10929,9 +10977,9 @@ } }, "commander": { - "version": "12.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", - "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", + "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", "dev": true }, "concat-map": { @@ -11961,9 +12009,9 @@ "dev": true }, "get-east-asian-width": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.2.0.tgz", - "integrity": "sha512-2nk+7SIVb14QrgXFHcm84tD4bKQz0RxPuMT8Ag5KPOq7J5fEmAg0UbXdTOSHqNuHSU28k55qnceesxXRZGzKWA==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz", + "integrity": "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==", "dev": true }, "get-intrinsic": { @@ -13037,9 +13085,9 @@ } }, "lilconfig": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.2.tgz", - "integrity": "sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", "dev": true }, "lines-and-columns": { @@ -13049,36 +13097,36 @@ "dev": true }, "lint-staged": { - "version": "15.2.10", - "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-15.2.10.tgz", - "integrity": "sha512-5dY5t743e1byO19P9I4b3x8HJwalIznL5E1FWYnU6OWw33KxNBSLAc6Cy7F2PsFEO8FKnLwjwm5hx7aMF0jzZg==", + "version": "15.4.3", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-15.4.3.tgz", + "integrity": "sha512-FoH1vOeouNh1pw+90S+cnuoFwRfUD9ijY2GKy5h7HS3OR7JVir2N2xrsa0+Twc1B7cW72L+88geG5cW4wIhn7g==", "dev": true, "requires": { - "chalk": "~5.3.0", - "commander": "~12.1.0", - "debug": "~4.3.6", - "execa": "~8.0.1", - "lilconfig": "~3.1.2", - "listr2": "~8.2.4", - "micromatch": "~4.0.8", - "pidtree": "~0.6.0", - "string-argv": "~0.3.2", - "yaml": "~2.5.0" + "chalk": "^5.4.1", + "commander": "^13.1.0", + "debug": "^4.4.0", + "execa": "^8.0.1", + "lilconfig": "^3.1.3", + "listr2": "^8.2.5", + "micromatch": "^4.0.8", + "pidtree": "^0.6.0", + "string-argv": "^0.3.2", + "yaml": "^2.7.0" }, "dependencies": { "chalk": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", - "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", + "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", "dev": true }, "debug": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", - "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", "dev": true, "requires": { - "ms": "2.1.2" + "ms": "^2.1.3" } }, "execa": { @@ -13122,6 +13170,12 @@ "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", "dev": true }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, "npm-run-path": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.1.0.tgz", @@ -13161,9 +13215,9 @@ } }, "listr2": { - "version": "8.2.4", - "resolved": "https://registry.npmjs.org/listr2/-/listr2-8.2.4.tgz", - "integrity": "sha512-opevsywziHd3zHCVQGAj8zu+Z3yHNkkoYhWIGnq54RrCVwLz0MozotJEDnKsIBLvkfLGN6BLOyAeRrYI0pKA4g==", + "version": "8.2.5", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-8.2.5.tgz", + "integrity": "sha512-iyAZCeyD+c1gPyE9qpFu8af0Y+MRtmKOncdGoA2S5EY8iFq99dmmvkNnHiWo+pj0s7yH7l3KPIgee77tKpXPWQ==", "dev": true, "requires": { "cli-truncate": "^4.0.0", @@ -13175,9 +13229,9 @@ }, "dependencies": { "ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", "dev": true }, "ansi-styles": { @@ -13187,9 +13241,9 @@ "dev": true }, "emoji-regex": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz", - "integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", "dev": true }, "string-width": { @@ -13274,9 +13328,9 @@ } }, "ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", "dev": true }, "ansi-styles": { @@ -13286,9 +13340,9 @@ "dev": true }, "emoji-regex": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz", - "integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", "dev": true }, "is-fullwidth-code-point": { @@ -15091,9 +15145,9 @@ "dev": true }, "yaml": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.0.tgz", - "integrity": "sha512-2wWLbGbYDiSqqIKoPjar3MPgB94ErzCtrNE1FdqGuaO0pi2JGjmE8aW8TDZwzU7vuxcGRdL/4gPQwQ7hD5AMSw==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz", + "integrity": "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==", "dev": true }, "yargs": { diff --git a/package.json b/package.json index 084475f1..09fd38fa 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "eslint-plugin-unused-imports": "^3.2.0", "husky": "^9.1.6", "jest": "^29.7.0", - "lint-staged": "^15.2.10", + "lint-staged": "^15.4.3", "prettier": "3.3.3", "ts-jest": "^29.2.5", "typescript": "^5.7.2" From 1dd5899c40d3a80959dbc021c26cbd04121436c7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Mar 2025 23:48:09 +0000 Subject: [PATCH 09/25] build(deps-dev): bump @types/node from 22.9.0 to 22.13.9 Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 22.9.0 to 22.13.9. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) --- updated-dependencies: - dependency-name: "@types/node" dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- package-lock.json | 32 +++++++++++++++++--------------- package.json | 2 +- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0232274c..904a0082 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,7 +28,7 @@ "@types/express": "^4.17.21", "@types/jest": "^29.5.14", "@types/knex": "^0.16.1", - "@types/node": "^22.9.0", + "@types/node": "^22.13.9", "@types/pg": "^8.11.10", "@typescript-eslint/eslint-plugin": "^7.16.1", "@typescript-eslint/parser": "^7.18.0", @@ -1588,11 +1588,12 @@ "dev": true }, "node_modules/@types/node": { - "version": "22.9.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.9.0.tgz", - "integrity": "sha512-vuyHg81vvWA1Z1ELfvLko2c8f34gyA0zaic0+Rllc5lbCnbSyuvb2Oxpm6TAUAC/2xZN3QGqxBNggD1nNR2AfQ==", + "version": "22.13.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.9.tgz", + "integrity": "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw==", + "license": "MIT", "dependencies": { - "undici-types": "~6.19.8" + "undici-types": "~6.20.0" } }, "node_modules/@types/pg": { @@ -8576,9 +8577,10 @@ } }, "node_modules/undici-types": { - "version": "6.19.8", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==" + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "license": "MIT" }, "node_modules/unhomoglyph": { "version": "1.0.6", @@ -10090,11 +10092,11 @@ "dev": true }, "@types/node": { - "version": "22.9.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.9.0.tgz", - "integrity": "sha512-vuyHg81vvWA1Z1ELfvLko2c8f34gyA0zaic0+Rllc5lbCnbSyuvb2Oxpm6TAUAC/2xZN3QGqxBNggD1nNR2AfQ==", + "version": "22.13.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.9.tgz", + "integrity": "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw==", "requires": { - "undici-types": "~6.19.8" + "undici-types": "~6.20.0" } }, "@types/pg": { @@ -14976,9 +14978,9 @@ } }, "undici-types": { - "version": "6.19.8", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==" + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==" }, "unhomoglyph": { "version": "1.0.6", diff --git a/package.json b/package.json index 09fd38fa..20f9e762 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "@types/express": "^4.17.21", "@types/jest": "^29.5.14", "@types/knex": "^0.16.1", - "@types/node": "^22.9.0", + "@types/node": "^22.13.9", "@types/pg": "^8.11.10", "@typescript-eslint/eslint-plugin": "^7.16.1", "@typescript-eslint/parser": "^7.18.0", From 621ae8ca00dee9682482a130c7ab4b2229caa5f8 Mon Sep 17 00:00:00 2001 From: janosbabik <143906591+janosbabik@users.noreply.github.com> Date: Thu, 6 Mar 2025 15:09:19 +0100 Subject: [PATCH 10/25] refactor: replace email with oidcSub in One Identity integration (#510) --- .../oneIdentityIntegrationHandler.spec.ts | 15 ++++++++------ .../oneIdentityIntegrationHandler.ts | 2 +- .../oneidentity/utils/ESSOneIdentity.spec.ts | 20 +++++++++---------- .../oneidentity/utils/ESSOneIdentity.ts | 10 ++++------ 4 files changed, 23 insertions(+), 24 deletions(-) diff --git a/src/queue/consumers/oneidentity/consumerCallbacks/oneIdentityIntegrationHandler.spec.ts b/src/queue/consumers/oneidentity/consumerCallbacks/oneIdentityIntegrationHandler.spec.ts index da3cd746..395b07a1 100644 --- a/src/queue/consumers/oneidentity/consumerCallbacks/oneIdentityIntegrationHandler.spec.ts +++ b/src/queue/consumers/oneidentity/consumerCallbacks/oneIdentityIntegrationHandler.spec.ts @@ -40,11 +40,11 @@ const setupMocks = (data: { mockOneIdentity.getPersons.mockResolvedValueOnce( data.getPersons ?? [ { - email: 'proposer@email', + oidcSub: 'proposer-oidc-sub', uidPerson: 'proposer-uid', }, { - email: 'member@email', + oidcSub: 'member-oidc-sub', uidPerson: 'member-uid', }, ] @@ -53,8 +53,8 @@ const setupMocks = (data: { const proposalMessage = { shortCode: 'shortCode', - proposer: { email: 'proposer@email' }, - members: [{ email: 'member@email' }], + proposer: { oidcSub: 'proposer-oidc-sub' }, + members: [{ oidcSub: 'member-oidc-sub' }], } as ProposalMessageData; describe('oneIdentityIntegrationHandler', () => { @@ -104,7 +104,7 @@ describe('oneIdentityIntegrationHandler', () => { getProposalPersonConnections: [], getPersons: [ { - email: 'proposer@email', + oidcSub: 'proposer-oidc-sub', uidPerson: 'proposer-uid', }, ], @@ -118,7 +118,10 @@ describe('oneIdentityIntegrationHandler', () => { expect(logger.logError).toHaveBeenCalledWith( 'Not all users found in One Identity (investigate)', { - users: [{ email: 'member@email' }, { email: 'proposer@email' }], + users: [ + { oidcSub: 'member-oidc-sub' }, + { oidcSub: 'proposer-oidc-sub' }, + ], uidPersons: ['proposer-uid'], } ); diff --git a/src/queue/consumers/oneidentity/consumerCallbacks/oneIdentityIntegrationHandler.ts b/src/queue/consumers/oneidentity/consumerCallbacks/oneIdentityIntegrationHandler.ts index 67f3aebb..d0008167 100644 --- a/src/queue/consumers/oneidentity/consumerCallbacks/oneIdentityIntegrationHandler.ts +++ b/src/queue/consumers/oneidentity/consumerCallbacks/oneIdentityIntegrationHandler.ts @@ -106,7 +106,7 @@ function getUidPersons( ): UID_Person[] { return userPersonConnections .filter( - (connection): connection is { email: string; uidPerson: UID_Person } => + (connection): connection is { oidcSub: string; uidPerson: UID_Person } => connection.uidPerson !== undefined ) .map(({ uidPerson }) => uidPerson); diff --git a/src/queue/consumers/oneidentity/utils/ESSOneIdentity.spec.ts b/src/queue/consumers/oneidentity/utils/ESSOneIdentity.spec.ts index bf73d3f1..c699bbf6 100644 --- a/src/queue/consumers/oneidentity/utils/ESSOneIdentity.spec.ts +++ b/src/queue/consumers/oneidentity/utils/ESSOneIdentity.spec.ts @@ -113,18 +113,17 @@ describe('ESSOneIdentity', () => { ]); const result = await essOneIdentity.getPerson({ - email: 'foo@email', + oidcSub: '0000-0000-0000-0000', }); expect(mockOneIdentityApi.getEntities).toHaveBeenCalledWith( 'Person', - "ContactEmail='foo@email' OR DefaultEmailAddress='foo@email'" + "CentralAccount='0000-0000-0000-0000'" ); expect(result).toEqual({ UID_Person: 'person-uid' }); }); - // Currently, the ContactEmail field is not unique in the 1IM.Person table. - // This means that it is possible to have multiple persons with the same email. + // The CentralAccount is unique, but the response is an array of entities it('should return the first person if multiple persons are found', async () => { mockOneIdentityApi.getEntities.mockResolvedValueOnce([ { @@ -140,7 +139,7 @@ describe('ESSOneIdentity', () => { ]); const result = await essOneIdentity.getPerson({ - email: 'foo@email', + oidcSub: '0000-0000-0000-0000', }); expect(result).toEqual({ UID_Person: 'person-1-uid' }); @@ -152,8 +151,7 @@ describe('ESSOneIdentity', () => { mockOneIdentityApi.getEntities.mockImplementation((table, filter) => { if ( table === 'Person' && - filter === - "ContactEmail='unknown-email' OR DefaultEmailAddress='unknown-email'" + filter === "CentralAccount='unknown-oidc-sub'" ) return Promise.resolve([]); else @@ -168,20 +166,20 @@ describe('ESSOneIdentity', () => { const result = await essOneIdentity.getPersons([ { - email: 'unknown-email', + oidcSub: 'unknown-oidc-sub', } as ProposalUser, { - email: 'known-email', + oidcSub: 'known-oidc-sub', } as ProposalUser, ]); expect(result).toEqual([ { - email: 'unknown-email', + oidcSub: 'unknown-oidc-sub', uidPerson: undefined, }, { - email: 'known-email', + oidcSub: 'known-oidc-sub', uidPerson: 'known-person-uid', }, ]); diff --git a/src/queue/consumers/oneidentity/utils/ESSOneIdentity.ts b/src/queue/consumers/oneidentity/utils/ESSOneIdentity.ts index d9afda4d..c5952b39 100644 --- a/src/queue/consumers/oneidentity/utils/ESSOneIdentity.ts +++ b/src/queue/consumers/oneidentity/utils/ESSOneIdentity.ts @@ -29,7 +29,7 @@ export interface PersonHasESETValues { } export interface UserPersonConnection { - email: string; + oidcSub: string; uidPerson: UID_Person | undefined; } @@ -95,15 +95,13 @@ export class ESSOneIdentity { } public async getPerson( - user: Pick + user: Pick ): Promise { const entities = await this.oneIdentityApi.getEntities( 'Person', - `ContactEmail='${user.email}' OR DefaultEmailAddress='${user.email}'` // ContactEmail is for scienceusers, DefaultEmailAddress is for ESS employees + `CentralAccount='${user.oidcSub}'` ); - // In theory there should be only one person with the same email, but the 1IM.Person table has no unique constraint on ContactEmail. - // We can't control this, so we just take the first one. return entities[0]?.values; } @@ -116,7 +114,7 @@ export class ESSOneIdentity { .map(async (user) => { const uidPerson = (await this.getPerson(user))?.UID_Person; - return { email: user.email, uidPerson }; + return { oidcSub: user.oidcSub, uidPerson }; }) ); } From 8878ec226b4fa7e5ad3332a8d99124ed3f090a4c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 11 Mar 2025 00:10:40 +0000 Subject: [PATCH 11/25] build(deps-dev): bump eslint-plugin-prettier from 5.2.1 to 5.2.3 Bumps [eslint-plugin-prettier](https://github.com/prettier/eslint-plugin-prettier) from 5.2.1 to 5.2.3. - [Release notes](https://github.com/prettier/eslint-plugin-prettier/releases) - [Changelog](https://github.com/prettier/eslint-plugin-prettier/blob/main/CHANGELOG.md) - [Commits](https://github.com/prettier/eslint-plugin-prettier/compare/v5.2.1...v5.2.3) --- updated-dependencies: - dependency-name: eslint-plugin-prettier dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- package-lock.json | 15 ++++++++------- package.json | 2 +- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index 904a0082..a586d83f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36,7 +36,7 @@ "eslint-config-prettier": "^9.1.0", "eslint-plugin-import": "^2.31.0", "eslint-plugin-jest": "^28.11.0", - "eslint-plugin-prettier": "^5.2.1", + "eslint-plugin-prettier": "^5.2.3", "eslint-plugin-unused-imports": "^3.2.0", "husky": "^9.1.6", "jest": "^29.7.0", @@ -3644,10 +3644,11 @@ } }, "node_modules/eslint-plugin-prettier": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.1.tgz", - "integrity": "sha512-gH3iR3g4JfF+yYPaJYkN7jEl9QbweL/YfkoRlNnuIEHEz1vHVlCmWOS+eGGiRuzHQXdJFCOTxRgvju9b8VUmrw==", + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.3.tgz", + "integrity": "sha512-qJ+y0FfCp/mQYQ/vWQ3s7eUlFEL4PyKfAJxsnYTJ4YT73nsJBWqmEpFryxV9OeUiqmsTsYJ5Y+KDNaeP31wrRw==", "dev": true, + "license": "MIT", "dependencies": { "prettier-linter-helpers": "^1.0.0", "synckit": "^0.9.1" @@ -11558,9 +11559,9 @@ } }, "eslint-plugin-prettier": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.1.tgz", - "integrity": "sha512-gH3iR3g4JfF+yYPaJYkN7jEl9QbweL/YfkoRlNnuIEHEz1vHVlCmWOS+eGGiRuzHQXdJFCOTxRgvju9b8VUmrw==", + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.3.tgz", + "integrity": "sha512-qJ+y0FfCp/mQYQ/vWQ3s7eUlFEL4PyKfAJxsnYTJ4YT73nsJBWqmEpFryxV9OeUiqmsTsYJ5Y+KDNaeP31wrRw==", "dev": true, "requires": { "prettier-linter-helpers": "^1.0.0", diff --git a/package.json b/package.json index 20f9e762..ad8a6600 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "eslint-config-prettier": "^9.1.0", "eslint-plugin-import": "^2.31.0", "eslint-plugin-jest": "^28.11.0", - "eslint-plugin-prettier": "^5.2.1", + "eslint-plugin-prettier": "^5.2.3", "eslint-plugin-unused-imports": "^3.2.0", "husky": "^9.1.6", "jest": "^29.7.0", From 7f3977408634956c86e61a0eda9887711c336b61 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 11 Mar 2025 00:15:46 +0000 Subject: [PATCH 12/25] build(deps): bump axios from 1.7.9 to 1.8.2 Bumps [axios](https://github.com/axios/axios) from 1.7.9 to 1.8.2. - [Release notes](https://github.com/axios/axios/releases) - [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md) - [Commits](https://github.com/axios/axios/compare/v1.7.9...v1.8.2) --- updated-dependencies: - dependency-name: axios dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- package-lock.json | 15 ++++++++------- package.json | 2 +- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index a586d83f..50e87d38 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "@user-office-software/duo-logger": "^2.2.1", "@user-office-software/duo-message-broker": "^1.7.0", - "axios": "^1.7.9", + "axios": "^1.8.2", "dotenv": "^16.4.5", "envalid": "^8.0.0", "express": "^4.21.2", @@ -2310,9 +2310,10 @@ } }, "node_modules/axios": { - "version": "1.7.9", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz", - "integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==", + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.2.tgz", + "integrity": "sha512-ls4GYBm5aig9vWx8AWDSGLpnpDQRtWAfrjU+EuytuODrFBkqesN2RkOQCBzrA1RQNHw1SmRMSDDDSwzNAYQ6Rg==", + "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", @@ -10588,9 +10589,9 @@ } }, "axios": { - "version": "1.7.9", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz", - "integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==", + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.2.tgz", + "integrity": "sha512-ls4GYBm5aig9vWx8AWDSGLpnpDQRtWAfrjU+EuytuODrFBkqesN2RkOQCBzrA1RQNHw1SmRMSDDDSwzNAYQ6Rg==", "requires": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", diff --git a/package.json b/package.json index ad8a6600..1c497e5b 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "dependencies": { "@user-office-software/duo-logger": "^2.2.1", "@user-office-software/duo-message-broker": "^1.7.0", - "axios": "^1.7.9", + "axios": "^1.8.2", "dotenv": "^16.4.5", "envalid": "^8.0.0", "express": "^4.21.2", From 7a0483ec09265f9e158f8196982e298b1effc733 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 11 Mar 2025 00:21:23 +0000 Subject: [PATCH 13/25] build(deps): bump pg and @types/pg Bumps [pg](https://github.com/brianc/node-postgres/tree/HEAD/packages/pg) and [@types/pg](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/pg). These dependencies needed to be updated together. Updates `pg` from 8.13.1 to 8.13.3 - [Changelog](https://github.com/brianc/node-postgres/blob/master/CHANGELOG.md) - [Commits](https://github.com/brianc/node-postgres/commits/pg@8.13.3/packages/pg) Updates `@types/pg` from 8.11.10 to 8.11.11 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/pg) --- updated-dependencies: - dependency-name: pg dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: "@types/pg" dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- package-lock.json | 64 +++++++++++++++++++++++++---------------------- package.json | 4 +-- 2 files changed, 36 insertions(+), 32 deletions(-) diff --git a/package-lock.json b/package-lock.json index 50e87d38..b863fdc6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,7 @@ "kafkajs": "^2.2.3", "knex": "^3.1.0", "matrix-js-sdk": "24.1.0", - "pg": "^8.13.1", + "pg": "^8.13.3", "prom-client": "^15.1.3", "reflect-metadata": "^0.2.2", "ts-node-dev": "^2.0.0", @@ -29,7 +29,7 @@ "@types/jest": "^29.5.14", "@types/knex": "^0.16.1", "@types/node": "^22.13.9", - "@types/pg": "^8.11.10", + "@types/pg": "^8.11.11", "@typescript-eslint/eslint-plugin": "^7.16.1", "@typescript-eslint/parser": "^7.18.0", "eslint": "^8.57.1", @@ -1597,10 +1597,11 @@ } }, "node_modules/@types/pg": { - "version": "8.11.10", - "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.11.10.tgz", - "integrity": "sha512-LczQUW4dbOQzsH2RQ5qoeJ6qJPdrcM/DcMLoqWQkMLMsq83J5lAX3LXjdkWdpscFy67JSOWDnh7Ny/sPFykmkg==", + "version": "8.11.11", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.11.11.tgz", + "integrity": "sha512-kGT1qKM8wJQ5qlawUrEkXgvMSXoV213KfMGXcwfDwUIfUHXqXYXOfS1nE1LINRJVVVx5wCm70XnFlMHaIcQAfw==", "dev": true, + "license": "MIT", "dependencies": { "@types/node": "*", "pg-protocol": "*", @@ -6879,13 +6880,14 @@ } }, "node_modules/pg": { - "version": "8.13.1", - "resolved": "https://registry.npmjs.org/pg/-/pg-8.13.1.tgz", - "integrity": "sha512-OUir1A0rPNZlX//c7ksiu7crsGZTKSOXJPgtNiHGIlC9H0lO+NC6ZDYksSgBYY/thSWhnSRBv8w1lieNNGATNQ==", + "version": "8.13.3", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.13.3.tgz", + "integrity": "sha512-P6tPt9jXbL9HVu/SSRERNYaYG++MjnscnegFh9pPHihfoBSujsrka0hyuymMzeJKFWrcG8wvCKy8rCe8e5nDUQ==", + "license": "MIT", "dependencies": { "pg-connection-string": "^2.7.0", - "pg-pool": "^3.7.0", - "pg-protocol": "^1.7.0", + "pg-pool": "^3.7.1", + "pg-protocol": "^1.7.1", "pg-types": "^2.1.0", "pgpass": "1.x" }, @@ -6933,17 +6935,19 @@ } }, "node_modules/pg-pool": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.7.0.tgz", - "integrity": "sha512-ZOBQForurqh4zZWjrgSwwAtzJ7QiRX0ovFkZr2klsen3Nm0aoh33Ls0fzfv3imeH/nw/O27cjdz5kzYJfeGp/g==", + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.7.1.tgz", + "integrity": "sha512-xIOsFoh7Vdhojas6q3596mXFsR8nwBQBXX5JiV7p9buEVAGqYL4yFzclON5P9vFrpu1u7Zwl2oriyDa89n0wbw==", + "license": "MIT", "peerDependencies": { "pg": ">=8.0" } }, "node_modules/pg-protocol": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.7.0.tgz", - "integrity": "sha512-hTK/mE36i8fDDhgDFjy6xNOG+LCorxLG3WO17tku+ij6sVHXh1jQUJ8hYAnRhNla4QVD2H8er/FOjc/+EgC6yQ==" + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.7.1.tgz", + "integrity": "sha512-gjTHWGYWsEgy9MsY0Gp6ZJxV24IjDqdpTW7Eh0x+WfJLFsm/TJx1MzL6T0D88mBvkpxotCQ6TwW6N+Kko7lhgQ==", + "license": "MIT" }, "node_modules/pg-types": { "version": "4.0.2", @@ -10102,9 +10106,9 @@ } }, "@types/pg": { - "version": "8.11.10", - "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.11.10.tgz", - "integrity": "sha512-LczQUW4dbOQzsH2RQ5qoeJ6qJPdrcM/DcMLoqWQkMLMsq83J5lAX3LXjdkWdpscFy67JSOWDnh7Ny/sPFykmkg==", + "version": "8.11.11", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.11.11.tgz", + "integrity": "sha512-kGT1qKM8wJQ5qlawUrEkXgvMSXoV213KfMGXcwfDwUIfUHXqXYXOfS1nE1LINRJVVVx5wCm70XnFlMHaIcQAfw==", "dev": true, "requires": { "@types/node": "*", @@ -13793,14 +13797,14 @@ "dev": true }, "pg": { - "version": "8.13.1", - "resolved": "https://registry.npmjs.org/pg/-/pg-8.13.1.tgz", - "integrity": "sha512-OUir1A0rPNZlX//c7ksiu7crsGZTKSOXJPgtNiHGIlC9H0lO+NC6ZDYksSgBYY/thSWhnSRBv8w1lieNNGATNQ==", + "version": "8.13.3", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.13.3.tgz", + "integrity": "sha512-P6tPt9jXbL9HVu/SSRERNYaYG++MjnscnegFh9pPHihfoBSujsrka0hyuymMzeJKFWrcG8wvCKy8rCe8e5nDUQ==", "requires": { "pg-cloudflare": "^1.1.1", "pg-connection-string": "^2.7.0", - "pg-pool": "^3.7.0", - "pg-protocol": "^1.7.0", + "pg-pool": "^3.7.1", + "pg-protocol": "^1.7.1", "pg-types": "^2.1.0", "pgpass": "1.x" }, @@ -13870,15 +13874,15 @@ "dev": true }, "pg-pool": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.7.0.tgz", - "integrity": "sha512-ZOBQForurqh4zZWjrgSwwAtzJ7QiRX0ovFkZr2klsen3Nm0aoh33Ls0fzfv3imeH/nw/O27cjdz5kzYJfeGp/g==", + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.7.1.tgz", + "integrity": "sha512-xIOsFoh7Vdhojas6q3596mXFsR8nwBQBXX5JiV7p9buEVAGqYL4yFzclON5P9vFrpu1u7Zwl2oriyDa89n0wbw==", "requires": {} }, "pg-protocol": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.7.0.tgz", - "integrity": "sha512-hTK/mE36i8fDDhgDFjy6xNOG+LCorxLG3WO17tku+ij6sVHXh1jQUJ8hYAnRhNla4QVD2H8er/FOjc/+EgC6yQ==" + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.7.1.tgz", + "integrity": "sha512-gjTHWGYWsEgy9MsY0Gp6ZJxV24IjDqdpTW7Eh0x+WfJLFsm/TJx1MzL6T0D88mBvkpxotCQ6TwW6N+Kko7lhgQ==" }, "pg-types": { "version": "4.0.2", diff --git a/package.json b/package.json index 1c497e5b..4596c194 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "@types/jest": "^29.5.14", "@types/knex": "^0.16.1", "@types/node": "^22.13.9", - "@types/pg": "^8.11.10", + "@types/pg": "^8.11.11", "@typescript-eslint/eslint-plugin": "^7.16.1", "@typescript-eslint/parser": "^7.18.0", "eslint": "^8.57.1", @@ -54,7 +54,7 @@ "kafkajs": "^2.2.3", "knex": "^3.1.0", "matrix-js-sdk": "24.1.0", - "pg": "^8.13.1", + "pg": "^8.13.3", "prom-client": "^15.1.3", "reflect-metadata": "^0.2.2", "ts-node-dev": "^2.0.0", From 9295086e17ecd3ef0b70a0c483e559bdece0cbd3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Mar 2025 23:23:05 +0000 Subject: [PATCH 14/25] build(deps): bump pg from 8.13.3 to 8.14.1 Bumps [pg](https://github.com/brianc/node-postgres/tree/HEAD/packages/pg) from 8.13.3 to 8.14.1. - [Changelog](https://github.com/brianc/node-postgres/blob/master/CHANGELOG.md) - [Commits](https://github.com/brianc/node-postgres/commits/pg@8.14.1/packages/pg) --- updated-dependencies: - dependency-name: pg dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- package-lock.json | 46 +++++++++++++++++++++++----------------------- package.json | 2 +- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/package-lock.json b/package-lock.json index b863fdc6..c8a5eb97 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,7 @@ "kafkajs": "^2.2.3", "knex": "^3.1.0", "matrix-js-sdk": "24.1.0", - "pg": "^8.13.3", + "pg": "^8.14.1", "prom-client": "^15.1.3", "reflect-metadata": "^0.2.2", "ts-node-dev": "^2.0.0", @@ -6880,14 +6880,14 @@ } }, "node_modules/pg": { - "version": "8.13.3", - "resolved": "https://registry.npmjs.org/pg/-/pg-8.13.3.tgz", - "integrity": "sha512-P6tPt9jXbL9HVu/SSRERNYaYG++MjnscnegFh9pPHihfoBSujsrka0hyuymMzeJKFWrcG8wvCKy8rCe8e5nDUQ==", + "version": "8.14.1", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.14.1.tgz", + "integrity": "sha512-0TdbqfjwIun9Fm/r89oB7RFQ0bLgduAhiIqIXOsyKoiC/L54DbuAAzIEN/9Op0f1Po9X7iCPXGoa/Ah+2aI8Xw==", "license": "MIT", "dependencies": { "pg-connection-string": "^2.7.0", - "pg-pool": "^3.7.1", - "pg-protocol": "^1.7.1", + "pg-pool": "^3.8.0", + "pg-protocol": "^1.8.0", "pg-types": "^2.1.0", "pgpass": "1.x" }, @@ -6935,18 +6935,18 @@ } }, "node_modules/pg-pool": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.7.1.tgz", - "integrity": "sha512-xIOsFoh7Vdhojas6q3596mXFsR8nwBQBXX5JiV7p9buEVAGqYL4yFzclON5P9vFrpu1u7Zwl2oriyDa89n0wbw==", + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.8.0.tgz", + "integrity": "sha512-VBw3jiVm6ZOdLBTIcXLNdSotb6Iy3uOCwDGFAksZCXmi10nyRvnP2v3jl4d+IsLYRyXf6o9hIm/ZtUzlByNUdw==", "license": "MIT", "peerDependencies": { "pg": ">=8.0" } }, "node_modules/pg-protocol": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.7.1.tgz", - "integrity": "sha512-gjTHWGYWsEgy9MsY0Gp6ZJxV24IjDqdpTW7Eh0x+WfJLFsm/TJx1MzL6T0D88mBvkpxotCQ6TwW6N+Kko7lhgQ==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.8.0.tgz", + "integrity": "sha512-jvuYlEkL03NRvOoyoRktBK7+qU5kOvlAwvmrH8sr3wbLrOdVWsRxQfz8mMy9sZFsqJ1hEWNfdWKI4SAmoL+j7g==", "license": "MIT" }, "node_modules/pg-types": { @@ -13797,14 +13797,14 @@ "dev": true }, "pg": { - "version": "8.13.3", - "resolved": "https://registry.npmjs.org/pg/-/pg-8.13.3.tgz", - "integrity": "sha512-P6tPt9jXbL9HVu/SSRERNYaYG++MjnscnegFh9pPHihfoBSujsrka0hyuymMzeJKFWrcG8wvCKy8rCe8e5nDUQ==", + "version": "8.14.1", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.14.1.tgz", + "integrity": "sha512-0TdbqfjwIun9Fm/r89oB7RFQ0bLgduAhiIqIXOsyKoiC/L54DbuAAzIEN/9Op0f1Po9X7iCPXGoa/Ah+2aI8Xw==", "requires": { "pg-cloudflare": "^1.1.1", "pg-connection-string": "^2.7.0", - "pg-pool": "^3.7.1", - "pg-protocol": "^1.7.1", + "pg-pool": "^3.8.0", + "pg-protocol": "^1.8.0", "pg-types": "^2.1.0", "pgpass": "1.x" }, @@ -13874,15 +13874,15 @@ "dev": true }, "pg-pool": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.7.1.tgz", - "integrity": "sha512-xIOsFoh7Vdhojas6q3596mXFsR8nwBQBXX5JiV7p9buEVAGqYL4yFzclON5P9vFrpu1u7Zwl2oriyDa89n0wbw==", + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.8.0.tgz", + "integrity": "sha512-VBw3jiVm6ZOdLBTIcXLNdSotb6Iy3uOCwDGFAksZCXmi10nyRvnP2v3jl4d+IsLYRyXf6o9hIm/ZtUzlByNUdw==", "requires": {} }, "pg-protocol": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.7.1.tgz", - "integrity": "sha512-gjTHWGYWsEgy9MsY0Gp6ZJxV24IjDqdpTW7Eh0x+WfJLFsm/TJx1MzL6T0D88mBvkpxotCQ6TwW6N+Kko7lhgQ==" + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.8.0.tgz", + "integrity": "sha512-jvuYlEkL03NRvOoyoRktBK7+qU5kOvlAwvmrH8sr3wbLrOdVWsRxQfz8mMy9sZFsqJ1hEWNfdWKI4SAmoL+j7g==" }, "pg-types": { "version": "4.0.2", diff --git a/package.json b/package.json index 4596c194..b855f9c6 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,7 @@ "kafkajs": "^2.2.3", "knex": "^3.1.0", "matrix-js-sdk": "24.1.0", - "pg": "^8.13.3", + "pg": "^8.14.1", "prom-client": "^15.1.3", "reflect-metadata": "^0.2.2", "ts-node-dev": "^2.0.0", From 9d3f5da8fe44790e4bd4563da41bdbf680b987e3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Mar 2025 23:35:06 +0000 Subject: [PATCH 15/25] build(deps): bump @babel/runtime from 7.21.0 to 7.26.10 Bumps [@babel/runtime](https://github.com/babel/babel/tree/HEAD/packages/babel-runtime) from 7.21.0 to 7.26.10. - [Release notes](https://github.com/babel/babel/releases) - [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md) - [Commits](https://github.com/babel/babel/commits/v7.26.10/packages/babel-runtime) --- updated-dependencies: - dependency-name: "@babel/runtime" dependency-type: indirect ... Signed-off-by: dependabot[bot] --- package-lock.json | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/package-lock.json b/package-lock.json index c8a5eb97..2e73f81d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -680,11 +680,12 @@ } }, "node_modules/@babel/runtime": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.21.0.tgz", - "integrity": "sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw==", + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.10.tgz", + "integrity": "sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw==", + "license": "MIT", "dependencies": { - "regenerator-runtime": "^0.13.11" + "regenerator-runtime": "^0.14.0" }, "engines": { "node": ">=6.9.0" @@ -7421,9 +7422,10 @@ "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==" }, "node_modules/regenerator-runtime": { - "version": "0.13.11", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", - "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", + "license": "MIT" }, "node_modules/regexp.prototype.flags": { "version": "1.5.3", @@ -9349,11 +9351,11 @@ } }, "@babel/runtime": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.21.0.tgz", - "integrity": "sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw==", + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.10.tgz", + "integrity": "sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw==", "requires": { - "regenerator-runtime": "^0.13.11" + "regenerator-runtime": "^0.14.0" } }, "@babel/template": { @@ -14176,9 +14178,9 @@ "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==" }, "regenerator-runtime": { - "version": "0.13.11", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", - "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" }, "regexp.prototype.flags": { "version": "1.5.3", From 0c148426a222451fe195ce26840c1711e41d58a7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Mar 2025 23:40:26 +0000 Subject: [PATCH 16/25] build(deps-dev): bump ts-jest from 29.2.5 to 29.2.6 Bumps [ts-jest](https://github.com/kulshekhar/ts-jest) from 29.2.5 to 29.2.6. - [Release notes](https://github.com/kulshekhar/ts-jest/releases) - [Changelog](https://github.com/kulshekhar/ts-jest/blob/main/CHANGELOG.md) - [Commits](https://github.com/kulshekhar/ts-jest/compare/v29.2.5...v29.2.6) --- updated-dependencies: - dependency-name: ts-jest dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- package-lock.json | 32 +++++++++++++++++--------------- package.json | 2 +- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2e73f81d..d00eeffe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -42,7 +42,7 @@ "jest": "^29.7.0", "lint-staged": "^15.4.3", "prettier": "3.3.3", - "ts-jest": "^29.2.5", + "ts-jest": "^29.2.6", "typescript": "^5.7.2" }, "engines": { @@ -7683,10 +7683,11 @@ } }, "node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", "dev": true, + "license": "ISC", "bin": { "semver": "bin/semver.js" }, @@ -8225,10 +8226,11 @@ } }, "node_modules/ts-jest": { - "version": "29.2.5", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.2.5.tgz", - "integrity": "sha512-KD8zB2aAZrcKIdGk4OwpJggeLcH1FgrICqDSROWqlnJXGCXK4Mn6FcdK2B6670Xr73lHMG1kHw8R87A0ecZ+vA==", + "version": "29.2.6", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.2.6.tgz", + "integrity": "sha512-yTNZVZqc8lSixm+QGVFcPe6+yj7+TWZwIesuOWvfcn4B9bz5x4NDzVCQQjOs7Hfouu36aEqfEbo9Qpo+gq8dDg==", "dev": true, + "license": "MIT", "dependencies": { "bs-logger": "^0.2.6", "ejs": "^3.1.10", @@ -8237,7 +8239,7 @@ "json5": "^2.2.3", "lodash.memoize": "^4.1.2", "make-error": "^1.3.6", - "semver": "^7.6.3", + "semver": "^7.7.1", "yargs-parser": "^21.1.1" }, "bin": { @@ -14353,9 +14355,9 @@ "integrity": "sha512-RjZyX3nVwJyCuTo5tGPx+PZWkDMCg7oOLpSlhjDdZfwUoNqG1mM8nyj31IGHyaPWXhjbP7cdK3qZ2bmkJ1GzRw==" }, "semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", "dev": true }, "send": { @@ -14751,9 +14753,9 @@ "requires": {} }, "ts-jest": { - "version": "29.2.5", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.2.5.tgz", - "integrity": "sha512-KD8zB2aAZrcKIdGk4OwpJggeLcH1FgrICqDSROWqlnJXGCXK4Mn6FcdK2B6670Xr73lHMG1kHw8R87A0ecZ+vA==", + "version": "29.2.6", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.2.6.tgz", + "integrity": "sha512-yTNZVZqc8lSixm+QGVFcPe6+yj7+TWZwIesuOWvfcn4B9bz5x4NDzVCQQjOs7Hfouu36aEqfEbo9Qpo+gq8dDg==", "dev": true, "requires": { "bs-logger": "^0.2.6", @@ -14763,7 +14765,7 @@ "json5": "^2.2.3", "lodash.memoize": "^4.1.2", "make-error": "^1.3.6", - "semver": "^7.6.3", + "semver": "^7.7.1", "yargs-parser": "^21.1.1" } }, diff --git a/package.json b/package.json index b855f9c6..da28f0e8 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "jest": "^29.7.0", "lint-staged": "^15.4.3", "prettier": "3.3.3", - "ts-jest": "^29.2.5", + "ts-jest": "^29.2.6", "typescript": "^5.7.2" }, "dependencies": { From c86f5c96dfefacbb14f35d134eec835a31e468b9 Mon Sep 17 00:00:00 2001 From: janosbabik <143906591+janosbabik@users.noreply.github.com> Date: Mon, 24 Mar 2025 10:40:29 +0100 Subject: [PATCH 17/25] feat: Add OneIdentity integration for visitor access management (#515) Co-authored-by: Jekabs Karklins <58165815+jekabs-karklins@users.noreply.github.com> --- src/models/Event.ts | 2 + ...neIdentityIntegrationQueueConsumer.spec.ts | 230 ++++++++-- .../OneIdentityIntegrationQueueConsumer.ts | 62 ++- src/queue/consumers/oneidentity/README.md | 102 +++++ ...salAndMembersToOneIdentityHandler.spec.ts} | 45 +- ...ProposalAndMembersToOneIdentityHandler.ts} | 19 +- .../syncVisitToOneIdentityHandler.spec.ts | 415 ++++++++++++++++++ .../syncVisitToOneIdentityHandler.ts | 164 +++++++ .../oneidentity/utils/ESSOneIdentity.spec.ts | 246 ++++++++++- .../oneidentity/utils/ESSOneIdentity.ts | 117 +++-- .../oneidentity/utils/OneIdentityApi.spec.ts | 54 +++ .../oneidentity/utils/OneIdentityApi.ts | 22 +- .../oneidentity/utils/interfaces/Eset.ts | 10 + .../oneidentity/utils/interfaces/EsetType.ts | 6 + .../oneidentity/utils/interfaces/Person.ts | 13 + .../utils/interfaces/PersonHasESET.ts | 7 + .../utils/interfaces/PersonWantsOrg.ts | 93 ++++ .../SCProposalSiteAccessResponse.ts | 14 + .../utils/interfaces/VisitMessage.ts | 5 + ...pec.ts => validateProposalMessage.spec.ts} | 16 +- ...geFields.ts => validateProposalMessage.ts} | 6 +- .../utils/validateVisitMessage.spec.ts | 62 +++ .../oneidentity/utils/validateVisitMessage.ts | 13 + 23 files changed, 1596 insertions(+), 127 deletions(-) create mode 100644 src/queue/consumers/oneidentity/README.md rename src/queue/consumers/oneidentity/consumerCallbacks/{oneIdentityIntegrationHandler.spec.ts => syncProposalAndMembersToOneIdentityHandler.spec.ts} (82%) rename src/queue/consumers/oneidentity/consumerCallbacks/{oneIdentityIntegrationHandler.ts => syncProposalAndMembersToOneIdentityHandler.ts} (91%) create mode 100644 src/queue/consumers/oneidentity/consumerCallbacks/syncVisitToOneIdentityHandler.spec.ts create mode 100644 src/queue/consumers/oneidentity/consumerCallbacks/syncVisitToOneIdentityHandler.ts create mode 100644 src/queue/consumers/oneidentity/utils/interfaces/Eset.ts create mode 100644 src/queue/consumers/oneidentity/utils/interfaces/EsetType.ts create mode 100644 src/queue/consumers/oneidentity/utils/interfaces/Person.ts create mode 100644 src/queue/consumers/oneidentity/utils/interfaces/PersonHasESET.ts create mode 100644 src/queue/consumers/oneidentity/utils/interfaces/PersonWantsOrg.ts create mode 100644 src/queue/consumers/oneidentity/utils/interfaces/SCProposalSiteAccessResponse.ts create mode 100644 src/queue/consumers/oneidentity/utils/interfaces/VisitMessage.ts rename src/queue/consumers/oneidentity/utils/{validateRequiredProposalMessageFields.spec.ts => validateProposalMessage.spec.ts} (63%) rename src/queue/consumers/oneidentity/utils/{validateRequiredProposalMessageFields.ts => validateProposalMessage.ts} (69%) create mode 100644 src/queue/consumers/oneidentity/utils/validateVisitMessage.spec.ts create mode 100644 src/queue/consumers/oneidentity/utils/validateVisitMessage.ts diff --git a/src/models/Event.ts b/src/models/Event.ts index 5661f87d..335d7216 100644 --- a/src/models/Event.ts +++ b/src/models/Event.ts @@ -4,4 +4,6 @@ export enum Event { PROPOSAL_STATUS_ACTION_EXECUTED = 'PROPOSAL_STATUS_ACTION_EXECUTED', PROPOSAL_ACCEPTED = 'PROPOSAL_ACCEPTED', PROPOSAL_UPDATED = 'PROPOSAL_UPDATED', + VISIT_CREATED = 'VISIT_CREATED', + VISIT_DELETED = 'VISIT_DELETED', } diff --git a/src/queue/consumers/oneidentity/OneIdentityIntegrationQueueConsumer.spec.ts b/src/queue/consumers/oneidentity/OneIdentityIntegrationQueueConsumer.spec.ts index 7ef8c4ef..4fccfc5f 100644 --- a/src/queue/consumers/oneidentity/OneIdentityIntegrationQueueConsumer.spec.ts +++ b/src/queue/consumers/oneidentity/OneIdentityIntegrationQueueConsumer.spec.ts @@ -4,14 +4,21 @@ jest.mock('../QueueConsumer', () => ({ start: jest.fn(), })), })); -jest.mock('./consumerCallbacks/oneIdentityIntegrationHandler'); +jest.mock('./consumerCallbacks/syncProposalAndMembersToOneIdentityHandler'); +jest.mock('./consumerCallbacks/syncVisitToOneIdentityHandler'); +jest.mock('axios', () => ({ + isAxiosError: jest.fn(), +})); import { logger } from '@user-office-software/duo-logger'; import { MessageBroker } from '@user-office-software/duo-message-broker'; import { MessageProperties } from 'amqplib'; +import { isAxiosError } from 'axios'; -import { oneIdentityIntegrationHandler } from './consumerCallbacks/oneIdentityIntegrationHandler'; +import { syncProposalAndMembersToOneIdentityHandler } from './consumerCallbacks/syncProposalAndMembersToOneIdentityHandler'; +import { syncVisitToOneIdentityHandler } from './consumerCallbacks/syncVisitToOneIdentityHandler'; import { OneIdentityIntegrationQueueConsumer } from './OneIdentityIntegrationQueueConsumer'; +import { VisitMessage } from './utils/interfaces/VisitMessage'; import { Event } from '../../../models/Event'; import { ProposalMessageData } from '../../../models/ProposalMessage'; @@ -47,54 +54,197 @@ describe('OneIdentityIntegrationQueueConsumer', () => { ).rejects.toThrow('Invalid proposal message'); }); - it('should call oneIdentityIntegrationHandler and log message handled', async () => { - const message = createProposalMessage({ - shortCode: 'shortCode', - proposerEmail: 'proposer-email', - memberEmails: [], - }); - const type = Event.PROPOSAL_ACCEPTED; + describe('syncProposalAndMembersToOneIdentityHandler', () => { + it('should call syncProposalAndMembersToOneIdentityHandler and log message handled', async () => { + const message = createProposalMessage({ + shortCode: 'shortCode', + proposerEmail: 'proposer-email', + memberEmails: [], + }); + const type = Event.PROPOSAL_ACCEPTED; - await consumer.onMessage(type, message, {} as MessageProperties); + await consumer.onMessage(type, message, {} as MessageProperties); - expect(logger.logInfo).toHaveBeenNthCalledWith( - 1, - 'OneIdentityIntegrationQueueConsumer', - { - type, + expect(syncProposalAndMembersToOneIdentityHandler).toHaveBeenCalledWith( message, - } - ); - expect(logger.logInfo).toHaveBeenNthCalledWith(2, 'Message handled', { - type, - message, + type + ); + expect(logger.logInfo).toHaveBeenNthCalledWith( + 1, + 'OneIdentityIntegrationQueueConsumer', + { + type, + message, + } + ); + expect(logger.logInfo).toHaveBeenNthCalledWith( + 2, + 'Message handled by OneIdentityIntegrationQueueConsumer', + { + type, + message, + } + ); + expect(logger.logException).not.toHaveBeenCalled(); }); - expect(logger.logException).not.toHaveBeenCalled(); - }); - it('should log exception and re-throw error if oneIdentityIntegrationHandler throws', async () => { - const message = createProposalMessage({ - shortCode: 'shortCode', - proposerEmail: 'proposer-email', - memberEmails: [], + it('should log exception and re-throw error if syncProposalAndMembersToOneIdentityHandler throws', async () => { + const message = createProposalMessage({ + shortCode: 'shortCode', + proposerEmail: 'proposer-email', + memberEmails: [], + }); + const type = Event.PROPOSAL_ACCEPTED; + const error = new Error('Error'); + + ( + syncProposalAndMembersToOneIdentityHandler as jest.Mock + ).mockRejectedValueOnce(error); + + await expect( + consumer.onMessage(type, message, {} as MessageProperties) + ).rejects.toThrow(error); + + expect(logger.logException).toHaveBeenCalledWith( + 'Error while handling message in OneIdentityIntegrationQueueConsumer', + error, + { + type, + message, + } + ); }); - const type = Event.PROPOSAL_ACCEPTED; - const error = new Error('Error'); - (oneIdentityIntegrationHandler as jest.Mock).mockRejectedValueOnce(error); + it('should include Axios error response data in logs when available', async () => { + const message = createProposalMessage({ + shortCode: 'shortCode', + proposerEmail: 'proposer-email', + memberEmails: [], + }); + const type = Event.PROPOSAL_ACCEPTED; - await expect( - consumer.onMessage(type, message, {} as MessageProperties) - ).rejects.toThrow(error); + const axiosError = new Error('Axios Error'); + const mockResponse = { + status: 400, + headers: { 'content-type': 'application/json' }, + data: { message: 'Bad Request' }, + }; + + Object.assign(axiosError, { + isAxiosError: true, + response: mockResponse, + }); + + (isAxiosError as unknown as jest.Mock).mockReturnValueOnce(true); + ( + syncProposalAndMembersToOneIdentityHandler as jest.Mock + ).mockRejectedValueOnce(axiosError); + + await expect( + consumer.onMessage(type, message, {} as MessageProperties) + ).rejects.toThrow(axiosError); - expect(logger.logException).toHaveBeenCalledWith( - 'Error while handling proposal', - error, - { - type, + expect(logger.logException).toHaveBeenCalledWith( + 'Error while handling message in OneIdentityIntegrationQueueConsumer', + axiosError, + { + type, + message, + response: { + status: mockResponse.status, + headers: mockResponse.headers, + data: mockResponse.data, + }, + } + ); + }); + }); + + describe('syncVisitToOneIdentityHandler', () => { + it('should call syncVisitToOneIdentityHandler and log message handled', async () => { + const message = { + visitorId: 'visitor-id', + startAt: '2021-01-01T00:00:00Z', + endAt: '2021-01-02T00:00:00Z', + } as VisitMessage; + const type = Event.VISIT_CREATED; + + jest.mock('./utils/validateVisitMessage', () => ({ + validateVisitMessage: jest.fn().mockReturnValue(message), + })); + + await consumer.onMessage(type, message as any, {} as MessageProperties); + + expect(syncVisitToOneIdentityHandler).toHaveBeenCalledWith( message, - } - ); + type + ); + expect(logger.logInfo).toHaveBeenNthCalledWith( + 1, + 'OneIdentityIntegrationQueueConsumer', + { + type, + message, + } + ); + expect(logger.logInfo).toHaveBeenNthCalledWith( + 2, + 'Message handled by OneIdentityIntegrationQueueConsumer', + { + type, + message, + } + ); + expect(logger.logException).not.toHaveBeenCalled(); + }); + + it('should log exception with Axios error details when syncVisitToOneIdentityHandler throws an Axios error', async () => { + const message = { + visitorId: 'visitor-id', + startAt: '2021-01-01T00:00:00Z', + endAt: '2021-01-02T00:00:00Z', + } as VisitMessage; + const type = Event.VISIT_CREATED; + + const axiosError = new Error('Axios Error'); + const mockResponse = { + status: 500, + headers: { 'content-type': 'application/json' }, + data: { message: 'Internal Server Error' }, + }; + + Object.assign(axiosError, { + isAxiosError: true, + response: mockResponse, + }); + + jest.mock('./utils/validateVisitMessage', () => ({ + validateVisitMessage: jest.fn().mockReturnValue(message), + })); + + (isAxiosError as unknown as jest.Mock).mockReturnValueOnce(true); + (syncVisitToOneIdentityHandler as jest.Mock) = jest + .fn() + .mockRejectedValueOnce(axiosError); + + await expect( + consumer.onMessage(type, message as any, {} as MessageProperties) + ).rejects.toThrow(axiosError); + + expect(logger.logException).toHaveBeenCalledWith( + 'Error while handling message in OneIdentityIntegrationQueueConsumer', + axiosError, + { + type, + message, + response: { + status: mockResponse.status, + headers: mockResponse.headers, + data: mockResponse.data, + }, + } + ); + }); }); }); diff --git a/src/queue/consumers/oneidentity/OneIdentityIntegrationQueueConsumer.ts b/src/queue/consumers/oneidentity/OneIdentityIntegrationQueueConsumer.ts index 680e31c7..44c9133f 100644 --- a/src/queue/consumers/oneidentity/OneIdentityIntegrationQueueConsumer.ts +++ b/src/queue/consumers/oneidentity/OneIdentityIntegrationQueueConsumer.ts @@ -2,17 +2,30 @@ import { logger } from '@user-office-software/duo-logger'; import { ConsumerCallback } from '@user-office-software/duo-message-broker'; import { isAxiosError } from 'axios'; -import { oneIdentityIntegrationHandler } from './consumerCallbacks/oneIdentityIntegrationHandler'; -import { validateRequiredProposalMessageFields } from './utils/validateRequiredProposalMessageFields'; +import { syncProposalAndMembersToOneIdentityHandler } from './consumerCallbacks/syncProposalAndMembersToOneIdentityHandler'; +import { syncVisitToOneIdentityHandler } from './consumerCallbacks/syncVisitToOneIdentityHandler'; +import { validateProposalMessage } from './utils/validateProposalMessage'; import { Event } from '../../../models/Event'; import { QueueConsumer } from '../QueueConsumer'; import { hasTriggeringType } from '../utils/hasTriggeringType'; +import { validateVisitMessage } from './utils/validateVisitMessage'; const ONE_IDENTITY_INTEGRATION_QUEUE_NAME = process.env.ONE_IDENTITY_INTEGRATION_QUEUE_NAME || ''; const USER_OFFICE_CORE_EXCHANGE_NAME = process.env.USER_OFFICE_CORE_EXCHANGE_NAME || ''; -const EVENTS_FOR_HANDLING = [Event.PROPOSAL_ACCEPTED, Event.PROPOSAL_UPDATED]; + +// Events that trigger the handling of syncing proposals and members to One Identity +const SYNC_PROPOSAL_AND_MEMBERS_EVENTS_FOR_HANDLING = [ + Event.PROPOSAL_ACCEPTED, + Event.PROPOSAL_UPDATED, +]; + +// Events that trigger the handling of syncing visits to One Identity +const SYNC_VISIT_EVENTS_FOR_HANDLING = [ + Event.VISIT_CREATED, + Event.VISIT_DELETED, +]; // Class for consuming messages from the ESS One Identity Integration Queue export class OneIdentityIntegrationQueueConsumer extends QueueConsumer { @@ -25,9 +38,17 @@ export class OneIdentityIntegrationQueueConsumer extends QueueConsumer { } onMessage: ConsumerCallback = async (type, message) => { - if (!hasTriggeringType(type, EVENTS_FOR_HANDLING)) { - return; - } + const eventType = type as Event; + const isProposalEvent = hasTriggeringType( + eventType, + SYNC_PROPOSAL_AND_MEMBERS_EVENTS_FOR_HANDLING + ); + const isVisitEvent = hasTriggeringType( + eventType, + SYNC_VISIT_EVENTS_FOR_HANDLING + ); + + if (!isProposalEvent && !isVisitEvent) return; logger.logInfo('OneIdentityIntegrationQueueConsumer', { type, @@ -35,22 +56,33 @@ export class OneIdentityIntegrationQueueConsumer extends QueueConsumer { }); try { - const proposalMessage = validateRequiredProposalMessageFields(message); + if (isProposalEvent) { + const proposalMessage = validateProposalMessage(message); + await syncProposalAndMembersToOneIdentityHandler( + proposalMessage, + eventType + ); + } else if (isVisitEvent) { + const visitMessage = validateVisitMessage(message); + await syncVisitToOneIdentityHandler(visitMessage, eventType); + } - await oneIdentityIntegrationHandler(proposalMessage, type as Event); - - logger.logInfo('Message handled', { + logger.logInfo('Message handled by OneIdentityIntegrationQueueConsumer', { type, message, }); } catch (error) { const response = extractAxiosErrorResponse(error); - logger.logException('Error while handling proposal', error, { - type, - message, - response, - }); + logger.logException( + 'Error while handling message in OneIdentityIntegrationQueueConsumer', + error, + { + type, + message, + response, + } + ); // Re-throw the error to make sure the message is not acknowledged throw error; diff --git a/src/queue/consumers/oneidentity/README.md b/src/queue/consumers/oneidentity/README.md new file mode 100644 index 00000000..ac591d19 --- /dev/null +++ b/src/queue/consumers/oneidentity/README.md @@ -0,0 +1,102 @@ +# One Identity Visit Access Management + +## Functional Specification + +### Purpose +The handler manages site and system access in One Identity based on visit creation and deletion events for science users. + +### Process Overview +- When a visit is created, both site access and system access are provisioned in One Identity +- When a visit is deleted, both site access and system access are cancelled in One Identity +- Only science users (users with `CCC_EmployeeSubType === ESSSCIENCEUSER`) are processed + +### Access Types +- **Site Access**: Physical access to facility for the exact visit duration +- **System Access**: Digital access to systems, extends beyond the visit end date by a configurable number of days (default: 30) + +### Key Relationships +- System access is linked to site access via `CustomProperty04` which stores the site access UID +- Both access types are identified by specific roles in `PersonWantsOrgRole` enum + +## Process Flow Chart + +``` +┌─────────────────────────┐ +│ Visit Event Received │ +└──────────┬──────────────┘ + ↓ +┌─────────────────────────┐ +│ Login to One Identity │ +└──────────┬──────────────┘ + ↓ +┌─────────────────────────┐ +│ Get Person from ID │ +└──────────┬──────────────┘ + ↓ +┌─────────────────────────┐ +< Is Science User? >──No──┐ +└──────────┬──────────────┘ │ + Yes │ + ↓ │ +┌─────────────────────────┐ │ +< Event Type? > │ +└──────────┬──────────────┘ │ + │ │ + ┌────────┴────────┐ │ + ↓ ↓ │ +┌─────────────┐ ┌───────────────┐│ +│VISIT_CREATED│ │VISIT_DELETED ││ +└──────┬──────┘ └───────┬───────┘│ + │ │ │ + ↓ ↓ │ +┌─────────────┐ ┌───────────────┐│ +│Create Site │ │Find Site ││ +│Access │ │Access ││ +└──────┬──────┘ └───────┬───────┘│ + │ │ │ + ↓ ↓ │ +┌─────────────┐ ┌───────────────┐│ +│Create System│ │Cancel Site ││ +│Access │ │Access ││ +└──────┬──────┘ └───────┬───────┘│ + │ │ │ + │ ↓ │ + │ ┌───────────────┐ │ + │ │Find System │ │ + │ │Access via │ │ + │ │CustomProperty4│ │ + │ └───────┬───────┘ │ + │ │ │ + │ ↓ │ + │ ┌───────────────┐ │ + │ │Cancel System │ │ + │ │Access │ │ + │ └───────┬───────┘ │ + │ │ │ + │ │ │ + └───────┐ │ │ + │ │ │ + │ │ │ + ↓ ↓ │ +┌─────────────────────────┐ │ +│ Logout from One Identity│◄─────┘ +└─────────────────────────┘ +``` + +## Key Implementation Details + +### System Access Duration +- Extends beyond visit by `ONE_IDENTITY_SYSTEM_ACCESS_LASTS_FOR_DAYS` (default: 30 days) + +### Access Creation +- Site access matches exact visit dates +- System access starts from the visit start date and extends beyond the visit end date by a configurable number of days (default: 30) +- System access links to site access via `CustomProperty04` + +### Access Cancellation +- System access cancellation depends on finding the parent site access first +- Both must be cancelled + +### Error Handling +- Proper error messages when person or access records are not found +- Always performs logout in finally block to ensure clean session management \ No newline at end of file diff --git a/src/queue/consumers/oneidentity/consumerCallbacks/oneIdentityIntegrationHandler.spec.ts b/src/queue/consumers/oneidentity/consumerCallbacks/syncProposalAndMembersToOneIdentityHandler.spec.ts similarity index 82% rename from src/queue/consumers/oneidentity/consumerCallbacks/oneIdentityIntegrationHandler.spec.ts rename to src/queue/consumers/oneidentity/consumerCallbacks/syncProposalAndMembersToOneIdentityHandler.spec.ts index 395b07a1..909f1edf 100644 --- a/src/queue/consumers/oneidentity/consumerCallbacks/oneIdentityIntegrationHandler.spec.ts +++ b/src/queue/consumers/oneidentity/consumerCallbacks/syncProposalAndMembersToOneIdentityHandler.spec.ts @@ -5,15 +5,12 @@ jest.mock('../utils/ESSOneIdentity', () => ({ import { logger } from '@user-office-software/duo-logger'; -import { oneIdentityIntegrationHandler } from './oneIdentityIntegrationHandler'; +import { syncProposalAndMembersToOneIdentityHandler } from './syncProposalAndMembersToOneIdentityHandler'; import { Event } from '../../../../models/Event'; import { ProposalMessageData } from '../../../../models/ProposalMessage'; -import { - ESSOneIdentity, - PersonHasESETValues, - UID_ESet, - UserPersonConnection, -} from '../utils/ESSOneIdentity'; +import { ESSOneIdentity, UserPersonConnection } from '../utils/ESSOneIdentity'; +import { UID_ESet } from '../utils/interfaces/Eset'; +import { PersonHasESET } from '../utils/interfaces/PersonHasESET'; const mockOneIdentity: jest.Mocked> = { login: jest.fn(), @@ -25,11 +22,14 @@ const mockOneIdentity: jest.Mocked> = { connectPersonToProposal: jest.fn(), getProposalPersonConnections: jest.fn(), removeConnectionBetweenPersonAndProposal: jest.fn(), + getPersonWantsOrg: jest.fn(), + createPersonWantsOrg: jest.fn(), + cancelPersonWantsOrg: jest.fn(), }; const setupMocks = (data: { getProposal: UID_ESet | undefined; - getProposalPersonConnections?: PersonHasESETValues[]; + getProposalPersonConnections?: PersonHasESET[]; getPersons?: UserPersonConnection[]; }) => { mockOneIdentity.createProposal.mockResolvedValueOnce('proposal-UID_ESet'); @@ -65,7 +65,7 @@ describe('oneIdentityIntegrationHandler', () => { getProposalPersonConnections: [], }); - await oneIdentityIntegrationHandler( + await syncProposalAndMembersToOneIdentityHandler( proposalMessage, Event.PROPOSAL_ACCEPTED ); @@ -110,7 +110,7 @@ describe('oneIdentityIntegrationHandler', () => { ], }); - await oneIdentityIntegrationHandler( + await syncProposalAndMembersToOneIdentityHandler( proposalMessage, Event.PROPOSAL_ACCEPTED ); @@ -127,6 +127,25 @@ describe('oneIdentityIntegrationHandler', () => { ); }); + it('should throw error if proposal creation fails', async () => { + // Set up mocks with getProposal returning undefined (proposal doesn't exist) + mockOneIdentity.getProposal.mockResolvedValueOnce(undefined); + + // Mock createProposal to return undefined (creation failed) + mockOneIdentity.createProposal.mockResolvedValueOnce(undefined); + + // Expect the handler to throw an error + await expect( + syncProposalAndMembersToOneIdentityHandler( + proposalMessage, + Event.PROPOSAL_ACCEPTED + ) + ).rejects.toThrow('Proposal creation failed in ESS One Identity'); + + // Verify that logout is still called (in the finally block) + expect(mockOneIdentity.logout).toHaveBeenCalled(); + }); + describe('when proposal already exists in One Identity (Retry logic)', () => { it('should not create proposal but handle connections if proposal exists', async () => { setupMocks({ @@ -139,7 +158,7 @@ describe('oneIdentityIntegrationHandler', () => { ], }); - await oneIdentityIntegrationHandler( + await syncProposalAndMembersToOneIdentityHandler( proposalMessage, Event.PROPOSAL_ACCEPTED ); @@ -180,7 +199,7 @@ describe('oneIdentityIntegrationHandler', () => { ], }); - await oneIdentityIntegrationHandler( + await syncProposalAndMembersToOneIdentityHandler( proposalMessage, Event.PROPOSAL_UPDATED ); @@ -212,7 +231,7 @@ describe('oneIdentityIntegrationHandler', () => { getProposal: undefined, }); - await oneIdentityIntegrationHandler( + await syncProposalAndMembersToOneIdentityHandler( proposalMessage, Event.PROPOSAL_UPDATED ); diff --git a/src/queue/consumers/oneidentity/consumerCallbacks/oneIdentityIntegrationHandler.ts b/src/queue/consumers/oneidentity/consumerCallbacks/syncProposalAndMembersToOneIdentityHandler.ts similarity index 91% rename from src/queue/consumers/oneidentity/consumerCallbacks/oneIdentityIntegrationHandler.ts rename to src/queue/consumers/oneidentity/consumerCallbacks/syncProposalAndMembersToOneIdentityHandler.ts index d0008167..5a88daed 100644 --- a/src/queue/consumers/oneidentity/consumerCallbacks/oneIdentityIntegrationHandler.ts +++ b/src/queue/consumers/oneidentity/consumerCallbacks/syncProposalAndMembersToOneIdentityHandler.ts @@ -3,15 +3,12 @@ import { logger } from '@user-office-software/duo-logger'; import { Event } from '../../../../models/Event'; import { ProposalMessageData } from '../../../../models/ProposalMessage'; import { collectUsersFromProposalMessage } from '../../utils/collectUsersFromProposalMessage'; -import { - ESSOneIdentity, - PersonHasESETValues, - UID_ESet, - UID_Person, - UserPersonConnection, -} from '../utils/ESSOneIdentity'; - -export async function oneIdentityIntegrationHandler( +import { ESSOneIdentity, UserPersonConnection } from '../utils/ESSOneIdentity'; +import { UID_ESet } from '../utils/interfaces/Eset'; +import { UID_Person } from '../utils/interfaces/Person'; +import { PersonHasESET } from '../utils/interfaces/PersonHasESET'; + +export async function syncProposalAndMembersToOneIdentityHandler( message: ProposalMessageData, type: Event ): Promise { @@ -115,7 +112,7 @@ function getUidPersons( async function addNewConnections( oneIdentity: ESSOneIdentity, uidESet: UID_ESet, - connections: PersonHasESETValues[], + connections: PersonHasESET[], uidPersons: UID_Person[] ): Promise { const connectionsToAdd = uidPersons.filter( @@ -132,7 +129,7 @@ async function addNewConnections( async function removeOldConnections( oneIdentity: ESSOneIdentity, - connections: PersonHasESETValues[], + connections: PersonHasESET[], uidPersons: UID_Person[] ): Promise { const connectionsToRemove = connections.filter( diff --git a/src/queue/consumers/oneidentity/consumerCallbacks/syncVisitToOneIdentityHandler.spec.ts b/src/queue/consumers/oneidentity/consumerCallbacks/syncVisitToOneIdentityHandler.spec.ts new file mode 100644 index 00000000..b60035d2 --- /dev/null +++ b/src/queue/consumers/oneidentity/consumerCallbacks/syncVisitToOneIdentityHandler.spec.ts @@ -0,0 +1,415 @@ +jest.mock('@user-office-software/duo-logger'); +jest.mock('../utils/ESSOneIdentity', () => ({ + ESSOneIdentity: jest.fn().mockImplementation(() => mockOneIdentity), +})); + +const ONE_IDENTITY_SYSTEM_ACCESS_LASTS_FOR_DAYS = '45'; + +jest.mock('process', () => ({ + env: { + ONE_IDENTITY_SYSTEM_ACCESS_LASTS_FOR_DAYS, + }, +})); + +import { logger } from '@user-office-software/duo-logger'; + +import { syncVisitToOneIdentityHandler } from './syncVisitToOneIdentityHandler'; +import { Event } from '../../../../models/Event'; +import { ESSOneIdentity } from '../utils/ESSOneIdentity'; +import { IdentityType, Person } from '../utils/interfaces/Person'; +import { + OrderState, + PersonWantsOrg, + PersonWantsOrgRole, +} from '../utils/interfaces/PersonWantsOrg'; +import { VisitMessage } from '../utils/interfaces/VisitMessage'; + +const mockOneIdentity: jest.Mocked> = { + login: jest.fn(), + logout: jest.fn(), + getPerson: jest.fn(), + getPersons: jest.fn(), + getPersonWantsOrg: jest.fn(), + getProposal: jest.fn(), + createProposal: jest.fn(), + connectPersonToProposal: jest.fn(), + getProposalPersonConnections: jest.fn(), + removeConnectionBetweenPersonAndProposal: jest.fn(), + createPersonWantsOrg: jest.fn(), + cancelPersonWantsOrg: jest.fn(), +}; + +const visitMessage: VisitMessage = { + visitorId: 'visitor-oidc-sub', + startAt: '2023-01-01T00:00:00.000Z', + endAt: '2023-01-10T00:00:00.000Z', +}; + +describe('syncVisitToOneIdentityHandler', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('Science user verification', () => { + it('should skip processing if visitor is not a science user', async () => { + // Mock person that is not a science user + const mockPerson = { + UID_Person: 'visitor-uid', + CCC_EmployeeSubType: 'EMPLOYEEDK', + } as Person; + + mockOneIdentity.getPerson.mockResolvedValueOnce(mockPerson); + + await syncVisitToOneIdentityHandler(visitMessage, Event.VISIT_CREATED); + + expect(mockOneIdentity.login).toHaveBeenCalled(); + expect(mockOneIdentity.getPerson).toHaveBeenCalledWith({ + oidcSub: 'visitor-oidc-sub', + }); + expect(logger.logInfo).toHaveBeenCalledWith( + 'Visitor is not a Science User, skipping', + {} + ); + expect(mockOneIdentity.createPersonWantsOrg).not.toHaveBeenCalled(); + expect(mockOneIdentity.logout).toHaveBeenCalled(); + }); + }); + + describe('VISITOR_CREATED', () => { + it('should create site access and system access in One Identity for science users', async () => { + // Mock the current time to a fixed value for testing + const mockNowDate = new Date('2022-12-15T00:00:00.000Z'); + const originalDateNow = Date.now; + Date.now = jest.fn(() => mockNowDate.getTime()); + + // Mock person that is a science user + const mockPerson = { + UID_Person: 'visitor-uid', + CCC_EmployeeSubType: IdentityType.ESSSCIENCEUSER, + } as Person; + + // Mock site access and system access creation responses + const mockSiteAccess = { + UID_PersonWantsOrg: 'site-access-uid', + } as PersonWantsOrg; + const mockSystemAccess = { + UID_PersonWantsOrg: 'system-access-uid', + } as PersonWantsOrg; + + mockOneIdentity.getPerson.mockResolvedValueOnce(mockPerson); + + // Mock sequential calls to createPersonWantsOrg with different responses + mockOneIdentity.createPersonWantsOrg + .mockResolvedValueOnce([mockSiteAccess]) + .mockResolvedValueOnce([mockSystemAccess]); + + await syncVisitToOneIdentityHandler(visitMessage, Event.VISIT_CREATED); + + expect(mockOneIdentity.login).toHaveBeenCalled(); + expect(mockOneIdentity.getPerson).toHaveBeenCalledWith({ + oidcSub: 'visitor-oidc-sub', + }); + + // Verify site access creation + expect(mockOneIdentity.createPersonWantsOrg).toHaveBeenCalledTimes(2); + expect(mockOneIdentity.createPersonWantsOrg).toHaveBeenNthCalledWith( + 1, + PersonWantsOrgRole.SITE_ACCESS, + visitMessage.visitorId, + visitMessage.startAt, + visitMessage.endAt + ); + + // Calculate expected system access dates + // validFrom should be the current mock date + const expectedValidFrom = mockNowDate.toISOString(); + const expectedEndDate = new Date(visitMessage.endAt); + expectedEndDate.setDate( + expectedEndDate.getDate() + + parseInt(ONE_IDENTITY_SYSTEM_ACCESS_LASTS_FOR_DAYS) + ); + + // Verify system access creation + expect(mockOneIdentity.createPersonWantsOrg).toHaveBeenNthCalledWith( + 2, + PersonWantsOrgRole.SYSTEM_ACCESS, + visitMessage.visitorId, + expectedValidFrom, + expectedEndDate.toISOString(), + 'site-access-uid' + ); + + expect(logger.logInfo).toHaveBeenCalledWith( + 'Site access created in One Identity', + { + UID_PersonWantsOrg: 'site-access-uid', + } + ); + expect(logger.logInfo).toHaveBeenCalledWith( + 'System access created in One Identity', + { + UID_PersonWantsOrg: 'system-access-uid', + } + ); + + expect(mockOneIdentity.logout).toHaveBeenCalled(); + + // Restore original Date.now + Date.now = originalDateNow; + }); + + it('should throw an error if site access creation fails', async () => { + // Mock person that is a science user + const mockPerson = { + UID_Person: 'visitor-uid', + CCC_EmployeeSubType: IdentityType.ESSSCIENCEUSER, + } as Person; + + mockOneIdentity.getPerson.mockResolvedValueOnce(mockPerson); + mockOneIdentity.createPersonWantsOrg.mockRejectedValueOnce( + new Error('Failed to create site access') + ); + + await expect( + syncVisitToOneIdentityHandler(visitMessage, Event.VISIT_CREATED) + ).rejects.toThrow('Failed to create site access'); + + expect(mockOneIdentity.login).toHaveBeenCalled(); + expect(mockOneIdentity.logout).toHaveBeenCalled(); + }); + + it('should throw an error when provided with an invalid date', async () => { + // Mock person that is a science user + const mockPerson = { + UID_Person: 'visitor-uid', + CCC_EmployeeSubType: IdentityType.ESSSCIENCEUSER, + } as Person; + + mockOneIdentity.getPerson.mockResolvedValueOnce(mockPerson); + + // Create a message with an invalid date + const invalidVisitMessage: VisitMessage = { + visitorId: 'visitor-oidc-sub', + startAt: 'invalid-date', + endAt: '2023-01-10T00:00:00.000Z', + }; + + await expect( + syncVisitToOneIdentityHandler(invalidVisitMessage, Event.VISIT_CREATED) + ).rejects.toThrow('Invalid date provided to toIsoString: invalid-date'); + + expect(mockOneIdentity.login).toHaveBeenCalled(); + expect(mockOneIdentity.getPerson).toHaveBeenCalledWith({ + oidcSub: 'visitor-oidc-sub', + }); + expect(mockOneIdentity.createPersonWantsOrg).not.toHaveBeenCalled(); + expect(mockOneIdentity.logout).toHaveBeenCalled(); + }); + }); + + describe('VISITOR_DELETED', () => { + it('should remove visitor access in One Identity for science users', async () => { + // Mock person that is a science user + const mockPerson = { + UID_Person: 'visitor-uid', + CCC_EmployeeSubType: IdentityType.ESSSCIENCEUSER, + } as Person; + + const mockPersonWantsOrgs = [ + { + UID_PersonWantsOrg: 'site-access-uid', + UID_PersonOrdered: 'visitor-uid', + DisplayOrg: PersonWantsOrgRole.SITE_ACCESS, + ValidFrom: '2023-01-01T00:00:00.000Z', + ValidUntil: '2023-01-10T00:00:00.000Z', + OrderState: OrderState.GRANTED, + } as PersonWantsOrg, + { + UID_PersonWantsOrg: 'system-access-uid', + UID_PersonOrdered: 'visitor-uid', + DisplayOrg: PersonWantsOrgRole.SYSTEM_ACCESS, + ValidFrom: '2023-01-01T00:00:00.000Z', + ValidUntil: '2023-01-10T00:00:00.000Z', + CustomProperty04: 'site-access-uid', + OrderState: OrderState.GRANTED, + } as PersonWantsOrg, + ]; + + mockOneIdentity.getPerson.mockResolvedValueOnce(mockPerson); + mockOneIdentity.getPersonWantsOrg.mockResolvedValueOnce( + mockPersonWantsOrgs + ); + + await syncVisitToOneIdentityHandler(visitMessage, Event.VISIT_DELETED); + + expect(mockOneIdentity.login).toHaveBeenCalled(); + + expect(mockOneIdentity.getPerson).toHaveBeenCalledWith({ + oidcSub: 'visitor-oidc-sub', + }); + expect(mockOneIdentity.getPersonWantsOrg).toHaveBeenCalledWith( + 'visitor-uid' + ); + + expect(logger.logInfo).toHaveBeenCalledWith( + 'One Identity successfully logged in', + {} + ); + + expect(mockOneIdentity.cancelPersonWantsOrg).toHaveBeenNthCalledWith( + 1, + 'site-access-uid' + ); + + expect(logger.logInfo).toHaveBeenCalledWith( + 'Site access cancelled in One Identity', + { + UID_PersonWantsOrg: 'site-access-uid', + } + ); + + expect(mockOneIdentity.cancelPersonWantsOrg).toHaveBeenNthCalledWith( + 2, + 'system-access-uid' + ); + + expect(logger.logInfo).toHaveBeenCalledWith( + 'System access cancelled in One Identity', + { + UID_PersonWantsOrg: 'system-access-uid', + } + ); + + expect(mockOneIdentity.cancelPersonWantsOrg).toHaveBeenCalledTimes(2); + + expect(mockOneIdentity.logout).toHaveBeenCalled(); + expect(logger.logInfo).toHaveBeenCalledWith( + 'One Identity successfully logged out', + {} + ); + }); + + it('should skip processing if visitor is not a science user', async () => { + // Mock person that is not a science user + const mockPerson = { + UID_Person: 'visitor-uid', + CCC_EmployeeSubType: 'EMPLOYEEDK', + } as Person; + + mockOneIdentity.getPerson.mockResolvedValueOnce(mockPerson); + + await syncVisitToOneIdentityHandler(visitMessage, Event.VISIT_DELETED); + + expect(mockOneIdentity.login).toHaveBeenCalled(); + expect(mockOneIdentity.getPerson).toHaveBeenCalledWith({ + oidcSub: 'visitor-oidc-sub', + }); + expect(logger.logInfo).toHaveBeenCalledWith( + 'Visitor is not a Science User, skipping', + {} + ); + expect(mockOneIdentity.getPersonWantsOrg).not.toHaveBeenCalled(); + expect(mockOneIdentity.cancelPersonWantsOrg).not.toHaveBeenCalled(); + expect(mockOneIdentity.logout).toHaveBeenCalled(); + }); + + it('should throw error if person not found', async () => { + mockOneIdentity.getPerson.mockResolvedValueOnce(undefined); + + await expect( + syncVisitToOneIdentityHandler(visitMessage, Event.VISIT_DELETED) + ).rejects.toThrow('Person not found in One Identity'); + + expect(mockOneIdentity.login).toHaveBeenCalled(); + expect(mockOneIdentity.getPerson).toHaveBeenCalledWith({ + oidcSub: 'visitor-oidc-sub', + }); + expect(mockOneIdentity.getPersonWantsOrg).not.toHaveBeenCalled(); + expect(mockOneIdentity.cancelPersonWantsOrg).not.toHaveBeenCalled(); + expect(mockOneIdentity.logout).toHaveBeenCalled(); + }); + + it('should throw error if site access not found', async () => { + const mockPerson = { + UID_Person: 'visitor-uid', + CCC_EmployeeSubType: IdentityType.ESSSCIENCEUSER, + } as Person; + const mockPersonWantsOrgs = [ + // No site access matching the dates + { + UID_PersonWantsOrg: 'site-access-uid', + UID_PersonOrdered: 'visitor-uid', + DisplayOrg: PersonWantsOrgRole.SITE_ACCESS, + ValidFrom: '2023-01-02T00:00:00.000Z', // Different from message.startAt + ValidUntil: '2023-01-10T00:00:00.000Z', + OrderState: OrderState.GRANTED, + } as PersonWantsOrg, + ]; + + mockOneIdentity.getPerson.mockResolvedValueOnce(mockPerson); + mockOneIdentity.getPersonWantsOrg.mockResolvedValueOnce( + mockPersonWantsOrgs + ); + + await expect( + syncVisitToOneIdentityHandler(visitMessage, Event.VISIT_DELETED) + ).rejects.toThrow( + 'Site access not found in One Identity, cannot remove access' + ); + + expect(mockOneIdentity.login).toHaveBeenCalled(); + expect(mockOneIdentity.logout).toHaveBeenCalled(); + }); + + it('should throw error if system access not found', async () => { + const mockPerson = { + UID_Person: 'visitor-uid', + CCC_EmployeeSubType: IdentityType.ESSSCIENCEUSER, + } as Person; + + const mockPersonWantsOrgs = [ + { + UID_PersonWantsOrg: 'site-access-uid', + UID_PersonOrdered: 'visitor-uid', + DisplayOrg: PersonWantsOrgRole.SITE_ACCESS, + ValidFrom: visitMessage.startAt, + ValidUntil: visitMessage.endAt, + OrderState: OrderState.GRANTED, + } as PersonWantsOrg, + // No system access with CustomProperty04 matching site-access-uid + { + UID_PersonWantsOrg: 'system-access-uid', + UID_PersonOrdered: 'visitor-uid', + DisplayOrg: PersonWantsOrgRole.SYSTEM_ACCESS, + ValidFrom: '2023-01-01T00:00:00.000Z', + ValidUntil: '2023-01-10T00:00:00.000Z', + CustomProperty04: 'different-site-access-uid', + OrderState: OrderState.GRANTED, + } as PersonWantsOrg, + ]; + + mockOneIdentity.getPerson.mockResolvedValueOnce(mockPerson); + mockOneIdentity.getPersonWantsOrg.mockResolvedValueOnce( + mockPersonWantsOrgs + ); + + await expect( + syncVisitToOneIdentityHandler(visitMessage, Event.VISIT_DELETED) + ).rejects.toThrow( + 'System access not found in One Identity, cannot remove access' + ); + + expect(mockOneIdentity.login).toHaveBeenCalled(); + expect(mockOneIdentity.cancelPersonWantsOrg).toHaveBeenCalledWith( + 'site-access-uid' + ); + expect(logger.logInfo).toHaveBeenCalledWith( + 'Site access cancelled in One Identity', + { + UID_PersonWantsOrg: 'site-access-uid', + } + ); + expect(mockOneIdentity.logout).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/queue/consumers/oneidentity/consumerCallbacks/syncVisitToOneIdentityHandler.ts b/src/queue/consumers/oneidentity/consumerCallbacks/syncVisitToOneIdentityHandler.ts new file mode 100644 index 00000000..84427150 --- /dev/null +++ b/src/queue/consumers/oneidentity/consumerCallbacks/syncVisitToOneIdentityHandler.ts @@ -0,0 +1,164 @@ +import process from 'process'; + +import { logger } from '@user-office-software/duo-logger'; + +import { Event } from '../../../../models/Event'; +import { ESSOneIdentity } from '../utils/ESSOneIdentity'; +import { IdentityType, UID_Person } from '../utils/interfaces/Person'; +import { + OrderState, + PersonWantsOrgRole, +} from '../utils/interfaces/PersonWantsOrg'; +import { VisitMessage } from '../utils/interfaces/VisitMessage'; + +const ONE_IDENTITY_SYSTEM_ACCESS_LASTS_FOR_DAYS = parseInt( + process.env.ONE_IDENTITY_SYSTEM_ACCESS_LASTS_FOR_DAYS || '30' +); + +export async function syncVisitToOneIdentityHandler( + { startAt, endAt, visitorId }: VisitMessage, + type: Event +): Promise { + const oneIdentity = new ESSOneIdentity(); + await oneIdentity.login(); + + logger.logInfo('One Identity successfully logged in', {}); + + try { + // Only Science Users' access should be managed! + const uidPerson = await getScienceUser(oneIdentity, visitorId); + if (!uidPerson) { + logger.logInfo('Visitor is not a Science User, skipping', {}); + + return; + } + + if (type === Event.VISIT_CREATED) { + await createAccessInOneIdentity(oneIdentity, startAt, endAt, visitorId); + } else if (type === Event.VISIT_DELETED) { + await removeAccessFromOneIdentity(oneIdentity, startAt, endAt, uidPerson); + } + } finally { + await oneIdentity.logout(); + logger.logInfo('One Identity successfully logged out', {}); + } +} + +// Find person UID from oidcSub +// If the person is not a science user, return undefined +async function getScienceUser( + oneIdentity: ESSOneIdentity, + centralAccount: string +): Promise { + // Find person UID from oidcSub + const person = await oneIdentity.getPerson({ + oidcSub: centralAccount, + }); + + if (!person) { + throw new Error('Person not found in One Identity'); + } + + if (person.CCC_EmployeeSubType === IdentityType.ESSSCIENCEUSER) + return person.UID_Person; + else return undefined; +} + +async function createAccessInOneIdentity( + oneIdentity: ESSOneIdentity, + startAt: string, + endAt: string, + centralAccount: string +) { + // Create site access + const [pwoSite] = await oneIdentity.createPersonWantsOrg( + PersonWantsOrgRole.SITE_ACCESS, + centralAccount, + toIsoString(startAt), + toIsoString(endAt) + ); + + logger.logInfo('Site access created in One Identity', { + UID_PersonWantsOrg: pwoSite.UID_PersonWantsOrg, + }); + + // validFrom in One Identity should be in the future so that the access is not immediately available + const validFrom = Date.now(); + const validUntil = new Date(endAt).setDate( + new Date(endAt).getDate() + ONE_IDENTITY_SYSTEM_ACCESS_LASTS_FOR_DAYS + ); + + // Create system access + const [pwoSystem] = await oneIdentity.createPersonWantsOrg( + PersonWantsOrgRole.SYSTEM_ACCESS, + centralAccount, + toIsoString(validFrom), + toIsoString(validUntil), + pwoSite.UID_PersonWantsOrg // CustomProperty04 + ); + + logger.logInfo('System access created in One Identity', { + UID_PersonWantsOrg: pwoSystem.UID_PersonWantsOrg, + }); +} + +async function removeAccessFromOneIdentity( + oneIdentity: ESSOneIdentity, + startAt: string, + endAt: string, + uidPerson: UID_Person +) { + // Find person wants orgs for the visitor + const personWantsOrgs = await oneIdentity.getPersonWantsOrg(uidPerson); + + // Find site access for the visitor + const siteAccess = personWantsOrgs.find( + (pwo) => + pwo.DisplayOrg === PersonWantsOrgRole.SITE_ACCESS && + toIsoString(pwo.ValidFrom) === toIsoString(startAt) && + toIsoString(pwo.ValidUntil) === toIsoString(endAt) && + pwo.OrderState !== OrderState.ABORTED + ); + + if (!siteAccess) { + throw new Error( + 'Site access not found in One Identity, cannot remove access' + ); + } + + await oneIdentity.cancelPersonWantsOrg(siteAccess.UID_PersonWantsOrg); + + logger.logInfo('Site access cancelled in One Identity', { + UID_PersonWantsOrg: siteAccess.UID_PersonWantsOrg, + }); + + // Find system access for the site access (CustomProperty04 is the site access UID) + const systemAccess = personWantsOrgs.find( + (pwo) => + pwo.CustomProperty04 === siteAccess.UID_PersonWantsOrg && + pwo.DisplayOrg === PersonWantsOrgRole.SYSTEM_ACCESS && + pwo.OrderState !== OrderState.UNSUBSCRIBED + ); + + if (!systemAccess) { + throw new Error( + 'System access not found in One Identity, cannot remove access' + ); + } + + await oneIdentity.cancelPersonWantsOrg(systemAccess.UID_PersonWantsOrg); + + logger.logInfo('System access cancelled in One Identity', { + UID_PersonWantsOrg: systemAccess.UID_PersonWantsOrg, + }); +} + +function toIsoString(date: string | number) { + const parsedDate = new Date(date); + + if (isNaN(parsedDate.getTime())) { + throw new Error(`Invalid date provided to toIsoString: ${date}`); + } + + return parsedDate.toISOString(); +} diff --git a/src/queue/consumers/oneidentity/utils/ESSOneIdentity.spec.ts b/src/queue/consumers/oneidentity/utils/ESSOneIdentity.spec.ts index c699bbf6..edb0e506 100644 --- a/src/queue/consumers/oneidentity/utils/ESSOneIdentity.spec.ts +++ b/src/queue/consumers/oneidentity/utils/ESSOneIdentity.spec.ts @@ -10,6 +10,10 @@ jest.mock('process', () => ({ })); import { ESSOneIdentity } from './ESSOneIdentity'; +import { + PersonWantsOrg, + PersonWantsOrgRole, +} from './interfaces/PersonWantsOrg'; import { ProposalMessageData } from '../../../../models/ProposalMessage'; import { ProposalUser } from '../../scicat/scicatProposal/dto'; @@ -19,6 +23,7 @@ const mockOneIdentityApi = { createEntity: jest.fn(), getEntities: jest.fn(), deleteEntity: jest.fn(), + callScript: jest.fn(), }; describe('ESSOneIdentity', () => { @@ -76,6 +81,18 @@ describe('ESSOneIdentity', () => { }); expect(result).toBe('created-uid'); }); + + it('should throw an error when UID_ESetType is not found', async () => { + const proposalMessage = { + shortCode: 'some-short-code', + } as ProposalMessageData; + + mockOneIdentityApi.getEntities.mockResolvedValueOnce([]); + + await expect( + essOneIdentity.createProposal(proposalMessage) + ).rejects.toThrow('UID_ESetType not found: PROPOSAL_IDENT_ESET_TYPE'); + }); }); describe('getProposal', () => { @@ -100,6 +117,18 @@ describe('ESSOneIdentity', () => { ); expect(result).toBe('proposal-uid'); }); + + it('should return undefined if proposal is not found', async () => { + const proposalMessage = { + shortCode: 'some-short-code', + } as ProposalMessageData; + + mockOneIdentityApi.getEntities.mockResolvedValueOnce([]); + + const result = await essOneIdentity.getProposal(proposalMessage); + + expect(result).toBeUndefined(); + }); }); describe('getPerson', () => { @@ -118,7 +147,8 @@ describe('ESSOneIdentity', () => { expect(mockOneIdentityApi.getEntities).toHaveBeenCalledWith( 'Person', - "CentralAccount='0000-0000-0000-0000'" + "CentralAccount='0000-0000-0000-0000'", + ['CCC_EmployeeSubType'] ); expect(result).toEqual({ UID_Person: 'person-uid' }); }); @@ -258,4 +288,218 @@ describe('ESSOneIdentity', () => { ]); }); }); + + describe('createPersonWantsOrg', () => { + const role = PersonWantsOrgRole.SITE_ACCESS; + const centralAccount = 'user123'; + const startDate = '2023-01-01'; + const endDate = '2023-12-31'; + const mockPersonWantsOrgData: PersonWantsOrg[] = [ + { + UID_PersonWantsOrg: 'pwo-123', + ValidFrom: startDate, + ValidUntil: endDate, + } as PersonWantsOrg, + ]; + + it('should successfully create site access', async () => { + mockOneIdentityApi.callScript.mockResolvedValueOnce({ + IsSuccess: true, + Data: mockPersonWantsOrgData, + Message: 'Success', + }); + + const result = await essOneIdentity.createPersonWantsOrg( + role, + centralAccount, + startDate, + endDate + ); + + expect(mockOneIdentityApi.callScript).toHaveBeenCalledWith( + 'SCProposalSiteAccess', + [role, centralAccount, centralAccount, startDate, endDate, '', ''] + ); + expect(result).toEqual(mockPersonWantsOrgData); + }); + + it('should successfully create site access with custom data', async () => { + const customData = 'custom-data-123'; + mockOneIdentityApi.callScript.mockResolvedValueOnce({ + IsSuccess: true, + Data: mockPersonWantsOrgData, + Message: 'Success', + }); + + const result = await essOneIdentity.createPersonWantsOrg( + role, + centralAccount, + startDate, + endDate, + customData + ); + + expect(mockOneIdentityApi.callScript).toHaveBeenCalledWith( + 'SCProposalSiteAccess', + [ + role, + centralAccount, + centralAccount, + startDate, + endDate, + customData, + '', + ] + ); + expect(result).toEqual(mockPersonWantsOrgData); + }); + + it('should throw an error when site access creation fails', async () => { + const errorMessage = 'Access denied'; + mockOneIdentityApi.callScript.mockResolvedValueOnce({ + IsSuccess: false, + Data: null, + Message: errorMessage, + }); + + await expect( + essOneIdentity.createPersonWantsOrg( + role, + centralAccount, + startDate, + endDate + ) + ).rejects.toThrow(`Failed to create site access: ${errorMessage}`); + expect(mockOneIdentityApi.callScript).toHaveBeenCalledWith( + 'SCProposalSiteAccess', + [role, centralAccount, centralAccount, startDate, endDate, '', ''] + ); + }); + }); + + describe('cancelPersonWantsOrg', () => { + const uidPersonWantsOrg = 'pwo-123'; + + it('should successfully cancel site access', async () => { + mockOneIdentityApi.callScript.mockResolvedValueOnce({ + IsSuccess: true, + Message: 'Success', + }); + + await essOneIdentity.cancelPersonWantsOrg(uidPersonWantsOrg); + + expect(mockOneIdentityApi.callScript).toHaveBeenCalledWith( + 'SCProposalSiteAccessCancel', + [uidPersonWantsOrg] + ); + }); + + it('should throw an error when site access cancellation fails', async () => { + const errorMessage = 'Access not found'; + mockOneIdentityApi.callScript.mockResolvedValueOnce({ + IsSuccess: false, + Message: errorMessage, + }); + + await expect( + essOneIdentity.cancelPersonWantsOrg(uidPersonWantsOrg) + ).rejects.toThrow(`Failed to cancel site access:${errorMessage}`); + expect(mockOneIdentityApi.callScript).toHaveBeenCalledWith( + 'SCProposalSiteAccessCancel', + [uidPersonWantsOrg] + ); + }); + }); + + describe('getPersonWantsOrg', () => { + const mockPersonWantsOrgData = [ + { + values: { + UID_PersonWantsOrg: 'pwo-123', + DisplayOrg: PersonWantsOrgRole.SITE_ACCESS, + ValidFrom: '2023-01-01', + ValidUntil: '2023-12-31', + OrderState: 'Granted', + }, + }, + { + values: { + UID_PersonWantsOrg: 'pwo-456', + DisplayOrg: PersonWantsOrgRole.SYSTEM_ACCESS, + ValidFrom: '2023-01-01', + ValidUntil: '2023-12-31', + OrderState: 'Granted', + }, + }, + ]; + + it('should get person wants org records with default parameters', async () => { + mockOneIdentityApi.getEntities.mockResolvedValueOnce( + mockPersonWantsOrgData + ); + + const result = await essOneIdentity.getPersonWantsOrg('person-uid'); + + expect(mockOneIdentityApi.getEntities).toHaveBeenCalledWith( + 'PersonWantsOrg', + "UID_PersonOrdered='person-uid' AND (DisplayOrg='Experiment visit - site access' OR DisplayOrg='Experiment visit - system access')", + [ + 'ValidFrom', + 'ValidUntil', + 'OrderState', + 'DisplayOrg', + 'CustomProperty04', + ] + ); + expect(result).toEqual([ + mockPersonWantsOrgData[0].values, + mockPersonWantsOrgData[1].values, + ]); + }); + + it('should get person wants org records with custom parameters', async () => { + mockOneIdentityApi.getEntities.mockResolvedValueOnce( + mockPersonWantsOrgData + ); + + const result = await essOneIdentity.getPersonWantsOrg('person-uid', [ + PersonWantsOrgRole.SITE_ACCESS, + ]); + + expect(mockOneIdentityApi.getEntities).toHaveBeenCalledWith( + 'PersonWantsOrg', + "UID_PersonOrdered='person-uid' AND (DisplayOrg='Experiment visit - site access')", + [ + 'ValidFrom', + 'ValidUntil', + 'OrderState', + 'DisplayOrg', + 'CustomProperty04', + ] + ); + expect(result).toEqual([ + mockPersonWantsOrgData[0].values, + mockPersonWantsOrgData[1].values, + ]); + }); + + it('should return empty array when no records found', async () => { + mockOneIdentityApi.getEntities.mockResolvedValueOnce([]); + + const result = await essOneIdentity.getPersonWantsOrg('person-uid'); + + expect(mockOneIdentityApi.getEntities).toHaveBeenCalledWith( + 'PersonWantsOrg', + "UID_PersonOrdered='person-uid' AND (DisplayOrg='Experiment visit - site access' OR DisplayOrg='Experiment visit - system access')", + [ + 'ValidFrom', + 'ValidUntil', + 'OrderState', + 'DisplayOrg', + 'CustomProperty04', + ] + ); + expect(result).toEqual([]); + }); + }); }); diff --git a/src/queue/consumers/oneidentity/utils/ESSOneIdentity.ts b/src/queue/consumers/oneidentity/utils/ESSOneIdentity.ts index c5952b39..8630de73 100644 --- a/src/queue/consumers/oneidentity/utils/ESSOneIdentity.ts +++ b/src/queue/consumers/oneidentity/utils/ESSOneIdentity.ts @@ -1,33 +1,21 @@ import { env } from 'process'; +import { Eset, UID_ESet } from './interfaces/Eset'; +import { EsetType } from './interfaces/EsetType'; +import { Person, UID_Person } from './interfaces/Person'; +import { PersonHasESET } from './interfaces/PersonHasESET'; +import { + PersonWantsOrg, + PersonWantsOrgRole, +} from './interfaces/PersonWantsOrg'; +import { + SCProposalSiteAccessCancelResponse, + SCProposalSiteAccessResponse, +} from './interfaces/SCProposalSiteAccessResponse'; import { OneIdentityApi } from './OneIdentityApi'; import { ProposalMessageData } from '../../../../models/ProposalMessage'; import { ProposalUser } from '../../scicat/scicatProposal/dto'; -type UID_ESetType = string; -export type UID_Person = string; -export type UID_ESet = string; - -interface EsetValues { - UID_ESet: UID_ESet; - UID_ESetType: UID_ESetType; - Ident_ESet: string; - DisplayName: string; -} - -interface EsetTypeValues { - UID_ESetType: UID_ESetType; -} - -interface PersonValues { - UID_Person: UID_Person; -} - -export interface PersonHasESETValues { - UID_Person: UID_Person; - UID_ESet: UID_ESet; -} - export interface UserPersonConnection { oidcSub: string; uidPerson: UID_Person | undefined; @@ -60,7 +48,7 @@ export class ESSOneIdentity { proposalMessage: ProposalMessageData ): Promise { // get "Science Proposal" UID_ESetType from ESS One Identity - const entities = await this.oneIdentityApi.getEntities( + const entities = await this.oneIdentityApi.getEntities( 'EsetType', `Ident_ESetType='${PROPOSAL_IDENT_ESET_TYPE}'` ); @@ -73,7 +61,7 @@ export class ESSOneIdentity { // create proposal in ESS One Identity const esetResponse = await this.oneIdentityApi.createEntity< - Omit + Omit >('ESET', { UID_ESetType: uidESetType, Ident_ESet: proposalMessage.shortCode, @@ -86,7 +74,7 @@ export class ESSOneIdentity { public async getProposal( proposalMessage: ProposalMessageData ): Promise { - const entities = await this.oneIdentityApi.getEntities( + const entities = await this.oneIdentityApi.getEntities( 'ESET', `Ident_ESet='${proposalMessage.shortCode}'` ); @@ -96,10 +84,11 @@ export class ESSOneIdentity { public async getPerson( user: Pick - ): Promise { - const entities = await this.oneIdentityApi.getEntities( + ): Promise { + const entities = await this.oneIdentityApi.getEntities( 'Person', - `CentralAccount='${user.oidcSub}'` + `CentralAccount='${user.oidcSub}'`, + ['CCC_EmployeeSubType'] ); return entities[0]?.values; @@ -123,7 +112,7 @@ export class ESSOneIdentity { uidEset: UID_ESet, uidPerson: UID_Person ): Promise { - const { uid } = await this.oneIdentityApi.createEntity( + const { uid } = await this.oneIdentityApi.createEntity( 'PersonHasESET', { UID_ESet: uidEset, @@ -146,12 +135,74 @@ export class ESSOneIdentity { public async getProposalPersonConnections( uidEset: UID_ESet - ): Promise { - const entities = await this.oneIdentityApi.getEntities( + ): Promise { + const entities = await this.oneIdentityApi.getEntities( 'PersonHasESET', `UID_ESet='${uidEset}'` ); return entities.map(({ values }) => values); } + + public async createPersonWantsOrg( + role: PersonWantsOrgRole, + centralAccount: string, + startDate: string, + endDate: string, + customData: string = '' + ): Promise { + const res = + await this.oneIdentityApi.callScript( + 'SCProposalSiteAccess', + [ + role, // access type + centralAccount, // requester + centralAccount, // recipient + startDate, + endDate, + customData, // PersonWantsOrg.CustomProperty04 + '', // UID_PersonWantsOrg (empty for new) + ] + ); + + if (!res.IsSuccess) + throw new Error('Failed to create site access: ' + res.Message); + + return res.Data; + } + + public async cancelPersonWantsOrg(uidPersonWantsOrg: string): Promise { + const res = + await this.oneIdentityApi.callScript( + 'SCProposalSiteAccessCancel', + [uidPersonWantsOrg] + ); + + if (!res.IsSuccess) + throw new Error('Failed to cancel site access:' + res.Message); + } + + public async getPersonWantsOrg( + uidPerson: UID_Person, + displayOrgs: PersonWantsOrgRole[] = [ + PersonWantsOrgRole.SITE_ACCESS, + PersonWantsOrgRole.SYSTEM_ACCESS, + ] + ): Promise { + const entities = await this.oneIdentityApi.getEntities( + 'PersonWantsOrg', + `UID_PersonOrdered='${uidPerson}' AND (${displayOrgs + .map((org) => `DisplayOrg='${org}'`) + .join(' OR ')})`, + [ + 'ValidFrom', + 'ValidUntil', + 'OrderState', + 'DisplayOrg', + 'CustomProperty04', + ] + ); + + return entities.map(({ values }) => values); + } } diff --git a/src/queue/consumers/oneidentity/utils/OneIdentityApi.spec.ts b/src/queue/consumers/oneidentity/utils/OneIdentityApi.spec.ts index 6ede6de9..ac6e232a 100644 --- a/src/queue/consumers/oneidentity/utils/OneIdentityApi.spec.ts +++ b/src/queue/consumers/oneidentity/utils/OneIdentityApi.spec.ts @@ -47,6 +47,23 @@ describe('OneIdentityApi', () => { } ); }); + + it('should throw error when no cookie is set', async () => { + (axios.post as jest.Mock).mockResolvedValueOnce({ + headers: {}, + }); + + await expect(api.login('user', 'password')).rejects.toThrow( + 'No cookie set' + ); + }); + + it('should handle API errors during login', async () => { + const errorMessage = 'Authentication failed'; + (axios.post as jest.Mock).mockRejectedValueOnce(new Error(errorMessage)); + + await expect(api.login('user', 'password')).rejects.toThrow(errorMessage); + }); }); describe('logout', () => { @@ -87,6 +104,18 @@ describe('OneIdentityApi', () => { }); expect(result).toEqual(mockEntities); }); + + it('should get entities with display columns successfully', async () => { + const mockEntities = [{ id: 1, name: 'test' }]; + (axios.get as jest.Mock).mockResolvedValueOnce({ data: mockEntities }); + + const result = await api.getEntities('testTable', 'id=1', ['name']); + + expect(axios.get).toHaveBeenCalledWith('/entities/testTable', { + params: { where: 'id=1', displayColumns: 'name' }, + }); + expect(result).toEqual(mockEntities); + }); }); describe('deleteEntity', () => { @@ -98,4 +127,29 @@ describe('OneIdentityApi', () => { expect(axios.delete).toHaveBeenCalledWith('/entity/testTable/1'); }); }); + + describe('callScript', () => { + it('should call script successfully', async () => { + const mockResult = { success: true, data: 'script result' }; + (axios.put as jest.Mock).mockResolvedValueOnce({ data: mockResult }); + + const scriptParams = ['param1', 'param2']; + const result = await api.callScript('TestScript', scriptParams); + + expect(axios.put).toHaveBeenCalledWith('/script/TestScript', { + parameters: scriptParams, + returnRawResult: true, + }); + expect(result).toEqual(mockResult); + }); + + it('should handle errors during script calls', async () => { + const errorMessage = 'Script execution failed'; + (axios.put as jest.Mock).mockRejectedValueOnce(new Error(errorMessage)); + + await expect(api.callScript('TestScript', [])).rejects.toThrow( + errorMessage + ); + }); + }); }); diff --git a/src/queue/consumers/oneidentity/utils/OneIdentityApi.ts b/src/queue/consumers/oneidentity/utils/OneIdentityApi.ts index 99e9e015..20c2045b 100644 --- a/src/queue/consumers/oneidentity/utils/OneIdentityApi.ts +++ b/src/queue/consumers/oneidentity/utils/OneIdentityApi.ts @@ -89,13 +89,19 @@ export class OneIdentityApi { public async getEntities( table: string, - where: string + where: string, + displayColumns?: (keyof T)[] ): Promise[]> { const { data } = await this.axiosInstance.get< T, AxiosResponse[]> >(`/entities/${table}`, { - params: { where }, + params: { + where, + ...(displayColumns && displayColumns.length > 0 + ? { displayColumns: displayColumns.join(',') } + : {}), + }, }); return data; @@ -104,4 +110,16 @@ export class OneIdentityApi { public deleteEntity(table: string, uid: string): Promise { return this.axiosInstance.delete(`/entity/${table}/${uid}`); } + + public async callScript(name: string, parameters: string[]): Promise { + const { data } = await this.axiosInstance.put>( + `/script/${name}`, + { + parameters, + returnRawResult: true, // One Identity API returns the result as a string from the script. Axios will be able to JSON parse it. + } + ); + + return data; + } } diff --git a/src/queue/consumers/oneidentity/utils/interfaces/Eset.ts b/src/queue/consumers/oneidentity/utils/interfaces/Eset.ts new file mode 100644 index 00000000..81e2b3a6 --- /dev/null +++ b/src/queue/consumers/oneidentity/utils/interfaces/Eset.ts @@ -0,0 +1,10 @@ +import { UID_ESetType } from './EsetType'; + +export type UID_ESet = string; + +export interface Eset { + UID_ESet: UID_ESet; + UID_ESetType: UID_ESetType; + Ident_ESet: string; + DisplayName: string; +} diff --git a/src/queue/consumers/oneidentity/utils/interfaces/EsetType.ts b/src/queue/consumers/oneidentity/utils/interfaces/EsetType.ts new file mode 100644 index 00000000..83113e56 --- /dev/null +++ b/src/queue/consumers/oneidentity/utils/interfaces/EsetType.ts @@ -0,0 +1,6 @@ +export type UID_ESetType = string; + +export interface EsetType { + Ident_ESetType: string; + UID_ESetType: UID_ESetType; +} diff --git a/src/queue/consumers/oneidentity/utils/interfaces/Person.ts b/src/queue/consumers/oneidentity/utils/interfaces/Person.ts new file mode 100644 index 00000000..a5dfc121 --- /dev/null +++ b/src/queue/consumers/oneidentity/utils/interfaces/Person.ts @@ -0,0 +1,13 @@ +export enum IdentityType { + ESSSCIENCEUSER = 'ESSSCIENCEUSER', + EMPLOYEEDK = 'EMPLOYEEDK', +} + +export type UID_Person = string; + +export interface Person { + CCC_EmployeeSubType: IdentityType; + CentralAccount: string; + InternalName: string; + UID_Person: UID_Person; +} diff --git a/src/queue/consumers/oneidentity/utils/interfaces/PersonHasESET.ts b/src/queue/consumers/oneidentity/utils/interfaces/PersonHasESET.ts new file mode 100644 index 00000000..a307c90a --- /dev/null +++ b/src/queue/consumers/oneidentity/utils/interfaces/PersonHasESET.ts @@ -0,0 +1,7 @@ +import { UID_ESet } from './Eset'; +import { UID_Person } from './Person'; + +export interface PersonHasESET { + UID_Person: UID_Person; + UID_ESet: UID_ESet; +} diff --git a/src/queue/consumers/oneidentity/utils/interfaces/PersonWantsOrg.ts b/src/queue/consumers/oneidentity/utils/interfaces/PersonWantsOrg.ts new file mode 100644 index 00000000..47cd44df --- /dev/null +++ b/src/queue/consumers/oneidentity/utils/interfaces/PersonWantsOrg.ts @@ -0,0 +1,93 @@ +export enum OrderState { + GRANTED = 'Granted', + ABORTED = 'Aborted', + WAITING = 'Waiting', + ASSIGNED = 'Assigned', + UNSUBSCRIBED = 'Unsubscribed', + ORDERPRODUCT = 'OrderProduct', +} + +export enum PersonWantsOrgRole { + SYSTEM_ACCESS = 'Experiment visit - system access', + SITE_ACCESS = 'Experiment visit - site access', +} + +export interface PersonWantsOrg { + AdditionalData: string; + CCC_CustomDate01: string; + CCC_CustomPerson01: string; + CheckResult: number; + CheckResultDetail: string; + CustomProperty01: string; + CustomProperty02: string; + CustomProperty03: string; + CustomProperty04: string; + CustomProperty05: string; + CustomProperty06: string; + CustomProperty07: string; + CustomProperty08: string; + CustomProperty09: string; + CustomProperty10: string; + DateActivated: string; + DateDeactivated: string; + DateHead: string; + DecisionLevel: number; + DisplayObjectKeyAssignment: string; + DisplayOrg: PersonWantsOrgRole; + DisplayOrgParent: string; + DisplayOrgParentOfParent: string; + DisplayPersonHead: string; + DisplayPersonInserted: string; + DisplayPersonOrdered: string; + GenProcID: string; + IsCrossFunctional: boolean; + IsOptionalChild: boolean; + IsOrderForWorkDesk: boolean; + IsReserved: boolean; + ObjectKeyAssignment: string; + ObjectKeyElementUsedInAssign: string; + ObjectKeyFinal: string; + ObjectKeyOrdered: string; + ObjectKeyOrgUsedInAssign: string; + OrderDate: string; + OrderDetail1: string; + OrderDetail2: string; + OrderReason: string; + OrderState: OrderState; + PeerGroupFactor: number; + PWOPriority: number; + Quantity: number; + ReasonHead: string; + Recommendation: number; + RecommendationDetail: string; + UID_Department: string; + UID_ITShopOrgFinal: string; + UID_Org: string; + UID_OrgParent: string; + UID_OrgParentOfParent: string; + UID_PersonHead: string; + UID_PersonInserted: string; + UID_PersonOrdered: string; + UID_PersonWantsOrg: string; + UID_PersonWantsOrgParent: string; + UID_ProfitCenter: string; + UID_PWOState: string; + UID_QERJustification: string; + UID_QERJustificationOrder: string; + UID_QERResourceType: string; + UID_QERWorkingMethod: string; + UID_ShoppingCartOrder: string; + UID_WorkDeskOrdered: string; + UiOrderState: string; + ValidFrom: string; + ValidUntil: string; + ValidUntilProlongation: string; + ValidUntilUnsubscribe: string; + XDateInserted: string; + XDateUpdated: string; + XMarkedForDeletion: number; + XObjectKey: string; + XTouched: string; + XUserInserted: string; + XUserUpdated: string; +} diff --git a/src/queue/consumers/oneidentity/utils/interfaces/SCProposalSiteAccessResponse.ts b/src/queue/consumers/oneidentity/utils/interfaces/SCProposalSiteAccessResponse.ts new file mode 100644 index 00000000..1ae848f2 --- /dev/null +++ b/src/queue/consumers/oneidentity/utils/interfaces/SCProposalSiteAccessResponse.ts @@ -0,0 +1,14 @@ +import { PersonWantsOrg } from './PersonWantsOrg'; + +export interface SiteAccessResponse { + IsSuccess: boolean; + Message: string | null; +} + +export interface SCProposalSiteAccessResponse extends SiteAccessResponse { + Data: PersonWantsOrg[]; +} + +export interface SCProposalSiteAccessCancelResponse extends SiteAccessResponse { + Data: []; +} diff --git a/src/queue/consumers/oneidentity/utils/interfaces/VisitMessage.ts b/src/queue/consumers/oneidentity/utils/interfaces/VisitMessage.ts new file mode 100644 index 00000000..7b60bd35 --- /dev/null +++ b/src/queue/consumers/oneidentity/utils/interfaces/VisitMessage.ts @@ -0,0 +1,5 @@ +export interface VisitMessage { + startAt: string; + endAt: string; + visitorId: string; +} diff --git a/src/queue/consumers/oneidentity/utils/validateRequiredProposalMessageFields.spec.ts b/src/queue/consumers/oneidentity/utils/validateProposalMessage.spec.ts similarity index 63% rename from src/queue/consumers/oneidentity/utils/validateRequiredProposalMessageFields.spec.ts rename to src/queue/consumers/oneidentity/utils/validateProposalMessage.spec.ts index f16bd774..eb26d670 100644 --- a/src/queue/consumers/oneidentity/utils/validateRequiredProposalMessageFields.spec.ts +++ b/src/queue/consumers/oneidentity/utils/validateProposalMessage.spec.ts @@ -1,10 +1,10 @@ -import { validateRequiredProposalMessageFields } from './validateRequiredProposalMessageFields'; +import { validateProposalMessage } from './validateProposalMessage'; -describe('validateRequiredProposalMessageFields', () => { +describe('validateProposalMessage', () => { it('should throw an error if message is not an object', () => { const message = 'not an object'; - expect(() => validateRequiredProposalMessageFields(message)).toThrow( + expect(() => validateProposalMessage(message)).toThrow( 'Invalid proposal message' ); }); @@ -12,7 +12,7 @@ describe('validateRequiredProposalMessageFields', () => { it('should throw an error if message is null', () => { const message = null; - expect(() => validateRequiredProposalMessageFields(message)).toThrow( + expect(() => validateProposalMessage(message)).toThrow( 'Invalid proposal message' ); }); @@ -23,7 +23,7 @@ describe('validateRequiredProposalMessageFields', () => { members: [], }; - expect(() => validateRequiredProposalMessageFields(message)).toThrow( + expect(() => validateProposalMessage(message)).toThrow( 'Invalid proposal message' ); }); @@ -34,7 +34,7 @@ describe('validateRequiredProposalMessageFields', () => { members: [], }; - expect(() => validateRequiredProposalMessageFields(message)).toThrow( + expect(() => validateProposalMessage(message)).toThrow( 'Invalid proposal message' ); }); @@ -45,7 +45,7 @@ describe('validateRequiredProposalMessageFields', () => { proposer: {}, }; - expect(() => validateRequiredProposalMessageFields(message)).toThrow( + expect(() => validateProposalMessage(message)).toThrow( 'Invalid proposal message' ); }); @@ -57,6 +57,6 @@ describe('validateRequiredProposalMessageFields', () => { members: [], }; - expect(validateRequiredProposalMessageFields(message)).toEqual(message); + expect(validateProposalMessage(message)).toEqual(message); }); }); diff --git a/src/queue/consumers/oneidentity/utils/validateRequiredProposalMessageFields.ts b/src/queue/consumers/oneidentity/utils/validateProposalMessage.ts similarity index 69% rename from src/queue/consumers/oneidentity/utils/validateRequiredProposalMessageFields.ts rename to src/queue/consumers/oneidentity/utils/validateProposalMessage.ts index 072590e6..67227867 100644 --- a/src/queue/consumers/oneidentity/utils/validateRequiredProposalMessageFields.ts +++ b/src/queue/consumers/oneidentity/utils/validateProposalMessage.ts @@ -1,12 +1,10 @@ import { ProposalMessageData } from '../../../../models/ProposalMessage'; // For OneIdentity, only these fields are required -export function validateRequiredProposalMessageFields( +export function validateProposalMessage( message: any ): ProposalMessageData | never { if ( - typeof message !== 'object' || - message === null || message?.shortCode === undefined || message?.proposer === undefined || message?.members === undefined @@ -14,5 +12,5 @@ export function validateRequiredProposalMessageFields( throw new Error('Invalid proposal message'); } - return message as ProposalMessageData; + return message; } diff --git a/src/queue/consumers/oneidentity/utils/validateVisitMessage.spec.ts b/src/queue/consumers/oneidentity/utils/validateVisitMessage.spec.ts new file mode 100644 index 00000000..7250b75d --- /dev/null +++ b/src/queue/consumers/oneidentity/utils/validateVisitMessage.spec.ts @@ -0,0 +1,62 @@ +import { validateVisitMessage } from './validateVisitMessage'; + +describe('validateVisitMessage', () => { + it('should throw an error if message is not an object', () => { + const message = 'not an object'; + + expect(() => validateVisitMessage(message)).toThrow( + 'Invalid Visit message' + ); + }); + + it('should throw an error if message is null', () => { + const message = null; + + expect(() => validateVisitMessage(message)).toThrow( + 'Invalid Visit message' + ); + }); + + it('should throw an error if visitorId is undefined', () => { + const message = { + startAt: '2023-01-01T00:00:00Z', + endAt: '2023-01-02T00:00:00Z', + }; + + expect(() => validateVisitMessage(message)).toThrow( + 'Invalid Visit message' + ); + }); + + it('should throw an error if startAt is undefined', () => { + const message = { + visitorId: 'visitor123', + endAt: '2023-01-02T00:00:00Z', + }; + + expect(() => validateVisitMessage(message)).toThrow( + 'Invalid Visit message' + ); + }); + + it('should throw an error if endAt is undefined', () => { + const message = { + visitorId: 'visitor123', + startAt: '2023-01-01T00:00:00Z', + }; + + expect(() => validateVisitMessage(message)).toThrow( + 'Invalid Visit message' + ); + }); + + it('should return the message if visitorId, startAt, and endAt are defined', () => { + const message = { + visitorId: 'visitor123', + startAt: '2023-01-01T00:00:00Z', + endAt: '2023-01-02T00:00:00Z', + }; + + expect(validateVisitMessage(message)).toEqual(message); + }); +}); diff --git a/src/queue/consumers/oneidentity/utils/validateVisitMessage.ts b/src/queue/consumers/oneidentity/utils/validateVisitMessage.ts new file mode 100644 index 00000000..f532889a --- /dev/null +++ b/src/queue/consumers/oneidentity/utils/validateVisitMessage.ts @@ -0,0 +1,13 @@ +import { VisitMessage } from './interfaces/VisitMessage'; + +export function validateVisitMessage(message: any): VisitMessage | never { + if ( + message?.visitorId === undefined || + message?.startAt === undefined || + message?.endAt === undefined + ) { + throw new Error('Invalid Visit message'); + } + + return message; +} From a4b1faf41605f408463676d1ed177584016e9976 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 25 Mar 2025 01:10:36 +0000 Subject: [PATCH 18/25] build(deps-dev): bump ts-jest from 29.2.6 to 29.3.0 Bumps [ts-jest](https://github.com/kulshekhar/ts-jest) from 29.2.6 to 29.3.0. - [Release notes](https://github.com/kulshekhar/ts-jest/releases) - [Changelog](https://github.com/kulshekhar/ts-jest/blob/main/CHANGELOG.md) - [Commits](https://github.com/kulshekhar/ts-jest/compare/v29.2.6...v29.3.0) --- updated-dependencies: - dependency-name: ts-jest dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- package-lock.json | 37 ++++++++++++++++++++++++++++++------- package.json | 2 +- 2 files changed, 31 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index d00eeffe..f23fbee9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -42,7 +42,7 @@ "jest": "^29.7.0", "lint-staged": "^15.4.3", "prettier": "3.3.3", - "ts-jest": "^29.2.6", + "ts-jest": "^29.3.0", "typescript": "^5.7.2" }, "engines": { @@ -8226,9 +8226,9 @@ } }, "node_modules/ts-jest": { - "version": "29.2.6", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.2.6.tgz", - "integrity": "sha512-yTNZVZqc8lSixm+QGVFcPe6+yj7+TWZwIesuOWvfcn4B9bz5x4NDzVCQQjOs7Hfouu36aEqfEbo9Qpo+gq8dDg==", + "version": "29.3.0", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.3.0.tgz", + "integrity": "sha512-4bfGBX7Gd1Aqz3SyeDS9O276wEU/BInZxskPrbhZLyv+c1wskDCqDFMJQJLWrIr/fKoAH4GE5dKUlrdyvo+39A==", "dev": true, "license": "MIT", "dependencies": { @@ -8240,6 +8240,7 @@ "lodash.memoize": "^4.1.2", "make-error": "^1.3.6", "semver": "^7.7.1", + "type-fest": "^4.37.0", "yargs-parser": "^21.1.1" }, "bin": { @@ -8274,6 +8275,19 @@ } } }, + "node_modules/ts-jest/node_modules/type-fest": { + "version": "4.38.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.38.0.tgz", + "integrity": "sha512-2dBz5D5ycHIoliLYLi0Q2V7KRaDlH0uWIvmk7TYlAg5slqwiPv1ezJdZm1QEM0xgk29oYWMCbIG7E6gHpvChlg==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ts-node": { "version": "10.9.1", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", @@ -14753,9 +14767,9 @@ "requires": {} }, "ts-jest": { - "version": "29.2.6", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.2.6.tgz", - "integrity": "sha512-yTNZVZqc8lSixm+QGVFcPe6+yj7+TWZwIesuOWvfcn4B9bz5x4NDzVCQQjOs7Hfouu36aEqfEbo9Qpo+gq8dDg==", + "version": "29.3.0", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.3.0.tgz", + "integrity": "sha512-4bfGBX7Gd1Aqz3SyeDS9O276wEU/BInZxskPrbhZLyv+c1wskDCqDFMJQJLWrIr/fKoAH4GE5dKUlrdyvo+39A==", "dev": true, "requires": { "bs-logger": "^0.2.6", @@ -14766,7 +14780,16 @@ "lodash.memoize": "^4.1.2", "make-error": "^1.3.6", "semver": "^7.7.1", + "type-fest": "^4.37.0", "yargs-parser": "^21.1.1" + }, + "dependencies": { + "type-fest": { + "version": "4.38.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.38.0.tgz", + "integrity": "sha512-2dBz5D5ycHIoliLYLi0Q2V7KRaDlH0uWIvmk7TYlAg5slqwiPv1ezJdZm1QEM0xgk29oYWMCbIG7E6gHpvChlg==", + "dev": true + } } }, "ts-node": { diff --git a/package.json b/package.json index da28f0e8..0e0b8c54 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "jest": "^29.7.0", "lint-staged": "^15.4.3", "prettier": "3.3.3", - "ts-jest": "^29.2.6", + "ts-jest": "^29.3.0", "typescript": "^5.7.2" }, "dependencies": { From 9e844afefe4bd9fec05378876592d4b478522de1 Mon Sep 17 00:00:00 2001 From: janosbabik <143906591+janosbabik@users.noreply.github.com> Date: Fri, 4 Apr 2025 10:38:44 +0200 Subject: [PATCH 19/25] refactor: remove event types and simplify event handling (#529) --- src/models/Event.ts | 2 -- .../consumers/ChatroomCreationQueueConsumer.ts | 2 -- .../scicatProposal/consumers/FolderCreationQueueConsumer.ts | 6 +----- .../consumers/ProposalCreationQueueConsumer.ts | 4 ++-- .../consumers/visa/consumers/syncProposalQueueConsumer.ts | 2 -- 5 files changed, 3 insertions(+), 13 deletions(-) diff --git a/src/models/Event.ts b/src/models/Event.ts index 335d7216..2ed1cff6 100644 --- a/src/models/Event.ts +++ b/src/models/Event.ts @@ -1,6 +1,4 @@ export enum Event { - PROPOSAL_STATUS_CHANGED_BY_WORKFLOW = 'PROPOSAL_STATUS_CHANGED_BY_WORKFLOW', - PROPOSAL_STATUS_CHANGED_BY_USER = 'PROPOSAL_STATUS_CHANGED_BY_USER', PROPOSAL_STATUS_ACTION_EXECUTED = 'PROPOSAL_STATUS_ACTION_EXECUTED', PROPOSAL_ACCEPTED = 'PROPOSAL_ACCEPTED', PROPOSAL_UPDATED = 'PROPOSAL_UPDATED', diff --git a/src/queue/consumers/scicat/scicatProposal/consumers/ChatroomCreationQueueConsumer.ts b/src/queue/consumers/scicat/scicatProposal/consumers/ChatroomCreationQueueConsumer.ts index 748d700b..cc431a52 100644 --- a/src/queue/consumers/scicat/scicatProposal/consumers/ChatroomCreationQueueConsumer.ts +++ b/src/queue/consumers/scicat/scicatProposal/consumers/ChatroomCreationQueueConsumer.ts @@ -8,8 +8,6 @@ import { validateProposalMessage } from '../../../utils/validateProposalMessage' import { createChatroom } from '../consumerCallbacks/createChatroom'; const EVENT_TYPES = [ - Event.PROPOSAL_STATUS_CHANGED_BY_WORKFLOW, - Event.PROPOSAL_STATUS_CHANGED_BY_USER, Event.PROPOSAL_STATUS_ACTION_EXECUTED, Event.PROPOSAL_UPDATED, ]; diff --git a/src/queue/consumers/scicat/scicatProposal/consumers/FolderCreationQueueConsumer.ts b/src/queue/consumers/scicat/scicatProposal/consumers/FolderCreationQueueConsumer.ts index e734aa10..fad6dc2e 100644 --- a/src/queue/consumers/scicat/scicatProposal/consumers/FolderCreationQueueConsumer.ts +++ b/src/queue/consumers/scicat/scicatProposal/consumers/FolderCreationQueueConsumer.ts @@ -7,11 +7,7 @@ import { hasTriggeringType } from '../../../utils/hasTriggeringType'; import { validateProposalMessage } from '../../../utils/validateProposalMessage'; import { proposalFoldersCreation } from '../consumerCallbacks/proposalFoldersCreation'; -const EVENT_TYPES = [ - Event.PROPOSAL_STATUS_CHANGED_BY_WORKFLOW, - Event.PROPOSAL_STATUS_CHANGED_BY_USER, - Event.PROPOSAL_STATUS_ACTION_EXECUTED, -]; +const EVENT_TYPES = [Event.PROPOSAL_STATUS_ACTION_EXECUTED]; const triggeringStatuses = process.env.PROPOSAL_FOLDERS_CREATION_TRIGGERING_STATUSES?.split(', '); diff --git a/src/queue/consumers/scicat/scicatProposal/consumers/ProposalCreationQueueConsumer.ts b/src/queue/consumers/scicat/scicatProposal/consumers/ProposalCreationQueueConsumer.ts index f3862bb2..e6f790c2 100644 --- a/src/queue/consumers/scicat/scicatProposal/consumers/ProposalCreationQueueConsumer.ts +++ b/src/queue/consumers/scicat/scicatProposal/consumers/ProposalCreationQueueConsumer.ts @@ -8,10 +8,10 @@ import { validateProposalMessage } from '../../../utils/validateProposalMessage' import { upsertProposalInScicat } from '../consumerCallbacks/upsertProposalInScicat'; const EVENT_TYPES = [ - Event.PROPOSAL_STATUS_CHANGED_BY_WORKFLOW, - Event.PROPOSAL_STATUS_CHANGED_BY_USER, Event.PROPOSAL_STATUS_ACTION_EXECUTED, + Event.PROPOSAL_UPDATED, ]; + const triggeringStatuses = process.env.SCICAT_PROPOSAL_TRIGGERING_STATUSES?.split(', '); diff --git a/src/queue/consumers/visa/consumers/syncProposalQueueConsumer.ts b/src/queue/consumers/visa/consumers/syncProposalQueueConsumer.ts index e7f25cbf..1347bb4f 100644 --- a/src/queue/consumers/visa/consumers/syncProposalQueueConsumer.ts +++ b/src/queue/consumers/visa/consumers/syncProposalQueueConsumer.ts @@ -11,8 +11,6 @@ import { syncVisaProposal } from '../consumerCallbacks/syncVisaProposal'; import { sanitizeProposalMessage } from '../utils/sanitizeProposalMessage'; const EVENTS_FOR_HANDLING = [ - Event.PROPOSAL_STATUS_CHANGED_BY_WORKFLOW, - Event.PROPOSAL_STATUS_CHANGED_BY_USER, Event.PROPOSAL_STATUS_ACTION_EXECUTED, Event.PROPOSAL_UPDATED, ]; From 78044fe4e63d1d84645aa35d01a430f4fba19a48 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 30 Apr 2025 16:42:20 +0000 Subject: [PATCH 20/25] build(deps): bump base-x from 4.0.0 to 4.0.1 Bumps [base-x](https://github.com/cryptocoinjs/base-x) from 4.0.0 to 4.0.1. - [Commits](https://github.com/cryptocoinjs/base-x/compare/v4.0.0...v4.0.1) --- updated-dependencies: - dependency-name: base-x dependency-version: 4.0.1 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- package-lock.json | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index f23fbee9..4f5bb036 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2419,9 +2419,10 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, "node_modules/base-x": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/base-x/-/base-x-4.0.0.tgz", - "integrity": "sha512-FuwxlW4H5kh37X/oW59pwTzzTKRzfrrQwhmyspRM7swOEZcHtDZSCt45U6oKgtuFE+WYPblePMVIPR4RZrh/hw==" + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-4.0.1.tgz", + "integrity": "sha512-uAZ8x6r6S3aUM9rbHGVOIsR15U/ZSc82b3ymnCPsT45Gk1DDvhDPdIgB5MrhirZWt+5K0EEPQH985kNqZgNPFw==", + "license": "MIT" }, "node_modules/binary-extensions": { "version": "2.2.0", @@ -10696,9 +10697,9 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, "base-x": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/base-x/-/base-x-4.0.0.tgz", - "integrity": "sha512-FuwxlW4H5kh37X/oW59pwTzzTKRzfrrQwhmyspRM7swOEZcHtDZSCt45U6oKgtuFE+WYPblePMVIPR4RZrh/hw==" + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-4.0.1.tgz", + "integrity": "sha512-uAZ8x6r6S3aUM9rbHGVOIsR15U/ZSc82b3ymnCPsT45Gk1DDvhDPdIgB5MrhirZWt+5K0EEPQH985kNqZgNPFw==" }, "binary-extensions": { "version": "2.2.0", From b5a53372a0e6093733be022080b4b6ef5c51e39c Mon Sep 17 00:00:00 2001 From: Jay Date: Tue, 17 Jun 2025 15:26:36 +0200 Subject: [PATCH 21/25] Merge pull request #558 from UserOfficeProject/SWAP-4738-connector-re-fetch-accesstoken-on-invalid-token-e refactor: token refresh in chatRoomCreateConsumer and added unit tests --- src/index.ts | 1 - .../nicos/NicosTopicConsumer.spec.ts | 127 ++++++++++++++++++ .../consumers/nicos/NicosTopicConsumer.ts | 17 ++- .../consumerCallbacks/postNicosMessage.ts | 19 --- .../nicos/utils/validateNicosMessage.spec.ts | 63 +++++++++ .../consumerCallbacks/createChatroom.ts | 3 + src/services/synapse/SynapseService.ts | 39 ++++-- 7 files changed, 233 insertions(+), 36 deletions(-) create mode 100644 src/queue/consumers/nicos/NicosTopicConsumer.spec.ts delete mode 100644 src/queue/consumers/nicos/consumerCallbacks/postNicosMessage.ts create mode 100644 src/queue/consumers/nicos/utils/validateNicosMessage.spec.ts diff --git a/src/index.ts b/src/index.ts index f3f5635d..23409f92 100644 --- a/src/index.ts +++ b/src/index.ts @@ -49,7 +49,6 @@ async function bootstrap() { const enableOneIdentityIntegration = str2Bool( process.env.ENABLE_ONE_IDENTITY_INTEGRATION as string ); - const enableSyncVisaProposals = str2Bool( process.env.ENABLE_SYNC_VISA_PROPOSALS as string ); diff --git a/src/queue/consumers/nicos/NicosTopicConsumer.spec.ts b/src/queue/consumers/nicos/NicosTopicConsumer.spec.ts new file mode 100644 index 00000000..f67ad5f4 --- /dev/null +++ b/src/queue/consumers/nicos/NicosTopicConsumer.spec.ts @@ -0,0 +1,127 @@ +jest.mock('tsyringe', () => ({ + container: { + resolve: jest.fn(), + }, +})); +jest.mock('./utils/validateNicosMessage'); +jest.mock('@user-office-software/duo-logger', () => ({ + logger: { + logError: jest.fn(), + }, +})); + +import { logger } from '@user-office-software/duo-logger'; +import { container } from 'tsyringe'; + +import { TopicSciChatConsumer } from './NicosTopicConsumer'; +import { validateNicosMessage } from './utils/validateNicosMessage'; +import { Tokens } from '../../../config/Tokens'; + +const mockLogin = jest.fn(); +const mockSendMessage = jest.fn(); + +const mockSynapseService = { + login: mockLogin, + sendMessage: mockSendMessage, +}; + +const mockConsume = jest.fn(); + +const mockConsumerService = { + consume: mockConsume, +}; + +describe('TopicSciChatConsumer', () => { + const topic = 'test-topic'; + const kafkaClientId = 'test-client-id'; + const validMessageData = { + proposal: 'p1', + instrument: 'testInstrument', + source: 'test-source', + message: 'hello', + }; + + beforeEach(() => { + jest.clearAllMocks(); + (container.resolve as jest.Mock).mockReturnValue(mockSynapseService); + (validateNicosMessage as jest.Mock).mockReturnValue(validMessageData); + process.env.KAFKA_CLIENTID = kafkaClientId; + }); + + it('should login and consume messages', async () => { + const consumer = new TopicSciChatConsumer(mockConsumerService as any); + + await consumer.start(topic); + + expect(container.resolve).toHaveBeenCalledWith(Tokens.SynapseService); + expect(mockLogin).toHaveBeenCalledWith('TopicSciChatConsumer'); + expect(mockConsume).toHaveBeenCalledWith( + kafkaClientId, + { topics: [topic] }, + expect.objectContaining({ + eachMessage: expect.any(Function), + }) + ); + }); + + it('should process a valid message and call sendMessage', async () => { + const consumer = new TopicSciChatConsumer(mockConsumerService as any); + await consumer.start(topic); + + // Simulate eachMessage handler + const { eachMessage } = mockConsume.mock.calls[0][2]; + const fakeKafkaMessage = { + value: Buffer.from(JSON.stringify(validMessageData)), + offset: '123', + }; + + await eachMessage({ message: fakeKafkaMessage }); + + expect(validateNicosMessage).toHaveBeenCalledWith(validMessageData); + expect(mockSendMessage).toHaveBeenCalledWith('p1', 'hello'); + expect(logger.logError).not.toHaveBeenCalled(); + }); + + it('should log error and not throw if message processing fails', async () => { + const consumer = new TopicSciChatConsumer(mockConsumerService as any); + await consumer.start(topic); + + // Simulate eachMessage handler with invalid JSON + const { eachMessage } = mockConsume.mock.calls[0][2]; + const fakeKafkaMessage = { + value: Buffer.from('not-json'), + offset: '456', + }; + + await eachMessage({ message: fakeKafkaMessage }); + + expect(logger.logError).toHaveBeenCalledWith('Failed processing message', { + messageOffset: '456', + reason: expect.any(String), + }); + expect(mockSendMessage).not.toHaveBeenCalled(); + }); + + it('should log error if validateNicosMessage throws', async () => { + (validateNicosMessage as jest.Mock).mockImplementation(() => { + throw new Error('validation failed'); + }); + + const consumer = new TopicSciChatConsumer(mockConsumerService as any); + await consumer.start(topic); + + const { eachMessage } = mockConsume.mock.calls[0][2]; + const fakeKafkaMessage = { + value: Buffer.from(JSON.stringify(validMessageData)), + offset: '789', + }; + + await eachMessage({ message: fakeKafkaMessage }); + + expect(logger.logError).toHaveBeenCalledWith('Failed processing message', { + messageOffset: '789', + reason: 'validation failed', + }); + expect(mockSendMessage).not.toHaveBeenCalled(); + }); +}); diff --git a/src/queue/consumers/nicos/NicosTopicConsumer.ts b/src/queue/consumers/nicos/NicosTopicConsumer.ts index 41b284f9..d06bffc5 100644 --- a/src/queue/consumers/nicos/NicosTopicConsumer.ts +++ b/src/queue/consumers/nicos/NicosTopicConsumer.ts @@ -1,12 +1,19 @@ import { logger } from '@user-office-software/duo-logger'; +import { container } from 'tsyringe'; import ConsumerService from '../KafkaConsumer'; -import { postNicosMessage } from './consumerCallbacks/postNicosMessage'; import { validateNicosMessage } from './utils/validateNicosMessage'; +import { Tokens } from '../../../config/Tokens'; +import { SynapseService } from '../../../services/synapse/SynapseService'; export class TopicSciChatConsumer { constructor(private _consumer: ConsumerService) {} async start(topic: string) { + const synapseService: SynapseService = container.resolve( + Tokens.SynapseService + ); + await synapseService.login('TopicSciChatConsumer'); + this._consumer.consume( `${process.env.KAFKA_CLIENTID}`, { topics: [topic] }, @@ -16,10 +23,10 @@ export class TopicSciChatConsumer { const messageData = JSON.parse(message.value?.toString() as string); const validMessageData = validateNicosMessage(messageData); - await postNicosMessage({ - roomName: validMessageData.proposal, - message: validMessageData.message, - }); + await synapseService.sendMessage( + validMessageData.proposal, + validMessageData.message + ); } catch (error) { logger.logError('Failed processing message', { // Note: offset is similar to the index of the message diff --git a/src/queue/consumers/nicos/consumerCallbacks/postNicosMessage.ts b/src/queue/consumers/nicos/consumerCallbacks/postNicosMessage.ts deleted file mode 100644 index ef9c5a07..00000000 --- a/src/queue/consumers/nicos/consumerCallbacks/postNicosMessage.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { container } from 'tsyringe'; - -import { Tokens } from '../../../../config/Tokens'; -import { SynapseService } from '../../../../services/synapse/SynapseService'; - -const postNicosMessage = async ({ - roomName, - message, -}: { - roomName: string; - message: string; -}) => { - const synapseService: SynapseService = container.resolve( - Tokens.SynapseService - ); - await synapseService.sendMessage(roomName, message); -}; - -export { postNicosMessage }; diff --git a/src/queue/consumers/nicos/utils/validateNicosMessage.spec.ts b/src/queue/consumers/nicos/utils/validateNicosMessage.spec.ts new file mode 100644 index 00000000..ef273ea1 --- /dev/null +++ b/src/queue/consumers/nicos/utils/validateNicosMessage.spec.ts @@ -0,0 +1,63 @@ +import { validateNicosMessage } from './validateNicosMessage'; +import { NicosMessageData } from '../../../../models/KafkaTypes'; +// Mock NicosMessageData type since it's imported from another file + +describe('validateNicosMessage', () => { + const validMessage: NicosMessageData = { + proposal: 'proposal1', + instrument: 'instrument1', + source: 'source1', + message: 'message1', + }; + + it('should return the message as ValidNicosMessage when all fields are valid', () => { + const result = validateNicosMessage(validMessage); + expect(result).toEqual(validMessage); + }); + + it('should throw error if proposal is missing', () => { + const msg = { ...validMessage, proposal: undefined }; + expect(() => validateNicosMessage(msg as any)).toThrow( + 'Proposal format is wrong' + ); + }); + + it('should throw error if proposal is not a string', () => { + const msg = { ...validMessage, proposal: 123 as any }; + expect(() => validateNicosMessage(msg)).toThrow('Proposal format is wrong'); + }); + + it('should throw error if instrument is missing', () => { + const msg = { ...validMessage, instrument: undefined as any }; + expect(() => validateNicosMessage(msg)).toThrow( + 'Instrument format is wrong' + ); + }); + + it('should throw error if instrument is not a string', () => { + const msg = { ...validMessage, instrument: 123 as any }; + expect(() => validateNicosMessage(msg)).toThrow( + 'Instrument format is wrong' + ); + }); + + it('should throw error if source is missing', () => { + const msg = { ...validMessage, source: undefined as any }; + expect(() => validateNicosMessage(msg)).toThrow('Source format is wrong'); + }); + + it('should throw error if source is not a string', () => { + const msg = { ...validMessage, source: 123 as any }; + expect(() => validateNicosMessage(msg)).toThrow('Source format is wrong'); + }); + + it('should throw error if message is missing', () => { + const msg = { ...validMessage, message: undefined as any }; + expect(() => validateNicosMessage(msg)).toThrow('message format is wrong'); + }); + + it('should throw error if message is not a string', () => { + const msg = { ...validMessage, message: 123 as any }; + expect(() => validateNicosMessage(msg)).toThrow('message format is wrong'); + }); +}); diff --git a/src/queue/consumers/scicat/scicatProposal/consumerCallbacks/createChatroom.ts b/src/queue/consumers/scicat/scicatProposal/consumerCallbacks/createChatroom.ts index db6e5824..20d0f21f 100644 --- a/src/queue/consumers/scicat/scicatProposal/consumerCallbacks/createChatroom.ts +++ b/src/queue/consumers/scicat/scicatProposal/consumerCallbacks/createChatroom.ts @@ -50,6 +50,7 @@ const createChatroom = async (message: ValidProposalMessageData) => { const synapseService: SynapseService = container.resolve( Tokens.SynapseService ); + await synapseService.login(); const allUsersOnProposal = [...message.members, message.proposer]; const { validUsers, invalidUsers } = validateUsersProfile(allUsersOnProposal); @@ -117,6 +118,8 @@ const createChatroom = async (message: ValidProposalMessageData) => { } } catch (err: unknown) { logger.logException('Error while creating chatroom: ', err, { message }); + } finally { + await synapseService.logout(); } }; diff --git a/src/services/synapse/SynapseService.ts b/src/services/synapse/SynapseService.ts index d0445b58..9bee5e8e 100644 --- a/src/services/synapse/SynapseService.ts +++ b/src/services/synapse/SynapseService.ts @@ -54,13 +54,36 @@ export class SynapseService { baseUrl: serverUrl, fetchFn: axiosFetch, }); - // TODO, If consumer service is started after downtime, and there are some pending messages in the queue // then it could be that queue handler will delegate handling of messages before connection to supabase is established - this.client.loginWithPassword( - serviceAccount.userId, - serviceAccount.password - ); + } + + async login(consumerName = 'ChatroomCreationQueueConsumer') { + if (!serviceAccount.userId) + throw new Error('SYNAPSE_SERVICE_USER is not set'); + if (!serviceAccount.password) + throw new Error('SYNAPSE_SERVICE_PASSWORD is not set'); + + try { + await this.client.loginWithPassword( + serviceAccount.userId, + serviceAccount.password + ); + } catch (error) { + logger.logError(`Failed to login to Synapse from ${consumerName}`, { + error, + }); + throw error; + } + } + + async logout() { + try { + await this.client.logout(); + } catch (error) { + logger.logError('Failed to logout from Synapse', { error }); + throw error; + } } async createRoom(name: string, topic: string, members: ProposalUser[]) { @@ -127,12 +150,6 @@ export class SynapseService { await this.client .sendEvent(roomId, EventType.RoomMessage, messageContent, '') - .then(() => { - logger.logInfo('Success sending message to chatroom ', { - roomId: roomId, - message: message, - }); - }) .catch((reason) => { logger.logError('Failed sending message to chatroom', { roomId: roomId, From 9dc01d08e3b179f5e1f74127ba153b3d02f6f6d4 Mon Sep 17 00:00:00 2001 From: janosbabik <143906591+janosbabik@users.noreply.github.com> Date: Tue, 24 Jun 2025 11:18:22 +0200 Subject: [PATCH 22/25] feat: Improve One Identity Sync: Link visitors to the proposal in OIM (#563) --- ...neIdentityIntegrationQueueConsumer.spec.ts | 38 ++- .../OneIdentityIntegrationQueueConsumer.ts | 10 +- src/queue/consumers/oneidentity/README.md | 129 ++++++++- ...osalAndMembersToOneIdentityHandler.spec.ts | 156 +++++++++-- ...cProposalAndMembersToOneIdentityHandler.ts | 63 +++-- .../syncVisitToOneIdentityHandler.spec.ts | 257 ++++++++++++++++-- .../syncVisitToOneIdentityHandler.ts | 99 ++++++- .../oneidentity/utils/ESSOneIdentity.spec.ts | 168 ++++++++++-- .../oneidentity/utils/ESSOneIdentity.ts | 45 +-- .../utils/interfaces/VisitMessage.ts | 3 + .../oneidentity/utils/isVisitMessage.spec.ts | 58 ++++ .../oneidentity/utils/isVisitMessage.ts | 12 + .../utils/validateVisitMessage.spec.ts | 62 ----- .../oneidentity/utils/validateVisitMessage.ts | 13 - 14 files changed, 896 insertions(+), 217 deletions(-) create mode 100644 src/queue/consumers/oneidentity/utils/isVisitMessage.spec.ts create mode 100644 src/queue/consumers/oneidentity/utils/isVisitMessage.ts delete mode 100644 src/queue/consumers/oneidentity/utils/validateVisitMessage.spec.ts delete mode 100644 src/queue/consumers/oneidentity/utils/validateVisitMessage.ts diff --git a/src/queue/consumers/oneidentity/OneIdentityIntegrationQueueConsumer.spec.ts b/src/queue/consumers/oneidentity/OneIdentityIntegrationQueueConsumer.spec.ts index 4fccfc5f..3bdb9416 100644 --- a/src/queue/consumers/oneidentity/OneIdentityIntegrationQueueConsumer.spec.ts +++ b/src/queue/consumers/oneidentity/OneIdentityIntegrationQueueConsumer.spec.ts @@ -6,6 +6,7 @@ jest.mock('../QueueConsumer', () => ({ })); jest.mock('./consumerCallbacks/syncProposalAndMembersToOneIdentityHandler'); jest.mock('./consumerCallbacks/syncVisitToOneIdentityHandler'); +jest.mock('./utils/isVisitMessage'); jest.mock('axios', () => ({ isAxiosError: jest.fn(), })); @@ -19,6 +20,7 @@ import { syncProposalAndMembersToOneIdentityHandler } from './consumerCallbacks/ import { syncVisitToOneIdentityHandler } from './consumerCallbacks/syncVisitToOneIdentityHandler'; import { OneIdentityIntegrationQueueConsumer } from './OneIdentityIntegrationQueueConsumer'; import { VisitMessage } from './utils/interfaces/VisitMessage'; +import { isVisitMessage } from './utils/isVisitMessage'; import { Event } from '../../../models/Event'; import { ProposalMessageData } from '../../../models/ProposalMessage'; @@ -166,12 +168,13 @@ describe('OneIdentityIntegrationQueueConsumer', () => { visitorId: 'visitor-id', startAt: '2021-01-01T00:00:00Z', endAt: '2021-01-02T00:00:00Z', + proposal: { + shortCode: 'proposal-short-code', + }, } as VisitMessage; const type = Event.VISIT_CREATED; - jest.mock('./utils/validateVisitMessage', () => ({ - validateVisitMessage: jest.fn().mockReturnValue(message), - })); + (isVisitMessage as unknown as jest.Mock).mockReturnValue(true); await consumer.onMessage(type, message as any, {} as MessageProperties); @@ -203,6 +206,9 @@ describe('OneIdentityIntegrationQueueConsumer', () => { visitorId: 'visitor-id', startAt: '2021-01-01T00:00:00Z', endAt: '2021-01-02T00:00:00Z', + proposal: { + shortCode: 'proposal-short-code', + }, } as VisitMessage; const type = Event.VISIT_CREATED; @@ -218,14 +224,12 @@ describe('OneIdentityIntegrationQueueConsumer', () => { response: mockResponse, }); - jest.mock('./utils/validateVisitMessage', () => ({ - validateVisitMessage: jest.fn().mockReturnValue(message), - })); + (isVisitMessage as unknown as jest.Mock).mockReturnValue(true); (isAxiosError as unknown as jest.Mock).mockReturnValueOnce(true); - (syncVisitToOneIdentityHandler as jest.Mock) = jest - .fn() - .mockRejectedValueOnce(axiosError); + (syncVisitToOneIdentityHandler as jest.Mock).mockRejectedValueOnce( + axiosError + ); await expect( consumer.onMessage(type, message as any, {} as MessageProperties) @@ -245,6 +249,22 @@ describe('OneIdentityIntegrationQueueConsumer', () => { } ); }); + + it('should throw an error if the visit message is invalid', async () => { + const message = { some: 'invalid message' }; + const type = Event.VISIT_CREATED; + + (isVisitMessage as unknown as jest.Mock).mockReturnValue(false); + + await expect( + consumer.onMessage(type, message as any, {} as MessageProperties) + ).rejects.toThrow( + `Invalid Visit message received: ${JSON.stringify(message)}` + ); + + expect(syncVisitToOneIdentityHandler).not.toHaveBeenCalled(); + expect(logger.logException).toHaveBeenCalled(); + }); }); }); diff --git a/src/queue/consumers/oneidentity/OneIdentityIntegrationQueueConsumer.ts b/src/queue/consumers/oneidentity/OneIdentityIntegrationQueueConsumer.ts index 44c9133f..7f03eb86 100644 --- a/src/queue/consumers/oneidentity/OneIdentityIntegrationQueueConsumer.ts +++ b/src/queue/consumers/oneidentity/OneIdentityIntegrationQueueConsumer.ts @@ -8,7 +8,7 @@ import { validateProposalMessage } from './utils/validateProposalMessage'; import { Event } from '../../../models/Event'; import { QueueConsumer } from '../QueueConsumer'; import { hasTriggeringType } from '../utils/hasTriggeringType'; -import { validateVisitMessage } from './utils/validateVisitMessage'; +import { isVisitMessage } from './utils/isVisitMessage'; const ONE_IDENTITY_INTEGRATION_QUEUE_NAME = process.env.ONE_IDENTITY_INTEGRATION_QUEUE_NAME || ''; @@ -63,8 +63,12 @@ export class OneIdentityIntegrationQueueConsumer extends QueueConsumer { eventType ); } else if (isVisitEvent) { - const visitMessage = validateVisitMessage(message); - await syncVisitToOneIdentityHandler(visitMessage, eventType); + if (isVisitMessage(message)) + await syncVisitToOneIdentityHandler(message, eventType); + else + throw new Error( + `Invalid Visit message received: ${JSON.stringify(message)}` + ); } logger.logInfo('Message handled by OneIdentityIntegrationQueueConsumer', { diff --git a/src/queue/consumers/oneidentity/README.md b/src/queue/consumers/oneidentity/README.md index ac591d19..3345b5fa 100644 --- a/src/queue/consumers/oneidentity/README.md +++ b/src/queue/consumers/oneidentity/README.md @@ -17,6 +17,7 @@ The handler manages site and system access in One Identity based on visit creati ### Key Relationships - System access is linked to site access via `CustomProperty04` which stores the site access UID - Both access types are identified by specific roles in `PersonWantsOrgRole` enum +- Proposal's short code is stored in `CustomProperty04` of the system access record ## Process Flow Chart @@ -99,4 +100,130 @@ The handler manages site and system access in One Identity based on visit creati ### Error Handling - Proper error messages when person or access records are not found -- Always performs logout in finally block to ensure clean session management \ No newline at end of file +- Always performs logout in finally block to ensure clean session management + +--- + +## One Identity Proposal and Member Sync + +### Purpose +The handler synchronizes proposal information and its members (proposer and co-proposers) with One Identity. This ensures that proposals and their associated personnel are accurately represented and connected in One Identity. + +### Process Overview +- Triggered by `PROPOSAL_ACCEPTED` and `PROPOSAL_UPDATED` events. +- **Login**: Establishes a session with One Identity. +- **Proposal Retrieval/Creation**: + - For both event types, it first attempts to retrieve the proposal (`ESet`) from One Identity using the proposal data. + - If `PROPOSAL_ACCEPTED` event: + - If the proposal does not exist, it creates the proposal in One Identity. + - If creation fails, an error is thrown. + - If `PROPOSAL_UPDATED` event: + - If the proposal does not exist, the process logs this information and concludes, as there's no existing record to update. +- **User Synchronization**: + - Collects all unique user OIDC sub identifiers from the proposal message (proposer and members). + - Retrieves the corresponding `UID_Person` for these users from One Identity. + - Logs an error if any users from the proposal message are not found in One Identity. +- **Connection Management**: + - Fetches all existing `PersonHasESET` connections for the identified proposal (`UID_ESet`). + - **Remove Old Connections**: + - Identifies connections in One Identity for persons who are no longer part of the current proposal members list. + - Before removing a connection, it checks if the person has "site access" to the proposal (e.g., as a visitor). + - If the person has site access, their connection to the proposal is *not* removed. + - Otherwise, the outdated connection is removed. + - **Add New Connections**: + - Identifies persons in the current proposal members list who are not yet connected to the proposal in One Identity. + - Creates new `PersonHasESET` connections for these persons. +- **Logout**: Ensures logout from One Identity in a `finally` block, regardless of success or failure. + +### Key Relationships +- **Proposals**: Mapped to `ESet` objects in One Identity. +- **Users**: (Proposers, members) are `Person` objects in One Identity, identified via their OIDC sub. +- **Connections**: The link between a `Person` and an `ESet` is represented by a `PersonHasESET` record. + +### Process Flow Chart + +``` +┌─────────────────────────┐ +│ Proposal Event Received │ +│ (ACCEPTED/UPDATED) │ +└──────────┬──────────────┘ + ↓ +┌─────────────────────────┐ +│ Login to One Identity │ +└──────────┬──────────────┘ + ↓ +┌─────────────────────────┐ +│ Get Proposal (ESet) │ +│ from One Identity │ +└──────────┬──────────────┘ + │ +┌──────────┴──────────┐ +│ Event Type? │ +└────┬───────────┬────┘ + ↓ ↓ +PROPOSAL_ACCEPTED PROPOSAL_UPDATED + │ │ +┌────┴────────┐ │ ┌──────────────────────────┐ +│ ESet Exists?│ No │ ESet Exists? ├─No─┐ +└──────┬───No─┘ │ └──────────┬───────────────┘ │ + Yes │ Yes │ + │ │ │ ┌──────────────────────────┐ + │ ┌───────┴─────────┐ │ │ Log "Proposal not found │ + │ │ Create ESet in │ │ │ for update", Logout & Exit│ + │ │ One Identity │ │ └──────────────────────────┘ + │ └───────┬─────────┘ │ + │ ↓ │ + │ ┌───────┴─────────┐ │ + │ │ ESet Created? ├─No─┼───►Error: Creation Failed, Logout + │ └───────┬─────────┘ │ + Yes Yes │ + └──────────┼─────────────┘ + ↓ +┌─────────────────────────────────┐ +│ Get UIDs for all proposal users │ +│ (proposer & members) via OIDC sub│ +└────────────────┬────────────────┘ + ↓ +┌─────────────────────────────────┐ +│ All users found in One Identity?├─No─►Log Error: Users Missing +└────────────────┬────────────────┘ + Yes + ↓ +┌─────────────────────────────────┐ +│ Get existing PersonHasESET │ +│ connections for the ESet │ +└────────────────┬────────────────┘ + │ +┌────────────────┴────────────────┐ +│ For each existing connection: │ +│ - Is person still in proposal? │ +│ No ─► Has person site access? │ +│ No ─► Remove Connection │ +└────────────────┬────────────────┘ + ↓ +┌─────────────────────────────────┐ +│ For each user in current proposal:│ +│ - Not already connected? │ +│ Yes ─► Add Connection │ +└────────────────┬────────────────┘ + ↓ +┌─────────────────────────────────┐ +│ Log "Connections updated" │ +└────────────────┬────────────────┘ + ↓ +┌─────────────────────────────────┐ +│ Logout from One Identity │ +└─────────────────────────────────┘ +``` + +### Key Implementation Details +- **User Identification**: Users are primarily identified by their `oidcSub` from the proposal message, which is used to look up their `UID_Person` in One Identity. +- **Proposal Identification**: The proposal itself is identified in One Identity using its `shortCode` and other details from the `ProposalMessageData`. +- **Conditional Connection Removal**: A crucial feature is that connections for users no longer in the proposal are only removed if those users do not also have separate site access to that proposal. This prevents inadvertently revoking access for individuals like visitors who might be associated with a proposal through a different mechanism (site access). +- **Idempotency for `PROPOSAL_ACCEPTED`**: If a `PROPOSAL_ACCEPTED` event is processed for a proposal that already exists in One Identity (e.g., due to a retry), the system does not attempt to re-create it but proceeds to synchronize the member connections. +- **Handling `PROPOSAL_UPDATED` for Non-existent Proposals**: If a `PROPOSAL_UPDATED` event is received for a proposal that isn't found in One Identity, the handler logs this and exits gracefully, as there's no record to update. + +### Error Handling +- **Proposal Creation Failure**: If creating a new proposal (`ESet`) in One Identity fails during a `PROPOSAL_ACCEPTED` event, an error is thrown, and the process is halted. +- **User Not Found**: If any users listed in the proposal message (proposer or members) cannot be found in One Identity, an error is logged. The process continues with the users that were found. +- **Logout Guarantee**: The One Identity session is always closed in a `finally` block, ensuring resources are released even if errors occur during the synchronization process. \ No newline at end of file diff --git a/src/queue/consumers/oneidentity/consumerCallbacks/syncProposalAndMembersToOneIdentityHandler.spec.ts b/src/queue/consumers/oneidentity/consumerCallbacks/syncProposalAndMembersToOneIdentityHandler.spec.ts index 909f1edf..ce45e904 100644 --- a/src/queue/consumers/oneidentity/consumerCallbacks/syncProposalAndMembersToOneIdentityHandler.spec.ts +++ b/src/queue/consumers/oneidentity/consumerCallbacks/syncProposalAndMembersToOneIdentityHandler.spec.ts @@ -8,7 +8,7 @@ import { logger } from '@user-office-software/duo-logger'; import { syncProposalAndMembersToOneIdentityHandler } from './syncProposalAndMembersToOneIdentityHandler'; import { Event } from '../../../../models/Event'; import { ProposalMessageData } from '../../../../models/ProposalMessage'; -import { ESSOneIdentity, UserPersonConnection } from '../utils/ESSOneIdentity'; +import { ESSOneIdentity } from '../utils/ESSOneIdentity'; import { UID_ESet } from '../utils/interfaces/Eset'; import { PersonHasESET } from '../utils/interfaces/PersonHasESET'; @@ -25,12 +25,14 @@ const mockOneIdentity: jest.Mocked> = { getPersonWantsOrg: jest.fn(), createPersonWantsOrg: jest.fn(), cancelPersonWantsOrg: jest.fn(), + hasPersonSiteAccessToProposal: jest.fn(), }; const setupMocks = (data: { getProposal: UID_ESet | undefined; getProposalPersonConnections?: PersonHasESET[]; - getPersons?: UserPersonConnection[]; + getPersons?: string[]; + hasPersonSiteAccessToProposalConfig?: { [key: string]: boolean }; }) => { mockOneIdentity.createProposal.mockResolvedValueOnce('proposal-UID_ESet'); mockOneIdentity.getProposal.mockResolvedValueOnce(data.getProposal); @@ -38,17 +40,17 @@ const setupMocks = (data: { data.getProposalPersonConnections ?? [] ); mockOneIdentity.getPersons.mockResolvedValueOnce( - data.getPersons ?? [ - { - oidcSub: 'proposer-oidc-sub', - uidPerson: 'proposer-uid', - }, - { - oidcSub: 'member-oidc-sub', - uidPerson: 'member-uid', - }, - ] + data.getPersons ?? ['proposer-uid', 'member-uid'] ); + if (data.hasPersonSiteAccessToProposalConfig) { + mockOneIdentity.hasPersonSiteAccessToProposal.mockImplementation( + async (uidPerson: string, _proposalUid: string) => { + return data.hasPersonSiteAccessToProposalConfig?.[uidPerson] ?? false; + } + ); + } else { + mockOneIdentity.hasPersonSiteAccessToProposal.mockResolvedValue(false); + } }; const proposalMessage = { @@ -102,12 +104,7 @@ describe('oneIdentityIntegrationHandler', () => { setupMocks({ getProposal: undefined, getProposalPersonConnections: [], - getPersons: [ - { - oidcSub: 'proposer-oidc-sub', - uidPerson: 'proposer-uid', - }, - ], + getPersons: ['proposer-uid'], }); await syncProposalAndMembersToOneIdentityHandler( @@ -116,13 +113,10 @@ describe('oneIdentityIntegrationHandler', () => { ); expect(logger.logError).toHaveBeenCalledWith( - 'Not all users found in One Identity (investigate)', + 'Not all users found in One Identity (Investigate). Missing central accounts:', { - users: [ - { oidcSub: 'member-oidc-sub' }, - { oidcSub: 'proposer-oidc-sub' }, - ], - uidPersons: ['proposer-uid'], + centralAccounts: ['member-oidc-sub', 'proposer-oidc-sub'], + foundUsersInOneIdentity: ['proposer-uid'], } ); }); @@ -184,7 +178,7 @@ describe('oneIdentityIntegrationHandler', () => { }); describe('PROPOSAL_UPDATED', () => { - it('should handle updated proposal', async () => { + it('should handle updated proposal and remove old connections', async () => { setupMocks({ getProposal: 'proposal-UID_ESet', getProposalPersonConnections: [ @@ -194,9 +188,10 @@ describe('oneIdentityIntegrationHandler', () => { }, { UID_ESet: 'proposal-UID_ESet', - UID_Person: 'old-member-uid', // this person should be removed, because it's not in the updated proposal + UID_Person: 'old-member-uid', // this person should be removed }, ], + hasPersonSiteAccessToProposalConfig: { 'old-member-uid': false }, }); await syncProposalAndMembersToOneIdentityHandler( @@ -226,6 +221,115 @@ describe('oneIdentityIntegrationHandler', () => { expect(mockOneIdentity.logout).toHaveBeenCalled(); }); + it('should not remove old connection if person has site access to proposal', async () => { + setupMocks({ + getProposal: 'proposal-UID_ESet', + getProposalPersonConnections: [ + { + UID_ESet: 'proposal-UID_ESet', + UID_Person: 'proposer-uid', + }, + { + UID_ESet: 'proposal-UID_ESet', + UID_Person: 'visitor-member-uid', // this person should NOT be removed due to site access + }, + ], + // 'visitor-member-uid' has site access + hasPersonSiteAccessToProposalConfig: { 'visitor-member-uid': true }, + }); + + await syncProposalAndMembersToOneIdentityHandler( + proposalMessage, + Event.PROPOSAL_UPDATED + ); + + expect(mockOneIdentity.createProposal).not.toHaveBeenCalled(); + expect(mockOneIdentity.getProposalPersonConnections).toHaveBeenCalledWith( + 'proposal-UID_ESet' + ); + // removeConnectionBetweenPersonAndProposal should NOT be called for 'visitor-member-uid' + expect( + mockOneIdentity.removeConnectionBetweenPersonAndProposal + ).not.toHaveBeenCalledWith('proposal-UID_ESet', 'visitor-member-uid'); + expect( + mockOneIdentity.removeConnectionBetweenPersonAndProposal + ).toHaveBeenCalledTimes(0); // No connections should be removed in this specific setup + + expect(mockOneIdentity.connectPersonToProposal).toHaveBeenCalledTimes(1); // 'member-uid' is new + expect(mockOneIdentity.connectPersonToProposal).toHaveBeenCalledWith( + 'proposal-UID_ESet', + 'member-uid' + ); + expect(logger.logInfo).toHaveBeenCalledWith('Connections updated', { + uidESet: 'proposal-UID_ESet', + uidPersons: ['proposer-uid', 'member-uid'], + }); + expect(mockOneIdentity.logout).toHaveBeenCalled(); + }); + + it('should remove one old connection and keep another due to site access', async () => { + setupMocks({ + getProposal: 'proposal-UID_ESet', + getProposalPersonConnections: [ + { + UID_ESet: 'proposal-UID_ESet', + UID_Person: 'proposer-uid', // Keep (in proposal) + }, + { + UID_ESet: 'proposal-UID_ESet', + UID_Person: 'old-member-to-remove-uid', // Remove (not in proposal, no site access) + }, + { + UID_ESet: 'proposal-UID_ESet', + UID_Person: 'visitor-member-to-keep-uid', // Keep (not in proposal, but has site access) + }, + ], + getPersons: ['proposer-uid', 'member-uid'], // Current members in the proposal message + hasPersonSiteAccessToProposalConfig: { + 'old-member-to-remove-uid': false, + 'visitor-member-to-keep-uid': true, + }, + }); + + await syncProposalAndMembersToOneIdentityHandler( + proposalMessage, // Contains proposer-uid and member-uid + Event.PROPOSAL_UPDATED + ); + + expect(mockOneIdentity.createProposal).not.toHaveBeenCalled(); + expect(mockOneIdentity.getProposalPersonConnections).toHaveBeenCalledWith( + 'proposal-UID_ESet' + ); + + // Check removals + expect( + mockOneIdentity.removeConnectionBetweenPersonAndProposal + ).toHaveBeenCalledTimes(1); + expect( + mockOneIdentity.removeConnectionBetweenPersonAndProposal + ).toHaveBeenCalledWith('proposal-UID_ESet', 'old-member-to-remove-uid'); + expect( + mockOneIdentity.removeConnectionBetweenPersonAndProposal + ).not.toHaveBeenCalledWith( + 'proposal-UID_ESet', + 'visitor-member-to-keep-uid' + ); + + // Check additions + // 'member-uid' is in proposalMessage.members and not in initial connections that are kept + expect(mockOneIdentity.connectPersonToProposal).toHaveBeenCalledTimes(1); + expect(mockOneIdentity.connectPersonToProposal).toHaveBeenCalledWith( + 'proposal-UID_ESet', + 'member-uid' + ); + + expect(logger.logInfo).toHaveBeenCalledWith('Connections updated', { + uidESet: 'proposal-UID_ESet', + uidPersons: ['proposer-uid', 'member-uid'], + }); + expect(mockOneIdentity.logout).toHaveBeenCalled(); + }); + it('should not handle proposal if there is no created proposal in One Identity', async () => { setupMocks({ getProposal: undefined, diff --git a/src/queue/consumers/oneidentity/consumerCallbacks/syncProposalAndMembersToOneIdentityHandler.ts b/src/queue/consumers/oneidentity/consumerCallbacks/syncProposalAndMembersToOneIdentityHandler.ts index 5a88daed..21d84957 100644 --- a/src/queue/consumers/oneidentity/consumerCallbacks/syncProposalAndMembersToOneIdentityHandler.ts +++ b/src/queue/consumers/oneidentity/consumerCallbacks/syncProposalAndMembersToOneIdentityHandler.ts @@ -3,7 +3,7 @@ import { logger } from '@user-office-software/duo-logger'; import { Event } from '../../../../models/Event'; import { ProposalMessageData } from '../../../../models/ProposalMessage'; import { collectUsersFromProposalMessage } from '../../utils/collectUsersFromProposalMessage'; -import { ESSOneIdentity, UserPersonConnection } from '../utils/ESSOneIdentity'; +import { ESSOneIdentity } from '../utils/ESSOneIdentity'; import { UID_ESet } from '../utils/interfaces/Eset'; import { UID_Person } from '../utils/interfaces/Person'; import { PersonHasESET } from '../utils/interfaces/PersonHasESET'; @@ -24,10 +24,11 @@ export async function syncProposalAndMembersToOneIdentityHandler( logger.logInfo('UID_ESet from One Identity', { uidESet }); if (uidESet) { + const users = collectUsersFromProposalMessage(message); await handleConnectionsBetweenProposalAndPersons( oneIdentity, uidESet, - message + users.map((user) => user.oidcSub) ); } } finally { @@ -68,22 +69,24 @@ async function getUIDESetFromOneIdentity( async function handleConnectionsBetweenProposalAndPersons( oneIdentity: ESSOneIdentity, uidESet: UID_ESet, - message: ProposalMessageData + centralAccounts: string[] ) { - const users = collectUsersFromProposalMessage(message); - - logger.logInfo('Users from proposal', { users }); + logger.logInfo('Users to be connected to proposal', { + centralAccounts, + }); // Get all users from One Identity - const userPersonConnections = await oneIdentity.getPersons(users); - const uidPersons = getUidPersons(userPersonConnections); + const uidPersons = await oneIdentity.getPersons(centralAccounts); // Log an error if not all users are found in One Identity to be able to investigate - if (uidPersons.length !== users.length) { - logger.logError('Not all users found in One Identity (investigate)', { - users, - uidPersons, - }); + if (uidPersons.length !== centralAccounts.length) { + logger.logError( + 'Not all users found in One Identity (Investigate). Missing central accounts:', + { + centralAccounts, + foundUsersInOneIdentity: uidPersons, + } + ); } logger.logInfo('Found persons in One Identity', { uidPersons }); @@ -97,18 +100,6 @@ async function handleConnectionsBetweenProposalAndPersons( logger.logInfo('Connections updated', { uidESet, uidPersons }); } -// Method to get UID_Person from UserPersonConnection -function getUidPersons( - userPersonConnections: UserPersonConnection[] -): UID_Person[] { - return userPersonConnections - .filter( - (connection): connection is { oidcSub: string; uidPerson: UID_Person } => - connection.uidPerson !== undefined - ) - .map(({ uidPerson }) => uidPerson); -} - async function addNewConnections( oneIdentity: ESSOneIdentity, uidESet: UID_ESet, @@ -132,10 +123,30 @@ async function removeOldConnections( connections: PersonHasESET[], uidPersons: UID_Person[] ): Promise { - const connectionsToRemove = connections.filter( + // Collect connections that are not in the list of current persons (OIM) + const potentiallyRemoveableConnections = connections.filter( (connection) => !uidPersons.includes(connection.UID_Person) ); + const removalChecks = await Promise.all( + potentiallyRemoveableConnections.map(async (connectionToRemove) => { + const hasAccess = await oneIdentity.hasPersonSiteAccessToProposal( + connectionToRemove.UID_Person, + connectionToRemove.UID_ESet + ); + + return { + connection: connectionToRemove, + shouldRemove: !hasAccess, // Remove if the person does NOT have site access + }; + }) + ); + + // Filter out connections that should not be removed + const connectionsToRemove = removalChecks + .filter((check) => check.shouldRemove) + .map((check) => check.connection); + await Promise.all( connectionsToRemove.map((connection) => oneIdentity.removeConnectionBetweenPersonAndProposal( diff --git a/src/queue/consumers/oneidentity/consumerCallbacks/syncVisitToOneIdentityHandler.spec.ts b/src/queue/consumers/oneidentity/consumerCallbacks/syncVisitToOneIdentityHandler.spec.ts index b60035d2..bcd45b9c 100644 --- a/src/queue/consumers/oneidentity/consumerCallbacks/syncVisitToOneIdentityHandler.spec.ts +++ b/src/queue/consumers/oneidentity/consumerCallbacks/syncVisitToOneIdentityHandler.spec.ts @@ -15,7 +15,9 @@ import { logger } from '@user-office-software/duo-logger'; import { syncVisitToOneIdentityHandler } from './syncVisitToOneIdentityHandler'; import { Event } from '../../../../models/Event'; +import { ProposalMessageData } from '../../../../models/ProposalMessage'; import { ESSOneIdentity } from '../utils/ESSOneIdentity'; +import { UID_ESet } from '../utils/interfaces/Eset'; import { IdentityType, Person } from '../utils/interfaces/Person'; import { OrderState, @@ -37,12 +39,30 @@ const mockOneIdentity: jest.Mocked> = { removeConnectionBetweenPersonAndProposal: jest.fn(), createPersonWantsOrg: jest.fn(), cancelPersonWantsOrg: jest.fn(), + hasPersonSiteAccessToProposal: jest.fn(), }; +const mockUidESet: UID_ESet = 'eset-uid-123'; + const visitMessage: VisitMessage = { visitorId: 'visitor-oidc-sub', startAt: '2023-01-01T00:00:00.000Z', endAt: '2023-01-10T00:00:00.000Z', + proposal: { + shortCode: 'proposal-short-code', + members: [ + { oidcSub: 'member-oidc-sub' }, + { oidcSub: 'visitor-oidc-sub' }, // Visitor is also a member + ], + } as ProposalMessageData, +}; + +const visitMessageVisitorNotMember: VisitMessage = { + ...visitMessage, + proposal: { + ...visitMessage.proposal, + members: [{ oidcSub: 'member-oidc-sub' }], // Visitor is NOT a member + } as ProposalMessageData, }; describe('syncVisitToOneIdentityHandler', () => { @@ -63,9 +83,9 @@ describe('syncVisitToOneIdentityHandler', () => { await syncVisitToOneIdentityHandler(visitMessage, Event.VISIT_CREATED); expect(mockOneIdentity.login).toHaveBeenCalled(); - expect(mockOneIdentity.getPerson).toHaveBeenCalledWith({ - oidcSub: 'visitor-oidc-sub', - }); + expect(mockOneIdentity.getPerson).toHaveBeenCalledWith( + 'visitor-oidc-sub' + ); expect(logger.logInfo).toHaveBeenCalledWith( 'Visitor is not a Science User, skipping', {} @@ -75,8 +95,8 @@ describe('syncVisitToOneIdentityHandler', () => { }); }); - describe('VISITOR_CREATED', () => { - it('should create site access and system access in One Identity for science users', async () => { + describe('VISIT_CREATED', () => { + it('should create site access and system access in One Identity for science users and connect to proposal', async () => { // Mock the current time to a fixed value for testing const mockNowDate = new Date('2022-12-15T00:00:00.000Z'); const originalDateNow = Date.now; @@ -97,6 +117,8 @@ describe('syncVisitToOneIdentityHandler', () => { } as PersonWantsOrg; mockOneIdentity.getPerson.mockResolvedValueOnce(mockPerson); + mockOneIdentity.getProposal.mockResolvedValueOnce(mockUidESet); + mockOneIdentity.getProposalPersonConnections.mockResolvedValueOnce([]); // No existing connection // Mock sequential calls to createPersonWantsOrg with different responses mockOneIdentity.createPersonWantsOrg @@ -106,9 +128,15 @@ describe('syncVisitToOneIdentityHandler', () => { await syncVisitToOneIdentityHandler(visitMessage, Event.VISIT_CREATED); expect(mockOneIdentity.login).toHaveBeenCalled(); - expect(mockOneIdentity.getPerson).toHaveBeenCalledWith({ - oidcSub: 'visitor-oidc-sub', - }); + expect(mockOneIdentity.getPerson).toHaveBeenCalledWith( + 'visitor-oidc-sub' + ); + expect(mockOneIdentity.getProposal).toHaveBeenCalledWith( + visitMessage.proposal + ); + expect(mockOneIdentity.getProposalPersonConnections).toHaveBeenCalledWith( + mockUidESet + ); // Verify site access creation expect(mockOneIdentity.createPersonWantsOrg).toHaveBeenCalledTimes(2); @@ -117,7 +145,8 @@ describe('syncVisitToOneIdentityHandler', () => { PersonWantsOrgRole.SITE_ACCESS, visitMessage.visitorId, visitMessage.startAt, - visitMessage.endAt + visitMessage.endAt, + visitMessage.proposal.shortCode ); // Calculate expected system access dates @@ -152,12 +181,84 @@ describe('syncVisitToOneIdentityHandler', () => { } ); + expect(mockOneIdentity.connectPersonToProposal).toHaveBeenCalledWith( + mockUidESet, + mockPerson.UID_Person + ); + expect(logger.logInfo).toHaveBeenCalledWith( + 'Connection created between proposal and visitor', + { + uidPerson: mockPerson.UID_Person, + uidESet: mockUidESet, + } + ); + expect(mockOneIdentity.logout).toHaveBeenCalled(); // Restore original Date.now Date.now = originalDateNow; }); + it('should skip creating proposal connection if it already exists', async () => { + const mockPerson = { + UID_Person: 'visitor-uid', + CCC_EmployeeSubType: IdentityType.ESSSCIENCEUSER, + } as Person; + const mockSiteAccess = { + UID_PersonWantsOrg: 'site-access-uid', + } as PersonWantsOrg; + const mockSystemAccess = { + UID_PersonWantsOrg: 'system-access-uid', + } as PersonWantsOrg; + + mockOneIdentity.getPerson.mockResolvedValueOnce(mockPerson); + mockOneIdentity.getProposal.mockResolvedValueOnce(mockUidESet); + mockOneIdentity.getProposalPersonConnections.mockResolvedValueOnce([ + { UID_Person: mockPerson.UID_Person, UID_ESet: mockUidESet }, + ]); // Connection exists + mockOneIdentity.createPersonWantsOrg + .mockResolvedValueOnce([mockSiteAccess]) + .mockResolvedValueOnce([mockSystemAccess]); + + await syncVisitToOneIdentityHandler(visitMessage, Event.VISIT_CREATED); + + expect(mockOneIdentity.connectPersonToProposal).not.toHaveBeenCalled(); + expect(logger.logInfo).toHaveBeenCalledWith( + 'Connection already exists, skipping', + { + uidPerson: mockPerson.UID_Person, + uidESet: mockUidESet, + } + ); + expect(mockOneIdentity.logout).toHaveBeenCalled(); + }); + + it('should throw an error if proposal is not found in One Identity', async () => { + const mockPerson = { + UID_Person: 'visitor-uid', + CCC_EmployeeSubType: IdentityType.ESSSCIENCEUSER, + } as Person; + + mockOneIdentity.getPerson.mockResolvedValueOnce(mockPerson); + mockOneIdentity.getProposal.mockResolvedValueOnce(undefined); // Proposal not found + + await expect( + syncVisitToOneIdentityHandler(visitMessage, Event.VISIT_CREATED) + ).rejects.toThrow( + 'Proposal not found in One Identity, cannot sync visit' + ); + + expect(mockOneIdentity.login).toHaveBeenCalled(); + expect(mockOneIdentity.getPerson).toHaveBeenCalledWith( + 'visitor-oidc-sub' + ); + expect(mockOneIdentity.getProposal).toHaveBeenCalledWith( + visitMessage.proposal + ); + expect(mockOneIdentity.createPersonWantsOrg).not.toHaveBeenCalled(); + expect(mockOneIdentity.logout).toHaveBeenCalled(); + }); + it('should throw an error if site access creation fails', async () => { // Mock person that is a science user const mockPerson = { @@ -166,6 +267,7 @@ describe('syncVisitToOneIdentityHandler', () => { } as Person; mockOneIdentity.getPerson.mockResolvedValueOnce(mockPerson); + mockOneIdentity.getProposal.mockResolvedValueOnce(mockUidESet); mockOneIdentity.createPersonWantsOrg.mockRejectedValueOnce( new Error('Failed to create site access') ); @@ -175,6 +277,9 @@ describe('syncVisitToOneIdentityHandler', () => { ).rejects.toThrow('Failed to create site access'); expect(mockOneIdentity.login).toHaveBeenCalled(); + expect(mockOneIdentity.getPerson).toHaveBeenCalledWith( + 'visitor-oidc-sub' + ); expect(mockOneIdentity.logout).toHaveBeenCalled(); }); @@ -186,12 +291,12 @@ describe('syncVisitToOneIdentityHandler', () => { } as Person; mockOneIdentity.getPerson.mockResolvedValueOnce(mockPerson); + mockOneIdentity.getProposal.mockResolvedValueOnce(mockUidESet); // Create a message with an invalid date const invalidVisitMessage: VisitMessage = { - visitorId: 'visitor-oidc-sub', + ...visitMessage, startAt: 'invalid-date', - endAt: '2023-01-10T00:00:00.000Z', }; await expect( @@ -199,16 +304,16 @@ describe('syncVisitToOneIdentityHandler', () => { ).rejects.toThrow('Invalid date provided to toIsoString: invalid-date'); expect(mockOneIdentity.login).toHaveBeenCalled(); - expect(mockOneIdentity.getPerson).toHaveBeenCalledWith({ - oidcSub: 'visitor-oidc-sub', - }); + expect(mockOneIdentity.getPerson).toHaveBeenCalledWith( + 'visitor-oidc-sub' + ); expect(mockOneIdentity.createPersonWantsOrg).not.toHaveBeenCalled(); expect(mockOneIdentity.logout).toHaveBeenCalled(); }); }); describe('VISITOR_DELETED', () => { - it('should remove visitor access in One Identity for science users', async () => { + it('should remove visitor access and proposal connection in One Identity for science users (if not a member)', async () => { // Mock person that is a science user const mockPerson = { UID_Person: 'visitor-uid', @@ -222,6 +327,7 @@ describe('syncVisitToOneIdentityHandler', () => { DisplayOrg: PersonWantsOrgRole.SITE_ACCESS, ValidFrom: '2023-01-01T00:00:00.000Z', ValidUntil: '2023-01-10T00:00:00.000Z', + CustomProperty04: 'proposal-short-code', OrderState: OrderState.GRANTED, } as PersonWantsOrg, { @@ -236,17 +342,24 @@ describe('syncVisitToOneIdentityHandler', () => { ]; mockOneIdentity.getPerson.mockResolvedValueOnce(mockPerson); + mockOneIdentity.getProposal.mockResolvedValueOnce(mockUidESet); mockOneIdentity.getPersonWantsOrg.mockResolvedValueOnce( mockPersonWantsOrgs ); - await syncVisitToOneIdentityHandler(visitMessage, Event.VISIT_DELETED); + await syncVisitToOneIdentityHandler( + visitMessageVisitorNotMember, // Visitor is NOT a member + Event.VISIT_DELETED + ); expect(mockOneIdentity.login).toHaveBeenCalled(); - expect(mockOneIdentity.getPerson).toHaveBeenCalledWith({ - oidcSub: 'visitor-oidc-sub', - }); + expect(mockOneIdentity.getPerson).toHaveBeenCalledWith( + visitMessageVisitorNotMember.visitorId + ); + expect(mockOneIdentity.getProposal).toHaveBeenCalledWith( + visitMessageVisitorNotMember.proposal + ); expect(mockOneIdentity.getPersonWantsOrg).toHaveBeenCalledWith( 'visitor-uid' ); @@ -282,6 +395,17 @@ describe('syncVisitToOneIdentityHandler', () => { expect(mockOneIdentity.cancelPersonWantsOrg).toHaveBeenCalledTimes(2); + expect( + mockOneIdentity.removeConnectionBetweenPersonAndProposal + ).toHaveBeenCalledWith(mockUidESet, mockPerson.UID_Person); + expect(logger.logInfo).toHaveBeenCalledWith( + 'Connection removed between proposal and visitor', + { + uidPerson: mockPerson.UID_Person, + uidESet: mockUidESet, + } + ); + expect(mockOneIdentity.logout).toHaveBeenCalled(); expect(logger.logInfo).toHaveBeenCalledWith( 'One Identity successfully logged out', @@ -289,6 +413,80 @@ describe('syncVisitToOneIdentityHandler', () => { ); }); + it('should skip removing proposal connection if visitor is a member of the proposal', async () => { + const mockPerson = { + UID_Person: 'visitor-uid', + CCC_EmployeeSubType: IdentityType.ESSSCIENCEUSER, + } as Person; + const mockPersonWantsOrgs = [ + { + UID_PersonWantsOrg: 'site-access-uid', + UID_PersonOrdered: 'visitor-uid', + DisplayOrg: PersonWantsOrgRole.SITE_ACCESS, + ValidFrom: '2023-01-01T00:00:00.000Z', + ValidUntil: '2023-01-10T00:00:00.000Z', + CustomProperty04: 'proposal-short-code', + OrderState: OrderState.GRANTED, + } as PersonWantsOrg, + { + UID_PersonWantsOrg: 'system-access-uid', + UID_PersonOrdered: 'visitor-uid', + DisplayOrg: PersonWantsOrgRole.SYSTEM_ACCESS, + ValidFrom: '2023-01-01T00:00:00.000Z', + ValidUntil: '2023-01-10T00:00:00.000Z', + CustomProperty04: 'site-access-uid', + OrderState: OrderState.GRANTED, + } as PersonWantsOrg, + ]; + + mockOneIdentity.getPerson.mockResolvedValueOnce(mockPerson); + mockOneIdentity.getProposal.mockResolvedValueOnce(mockUidESet); + mockOneIdentity.getPersonWantsOrg.mockResolvedValueOnce( + mockPersonWantsOrgs + ); + + // Using original visitMessage where visitor IS a member + await syncVisitToOneIdentityHandler(visitMessage, Event.VISIT_DELETED); + + expect( + mockOneIdentity.removeConnectionBetweenPersonAndProposal + ).not.toHaveBeenCalled(); + expect(logger.logInfo).toHaveBeenCalledWith( + 'Visitor is a proposal member, skipping removal', + { + uidPerson: mockPerson.UID_Person, + uidESet: mockUidESet, + } + ); + expect(mockOneIdentity.logout).toHaveBeenCalled(); + }); + + it('should throw an error if proposal is not found in One Identity on delete', async () => { + const mockPerson = { + UID_Person: 'visitor-uid', + CCC_EmployeeSubType: IdentityType.ESSSCIENCEUSER, + } as Person; + + mockOneIdentity.getPerson.mockResolvedValueOnce(mockPerson); + mockOneIdentity.getProposal.mockResolvedValueOnce(undefined); // Proposal not found + + await expect( + syncVisitToOneIdentityHandler(visitMessage, Event.VISIT_DELETED) + ).rejects.toThrow( + 'Proposal not found in One Identity, cannot sync visit' + ); + + expect(mockOneIdentity.login).toHaveBeenCalled(); + expect(mockOneIdentity.getPerson).toHaveBeenCalledWith( + 'visitor-oidc-sub' + ); + expect(mockOneIdentity.getProposal).toHaveBeenCalledWith( + visitMessage.proposal + ); + expect(mockOneIdentity.cancelPersonWantsOrg).not.toHaveBeenCalled(); + expect(mockOneIdentity.logout).toHaveBeenCalled(); + }); + it('should skip processing if visitor is not a science user', async () => { // Mock person that is not a science user const mockPerson = { @@ -301,9 +499,9 @@ describe('syncVisitToOneIdentityHandler', () => { await syncVisitToOneIdentityHandler(visitMessage, Event.VISIT_DELETED); expect(mockOneIdentity.login).toHaveBeenCalled(); - expect(mockOneIdentity.getPerson).toHaveBeenCalledWith({ - oidcSub: 'visitor-oidc-sub', - }); + expect(mockOneIdentity.getPerson).toHaveBeenCalledWith( + 'visitor-oidc-sub' + ); expect(logger.logInfo).toHaveBeenCalledWith( 'Visitor is not a Science User, skipping', {} @@ -321,9 +519,9 @@ describe('syncVisitToOneIdentityHandler', () => { ).rejects.toThrow('Person not found in One Identity'); expect(mockOneIdentity.login).toHaveBeenCalled(); - expect(mockOneIdentity.getPerson).toHaveBeenCalledWith({ - oidcSub: 'visitor-oidc-sub', - }); + expect(mockOneIdentity.getPerson).toHaveBeenCalledWith( + 'visitor-oidc-sub' + ); expect(mockOneIdentity.getPersonWantsOrg).not.toHaveBeenCalled(); expect(mockOneIdentity.cancelPersonWantsOrg).not.toHaveBeenCalled(); expect(mockOneIdentity.logout).toHaveBeenCalled(); @@ -347,6 +545,7 @@ describe('syncVisitToOneIdentityHandler', () => { ]; mockOneIdentity.getPerson.mockResolvedValueOnce(mockPerson); + mockOneIdentity.getProposal.mockResolvedValueOnce(mockUidESet); mockOneIdentity.getPersonWantsOrg.mockResolvedValueOnce( mockPersonWantsOrgs ); @@ -358,6 +557,9 @@ describe('syncVisitToOneIdentityHandler', () => { ); expect(mockOneIdentity.login).toHaveBeenCalled(); + expect(mockOneIdentity.getPerson).toHaveBeenCalledWith( + 'visitor-oidc-sub' + ); expect(mockOneIdentity.logout).toHaveBeenCalled(); }); @@ -374,6 +576,7 @@ describe('syncVisitToOneIdentityHandler', () => { DisplayOrg: PersonWantsOrgRole.SITE_ACCESS, ValidFrom: visitMessage.startAt, ValidUntil: visitMessage.endAt, + CustomProperty04: 'proposal-short-code', OrderState: OrderState.GRANTED, } as PersonWantsOrg, // No system access with CustomProperty04 matching site-access-uid @@ -389,6 +592,7 @@ describe('syncVisitToOneIdentityHandler', () => { ]; mockOneIdentity.getPerson.mockResolvedValueOnce(mockPerson); + mockOneIdentity.getProposal.mockResolvedValueOnce(mockUidESet); mockOneIdentity.getPersonWantsOrg.mockResolvedValueOnce( mockPersonWantsOrgs ); @@ -400,6 +604,9 @@ describe('syncVisitToOneIdentityHandler', () => { ); expect(mockOneIdentity.login).toHaveBeenCalled(); + expect(mockOneIdentity.getPerson).toHaveBeenCalledWith( + 'visitor-oidc-sub' + ); expect(mockOneIdentity.cancelPersonWantsOrg).toHaveBeenCalledWith( 'site-access-uid' ); diff --git a/src/queue/consumers/oneidentity/consumerCallbacks/syncVisitToOneIdentityHandler.ts b/src/queue/consumers/oneidentity/consumerCallbacks/syncVisitToOneIdentityHandler.ts index 84427150..5479bc95 100644 --- a/src/queue/consumers/oneidentity/consumerCallbacks/syncVisitToOneIdentityHandler.ts +++ b/src/queue/consumers/oneidentity/consumerCallbacks/syncVisitToOneIdentityHandler.ts @@ -3,6 +3,8 @@ import process from 'process'; import { logger } from '@user-office-software/duo-logger'; import { Event } from '../../../../models/Event'; +import { ProposalMessageData } from '../../../../models/ProposalMessage'; +import { collectUsersFromProposalMessage } from '../../utils/collectUsersFromProposalMessage'; import { ESSOneIdentity } from '../utils/ESSOneIdentity'; import { IdentityType, UID_Person } from '../utils/interfaces/Person'; import { @@ -16,7 +18,7 @@ const ONE_IDENTITY_SYSTEM_ACCESS_LASTS_FOR_DAYS = parseInt( ); export async function syncVisitToOneIdentityHandler( - { startAt, endAt, visitorId }: VisitMessage, + { startAt, endAt, visitorId: oidcSub, proposal }: VisitMessage, type: Event ): Promise { const oneIdentity = new ESSOneIdentity(); @@ -26,17 +28,41 @@ export async function syncVisitToOneIdentityHandler( try { // Only Science Users' access should be managed! - const uidPerson = await getScienceUser(oneIdentity, visitorId); + const uidPerson = await getScienceUser(oneIdentity, oidcSub); if (!uidPerson) { logger.logInfo('Visitor is not a Science User, skipping', {}); return; } + const uidESet = await oneIdentity.getProposal(proposal); + + if (!uidESet) { + throw new Error('Proposal not found in One Identity, cannot sync visit'); + } + if (type === Event.VISIT_CREATED) { - await createAccessInOneIdentity(oneIdentity, startAt, endAt, visitorId); + await createAccessInOneIdentity( + oneIdentity, + startAt, + endAt, + oidcSub, + proposal + ); + + // Every visitor should have access to the proposal folders + await createProposalConnection(oneIdentity, uidESet, uidPerson); } else if (type === Event.VISIT_DELETED) { await removeAccessFromOneIdentity(oneIdentity, startAt, endAt, uidPerson); + + // Remove the connection between the proposal and the visitor + await removeProposalConnection( + oneIdentity, + uidESet, + uidPerson, + oidcSub, + proposal + ); } } finally { await oneIdentity.logout(); @@ -51,9 +77,7 @@ async function getScienceUser( centralAccount: string ): Promise { // Find person UID from oidcSub - const person = await oneIdentity.getPerson({ - oidcSub: centralAccount, - }); + const person = await oneIdentity.getPerson(centralAccount); if (!person) { throw new Error('Person not found in One Identity'); @@ -68,14 +92,16 @@ async function createAccessInOneIdentity( oneIdentity: ESSOneIdentity, startAt: string, endAt: string, - centralAccount: string + centralAccount: string, + proposal: ProposalMessageData ) { // Create site access const [pwoSite] = await oneIdentity.createPersonWantsOrg( PersonWantsOrgRole.SITE_ACCESS, centralAccount, toIsoString(startAt), - toIsoString(endAt) + toIsoString(endAt), + proposal.shortCode // CustomProperty04 - We store the proposal short code for the site access to be able to find it later ); logger.logInfo('Site access created in One Identity', { @@ -94,7 +120,7 @@ async function createAccessInOneIdentity( centralAccount, toIsoString(validFrom), toIsoString(validUntil), - pwoSite.UID_PersonWantsOrg // CustomProperty04 + pwoSite.UID_PersonWantsOrg // CustomProperty04 - We store the site access UID for the system access to be able to find it later ); logger.logInfo('System access created in One Identity', { @@ -153,6 +179,61 @@ async function removeAccessFromOneIdentity( }); } +async function createProposalConnection( + oneIdentity: ESSOneIdentity, + uidESet: string, + uidPerson: string +) { + // Check if the connection already exists + // If connection already exists, no need to create it again, reasons could be: + // - The visitor is a member of the proposal + // - The visitor has been added to the proposal in the past + const exists = (await oneIdentity.getProposalPersonConnections(uidESet)).some( + (c) => c.UID_Person === uidPerson + ); + + if (exists) { + logger.logInfo('Connection already exists, skipping', { + uidPerson, + uidESet, + }); + } else { + await oneIdentity.connectPersonToProposal(uidESet, uidPerson); + logger.logInfo('Connection created between proposal and visitor', { + uidPerson, + uidESet, + }); + } +} + +async function removeProposalConnection( + oneIdentity: ESSOneIdentity, + uidESet: string, + uidPerson: string, + oidcSub: string, + proposal: ProposalMessageData +) { + const isMember = collectUsersFromProposalMessage(proposal).some( + (m) => m.oidcSub === oidcSub + ); + + if (isMember) { + logger.logInfo('Visitor is a proposal member, skipping removal', { + uidPerson, + uidESet, + }); + } else { + await oneIdentity.removeConnectionBetweenPersonAndProposal( + uidESet, + uidPerson + ); + logger.logInfo('Connection removed between proposal and visitor', { + uidPerson, + uidESet, + }); + } +} + function toIsoString(date: string | number) { const parsedDate = new Date(date); diff --git a/src/queue/consumers/oneidentity/utils/ESSOneIdentity.spec.ts b/src/queue/consumers/oneidentity/utils/ESSOneIdentity.spec.ts index edb0e506..3a037a22 100644 --- a/src/queue/consumers/oneidentity/utils/ESSOneIdentity.spec.ts +++ b/src/queue/consumers/oneidentity/utils/ESSOneIdentity.spec.ts @@ -13,9 +13,9 @@ import { ESSOneIdentity } from './ESSOneIdentity'; import { PersonWantsOrg, PersonWantsOrgRole, + OrderState, } from './interfaces/PersonWantsOrg'; import { ProposalMessageData } from '../../../../models/ProposalMessage'; -import { ProposalUser } from '../../scicat/scicatProposal/dto'; const mockOneIdentityApi = { login: jest.fn(), @@ -137,20 +137,22 @@ describe('ESSOneIdentity', () => { { values: { UID_Person: 'person-uid', + CCC_EmployeeSubType: 'ESSSCIENCEUSER', }, }, ]); - const result = await essOneIdentity.getPerson({ - oidcSub: '0000-0000-0000-0000', - }); + const result = await essOneIdentity.getPerson('0000-0000-0000-0000'); expect(mockOneIdentityApi.getEntities).toHaveBeenCalledWith( 'Person', "CentralAccount='0000-0000-0000-0000'", ['CCC_EmployeeSubType'] ); - expect(result).toEqual({ UID_Person: 'person-uid' }); + expect(result).toEqual({ + UID_Person: 'person-uid', + CCC_EmployeeSubType: 'ESSSCIENCEUSER', + }); }); // The CentralAccount is unique, but the response is an array of entities @@ -159,20 +161,23 @@ describe('ESSOneIdentity', () => { { values: { UID_Person: 'person-1-uid', + CCC_EmployeeSubType: 'ESSSCIENCEUSER', }, }, { values: { UID_Person: 'person-2-uid', + CCC_EmployeeSubType: 'ESSSCIENCEUSER', }, }, ]); - const result = await essOneIdentity.getPerson({ - oidcSub: '0000-0000-0000-0000', - }); + const result = await essOneIdentity.getPerson('0000-0000-0000-0000'); - expect(result).toEqual({ UID_Person: 'person-1-uid' }); + expect(result).toEqual({ + UID_Person: 'person-1-uid', + CCC_EmployeeSubType: 'ESSSCIENCEUSER', + }); }); }); @@ -195,24 +200,11 @@ describe('ESSOneIdentity', () => { }); const result = await essOneIdentity.getPersons([ - { - oidcSub: 'unknown-oidc-sub', - } as ProposalUser, - { - oidcSub: 'known-oidc-sub', - } as ProposalUser, + 'unknown-oidc-sub', + 'known-oidc-sub', ]); - expect(result).toEqual([ - { - oidcSub: 'unknown-oidc-sub', - uidPerson: undefined, - }, - { - oidcSub: 'known-oidc-sub', - uidPerson: 'known-person-uid', - }, - ]); + expect(result).toEqual(['known-person-uid']); }); }); @@ -484,7 +476,7 @@ describe('ESSOneIdentity', () => { }); it('should return empty array when no records found', async () => { - mockOneIdentityApi.getEntities.mockResolvedValueOnce([]); + mockOneIdentityApi.getEntities.mockResolvedValueOnce([]); // No records const result = await essOneIdentity.getPersonWantsOrg('person-uid'); @@ -502,4 +494,128 @@ describe('ESSOneIdentity', () => { expect(result).toEqual([]); }); }); + + describe('hasPersonSiteAccessToProposal', () => { + const uidPerson = 'person-123'; + const proposalUid = 'proposal-abc'; + + it('should return true if person has site access to the proposal', async () => { + mockOneIdentityApi.getEntities.mockResolvedValueOnce([ + { + values: { + DisplayOrg: PersonWantsOrgRole.SITE_ACCESS, + CustomProperty04: proposalUid, + OrderState: OrderState.GRANTED, + } as PersonWantsOrg, + }, + ]); + + const result = await essOneIdentity.hasPersonSiteAccessToProposal( + uidPerson, + proposalUid + ); + + expect(mockOneIdentityApi.getEntities).toHaveBeenCalledWith( + 'PersonWantsOrg', + `UID_PersonOrdered='${uidPerson}' AND (DisplayOrg='${PersonWantsOrgRole.SITE_ACCESS}')`, + [ + 'ValidFrom', + 'ValidUntil', + 'OrderState', + 'DisplayOrg', + 'CustomProperty04', + ] + ); + expect(result).toBe(true); + }); + + it('should return false if person does not have site access to the proposal (different proposal)', async () => { + mockOneIdentityApi.getEntities.mockResolvedValueOnce([ + { + values: { + DisplayOrg: PersonWantsOrgRole.SITE_ACCESS, + CustomProperty04: 'other-proposal-uid', + OrderState: OrderState.GRANTED, + } as PersonWantsOrg, + }, + ]); + + const result = await essOneIdentity.hasPersonSiteAccessToProposal( + uidPerson, + proposalUid + ); + expect(result).toBe(false); + }); + + it('should return false if person does not have site access to the proposal (different role)', async () => { + mockOneIdentityApi.getEntities.mockResolvedValueOnce([ + { + values: { + DisplayOrg: PersonWantsOrgRole.SYSTEM_ACCESS, // Different role + CustomProperty04: proposalUid, + OrderState: OrderState.GRANTED, + } as PersonWantsOrg, + }, + ]); + + const result = await essOneIdentity.hasPersonSiteAccessToProposal( + uidPerson, + proposalUid + ); + expect(result).toBe(false); + }); + + it('should return false if site access is aborted', async () => { + mockOneIdentityApi.getEntities.mockResolvedValueOnce([ + { + values: { + DisplayOrg: PersonWantsOrgRole.SITE_ACCESS, + CustomProperty04: proposalUid, + OrderState: OrderState.ABORTED, // Aborted state + } as PersonWantsOrg, + }, + ]); + + const result = await essOneIdentity.hasPersonSiteAccessToProposal( + uidPerson, + proposalUid + ); + expect(result).toBe(false); + }); + + it('should return false if no PersonWantsOrg records are found', async () => { + mockOneIdentityApi.getEntities.mockResolvedValueOnce([]); // No records + + const result = await essOneIdentity.hasPersonSiteAccessToProposal( + uidPerson, + proposalUid + ); + expect(result).toBe(false); + }); + + it('should return true if person has multiple site access records and one matches', async () => { + mockOneIdentityApi.getEntities.mockResolvedValueOnce([ + { + values: { + DisplayOrg: PersonWantsOrgRole.SITE_ACCESS, + CustomProperty04: 'other-proposal-uid', + OrderState: OrderState.GRANTED, + } as PersonWantsOrg, + }, + { + values: { + DisplayOrg: PersonWantsOrgRole.SITE_ACCESS, + CustomProperty04: proposalUid, + OrderState: OrderState.GRANTED, + } as PersonWantsOrg, + }, + ]); + + const result = await essOneIdentity.hasPersonSiteAccessToProposal( + uidPerson, + proposalUid + ); + expect(result).toBe(true); + }); + }); }); diff --git a/src/queue/consumers/oneidentity/utils/ESSOneIdentity.ts b/src/queue/consumers/oneidentity/utils/ESSOneIdentity.ts index 8630de73..ad79a121 100644 --- a/src/queue/consumers/oneidentity/utils/ESSOneIdentity.ts +++ b/src/queue/consumers/oneidentity/utils/ESSOneIdentity.ts @@ -5,6 +5,7 @@ import { EsetType } from './interfaces/EsetType'; import { Person, UID_Person } from './interfaces/Person'; import { PersonHasESET } from './interfaces/PersonHasESET'; import { + OrderState, PersonWantsOrg, PersonWantsOrgRole, } from './interfaces/PersonWantsOrg'; @@ -14,7 +15,6 @@ import { } from './interfaces/SCProposalSiteAccessResponse'; import { OneIdentityApi } from './OneIdentityApi'; import { ProposalMessageData } from '../../../../models/ProposalMessage'; -import { ProposalUser } from '../../scicat/scicatProposal/dto'; export interface UserPersonConnection { oidcSub: string; @@ -82,30 +82,25 @@ export class ESSOneIdentity { return entities[0]?.values?.UID_ESet; } - public async getPerson( - user: Pick - ): Promise { + public async getPerson(centralAccount: string): Promise { const entities = await this.oneIdentityApi.getEntities( 'Person', - `CentralAccount='${user.oidcSub}'`, + `CentralAccount='${centralAccount}'`, ['CCC_EmployeeSubType'] ); return entities[0]?.values; } - public async getPersons( - users: ProposalUser[] - ): Promise { - return await Promise.all( - users - .filter((user): user is ProposalUser => user !== undefined) - .map(async (user) => { - const uidPerson = (await this.getPerson(user))?.UID_Person; - - return { oidcSub: user.oidcSub, uidPerson }; - }) - ); + public async getPersons(centralAccounts: string[]): Promise { + return ( + await Promise.all( + centralAccounts.map( + async (centralAccount) => + (await this.getPerson(centralAccount))?.UID_Person + ) + ) + ).filter((uidPerson): uidPerson is string => uidPerson !== undefined); } public async connectPersonToProposal( @@ -205,4 +200,20 @@ export class ESSOneIdentity { return entities.map(({ values }) => values); } + + public async hasPersonSiteAccessToProposal( + uidPerson: UID_Person, + proposal: UID_ESet + ): Promise { + const personWantsOrgs = await this.getPersonWantsOrg(uidPerson, [ + PersonWantsOrgRole.SITE_ACCESS, + ]); + + return personWantsOrgs.some( + (pwo) => + pwo.DisplayOrg === PersonWantsOrgRole.SITE_ACCESS && + pwo.CustomProperty04 === proposal && + pwo.OrderState !== OrderState.ABORTED + ); + } } diff --git a/src/queue/consumers/oneidentity/utils/interfaces/VisitMessage.ts b/src/queue/consumers/oneidentity/utils/interfaces/VisitMessage.ts index 7b60bd35..ce180475 100644 --- a/src/queue/consumers/oneidentity/utils/interfaces/VisitMessage.ts +++ b/src/queue/consumers/oneidentity/utils/interfaces/VisitMessage.ts @@ -1,5 +1,8 @@ +import { ProposalMessageData } from '../../../../../models/ProposalMessage'; + export interface VisitMessage { startAt: string; endAt: string; visitorId: string; + proposal: ProposalMessageData; } diff --git a/src/queue/consumers/oneidentity/utils/isVisitMessage.spec.ts b/src/queue/consumers/oneidentity/utils/isVisitMessage.spec.ts new file mode 100644 index 00000000..5f7c0d78 --- /dev/null +++ b/src/queue/consumers/oneidentity/utils/isVisitMessage.spec.ts @@ -0,0 +1,58 @@ +import { isVisitMessage } from './isVisitMessage'; + +describe('isVisitMessage', () => { + it('should return false if message is not an object', () => { + const message = 'not an object'; + expect(isVisitMessage(message)).toBe(false); + }); + + it('should return false if message is null', () => { + const message = null; + expect(isVisitMessage(message)).toBe(false); + }); + + it('should return false if visitorId is undefined', () => { + const message = { + startAt: '2023-01-01T00:00:00Z', + endAt: '2023-01-02T00:00:00Z', + }; + expect(isVisitMessage(message)).toBe(false); + }); + + it('should return false if startAt is undefined', () => { + const message = { + visitorId: 'visitor123', + endAt: '2023-01-02T00:00:00Z', + }; + expect(isVisitMessage(message)).toBe(false); + }); + + it('should return false if endAt is undefined', () => { + const message = { + visitorId: 'visitor123', + startAt: '2023-01-01T00:00:00Z', + }; + expect(isVisitMessage(message)).toBe(false); + }); + + it('should return false if proposal is undefined', () => { + const message = { + visitorId: 'visitor123', + startAt: '2023-01-01T00:00:00Z', + endAt: '2023-01-02T00:00:00Z', + }; + expect(isVisitMessage(message)).toBe(false); + }); + + it('should return true if the message is valid', () => { + const message = { + visitorId: 'visitor123', + startAt: '2023-01-01T00:00:00Z', + endAt: '2023-01-02T00:00:00Z', + proposal: { + shortCode: 'proposal-short-code', + }, + }; + expect(isVisitMessage(message)).toBe(true); + }); +}); diff --git a/src/queue/consumers/oneidentity/utils/isVisitMessage.ts b/src/queue/consumers/oneidentity/utils/isVisitMessage.ts new file mode 100644 index 00000000..ed7cb282 --- /dev/null +++ b/src/queue/consumers/oneidentity/utils/isVisitMessage.ts @@ -0,0 +1,12 @@ +import { VisitMessage } from './interfaces/VisitMessage'; + +export function isVisitMessage(message: any): message is VisitMessage { + return ( + message != null && + typeof message === 'object' && + 'visitorId' in message && + 'startAt' in message && + 'endAt' in message && + 'proposal' in message + ); +} diff --git a/src/queue/consumers/oneidentity/utils/validateVisitMessage.spec.ts b/src/queue/consumers/oneidentity/utils/validateVisitMessage.spec.ts deleted file mode 100644 index 7250b75d..00000000 --- a/src/queue/consumers/oneidentity/utils/validateVisitMessage.spec.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { validateVisitMessage } from './validateVisitMessage'; - -describe('validateVisitMessage', () => { - it('should throw an error if message is not an object', () => { - const message = 'not an object'; - - expect(() => validateVisitMessage(message)).toThrow( - 'Invalid Visit message' - ); - }); - - it('should throw an error if message is null', () => { - const message = null; - - expect(() => validateVisitMessage(message)).toThrow( - 'Invalid Visit message' - ); - }); - - it('should throw an error if visitorId is undefined', () => { - const message = { - startAt: '2023-01-01T00:00:00Z', - endAt: '2023-01-02T00:00:00Z', - }; - - expect(() => validateVisitMessage(message)).toThrow( - 'Invalid Visit message' - ); - }); - - it('should throw an error if startAt is undefined', () => { - const message = { - visitorId: 'visitor123', - endAt: '2023-01-02T00:00:00Z', - }; - - expect(() => validateVisitMessage(message)).toThrow( - 'Invalid Visit message' - ); - }); - - it('should throw an error if endAt is undefined', () => { - const message = { - visitorId: 'visitor123', - startAt: '2023-01-01T00:00:00Z', - }; - - expect(() => validateVisitMessage(message)).toThrow( - 'Invalid Visit message' - ); - }); - - it('should return the message if visitorId, startAt, and endAt are defined', () => { - const message = { - visitorId: 'visitor123', - startAt: '2023-01-01T00:00:00Z', - endAt: '2023-01-02T00:00:00Z', - }; - - expect(validateVisitMessage(message)).toEqual(message); - }); -}); diff --git a/src/queue/consumers/oneidentity/utils/validateVisitMessage.ts b/src/queue/consumers/oneidentity/utils/validateVisitMessage.ts deleted file mode 100644 index f532889a..00000000 --- a/src/queue/consumers/oneidentity/utils/validateVisitMessage.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { VisitMessage } from './interfaces/VisitMessage'; - -export function validateVisitMessage(message: any): VisitMessage | never { - if ( - message?.visitorId === undefined || - message?.startAt === undefined || - message?.endAt === undefined - ) { - throw new Error('Invalid Visit message'); - } - - return message; -} From 839a345f4b637226af25e0dfdf7cd8e665ab33b8 Mon Sep 17 00:00:00 2001 From: Jay Date: Thu, 17 Jul 2025 11:34:16 +0200 Subject: [PATCH 23/25] =?UTF-8?q?feat:=20Add=20metadata=20field=20to=20pro?= =?UTF-8?q?posal=20DTOs=20and=20implement=20instruments=20o=E2=80=A6=20(#5?= =?UTF-8?q?77)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .husky/pre-commit | 6 ++--- .../upsertProposalInScicat.ts | 25 +++++++++++++++++++ .../consumers/scicat/scicatProposal/dto.ts | 2 ++ 3 files changed, 29 insertions(+), 4 deletions(-) diff --git a/.husky/pre-commit b/.husky/pre-commit index 22e8b022..08404245 100644 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,4 +1,2 @@ -#!/usr/bin/env sh -. "$(dirname -- "$0")/_/husky.sh" - -npx lint-staged --shell +#!/bin/sh +npx --no-install lint-staged \ No newline at end of file diff --git a/src/queue/consumers/scicat/scicatProposal/consumerCallbacks/upsertProposalInScicat.ts b/src/queue/consumers/scicat/scicatProposal/consumerCallbacks/upsertProposalInScicat.ts index 2c81043a..215b697c 100644 --- a/src/queue/consumers/scicat/scicatProposal/consumerCallbacks/upsertProposalInScicat.ts +++ b/src/queue/consumers/scicat/scicatProposal/consumerCallbacks/upsertProposalInScicat.ts @@ -1,5 +1,6 @@ import { logger } from '@user-office-software/duo-logger'; +import { Instrument } from '../../../../../models/ProposalMessage'; import { ValidProposalMessageData } from '../../../utils/validateProposalMessage'; import { CreateProposalDto, UpdateProposalDto } from '../dto'; @@ -64,6 +65,7 @@ const getCreateProposalDto = (proposalMessage: ValidProposalMessageData) => { startTime: new Date(), endTime: new Date(), MeasurementPeriodList: [], + metadata: createInstrumentsObject(proposalMessage.instruments), }; return createProposalDto; @@ -84,11 +86,34 @@ const getUpdateProposalDto = (proposalMessage: ValidProposalMessageData) => { startTime: new Date(), endTime: new Date(), MeasurementPeriodList: [], + metadata: createInstrumentsObject(proposalMessage.instruments), }; return updateProposalDto; }; +const createInstrumentsObject = (instruments: Instrument[]) => { + const instrumentsObject: Record< + string, + { value: string | number; unit: string } + > = {}; + + instruments.forEach((instrument, index) => { + if (instrument) { + instrumentsObject[`instrument_${index + 1}`] = { + value: instrument.shortCode, + unit: '', + }; + instrumentsObject[`instrument_time_${index + 1}`] = { + value: instrument.allocatedTime / 86400 || NaN, + unit: 'days', + }; + } + }); + + return instrumentsObject; +}; + const createProposal = async ( proposalMessage: ValidProposalMessageData, sciCatAccessToken: string diff --git a/src/queue/consumers/scicat/scicatProposal/dto.ts b/src/queue/consumers/scicat/scicatProposal/dto.ts index 04e90535..5380e2aa 100644 --- a/src/queue/consumers/scicat/scicatProposal/dto.ts +++ b/src/queue/consumers/scicat/scicatProposal/dto.ts @@ -13,6 +13,7 @@ export type CreateProposalDto = { startTime?: Date; endTime?: Date; MeasurementPeriodList: any[]; + metadata?: Record; }; export type UpdateProposalDto = { @@ -29,6 +30,7 @@ export type UpdateProposalDto = { startTime?: Date; endTime?: Date; MeasurementPeriodList?: any[]; + metadata?: Record; }; export interface Institution { From c5861bbda6483923f1ee3a1977c90b9573e88441 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 13 Sep 2025 11:51:11 +0000 Subject: [PATCH 24/25] build(deps): bump axios from 1.8.2 to 1.12.0 Bumps [axios](https://github.com/axios/axios) from 1.8.2 to 1.12.0. - [Release notes](https://github.com/axios/axios/releases) - [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md) - [Commits](https://github.com/axios/axios/compare/v1.8.2...v1.12.0) --- updated-dependencies: - dependency-name: axios dependency-version: 1.12.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- package-lock.json | 255 +++++++++++++++++++++++++++++++--------------- package.json | 2 +- 2 files changed, 176 insertions(+), 81 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4f5bb036..f8c57a56 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "@user-office-software/duo-logger": "^2.2.1", "@user-office-software/duo-message-broker": "^1.7.0", - "axios": "^1.8.2", + "axios": "^1.12.0", "dotenv": "^16.4.5", "envalid": "^8.0.0", "express": "^4.21.2", @@ -2294,7 +2294,8 @@ "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" }, "node_modules/available-typed-arrays": { "version": "1.0.7", @@ -2312,13 +2313,13 @@ } }, "node_modules/axios": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.2.tgz", - "integrity": "sha512-ls4GYBm5aig9vWx8AWDSGLpnpDQRtWAfrjU+EuytuODrFBkqesN2RkOQCBzrA1RQNHw1SmRMSDDDSwzNAYQ6Rg==", + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.0.tgz", + "integrity": "sha512-oXTDccv8PcfjZmPGlWsPSwtOJCZ/b6W5jAMCNcfwJbCzDckwG0jrYJFaWH1yvivfCXjVzV/SPDEhMB3Q+DSurg==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", + "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, @@ -2590,6 +2591,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -2857,6 +2871,7 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", "dependencies": { "delayed-stream": "~1.0.0" }, @@ -3114,6 +3129,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", "engines": { "node": ">=0.4.0" } @@ -3196,6 +3212,20 @@ "url": "https://dotenvx.com" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/dynamic-dedupe": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/dynamic-dedupe/-/dynamic-dedupe-0.3.0.tgz", @@ -3350,12 +3380,10 @@ } }, "node_modules/es-define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", - "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", - "dependencies": { - "get-intrinsic": "^1.2.4" - }, + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", "engines": { "node": ">= 0.4" } @@ -3369,10 +3397,10 @@ } }, "node_modules/es-object-atoms": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", - "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", - "dev": true, + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", "dependencies": { "es-errors": "^1.3.0" }, @@ -3381,14 +3409,15 @@ } }, "node_modules/es-set-tostringtag": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz", - "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==", - "dev": true, + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", "dependencies": { - "get-intrinsic": "^1.2.4", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", - "hasown": "^2.0.1" + "hasown": "^2.0.2" }, "engines": { "node": ">= 0.4" @@ -4186,12 +4215,15 @@ } }, "node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", "mime-types": "^2.1.12" }, "engines": { @@ -4307,15 +4339,21 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -4332,6 +4370,19 @@ "node": ">=8.0.0" } }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/get-stream": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", @@ -4448,11 +4499,12 @@ } }, "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "dependencies": { - "get-intrinsic": "^1.1.3" + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -4503,6 +4555,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "dev": true, "engines": { "node": ">= 0.4" }, @@ -4511,9 +4564,10 @@ } }, "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -4525,7 +4579,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, "dependencies": { "has-symbols": "^1.0.3" }, @@ -6385,6 +6438,15 @@ "tmpl": "1.0.5" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/matrix-events-sdk": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/matrix-events-sdk/-/matrix-events-sdk-0.0.1.tgz", @@ -10612,12 +10674,12 @@ } }, "axios": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.2.tgz", - "integrity": "sha512-ls4GYBm5aig9vWx8AWDSGLpnpDQRtWAfrjU+EuytuODrFBkqesN2RkOQCBzrA1RQNHw1SmRMSDDDSwzNAYQ6Rg==", + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.0.tgz", + "integrity": "sha512-oXTDccv8PcfjZmPGlWsPSwtOJCZ/b6W5jAMCNcfwJbCzDckwG0jrYJFaWH1yvivfCXjVzV/SPDEhMB3Q+DSurg==", "requires": { "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", + "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, @@ -10827,6 +10889,15 @@ "set-function-length": "^1.2.1" } }, + "call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "requires": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + } + }, "callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -11222,6 +11293,16 @@ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==" }, + "dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "requires": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + } + }, "dynamic-dedupe": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/dynamic-dedupe/-/dynamic-dedupe-0.3.0.tgz", @@ -11345,12 +11426,9 @@ } }, "es-define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", - "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", - "requires": { - "get-intrinsic": "^1.2.4" - } + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==" }, "es-errors": { "version": "1.3.0", @@ -11358,23 +11436,22 @@ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==" }, "es-object-atoms": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", - "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", - "dev": true, + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", "requires": { "es-errors": "^1.3.0" } }, "es-set-tostringtag": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz", - "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==", - "dev": true, + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", "requires": { - "get-intrinsic": "^1.2.4", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", - "hasown": "^2.0.1" + "hasown": "^2.0.2" } }, "es-shim-unscopables": { @@ -11962,12 +12039,14 @@ } }, "form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", "requires": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, @@ -12042,15 +12121,20 @@ "dev": true }, "get-intrinsic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "requires": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" } }, "get-package-type": { @@ -12058,6 +12142,15 @@ "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==" }, + "get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "requires": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + } + }, "get-stream": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", @@ -12135,12 +12228,9 @@ } }, "gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "requires": { - "get-intrinsic": "^1.1.3" - } + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==" }, "graceful-fs": { "version": "4.2.11", @@ -12177,18 +12267,18 @@ "has-proto": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", - "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==" + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "dev": true }, "has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==" }, "has-tostringtag": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, "requires": { "has-symbols": "^1.0.3" } @@ -13461,6 +13551,11 @@ "tmpl": "1.0.5" } }, + "math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==" + }, "matrix-events-sdk": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/matrix-events-sdk/-/matrix-events-sdk-0.0.1.tgz", diff --git a/package.json b/package.json index 0e0b8c54..f1291f4b 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "dependencies": { "@user-office-software/duo-logger": "^2.2.1", "@user-office-software/duo-message-broker": "^1.7.0", - "axios": "^1.8.2", + "axios": "^1.12.0", "dotenv": "^16.4.5", "envalid": "^8.0.0", "express": "^4.21.2", From b36b46939106d480794f9ef101eeb245c222f101 Mon Sep 17 00:00:00 2001 From: Yoganandan Pandiyan <132274772+yoganandaness@users.noreply.github.com> Date: Fri, 3 Oct 2025 14:12:06 +0200 Subject: [PATCH 25/25] providing visa to data access users and visitors (#605) --- src/models/ProposalMessage.ts | 2 ++ .../visa/consumerCallbacks/syncVisaProposal.ts | 10 ++++++---- .../consumers/visa/utils/sanitizeProposalMessage.ts | 10 ++++++++++ 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src/models/ProposalMessage.ts b/src/models/ProposalMessage.ts index b61dfb02..a075c42a 100644 --- a/src/models/ProposalMessage.ts +++ b/src/models/ProposalMessage.ts @@ -29,6 +29,8 @@ export type ProposalMessageData = { newStatus?: ProposalStatusDefaultShortCodes; submitted: boolean; members: ProposalUser[]; + dataAccessUsers: ProposalUser[]; + visitors: ProposalUser[]; proposer?: ProposalUser; instruments?: Instrument[]; }; diff --git a/src/queue/consumers/visa/consumerCallbacks/syncVisaProposal.ts b/src/queue/consumers/visa/consumerCallbacks/syncVisaProposal.ts index 824bb39a..0dd0b74d 100644 --- a/src/queue/consumers/visa/consumerCallbacks/syncVisaProposal.ts +++ b/src/queue/consumers/visa/consumerCallbacks/syncVisaProposal.ts @@ -104,13 +104,15 @@ export async function syncVisaProposal( }); } - const proposersAndCoproposers = [ + const experimenters = [ ...(proposalWithNewStatus.proposer ? [proposalWithNewStatus.proposer] : []), ...proposalWithNewStatus.members, + ...proposalWithNewStatus.dataAccessUsers, + ...proposalWithNewStatus.visitors, ]; - // Create new user for the Principal Investigator and Coproposers - for (const member of proposersAndCoproposers) { + // Create new user for the Principal Investigator, Coproposers, Data Access Users and Visitors + for (const member of experimenters) { await createUserAndAssignToExperiment( member, proposalWithNewStatus.proposalPk @@ -120,6 +122,6 @@ export async function syncVisaProposal( // Delete the users that are saved in the experiment users table, but not in the Proposal Payload await deleteMissingUsersFromExperiment( proposalWithNewStatus.proposalPk, - proposersAndCoproposers + experimenters ); } diff --git a/src/queue/consumers/visa/utils/sanitizeProposalMessage.ts b/src/queue/consumers/visa/utils/sanitizeProposalMessage.ts index 053354ce..648cc0dd 100644 --- a/src/queue/consumers/visa/utils/sanitizeProposalMessage.ts +++ b/src/queue/consumers/visa/utils/sanitizeProposalMessage.ts @@ -10,6 +10,16 @@ export function sanitizeProposalMessage(proposalMessage: ProposalMessageData) { oidcSub: member.oidcSub.toLowerCase(), email: member.email.toLowerCase(), })), + dataAccessUsers: proposalMessage.dataAccessUsers.map((user) => ({ + ...user, + oidcSub: user.oidcSub.toLowerCase(), + email: user.email.toLowerCase(), + })), + visitors: proposalMessage.visitors.map((visitor) => ({ + ...visitor, + oidcSub: visitor.oidcSub.toLowerCase(), + email: visitor.email.toLowerCase(), + })), proposer: proposalMessage.proposer ? { ...proposalMessage.proposer,