feat: slack integration deletion

This commit is contained in:
Sheen Capadngan
2024-09-02 20:34:13 +08:00
parent f6d3831d6d
commit 07fd489982
7 changed files with 276 additions and 135 deletions

View File

@@ -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",

View File

@@ -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<TSlackIntegrationDALFactory, "create" | "findOne" | "findById" | "updateById">;
slackIntegrationDAL: Pick<
TSlackIntegrationDALFactory,
"create" | "findOne" | "findById" | "updateById" | "deleteById"
>;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
projectDAL: Pick<TProjectDALFactory, "findById">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
@@ -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
};
};

View File

@@ -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<TProjectPermission, "projectId">;
export type TDeleteSlackIntegrationDTO = {
id: string;
} & Omit<TProjectPermission, "projectId">;
export type TCompleteSlackIntegrationDTO = {
projectId: string;
teamId: string;

View File

@@ -1,2 +1,2 @@
export { useUpdateSlackIntegration } from "./mutation";
export { useDeleteSlackIntegration, useUpdateSlackIntegration } from "./mutation";
export { fetchSlackInstallUrl, useGetSlackIntegrationByProject } from "./queries";

View File

@@ -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));
}
});
};

View File

@@ -15,3 +15,8 @@ export type TUpdateSlackIntegrationDTO = {
isSecretRequestNotificationEnabled?: boolean;
secretRequestChannels?: string;
};
export type TDeleteSlackIntegrationDTO = {
id: string;
workspaceId: string;
};

View File

@@ -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 (
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="flex justify-between">
<h2 className="mb-2 flex-1 text-xl font-semibold text-mineshaft-100">Slack Integration</h2>
</div>
<p className="mb-4 text-gray-400">
This integration allows you to send notifications to your Slack workspace in response to
events in your project.
</p>
{!slackIntegration && (
<Button
isLoading={isConnectToSlackLoading}
onClick={async () => {
setIsConnectToSlackLoading.on();
const slackInstallUrl = await fetchSlackInstallUrl(currentWorkspace?.id);
if (slackInstallUrl) {
router.push(slackInstallUrl);
}
}}
>
Connect to Slack
</Button>
)}
{slackIntegration && (
<form onSubmit={handleSubmit(handleIntegrationSave)}>
<div>Connected Slack workspace: {slackIntegration.teamName}</div>
<div className="mt-2 mb-6">
<Button colorSchema="secondary" size="xs">
Reinstall integration
</Button>
<Button colorSchema="secondary" size="xs" className="ml-2">
Delete
</Button>
</div>
<Controller
control={control}
name="isSecretRequestNotificationEnabled"
render={({ field, fieldState: { error } }) => {
return (
<FormControl
isError={Boolean(error)}
errorText={error?.message}
className="mt-3 mb-2"
>
<Switch
id="secret-approval-notification"
onCheckedChange={(value) => field.onChange(value)}
isChecked={field.value}
>
<p className="w-full">Secret Approval Requests</p>
</Switch>
</FormControl>
);
}}
/>
{secretRequestNotifState && (
<Controller
control={control}
name="secretRequestChannels"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Slack channels"
errorText={error?.message}
isError={Boolean(error)}
isRequired={false}
>
<Input
autoCorrect="off"
spellCheck={false}
placeholder="general, bot"
{...field}
/>
</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, fieldState: { error } }) => (
<FormControl
label="Slack channels"
className="mt-0"
errorText={error?.message}
isError={Boolean(error)}
isRequired={false}
>
<Input
autoCorrect="off"
spellCheck={false}
placeholder="general, bot"
{...field}
/>
</FormControl>
)}
/>
)}
<>
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="flex justify-between">
<h2 className="mb-2 flex-1 text-xl font-semibold text-mineshaft-100">
Slack Integration
</h2>
</div>
<p className="mb-4 text-gray-400">
This integration allows you to send notifications to your Slack workspace in response to
events in your project.
</p>
{!slackIntegration && (
<Button
colorSchema="secondary"
className="mt-4"
type="submit"
isDisabled={!isDirty}
isLoading={isSubmitting}
isLoading={isConnectToSlackLoading}
onClick={async () => {
setIsConnectToSlackLoading.on();
const slackInstallUrl = await fetchSlackInstallUrl(currentWorkspace?.id);
if (slackInstallUrl) {
router.push(slackInstallUrl);
}
}}
>
Save
Connect to Slack
</Button>
</form>
)}
</div>
)}
{slackIntegration && (
<form onSubmit={handleSubmit(handleIntegrationSave)}>
<div>Connected Slack workspace: {slackIntegration.teamName}</div>
<div className="mt-2 mb-6">
<Button colorSchema="secondary" size="xs">
Reinstall integration
</Button>
<Button
colorSchema="secondary"
size="xs"
className="ml-2"
onClick={() => handlePopUpOpen("deleteSlackIntegration")}
>
Delete
</Button>
</div>
<Controller
control={control}
name="isSecretRequestNotificationEnabled"
render={({ field, fieldState: { error } }) => {
return (
<FormControl
isError={Boolean(error)}
errorText={error?.message}
className="mt-3 mb-2"
>
<Switch
id="secret-approval-notification"
onCheckedChange={(value) => field.onChange(value)}
isChecked={field.value}
>
<p className="w-full">Secret Approval Requests</p>
</Switch>
</FormControl>
);
}}
/>
{secretRequestNotifState && (
<Controller
control={control}
name="secretRequestChannels"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Slack channels"
errorText={error?.message}
isError={Boolean(error)}
isRequired={false}
>
<Input
autoCorrect="off"
spellCheck={false}
placeholder="general, bot"
{...field}
/>
</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, fieldState: { error } }) => (
<FormControl
label="Slack channels"
className="mt-0"
errorText={error?.message}
isError={Boolean(error)}
isRequired={false}
>
<Input
autoCorrect="off"
spellCheck={false}
placeholder="general, bot"
{...field}
/>
</FormControl>
)}
/>
)}
<Button
colorSchema="secondary"
className="mt-4"
type="submit"
isDisabled={!isDirty}
isLoading={isSubmitting}
>
Save
</Button>
</form>
)}
</div>
<DeleteActionModal
isOpen={popUp.deleteSlackIntegration.isOpen}
title="Are you sure want to delete your Slack integration?"
onChange={(isOpen) => handlePopUpToggle("deleteSlackIntegration", isOpen)}
deleteKey="confirm"
onDeleteApproved={handleIntegrationDelete}
/>
</>
);
};