From 15c5fe40951f34fbf9255e096edf4cb0e19ffe8e Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Wed, 4 Sep 2024 15:44:58 +0800 Subject: [PATCH] misc: slack integration reinstall --- .../ee/services/audit-log/audit-log-types.ts | 9 ++ backend/src/server/routes/v1/slack-router.ts | 44 ++++++++++ backend/src/services/slack/slack-service.ts | 82 +++++++++++++++++++ backend/src/services/slack/slack-types.ts | 15 ++++ .../hooks/api/workflowIntegrations/index.ts | 1 + .../api/workflowIntegrations/queries.tsx | 14 ++++ .../OrgWorkflowIntegrationTab.tsx | 43 +++++++++- 7 files changed, 207 insertions(+), 1 deletion(-) diff --git a/backend/src/ee/services/audit-log/audit-log-types.ts b/backend/src/ee/services/audit-log/audit-log-types.ts index b57505f60..b70f9dfea 100644 --- a/backend/src/ee/services/audit-log/audit-log-types.ts +++ b/backend/src/ee/services/audit-log/audit-log-types.ts @@ -171,6 +171,7 @@ export enum EventType { UPDATE_CERTIFICATE_TEMPLATE_EST_CONFIG = "update-certificate-template-est-config", GET_CERTIFICATE_TEMPLATE_EST_CONFIG = "get-certificate-template-est-config", ATTEMPT_CREATE_SLACK_INTEGRATION = "attempt-create-slack-integration", + ATTEMPT_REINSTALL_SLACK_INTEGRATION = "attempt-reinstall-slack-integration", GET_SLACK_INTEGRATION = "get-slack-integration", UPDATE_SLACK_INTEGRATION = "update-slack-integration", DELETE_SLACK_INTEGRATION = "delete-slack-integration", @@ -1460,6 +1461,13 @@ interface AttemptCreateSlackIntegration { }; } +interface AttemptReinstallSlackIntegration { + type: EventType.ATTEMPT_REINSTALL_SLACK_INTEGRATION; + metadata: { + id: string; + }; +} + interface UpdateSlackIntegration { type: EventType.UPDATE_SLACK_INTEGRATION; metadata: { @@ -1634,6 +1642,7 @@ export type Event = | UpdateCertificateTemplateEstConfig | GetCertificateTemplateEstConfig | AttemptCreateSlackIntegration + | AttemptReinstallSlackIntegration | UpdateSlackIntegration | DeleteSlackIntegration | GetSlackIntegration diff --git a/backend/src/server/routes/v1/slack-router.ts b/backend/src/server/routes/v1/slack-router.ts index 1033b6d51..5a9bc67e5 100644 --- a/backend/src/server/routes/v1/slack-router.ts +++ b/backend/src/server/routes/v1/slack-router.ts @@ -56,6 +56,50 @@ export const registerSlackRouter = async (server: FastifyZodProvider) => { } }); + server.route({ + method: "GET", + url: "/reinstall", + config: { + rateLimit: readLimit + }, + schema: { + security: [ + { + bearerAuth: [] + } + ], + querystring: z.object({ + slackIntegrationId: z.string() + }), + response: { + 200: z.string() + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const url = await server.services.slack.getReinstallUrl({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + id: req.query.slackIntegrationId + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + event: { + type: EventType.ATTEMPT_REINSTALL_SLACK_INTEGRATION, + metadata: { + id: req.query.slackIntegrationId + } + } + }); + + return url; + } + }); + server.route({ method: "GET", url: "/", diff --git a/backend/src/services/slack/slack-service.ts b/backend/src/services/slack/slack-service.ts index b6a60492d..f3da106c9 100644 --- a/backend/src/services/slack/slack-service.ts +++ b/backend/src/services/slack/slack-service.ts @@ -12,9 +12,11 @@ import { TSlackIntegrationDALFactory } from "./slack-integration-dal"; import { TCompleteSlackIntegrationDTO, TDeleteSlackIntegrationDTO, + TGetReinstallUrlDTO, TGetSlackInstallUrlDTO, TGetSlackIntegrationByIdDTO, TGetSlackIntegrationByOrgDTO, + TReinstallSlackIntegrationDTO, TUpdateSlackIntegrationDTO } from "./slack-types"; @@ -66,6 +68,38 @@ export const slackServiceFactory = ({ }); }; + const reinstallSlackIntegration = async ({ + id, + teamId, + teamName, + slackUserId, + slackAppId, + botAccessToken, + slackBotId, + slackBotUserId + }: TReinstallSlackIntegrationDTO) => { + const slackIntegration = await slackIntegrationDAL.findById(id); + + const { encryptor: orgDataKeyEncryptor } = await kmsService.createCipherPairWithDataKey({ + orgId: slackIntegration.orgId, + type: KmsDataKey.Organization + }); + + const { cipherTextBlob: encryptedBotAccessToken } = orgDataKeyEncryptor({ + plainText: Buffer.from(botAccessToken, "utf8") + }); + + await slackIntegrationDAL.updateById(id, { + teamId, + teamName, + slackUserId, + slackAppId, + slackBotId, + slackBotUserId, + encryptedBotAccessToken + }); + }; + const getSlackInstaller = async () => { const appCfg = getConfig(); @@ -89,11 +123,25 @@ export const slackServiceFactory = ({ } const metadata = JSON.parse(installation.metadata || "") as { + id?: string; orgId: string; slug: string; description?: string; }; + if (metadata.id) { + return reinstallSlackIntegration({ + id: metadata.id, + teamId: installation.team?.id || "", + teamName: installation.team?.name || "", + slackUserId: installation.user.id, + slackAppId: installation.appId || "", + botAccessToken: installation.bot?.token || "", + slackBotId: installation.bot?.id || "", + slackBotUserId: installation.bot?.userId || "" + }); + } + return completeSlackIntegration({ orgId: metadata.orgId, slug: metadata.slug, @@ -154,6 +202,39 @@ export const slackServiceFactory = ({ return url; }; + const getReinstallUrl = async ({ actorId, actor, actorOrgId, actorAuthMethod, id }: TGetReinstallUrlDTO) => { + const appCfg = getConfig(); + const slackIntegration = await slackIntegrationDAL.findById(id); + + if (!slackIntegration) { + throw new NotFoundError({ + message: "Slack integration not found" + }); + } + + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + slackIntegration.orgId, + actorAuthMethod, + actorOrgId + ); + + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Settings); + + const installer = await getSlackInstaller(); + const url = await installer.generateInstallUrl({ + scopes: ["chat:write.public", "chat:write", "channels:read", "groups:read", "im:read", "mpim:read"], + metadata: JSON.stringify({ + id, + orgId: slackIntegration.orgId + }), + redirectUri: `${appCfg.SITE_URL}/api/v1/workflow-integrations/slack/oauth_redirect` + }); + + return url; + }; + const getSlackIntegrationsByOrg = async ({ actorId, actor, @@ -265,6 +346,7 @@ export const slackServiceFactory = ({ return { getInstallUrl, + getReinstallUrl, getSlackIntegrationsByOrg, getSlackIntegrationById, completeSlackIntegration, diff --git a/backend/src/services/slack/slack-types.ts b/backend/src/services/slack/slack-types.ts index ab9035b09..b1b64ecba 100644 --- a/backend/src/services/slack/slack-types.ts +++ b/backend/src/services/slack/slack-types.ts @@ -5,6 +5,10 @@ export type TGetSlackInstallUrlDTO = { description?: string; } & Omit; +export type TGetReinstallUrlDTO = { + id: string; +} & Omit; + export type TGetSlackIntegrationByOrgDTO = Omit; export type TGetSlackIntegrationByIdDTO = { id: string } & Omit; @@ -31,6 +35,17 @@ export type TCompleteSlackIntegrationDTO = { slackBotUserId: string; }; +export type TReinstallSlackIntegrationDTO = { + id: string; + teamId: string; + teamName: string; + slackUserId: string; + slackAppId: string; + botAccessToken: string; + slackBotId: string; + slackBotUserId: string; +}; + export enum SlackTriggerFeature { SECRET_APPROVAL = "secret-approval", ACCESS_REQUEST = "access-request" diff --git a/frontend/src/hooks/api/workflowIntegrations/index.ts b/frontend/src/hooks/api/workflowIntegrations/index.ts index 9de1406fc..5ad8f9105 100644 --- a/frontend/src/hooks/api/workflowIntegrations/index.ts +++ b/frontend/src/hooks/api/workflowIntegrations/index.ts @@ -5,6 +5,7 @@ export { } from "./mutation"; export { fetchSlackInstallUrl, + fetchSlackReinstallUrl, useGetSlackIntegrationById, useGetSlackIntegrations } from "./queries"; diff --git a/frontend/src/hooks/api/workflowIntegrations/queries.tsx b/frontend/src/hooks/api/workflowIntegrations/queries.tsx index 30e028828..7f4c96e62 100644 --- a/frontend/src/hooks/api/workflowIntegrations/queries.tsx +++ b/frontend/src/hooks/api/workflowIntegrations/queries.tsx @@ -26,6 +26,20 @@ export const fetchSlackInstallUrl = async ({ return data; }; +export const fetchSlackReinstallUrl = async ({ + slackIntegrationId +}: { + slackIntegrationId: string; +}) => { + const { data } = await apiRequest.get("/api/v1/workflow-integrations/slack/reinstall", { + params: { + slackIntegrationId + } + }); + + return data; +}; + export const fetchSlackIntegrations = async () => { const { data } = await apiRequest.get("/api/v1/workflow-integrations/slack"); diff --git a/frontend/src/views/Settings/OrgSettingsPage/components/OrgWorkflowIntegrationTab/OrgWorkflowIntegrationTab.tsx b/frontend/src/views/Settings/OrgSettingsPage/components/OrgWorkflowIntegrationTab/OrgWorkflowIntegrationTab.tsx index 9c2018168..ee37f5685 100644 --- a/frontend/src/views/Settings/OrgSettingsPage/components/OrgWorkflowIntegrationTab/OrgWorkflowIntegrationTab.tsx +++ b/frontend/src/views/Settings/OrgSettingsPage/components/OrgWorkflowIntegrationTab/OrgWorkflowIntegrationTab.tsx @@ -1,3 +1,4 @@ +import { useRouter } from "next/router"; import { faSlack } from "@fortawesome/free-brands-svg-icons"; import { faEllipsis, faGear, faPlus } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; @@ -24,7 +25,11 @@ import { import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context"; import { withPermission } from "@app/hoc"; import { usePopUp } from "@app/hooks"; -import { useDeleteSlackIntegration, useGetSlackIntegrations } from "@app/hooks/api"; +import { + fetchSlackReinstallUrl, + useDeleteSlackIntegration, + useGetSlackIntegrations +} from "@app/hooks/api"; import { WorkflowIntegrationPlatform } from "@app/hooks/api/workflowIntegrations/types"; import { AddWorkflowIntegrationForm } from "./AddWorkflowIntegrationForm"; @@ -39,6 +44,7 @@ export const OrgWorkflowIntegrationTab = withPermission( ] as const); const { currentOrg } = useOrganization(); + const router = useRouter(); const { data: slackIntegrations, isLoading: isSlackIntegrationsLoading } = useGetSlackIntegrations(currentOrg?.id); const { mutateAsync: deleteSlackIntegration } = useDeleteSlackIntegration(); @@ -63,6 +69,18 @@ export const OrgWorkflowIntegrationTab = withPermission( }); }; + const triggerReinstall = async (platform: WorkflowIntegrationPlatform, id: string) => { + if (platform === WorkflowIntegrationPlatform.SLACK) { + const slackReinstallUrl = await fetchSlackReinstallUrl({ + slackIntegrationId: id + }); + + if (slackReinstallUrl) { + router.push(slackReinstallUrl); + } + } + }; + const isIntegrationsLoading = isSlackIntegrationsLoading; return ( @@ -132,6 +150,29 @@ export const OrgWorkflowIntegrationTab = withPermission( > More details + + {(isAllowed) => ( + { + e.stopPropagation(); + + triggerReinstall( + WorkflowIntegrationPlatform.SLACK, + slackIntegration.id + ); + }} + > + Reinstall + + )} +