misc: slack integration reinstall

This commit is contained in:
Sheen Capadngan
2024-09-04 15:44:58 +08:00
parent 5343c7af00
commit 15c5fe4095
7 changed files with 207 additions and 1 deletions
@@ -171,6 +171,7 @@ export enum EventType {
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",
ATTEMPT_CREATE_SLACK_INTEGRATION = "attempt-create-slack-integration", ATTEMPT_CREATE_SLACK_INTEGRATION = "attempt-create-slack-integration",
ATTEMPT_REINSTALL_SLACK_INTEGRATION = "attempt-reinstall-slack-integration",
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",
@@ -1460,6 +1461,13 @@ interface AttemptCreateSlackIntegration {
}; };
} }
interface AttemptReinstallSlackIntegration {
type: EventType.ATTEMPT_REINSTALL_SLACK_INTEGRATION;
metadata: {
id: string;
};
}
interface UpdateSlackIntegration { interface UpdateSlackIntegration {
type: EventType.UPDATE_SLACK_INTEGRATION; type: EventType.UPDATE_SLACK_INTEGRATION;
metadata: { metadata: {
@@ -1634,6 +1642,7 @@ export type Event =
| UpdateCertificateTemplateEstConfig | UpdateCertificateTemplateEstConfig
| GetCertificateTemplateEstConfig | GetCertificateTemplateEstConfig
| AttemptCreateSlackIntegration | AttemptCreateSlackIntegration
| AttemptReinstallSlackIntegration
| UpdateSlackIntegration | UpdateSlackIntegration
| DeleteSlackIntegration | DeleteSlackIntegration
| GetSlackIntegration | GetSlackIntegration
@@ -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({ server.route({
method: "GET", method: "GET",
url: "/", url: "/",
@@ -12,9 +12,11 @@ import { TSlackIntegrationDALFactory } from "./slack-integration-dal";
import { import {
TCompleteSlackIntegrationDTO, TCompleteSlackIntegrationDTO,
TDeleteSlackIntegrationDTO, TDeleteSlackIntegrationDTO,
TGetReinstallUrlDTO,
TGetSlackInstallUrlDTO, TGetSlackInstallUrlDTO,
TGetSlackIntegrationByIdDTO, TGetSlackIntegrationByIdDTO,
TGetSlackIntegrationByOrgDTO, TGetSlackIntegrationByOrgDTO,
TReinstallSlackIntegrationDTO,
TUpdateSlackIntegrationDTO TUpdateSlackIntegrationDTO
} from "./slack-types"; } 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 getSlackInstaller = async () => {
const appCfg = getConfig(); const appCfg = getConfig();
@@ -89,11 +123,25 @@ export const slackServiceFactory = ({
} }
const metadata = JSON.parse(installation.metadata || "") as { const metadata = JSON.parse(installation.metadata || "") as {
id?: string;
orgId: string; orgId: string;
slug: string; slug: string;
description?: 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({ return completeSlackIntegration({
orgId: metadata.orgId, orgId: metadata.orgId,
slug: metadata.slug, slug: metadata.slug,
@@ -154,6 +202,39 @@ export const slackServiceFactory = ({
return url; 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 ({ const getSlackIntegrationsByOrg = async ({
actorId, actorId,
actor, actor,
@@ -265,6 +346,7 @@ export const slackServiceFactory = ({
return { return {
getInstallUrl, getInstallUrl,
getReinstallUrl,
getSlackIntegrationsByOrg, getSlackIntegrationsByOrg,
getSlackIntegrationById, getSlackIntegrationById,
completeSlackIntegration, completeSlackIntegration,
+15
View File
@@ -5,6 +5,10 @@ export type TGetSlackInstallUrlDTO = {
description?: string; description?: string;
} & Omit<TOrgPermission, "orgId">; } & Omit<TOrgPermission, "orgId">;
export type TGetReinstallUrlDTO = {
id: string;
} & Omit<TOrgPermission, "orgId">;
export type TGetSlackIntegrationByOrgDTO = Omit<TOrgPermission, "orgId">; export type TGetSlackIntegrationByOrgDTO = Omit<TOrgPermission, "orgId">;
export type TGetSlackIntegrationByIdDTO = { id: string } & Omit<TOrgPermission, "orgId">; export type TGetSlackIntegrationByIdDTO = { id: string } & Omit<TOrgPermission, "orgId">;
@@ -31,6 +35,17 @@ export type TCompleteSlackIntegrationDTO = {
slackBotUserId: string; slackBotUserId: string;
}; };
export type TReinstallSlackIntegrationDTO = {
id: string;
teamId: string;
teamName: string;
slackUserId: string;
slackAppId: string;
botAccessToken: string;
slackBotId: string;
slackBotUserId: string;
};
export enum SlackTriggerFeature { export enum SlackTriggerFeature {
SECRET_APPROVAL = "secret-approval", SECRET_APPROVAL = "secret-approval",
ACCESS_REQUEST = "access-request" ACCESS_REQUEST = "access-request"
@@ -5,6 +5,7 @@ export {
} from "./mutation"; } from "./mutation";
export { export {
fetchSlackInstallUrl, fetchSlackInstallUrl,
fetchSlackReinstallUrl,
useGetSlackIntegrationById, useGetSlackIntegrationById,
useGetSlackIntegrations useGetSlackIntegrations
} from "./queries"; } from "./queries";
@@ -26,6 +26,20 @@ export const fetchSlackInstallUrl = async ({
return data; return data;
}; };
export const fetchSlackReinstallUrl = async ({
slackIntegrationId
}: {
slackIntegrationId: string;
}) => {
const { data } = await apiRequest.get<string>("/api/v1/workflow-integrations/slack/reinstall", {
params: {
slackIntegrationId
}
});
return data;
};
export const fetchSlackIntegrations = async () => { export const fetchSlackIntegrations = async () => {
const { data } = await apiRequest.get<SlackIntegration[]>("/api/v1/workflow-integrations/slack"); const { data } = await apiRequest.get<SlackIntegration[]>("/api/v1/workflow-integrations/slack");
@@ -1,3 +1,4 @@
import { useRouter } from "next/router";
import { faSlack } from "@fortawesome/free-brands-svg-icons"; import { faSlack } from "@fortawesome/free-brands-svg-icons";
import { faEllipsis, faGear, faPlus } from "@fortawesome/free-solid-svg-icons"; import { faEllipsis, faGear, faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
@@ -24,7 +25,11 @@ import {
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context"; import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
import { withPermission } from "@app/hoc"; import { withPermission } from "@app/hoc";
import { usePopUp } from "@app/hooks"; 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 { WorkflowIntegrationPlatform } from "@app/hooks/api/workflowIntegrations/types";
import { AddWorkflowIntegrationForm } from "./AddWorkflowIntegrationForm"; import { AddWorkflowIntegrationForm } from "./AddWorkflowIntegrationForm";
@@ -39,6 +44,7 @@ export const OrgWorkflowIntegrationTab = withPermission(
] as const); ] as const);
const { currentOrg } = useOrganization(); const { currentOrg } = useOrganization();
const router = useRouter();
const { data: slackIntegrations, isLoading: isSlackIntegrationsLoading } = const { data: slackIntegrations, isLoading: isSlackIntegrationsLoading } =
useGetSlackIntegrations(currentOrg?.id); useGetSlackIntegrations(currentOrg?.id);
const { mutateAsync: deleteSlackIntegration } = useDeleteSlackIntegration(); 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; const isIntegrationsLoading = isSlackIntegrationsLoading;
return ( return (
@@ -132,6 +150,29 @@ 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(
WorkflowIntegrationPlatform.SLACK,
slackIntegration.id
);
}}
>
Reinstall
</DropdownMenuItem>
)}
</OrgPermissionCan>
<OrgPermissionCan <OrgPermissionCan
I={OrgPermissionActions.Delete} I={OrgPermissionActions.Delete}
an={OrgPermissionSubjects.Settings} an={OrgPermissionSubjects.Settings}