1
0
mirror of https://github.com/Infisical/infisical.git synced 2025-03-31 22:09:57 +00:00

misc: added root workflow integration structure

This commit is contained in:
Sheen Capadngan
2024-09-08 13:49:32 +08:00
parent dbc5b5a3d1
commit ecf177fecc
23 changed files with 380 additions and 116 deletions

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

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

@ -4,13 +4,25 @@ import { TableName } from "../schemas";
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
export async function up(knex: Knex): Promise<void> {
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<void> {
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<void> {
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);
}

@ -112,3 +112,4 @@ export * from "./user-encryption-keys";
export * from "./user-group-membership";
export * from "./users";
export * from "./webhooks";
export * from "./workflow-integrations";

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

@ -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(),

@ -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<typeof WorkflowIntegrationsSchema>;
export type TWorkflowIntegrationsInsert = Omit<z.input<typeof WorkflowIntegrationsSchema>, TImmutableDBKeys>;
export type TWorkflowIntegrationsUpdate = Partial<Omit<z.input<typeof WorkflowIntegrationsSchema>, TImmutableDBKeys>>;

@ -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[] = [];

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

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

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

@ -81,7 +81,7 @@ type TProjectServiceFactoryDep = {
projectKeyDAL: Pick<TProjectKeyDALFactory, "create" | "findLatestProjectKey" | "delete" | "find" | "insertMany">;
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "create" | "findProjectGhostUser" | "findOne">;
projectSlackConfigDAL: Pick<TProjectSlackConfigDALFactory, "findOne" | "transaction" | "updateById" | "create">;
slackIntegrationDAL: Pick<TSlackIntegrationDALFactory, "findById">;
slackIntegrationDAL: Pick<TSlackIntegrationDALFactory, "findById" | "findByIdWithWorkflowIntegrationDetails">;
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "create">;
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "find">;
certificateDAL: Pick<TCertificateDALFactory, "find" | "countCertificatesInProject">;
@ -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"

@ -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<typeof slackIntegrationDALFactory>;
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<TSlackIntegrations> & Partial<TWorkflowIntegrations>,
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 };
};

@ -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<TSlackIntegrationDALFactory, "find" | "findById" | "deleteById" | "updateById" | "create">;
slackIntegrationDAL: Pick<
TSlackIntegrationDALFactory,
| "deleteById"
| "updateById"
| "create"
| "findByIdWithWorkflowIntegrationDetails"
| "findWithWorkflowIntegrationDetails"
>;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getOrgPermission">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey" | "encryptWithRootKey" | "decryptWithRootKey">;
adminSlackConfigDAL: Pick<TAdminSlackConfigDALFactory, "findById">;
workflowIntegrationDAL: Pick<TWorkflowIntegrationDALFactory, "transaction" | "create" | "updateById" | "deleteById">;
};
export type TSlackServiceFactory = ReturnType<typeof slackServiceFactory>;
@ -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 {

@ -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<typeof workflowIntegrationDALFactory>;
export const workflowIntegrationDALFactory = (db: TDbClient) => {
const workflowIntegrationOrm = ormify(db, TableName.WorkflowIntegrations);
return workflowIntegrationOrm;
};

@ -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<TWorkflowIntegrationDALFactory, "find">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getOrgPermission">;
};
export type TWorkflowIntegrationServiceFactory = ReturnType<typeof workflowIntegrationServiceFactory>;
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
};
};

@ -0,0 +1,7 @@
import { TOrgPermission } from "@app/lib/types";
export enum WorkflowIntegration {
SLACK = "slack"
}
export type TGetWorkflowIntegrationsByOrg = Omit<TOrgPermission, "orgId">;

@ -8,5 +8,6 @@ export {
fetchSlackReinstallUrl,
useGetSlackIntegrationById,
useGetSlackIntegrationChannels,
useGetSlackIntegrations
useGetSlackIntegrations,
useGetWorkflowIntegrations
} from "./queries";

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

@ -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<string>("/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<WorkflowIntegration[]>("/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)
});

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

@ -11,9 +11,12 @@ type Props = {
};
export const IntegrationFormDetails = ({ isOpen, id, onOpenChange, workflowPlatform }: Props) => {
const modalTitle =
workflowPlatform === WorkflowIntegrationPlatform.SLACK ? "Slack integration" : "Integration";
return (
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
<ModalContent title={`${WorkflowIntegrationPlatform.SLACK} integration`}>
<ModalContent title={modalTitle}>
{workflowPlatform === WorkflowIntegrationPlatform.SLACK && (
<SlackIntegrationForm id={id} onClose={() => onOpenChange(false)} />
)}

@ -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 (
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="flex justify-between">
@ -123,23 +122,25 @@ export const OrgWorkflowIntegrationTab = withPermission(
</Tr>
</THead>
<TBody>
{isIntegrationsLoading && (
{isWorkflowIntegrationsLoading && (
<TableSkeleton columns={2} innerKey="integrations-loading" />
)}
{!isIntegrationsLoading && slackIntegrations && slackIntegrations.length === 0 && (
<Tr>
<Td colSpan={5}>
<EmptyState title="No workflow integrations found" icon={faGear} />
</Td>
</Tr>
)}
{slackIntegrations?.map((slackIntegration) => (
<Tr key={slackIntegration.id}>
{!isWorkflowIntegrationsLoading &&
workflowIntegrations &&
workflowIntegrations.length === 0 && (
<Tr>
<Td colSpan={5}>
<EmptyState title="No workflow integrations found" icon={faGear} />
</Td>
</Tr>
)}
{workflowIntegrations?.map((workflowIntegration) => (
<Tr key={workflowIntegration.id}>
<Td className="flex max-w-xs items-center overflow-hidden text-ellipsis hover:overflow-auto hover:break-all">
<FontAwesomeIcon icon={faSlack} />
<div className="ml-2">SLACK</div>
<div className="ml-2">{workflowIntegration.integration.toUpperCase()}</div>
</Td>
<Td>{slackIntegration.slug}</Td>
<Td>{workflowIntegration.slug}</Td>
<Td>
<DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg">
@ -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
});
}}
>