Merge pull request #3486 from Infisical/daniel/ms-teams-integration
feat(workflow-integrations): microsoft teams
@@ -24,3 +24,5 @@ frontend/src/hooks/api/secretRotationsV2/types/index.ts:generic-api-key:65
|
|||||||
frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretRotationListView/SecretRotationItem.tsx:generic-api-key:26
|
frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretRotationListView/SecretRotationItem.tsx:generic-api-key:26
|
||||||
docs/documentation/platform/kms/overview.mdx:generic-api-key:281
|
docs/documentation/platform/kms/overview.mdx:generic-api-key:281
|
||||||
docs/documentation/platform/kms/overview.mdx:generic-api-key:344
|
docs/documentation/platform/kms/overview.mdx:generic-api-key:344
|
||||||
|
docs/cli/commands/user.mdx:generic-api-key:51
|
||||||
|
frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretOverviewTableRow.tsx:generic-api-key:76
|
||||||
@@ -59,6 +59,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",
|
||||||
@@ -10011,9 +10041,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": "*"
|
||||||
}
|
}
|
||||||
@@ -10398,6 +10429,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",
|
||||||
@@ -11150,6 +11190,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",
|
||||||
@@ -12012,6 +12058,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",
|
||||||
@@ -12139,6 +12424,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",
|
||||||
@@ -12791,6 +13091,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",
|
||||||
@@ -12872,6 +13181,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",
|
||||||
@@ -13011,6 +13326,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",
|
||||||
@@ -13029,11 +13372,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": {
|
||||||
@@ -13093,6 +13440,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",
|
||||||
@@ -13166,6 +13522,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",
|
||||||
@@ -13174,6 +13571,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",
|
||||||
@@ -14494,6 +14905,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",
|
||||||
@@ -15678,6 +16116,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",
|
||||||
@@ -16191,6 +16648,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",
|
||||||
@@ -16718,7 +17208,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"
|
||||||
},
|
},
|
||||||
@@ -18767,16 +19256,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"
|
||||||
@@ -18801,6 +19307,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",
|
||||||
@@ -20854,6 +21366,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",
|
||||||
@@ -21728,15 +22258,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",
|
||||||
@@ -23520,7 +24041,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"
|
||||||
}
|
}
|
||||||
@@ -24951,9 +25471,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",
|
||||||
|
|||||||
@@ -100,6 +100,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 {
|
||||||
@@ -246,6 +247,7 @@ declare module "fastify" {
|
|||||||
kmipOperation: TKmipOperationServiceFactory;
|
kmipOperation: TKmipOperationServiceFactory;
|
||||||
gateway: TGatewayServiceFactory;
|
gateway: TGatewayServiceFactory;
|
||||||
secretRotationV2: TSecretRotationV2ServiceFactory;
|
secretRotationV2: TSecretRotationV2ServiceFactory;
|
||||||
|
microsoftTeams: TMicrosoftTeamsServiceFactory;
|
||||||
assumePrivileges: TAssumePrivilegeServiceFactory;
|
assumePrivileges: TAssumePrivilegeServiceFactory;
|
||||||
githubOrgSync: TGithubOrgSyncServiceFactory;
|
githubOrgSync: TGithubOrgSyncServiceFactory;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -426,6 +426,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";
|
||||||
import {
|
import {
|
||||||
TSecretReminderRecipients,
|
TSecretReminderRecipients,
|
||||||
TSecretReminderRecipientsInsert,
|
TSecretReminderRecipientsInsert,
|
||||||
@@ -1002,6 +1012,16 @@ declare module "knex/types/tables" {
|
|||||||
TSecretRotationV2SecretMappingsInsert,
|
TSecretRotationV2SecretMappingsInsert,
|
||||||
TSecretRotationV2SecretMappingsUpdate
|
TSecretRotationV2SecretMappingsUpdate
|
||||||
>;
|
>;
|
||||||
|
[TableName.MicrosoftTeamsIntegrations]: KnexOriginal.CompositeTableType<
|
||||||
|
TMicrosoftTeamsIntegrations,
|
||||||
|
TMicrosoftTeamsIntegrationsInsert,
|
||||||
|
TMicrosoftTeamsIntegrationsUpdate
|
||||||
|
>;
|
||||||
|
[TableName.ProjectMicrosoftTeamsConfigs]: KnexOriginal.CompositeTableType<
|
||||||
|
TProjectMicrosoftTeamsConfigs,
|
||||||
|
TProjectMicrosoftTeamsConfigsInsert,
|
||||||
|
TProjectMicrosoftTeamsConfigsUpdate
|
||||||
|
>;
|
||||||
[TableName.SecretReminderRecipients]: KnexOriginal.CompositeTableType<
|
[TableName.SecretReminderRecipients]: KnexOriginal.CompositeTableType<
|
||||||
TSecretReminderRecipients,
|
TSecretReminderRecipients,
|
||||||
TSecretReminderRecipientsInsert,
|
TSecretReminderRecipientsInsert,
|
||||||
|
|||||||
@@ -0,0 +1,130 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
const superAdminHasEncryptedMicrosoftTeamsClientIdColumn = await knex.schema.hasColumn(
|
||||||
|
TableName.SuperAdmin,
|
||||||
|
"encryptedMicrosoftTeamsAppId"
|
||||||
|
);
|
||||||
|
const superAdminHasEncryptedMicrosoftTeamsClientSecret = await knex.schema.hasColumn(
|
||||||
|
TableName.SuperAdmin,
|
||||||
|
"encryptedMicrosoftTeamsClientSecret"
|
||||||
|
);
|
||||||
|
const superAdminHasEncryptedMicrosoftTeamsBotId = await knex.schema.hasColumn(
|
||||||
|
TableName.SuperAdmin,
|
||||||
|
"encryptedMicrosoftTeamsBotId"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
!superAdminHasEncryptedMicrosoftTeamsClientIdColumn ||
|
||||||
|
!superAdminHasEncryptedMicrosoftTeamsClientSecret ||
|
||||||
|
!superAdminHasEncryptedMicrosoftTeamsBotId
|
||||||
|
) {
|
||||||
|
await knex.schema.alterTable(TableName.SuperAdmin, (table) => {
|
||||||
|
if (!superAdminHasEncryptedMicrosoftTeamsClientIdColumn) {
|
||||||
|
table.binary("encryptedMicrosoftTeamsAppId").nullable();
|
||||||
|
}
|
||||||
|
if (!superAdminHasEncryptedMicrosoftTeamsClientSecret) {
|
||||||
|
table.binary("encryptedMicrosoftTeamsClientSecret").nullable();
|
||||||
|
}
|
||||||
|
if (!superAdminHasEncryptedMicrosoftTeamsBotId) {
|
||||||
|
table.binary("encryptedMicrosoftTeamsBotId").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.timestamp("accessTokenExpiresAt").nullable();
|
||||||
|
table.timestamp("botAccessTokenExpiresAt").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,
|
||||||
|
"encryptedMicrosoftTeamsAppId"
|
||||||
|
);
|
||||||
|
const hasEncryptedMicrosoftTeamsClientSecret = await knex.schema.hasColumn(
|
||||||
|
TableName.SuperAdmin,
|
||||||
|
"encryptedMicrosoftTeamsClientSecret"
|
||||||
|
);
|
||||||
|
const hasEncryptedMicrosoftTeamsBotId = await knex.schema.hasColumn(
|
||||||
|
TableName.SuperAdmin,
|
||||||
|
"encryptedMicrosoftTeamsBotId"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
hasEncryptedMicrosoftTeamsClientIdColumn ||
|
||||||
|
hasEncryptedMicrosoftTeamsClientSecret ||
|
||||||
|
hasEncryptedMicrosoftTeamsBotId
|
||||||
|
) {
|
||||||
|
await knex.schema.alterTable(TableName.SuperAdmin, (table) => {
|
||||||
|
if (hasEncryptedMicrosoftTeamsClientIdColumn) {
|
||||||
|
table.dropColumn("encryptedMicrosoftTeamsAppId");
|
||||||
|
}
|
||||||
|
if (hasEncryptedMicrosoftTeamsClientSecret) {
|
||||||
|
table.dropColumn("encryptedMicrosoftTeamsClientSecret");
|
||||||
|
}
|
||||||
|
if (hasEncryptedMicrosoftTeamsBotId) {
|
||||||
|
table.dropColumn("encryptedMicrosoftTeamsBotId");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -58,6 +58,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";
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
// 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(),
|
||||||
|
accessTokenExpiresAt: z.date().nullable().optional(),
|
||||||
|
botAccessTokenExpiresAt: z.date().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>
|
||||||
|
>;
|
||||||
@@ -147,6 +147,8 @@ export enum TableName {
|
|||||||
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",
|
||||||
SecretReminderRecipients = "secret_reminder_recipients",
|
SecretReminderRecipients = "secret_reminder_recipients",
|
||||||
GithubOrgSyncConfig = "github_org_sync_configs"
|
GithubOrgSyncConfig = "github_org_sync_configs"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
>;
|
||||||
@@ -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,24 +225,30 @@ 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({
|
await triggerWorkflowIntegrationNotification({
|
||||||
projectId: project.id,
|
input: {
|
||||||
projectSlackConfigDAL,
|
notification: {
|
||||||
projectDAL,
|
type: TriggerFeature.ACCESS_REQUEST,
|
||||||
kmsService,
|
payload: {
|
||||||
notification: {
|
projectName: project.name,
|
||||||
type: SlackTriggerFeature.ACCESS_REQUEST,
|
requesterFullName,
|
||||||
payload: {
|
isTemporary,
|
||||||
projectName: project.name,
|
requesterEmail: requestedByUser.email as string,
|
||||||
requesterFullName,
|
secretPath,
|
||||||
isTemporary,
|
environment: envSlug,
|
||||||
requesterEmail: requestedByUser.email as string,
|
permissions: accessTypes,
|
||||||
secretPath,
|
approvalUrl,
|
||||||
environment: envSlug,
|
note
|
||||||
permissions: accessTypes,
|
}
|
||||||
approvalUrl,
|
},
|
||||||
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";
|
||||||
@@ -244,11 +245,14 @@ export enum EventType {
|
|||||||
GET_CERTIFICATE_TEMPLATE_EST_CONFIG = "get-certificate-template-est-config",
|
GET_CERTIFICATE_TEMPLATE_EST_CONFIG = "get-certificate-template-est-config",
|
||||||
ATTEMPT_CREATE_SLACK_INTEGRATION = "attempt-create-slack-integration",
|
ATTEMPT_CREATE_SLACK_INTEGRATION = "attempt-create-slack-integration",
|
||||||
ATTEMPT_REINSTALL_SLACK_INTEGRATION = "attempt-reinstall-slack-integration",
|
ATTEMPT_REINSTALL_SLACK_INTEGRATION = "attempt-reinstall-slack-integration",
|
||||||
|
GET_PROJECT_SLACK_CONFIG = "get-project-slack-config",
|
||||||
|
UPDATE_PROJECT_SLACK_CONFIG = "update-project-slack-config",
|
||||||
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",
|
||||||
|
|
||||||
GET_PROJECT_SSH_CONFIG = "get-project-ssh-config",
|
GET_PROJECT_SSH_CONFIG = "get-project-ssh-config",
|
||||||
UPDATE_PROJECT_SSH_CONFIG = "update-project-ssh-config",
|
UPDATE_PROJECT_SSH_CONFIG = "update-project-ssh-config",
|
||||||
INTEGRATION_SYNCED = "integration-synced",
|
INTEGRATION_SYNCED = "integration-synced",
|
||||||
@@ -321,6 +325,15 @@ export enum EventType {
|
|||||||
SECRET_ROTATION_ROTATE_SECRETS = "secret-rotation-rotate-secrets",
|
SECRET_ROTATION_ROTATE_SECRETS = "secret-rotation-rotate-secrets",
|
||||||
|
|
||||||
PROJECT_ACCESS_REQUEST = "project-access-request",
|
PROJECT_ACCESS_REQUEST = "project-access-request",
|
||||||
|
|
||||||
|
MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_CREATE = "microsoft-teams-workflow-integration-create",
|
||||||
|
MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_DELETE = "microsoft-teams-workflow-integration-delete",
|
||||||
|
MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_UPDATE = "microsoft-teams-workflow-integration-update",
|
||||||
|
MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_CHECK_INSTALLATION_STATUS = "microsoft-teams-workflow-integration-check-installation-status",
|
||||||
|
MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_GET_TEAMS = "microsoft-teams-workflow-integration-get-teams",
|
||||||
|
MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_GET = "microsoft-teams-workflow-integration-get",
|
||||||
|
MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_LIST = "microsoft-teams-workflow-integration-list",
|
||||||
|
|
||||||
PROJECT_ASSUME_PRIVILEGE_SESSION_START = "project-assume-privileges-session-start",
|
PROJECT_ASSUME_PRIVILEGE_SESSION_START = "project-assume-privileges-session-start",
|
||||||
PROJECT_ASSUME_PRIVILEGE_SESSION_END = "project-assume-privileges-session-end"
|
PROJECT_ASSUME_PRIVILEGE_SESSION_END = "project-assume-privileges-session-end"
|
||||||
}
|
}
|
||||||
@@ -1980,22 +1993,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;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2561,6 +2576,66 @@ interface RotateSecretRotationEvent {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface MicrosoftTeamsWorkflowIntegrationCreateEvent {
|
||||||
|
type: EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_CREATE;
|
||||||
|
metadata: {
|
||||||
|
tenantId: string;
|
||||||
|
slug: string;
|
||||||
|
description?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MicrosoftTeamsWorkflowIntegrationDeleteEvent {
|
||||||
|
type: EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_DELETE;
|
||||||
|
metadata: {
|
||||||
|
tenantId: string;
|
||||||
|
id: string;
|
||||||
|
slug: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MicrosoftTeamsWorkflowIntegrationCheckInstallationStatusEvent {
|
||||||
|
type: EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_CHECK_INSTALLATION_STATUS;
|
||||||
|
metadata: {
|
||||||
|
tenantId: string;
|
||||||
|
slug: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MicrosoftTeamsWorkflowIntegrationGetTeamsEvent {
|
||||||
|
type: EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_GET_TEAMS;
|
||||||
|
metadata: {
|
||||||
|
tenantId: string;
|
||||||
|
slug: string;
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MicrosoftTeamsWorkflowIntegrationGetEvent {
|
||||||
|
type: EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_GET;
|
||||||
|
metadata: {
|
||||||
|
tenantId: string;
|
||||||
|
slug: string;
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MicrosoftTeamsWorkflowIntegrationListEvent {
|
||||||
|
type: EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_LIST;
|
||||||
|
metadata: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MicrosoftTeamsWorkflowIntegrationUpdateEvent {
|
||||||
|
type: EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_UPDATE;
|
||||||
|
metadata: {
|
||||||
|
tenantId: string;
|
||||||
|
slug: string;
|
||||||
|
id: string;
|
||||||
|
newSlug?: string;
|
||||||
|
newDescription?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export type Event =
|
export type Event =
|
||||||
| GetSecretsEvent
|
| GetSecretsEvent
|
||||||
| GetSecretEvent
|
| GetSecretEvent
|
||||||
@@ -2723,8 +2798,8 @@ export type Event =
|
|||||||
| UpdateSlackIntegration
|
| UpdateSlackIntegration
|
||||||
| DeleteSlackIntegration
|
| DeleteSlackIntegration
|
||||||
| GetSlackIntegration
|
| GetSlackIntegration
|
||||||
| UpdateProjectSlackConfig
|
| UpdateProjectWorkflowIntegrationConfig
|
||||||
| GetProjectSlackConfig
|
| GetProjectWorkflowIntegrationConfig
|
||||||
| GetProjectSshConfig
|
| GetProjectSshConfig
|
||||||
| UpdateProjectSshConfig
|
| UpdateProjectSshConfig
|
||||||
| IntegrationSyncedEvent
|
| IntegrationSyncedEvent
|
||||||
@@ -2794,4 +2869,11 @@ export type Event =
|
|||||||
| CreateSecretRotationEvent
|
| CreateSecretRotationEvent
|
||||||
| UpdateSecretRotationEvent
|
| UpdateSecretRotationEvent
|
||||||
| DeleteSecretRotationEvent
|
| DeleteSecretRotationEvent
|
||||||
| RotateSecretRotationEvent;
|
| RotateSecretRotationEvent
|
||||||
|
| MicrosoftTeamsWorkflowIntegrationCreateEvent
|
||||||
|
| MicrosoftTeamsWorkflowIntegrationDeleteEvent
|
||||||
|
| MicrosoftTeamsWorkflowIntegrationCheckInstallationStatusEvent
|
||||||
|
| MicrosoftTeamsWorkflowIntegrationGetTeamsEvent
|
||||||
|
| MicrosoftTeamsWorkflowIntegrationGetEvent
|
||||||
|
| MicrosoftTeamsWorkflowIntegrationListEvent
|
||||||
|
| MicrosoftTeamsWorkflowIntegrationUpdateEvent;
|
||||||
|
|||||||
@@ -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,21 +1177,28 @@ 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({
|
|
||||||
projectId,
|
await triggerWorkflowIntegrationNotification({
|
||||||
projectDAL,
|
input: {
|
||||||
kmsService,
|
projectId,
|
||||||
projectSlackConfigDAL,
|
notification: {
|
||||||
notification: {
|
type: TriggerFeature.SECRET_APPROVAL,
|
||||||
type: SlackTriggerFeature.SECRET_APPROVAL,
|
payload: {
|
||||||
payload: {
|
userEmail: user.email as string,
|
||||||
userEmail: user.email as string,
|
environment: env.name,
|
||||||
environment: env.name,
|
secretPath,
|
||||||
secretPath,
|
projectId,
|
||||||
projectId,
|
requestId: secretApprovalRequest.id,
|
||||||
requestId: secretApprovalRequest.id,
|
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
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1503,21 +1516,28 @@ 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({
|
|
||||||
projectId,
|
await triggerWorkflowIntegrationNotification({
|
||||||
projectDAL,
|
input: {
|
||||||
kmsService,
|
projectId,
|
||||||
projectSlackConfigDAL,
|
notification: {
|
||||||
notification: {
|
type: TriggerFeature.SECRET_APPROVAL,
|
||||||
type: SlackTriggerFeature.SECRET_APPROVAL,
|
payload: {
|
||||||
payload: {
|
userEmail: user.email as string,
|
||||||
userEmail: user.email as string,
|
environment: env.name,
|
||||||
environment: env.name,
|
secretPath,
|
||||||
secretPath,
|
projectId,
|
||||||
projectId,
|
requestId: secretApprovalRequest.id,
|
||||||
requestId: secretApprovalRequest.id,
|
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
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export const BaseSecretRotationSchema = (type: SecretRotation) =>
|
|||||||
// unique to provider
|
// unique to provider
|
||||||
type: true,
|
type: true,
|
||||||
parameters: true,
|
parameters: true,
|
||||||
secretMappings: true
|
secretsMapping: true
|
||||||
}).extend({
|
}).extend({
|
||||||
connection: z.object({
|
connection: z.object({
|
||||||
app: z.literal(SECRET_ROTATION_CONNECTION_MAP[type]),
|
app: z.literal(SECRET_ROTATION_CONNECTION_MAP[type]),
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
import { validateMicrosoftTeamsChannelsSchema } 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");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (microsoftTeamsConfig) {
|
||||||
|
if (notification.type === TriggerFeature.ACCESS_REQUEST) {
|
||||||
|
if (microsoftTeamsConfig.isAccessRequestNotificationEnabled && microsoftTeamsConfig.accessRequestChannels) {
|
||||||
|
const { success, data } = validateMicrosoftTeamsChannelsSchema.safeParse(
|
||||||
|
microsoftTeamsConfig.accessRequestChannels
|
||||||
|
);
|
||||||
|
|
||||||
|
if (success && data) {
|
||||||
|
await microsoftTeamsService
|
||||||
|
.sendNotification({
|
||||||
|
notification,
|
||||||
|
target: data,
|
||||||
|
tenantId: microsoftTeamsConfig.tenantId,
|
||||||
|
microsoftTeamsIntegrationId: microsoftTeamsConfig.id,
|
||||||
|
orgId: project.orgId
|
||||||
|
})
|
||||||
|
.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 } = validateMicrosoftTeamsChannelsSchema.safeParse(
|
||||||
|
microsoftTeamsConfig.secretRequestChannels
|
||||||
|
);
|
||||||
|
|
||||||
|
if (success && data) {
|
||||||
|
await microsoftTeamsService
|
||||||
|
.sendNotification({
|
||||||
|
notification,
|
||||||
|
target: data,
|
||||||
|
tenantId: microsoftTeamsConfig.tenantId,
|
||||||
|
microsoftTeamsIntegrationId: microsoftTeamsConfig.id,
|
||||||
|
orgId: project.orgId
|
||||||
|
})
|
||||||
|
.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,11 @@ export const injectIdentity = fp(async (server: FastifyZodProvider) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Authentication is handled on a route-level here.
|
||||||
|
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;
|
||||||
|
|||||||
@@ -174,6 +174,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";
|
||||||
@@ -426,6 +429,8 @@ export const registerRoutes = async (
|
|||||||
const githubOrgSyncDAL = githubOrgSyncDALFactory(db);
|
const githubOrgSyncDAL = githubOrgSyncDALFactory(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,
|
||||||
@@ -687,6 +692,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,
|
||||||
@@ -700,7 +714,8 @@ export const registerRoutes = async (
|
|||||||
orgService,
|
orgService,
|
||||||
keyStore,
|
keyStore,
|
||||||
licenseService,
|
licenseService,
|
||||||
kmsService
|
kmsService,
|
||||||
|
microsoftTeamsService
|
||||||
});
|
});
|
||||||
|
|
||||||
const orgAdminService = orgAdminServiceFactory({
|
const orgAdminService = orgAdminServiceFactory({
|
||||||
@@ -1026,6 +1041,8 @@ export const registerRoutes = async (
|
|||||||
certificateTemplateDAL,
|
certificateTemplateDAL,
|
||||||
projectSlackConfigDAL,
|
projectSlackConfigDAL,
|
||||||
slackIntegrationDAL,
|
slackIntegrationDAL,
|
||||||
|
projectMicrosoftTeamsConfigDAL,
|
||||||
|
microsoftTeamsIntegrationDAL,
|
||||||
projectTemplateService,
|
projectTemplateService,
|
||||||
groupProjectDAL,
|
groupProjectDAL,
|
||||||
smtpService
|
smtpService
|
||||||
@@ -1150,7 +1167,9 @@ export const registerRoutes = async (
|
|||||||
userDAL,
|
userDAL,
|
||||||
licenseService,
|
licenseService,
|
||||||
projectSlackConfigDAL,
|
projectSlackConfigDAL,
|
||||||
resourceMetadataDAL
|
resourceMetadataDAL,
|
||||||
|
projectMicrosoftTeamsConfigDAL,
|
||||||
|
microsoftTeamsService
|
||||||
});
|
});
|
||||||
|
|
||||||
const secretService = secretServiceFactory({
|
const secretService = secretServiceFactory({
|
||||||
@@ -1212,7 +1231,9 @@ export const registerRoutes = async (
|
|||||||
accessApprovalPolicyApproverDAL,
|
accessApprovalPolicyApproverDAL,
|
||||||
projectSlackConfigDAL,
|
projectSlackConfigDAL,
|
||||||
kmsService,
|
kmsService,
|
||||||
groupDAL
|
groupDAL,
|
||||||
|
microsoftTeamsService,
|
||||||
|
projectMicrosoftTeamsConfigDAL
|
||||||
});
|
});
|
||||||
|
|
||||||
const secretReplicationService = secretReplicationServiceFactory({
|
const secretReplicationService = secretReplicationServiceFactory({
|
||||||
@@ -1610,6 +1631,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", {
|
||||||
@@ -1702,6 +1724,7 @@ export const registerRoutes = async (
|
|||||||
kmipOperation: kmipOperationService,
|
kmipOperation: kmipOperationService,
|
||||||
gateway: gatewayService,
|
gateway: gatewayService,
|
||||||
secretRotationV2: secretRotationV2Service,
|
secretRotationV2: secretRotationV2Service,
|
||||||
|
microsoftTeams: microsoftTeamsService,
|
||||||
assumePrivileges: assumePrivilegeService,
|
assumePrivileges: assumePrivilegeService,
|
||||||
githubOrgSync: githubOrgSyncConfigService
|
githubOrgSync: githubOrgSyncConfigService
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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({
|
||||||
clientId: z.string(),
|
slack: z.object({
|
||||||
clientSecret: z.string()
|
clientId: 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,381 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { MicrosoftTeamsIntegrationsSchema, WorkflowIntegrationsSchema } from "@app/db/schemas";
|
||||||
|
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
|
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: "GET",
|
||||||
|
url: "/client-id",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
clientId: z.string()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const clientId = await server.services.microsoftTeams.getClientId({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
clientId
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
body: z.object({
|
||||||
|
redirectUri: z.string(),
|
||||||
|
tenantId: z.string().uuid(),
|
||||||
|
slug: z.string(),
|
||||||
|
description: z.string().optional(),
|
||||||
|
code: z.string().trim()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
await server.services.microsoftTeams.completeMicrosoftTeamsIntegration({
|
||||||
|
tenantId: req.body.tenantId,
|
||||||
|
slug: req.body.slug,
|
||||||
|
description: req.body.description,
|
||||||
|
redirectUri: req.body.redirectUri,
|
||||||
|
code: req.body.code,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
orgId: req.permission.orgId,
|
||||||
|
event: {
|
||||||
|
type: EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_CREATE,
|
||||||
|
metadata: {
|
||||||
|
tenantId: req.body.tenantId,
|
||||||
|
slug: req.body.slug,
|
||||||
|
description: req.body.description
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
orgId: req.permission.orgId,
|
||||||
|
event: {
|
||||||
|
type: EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_LIST,
|
||||||
|
metadata: {}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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) => {
|
||||||
|
const microsoftTeamsIntegration = 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
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
orgId: req.permission.orgId,
|
||||||
|
event: {
|
||||||
|
type: EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_CHECK_INSTALLATION_STATUS,
|
||||||
|
metadata: {
|
||||||
|
tenantId: microsoftTeamsIntegration.tenantId,
|
||||||
|
slug: microsoftTeamsIntegration.slug
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
orgId: req.permission.orgId,
|
||||||
|
event: {
|
||||||
|
type: EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_DELETE,
|
||||||
|
metadata: {
|
||||||
|
tenantId: deletedMicrosoftTeamsIntegration.tenantId,
|
||||||
|
slug: deletedMicrosoftTeamsIntegration.slug,
|
||||||
|
id: deletedMicrosoftTeamsIntegration.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
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
orgId: req.permission.orgId,
|
||||||
|
event: {
|
||||||
|
type: EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_GET,
|
||||||
|
metadata: {
|
||||||
|
slug: microsoftTeamsIntegration.slug,
|
||||||
|
id: microsoftTeamsIntegration.id,
|
||||||
|
tenantId: microsoftTeamsIntegration.tenantId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
orgId: req.permission.orgId,
|
||||||
|
event: {
|
||||||
|
type: EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_UPDATE,
|
||||||
|
metadata: {
|
||||||
|
slug: microsoftTeamsIntegration.slug,
|
||||||
|
id: microsoftTeamsIntegration.id,
|
||||||
|
tenantId: microsoftTeamsIntegration.tenantId,
|
||||||
|
newSlug: req.body.slug,
|
||||||
|
newDescription: req.body.description
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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 microsoftTeamsIntegration = 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
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
orgId: req.permission.orgId,
|
||||||
|
event: {
|
||||||
|
type: EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_GET_TEAMS,
|
||||||
|
metadata: {
|
||||||
|
tenantId: microsoftTeamsIntegration.tenantId,
|
||||||
|
slug: microsoftTeamsIntegration.slug,
|
||||||
|
id: microsoftTeamsIntegration.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return microsoftTeamsIntegration.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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -14,6 +14,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 { ApiDocsTags, PROJECTS } from "@app/lib/api-docs";
|
import { ApiDocsTags, PROJECTS } from "@app/lib/api-docs";
|
||||||
import { CharacterType, characterValidator } from "@app/lib/validator/validate-string";
|
import { CharacterType, characterValidator } from "@app/lib/validator/validate-string";
|
||||||
@@ -21,8 +22,10 @@ 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 { validateMicrosoftTeamsChannelsSchema } 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 { 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";
|
||||||
@@ -740,55 +743,112 @@ 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", [
|
||||||
id: true,
|
ProjectSlackConfigsSchema.pick({
|
||||||
slackIntegrationId: true,
|
id: 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]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const config = await server.services.project.getProjectWorkflowIntegrationConfig({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
projectId: req.params.workspaceId,
|
||||||
|
integration: req.params.integration
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
projectId: req.params.workspaceId,
|
||||||
|
event: {
|
||||||
|
type: EventType.GET_PROJECT_WORKFLOW_INTEGRATION_CONFIG,
|
||||||
|
metadata: {
|
||||||
|
id: config.id,
|
||||||
|
integration: config.integration
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "DELETE",
|
||||||
|
url: "/:projectId/workflow-integration/:integration/:integrationId",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
projectId: z.string().trim(),
|
||||||
|
integration: z.nativeEnum(WorkflowIntegration),
|
||||||
|
integrationId: z.string()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
integrationConfig: z.object({
|
||||||
|
id: 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 deletedIntegration = await server.services.project.deleteProjectWorkflowIntegration({
|
||||||
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.projectId,
|
||||||
|
integration: req.params.integration,
|
||||||
|
integrationId: req.params.integrationId
|
||||||
});
|
});
|
||||||
|
|
||||||
if (slackConfig) {
|
return {
|
||||||
await server.services.auditLog.createAuditLog({
|
integrationConfig: deletedIntegration
|
||||||
...req.auditLogInfo,
|
};
|
||||||
projectId: req.params.workspaceId,
|
|
||||||
event: {
|
|
||||||
type: EventType.GET_PROJECT_SLACK_CONFIG,
|
|
||||||
metadata: {
|
|
||||||
id: slackConfig.id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return slackConfig;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
url: "/:workspaceId/slack-config",
|
url: "/:workspaceId/workflow-integration",
|
||||||
config: {
|
config: {
|
||||||
rateLimit: readLimit
|
rateLimit: readLimit
|
||||||
},
|
},
|
||||||
@@ -796,27 +856,57 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
params: z.object({
|
params: z.object({
|
||||||
workspaceId: z.string().trim()
|
workspaceId: z.string().trim()
|
||||||
}),
|
}),
|
||||||
body: z.object({
|
|
||||||
slackIntegrationId: z.string(),
|
body: z.discriminatedUnion("integration", [
|
||||||
isAccessRequestNotificationEnabled: z.boolean(),
|
z.object({
|
||||||
accessRequestChannels: validateSlackChannelsField,
|
integration: z.literal(WorkflowIntegration.SLACK),
|
||||||
isSecretRequestNotificationEnabled: z.boolean(),
|
integrationId: z.string(),
|
||||||
secretRequestChannels: validateSlackChannelsField
|
accessRequestChannels: validateSlackChannelsField,
|
||||||
}),
|
secretRequestChannels: validateSlackChannelsField,
|
||||||
response: {
|
isAccessRequestNotificationEnabled: z.boolean(),
|
||||||
200: ProjectSlackConfigsSchema.pick({
|
isSecretRequestNotificationEnabled: z.boolean()
|
||||||
id: true,
|
}),
|
||||||
slackIntegrationId: true,
|
z.object({
|
||||||
isAccessRequestNotificationEnabled: true,
|
integration: z.literal(WorkflowIntegration.MICROSOFT_TEAMS),
|
||||||
accessRequestChannels: true,
|
integrationId: z.string(),
|
||||||
isSecretRequestNotificationEnabled: true,
|
accessRequestChannels: validateMicrosoftTeamsChannelsSchema,
|
||||||
secretRequestChannels: true
|
secretRequestChannels: validateMicrosoftTeamsChannelsSchema,
|
||||||
|
isAccessRequestNotificationEnabled: z.boolean(),
|
||||||
|
isSecretRequestNotificationEnabled: z.boolean()
|
||||||
})
|
})
|
||||||
|
]),
|
||||||
|
response: {
|
||||||
|
200: z.discriminatedUnion("integration", [
|
||||||
|
ProjectSlackConfigsSchema.pick({
|
||||||
|
id: true,
|
||||||
|
isAccessRequestNotificationEnabled: true,
|
||||||
|
accessRequestChannels: true,
|
||||||
|
isSecretRequestNotificationEnabled: 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: validateMicrosoftTeamsChannelsSchema,
|
||||||
|
secretRequestChannels: validateMicrosoftTeamsChannelsSchema
|
||||||
|
})
|
||||||
|
)
|
||||||
|
])
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
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,
|
||||||
@@ -829,19 +919,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,706 @@
|
|||||||
|
/* eslint-disable class-methods-use-this */
|
||||||
|
import axios from "axios";
|
||||||
|
import { TeamsActivityHandler, TurnContext } from "botbuilder";
|
||||||
|
import jwt from "jsonwebtoken";
|
||||||
|
import { Knex } from "knex";
|
||||||
|
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 { TKmsServiceFactory } from "../kms/kms-service";
|
||||||
|
import { KmsDataKey } from "../kms/kms-types";
|
||||||
|
import { TWorkflowIntegrationDALFactory } from "../workflow-integration/workflow-integration-dal";
|
||||||
|
import { WorkflowIntegrationStatus } from "../workflow-integration/workflow-integration-types";
|
||||||
|
import { TMicrosoftTeamsIntegrationDALFactory } from "./microsoft-teams-integration-dal";
|
||||||
|
|
||||||
|
const ConsentError = "AADSTS65001";
|
||||||
|
|
||||||
|
export const verifyTenantFromCode = async (
|
||||||
|
tenantId: string,
|
||||||
|
code: string,
|
||||||
|
redirectUri: string,
|
||||||
|
clientId: string,
|
||||||
|
clientSecret: string
|
||||||
|
) => {
|
||||||
|
const getAccessToken = async (params: URLSearchParams) => {
|
||||||
|
const response = await axios
|
||||||
|
.post<{ access_token: string }>(`https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`, params, {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
if (axios.isAxiosError(err)) {
|
||||||
|
if ((err.response?.data as { error_description?: string })?.error_description?.includes(ConsentError)) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Unable to verify tenant, please ensure that you have granted admin consent."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
logger.error(err.response?.data, "Error fetching Microsoft Teams access token");
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.data.access_token;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Azure App-based auth
|
||||||
|
const applicationAccessToken = await getAccessToken(
|
||||||
|
new URLSearchParams({
|
||||||
|
client_id: clientId,
|
||||||
|
client_secret: clientSecret,
|
||||||
|
scope: "https://graph.microsoft.com/.default",
|
||||||
|
redirect_uri: redirectUri,
|
||||||
|
grant_type: "client_credentials"
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// User-based auth
|
||||||
|
const authorizationAccessToken = await getAccessToken(
|
||||||
|
new URLSearchParams({
|
||||||
|
client_id: clientId,
|
||||||
|
client_secret: clientSecret,
|
||||||
|
scope: "https://graph.microsoft.com/.default",
|
||||||
|
redirect_uri: redirectUri,
|
||||||
|
grant_type: "authorization_code",
|
||||||
|
code
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verify application token
|
||||||
|
const { tid: tenantIdFromApplicationAccessToken } = jwt.decode(applicationAccessToken) as { tid: string };
|
||||||
|
|
||||||
|
if (tenantIdFromApplicationAccessToken !== tenantId) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Invalid application token tenant ID. Expected ${tenantId}, got ${tenantIdFromApplicationAccessToken}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify user authorization token
|
||||||
|
const { tid: tenantIdFromAuthorizationAccessToken } = jwt.decode(authorizationAccessToken) as { tid: string };
|
||||||
|
|
||||||
|
if (tenantIdFromAuthorizationAccessToken !== tenantId) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Invalid authorization token tenant ID. Expected ${tenantId}, got ${tenantIdFromAuthorizationAccessToken}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getMicrosoftTeamsAccessToken = async (
|
||||||
|
{
|
||||||
|
orgId,
|
||||||
|
microsoftTeamsIntegrationId,
|
||||||
|
tenantId,
|
||||||
|
clientId,
|
||||||
|
clientSecret,
|
||||||
|
kmsService,
|
||||||
|
microsoftTeamsIntegrationDAL,
|
||||||
|
getBotFrameworkToken
|
||||||
|
}: {
|
||||||
|
microsoftTeamsIntegrationId: string;
|
||||||
|
orgId: string;
|
||||||
|
tenantId: string;
|
||||||
|
clientId: string;
|
||||||
|
clientSecret: string;
|
||||||
|
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||||
|
microsoftTeamsIntegrationDAL: Pick<TMicrosoftTeamsIntegrationDALFactory, "findOne" | "update">;
|
||||||
|
getBotFrameworkToken?: boolean;
|
||||||
|
},
|
||||||
|
tx?: Knex
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
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 integration = await microsoftTeamsIntegrationDAL.findOne(
|
||||||
|
{
|
||||||
|
id: microsoftTeamsIntegrationId
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!integration) {
|
||||||
|
throw new BadRequestError({ message: "Microsoft Teams integration not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (getBotFrameworkToken) {
|
||||||
|
// If the token expires within the next 5 minutes, we'll get a new token instead of using the stored one.
|
||||||
|
const currentTime = new Date(new Date().getTime() + 5 * 60 * 1000);
|
||||||
|
|
||||||
|
if (
|
||||||
|
integration.encryptedBotAccessToken &&
|
||||||
|
integration.botAccessTokenExpiresAt &&
|
||||||
|
integration.botAccessTokenExpiresAt > currentTime
|
||||||
|
) {
|
||||||
|
const { decryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
|
orgId,
|
||||||
|
type: KmsDataKey.Organization
|
||||||
|
});
|
||||||
|
|
||||||
|
const botAccessToken = decryptor({
|
||||||
|
cipherTextBlob: integration.encryptedBotAccessToken
|
||||||
|
});
|
||||||
|
|
||||||
|
return botAccessToken.toString();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// If the token expires within the next 5 minutes, we'll get a new token instead of using the stored one.
|
||||||
|
const currentTime = new Date(new Date().getTime() + 5 * 60 * 1000);
|
||||||
|
|
||||||
|
if (
|
||||||
|
integration.encryptedAccessToken &&
|
||||||
|
integration.accessTokenExpiresAt &&
|
||||||
|
integration.accessTokenExpiresAt > currentTime
|
||||||
|
) {
|
||||||
|
const { decryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
|
orgId,
|
||||||
|
type: KmsDataKey.Organization
|
||||||
|
});
|
||||||
|
|
||||||
|
const accessToken = decryptor({
|
||||||
|
cipherTextBlob: integration.encryptedAccessToken
|
||||||
|
});
|
||||||
|
|
||||||
|
return accessToken.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const tokenResponse = await axios.post<{ access_token: string; expires_in: number }>(
|
||||||
|
details.uri,
|
||||||
|
new URLSearchParams({
|
||||||
|
client_id: clientId,
|
||||||
|
client_secret: clientSecret,
|
||||||
|
scope: details.scope,
|
||||||
|
grant_type: "client_credentials"
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
if (getBotFrameworkToken) {
|
||||||
|
const { encryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
|
orgId,
|
||||||
|
type: KmsDataKey.Organization
|
||||||
|
});
|
||||||
|
|
||||||
|
const { cipherTextBlob: encryptedBotAccessToken } = encryptor({
|
||||||
|
plainText: Buffer.from(tokenResponse.data.access_token)
|
||||||
|
});
|
||||||
|
|
||||||
|
const expiresAt = new Date(new Date().getTime() + tokenResponse.data.expires_in * 1000);
|
||||||
|
|
||||||
|
await microsoftTeamsIntegrationDAL.update(
|
||||||
|
{
|
||||||
|
id: microsoftTeamsIntegrationId
|
||||||
|
},
|
||||||
|
{
|
||||||
|
botAccessTokenExpiresAt: expiresAt,
|
||||||
|
encryptedBotAccessToken
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const { encryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
|
orgId,
|
||||||
|
type: KmsDataKey.Organization
|
||||||
|
});
|
||||||
|
|
||||||
|
const { cipherTextBlob: encryptedAccessToken } = encryptor({
|
||||||
|
plainText: Buffer.from(tokenResponse.data.access_token)
|
||||||
|
});
|
||||||
|
|
||||||
|
const expiresAt = new Date(new Date().getTime() + tokenResponse.data.expires_in * 1000);
|
||||||
|
|
||||||
|
await microsoftTeamsIntegrationDAL.update(
|
||||||
|
{
|
||||||
|
id: microsoftTeamsIntegrationId
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessTokenExpiresAt: expiresAt,
|
||||||
|
encryptedAccessToken
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return tokenResponse.data.access_token;
|
||||||
|
} catch (error) {
|
||||||
|
if (axios.isAxiosError(error)) {
|
||||||
|
logger.error(
|
||||||
|
error.response?.data,
|
||||||
|
`getMicrosoftTeamsAccessToken: Error fetching Microsoft Teams access token [status-code=${error.response?.status}]`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
logger.error(error, "getMicrosoftTeamsAccessToken: Error fetching Microsoft Teams access token");
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isBotInstalledInTenant = async (
|
||||||
|
{
|
||||||
|
tenantId,
|
||||||
|
botAppId,
|
||||||
|
botAppPassword,
|
||||||
|
botId,
|
||||||
|
orgId,
|
||||||
|
kmsService,
|
||||||
|
microsoftTeamsIntegrationDAL,
|
||||||
|
microsoftTeamsIntegrationId
|
||||||
|
}: {
|
||||||
|
tenantId: string;
|
||||||
|
botAppId: string;
|
||||||
|
botAppPassword: string;
|
||||||
|
botId: string;
|
||||||
|
orgId: string;
|
||||||
|
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||||
|
microsoftTeamsIntegrationDAL: Pick<TMicrosoftTeamsIntegrationDALFactory, "findOne" | "update">;
|
||||||
|
microsoftTeamsIntegrationId: string;
|
||||||
|
},
|
||||||
|
tx?: Knex
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const botAccessToken = await getMicrosoftTeamsAccessToken(
|
||||||
|
{
|
||||||
|
tenantId,
|
||||||
|
clientId: botAppId.toString(),
|
||||||
|
clientSecret: botAppPassword.toString(),
|
||||||
|
getBotFrameworkToken: true,
|
||||||
|
orgId,
|
||||||
|
kmsService,
|
||||||
|
microsoftTeamsIntegrationDAL,
|
||||||
|
microsoftTeamsIntegrationId
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
).catch(() => null);
|
||||||
|
|
||||||
|
const accessToken = await getMicrosoftTeamsAccessToken(
|
||||||
|
{
|
||||||
|
orgId,
|
||||||
|
tenantId,
|
||||||
|
clientId: botAppId.toString(),
|
||||||
|
clientSecret: botAppPassword.toString(),
|
||||||
|
kmsService,
|
||||||
|
microsoftTeamsIntegrationDAL,
|
||||||
|
microsoftTeamsIntegrationId
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
).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
|
||||||
|
})
|
||||||
|
.catch(() => null);
|
||||||
|
|
||||||
|
if (microsoftTeamIntegration) {
|
||||||
|
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) {
|
||||||
|
logger.info(context, "Processing Microsoft Teams context");
|
||||||
|
await super.run(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendMessageToChannel(
|
||||||
|
botAccessToken: string,
|
||||||
|
tenantId: string,
|
||||||
|
channelId: string,
|
||||||
|
teamId: string,
|
||||||
|
notification: TNotification
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const { adaptiveCard } = buildTeamsPayload(notification);
|
||||||
|
|
||||||
|
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 ${botAccessToken}`,
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
if (axios.isAxiosError(error)) {
|
||||||
|
logger.error(
|
||||||
|
error.response?.data,
|
||||||
|
`sendMessageToChannel: Axios Error, Microsoft Teams Workflow Integration: Failed to send message to channel [channelId=${channelId}] [teamId=${teamId}] [tenantId=${tenantId}]`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
logger.error(
|
||||||
|
error,
|
||||||
|
`sendMessageToChannel: Microsoft Teams Workflow Integration: Failed to send message to channel [channelId=${channelId}] [teamId=${teamId}] [tenantId=${tenantId}]`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTeamsAndChannels(accessToken: string, internalAppId: string) {
|
||||||
|
try {
|
||||||
|
let teamsNextLink: string = "https://graph.microsoft.com/v1.0/teams";
|
||||||
|
|
||||||
|
let allTeams: { displayName: string; id: string }[] = [];
|
||||||
|
while (teamsNextLink?.length) {
|
||||||
|
try {
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
const response = await axios.get<{
|
||||||
|
value: { displayName: string; id: string }[];
|
||||||
|
"@odata.nextLink"?: string;
|
||||||
|
}>(teamsNextLink, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
allTeams = allTeams.concat(response.data.value);
|
||||||
|
teamsNextLink = response.data["@odata.nextLink"] || "";
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error, "Microsoft Teams Workflow Integration: Failed to fetch teams");
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = [];
|
||||||
|
|
||||||
|
for await (const team of allTeams) {
|
||||||
|
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 ${accessToken}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
let allChannels: { displayName: string; id: string }[] = [];
|
||||||
|
|
||||||
|
let channelNextLink: string = `https://graph.microsoft.com/v1.0/teams/${team.id}/channels`;
|
||||||
|
|
||||||
|
while (channelNextLink?.length) {
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
const resp = await axios
|
||||||
|
.get<{
|
||||||
|
value: { displayName: string; id: string }[];
|
||||||
|
"@odata.nextLink"?: string;
|
||||||
|
}>(channelNextLink, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
if (axios.isAxiosError(error)) {
|
||||||
|
logger.error(
|
||||||
|
error.response?.data,
|
||||||
|
"getTeamsAndChannels: Axios error, Microsoft Teams Workflow Integration: Failed to fetch channels"
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
logger.error(
|
||||||
|
error,
|
||||||
|
"getTeamsAndChannels: Microsoft Teams Workflow Integration: Failed to fetch channels"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
|
||||||
|
allChannels = allChannels.concat(resp.data.value);
|
||||||
|
channelNextLink = resp.data["@odata.nextLink"] || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const channels = allChannels.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 validateMicrosoftTeamsChannelsSchema = z
|
||||||
|
.object({
|
||||||
|
teamId: z.string(),
|
||||||
|
channelIds: z.array(z.string()).min(1)
|
||||||
|
})
|
||||||
|
.optional()
|
||||||
|
.refine((data) => data === undefined || data?.channelIds.length <= 20, {
|
||||||
|
message: "You can only select up to 20 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,710 @@
|
|||||||
|
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, DatabaseError, 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 {
|
||||||
|
getMicrosoftTeamsAccessToken,
|
||||||
|
isBotInstalledInTenant,
|
||||||
|
TeamsBot,
|
||||||
|
verifyTenantFromCode
|
||||||
|
} from "./microsoft-teams-fns";
|
||||||
|
import { TMicrosoftTeamsIntegrationDALFactory } from "./microsoft-teams-integration-dal";
|
||||||
|
import {
|
||||||
|
TCheckInstallationStatusDTO,
|
||||||
|
TCreateMicrosoftTeamsIntegrationDTO,
|
||||||
|
TDeleteMicrosoftTeamsIntegrationDTO,
|
||||||
|
TGetClientIdDTO,
|
||||||
|
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"
|
||||||
|
| "update"
|
||||||
|
>;
|
||||||
|
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 microsoftTeamsIntegration =
|
||||||
|
await microsoftTeamsIntegrationDAL.findByIdWithWorkflowIntegrationDetails(workflowIntegrationId);
|
||||||
|
|
||||||
|
if (!microsoftTeamsIntegration) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `Microsoft Teams integration with ID ${workflowIntegrationId} not found`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
microsoftTeamsIntegration.orgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings);
|
||||||
|
|
||||||
|
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(),
|
||||||
|
orgId: microsoftTeamsIntegration.orgId,
|
||||||
|
kmsService,
|
||||||
|
microsoftTeamsIntegrationDAL,
|
||||||
|
microsoftTeamsIntegrationId: microsoftTeamsIntegration.id
|
||||||
|
});
|
||||||
|
|
||||||
|
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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return microsoftTeamsIntegration;
|
||||||
|
};
|
||||||
|
|
||||||
|
const completeMicrosoftTeamsIntegration = async ({
|
||||||
|
code,
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
tenantId,
|
||||||
|
slug,
|
||||||
|
description,
|
||||||
|
redirectUri
|
||||||
|
}: 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 decryptWithRoot = kmsService.decryptWithRootKey();
|
||||||
|
const botAppId = decryptWithRoot(encryptedMicrosoftTeamsAppId);
|
||||||
|
const botAppPassword = decryptWithRoot(encryptedMicrosoftTeamsClientSecret);
|
||||||
|
const botId = decryptWithRoot(encryptedMicrosoftTeamsBotId);
|
||||||
|
|
||||||
|
await verifyTenantFromCode(tenantId, code, redirectUri, botAppId.toString(), botAppPassword.toString());
|
||||||
|
|
||||||
|
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
|
||||||
|
)
|
||||||
|
.catch((err) => {
|
||||||
|
if (err instanceof DatabaseError) {
|
||||||
|
if ((err.error as Error)?.stack?.includes("duplicate key value violates unique constraint"))
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Microsoft Teams integration with the same Tenant ID already exists."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
|
||||||
|
const teamsBotInfo = await isBotInstalledInTenant(
|
||||||
|
{
|
||||||
|
tenantId: microsoftTeamsIntegration.tenantId,
|
||||||
|
botAppId: botAppId.toString(),
|
||||||
|
botAppPassword: botAppPassword.toString(),
|
||||||
|
botId: botId.toString(),
|
||||||
|
orgId: workflowIntegration.orgId,
|
||||||
|
kmsService,
|
||||||
|
microsoftTeamsIntegrationDAL,
|
||||||
|
microsoftTeamsIntegrationId: microsoftTeamsIntegration.id
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const getClientId = async ({ actorId, actor, actorOrgId, actorAuthMethod }: TGetClientIdDTO) => {
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Settings);
|
||||||
|
|
||||||
|
const serverCfg = await serverCfgDAL.findById(ADMIN_CONFIG_DB_UUID);
|
||||||
|
if (!serverCfg) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to get server configuration."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!serverCfg.encryptedMicrosoftTeamsAppId) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Microsoft Teams app ID is not set"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const decryptWithRoot = kmsService.decryptWithRootKey();
|
||||||
|
const clientId = decryptWithRoot(serverCfg.encryptedMicrosoftTeamsAppId);
|
||||||
|
|
||||||
|
return clientId.toString();
|
||||||
|
};
|
||||||
|
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,
|
||||||
|
status: WorkflowIntegrationStatus.INSTALLED
|
||||||
|
});
|
||||||
|
|
||||||
|
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
|
||||||
|
}: TDeleteMicrosoftTeamsIntegrationDTO) => {
|
||||||
|
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 microsoftTeamsIntegration =
|
||||||
|
await microsoftTeamsIntegrationDAL.findByIdWithWorkflowIntegrationDetails(workflowIntegrationId);
|
||||||
|
|
||||||
|
if (!microsoftTeamsIntegration) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `Microsoft Teams integration with ID ${workflowIntegrationId} not found`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
microsoftTeamsIntegration.orgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Settings);
|
||||||
|
|
||||||
|
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, accessToken } = await isBotInstalledInTenant({
|
||||||
|
tenantId: microsoftTeamsIntegration.tenantId,
|
||||||
|
botAppId: decryptedAppId.toString(),
|
||||||
|
botAppPassword: decryptedAppPassword.toString(),
|
||||||
|
botId: decryptedBotId.toString(),
|
||||||
|
orgId: microsoftTeamsIntegration.orgId,
|
||||||
|
kmsService,
|
||||||
|
microsoftTeamsIntegrationDAL,
|
||||||
|
microsoftTeamsIntegrationId: microsoftTeamsIntegration.id
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!installed) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Microsoft Teams bot is not installed in the configured Microsoft Teams Tenant"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const teams = await teamsBot.getTeamsAndChannels(accessToken, internalId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...microsoftTeamsIntegration,
|
||||||
|
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,
|
||||||
|
orgId,
|
||||||
|
microsoftTeamsIntegrationId
|
||||||
|
}: TSendNotificationDTO) => {
|
||||||
|
if (!teamsBot || !adapter) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Unable to send notification 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 botAppId = decryptWithRoot(serverCfg.encryptedMicrosoftTeamsAppId);
|
||||||
|
const botAppPassword = decryptWithRoot(serverCfg.encryptedMicrosoftTeamsClientSecret);
|
||||||
|
|
||||||
|
const botAccessToken = await getMicrosoftTeamsAccessToken({
|
||||||
|
tenantId,
|
||||||
|
clientId: botAppId.toString(),
|
||||||
|
clientSecret: botAppPassword.toString(),
|
||||||
|
getBotFrameworkToken: true,
|
||||||
|
orgId,
|
||||||
|
kmsService,
|
||||||
|
microsoftTeamsIntegrationDAL,
|
||||||
|
microsoftTeamsIntegrationId
|
||||||
|
});
|
||||||
|
|
||||||
|
for await (const channelId of target.channelIds) {
|
||||||
|
await teamsBot.sendMessageToChannel(botAccessToken, tenantId, channelId, target.teamId, notification);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
getMicrosoftTeamsIntegrationsByOrg,
|
||||||
|
getMicrosoftTeamsIntegrationById,
|
||||||
|
updateMicrosoftTeamsIntegration,
|
||||||
|
deleteMicrosoftTeamsIntegration,
|
||||||
|
completeMicrosoftTeamsIntegration,
|
||||||
|
initializeTeamsBot,
|
||||||
|
getTeams,
|
||||||
|
handleMessageEndpoint,
|
||||||
|
start,
|
||||||
|
sendNotification,
|
||||||
|
checkInstallationStatus,
|
||||||
|
getClientId
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { TOrgPermission } from "@app/lib/types";
|
||||||
|
import { TNotification } from "@app/lib/workflow-integrations/types";
|
||||||
|
|
||||||
|
export type TGetMicrosoftTeamsIntegrationByOrgDTO = Omit<TOrgPermission, "orgId">;
|
||||||
|
|
||||||
|
export type TGetClientIdDTO = Omit<TOrgPermission, "orgId">;
|
||||||
|
|
||||||
|
export type TCreateMicrosoftTeamsIntegrationDTO = Omit<TOrgPermission, "orgId"> & {
|
||||||
|
tenantId: string;
|
||||||
|
slug: string;
|
||||||
|
redirectUri: string;
|
||||||
|
description?: string;
|
||||||
|
code: 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 TDeleteMicrosoftTeamsIntegrationDTO = {
|
||||||
|
id: string;
|
||||||
|
} & Omit<TOrgPermission, "orgId">;
|
||||||
|
|
||||||
|
export type TSendNotificationDTO = {
|
||||||
|
tenantId: string;
|
||||||
|
microsoftTeamsIntegrationId: string;
|
||||||
|
orgId: 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 { validateMicrosoftTeamsChannelsSchema } 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, WorkflowIntegrationStatus } 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,10 +75,11 @@ import { TProjectSshConfigDALFactory } from "./project-ssh-config-dal";
|
|||||||
import {
|
import {
|
||||||
TCreateProjectDTO,
|
TCreateProjectDTO,
|
||||||
TDeleteProjectDTO,
|
TDeleteProjectDTO,
|
||||||
|
TDeleteProjectWorkflowIntegration,
|
||||||
TGetProjectDTO,
|
TGetProjectDTO,
|
||||||
TGetProjectKmsKey,
|
TGetProjectKmsKey,
|
||||||
TGetProjectSlackConfig,
|
|
||||||
TGetProjectSshConfig,
|
TGetProjectSshConfig,
|
||||||
|
TGetProjectWorkflowIntegrationConfig,
|
||||||
TListProjectAlertsDTO,
|
TListProjectAlertsDTO,
|
||||||
TListProjectCasDTO,
|
TListProjectCasDTO,
|
||||||
TListProjectCertificateTemplatesDTO,
|
TListProjectCertificateTemplatesDTO,
|
||||||
@@ -92,9 +98,9 @@ import {
|
|||||||
TUpdateProjectDTO,
|
TUpdateProjectDTO,
|
||||||
TUpdateProjectKmsDTO,
|
TUpdateProjectKmsDTO,
|
||||||
TUpdateProjectNameDTO,
|
TUpdateProjectNameDTO,
|
||||||
TUpdateProjectSlackConfig,
|
|
||||||
TUpdateProjectSshConfig,
|
TUpdateProjectSshConfig,
|
||||||
TUpdateProjectVersionLimitDTO,
|
TUpdateProjectVersionLimitDTO,
|
||||||
|
TUpdateProjectWorkflowIntegration,
|
||||||
TUpgradeProjectDTO
|
TUpgradeProjectDTO
|
||||||
} from "./project-types";
|
} from "./project-types";
|
||||||
|
|
||||||
@@ -123,8 +129,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">;
|
||||||
@@ -197,7 +214,9 @@ export const projectServiceFactory = ({
|
|||||||
kmsService,
|
kmsService,
|
||||||
projectBotDAL,
|
projectBotDAL,
|
||||||
projectSlackConfigDAL,
|
projectSlackConfigDAL,
|
||||||
|
projectMicrosoftTeamsConfigDAL,
|
||||||
slackIntegrationDAL,
|
slackIntegrationDAL,
|
||||||
|
microsoftTeamsIntegrationDAL,
|
||||||
projectTemplateService,
|
projectTemplateService,
|
||||||
groupProjectDAL,
|
groupProjectDAL,
|
||||||
smtpService
|
smtpService
|
||||||
@@ -1452,13 +1471,14 @@ export const projectServiceFactory = ({
|
|||||||
return projectSshConfig;
|
return projectSshConfig;
|
||||||
};
|
};
|
||||||
|
|
||||||
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({
|
||||||
@@ -1477,23 +1497,60 @@ 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) {
|
||||||
projectId: project.id
|
const config = await projectSlackConfigDAL.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.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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Integration type '${integration as string}' not supported`
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
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({
|
||||||
@@ -1501,17 +1558,206 @@ export const projectServiceFactory = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const slackIntegration = await slackIntegrationDAL.findByIdWithWorkflowIntegrationDetails(slackIntegrationId);
|
if (integration === WorkflowIntegration.SLACK) {
|
||||||
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
if (!slackIntegration) {
|
actor,
|
||||||
throw new NotFoundError({
|
actorId,
|
||||||
message: `Slack integration with ID '${slackIntegrationId}' not found`
|
projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId,
|
||||||
|
actionProjectType: ActionProjectType.Any
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Settings);
|
||||||
|
|
||||||
|
const sanitizedAccessRequestChannels = validateSlackChannelsField.parse(accessRequestChannels);
|
||||||
|
const sanitizedSecretRequestChannels = validateSlackChannelsField.parse(secretRequestChannels);
|
||||||
|
|
||||||
|
const slackIntegration = await slackIntegrationDAL.findByIdWithWorkflowIntegrationDetails(integrationId);
|
||||||
|
|
||||||
|
if (!slackIntegration) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `Slack integration with ID '${integrationId}' not found`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (slackIntegration.orgId !== actorOrgId) {
|
||||||
|
throw new ForbiddenRequestError({
|
||||||
|
message: "Selected slack integration is not in the same organization"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (slackIntegration.orgId !== project.orgId) {
|
||||||
|
throw new ForbiddenRequestError({
|
||||||
|
message: "Selected slack integration is not in the same organization"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedWorkflowIntegration = await projectSlackConfigDAL.transaction(async (tx) => {
|
||||||
|
const slackConfig = await projectSlackConfigDAL.findOne(
|
||||||
|
{
|
||||||
|
projectId
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
if (slackConfig) {
|
||||||
|
return projectSlackConfigDAL.updateById(
|
||||||
|
slackConfig.id,
|
||||||
|
{
|
||||||
|
slackIntegrationId: integrationId,
|
||||||
|
isAccessRequestNotificationEnabled,
|
||||||
|
accessRequestChannels: sanitizedAccessRequestChannels,
|
||||||
|
isSecretRequestNotificationEnabled,
|
||||||
|
secretRequestChannels: sanitizedSecretRequestChannels
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return projectSlackConfigDAL.create(
|
||||||
|
{
|
||||||
|
projectId,
|
||||||
|
slackIntegrationId: integrationId,
|
||||||
|
isAccessRequestNotificationEnabled,
|
||||||
|
accessRequestChannels: sanitizedAccessRequestChannels,
|
||||||
|
isSecretRequestNotificationEnabled,
|
||||||
|
secretRequestChannels: sanitizedSecretRequestChannels
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
...updatedWorkflowIntegration,
|
||||||
|
accessRequestChannels: sanitizedAccessRequestChannels,
|
||||||
|
secretRequestChannels: sanitizedSecretRequestChannels,
|
||||||
|
integrationId: slackIntegration.id,
|
||||||
|
integration: WorkflowIntegration.SLACK
|
||||||
|
} as const;
|
||||||
|
}
|
||||||
|
if (integration === WorkflowIntegration.MICROSOFT_TEAMS) {
|
||||||
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId,
|
||||||
|
actionProjectType: ActionProjectType.Any
|
||||||
|
});
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Settings);
|
||||||
|
|
||||||
|
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 microsoftTeamsIntegration =
|
||||||
|
await microsoftTeamsIntegrationDAL.findByIdWithWorkflowIntegrationDetails(integrationId);
|
||||||
|
|
||||||
|
if (!microsoftTeamsIntegration) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `Microsoft Teams integration with ID '${integrationId}' not found`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (microsoftTeamsIntegration.status !== WorkflowIntegrationStatus.INSTALLED) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Microsoft Teams integration is not properly installed in your tenant."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (microsoftTeamsIntegration.orgId !== actorOrgId) {
|
||||||
|
throw new ForbiddenRequestError({
|
||||||
|
message: "Selected Microsoft Teams integration is not in the same organization"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (microsoftTeamsIntegration.orgId !== project.orgId) {
|
||||||
|
throw new ForbiddenRequestError({
|
||||||
|
message: "Selected Microsoft Teams integration is not in the same organization"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const sanitizedAccessRequestChannels = validateMicrosoftTeamsChannelsSchema.parse(accessRequestChannels);
|
||||||
|
const sanitizedSecretRequestChannels = validateMicrosoftTeamsChannelsSchema.parse(secretRequestChannels);
|
||||||
|
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (slackIntegration.orgId !== actorOrgId) {
|
throw new BadRequestError({
|
||||||
throw new ForbiddenRequestError({
|
message: `Integration type '${integration as string}' not supported`
|
||||||
message: "Selected slack integration is not in the same organization"
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
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`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1524,47 +1770,28 @@ export const projectServiceFactory = ({
|
|||||||
actionProjectType: ActionProjectType.Any
|
actionProjectType: ActionProjectType.Any
|
||||||
});
|
});
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Settings);
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Settings);
|
||||||
|
|
||||||
if (slackIntegration.orgId !== project.orgId) {
|
if (integration === WorkflowIntegration.SLACK) {
|
||||||
throw new ForbiddenRequestError({
|
const [deletedIntegration] = await projectSlackConfigDAL.delete({
|
||||||
message: "Selected slack integration is not in the same organization"
|
projectId,
|
||||||
|
slackIntegrationId: integrationId
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return deletedIntegration;
|
||||||
}
|
}
|
||||||
|
|
||||||
return projectSlackConfigDAL.transaction(async (tx) => {
|
if (integration === WorkflowIntegration.MICROSOFT_TEAMS) {
|
||||||
const slackConfig = await projectSlackConfigDAL.findOne(
|
const [deletedIntegration] = await projectMicrosoftTeamsConfigDAL.delete({
|
||||||
{
|
projectId,
|
||||||
projectId
|
microsoftTeamsIntegrationId: integrationId
|
||||||
},
|
});
|
||||||
tx
|
|
||||||
);
|
|
||||||
|
|
||||||
if (slackConfig) {
|
return deletedIntegration;
|
||||||
return projectSlackConfigDAL.updateById(
|
}
|
||||||
slackConfig.id,
|
|
||||||
{
|
|
||||||
slackIntegrationId,
|
|
||||||
isAccessRequestNotificationEnabled,
|
|
||||||
accessRequestChannels,
|
|
||||||
isSecretRequestNotificationEnabled,
|
|
||||||
secretRequestChannels
|
|
||||||
},
|
|
||||||
tx
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return projectSlackConfigDAL.create(
|
throw new BadRequestError({
|
||||||
{
|
message: `Integration with ID '${integrationId}' not found`
|
||||||
projectId,
|
|
||||||
slackIntegrationId,
|
|
||||||
isAccessRequestNotificationEnabled,
|
|
||||||
accessRequestChannels,
|
|
||||||
isSecretRequestNotificationEnabled,
|
|
||||||
secretRequestChannels
|
|
||||||
},
|
|
||||||
tx
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1673,10 +1900,11 @@ export const projectServiceFactory = ({
|
|||||||
getProjectKmsBackup,
|
getProjectKmsBackup,
|
||||||
loadProjectKmsBackup,
|
loadProjectKmsBackup,
|
||||||
getProjectKmsKeys,
|
getProjectKmsKeys,
|
||||||
|
getProjectWorkflowIntegrationConfig,
|
||||||
|
updateProjectWorkflowIntegration,
|
||||||
|
deleteProjectWorkflowIntegration,
|
||||||
getProjectSshConfig,
|
getProjectSshConfig,
|
||||||
updateProjectSshConfig,
|
updateProjectSshConfig,
|
||||||
getProjectSlackConfig,
|
|
||||||
updateProjectSlackConfig,
|
|
||||||
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",
|
||||||
@@ -166,14 +167,33 @@ export type TUpdateProjectSshConfig = {
|
|||||||
|
|
||||||
export type TGetProjectSshConfig = TProjectPermission;
|
export type TGetProjectSshConfig = TProjectPermission;
|
||||||
|
|
||||||
export type TGetProjectSlackConfig = TProjectPermission;
|
export type TGetProjectWorkflowIntegrationConfig = TProjectPermission & {
|
||||||
|
integration: WorkflowIntegration;
|
||||||
|
};
|
||||||
|
|
||||||
export type TUpdateProjectSlackConfig = {
|
export type TUpdateProjectWorkflowIntegration = (
|
||||||
slackIntegrationId: string;
|
| {
|
||||||
isAccessRequestNotificationEnabled: boolean;
|
integrationId: string;
|
||||||
accessRequestChannels: string;
|
integration: WorkflowIntegration.SLACK;
|
||||||
isSecretRequestNotificationEnabled: boolean;
|
isAccessRequestNotificationEnabled: boolean;
|
||||||
secretRequestChannels: string;
|
isSecretRequestNotificationEnabled: boolean;
|
||||||
|
accessRequestChannels?: string;
|
||||||
|
secretRequestChannels?: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
integrationId: string;
|
||||||
|
integration: WorkflowIntegration.MICROSOFT_TEAMS;
|
||||||
|
isAccessRequestNotificationEnabled: boolean;
|
||||||
|
isSecretRequestNotificationEnabled: boolean;
|
||||||
|
accessRequestChannels?: { teamId: string; channelIds: string[] };
|
||||||
|
secretRequestChannels?: { teamId: string; channelIds: string[] };
|
||||||
|
}
|
||||||
|
) &
|
||||||
|
TProjectPermission;
|
||||||
|
|
||||||
|
export type TDeleteProjectWorkflowIntegration = {
|
||||||
|
integrationId: string;
|
||||||
|
integration: WorkflowIntegration;
|
||||||
} & TProjectPermission;
|
} & TProjectPermission;
|
||||||
|
|
||||||
export type TBootstrapSshProjectDTO = {
|
export type TBootstrapSshProjectDTO = {
|
||||||
|
|||||||
@@ -3,12 +3,10 @@ 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 { KmsDataKey } from "../kms/kms-types";
|
import { KmsDataKey } from "../kms/kms-types";
|
||||||
import { TProjectDALFactory } from "../project/project-dal";
|
import { TSendSlackNotificationDTO } from "./slack-types";
|
||||||
import { TProjectSlackConfigDALFactory } from "./project-slack-config-dal";
|
|
||||||
import { SlackTriggerFeature, TSlackNotification } from "./slack-types";
|
|
||||||
|
|
||||||
export const fetchSlackChannels = async (botKey: string) => {
|
export const fetchSlackChannels = async (botKey: string) => {
|
||||||
const slackChannels: {
|
const slackChannels: {
|
||||||
@@ -41,11 +39,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 +77,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 +123,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,51 @@ export const superAdminServiceFactory = ({
|
|||||||
updatedData.slackClientSecret = undefined;
|
updatedData.slackClientSecret = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let microsoftTeamsSettingsUpdated = false;
|
||||||
|
if (data.microsoftTeamsAppId) {
|
||||||
|
const encryptedClientId = encryptWithRoot(Buffer.from(data.microsoftTeamsAppId));
|
||||||
|
|
||||||
|
updatedData.encryptedMicrosoftTeamsAppId = encryptedClientId;
|
||||||
|
updatedData.microsoftTeamsAppId = undefined;
|
||||||
|
microsoftTeamsSettingsUpdated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.microsoftTeamsClientSecret) {
|
||||||
|
const encryptedClientSecret = encryptWithRoot(Buffer.from(data.microsoftTeamsClientSecret));
|
||||||
|
|
||||||
|
updatedData.encryptedMicrosoftTeamsClientSecret = encryptedClientSecret;
|
||||||
|
updatedData.microsoftTeamsClientSecret = undefined;
|
||||||
|
microsoftTeamsSettingsUpdated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.microsoftTeamsBotId) {
|
||||||
|
const encryptedBotId = encryptWithRoot(Buffer.from(data.microsoftTeamsBotId));
|
||||||
|
|
||||||
|
updatedData.encryptedMicrosoftTeamsBotId = encryptedBotId;
|
||||||
|
updatedData.microsoftTeamsBotId = undefined;
|
||||||
|
microsoftTeamsSettingsUpdated = true;
|
||||||
|
}
|
||||||
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 &&
|
||||||
|
microsoftTeamsSettingsUpdated
|
||||||
|
) {
|
||||||
|
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 +538,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 +639,7 @@ export const superAdminServiceFactory = ({
|
|||||||
getUsers,
|
getUsers,
|
||||||
deleteUser,
|
deleteUser,
|
||||||
getIdentities,
|
getIdentities,
|
||||||
getAdminSlackConfig,
|
getAdminIntegrationsConfig,
|
||||||
updateRootEncryptionStrategy,
|
updateRootEncryptionStrategy,
|
||||||
getConfiguredEncryptionStrategies,
|
getConfiguredEncryptionStrategies,
|
||||||
grantServerAdminAccessToUser,
|
grantServerAdminAccessToUser,
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
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"
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TGetWorkflowIntegrationsByOrg = Omit<TOrgPermission, "orgId">;
|
export type TGetWorkflowIntegrationsByOrg = Omit<TOrgPermission, "orgId">;
|
||||||
|
|||||||
@@ -0,0 +1,192 @@
|
|||||||
|
---
|
||||||
|
title: "Microsoft Teams Integration"
|
||||||
|
description: "Learn how to setup the Microsoft Teams integration"
|
||||||
|
---
|
||||||
|
|
||||||
|
import MicrosoftTeamsWorkflowIntegration from '/snippets/documentation/platform/workflow-integrations/microsoft-teams-integration.mdx';
|
||||||
|
|
||||||
|
|
||||||
|
This guide will provide step by step instructions on how to configure Microsoft Teams integration for your Infisical projects.
|
||||||
|
|
||||||
|
## Setting up Microsoft Teams integration in your projects
|
||||||
|
|
||||||
|
<Tabs>
|
||||||
|
<Tab title="Infisical Cloud">
|
||||||
|
<MicrosoftTeamsWorkflowIntegration />
|
||||||
|
</Tab>
|
||||||
|
<Tab title="Self-hosted setup">
|
||||||
|
### Configure Azure Resources
|
||||||
|
To create a Microsoft Teams bot, you must first create an Azure Bot from the Azure Marketplace, an app registration, and a Microsoft Teams app. The steps below document in detail how to create and configure these resources.
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
|
||||||
|
<Step title="Create Microsoft Teams app">
|
||||||
|
Navigate to the [Microsoft Teams Developer Portal](https://dev.teams.microsoft.com/).
|
||||||
|
|
||||||
|
Once you're on the Microsoft Teams Developer Portal, press the "Create a new app" button on the overview page. Give the bot a name and press the "Add" button.
|
||||||
|
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step title="Create a Microsoft Teams bot">
|
||||||
|
After creating the Microsoft Teams app, you'll need to create a Microsoft Teams bot.
|
||||||
|
|
||||||
|
Navigate to the app's bot settings page and click "Create a new bot".
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
After clicking the "Create a new bot" button, you'll be navigated to the Teams Developer Portal for bot management. Press the "New bot" button, and enter the name of the bot.
|
||||||
|
Please keep in mind that the name of the bot can only contain alphanumeric characters, dashes, and underscores.
|
||||||
|
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step title="Add a message endpoint to the bot">
|
||||||
|
After creating the bot, you'll need to add a message endpoint to the bot. Navigate to the "Configure" tab, and input the following endpoint under "Endpoint address":
|
||||||
|
`https://<your-infisical-instance-url>/api/v1/workflow-integrations/microsoft-teams/message-endpoint`
|
||||||
|
Replace `<your-infisical-instance-url>` with the URL of your Infisical instance.
|
||||||
|
|
||||||
|
Press the "Save" button to save the changes.
|
||||||
|
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step title="Find the bot in Azure App Registrations">
|
||||||
|
When you create a bot through the Teams Developer Portal, an Azure App Registration is also created.
|
||||||
|
|
||||||
|
Open your [Azure Portal](https://portal.azure.com/) and navigate to the "App Registrations" section to find the newly created app registration.
|
||||||
|
The name of the app registration will be the same as the name of the bot you created in the previous step.
|
||||||
|
|
||||||
|
|
||||||
|
Press the app registration to open the app registration overview page.
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step title="Add API Permissions to the App Registration">
|
||||||
|
Navigate to the "API Permissions" section of the app registration, and add the following permissions:
|
||||||
|
- `AppCatalog.Read.All`
|
||||||
|
- `ChannelSettings.Read.All`
|
||||||
|
- `MultiTenantOrganization.Read.All`
|
||||||
|
- `Organization.Read.All`
|
||||||
|
- `Team.ReadBasic.All`
|
||||||
|
- `TeamsAppInstallation.Read.All`
|
||||||
|
|
||||||
|
After adding the API permissions, press the "Grant admin consent" button to grant the permissions.
|
||||||
|
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step title="Add a new web application to the App Registration">
|
||||||
|
Navigate to the "Authentication" section of the App Registration, and press the "Add a platform" button. Select the "Web" platform and enter the following redirect URI:
|
||||||
|
`https://<your-infisical-instance-url>/organization/settings/oauth/callback`. Replace `<your-infisical-instance-url>` with the URL of your Infisical instance.
|
||||||
|
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step title="Get App Registration Client ID and Client Secret">
|
||||||
|
Next we need to get the application client ID and create a new client secret. **Save these values for later, as they're required to configure the Microsoft Teams integration in Infisical.**
|
||||||
|
|
||||||
|
**Get the Application (Client) ID**
|
||||||
|
|
||||||
|
To get the Application (Client) ID, press the "Copy" button next to the "Application (client) ID" field.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
**Create a new client secret**
|
||||||
|
|
||||||
|
Create a new client secret within the app registration. Navigate to the "Certificates & Secrets" section of the app registration, and press the "New client secret" button.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
<Warning>
|
||||||
|
Remember to rotate your client secret before it expires. Consider setting up a reminder or automated process to replace the secret and update your Infisical configuration before expiration.
|
||||||
|
</Warning>
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step title="Get the Microsoft Teams App ID">
|
||||||
|
Navigate back to the [Microsoft Teams Developer Portal](https://dev.teams.microsoft.com/), and press the "Apps" tab and select the app you created earlier.
|
||||||
|
Here you can find the Microsoft Teams App ID in the overview page, which you need to copy and save for later.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step title="Link the Microsoft Teams App to the bot">
|
||||||
|
You need to link the Microsoft Teams App with the bot/app registration you created earlier.
|
||||||
|
Inside the [Microsoft Teams Developer Portal](https://dev.teams.microsoft.com/), navigate to the "Bot" tab and select the bot you created earlier. Navigate to the "App Features" section, and press the "Bot" button.
|
||||||
|
|
||||||
|
Under the "What can your bot do?" section, enable `Only send notifications (one-way conversations)`.
|
||||||
|
|
||||||
|
Under the "Select the scopes where people can use your bot" section, select `Personal`, `Team`, and `Group Chat`.
|
||||||
|
|
||||||
|
Finally, press the "Save" button to save the changes.
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step title="Run an app validation test (Recommended)">
|
||||||
|
To ensure that the Microsoft Teams App is working correctly, you can run an app validation test. This step is optional, but recommended to ensure the app is working correctly.
|
||||||
|
|
||||||
|
You should expect to see two errors related to sending welcome messages, because we haven't configured the Microsoft Teams App inside Infisical yet, which is required for proactive messages.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
<Note>
|
||||||
|
You may see manifest validation errors. Before running an app validation test, you must ensure that your app has all errors resolved, such as having a description and a valid name.
|
||||||
|
</Note>
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
<Note>
|
||||||
|
If you see two errors for bot welcome messages, you can ignore them. This is expected until you configure the Microsoft Teams App inside Infisical.
|
||||||
|
</Note>
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step title="Download the App Package">
|
||||||
|
Once the Microsoft Teams App is working correctly, you can download the app package by navigating to the "Publish to Store" page, and pressing the "Download app package" button.
|
||||||
|
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
</Steps>
|
||||||
|
|
||||||
|
### Configure Microsoft Teams Bot in Infisical
|
||||||
|
|
||||||
|
After creating the Microsoft Teams App and Bot, you are ready to configure the Microsoft Teams integration in Infisical.
|
||||||
|
Please note that you must be an instance admin in order to configure the Microsoft Teams instance-wide settings.
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
<Step title="Navigate to the Integrations tab in your Server Admin Console settings">
|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
Enter the values you saved from the earlier steps into the respective fields.
|
||||||
|
|
||||||
|
- **Application (Client) ID**: The Client ID of the App Registration from the previous steps.
|
||||||
|
- **Client Secret**: The Client Secret of the App Registration from the previous steps.
|
||||||
|
- **Microsoft Teams App ID**: The App ID of the Microsoft Teams App from the previous steps.
|
||||||
|
|
||||||
|
Once completed, press the "Save" button to save your changes.
|
||||||
|
|
||||||
|
</Step>
|
||||||
|
</Steps>
|
||||||
|
<MicrosoftTeamsWorkflowIntegration />
|
||||||
|
</Tab>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
<Accordion title="Notifications are not being sent to Microsoft Teams even though the integration is configured">
|
||||||
|
If you recently added the Microsoft Teams app to your tenant, **it may take up to 24 hours for Microsoft Teams to propagate the changes.**
|
||||||
|
A common indication of propagation issues is that the workflow integration is shown as "Installed", and you're able to view the teams and channels when configuring the workflow integration on your project, but no notification is being sent.
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
|
<Accordion title="Workflow Integration is stuck on 'Pending' status">
|
||||||
|
The workflow integration can get stuck on Pending if you created the workflow integration before the Infisical Microsoft Teams bot was installed in the tenant.
|
||||||
|
To resolve this, make sure you have installed the Infisical Microsoft Teams app in your tenant.
|
||||||
|
|
||||||
|
You can manually recheck the installation status by pressing the "Check Installation Status" button in the workflow organization settings.
|
||||||
|
|
||||||
|

|
||||||
|
</Accordion>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
title: "Slack integration"
|
title: "Slack Integration"
|
||||||
description: "Learn how to setup Slack integration"
|
description: "Learn how to setup the Slack integration"
|
||||||
---
|
---
|
||||||
|
|
||||||
This guide will provide step by step instructions on how to configure Slack integration for your Infisical projects.
|
This guide will provide step by step instructions on how to configure Slack integration for your Infisical projects.
|
||||||
|
|||||||
|
After Width: | Height: | Size: 230 KiB |
|
After Width: | Height: | Size: 259 KiB |
|
After Width: | Height: | Size: 192 KiB |
|
After Width: | Height: | Size: 271 KiB |
|
After Width: | Height: | Size: 125 KiB |
|
After Width: | Height: | Size: 278 KiB |
|
After Width: | Height: | Size: 206 KiB |
|
After Width: | Height: | Size: 227 KiB |
|
After Width: | Height: | Size: 299 KiB |
|
After Width: | Height: | Size: 229 KiB |
|
After Width: | Height: | Size: 292 KiB |
|
After Width: | Height: | Size: 156 KiB |
|
After Width: | Height: | Size: 787 KiB |
|
After Width: | Height: | Size: 139 KiB |
|
After Width: | Height: | Size: 304 KiB |
|
After Width: | Height: | Size: 194 KiB |
|
After Width: | Height: | Size: 206 KiB |
|
After Width: | Height: | Size: 300 KiB |
|
After Width: | Height: | Size: 83 KiB |
|
After Width: | Height: | Size: 206 KiB |
|
After Width: | Height: | Size: 152 KiB |
|
After Width: | Height: | Size: 192 KiB |
|
After Width: | Height: | Size: 154 KiB |
|
After Width: | Height: | Size: 207 KiB |
|
After Width: | Height: | Size: 137 KiB |
|
After Width: | Height: | Size: 421 KiB |
|
After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 230 KiB |
@@ -222,7 +222,8 @@
|
|||||||
{
|
{
|
||||||
"group": "Workflow Integrations",
|
"group": "Workflow Integrations",
|
||||||
"pages": [
|
"pages": [
|
||||||
"documentation/platform/workflow-integrations/slack-integration"
|
"documentation/platform/workflow-integrations/slack-integration",
|
||||||
|
"documentation/platform/workflow-integrations/microsoft-teams-integration"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
### Create Microsoft Teams workflow integration
|
||||||
|
<Steps>
|
||||||
|
<Step title="Install the Infisical Microsoft Teams App in Microsoft Teams tenant">
|
||||||
|
|
||||||
|
Currently, Infisical requires you to install a custom Microsoft Teams app into your Microsoft Teams tenant.
|
||||||
|
|
||||||
|
You can download the Infisical Microsoft Teams app package here:
|
||||||
|
- [Infisical Microsoft Teams app package](https://infisical-microsoft-teams-app.s3.us-east-1.amazonaws.com/Infisical.zip)
|
||||||
|
|
||||||
|
<Note>
|
||||||
|
**Important for self-hosted users:**
|
||||||
|
|
||||||
|
If you're self-hosting Infisical, you can skip the download step. Instead you should use the app package file you downloaded from the Microsoft Teams Developer Portal when you followed the Self-hosted guide.
|
||||||
|
</Note>
|
||||||
|
|
||||||
|
Once you've downloaded the app package, you can install the app in your Microsoft Teams tenant by navigating to the **Apps** > **Upload a custom app** page, and selecting the "Upload an app" button.
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
Once the app has been submitted, your Microsoft Teams tenant admin will need to approve the app in the [Microsoft Teams Admin Center](https://admin.teams.microsoft.com/policies/manage-apps).
|
||||||
|
|
||||||
|
<Warning>
|
||||||
|
After the app has been approved, it can take a few hours _(up to 24 hours in some cases)_ for Microsoft Teams to reflect the new app. During this period, the Infisical app will not be visible in Microsoft Teams, and won't be usable.
|
||||||
|
</Warning>
|
||||||
|
|
||||||
|
Once the app has been approved, you will be able to use the Infisical Microsoft Teams integration in your projects.
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step title="Add the Infisical Microsoft Teams app to your Microsoft Teams teams">
|
||||||
|
Once the app has been approved and installed in your Microsoft Teams tenant, you can add the app to your Microsoft Teams teams.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Navigate to **Apps** > **Built for your org**, select the "Infisical" app, and press the "Add" button to select the teams and channels you wish to add the app to.
|
||||||
|
|
||||||
|
<Info>
|
||||||
|
This can also be done later through the [Microsoft Teams Admin Center](https://admin.teams.microsoft.com/policies/manage-apps), or through the Microsoft Teams client itself by navigating to the individual team's app settings.
|
||||||
|
</Info>
|
||||||
|
|
||||||
|
Once the app has been added to the team, you will be able to use the Infisical Microsoft Teams integration in the team.
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step title="Navigate to the Workflow Integrations tab in your organization settings">
|
||||||
|
|
||||||
|
After installing the Microsoft Teams app, you are now ready to configure the Microsoft Teams integration within Infisical.
|
||||||
|
|
||||||
|
Navigate to the **Workflow Integrations** tab in your organization settings, and press the "Add" button.
|
||||||
|
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step title="Create a Microsoft Teams workflow integration">
|
||||||
|
In order to use the Infisical Microsoft Teams integration, you will need to grant admin consent to the app. Once the consent is granted, the Microsoft Teams workflow integration will be created in your Infisical organization.
|
||||||
|
|
||||||
|
Press the "Add" button and select the "Microsoft Teams" platform option.
|
||||||
|

|
||||||
|
|
||||||
|
Select the Microsoft Teams integration you wish to configure, and press the "Configure" button.
|
||||||
|
|
||||||
|
Here you will be prompted to enter an alias, tenant ID, and an optional description for your workflow integration.
|
||||||
|
The tenant ID is the ID of the Microsoft 365 / Azure AD tenant that you installed the Infisical Microsoft Teams app in, in the previous steps.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Press the "Create Microsoft Teams Integration" button, and you'll be navigated to the Azure AD consent page.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
<Note>
|
||||||
|
Please note that you must be a privileged administrator user of your Microsoft 365 / Azure AD tenant in order to grant admin consent to the app.
|
||||||
|
</Note>
|
||||||
|
|
||||||
|
Once you've granted admin consent, you'll be navigated back to the Infisical organization settings, where you can now select the Microsoft Teams integration you just created.
|
||||||
|
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
</Steps>
|
||||||
|
|
||||||
|
### Configure project to use Microsoft Teams workflow integration
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
<Step title="Navigate to the Workflow Integrations tab in the project settings">
|
||||||
|
|
||||||
|
To add a new Microsoft Teams workflow integration, navigate to **Project Settings** > **Workflow Integrations** and press the "Add".
|
||||||
|

|
||||||
|
|
||||||
|
Select the "Microsoft Teams" option from the list of available workflow integrations.
|
||||||
|
|
||||||
|
</Step>
|
||||||
|
<Step title="Configure the Microsoft Teams workflow integration">
|
||||||
|
Your project will send notifications to the connected Microsoft Teams team of the
|
||||||
|
selected Microsoft Teams integration when the configured events are triggered.
|
||||||
|
|
||||||
|
|
||||||
|
Press the "Save" button to save your Microsoft Teams workflow integration.
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
</Steps>
|
||||||
|
Once you've created the project Microsoft Teams workflow integration, you will now receive Access Requests and Secret Approval Requests notifications in Microsoft Teams according to your configuration.
|
||||||
@@ -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>
|
||||||
));
|
));
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,12 @@ export const ROUTE_PATHS = Object.freeze({
|
|||||||
PasswordSetupPage: setRoute("/password-setup", "/_authenticate/password-setup")
|
PasswordSetupPage: setRoute("/password-setup", "/_authenticate/password-setup")
|
||||||
},
|
},
|
||||||
Organization: {
|
Organization: {
|
||||||
|
Settings: {
|
||||||
|
OauthCallbackPage: setRoute(
|
||||||
|
"/organization/settings/oauth/callback",
|
||||||
|
"/_authenticate/_inject-org-details/_org-layout/organization/settings/oauth/callback"
|
||||||
|
)
|
||||||
|
},
|
||||||
SecretScanning: setRoute(
|
SecretScanning: setRoute(
|
||||||
"/organization/secret-scanning",
|
"/organization/secret-scanning",
|
||||||
"/_authenticate/_inject-org-details/_org-layout/organization/secret-scanning"
|
"/_authenticate/_inject-org-details/_org-layout/organization/secret-scanning"
|
||||||
@@ -31,7 +37,7 @@ export const ROUTE_PATHS = Object.freeze({
|
|||||||
),
|
),
|
||||||
SettingsPage: setRoute(
|
SettingsPage: setRoute(
|
||||||
"/organization/settings",
|
"/organization/settings",
|
||||||
"/_authenticate/_inject-org-details/_org-layout/organization/settings"
|
"/_authenticate/_inject-org-details/_org-layout/organization/settings/"
|
||||||
),
|
),
|
||||||
GroupDetailsByIDPage: setRoute(
|
GroupDetailsByIDPage: setRoute(
|
||||||
"/organization/groups/$groupId",
|
"/organization/groups/$groupId",
|
||||||
|
|||||||
@@ -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,9 +61,16 @@ export type AdminGetIdentitiesFilters = {
|
|||||||
searchTerm: string;
|
searchTerm: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AdminSlackConfig = {
|
export type AdminIntegrationsConfig = {
|
||||||
clientId: string;
|
slack: {
|
||||||
clientSecret: string;
|
clientId: string;
|
||||||
|
clientSecret: string;
|
||||||
|
};
|
||||||
|
microsoftTeams: {
|
||||||
|
appId: string;
|
||||||
|
clientSecret: string;
|
||||||
|
botId: string;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TGetServerRootKmsEncryptionDetails = {
|
export type TGetServerRootKmsEncryptionDetails = {
|
||||||
|
|||||||
@@ -94,8 +94,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",
|
||||||
@@ -163,7 +165,22 @@ export const eventToNameMap: { [K in EventType]: string } = {
|
|||||||
[EventType.CREATE_SECRET_ROTATION]: "Create Secret Rotation",
|
[EventType.CREATE_SECRET_ROTATION]: "Create Secret Rotation",
|
||||||
[EventType.UPDATE_SECRET_ROTATION]: "Update Secret Rotation",
|
[EventType.UPDATE_SECRET_ROTATION]: "Update Secret Rotation",
|
||||||
[EventType.DELETE_SECRET_ROTATION]: "Delete Secret Rotation",
|
[EventType.DELETE_SECRET_ROTATION]: "Delete Secret Rotation",
|
||||||
[EventType.SECRET_ROTATION_ROTATE_SECRETS]: "Secret Rotation secrets rotated"
|
[EventType.SECRET_ROTATION_ROTATE_SECRETS]: "Secret Rotation secrets rotated",
|
||||||
|
|
||||||
|
[EventType.GET_PROJECT_SLACK_CONFIG]: "Get Project Slack Config",
|
||||||
|
[EventType.UPDATE_PROJECT_SLACK_CONFIG]: "Update Project Slack Config",
|
||||||
|
|
||||||
|
[EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_CREATE]:
|
||||||
|
"Create Microsoft Teams Workflow Integration",
|
||||||
|
[EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_DELETE]:
|
||||||
|
"Delete Microsoft Teams Workflow Integration",
|
||||||
|
[EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_UPDATE]:
|
||||||
|
"Update Microsoft Teams Workflow Integration",
|
||||||
|
[EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_CHECK_INSTALLATION_STATUS]:
|
||||||
|
"Microsoft Teams Workflow Integration Check Installation Status",
|
||||||
|
[EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_GET_TEAMS]: "Get Microsoft Teams tenant teams",
|
||||||
|
[EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_GET]: "Get Microsoft Teams Workflow Integration",
|
||||||
|
[EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_LIST]: "List Microsoft Teams Workflow Integration"
|
||||||
};
|
};
|
||||||
|
|
||||||
export const userAgentTTypeoNameMap: { [K in UserAgentType]: string } = {
|
export const userAgentTTypeoNameMap: { [K in UserAgentType]: string } = {
|
||||||
|
|||||||
@@ -98,8 +98,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",
|
||||||
@@ -163,5 +163,16 @@ export enum EventType {
|
|||||||
CREATE_SECRET_ROTATION = "create-secret-rotation",
|
CREATE_SECRET_ROTATION = "create-secret-rotation",
|
||||||
UPDATE_SECRET_ROTATION = "update-secret-rotation",
|
UPDATE_SECRET_ROTATION = "update-secret-rotation",
|
||||||
DELETE_SECRET_ROTATION = "delete-secret-rotation",
|
DELETE_SECRET_ROTATION = "delete-secret-rotation",
|
||||||
SECRET_ROTATION_ROTATE_SECRETS = "secret-rotation-rotate-secrets"
|
SECRET_ROTATION_ROTATE_SECRETS = "secret-rotation-rotate-secrets",
|
||||||
|
|
||||||
|
GET_PROJECT_SLACK_CONFIG = "get-project-slack-config",
|
||||||
|
UPDATE_PROJECT_SLACK_CONFIG = "update-project-slack-config",
|
||||||
|
|
||||||
|
MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_CREATE = "microsoft-teams-workflow-integration-create",
|
||||||
|
MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_DELETE = "microsoft-teams-workflow-integration-delete",
|
||||||
|
MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_UPDATE = "microsoft-teams-workflow-integration-update",
|
||||||
|
MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_CHECK_INSTALLATION_STATUS = "microsoft-teams-workflow-integration-check-installation-status",
|
||||||
|
MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_GET_TEAMS = "microsoft-teams-workflow-integration-get-teams",
|
||||||
|
MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_GET = "microsoft-teams-workflow-integration-get",
|
||||||
|
MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_LIST = "microsoft-teams-workflow-integration-list"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 = {
|
||||||
@@ -786,11 +787,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;
|
||||||
@@ -798,10 +800,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;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -898,8 +901,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,
|
||||||
|
TDeleteMicrosoftTeamsIntegrationDTO,
|
||||||
|
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, TDeleteMicrosoftTeamsIntegrationDTO>({
|
||||||
|
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
|
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-integration/${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,13 +2,29 @@ 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"],
|
||||||
getSlackIntegrationChannels: (id?: string) => [{ id }, "slack-workflow-integration-channels"]
|
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"],
|
||||||
|
getMicrosoftTeamsClientId: () => ["microsoft-teams-client-id"]
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchSlackInstallUrl = async ({
|
export const fetchSlackInstallUrl = async ({
|
||||||
@@ -66,13 +82,58 @@ 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 fetchMicrosoftTeamsClientId = async () => {
|
||||||
|
const { data } = await apiRequest.get<{ clientId: string }>(
|
||||||
|
"/api/v1/workflow-integrations/microsoft-teams/client-id"
|
||||||
|
);
|
||||||
|
|
||||||
|
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),
|
||||||
@@ -93,3 +154,10 @@ export const useGetWorkflowIntegrations = (id?: string) =>
|
|||||||
queryFn: () => fetchWorkflowIntegrations(),
|
queryFn: () => fetchWorkflowIntegrations(),
|
||||||
enabled: Boolean(id)
|
enabled: Boolean(id)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const useGetMicrosoftTeamsClientId = () =>
|
||||||
|
useQuery({
|
||||||
|
queryKey: workflowIntegrationKeys.getMicrosoftTeamsClientId(),
|
||||||
|
queryFn: () => fetchMicrosoftTeamsClientId(),
|
||||||
|
enabled: true
|
||||||
|
});
|
||||||
|
|||||||
@@ -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,90 @@ export type TUpdateSlackIntegrationDTO = {
|
|||||||
description?: string;
|
description?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TUpdateMicrosoftTeamsIntegrationDTO = {
|
||||||
|
id: string;
|
||||||
|
orgId: string;
|
||||||
|
slug?: string;
|
||||||
|
description?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TCreateMicrosoftTeamsIntegrationDTO = {
|
||||||
|
code: string;
|
||||||
|
tenantId: string;
|
||||||
|
slug: string;
|
||||||
|
description?: string;
|
||||||
|
redirectUri: string;
|
||||||
|
orgId: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type TDeleteSlackIntegrationDTO = {
|
export type TDeleteSlackIntegrationDTO = {
|
||||||
id: string;
|
id: string;
|
||||||
orgId: string;
|
orgId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ProjectSlackConfig = {
|
export type TDeleteMicrosoftTeamsIntegrationDTO = {
|
||||||
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;
|
||||||
isAccessRequestNotificationEnabled: boolean;
|
integration: WorkflowIntegrationPlatform.SLACK;
|
||||||
accessRequestChannels: string;
|
integrationId: string;
|
||||||
isSecretRequestNotificationEnabled: boolean;
|
isAccessRequestNotificationEnabled: boolean;
|
||||||
secretRequestChannels: string;
|
accessRequestChannels: string;
|
||||||
|
isSecretRequestNotificationEnabled: boolean;
|
||||||
|
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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -26,9 +26,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,
|
||||||
@@ -883,13 +886,27 @@ 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
|
||||||
`/api/v1/workspace/${workspaceId}/slack-config`
|
.get<ProjectWorkflowIntegrationConfig>(
|
||||||
);
|
`/api/v1/workspace/${workspaceId}/workflow-integration-config/${integration}`
|
||||||
|
)
|
||||||
|
.catch((err) => {
|
||||||
|
if (err.response.status === 404) {
|
||||||
|
return { data: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
|
||||||
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,
|
||||||
@@ -54,8 +55,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,
|
||||||
|
|||||||
@@ -197,4 +197,4 @@ html {
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0.5rem;
|
top: 0.5rem;
|
||||||
@apply text-sm text-gray-500 opacity-50;
|
@apply text-sm text-gray-500 opacity-50;
|
||||||
}
|
}
|
||||||
@@ -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 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>
|
</div>
|
||||||
<Button
|
<div className="flex flex-col gap-2">
|
||||||
className="mt-2"
|
<SlackIntegrationForm adminIntegrationsConfig={adminIntegrationsConfig} />
|
||||||
type="submit"
|
<MicrosoftTeamsIntegrationForm adminIntegrationsConfig={adminIntegrationsConfig} />
|
||||||
isLoading={isSubmitting}
|
</div>
|
||||||
isDisabled={isSubmitting || !isDirty}
|
</div>
|
||||||
>
|
|
||||||
Save
|
|
||||||
</Button>
|
|
||||||
</form>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,183 @@
|
|||||||
|
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-2 max-w-lg text-sm text-mineshaft-300">
|
||||||
|
Step 1: Create and configure Microsoft Teams bot and Azure Resources. Please refer
|
||||||
|
to the documentation below for more information.
|
||||||
|
</div>
|
||||||
|
<div className="mb-6">
|
||||||
|
<a
|
||||||
|
href="https://infisical.com/docs/documentation/platform/workflow-integrations/microsoft-teams-integration"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<Button colorSchema="secondary">Documentation</Button>
|
||||||
|
</a>
|
||||||
|
</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="Microsoft Teams App ID"
|
||||||
|
tooltipText="You can find the Microsoft Teams App ID in the overview of your Microsoft Teams App inside the Microsoft Teams Developer Portal."
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
import { useNavigate, useSearch } from "@tanstack/react-router";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import { ContentLoader } from "@app/components/v2";
|
||||||
|
import { ROUTE_PATHS } from "@app/const/routes";
|
||||||
|
import { useOrganization } from "@app/context";
|
||||||
|
import { useCreateMicrosoftTeamsIntegration } from "@app/hooks/api";
|
||||||
|
|
||||||
|
const stateSchema = z.object({
|
||||||
|
redirectUri: z.string(),
|
||||||
|
tenantId: z.string(),
|
||||||
|
slug: z.string(),
|
||||||
|
description: z.string().optional(),
|
||||||
|
csrfToken: z.string(),
|
||||||
|
clientId: z.string()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const OAuthCallbackPage = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { currentOrg } = useOrganization();
|
||||||
|
const [isReady, setIsReady] = useState(false);
|
||||||
|
|
||||||
|
const search = useSearch({
|
||||||
|
from: ROUTE_PATHS.Organization.Settings.OauthCallbackPage.id
|
||||||
|
});
|
||||||
|
|
||||||
|
const createMicrosoftTeamsWorkflowIntegration = useCreateMicrosoftTeamsIntegration();
|
||||||
|
|
||||||
|
const { state: rawState, code } = search;
|
||||||
|
|
||||||
|
const state = stateSchema.parse(rawState);
|
||||||
|
|
||||||
|
const clearState = () => {
|
||||||
|
if (state.csrfToken !== localStorage.getItem("latestCSRFToken")) {
|
||||||
|
throw new Error("Invalid CSRF token");
|
||||||
|
}
|
||||||
|
|
||||||
|
localStorage.removeItem("latestCSRFToken");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMicrosoftTeams = useCallback(async () => {
|
||||||
|
clearState();
|
||||||
|
|
||||||
|
if (!code) {
|
||||||
|
throw new Error("No code provided");
|
||||||
|
}
|
||||||
|
|
||||||
|
await createMicrosoftTeamsWorkflowIntegration.mutateAsync({
|
||||||
|
orgId: currentOrg.id,
|
||||||
|
tenantId: state.tenantId,
|
||||||
|
code,
|
||||||
|
slug: state.slug,
|
||||||
|
description: state.description ?? "",
|
||||||
|
redirectUri: state.redirectUri
|
||||||
|
});
|
||||||
|
|
||||||
|
navigate({
|
||||||
|
to: ROUTE_PATHS.Organization.SettingsPage.path
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Ensure that the localstorage is ready for use, to avoid the form data being malformed
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isReady) {
|
||||||
|
setIsReady(!!localStorage.length);
|
||||||
|
}
|
||||||
|
}, [localStorage.length]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isReady) return;
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
await handleMicrosoftTeams();
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
text: "Successfully created Microsoft Teams workflow integration",
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
createNotification({
|
||||||
|
text: "Failed to create Microsoft Teams workflow integration",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}, [isReady]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex h-full w-full items-center justify-center">
|
||||||
|
<ContentLoader text="Please wait! Authentication in process." />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
import { zodValidator } from "@tanstack/zod-adapter";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { OAuthCallbackPage } from "./OauthCallbackPage";
|
||||||
|
|
||||||
|
const SettingsOAuthCallbackPageQueryParamsSchema = z.object({
|
||||||
|
state: z
|
||||||
|
.object({
|
||||||
|
clientId: z.string(),
|
||||||
|
tenantId: z.string(),
|
||||||
|
slug: z.string(),
|
||||||
|
description: z.string().optional(),
|
||||||
|
redirectUri: z.string(),
|
||||||
|
csrfToken: z.string()
|
||||||
|
})
|
||||||
|
.nullable()
|
||||||
|
.catch(null),
|
||||||
|
code: z.string().catch("")
|
||||||
|
});
|
||||||
|
|
||||||
|
export const Route = createFileRoute(
|
||||||
|
"/_authenticate/_inject-org-details/_org-layout/organization/settings/oauth/callback"
|
||||||
|
)({
|
||||||
|
component: OAuthCallbackPage,
|
||||||
|
validateSearch: zodValidator(SettingsOAuthCallbackPageQueryParamsSchema)
|
||||||
|
});
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,192 @@
|
|||||||
|
import crypto from "crypto";
|
||||||
|
|
||||||
|
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 {
|
||||||
|
useGetMicrosoftTeamsClientId,
|
||||||
|
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()
|
||||||
|
.uuid("Tenant ID must be a valid UUID"),
|
||||||
|
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: updateMicrosoftTeamsIntegration } = useUpdateMicrosoftTeamsIntegration();
|
||||||
|
const { data: microsoftTeamsClientId } = useGetMicrosoftTeamsClientId();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (microsoftTeamsIntegration) {
|
||||||
|
setValue("slug", microsoftTeamsIntegration.slug);
|
||||||
|
setValue("description", microsoftTeamsIntegration.description ?? "");
|
||||||
|
setValue("tenantId", microsoftTeamsIntegration.tenantId);
|
||||||
|
}
|
||||||
|
}, [microsoftTeamsIntegration]);
|
||||||
|
|
||||||
|
const handleMicrosoftTeamsFormSubmit = async ({
|
||||||
|
slug,
|
||||||
|
description,
|
||||||
|
tenantId
|
||||||
|
}: TMicrosoftTeamsFormData) => {
|
||||||
|
if (!microsoftTeamsClientId) {
|
||||||
|
createNotification({
|
||||||
|
text: "Microsoft Teams client ID is not set. Please contact your instance administrator.",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
const csrfToken = crypto.randomBytes(32).toString("hex");
|
||||||
|
localStorage.setItem("latestCSRFToken", csrfToken);
|
||||||
|
|
||||||
|
const state = {
|
||||||
|
redirectUri: `${window.location.origin}/organization/settings/oauth/callback`,
|
||||||
|
tenantId,
|
||||||
|
slug,
|
||||||
|
description,
|
||||||
|
csrfToken,
|
||||||
|
clientId: microsoftTeamsClientId.clientId
|
||||||
|
};
|
||||||
|
|
||||||
|
const url = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/authorize?
|
||||||
|
client_id=${microsoftTeamsClientId.clientId}
|
||||||
|
&redirect_uri=${state.redirectUri}
|
||||||
|
&response_type=code
|
||||||
|
&response_mode=query
|
||||||
|
&scope=https://graph.microsoft.com/.default
|
||||||
|
&state=${encodeURIComponent(JSON.stringify(state))}
|
||||||
|
&prompt=consent
|
||||||
|
&admin_consent=true`;
|
||||||
|
|
||||||
|
window.location.href = url;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit(handleMicrosoftTeamsFormSubmit)} autoComplete="off">
|
||||||
|
<div className="mb-4 text-xs text-mineshaft-200">
|
||||||
|
For seamless installations, ensure that the Infisical bot is already installed in your
|
||||||
|
Microsoft Teams tenant. For more information, please refer to the{" "}
|
||||||
|
<a
|
||||||
|
className="text-primary-500"
|
||||||
|
href="https://infisical.com/docs/documentation/platform/workflow-integrations/microsoft-teams-integration"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
Microsoft Teams Workflow Integration Documentation
|
||||||
|
</a>
|
||||||
|
, which will guide you through the download and installation process.
|
||||||
|
</div>
|
||||||
|
<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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { faSlack } from "@fortawesome/free-brands-svg-icons";
|
import { BsMicrosoftTeams, BsSlack } from "react-icons/bs";
|
||||||
import { faEllipsis, faGear, faPlus } from "@fortawesome/free-solid-svg-icons";
|
import { faEllipsis, faGear, faInfoCircle, faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
@@ -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,
|
||||||
@@ -20,6 +21,7 @@ import {
|
|||||||
TBody,
|
TBody,
|
||||||
Td,
|
Td,
|
||||||
THead,
|
THead,
|
||||||
|
Tooltip,
|
||||||
Tr
|
Tr
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
|
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
|
||||||
@@ -27,14 +29,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 +67,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 +84,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,27 +98,37 @@ 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
|
});
|
||||||
});
|
|
||||||
|
|
||||||
if (slackReinstallUrl) {
|
if (slackReinstallUrl) {
|
||||||
window.location.href = slackReinstallUrl;
|
window.location.href = slackReinstallUrl;
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (axios.isAxiosError(err)) {
|
if (axios.isAxiosError(err)) {
|
||||||
createNotification({
|
createNotification({
|
||||||
text: (err.response?.data as { message: string })?.message,
|
text: (err.response?.data as { message: string })?.message,
|
||||||
type: "error"
|
type: "error"
|
||||||
});
|
});
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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 (
|
||||||
<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">
|
||||||
@@ -117,6 +156,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>
|
||||||
@@ -134,11 +174,23 @@ export const OrgWorkflowIntegrationTab = withPermission(
|
|||||||
)}
|
)}
|
||||||
{workflowIntegrations?.map((workflowIntegration) => (
|
{workflowIntegrations?.map((workflowIntegration) => (
|
||||||
<Tr key={workflowIntegration.id}>
|
<Tr key={workflowIntegration.id}>
|
||||||
<Td className="flex max-w-xs items-center overflow-hidden text-ellipsis hover:overflow-auto hover:break-all">
|
<Td className="flex max-w-xs items-center gap-2 overflow-hidden text-ellipsis hover:overflow-auto hover:break-all">
|
||||||
<FontAwesomeIcon icon={faSlack} />
|
{workflowIntegration.integration === WorkflowIntegrationPlatform.SLACK ? (
|
||||||
<div className="ml-2">{workflowIntegration.integration.toUpperCase()}</div>
|
<BsSlack />
|
||||||
|
) : (
|
||||||
|
<BsMicrosoftTeams />
|
||||||
|
)}
|
||||||
|
<div className="capitalize">
|
||||||
|
{workflowIntegration.integration.replaceAll("-", " ")}
|
||||||
|
</div>
|
||||||
|
{workflowIntegration.description && (
|
||||||
|
<Tooltip content={workflowIntegration.description}>
|
||||||
|
<FontAwesomeIcon icon={faInfoCircle} className="text-gray-400" size="sm" />
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
</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,29 +211,56 @@ export const OrgWorkflowIntegrationTab = withPermission(
|
|||||||
>
|
>
|
||||||
More details
|
More details
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<OrgPermissionCan
|
|
||||||
I={OrgPermissionActions.Create}
|
|
||||||
an={OrgPermissionSubjects.Settings}
|
|
||||||
>
|
|
||||||
{(isAllowed) => (
|
|
||||||
<DropdownMenuItem
|
|
||||||
disabled={!isAllowed}
|
|
||||||
className={twMerge(
|
|
||||||
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
|
|
||||||
)}
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
|
|
||||||
triggerReinstall(
|
{workflowIntegration.integration === WorkflowIntegrationPlatform.SLACK && (
|
||||||
workflowIntegration.integration,
|
<OrgPermissionCan
|
||||||
workflowIntegration.id
|
I={OrgPermissionActions.Create}
|
||||||
);
|
an={OrgPermissionSubjects.Settings}
|
||||||
}}
|
>
|
||||||
>
|
{(isAllowed) => (
|
||||||
Reinstall
|
<DropdownMenuItem
|
||||||
</DropdownMenuItem>
|
disabled={!isAllowed}
|
||||||
)}
|
className={twMerge(
|
||||||
</OrgPermissionCan>
|
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
|
||||||
|
)}
|
||||||
|
onClick={async (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
await triggerSlackReinstall(workflowIntegration.id);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Reinstall
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
</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}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ const SettingsPageQueryParams = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const Route = createFileRoute(
|
export const Route = createFileRoute(
|
||||||
"/_authenticate/_inject-org-details/_org-layout/organization/settings"
|
"/_authenticate/_inject-org-details/_org-layout/organization/settings/"
|
||||||
)({
|
)({
|
||||||
component: SettingsPage,
|
component: SettingsPage,
|
||||||
validateSearch: zodValidator(SettingsPageQueryParams),
|
validateSearch: zodValidator(SettingsPageQueryParams),
|
||||||
|
|||||||
@@ -1,351 +1,163 @@
|
|||||||
import { useEffect } from "react";
|
import { BsMicrosoftTeams, BsSlack } from "react-icons/bs";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { faGear, 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,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
EmptyState,
|
EmptyState,
|
||||||
FormControl,
|
Table,
|
||||||
Input,
|
TableContainer,
|
||||||
Select,
|
TBody,
|
||||||
SelectItem,
|
Td,
|
||||||
Switch
|
THead,
|
||||||
|
Tr
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
import { OrgPermissionActions, OrgPermissionSubjects, useWorkspace } from "@app/context";
|
||||||
|
import { usePopUp } from "@app/hooks";
|
||||||
import {
|
import {
|
||||||
useGetSlackIntegrationChannels,
|
useDeleteProjectWorkflowIntegration,
|
||||||
useGetSlackIntegrations,
|
useGetWorkspaceWorkflowIntegrationConfig
|
||||||
useGetWorkspaceSlackConfig,
|
|
||||||
useUpdateProjectSlackConfig
|
|
||||||
} from "@app/hooks/api";
|
} from "@app/hooks/api";
|
||||||
|
import { WorkflowIntegrationPlatform } from "@app/hooks/api/workflowIntegrations/types";
|
||||||
|
|
||||||
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({
|
const { data: slackConfig, isPending: isSlackConfigLoading } =
|
||||||
workspaceId: currentWorkspace?.id ?? ""
|
useGetWorkspaceWorkflowIntegrationConfig({
|
||||||
});
|
workspaceId: currentWorkspace?.id ?? "",
|
||||||
const { data: slackIntegrations } = useGetSlackIntegrations(currentWorkspace?.orgId);
|
integration: WorkflowIntegrationPlatform.SLACK
|
||||||
const { mutateAsync: updateProjectSlackConfig } = useUpdateProjectSlackConfig();
|
});
|
||||||
|
|
||||||
const {
|
const { data: microsoftTeamsConfig, isPending: isMicrosoftTeamsConfigLoading } =
|
||||||
control,
|
useGetWorkspaceWorkflowIntegrationConfig({
|
||||||
watch,
|
workspaceId: currentWorkspace?.id ?? "",
|
||||||
handleSubmit,
|
integration: WorkflowIntegrationPlatform.MICROSOFT_TEAMS
|
||||||
setValue,
|
});
|
||||||
formState: { isDirty, isSubmitting }
|
|
||||||
} = useForm<TSlackConfigForm>({
|
|
||||||
resolver: zodResolver(formSchema),
|
|
||||||
defaultValues: {
|
|
||||||
isAccessRequestNotificationEnabled: false,
|
|
||||||
accessRequestChannels: [],
|
|
||||||
isSecretRequestNotificationEnabled: false,
|
|
||||||
secretRequestChannels: []
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const secretRequestNotifState = watch("isSecretRequestNotificationEnabled");
|
const { mutateAsync: deleteIntegration } = useDeleteProjectWorkflowIntegration();
|
||||||
const selectedSlackIntegrationId = watch("slackIntegrationId");
|
|
||||||
const accessRequestNotifState = watch("isAccessRequestNotificationEnabled");
|
|
||||||
|
|
||||||
const { data: slackChannels } = useGetSlackIntegrationChannels(selectedSlackIntegrationId);
|
const handleRemoveIntegration = async (
|
||||||
const slackChannelIdToName = Object.fromEntries(
|
integrationType: WorkflowIntegrationPlatform,
|
||||||
(slackChannels || []).map((channel) => [channel.id, channel.name])
|
integrationId: string
|
||||||
);
|
) => {
|
||||||
const sortedSlackChannels = slackChannels?.sort((a, b) =>
|
if (!currentWorkspace.id) {
|
||||||
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">
|
{!!slackConfig || !!microsoftTeamsConfig ? (
|
||||||
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Settings}>
|
<Table>
|
||||||
{(isAllowed) => (
|
<THead>
|
||||||
<Controller
|
<Tr>
|
||||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
<Td>Provider</Td>
|
||||||
<FormControl errorText={error?.message} isError={Boolean(error)}>
|
<Td>Access Request Notifications Destination</Td>
|
||||||
<Select
|
<Td>Secret Request Notifications Destination</Td>
|
||||||
{...field}
|
<Td />
|
||||||
isDisabled={!isAllowed}
|
</Tr>
|
||||||
placeholder="None"
|
</THead>
|
||||||
onValueChange={onChange}
|
<TBody>
|
||||||
defaultValue={slackConfig?.slackIntegrationId}
|
<SlackConfigRow
|
||||||
className="w-3/4 bg-mineshaft-600"
|
handlePopUpOpen={handlePopUpOpen}
|
||||||
>
|
isSlackConfigLoading={isSlackConfigLoading}
|
||||||
{slackIntegrations?.map((slackIntegration) => (
|
slackConfig={slackConfig}
|
||||||
<SelectItem
|
|
||||||
value={slackIntegration.id}
|
|
||||||
key={`slack-integration-${slackIntegration.id}`}
|
|
||||||
>
|
|
||||||
{slackIntegration.slug}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
control={control}
|
|
||||||
name="slackIntegrationId"
|
|
||||||
/>
|
/>
|
||||||
)}
|
<MicrosoftTeamsConfigRow
|
||||||
</ProjectPermissionCan>
|
handlePopUpOpen={handlePopUpOpen}
|
||||||
</div>
|
isMicrosoftTeamsConfigLoading={isMicrosoftTeamsConfigLoading}
|
||||||
{selectedSlackIntegrationId && (
|
microsoftTeamsConfig={microsoftTeamsConfig}
|
||||||
<>
|
/>
|
||||||
<h2 className="mb-2 flex-1 text-lg font-semibold text-mineshaft-100">Events</h2>
|
</TBody>
|
||||||
<Controller
|
</Table>
|
||||||
control={control}
|
) : (
|
||||||
name="isSecretRequestNotificationEnabled"
|
<div className="flex h-full w-full items-center justify-center">
|
||||||
render={({ field, fieldState: { error } }) => {
|
<EmptyState
|
||||||
return (
|
title="No project workflow integrations configured. Add a new workflow integration to get started"
|
||||||
<FormControl
|
icon={faGear}
|
||||||
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 && (
|
</div>
|
||||||
<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>
|
</TableContainer>
|
||||||
|
<AddWorkflowIntegrationModal
|
||||||
|
isOpen={popUp.addWorkflowIntegration.isOpen}
|
||||||
|
onToggle={(state) => handlePopUpToggle("addWorkflowIntegration", state)}
|
||||||
|
/>
|
||||||
|
<DeleteActionModal
|
||||||
|
isOpen={popUp.removeIntegration.isOpen}
|
||||||
|
title="Are you sure want to remove this integration?"
|
||||||
|
onChange={(isOpen) => handlePopUpToggle("removeIntegration", isOpen)}
|
||||||
|
deleteKey="confirm"
|
||||||
|
onDeleteApproved={async () => {
|
||||||
|
await handleRemoveIntegration(
|
||||||
|
popUp.removeIntegration.data?.integration,
|
||||||
|
popUp.removeIntegration.data?.integrationId
|
||||||
|
);
|
||||||
|
handlePopUpClose("removeIntegration");
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<EditWorkflowIntegrationModal
|
||||||
|
isOpen={popUp.editIntegration.isOpen}
|
||||||
|
onClose={() => handlePopUpClose("editIntegration")}
|
||||||
|
integration={popUp.editIntegration.data?.integration}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,153 @@
|
|||||||
|
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" key={platform}>
|
||||||
|
<div
|
||||||
|
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">
|
||||||
|
Already Configured
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{wizardStep === WizardSteps.PlatformInputs &&
|
||||||
|
selectedPlatform === WorkflowIntegrationPlatform.SLACK && (
|
||||||
|
<motion.div
|
||||||
|
key="platform-inputs-step-slack"
|
||||||
|
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-ms-teams"
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { Modal, ModalContent } from "@app/components/v2";
|
||||||
|
import { WorkflowIntegrationPlatform } from "@app/hooks/api/workflowIntegrations/types";
|
||||||
|
|
||||||
|
import { MicrosoftTeamsIntegrationForm } from "./MicrosoftTeamsIntegrationForm";
|
||||||
|
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} />
|
||||||
|
)}
|
||||||
|
{integration === WorkflowIntegrationPlatform.MICROSOFT_TEAMS && (
|
||||||
|
<MicrosoftTeamsIntegrationForm onClose={handleFormReset} />
|
||||||
|
)}
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
/* eslint-disable no-nested-ternary */
|
||||||
|
import { BsMicrosoftTeams } from "react-icons/bs";
|
||||||
|
import { faEllipsis } 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 } from "@app/context";
|
||||||
|
import { useGetMicrosoftTeamsIntegrationTeams } from "@app/hooks/api";
|
||||||
|
import {
|
||||||
|
ProjectWorkflowIntegrationConfig,
|
||||||
|
WorkflowIntegrationPlatform
|
||||||
|
} from "@app/hooks/api/workflowIntegrations/types";
|
||||||
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
microsoftTeamsConfig?: ProjectWorkflowIntegrationConfig | null;
|
||||||
|
isMicrosoftTeamsConfigLoading: boolean;
|
||||||
|
handlePopUpOpen: (
|
||||||
|
popUpName: keyof UsePopUpState<["removeIntegration", "editIntegration"]>,
|
||||||
|
data?: {
|
||||||
|
integrationId?: string;
|
||||||
|
integration: WorkflowIntegrationPlatform;
|
||||||
|
}
|
||||||
|
) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MicrosoftTeamsConfigRow = ({
|
||||||
|
handlePopUpOpen,
|
||||||
|
isMicrosoftTeamsConfigLoading,
|
||||||
|
microsoftTeamsConfig
|
||||||
|
}: Props) => {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isLoadingConfig = isMicrosoftTeamsChannelsLoading || isMicrosoftTeamsConfigLoading;
|
||||||
|
|
||||||
|
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 &&
|
||||||
|
!isLoadingConfig &&
|
||||||
|
microsoftTeamsConfig.accessRequestChannels?.channelIds?.length > 0 ? (
|
||||||
|
<Badge>
|
||||||
|
{microsoftTeamsConfig.accessRequestChannels.channelIds
|
||||||
|
.map((channel) => microsoftTeamsChannelIdToName[channel])
|
||||||
|
.join(", ")}
|
||||||
|
</Badge>
|
||||||
|
) : isLoadingConfig ? (
|
||||||
|
<Spinner size="xs" />
|
||||||
|
) : (
|
||||||
|
<Badge variant="danger">Disabled</Badge>
|
||||||
|
)}
|
||||||
|
</Td>
|
||||||
|
<Td>
|
||||||
|
{microsoftTeamsConfig.isSecretRequestNotificationEnabled &&
|
||||||
|
!isLoadingConfig &&
|
||||||
|
microsoftTeamsConfig.secretRequestChannels?.channelIds?.length > 0 ? (
|
||||||
|
<Badge>
|
||||||
|
{microsoftTeamsConfig.secretRequestChannels.channelIds
|
||||||
|
.map((channel) => microsoftTeamsChannelIdToName[channel])
|
||||||
|
.join(", ")}
|
||||||
|
</Badge>
|
||||||
|
) : isLoadingConfig ? (
|
||||||
|
<Spinner size="xs" />
|
||||||
|
) : (
|
||||||
|
<Badge variant="danger">Disabled</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.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>
|
||||||
|
<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>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,556 @@
|
|||||||
|
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()
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
accessRequestChannels: z
|
||||||
|
.object({
|
||||||
|
teamId: z.string(),
|
||||||
|
channelIds: z.string().array()
|
||||||
|
})
|
||||||
|
.optional()
|
||||||
|
})
|
||||||
|
.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 TMicrosoftTeamsConfigForm = 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<TMicrosoftTeamsConfigForm>({
|
||||||
|
resolver: zodResolver(formSchema),
|
||||||
|
defaultValues: {
|
||||||
|
isAccessRequestNotificationEnabled: false,
|
||||||
|
isSecretRequestNotificationEnabled: false,
|
||||||
|
accessRequestChannels: {
|
||||||
|
teamId: "",
|
||||||
|
channelIds: []
|
||||||
|
},
|
||||||
|
secretRequestChannels: {
|
||||||
|
teamId: "",
|
||||||
|
channelIds: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleIntegrationSave = async (data: TMicrosoftTeamsConfigForm) => {
|
||||||
|
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");
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
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) {
|
||||||
|
if (Object.entries(microsoftTeamsConfig.accessRequestChannels).length) {
|
||||||
|
setValue("accessRequestChannels", microsoftTeamsConfig.accessRequestChannels);
|
||||||
|
}
|
||||||
|
if (Object.entries(microsoftTeamsConfig.secretRequestChannels).length) {
|
||||||
|
setValue("secretRequestChannels", microsoftTeamsConfig.secretRequestChannels);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [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={(v) => {
|
||||||
|
onChange(v);
|
||||||
|
|
||||||
|
setValue("isAccessRequestNotificationEnabled", false);
|
||||||
|
setValue("isSecretRequestNotificationEnabled", false);
|
||||||
|
setValue("accessRequestChannels", {
|
||||||
|
teamId: "",
|
||||||
|
channelIds: []
|
||||||
|
});
|
||||||
|
setValue("secretRequestChannels", {
|
||||||
|
teamId: "",
|
||||||
|
channelIds: []
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
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-microsoft-teams-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"
|
||||||
|
disabled={!isDirty}
|
||||||
|
isDisabled={!isDirty}
|
||||||
|
isLoading={isSubmitting}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||