diff --git a/backend/src/@types/fastify.d.ts b/backend/src/@types/fastify.d.ts index de64e58ea..1b487ac1f 100644 --- a/backend/src/@types/fastify.d.ts +++ b/backend/src/@types/fastify.d.ts @@ -77,6 +77,7 @@ import { TUserDALFactory } from "@app/services/user/user-dal"; import { TUserServiceFactory } from "@app/services/user/user-service"; import { TUserEngagementServiceFactory } from "@app/services/user-engagement/user-engagement-service"; import { TWebhookServiceFactory } from "@app/services/webhook/webhook-service"; +import { TWorkflowIntegrationServiceFactory } from "@app/services/workflow-integration/workflow-integration-service"; declare module "fastify" { interface FastifyRequest { @@ -179,6 +180,7 @@ declare module "fastify" { externalKms: TExternalKmsServiceFactory; orgAdmin: TOrgAdminServiceFactory; slack: TSlackServiceFactory; + workflowIntegration: TWorkflowIntegrationServiceFactory; }; // this is exclusive use for middlewares in which we need to inject data // everywhere else access using service layer diff --git a/backend/src/@types/knex.d.ts b/backend/src/@types/knex.d.ts index 7c9d88a46..90d5c86f0 100644 --- a/backend/src/@types/knex.d.ts +++ b/backend/src/@types/knex.d.ts @@ -331,7 +331,10 @@ import { TUsersUpdate, TWebhooks, TWebhooksInsert, - TWebhooksUpdate + TWebhooksUpdate, + TWorkflowIntegrations, + TWorkflowIntegrationsInsert, + TWorkflowIntegrationsUpdate } from "@app/db/schemas"; import { TSecretV2TagJunction, @@ -800,5 +803,10 @@ declare module "knex/types/tables" { TAdminSlackConfigsInsert, TAdminSlackConfigsUpdate >; + [TableName.WorkflowIntegrations]: KnexOriginal.CompositeTableType< + TWorkflowIntegrations, + TWorkflowIntegrationsInsert, + TWorkflowIntegrationsUpdate + >; } } diff --git a/backend/src/db/migrations/20240830142938_native-slack-integration.ts b/backend/src/db/migrations/20240830142938_native-slack-integration.ts index 15842af05..e7e8f8458 100644 --- a/backend/src/db/migrations/20240830142938_native-slack-integration.ts +++ b/backend/src/db/migrations/20240830142938_native-slack-integration.ts @@ -4,13 +4,25 @@ import { TableName } from "../schemas"; import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils"; export async function up(knex: Knex): Promise { - if (!(await knex.schema.hasTable(TableName.SlackIntegrations))) { - await knex.schema.createTable(TableName.SlackIntegrations, (tb) => { + if (!(await knex.schema.hasTable(TableName.WorkflowIntegrations))) { + await knex.schema.createTable(TableName.WorkflowIntegrations, (tb) => { tb.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + tb.string("integration").notNullable(); tb.string("slug").notNullable(); tb.uuid("orgId").notNullable(); tb.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE"); tb.string("description"); + tb.unique(["orgId", "slug"]); + tb.timestamps(true, true, true); + }); + + await createOnUpdateTrigger(knex, TableName.WorkflowIntegrations); + } + + if (!(await knex.schema.hasTable(TableName.SlackIntegrations))) { + await knex.schema.createTable(TableName.SlackIntegrations, (tb) => { + tb.uuid("id", { primaryKey: true }).notNullable(); + tb.foreign("id").references("id").inTable(TableName.WorkflowIntegrations).onDelete("CASCADE"); tb.string("teamId").notNullable(); tb.string("teamName").notNullable(); tb.string("slackUserId").notNullable(); @@ -18,7 +30,6 @@ export async function up(knex: Knex): Promise { tb.binary("encryptedBotAccessToken").notNullable(); tb.string("slackBotId").notNullable(); tb.string("slackBotUserId").notNullable(); - tb.unique(["orgId", "slug"]); tb.timestamps(true, true, true); }); @@ -58,9 +69,12 @@ export async function down(knex: Knex): Promise { await knex.schema.dropTableIfExists(TableName.ProjectSlackConfigs); await dropOnUpdateTrigger(knex, TableName.ProjectSlackConfigs); + await knex.schema.dropTableIfExists(TableName.SlackIntegrations); + await dropOnUpdateTrigger(knex, TableName.SlackIntegrations); + await knex.schema.dropTableIfExists(TableName.AdminSlackConfig); await dropOnUpdateTrigger(knex, TableName.AdminSlackConfig); - await knex.schema.dropTableIfExists(TableName.SlackIntegrations); - await dropOnUpdateTrigger(knex, TableName.SlackIntegrations); + await knex.schema.dropTableIfExists(TableName.WorkflowIntegrations); + await dropOnUpdateTrigger(knex, TableName.WorkflowIntegrations); } diff --git a/backend/src/db/schemas/index.ts b/backend/src/db/schemas/index.ts index 6fb79982a..5c9a3a7c0 100644 --- a/backend/src/db/schemas/index.ts +++ b/backend/src/db/schemas/index.ts @@ -112,3 +112,4 @@ export * from "./user-encryption-keys"; export * from "./user-group-membership"; export * from "./users"; export * from "./webhooks"; +export * from "./workflow-integrations"; diff --git a/backend/src/db/schemas/models.ts b/backend/src/db/schemas/models.ts index 3e4f259ff..74474147a 100644 --- a/backend/src/db/schemas/models.ts +++ b/backend/src/db/schemas/models.ts @@ -115,6 +115,7 @@ export enum TableName { InternalKmsKeyVersion = "internal_kms_key_version", // @depreciated KmsKeyVersion = "kms_key_versions", + WorkflowIntegrations = "workflow_integrations", SlackIntegrations = "slack_integrations", ProjectSlackConfigs = "project_slack_configs", AdminSlackConfig = "admin_slack_configs" diff --git a/backend/src/db/schemas/slack-integrations.ts b/backend/src/db/schemas/slack-integrations.ts index 3cd33bed2..c989d4224 100644 --- a/backend/src/db/schemas/slack-integrations.ts +++ b/backend/src/db/schemas/slack-integrations.ts @@ -11,9 +11,6 @@ import { TImmutableDBKeys } from "./models"; export const SlackIntegrationsSchema = z.object({ id: z.string().uuid(), - slug: z.string(), - orgId: z.string().uuid(), - description: z.string().nullable().optional(), teamId: z.string(), teamName: z.string(), slackUserId: z.string(), diff --git a/backend/src/db/schemas/workflow-integrations.ts b/backend/src/db/schemas/workflow-integrations.ts new file mode 100644 index 000000000..ae1ae9a25 --- /dev/null +++ b/backend/src/db/schemas/workflow-integrations.ts @@ -0,0 +1,22 @@ +// Code generated by automation script, DO NOT EDIT. +// Automated by pulling database and generating zod schema +// To update. Just run npm run generate:schema +// Written by akhilmhdh. + +import { z } from "zod"; + +import { TImmutableDBKeys } from "./models"; + +export const WorkflowIntegrationsSchema = z.object({ + id: z.string().uuid(), + integration: z.string(), + slug: z.string(), + orgId: z.string().uuid(), + description: z.string().nullable().optional(), + createdAt: z.date(), + updatedAt: z.date() +}); + +export type TWorkflowIntegrations = z.infer; +export type TWorkflowIntegrationsInsert = Omit, TImmutableDBKeys>; +export type TWorkflowIntegrationsUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 18aaa302f..00ab3a7db 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -198,6 +198,8 @@ import { userAliasDALFactory } from "@app/services/user-alias/user-alias-dal"; import { userEngagementServiceFactory } from "@app/services/user-engagement/user-engagement-service"; import { webhookDALFactory } from "@app/services/webhook/webhook-dal"; import { webhookServiceFactory } from "@app/services/webhook/webhook-service"; +import { workflowIntegrationDALFactory } from "@app/services/workflow-integration/workflow-integration-dal"; +import { workflowIntegrationServiceFactory } from "@app/services/workflow-integration/workflow-integration-service"; import { injectAuditLogInfo } from "../plugins/audit-log"; import { injectIdentity } from "../plugins/auth/inject-identity"; @@ -329,6 +331,7 @@ export const registerRoutes = async ( const slackIntegrationDAL = slackIntegrationDALFactory(db); const projectSlackConfigDAL = projectSlackConfigDALFactory(db); const adminSlackConfigDAL = adminSlackConfigDALFactory(db); + const workflowIntegrationDAL = workflowIntegrationDALFactory(db); const permissionService = permissionServiceFactory({ permissionDAL, @@ -1170,7 +1173,13 @@ export const registerRoutes = async ( permissionService, kmsService, slackIntegrationDAL, - adminSlackConfigDAL + adminSlackConfigDAL, + workflowIntegrationDAL + }); + + const workflowIntegrationService = workflowIntegrationServiceFactory({ + permissionService, + workflowIntegrationDAL }); await superAdminService.initServerCfg(); @@ -1255,7 +1264,8 @@ export const registerRoutes = async ( userEngagement: userEngagementService, externalKms: externalKmsService, orgAdmin: orgAdminService, - slack: slackService + slack: slackService, + workflowIntegration: workflowIntegrationService }); const cronJobs: CronJob[] = []; diff --git a/backend/src/server/routes/v1/index.ts b/backend/src/server/routes/v1/index.ts index 734352b58..d13d0db80 100644 --- a/backend/src/server/routes/v1/index.ts +++ b/backend/src/server/routes/v1/index.ts @@ -35,6 +35,7 @@ import { registerUserActionRouter } from "./user-action-router"; import { registerUserEngagementRouter } from "./user-engagement-router"; import { registerUserRouter } from "./user-router"; import { registerWebhookRouter } from "./webhook-router"; +import { registerWorkflowIntegrationRouter } from "./workflow-integration-router"; export const registerV1Routes = async (server: FastifyZodProvider) => { await server.register(registerSsoRouter, { prefix: "/sso" }); @@ -64,6 +65,7 @@ export const registerV1Routes = async (server: FastifyZodProvider) => { await server.register( async (workflowIntegrationRouter) => { + await workflowIntegrationRouter.register(registerWorkflowIntegrationRouter); await workflowIntegrationRouter.register(registerSlackRouter, { prefix: "/slack" }); }, { prefix: "/workflow-integrations" } diff --git a/backend/src/server/routes/v1/slack-router.ts b/backend/src/server/routes/v1/slack-router.ts index f5b6c8f06..99e878bf8 100644 --- a/backend/src/server/routes/v1/slack-router.ts +++ b/backend/src/server/routes/v1/slack-router.ts @@ -1,12 +1,23 @@ import { z } from "zod"; -import { SlackIntegrationsSchema } from "@app/db/schemas"; +import { SlackIntegrationsSchema, WorkflowIntegrationsSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { getConfig } from "@app/lib/config/env"; 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"; +const sanitizedSlackIntegrationSchema = WorkflowIntegrationsSchema.pick({ + id: true, + description: true, + slug: true, + integration: true +}).merge( + SlackIntegrationsSchema.pick({ + teamName: true + }) +); + export const registerSlackRouter = async (server: FastifyZodProvider) => { const appCfg = getConfig(); @@ -69,7 +80,7 @@ export const registerSlackRouter = async (server: FastifyZodProvider) => { } ], querystring: z.object({ - slackIntegrationId: z.string() + id: z.string() }), response: { 200: z.string() @@ -82,7 +93,7 @@ export const registerSlackRouter = async (server: FastifyZodProvider) => { actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, - id: req.query.slackIntegrationId + id: req.query.id }); await server.services.auditLog.createAuditLog({ @@ -91,7 +102,7 @@ export const registerSlackRouter = async (server: FastifyZodProvider) => { event: { type: EventType.ATTEMPT_REINSTALL_SLACK_INTEGRATION, metadata: { - id: req.query.slackIntegrationId + id: req.query.id } } }); @@ -113,12 +124,7 @@ export const registerSlackRouter = async (server: FastifyZodProvider) => { } ], response: { - 200: SlackIntegrationsSchema.pick({ - id: true, - slug: true, - description: true, - teamName: true - }).array() + 200: sanitizedSlackIntegrationSchema.array() } }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), @@ -136,7 +142,7 @@ export const registerSlackRouter = async (server: FastifyZodProvider) => { server.route({ method: "DELETE", - url: "/:slackIntegrationId", + url: "/:id", config: { rateLimit: writeLimit }, @@ -147,15 +153,10 @@ export const registerSlackRouter = async (server: FastifyZodProvider) => { } ], params: z.object({ - slackIntegrationId: z.string() + id: z.string() }), response: { - 200: SlackIntegrationsSchema.pick({ - id: true, - slug: true, - description: true, - teamName: true - }) + 200: sanitizedSlackIntegrationSchema } }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), @@ -165,7 +166,7 @@ export const registerSlackRouter = async (server: FastifyZodProvider) => { actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, - id: req.params.slackIntegrationId + id: req.params.id }); await server.services.auditLog.createAuditLog({ @@ -185,7 +186,7 @@ export const registerSlackRouter = async (server: FastifyZodProvider) => { server.route({ method: "GET", - url: "/:slackIntegrationId", + url: "/:id", config: { rateLimit: readLimit }, @@ -196,15 +197,10 @@ export const registerSlackRouter = async (server: FastifyZodProvider) => { } ], params: z.object({ - slackIntegrationId: z.string() + id: z.string() }), response: { - 200: SlackIntegrationsSchema.pick({ - id: true, - slug: true, - description: true, - teamName: true - }) + 200: sanitizedSlackIntegrationSchema } }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), @@ -214,7 +210,7 @@ export const registerSlackRouter = async (server: FastifyZodProvider) => { actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, - id: req.params.slackIntegrationId + id: req.params.id }); await server.services.auditLog.createAuditLog({ @@ -234,7 +230,7 @@ export const registerSlackRouter = async (server: FastifyZodProvider) => { server.route({ method: "GET", - url: "/:slackIntegrationId/channels", + url: "/:id/channels", config: { rateLimit: readLimit }, @@ -245,7 +241,7 @@ export const registerSlackRouter = async (server: FastifyZodProvider) => { } ], params: z.object({ - slackIntegrationId: z.string() + id: z.string() }), response: { 200: z @@ -263,7 +259,7 @@ export const registerSlackRouter = async (server: FastifyZodProvider) => { actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, - id: req.params.slackIntegrationId + id: req.params.id }); return slackChannels; @@ -272,7 +268,7 @@ export const registerSlackRouter = async (server: FastifyZodProvider) => { server.route({ method: "PATCH", - url: "/:slackIntegrationId", + url: "/:id", config: { rateLimit: readLimit }, @@ -283,19 +279,14 @@ export const registerSlackRouter = async (server: FastifyZodProvider) => { } ], params: z.object({ - slackIntegrationId: z.string() + id: z.string() }), body: z.object({ slug: z.string().optional(), description: z.string().optional() }), response: { - 200: SlackIntegrationsSchema.pick({ - id: true, - slug: true, - description: true, - teamName: true - }) + 200: sanitizedSlackIntegrationSchema } }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), @@ -305,7 +296,7 @@ export const registerSlackRouter = async (server: FastifyZodProvider) => { actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, - id: req.params.slackIntegrationId, + id: req.params.id, ...req.body }); diff --git a/backend/src/server/routes/v1/workflow-integration-router.ts b/backend/src/server/routes/v1/workflow-integration-router.ts new file mode 100644 index 000000000..839d7b056 --- /dev/null +++ b/backend/src/server/routes/v1/workflow-integration-router.ts @@ -0,0 +1,42 @@ +import { WorkflowIntegrationsSchema } from "@app/db/schemas"; +import { readLimit } from "@app/server/config/rateLimiter"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AuthMode } from "@app/services/auth/auth-type"; + +const sanitizedWorkflowIntegrationSchema = WorkflowIntegrationsSchema.pick({ + id: true, + description: true, + slug: true, + integration: true +}); + +export const registerWorkflowIntegrationRouter = async (server: FastifyZodProvider) => { + server.route({ + method: "GET", + url: "/", + config: { + rateLimit: readLimit + }, + schema: { + security: [ + { + bearerAuth: [] + } + ], + response: { + 200: sanitizedWorkflowIntegrationSchema.array() + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const workflowIntegrations = await server.services.workflowIntegration.getIntegrationsByOrg({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId + }); + + return workflowIntegrations; + } + }); +}; diff --git a/backend/src/services/project/project-service.ts b/backend/src/services/project/project-service.ts index 5900ea351..f51c561d7 100644 --- a/backend/src/services/project/project-service.ts +++ b/backend/src/services/project/project-service.ts @@ -81,7 +81,7 @@ type TProjectServiceFactoryDep = { projectKeyDAL: Pick; projectMembershipDAL: Pick; projectSlackConfigDAL: Pick; - slackIntegrationDAL: Pick; + slackIntegrationDAL: Pick; projectUserMembershipRoleDAL: Pick; certificateAuthorityDAL: Pick; certificateDAL: Pick; @@ -965,7 +965,7 @@ export const projectServiceFactory = ({ }); } - const slackIntegration = await slackIntegrationDAL.findById(slackIntegrationId); + const slackIntegration = await slackIntegrationDAL.findByIdWithWorkflowIntegrationDetails(slackIntegrationId); if (!slackIntegration) { throw new NotFoundError({ message: "Slack integration not found" diff --git a/backend/src/services/slack/slack-integration-dal.ts b/backend/src/services/slack/slack-integration-dal.ts index 8c9b781bf..d52c09961 100644 --- a/backend/src/services/slack/slack-integration-dal.ts +++ b/backend/src/services/slack/slack-integration-dal.ts @@ -1,11 +1,56 @@ +import { Knex } from "knex"; + import { TDbClient } from "@app/db"; -import { TableName } from "@app/db/schemas"; -import { ormify } from "@app/lib/knex"; +import { TableName, TSlackIntegrations, TWorkflowIntegrations } from "@app/db/schemas"; +import { DatabaseError } from "@app/lib/errors"; +import { ormify, selectAllTableCols } from "@app/lib/knex"; export type TSlackIntegrationDALFactory = ReturnType; export const slackIntegrationDALFactory = (db: TDbClient) => { const slackIntegrationOrm = ormify(db, TableName.SlackIntegrations); - return slackIntegrationOrm; + const findByIdWithWorkflowIntegrationDetails = async (id: string, tx?: Knex) => { + try { + return await (tx || db.replicaNode())(TableName.SlackIntegrations) + .join( + TableName.WorkflowIntegrations, + `${TableName.SlackIntegrations}.id`, + `${TableName.WorkflowIntegrations}.id` + ) + .select(selectAllTableCols(TableName.SlackIntegrations)) + .select(db.ref("orgId").withSchema(TableName.WorkflowIntegrations)) + .select(db.ref("description").withSchema(TableName.WorkflowIntegrations)) + .select(db.ref("integration").withSchema(TableName.WorkflowIntegrations)) + .select(db.ref("slug").withSchema(TableName.WorkflowIntegrations)) + .where(`${TableName.WorkflowIntegrations}.id`, "=", id) + .first(); + } catch (error) { + throw new DatabaseError({ error, name: "Find by ID with Workflow integration details" }); + } + }; + + const findWithWorkflowIntegrationDetails = async ( + filter: Partial & Partial, + tx?: Knex + ) => { + try { + return await (tx || db.replicaNode())(TableName.SlackIntegrations) + .join( + TableName.WorkflowIntegrations, + `${TableName.SlackIntegrations}.id`, + `${TableName.WorkflowIntegrations}.id` + ) + .select(selectAllTableCols(TableName.SlackIntegrations)) + .select(db.ref("orgId").withSchema(TableName.WorkflowIntegrations)) + .select(db.ref("description").withSchema(TableName.WorkflowIntegrations)) + .select(db.ref("integration").withSchema(TableName.WorkflowIntegrations)) + .select(db.ref("slug").withSchema(TableName.WorkflowIntegrations)) + .where(filter); + } catch (error) { + throw new DatabaseError({ error, name: "Find with Workflow integration details" }); + } + }; + + return { ...slackIntegrationOrm, findByIdWithWorkflowIntegrationDetails, findWithWorkflowIntegrationDetails }; }; diff --git a/backend/src/services/slack/slack-service.ts b/backend/src/services/slack/slack-service.ts index 062b5e313..5e8c6e055 100644 --- a/backend/src/services/slack/slack-service.ts +++ b/backend/src/services/slack/slack-service.ts @@ -8,6 +8,8 @@ import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { TKmsServiceFactory } from "../kms/kms-service"; import { KmsDataKey } from "../kms/kms-types"; +import { TWorkflowIntegrationDALFactory } from "../workflow-integration/workflow-integration-dal"; +import { WorkflowIntegration } from "../workflow-integration/workflow-integration-types"; import { TAdminSlackConfigDALFactory } from "./admin-slack-config-dal"; import { fetchSlackChannels, getAdminSlackCredentials } from "./slack-fns"; import { TSlackIntegrationDALFactory } from "./slack-integration-dal"; @@ -24,10 +26,18 @@ import { } from "./slack-types"; type TSlackServiceFactoryDep = { - slackIntegrationDAL: Pick; + slackIntegrationDAL: Pick< + TSlackIntegrationDALFactory, + | "deleteById" + | "updateById" + | "create" + | "findByIdWithWorkflowIntegrationDetails" + | "findWithWorkflowIntegrationDetails" + >; permissionService: Pick; kmsService: Pick; adminSlackConfigDAL: Pick; + workflowIntegrationDAL: Pick; }; export type TSlackServiceFactory = ReturnType; @@ -36,7 +46,8 @@ export const slackServiceFactory = ({ permissionService, slackIntegrationDAL, kmsService, - adminSlackConfigDAL + adminSlackConfigDAL, + workflowIntegrationDAL }: TSlackServiceFactoryDep) => { const completeSlackIntegration = async ({ orgId, @@ -59,17 +70,31 @@ export const slackServiceFactory = ({ plainText: Buffer.from(botAccessToken, "utf8") }); - await slackIntegrationDAL.create({ - orgId, - slug, - description, - teamId, - teamName, - slackUserId, - slackAppId, - slackBotId, - slackBotUserId, - encryptedBotAccessToken + await workflowIntegrationDAL.transaction(async (tx) => { + const workflowIntegration = await workflowIntegrationDAL.create( + { + description, + orgId, + slug, + integration: WorkflowIntegration.SLACK + }, + tx + ); + + await slackIntegrationDAL.create( + { + // @ts-expect-error id is kept as fixed because it is always equal to the workflow integration ID + id: workflowIntegration.id, + teamId, + teamName, + slackUserId, + slackAppId, + slackBotId, + slackBotUserId, + encryptedBotAccessToken + }, + tx + ); }); }; @@ -83,7 +108,13 @@ export const slackServiceFactory = ({ slackBotId, slackBotUserId }: TReinstallSlackIntegrationDTO) => { - const slackIntegration = await slackIntegrationDAL.findById(id); + const slackIntegration = await slackIntegrationDAL.findByIdWithWorkflowIntegrationDetails(id); + + if (!slackIntegration) { + throw new NotFoundError({ + message: "Slack integration not found" + }); + } const { encryptor: orgDataKeyEncryptor } = await kmsService.createCipherPairWithDataKey({ orgId: slackIntegration.orgId, @@ -228,7 +259,7 @@ export const slackServiceFactory = ({ const getReinstallUrl = async ({ actorId, actor, actorOrgId, actorAuthMethod, id }: TGetReinstallUrlDTO) => { const appCfg = getConfig(); - const slackIntegration = await slackIntegrationDAL.findById(id); + const slackIntegration = await slackIntegrationDAL.findByIdWithWorkflowIntegrationDetails(id); if (!slackIntegration) { throw new NotFoundError({ @@ -275,7 +306,7 @@ export const slackServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Settings); - const slackIntegrations = await slackIntegrationDAL.find({ + const slackIntegrations = await slackIntegrationDAL.findWithWorkflowIntegrationDetails({ orgId: actorOrgId }); @@ -289,7 +320,7 @@ export const slackServiceFactory = ({ actorAuthMethod, id }: TGetSlackIntegrationByIdDTO) => { - const slackIntegration = await slackIntegrationDAL.findById(id); + const slackIntegration = await slackIntegrationDAL.findByIdWithWorkflowIntegrationDetails(id); if (!slackIntegration) { throw new NotFoundError({ message: "Slack integration not found." @@ -316,7 +347,7 @@ export const slackServiceFactory = ({ actorAuthMethod, id }: TGetSlackIntegrationChannelsDTO) => { - const slackIntegration = await slackIntegrationDAL.findById(id); + const slackIntegration = await slackIntegrationDAL.findByIdWithWorkflowIntegrationDetails(id); if (!slackIntegration) { throw new NotFoundError({ message: "Slack integration not found." @@ -354,7 +385,7 @@ export const slackServiceFactory = ({ slug, description }: TUpdateSlackIntegrationDTO) => { - const slackIntegration = await slackIntegrationDAL.findById(id); + const slackIntegration = await slackIntegrationDAL.findByIdWithWorkflowIntegrationDetails(id); if (!slackIntegration) { throw new NotFoundError({ message: "Slack integration not found" @@ -371,9 +402,22 @@ export const slackServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings); - return slackIntegrationDAL.updateById(slackIntegration.id, { - slug, - description + return workflowIntegrationDAL.transaction(async (tx) => { + await workflowIntegrationDAL.updateById( + slackIntegration.id, + { + slug, + description + }, + tx + ); + + const updatedIntegration = await slackIntegrationDAL.findByIdWithWorkflowIntegrationDetails( + slackIntegration.id, + tx + ); + + return updatedIntegration!; }); }; @@ -384,7 +428,7 @@ export const slackServiceFactory = ({ actorAuthMethod, id }: TDeleteSlackIntegrationDTO) => { - const slackIntegration = await slackIntegrationDAL.findById(id); + const slackIntegration = await slackIntegrationDAL.findByIdWithWorkflowIntegrationDetails(id); if (!slackIntegration) { throw new NotFoundError({ message: "Slack integration not found" @@ -401,7 +445,9 @@ export const slackServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Settings); - return slackIntegrationDAL.deleteById(id); + await workflowIntegrationDAL.deleteById(id); + + return slackIntegration; }; return { diff --git a/backend/src/services/workflow-integration/workflow-integration-dal.ts b/backend/src/services/workflow-integration/workflow-integration-dal.ts new file mode 100644 index 000000000..44fb44167 --- /dev/null +++ b/backend/src/services/workflow-integration/workflow-integration-dal.ts @@ -0,0 +1,11 @@ +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { ormify } from "@app/lib/knex"; + +export type TWorkflowIntegrationDALFactory = ReturnType; + +export const workflowIntegrationDALFactory = (db: TDbClient) => { + const workflowIntegrationOrm = ormify(db, TableName.WorkflowIntegrations); + + return workflowIntegrationOrm; +}; diff --git a/backend/src/services/workflow-integration/workflow-integration-service.ts b/backend/src/services/workflow-integration/workflow-integration-service.ts new file mode 100644 index 000000000..41419061b --- /dev/null +++ b/backend/src/services/workflow-integration/workflow-integration-service.ts @@ -0,0 +1,43 @@ +import { ForbiddenError } from "@casl/ability"; + +import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; + +import { TWorkflowIntegrationDALFactory } from "./workflow-integration-dal"; +import { TGetWorkflowIntegrationsByOrg } from "./workflow-integration-types"; + +type TWorkflowIntegrationServiceFactoryDep = { + workflowIntegrationDAL: Pick; + permissionService: Pick; +}; + +export type TWorkflowIntegrationServiceFactory = ReturnType; + +export const workflowIntegrationServiceFactory = ({ + workflowIntegrationDAL, + permissionService +}: TWorkflowIntegrationServiceFactoryDep) => { + const getIntegrationsByOrg = async ({ + actorId, + actor, + actorOrgId, + actorAuthMethod + }: TGetWorkflowIntegrationsByOrg) => { + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + actorOrgId, + actorAuthMethod, + actorOrgId + ); + + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Settings); + + return workflowIntegrationDAL.find({ + orgId: actorOrgId + }); + }; + return { + getIntegrationsByOrg + }; +}; diff --git a/backend/src/services/workflow-integration/workflow-integration-types.ts b/backend/src/services/workflow-integration/workflow-integration-types.ts new file mode 100644 index 000000000..9ae56b840 --- /dev/null +++ b/backend/src/services/workflow-integration/workflow-integration-types.ts @@ -0,0 +1,7 @@ +import { TOrgPermission } from "@app/lib/types"; + +export enum WorkflowIntegration { + SLACK = "slack" +} + +export type TGetWorkflowIntegrationsByOrg = Omit; diff --git a/frontend/src/hooks/api/workflowIntegrations/index.ts b/frontend/src/hooks/api/workflowIntegrations/index.ts index b120f4fa4..e4730bd7d 100644 --- a/frontend/src/hooks/api/workflowIntegrations/index.ts +++ b/frontend/src/hooks/api/workflowIntegrations/index.ts @@ -8,5 +8,6 @@ export { fetchSlackReinstallUrl, useGetSlackIntegrationById, useGetSlackIntegrationChannels, - useGetSlackIntegrations + useGetSlackIntegrations, + useGetWorkflowIntegrations } from "./queries"; diff --git a/frontend/src/hooks/api/workflowIntegrations/mutation.tsx b/frontend/src/hooks/api/workflowIntegrations/mutation.tsx index aa060ba17..18e3f4656 100644 --- a/frontend/src/hooks/api/workflowIntegrations/mutation.tsx +++ b/frontend/src/hooks/api/workflowIntegrations/mutation.tsx @@ -37,7 +37,7 @@ export const useDeleteSlackIntegration = () => { }, onSuccess: (_, { orgId, id }) => { queryClient.invalidateQueries(workflowIntegrationKeys.getSlackIntegration(id)); - queryClient.invalidateQueries(workflowIntegrationKeys.getSlackIntegrations(orgId)); + queryClient.invalidateQueries(workflowIntegrationKeys.getIntegrations(orgId)); } }); }; diff --git a/frontend/src/hooks/api/workflowIntegrations/queries.tsx b/frontend/src/hooks/api/workflowIntegrations/queries.tsx index 15b987281..bcf092c73 100644 --- a/frontend/src/hooks/api/workflowIntegrations/queries.tsx +++ b/frontend/src/hooks/api/workflowIntegrations/queries.tsx @@ -2,9 +2,10 @@ import { useQuery } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; -import { SlackIntegration, SlackIntegrationChannel } from "./types"; +import { SlackIntegration, SlackIntegrationChannel, WorkflowIntegration } from "./types"; export const workflowIntegrationKeys = { + getIntegrations: (orgId?: string) => [{ orgId }, "workflow-integrations"], getSlackIntegrations: (orgId?: string) => [{ orgId }, "slack-workflow-integrations"], getSlackIntegration: (id?: string) => [{ id }, "slack-workflow-integration"], getSlackIntegrationChannels: (id?: string) => [{ id }, "slack-workflow-integration-channels"] @@ -27,14 +28,10 @@ export const fetchSlackInstallUrl = async ({ return data; }; -export const fetchSlackReinstallUrl = async ({ - slackIntegrationId -}: { - slackIntegrationId: string; -}) => { +export const fetchSlackReinstallUrl = async ({ id }: { id: string }) => { const { data } = await apiRequest.get("/api/v1/workflow-integrations/slack/reinstall", { params: { - slackIntegrationId + id } }); @@ -63,6 +60,12 @@ export const fetchSlackIntegrationChannels = async (id?: string) => { return data; }; +export const fetchWorkflowIntegrations = async () => { + const { data } = await apiRequest.get("/api/v1/workflow-integrations"); + + return data; +}; + export const useGetSlackIntegrations = (orgId?: string) => useQuery({ queryKey: workflowIntegrationKeys.getSlackIntegrations(orgId), @@ -83,3 +86,10 @@ export const useGetSlackIntegrationChannels = (id?: string) => queryFn: () => fetchSlackIntegrationChannels(id), enabled: Boolean(id) }); + +export const useGetWorkflowIntegrations = (id?: string) => + useQuery({ + queryKey: workflowIntegrationKeys.getIntegrations(id), + queryFn: () => fetchWorkflowIntegrations(), + enabled: Boolean(id) + }); diff --git a/frontend/src/hooks/api/workflowIntegrations/types.ts b/frontend/src/hooks/api/workflowIntegrations/types.ts index 6d8820f2e..e30193776 100644 --- a/frontend/src/hooks/api/workflowIntegrations/types.ts +++ b/frontend/src/hooks/api/workflowIntegrations/types.ts @@ -1,7 +1,14 @@ export enum WorkflowIntegrationPlatform { - SLACK = "Slack" + SLACK = "slack" } +export type WorkflowIntegration = { + id: string; + slug: string; + description: string; + integration: WorkflowIntegrationPlatform; +}; + export type SlackIntegration = { id: string; slug: string; diff --git a/frontend/src/views/Settings/OrgSettingsPage/components/OrgWorkflowIntegrationTab/IntegrationFormDetails.tsx b/frontend/src/views/Settings/OrgSettingsPage/components/OrgWorkflowIntegrationTab/IntegrationFormDetails.tsx index aef48deb1..a9af20247 100644 --- a/frontend/src/views/Settings/OrgSettingsPage/components/OrgWorkflowIntegrationTab/IntegrationFormDetails.tsx +++ b/frontend/src/views/Settings/OrgSettingsPage/components/OrgWorkflowIntegrationTab/IntegrationFormDetails.tsx @@ -11,9 +11,12 @@ type Props = { }; export const IntegrationFormDetails = ({ isOpen, id, onOpenChange, workflowPlatform }: Props) => { + const modalTitle = + workflowPlatform === WorkflowIntegrationPlatform.SLACK ? "Slack integration" : "Integration"; + return ( - + {workflowPlatform === WorkflowIntegrationPlatform.SLACK && ( onOpenChange(false)} /> )} diff --git a/frontend/src/views/Settings/OrgSettingsPage/components/OrgWorkflowIntegrationTab/OrgWorkflowIntegrationTab.tsx b/frontend/src/views/Settings/OrgSettingsPage/components/OrgWorkflowIntegrationTab/OrgWorkflowIntegrationTab.tsx index 347c0e71f..2697a657e 100644 --- a/frontend/src/views/Settings/OrgSettingsPage/components/OrgWorkflowIntegrationTab/OrgWorkflowIntegrationTab.tsx +++ b/frontend/src/views/Settings/OrgSettingsPage/components/OrgWorkflowIntegrationTab/OrgWorkflowIntegrationTab.tsx @@ -29,7 +29,7 @@ import { usePopUp } from "@app/hooks"; import { fetchSlackReinstallUrl, useDeleteSlackIntegration, - useGetSlackIntegrations + useGetWorkflowIntegrations } from "@app/hooks/api"; import { WorkflowIntegrationPlatform } from "@app/hooks/api/workflowIntegrations/types"; @@ -46,8 +46,9 @@ export const OrgWorkflowIntegrationTab = withPermission( const { currentOrg } = useOrganization(); const router = useRouter(); - const { data: slackIntegrations, isLoading: isSlackIntegrationsLoading } = - useGetSlackIntegrations(currentOrg?.id); + const { data: workflowIntegrations, isLoading: isWorkflowIntegrationsLoading } = + useGetWorkflowIntegrations(currentOrg?.id); + const { mutateAsync: deleteSlackIntegration } = useDeleteSlackIntegration(); const handleRemoveIntegration = async () => { @@ -74,7 +75,7 @@ export const OrgWorkflowIntegrationTab = withPermission( if (platform === WorkflowIntegrationPlatform.SLACK) { try { const slackReinstallUrl = await fetchSlackReinstallUrl({ - slackIntegrationId: id + id }); if (slackReinstallUrl) { @@ -91,8 +92,6 @@ export const OrgWorkflowIntegrationTab = withPermission( } }; - const isIntegrationsLoading = isSlackIntegrationsLoading; - return (
@@ -123,23 +122,25 @@ export const OrgWorkflowIntegrationTab = withPermission( - {isIntegrationsLoading && ( + {isWorkflowIntegrationsLoading && ( )} - {!isIntegrationsLoading && slackIntegrations && slackIntegrations.length === 0 && ( - - - - - - )} - {slackIntegrations?.map((slackIntegration) => ( - + {!isWorkflowIntegrationsLoading && + workflowIntegrations && + workflowIntegrations.length === 0 && ( + + + + + + )} + {workflowIntegrations?.map((workflowIntegration) => ( + -
SLACK
+
{workflowIntegration.integration.toUpperCase()}
- {slackIntegration.slug} + {workflowIntegration.slug} @@ -153,8 +154,8 @@ export const OrgWorkflowIntegrationTab = withPermission( e.stopPropagation(); handlePopUpOpen("integrationDetails", { - id: slackIntegration.id, - platform: WorkflowIntegrationPlatform.SLACK + id: workflowIntegration.id, + platform: workflowIntegration.integration }); }} > @@ -174,8 +175,8 @@ export const OrgWorkflowIntegrationTab = withPermission( e.stopPropagation(); triggerReinstall( - WorkflowIntegrationPlatform.SLACK, - slackIntegration.id + workflowIntegration.integration, + workflowIntegration.id ); }} > @@ -197,9 +198,9 @@ export const OrgWorkflowIntegrationTab = withPermission( e.stopPropagation(); handlePopUpOpen("removeIntegration", { - id: slackIntegration.id, - slug: slackIntegration.slug, - platform: WorkflowIntegrationPlatform.SLACK + id: workflowIntegration.id, + slug: workflowIntegration.slug, + platform: workflowIntegration.integration }); }} >