mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-12 12:29:10 +00:00
feat(native-integrations): microsoft teams
This commit is contained in:
Generated
+598
-77
@@ -58,6 +58,7 @@
|
|||||||
"axios": "^1.6.7",
|
"axios": "^1.6.7",
|
||||||
"axios-retry": "^4.0.0",
|
"axios-retry": "^4.0.0",
|
||||||
"bcrypt": "^5.1.1",
|
"bcrypt": "^5.1.1",
|
||||||
|
"botbuilder": "^4.23.2",
|
||||||
"bullmq": "^5.4.2",
|
"bullmq": "^5.4.2",
|
||||||
"cassandra-driver": "^4.7.2",
|
"cassandra-driver": "^4.7.2",
|
||||||
"connect-redis": "^7.1.1",
|
"connect-redis": "^7.1.1",
|
||||||
@@ -2358,12 +2359,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@azure/core-auth": {
|
"node_modules/@azure/core-auth": {
|
||||||
"version": "1.7.2",
|
"version": "1.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.7.2.tgz",
|
"resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.9.0.tgz",
|
||||||
"integrity": "sha512-Igm/S3fDYmnMq1uKS38Ae1/m37B3zigdlZw+kocwEhh5GjyKjPrXKO2J6rzpC1wAxrNil/jX9BJRqBshyjnF3g==",
|
"integrity": "sha512-FPwHpZywuyasDSLMqJ6fhbOK3TqUdviZNF8OqRGA4W5Ewib2lEEZ+pBsYcBa88B2NGO/SEnYPGhyBqNlE8ilSw==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@azure/abort-controller": "^2.0.0",
|
"@azure/abort-controller": "^2.0.0",
|
||||||
"@azure/core-util": "^1.1.0",
|
"@azure/core-util": "^1.11.0",
|
||||||
"tslib": "^2.6.2"
|
"tslib": "^2.6.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -2518,14 +2520,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@azure/core-rest-pipeline": {
|
"node_modules/@azure/core-rest-pipeline": {
|
||||||
"version": "1.16.1",
|
"version": "1.19.1",
|
||||||
"resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.16.1.tgz",
|
"resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.19.1.tgz",
|
||||||
"integrity": "sha512-ExPSbgjwCoht6kB7B4MeZoBAxcQSIl29r/bPeazZJx50ej4JJCByimLOrZoIsurISNyJQQHf30b3JfqC3Hb88A==",
|
"integrity": "sha512-zHeoI3NCs53lLBbWNzQycjnYKsA1CVKlnzSNuSFcUDwBp8HHVObePxrM7HaX+Ha5Ks639H7chNC9HOaIhNS03w==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@azure/abort-controller": "^2.0.0",
|
"@azure/abort-controller": "^2.0.0",
|
||||||
"@azure/core-auth": "^1.4.0",
|
"@azure/core-auth": "^1.8.0",
|
||||||
"@azure/core-tracing": "^1.0.1",
|
"@azure/core-tracing": "^1.0.1",
|
||||||
"@azure/core-util": "^1.9.0",
|
"@azure/core-util": "^1.11.0",
|
||||||
"@azure/logger": "^1.0.0",
|
"@azure/logger": "^1.0.0",
|
||||||
"http-proxy-agent": "^7.0.0",
|
"http-proxy-agent": "^7.0.0",
|
||||||
"https-proxy-agent": "^7.0.0",
|
"https-proxy-agent": "^7.0.0",
|
||||||
@@ -2602,9 +2605,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@azure/core-util": {
|
"node_modules/@azure/core-util": {
|
||||||
"version": "1.9.0",
|
"version": "1.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.11.0.tgz",
|
||||||
"integrity": "sha512-AfalUQ1ZppaKuxPPMsFEUdX6GZPB3d9paR9d/TTL7Ow2De8cJaC7ibi7kWVlFAVPCYo31OcnGymc0R89DX8Oaw==",
|
"integrity": "sha512-DxOSLua+NdpWoSqULhjDyAZTXFdP/LKkqtYuxxz1SCN289zk3OG8UOpnCQAz/tygyACBtWp/BoO72ptK7msY8g==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@azure/abort-controller": "^2.0.0",
|
"@azure/abort-controller": "^2.0.0",
|
||||||
"tslib": "^2.6.2"
|
"tslib": "^2.6.2"
|
||||||
@@ -2625,46 +2629,60 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@azure/identity": {
|
"node_modules/@azure/identity": {
|
||||||
"version": "4.3.0",
|
"version": "4.9.1",
|
||||||
"resolved": "https://registry.npmjs.org/@azure/identity/-/identity-4.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/@azure/identity/-/identity-4.9.1.tgz",
|
||||||
"integrity": "sha512-LHZ58/RsIpIWa4hrrE2YuJ/vzG1Jv9f774RfTTAVDZDriubvJ0/S5u4pnw4akJDlS0TiJb6VMphmVUFsWmgodQ==",
|
"integrity": "sha512-986D7Cf1AOwYqSDtO/FnMAyk/Jc8qpftkGsxuehoh4F85MhQ4fICBGX/44+X1y78lN4Sqib3Bsoaoh/FvOGgmg==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@azure/abort-controller": "^1.0.0",
|
"@azure/abort-controller": "^2.0.0",
|
||||||
"@azure/core-auth": "^1.5.0",
|
"@azure/core-auth": "^1.9.0",
|
||||||
"@azure/core-client": "^1.9.2",
|
"@azure/core-client": "^1.9.2",
|
||||||
"@azure/core-rest-pipeline": "^1.1.0",
|
"@azure/core-rest-pipeline": "^1.17.0",
|
||||||
"@azure/core-tracing": "^1.0.0",
|
"@azure/core-tracing": "^1.0.0",
|
||||||
"@azure/core-util": "^1.3.0",
|
"@azure/core-util": "^1.11.0",
|
||||||
"@azure/logger": "^1.0.0",
|
"@azure/logger": "^1.0.0",
|
||||||
"@azure/msal-browser": "^3.11.1",
|
"@azure/msal-browser": "^4.2.0",
|
||||||
"@azure/msal-node": "^2.9.2",
|
"@azure/msal-node": "^3.5.0",
|
||||||
"events": "^3.0.0",
|
"open": "^10.1.0",
|
||||||
"jws": "^4.0.0",
|
|
||||||
"open": "^8.0.0",
|
|
||||||
"stoppable": "^1.1.0",
|
|
||||||
"tslib": "^2.2.0"
|
"tslib": "^2.2.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.0.0"
|
"node": ">=18.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@azure/identity/node_modules/jwa": {
|
"node_modules/@azure/identity/node_modules/@azure/abort-controller": {
|
||||||
"version": "2.0.0",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz",
|
||||||
"integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==",
|
"integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"buffer-equal-constant-time": "1.0.1",
|
"tslib": "^2.6.2"
|
||||||
"ecdsa-sig-formatter": "1.0.11",
|
},
|
||||||
"safe-buffer": "^5.0.1"
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@azure/identity/node_modules/jws": {
|
"node_modules/@azure/identity/node_modules/@azure/msal-node": {
|
||||||
"version": "4.0.0",
|
"version": "3.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-3.5.1.tgz",
|
||||||
"integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==",
|
"integrity": "sha512-dkgMYM5B6tI88r/oqf5bYd93WkenQpaWwiszJDk7avVjso8cmuKRTW97dA1RMi6RhihZFLtY1VtWxU9+sW2T5g==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"jwa": "^2.0.0",
|
"@azure/msal-common": "15.5.1",
|
||||||
"safe-buffer": "^5.0.1"
|
"jsonwebtoken": "^9.0.0",
|
||||||
|
"uuid": "^8.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@azure/identity/node_modules/uuid": {
|
||||||
|
"version": "8.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||||
|
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"uuid": "dist/bin/uuid"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@azure/keyvault-keys": {
|
"node_modules/@azure/keyvault-keys": {
|
||||||
@@ -2700,30 +2718,33 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@azure/msal-browser": {
|
"node_modules/@azure/msal-browser": {
|
||||||
"version": "3.18.0",
|
"version": "4.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-3.18.0.tgz",
|
"resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-4.11.0.tgz",
|
||||||
"integrity": "sha512-jvK5bDUWbpOaJt2Io/rjcaOVcUzkqkrCme/WntdV1SMUc67AiTcEdKuY6G/nMQ7N5Cfsk9SfpugflQwDku53yg==",
|
"integrity": "sha512-0p5Ut3wORMP+975AKvaSPIO4UytgsfAvJ7RxaTx+nkP+Hpkmm93AuiMkBWKI2x9tApU/SLgIyPz/ZwLYUIWb5Q==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@azure/msal-common": "14.13.0"
|
"@azure/msal-common": "15.5.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.8.0"
|
"node": ">=0.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@azure/msal-common": {
|
"node_modules/@azure/msal-common": {
|
||||||
"version": "14.13.0",
|
"version": "15.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-14.13.0.tgz",
|
"resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-15.5.1.tgz",
|
||||||
"integrity": "sha512-b4M/tqRzJ4jGU91BiwCsLTqChveUEyFK3qY2wGfZ0zBswIBZjAxopx5CYt5wzZFKuN15HqRDYXQbztttuIC3nA==",
|
"integrity": "sha512-oxK0khbc4Bg1bKQnqDr7ikULhVL2OHgSrIq0Vlh4b6+hm4r0lr6zPMQE8ZvmacJuh+ZZGKBM5iIObhF1q1QimQ==",
|
||||||
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.8.0"
|
"node": ">=0.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@azure/msal-node": {
|
"node_modules/@azure/msal-node": {
|
||||||
"version": "2.10.0",
|
"version": "2.16.2",
|
||||||
"resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-2.10.0.tgz",
|
"resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-2.16.2.tgz",
|
||||||
"integrity": "sha512-JxsSE0464a8IA/+q5EHKmchwNyUFJHtCH00tSXsLaOddwLjG6yVvTH6lGgPcWMhO7YWUXj/XVgVgeE9kZtsPUQ==",
|
"integrity": "sha512-An7l1hEr0w1HMMh1LU+rtDtqL7/jw74ORlc9Wnh06v7TU/xpG39/Zdr1ZJu3QpjUfKJ+E0/OXMW8DRSWTlh7qQ==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@azure/msal-common": "14.13.0",
|
"@azure/msal-common": "14.16.0",
|
||||||
"jsonwebtoken": "^9.0.0",
|
"jsonwebtoken": "^9.0.0",
|
||||||
"uuid": "^8.3.0"
|
"uuid": "^8.3.0"
|
||||||
},
|
},
|
||||||
@@ -2731,6 +2752,15 @@
|
|||||||
"node": ">=16"
|
"node": ">=16"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@azure/msal-node/node_modules/@azure/msal-common": {
|
||||||
|
"version": "14.16.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-14.16.0.tgz",
|
||||||
|
"integrity": "sha512-1KOZj9IpcDSwpNiQNjt0jDYZpQvNZay7QAEi/5DLubay40iGYtLzya/jbjRPLyOTZhEKyL1MzPuw2HqBCjceYA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@azure/msal-node/node_modules/uuid": {
|
"node_modules/@azure/msal-node/node_modules/uuid": {
|
||||||
"version": "8.3.2",
|
"version": "8.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||||
@@ -9759,9 +9789,10 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/@types/jsonwebtoken": {
|
"node_modules/@types/jsonwebtoken": {
|
||||||
"version": "9.0.5",
|
"version": "9.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.6.tgz",
|
||||||
"integrity": "sha512-VRLSGzik+Unrup6BsouBeHsf4d1hOEgYWTm/7Nmw1sXoN1+tRly/Gy/po3yeahnP4jfnQWWAhQAqcNfH7ngOkA==",
|
"integrity": "sha512-/5hndP5dCjloafCXns6SZyESp3Ldq7YjH3zwzwczYnjxIT0Fqzk5ROSYVGfFyczIue7IUEj8hkvLbPoLQ18vQw==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
@@ -10157,6 +10188,15 @@
|
|||||||
"@types/webidl-conversions": "*"
|
"@types/webidl-conversions": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/ws": {
|
||||||
|
"version": "6.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-6.0.4.tgz",
|
||||||
|
"integrity": "sha512-PpPrX7SZW9re6+Ha8ojZG4Se8AZXgf0GK6zmfqEuCsY49LFDNXO3SByp44X3dFEqtB73lkCDAdUazhAjVPiNwg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/xml-encryption": {
|
"node_modules/@types/xml-encryption": {
|
||||||
"version": "1.2.4",
|
"version": "1.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/@types/xml-encryption/-/xml-encryption-1.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/@types/xml-encryption/-/xml-encryption-1.2.4.tgz",
|
||||||
@@ -10909,6 +10949,12 @@
|
|||||||
"node": ">=0.4.0"
|
"node": ">=0.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/adaptivecards": {
|
||||||
|
"version": "1.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/adaptivecards/-/adaptivecards-1.2.3.tgz",
|
||||||
|
"integrity": "sha512-amQ5OSW3OpIkrxVKLjxVBPk/T49yuOtnqs1z5ZPfZr0+OpTovzmiHbyoAGDIsu5SNYHwOZFp/3LGOnRaALFa/g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/adm-zip": {
|
"node_modules/adm-zip": {
|
||||||
"version": "0.5.12",
|
"version": "0.5.12",
|
||||||
"resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.12.tgz",
|
"resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.12.tgz",
|
||||||
@@ -11769,6 +11815,245 @@
|
|||||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/botbuilder": {
|
||||||
|
"version": "4.23.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/botbuilder/-/botbuilder-4.23.2.tgz",
|
||||||
|
"integrity": "sha512-E3UjkPlAmT8TidZIAW1ucVRejz0KBbWEn0wNxJ37GncPl8txhmWvs21xn3hBrifSR4++Y6q/hp/A5cHFQcFGJw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@azure/core-http": "^3.0.4",
|
||||||
|
"@azure/msal-node": "^2.13.1",
|
||||||
|
"axios": "^1.7.7",
|
||||||
|
"botbuilder-core": "4.23.2",
|
||||||
|
"botbuilder-stdlib": "4.23.2-internal",
|
||||||
|
"botframework-connector": "4.23.2",
|
||||||
|
"botframework-schema": "4.23.2",
|
||||||
|
"botframework-streaming": "4.23.2",
|
||||||
|
"dayjs": "^1.11.13",
|
||||||
|
"filenamify": "^6.0.0",
|
||||||
|
"fs-extra": "^11.2.0",
|
||||||
|
"htmlparser2": "^9.0.1",
|
||||||
|
"uuid": "^10.0.0",
|
||||||
|
"zod": "^3.23.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/botbuilder-core": {
|
||||||
|
"version": "4.23.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/botbuilder-core/-/botbuilder-core-4.23.2.tgz",
|
||||||
|
"integrity": "sha512-GwrfkfbEJqCLnhDVc6uKlzKtrptfYTxQxHYfF22s1AxTKdTiA9vsDN9rXq8We7QUPXFOF1ylF1e87k0fQ3Sf+A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"botbuilder-dialogs-adaptive-runtime-core": "4.23.2-preview",
|
||||||
|
"botbuilder-stdlib": "4.23.2-internal",
|
||||||
|
"botframework-connector": "4.23.2",
|
||||||
|
"botframework-schema": "4.23.2",
|
||||||
|
"uuid": "^10.0.0",
|
||||||
|
"zod": "^3.23.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/botbuilder-core/node_modules/uuid": {
|
||||||
|
"version": "10.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz",
|
||||||
|
"integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==",
|
||||||
|
"funding": [
|
||||||
|
"https://github.com/sponsors/broofa",
|
||||||
|
"https://github.com/sponsors/ctavan"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"uuid": "dist/bin/uuid"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/botbuilder-dialogs-adaptive-runtime-core": {
|
||||||
|
"version": "4.23.2-preview",
|
||||||
|
"resolved": "https://registry.npmjs.org/botbuilder-dialogs-adaptive-runtime-core/-/botbuilder-dialogs-adaptive-runtime-core-4.23.2-preview.tgz",
|
||||||
|
"integrity": "sha512-+b5oHSDNodYXPnQbub+hTNmQLtBB4hj/ZW73g4Sqv5oAdqHoK/dX181UpiFAvDpHGe8Kx3SNYtRHJIj71u4t0Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"dependency-graph": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/botbuilder-stdlib": {
|
||||||
|
"version": "4.23.2-internal",
|
||||||
|
"resolved": "https://registry.npmjs.org/botbuilder-stdlib/-/botbuilder-stdlib-4.23.2-internal.tgz",
|
||||||
|
"integrity": "sha512-5WAu59gCZX3lz2NNw28q+IlAAFIQjXij0wXmN8qh+Tg4PQOCl+5P3hoYqcHIWtGd5Kgn+dpaHtBIewl2LaOXKQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/botbuilder/node_modules/fs-extra": {
|
||||||
|
"version": "11.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.0.tgz",
|
||||||
|
"integrity": "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"graceful-fs": "^4.2.0",
|
||||||
|
"jsonfile": "^6.0.1",
|
||||||
|
"universalify": "^2.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/botbuilder/node_modules/uuid": {
|
||||||
|
"version": "10.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz",
|
||||||
|
"integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==",
|
||||||
|
"funding": [
|
||||||
|
"https://github.com/sponsors/broofa",
|
||||||
|
"https://github.com/sponsors/ctavan"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"uuid": "dist/bin/uuid"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/botframework-connector": {
|
||||||
|
"version": "4.23.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/botframework-connector/-/botframework-connector-4.23.2.tgz",
|
||||||
|
"integrity": "sha512-G4gDpEHhA8AUKbgHMJ1LUjsuDlRPFEcXnH8ouxLI0opT2p1LcUSAAgS4hoOrkaylr04zxrUI0nEWkuWDiWDwzw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@azure/core-http": "^3.0.4",
|
||||||
|
"@azure/identity": "^4.4.1",
|
||||||
|
"@azure/msal-node": "^2.13.1",
|
||||||
|
"@types/jsonwebtoken": "9.0.6",
|
||||||
|
"axios": "^1.7.7",
|
||||||
|
"base64url": "^3.0.0",
|
||||||
|
"botbuilder-stdlib": "4.23.2-internal",
|
||||||
|
"botframework-schema": "4.23.2",
|
||||||
|
"buffer": "^6.0.3",
|
||||||
|
"cross-fetch": "^4.0.0",
|
||||||
|
"https-proxy-agent": "^7.0.5",
|
||||||
|
"jsonwebtoken": "^9.0.2",
|
||||||
|
"node-fetch": "^2.7.0",
|
||||||
|
"openssl-wrapper": "^0.3.4",
|
||||||
|
"rsa-pem-from-mod-exp": "^0.8.6",
|
||||||
|
"zod": "^3.23.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/botframework-connector/node_modules/agent-base": {
|
||||||
|
"version": "7.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz",
|
||||||
|
"integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/botframework-connector/node_modules/debug": {
|
||||||
|
"version": "4.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
|
||||||
|
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "^2.1.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"supports-color": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/botframework-connector/node_modules/https-proxy-agent": {
|
||||||
|
"version": "7.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
|
||||||
|
"integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"agent-base": "^7.1.2",
|
||||||
|
"debug": "4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/botframework-schema": {
|
||||||
|
"version": "4.23.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/botframework-schema/-/botframework-schema-4.23.2.tgz",
|
||||||
|
"integrity": "sha512-eO1fmvfCEVJfnqNNAerQU8CHp0FMYTyE459ztNx2k1QJYMl/ds+LNNkGIUlQQFsdVbi2umadK+6hL2a9kqXMqQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"adaptivecards": "1.2.3",
|
||||||
|
"uuid": "^10.0.0",
|
||||||
|
"zod": "^3.23.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/botframework-schema/node_modules/uuid": {
|
||||||
|
"version": "10.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz",
|
||||||
|
"integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==",
|
||||||
|
"funding": [
|
||||||
|
"https://github.com/sponsors/broofa",
|
||||||
|
"https://github.com/sponsors/ctavan"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"uuid": "dist/bin/uuid"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/botframework-streaming": {
|
||||||
|
"version": "4.23.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/botframework-streaming/-/botframework-streaming-4.23.2.tgz",
|
||||||
|
"integrity": "sha512-UBF0puC2RX8Z0dkN/ag9BuSNWdB5MUtobZLzeaH1h5t7QAYAVNk/SrsUgkBwUsqWpwaZqU+vrGOeByLShDcvaQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "18.19.47",
|
||||||
|
"@types/ws": "^6.0.3",
|
||||||
|
"uuid": "^10.0.0",
|
||||||
|
"ws": "^7.5.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/botframework-streaming/node_modules/@types/node": {
|
||||||
|
"version": "18.19.47",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.47.tgz",
|
||||||
|
"integrity": "sha512-1f7dB3BL/bpd9tnDJrrHb66Y+cVrhxSOTGorRNdHwYTUlTay3HuTDPKo9a/4vX9pMQkhYBcAbL4jQdNlhCFP9A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"undici-types": "~5.26.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/botframework-streaming/node_modules/undici-types": {
|
||||||
|
"version": "5.26.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
|
||||||
|
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/botframework-streaming/node_modules/uuid": {
|
||||||
|
"version": "10.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz",
|
||||||
|
"integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==",
|
||||||
|
"funding": [
|
||||||
|
"https://github.com/sponsors/broofa",
|
||||||
|
"https://github.com/sponsors/ctavan"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"uuid": "dist/bin/uuid"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/botframework-streaming/node_modules/ws": {
|
||||||
|
"version": "7.5.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz",
|
||||||
|
"integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.3.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"bufferutil": "^4.0.1",
|
||||||
|
"utf-8-validate": "^5.0.2"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"bufferutil": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"utf-8-validate": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/bottleneck": {
|
"node_modules/bottleneck": {
|
||||||
"version": "2.19.5",
|
"version": "2.19.5",
|
||||||
"resolved": "https://registry.npmjs.org/bottleneck/-/bottleneck-2.19.5.tgz",
|
"resolved": "https://registry.npmjs.org/bottleneck/-/bottleneck-2.19.5.tgz",
|
||||||
@@ -11896,6 +12181,21 @@
|
|||||||
"uuid": "^9.0.0"
|
"uuid": "^9.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/bundle-name": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"run-applescript": "^7.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/bundle-require": {
|
"node_modules/bundle-require": {
|
||||||
"version": "4.0.2",
|
"version": "4.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/bundle-require/-/bundle-require-4.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/bundle-require/-/bundle-require-4.0.2.tgz",
|
||||||
@@ -12548,6 +12848,15 @@
|
|||||||
"node": ">=12.0.0"
|
"node": ">=12.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/cross-fetch": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-uKm5PU+MHTootlWEY+mZ4vvXoCn4fLQxT9dSc1sXVMSFkINTJVN8cAQROpwcKm8bJ/c7rgZVIBWzH5T78sNZZw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"node-fetch": "^2.7.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/cross-spawn": {
|
"node_modules/cross-spawn": {
|
||||||
"version": "7.0.6",
|
"version": "7.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||||
@@ -12629,6 +12938,12 @@
|
|||||||
"node": "*"
|
"node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/dayjs": {
|
||||||
|
"version": "1.11.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz",
|
||||||
|
"integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/dc-polyfill": {
|
"node_modules/dc-polyfill": {
|
||||||
"version": "0.1.6",
|
"version": "0.1.6",
|
||||||
"resolved": "https://registry.npmjs.org/dc-polyfill/-/dc-polyfill-0.1.6.tgz",
|
"resolved": "https://registry.npmjs.org/dc-polyfill/-/dc-polyfill-0.1.6.tgz",
|
||||||
@@ -12768,6 +13083,34 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/default-browser": {
|
||||||
|
"version": "5.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz",
|
||||||
|
"integrity": "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"bundle-name": "^4.1.0",
|
||||||
|
"default-browser-id": "^5.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/default-browser-id": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/define-data-property": {
|
"node_modules/define-data-property": {
|
||||||
"version": "1.1.4",
|
"version": "1.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
|
||||||
@@ -12786,11 +13129,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/define-lazy-prop": {
|
"node_modules/define-lazy-prop": {
|
||||||
"version": "2.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz",
|
||||||
"integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==",
|
"integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==",
|
||||||
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/define-properties": {
|
"node_modules/define-properties": {
|
||||||
@@ -12850,6 +13197,15 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/dependency-graph": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-cW3gggJ28HZ/LExwxP2B++aiKxhJXMSIt9K48FOXQkm+vuG5gyatXnLsONRJdzO/7VfjDIiaOOa/bs4l464Lwg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/deprecation": {
|
"node_modules/deprecation": {
|
||||||
"version": "2.3.1",
|
"version": "2.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz",
|
||||||
@@ -12923,6 +13279,47 @@
|
|||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/dom-serializer": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"domelementtype": "^2.3.0",
|
||||||
|
"domhandler": "^5.0.2",
|
||||||
|
"entities": "^4.2.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/domelementtype": {
|
||||||
|
"version": "2.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
|
||||||
|
"integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/fb55"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "BSD-2-Clause"
|
||||||
|
},
|
||||||
|
"node_modules/domhandler": {
|
||||||
|
"version": "5.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
|
||||||
|
"integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"domelementtype": "^2.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/domhandler?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/dompurify": {
|
"node_modules/dompurify": {
|
||||||
"version": "3.2.4",
|
"version": "3.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.4.tgz",
|
||||||
@@ -12931,6 +13328,20 @@
|
|||||||
"@types/trusted-types": "^2.0.7"
|
"@types/trusted-types": "^2.0.7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/domutils": {
|
||||||
|
"version": "3.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
|
||||||
|
"integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"dom-serializer": "^2.0.0",
|
||||||
|
"domelementtype": "^2.3.0",
|
||||||
|
"domhandler": "^5.0.3"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/domutils?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/dotenv": {
|
"node_modules/dotenv": {
|
||||||
"version": "16.4.1",
|
"version": "16.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.1.tgz",
|
||||||
@@ -14251,6 +14662,33 @@
|
|||||||
"node": "^10.12.0 || >=12.0.0"
|
"node": "^10.12.0 || >=12.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/filename-reserved-regex": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-hn4cQfU6GOT/7cFHXBqeBg2TbrMBgdD0kcjLhvSQYYwm3s4B6cjvBfb7nBALJLAXqmU5xajSa7X2NnUud/VCdw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/filenamify": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/filenamify/-/filenamify-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-vqIlNogKeyD3yzrm0yhRMQg8hOVwYcYRfjEoODd49iCprMn4HL85gK3HcykQE53EPIpX3HcAbGA5ELQv216dAQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"filename-reserved-regex": "^3.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fill-range": {
|
"node_modules/fill-range": {
|
||||||
"version": "7.1.1",
|
"version": "7.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||||
@@ -15435,6 +15873,25 @@
|
|||||||
],
|
],
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/htmlparser2": {
|
||||||
|
"version": "9.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.1.0.tgz",
|
||||||
|
"integrity": "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==",
|
||||||
|
"funding": [
|
||||||
|
"https://github.com/fb55/htmlparser2?sponsor=1",
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/fb55"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"domelementtype": "^2.3.0",
|
||||||
|
"domhandler": "^5.0.3",
|
||||||
|
"domutils": "^3.1.0",
|
||||||
|
"entities": "^4.5.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/http-cache-semantics": {
|
"node_modules/http-cache-semantics": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz",
|
||||||
@@ -15948,6 +16405,39 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/is-inside-container": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"is-docker": "^3.0.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"is-inside-container": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.16"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/is-inside-container/node_modules/is-docker": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"is-docker": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/is-interactive": {
|
"node_modules/is-interactive": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz",
|
||||||
@@ -16475,7 +16965,6 @@
|
|||||||
"version": "6.1.0",
|
"version": "6.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
|
||||||
"integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
|
"integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"universalify": "^2.0.0"
|
"universalify": "^2.0.0"
|
||||||
},
|
},
|
||||||
@@ -18523,16 +19012,33 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/open": {
|
"node_modules/open": {
|
||||||
"version": "8.4.2",
|
"version": "10.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/open/-/open-10.1.1.tgz",
|
||||||
"integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==",
|
"integrity": "sha512-zy1wx4+P3PfhXSEPJNtZmJXfhkkIaxU1VauWIrDZw1O7uJRDRJtKr9n3Ic4NgbA16KyOxOXO2ng9gYwCdXuSXA==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"define-lazy-prop": "^2.0.0",
|
"default-browser": "^5.2.1",
|
||||||
"is-docker": "^2.1.1",
|
"define-lazy-prop": "^3.0.0",
|
||||||
"is-wsl": "^2.2.0"
|
"is-inside-container": "^1.0.0",
|
||||||
|
"is-wsl": "^3.1.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/open/node_modules/is-wsl": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"is-inside-container": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
@@ -18557,6 +19063,12 @@
|
|||||||
"url": "https://github.com/sponsors/panva"
|
"url": "https://github.com/sponsors/panva"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/openssl-wrapper": {
|
||||||
|
"version": "0.3.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/openssl-wrapper/-/openssl-wrapper-0.3.4.tgz",
|
||||||
|
"integrity": "sha512-iITsrx6Ho8V3/2OVtmZzzX8wQaKAaFXEJQdzoPUZDtyf5jWFlqo+h+OhGT4TATQ47f9ACKHua8nw7Qoy85aeKQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/opentracing": {
|
"node_modules/opentracing": {
|
||||||
"version": "0.14.7",
|
"version": "0.14.7",
|
||||||
"resolved": "https://registry.npmjs.org/opentracing/-/opentracing-0.14.7.tgz",
|
"resolved": "https://registry.npmjs.org/opentracing/-/opentracing-0.14.7.tgz",
|
||||||
@@ -20542,6 +21054,24 @@
|
|||||||
"resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz",
|
"resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz",
|
||||||
"integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw=="
|
"integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw=="
|
||||||
},
|
},
|
||||||
|
"node_modules/rsa-pem-from-mod-exp": {
|
||||||
|
"version": "0.8.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/rsa-pem-from-mod-exp/-/rsa-pem-from-mod-exp-0.8.6.tgz",
|
||||||
|
"integrity": "sha512-c5ouQkOvGHF1qomUUDJGFcXsomeSO2gbEs6hVhMAtlkE1CuaZase/WzoaKFG/EZQuNmq6pw/EMCeEnDvOgCJYQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/run-applescript": {
|
||||||
|
"version": "7.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.0.0.tgz",
|
||||||
|
"integrity": "sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/run-parallel": {
|
"node_modules/run-parallel": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
|
||||||
@@ -21416,15 +21946,6 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/stoppable": {
|
|
||||||
"version": "1.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/stoppable/-/stoppable-1.1.0.tgz",
|
|
||||||
"integrity": "sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=4",
|
|
||||||
"npm": ">=6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/stream-events": {
|
"node_modules/stream-events": {
|
||||||
"version": "1.0.5",
|
"version": "1.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz",
|
||||||
@@ -23208,7 +23729,6 @@
|
|||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
|
||||||
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
|
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 10.0.0"
|
"node": ">= 10.0.0"
|
||||||
}
|
}
|
||||||
@@ -24639,9 +25159,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/zod": {
|
"node_modules/zod": {
|
||||||
"version": "3.22.4",
|
"version": "3.24.3",
|
||||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz",
|
"resolved": "https://registry.npmjs.org/zod/-/zod-3.24.3.tgz",
|
||||||
"integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==",
|
"integrity": "sha512-HhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+C4+MsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg==",
|
||||||
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -175,6 +175,7 @@
|
|||||||
"axios": "^1.6.7",
|
"axios": "^1.6.7",
|
||||||
"axios-retry": "^4.0.0",
|
"axios-retry": "^4.0.0",
|
||||||
"bcrypt": "^5.1.1",
|
"bcrypt": "^5.1.1",
|
||||||
|
"botbuilder": "^4.23.2",
|
||||||
"bullmq": "^5.4.2",
|
"bullmq": "^5.4.2",
|
||||||
"cassandra-driver": "^4.7.2",
|
"cassandra-driver": "^4.7.2",
|
||||||
"connect-redis": "^7.1.1",
|
"connect-redis": "^7.1.1",
|
||||||
|
|||||||
Vendored
+2
@@ -98,6 +98,7 @@ import { TUserServiceFactory } from "@app/services/user/user-service";
|
|||||||
import { TUserEngagementServiceFactory } from "@app/services/user-engagement/user-engagement-service";
|
import { TUserEngagementServiceFactory } from "@app/services/user-engagement/user-engagement-service";
|
||||||
import { TWebhookServiceFactory } from "@app/services/webhook/webhook-service";
|
import { TWebhookServiceFactory } from "@app/services/webhook/webhook-service";
|
||||||
import { TWorkflowIntegrationServiceFactory } from "@app/services/workflow-integration/workflow-integration-service";
|
import { TWorkflowIntegrationServiceFactory } from "@app/services/workflow-integration/workflow-integration-service";
|
||||||
|
import { TMicrosoftTeamsServiceFactory } from "@app/services/microsoft-teams/microsoft-teams-service";
|
||||||
|
|
||||||
declare module "@fastify/request-context" {
|
declare module "@fastify/request-context" {
|
||||||
interface RequestContextData {
|
interface RequestContextData {
|
||||||
@@ -241,6 +242,7 @@ declare module "fastify" {
|
|||||||
kmipOperation: TKmipOperationServiceFactory;
|
kmipOperation: TKmipOperationServiceFactory;
|
||||||
gateway: TGatewayServiceFactory;
|
gateway: TGatewayServiceFactory;
|
||||||
secretRotationV2: TSecretRotationV2ServiceFactory;
|
secretRotationV2: TSecretRotationV2ServiceFactory;
|
||||||
|
microsoftTeams: TMicrosoftTeamsServiceFactory;
|
||||||
};
|
};
|
||||||
// this is exclusive use for middlewares in which we need to inject data
|
// this is exclusive use for middlewares in which we need to inject data
|
||||||
// everywhere else access using service layer
|
// everywhere else access using service layer
|
||||||
|
|||||||
Vendored
+20
@@ -423,6 +423,16 @@ import {
|
|||||||
TWorkflowIntegrationsInsert,
|
TWorkflowIntegrationsInsert,
|
||||||
TWorkflowIntegrationsUpdate
|
TWorkflowIntegrationsUpdate
|
||||||
} from "@app/db/schemas";
|
} from "@app/db/schemas";
|
||||||
|
import {
|
||||||
|
TMicrosoftTeamsIntegrations,
|
||||||
|
TMicrosoftTeamsIntegrationsInsert,
|
||||||
|
TMicrosoftTeamsIntegrationsUpdate
|
||||||
|
} from "@app/db/schemas/microsoft-teams-integrations";
|
||||||
|
import {
|
||||||
|
TProjectMicrosoftTeamsConfigs,
|
||||||
|
TProjectMicrosoftTeamsConfigsInsert,
|
||||||
|
TProjectMicrosoftTeamsConfigsUpdate
|
||||||
|
} from "@app/db/schemas/project-microsoft-teams-configs";
|
||||||
|
|
||||||
declare module "knex" {
|
declare module "knex" {
|
||||||
namespace Knex {
|
namespace Knex {
|
||||||
@@ -994,5 +1004,15 @@ declare module "knex/types/tables" {
|
|||||||
TSecretRotationV2SecretMappingsInsert,
|
TSecretRotationV2SecretMappingsInsert,
|
||||||
TSecretRotationV2SecretMappingsUpdate
|
TSecretRotationV2SecretMappingsUpdate
|
||||||
>;
|
>;
|
||||||
|
[TableName.MicrosoftTeamsIntegrations]: KnexOriginal.CompositeTableType<
|
||||||
|
TMicrosoftTeamsIntegrations,
|
||||||
|
TMicrosoftTeamsIntegrationsInsert,
|
||||||
|
TMicrosoftTeamsIntegrationsUpdate
|
||||||
|
>;
|
||||||
|
[TableName.ProjectMicrosoftTeamsConfigs]: KnexOriginal.CompositeTableType<
|
||||||
|
TProjectMicrosoftTeamsConfigs,
|
||||||
|
TProjectMicrosoftTeamsConfigsInsert,
|
||||||
|
TProjectMicrosoftTeamsConfigsUpdate
|
||||||
|
>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,123 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
|
||||||
|
|
||||||
|
const encryptedMicrosoftTeamsAppIdColumn = "encryptedMicrosoftTeamsAppId";
|
||||||
|
const encryptedMicrosoftTeamsClientSecretColumn = "encryptedMicrosoftTeamsClientSecret";
|
||||||
|
const encryptedMicrosoftTeamsBotIdColumn = "encryptedMicrosoftTeamsBotId";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
const superAdminHasEncryptedMicrosoftTeamsClientIdColumn = await knex.schema.hasColumn(
|
||||||
|
TableName.SuperAdmin,
|
||||||
|
encryptedMicrosoftTeamsAppIdColumn
|
||||||
|
);
|
||||||
|
const superAdminHasEncryptedMicrosoftTeamsClientSecretColumn = await knex.schema.hasColumn(
|
||||||
|
TableName.SuperAdmin,
|
||||||
|
encryptedMicrosoftTeamsClientSecretColumn
|
||||||
|
);
|
||||||
|
const superAdminHasEncryptedMicrosoftTeamsBotIdColumn = await knex.schema.hasColumn(
|
||||||
|
TableName.SuperAdmin,
|
||||||
|
encryptedMicrosoftTeamsBotIdColumn
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!superAdminHasEncryptedMicrosoftTeamsClientIdColumn || !superAdminHasEncryptedMicrosoftTeamsClientSecretColumn) {
|
||||||
|
await knex.schema.alterTable(TableName.SuperAdmin, (table) => {
|
||||||
|
if (!superAdminHasEncryptedMicrosoftTeamsClientIdColumn) {
|
||||||
|
table.binary(encryptedMicrosoftTeamsAppIdColumn).nullable();
|
||||||
|
}
|
||||||
|
if (!superAdminHasEncryptedMicrosoftTeamsClientSecretColumn) {
|
||||||
|
table.binary(encryptedMicrosoftTeamsClientSecretColumn).nullable();
|
||||||
|
}
|
||||||
|
if (!superAdminHasEncryptedMicrosoftTeamsBotIdColumn) {
|
||||||
|
table.binary(encryptedMicrosoftTeamsBotIdColumn).nullable();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(await knex.schema.hasColumn(TableName.WorkflowIntegrations, "status"))) {
|
||||||
|
await knex.schema.alterTable(TableName.WorkflowIntegrations, (table) => {
|
||||||
|
table.enu("status", ["pending", "installed", "failed"]).notNullable().defaultTo("installed"); // defaults to installed so we can have backwards compatibility with existing workflow integrations
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(await knex.schema.hasTable(TableName.MicrosoftTeamsIntegrations))) {
|
||||||
|
await knex.schema.createTable(TableName.MicrosoftTeamsIntegrations, (table) => {
|
||||||
|
table.uuid("id", { primaryKey: true }).notNullable();
|
||||||
|
table.foreign("id").references("id").inTable(TableName.WorkflowIntegrations).onDelete("CASCADE"); // the ID itself is the workflow integration ID
|
||||||
|
|
||||||
|
table.string("internalTeamsAppId").nullable();
|
||||||
|
table.string("tenantId").notNullable();
|
||||||
|
table.binary("encryptedAccessToken").nullable();
|
||||||
|
table.binary("encryptedBotAccessToken").nullable();
|
||||||
|
|
||||||
|
table.timestamps(true, true, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
await createOnUpdateTrigger(knex, TableName.MicrosoftTeamsIntegrations);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(await knex.schema.hasTable(TableName.ProjectMicrosoftTeamsConfigs))) {
|
||||||
|
await knex.schema.createTable(TableName.ProjectMicrosoftTeamsConfigs, (tb) => {
|
||||||
|
tb.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
|
tb.string("projectId").notNullable().unique();
|
||||||
|
tb.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
|
||||||
|
tb.uuid("microsoftTeamsIntegrationId").notNullable();
|
||||||
|
tb.foreign("microsoftTeamsIntegrationId")
|
||||||
|
.references("id")
|
||||||
|
.inTable(TableName.MicrosoftTeamsIntegrations)
|
||||||
|
.onDelete("CASCADE");
|
||||||
|
tb.boolean("isAccessRequestNotificationEnabled").notNullable().defaultTo(false);
|
||||||
|
tb.boolean("isSecretRequestNotificationEnabled").notNullable().defaultTo(false);
|
||||||
|
|
||||||
|
tb.jsonb("accessRequestChannels").notNullable(); // {teamId: string, channelIds: string[]}
|
||||||
|
tb.jsonb("secretRequestChannels").notNullable(); // {teamId: string, channelIds: string[]}
|
||||||
|
tb.timestamps(true, true, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
await createOnUpdateTrigger(knex, TableName.ProjectMicrosoftTeamsConfigs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
const hasEncryptedMicrosoftTeamsClientIdColumn = await knex.schema.hasColumn(
|
||||||
|
TableName.SuperAdmin,
|
||||||
|
encryptedMicrosoftTeamsAppIdColumn
|
||||||
|
);
|
||||||
|
const hasEncryptedMicrosoftTeamsClientSecretColumn = await knex.schema.hasColumn(
|
||||||
|
TableName.SuperAdmin,
|
||||||
|
encryptedMicrosoftTeamsClientSecretColumn
|
||||||
|
);
|
||||||
|
const hasEncryptedMicrosoftTeamsBotIdColumn = await knex.schema.hasColumn(
|
||||||
|
TableName.SuperAdmin,
|
||||||
|
encryptedMicrosoftTeamsBotIdColumn
|
||||||
|
);
|
||||||
|
|
||||||
|
if (hasEncryptedMicrosoftTeamsClientIdColumn || hasEncryptedMicrosoftTeamsClientSecretColumn) {
|
||||||
|
await knex.schema.alterTable(TableName.SuperAdmin, (table) => {
|
||||||
|
if (hasEncryptedMicrosoftTeamsClientIdColumn) {
|
||||||
|
table.dropColumn(encryptedMicrosoftTeamsAppIdColumn);
|
||||||
|
}
|
||||||
|
if (hasEncryptedMicrosoftTeamsClientSecretColumn) {
|
||||||
|
table.dropColumn(encryptedMicrosoftTeamsClientSecretColumn);
|
||||||
|
}
|
||||||
|
if (hasEncryptedMicrosoftTeamsBotIdColumn) {
|
||||||
|
table.dropColumn(encryptedMicrosoftTeamsBotIdColumn);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (await knex.schema.hasColumn(TableName.WorkflowIntegrations, "status")) {
|
||||||
|
await knex.schema.alterTable(TableName.WorkflowIntegrations, (table) => {
|
||||||
|
table.dropColumn("status");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await knex.schema.hasTable(TableName.ProjectMicrosoftTeamsConfigs)) {
|
||||||
|
await knex.schema.dropTableIfExists(TableName.ProjectMicrosoftTeamsConfigs);
|
||||||
|
await dropOnUpdateTrigger(knex, TableName.ProjectMicrosoftTeamsConfigs);
|
||||||
|
}
|
||||||
|
if (await knex.schema.hasTable(TableName.MicrosoftTeamsIntegrations)) {
|
||||||
|
await knex.schema.dropTableIfExists(TableName.MicrosoftTeamsIntegrations);
|
||||||
|
await dropOnUpdateTrigger(knex, TableName.MicrosoftTeamsIntegrations);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,7 +20,7 @@ export const CertificatesSchema = z.object({
|
|||||||
notAfter: z.date(),
|
notAfter: z.date(),
|
||||||
revokedAt: z.date().nullable().optional(),
|
revokedAt: z.date().nullable().optional(),
|
||||||
revocationReason: z.number().nullable().optional(),
|
revocationReason: z.number().nullable().optional(),
|
||||||
altNames: z.string().default("").nullable().optional(),
|
altNames: z.string().nullable().optional(),
|
||||||
caCertId: z.string().uuid(),
|
caCertId: z.string().uuid(),
|
||||||
certificateTemplateId: z.string().uuid().nullable().optional(),
|
certificateTemplateId: z.string().uuid().nullable().optional(),
|
||||||
keyUsages: z.string().array().nullable().optional(),
|
keyUsages: z.string().array().nullable().optional(),
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ export * from "./kms-keys";
|
|||||||
export * from "./kms-root-config";
|
export * from "./kms-root-config";
|
||||||
export * from "./ldap-configs";
|
export * from "./ldap-configs";
|
||||||
export * from "./ldap-group-maps";
|
export * from "./ldap-group-maps";
|
||||||
|
export * from "./microsoft-teams-integrations";
|
||||||
export * from "./models";
|
export * from "./models";
|
||||||
export * from "./oidc-configs";
|
export * from "./oidc-configs";
|
||||||
export * from "./org-bots";
|
export * from "./org-bots";
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export const KmipOrgServerCertificatesSchema = z.object({
|
|||||||
id: z.string().uuid(),
|
id: z.string().uuid(),
|
||||||
orgId: z.string().uuid(),
|
orgId: z.string().uuid(),
|
||||||
commonName: z.string(),
|
commonName: z.string(),
|
||||||
altNames: z.string(),
|
altNames: z.string().nullable().optional(),
|
||||||
serialNumber: z.string(),
|
serialNumber: z.string(),
|
||||||
keyAlgorithm: z.string(),
|
keyAlgorithm: z.string(),
|
||||||
issuedAt: z.date(),
|
issuedAt: z.date(),
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
// Code generated by automation script, DO NOT EDIT.
|
||||||
|
// Automated by pulling database and generating zod schema
|
||||||
|
// To update. Just run npm run generate:schema
|
||||||
|
// Written by akhilmhdh.
|
||||||
|
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { zodBuffer } from "@app/lib/zod";
|
||||||
|
|
||||||
|
import { TImmutableDBKeys } from "./models";
|
||||||
|
|
||||||
|
export const MicrosoftTeamsIntegrationsSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
internalTeamsAppId: z.string().nullable().optional(),
|
||||||
|
tenantId: z.string(),
|
||||||
|
encryptedAccessToken: zodBuffer.nullable().optional(),
|
||||||
|
encryptedBotAccessToken: zodBuffer.nullable().optional(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TMicrosoftTeamsIntegrations = z.infer<typeof MicrosoftTeamsIntegrationsSchema>;
|
||||||
|
export type TMicrosoftTeamsIntegrationsInsert = Omit<
|
||||||
|
z.input<typeof MicrosoftTeamsIntegrationsSchema>,
|
||||||
|
TImmutableDBKeys
|
||||||
|
>;
|
||||||
|
export type TMicrosoftTeamsIntegrationsUpdate = Partial<
|
||||||
|
Omit<z.input<typeof MicrosoftTeamsIntegrationsSchema>, TImmutableDBKeys>
|
||||||
|
>;
|
||||||
@@ -146,7 +146,9 @@ export enum TableName {
|
|||||||
KmipOrgServerCertificates = "kmip_org_server_certificates",
|
KmipOrgServerCertificates = "kmip_org_server_certificates",
|
||||||
KmipClientCertificates = "kmip_client_certificates",
|
KmipClientCertificates = "kmip_client_certificates",
|
||||||
SecretRotationV2 = "secret_rotations_v2",
|
SecretRotationV2 = "secret_rotations_v2",
|
||||||
SecretRotationV2SecretMapping = "secret_rotation_v2_secret_mappings"
|
SecretRotationV2SecretMapping = "secret_rotation_v2_secret_mappings",
|
||||||
|
MicrosoftTeamsIntegrations = "microsoft_teams_integrations",
|
||||||
|
ProjectMicrosoftTeamsConfigs = "project_microsoft_teams_configs"
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TImmutableDBKeys = "id" | "createdAt" | "updatedAt";
|
export type TImmutableDBKeys = "id" | "createdAt" | "updatedAt";
|
||||||
|
|||||||
@@ -30,9 +30,9 @@ export const OidcConfigsSchema = z.object({
|
|||||||
updatedAt: z.date(),
|
updatedAt: z.date(),
|
||||||
orgId: z.string().uuid(),
|
orgId: z.string().uuid(),
|
||||||
lastUsed: z.date().nullable().optional(),
|
lastUsed: z.date().nullable().optional(),
|
||||||
|
manageGroupMemberships: z.boolean().default(false),
|
||||||
encryptedOidcClientId: zodBuffer,
|
encryptedOidcClientId: zodBuffer,
|
||||||
encryptedOidcClientSecret: zodBuffer,
|
encryptedOidcClientSecret: zodBuffer,
|
||||||
manageGroupMemberships: z.boolean().default(false),
|
|
||||||
jwtSignatureAlgorithm: z.string().default("RS256")
|
jwtSignatureAlgorithm: z.string().default("RS256")
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ export const OrganizationsSchema = z.object({
|
|||||||
defaultMembershipRole: z.string().default("member"),
|
defaultMembershipRole: z.string().default("member"),
|
||||||
enforceMfa: z.boolean().default(false),
|
enforceMfa: z.boolean().default(false),
|
||||||
selectedMfaMethod: z.string().nullable().optional(),
|
selectedMfaMethod: z.string().nullable().optional(),
|
||||||
|
secretShareSendToAnyone: z.boolean().default(true).nullable().optional(),
|
||||||
allowSecretSharingOutsideOrganization: z.boolean().default(true).nullable().optional(),
|
allowSecretSharingOutsideOrganization: z.boolean().default(true).nullable().optional(),
|
||||||
shouldUseNewPrivilegeSystem: z.boolean().default(true),
|
shouldUseNewPrivilegeSystem: z.boolean().default(true),
|
||||||
privilegeUpgradeInitiatedByUsername: z.string().nullable().optional(),
|
privilegeUpgradeInitiatedByUsername: z.string().nullable().optional(),
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
// Code generated by automation script, DO NOT EDIT.
|
||||||
|
// Automated by pulling database and generating zod schema
|
||||||
|
// To update. Just run npm run generate:schema
|
||||||
|
// Written by akhilmhdh.
|
||||||
|
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { TImmutableDBKeys } from "./models";
|
||||||
|
|
||||||
|
export const ProjectMicrosoftTeamsConfigsSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
projectId: z.string(),
|
||||||
|
microsoftTeamsIntegrationId: z.string().uuid(),
|
||||||
|
isAccessRequestNotificationEnabled: z.boolean().default(false),
|
||||||
|
isSecretRequestNotificationEnabled: z.boolean().default(false),
|
||||||
|
accessRequestChannels: z.unknown(),
|
||||||
|
secretRequestChannels: z.unknown(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TProjectMicrosoftTeamsConfigs = z.infer<typeof ProjectMicrosoftTeamsConfigsSchema>;
|
||||||
|
export type TProjectMicrosoftTeamsConfigsInsert = Omit<
|
||||||
|
z.input<typeof ProjectMicrosoftTeamsConfigsSchema>,
|
||||||
|
TImmutableDBKeys
|
||||||
|
>;
|
||||||
|
export type TProjectMicrosoftTeamsConfigsUpdate = Partial<
|
||||||
|
Omit<z.input<typeof ProjectMicrosoftTeamsConfigsSchema>, TImmutableDBKeys>
|
||||||
|
>;
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
// Code generated by automation script, DO NOT EDIT.
|
||||||
|
// Automated by pulling database and generating zod schema
|
||||||
|
// To update. Just run npm run generate:schema
|
||||||
|
// Written by akhilmhdh.
|
||||||
|
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { TImmutableDBKeys } from "./models";
|
||||||
|
|
||||||
|
export const SecretReminderRecipientsSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date(),
|
||||||
|
secretId: z.string().uuid(),
|
||||||
|
userId: z.string().uuid(),
|
||||||
|
projectId: z.string()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TSecretReminderRecipients = z.infer<typeof SecretReminderRecipientsSchema>;
|
||||||
|
export type TSecretReminderRecipientsInsert = Omit<z.input<typeof SecretReminderRecipientsSchema>, TImmutableDBKeys>;
|
||||||
|
export type TSecretReminderRecipientsUpdate = Partial<
|
||||||
|
Omit<z.input<typeof SecretReminderRecipientsSchema>, TImmutableDBKeys>
|
||||||
|
>;
|
||||||
@@ -26,7 +26,10 @@ export const SuperAdminSchema = z.object({
|
|||||||
encryptedSlackClientSecret: zodBuffer.nullable().optional(),
|
encryptedSlackClientSecret: zodBuffer.nullable().optional(),
|
||||||
authConsentContent: z.string().nullable().optional(),
|
authConsentContent: z.string().nullable().optional(),
|
||||||
pageFrameContent: z.string().nullable().optional(),
|
pageFrameContent: z.string().nullable().optional(),
|
||||||
adminIdentityIds: z.string().array().nullable().optional()
|
adminIdentityIds: z.string().array().nullable().optional(),
|
||||||
|
encryptedMicrosoftTeamsAppId: zodBuffer.nullable().optional(),
|
||||||
|
encryptedMicrosoftTeamsClientSecret: zodBuffer.nullable().optional(),
|
||||||
|
encryptedMicrosoftTeamsBotId: zodBuffer.nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TSuperAdmin = z.infer<typeof SuperAdminSchema>;
|
export type TSuperAdmin = z.infer<typeof SuperAdminSchema>;
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ export const WorkflowIntegrationsSchema = z.object({
|
|||||||
orgId: z.string().uuid(),
|
orgId: z.string().uuid(),
|
||||||
description: z.string().nullable().optional(),
|
description: z.string().nullable().optional(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date()
|
updatedAt: z.date(),
|
||||||
|
status: z.string().default("installed")
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TWorkflowIntegrations = z.infer<typeof WorkflowIntegrationsSchema>;
|
export type TWorkflowIntegrations = z.infer<typeof WorkflowIntegrationsSchema>;
|
||||||
|
|||||||
@@ -6,13 +6,15 @@ import { getConfig } from "@app/lib/config/env";
|
|||||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
import { ms } from "@app/lib/ms";
|
import { ms } from "@app/lib/ms";
|
||||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||||
|
import { triggerWorkflowIntegrationNotification } from "@app/lib/workflow-integrations/trigger-notification";
|
||||||
|
import { TriggerFeature } from "@app/lib/workflow-integrations/types";
|
||||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
|
import { TMicrosoftTeamsServiceFactory } from "@app/services/microsoft-teams/microsoft-teams-service";
|
||||||
|
import { TProjectMicrosoftTeamsConfigDALFactory } from "@app/services/microsoft-teams/project-microsoft-teams-config-dal";
|
||||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||||
import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal";
|
import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal";
|
||||||
import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
|
import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
|
||||||
import { TProjectSlackConfigDALFactory } from "@app/services/slack/project-slack-config-dal";
|
import { TProjectSlackConfigDALFactory } from "@app/services/slack/project-slack-config-dal";
|
||||||
import { triggerSlackNotification } from "@app/services/slack/slack-fns";
|
|
||||||
import { SlackTriggerFeature } from "@app/services/slack/slack-types";
|
|
||||||
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
|
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
|
||||||
import { TUserDALFactory } from "@app/services/user/user-dal";
|
import { TUserDALFactory } from "@app/services/user/user-dal";
|
||||||
|
|
||||||
@@ -67,6 +69,8 @@ type TSecretApprovalRequestServiceFactoryDep = {
|
|||||||
>;
|
>;
|
||||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||||
projectSlackConfigDAL: Pick<TProjectSlackConfigDALFactory, "getIntegrationDetailsByProject">;
|
projectSlackConfigDAL: Pick<TProjectSlackConfigDALFactory, "getIntegrationDetailsByProject">;
|
||||||
|
microsoftTeamsService: Pick<TMicrosoftTeamsServiceFactory, "sendNotification">;
|
||||||
|
projectMicrosoftTeamsConfigDAL: Pick<TProjectMicrosoftTeamsConfigDALFactory, "getIntegrationDetailsByProject">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TAccessApprovalRequestServiceFactory = ReturnType<typeof accessApprovalRequestServiceFactory>;
|
export type TAccessApprovalRequestServiceFactory = ReturnType<typeof accessApprovalRequestServiceFactory>;
|
||||||
@@ -84,6 +88,8 @@ export const accessApprovalRequestServiceFactory = ({
|
|||||||
smtpService,
|
smtpService,
|
||||||
userDAL,
|
userDAL,
|
||||||
kmsService,
|
kmsService,
|
||||||
|
microsoftTeamsService,
|
||||||
|
projectMicrosoftTeamsConfigDAL,
|
||||||
projectSlackConfigDAL
|
projectSlackConfigDAL
|
||||||
}: TSecretApprovalRequestServiceFactoryDep) => {
|
}: TSecretApprovalRequestServiceFactoryDep) => {
|
||||||
const createAccessApprovalRequest = async ({
|
const createAccessApprovalRequest = async ({
|
||||||
@@ -219,13 +225,11 @@ export const accessApprovalRequestServiceFactory = ({
|
|||||||
const requesterFullName = `${requestedByUser.firstName} ${requestedByUser.lastName}`;
|
const requesterFullName = `${requestedByUser.firstName} ${requestedByUser.lastName}`;
|
||||||
const approvalUrl = `${cfg.SITE_URL}/secret-manager/${project.id}/approval`;
|
const approvalUrl = `${cfg.SITE_URL}/secret-manager/${project.id}/approval`;
|
||||||
|
|
||||||
await triggerSlackNotification({
|
console.log("triggering workflow integration notification");
|
||||||
projectId: project.id,
|
await triggerWorkflowIntegrationNotification({
|
||||||
projectSlackConfigDAL,
|
input: {
|
||||||
projectDAL,
|
|
||||||
kmsService,
|
|
||||||
notification: {
|
notification: {
|
||||||
type: SlackTriggerFeature.ACCESS_REQUEST,
|
type: TriggerFeature.ACCESS_REQUEST,
|
||||||
payload: {
|
payload: {
|
||||||
projectName: project.name,
|
projectName: project.name,
|
||||||
requesterFullName,
|
requesterFullName,
|
||||||
@@ -237,6 +241,15 @@ export const accessApprovalRequestServiceFactory = ({
|
|||||||
approvalUrl,
|
approvalUrl,
|
||||||
note
|
note
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
projectId: project.id
|
||||||
|
},
|
||||||
|
dependencies: {
|
||||||
|
projectDAL,
|
||||||
|
projectSlackConfigDAL,
|
||||||
|
kmsService,
|
||||||
|
microsoftTeamsService,
|
||||||
|
projectMicrosoftTeamsConfigDAL
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import {
|
|||||||
TSecretSyncRaw,
|
TSecretSyncRaw,
|
||||||
TUpdateSecretSyncDTO
|
TUpdateSecretSyncDTO
|
||||||
} from "@app/services/secret-sync/secret-sync-types";
|
} from "@app/services/secret-sync/secret-sync-types";
|
||||||
|
import { WorkflowIntegration } from "@app/services/workflow-integration/workflow-integration-types";
|
||||||
|
|
||||||
import { KmipPermission } from "../kmip/kmip-enum";
|
import { KmipPermission } from "../kmip/kmip-enum";
|
||||||
import { ApprovalStatus } from "../secret-approval-request/secret-approval-request-types";
|
import { ApprovalStatus } from "../secret-approval-request/secret-approval-request-types";
|
||||||
@@ -246,8 +247,9 @@ export enum EventType {
|
|||||||
GET_SLACK_INTEGRATION = "get-slack-integration",
|
GET_SLACK_INTEGRATION = "get-slack-integration",
|
||||||
UPDATE_SLACK_INTEGRATION = "update-slack-integration",
|
UPDATE_SLACK_INTEGRATION = "update-slack-integration",
|
||||||
DELETE_SLACK_INTEGRATION = "delete-slack-integration",
|
DELETE_SLACK_INTEGRATION = "delete-slack-integration",
|
||||||
GET_PROJECT_SLACK_CONFIG = "get-project-slack-config",
|
GET_PROJECT_WORKFLOW_INTEGRATION_CONFIG = "get-project-workflow-integration-config",
|
||||||
UPDATE_PROJECT_SLACK_CONFIG = "update-project-slack-config",
|
|
||||||
|
UPDATE_PROJECT_WORKFLOW_INTEGRATION_CONFIG = "update-project-workflow-integration-config",
|
||||||
INTEGRATION_SYNCED = "integration-synced",
|
INTEGRATION_SYNCED = "integration-synced",
|
||||||
CREATE_CMEK = "create-cmek",
|
CREATE_CMEK = "create-cmek",
|
||||||
UPDATE_CMEK = "update-cmek",
|
UPDATE_CMEK = "update-cmek",
|
||||||
@@ -1968,22 +1970,24 @@ interface GetSlackIntegration {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UpdateProjectSlackConfig {
|
interface UpdateProjectWorkflowIntegrationConfig {
|
||||||
type: EventType.UPDATE_PROJECT_SLACK_CONFIG;
|
type: EventType.UPDATE_PROJECT_WORKFLOW_INTEGRATION_CONFIG;
|
||||||
metadata: {
|
metadata: {
|
||||||
id: string;
|
id: string;
|
||||||
slackIntegrationId: string;
|
integrationId: string;
|
||||||
|
integration: WorkflowIntegration;
|
||||||
isAccessRequestNotificationEnabled: boolean;
|
isAccessRequestNotificationEnabled: boolean;
|
||||||
accessRequestChannels: string;
|
accessRequestChannels?: string | { teamId: string; channelIds: string[] };
|
||||||
isSecretRequestNotificationEnabled: boolean;
|
isSecretRequestNotificationEnabled: boolean;
|
||||||
secretRequestChannels: string;
|
secretRequestChannels?: string | { teamId: string; channelIds: string[] };
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
interface GetProjectSlackConfig {
|
interface GetProjectWorkflowIntegrationConfig {
|
||||||
type: EventType.GET_PROJECT_SLACK_CONFIG;
|
type: EventType.GET_PROJECT_WORKFLOW_INTEGRATION_CONFIG;
|
||||||
metadata: {
|
metadata: {
|
||||||
id: string;
|
id: string;
|
||||||
|
integration: WorkflowIntegration;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
interface IntegrationSyncedEvent {
|
interface IntegrationSyncedEvent {
|
||||||
@@ -2668,8 +2672,8 @@ export type Event =
|
|||||||
| UpdateSlackIntegration
|
| UpdateSlackIntegration
|
||||||
| DeleteSlackIntegration
|
| DeleteSlackIntegration
|
||||||
| GetSlackIntegration
|
| GetSlackIntegration
|
||||||
| UpdateProjectSlackConfig
|
| UpdateProjectWorkflowIntegrationConfig
|
||||||
| GetProjectSlackConfig
|
| GetProjectWorkflowIntegrationConfig
|
||||||
| IntegrationSyncedEvent
|
| IntegrationSyncedEvent
|
||||||
| CreateCmekEvent
|
| CreateCmekEvent
|
||||||
| UpdateCmekEvent
|
| UpdateCmekEvent
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
|
|||||||
auditLogsRetentionDays: 0,
|
auditLogsRetentionDays: 0,
|
||||||
auditLogStreams: false,
|
auditLogStreams: false,
|
||||||
auditLogStreamLimit: 3,
|
auditLogStreamLimit: 3,
|
||||||
samlSSO: false,
|
samlSSO: true,
|
||||||
hsm: false,
|
hsm: false,
|
||||||
oidcSSO: false,
|
oidcSSO: false,
|
||||||
scim: false,
|
scim: false,
|
||||||
@@ -38,7 +38,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
|
|||||||
status: null,
|
status: null,
|
||||||
trial_end: null,
|
trial_end: null,
|
||||||
has_used_trial: true,
|
has_used_trial: true,
|
||||||
secretApproval: false,
|
secretApproval: true,
|
||||||
secretRotation: false,
|
secretRotation: false,
|
||||||
caCrl: false,
|
caCrl: false,
|
||||||
instanceUserManagement: false,
|
instanceUserManagement: false,
|
||||||
|
|||||||
+33
-13
@@ -17,9 +17,13 @@ import { groupBy, pick, unique } from "@app/lib/fn";
|
|||||||
import { setKnexStringValue } from "@app/lib/knex";
|
import { setKnexStringValue } from "@app/lib/knex";
|
||||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||||
import { EnforcementLevel } from "@app/lib/types";
|
import { EnforcementLevel } from "@app/lib/types";
|
||||||
|
import { triggerWorkflowIntegrationNotification } from "@app/lib/workflow-integrations/trigger-notification";
|
||||||
|
import { TriggerFeature } from "@app/lib/workflow-integrations/types";
|
||||||
import { ActorType } from "@app/services/auth/auth-type";
|
import { ActorType } from "@app/services/auth/auth-type";
|
||||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||||
|
import { TMicrosoftTeamsServiceFactory } from "@app/services/microsoft-teams/microsoft-teams-service";
|
||||||
|
import { TProjectMicrosoftTeamsConfigDALFactory } from "@app/services/microsoft-teams/project-microsoft-teams-config-dal";
|
||||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||||
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
|
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
|
||||||
import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal";
|
import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal";
|
||||||
@@ -52,8 +56,6 @@ import {
|
|||||||
import { TSecretVersionV2DALFactory } from "@app/services/secret-v2-bridge/secret-version-dal";
|
import { TSecretVersionV2DALFactory } from "@app/services/secret-v2-bridge/secret-version-dal";
|
||||||
import { TSecretVersionV2TagDALFactory } from "@app/services/secret-v2-bridge/secret-version-tag-dal";
|
import { TSecretVersionV2TagDALFactory } from "@app/services/secret-v2-bridge/secret-version-tag-dal";
|
||||||
import { TProjectSlackConfigDALFactory } from "@app/services/slack/project-slack-config-dal";
|
import { TProjectSlackConfigDALFactory } from "@app/services/slack/project-slack-config-dal";
|
||||||
import { triggerSlackNotification } from "@app/services/slack/slack-fns";
|
|
||||||
import { SlackTriggerFeature } from "@app/services/slack/slack-types";
|
|
||||||
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
|
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
|
||||||
import { TUserDALFactory } from "@app/services/user/user-dal";
|
import { TUserDALFactory } from "@app/services/user/user-dal";
|
||||||
|
|
||||||
@@ -126,6 +128,8 @@ type TSecretApprovalRequestServiceFactoryDep = {
|
|||||||
secretApprovalPolicyDAL: Pick<TSecretApprovalPolicyDALFactory, "findById">;
|
secretApprovalPolicyDAL: Pick<TSecretApprovalPolicyDALFactory, "findById">;
|
||||||
projectSlackConfigDAL: Pick<TProjectSlackConfigDALFactory, "getIntegrationDetailsByProject">;
|
projectSlackConfigDAL: Pick<TProjectSlackConfigDALFactory, "getIntegrationDetailsByProject">;
|
||||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||||
|
projectMicrosoftTeamsConfigDAL: Pick<TProjectMicrosoftTeamsConfigDALFactory, "getIntegrationDetailsByProject">;
|
||||||
|
microsoftTeamsService: Pick<TMicrosoftTeamsServiceFactory, "sendNotification">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TSecretApprovalRequestServiceFactory = ReturnType<typeof secretApprovalRequestServiceFactory>;
|
export type TSecretApprovalRequestServiceFactory = ReturnType<typeof secretApprovalRequestServiceFactory>;
|
||||||
@@ -155,7 +159,9 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
secretVersionTagV2BridgeDAL,
|
secretVersionTagV2BridgeDAL,
|
||||||
licenseService,
|
licenseService,
|
||||||
projectSlackConfigDAL,
|
projectSlackConfigDAL,
|
||||||
resourceMetadataDAL
|
resourceMetadataDAL,
|
||||||
|
projectMicrosoftTeamsConfigDAL,
|
||||||
|
microsoftTeamsService
|
||||||
}: TSecretApprovalRequestServiceFactoryDep) => {
|
}: TSecretApprovalRequestServiceFactoryDep) => {
|
||||||
const requestCount = async ({ projectId, actor, actorId, actorOrgId, actorAuthMethod }: TApprovalRequestCountDTO) => {
|
const requestCount = async ({ projectId, actor, actorId, actorOrgId, actorAuthMethod }: TApprovalRequestCountDTO) => {
|
||||||
if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" });
|
if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" });
|
||||||
@@ -1171,13 +1177,12 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
|
|
||||||
const env = await projectEnvDAL.findOne({ id: policy.envId });
|
const env = await projectEnvDAL.findOne({ id: policy.envId });
|
||||||
const user = await userDAL.findById(secretApprovalRequest.committerUserId);
|
const user = await userDAL.findById(secretApprovalRequest.committerUserId);
|
||||||
await triggerSlackNotification({
|
|
||||||
|
await triggerWorkflowIntegrationNotification({
|
||||||
|
input: {
|
||||||
projectId,
|
projectId,
|
||||||
projectDAL,
|
|
||||||
kmsService,
|
|
||||||
projectSlackConfigDAL,
|
|
||||||
notification: {
|
notification: {
|
||||||
type: SlackTriggerFeature.SECRET_APPROVAL,
|
type: TriggerFeature.SECRET_APPROVAL,
|
||||||
payload: {
|
payload: {
|
||||||
userEmail: user.email as string,
|
userEmail: user.email as string,
|
||||||
environment: env.name,
|
environment: env.name,
|
||||||
@@ -1187,6 +1192,14 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
secretKeys: [...new Set(Object.values(data).flatMap((arr) => arr?.map((item) => item.secretName) ?? []))]
|
secretKeys: [...new Set(Object.values(data).flatMap((arr) => arr?.map((item) => item.secretName) ?? []))]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
dependencies: {
|
||||||
|
projectDAL,
|
||||||
|
projectSlackConfigDAL,
|
||||||
|
kmsService,
|
||||||
|
projectMicrosoftTeamsConfigDAL,
|
||||||
|
microsoftTeamsService
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
await sendApprovalEmailsFn({
|
await sendApprovalEmailsFn({
|
||||||
@@ -1503,13 +1516,12 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
|
|
||||||
const user = await userDAL.findById(secretApprovalRequest.committerUserId);
|
const user = await userDAL.findById(secretApprovalRequest.committerUserId);
|
||||||
const env = await projectEnvDAL.findOne({ id: policy.envId });
|
const env = await projectEnvDAL.findOne({ id: policy.envId });
|
||||||
await triggerSlackNotification({
|
|
||||||
|
await triggerWorkflowIntegrationNotification({
|
||||||
|
input: {
|
||||||
projectId,
|
projectId,
|
||||||
projectDAL,
|
|
||||||
kmsService,
|
|
||||||
projectSlackConfigDAL,
|
|
||||||
notification: {
|
notification: {
|
||||||
type: SlackTriggerFeature.SECRET_APPROVAL,
|
type: TriggerFeature.SECRET_APPROVAL,
|
||||||
payload: {
|
payload: {
|
||||||
userEmail: user.email as string,
|
userEmail: user.email as string,
|
||||||
environment: env.name,
|
environment: env.name,
|
||||||
@@ -1519,6 +1531,14 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
secretKeys: [...new Set(Object.values(data).flatMap((arr) => arr?.map((item) => item.secretKey) ?? []))]
|
secretKeys: [...new Set(Object.values(data).flatMap((arr) => arr?.map((item) => item.secretKey) ?? []))]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
dependencies: {
|
||||||
|
projectDAL,
|
||||||
|
kmsService,
|
||||||
|
projectSlackConfigDAL,
|
||||||
|
microsoftTeamsService,
|
||||||
|
projectMicrosoftTeamsConfigDAL
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
await sendApprovalEmailsFn({
|
await sendApprovalEmailsFn({
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import { MicrosoftTeamsChannelsSchema } from "@app/services/microsoft-teams/microsoft-teams-fns";
|
||||||
|
import { sendSlackNotification } from "@app/services/slack/slack-fns";
|
||||||
|
|
||||||
|
import { logger } from "../logger";
|
||||||
|
import { TriggerFeature, TTriggerWorkflowNotificationDTO } from "./types";
|
||||||
|
|
||||||
|
export const triggerWorkflowIntegrationNotification = async (dto: TTriggerWorkflowNotificationDTO) => {
|
||||||
|
try {
|
||||||
|
const { projectId, notification } = dto.input;
|
||||||
|
const { projectDAL, projectSlackConfigDAL, kmsService, projectMicrosoftTeamsConfigDAL, microsoftTeamsService } =
|
||||||
|
dto.dependencies;
|
||||||
|
|
||||||
|
const project = await projectDAL.findById(projectId);
|
||||||
|
|
||||||
|
if (!project) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const microsoftTeamsConfig = await projectMicrosoftTeamsConfigDAL.getIntegrationDetailsByProject(projectId);
|
||||||
|
const slackConfig = await projectSlackConfigDAL.getIntegrationDetailsByProject(projectId);
|
||||||
|
|
||||||
|
if (slackConfig) {
|
||||||
|
if (notification.type === TriggerFeature.ACCESS_REQUEST) {
|
||||||
|
const targetChannelIds = slackConfig.accessRequestChannels?.split(", ") || [];
|
||||||
|
if (targetChannelIds.length && slackConfig.isAccessRequestNotificationEnabled) {
|
||||||
|
await sendSlackNotification({
|
||||||
|
orgId: project.orgId,
|
||||||
|
notification,
|
||||||
|
kmsService,
|
||||||
|
targetChannelIds,
|
||||||
|
slackIntegration: slackConfig
|
||||||
|
}).catch((error) => {
|
||||||
|
logger.error(error, "Error sending Slack notification");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (notification.type === TriggerFeature.SECRET_APPROVAL) {
|
||||||
|
const targetChannelIds = slackConfig.secretRequestChannels?.split(", ") || [];
|
||||||
|
if (targetChannelIds.length && slackConfig.isSecretRequestNotificationEnabled) {
|
||||||
|
await sendSlackNotification({
|
||||||
|
orgId: project.orgId,
|
||||||
|
notification,
|
||||||
|
kmsService,
|
||||||
|
targetChannelIds,
|
||||||
|
slackIntegration: slackConfig
|
||||||
|
}).catch((error) => {
|
||||||
|
logger.error(error, "Error sending Slack notification");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("microsoftTeamsConfig", microsoftTeamsConfig);
|
||||||
|
if (microsoftTeamsConfig) {
|
||||||
|
if (notification.type === TriggerFeature.ACCESS_REQUEST) {
|
||||||
|
if (microsoftTeamsConfig.isAccessRequestNotificationEnabled && microsoftTeamsConfig.accessRequestChannels) {
|
||||||
|
const { success, data } = MicrosoftTeamsChannelsSchema.safeParse(microsoftTeamsConfig.accessRequestChannels);
|
||||||
|
|
||||||
|
console.log("success", success);
|
||||||
|
console.log("data", data);
|
||||||
|
|
||||||
|
if (success && data) {
|
||||||
|
await microsoftTeamsService
|
||||||
|
.sendNotification({
|
||||||
|
notification,
|
||||||
|
target: data,
|
||||||
|
tenantId: microsoftTeamsConfig.tenantId
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
logger.error(error, "Error sending Microsoft Teams notification");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (notification.type === TriggerFeature.SECRET_APPROVAL) {
|
||||||
|
if (microsoftTeamsConfig.isSecretRequestNotificationEnabled && microsoftTeamsConfig.secretRequestChannels) {
|
||||||
|
const { success, data } = MicrosoftTeamsChannelsSchema.safeParse(microsoftTeamsConfig.secretRequestChannels);
|
||||||
|
|
||||||
|
if (success && data) {
|
||||||
|
await microsoftTeamsService
|
||||||
|
.sendNotification({
|
||||||
|
notification,
|
||||||
|
target: data,
|
||||||
|
tenantId: microsoftTeamsConfig.tenantId
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
logger.error(error, "Error sending Microsoft Teams notification");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error, "Error triggering workflow integration notification");
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
|
import { TMicrosoftTeamsServiceFactory } from "@app/services/microsoft-teams/microsoft-teams-service";
|
||||||
|
import { TProjectMicrosoftTeamsConfigDALFactory } from "@app/services/microsoft-teams/project-microsoft-teams-config-dal";
|
||||||
|
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||||
|
import { TProjectSlackConfigDALFactory } from "@app/services/slack/project-slack-config-dal";
|
||||||
|
|
||||||
|
export enum TriggerFeature {
|
||||||
|
SECRET_APPROVAL = "secret-approval",
|
||||||
|
ACCESS_REQUEST = "access-request"
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TNotification =
|
||||||
|
| {
|
||||||
|
type: TriggerFeature.SECRET_APPROVAL;
|
||||||
|
payload: {
|
||||||
|
userEmail: string;
|
||||||
|
environment: string;
|
||||||
|
secretPath: string;
|
||||||
|
requestId: string;
|
||||||
|
projectId: string;
|
||||||
|
secretKeys: string[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: TriggerFeature.ACCESS_REQUEST;
|
||||||
|
payload: {
|
||||||
|
requesterFullName: string;
|
||||||
|
requesterEmail: string;
|
||||||
|
isTemporary: boolean;
|
||||||
|
secretPath: string;
|
||||||
|
environment: string;
|
||||||
|
projectName: string;
|
||||||
|
permissions: string[];
|
||||||
|
approvalUrl: string;
|
||||||
|
note?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TTriggerWorkflowNotificationDTO = {
|
||||||
|
input: {
|
||||||
|
projectId: string;
|
||||||
|
notification: TNotification;
|
||||||
|
};
|
||||||
|
dependencies: {
|
||||||
|
projectDAL: Pick<TProjectDALFactory, "findById">;
|
||||||
|
projectSlackConfigDAL: Pick<TProjectSlackConfigDALFactory, "getIntegrationDetailsByProject">;
|
||||||
|
projectMicrosoftTeamsConfigDAL: Pick<TProjectMicrosoftTeamsConfigDALFactory, "getIntegrationDetailsByProject">;
|
||||||
|
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||||
|
microsoftTeamsService: Pick<TMicrosoftTeamsServiceFactory, "sendNotification">;
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -111,6 +111,10 @@ export const injectIdentity = fp(async (server: FastifyZodProvider) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (req.url.includes("/api/v1/workflow-integrations/microsoft-teams/message-endpoint")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const { authMode, token, actor } = await extractAuth(req, appCfg.AUTH_SECRET);
|
const { authMode, token, actor } = await extractAuth(req, appCfg.AUTH_SECRET);
|
||||||
|
|
||||||
if (!authMode) return;
|
if (!authMode) return;
|
||||||
|
|||||||
@@ -171,6 +171,9 @@ import { internalKmsDALFactory } from "@app/services/kms/internal-kms-dal";
|
|||||||
import { kmskeyDALFactory } from "@app/services/kms/kms-key-dal";
|
import { kmskeyDALFactory } from "@app/services/kms/kms-key-dal";
|
||||||
import { kmsRootConfigDALFactory } from "@app/services/kms/kms-root-config-dal";
|
import { kmsRootConfigDALFactory } from "@app/services/kms/kms-root-config-dal";
|
||||||
import { kmsServiceFactory } from "@app/services/kms/kms-service";
|
import { kmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
|
import { microsoftTeamsIntegrationDALFactory } from "@app/services/microsoft-teams/microsoft-teams-integration-dal";
|
||||||
|
import { microsoftTeamsServiceFactory } from "@app/services/microsoft-teams/microsoft-teams-service";
|
||||||
|
import { projectMicrosoftTeamsConfigDALFactory } from "@app/services/microsoft-teams/project-microsoft-teams-config-dal";
|
||||||
import { incidentContactDALFactory } from "@app/services/org/incident-contacts-dal";
|
import { incidentContactDALFactory } from "@app/services/org/incident-contacts-dal";
|
||||||
import { orgBotDALFactory } from "@app/services/org/org-bot-dal";
|
import { orgBotDALFactory } from "@app/services/org/org-bot-dal";
|
||||||
import { orgDALFactory } from "@app/services/org/org-dal";
|
import { orgDALFactory } from "@app/services/org/org-dal";
|
||||||
@@ -419,6 +422,8 @@ export const registerRoutes = async (
|
|||||||
const projectGatewayDAL = projectGatewayDALFactory(db);
|
const projectGatewayDAL = projectGatewayDALFactory(db);
|
||||||
|
|
||||||
const secretRotationV2DAL = secretRotationV2DALFactory(db, folderDAL);
|
const secretRotationV2DAL = secretRotationV2DALFactory(db, folderDAL);
|
||||||
|
const microsoftTeamsIntegrationDAL = microsoftTeamsIntegrationDALFactory(db);
|
||||||
|
const projectMicrosoftTeamsConfigDAL = projectMicrosoftTeamsConfigDALFactory(db);
|
||||||
|
|
||||||
const permissionService = permissionServiceFactory({
|
const permissionService = permissionServiceFactory({
|
||||||
permissionDAL,
|
permissionDAL,
|
||||||
@@ -659,6 +664,15 @@ export const registerRoutes = async (
|
|||||||
orgDAL,
|
orgDAL,
|
||||||
externalGroupOrgRoleMappingDAL
|
externalGroupOrgRoleMappingDAL
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const microsoftTeamsService = microsoftTeamsServiceFactory({
|
||||||
|
microsoftTeamsIntegrationDAL,
|
||||||
|
permissionService,
|
||||||
|
workflowIntegrationDAL,
|
||||||
|
kmsService,
|
||||||
|
serverCfgDAL: superAdminDAL
|
||||||
|
});
|
||||||
|
|
||||||
const superAdminService = superAdminServiceFactory({
|
const superAdminService = superAdminServiceFactory({
|
||||||
userDAL,
|
userDAL,
|
||||||
identityDAL,
|
identityDAL,
|
||||||
@@ -672,7 +686,8 @@ export const registerRoutes = async (
|
|||||||
orgService,
|
orgService,
|
||||||
keyStore,
|
keyStore,
|
||||||
licenseService,
|
licenseService,
|
||||||
kmsService
|
kmsService,
|
||||||
|
microsoftTeamsService
|
||||||
});
|
});
|
||||||
|
|
||||||
const orgAdminService = orgAdminServiceFactory({
|
const orgAdminService = orgAdminServiceFactory({
|
||||||
@@ -996,6 +1011,8 @@ export const registerRoutes = async (
|
|||||||
certificateTemplateDAL,
|
certificateTemplateDAL,
|
||||||
projectSlackConfigDAL,
|
projectSlackConfigDAL,
|
||||||
slackIntegrationDAL,
|
slackIntegrationDAL,
|
||||||
|
projectMicrosoftTeamsConfigDAL,
|
||||||
|
microsoftTeamsIntegrationDAL,
|
||||||
projectTemplateService,
|
projectTemplateService,
|
||||||
groupProjectDAL,
|
groupProjectDAL,
|
||||||
smtpService
|
smtpService
|
||||||
@@ -1179,7 +1196,9 @@ export const registerRoutes = async (
|
|||||||
accessApprovalPolicyApproverDAL,
|
accessApprovalPolicyApproverDAL,
|
||||||
projectSlackConfigDAL,
|
projectSlackConfigDAL,
|
||||||
kmsService,
|
kmsService,
|
||||||
groupDAL
|
groupDAL,
|
||||||
|
microsoftTeamsService,
|
||||||
|
projectMicrosoftTeamsConfigDAL
|
||||||
});
|
});
|
||||||
|
|
||||||
const secretReplicationService = secretReplicationServiceFactory({
|
const secretReplicationService = secretReplicationServiceFactory({
|
||||||
@@ -1576,6 +1595,7 @@ export const registerRoutes = async (
|
|||||||
await dailyResourceCleanUp.startCleanUp();
|
await dailyResourceCleanUp.startCleanUp();
|
||||||
await dailyExpiringPkiItemAlert.startSendingAlerts();
|
await dailyExpiringPkiItemAlert.startSendingAlerts();
|
||||||
await kmsService.startService();
|
await kmsService.startService();
|
||||||
|
await microsoftTeamsService.start();
|
||||||
|
|
||||||
// inject all services
|
// inject all services
|
||||||
server.decorate<FastifyZodProvider["services"]>("services", {
|
server.decorate<FastifyZodProvider["services"]>("services", {
|
||||||
@@ -1667,7 +1687,8 @@ export const registerRoutes = async (
|
|||||||
kmip: kmipService,
|
kmip: kmipService,
|
||||||
kmipOperation: kmipOperationService,
|
kmipOperation: kmipOperationService,
|
||||||
gateway: gatewayService,
|
gateway: gatewayService,
|
||||||
secretRotationV2: secretRotationV2Service
|
secretRotationV2: secretRotationV2Service,
|
||||||
|
microsoftTeams: microsoftTeamsService
|
||||||
});
|
});
|
||||||
|
|
||||||
const cronJobs: CronJob[] = [];
|
const cronJobs: CronJob[] = [];
|
||||||
|
|||||||
@@ -27,7 +27,10 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
|
|||||||
createdAt: true,
|
createdAt: true,
|
||||||
updatedAt: true,
|
updatedAt: true,
|
||||||
encryptedSlackClientId: true,
|
encryptedSlackClientId: true,
|
||||||
encryptedSlackClientSecret: true
|
encryptedSlackClientSecret: true,
|
||||||
|
encryptedMicrosoftTeamsAppId: true,
|
||||||
|
encryptedMicrosoftTeamsClientSecret: true,
|
||||||
|
encryptedMicrosoftTeamsBotId: true
|
||||||
}).extend({
|
}).extend({
|
||||||
isMigrationModeOn: z.boolean(),
|
isMigrationModeOn: z.boolean(),
|
||||||
defaultAuthOrgSlug: z.string().nullable(),
|
defaultAuthOrgSlug: z.string().nullable(),
|
||||||
@@ -74,6 +77,9 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
|
|||||||
}),
|
}),
|
||||||
slackClientId: z.string().optional(),
|
slackClientId: z.string().optional(),
|
||||||
slackClientSecret: z.string().optional(),
|
slackClientSecret: z.string().optional(),
|
||||||
|
microsoftTeamsAppId: z.string().optional(),
|
||||||
|
microsoftTeamsClientSecret: z.string().optional(),
|
||||||
|
microsoftTeamsBotId: z.string().optional(),
|
||||||
authConsentContent: z
|
authConsentContent: z
|
||||||
.string()
|
.string()
|
||||||
.trim()
|
.trim()
|
||||||
@@ -197,15 +203,22 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
|
|||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
url: "/integrations/slack/config",
|
url: "/integrations",
|
||||||
config: {
|
config: {
|
||||||
rateLimit: readLimit
|
rateLimit: readLimit
|
||||||
},
|
},
|
||||||
schema: {
|
schema: {
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
|
slack: z.object({
|
||||||
clientId: z.string(),
|
clientId: z.string(),
|
||||||
clientSecret: z.string()
|
clientSecret: z.string()
|
||||||
|
}),
|
||||||
|
microsoftTeams: z.object({
|
||||||
|
appId: z.string(),
|
||||||
|
clientSecret: z.string(),
|
||||||
|
botId: z.string()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -215,9 +228,9 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
handler: async () => {
|
handler: async () => {
|
||||||
const adminSlackConfig = await server.services.superAdmin.getAdminSlackConfig();
|
const adminIntegrationsConfig = await server.services.superAdmin.getAdminIntegrationsConfig();
|
||||||
|
|
||||||
return adminSlackConfig;
|
return adminIntegrationsConfig;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ import { registerUserEngagementRouter } from "./user-engagement-router";
|
|||||||
import { registerUserRouter } from "./user-router";
|
import { registerUserRouter } from "./user-router";
|
||||||
import { registerWebhookRouter } from "./webhook-router";
|
import { registerWebhookRouter } from "./webhook-router";
|
||||||
import { registerWorkflowIntegrationRouter } from "./workflow-integration-router";
|
import { registerWorkflowIntegrationRouter } from "./workflow-integration-router";
|
||||||
|
import { registerMicrosoftTeamsRouter } from "./microsoft-teams-router";
|
||||||
|
|
||||||
export const registerV1Routes = async (server: FastifyZodProvider) => {
|
export const registerV1Routes = async (server: FastifyZodProvider) => {
|
||||||
await server.register(registerSsoRouter, { prefix: "/sso" });
|
await server.register(registerSsoRouter, { prefix: "/sso" });
|
||||||
@@ -79,6 +80,7 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
|
|||||||
async (workflowIntegrationRouter) => {
|
async (workflowIntegrationRouter) => {
|
||||||
await workflowIntegrationRouter.register(registerWorkflowIntegrationRouter);
|
await workflowIntegrationRouter.register(registerWorkflowIntegrationRouter);
|
||||||
await workflowIntegrationRouter.register(registerSlackRouter, { prefix: "/slack" });
|
await workflowIntegrationRouter.register(registerSlackRouter, { prefix: "/slack" });
|
||||||
|
await workflowIntegrationRouter.register(registerMicrosoftTeamsRouter, { prefix: "/microsoft-teams" });
|
||||||
},
|
},
|
||||||
{ prefix: "/workflow-integrations" }
|
{ prefix: "/workflow-integrations" }
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,260 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { MicrosoftTeamsIntegrationsSchema, WorkflowIntegrationsSchema } from "@app/db/schemas";
|
||||||
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
|
import { slugSchema } from "@app/server/lib/schemas";
|
||||||
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
import { WorkflowIntegrationStatus } from "@app/services/workflow-integration/workflow-integration-types";
|
||||||
|
|
||||||
|
const sanitizedMicrosoftTeamsIntegrationSchema = WorkflowIntegrationsSchema.pick({
|
||||||
|
id: true,
|
||||||
|
description: true,
|
||||||
|
slug: true,
|
||||||
|
integration: true
|
||||||
|
}).merge(
|
||||||
|
MicrosoftTeamsIntegrationsSchema.pick({
|
||||||
|
tenantId: true
|
||||||
|
}).extend({
|
||||||
|
status: z.nativeEnum(WorkflowIntegrationStatus)
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
export const registerMicrosoftTeamsRouter = async (server: FastifyZodProvider) => {
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
body: z.object({
|
||||||
|
tenantId: z.string(),
|
||||||
|
slug: z.string()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: sanitizedMicrosoftTeamsIntegrationSchema
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const microsoftTeamsIntegration = await server.services.microsoftTeams.createMicrosoftTeamsIntegration({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
...req.body
|
||||||
|
});
|
||||||
|
|
||||||
|
return microsoftTeamsIntegration;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
security: [
|
||||||
|
{
|
||||||
|
bearerAuth: []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
response: {
|
||||||
|
200: sanitizedMicrosoftTeamsIntegrationSchema.array()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const microsoftTeamsIntegrations = await server.services.microsoftTeams.getMicrosoftTeamsIntegrationsByOrg({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
return microsoftTeamsIntegrations;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/:id/installation-status",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
id: z.string()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
await server.services.microsoftTeams.checkInstallationStatus({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
workflowIntegrationId: req.params.id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "DELETE",
|
||||||
|
url: "/:id",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
security: [
|
||||||
|
{
|
||||||
|
bearerAuth: []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
params: z.object({
|
||||||
|
id: z.string()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: sanitizedMicrosoftTeamsIntegrationSchema
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const deletedMicrosoftTeamsIntegration = await server.services.microsoftTeams.deleteMicrosoftTeamsIntegration({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
id: req.params.id
|
||||||
|
});
|
||||||
|
|
||||||
|
return deletedMicrosoftTeamsIntegration;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/:id",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
security: [
|
||||||
|
{
|
||||||
|
bearerAuth: []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
params: z.object({
|
||||||
|
id: z.string()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: sanitizedMicrosoftTeamsIntegrationSchema
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const microsoftTeamsIntegration = await server.services.microsoftTeams.getMicrosoftTeamsIntegrationById({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
id: req.params.id
|
||||||
|
});
|
||||||
|
|
||||||
|
return microsoftTeamsIntegration;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "PATCH",
|
||||||
|
url: "/:id",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
security: [
|
||||||
|
{
|
||||||
|
bearerAuth: []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
params: z.object({
|
||||||
|
id: z.string()
|
||||||
|
}),
|
||||||
|
body: z.object({
|
||||||
|
slug: slugSchema({ max: 64 }).optional(),
|
||||||
|
description: z.string().optional()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: sanitizedMicrosoftTeamsIntegrationSchema
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const microsoftTeamsIntegration = await server.services.microsoftTeams.updateMicrosoftTeamsIntegration({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
id: req.params.id,
|
||||||
|
...req.body
|
||||||
|
});
|
||||||
|
|
||||||
|
return microsoftTeamsIntegration;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/:workflowIntegrationId/teams",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
workflowIntegrationId: z.string()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z
|
||||||
|
.object({
|
||||||
|
teamId: z.string(),
|
||||||
|
teamName: z.string(),
|
||||||
|
channels: z
|
||||||
|
.object({
|
||||||
|
channelName: z.string(),
|
||||||
|
channelId: z.string()
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const teams = await server.services.microsoftTeams.getTeams({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
workflowIntegrationId: req.params.workflowIntegrationId
|
||||||
|
});
|
||||||
|
|
||||||
|
return teams;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/message-endpoint",
|
||||||
|
schema: {
|
||||||
|
body: z.any(),
|
||||||
|
response: {
|
||||||
|
200: z.any()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req, res) => {
|
||||||
|
await server.services.microsoftTeams.handleMessageEndpoint(req, res);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
UserEncryptionKeysSchema,
|
UserEncryptionKeysSchema,
|
||||||
UsersSchema
|
UsersSchema
|
||||||
} from "@app/db/schemas";
|
} from "@app/db/schemas";
|
||||||
|
import { ProjectMicrosoftTeamsConfigsSchema } from "@app/db/schemas/project-microsoft-teams-configs";
|
||||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
import { PROJECTS } from "@app/lib/api-docs";
|
import { PROJECTS } from "@app/lib/api-docs";
|
||||||
import { CharacterType, characterValidator } from "@app/lib/validator/validate-string";
|
import { CharacterType, characterValidator } from "@app/lib/validator/validate-string";
|
||||||
@@ -19,8 +20,9 @@ import { re2Validator } from "@app/lib/zod";
|
|||||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { ActorType, AuthMode } from "@app/services/auth/auth-type";
|
import { ActorType, AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
import { MicrosoftTeamsChannelsSchema } from "@app/services/microsoft-teams/microsoft-teams-fns";
|
||||||
import { ProjectFilterType, SearchProjectSortBy } from "@app/services/project/project-types";
|
import { ProjectFilterType, SearchProjectSortBy } from "@app/services/project/project-types";
|
||||||
import { validateSlackChannelsField } from "@app/services/slack/slack-auth-validators";
|
import { WorkflowIntegration } from "@app/services/workflow-integration/workflow-integration-types";
|
||||||
|
|
||||||
import { integrationAuthPubSchema, SanitizedProjectSchema } from "../sanitizedSchemas";
|
import { integrationAuthPubSchema, SanitizedProjectSchema } from "../sanitizedSchemas";
|
||||||
import { sanitizedServiceTokenSchema } from "../v2/service-token-router";
|
import { sanitizedServiceTokenSchema } from "../v2/service-token-router";
|
||||||
@@ -615,88 +617,172 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
url: "/:workspaceId/slack-config",
|
url: "/:workspaceId/workflow-integration-config/:integration",
|
||||||
config: {
|
config: {
|
||||||
rateLimit: readLimit
|
rateLimit: readLimit
|
||||||
},
|
},
|
||||||
schema: {
|
schema: {
|
||||||
params: z.object({
|
params: z.object({
|
||||||
workspaceId: z.string().trim()
|
workspaceId: z.string().trim(),
|
||||||
|
integration: z.nativeEnum(WorkflowIntegration)
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: ProjectSlackConfigsSchema.pick({
|
200: z.discriminatedUnion("integration", [
|
||||||
|
ProjectSlackConfigsSchema.pick({
|
||||||
id: true,
|
id: true,
|
||||||
slackIntegrationId: true,
|
|
||||||
isAccessRequestNotificationEnabled: true,
|
isAccessRequestNotificationEnabled: true,
|
||||||
accessRequestChannels: true,
|
accessRequestChannels: true,
|
||||||
isSecretRequestNotificationEnabled: true,
|
isSecretRequestNotificationEnabled: true,
|
||||||
secretRequestChannels: true
|
secretRequestChannels: true
|
||||||
|
}).merge(
|
||||||
|
z.object({
|
||||||
|
integration: z.literal(WorkflowIntegration.SLACK),
|
||||||
|
integrationId: z.string()
|
||||||
})
|
})
|
||||||
|
),
|
||||||
|
ProjectMicrosoftTeamsConfigsSchema.pick({
|
||||||
|
id: true,
|
||||||
|
isAccessRequestNotificationEnabled: true,
|
||||||
|
accessRequestChannels: true,
|
||||||
|
isSecretRequestNotificationEnabled: true,
|
||||||
|
secretRequestChannels: true
|
||||||
|
}).merge(
|
||||||
|
z.object({
|
||||||
|
integration: z.literal(WorkflowIntegration.MICROSOFT_TEAMS),
|
||||||
|
integrationId: z.string()
|
||||||
|
})
|
||||||
|
)
|
||||||
|
])
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const slackConfig = await server.services.project.getProjectSlackConfig({
|
const config = await server.services.project.getProjectWorkflowIntegrationConfig({
|
||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
actorAuthMethod: req.permission.authMethod,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
actorOrgId: req.permission.orgId,
|
actorOrgId: req.permission.orgId,
|
||||||
projectId: req.params.workspaceId
|
projectId: req.params.workspaceId,
|
||||||
|
integration: req.params.integration
|
||||||
});
|
});
|
||||||
|
|
||||||
if (slackConfig) {
|
if (config) {
|
||||||
await server.services.auditLog.createAuditLog({
|
await server.services.auditLog.createAuditLog({
|
||||||
...req.auditLogInfo,
|
...req.auditLogInfo,
|
||||||
projectId: req.params.workspaceId,
|
projectId: req.params.workspaceId,
|
||||||
event: {
|
event: {
|
||||||
type: EventType.GET_PROJECT_SLACK_CONFIG,
|
type: EventType.GET_PROJECT_WORKFLOW_INTEGRATION_CONFIG,
|
||||||
metadata: {
|
metadata: {
|
||||||
id: slackConfig.id
|
id: config.id,
|
||||||
|
integration: config.integration
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return slackConfig;
|
return config;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "DELETE",
|
||||||
|
url: "/:projectId/workflow-integrations/:integration/:integrationId",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
projectId: z.string().trim(),
|
||||||
|
integration: z.nativeEnum(WorkflowIntegration),
|
||||||
|
integrationId: z.string()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const deletedIntegration = await server.services.project.deleteProjectWorkflowIntegration({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
projectId: req.params.projectId,
|
||||||
|
integration: req.params.integration,
|
||||||
|
integrationId: req.params.integrationId
|
||||||
|
});
|
||||||
|
|
||||||
|
return deletedIntegration;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
url: "/:workspaceId/slack-config",
|
url: "/:workspaceId/workflow-integration/:integration",
|
||||||
config: {
|
config: {
|
||||||
rateLimit: readLimit
|
rateLimit: readLimit
|
||||||
},
|
},
|
||||||
schema: {
|
schema: {
|
||||||
params: z.object({
|
params: z.object({
|
||||||
workspaceId: z.string().trim()
|
workspaceId: z.string().trim(),
|
||||||
|
integration: z.nativeEnum(WorkflowIntegration)
|
||||||
}),
|
}),
|
||||||
body: z.object({
|
body: z.object({
|
||||||
slackIntegrationId: z.string(),
|
integrationId: z.string(),
|
||||||
isAccessRequestNotificationEnabled: z.boolean(),
|
isAccessRequestNotificationEnabled: z.boolean(),
|
||||||
accessRequestChannels: validateSlackChannelsField,
|
accessRequestChannels: z.string().or(
|
||||||
|
z
|
||||||
|
.object({
|
||||||
|
teamId: z.string(),
|
||||||
|
channelIds: z.string().array()
|
||||||
|
})
|
||||||
|
.optional()
|
||||||
|
),
|
||||||
isSecretRequestNotificationEnabled: z.boolean(),
|
isSecretRequestNotificationEnabled: z.boolean(),
|
||||||
secretRequestChannels: validateSlackChannelsField
|
secretRequestChannels: z.string().or(
|
||||||
|
z
|
||||||
|
.object({
|
||||||
|
teamId: z.string(),
|
||||||
|
channelIds: z.string().array()
|
||||||
|
})
|
||||||
|
.optional()
|
||||||
|
)
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: ProjectSlackConfigsSchema.pick({
|
200: z.discriminatedUnion("integration", [
|
||||||
|
ProjectSlackConfigsSchema.pick({
|
||||||
id: true,
|
id: true,
|
||||||
slackIntegrationId: true,
|
|
||||||
isAccessRequestNotificationEnabled: true,
|
isAccessRequestNotificationEnabled: true,
|
||||||
accessRequestChannels: true,
|
accessRequestChannels: true,
|
||||||
isSecretRequestNotificationEnabled: true,
|
isSecretRequestNotificationEnabled: true,
|
||||||
secretRequestChannels: true
|
secretRequestChannels: true
|
||||||
|
}).merge(
|
||||||
|
z.object({
|
||||||
|
integration: z.literal(WorkflowIntegration.SLACK),
|
||||||
|
integrationId: z.string()
|
||||||
})
|
})
|
||||||
|
),
|
||||||
|
ProjectMicrosoftTeamsConfigsSchema.pick({
|
||||||
|
id: true,
|
||||||
|
isAccessRequestNotificationEnabled: true,
|
||||||
|
isSecretRequestNotificationEnabled: true
|
||||||
|
}).merge(
|
||||||
|
z.object({
|
||||||
|
integration: z.literal(WorkflowIntegration.MICROSOFT_TEAMS),
|
||||||
|
integrationId: z.string(),
|
||||||
|
accessRequestChannels: MicrosoftTeamsChannelsSchema,
|
||||||
|
secretRequestChannels: MicrosoftTeamsChannelsSchema
|
||||||
|
})
|
||||||
|
)
|
||||||
|
])
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const slackConfig = await server.services.project.updateProjectSlackConfig({
|
const workflowIntegrationConfig = await server.services.project.updateProjectWorkflowIntegration({
|
||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
actorAuthMethod: req.permission.authMethod,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
actorOrgId: req.permission.orgId,
|
actorOrgId: req.permission.orgId,
|
||||||
projectId: req.params.workspaceId,
|
projectId: req.params.workspaceId,
|
||||||
|
integration: req.params.integration,
|
||||||
...req.body
|
...req.body
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -704,19 +790,20 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
...req.auditLogInfo,
|
...req.auditLogInfo,
|
||||||
projectId: req.params.workspaceId,
|
projectId: req.params.workspaceId,
|
||||||
event: {
|
event: {
|
||||||
type: EventType.UPDATE_PROJECT_SLACK_CONFIG,
|
type: EventType.UPDATE_PROJECT_WORKFLOW_INTEGRATION_CONFIG,
|
||||||
metadata: {
|
metadata: {
|
||||||
id: slackConfig.id,
|
id: workflowIntegrationConfig.id,
|
||||||
slackIntegrationId: slackConfig.slackIntegrationId,
|
integrationId: workflowIntegrationConfig.integrationId,
|
||||||
isAccessRequestNotificationEnabled: slackConfig.isAccessRequestNotificationEnabled,
|
integration: workflowIntegrationConfig.integration,
|
||||||
accessRequestChannels: slackConfig.accessRequestChannels,
|
isAccessRequestNotificationEnabled: workflowIntegrationConfig.isAccessRequestNotificationEnabled,
|
||||||
isSecretRequestNotificationEnabled: slackConfig.isSecretRequestNotificationEnabled,
|
accessRequestChannels: workflowIntegrationConfig.accessRequestChannels,
|
||||||
secretRequestChannels: slackConfig.secretRequestChannels
|
isSecretRequestNotificationEnabled: workflowIntegrationConfig.isSecretRequestNotificationEnabled,
|
||||||
|
secretRequestChannels: workflowIntegrationConfig.secretRequestChannels
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return slackConfig;
|
return workflowIntegrationConfig;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ const sanitizedWorkflowIntegrationSchema = WorkflowIntegrationsSchema.pick({
|
|||||||
id: true,
|
id: true,
|
||||||
description: true,
|
description: true,
|
||||||
slug: true,
|
slug: true,
|
||||||
integration: true
|
integration: true,
|
||||||
|
status: true
|
||||||
});
|
});
|
||||||
|
|
||||||
export const registerWorkflowIntegrationRouter = async (server: FastifyZodProvider) => {
|
export const registerWorkflowIntegrationRouter = async (server: FastifyZodProvider) => {
|
||||||
|
|||||||
@@ -0,0 +1,442 @@
|
|||||||
|
/* eslint-disable class-methods-use-this */
|
||||||
|
import axios from "axios";
|
||||||
|
import { TeamsActivityHandler, TurnContext } from "botbuilder";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { getConfig } from "@app/lib/config/env";
|
||||||
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
|
import { logger } from "@app/lib/logger";
|
||||||
|
import { TNotification, TriggerFeature } from "@app/lib/workflow-integrations/types";
|
||||||
|
|
||||||
|
import { TWorkflowIntegrationDALFactory } from "../workflow-integration/workflow-integration-dal";
|
||||||
|
import { WorkflowIntegrationStatus } from "../workflow-integration/workflow-integration-types";
|
||||||
|
import { TMicrosoftTeamsIntegrationDALFactory } from "./microsoft-teams-integration-dal";
|
||||||
|
|
||||||
|
export const getMicrosoftTeamsAccessToken = async ({
|
||||||
|
tenantId,
|
||||||
|
clientId,
|
||||||
|
clientSecret,
|
||||||
|
getBotFrameworkToken = false
|
||||||
|
}: {
|
||||||
|
tenantId: string;
|
||||||
|
clientId: string;
|
||||||
|
clientSecret: string;
|
||||||
|
getBotFrameworkToken?: boolean;
|
||||||
|
}) => {
|
||||||
|
const details = getBotFrameworkToken
|
||||||
|
? {
|
||||||
|
uri: "https://login.microsoftonline.com/botframework.com/oauth2/v2.0/token",
|
||||||
|
scope: "https://api.botframework.com/.default"
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
uri: `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`,
|
||||||
|
scope: "https://graph.microsoft.com/.default"
|
||||||
|
};
|
||||||
|
|
||||||
|
const tokenResponse = await axios.post<{ access_token: string }>(
|
||||||
|
details.uri,
|
||||||
|
new URLSearchParams({
|
||||||
|
client_id: clientId,
|
||||||
|
client_secret: clientSecret,
|
||||||
|
scope: details.scope,
|
||||||
|
grant_type: "client_credentials"
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return tokenResponse.data.access_token;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isBotInstalledInTenant = async ({
|
||||||
|
tenantId,
|
||||||
|
botAppId,
|
||||||
|
botAppPassword,
|
||||||
|
botId
|
||||||
|
}: {
|
||||||
|
tenantId: string;
|
||||||
|
botAppId: string;
|
||||||
|
botAppPassword: string;
|
||||||
|
botId: string;
|
||||||
|
}) => {
|
||||||
|
try {
|
||||||
|
const botAccessToken = await getMicrosoftTeamsAccessToken({
|
||||||
|
tenantId,
|
||||||
|
clientId: botAppId.toString(),
|
||||||
|
clientSecret: botAppPassword.toString(),
|
||||||
|
getBotFrameworkToken: true
|
||||||
|
}).catch(() => null);
|
||||||
|
|
||||||
|
const accessToken = await getMicrosoftTeamsAccessToken({
|
||||||
|
tenantId,
|
||||||
|
clientId: botAppId.toString(),
|
||||||
|
clientSecret: botAppPassword.toString()
|
||||||
|
}).catch(() => null);
|
||||||
|
|
||||||
|
if (!botAccessToken || !accessToken) {
|
||||||
|
return {
|
||||||
|
accessToken: null,
|
||||||
|
botAccessToken: null,
|
||||||
|
installed: false,
|
||||||
|
internalId: null
|
||||||
|
} as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
const appsResponse = await axios
|
||||||
|
.get<{ value: { id: string; displayName: string; distributionMethod: string; externalId: string }[] }>(
|
||||||
|
"https://graph.microsoft.com/v1.0/appCatalogs/teamsApps",
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.catch((error) => {
|
||||||
|
logger.error(error, "Error fetching installed apps");
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!appsResponse) {
|
||||||
|
return {
|
||||||
|
installed: false,
|
||||||
|
internalId: null,
|
||||||
|
accessToken,
|
||||||
|
botAccessToken
|
||||||
|
} as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
const botInstalledInTenant = appsResponse.data.value.find((a) => a.externalId === botId);
|
||||||
|
|
||||||
|
if (!botInstalledInTenant) {
|
||||||
|
return {
|
||||||
|
installed: false,
|
||||||
|
internalId: null,
|
||||||
|
accessToken,
|
||||||
|
botAccessToken
|
||||||
|
} as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
installed: true,
|
||||||
|
internalId: botInstalledInTenant.id,
|
||||||
|
accessToken,
|
||||||
|
botAccessToken
|
||||||
|
} as const;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error, "Error fetching installed apps");
|
||||||
|
return {
|
||||||
|
installed: false,
|
||||||
|
internalId: null,
|
||||||
|
accessToken: null,
|
||||||
|
botAccessToken: null
|
||||||
|
} as const;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const buildTeamsPayload = (notification: TNotification) => {
|
||||||
|
const appCfg = getConfig();
|
||||||
|
|
||||||
|
switch (notification.type) {
|
||||||
|
case TriggerFeature.SECRET_APPROVAL: {
|
||||||
|
const { payload } = notification;
|
||||||
|
|
||||||
|
const adaptiveCard = {
|
||||||
|
type: "AdaptiveCard",
|
||||||
|
$schema: "http://adaptivecards.io/schemas/adaptive-card.json",
|
||||||
|
version: "1.5",
|
||||||
|
body: [
|
||||||
|
{
|
||||||
|
type: "TextBlock",
|
||||||
|
text: "Secret approval request",
|
||||||
|
weight: "Bolder",
|
||||||
|
size: "Large"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "TextBlock",
|
||||||
|
text: `A secret approval request has been opened by ${payload.userEmail}.`,
|
||||||
|
wrap: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "FactSet",
|
||||||
|
facts: [
|
||||||
|
{
|
||||||
|
title: "Environment",
|
||||||
|
value: payload.environment
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Secret path",
|
||||||
|
value: payload.secretPath || "/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: `Secret Key${payload.secretKeys.length > 1 ? "s" : ""}`,
|
||||||
|
value: payload.secretKeys.join(", ")
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
type: "Action.OpenUrl",
|
||||||
|
title: "View request in Infisical",
|
||||||
|
url: `${appCfg.SITE_URL}/secret-manager/${payload.projectId}/approval?requestId=${payload.requestId}`
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
adaptiveCard
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
case TriggerFeature.ACCESS_REQUEST: {
|
||||||
|
const { payload } = notification;
|
||||||
|
|
||||||
|
const adaptiveCard = {
|
||||||
|
type: "AdaptiveCard",
|
||||||
|
$schema: "http://adaptivecards.io/schemas/adaptive-card.json",
|
||||||
|
version: "1.5",
|
||||||
|
body: [
|
||||||
|
{
|
||||||
|
type: "TextBlock",
|
||||||
|
text: "New access approval request pending for review",
|
||||||
|
weight: "Bolder",
|
||||||
|
size: "Large"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "TextBlock",
|
||||||
|
text: `${payload.requesterFullName} (${payload.requesterEmail}) has requested ${
|
||||||
|
payload.isTemporary ? "temporary" : "permanent"
|
||||||
|
} access to path '${payload.secretPath}' in the ${payload.environment} environment of ${
|
||||||
|
payload.projectName
|
||||||
|
} project.`,
|
||||||
|
wrap: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "TextBlock",
|
||||||
|
text: `The following permissions are requested: ${payload.permissions.join(", ")}`,
|
||||||
|
wrap: true
|
||||||
|
},
|
||||||
|
payload.note
|
||||||
|
? {
|
||||||
|
type: "TextBlock",
|
||||||
|
text: `**User Note**: ${payload.note}`,
|
||||||
|
wrap: true
|
||||||
|
}
|
||||||
|
: null
|
||||||
|
].filter(Boolean),
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
type: "Action.OpenUrl",
|
||||||
|
title: "View request in Infisical",
|
||||||
|
url: payload.approvalUrl
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
adaptiveCard
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
default: {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Teams notification type not supported."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export class TeamsBot extends TeamsActivityHandler {
|
||||||
|
private botAppId: string;
|
||||||
|
|
||||||
|
private botAppPassword: string;
|
||||||
|
|
||||||
|
private workflowIntegrationDAL: Pick<TWorkflowIntegrationDALFactory, "update">;
|
||||||
|
|
||||||
|
private microsoftTeamsIntegrationDAL: Pick<TMicrosoftTeamsIntegrationDALFactory, "findOne">;
|
||||||
|
|
||||||
|
constructor({
|
||||||
|
botAppId,
|
||||||
|
botAppPassword,
|
||||||
|
workflowIntegrationDAL,
|
||||||
|
microsoftTeamsIntegrationDAL
|
||||||
|
}: {
|
||||||
|
botAppId: string;
|
||||||
|
botAppPassword: string;
|
||||||
|
workflowIntegrationDAL: Pick<TWorkflowIntegrationDALFactory, "update">;
|
||||||
|
microsoftTeamsIntegrationDAL: Pick<TMicrosoftTeamsIntegrationDALFactory, "findOne">;
|
||||||
|
}) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.botAppId = botAppId;
|
||||||
|
this.botAppPassword = botAppPassword;
|
||||||
|
this.workflowIntegrationDAL = workflowIntegrationDAL;
|
||||||
|
this.microsoftTeamsIntegrationDAL = microsoftTeamsIntegrationDAL;
|
||||||
|
|
||||||
|
// We know when a bot is added, but we can't know when it's fully removed from the tenant.
|
||||||
|
this.onTeamsMembersAddedEvent(async (membersAdded, _, context) => {
|
||||||
|
const botWasAdded = membersAdded.some((member) => member.id === context.activity.recipient.id);
|
||||||
|
|
||||||
|
if (botWasAdded && context.activity.conversation.tenantId) {
|
||||||
|
const microsoftTeamIntegration = await this.microsoftTeamsIntegrationDAL.findOne({
|
||||||
|
tenantId: context.activity.conversation.tenantId
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.workflowIntegrationDAL
|
||||||
|
.update(
|
||||||
|
{
|
||||||
|
id: microsoftTeamIntegration.id,
|
||||||
|
status: WorkflowIntegrationStatus.PENDING
|
||||||
|
},
|
||||||
|
{
|
||||||
|
status: WorkflowIntegrationStatus.INSTALLED
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.catch((error) => {
|
||||||
|
logger.error(error, "Microsoft Teams Workflow Integration: Failed to update workflow integration");
|
||||||
|
});
|
||||||
|
|
||||||
|
// This is required in order for the bot to send proactive messages, which is required for the bot to pass the bot release validation step.
|
||||||
|
await context.sendActivity(
|
||||||
|
"👋 Thanks for installing the Infisical app! You can now use the bot to send notifications to your selected teams."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async run(context: TurnContext) {
|
||||||
|
console.log("Running Microsoft Teams bot", {
|
||||||
|
context
|
||||||
|
});
|
||||||
|
await super.run(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendMessageToChannel(tenantId: string, channelId: string, teamId: string, notification: TNotification) {
|
||||||
|
const { adaptiveCard } = buildTeamsPayload(notification);
|
||||||
|
|
||||||
|
const botToken = await getMicrosoftTeamsAccessToken({
|
||||||
|
tenantId,
|
||||||
|
clientId: this.botAppId,
|
||||||
|
clientSecret: this.botAppPassword,
|
||||||
|
getBotFrameworkToken: true
|
||||||
|
});
|
||||||
|
|
||||||
|
const adaptiveCardActivity = {
|
||||||
|
type: "message",
|
||||||
|
attachments: [
|
||||||
|
{
|
||||||
|
contentType: "application/vnd.microsoft.card.adaptive",
|
||||||
|
content: adaptiveCard
|
||||||
|
}
|
||||||
|
],
|
||||||
|
conversation: {
|
||||||
|
id: channelId,
|
||||||
|
isGroup: true
|
||||||
|
},
|
||||||
|
channelData: {
|
||||||
|
channel: {
|
||||||
|
id: channelId
|
||||||
|
},
|
||||||
|
team: {
|
||||||
|
id: teamId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await axios.post(
|
||||||
|
`https://smba.trafficmanager.net/amer/v3/conversations/${channelId}/activities`,
|
||||||
|
adaptiveCardActivity,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${botToken}`,
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// todo: filter out teams that the bot is not a member of
|
||||||
|
async getTeamsAndChannels(tenantId: string, internalAppId: string) {
|
||||||
|
try {
|
||||||
|
const token = await getMicrosoftTeamsAccessToken({
|
||||||
|
tenantId,
|
||||||
|
clientId: this.botAppId,
|
||||||
|
clientSecret: this.botAppPassword
|
||||||
|
});
|
||||||
|
|
||||||
|
const teamsResponse = await axios
|
||||||
|
.get<{ value: { displayName: string; id: string }[] }>(`https://graph.microsoft.com/v1.0/teams`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
logger.error(error, "Microsoft Teams Workflow Integration: Failed to fetch teams");
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
|
||||||
|
const teams = teamsResponse.data.value;
|
||||||
|
const result = [];
|
||||||
|
|
||||||
|
for await (const team of teams) {
|
||||||
|
try {
|
||||||
|
// Get installed apps for this team
|
||||||
|
const installedAppsResponse = await axios.get<{ value: { teamsAppDefinition: { teamsAppId: string } }[] }>(
|
||||||
|
`https://graph.microsoft.com/v1.0/teams/${team.id}/installedApps?$expand=teamsAppDefinition`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!installedAppsResponse.data.value.some((app) => app.teamsAppDefinition.teamsAppId === internalAppId)) {
|
||||||
|
// eslint-disable-next-line no-continue
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// eslint-disable-next-line no-continue
|
||||||
|
continue; // skip this team if we can't determine if the bot is installed
|
||||||
|
}
|
||||||
|
|
||||||
|
const channelsResponse = await axios
|
||||||
|
.get<{ value: { displayName: string; id: string }[] }>(
|
||||||
|
`https://graph.microsoft.com/v1.0/teams/${team.id}/channels`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.catch((error) => {
|
||||||
|
logger.error(error, "Microsoft Teams Workflow Integration: Failed to fetch channels");
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
|
||||||
|
const channels = channelsResponse.data.value.map((channel) => ({
|
||||||
|
channelName: channel.displayName,
|
||||||
|
channelId: channel.id
|
||||||
|
}));
|
||||||
|
|
||||||
|
result.push({
|
||||||
|
teamId: team.id,
|
||||||
|
teamName: team.displayName,
|
||||||
|
channels
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error, "Microsoft Teams Workflow Integration: Error fetching teams and channels");
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MicrosoftTeamsChannelsSchema = z
|
||||||
|
.object({
|
||||||
|
teamId: z.string(),
|
||||||
|
channelIds: z.array(z.string()).min(1)
|
||||||
|
})
|
||||||
|
.optional()
|
||||||
|
.refine((data) => data === undefined || data?.channelIds.length <= 5, {
|
||||||
|
message: "You can only select up to 5 microsoft teams channels"
|
||||||
|
});
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TDbClient } from "@app/db";
|
||||||
|
import { TableName, TMicrosoftTeamsIntegrations, TWorkflowIntegrations } from "@app/db/schemas";
|
||||||
|
import { DatabaseError } from "@app/lib/errors";
|
||||||
|
import { ormify, selectAllTableCols } from "@app/lib/knex";
|
||||||
|
|
||||||
|
export type TMicrosoftTeamsIntegrationDALFactory = ReturnType<typeof microsoftTeamsIntegrationDALFactory>;
|
||||||
|
|
||||||
|
export const microsoftTeamsIntegrationDALFactory = (db: TDbClient) => {
|
||||||
|
const microsoftTeamsIntegrationOrm = ormify(db, TableName.MicrosoftTeamsIntegrations);
|
||||||
|
|
||||||
|
const findByIdWithWorkflowIntegrationDetails = async (id: string, tx?: Knex) => {
|
||||||
|
try {
|
||||||
|
return await (tx || db.replicaNode())(TableName.MicrosoftTeamsIntegrations)
|
||||||
|
.join(
|
||||||
|
TableName.WorkflowIntegrations,
|
||||||
|
`${TableName.MicrosoftTeamsIntegrations}.id`,
|
||||||
|
`${TableName.WorkflowIntegrations}.id`
|
||||||
|
)
|
||||||
|
.select(selectAllTableCols(TableName.MicrosoftTeamsIntegrations))
|
||||||
|
.select(db.ref("orgId").withSchema(TableName.WorkflowIntegrations))
|
||||||
|
.select(db.ref("description").withSchema(TableName.WorkflowIntegrations))
|
||||||
|
.select(db.ref("integration").withSchema(TableName.WorkflowIntegrations))
|
||||||
|
.select(db.ref("slug").withSchema(TableName.WorkflowIntegrations))
|
||||||
|
.select(db.ref("status").withSchema(TableName.WorkflowIntegrations))
|
||||||
|
.where(`${TableName.WorkflowIntegrations}.id`, id)
|
||||||
|
.first();
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "Find by ID with Workflow integration details" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const findWithWorkflowIntegrationDetails = async (
|
||||||
|
filter: Partial<TMicrosoftTeamsIntegrations> & Partial<TWorkflowIntegrations>,
|
||||||
|
tx?: Knex
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
return await (tx || db.replicaNode())(TableName.MicrosoftTeamsIntegrations)
|
||||||
|
.join(
|
||||||
|
TableName.WorkflowIntegrations,
|
||||||
|
`${TableName.MicrosoftTeamsIntegrations}.id`,
|
||||||
|
`${TableName.WorkflowIntegrations}.id`
|
||||||
|
)
|
||||||
|
.select(selectAllTableCols(TableName.MicrosoftTeamsIntegrations))
|
||||||
|
.select(db.ref("orgId").withSchema(TableName.WorkflowIntegrations))
|
||||||
|
.select(db.ref("description").withSchema(TableName.WorkflowIntegrations))
|
||||||
|
.select(db.ref("integration").withSchema(TableName.WorkflowIntegrations))
|
||||||
|
.select(db.ref("slug").withSchema(TableName.WorkflowIntegrations))
|
||||||
|
.select(db.ref("status").withSchema(TableName.WorkflowIntegrations))
|
||||||
|
.where(filter);
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "Find with Workflow integration details" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
...microsoftTeamsIntegrationOrm,
|
||||||
|
findByIdWithWorkflowIntegrationDetails,
|
||||||
|
findWithWorkflowIntegrationDetails
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,608 @@
|
|||||||
|
import { ForbiddenError } from "@casl/ability";
|
||||||
|
import {
|
||||||
|
CloudAdapter,
|
||||||
|
ConfigurationBotFrameworkAuthentication,
|
||||||
|
ConfigurationServiceClientCredentialFactory,
|
||||||
|
Request,
|
||||||
|
Response
|
||||||
|
} from "botbuilder";
|
||||||
|
import { FastifyReply, FastifyRequest } from "fastify";
|
||||||
|
|
||||||
|
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||||
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
|
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
|
import { logger } from "@app/lib/logger";
|
||||||
|
|
||||||
|
import { TKmsServiceFactory } from "../kms/kms-service";
|
||||||
|
import { KmsDataKey } from "../kms/kms-types";
|
||||||
|
import { TSuperAdminDALFactory } from "../super-admin/super-admin-dal";
|
||||||
|
import { TWorkflowIntegrationDALFactory } from "../workflow-integration/workflow-integration-dal";
|
||||||
|
import { WorkflowIntegration, WorkflowIntegrationStatus } from "../workflow-integration/workflow-integration-types";
|
||||||
|
import { isBotInstalledInTenant, TeamsBot } from "./microsoft-teams-fns";
|
||||||
|
import { TMicrosoftTeamsIntegrationDALFactory } from "./microsoft-teams-integration-dal";
|
||||||
|
import {
|
||||||
|
TCheckInstallationStatusDTO,
|
||||||
|
TCreateMicrosoftTeamsIntegrationDTO,
|
||||||
|
TDeleteMicrosoftTeamsIntegrationIntegrationDTO,
|
||||||
|
TGetMicrosoftTeamsIntegrationByIdDTO,
|
||||||
|
TGetMicrosoftTeamsIntegrationByOrgDTO,
|
||||||
|
TGetTeamsDTO,
|
||||||
|
TSendNotificationDTO,
|
||||||
|
TUpdateMicrosoftTeamsIntegrationDTO
|
||||||
|
} from "./microsoft-teams-types";
|
||||||
|
|
||||||
|
function requestBodyToRecord(body: unknown): Record<string, unknown> {
|
||||||
|
// if body is null or undefined, return an empty object
|
||||||
|
if (body === null || body === undefined) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// if body is not an object or is an array, return an empty object
|
||||||
|
if (typeof body !== "object" || Array.isArray(body)) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// at this point, we know body is an object, so safe to cast
|
||||||
|
return body as Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
type TMicrosoftTeamsServiceFactoryDep = {
|
||||||
|
microsoftTeamsIntegrationDAL: Pick<
|
||||||
|
TMicrosoftTeamsIntegrationDALFactory,
|
||||||
|
| "deleteById"
|
||||||
|
| "updateById"
|
||||||
|
| "create"
|
||||||
|
| "findOne"
|
||||||
|
| "findById"
|
||||||
|
| "findByIdWithWorkflowIntegrationDetails"
|
||||||
|
| "findWithWorkflowIntegrationDetails"
|
||||||
|
>;
|
||||||
|
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getOrgPermission">;
|
||||||
|
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey" | "encryptWithRootKey" | "decryptWithRootKey">;
|
||||||
|
workflowIntegrationDAL: Pick<
|
||||||
|
TWorkflowIntegrationDALFactory,
|
||||||
|
"transaction" | "create" | "updateById" | "deleteById" | "update" | "findOne"
|
||||||
|
>;
|
||||||
|
serverCfgDAL: Pick<TSuperAdminDALFactory, "findById">;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TMicrosoftTeamsServiceFactory = ReturnType<typeof microsoftTeamsServiceFactory>;
|
||||||
|
|
||||||
|
const ADMIN_CONFIG_DB_UUID = "00000000-0000-0000-0000-000000000000";
|
||||||
|
|
||||||
|
export const microsoftTeamsServiceFactory = ({
|
||||||
|
permissionService,
|
||||||
|
serverCfgDAL,
|
||||||
|
kmsService,
|
||||||
|
microsoftTeamsIntegrationDAL,
|
||||||
|
workflowIntegrationDAL
|
||||||
|
}: TMicrosoftTeamsServiceFactoryDep) => {
|
||||||
|
let teamsBot: TeamsBot | null = null;
|
||||||
|
let adapter: CloudAdapter | null = null;
|
||||||
|
|
||||||
|
const initializeTeamsBot = async ({ botAppId, botAppPassword }: { botAppId: string; botAppPassword: string }) => {
|
||||||
|
logger.info("Initializing Microsoft Teams bot");
|
||||||
|
teamsBot = new TeamsBot({
|
||||||
|
botAppId,
|
||||||
|
botAppPassword,
|
||||||
|
workflowIntegrationDAL,
|
||||||
|
microsoftTeamsIntegrationDAL
|
||||||
|
});
|
||||||
|
|
||||||
|
adapter = new CloudAdapter(
|
||||||
|
new ConfigurationBotFrameworkAuthentication(
|
||||||
|
{},
|
||||||
|
new ConfigurationServiceClientCredentialFactory({
|
||||||
|
MicrosoftAppId: botAppId,
|
||||||
|
MicrosoftAppPassword: botAppPassword,
|
||||||
|
MicrosoftAppType: "MultiTenant"
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const start = async () => {
|
||||||
|
try {
|
||||||
|
const serverCfg = await serverCfgDAL.findById(ADMIN_CONFIG_DB_UUID);
|
||||||
|
|
||||||
|
if (
|
||||||
|
serverCfg?.encryptedMicrosoftTeamsAppId &&
|
||||||
|
serverCfg?.encryptedMicrosoftTeamsClientSecret &&
|
||||||
|
serverCfg?.encryptedMicrosoftTeamsBotId
|
||||||
|
) {
|
||||||
|
const decryptWithRoot = kmsService.decryptWithRootKey();
|
||||||
|
const decryptedAppId = decryptWithRoot(serverCfg.encryptedMicrosoftTeamsAppId);
|
||||||
|
const decryptedAppPassword = decryptWithRoot(serverCfg.encryptedMicrosoftTeamsClientSecret);
|
||||||
|
|
||||||
|
await initializeTeamsBot({
|
||||||
|
botAppId: decryptedAppId.toString(),
|
||||||
|
botAppPassword: decryptedAppPassword.toString()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(err, "Error initializing Microsoft Teams bot on startup");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const checkInstallationStatus = async ({
|
||||||
|
actorId,
|
||||||
|
actor,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
workflowIntegrationId
|
||||||
|
}: TCheckInstallationStatusDTO) => {
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings);
|
||||||
|
|
||||||
|
const microsoftTeamsIntegration =
|
||||||
|
await microsoftTeamsIntegrationDAL.findByIdWithWorkflowIntegrationDetails(workflowIntegrationId);
|
||||||
|
|
||||||
|
if (!microsoftTeamsIntegration) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `Microsoft Teams integration with ID ${workflowIntegrationId} not found`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const serverCfg = await serverCfgDAL.findById(ADMIN_CONFIG_DB_UUID);
|
||||||
|
if (!serverCfg) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to get server configuration."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!serverCfg.encryptedMicrosoftTeamsAppId ||
|
||||||
|
!serverCfg.encryptedMicrosoftTeamsClientSecret ||
|
||||||
|
!serverCfg.encryptedMicrosoftTeamsBotId
|
||||||
|
) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Microsoft Teams app ID, client secret, or bot ID is not set"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const decryptWithRoot = kmsService.decryptWithRootKey();
|
||||||
|
const decryptedAppId = decryptWithRoot(serverCfg.encryptedMicrosoftTeamsAppId);
|
||||||
|
const decryptedAppPassword = decryptWithRoot(serverCfg.encryptedMicrosoftTeamsClientSecret);
|
||||||
|
const decryptedBotId = decryptWithRoot(serverCfg.encryptedMicrosoftTeamsBotId);
|
||||||
|
|
||||||
|
const teamsBotInfo = await isBotInstalledInTenant({
|
||||||
|
tenantId: microsoftTeamsIntegration.tenantId,
|
||||||
|
botAppId: decryptedAppId.toString(),
|
||||||
|
botAppPassword: decryptedAppPassword.toString(),
|
||||||
|
botId: decryptedBotId.toString()
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!teamsBotInfo.installed) {
|
||||||
|
if (microsoftTeamsIntegration.status === WorkflowIntegrationStatus.INSTALLED) {
|
||||||
|
await workflowIntegrationDAL.updateById(microsoftTeamsIntegration.id, {
|
||||||
|
status: WorkflowIntegrationStatus.PENDING
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Microsoft Teams bot is not installed in the configured Microsoft Teams Tenant"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (microsoftTeamsIntegration.status !== WorkflowIntegrationStatus.INSTALLED) {
|
||||||
|
await workflowIntegrationDAL.updateById(microsoftTeamsIntegration.id, {
|
||||||
|
status: WorkflowIntegrationStatus.INSTALLED
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const createMicrosoftTeamsIntegration = async ({
|
||||||
|
actorId,
|
||||||
|
actor,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
slug,
|
||||||
|
description,
|
||||||
|
tenantId
|
||||||
|
}: TCreateMicrosoftTeamsIntegrationDTO) => {
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Settings);
|
||||||
|
|
||||||
|
const serverCfg = await serverCfgDAL.findById(ADMIN_CONFIG_DB_UUID);
|
||||||
|
if (!serverCfg) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to get server configuration."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { encryptedMicrosoftTeamsAppId, encryptedMicrosoftTeamsClientSecret, encryptedMicrosoftTeamsBotId } =
|
||||||
|
serverCfg;
|
||||||
|
|
||||||
|
if (!encryptedMicrosoftTeamsAppId || !encryptedMicrosoftTeamsClientSecret || !encryptedMicrosoftTeamsBotId) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Microsoft Teams app ID, client secret, or bot ID is not set"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const integration = await workflowIntegrationDAL.transaction(async (tx) => {
|
||||||
|
const workflowIntegration = await workflowIntegrationDAL.create(
|
||||||
|
{
|
||||||
|
description,
|
||||||
|
orgId: actorOrgId,
|
||||||
|
slug,
|
||||||
|
integration: WorkflowIntegration.MICROSOFT_TEAMS,
|
||||||
|
status: WorkflowIntegrationStatus.PENDING
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
const microsoftTeamsIntegration = await microsoftTeamsIntegrationDAL.create(
|
||||||
|
{
|
||||||
|
// @ts-expect-error id is kept as fixed because it is always equal to the workflow integration ID
|
||||||
|
id: workflowIntegration.id,
|
||||||
|
tenantId
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
const decryptWithRoot = kmsService.decryptWithRootKey();
|
||||||
|
const botAppId = decryptWithRoot(encryptedMicrosoftTeamsAppId);
|
||||||
|
const botAppPassword = decryptWithRoot(encryptedMicrosoftTeamsClientSecret);
|
||||||
|
const botId = decryptWithRoot(encryptedMicrosoftTeamsBotId);
|
||||||
|
|
||||||
|
const teamsBotInfo = await isBotInstalledInTenant({
|
||||||
|
tenantId: microsoftTeamsIntegration.tenantId,
|
||||||
|
botAppId: botAppId.toString(),
|
||||||
|
botAppPassword: botAppPassword.toString(),
|
||||||
|
botId: botId.toString()
|
||||||
|
});
|
||||||
|
if (teamsBotInfo.installed) {
|
||||||
|
const { encryptor: orgDataKeyEncryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
|
orgId: workflowIntegration.orgId,
|
||||||
|
type: KmsDataKey.Organization
|
||||||
|
});
|
||||||
|
const { cipherTextBlob: encryptedAccessToken } = orgDataKeyEncryptor({
|
||||||
|
plainText: Buffer.from(teamsBotInfo.accessToken, "utf8")
|
||||||
|
});
|
||||||
|
|
||||||
|
const { cipherTextBlob: encryptedBotAccessToken } = orgDataKeyEncryptor({
|
||||||
|
plainText: Buffer.from(teamsBotInfo.botAccessToken, "utf8")
|
||||||
|
});
|
||||||
|
await microsoftTeamsIntegrationDAL.updateById(
|
||||||
|
microsoftTeamsIntegration.id,
|
||||||
|
{
|
||||||
|
internalTeamsAppId: teamsBotInfo.internalId,
|
||||||
|
encryptedAccessToken,
|
||||||
|
encryptedBotAccessToken
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
await workflowIntegrationDAL.updateById(
|
||||||
|
workflowIntegration.id,
|
||||||
|
{
|
||||||
|
status: WorkflowIntegrationStatus.INSTALLED
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...workflowIntegration,
|
||||||
|
status: workflowIntegration.status as WorkflowIntegrationStatus,
|
||||||
|
tenantId: microsoftTeamsIntegration.tenantId
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return integration;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getMicrosoftTeamsIntegrationsByOrg = async ({
|
||||||
|
actorId,
|
||||||
|
actor,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod
|
||||||
|
}: TGetMicrosoftTeamsIntegrationByOrgDTO) => {
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Settings);
|
||||||
|
|
||||||
|
const microsoftTeamsIntegrations = await microsoftTeamsIntegrationDAL.findWithWorkflowIntegrationDetails({
|
||||||
|
orgId: actorOrgId
|
||||||
|
});
|
||||||
|
|
||||||
|
return microsoftTeamsIntegrations.map((integration) => ({
|
||||||
|
...integration,
|
||||||
|
status: integration.status as WorkflowIntegrationStatus,
|
||||||
|
tenantId: integration.tenantId
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const getMicrosoftTeamsIntegrationById = async ({
|
||||||
|
actorId,
|
||||||
|
actor,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
id
|
||||||
|
}: TGetMicrosoftTeamsIntegrationByIdDTO) => {
|
||||||
|
const microsoftTeamsIntegration = await microsoftTeamsIntegrationDAL.findByIdWithWorkflowIntegrationDetails(id);
|
||||||
|
if (!microsoftTeamsIntegration) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: "Microsoft Teams integration not found."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
microsoftTeamsIntegration.orgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Settings);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...microsoftTeamsIntegration,
|
||||||
|
status: microsoftTeamsIntegration.status as WorkflowIntegrationStatus
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateMicrosoftTeamsIntegration = async ({
|
||||||
|
actorId,
|
||||||
|
actor,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
id,
|
||||||
|
slug,
|
||||||
|
description
|
||||||
|
}: TUpdateMicrosoftTeamsIntegrationDTO) => {
|
||||||
|
const microsoftTeamsIntegration = await microsoftTeamsIntegrationDAL.findByIdWithWorkflowIntegrationDetails(id);
|
||||||
|
if (!microsoftTeamsIntegration) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `Microsoft Teams integration with ID ${id} not found`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
microsoftTeamsIntegration.orgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings);
|
||||||
|
|
||||||
|
const updatedIntegration = await workflowIntegrationDAL.transaction(async (tx) => {
|
||||||
|
await workflowIntegrationDAL.updateById(
|
||||||
|
microsoftTeamsIntegration.id,
|
||||||
|
{
|
||||||
|
slug,
|
||||||
|
description
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
const integration = await microsoftTeamsIntegrationDAL.findByIdWithWorkflowIntegrationDetails(
|
||||||
|
microsoftTeamsIntegration.id,
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!integration) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `Microsoft Teams integration with ID ${microsoftTeamsIntegration.id} not found`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...integration,
|
||||||
|
status: integration.status as WorkflowIntegrationStatus
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return updatedIntegration;
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteMicrosoftTeamsIntegration = async ({
|
||||||
|
actorId,
|
||||||
|
actor,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
id
|
||||||
|
}: TDeleteMicrosoftTeamsIntegrationIntegrationDTO) => {
|
||||||
|
const microsoftTeamsIntegration = await microsoftTeamsIntegrationDAL.findByIdWithWorkflowIntegrationDetails(id);
|
||||||
|
if (!microsoftTeamsIntegration) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `Microsoft Teams integration with ID ${id} not found`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
microsoftTeamsIntegration.orgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Settings);
|
||||||
|
|
||||||
|
await workflowIntegrationDAL.deleteById(id);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...microsoftTeamsIntegration,
|
||||||
|
status: microsoftTeamsIntegration.status as WorkflowIntegrationStatus
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTeams = async ({ actorId, actor, actorOrgId, actorAuthMethod, workflowIntegrationId }: TGetTeamsDTO) => {
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Settings);
|
||||||
|
|
||||||
|
const microsoftTeamsIntegration =
|
||||||
|
await microsoftTeamsIntegrationDAL.findByIdWithWorkflowIntegrationDetails(workflowIntegrationId);
|
||||||
|
|
||||||
|
if (!microsoftTeamsIntegration) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `Microsoft Teams integration with ID ${workflowIntegrationId} not found`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!teamsBot || !adapter) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Unable to get teams and channels because the Microsoft Teams bot is uninitialized"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const serverCfg = await serverCfgDAL.findById(ADMIN_CONFIG_DB_UUID);
|
||||||
|
if (!serverCfg) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to get server configuration."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!serverCfg.encryptedMicrosoftTeamsAppId ||
|
||||||
|
!serverCfg.encryptedMicrosoftTeamsClientSecret ||
|
||||||
|
!serverCfg.encryptedMicrosoftTeamsBotId
|
||||||
|
) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Microsoft Teams app ID, client secret, or bot ID is not set"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const decryptWithRoot = kmsService.decryptWithRootKey();
|
||||||
|
const decryptedAppId = decryptWithRoot(serverCfg.encryptedMicrosoftTeamsAppId);
|
||||||
|
const decryptedAppPassword = decryptWithRoot(serverCfg.encryptedMicrosoftTeamsClientSecret);
|
||||||
|
const decryptedBotId = decryptWithRoot(serverCfg.encryptedMicrosoftTeamsBotId);
|
||||||
|
|
||||||
|
const { installed, internalId } = await isBotInstalledInTenant({
|
||||||
|
tenantId: microsoftTeamsIntegration.tenantId,
|
||||||
|
botAppId: decryptedAppId.toString(),
|
||||||
|
botAppPassword: decryptedAppPassword.toString(),
|
||||||
|
botId: decryptedBotId.toString()
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!installed) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Microsoft Teams bot is not installed in the configured Microsoft Teams Tenant"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const teams = await teamsBot.getTeamsAndChannels(microsoftTeamsIntegration.tenantId, internalId);
|
||||||
|
|
||||||
|
return teams;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMessageEndpoint = async (req: FastifyRequest, res: FastifyReply) => {
|
||||||
|
if (!teamsBot || !adapter) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Unable to handle message endpoint because the Microsoft Teams bot is uninitialized"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// We need to manually build a Response object because the BotFrameworkAdapter expects a Response object. We are using FastifyReply as the underlying socket.
|
||||||
|
const response: Response = {
|
||||||
|
socket: res.raw.socket,
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
end(...args: any[]): unknown {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||||
|
res.raw.end(...args);
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
|
||||||
|
header(name: string, value: unknown): unknown {
|
||||||
|
res.raw.setHeader(name, value as string);
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
|
||||||
|
send(...args: unknown[]): unknown {
|
||||||
|
// For the first argument, which is typically the body
|
||||||
|
if (args.length > 0) {
|
||||||
|
const body = args[0];
|
||||||
|
|
||||||
|
if (typeof body === "string" || Buffer.isBuffer(body)) {
|
||||||
|
res.raw.write(body);
|
||||||
|
} else if (body !== null && body !== undefined) {
|
||||||
|
const json = JSON.stringify(body);
|
||||||
|
if (!res.raw.headersSent && !res.raw.getHeader("content-type")) {
|
||||||
|
res.raw.setHeader("content-type", "application/json");
|
||||||
|
}
|
||||||
|
res.raw.write(json);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastArg = args[args.length - 1];
|
||||||
|
if (typeof lastArg === "function") {
|
||||||
|
lastArg();
|
||||||
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
|
||||||
|
status(code: number): unknown {
|
||||||
|
res.raw.statusCode = code;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const request: Request = {
|
||||||
|
body: requestBodyToRecord(req.body),
|
||||||
|
headers: req.headers,
|
||||||
|
method: req.method
|
||||||
|
};
|
||||||
|
|
||||||
|
await adapter.process(request, response, async (context) => {
|
||||||
|
await teamsBot?.run(context);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const sendNotification = async ({ tenantId, target, notification }: TSendNotificationDTO) => {
|
||||||
|
if (!teamsBot || !adapter) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Unable to send notification because the Microsoft Teams bot is uninitialized"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for await (const channelId of target.channelIds) {
|
||||||
|
await teamsBot.sendMessageToChannel(tenantId, channelId, target.teamId, notification);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
getMicrosoftTeamsIntegrationsByOrg,
|
||||||
|
getMicrosoftTeamsIntegrationById,
|
||||||
|
updateMicrosoftTeamsIntegration,
|
||||||
|
deleteMicrosoftTeamsIntegration,
|
||||||
|
createMicrosoftTeamsIntegration,
|
||||||
|
initializeTeamsBot,
|
||||||
|
getTeams,
|
||||||
|
handleMessageEndpoint,
|
||||||
|
start,
|
||||||
|
sendNotification,
|
||||||
|
checkInstallationStatus
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { TOrgPermission } from "@app/lib/types";
|
||||||
|
import { TNotification } from "@app/lib/workflow-integrations/types";
|
||||||
|
|
||||||
|
export type TGetMicrosoftTeamsIntegrationByOrgDTO = Omit<TOrgPermission, "orgId">;
|
||||||
|
|
||||||
|
export type TCreateMicrosoftTeamsIntegrationDTO = Omit<TOrgPermission, "orgId"> & {
|
||||||
|
slug: string;
|
||||||
|
description?: string;
|
||||||
|
tenantId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TCheckInstallationStatusDTO = { workflowIntegrationId: string } & Omit<TOrgPermission, "orgId">;
|
||||||
|
|
||||||
|
export type TGetMicrosoftTeamsIntegrationByIdDTO = { id: string } & Omit<TOrgPermission, "orgId">;
|
||||||
|
|
||||||
|
export type TUpdateMicrosoftTeamsIntegrationDTO = { id: string; slug?: string; description?: string } & Omit<
|
||||||
|
TOrgPermission,
|
||||||
|
"orgId"
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type TGetTeamsDTO = Omit<TOrgPermission, "orgId"> & {
|
||||||
|
workflowIntegrationId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TDeleteMicrosoftTeamsIntegrationIntegrationDTO = {
|
||||||
|
id: string;
|
||||||
|
} & Omit<TOrgPermission, "orgId">;
|
||||||
|
|
||||||
|
export type TSendNotificationDTO = {
|
||||||
|
tenantId: string;
|
||||||
|
target: {
|
||||||
|
teamId: string;
|
||||||
|
channelIds: string[];
|
||||||
|
};
|
||||||
|
notification: TNotification;
|
||||||
|
};
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TDbClient } from "@app/db";
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
import { ormify, selectAllTableCols } from "@app/lib/knex";
|
||||||
|
|
||||||
|
export type TProjectMicrosoftTeamsConfigDALFactory = ReturnType<typeof projectMicrosoftTeamsConfigDALFactory>;
|
||||||
|
|
||||||
|
export const projectMicrosoftTeamsConfigDALFactory = (db: TDbClient) => {
|
||||||
|
const projectMicrosoftTeamsConfigOrm = ormify(db, TableName.ProjectMicrosoftTeamsConfigs);
|
||||||
|
|
||||||
|
const getIntegrationDetailsByProject = (projectId: string, tx?: Knex) => {
|
||||||
|
return (tx || db.replicaNode())(TableName.ProjectMicrosoftTeamsConfigs)
|
||||||
|
.join(
|
||||||
|
TableName.MicrosoftTeamsIntegrations,
|
||||||
|
`${TableName.ProjectMicrosoftTeamsConfigs}.microsoftTeamsIntegrationId`,
|
||||||
|
`${TableName.MicrosoftTeamsIntegrations}.id`
|
||||||
|
)
|
||||||
|
.where("projectId", "=", projectId)
|
||||||
|
.select(
|
||||||
|
selectAllTableCols(TableName.ProjectMicrosoftTeamsConfigs),
|
||||||
|
selectAllTableCols(TableName.MicrosoftTeamsIntegrations)
|
||||||
|
)
|
||||||
|
.first();
|
||||||
|
};
|
||||||
|
|
||||||
|
return { ...projectMicrosoftTeamsConfigOrm, getIntegrationDetailsByProject };
|
||||||
|
};
|
||||||
@@ -43,6 +43,9 @@ import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
|
|||||||
import { TIdentityProjectDALFactory } from "../identity-project/identity-project-dal";
|
import { TIdentityProjectDALFactory } from "../identity-project/identity-project-dal";
|
||||||
import { TIdentityProjectMembershipRoleDALFactory } from "../identity-project/identity-project-membership-role-dal";
|
import { TIdentityProjectMembershipRoleDALFactory } from "../identity-project/identity-project-membership-role-dal";
|
||||||
import { TKmsServiceFactory } from "../kms/kms-service";
|
import { TKmsServiceFactory } from "../kms/kms-service";
|
||||||
|
import { MicrosoftTeamsChannelsSchema } from "../microsoft-teams/microsoft-teams-fns";
|
||||||
|
import { TMicrosoftTeamsIntegrationDALFactory } from "../microsoft-teams/microsoft-teams-integration-dal";
|
||||||
|
import { TProjectMicrosoftTeamsConfigDALFactory } from "../microsoft-teams/project-microsoft-teams-config-dal";
|
||||||
import { TOrgDALFactory } from "../org/org-dal";
|
import { TOrgDALFactory } from "../org/org-dal";
|
||||||
import { TOrgServiceFactory } from "../org/org-service";
|
import { TOrgServiceFactory } from "../org/org-service";
|
||||||
import { TPkiAlertDALFactory } from "../pki-alert/pki-alert-dal";
|
import { TPkiAlertDALFactory } from "../pki-alert/pki-alert-dal";
|
||||||
@@ -60,9 +63,11 @@ import { fnDeleteProjectSecretReminders } from "../secret/secret-fns";
|
|||||||
import { ROOT_FOLDER_NAME, TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
import { ROOT_FOLDER_NAME, TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
||||||
import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal";
|
import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal";
|
||||||
import { TProjectSlackConfigDALFactory } from "../slack/project-slack-config-dal";
|
import { TProjectSlackConfigDALFactory } from "../slack/project-slack-config-dal";
|
||||||
|
import { validateSlackChannelsField } from "../slack/slack-auth-validators";
|
||||||
import { TSlackIntegrationDALFactory } from "../slack/slack-integration-dal";
|
import { TSlackIntegrationDALFactory } from "../slack/slack-integration-dal";
|
||||||
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
||||||
import { TUserDALFactory } from "../user/user-dal";
|
import { TUserDALFactory } from "../user/user-dal";
|
||||||
|
import { WorkflowIntegration } from "../workflow-integration/workflow-integration-types";
|
||||||
import { TProjectDALFactory } from "./project-dal";
|
import { TProjectDALFactory } from "./project-dal";
|
||||||
import { assignWorkspaceKeysToMembers, bootstrapSshProject, createProjectKey } from "./project-fns";
|
import { assignWorkspaceKeysToMembers, bootstrapSshProject, createProjectKey } from "./project-fns";
|
||||||
import { TProjectQueueFactory } from "./project-queue";
|
import { TProjectQueueFactory } from "./project-queue";
|
||||||
@@ -70,9 +75,10 @@ import { TProjectSshConfigDALFactory } from "./project-ssh-config-dal";
|
|||||||
import {
|
import {
|
||||||
TCreateProjectDTO,
|
TCreateProjectDTO,
|
||||||
TDeleteProjectDTO,
|
TDeleteProjectDTO,
|
||||||
|
TDeleteProjectWorkflowIntegration,
|
||||||
TGetProjectDTO,
|
TGetProjectDTO,
|
||||||
TGetProjectKmsKey,
|
TGetProjectKmsKey,
|
||||||
TGetProjectSlackConfig,
|
TGetProjectWorkflowIntegrationConfig,
|
||||||
TListProjectAlertsDTO,
|
TListProjectAlertsDTO,
|
||||||
TListProjectCasDTO,
|
TListProjectCasDTO,
|
||||||
TListProjectCertificateTemplatesDTO,
|
TListProjectCertificateTemplatesDTO,
|
||||||
@@ -91,8 +97,8 @@ import {
|
|||||||
TUpdateProjectDTO,
|
TUpdateProjectDTO,
|
||||||
TUpdateProjectKmsDTO,
|
TUpdateProjectKmsDTO,
|
||||||
TUpdateProjectNameDTO,
|
TUpdateProjectNameDTO,
|
||||||
TUpdateProjectSlackConfig,
|
|
||||||
TUpdateProjectVersionLimitDTO,
|
TUpdateProjectVersionLimitDTO,
|
||||||
|
TUpdateProjectWorkflowIntegration,
|
||||||
TUpgradeProjectDTO
|
TUpgradeProjectDTO
|
||||||
} from "./project-types";
|
} from "./project-types";
|
||||||
|
|
||||||
@@ -121,8 +127,19 @@ type TProjectServiceFactoryDep = {
|
|||||||
"create" | "findProjectGhostUser" | "findOne" | "delete" | "findAllProjectMembers"
|
"create" | "findProjectGhostUser" | "findOne" | "delete" | "findAllProjectMembers"
|
||||||
>;
|
>;
|
||||||
groupProjectDAL: Pick<TGroupProjectDALFactory, "delete">;
|
groupProjectDAL: Pick<TGroupProjectDALFactory, "delete">;
|
||||||
projectSlackConfigDAL: Pick<TProjectSlackConfigDALFactory, "findOne" | "transaction" | "updateById" | "create">;
|
projectSlackConfigDAL: Pick<
|
||||||
|
TProjectSlackConfigDALFactory,
|
||||||
|
"findOne" | "transaction" | "updateById" | "create" | "delete"
|
||||||
|
>;
|
||||||
|
projectMicrosoftTeamsConfigDAL: Pick<
|
||||||
|
TProjectMicrosoftTeamsConfigDALFactory,
|
||||||
|
"findOne" | "transaction" | "updateById" | "create" | "delete"
|
||||||
|
>;
|
||||||
slackIntegrationDAL: Pick<TSlackIntegrationDALFactory, "findById" | "findByIdWithWorkflowIntegrationDetails">;
|
slackIntegrationDAL: Pick<TSlackIntegrationDALFactory, "findById" | "findByIdWithWorkflowIntegrationDetails">;
|
||||||
|
microsoftTeamsIntegrationDAL: Pick<
|
||||||
|
TMicrosoftTeamsIntegrationDALFactory,
|
||||||
|
"findById" | "findByIdWithWorkflowIntegrationDetails"
|
||||||
|
>;
|
||||||
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "create">;
|
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "create">;
|
||||||
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "find">;
|
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "find">;
|
||||||
certificateDAL: Pick<TCertificateDALFactory, "find" | "countCertificatesInProject">;
|
certificateDAL: Pick<TCertificateDALFactory, "find" | "countCertificatesInProject">;
|
||||||
@@ -195,7 +212,9 @@ export const projectServiceFactory = ({
|
|||||||
kmsService,
|
kmsService,
|
||||||
projectBotDAL,
|
projectBotDAL,
|
||||||
projectSlackConfigDAL,
|
projectSlackConfigDAL,
|
||||||
|
projectMicrosoftTeamsConfigDAL,
|
||||||
slackIntegrationDAL,
|
slackIntegrationDAL,
|
||||||
|
microsoftTeamsIntegrationDAL,
|
||||||
projectTemplateService,
|
projectTemplateService,
|
||||||
groupProjectDAL,
|
groupProjectDAL,
|
||||||
smtpService
|
smtpService
|
||||||
@@ -1327,13 +1346,14 @@ export const projectServiceFactory = ({
|
|||||||
return { secretManagerKmsKey: kmsKey };
|
return { secretManagerKmsKey: kmsKey };
|
||||||
};
|
};
|
||||||
|
|
||||||
const getProjectSlackConfig = async ({
|
const getProjectWorkflowIntegrationConfig = async ({
|
||||||
actorId,
|
actorId,
|
||||||
actor,
|
actor,
|
||||||
actorOrgId,
|
actorOrgId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
projectId
|
projectId,
|
||||||
}: TGetProjectSlackConfig) => {
|
integration
|
||||||
|
}: TGetProjectWorkflowIntegrationConfig) => {
|
||||||
const project = await projectDAL.findById(projectId);
|
const project = await projectDAL.findById(projectId);
|
||||||
if (!project) {
|
if (!project) {
|
||||||
throw new NotFoundError({
|
throw new NotFoundError({
|
||||||
@@ -1352,23 +1372,56 @@ export const projectServiceFactory = ({
|
|||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Settings);
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Settings);
|
||||||
|
|
||||||
return projectSlackConfigDAL.findOne({
|
if (integration === WorkflowIntegration.SLACK) {
|
||||||
|
const config = await projectSlackConfigDAL.findOne({
|
||||||
projectId: project.id
|
projectId: project.id
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!config) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `Workflow integration config for project '${projectId}' and integration '${integration}' not found`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...config,
|
||||||
|
integration,
|
||||||
|
integrationId: config.slackIntegrationId
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (integration === WorkflowIntegration.MICROSOFT_TEAMS) {
|
||||||
|
const config = await projectMicrosoftTeamsConfigDAL.findOne({
|
||||||
|
projectId: project.id
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!config) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `Workflow integration config for project '${projectId}' and integration '${integration}' not found`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...config,
|
||||||
|
integration,
|
||||||
|
integrationId: config.microsoftTeamsIntegrationId
|
||||||
|
};
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateProjectSlackConfig = async ({
|
const updateProjectWorkflowIntegration = async ({
|
||||||
actorId,
|
actorId,
|
||||||
actor,
|
actor,
|
||||||
actorOrgId,
|
actorOrgId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
projectId,
|
projectId,
|
||||||
slackIntegrationId,
|
integration,
|
||||||
|
integrationId,
|
||||||
isAccessRequestNotificationEnabled,
|
isAccessRequestNotificationEnabled,
|
||||||
accessRequestChannels,
|
accessRequestChannels,
|
||||||
isSecretRequestNotificationEnabled,
|
isSecretRequestNotificationEnabled,
|
||||||
secretRequestChannels
|
secretRequestChannels
|
||||||
}: TUpdateProjectSlackConfig) => {
|
}: TUpdateProjectWorkflowIntegration) => {
|
||||||
const project = await projectDAL.findById(projectId);
|
const project = await projectDAL.findById(projectId);
|
||||||
if (!project) {
|
if (!project) {
|
||||||
throw new NotFoundError({
|
throw new NotFoundError({
|
||||||
@@ -1376,11 +1429,15 @@ export const projectServiceFactory = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const slackIntegration = await slackIntegrationDAL.findByIdWithWorkflowIntegrationDetails(slackIntegrationId);
|
if (integration === WorkflowIntegration.SLACK) {
|
||||||
|
const sanitizedAccessRequestChannels = validateSlackChannelsField.parse(accessRequestChannels);
|
||||||
|
const sanitizedSecretRequestChannels = validateSlackChannelsField.parse(secretRequestChannels);
|
||||||
|
|
||||||
|
const slackIntegration = await slackIntegrationDAL.findByIdWithWorkflowIntegrationDetails(integrationId);
|
||||||
|
|
||||||
if (!slackIntegration) {
|
if (!slackIntegration) {
|
||||||
throw new NotFoundError({
|
throw new NotFoundError({
|
||||||
message: `Slack integration with ID '${slackIntegrationId}' not found`
|
message: `Slack integration with ID '${integrationId}' not found`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1407,7 +1464,7 @@ export const projectServiceFactory = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return projectSlackConfigDAL.transaction(async (tx) => {
|
const updatedWorkflowIntegration = await projectSlackConfigDAL.transaction(async (tx) => {
|
||||||
const slackConfig = await projectSlackConfigDAL.findOne(
|
const slackConfig = await projectSlackConfigDAL.findOne(
|
||||||
{
|
{
|
||||||
projectId
|
projectId
|
||||||
@@ -1419,11 +1476,11 @@ export const projectServiceFactory = ({
|
|||||||
return projectSlackConfigDAL.updateById(
|
return projectSlackConfigDAL.updateById(
|
||||||
slackConfig.id,
|
slackConfig.id,
|
||||||
{
|
{
|
||||||
slackIntegrationId,
|
slackIntegrationId: integrationId,
|
||||||
isAccessRequestNotificationEnabled,
|
isAccessRequestNotificationEnabled,
|
||||||
accessRequestChannels,
|
accessRequestChannels: sanitizedAccessRequestChannels,
|
||||||
isSecretRequestNotificationEnabled,
|
isSecretRequestNotificationEnabled,
|
||||||
secretRequestChannels
|
secretRequestChannels: sanitizedSecretRequestChannels
|
||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
@@ -1432,15 +1489,152 @@ export const projectServiceFactory = ({
|
|||||||
return projectSlackConfigDAL.create(
|
return projectSlackConfigDAL.create(
|
||||||
{
|
{
|
||||||
projectId,
|
projectId,
|
||||||
slackIntegrationId,
|
slackIntegrationId: integrationId,
|
||||||
isAccessRequestNotificationEnabled,
|
isAccessRequestNotificationEnabled,
|
||||||
accessRequestChannels,
|
accessRequestChannels: sanitizedAccessRequestChannels,
|
||||||
isSecretRequestNotificationEnabled,
|
isSecretRequestNotificationEnabled,
|
||||||
secretRequestChannels
|
secretRequestChannels: sanitizedSecretRequestChannels
|
||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
...updatedWorkflowIntegration,
|
||||||
|
accessRequestChannels: sanitizedAccessRequestChannels,
|
||||||
|
secretRequestChannels: sanitizedSecretRequestChannels,
|
||||||
|
integrationId: slackIntegration.id,
|
||||||
|
integration: WorkflowIntegration.SLACK
|
||||||
|
} as const;
|
||||||
|
}
|
||||||
|
if (integration === WorkflowIntegration.MICROSOFT_TEAMS) {
|
||||||
|
if (isAccessRequestNotificationEnabled && !accessRequestChannels) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Access request channels are required when access request notifications are enabled"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isSecretRequestNotificationEnabled && !secretRequestChannels) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Secret request channels are required when secret request notifications are enabled"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!secretRequestChannels && !accessRequestChannels) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "At least one of access request channels or secret request channels is required"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const sanitizedAccessRequestChannels = MicrosoftTeamsChannelsSchema.parse(accessRequestChannels);
|
||||||
|
const sanitizedSecretRequestChannels = MicrosoftTeamsChannelsSchema.parse(secretRequestChannels);
|
||||||
|
|
||||||
|
const microsoftTeamsIntegration =
|
||||||
|
await microsoftTeamsIntegrationDAL.findByIdWithWorkflowIntegrationDetails(integrationId);
|
||||||
|
|
||||||
|
if (!microsoftTeamsIntegration) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `Microsoft Teams integration with ID '${integrationId}' not found`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedWorkflowIntegration = await projectMicrosoftTeamsConfigDAL.transaction(async (tx) => {
|
||||||
|
const microsoftTeamsConfig = await projectMicrosoftTeamsConfigDAL.findOne(
|
||||||
|
{
|
||||||
|
projectId
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
if (microsoftTeamsConfig) {
|
||||||
|
return projectMicrosoftTeamsConfigDAL.updateById(
|
||||||
|
microsoftTeamsConfig.id,
|
||||||
|
{
|
||||||
|
microsoftTeamsIntegrationId: integrationId,
|
||||||
|
isAccessRequestNotificationEnabled,
|
||||||
|
accessRequestChannels: sanitizedAccessRequestChannels || {},
|
||||||
|
isSecretRequestNotificationEnabled,
|
||||||
|
secretRequestChannels: sanitizedSecretRequestChannels || {}
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return projectMicrosoftTeamsConfigDAL.create(
|
||||||
|
{
|
||||||
|
projectId,
|
||||||
|
microsoftTeamsIntegrationId: integrationId,
|
||||||
|
isAccessRequestNotificationEnabled,
|
||||||
|
accessRequestChannels: sanitizedAccessRequestChannels || {},
|
||||||
|
isSecretRequestNotificationEnabled,
|
||||||
|
secretRequestChannels: sanitizedSecretRequestChannels || {}
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
...updatedWorkflowIntegration,
|
||||||
|
accessRequestChannels: sanitizedAccessRequestChannels,
|
||||||
|
secretRequestChannels: sanitizedSecretRequestChannels,
|
||||||
|
integrationId: microsoftTeamsIntegration.id,
|
||||||
|
integration: WorkflowIntegration.MICROSOFT_TEAMS
|
||||||
|
} as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Integration type '${integration as string}' not supported`
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteProjectWorkflowIntegration = async ({
|
||||||
|
actorId,
|
||||||
|
actor,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
projectId,
|
||||||
|
integrationId,
|
||||||
|
integration
|
||||||
|
}: TDeleteProjectWorkflowIntegration) => {
|
||||||
|
const project = await projectDAL.findById(projectId);
|
||||||
|
if (!project) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `Project with ID '${projectId}' not found`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId,
|
||||||
|
actionProjectType: ActionProjectType.Any
|
||||||
|
});
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Settings);
|
||||||
|
|
||||||
|
if (integration === WorkflowIntegration.SLACK) {
|
||||||
|
const [deletedIntegration] = await projectSlackConfigDAL.delete({
|
||||||
|
projectId,
|
||||||
|
slackIntegrationId: integrationId
|
||||||
|
});
|
||||||
|
|
||||||
|
return deletedIntegration;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (integration === WorkflowIntegration.MICROSOFT_TEAMS) {
|
||||||
|
const [deletedIntegration] = await projectMicrosoftTeamsConfigDAL.delete({
|
||||||
|
projectId,
|
||||||
|
microsoftTeamsIntegrationId: integrationId
|
||||||
|
});
|
||||||
|
|
||||||
|
return deletedIntegration;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Integration with ID '${integrationId}' not found`
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const searchProjects = async ({
|
const searchProjects = async ({
|
||||||
@@ -1548,8 +1742,9 @@ export const projectServiceFactory = ({
|
|||||||
getProjectKmsBackup,
|
getProjectKmsBackup,
|
||||||
loadProjectKmsBackup,
|
loadProjectKmsBackup,
|
||||||
getProjectKmsKeys,
|
getProjectKmsKeys,
|
||||||
getProjectSlackConfig,
|
getProjectWorkflowIntegrationConfig,
|
||||||
updateProjectSlackConfig,
|
updateProjectWorkflowIntegration,
|
||||||
|
deleteProjectWorkflowIntegration,
|
||||||
requestProjectAccess,
|
requestProjectAccess,
|
||||||
searchProjects
|
searchProjects
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
|||||||
import { TProjectSshConfigDALFactory } from "@app/services/project/project-ssh-config-dal";
|
import { TProjectSshConfigDALFactory } from "@app/services/project/project-ssh-config-dal";
|
||||||
|
|
||||||
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
|
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
|
||||||
|
import { WorkflowIntegration } from "../workflow-integration/workflow-integration-types";
|
||||||
|
|
||||||
enum KmsType {
|
enum KmsType {
|
||||||
External = "external",
|
External = "external",
|
||||||
@@ -159,14 +160,22 @@ export type TListProjectSshCertificatesDTO = {
|
|||||||
limit: number;
|
limit: number;
|
||||||
} & TProjectPermission;
|
} & TProjectPermission;
|
||||||
|
|
||||||
export type TGetProjectSlackConfig = TProjectPermission;
|
export type TGetProjectWorkflowIntegrationConfig = TProjectPermission & {
|
||||||
|
integration: WorkflowIntegration;
|
||||||
|
};
|
||||||
|
|
||||||
export type TUpdateProjectSlackConfig = {
|
export type TUpdateProjectWorkflowIntegration = {
|
||||||
slackIntegrationId: string;
|
integrationId: string;
|
||||||
|
integration: WorkflowIntegration;
|
||||||
isAccessRequestNotificationEnabled: boolean;
|
isAccessRequestNotificationEnabled: boolean;
|
||||||
accessRequestChannels: string;
|
accessRequestChannels?: string | { teamId: string; channelIds: string[] };
|
||||||
isSecretRequestNotificationEnabled: boolean;
|
isSecretRequestNotificationEnabled: boolean;
|
||||||
secretRequestChannels: string;
|
secretRequestChannels?: string | { teamId: string; channelIds: string[] };
|
||||||
|
} & TProjectPermission;
|
||||||
|
|
||||||
|
export type TDeleteProjectWorkflowIntegration = {
|
||||||
|
integrationId: string;
|
||||||
|
integration: WorkflowIntegration;
|
||||||
} & TProjectPermission;
|
} & TProjectPermission;
|
||||||
|
|
||||||
export type TBootstrapSshProjectDTO = {
|
export type TBootstrapSshProjectDTO = {
|
||||||
|
|||||||
@@ -3,12 +3,13 @@ import { WebClient } from "@slack/web-api";
|
|||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
|
import { TNotification, TriggerFeature } from "@app/lib/workflow-integrations/types";
|
||||||
|
|
||||||
import { TKmsServiceFactory } from "../kms/kms-service";
|
import { TKmsServiceFactory } from "../kms/kms-service";
|
||||||
import { KmsDataKey } from "../kms/kms-types";
|
import { KmsDataKey } from "../kms/kms-types";
|
||||||
import { TProjectDALFactory } from "../project/project-dal";
|
import { TProjectDALFactory } from "../project/project-dal";
|
||||||
import { TProjectSlackConfigDALFactory } from "./project-slack-config-dal";
|
import { TProjectSlackConfigDALFactory } from "./project-slack-config-dal";
|
||||||
import { SlackTriggerFeature, TSlackNotification } from "./slack-types";
|
import { TSendSlackNotificationDTO } from "./slack-types";
|
||||||
|
|
||||||
export const fetchSlackChannels = async (botKey: string) => {
|
export const fetchSlackChannels = async (botKey: string) => {
|
||||||
const slackChannels: {
|
const slackChannels: {
|
||||||
@@ -41,11 +42,11 @@ export const fetchSlackChannels = async (botKey: string) => {
|
|||||||
return slackChannels;
|
return slackChannels;
|
||||||
};
|
};
|
||||||
|
|
||||||
const buildSlackPayload = (notification: TSlackNotification) => {
|
const buildSlackPayload = (notification: TNotification) => {
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
|
|
||||||
switch (notification.type) {
|
switch (notification.type) {
|
||||||
case SlackTriggerFeature.SECRET_APPROVAL: {
|
case TriggerFeature.SECRET_APPROVAL: {
|
||||||
const { payload } = notification;
|
const { payload } = notification;
|
||||||
const messageBody = `A secret approval request has been opened by ${payload.userEmail}.
|
const messageBody = `A secret approval request has been opened by ${payload.userEmail}.
|
||||||
*Environment*: ${payload.environment}
|
*Environment*: ${payload.environment}
|
||||||
@@ -79,7 +80,7 @@ View the complete details <${appCfg.SITE_URL}/secret-manager/${payload.projectId
|
|||||||
payloadBlocks
|
payloadBlocks
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case SlackTriggerFeature.ACCESS_REQUEST: {
|
case TriggerFeature.ACCESS_REQUEST: {
|
||||||
const { payload } = notification;
|
const { payload } = notification;
|
||||||
const messageBody = `${payload.requesterFullName} (${payload.requesterEmail}) has requested ${
|
const messageBody = `${payload.requesterFullName} (${payload.requesterEmail}) has requested ${
|
||||||
payload.isTemporary ? "temporary" : "permanent"
|
payload.isTemporary ? "temporary" : "permanent"
|
||||||
@@ -125,51 +126,24 @@ User Note: ${payload.note}`
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const triggerSlackNotification = async ({
|
export const sendSlackNotification = async ({
|
||||||
projectId,
|
orgId,
|
||||||
notification,
|
notification,
|
||||||
projectSlackConfigDAL,
|
kmsService,
|
||||||
projectDAL,
|
targetChannelIds,
|
||||||
kmsService
|
slackIntegration
|
||||||
}: {
|
}: TSendSlackNotificationDTO) => {
|
||||||
projectId: string;
|
|
||||||
notification: TSlackNotification;
|
|
||||||
projectSlackConfigDAL: Pick<TProjectSlackConfigDALFactory, "getIntegrationDetailsByProject">;
|
|
||||||
projectDAL: Pick<TProjectDALFactory, "findById">;
|
|
||||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
|
||||||
}) => {
|
|
||||||
const { payloadMessage, payloadBlocks } = buildSlackPayload(notification);
|
|
||||||
const project = await projectDAL.findById(projectId);
|
|
||||||
const slackIntegration = await projectSlackConfigDAL.getIntegrationDetailsByProject(project.id);
|
|
||||||
|
|
||||||
if (!slackIntegration) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let targetChannelIds: string[] = [];
|
|
||||||
if (notification.type === SlackTriggerFeature.ACCESS_REQUEST) {
|
|
||||||
targetChannelIds = slackIntegration.accessRequestChannels?.split(", ") || [];
|
|
||||||
if (!targetChannelIds.length || !slackIntegration.isAccessRequestNotificationEnabled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} else if (notification.type === SlackTriggerFeature.SECRET_APPROVAL) {
|
|
||||||
targetChannelIds = slackIntegration.secretRequestChannels?.split(", ") || [];
|
|
||||||
if (!targetChannelIds.length || !slackIntegration.isSecretRequestNotificationEnabled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const { decryptor: orgDataKeyDecryptor } = await kmsService.createCipherPairWithDataKey({
|
const { decryptor: orgDataKeyDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
type: KmsDataKey.Organization,
|
type: KmsDataKey.Organization,
|
||||||
orgId: project.orgId
|
orgId
|
||||||
});
|
});
|
||||||
|
|
||||||
const botKey = orgDataKeyDecryptor({
|
const botKey = orgDataKeyDecryptor({
|
||||||
cipherTextBlob: slackIntegration.encryptedBotAccessToken
|
cipherTextBlob: slackIntegration.encryptedBotAccessToken
|
||||||
}).toString("utf8");
|
}).toString("utf8");
|
||||||
|
|
||||||
const slackWebClient = new WebClient(botKey);
|
const slackWebClient = new WebClient(botKey);
|
||||||
|
|
||||||
|
const { payloadMessage, payloadBlocks } = buildSlackPayload(notification);
|
||||||
|
|
||||||
for await (const conversationId of targetChannelIds) {
|
for await (const conversationId of targetChannelIds) {
|
||||||
// we send both text and blocks for compatibility with barebone clients
|
// we send both text and blocks for compatibility with barebone clients
|
||||||
await slackWebClient.chat
|
await slackWebClient.chat
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
|
import { TSlackIntegrations } from "@app/db/schemas";
|
||||||
import { TOrgPermission } from "@app/lib/types";
|
import { TOrgPermission } from "@app/lib/types";
|
||||||
|
import { TNotification } from "@app/lib/workflow-integrations/types";
|
||||||
|
|
||||||
|
import { TKmsServiceFactory } from "../kms/kms-service";
|
||||||
|
|
||||||
export type TGetSlackInstallUrlDTO = {
|
export type TGetSlackInstallUrlDTO = {
|
||||||
slug: string;
|
slug: string;
|
||||||
@@ -48,34 +52,10 @@ export type TReinstallSlackIntegrationDTO = {
|
|||||||
slackBotUserId: string;
|
slackBotUserId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export enum SlackTriggerFeature {
|
export type TSendSlackNotificationDTO = {
|
||||||
SECRET_APPROVAL = "secret-approval",
|
orgId: string;
|
||||||
ACCESS_REQUEST = "access-request"
|
notification: TNotification;
|
||||||
}
|
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||||
|
targetChannelIds: string[];
|
||||||
export type TSlackNotification =
|
slackIntegration: TSlackIntegrations;
|
||||||
| {
|
|
||||||
type: SlackTriggerFeature.SECRET_APPROVAL;
|
|
||||||
payload: {
|
|
||||||
userEmail: string;
|
|
||||||
environment: string;
|
|
||||||
secretPath: string;
|
|
||||||
requestId: string;
|
|
||||||
projectId: string;
|
|
||||||
secretKeys: string[];
|
|
||||||
};
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: SlackTriggerFeature.ACCESS_REQUEST;
|
|
||||||
payload: {
|
|
||||||
requesterFullName: string;
|
|
||||||
requesterEmail: string;
|
|
||||||
isTemporary: boolean;
|
|
||||||
secretPath: string;
|
|
||||||
environment: string;
|
|
||||||
projectName: string;
|
|
||||||
permissions: string[];
|
|
||||||
approvalUrl: string;
|
|
||||||
note?: string;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import { KMS_ROOT_CONFIG_UUID } from "../kms/kms-fns";
|
|||||||
import { TKmsRootConfigDALFactory } from "../kms/kms-root-config-dal";
|
import { TKmsRootConfigDALFactory } from "../kms/kms-root-config-dal";
|
||||||
import { TKmsServiceFactory } from "../kms/kms-service";
|
import { TKmsServiceFactory } from "../kms/kms-service";
|
||||||
import { RootKeyEncryptionStrategy } from "../kms/kms-types";
|
import { RootKeyEncryptionStrategy } from "../kms/kms-types";
|
||||||
|
import { TMicrosoftTeamsServiceFactory } from "../microsoft-teams/microsoft-teams-service";
|
||||||
import { TOrgServiceFactory } from "../org/org-service";
|
import { TOrgServiceFactory } from "../org/org-service";
|
||||||
import { TUserDALFactory } from "../user/user-dal";
|
import { TUserDALFactory } from "../user/user-dal";
|
||||||
import { TUserAliasDALFactory } from "../user-alias/user-alias-dal";
|
import { TUserAliasDALFactory } from "../user-alias/user-alias-dal";
|
||||||
@@ -47,6 +48,7 @@ type TSuperAdminServiceFactoryDep = {
|
|||||||
orgService: Pick<TOrgServiceFactory, "createOrganization">;
|
orgService: Pick<TOrgServiceFactory, "createOrganization">;
|
||||||
keyStore: Pick<TKeyStoreFactory, "getItem" | "setItemWithExpiry" | "deleteItem">;
|
keyStore: Pick<TKeyStoreFactory, "getItem" | "setItemWithExpiry" | "deleteItem">;
|
||||||
licenseService: Pick<TLicenseServiceFactory, "onPremFeatures">;
|
licenseService: Pick<TLicenseServiceFactory, "onPremFeatures">;
|
||||||
|
microsoftTeamsService: Pick<TMicrosoftTeamsServiceFactory, "initializeTeamsBot">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TSuperAdminServiceFactory = ReturnType<typeof superAdminServiceFactory>;
|
export type TSuperAdminServiceFactory = ReturnType<typeof superAdminServiceFactory>;
|
||||||
@@ -77,7 +79,8 @@ export const superAdminServiceFactory = ({
|
|||||||
licenseService,
|
licenseService,
|
||||||
identityAccessTokenDAL,
|
identityAccessTokenDAL,
|
||||||
identityTokenAuthDAL,
|
identityTokenAuthDAL,
|
||||||
identityOrgMembershipDAL
|
identityOrgMembershipDAL,
|
||||||
|
microsoftTeamsService
|
||||||
}: TSuperAdminServiceFactoryDep) => {
|
}: TSuperAdminServiceFactoryDep) => {
|
||||||
const initServerCfg = async () => {
|
const initServerCfg = async () => {
|
||||||
// TODO(akhilmhdh): bad pattern time less change this later to me itself
|
// TODO(akhilmhdh): bad pattern time less change this later to me itself
|
||||||
@@ -125,7 +128,13 @@ export const superAdminServiceFactory = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const updateServerCfg = async (
|
const updateServerCfg = async (
|
||||||
data: TSuperAdminUpdate & { slackClientId?: string; slackClientSecret?: string },
|
data: TSuperAdminUpdate & {
|
||||||
|
slackClientId?: string;
|
||||||
|
slackClientSecret?: string;
|
||||||
|
microsoftTeamsAppId?: string;
|
||||||
|
microsoftTeamsClientSecret?: string;
|
||||||
|
microsoftTeamsBotId?: string;
|
||||||
|
},
|
||||||
userId: string
|
userId: string
|
||||||
) => {
|
) => {
|
||||||
const updatedData = data;
|
const updatedData = data;
|
||||||
@@ -192,10 +201,46 @@ export const superAdminServiceFactory = ({
|
|||||||
updatedData.slackClientSecret = undefined;
|
updatedData.slackClientSecret = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (data.microsoftTeamsAppId) {
|
||||||
|
const encryptedClientId = encryptWithRoot(Buffer.from(data.microsoftTeamsAppId));
|
||||||
|
|
||||||
|
updatedData.encryptedMicrosoftTeamsAppId = encryptedClientId;
|
||||||
|
updatedData.microsoftTeamsAppId = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.microsoftTeamsClientSecret) {
|
||||||
|
const encryptedClientSecret = encryptWithRoot(Buffer.from(data.microsoftTeamsClientSecret));
|
||||||
|
|
||||||
|
updatedData.encryptedMicrosoftTeamsClientSecret = encryptedClientSecret;
|
||||||
|
updatedData.microsoftTeamsClientSecret = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.microsoftTeamsBotId) {
|
||||||
|
const encryptedBotId = encryptWithRoot(Buffer.from(data.microsoftTeamsBotId));
|
||||||
|
|
||||||
|
updatedData.encryptedMicrosoftTeamsBotId = encryptedBotId;
|
||||||
|
updatedData.microsoftTeamsBotId = undefined;
|
||||||
|
}
|
||||||
const updatedServerCfg = await serverCfgDAL.updateById(ADMIN_CONFIG_DB_UUID, updatedData);
|
const updatedServerCfg = await serverCfgDAL.updateById(ADMIN_CONFIG_DB_UUID, updatedData);
|
||||||
|
|
||||||
await keyStore.setItemWithExpiry(ADMIN_CONFIG_KEY, ADMIN_CONFIG_KEY_EXP, JSON.stringify(updatedServerCfg));
|
await keyStore.setItemWithExpiry(ADMIN_CONFIG_KEY, ADMIN_CONFIG_KEY_EXP, JSON.stringify(updatedServerCfg));
|
||||||
|
|
||||||
|
if (
|
||||||
|
updatedServerCfg.encryptedMicrosoftTeamsAppId &&
|
||||||
|
updatedServerCfg.encryptedMicrosoftTeamsClientSecret &&
|
||||||
|
updatedServerCfg.encryptedMicrosoftTeamsBotId
|
||||||
|
) {
|
||||||
|
const decryptWithRoot = kmsService.decryptWithRootKey();
|
||||||
|
decryptWithRoot(updatedServerCfg.encryptedMicrosoftTeamsBotId); // validate that we're able to decrypt the bot ID
|
||||||
|
const decryptedAppId = decryptWithRoot(updatedServerCfg.encryptedMicrosoftTeamsAppId);
|
||||||
|
const decryptedAppPassword = decryptWithRoot(updatedServerCfg.encryptedMicrosoftTeamsClientSecret);
|
||||||
|
|
||||||
|
await microsoftTeamsService.initializeTeamsBot({
|
||||||
|
botAppId: decryptedAppId.toString(),
|
||||||
|
botAppPassword: decryptedAppPassword.toString()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return updatedServerCfg;
|
return updatedServerCfg;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -488,29 +533,40 @@ export const superAdminServiceFactory = ({
|
|||||||
await userDAL.updateById(userId, { superAdmin: true });
|
await userDAL.updateById(userId, { superAdmin: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
const getAdminSlackConfig = async () => {
|
const getAdminIntegrationsConfig = async () => {
|
||||||
const serverCfg = await serverCfgDAL.findById(ADMIN_CONFIG_DB_UUID);
|
const serverCfg = await serverCfgDAL.findById(ADMIN_CONFIG_DB_UUID);
|
||||||
|
|
||||||
if (!serverCfg) {
|
if (!serverCfg) {
|
||||||
throw new NotFoundError({ name: "AdminConfig", message: "Admin config not found" });
|
throw new NotFoundError({ name: "AdminConfig", message: "Admin config not found" });
|
||||||
}
|
}
|
||||||
|
|
||||||
let clientId = "";
|
|
||||||
let clientSecret = "";
|
|
||||||
|
|
||||||
const decrypt = kmsService.decryptWithRootKey();
|
const decrypt = kmsService.decryptWithRootKey();
|
||||||
|
|
||||||
if (serverCfg.encryptedSlackClientId) {
|
const slackClientId = serverCfg.encryptedSlackClientId ? decrypt(serverCfg.encryptedSlackClientId).toString() : "";
|
||||||
clientId = decrypt(serverCfg.encryptedSlackClientId).toString();
|
const slackClientSecret = serverCfg.encryptedSlackClientSecret
|
||||||
}
|
? decrypt(serverCfg.encryptedSlackClientSecret).toString()
|
||||||
|
: "";
|
||||||
|
|
||||||
if (serverCfg.encryptedSlackClientSecret) {
|
const microsoftAppId = serverCfg.encryptedMicrosoftTeamsAppId
|
||||||
clientSecret = decrypt(serverCfg.encryptedSlackClientSecret).toString();
|
? decrypt(serverCfg.encryptedMicrosoftTeamsAppId).toString()
|
||||||
}
|
: "";
|
||||||
|
const microsoftClientSecret = serverCfg.encryptedMicrosoftTeamsClientSecret
|
||||||
|
? decrypt(serverCfg.encryptedMicrosoftTeamsClientSecret).toString()
|
||||||
|
: "";
|
||||||
|
const microsoftBotId = serverCfg.encryptedMicrosoftTeamsBotId
|
||||||
|
? decrypt(serverCfg.encryptedMicrosoftTeamsBotId).toString()
|
||||||
|
: "";
|
||||||
|
|
||||||
return {
|
return {
|
||||||
clientId,
|
slack: {
|
||||||
clientSecret
|
clientSecret: slackClientSecret,
|
||||||
|
clientId: slackClientId
|
||||||
|
},
|
||||||
|
microsoftTeams: {
|
||||||
|
appId: microsoftAppId,
|
||||||
|
clientSecret: microsoftClientSecret,
|
||||||
|
botId: microsoftBotId
|
||||||
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -578,7 +634,7 @@ export const superAdminServiceFactory = ({
|
|||||||
getUsers,
|
getUsers,
|
||||||
deleteUser,
|
deleteUser,
|
||||||
getIdentities,
|
getIdentities,
|
||||||
getAdminSlackConfig,
|
getAdminIntegrationsConfig,
|
||||||
updateRootEncryptionStrategy,
|
updateRootEncryptionStrategy,
|
||||||
getConfiguredEncryptionStrategies,
|
getConfiguredEncryptionStrategies,
|
||||||
grantServerAdminAccessToUser,
|
grantServerAdminAccessToUser,
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
import { TOrgPermission } from "@app/lib/types";
|
import { TOrgPermission } from "@app/lib/types";
|
||||||
|
|
||||||
export enum WorkflowIntegration {
|
export enum WorkflowIntegration {
|
||||||
SLACK = "slack"
|
SLACK = "slack",
|
||||||
|
MICROSOFT_TEAMS = "microsoft-teams"
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum WorkflowIntegrationStatus {
|
||||||
|
PENDING = "pending",
|
||||||
|
INSTALLED = "installed",
|
||||||
|
FAILED = "failed"
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TGetWorkflowIntegrationsByOrg = Omit<TOrgPermission, "orgId">;
|
export type TGetWorkflowIntegrationsByOrg = Omit<TOrgPermission, "orgId">;
|
||||||
|
|||||||
@@ -47,8 +47,10 @@ AccordionTrigger.displayName = "AccordionTrigger";
|
|||||||
|
|
||||||
export const AccordionContent = forwardRef<
|
export const AccordionContent = forwardRef<
|
||||||
HTMLDivElement,
|
HTMLDivElement,
|
||||||
AccordionPrimitive.AccordionContentProps
|
AccordionPrimitive.AccordionContentProps & {
|
||||||
>(({ children, className, ...props }, forwardedRef) => (
|
childrenClassName?: string;
|
||||||
|
}
|
||||||
|
>(({ children, className, childrenClassName, ...props }, forwardedRef) => (
|
||||||
<AccordionPrimitive.Content
|
<AccordionPrimitive.Content
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
"overflow-hidden data-[state=closed]:animate-slideUp data-[state=open]:animate-slideDown",
|
"overflow-hidden data-[state=closed]:animate-slideUp data-[state=open]:animate-slideDown",
|
||||||
@@ -57,7 +59,7 @@ export const AccordionContent = forwardRef<
|
|||||||
{...props}
|
{...props}
|
||||||
ref={forwardedRef}
|
ref={forwardedRef}
|
||||||
>
|
>
|
||||||
<div className="px-4 py-2 text-sm">{children}</div>
|
<div className={twMerge("px-4 py-2 text-sm", childrenClassName)}>{children}</div>
|
||||||
</AccordionPrimitive.Content>
|
</AccordionPrimitive.Content>
|
||||||
));
|
));
|
||||||
|
|
||||||
|
|||||||
@@ -4,13 +4,12 @@ export {
|
|||||||
useAdminRemoveIdentitySuperAdminAccess,
|
useAdminRemoveIdentitySuperAdminAccess,
|
||||||
useCreateAdminUser,
|
useCreateAdminUser,
|
||||||
useRemoveUserServerAdminAccess,
|
useRemoveUserServerAdminAccess,
|
||||||
useUpdateAdminSlackConfig,
|
|
||||||
useUpdateServerConfig,
|
useUpdateServerConfig,
|
||||||
useUpdateServerEncryptionStrategy
|
useUpdateServerEncryptionStrategy
|
||||||
} from "./mutation";
|
} from "./mutation";
|
||||||
export {
|
export {
|
||||||
useAdminGetUsers,
|
useAdminGetUsers,
|
||||||
useGetAdminSlackConfig,
|
useGetAdminIntegrationsConfig,
|
||||||
useGetServerConfig,
|
useGetServerConfig,
|
||||||
useGetServerRootKmsEncryptionDetails
|
useGetServerRootKmsEncryptionDetails
|
||||||
} from "./queries";
|
} from "./queries";
|
||||||
|
|||||||
@@ -6,11 +6,10 @@ import { organizationKeys } from "../organization/queries";
|
|||||||
import { User } from "../users/types";
|
import { User } from "../users/types";
|
||||||
import { adminQueryKeys, adminStandaloneKeys } from "./queries";
|
import { adminQueryKeys, adminStandaloneKeys } from "./queries";
|
||||||
import {
|
import {
|
||||||
AdminSlackConfig,
|
|
||||||
RootKeyEncryptionStrategy,
|
RootKeyEncryptionStrategy,
|
||||||
TCreateAdminUserDTO,
|
TCreateAdminUserDTO,
|
||||||
TServerConfig,
|
TServerConfig,
|
||||||
TUpdateAdminSlackConfigDTO
|
TUpdateServerConfigDTO
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
export const useCreateAdminUser = () => {
|
export const useCreateAdminUser = () => {
|
||||||
@@ -34,11 +33,7 @@ export const useCreateAdminUser = () => {
|
|||||||
export const useUpdateServerConfig = () => {
|
export const useUpdateServerConfig = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
return useMutation<
|
return useMutation<TServerConfig, object, TUpdateServerConfigDTO>({
|
||||||
TServerConfig,
|
|
||||||
object,
|
|
||||||
Partial<TServerConfig & { slackClientId: string; slackClientSecret: string }>
|
|
||||||
>({
|
|
||||||
mutationFn: async (opt) => {
|
mutationFn: async (opt) => {
|
||||||
const { data } = await apiRequest.patch<{ config: TServerConfig }>(
|
const { data } = await apiRequest.patch<{ config: TServerConfig }>(
|
||||||
"/api/v1/admin/config",
|
"/api/v1/admin/config",
|
||||||
@@ -48,6 +43,7 @@ export const useUpdateServerConfig = () => {
|
|||||||
},
|
},
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
queryClient.setQueryData(adminQueryKeys.serverConfig(), data);
|
queryClient.setQueryData(adminQueryKeys.serverConfig(), data);
|
||||||
|
queryClient.invalidateQueries({ queryKey: adminQueryKeys.getAdminIntegrationsConfig() });
|
||||||
queryClient.invalidateQueries({ queryKey: adminQueryKeys.serverConfig() });
|
queryClient.invalidateQueries({ queryKey: adminQueryKeys.serverConfig() });
|
||||||
queryClient.invalidateQueries({ queryKey: organizationKeys.getUserOrganizations });
|
queryClient.invalidateQueries({ queryKey: organizationKeys.getUserOrganizations });
|
||||||
}
|
}
|
||||||
@@ -119,23 +115,6 @@ export const useAdminGrantServerAdminAccess = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useUpdateAdminSlackConfig = () => {
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
return useMutation<AdminSlackConfig, object, TUpdateAdminSlackConfigDTO>({
|
|
||||||
mutationFn: async (dto) => {
|
|
||||||
const { data } = await apiRequest.put<AdminSlackConfig>(
|
|
||||||
"/api/v1/admin/integrations/slack/config",
|
|
||||||
dto
|
|
||||||
);
|
|
||||||
|
|
||||||
return data;
|
|
||||||
},
|
|
||||||
onSuccess: () => {
|
|
||||||
queryClient.invalidateQueries({ queryKey: adminQueryKeys.getAdminSlackConfig() });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useUpdateServerEncryptionStrategy = () => {
|
export const useUpdateServerEncryptionStrategy = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation({
|
return useMutation({
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { User } from "../types";
|
|||||||
import {
|
import {
|
||||||
AdminGetIdentitiesFilters,
|
AdminGetIdentitiesFilters,
|
||||||
AdminGetUsersFilters,
|
AdminGetUsersFilters,
|
||||||
AdminSlackConfig,
|
AdminIntegrationsConfig,
|
||||||
TGetServerRootKmsEncryptionDetails,
|
TGetServerRootKmsEncryptionDetails,
|
||||||
TServerConfig
|
TServerConfig
|
||||||
} from "./types";
|
} from "./types";
|
||||||
@@ -22,7 +22,7 @@ export const adminQueryKeys = {
|
|||||||
getUsers: (filters: AdminGetUsersFilters) => [adminStandaloneKeys.getUsers, { filters }] as const,
|
getUsers: (filters: AdminGetUsersFilters) => [adminStandaloneKeys.getUsers, { filters }] as const,
|
||||||
getIdentities: (filters: AdminGetIdentitiesFilters) =>
|
getIdentities: (filters: AdminGetIdentitiesFilters) =>
|
||||||
[adminStandaloneKeys.getIdentities, { filters }] as const,
|
[adminStandaloneKeys.getIdentities, { filters }] as const,
|
||||||
getAdminSlackConfig: () => ["admin-slack-config"] as const,
|
getAdminIntegrationsConfig: () => ["admin-integrations-config"] as const,
|
||||||
getServerEncryptionStrategies: () => ["server-encryption-strategies"] as const
|
getServerEncryptionStrategies: () => ["server-encryption-strategies"] as const
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -95,13 +95,11 @@ export const useAdminGetIdentities = (filters: AdminGetIdentitiesFilters) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useGetAdminSlackConfig = () => {
|
export const useGetAdminIntegrationsConfig = () => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: adminQueryKeys.getAdminSlackConfig(),
|
queryKey: adminQueryKeys.getAdminIntegrationsConfig(),
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const { data } = await apiRequest.get<AdminSlackConfig>(
|
const { data } = await apiRequest.get<AdminIntegrationsConfig>("/api/v1/admin/integrations");
|
||||||
"/api/v1/admin/integrations/slack/config"
|
|
||||||
);
|
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,14 @@ export type TServerConfig = {
|
|||||||
pageFrameContent?: string;
|
pageFrameContent?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TUpdateServerConfigDTO = {
|
||||||
|
slackClientId?: string;
|
||||||
|
slackClientSecret?: string;
|
||||||
|
microsoftTeamsAppId?: string;
|
||||||
|
microsoftTeamsClientSecret?: string;
|
||||||
|
microsoftTeamsBotId?: string;
|
||||||
|
} & Partial<TServerConfig>;
|
||||||
|
|
||||||
export type TCreateAdminUserDTO = {
|
export type TCreateAdminUserDTO = {
|
||||||
email: string;
|
email: string;
|
||||||
password: string;
|
password: string;
|
||||||
@@ -42,11 +50,6 @@ export type TCreateAdminUserDTO = {
|
|||||||
salt: string;
|
salt: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TUpdateAdminSlackConfigDTO = {
|
|
||||||
clientId: string;
|
|
||||||
clientSecret: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type AdminGetUsersFilters = {
|
export type AdminGetUsersFilters = {
|
||||||
limit: number;
|
limit: number;
|
||||||
searchTerm: string;
|
searchTerm: string;
|
||||||
@@ -58,10 +61,17 @@ export type AdminGetIdentitiesFilters = {
|
|||||||
searchTerm: string;
|
searchTerm: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AdminSlackConfig = {
|
export type AdminIntegrationsConfig = {
|
||||||
|
slack: {
|
||||||
clientId: string;
|
clientId: string;
|
||||||
clientSecret: string;
|
clientSecret: string;
|
||||||
};
|
};
|
||||||
|
microsoftTeams: {
|
||||||
|
appId: string;
|
||||||
|
clientSecret: string;
|
||||||
|
botId: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export type TGetServerRootKmsEncryptionDetails = {
|
export type TGetServerRootKmsEncryptionDetails = {
|
||||||
strategies: {
|
strategies: {
|
||||||
|
|||||||
@@ -93,8 +93,10 @@ export const eventToNameMap: { [K in EventType]: string } = {
|
|||||||
"Create certificate template EST configuration",
|
"Create certificate template EST configuration",
|
||||||
[EventType.UPDATE_CERTIFICATE_TEMPLATE_EST_CONFIG]:
|
[EventType.UPDATE_CERTIFICATE_TEMPLATE_EST_CONFIG]:
|
||||||
"Update certificate template EST configuration",
|
"Update certificate template EST configuration",
|
||||||
[EventType.UPDATE_PROJECT_SLACK_CONFIG]: "Update project slack configuration",
|
[EventType.UPDATE_PROJECT_WORKFLOW_INTEGRATION_CONFIG]:
|
||||||
[EventType.GET_PROJECT_SLACK_CONFIG]: "Get project slack configuration",
|
"Update project workflow integration configuration",
|
||||||
|
[EventType.GET_PROJECT_WORKFLOW_INTEGRATION_CONFIG]:
|
||||||
|
"Get project workflow integration configuration",
|
||||||
[EventType.INTEGRATION_SYNCED]: "Integration sync",
|
[EventType.INTEGRATION_SYNCED]: "Integration sync",
|
||||||
[EventType.CREATE_SHARED_SECRET]: "Create shared secret",
|
[EventType.CREATE_SHARED_SECRET]: "Create shared secret",
|
||||||
[EventType.DELETE_SHARED_SECRET]: "Delete shared secret",
|
[EventType.DELETE_SHARED_SECRET]: "Delete shared secret",
|
||||||
|
|||||||
@@ -97,8 +97,8 @@ export enum EventType {
|
|||||||
CREATE_CERTIFICATE_TEMPLATE_EST_CONFIG = "create-certificate-template-est-config",
|
CREATE_CERTIFICATE_TEMPLATE_EST_CONFIG = "create-certificate-template-est-config",
|
||||||
UPDATE_CERTIFICATE_TEMPLATE_EST_CONFIG = "update-certificate-template-est-config",
|
UPDATE_CERTIFICATE_TEMPLATE_EST_CONFIG = "update-certificate-template-est-config",
|
||||||
GET_CERTIFICATE_TEMPLATE_EST_CONFIG = "get-certificate-template-est-config",
|
GET_CERTIFICATE_TEMPLATE_EST_CONFIG = "get-certificate-template-est-config",
|
||||||
UPDATE_PROJECT_SLACK_CONFIG = "update-project-slack-config",
|
UPDATE_PROJECT_WORKFLOW_INTEGRATION_CONFIG = "update-project-workflow-integration-config",
|
||||||
GET_PROJECT_SLACK_CONFIG = "get-project-slack-config",
|
GET_PROJECT_WORKFLOW_INTEGRATION_CONFIG = "get-project-workflow-integration-config",
|
||||||
INTEGRATION_SYNCED = "integration-synced",
|
INTEGRATION_SYNCED = "integration-synced",
|
||||||
CREATE_SHARED_SECRET = "create-shared-secret",
|
CREATE_SHARED_SECRET = "create-shared-secret",
|
||||||
DELETE_SHARED_SECRET = "delete-shared-secret",
|
DELETE_SHARED_SECRET = "delete-shared-secret",
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { CaStatus } from "../ca";
|
import { CaStatus } from "../ca";
|
||||||
import { IdentityTrustedIp } from "../identities/types";
|
import { IdentityTrustedIp } from "../identities/types";
|
||||||
import { PkiItemType } from "../pkiCollections/constants";
|
import { PkiItemType } from "../pkiCollections/constants";
|
||||||
|
import { WorkflowIntegration } from "../workflowIntegrations/types";
|
||||||
import { ActorType, EventType, UserAgentType } from "./enums";
|
import { ActorType, EventType, UserAgentType } from "./enums";
|
||||||
|
|
||||||
export type TGetAuditLogsFilter = {
|
export type TGetAuditLogsFilter = {
|
||||||
@@ -781,11 +782,12 @@ interface GetCertificateTemplateEstConfig {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UpdateProjectSlackConfig {
|
interface UpdateProjectWorkflowIntegrationConfig {
|
||||||
type: EventType.UPDATE_PROJECT_SLACK_CONFIG;
|
type: EventType.UPDATE_PROJECT_WORKFLOW_INTEGRATION_CONFIG;
|
||||||
metadata: {
|
metadata: {
|
||||||
id: string;
|
id: string;
|
||||||
slackIntegrationId: string;
|
integrationId: string;
|
||||||
|
integration: WorkflowIntegration;
|
||||||
isAccessRequestNotificationEnabled: boolean;
|
isAccessRequestNotificationEnabled: boolean;
|
||||||
accessRequestChannels: string;
|
accessRequestChannels: string;
|
||||||
isSecretRequestNotificationEnabled: boolean;
|
isSecretRequestNotificationEnabled: boolean;
|
||||||
@@ -793,10 +795,11 @@ interface UpdateProjectSlackConfig {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
interface GetProjectSlackConfig {
|
interface GetProjectWorkflowIntegrationConfig {
|
||||||
type: EventType.GET_PROJECT_SLACK_CONFIG;
|
type: EventType.GET_PROJECT_WORKFLOW_INTEGRATION_CONFIG;
|
||||||
metadata: {
|
metadata: {
|
||||||
id: string;
|
id: string;
|
||||||
|
integration: WorkflowIntegration;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -892,8 +895,8 @@ export type Event =
|
|||||||
| UpdateCertificateTemplateEstConfig
|
| UpdateCertificateTemplateEstConfig
|
||||||
| CreateCertificateTemplateEstConfig
|
| CreateCertificateTemplateEstConfig
|
||||||
| GetCertificateTemplateEstConfig
|
| GetCertificateTemplateEstConfig
|
||||||
| UpdateProjectSlackConfig
|
| UpdateProjectWorkflowIntegrationConfig
|
||||||
| GetProjectSlackConfig
|
| GetProjectWorkflowIntegrationConfig
|
||||||
| IntegrationSyncedEvent;
|
| IntegrationSyncedEvent;
|
||||||
|
|
||||||
export type AuditLog = {
|
export type AuditLog = {
|
||||||
|
|||||||
@@ -1,13 +1,2 @@
|
|||||||
export {
|
export * from "./mutation";
|
||||||
useDeleteSlackIntegration,
|
export * from "./queries";
|
||||||
useUpdateProjectSlackConfig,
|
|
||||||
useUpdateSlackIntegration
|
|
||||||
} from "./mutation";
|
|
||||||
export {
|
|
||||||
fetchSlackInstallUrl,
|
|
||||||
fetchSlackReinstallUrl,
|
|
||||||
useGetSlackIntegrationById,
|
|
||||||
useGetSlackIntegrationChannels,
|
|
||||||
useGetSlackIntegrations,
|
|
||||||
useGetWorkflowIntegrations
|
|
||||||
} from "./queries";
|
|
||||||
|
|||||||
@@ -5,8 +5,13 @@ import { apiRequest } from "@app/config/request";
|
|||||||
import { workspaceKeys } from "../workspace/query-keys";
|
import { workspaceKeys } from "../workspace/query-keys";
|
||||||
import { workflowIntegrationKeys } from "./queries";
|
import { workflowIntegrationKeys } from "./queries";
|
||||||
import {
|
import {
|
||||||
|
TCheckMicrosoftTeamsIntegrationInstallationStatusDTO,
|
||||||
|
TCreateMicrosoftTeamsIntegrationDTO,
|
||||||
|
TDeleteMicrosoftTeamsIntegrationIntegrationDTO,
|
||||||
|
TDeleteProjectWorkflowIntegrationDTO,
|
||||||
TDeleteSlackIntegrationDTO,
|
TDeleteSlackIntegrationDTO,
|
||||||
TUpdateProjectSlackConfigDTO,
|
TUpdateMicrosoftTeamsIntegrationDTO,
|
||||||
|
TUpdateProjectWorkflowIntegrationConfigDTO,
|
||||||
TUpdateSlackIntegrationDTO
|
TUpdateSlackIntegrationDTO
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
@@ -28,6 +33,47 @@ export const useUpdateSlackIntegration = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useUpdateMicrosoftTeamsIntegration = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation<object, object, TUpdateMicrosoftTeamsIntegrationDTO>({
|
||||||
|
mutationFn: async (dto) => {
|
||||||
|
const { data } = await apiRequest.patch(
|
||||||
|
`/api/v1/workflow-integrations/microsoft-teams/${dto.id}`,
|
||||||
|
dto
|
||||||
|
);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
onSuccess: (_, { orgId, id }) => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: workflowIntegrationKeys.getMicrosoftTeamsIntegration(id)
|
||||||
|
});
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: workflowIntegrationKeys.getMicrosoftTeamsIntegrations(orgId)
|
||||||
|
});
|
||||||
|
queryClient.invalidateQueries({ queryKey: workflowIntegrationKeys.getIntegrations(orgId) });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
export const useCreateMicrosoftTeamsIntegration = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation<object, object, TCreateMicrosoftTeamsIntegrationDTO>({
|
||||||
|
mutationFn: async (dto) => {
|
||||||
|
const { data } = await apiRequest.post("/api/v1/workflow-integrations/microsoft-teams", dto);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
onSuccess: (_, { orgId }) => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: workflowIntegrationKeys.getMicrosoftTeamsIntegrations(orgId)
|
||||||
|
});
|
||||||
|
queryClient.invalidateQueries({ queryKey: workflowIntegrationKeys.getIntegrations(orgId) });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const useDeleteSlackIntegration = () => {
|
export const useDeleteSlackIntegration = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
@@ -44,21 +90,86 @@ export const useDeleteSlackIntegration = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useUpdateProjectSlackConfig = () => {
|
export const useDeleteMicrosoftTeamsIntegration = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation<object, object, TDeleteMicrosoftTeamsIntegrationIntegrationDTO>({
|
||||||
|
mutationFn: async (dto) => {
|
||||||
|
const { data } = await apiRequest.delete(
|
||||||
|
`/api/v1/workflow-integrations/microsoft-teams/${dto.id}`
|
||||||
|
);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
onSuccess: (_, { orgId, id }) => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: workflowIntegrationKeys.getMicrosoftTeamsIntegration(id)
|
||||||
|
});
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: workflowIntegrationKeys.getMicrosoftTeamsIntegrations(orgId)
|
||||||
|
});
|
||||||
|
queryClient.invalidateQueries({ queryKey: workflowIntegrationKeys.getIntegrations(orgId) });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useUpdateProjectWorkflowIntegrationConfig = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: async (dto: TUpdateProjectSlackConfigDTO) => {
|
mutationFn: async (dto: TUpdateProjectWorkflowIntegrationConfigDTO) => {
|
||||||
const { data } = await apiRequest.put(
|
const { data } = await apiRequest.put(
|
||||||
`/api/v1/workspace/${dto.workspaceId}/slack-config`,
|
`/api/v1/workspace/${dto.workspaceId}/workflow-integration/${dto.integration}`,
|
||||||
dto
|
dto
|
||||||
);
|
);
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
onSuccess: (_, { workspaceId }) => {
|
onSuccess: (_, { workspaceId, integration }) => {
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: workspaceKeys.getWorkspaceSlackConfig(workspaceId)
|
queryKey: workspaceKeys.getWorkspaceWorkflowIntegrationConfig(workspaceId, integration)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useDeleteProjectWorkflowIntegration = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async (dto: TDeleteProjectWorkflowIntegrationDTO) => {
|
||||||
|
const { data } = await apiRequest.delete(
|
||||||
|
`/api/v1/workspace/${dto.projectId}/workflow-integrations/${dto.integration}/${dto.integrationId}`
|
||||||
|
);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
onSuccess: (_, { projectId, integration }) => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: workspaceKeys.getWorkspaceWorkflowIntegrationConfig(projectId, integration)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useCheckMicrosoftTeamsIntegrationInstallationStatus = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async (dto: TCheckMicrosoftTeamsIntegrationInstallationStatusDTO) => {
|
||||||
|
const { data } = await apiRequest.post(
|
||||||
|
`/api/v1/workflow-integrations/microsoft-teams/${dto.workflowIntegrationId}/installation-status`
|
||||||
|
);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
onSuccess: (_, { workflowIntegrationId, orgId }) => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: workflowIntegrationKeys.getMicrosoftTeamsIntegration(workflowIntegrationId)
|
||||||
|
});
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: workflowIntegrationKeys.getMicrosoftTeamsIntegrations(orgId)
|
||||||
|
});
|
||||||
|
queryClient.invalidateQueries({ queryKey: workflowIntegrationKeys.getIntegrations(orgId) });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -2,12 +2,27 @@ import { useQuery } from "@tanstack/react-query";
|
|||||||
|
|
||||||
import { apiRequest } from "@app/config/request";
|
import { apiRequest } from "@app/config/request";
|
||||||
|
|
||||||
import { SlackIntegration, SlackIntegrationChannel, WorkflowIntegration } from "./types";
|
import {
|
||||||
|
MicrosoftTeamsIntegration,
|
||||||
|
MicrosoftTeamsIntegrationTeam,
|
||||||
|
SlackIntegration,
|
||||||
|
SlackIntegrationChannel,
|
||||||
|
WorkflowIntegration
|
||||||
|
} from "./types";
|
||||||
|
|
||||||
export const workflowIntegrationKeys = {
|
export const workflowIntegrationKeys = {
|
||||||
getIntegrations: (orgId?: string) => [{ orgId }, "workflow-integrations"],
|
getIntegrations: (orgId?: string) => [{ orgId }, "workflow-integrations"],
|
||||||
getSlackIntegrations: (orgId?: string) => [{ orgId }, "slack-workflow-integrations"],
|
getSlackIntegrations: (orgId?: string) => [{ orgId }, "slack-workflow-integrations"],
|
||||||
getSlackIntegration: (id?: string) => [{ id }, "slack-workflow-integration"],
|
getSlackIntegration: (id?: string) => [{ id }, "slack-workflow-integration"],
|
||||||
|
getMicrosoftTeamsIntegrations: (orgId?: string) => [
|
||||||
|
{ orgId },
|
||||||
|
"microsoft-teams-workflow-integrations"
|
||||||
|
],
|
||||||
|
getMicrosoftTeamsIntegration: (id?: string) => [{ id }, "microsoft-teams-workflow-integration"],
|
||||||
|
getMicrosoftTeamsIntegrationTeams: (id?: string) => [
|
||||||
|
{ id },
|
||||||
|
"microsoft-teams-workflow-integration-teams"
|
||||||
|
],
|
||||||
getSlackIntegrationChannels: (id?: string) => [{ id }, "slack-workflow-integration-channels"]
|
getSlackIntegrationChannels: (id?: string) => [{ id }, "slack-workflow-integration-channels"]
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -66,13 +81,50 @@ export const fetchWorkflowIntegrations = async () => {
|
|||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useGetSlackIntegrations = (orgId?: string) =>
|
export const fetchMicrosoftTeamsIntegrations = async () => {
|
||||||
|
const { data } = await apiRequest.get<MicrosoftTeamsIntegration[]>(
|
||||||
|
"/api/v1/workflow-integrations/microsoft-teams"
|
||||||
|
);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchMicrosoftTeamsIntegrationById = async (id?: string) => {
|
||||||
|
const { data } = await apiRequest.get<MicrosoftTeamsIntegration>(
|
||||||
|
`/api/v1/workflow-integrations/microsoft-teams/${id}`
|
||||||
|
);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
export const fetchMicrosoftTeamsIntegrationTeams = async (id?: string) => {
|
||||||
|
const { data } = await apiRequest.get<MicrosoftTeamsIntegrationTeam[]>(
|
||||||
|
`/api/v1/workflow-integrations/microsoft-teams/${id}/teams`
|
||||||
|
);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGetMicrosoftTeamsIntegrations = (orgId?: string) =>
|
||||||
useQuery({
|
useQuery({
|
||||||
queryKey: workflowIntegrationKeys.getSlackIntegrations(orgId),
|
queryKey: workflowIntegrationKeys.getMicrosoftTeamsIntegrations(orgId),
|
||||||
queryFn: () => fetchSlackIntegrations(),
|
queryFn: () => fetchMicrosoftTeamsIntegrations(),
|
||||||
enabled: Boolean(orgId)
|
enabled: Boolean(orgId)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const useGetMicrosoftTeamsIntegrationById = (id?: string) =>
|
||||||
|
useQuery({
|
||||||
|
queryKey: workflowIntegrationKeys.getMicrosoftTeamsIntegration(id),
|
||||||
|
queryFn: () => fetchMicrosoftTeamsIntegrationById(id),
|
||||||
|
enabled: Boolean(id)
|
||||||
|
});
|
||||||
|
|
||||||
|
export const useGetMicrosoftTeamsIntegrationTeams = (id?: string) =>
|
||||||
|
useQuery({
|
||||||
|
queryKey: workflowIntegrationKeys.getMicrosoftTeamsIntegrationTeams(id),
|
||||||
|
queryFn: () => fetchMicrosoftTeamsIntegrationTeams(id),
|
||||||
|
enabled: Boolean(id)
|
||||||
|
});
|
||||||
|
|
||||||
export const useGetSlackIntegrationById = (id?: string) =>
|
export const useGetSlackIntegrationById = (id?: string) =>
|
||||||
useQuery({
|
useQuery({
|
||||||
queryKey: workflowIntegrationKeys.getSlackIntegration(id),
|
queryKey: workflowIntegrationKeys.getSlackIntegration(id),
|
||||||
|
|||||||
@@ -1,14 +1,25 @@
|
|||||||
export enum WorkflowIntegrationPlatform {
|
export enum WorkflowIntegrationPlatform {
|
||||||
SLACK = "slack"
|
SLACK = "slack",
|
||||||
|
MICROSOFT_TEAMS = "microsoft-teams"
|
||||||
}
|
}
|
||||||
|
|
||||||
export type WorkflowIntegration = {
|
export type WorkflowIntegration = {
|
||||||
id: string;
|
id: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
description: string;
|
description: string;
|
||||||
|
status: WorkflowIntegrationStatus;
|
||||||
integration: WorkflowIntegrationPlatform;
|
integration: WorkflowIntegrationPlatform;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type MicrosoftTeamsIntegrationTeam = {
|
||||||
|
teamId: string;
|
||||||
|
teamName: string;
|
||||||
|
channels: {
|
||||||
|
channelId: string;
|
||||||
|
channelName: string;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
|
||||||
export type SlackIntegration = {
|
export type SlackIntegration = {
|
||||||
id: string;
|
id: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
@@ -16,6 +27,18 @@ export type SlackIntegration = {
|
|||||||
teamName: string;
|
teamName: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export enum WorkflowIntegrationStatus {
|
||||||
|
Pending = "pending",
|
||||||
|
Installed = "installed"
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MicrosoftTeamsIntegration = {
|
||||||
|
id: string;
|
||||||
|
slug: string;
|
||||||
|
description: string;
|
||||||
|
tenantId: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type SlackIntegrationChannel = {
|
export type SlackIntegrationChannel = {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -28,25 +51,88 @@ export type TUpdateSlackIntegrationDTO = {
|
|||||||
description?: string;
|
description?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TUpdateMicrosoftTeamsIntegrationDTO = {
|
||||||
|
id: string;
|
||||||
|
orgId: string;
|
||||||
|
slug?: string;
|
||||||
|
description?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TCreateMicrosoftTeamsIntegrationDTO = {
|
||||||
|
orgId: string;
|
||||||
|
tenantId: string;
|
||||||
|
slug: string;
|
||||||
|
description?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type TDeleteSlackIntegrationDTO = {
|
export type TDeleteSlackIntegrationDTO = {
|
||||||
id: string;
|
id: string;
|
||||||
orgId: string;
|
orgId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ProjectSlackConfig = {
|
export type TDeleteMicrosoftTeamsIntegrationIntegrationDTO = {
|
||||||
id: string;
|
id: string;
|
||||||
slackIntegrationId: string;
|
orgId: string;
|
||||||
isAccessRequestNotificationEnabled: boolean;
|
|
||||||
accessRequestChannels: string;
|
|
||||||
isSecretRequestNotificationEnabled: boolean;
|
|
||||||
secretRequestChannels: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TUpdateProjectSlackConfigDTO = {
|
export type ProjectWorkflowIntegrationConfig =
|
||||||
workspaceId: string;
|
| {
|
||||||
slackIntegrationId: string;
|
id: string;
|
||||||
|
integration: WorkflowIntegrationPlatform.SLACK;
|
||||||
|
integrationId: string;
|
||||||
isAccessRequestNotificationEnabled: boolean;
|
isAccessRequestNotificationEnabled: boolean;
|
||||||
accessRequestChannels: string;
|
accessRequestChannels: string;
|
||||||
isSecretRequestNotificationEnabled: boolean;
|
isSecretRequestNotificationEnabled: boolean;
|
||||||
secretRequestChannels: string;
|
secretRequestChannels: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
id: string;
|
||||||
|
integration: WorkflowIntegrationPlatform.MICROSOFT_TEAMS;
|
||||||
|
integrationId: string;
|
||||||
|
isAccessRequestNotificationEnabled: boolean;
|
||||||
|
isSecretRequestNotificationEnabled: boolean;
|
||||||
|
accessRequestChannels: {
|
||||||
|
teamId: string;
|
||||||
|
channelIds: string[];
|
||||||
|
};
|
||||||
|
secretRequestChannels: {
|
||||||
|
teamId: string;
|
||||||
|
channelIds: string[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TUpdateProjectWorkflowIntegrationConfigDTO =
|
||||||
|
| {
|
||||||
|
integration: WorkflowIntegrationPlatform.SLACK;
|
||||||
|
workspaceId: string;
|
||||||
|
integrationId: string;
|
||||||
|
isAccessRequestNotificationEnabled: boolean;
|
||||||
|
accessRequestChannels: string;
|
||||||
|
isSecretRequestNotificationEnabled: boolean;
|
||||||
|
secretRequestChannels: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
integration: WorkflowIntegrationPlatform.MICROSOFT_TEAMS;
|
||||||
|
workspaceId: string;
|
||||||
|
integrationId: string;
|
||||||
|
isAccessRequestNotificationEnabled: boolean;
|
||||||
|
isSecretRequestNotificationEnabled: boolean;
|
||||||
|
accessRequestChannels?: {
|
||||||
|
teamId: string;
|
||||||
|
channelIds: string[];
|
||||||
|
};
|
||||||
|
secretRequestChannels?: {
|
||||||
|
teamId: string;
|
||||||
|
channelIds: string[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
export type TDeleteProjectWorkflowIntegrationDTO = {
|
||||||
|
projectId: string;
|
||||||
|
integration: WorkflowIntegrationPlatform;
|
||||||
|
integrationId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TCheckMicrosoftTeamsIntegrationInstallationStatusDTO = {
|
||||||
|
workflowIntegrationId: string;
|
||||||
|
orgId: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -24,9 +24,9 @@ export {
|
|||||||
useGetWorkspaceIndexStatus,
|
useGetWorkspaceIndexStatus,
|
||||||
useGetWorkspaceIntegrations,
|
useGetWorkspaceIntegrations,
|
||||||
useGetWorkspaceSecrets,
|
useGetWorkspaceSecrets,
|
||||||
useGetWorkspaceSlackConfig,
|
|
||||||
useGetWorkspaceUserDetails,
|
useGetWorkspaceUserDetails,
|
||||||
useGetWorkspaceUsers,
|
useGetWorkspaceUsers,
|
||||||
|
useGetWorkspaceWorkflowIntegrationConfig,
|
||||||
useListWorkspaceCas,
|
useListWorkspaceCas,
|
||||||
useListWorkspaceCertificates,
|
useListWorkspaceCertificates,
|
||||||
useListWorkspaceCertificateTemplates,
|
useListWorkspaceCertificateTemplates,
|
||||||
|
|||||||
@@ -20,7 +20,10 @@ import { TSshCertificateTemplate } from "../sshCertificateTemplates/types";
|
|||||||
import { TSshHost } from "../sshHost/types";
|
import { TSshHost } from "../sshHost/types";
|
||||||
import { userKeys } from "../users/query-keys";
|
import { userKeys } from "../users/query-keys";
|
||||||
import { TWorkspaceUser } from "../users/types";
|
import { TWorkspaceUser } from "../users/types";
|
||||||
import { ProjectSlackConfig } from "../workflowIntegrations/types";
|
import {
|
||||||
|
ProjectWorkflowIntegrationConfig,
|
||||||
|
WorkflowIntegrationPlatform
|
||||||
|
} from "../workflowIntegrations/types";
|
||||||
import { workspaceKeys } from "./query-keys";
|
import { workspaceKeys } from "./query-keys";
|
||||||
import {
|
import {
|
||||||
CreateEnvironmentDTO,
|
CreateEnvironmentDTO,
|
||||||
@@ -874,12 +877,18 @@ export const useListWorkspaceSshCertificateTemplates = (projectId: string) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useGetWorkspaceSlackConfig = ({ workspaceId }: { workspaceId: string }) => {
|
export const useGetWorkspaceWorkflowIntegrationConfig = ({
|
||||||
|
workspaceId,
|
||||||
|
integration
|
||||||
|
}: {
|
||||||
|
workspaceId: string;
|
||||||
|
integration: WorkflowIntegrationPlatform;
|
||||||
|
}) => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: workspaceKeys.getWorkspaceSlackConfig(workspaceId),
|
queryKey: workspaceKeys.getWorkspaceWorkflowIntegrationConfig(workspaceId, integration),
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const { data } = await apiRequest.get<ProjectSlackConfig>(
|
const { data } = await apiRequest.get<ProjectWorkflowIntegrationConfig>(
|
||||||
`/api/v1/workspace/${workspaceId}/slack-config`
|
`/api/v1/workspace/${workspaceId}/workflow-integration-config/${integration}`
|
||||||
);
|
);
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { TListProjectIdentitiesDTO, TSearchProjectsDTO } from "@app/hooks/api/workspace/types";
|
import { TListProjectIdentitiesDTO, TSearchProjectsDTO } from "@app/hooks/api/workspace/types";
|
||||||
|
|
||||||
import type { CaStatus } from "../ca";
|
import type { CaStatus } from "../ca";
|
||||||
|
import { WorkflowIntegrationPlatform } from "../workflowIntegrations/types";
|
||||||
|
|
||||||
export const workspaceKeys = {
|
export const workspaceKeys = {
|
||||||
getWorkspaceById: (workspaceId: string) => ["workspaces", { workspaceId }] as const,
|
getWorkspaceById: (workspaceId: string) => ["workspaces", { workspaceId }] as const,
|
||||||
@@ -53,8 +54,10 @@ export const workspaceKeys = {
|
|||||||
[{ workspaceId }, "workspace-pki-collections"] as const,
|
[{ workspaceId }, "workspace-pki-collections"] as const,
|
||||||
getWorkspaceCertificateTemplates: (workspaceId: string) =>
|
getWorkspaceCertificateTemplates: (workspaceId: string) =>
|
||||||
[{ workspaceId }, "workspace-certificate-templates"] as const,
|
[{ workspaceId }, "workspace-certificate-templates"] as const,
|
||||||
getWorkspaceSlackConfig: (workspaceId: string) =>
|
getWorkspaceWorkflowIntegrationConfig: (
|
||||||
[{ workspaceId }, "workspace-slack-config"] as const,
|
workspaceId: string,
|
||||||
|
integration: WorkflowIntegrationPlatform
|
||||||
|
) => [{ workspaceId, integration }, "workspace-workflow-integration-config"] as const,
|
||||||
getWorkspaceSshCas: (projectId: string) => [{ projectId }, "workspace-ssh-cas"] as const,
|
getWorkspaceSshCas: (projectId: string) => [{ projectId }, "workspace-ssh-cas"] as const,
|
||||||
allWorkspaceSshCertificates: (projectId: string) =>
|
allWorkspaceSshCertificates: (projectId: string) =>
|
||||||
[{ projectId }, "workspace-ssh-certificates"] as const,
|
[{ projectId }, "workspace-ssh-certificates"] as const,
|
||||||
|
|||||||
@@ -198,3 +198,19 @@ html {
|
|||||||
top: 0.5rem;
|
top: 0.5rem;
|
||||||
@apply text-sm text-gray-500 opacity-50;
|
@apply text-sm text-gray-500 opacity-50;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.accordion {
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: 0fr;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: grid-template-rows 200ms ease !important;
|
||||||
|
animation: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordion[data-state="open"] {
|
||||||
|
grid-template-rows: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordion > div {
|
||||||
|
min-height: 0px;
|
||||||
|
}
|
||||||
@@ -1,166 +1,24 @@
|
|||||||
import { useEffect } from "react";
|
import { useGetAdminIntegrationsConfig } from "@app/hooks/api";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { MicrosoftTeamsIntegrationForm } from "./MicrosoftTeamsIntegrationForm";
|
||||||
import { Button, FormControl, Input } from "@app/components/v2";
|
import { SlackIntegrationForm } from "./SlackIntegrationForm";
|
||||||
import { useToggle } from "@app/hooks";
|
|
||||||
import { useGetAdminSlackConfig, useUpdateServerConfig } from "@app/hooks/api";
|
|
||||||
|
|
||||||
const slackFormSchema = z.object({
|
|
||||||
clientId: z.string(),
|
|
||||||
clientSecret: z.string()
|
|
||||||
});
|
|
||||||
|
|
||||||
type TSlackForm = z.infer<typeof slackFormSchema>;
|
|
||||||
|
|
||||||
const getCustomSlackAppCreationUrl = () =>
|
|
||||||
`https://api.slack.com/apps?new_app=1&manifest_json=${encodeURIComponent(
|
|
||||||
JSON.stringify({
|
|
||||||
display_information: {
|
|
||||||
name: "Infisical",
|
|
||||||
description: "Get real-time Infisical updates in Slack",
|
|
||||||
background_color: "#c2d62b",
|
|
||||||
long_description: `This Slack application is designed specifically for use with your self-hosted Infisical instance, allowing seamless integration between your Infisical projects and your Slack workspace. With this integration, your team can stay up-to-date with the latest events, changes, and notifications directly inside Slack.
|
|
||||||
- Notifications: Receive real-time updates and alerts about critical events in your Infisical projects. Whether it's a new project being created, updates to secrets, or changes to your team's configuration, you will be promptly notified within the designated Slack channels of your choice.
|
|
||||||
- Customization: Tailor the notifications to your team's specific needs by configuring which types of events trigger alerts and in which channels they are sent.
|
|
||||||
- Collaboration: Keep your entire team in the loop with notifications that help facilitate more efficient collaboration by ensuring that everyone is aware of important developments in your Infisical projects.
|
|
||||||
|
|
||||||
By integrating Infisical with Slack, you can enhance your workflow by combining the power of secure secrets management with the communication capabilities of Slack.`
|
|
||||||
},
|
|
||||||
features: {
|
|
||||||
app_home: {
|
|
||||||
home_tab_enabled: false,
|
|
||||||
messages_tab_enabled: false,
|
|
||||||
messages_tab_read_only_enabled: true
|
|
||||||
},
|
|
||||||
bot_user: {
|
|
||||||
display_name: "Infisical",
|
|
||||||
always_online: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
oauth_config: {
|
|
||||||
redirect_urls: [`${window.origin}/api/v1/workflow-integrations/slack/oauth_redirect`],
|
|
||||||
scopes: {
|
|
||||||
bot: ["chat:write.public", "chat:write", "channels:read", "groups:read"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
settings: {
|
|
||||||
org_deploy_enabled: false,
|
|
||||||
socket_mode_enabled: false,
|
|
||||||
token_rotation_enabled: false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
)}`;
|
|
||||||
|
|
||||||
export const IntegrationPanel = () => {
|
export const IntegrationPanel = () => {
|
||||||
const {
|
const { data: adminIntegrationsConfig } = useGetAdminIntegrationsConfig();
|
||||||
control,
|
|
||||||
handleSubmit,
|
|
||||||
setValue,
|
|
||||||
formState: { isSubmitting, isDirty }
|
|
||||||
} = useForm<TSlackForm>({
|
|
||||||
resolver: zodResolver(slackFormSchema)
|
|
||||||
});
|
|
||||||
|
|
||||||
const { data: adminSlackConfig } = useGetAdminSlackConfig();
|
|
||||||
const { mutateAsync: updateAdminServerConfig } = useUpdateServerConfig();
|
|
||||||
const [isSlackClientIdFocused, setIsSlackClientIdFocused] = useToggle();
|
|
||||||
const [isSlackClientSecretFocused, setIsSlackClientSecretFocused] = useToggle();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (adminSlackConfig) {
|
|
||||||
setValue("clientId", adminSlackConfig.clientId);
|
|
||||||
setValue("clientSecret", adminSlackConfig.clientSecret);
|
|
||||||
}
|
|
||||||
}, [adminSlackConfig]);
|
|
||||||
|
|
||||||
const onSlackFormSubmit = async (data: TSlackForm) => {
|
|
||||||
await updateAdminServerConfig({
|
|
||||||
slackClientId: data.clientId,
|
|
||||||
slackClientSecret: data.clientSecret
|
|
||||||
});
|
|
||||||
|
|
||||||
createNotification({
|
|
||||||
text: "Updated admin slack configuration",
|
|
||||||
type: "success"
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form
|
<div className="mb-6 min-h-64 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||||
className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"
|
<div className="mb-4">
|
||||||
onSubmit={handleSubmit(onSlackFormSubmit)}
|
<div className="text-xl font-semibold text-mineshaft-100">Integrations</div>
|
||||||
>
|
<div className="text-sm text-mineshaft-300">
|
||||||
<div className="flex flex-col justify-start">
|
Configure your instance-wide settings to enable integration with Slack and Microsoft
|
||||||
<div className="mb-2 text-xl font-semibold text-mineshaft-100">Slack Integration</div>
|
Teams.
|
||||||
<div className="mb-4 max-w-lg text-sm text-mineshaft-300">
|
|
||||||
Step 1: Create your Infisical Slack App
|
|
||||||
</div>
|
</div>
|
||||||
<div className="mb-6">
|
|
||||||
<Button
|
|
||||||
colorSchema="secondary"
|
|
||||||
onClick={() => window.open(getCustomSlackAppCreationUrl())}
|
|
||||||
>
|
|
||||||
Create Slack App
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="mb-4 max-w-lg text-sm text-mineshaft-300">
|
<div className="flex flex-col gap-2">
|
||||||
Step 2: Configure your instance-wide settings to enable integration with Slack. Copy the
|
<SlackIntegrationForm adminIntegrationsConfig={adminIntegrationsConfig} />
|
||||||
values from the App Credentials page of your custom Slack App.
|
<MicrosoftTeamsIntegrationForm adminIntegrationsConfig={adminIntegrationsConfig} />
|
||||||
</div>
|
</div>
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="clientId"
|
|
||||||
render={({ field, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
label="Client ID"
|
|
||||||
className="w-96"
|
|
||||||
isError={Boolean(error)}
|
|
||||||
errorText={error?.message}
|
|
||||||
>
|
|
||||||
<Input
|
|
||||||
{...field}
|
|
||||||
value={field.value || ""}
|
|
||||||
type={isSlackClientIdFocused ? "text" : "password"}
|
|
||||||
onFocus={() => setIsSlackClientIdFocused.on()}
|
|
||||||
onBlur={() => setIsSlackClientIdFocused.off()}
|
|
||||||
onChange={(e) => field.onChange(e.target.value)}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="clientSecret"
|
|
||||||
render={({ field, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
label="Client Secret"
|
|
||||||
className="w-96"
|
|
||||||
isError={Boolean(error)}
|
|
||||||
errorText={error?.message}
|
|
||||||
>
|
|
||||||
<Input
|
|
||||||
{...field}
|
|
||||||
value={field.value || ""}
|
|
||||||
type={isSlackClientSecretFocused ? "text" : "password"}
|
|
||||||
onFocus={() => setIsSlackClientSecretFocused.on()}
|
|
||||||
onBlur={() => setIsSlackClientSecretFocused.off()}
|
|
||||||
onChange={(e) => field.onChange(e.target.value)}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<Button
|
|
||||||
className="mt-2"
|
|
||||||
type="submit"
|
|
||||||
isLoading={isSubmitting}
|
|
||||||
isDisabled={isSubmitting || !isDirty}
|
|
||||||
>
|
|
||||||
Save
|
|
||||||
</Button>
|
|
||||||
</form>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,176 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import { BsMicrosoftTeams } from "react-icons/bs";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import {
|
||||||
|
Accordion,
|
||||||
|
AccordionContent,
|
||||||
|
AccordionItem,
|
||||||
|
AccordionTrigger,
|
||||||
|
Button,
|
||||||
|
FormControl,
|
||||||
|
Input
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { useToggle } from "@app/hooks";
|
||||||
|
import { useUpdateServerConfig } from "@app/hooks/api";
|
||||||
|
import { AdminIntegrationsConfig } from "@app/hooks/api/admin/types";
|
||||||
|
|
||||||
|
const microsoftTeamsFormSchema = z.object({
|
||||||
|
appId: z.string(),
|
||||||
|
clientSecret: z.string(),
|
||||||
|
botId: z.string()
|
||||||
|
});
|
||||||
|
|
||||||
|
type TMicrosoftTeamsForm = z.infer<typeof microsoftTeamsFormSchema>;
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
adminIntegrationsConfig?: AdminIntegrationsConfig;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MicrosoftTeamsIntegrationForm = ({ adminIntegrationsConfig }: Props) => {
|
||||||
|
const { mutateAsync: updateAdminServerConfig } = useUpdateServerConfig();
|
||||||
|
const [isMicrosoftTeamsAppIdFocused, setIsMicrosoftTeamsAppIdFocused] = useToggle();
|
||||||
|
const [isMicrosoftTeamsClientSecretFocused, setIsMicrosoftTeamsClientSecretFocused] = useToggle();
|
||||||
|
const [isMicrosoftTeamsBotIdFocused, setIsMicrosoftTeamsBotIdFocused] = useToggle();
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
handleSubmit,
|
||||||
|
setValue,
|
||||||
|
formState: { isSubmitting, isDirty }
|
||||||
|
} = useForm<TMicrosoftTeamsForm>({
|
||||||
|
resolver: zodResolver(microsoftTeamsFormSchema)
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSubmit = async (data: TMicrosoftTeamsForm) => {
|
||||||
|
await updateAdminServerConfig({
|
||||||
|
microsoftTeamsAppId: data.appId,
|
||||||
|
microsoftTeamsClientSecret: data.clientSecret,
|
||||||
|
microsoftTeamsBotId: data.botId
|
||||||
|
});
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
text: "Updated admin Microsoft Teams configuration",
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (adminIntegrationsConfig) {
|
||||||
|
setValue("appId", adminIntegrationsConfig.microsoftTeams.appId);
|
||||||
|
setValue("clientSecret", adminIntegrationsConfig.microsoftTeams.clientSecret);
|
||||||
|
setValue("botId", adminIntegrationsConfig.microsoftTeams.botId);
|
||||||
|
}
|
||||||
|
}, [adminIntegrationsConfig]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
|
<Accordion type="single" collapsible className="w-full">
|
||||||
|
<AccordionItem
|
||||||
|
value="microsoft-teams-integration"
|
||||||
|
className="data-[state=open]:border-none"
|
||||||
|
>
|
||||||
|
<AccordionTrigger className="flex h-fit w-full justify-start rounded-md border border-mineshaft-500 bg-mineshaft-700 px-4 py-6 text-sm transition-colors data-[state=open]:rounded-b-none">
|
||||||
|
<div className="text-md group order-1 ml-3 flex items-center gap-2">
|
||||||
|
<BsMicrosoftTeams className="text-lg group-hover:text-primary-400" />
|
||||||
|
<div className="text-[15px] font-semibold">Microsoft Teams Integration</div>
|
||||||
|
</div>
|
||||||
|
</AccordionTrigger>
|
||||||
|
<AccordionContent childrenClassName="px-0 py-0">
|
||||||
|
<div className="flex w-full flex-col justify-start rounded-md rounded-t-none border border-t-0 border-mineshaft-500 bg-mineshaft-700 px-4 py-4">
|
||||||
|
<div className="mb-4 max-w-lg text-sm text-mineshaft-300">
|
||||||
|
Step 1: Create Microsoft Teams bot
|
||||||
|
</div>
|
||||||
|
<div className="mb-6">
|
||||||
|
<Button colorSchema="secondary">Documentation</Button>
|
||||||
|
</div>
|
||||||
|
<div className="mb-4 max-w-lg text-sm text-mineshaft-300">
|
||||||
|
Step 2: Configure your instance-wide settings to enable integration with Microsoft
|
||||||
|
Teams. Copy the App ID and Client Secret from your Microsoft Teams bot's App
|
||||||
|
Registration page. The Client Secret is the password for the bot.
|
||||||
|
</div>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="appId"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Application (Client) ID"
|
||||||
|
className="w-96"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
value={field.value || ""}
|
||||||
|
type={isMicrosoftTeamsAppIdFocused ? "text" : "password"}
|
||||||
|
onFocus={() => setIsMicrosoftTeamsAppIdFocused.on()}
|
||||||
|
onBlur={() => setIsMicrosoftTeamsAppIdFocused.off()}
|
||||||
|
onChange={(e) => field.onChange(e.target.value)}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="clientSecret"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Client Secret"
|
||||||
|
tooltipText="You can find your Client Secret in the Certificates & Secrets section of your Microsoft Teams bot's App Registration."
|
||||||
|
className="w-96"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
value={field.value || ""}
|
||||||
|
type={isMicrosoftTeamsClientSecretFocused ? "text" : "password"}
|
||||||
|
onFocus={() => setIsMicrosoftTeamsClientSecretFocused.on()}
|
||||||
|
onBlur={() => setIsMicrosoftTeamsClientSecretFocused.off()}
|
||||||
|
onChange={(e) => field.onChange(e.target.value)}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="botId"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Bot ID"
|
||||||
|
tooltipClassName="You can find the bot ID in your bot's manifest.json file as the `id` field."
|
||||||
|
className="w-96"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
value={field.value || ""}
|
||||||
|
type={isMicrosoftTeamsBotIdFocused ? "text" : "password"}
|
||||||
|
onFocus={() => setIsMicrosoftTeamsBotIdFocused.on()}
|
||||||
|
onBlur={() => setIsMicrosoftTeamsBotIdFocused.off()}
|
||||||
|
onChange={(e) => field.onChange(e.target.value)}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
className="mt-2"
|
||||||
|
type="submit"
|
||||||
|
isLoading={isSubmitting}
|
||||||
|
isDisabled={isSubmitting || !isDirty}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
|
</Accordion>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,189 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import { BsSlack } from "react-icons/bs";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import {
|
||||||
|
Accordion,
|
||||||
|
AccordionContent,
|
||||||
|
AccordionItem,
|
||||||
|
AccordionTrigger,
|
||||||
|
Button,
|
||||||
|
FormControl,
|
||||||
|
Input
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { useToggle } from "@app/hooks";
|
||||||
|
import { useUpdateServerConfig } from "@app/hooks/api";
|
||||||
|
import { AdminIntegrationsConfig } from "@app/hooks/api/admin/types";
|
||||||
|
|
||||||
|
const getCustomSlackAppCreationUrl = () =>
|
||||||
|
`https://api.slack.com/apps?new_app=1&manifest_json=${encodeURIComponent(
|
||||||
|
JSON.stringify({
|
||||||
|
display_information: {
|
||||||
|
name: "Infisical",
|
||||||
|
description: "Get real-time Infisical updates in Slack",
|
||||||
|
background_color: "#c2d62b",
|
||||||
|
long_description: `This Slack application is designed specifically for use with your self-hosted Infisical instance, allowing seamless integration between your Infisical projects and your Slack workspace. With this integration, your team can stay up-to-date with the latest events, changes, and notifications directly inside Slack.
|
||||||
|
- Notifications: Receive real-time updates and alerts about critical events in your Infisical projects. Whether it's a new project being created, updates to secrets, or changes to your team's configuration, you will be promptly notified within the designated Slack channels of your choice.
|
||||||
|
- Customization: Tailor the notifications to your team's specific needs by configuring which types of events trigger alerts and in which channels they are sent.
|
||||||
|
- Collaboration: Keep your entire team in the loop with notifications that help facilitate more efficient collaboration by ensuring that everyone is aware of important developments in your Infisical projects.
|
||||||
|
|
||||||
|
By integrating Infisical with Slack, you can enhance your workflow by combining the power of secure secrets management with the communication capabilities of Slack.`
|
||||||
|
},
|
||||||
|
features: {
|
||||||
|
app_home: {
|
||||||
|
home_tab_enabled: false,
|
||||||
|
messages_tab_enabled: false,
|
||||||
|
messages_tab_read_only_enabled: true
|
||||||
|
},
|
||||||
|
bot_user: {
|
||||||
|
display_name: "Infisical",
|
||||||
|
always_online: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
oauth_config: {
|
||||||
|
redirect_urls: [`${window.origin}/api/v1/workflow-integrations/slack/oauth_redirect`],
|
||||||
|
scopes: {
|
||||||
|
bot: ["chat:write.public", "chat:write", "channels:read", "groups:read"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
org_deploy_enabled: false,
|
||||||
|
socket_mode_enabled: false,
|
||||||
|
token_rotation_enabled: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)}`;
|
||||||
|
|
||||||
|
const slackFormSchema = z.object({
|
||||||
|
clientId: z.string(),
|
||||||
|
clientSecret: z.string()
|
||||||
|
});
|
||||||
|
|
||||||
|
type TSlackForm = z.infer<typeof slackFormSchema>;
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
adminIntegrationsConfig?: AdminIntegrationsConfig;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SlackIntegrationForm = ({ adminIntegrationsConfig }: Props) => {
|
||||||
|
const { mutateAsync: updateAdminServerConfig } = useUpdateServerConfig();
|
||||||
|
const [isSlackClientIdFocused, setIsSlackClientIdFocused] = useToggle();
|
||||||
|
const [isSlackClientSecretFocused, setIsSlackClientSecretFocused] = useToggle();
|
||||||
|
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
handleSubmit,
|
||||||
|
setValue,
|
||||||
|
formState: { isSubmitting, isDirty }
|
||||||
|
} = useForm<TSlackForm>({
|
||||||
|
resolver: zodResolver(slackFormSchema)
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSubmit = async (data: TSlackForm) => {
|
||||||
|
await updateAdminServerConfig({
|
||||||
|
slackClientId: data.clientId,
|
||||||
|
slackClientSecret: data.clientSecret
|
||||||
|
});
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
text: "Updated admin slack configuration",
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (adminIntegrationsConfig) {
|
||||||
|
setValue("clientId", adminIntegrationsConfig.slack.clientId);
|
||||||
|
setValue("clientSecret", adminIntegrationsConfig.slack.clientSecret);
|
||||||
|
}
|
||||||
|
}, [adminIntegrationsConfig]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
|
<Accordion type="single" collapsible className="w-full">
|
||||||
|
<AccordionItem value="slack-integration" className="data-[state=open]:border-none">
|
||||||
|
<AccordionTrigger className="flex h-fit w-full justify-start rounded-md border border-mineshaft-500 bg-mineshaft-700 px-4 py-6 text-sm transition-colors data-[state=open]:rounded-b-none">
|
||||||
|
<div className="text-md group order-1 ml-3 flex items-center gap-2">
|
||||||
|
<BsSlack className="text-lg group-hover:text-primary-400" />
|
||||||
|
<div className="text-[15px] font-semibold">Slack Integration</div>
|
||||||
|
</div>
|
||||||
|
</AccordionTrigger>
|
||||||
|
<AccordionContent childrenClassName="px-0 py-0">
|
||||||
|
<div className="flex w-full flex-col justify-start rounded-md rounded-t-none border border-t-0 border-mineshaft-500 bg-mineshaft-700 px-4 py-4">
|
||||||
|
<div className="mb-4 max-w-lg text-sm text-mineshaft-300">
|
||||||
|
Step 1: Create your Infisical Slack App
|
||||||
|
</div>
|
||||||
|
<div className="mb-6">
|
||||||
|
<Button
|
||||||
|
colorSchema="secondary"
|
||||||
|
onClick={() => window.open(getCustomSlackAppCreationUrl())}
|
||||||
|
>
|
||||||
|
Create Slack App
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="mb-4 max-w-lg text-sm text-mineshaft-300">
|
||||||
|
Step 2: Configure your instance-wide settings to enable integration with Slack. Copy
|
||||||
|
the values from the App Credentials page of your custom Slack App.
|
||||||
|
</div>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="clientId"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Client ID"
|
||||||
|
className="w-96"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
value={field.value || ""}
|
||||||
|
type={isSlackClientIdFocused ? "text" : "password"}
|
||||||
|
onFocus={() => setIsSlackClientIdFocused.on()}
|
||||||
|
onBlur={() => setIsSlackClientIdFocused.off()}
|
||||||
|
onChange={(e) => field.onChange(e.target.value)}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="clientSecret"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Client Secret"
|
||||||
|
className="w-96"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
value={field.value || ""}
|
||||||
|
type={isSlackClientSecretFocused ? "text" : "password"}
|
||||||
|
onFocus={() => setIsSlackClientSecretFocused.on()}
|
||||||
|
onBlur={() => setIsSlackClientSecretFocused.off()}
|
||||||
|
onChange={(e) => field.onChange(e.target.value)}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
className="mt-2"
|
||||||
|
type="submit"
|
||||||
|
isLoading={isSubmitting}
|
||||||
|
isDisabled={isSubmitting || !isDirty}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
|
</Accordion>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
+25
-3
@@ -1,4 +1,5 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import { BsMicrosoftTeams } from "react-icons/bs";
|
||||||
import { faSlack } from "@fortawesome/free-brands-svg-icons";
|
import { faSlack } from "@fortawesome/free-brands-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { AnimatePresence, motion } from "framer-motion";
|
import { AnimatePresence, motion } from "framer-motion";
|
||||||
@@ -6,6 +7,7 @@ import { AnimatePresence, motion } from "framer-motion";
|
|||||||
import { Modal, ModalContent } from "@app/components/v2";
|
import { Modal, ModalContent } from "@app/components/v2";
|
||||||
import { WorkflowIntegrationPlatform } from "@app/hooks/api/workflowIntegrations/types";
|
import { WorkflowIntegrationPlatform } from "@app/hooks/api/workflowIntegrations/types";
|
||||||
|
|
||||||
|
import { MicrosoftTeamsIntegrationForm } from "./MicrosoftTeamsIntegrationForm";
|
||||||
import { SlackIntegrationForm } from "./SlackIntegrationForm";
|
import { SlackIntegrationForm } from "./SlackIntegrationForm";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -20,9 +22,14 @@ enum WizardSteps {
|
|||||||
|
|
||||||
const PLATFORM_LIST = [
|
const PLATFORM_LIST = [
|
||||||
{
|
{
|
||||||
icon: faSlack,
|
icon: <FontAwesomeIcon icon={faSlack} size="lg" />,
|
||||||
platform: WorkflowIntegrationPlatform.SLACK,
|
platform: WorkflowIntegrationPlatform.SLACK,
|
||||||
title: "Slack"
|
title: "Slack"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <BsMicrosoftTeams className="text-lg" />,
|
||||||
|
platform: WorkflowIntegrationPlatform.MICROSOFT_TEAMS,
|
||||||
|
title: "Microsoft Teams"
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -38,7 +45,10 @@ export const AddWorkflowIntegrationForm = ({ isOpen, onToggle }: Props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal isOpen={isOpen} onOpenChange={(state) => handleFormReset(state)}>
|
<Modal isOpen={isOpen} onOpenChange={(state) => handleFormReset(state)}>
|
||||||
<ModalContent title={`Add a ${selectedPlatform ?? "workflow"} integration`} className="my-4">
|
<ModalContent
|
||||||
|
title={`Add a ${selectedPlatform?.replace("-", " ").replace(/\b\w/g, (char) => char.toUpperCase()) ?? "workflow"} integration`}
|
||||||
|
className="my-4"
|
||||||
|
>
|
||||||
<AnimatePresence mode="wait">
|
<AnimatePresence mode="wait">
|
||||||
{wizardStep === WizardSteps.SelectPlatform && (
|
{wizardStep === WizardSteps.SelectPlatform && (
|
||||||
<motion.div
|
<motion.div
|
||||||
@@ -67,7 +77,7 @@ export const AddWorkflowIntegrationForm = ({ isOpen, onToggle }: Props) => {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={icon} size="lg" />
|
<div>{icon}</div>
|
||||||
<div className="whitespace-pre-wrap text-center text-sm">{title}</div>
|
<div className="whitespace-pre-wrap text-center text-sm">{title}</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@@ -86,6 +96,18 @@ export const AddWorkflowIntegrationForm = ({ isOpen, onToggle }: Props) => {
|
|||||||
<SlackIntegrationForm onClose={() => onToggle(false)} />
|
<SlackIntegrationForm onClose={() => onToggle(false)} />
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
|
{wizardStep === WizardSteps.PlatformInputs &&
|
||||||
|
selectedPlatform === WorkflowIntegrationPlatform.MICROSOFT_TEAMS && (
|
||||||
|
<motion.div
|
||||||
|
key="microsoft-teams-platform"
|
||||||
|
transition={{ duration: 0.1 }}
|
||||||
|
initial={{ opacity: 0, translateX: 30 }}
|
||||||
|
animate={{ opacity: 1, translateX: 0 }}
|
||||||
|
exit={{ opacity: 0, translateX: -30 }}
|
||||||
|
>
|
||||||
|
<MicrosoftTeamsIntegrationForm onClose={() => onToggle(false)} />
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
+4
@@ -1,6 +1,7 @@
|
|||||||
import { Modal, ModalContent } from "@app/components/v2";
|
import { Modal, ModalContent } from "@app/components/v2";
|
||||||
import { WorkflowIntegrationPlatform } from "@app/hooks/api/workflowIntegrations/types";
|
import { WorkflowIntegrationPlatform } from "@app/hooks/api/workflowIntegrations/types";
|
||||||
|
|
||||||
|
import { MicrosoftTeamsIntegrationForm } from "./MicrosoftTeamsIntegrationForm";
|
||||||
import { SlackIntegrationForm } from "./SlackIntegrationForm";
|
import { SlackIntegrationForm } from "./SlackIntegrationForm";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -20,6 +21,9 @@ export const IntegrationFormDetails = ({ isOpen, id, onOpenChange, workflowPlatf
|
|||||||
{workflowPlatform === WorkflowIntegrationPlatform.SLACK && (
|
{workflowPlatform === WorkflowIntegrationPlatform.SLACK && (
|
||||||
<SlackIntegrationForm id={id} onClose={() => onOpenChange(false)} />
|
<SlackIntegrationForm id={id} onClose={() => onOpenChange(false)} />
|
||||||
)}
|
)}
|
||||||
|
{workflowPlatform === WorkflowIntegrationPlatform.MICROSOFT_TEAMS && (
|
||||||
|
<MicrosoftTeamsIntegrationForm id={id} onClose={() => onOpenChange(false)} />
|
||||||
|
)}
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
|||||||
+154
@@ -0,0 +1,154 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import { Button, FormControl, Input } from "@app/components/v2";
|
||||||
|
import { useOrganization } from "@app/context";
|
||||||
|
import {
|
||||||
|
useCreateMicrosoftTeamsIntegration,
|
||||||
|
useGetMicrosoftTeamsIntegrationById,
|
||||||
|
useUpdateMicrosoftTeamsIntegration
|
||||||
|
} from "@app/hooks/api";
|
||||||
|
import { slugSchema } from "@app/lib/schemas";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
id?: string;
|
||||||
|
onClose: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const microsoftTeamsFormSchema = z.object({
|
||||||
|
slug: slugSchema({ min: 1, field: "Alias" }),
|
||||||
|
tenantId: z.string().min(1, { message: "Tenant ID is required" }).trim(),
|
||||||
|
description: z.string().optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
type TMicrosoftTeamsFormData = z.infer<typeof microsoftTeamsFormSchema>;
|
||||||
|
|
||||||
|
export const MicrosoftTeamsIntegrationForm = ({ id, onClose }: Props) => {
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
handleSubmit,
|
||||||
|
setValue,
|
||||||
|
formState: { isSubmitting, isDirty }
|
||||||
|
} = useForm<TMicrosoftTeamsFormData>({
|
||||||
|
resolver: zodResolver(microsoftTeamsFormSchema)
|
||||||
|
});
|
||||||
|
|
||||||
|
const { currentOrg } = useOrganization();
|
||||||
|
const { data: microsoftTeamsIntegration } = useGetMicrosoftTeamsIntegrationById(id);
|
||||||
|
const { mutateAsync: createMicrosoftTeamsIntegration } = useCreateMicrosoftTeamsIntegration();
|
||||||
|
const { mutateAsync: updateMicrosoftTeamsIntegration } = useUpdateMicrosoftTeamsIntegration();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (microsoftTeamsIntegration) {
|
||||||
|
setValue("slug", microsoftTeamsIntegration.slug);
|
||||||
|
setValue("description", microsoftTeamsIntegration.description ?? "");
|
||||||
|
setValue("tenantId", microsoftTeamsIntegration.tenantId);
|
||||||
|
}
|
||||||
|
}, [microsoftTeamsIntegration]);
|
||||||
|
|
||||||
|
const handleSlackFormSubmit = async ({
|
||||||
|
slug,
|
||||||
|
description,
|
||||||
|
tenantId
|
||||||
|
}: TMicrosoftTeamsFormData) => {
|
||||||
|
if (id && microsoftTeamsIntegration) {
|
||||||
|
if (!currentOrg) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tenantId !== microsoftTeamsIntegration.tenantId) {
|
||||||
|
createNotification({
|
||||||
|
text: "Tenant ID cannot be changed",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await updateMicrosoftTeamsIntegration({
|
||||||
|
id,
|
||||||
|
orgId: currentOrg.id,
|
||||||
|
slug,
|
||||||
|
description
|
||||||
|
});
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
text: "Successfully updated Microsoft Teams integration",
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
|
||||||
|
onClose();
|
||||||
|
} else {
|
||||||
|
await createMicrosoftTeamsIntegration({
|
||||||
|
orgId: currentOrg.id,
|
||||||
|
tenantId,
|
||||||
|
slug,
|
||||||
|
description
|
||||||
|
});
|
||||||
|
|
||||||
|
onClose();
|
||||||
|
createNotification({
|
||||||
|
text: "Successfully created Microsoft Teams integration",
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit(handleSlackFormSubmit)} autoComplete="off">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="slug"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl label="Alias" isRequired errorText={error?.message} isError={Boolean(error)}>
|
||||||
|
<Input placeholder="" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{!microsoftTeamsIntegration && (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="tenantId"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
isRequired
|
||||||
|
label="Tenant ID"
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
>
|
||||||
|
<Input placeholder="" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="description"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl label="Description" errorText={error?.message} isError={Boolean(error)}>
|
||||||
|
<Input placeholder="" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{microsoftTeamsIntegration && (
|
||||||
|
<FormControl label="Connected Microsoft Teams Tenant">
|
||||||
|
<Input
|
||||||
|
value={microsoftTeamsIntegration.tenantId}
|
||||||
|
isReadOnly
|
||||||
|
className="bg-white/[0.07]"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
<div className="mt-6 flex items-center space-x-4">
|
||||||
|
<Button type="submit" isLoading={isSubmitting} isDisabled={!isDirty || isSubmitting}>
|
||||||
|
{id && microsoftTeamsIntegration ? "Save" : "Create Microsoft Teams Integration"}
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline_bg" onClick={onClose}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
+76
-9
@@ -7,6 +7,7 @@ import { twMerge } from "tailwind-merge";
|
|||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import { OrgPermissionCan } from "@app/components/permissions";
|
import { OrgPermissionCan } from "@app/components/permissions";
|
||||||
import {
|
import {
|
||||||
|
Badge,
|
||||||
Button,
|
Button,
|
||||||
DeleteActionModal,
|
DeleteActionModal,
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
@@ -27,14 +28,31 @@ import { withPermission } from "@app/hoc";
|
|||||||
import { usePopUp } from "@app/hooks";
|
import { usePopUp } from "@app/hooks";
|
||||||
import {
|
import {
|
||||||
fetchSlackReinstallUrl,
|
fetchSlackReinstallUrl,
|
||||||
|
useCheckMicrosoftTeamsIntegrationInstallationStatus,
|
||||||
|
useDeleteMicrosoftTeamsIntegration,
|
||||||
useDeleteSlackIntegration,
|
useDeleteSlackIntegration,
|
||||||
useGetWorkflowIntegrations
|
useGetWorkflowIntegrations
|
||||||
} from "@app/hooks/api";
|
} from "@app/hooks/api";
|
||||||
import { WorkflowIntegrationPlatform } from "@app/hooks/api/workflowIntegrations/types";
|
import {
|
||||||
|
WorkflowIntegrationPlatform,
|
||||||
|
WorkflowIntegrationStatus
|
||||||
|
} from "@app/hooks/api/workflowIntegrations/types";
|
||||||
|
|
||||||
import { AddWorkflowIntegrationForm } from "./AddWorkflowIntegrationForm";
|
import { AddWorkflowIntegrationForm } from "./AddWorkflowIntegrationForm";
|
||||||
import { IntegrationFormDetails } from "./IntegrationFormDetails";
|
import { IntegrationFormDetails } from "./IntegrationFormDetails";
|
||||||
|
|
||||||
|
const renderStatus = (status: WorkflowIntegrationStatus) => {
|
||||||
|
if (status === WorkflowIntegrationStatus.Installed) {
|
||||||
|
return <Badge variant="success">Installed</Badge>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === WorkflowIntegrationStatus.Pending) {
|
||||||
|
return <Badge variant="primary">Pending</Badge>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Badge variant="danger">Failed</Badge>;
|
||||||
|
};
|
||||||
|
|
||||||
export const OrgWorkflowIntegrationTab = withPermission(
|
export const OrgWorkflowIntegrationTab = withPermission(
|
||||||
() => {
|
() => {
|
||||||
const { popUp, handlePopUpOpen, handlePopUpToggle, handlePopUpClose } = usePopUp([
|
const { popUp, handlePopUpOpen, handlePopUpToggle, handlePopUpClose } = usePopUp([
|
||||||
@@ -48,6 +66,9 @@ export const OrgWorkflowIntegrationTab = withPermission(
|
|||||||
useGetWorkflowIntegrations(currentOrg?.id);
|
useGetWorkflowIntegrations(currentOrg?.id);
|
||||||
|
|
||||||
const { mutateAsync: deleteSlackIntegration } = useDeleteSlackIntegration();
|
const { mutateAsync: deleteSlackIntegration } = useDeleteSlackIntegration();
|
||||||
|
const { mutateAsync: deleteMicrosoftTeamsIntegration } = useDeleteMicrosoftTeamsIntegration();
|
||||||
|
const { mutateAsync: checkMicrosoftTeamsInstallationStatus } =
|
||||||
|
useCheckMicrosoftTeamsIntegrationInstallationStatus();
|
||||||
|
|
||||||
const handleRemoveIntegration = async () => {
|
const handleRemoveIntegration = async () => {
|
||||||
if (!currentOrg) {
|
if (!currentOrg) {
|
||||||
@@ -62,6 +83,13 @@ export const OrgWorkflowIntegrationTab = withPermission(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (platform === WorkflowIntegrationPlatform.MICROSOFT_TEAMS) {
|
||||||
|
await deleteMicrosoftTeamsIntegration({
|
||||||
|
id,
|
||||||
|
orgId: currentOrg?.id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
handlePopUpClose("removeIntegration");
|
handlePopUpClose("removeIntegration");
|
||||||
createNotification({
|
createNotification({
|
||||||
text: "Successfully deleted integration",
|
text: "Successfully deleted integration",
|
||||||
@@ -69,8 +97,7 @@ export const OrgWorkflowIntegrationTab = withPermission(
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const triggerReinstall = async (platform: WorkflowIntegrationPlatform, id: string) => {
|
const triggerSlackReinstall = async (id: string) => {
|
||||||
if (platform === WorkflowIntegrationPlatform.SLACK) {
|
|
||||||
try {
|
try {
|
||||||
const slackReinstallUrl = await fetchSlackReinstallUrl({
|
const slackReinstallUrl = await fetchSlackReinstallUrl({
|
||||||
id
|
id
|
||||||
@@ -87,7 +114,18 @@ export const OrgWorkflowIntegrationTab = withPermission(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
|
const triggerMicrosoftTeamsInstallationStatusCheck = async (id: string) => {
|
||||||
|
await checkMicrosoftTeamsInstallationStatus({
|
||||||
|
workflowIntegrationId: id,
|
||||||
|
orgId: currentOrg?.id
|
||||||
|
});
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
text: "The Microsoft Teams bot is successfully installed in your Microsoft Teams tenant",
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -117,6 +155,7 @@ export const OrgWorkflowIntegrationTab = withPermission(
|
|||||||
<Tr>
|
<Tr>
|
||||||
<Td>Provider</Td>
|
<Td>Provider</Td>
|
||||||
<Td>Alias</Td>
|
<Td>Alias</Td>
|
||||||
|
<Td>Status</Td>
|
||||||
</Tr>
|
</Tr>
|
||||||
</THead>
|
</THead>
|
||||||
<TBody>
|
<TBody>
|
||||||
@@ -139,6 +178,7 @@ export const OrgWorkflowIntegrationTab = withPermission(
|
|||||||
<div className="ml-2">{workflowIntegration.integration.toUpperCase()}</div>
|
<div className="ml-2">{workflowIntegration.integration.toUpperCase()}</div>
|
||||||
</Td>
|
</Td>
|
||||||
<Td>{workflowIntegration.slug}</Td>
|
<Td>{workflowIntegration.slug}</Td>
|
||||||
|
<Td>{renderStatus(workflowIntegration.status)}</Td>
|
||||||
<Td>
|
<Td>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild className="rounded-lg">
|
<DropdownMenuTrigger asChild className="rounded-lg">
|
||||||
@@ -159,6 +199,8 @@ export const OrgWorkflowIntegrationTab = withPermission(
|
|||||||
>
|
>
|
||||||
More details
|
More details
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
|
||||||
|
{workflowIntegration.integration === WorkflowIntegrationPlatform.SLACK && (
|
||||||
<OrgPermissionCan
|
<OrgPermissionCan
|
||||||
I={OrgPermissionActions.Create}
|
I={OrgPermissionActions.Create}
|
||||||
an={OrgPermissionSubjects.Settings}
|
an={OrgPermissionSubjects.Settings}
|
||||||
@@ -169,19 +211,44 @@ export const OrgWorkflowIntegrationTab = withPermission(
|
|||||||
className={twMerge(
|
className={twMerge(
|
||||||
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
|
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
|
||||||
)}
|
)}
|
||||||
onClick={(e) => {
|
onClick={async (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
triggerReinstall(
|
await triggerSlackReinstall(workflowIntegration.integration);
|
||||||
workflowIntegration.integration,
|
|
||||||
workflowIntegration.id
|
|
||||||
);
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Reinstall
|
Reinstall
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
)}
|
)}
|
||||||
</OrgPermissionCan>
|
</OrgPermissionCan>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{workflowIntegration.integration ===
|
||||||
|
WorkflowIntegrationPlatform.MICROSOFT_TEAMS && (
|
||||||
|
<OrgPermissionCan
|
||||||
|
I={OrgPermissionActions.Edit}
|
||||||
|
an={OrgPermissionSubjects.Settings}
|
||||||
|
>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<DropdownMenuItem
|
||||||
|
disabled={!isAllowed}
|
||||||
|
className={twMerge(
|
||||||
|
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
|
||||||
|
)}
|
||||||
|
onClick={async (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
await triggerMicrosoftTeamsInstallationStatusCheck(
|
||||||
|
workflowIntegration.id
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Check Installation Status
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
</OrgPermissionCan>
|
||||||
|
)}
|
||||||
|
|
||||||
<OrgPermissionCan
|
<OrgPermissionCan
|
||||||
I={OrgPermissionActions.Delete}
|
I={OrgPermissionActions.Delete}
|
||||||
an={OrgPermissionSubjects.Settings}
|
an={OrgPermissionSubjects.Settings}
|
||||||
|
|||||||
+97
-315
@@ -1,351 +1,133 @@
|
|||||||
import { useEffect } from "react";
|
import { BsMicrosoftTeams, BsSlack } from "react-icons/bs";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { faCheckCircle } from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { Link } from "@tanstack/react-router";
|
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
import { OrgPermissionCan } from "@app/components/permissions";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
ContentLoader,
|
DeleteActionModal,
|
||||||
DropdownMenu,
|
Table,
|
||||||
DropdownMenuContent,
|
TableContainer,
|
||||||
DropdownMenuItem,
|
TBody,
|
||||||
DropdownMenuTrigger,
|
Td,
|
||||||
EmptyState,
|
THead,
|
||||||
FormControl,
|
Tr
|
||||||
Input,
|
|
||||||
Select,
|
|
||||||
SelectItem,
|
|
||||||
Switch
|
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
import { OrgPermissionActions, OrgPermissionSubjects, useWorkspace } from "@app/context";
|
||||||
import {
|
import { usePopUp } from "@app/hooks";
|
||||||
useGetSlackIntegrationChannels,
|
import { useDeleteProjectWorkflowIntegration } from "@app/hooks/api";
|
||||||
useGetSlackIntegrations,
|
import { WorkflowIntegrationPlatform } from "@app/hooks/api/workflowIntegrations/types";
|
||||||
useGetWorkspaceSlackConfig,
|
|
||||||
useUpdateProjectSlackConfig
|
|
||||||
} from "@app/hooks/api";
|
|
||||||
|
|
||||||
const formSchema = z.object({
|
import { AddWorkflowIntegrationModal } from "./components/AddWorkflowIntegrationModal";
|
||||||
slackIntegrationId: z.string(),
|
import { EditWorkflowIntegrationModal } from "./components/EditWorkflowIntegrationModal";
|
||||||
isSecretRequestNotificationEnabled: z.boolean(),
|
import { MicrosoftTeamsConfigRow } from "./components/MicrosoftTeamsConfigRow";
|
||||||
secretRequestChannels: z.string().array(),
|
import { SlackConfigRow } from "./components/SlackConfigRow";
|
||||||
isAccessRequestNotificationEnabled: z.boolean(),
|
|
||||||
accessRequestChannels: z.string().array()
|
|
||||||
});
|
|
||||||
|
|
||||||
type TSlackConfigForm = z.infer<typeof formSchema>;
|
export const renderProvider = (integration: WorkflowIntegrationPlatform) => {
|
||||||
|
if (integration === WorkflowIntegrationPlatform.SLACK) {
|
||||||
|
return <BsSlack />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (integration === WorkflowIntegrationPlatform.MICROSOFT_TEAMS) {
|
||||||
|
return <BsMicrosoftTeams />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
export const WorkflowIntegrationTab = () => {
|
export const WorkflowIntegrationTab = () => {
|
||||||
|
const { popUp, handlePopUpOpen, handlePopUpToggle, handlePopUpClose } = usePopUp([
|
||||||
|
"addWorkflowIntegration",
|
||||||
|
"removeIntegration",
|
||||||
|
"editIntegration"
|
||||||
|
] as const);
|
||||||
|
|
||||||
const { currentWorkspace } = useWorkspace();
|
const { currentWorkspace } = useWorkspace();
|
||||||
const { data: slackConfig, isPending: isSlackConfigLoading } = useGetWorkspaceSlackConfig({
|
|
||||||
workspaceId: currentWorkspace?.id ?? ""
|
|
||||||
});
|
|
||||||
const { data: slackIntegrations } = useGetSlackIntegrations(currentWorkspace?.orgId);
|
|
||||||
const { mutateAsync: updateProjectSlackConfig } = useUpdateProjectSlackConfig();
|
|
||||||
|
|
||||||
const {
|
const { mutateAsync: deleteIntegration } = useDeleteProjectWorkflowIntegration();
|
||||||
control,
|
|
||||||
watch,
|
|
||||||
handleSubmit,
|
|
||||||
setValue,
|
|
||||||
formState: { isDirty, isSubmitting }
|
|
||||||
} = useForm<TSlackConfigForm>({
|
|
||||||
resolver: zodResolver(formSchema),
|
|
||||||
defaultValues: {
|
|
||||||
isAccessRequestNotificationEnabled: false,
|
|
||||||
accessRequestChannels: [],
|
|
||||||
isSecretRequestNotificationEnabled: false,
|
|
||||||
secretRequestChannels: []
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const secretRequestNotifState = watch("isSecretRequestNotificationEnabled");
|
const handleRemoveIntegration = async (
|
||||||
const selectedSlackIntegrationId = watch("slackIntegrationId");
|
integrationType: WorkflowIntegrationPlatform,
|
||||||
const accessRequestNotifState = watch("isAccessRequestNotificationEnabled");
|
integrationId: string
|
||||||
|
) => {
|
||||||
const { data: slackChannels } = useGetSlackIntegrationChannels(selectedSlackIntegrationId);
|
if (!currentWorkspace.id) {
|
||||||
const slackChannelIdToName = Object.fromEntries(
|
|
||||||
(slackChannels || []).map((channel) => [channel.id, channel.name])
|
|
||||||
);
|
|
||||||
const sortedSlackChannels = slackChannels?.sort((a, b) =>
|
|
||||||
a.name.toLowerCase().localeCompare(b.name.toLowerCase())
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleIntegrationSave = async (data: TSlackConfigForm) => {
|
|
||||||
if (!currentWorkspace) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await updateProjectSlackConfig({
|
await deleteIntegration({
|
||||||
workspaceId: currentWorkspace.id,
|
projectId: currentWorkspace?.id ?? "",
|
||||||
...data,
|
integration: integrationType,
|
||||||
accessRequestChannels: data.accessRequestChannels.filter(Boolean).join(", "),
|
integrationId
|
||||||
secretRequestChannels: data.secretRequestChannels.filter(Boolean).join(", ")
|
|
||||||
});
|
});
|
||||||
|
|
||||||
createNotification({
|
createNotification({
|
||||||
type: "success",
|
type: "success",
|
||||||
text: "Successfully updated slack integration"
|
text: `Successfully removed ${integrationType.replace("-", " ").replace(/\b\w/g, (char) => char.toUpperCase())} integration`
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
return (
|
||||||
if (slackConfig) {
|
|
||||||
setValue("slackIntegrationId", slackConfig.slackIntegrationId);
|
|
||||||
setValue(
|
|
||||||
"isSecretRequestNotificationEnabled",
|
|
||||||
slackConfig.isSecretRequestNotificationEnabled
|
|
||||||
);
|
|
||||||
setValue(
|
|
||||||
"isAccessRequestNotificationEnabled",
|
|
||||||
slackConfig.isAccessRequestNotificationEnabled
|
|
||||||
);
|
|
||||||
|
|
||||||
if (slackChannels) {
|
|
||||||
setValue(
|
|
||||||
"secretRequestChannels",
|
|
||||||
slackConfig.secretRequestChannels
|
|
||||||
.split(", ")
|
|
||||||
.filter((channel) => channel in slackChannelIdToName)
|
|
||||||
);
|
|
||||||
setValue(
|
|
||||||
"accessRequestChannels",
|
|
||||||
slackConfig.accessRequestChannels
|
|
||||||
.split(", ")
|
|
||||||
.filter((channel) => channel in slackChannelIdToName)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [slackConfig, slackChannels]);
|
|
||||||
|
|
||||||
if (isSlackConfigLoading) {
|
|
||||||
return <ContentLoader />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return !slackIntegrations?.length ? (
|
|
||||||
<EmptyState title="You do not have any integrations configured.">
|
|
||||||
<Link
|
|
||||||
to="/organization/settings"
|
|
||||||
search={{
|
|
||||||
selectedTab: "workflow-integrations"
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="mt-2 underline decoration-primary-800 underline-offset-4 duration-200 hover:cursor-pointer hover:text-mineshaft-100 hover:decoration-primary-600">
|
|
||||||
Create one now
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
</EmptyState>
|
|
||||||
) : (
|
|
||||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<h2 className="mb-2 flex-1 text-xl font-semibold text-mineshaft-100">Slack Integration</h2>
|
<p className="text-xl font-semibold text-mineshaft-100">Workflow Integrations</p>
|
||||||
|
<OrgPermissionCan I={OrgPermissionActions.Create} an={OrgPermissionSubjects.Settings}>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
handlePopUpOpen("addWorkflowIntegration");
|
||||||
|
}}
|
||||||
|
isDisabled={!isAllowed}
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
|
>
|
||||||
|
Add
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</OrgPermissionCan>
|
||||||
</div>
|
</div>
|
||||||
<p className="mb-4 text-gray-400">
|
<p className="mb-4 text-gray-400">
|
||||||
This integration allows you to send notifications to your Slack workspace in response to
|
Connect Infisical to other platforms for notification and workflow integrations.
|
||||||
events in your project.
|
|
||||||
</p>
|
</p>
|
||||||
<form onSubmit={handleSubmit(handleIntegrationSave)}>
|
<TableContainer>
|
||||||
<div className="max-w-md">
|
<Table>
|
||||||
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Settings}>
|
<THead>
|
||||||
{(isAllowed) => (
|
<Tr>
|
||||||
<Controller
|
<Td>Provider</Td>
|
||||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
<Td>Access Request Notifications</Td>
|
||||||
<FormControl errorText={error?.message} isError={Boolean(error)}>
|
<Td>Secret Request Notifications</Td>
|
||||||
<Select
|
<Td>Access Request Notifications Destination</Td>
|
||||||
{...field}
|
<Td>Secret Request Notifications Destination</Td>
|
||||||
isDisabled={!isAllowed}
|
<Td>Actions</Td>
|
||||||
placeholder="None"
|
</Tr>
|
||||||
onValueChange={onChange}
|
</THead>
|
||||||
defaultValue={slackConfig?.slackIntegrationId}
|
<TBody>
|
||||||
className="w-3/4 bg-mineshaft-600"
|
<SlackConfigRow handlePopUpOpen={handlePopUpOpen} />
|
||||||
>
|
<MicrosoftTeamsConfigRow handlePopUpOpen={handlePopUpOpen} />
|
||||||
{slackIntegrations?.map((slackIntegration) => (
|
</TBody>
|
||||||
<SelectItem
|
</Table>
|
||||||
value={slackIntegration.id}
|
</TableContainer>
|
||||||
key={`slack-integration-${slackIntegration.id}`}
|
<AddWorkflowIntegrationModal
|
||||||
>
|
isOpen={popUp.addWorkflowIntegration.isOpen}
|
||||||
{slackIntegration.slug}
|
onToggle={(state) => handlePopUpToggle("addWorkflowIntegration", state)}
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
control={control}
|
|
||||||
name="slackIntegrationId"
|
|
||||||
/>
|
/>
|
||||||
)}
|
<DeleteActionModal
|
||||||
</ProjectPermissionCan>
|
isOpen={popUp.removeIntegration.isOpen}
|
||||||
</div>
|
title="Are you sure want to remove this integration?"
|
||||||
{selectedSlackIntegrationId && (
|
onChange={(isOpen) => handlePopUpToggle("removeIntegration", isOpen)}
|
||||||
<>
|
deleteKey="confirm"
|
||||||
<h2 className="mb-2 flex-1 text-lg font-semibold text-mineshaft-100">Events</h2>
|
onDeleteApproved={async () => {
|
||||||
<Controller
|
await handleRemoveIntegration(
|
||||||
control={control}
|
popUp.removeIntegration.data?.integration,
|
||||||
name="isSecretRequestNotificationEnabled"
|
popUp.removeIntegration.data?.integrationId
|
||||||
render={({ field, fieldState: { error } }) => {
|
|
||||||
return (
|
|
||||||
<FormControl
|
|
||||||
isError={Boolean(error)}
|
|
||||||
errorText={error?.message}
|
|
||||||
className="mb-2 mt-3"
|
|
||||||
>
|
|
||||||
<Switch
|
|
||||||
id="secret-approval-notification"
|
|
||||||
onCheckedChange={(value) => field.onChange(value)}
|
|
||||||
isChecked={field.value}
|
|
||||||
>
|
|
||||||
<p className="w-full">Secret Approval Requests</p>
|
|
||||||
</Switch>
|
|
||||||
</FormControl>
|
|
||||||
);
|
);
|
||||||
|
handlePopUpClose("removeIntegration");
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{secretRequestNotifState && (
|
<EditWorkflowIntegrationModal
|
||||||
<Controller
|
isOpen={popUp.editIntegration.isOpen}
|
||||||
control={control}
|
onClose={() => handlePopUpClose("editIntegration")}
|
||||||
name="secretRequestChannels"
|
integration={popUp.editIntegration.data?.integration}
|
||||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
label="Slack channels"
|
|
||||||
isError={Boolean(error)}
|
|
||||||
errorText={error?.message}
|
|
||||||
>
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<Input
|
|
||||||
isReadOnly
|
|
||||||
value={value
|
|
||||||
?.filter(Boolean)
|
|
||||||
.map((entry) => slackChannelIdToName[entry])
|
|
||||||
.join(", ")}
|
|
||||||
className="text-left"
|
|
||||||
/>
|
/>
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent
|
|
||||||
style={{
|
|
||||||
width: "var(--radix-dropdown-menu-trigger-width)",
|
|
||||||
maxHeight: "350px",
|
|
||||||
overflowY: "auto"
|
|
||||||
}}
|
|
||||||
side="bottom"
|
|
||||||
align="start"
|
|
||||||
>
|
|
||||||
{sortedSlackChannels?.map((slackChannel) => {
|
|
||||||
const isChecked = value?.includes(slackChannel.id);
|
|
||||||
return (
|
|
||||||
<DropdownMenuItem
|
|
||||||
onClick={(evt) => {
|
|
||||||
evt.preventDefault();
|
|
||||||
onChange(
|
|
||||||
isChecked
|
|
||||||
? value?.filter((el: string) => el !== slackChannel.id)
|
|
||||||
: [...(value || []), slackChannel.id]
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
key={`secret-requests-slack-channel-${slackChannel.id}`}
|
|
||||||
iconPos="right"
|
|
||||||
icon={isChecked && <FontAwesomeIcon icon={faCheckCircle} />}
|
|
||||||
>
|
|
||||||
{slackChannel.name}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="isAccessRequestNotificationEnabled"
|
|
||||||
render={({ field, fieldState: { error } }) => {
|
|
||||||
return (
|
|
||||||
<FormControl isError={Boolean(error)} errorText={error?.message} className="mb-2">
|
|
||||||
<Switch
|
|
||||||
id="access-request-notification"
|
|
||||||
onCheckedChange={(value) => field.onChange(value)}
|
|
||||||
isChecked={field.value}
|
|
||||||
>
|
|
||||||
<p className="w-full">Access Requests</p>
|
|
||||||
</Switch>
|
|
||||||
</FormControl>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{accessRequestNotifState && (
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="accessRequestChannels"
|
|
||||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
label="Slack channels"
|
|
||||||
isError={Boolean(error)}
|
|
||||||
errorText={error?.message}
|
|
||||||
>
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<Input
|
|
||||||
isReadOnly
|
|
||||||
value={value
|
|
||||||
?.filter(Boolean)
|
|
||||||
.map((entry) => slackChannelIdToName[entry])
|
|
||||||
.join(", ")}
|
|
||||||
className="text-left"
|
|
||||||
/>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent
|
|
||||||
style={{
|
|
||||||
width: "var(--radix-dropdown-menu-trigger-width)",
|
|
||||||
maxHeight: "350px",
|
|
||||||
overflowY: "auto"
|
|
||||||
}}
|
|
||||||
side="bottom"
|
|
||||||
align="start"
|
|
||||||
>
|
|
||||||
{sortedSlackChannels?.map((slackChannel) => {
|
|
||||||
const isChecked = value?.includes(slackChannel.id);
|
|
||||||
return (
|
|
||||||
<DropdownMenuItem
|
|
||||||
onClick={(evt) => {
|
|
||||||
evt.preventDefault();
|
|
||||||
onChange(
|
|
||||||
isChecked
|
|
||||||
? value?.filter((el: string) => el !== slackChannel.id)
|
|
||||||
: [...(value || []), slackChannel.id]
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
key={`access-requests-slack-channel-${slackChannel.id}`}
|
|
||||||
iconPos="right"
|
|
||||||
icon={isChecked && <FontAwesomeIcon icon={faCheckCircle} />}
|
|
||||||
>
|
|
||||||
{slackChannel.name}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<Button
|
|
||||||
colorSchema="secondary"
|
|
||||||
className="mt-4"
|
|
||||||
type="submit"
|
|
||||||
isDisabled={!isDirty}
|
|
||||||
isLoading={isSubmitting}
|
|
||||||
>
|
|
||||||
Save
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+154
@@ -0,0 +1,154 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { BsMicrosoftTeams } from "react-icons/bs";
|
||||||
|
import { faSlack } from "@fortawesome/free-brands-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { AnimatePresence, motion } from "framer-motion";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
import { Modal, ModalContent } from "@app/components/v2";
|
||||||
|
import { useWorkspace } from "@app/context";
|
||||||
|
import { useGetWorkspaceWorkflowIntegrationConfig } from "@app/hooks/api";
|
||||||
|
import { WorkflowIntegrationPlatform } from "@app/hooks/api/workflowIntegrations/types";
|
||||||
|
|
||||||
|
import { MicrosoftTeamsIntegrationForm } from "./MicrosoftTeamsIntegrationForm";
|
||||||
|
import { SlackIntegrationForm } from "./SlackIntegrationForm";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
isOpen?: boolean;
|
||||||
|
onToggle: (isOpen: boolean) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
enum WizardSteps {
|
||||||
|
SelectPlatform = "select-platform",
|
||||||
|
PlatformInputs = "platform-inputs"
|
||||||
|
}
|
||||||
|
|
||||||
|
const PLATFORM_LIST = [
|
||||||
|
{
|
||||||
|
icon: <FontAwesomeIcon icon={faSlack} size="lg" />,
|
||||||
|
platform: WorkflowIntegrationPlatform.SLACK,
|
||||||
|
title: "Slack"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <BsMicrosoftTeams className="text-lg" />,
|
||||||
|
platform: WorkflowIntegrationPlatform.MICROSOFT_TEAMS,
|
||||||
|
title: "Microsoft Teams"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
export const AddWorkflowIntegrationModal = ({ isOpen, onToggle }: Props) => {
|
||||||
|
const [wizardStep, setWizardStep] = useState(WizardSteps.SelectPlatform);
|
||||||
|
const [selectedPlatform, setSelectedPlatform] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
const { data: microsoftTeamsConfig } = useGetWorkspaceWorkflowIntegrationConfig({
|
||||||
|
workspaceId: currentWorkspace?.id ?? "",
|
||||||
|
integration: WorkflowIntegrationPlatform.MICROSOFT_TEAMS
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data: slackConfig } = useGetWorkspaceWorkflowIntegrationConfig({
|
||||||
|
workspaceId: currentWorkspace?.id ?? "",
|
||||||
|
integration: WorkflowIntegrationPlatform.SLACK
|
||||||
|
});
|
||||||
|
|
||||||
|
const microsoftTeamsConfigured = !!microsoftTeamsConfig;
|
||||||
|
const slackConfigured = !!slackConfig;
|
||||||
|
|
||||||
|
const handleFormReset = (state: boolean = false) => {
|
||||||
|
onToggle(state);
|
||||||
|
setWizardStep(WizardSteps.SelectPlatform);
|
||||||
|
setSelectedPlatform(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} onOpenChange={(state) => handleFormReset(state)}>
|
||||||
|
<ModalContent
|
||||||
|
title={`Add a ${selectedPlatform?.replace("-", " ").replace(/\b\w/g, (char) => char.toUpperCase()) ?? "workflow"} integration`}
|
||||||
|
className="my-4"
|
||||||
|
>
|
||||||
|
<AnimatePresence mode="wait">
|
||||||
|
{wizardStep === WizardSteps.SelectPlatform && (
|
||||||
|
<motion.div
|
||||||
|
key="select-type-step"
|
||||||
|
transition={{ duration: 0.1 }}
|
||||||
|
initial={{ opacity: 0, translateX: 30 }}
|
||||||
|
animate={{ opacity: 1, translateX: 0 }}
|
||||||
|
exit={{ opacity: 0, translateX: -30 }}
|
||||||
|
>
|
||||||
|
<div className="mb-4 text-mineshaft-300">Select a workflow integration</div>
|
||||||
|
<div className="flex items-center space-x-4">
|
||||||
|
{PLATFORM_LIST.map(({ icon, platform, title }) => {
|
||||||
|
const isConfigured =
|
||||||
|
(platform === WorkflowIntegrationPlatform.MICROSOFT_TEAMS &&
|
||||||
|
microsoftTeamsConfigured) ||
|
||||||
|
(platform === WorkflowIntegrationPlatform.SLACK && slackConfigured);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative">
|
||||||
|
<div
|
||||||
|
key={platform}
|
||||||
|
className={twMerge(
|
||||||
|
"flex h-32 w-36 cursor-pointer flex-col items-center space-y-4 rounded border border-mineshaft-500 bg-bunker-600 p-6 transition-all hover:border-primary/70 hover:bg-primary/10 hover:text-white",
|
||||||
|
isConfigured && "border-primary/50 opacity-60"
|
||||||
|
)}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedPlatform(platform);
|
||||||
|
setWizardStep(WizardSteps.PlatformInputs);
|
||||||
|
}}
|
||||||
|
onKeyDown={(evt) => {
|
||||||
|
if (evt.key === "Enter") {
|
||||||
|
setSelectedPlatform(platform);
|
||||||
|
setWizardStep(WizardSteps.PlatformInputs);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div>{icon}</div>
|
||||||
|
<div className="whitespace-pre-wrap text-center text-sm">{title}</div>
|
||||||
|
</div>
|
||||||
|
{isConfigured && (
|
||||||
|
<div className="absolute bottom-0 left-0 right-0 z-30 h-full">
|
||||||
|
<div className="relative h-full">
|
||||||
|
<div className="absolute bottom-0 right-0 w-full flex-row items-center overflow-hidden whitespace-nowrap rounded-b-md bg-primary px-2 py-0.5 text-center text-xs text-black transition-all duration-300 group-hover:w-0 group-hover:p-0">
|
||||||
|
Already Configured
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{wizardStep === WizardSteps.PlatformInputs &&
|
||||||
|
selectedPlatform === WorkflowIntegrationPlatform.SLACK && (
|
||||||
|
<motion.div
|
||||||
|
key="platform-inputs-step"
|
||||||
|
transition={{ duration: 0.1 }}
|
||||||
|
initial={{ opacity: 0, translateX: 30 }}
|
||||||
|
animate={{ opacity: 1, translateX: 0 }}
|
||||||
|
exit={{ opacity: 0, translateX: -30 }}
|
||||||
|
>
|
||||||
|
<SlackIntegrationForm onClose={() => handleFormReset(false)} />
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
{wizardStep === WizardSteps.PlatformInputs &&
|
||||||
|
selectedPlatform === WorkflowIntegrationPlatform.MICROSOFT_TEAMS && (
|
||||||
|
<motion.div
|
||||||
|
key="platform-inputs-step"
|
||||||
|
transition={{ duration: 0.1 }}
|
||||||
|
initial={{ opacity: 0, translateX: 30 }}
|
||||||
|
animate={{ opacity: 1, translateX: 0 }}
|
||||||
|
exit={{ opacity: 0, translateX: -30 }}
|
||||||
|
>
|
||||||
|
<MicrosoftTeamsIntegrationForm onClose={() => handleFormReset(false)} />
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
+29
@@ -0,0 +1,29 @@
|
|||||||
|
import { Modal, ModalContent } from "@app/components/v2";
|
||||||
|
import { WorkflowIntegrationPlatform } from "@app/hooks/api/workflowIntegrations/types";
|
||||||
|
|
||||||
|
import { SlackIntegrationForm } from "./SlackIntegrationForm";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
isOpen?: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
|
||||||
|
integration: WorkflowIntegrationPlatform;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const EditWorkflowIntegrationModal = ({ isOpen, onClose, integration }: Props) => {
|
||||||
|
const handleFormReset = () => {
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} onOpenChange={handleFormReset}>
|
||||||
|
<ModalContent
|
||||||
|
title={`Edit ${integration?.replace("-", " ").replace(/\b\w/g, (char) => char.toUpperCase()) ?? "workflow"} integration`}
|
||||||
|
>
|
||||||
|
{integration === WorkflowIntegrationPlatform.SLACK && (
|
||||||
|
<SlackIntegrationForm onClose={handleFormReset} />
|
||||||
|
)}
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
+153
@@ -0,0 +1,153 @@
|
|||||||
|
import { BsMicrosoftTeams } from "react-icons/bs";
|
||||||
|
import { faCheck, faEllipsis, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
import { OrgPermissionCan } from "@app/components/permissions";
|
||||||
|
import {
|
||||||
|
Badge,
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
Spinner,
|
||||||
|
Td,
|
||||||
|
Tr
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { OrgPermissionActions, OrgPermissionSubjects, useWorkspace } from "@app/context";
|
||||||
|
import {
|
||||||
|
useGetMicrosoftTeamsIntegrationTeams,
|
||||||
|
useGetWorkspaceWorkflowIntegrationConfig
|
||||||
|
} from "@app/hooks/api";
|
||||||
|
import { WorkflowIntegrationPlatform } from "@app/hooks/api/workflowIntegrations/types";
|
||||||
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
handlePopUpOpen: (
|
||||||
|
popUpName: keyof UsePopUpState<["removeIntegration", "editIntegration"]>,
|
||||||
|
data?: {
|
||||||
|
integrationId?: string;
|
||||||
|
integration: WorkflowIntegrationPlatform;
|
||||||
|
}
|
||||||
|
) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MicrosoftTeamsConfigRow = ({ handlePopUpOpen }: Props) => {
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
const { data: microsoftTeamsConfig } = useGetWorkspaceWorkflowIntegrationConfig({
|
||||||
|
workspaceId: currentWorkspace?.id ?? "",
|
||||||
|
integration: WorkflowIntegrationPlatform.MICROSOFT_TEAMS
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data: microsoftTeamsChannels, isPending: isMicrosoftTeamsChannelsLoading } =
|
||||||
|
useGetMicrosoftTeamsIntegrationTeams(microsoftTeamsConfig?.integrationId);
|
||||||
|
const microsoftTeamsChannelIdToName = Object.fromEntries(
|
||||||
|
microsoftTeamsChannels?.flatMap((team) =>
|
||||||
|
team.channels.map((channel) => [channel.channelId, channel.channelName])
|
||||||
|
) ?? []
|
||||||
|
);
|
||||||
|
|
||||||
|
if (microsoftTeamsConfig?.integration !== WorkflowIntegrationPlatform.MICROSOFT_TEAMS) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("after");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tr>
|
||||||
|
<Td className="flex max-w-xs items-center overflow-hidden text-ellipsis">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<BsMicrosoftTeams />
|
||||||
|
Microsoft Teams
|
||||||
|
</div>
|
||||||
|
</Td>
|
||||||
|
<Td>
|
||||||
|
{microsoftTeamsConfig.isAccessRequestNotificationEnabled ? (
|
||||||
|
<FontAwesomeIcon icon={faCheck} className="text-green-500" />
|
||||||
|
) : (
|
||||||
|
<FontAwesomeIcon icon={faXmark} className="text-red-500" />
|
||||||
|
)}
|
||||||
|
</Td>
|
||||||
|
<Td>
|
||||||
|
{microsoftTeamsConfig.isSecretRequestNotificationEnabled ? (
|
||||||
|
<FontAwesomeIcon icon={faCheck} className="text-green-500" />
|
||||||
|
) : (
|
||||||
|
<FontAwesomeIcon icon={faXmark} className="text-red-500" />
|
||||||
|
)}
|
||||||
|
</Td>
|
||||||
|
<Td>
|
||||||
|
{isMicrosoftTeamsChannelsLoading ? (
|
||||||
|
<Spinner size="xs" />
|
||||||
|
) : (
|
||||||
|
<Badge>
|
||||||
|
{microsoftTeamsConfig.accessRequestChannels?.channelIds
|
||||||
|
?.map((channel) => microsoftTeamsChannelIdToName[channel])
|
||||||
|
.join(", ")}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</Td>
|
||||||
|
<Td>
|
||||||
|
{isMicrosoftTeamsChannelsLoading ? (
|
||||||
|
<Spinner size="xs" />
|
||||||
|
) : (
|
||||||
|
<Badge>
|
||||||
|
{microsoftTeamsConfig.secretRequestChannels?.channelIds
|
||||||
|
?.map((channel) => microsoftTeamsChannelIdToName[channel])
|
||||||
|
.join(", ")}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</Td>
|
||||||
|
|
||||||
|
<Td>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild className="rounded-lg">
|
||||||
|
<div className="flex justify-end hover:text-primary-400 data-[state=open]:text-primary-400">
|
||||||
|
<FontAwesomeIcon size="sm" icon={faEllipsis} />
|
||||||
|
</div>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="start" className="p-1">
|
||||||
|
<OrgPermissionCan I={OrgPermissionActions.Delete} an={OrgPermissionSubjects.Settings}>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<DropdownMenuItem
|
||||||
|
disabled={!isAllowed}
|
||||||
|
className={twMerge(
|
||||||
|
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
|
||||||
|
)}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
handlePopUpOpen("removeIntegration", {
|
||||||
|
integrationId: microsoftTeamsConfig.integrationId,
|
||||||
|
integration: WorkflowIntegrationPlatform.MICROSOFT_TEAMS
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
</OrgPermissionCan>
|
||||||
|
<OrgPermissionCan I={OrgPermissionActions.Edit} an={OrgPermissionSubjects.Settings}>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<DropdownMenuItem
|
||||||
|
disabled={!isAllowed}
|
||||||
|
className={twMerge(
|
||||||
|
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
|
||||||
|
)}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
handlePopUpOpen("editIntegration", {
|
||||||
|
integration: WorkflowIntegrationPlatform.MICROSOFT_TEAMS
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
</OrgPermissionCan>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
);
|
||||||
|
};
|
||||||
+540
@@ -0,0 +1,540 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import { faCheckCircle } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
FormControl,
|
||||||
|
Input,
|
||||||
|
Select,
|
||||||
|
SelectItem,
|
||||||
|
Switch
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { useWorkspace } from "@app/context";
|
||||||
|
import {
|
||||||
|
ProjectPermissionActions,
|
||||||
|
ProjectPermissionSub
|
||||||
|
} from "@app/context/ProjectPermissionContext";
|
||||||
|
import {
|
||||||
|
useGetMicrosoftTeamsIntegrations,
|
||||||
|
useGetMicrosoftTeamsIntegrationTeams,
|
||||||
|
useGetWorkspaceWorkflowIntegrationConfig,
|
||||||
|
useUpdateProjectWorkflowIntegrationConfig
|
||||||
|
} from "@app/hooks/api";
|
||||||
|
import { WorkflowIntegrationPlatform } from "@app/hooks/api/workflowIntegrations/types";
|
||||||
|
|
||||||
|
const formSchema = z
|
||||||
|
.object({
|
||||||
|
microsoftTeamsIntegrationId: z.string(),
|
||||||
|
isSecretRequestNotificationEnabled: z.boolean(),
|
||||||
|
isAccessRequestNotificationEnabled: z.boolean(),
|
||||||
|
secretRequestChannels: z.object({
|
||||||
|
teamId: z.string(),
|
||||||
|
channelIds: z.string().array()
|
||||||
|
}),
|
||||||
|
accessRequestChannels: z.object({
|
||||||
|
teamId: z.string(),
|
||||||
|
channelIds: z.string().array()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.superRefine((data, ctx) => {
|
||||||
|
if (data.isSecretRequestNotificationEnabled) {
|
||||||
|
if (!data.secretRequestChannels.teamId) {
|
||||||
|
ctx.addIssue({
|
||||||
|
path: ["secretRequestChannels", "teamId"],
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message: "Team is required"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data.secretRequestChannels.channelIds.length) {
|
||||||
|
ctx.addIssue({
|
||||||
|
path: ["secretRequestChannels", "channelIds"],
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message: "At least one channel is required"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.isAccessRequestNotificationEnabled) {
|
||||||
|
if (!data.accessRequestChannels.teamId) {
|
||||||
|
ctx.addIssue({
|
||||||
|
path: ["accessRequestChannels", "teamId"],
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message: "Team is required"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data.accessRequestChannels.channelIds.length) {
|
||||||
|
ctx.addIssue({
|
||||||
|
path: ["accessRequestChannels", "channelIds"],
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message: "At least one channel is required"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
type TSlackConfigForm = z.infer<typeof formSchema>;
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onClose: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MicrosoftTeamsIntegrationForm = ({ onClose }: Props) => {
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
const { data: microsoftTeamsConfig } = useGetWorkspaceWorkflowIntegrationConfig({
|
||||||
|
workspaceId: currentWorkspace?.id ?? "",
|
||||||
|
integration: WorkflowIntegrationPlatform.MICROSOFT_TEAMS
|
||||||
|
});
|
||||||
|
const { data: microsoftTeamsIntegrations } = useGetMicrosoftTeamsIntegrations(
|
||||||
|
currentWorkspace?.orgId
|
||||||
|
);
|
||||||
|
|
||||||
|
const { mutateAsync: updateProjectMicrosoftTeamsConfig } =
|
||||||
|
useUpdateProjectWorkflowIntegrationConfig();
|
||||||
|
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
watch,
|
||||||
|
handleSubmit,
|
||||||
|
setValue,
|
||||||
|
formState: { isDirty, isSubmitting }
|
||||||
|
} = useForm<TSlackConfigForm>({
|
||||||
|
resolver: zodResolver(formSchema),
|
||||||
|
defaultValues: {
|
||||||
|
isAccessRequestNotificationEnabled: false,
|
||||||
|
isSecretRequestNotificationEnabled: false,
|
||||||
|
accessRequestChannels: {
|
||||||
|
teamId: "",
|
||||||
|
channelIds: []
|
||||||
|
},
|
||||||
|
secretRequestChannels: {
|
||||||
|
teamId: "",
|
||||||
|
channelIds: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleIntegrationSave = async (data: TSlackConfigForm) => {
|
||||||
|
try {
|
||||||
|
if (!currentWorkspace) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await updateProjectMicrosoftTeamsConfig({
|
||||||
|
workspaceId: currentWorkspace.id,
|
||||||
|
isAccessRequestNotificationEnabled: data.isAccessRequestNotificationEnabled,
|
||||||
|
isSecretRequestNotificationEnabled: data.isSecretRequestNotificationEnabled,
|
||||||
|
...(data.isAccessRequestNotificationEnabled && {
|
||||||
|
accessRequestChannels: data.accessRequestChannels
|
||||||
|
}),
|
||||||
|
...(data.isSecretRequestNotificationEnabled && {
|
||||||
|
secretRequestChannels: data.secretRequestChannels
|
||||||
|
}),
|
||||||
|
integration: WorkflowIntegrationPlatform.MICROSOFT_TEAMS,
|
||||||
|
integrationId: data.microsoftTeamsIntegrationId
|
||||||
|
});
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
type: "success",
|
||||||
|
text: "Successfully created microsoft teams integration"
|
||||||
|
});
|
||||||
|
|
||||||
|
onClose();
|
||||||
|
} catch {
|
||||||
|
createNotification({
|
||||||
|
type: "error",
|
||||||
|
text: "Failed to create microsoft teams integration"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectedAccessRequestTeamId = watch("accessRequestChannels.teamId");
|
||||||
|
const selectedSecretRequestTeamId = watch("secretRequestChannels.teamId");
|
||||||
|
|
||||||
|
console.log("selectedAccessRequestTeamId", selectedAccessRequestTeamId);
|
||||||
|
console.log("selectedSecretRequestTeamId", selectedSecretRequestTeamId);
|
||||||
|
|
||||||
|
const selectedMicrosoftTeamsIntegrationId = watch("microsoftTeamsIntegrationId");
|
||||||
|
|
||||||
|
const accessRequestNotificationsEnabled = watch("isAccessRequestNotificationEnabled");
|
||||||
|
const secretRequestNotificationsEnabled = watch("isSecretRequestNotificationEnabled");
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: microsoftTeamsIntegrationTeams,
|
||||||
|
isPending: isLoadingMicrosoftTeamsIntegrationTeams
|
||||||
|
} = useGetMicrosoftTeamsIntegrationTeams(selectedMicrosoftTeamsIntegrationId);
|
||||||
|
|
||||||
|
const sortedMicrosoftTeamsIntegrationTeams = microsoftTeamsIntegrationTeams?.sort((a, b) =>
|
||||||
|
a.teamName.toLowerCase().localeCompare(b.teamName.toLowerCase())
|
||||||
|
);
|
||||||
|
|
||||||
|
const selectableAccessRequestChannelIds = sortedMicrosoftTeamsIntegrationTeams
|
||||||
|
?.filter((team) => team.teamId === selectedAccessRequestTeamId)
|
||||||
|
.map((team) => team.channels)
|
||||||
|
.flat();
|
||||||
|
const selectableSecretRequestChannelIds = sortedMicrosoftTeamsIntegrationTeams
|
||||||
|
?.filter((team) => team.teamId === selectedSecretRequestTeamId)
|
||||||
|
.map((team) => team.channels)
|
||||||
|
.flat();
|
||||||
|
|
||||||
|
console.log("selectableAccessRequestChannelIds", selectableAccessRequestChannelIds);
|
||||||
|
console.log("selectableSecretRequestChannelIds", selectableSecretRequestChannelIds);
|
||||||
|
|
||||||
|
const channelIdToName = [
|
||||||
|
...(selectableAccessRequestChannelIds || []),
|
||||||
|
...(selectableSecretRequestChannelIds || [])
|
||||||
|
].reduce(
|
||||||
|
(acc, channel) => {
|
||||||
|
acc[channel.channelId] = channel.channelName;
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{} as Record<string, string>
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (microsoftTeamsConfig) {
|
||||||
|
setValue("microsoftTeamsIntegrationId", microsoftTeamsConfig.integrationId);
|
||||||
|
setValue(
|
||||||
|
"isSecretRequestNotificationEnabled",
|
||||||
|
microsoftTeamsConfig.isSecretRequestNotificationEnabled
|
||||||
|
);
|
||||||
|
setValue(
|
||||||
|
"isAccessRequestNotificationEnabled",
|
||||||
|
microsoftTeamsConfig.isAccessRequestNotificationEnabled
|
||||||
|
);
|
||||||
|
|
||||||
|
if (microsoftTeamsConfig.integration === WorkflowIntegrationPlatform.MICROSOFT_TEAMS) {
|
||||||
|
if (microsoftTeamsConfig.secretRequestChannels) {
|
||||||
|
setValue("secretRequestChannels", microsoftTeamsConfig.secretRequestChannels);
|
||||||
|
setValue("accessRequestChannels", microsoftTeamsConfig.accessRequestChannels);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [microsoftTeamsConfig]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit(handleIntegrationSave)}>
|
||||||
|
<div className="flex w-full flex-col justify-start">
|
||||||
|
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Settings}>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="microsoftTeamsIntegrationId"
|
||||||
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Microsoft Teams Integration"
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
{...field}
|
||||||
|
isDisabled={!isAllowed}
|
||||||
|
placeholder="None"
|
||||||
|
onValueChange={onChange}
|
||||||
|
className="w-full"
|
||||||
|
defaultValue={microsoftTeamsConfig?.integrationId}
|
||||||
|
>
|
||||||
|
{microsoftTeamsIntegrations?.map((microsoftTeamsIntegration) => (
|
||||||
|
<SelectItem
|
||||||
|
value={microsoftTeamsIntegration.id}
|
||||||
|
key={`microsoft-teams-integration-${microsoftTeamsIntegration.id}`}
|
||||||
|
>
|
||||||
|
{microsoftTeamsIntegration.slug}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</ProjectPermissionCan>
|
||||||
|
</div>
|
||||||
|
{selectedMicrosoftTeamsIntegrationId && (
|
||||||
|
<>
|
||||||
|
<h2 className="mb-2 flex-1 text-sm text-mineshaft-400">Configure Events</h2>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="isSecretRequestNotificationEnabled"
|
||||||
|
render={({ field, fieldState: { error } }) => {
|
||||||
|
return (
|
||||||
|
<FormControl
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
className="mb-2 mt-3"
|
||||||
|
>
|
||||||
|
<Switch
|
||||||
|
id="secret-approval-notification"
|
||||||
|
onCheckedChange={(value) => field.onChange(value)}
|
||||||
|
isChecked={field.value}
|
||||||
|
>
|
||||||
|
<p className="w-full">Secret Approval Requests</p>
|
||||||
|
</Switch>
|
||||||
|
</FormControl>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{secretRequestNotificationsEnabled && (
|
||||||
|
<>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="secretRequestChannels.teamId"
|
||||||
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Secret Approval Requests Notifications Team"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
{...field}
|
||||||
|
placeholder={
|
||||||
|
isLoadingMicrosoftTeamsIntegrationTeams ? "Loading..." : "Select a team..."
|
||||||
|
}
|
||||||
|
className="w-full"
|
||||||
|
onValueChange={(value) => onChange(value)}
|
||||||
|
isLoading={isLoadingMicrosoftTeamsIntegrationTeams}
|
||||||
|
>
|
||||||
|
{!isLoadingMicrosoftTeamsIntegrationTeams &&
|
||||||
|
sortedMicrosoftTeamsIntegrationTeams?.map((team) => (
|
||||||
|
<SelectItem
|
||||||
|
key={`secret-requests-microsoft-teams-team-${team.teamId}`}
|
||||||
|
value={team.teamId}
|
||||||
|
>
|
||||||
|
{team.teamName}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="secretRequestChannels.channelIds"
|
||||||
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Microsoft Teams channels"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger
|
||||||
|
className={
|
||||||
|
isLoadingMicrosoftTeamsIntegrationTeams || !selectedSecretRequestTeamId
|
||||||
|
? "opacity-50"
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
asChild
|
||||||
|
disabled={
|
||||||
|
isLoadingMicrosoftTeamsIntegrationTeams || !selectedSecretRequestTeamId
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{selectedSecretRequestTeamId ? (
|
||||||
|
<Input
|
||||||
|
isReadOnly
|
||||||
|
value={
|
||||||
|
isLoadingMicrosoftTeamsIntegrationTeams
|
||||||
|
? "Loading..."
|
||||||
|
: value
|
||||||
|
?.filter(Boolean)
|
||||||
|
.map((entry) => channelIdToName[entry])
|
||||||
|
.join(", ")
|
||||||
|
}
|
||||||
|
className="text-left"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Input
|
||||||
|
isReadOnly
|
||||||
|
value="Select a team..."
|
||||||
|
className="py-2 text-left text-sm"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent
|
||||||
|
style={{
|
||||||
|
width: "var(--radix-dropdown-menu-trigger-width)",
|
||||||
|
maxHeight: "350px",
|
||||||
|
overflowY: "auto"
|
||||||
|
}}
|
||||||
|
side="bottom"
|
||||||
|
align="start"
|
||||||
|
>
|
||||||
|
{selectableSecretRequestChannelIds?.map((channel) => {
|
||||||
|
const isChecked = value?.includes(channel.channelId);
|
||||||
|
return (
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={(evt) => {
|
||||||
|
evt.preventDefault();
|
||||||
|
onChange(
|
||||||
|
isChecked
|
||||||
|
? value?.filter((el: string) => el !== channel.channelId)
|
||||||
|
: [...(value || []), channel.channelId]
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
key={`secret-requests-slack-channel-${channel.channelId}`}
|
||||||
|
iconPos="right"
|
||||||
|
icon={isChecked && <FontAwesomeIcon icon={faCheckCircle} />}
|
||||||
|
>
|
||||||
|
{channel.channelName}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="isAccessRequestNotificationEnabled"
|
||||||
|
render={({ field, fieldState: { error } }) => {
|
||||||
|
return (
|
||||||
|
<FormControl isError={Boolean(error)} errorText={error?.message} className="mb-2">
|
||||||
|
<Switch
|
||||||
|
id="access-request-notification"
|
||||||
|
onCheckedChange={(value) => field.onChange(value)}
|
||||||
|
isChecked={field.value}
|
||||||
|
>
|
||||||
|
<p className="w-full">Access Requests</p>
|
||||||
|
</Switch>
|
||||||
|
</FormControl>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{accessRequestNotificationsEnabled && (
|
||||||
|
<>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="accessRequestChannels.teamId"
|
||||||
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Access Requests Notifications Team"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
{...field}
|
||||||
|
placeholder={
|
||||||
|
isLoadingMicrosoftTeamsIntegrationTeams ? "Loading..." : "Select a team..."
|
||||||
|
}
|
||||||
|
className="w-full"
|
||||||
|
onValueChange={(value) => onChange(value)}
|
||||||
|
isLoading={isLoadingMicrosoftTeamsIntegrationTeams}
|
||||||
|
>
|
||||||
|
{!isLoadingMicrosoftTeamsIntegrationTeams &&
|
||||||
|
sortedMicrosoftTeamsIntegrationTeams?.map((team) => (
|
||||||
|
<SelectItem
|
||||||
|
key={`access-requests-microsoft-teams-team-${team.teamId}`}
|
||||||
|
value={team.teamId}
|
||||||
|
>
|
||||||
|
{team.teamName}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="accessRequestChannels.channelIds"
|
||||||
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Microsoft Teams channels"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger
|
||||||
|
className={
|
||||||
|
isLoadingMicrosoftTeamsIntegrationTeams || !selectedAccessRequestTeamId
|
||||||
|
? "opacity-50"
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
asChild
|
||||||
|
disabled={
|
||||||
|
isLoadingMicrosoftTeamsIntegrationTeams || !selectedAccessRequestTeamId
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{selectedAccessRequestTeamId ? (
|
||||||
|
<Input
|
||||||
|
isReadOnly
|
||||||
|
value={
|
||||||
|
isLoadingMicrosoftTeamsIntegrationTeams
|
||||||
|
? "Loading..."
|
||||||
|
: value
|
||||||
|
?.filter(Boolean)
|
||||||
|
.map((entry) => channelIdToName[entry])
|
||||||
|
.join(", ")
|
||||||
|
}
|
||||||
|
className="text-left"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Input
|
||||||
|
isReadOnly
|
||||||
|
value="Select a team..."
|
||||||
|
className="py-2 text-left text-sm"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent
|
||||||
|
style={{
|
||||||
|
width: "var(--radix-dropdown-menu-trigger-width)",
|
||||||
|
maxHeight: "350px",
|
||||||
|
overflowY: "auto"
|
||||||
|
}}
|
||||||
|
side="bottom"
|
||||||
|
align="start"
|
||||||
|
>
|
||||||
|
{selectableAccessRequestChannelIds?.map((channel) => {
|
||||||
|
const isChecked = value?.includes(channel.channelId);
|
||||||
|
return (
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={(evt) => {
|
||||||
|
evt.preventDefault();
|
||||||
|
onChange(
|
||||||
|
isChecked
|
||||||
|
? value?.filter((el: string) => el !== channel.channelId)
|
||||||
|
: [...(value || []), channel.channelId]
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
key={`access-requests-slack-channel-${channel.channelId}`}
|
||||||
|
iconPos="right"
|
||||||
|
icon={isChecked && <FontAwesomeIcon icon={faCheckCircle} />}
|
||||||
|
>
|
||||||
|
{channel.channelName}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
colorSchema="secondary"
|
||||||
|
className="mt-4"
|
||||||
|
type="submit"
|
||||||
|
isDisabled={!isDirty}
|
||||||
|
isLoading={isSubmitting}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
+143
@@ -0,0 +1,143 @@
|
|||||||
|
import { BsSlack } from "react-icons/bs";
|
||||||
|
import { faCheck, faEllipsis, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
import { OrgPermissionCan } from "@app/components/permissions";
|
||||||
|
import {
|
||||||
|
Badge,
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
Td,
|
||||||
|
Tr
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { OrgPermissionActions, OrgPermissionSubjects, useWorkspace } from "@app/context";
|
||||||
|
import {
|
||||||
|
useGetSlackIntegrationChannels,
|
||||||
|
useGetWorkspaceWorkflowIntegrationConfig
|
||||||
|
} from "@app/hooks/api";
|
||||||
|
import { WorkflowIntegrationPlatform } from "@app/hooks/api/workflowIntegrations/types";
|
||||||
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
handlePopUpOpen: (
|
||||||
|
popUpName: keyof UsePopUpState<["removeIntegration", "editIntegration"]>,
|
||||||
|
data?: {
|
||||||
|
integrationId?: string;
|
||||||
|
integration: WorkflowIntegrationPlatform;
|
||||||
|
}
|
||||||
|
) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SlackConfigRow = ({ handlePopUpOpen }: Props) => {
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
const { data: slackConfig } = useGetWorkspaceWorkflowIntegrationConfig({
|
||||||
|
workspaceId: currentWorkspace?.id ?? "",
|
||||||
|
integration: WorkflowIntegrationPlatform.SLACK
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data: slackChannels } = useGetSlackIntegrationChannels(slackConfig?.integrationId);
|
||||||
|
const slackChannelIdToName = Object.fromEntries(
|
||||||
|
(slackChannels || []).map((channel) => [channel.id, channel.name])
|
||||||
|
);
|
||||||
|
|
||||||
|
if (slackConfig?.integration !== WorkflowIntegrationPlatform.SLACK) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("after slakc");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tr>
|
||||||
|
<Td className="flex max-w-xs items-center overflow-hidden text-ellipsis">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<BsSlack />
|
||||||
|
Slack
|
||||||
|
</div>
|
||||||
|
</Td>
|
||||||
|
<Td>
|
||||||
|
{slackConfig.isAccessRequestNotificationEnabled ? (
|
||||||
|
<FontAwesomeIcon icon={faCheck} className="text-green-500" />
|
||||||
|
) : (
|
||||||
|
<FontAwesomeIcon icon={faXmark} className="text-red-500" />
|
||||||
|
)}
|
||||||
|
</Td>
|
||||||
|
<Td>
|
||||||
|
{slackConfig.isSecretRequestNotificationEnabled ? (
|
||||||
|
<FontAwesomeIcon icon={faCheck} className="text-green-500" />
|
||||||
|
) : (
|
||||||
|
<FontAwesomeIcon icon={faXmark} className="text-red-500" />
|
||||||
|
)}
|
||||||
|
</Td>
|
||||||
|
<Td>
|
||||||
|
<Badge>
|
||||||
|
{slackConfig.accessRequestChannels
|
||||||
|
.split(", ")
|
||||||
|
.map((channel) => slackChannelIdToName[channel])
|
||||||
|
.join(", ")}
|
||||||
|
</Badge>
|
||||||
|
</Td>
|
||||||
|
<Td>
|
||||||
|
<Badge>
|
||||||
|
{slackConfig.secretRequestChannels
|
||||||
|
.split(", ")
|
||||||
|
.map((channel) => slackChannelIdToName[channel])
|
||||||
|
.join(", ")}
|
||||||
|
</Badge>
|
||||||
|
</Td>
|
||||||
|
|
||||||
|
<Td>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild className="rounded-lg">
|
||||||
|
<div className="flex justify-end hover:text-primary-400 data-[state=open]:text-primary-400">
|
||||||
|
<FontAwesomeIcon size="sm" icon={faEllipsis} />
|
||||||
|
</div>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="start" className="p-1">
|
||||||
|
<OrgPermissionCan I={OrgPermissionActions.Delete} an={OrgPermissionSubjects.Settings}>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<DropdownMenuItem
|
||||||
|
disabled={!isAllowed}
|
||||||
|
className={twMerge(
|
||||||
|
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
|
||||||
|
)}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
handlePopUpOpen("removeIntegration", {
|
||||||
|
integrationId: slackConfig.integrationId,
|
||||||
|
integration: WorkflowIntegrationPlatform.SLACK
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
</OrgPermissionCan>
|
||||||
|
<OrgPermissionCan I={OrgPermissionActions.Edit} an={OrgPermissionSubjects.Settings}>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<DropdownMenuItem
|
||||||
|
disabled={!isAllowed}
|
||||||
|
className={twMerge(
|
||||||
|
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
|
||||||
|
)}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
handlePopUpOpen("editIntegration", {
|
||||||
|
integration: WorkflowIntegrationPlatform.SLACK
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
</OrgPermissionCan>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
);
|
||||||
|
};
|
||||||
+356
@@ -0,0 +1,356 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import { faCheckCircle } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
FormControl,
|
||||||
|
Input,
|
||||||
|
Select,
|
||||||
|
SelectItem,
|
||||||
|
Switch
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { useWorkspace } from "@app/context";
|
||||||
|
import {
|
||||||
|
ProjectPermissionActions,
|
||||||
|
ProjectPermissionSub
|
||||||
|
} from "@app/context/ProjectPermissionContext";
|
||||||
|
import {
|
||||||
|
useGetSlackIntegrationChannels,
|
||||||
|
useGetWorkflowIntegrations,
|
||||||
|
useGetWorkspaceWorkflowIntegrationConfig,
|
||||||
|
useUpdateProjectWorkflowIntegrationConfig
|
||||||
|
} from "@app/hooks/api";
|
||||||
|
import { WorkflowIntegrationPlatform } from "@app/hooks/api/workflowIntegrations/types";
|
||||||
|
|
||||||
|
const formSchema = z.object({
|
||||||
|
slackIntegrationId: z.string(),
|
||||||
|
isSecretRequestNotificationEnabled: z.boolean(),
|
||||||
|
secretRequestChannels: z.string().array(),
|
||||||
|
isAccessRequestNotificationEnabled: z.boolean(),
|
||||||
|
accessRequestChannels: z.string().array()
|
||||||
|
});
|
||||||
|
|
||||||
|
type TSlackConfigForm = z.infer<typeof formSchema>;
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onClose: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SlackIntegrationForm = ({ onClose }: Props) => {
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
const { data: slackConfig } = useGetWorkspaceWorkflowIntegrationConfig({
|
||||||
|
workspaceId: currentWorkspace?.id ?? "",
|
||||||
|
integration: WorkflowIntegrationPlatform.SLACK
|
||||||
|
});
|
||||||
|
const { data: workflowIntegrations } = useGetWorkflowIntegrations(currentWorkspace?.orgId);
|
||||||
|
const { mutateAsync: updateProjectSlackConfig } = useUpdateProjectWorkflowIntegrationConfig();
|
||||||
|
|
||||||
|
const slackIntegrations = workflowIntegrations?.filter(
|
||||||
|
(integration) => integration.integration === WorkflowIntegrationPlatform.SLACK
|
||||||
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
watch,
|
||||||
|
handleSubmit,
|
||||||
|
setValue,
|
||||||
|
formState: { isDirty, isSubmitting }
|
||||||
|
} = useForm<TSlackConfigForm>({
|
||||||
|
resolver: zodResolver(formSchema),
|
||||||
|
defaultValues: {
|
||||||
|
isAccessRequestNotificationEnabled: false,
|
||||||
|
accessRequestChannels: [],
|
||||||
|
isSecretRequestNotificationEnabled: false,
|
||||||
|
secretRequestChannels: []
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleIntegrationSave = async (data: TSlackConfigForm) => {
|
||||||
|
try {
|
||||||
|
if (!currentWorkspace) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await updateProjectSlackConfig({
|
||||||
|
...data,
|
||||||
|
workspaceId: currentWorkspace.id,
|
||||||
|
integration: WorkflowIntegrationPlatform.SLACK,
|
||||||
|
integrationId: data.slackIntegrationId,
|
||||||
|
accessRequestChannels: data.accessRequestChannels.filter(Boolean).join(", "),
|
||||||
|
secretRequestChannels: data.secretRequestChannels.filter(Boolean).join(", ")
|
||||||
|
});
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
type: "success",
|
||||||
|
text: "Successfully created slack integration"
|
||||||
|
});
|
||||||
|
|
||||||
|
onClose();
|
||||||
|
} catch {
|
||||||
|
createNotification({
|
||||||
|
type: "error",
|
||||||
|
text: "Failed to create slack integration"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const secretRequestNotifState = watch("isSecretRequestNotificationEnabled");
|
||||||
|
const selectedSlackIntegrationId = watch("slackIntegrationId");
|
||||||
|
const accessRequestNotifState = watch("isAccessRequestNotificationEnabled");
|
||||||
|
|
||||||
|
const { data: slackChannels } = useGetSlackIntegrationChannels(selectedSlackIntegrationId);
|
||||||
|
const slackChannelIdToName = Object.fromEntries(
|
||||||
|
(slackChannels || []).map((channel) => [channel.id, channel.name])
|
||||||
|
);
|
||||||
|
const sortedSlackChannels = slackChannels?.sort((a, b) =>
|
||||||
|
a.name.toLowerCase().localeCompare(b.name.toLowerCase())
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (slackConfig) {
|
||||||
|
setValue("slackIntegrationId", slackConfig.integrationId);
|
||||||
|
setValue(
|
||||||
|
"isSecretRequestNotificationEnabled",
|
||||||
|
slackConfig.isSecretRequestNotificationEnabled
|
||||||
|
);
|
||||||
|
setValue(
|
||||||
|
"isAccessRequestNotificationEnabled",
|
||||||
|
slackConfig.isAccessRequestNotificationEnabled
|
||||||
|
);
|
||||||
|
|
||||||
|
if (slackConfig.integration === WorkflowIntegrationPlatform.SLACK) {
|
||||||
|
if (slackChannels) {
|
||||||
|
setValue(
|
||||||
|
"secretRequestChannels",
|
||||||
|
slackConfig.secretRequestChannels
|
||||||
|
.split(", ")
|
||||||
|
.filter((channel) => channel in slackChannelIdToName)
|
||||||
|
);
|
||||||
|
setValue(
|
||||||
|
"accessRequestChannels",
|
||||||
|
slackConfig.accessRequestChannels
|
||||||
|
.split(", ")
|
||||||
|
.filter((channel) => channel in slackChannelIdToName)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [slackConfig, slackChannels]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit(handleIntegrationSave)}>
|
||||||
|
<div className="flex w-full flex-col justify-start">
|
||||||
|
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Settings}>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="slackIntegrationId"
|
||||||
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Slack Integration"
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
{...field}
|
||||||
|
isDisabled={!isAllowed}
|
||||||
|
placeholder="None"
|
||||||
|
onValueChange={(val) => {
|
||||||
|
console.log("Selecting ", val);
|
||||||
|
onChange(val);
|
||||||
|
}}
|
||||||
|
className="w-full"
|
||||||
|
defaultValue={slackConfig?.integrationId}
|
||||||
|
>
|
||||||
|
{slackIntegrations?.map((slackIntegration) => (
|
||||||
|
<SelectItem
|
||||||
|
value={slackIntegration.id}
|
||||||
|
key={`slack-integration-${slackIntegration.id}`}
|
||||||
|
>
|
||||||
|
{slackIntegration.slug}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</ProjectPermissionCan>
|
||||||
|
</div>
|
||||||
|
{selectedSlackIntegrationId && (
|
||||||
|
<>
|
||||||
|
<h2 className="mb-2 flex-1 text-sm text-mineshaft-400">Configure Events</h2>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="isSecretRequestNotificationEnabled"
|
||||||
|
render={({ field, fieldState: { error } }) => {
|
||||||
|
return (
|
||||||
|
<FormControl
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
className="mb-2 mt-3"
|
||||||
|
>
|
||||||
|
<Switch
|
||||||
|
id="secret-approval-notification"
|
||||||
|
onCheckedChange={(value) => field.onChange(value)}
|
||||||
|
isChecked={field.value}
|
||||||
|
>
|
||||||
|
<p className="w-full">Secret Approval Requests</p>
|
||||||
|
</Switch>
|
||||||
|
</FormControl>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{secretRequestNotifState && (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="secretRequestChannels"
|
||||||
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Slack channels"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Input
|
||||||
|
isReadOnly
|
||||||
|
value={value
|
||||||
|
?.filter(Boolean)
|
||||||
|
.map((entry) => slackChannelIdToName[entry])
|
||||||
|
.join(", ")}
|
||||||
|
className="text-left"
|
||||||
|
/>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent
|
||||||
|
style={{
|
||||||
|
width: "var(--radix-dropdown-menu-trigger-width)",
|
||||||
|
maxHeight: "350px",
|
||||||
|
overflowY: "auto"
|
||||||
|
}}
|
||||||
|
side="bottom"
|
||||||
|
align="start"
|
||||||
|
>
|
||||||
|
{sortedSlackChannels?.map((slackChannel) => {
|
||||||
|
const isChecked = value?.includes(slackChannel.id);
|
||||||
|
return (
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={(evt) => {
|
||||||
|
evt.preventDefault();
|
||||||
|
onChange(
|
||||||
|
isChecked
|
||||||
|
? value?.filter((el: string) => el !== slackChannel.id)
|
||||||
|
: [...(value || []), slackChannel.id]
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
key={`secret-requests-slack-channel-${slackChannel.id}`}
|
||||||
|
iconPos="right"
|
||||||
|
icon={isChecked && <FontAwesomeIcon icon={faCheckCircle} />}
|
||||||
|
>
|
||||||
|
{slackChannel.name}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="isAccessRequestNotificationEnabled"
|
||||||
|
render={({ field, fieldState: { error } }) => {
|
||||||
|
return (
|
||||||
|
<FormControl isError={Boolean(error)} errorText={error?.message} className="mb-2">
|
||||||
|
<Switch
|
||||||
|
id="access-request-notification"
|
||||||
|
onCheckedChange={(value) => field.onChange(value)}
|
||||||
|
isChecked={field.value}
|
||||||
|
>
|
||||||
|
<p className="w-full">Access Requests</p>
|
||||||
|
</Switch>
|
||||||
|
</FormControl>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{accessRequestNotifState && (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="accessRequestChannels"
|
||||||
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Slack channels"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Input
|
||||||
|
isReadOnly
|
||||||
|
value={value
|
||||||
|
?.filter(Boolean)
|
||||||
|
.map((entry) => slackChannelIdToName[entry])
|
||||||
|
.join(", ")}
|
||||||
|
className="text-left"
|
||||||
|
/>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent
|
||||||
|
style={{
|
||||||
|
width: "var(--radix-dropdown-menu-trigger-width)",
|
||||||
|
maxHeight: "350px",
|
||||||
|
overflowY: "auto"
|
||||||
|
}}
|
||||||
|
side="bottom"
|
||||||
|
align="start"
|
||||||
|
>
|
||||||
|
{sortedSlackChannels?.map((slackChannel) => {
|
||||||
|
const isChecked = value?.includes(slackChannel.id);
|
||||||
|
return (
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={(evt) => {
|
||||||
|
evt.preventDefault();
|
||||||
|
onChange(
|
||||||
|
isChecked
|
||||||
|
? value?.filter((el: string) => el !== slackChannel.id)
|
||||||
|
: [...(value || []), slackChannel.id]
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
key={`access-requests-slack-channel-${slackChannel.id}`}
|
||||||
|
iconPos="right"
|
||||||
|
icon={isChecked && <FontAwesomeIcon icon={faCheckCircle} />}
|
||||||
|
>
|
||||||
|
{slackChannel.name}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
colorSchema="secondary"
|
||||||
|
className="mt-4"
|
||||||
|
type="submit"
|
||||||
|
isDisabled={!isDirty}
|
||||||
|
isLoading={isSubmitting}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.11/MicrosoftTeams.schema.json",
|
||||||
|
"manifestVersion": "1.11",
|
||||||
|
"id": "78c44cc5-275f-468d-862d-4191485e5267",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"name": {
|
||||||
|
"short": "Infisical",
|
||||||
|
"full": "Infisical"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"short": "Infisical",
|
||||||
|
"full": "Infisical"
|
||||||
|
},
|
||||||
|
"icons": {
|
||||||
|
"outline": "outline.png",
|
||||||
|
"color": "color.png"
|
||||||
|
},
|
||||||
|
"accentColor": "#FFFFFF",
|
||||||
|
"developer": {
|
||||||
|
"name": "Infisical Inc.",
|
||||||
|
"websiteUrl": "https://infisical.com",
|
||||||
|
"privacyUrl": "https://infisical.com/privacy",
|
||||||
|
"termsOfUseUrl": "https://infisical.com/terms"
|
||||||
|
},
|
||||||
|
"webApplicationInfo": {
|
||||||
|
"id": "78c44cc5-275f-468d-862d-4191485e5267",
|
||||||
|
"resource": "api://78c44cc5-275f-468d-862d-4191485e5267"
|
||||||
|
},
|
||||||
|
"bots": [
|
||||||
|
{
|
||||||
|
"botId": "78c44cc5-275f-468d-862d-4191485e5267",
|
||||||
|
"scopes": ["team", "personal"],
|
||||||
|
"isNotificationOnly": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"validDomains": [
|
||||||
|
"*.infisical.com",
|
||||||
|
"*danielinfisical.onmicrosoft.com"
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user