feat: finished slack integration update

This commit is contained in:
Sheen Capadngan
2024-09-02 20:13:01 +08:00
parent d604ef2480
commit f6d3831d6d
12 changed files with 448 additions and 28 deletions
@@ -17,9 +17,9 @@ export async function up(knex: Knex): Promise<void> {
tb.string("slackBotId").notNullable();
tb.string("slackBotUserId").notNullable();
tb.boolean("isAccessRequestNotificationEnabled").defaultTo(false);
tb.string("accessRequestChannels");
tb.string("accessRequestChannels").defaultTo("");
tb.boolean("isSecretRequestNotificationEnabled").defaultTo(false);
tb.string("secretRequestChannels");
tb.string("secretRequestChannels").defaultTo("");
tb.timestamps(true, true, true);
});
+89 -2
View File
@@ -1,9 +1,11 @@
import { z } from "zod";
import { SlackIntegrationsSchema } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env";
import { readLimit } from "@app/server/config/rateLimiter";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import { validateSlackChannelsField } from "@app/services/slack/slack-auth-validators";
export const registerSlackRouter = async (server: FastifyZodProvider) => {
const appCfg = getConfig();
@@ -27,8 +29,9 @@ export const registerSlackRouter = async (server: FastifyZodProvider) => {
200: z.string()
}
},
onRequest: verifyAuth([AuthMode.JWT]),
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
// TODO: add audit logs
return server.services.slack.getInstallUrl({
actor: req.permission.type,
actorId: req.permission.id,
@@ -39,6 +42,90 @@ export const registerSlackRouter = async (server: FastifyZodProvider) => {
}
});
server.route({
method: "GET",
url: "/",
config: {
rateLimit: readLimit
},
schema: {
security: [
{
bearerAuth: []
}
],
querystring: z.object({
projectId: 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) => {
return server.services.slack.getSlackIntegrationByProjectId({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
projectId: req.query.projectId
});
}
});
server.route({
method: "PATCH",
url: "/:slackIntegrationId",
config: {
rateLimit: writeLimit
},
schema: {
security: [
{
bearerAuth: []
}
],
params: z.object({
slackIntegrationId: z.string()
}),
body: z.object({
isAccessRequestNotificationEnabled: z.boolean().optional(),
accessRequestChannels: validateSlackChannelsField.optional(),
isSecretRequestNotificationEnabled: z.boolean().optional(),
secretRequestChannels: validateSlackChannelsField.optional()
}),
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.updateSlackIntegration({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
id: req.params.slackIntegrationId,
...req.body
});
}
});
server.route({
method: "GET",
url: "/oauth_redirect",
@@ -0,0 +1,13 @@
import z from "zod";
export const validateSlackChannelsField = z
.string()
.trim()
.default("")
.transform((data) => {
if (data === "") return "";
return data
.split(",")
.map((id) => id.trim())
.join(", ");
});
+2 -2
View File
@@ -34,12 +34,12 @@ export const triggerSlackNotification = async ({
let targetChannels: string[] = [];
if (feature === SlackTriggerFeature.ACCESS_REQUEST) {
targetChannels = slackIntegration.accessRequestChannels?.split(",") || [];
targetChannels = slackIntegration.accessRequestChannels?.split(", ") || [];
if (!targetChannels.length || !slackIntegration.isAccessRequestNotificationEnabled) {
return;
}
} else if (feature === SlackTriggerFeature.SECRET_APPROVAL) {
targetChannels = slackIntegration.secretRequestChannels?.split(",") || [];
targetChannels = slackIntegration.secretRequestChannels?.split(", ") || [];
if (!targetChannels.length || !slackIntegration.isSecretRequestNotificationEnabled) {
return;
}
+68 -5
View File
@@ -10,10 +10,15 @@ import { TKmsServiceFactory } from "../kms/kms-service";
import { KmsDataKey } from "../kms/kms-types";
import { TProjectDALFactory } from "../project/project-dal";
import { TSlackIntegrationDALFactory } from "./slack-integration-dal";
import { TCompleteSlackIntegrationDTO, TGetSlackInstallUrlDTO } from "./slack-types";
import {
TCompleteSlackIntegrationDTO,
TGetSlackInstallUrlDTO,
TGetSlackIntegrationByProjectId,
TUpdateSlackIntegration
} from "./slack-types";
type TSlackServiceFactoryDep = {
slackIntegrationDAL: Pick<TSlackIntegrationDALFactory, "create">;
slackIntegrationDAL: Pick<TSlackIntegrationDALFactory, "create" | "findOne" | "findById" | "updateById">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
projectDAL: Pick<TProjectDALFactory, "findById">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
@@ -142,14 +147,72 @@ export const slackServiceFactory = ({
redirectUri: `${appCfg.SITE_URL}/api/v1/slack/oauth_redirect`
});
// TODO: add audit log here
return url;
};
const getSlackIntegrationByProjectId = async ({
actorId,
actor,
actorOrgId,
actorAuthMethod,
projectId
}: TGetSlackIntegrationByProjectId) => {
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Settings);
const slackIntegration = await slackIntegrationDAL.findOne({
projectId
});
return slackIntegration;
};
const updateSlackIntegration = async ({
actorId,
actor,
actorOrgId,
actorAuthMethod,
id,
isAccessRequestNotificationEnabled,
accessRequestChannels,
isSecretRequestNotificationEnabled,
secretRequestChannels
}: TUpdateSlackIntegration) => {
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.Edit, ProjectPermissionSub.Settings);
return slackIntegrationDAL.updateById(slackIntegration.id, {
isAccessRequestNotificationEnabled,
accessRequestChannels,
isSecretRequestNotificationEnabled,
secretRequestChannels
});
};
return {
getInstallUrl,
getSlackIntegrationByProjectId,
completeSlackIntegration,
getSlackInstaller
getSlackInstaller,
updateSlackIntegration
};
};
+10
View File
@@ -2,6 +2,16 @@ import { TProjectPermission } from "@app/lib/types";
export type TGetSlackInstallUrlDTO = TProjectPermission;
export type TGetSlackIntegrationByProjectId = TProjectPermission;
export type TUpdateSlackIntegration = {
id: string;
isAccessRequestNotificationEnabled?: boolean;
accessRequestChannels?: string;
isSecretRequestNotificationEnabled?: boolean;
secretRequestChannels?: string;
} & Omit<TProjectPermission, "projectId">;
export type TCompleteSlackIntegrationDTO = {
projectId: string;
teamId: string;