From 07fd489982c60c7533af4b82cdbcecac8a803753 Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Mon, 2 Sep 2024 20:34:13 +0800 Subject: [PATCH] feat: slack integration deletion --- backend/src/server/routes/v1/slack-router.ts | 40 +++ backend/src/services/slack/slack-service.ts | 43 ++- backend/src/services/slack/slack-types.ts | 8 +- frontend/src/hooks/api/slack/index.ts | 2 +- frontend/src/hooks/api/slack/mutation.tsx | 17 +- frontend/src/hooks/api/slack/types.ts | 5 + .../NotificationSection/NotificationTab.tsx | 296 ++++++++++-------- 7 files changed, 276 insertions(+), 135 deletions(-) diff --git a/backend/src/server/routes/v1/slack-router.ts b/backend/src/server/routes/v1/slack-router.ts index 57fcd5ec3..ac4800ccb 100644 --- a/backend/src/server/routes/v1/slack-router.ts +++ b/backend/src/server/routes/v1/slack-router.ts @@ -126,6 +126,46 @@ export const registerSlackRouter = async (server: FastifyZodProvider) => { } }); + server.route({ + method: "DELETE", + url: "/:slackIntegrationId", + config: { + rateLimit: writeLimit + }, + schema: { + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + slackIntegrationId: z.string() + }), + response: { + 200: SlackIntegrationsSchema.pick({ + id: true, + teamName: true, + isAccessRequestNotificationEnabled: true, + accessRequestChannels: true, + isSecretRequestNotificationEnabled: true, + secretRequestChannels: true + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + // TODO: add audit logs + + return server.services.slack.deleteSlackIntegration({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + id: req.params.slackIntegrationId + }); + } + }); + server.route({ method: "GET", url: "/oauth_redirect", diff --git a/backend/src/services/slack/slack-service.ts b/backend/src/services/slack/slack-service.ts index 347d2acd3..d4a2dd81b 100644 --- a/backend/src/services/slack/slack-service.ts +++ b/backend/src/services/slack/slack-service.ts @@ -12,13 +12,17 @@ import { TProjectDALFactory } from "../project/project-dal"; import { TSlackIntegrationDALFactory } from "./slack-integration-dal"; import { TCompleteSlackIntegrationDTO, + TDeleteSlackIntegrationDTO, TGetSlackInstallUrlDTO, - TGetSlackIntegrationByProjectId, - TUpdateSlackIntegration + TGetSlackIntegrationByProjectIdDTO, + TUpdateSlackIntegrationDTO } from "./slack-types"; type TSlackServiceFactoryDep = { - slackIntegrationDAL: Pick; + slackIntegrationDAL: Pick< + TSlackIntegrationDALFactory, + "create" | "findOne" | "findById" | "updateById" | "deleteById" + >; permissionService: Pick; projectDAL: Pick; kmsService: Pick; @@ -156,7 +160,7 @@ export const slackServiceFactory = ({ actorOrgId, actorAuthMethod, projectId - }: TGetSlackIntegrationByProjectId) => { + }: TGetSlackIntegrationByProjectIdDTO) => { const { permission } = await permissionService.getProjectPermission( actor, actorId, @@ -183,7 +187,7 @@ export const slackServiceFactory = ({ accessRequestChannels, isSecretRequestNotificationEnabled, secretRequestChannels - }: TUpdateSlackIntegration) => { + }: TUpdateSlackIntegrationDTO) => { const slackIntegration = await slackIntegrationDAL.findById(id); if (!slackIntegration) { throw new NotFoundError({ @@ -208,11 +212,38 @@ export const slackServiceFactory = ({ }); }; + const deleteSlackIntegration = async ({ + actorId, + actor, + actorOrgId, + actorAuthMethod, + id + }: TDeleteSlackIntegrationDTO) => { + const slackIntegration = await slackIntegrationDAL.findById(id); + if (!slackIntegration) { + throw new NotFoundError({ + message: "Slack integration not found" + }); + } + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + slackIntegration.projectId, + actorAuthMethod, + actorOrgId + ); + + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Settings); + + return slackIntegrationDAL.deleteById(id); + }; + return { getInstallUrl, getSlackIntegrationByProjectId, completeSlackIntegration, getSlackInstaller, - updateSlackIntegration + updateSlackIntegration, + deleteSlackIntegration }; }; diff --git a/backend/src/services/slack/slack-types.ts b/backend/src/services/slack/slack-types.ts index c9f0127c0..55d836662 100644 --- a/backend/src/services/slack/slack-types.ts +++ b/backend/src/services/slack/slack-types.ts @@ -2,9 +2,9 @@ import { TProjectPermission } from "@app/lib/types"; export type TGetSlackInstallUrlDTO = TProjectPermission; -export type TGetSlackIntegrationByProjectId = TProjectPermission; +export type TGetSlackIntegrationByProjectIdDTO = TProjectPermission; -export type TUpdateSlackIntegration = { +export type TUpdateSlackIntegrationDTO = { id: string; isAccessRequestNotificationEnabled?: boolean; accessRequestChannels?: string; @@ -12,6 +12,10 @@ export type TUpdateSlackIntegration = { secretRequestChannels?: string; } & Omit; +export type TDeleteSlackIntegrationDTO = { + id: string; +} & Omit; + export type TCompleteSlackIntegrationDTO = { projectId: string; teamId: string; diff --git a/frontend/src/hooks/api/slack/index.ts b/frontend/src/hooks/api/slack/index.ts index e015740d7..73df6a19c 100644 --- a/frontend/src/hooks/api/slack/index.ts +++ b/frontend/src/hooks/api/slack/index.ts @@ -1,2 +1,2 @@ -export { useUpdateSlackIntegration } from "./mutation"; +export { useDeleteSlackIntegration, useUpdateSlackIntegration } from "./mutation"; export { fetchSlackInstallUrl, useGetSlackIntegrationByProject } from "./queries"; diff --git a/frontend/src/hooks/api/slack/mutation.tsx b/frontend/src/hooks/api/slack/mutation.tsx index 338952aef..67f34d9a4 100644 --- a/frontend/src/hooks/api/slack/mutation.tsx +++ b/frontend/src/hooks/api/slack/mutation.tsx @@ -3,7 +3,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; import { slackKeys } from "./queries"; -import { TUpdateSlackIntegrationDTO } from "./types"; +import { TDeleteSlackIntegrationDTO, TUpdateSlackIntegrationDTO } from "./types"; export const useUpdateSlackIntegration = () => { const queryClient = useQueryClient(); @@ -19,3 +19,18 @@ export const useUpdateSlackIntegration = () => { } }); }; + +export const useDeleteSlackIntegration = () => { + const queryClient = useQueryClient(); + + return useMutation<{}, {}, TDeleteSlackIntegrationDTO>({ + mutationFn: async (dto) => { + const { data } = await apiRequest.delete(`/api/v1/slack/${dto.id}`); + + return data; + }, + onSuccess: (_, { workspaceId }) => { + queryClient.invalidateQueries(slackKeys.getSlackIntegrationByProject(workspaceId)); + } + }); +}; diff --git a/frontend/src/hooks/api/slack/types.ts b/frontend/src/hooks/api/slack/types.ts index 9453c5d75..9adad3f1b 100644 --- a/frontend/src/hooks/api/slack/types.ts +++ b/frontend/src/hooks/api/slack/types.ts @@ -15,3 +15,8 @@ export type TUpdateSlackIntegrationDTO = { isSecretRequestNotificationEnabled?: boolean; secretRequestChannels?: string; }; + +export type TDeleteSlackIntegrationDTO = { + id: string; + workspaceId: string; +}; diff --git a/frontend/src/views/Settings/ProjectSettingsPage/components/NotificationSection/NotificationTab.tsx b/frontend/src/views/Settings/ProjectSettingsPage/components/NotificationSection/NotificationTab.tsx index da77b91b0..9f0dadaaf 100644 --- a/frontend/src/views/Settings/ProjectSettingsPage/components/NotificationSection/NotificationTab.tsx +++ b/frontend/src/views/Settings/ProjectSettingsPage/components/NotificationSection/NotificationTab.tsx @@ -5,11 +5,19 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; import { createNotification } from "@app/components/notifications"; -import { Button, ContentLoader, FormControl, Input, Switch } from "@app/components/v2"; +import { + Button, + ContentLoader, + DeleteActionModal, + FormControl, + Input, + Switch +} from "@app/components/v2"; import { useWorkspace } from "@app/context"; -import { useToggle } from "@app/hooks"; +import { usePopUp, useToggle } from "@app/hooks"; import { fetchSlackInstallUrl, + useDeleteSlackIntegration, useGetSlackIntegrationByProject, useUpdateSlackIntegration } from "@app/hooks/api"; @@ -28,6 +36,11 @@ export const NotificationTab = () => { const { data: slackIntegration, isLoading: isSlackIntegrationLoading } = useGetSlackIntegrationByProject(currentWorkspace?.id); const { mutateAsync: updateSlackIntegration } = useUpdateSlackIntegration(); + const { mutateAsync: deleteSlackIntegration } = useDeleteSlackIntegration(); + const { popUp, handlePopUpToggle, handlePopUpOpen } = usePopUp([ + "deleteSlackIntegration" + ] as const); + const { control, watch, @@ -65,6 +78,23 @@ export const NotificationTab = () => { }); }; + const handleIntegrationDelete = async () => { + if (!currentWorkspace || !slackIntegration) { + return; + } + await deleteSlackIntegration({ + workspaceId: currentWorkspace.id, + id: slackIntegration.id + }); + + handlePopUpToggle("deleteSlackIntegration", false); + + createNotification({ + type: "success", + text: "Successfully deleted slack integration" + }); + }; + useEffect(() => { if (slackIntegration) { setValue( @@ -85,131 +115,147 @@ export const NotificationTab = () => { } return ( -
-
-

Slack Integration

-
-

- This integration allows you to send notifications to your Slack workspace in response to - events in your project. -

- {!slackIntegration && ( - - )} - {slackIntegration && ( -
-
Connected Slack workspace: {slackIntegration.teamName}
-
- - -
- { - return ( - - field.onChange(value)} - isChecked={field.value} - > -

Secret Approval Requests

-
-
- ); - }} - /> - {secretRequestNotifState && ( - ( - - - - )} - /> - )} - { - return ( - - field.onChange(value)} - isChecked={field.value} - > -

Access Requests

-
-
- ); - }} - /> - {accessRequestNotifState && ( - ( - - - - )} - /> - )} + <> +
+
+

+ Slack Integration +

+
+

+ This integration allows you to send notifications to your Slack workspace in response to + events in your project. +

+ {!slackIntegration && ( - - )} -
+ )} + {slackIntegration && ( +
+
Connected Slack workspace: {slackIntegration.teamName}
+
+ + +
+ { + return ( + + field.onChange(value)} + isChecked={field.value} + > +

Secret Approval Requests

+
+
+ ); + }} + /> + {secretRequestNotifState && ( + ( + + + + )} + /> + )} + { + return ( + + field.onChange(value)} + isChecked={field.value} + > +

Access Requests

+
+
+ ); + }} + /> + {accessRequestNotifState && ( + ( + + + + )} + /> + )} + + + )} +
+ handlePopUpToggle("deleteSlackIntegration", isOpen)} + deleteKey="confirm" + onDeleteApproved={handleIntegrationDelete} + /> + ); };