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
+2
View File
@@ -77,6 +77,7 @@ import { TUserDALFactory } from "@app/services/user/user-dal";
import { TUserServiceFactory } from "@app/services/user/user-service"; import { TUserServiceFactory } from "@app/services/user/user-service";
import { TUserEngagementServiceFactory } from "@app/services/user-engagement/user-engagement-service"; import { TUserEngagementServiceFactory } from "@app/services/user-engagement/user-engagement-service";
import { TWebhookServiceFactory } from "@app/services/webhook/webhook-service"; import { TWebhookServiceFactory } from "@app/services/webhook/webhook-service";
import { TWorkflowIntegrationServiceFactory } from "@app/services/workflow-integration/workflow-integration-service";
declare module "fastify" { declare module "fastify" {
interface FastifyRequest { interface FastifyRequest {
@@ -179,6 +180,7 @@ declare module "fastify" {
externalKms: TExternalKmsServiceFactory; externalKms: TExternalKmsServiceFactory;
orgAdmin: TOrgAdminServiceFactory; orgAdmin: TOrgAdminServiceFactory;
slack: TSlackServiceFactory; slack: TSlackServiceFactory;
workflowIntegration: TWorkflowIntegrationServiceFactory;
}; };
// this is exclusive use for middlewares in which we need to inject data // this is exclusive use for middlewares in which we need to inject data
// everywhere else access using service layer // everywhere else access using service layer
+9 -1
View File
@@ -331,7 +331,10 @@ import {
TUsersUpdate, TUsersUpdate,
TWebhooks, TWebhooks,
TWebhooksInsert, TWebhooksInsert,
TWebhooksUpdate TWebhooksUpdate,
TWorkflowIntegrations,
TWorkflowIntegrationsInsert,
TWorkflowIntegrationsUpdate
} from "@app/db/schemas"; } from "@app/db/schemas";
import { import {
TSecretV2TagJunction, TSecretV2TagJunction,
@@ -800,5 +803,10 @@ declare module "knex/types/tables" {
TAdminSlackConfigsInsert, TAdminSlackConfigsInsert,
TAdminSlackConfigsUpdate TAdminSlackConfigsUpdate
>; >;
[TableName.WorkflowIntegrations]: KnexOriginal.CompositeTableType<
TWorkflowIntegrations,
TWorkflowIntegrationsInsert,
TWorkflowIntegrationsUpdate
>;
} }
} }
@@ -4,13 +4,25 @@ import { TableName } from "../schemas";
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils"; import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
export async function up(knex: Knex): Promise<void> { export async function up(knex: Knex): Promise<void> {
if (!(await knex.schema.hasTable(TableName.SlackIntegrations))) { if (!(await knex.schema.hasTable(TableName.WorkflowIntegrations))) {
await knex.schema.createTable(TableName.SlackIntegrations, (tb) => { await knex.schema.createTable(TableName.WorkflowIntegrations, (tb) => {
tb.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); tb.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
tb.string("integration").notNullable();
tb.string("slug").notNullable(); tb.string("slug").notNullable();
tb.uuid("orgId").notNullable(); tb.uuid("orgId").notNullable();
tb.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE"); tb.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
tb.string("description"); 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("teamId").notNullable();
tb.string("teamName").notNullable(); tb.string("teamName").notNullable();
tb.string("slackUserId").notNullable(); tb.string("slackUserId").notNullable();
@@ -18,7 +30,6 @@ export async function up(knex: Knex): Promise<void> {
tb.binary("encryptedBotAccessToken").notNullable(); tb.binary("encryptedBotAccessToken").notNullable();
tb.string("slackBotId").notNullable(); tb.string("slackBotId").notNullable();
tb.string("slackBotUserId").notNullable(); tb.string("slackBotUserId").notNullable();
tb.unique(["orgId", "slug"]);
tb.timestamps(true, true, true); tb.timestamps(true, true, true);
}); });
@@ -58,9 +69,12 @@ export async function down(knex: Knex): Promise<void> {
await knex.schema.dropTableIfExists(TableName.ProjectSlackConfigs); await knex.schema.dropTableIfExists(TableName.ProjectSlackConfigs);
await dropOnUpdateTrigger(knex, 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 knex.schema.dropTableIfExists(TableName.AdminSlackConfig);
await dropOnUpdateTrigger(knex, TableName.AdminSlackConfig); await dropOnUpdateTrigger(knex, TableName.AdminSlackConfig);
await knex.schema.dropTableIfExists(TableName.SlackIntegrations); await knex.schema.dropTableIfExists(TableName.WorkflowIntegrations);
await dropOnUpdateTrigger(knex, TableName.SlackIntegrations); await dropOnUpdateTrigger(knex, TableName.WorkflowIntegrations);
} }
+1
View File
@@ -112,3 +112,4 @@ export * from "./user-encryption-keys";
export * from "./user-group-membership"; export * from "./user-group-membership";
export * from "./users"; export * from "./users";
export * from "./webhooks"; export * from "./webhooks";
export * from "./workflow-integrations";
+1
View File
@@ -115,6 +115,7 @@ export enum TableName {
InternalKmsKeyVersion = "internal_kms_key_version", InternalKmsKeyVersion = "internal_kms_key_version",
// @depreciated // @depreciated
KmsKeyVersion = "kms_key_versions", KmsKeyVersion = "kms_key_versions",
WorkflowIntegrations = "workflow_integrations",
SlackIntegrations = "slack_integrations", SlackIntegrations = "slack_integrations",
ProjectSlackConfigs = "project_slack_configs", ProjectSlackConfigs = "project_slack_configs",
AdminSlackConfig = "admin_slack_configs" AdminSlackConfig = "admin_slack_configs"
@@ -11,9 +11,6 @@ import { TImmutableDBKeys } from "./models";
export const SlackIntegrationsSchema = z.object({ export const SlackIntegrationsSchema = z.object({
id: z.string().uuid(), id: z.string().uuid(),
slug: z.string(),
orgId: z.string().uuid(),
description: z.string().nullable().optional(),
teamId: z.string(), teamId: z.string(),
teamName: z.string(), teamName: z.string(),
slackUserId: 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>>;
+12 -2
View File
@@ -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 { userEngagementServiceFactory } from "@app/services/user-engagement/user-engagement-service";
import { webhookDALFactory } from "@app/services/webhook/webhook-dal"; import { webhookDALFactory } from "@app/services/webhook/webhook-dal";
import { webhookServiceFactory } from "@app/services/webhook/webhook-service"; 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 { injectAuditLogInfo } from "../plugins/audit-log";
import { injectIdentity } from "../plugins/auth/inject-identity"; import { injectIdentity } from "../plugins/auth/inject-identity";
@@ -329,6 +331,7 @@ export const registerRoutes = async (
const slackIntegrationDAL = slackIntegrationDALFactory(db); const slackIntegrationDAL = slackIntegrationDALFactory(db);
const projectSlackConfigDAL = projectSlackConfigDALFactory(db); const projectSlackConfigDAL = projectSlackConfigDALFactory(db);
const adminSlackConfigDAL = adminSlackConfigDALFactory(db); const adminSlackConfigDAL = adminSlackConfigDALFactory(db);
const workflowIntegrationDAL = workflowIntegrationDALFactory(db);
const permissionService = permissionServiceFactory({ const permissionService = permissionServiceFactory({
permissionDAL, permissionDAL,
@@ -1170,7 +1173,13 @@ export const registerRoutes = async (
permissionService, permissionService,
kmsService, kmsService,
slackIntegrationDAL, slackIntegrationDAL,
adminSlackConfigDAL adminSlackConfigDAL,
workflowIntegrationDAL
});
const workflowIntegrationService = workflowIntegrationServiceFactory({
permissionService,
workflowIntegrationDAL
}); });
await superAdminService.initServerCfg(); await superAdminService.initServerCfg();
@@ -1255,7 +1264,8 @@ export const registerRoutes = async (
userEngagement: userEngagementService, userEngagement: userEngagementService,
externalKms: externalKmsService, externalKms: externalKmsService,
orgAdmin: orgAdminService, orgAdmin: orgAdminService,
slack: slackService slack: slackService,
workflowIntegration: workflowIntegrationService
}); });
const cronJobs: CronJob[] = []; const cronJobs: CronJob[] = [];
+2
View File
@@ -35,6 +35,7 @@ import { registerUserActionRouter } from "./user-action-router";
import { registerUserEngagementRouter } from "./user-engagement-router"; import { registerUserEngagementRouter } from "./user-engagement-router";
import { registerUserRouter } from "./user-router"; import { registerUserRouter } from "./user-router";
import { registerWebhookRouter } from "./webhook-router"; import { registerWebhookRouter } from "./webhook-router";
import { registerWorkflowIntegrationRouter } from "./workflow-integration-router";
export const registerV1Routes = async (server: FastifyZodProvider) => { export const registerV1Routes = async (server: FastifyZodProvider) => {
await server.register(registerSsoRouter, { prefix: "/sso" }); await server.register(registerSsoRouter, { prefix: "/sso" });
@@ -64,6 +65,7 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
await server.register( await server.register(
async (workflowIntegrationRouter) => { async (workflowIntegrationRouter) => {
await workflowIntegrationRouter.register(registerWorkflowIntegrationRouter);
await workflowIntegrationRouter.register(registerSlackRouter, { prefix: "/slack" }); await workflowIntegrationRouter.register(registerSlackRouter, { prefix: "/slack" });
}, },
{ prefix: "/workflow-integrations" } { prefix: "/workflow-integrations" }
+31 -40
View File
@@ -1,12 +1,23 @@
import { z } from "zod"; 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 { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type"; 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) => { export const registerSlackRouter = async (server: FastifyZodProvider) => {
const appCfg = getConfig(); const appCfg = getConfig();
@@ -69,7 +80,7 @@ export const registerSlackRouter = async (server: FastifyZodProvider) => {
} }
], ],
querystring: z.object({ querystring: z.object({
slackIntegrationId: z.string() id: z.string()
}), }),
response: { response: {
200: z.string() 200: z.string()
@@ -82,7 +93,7 @@ export const registerSlackRouter = async (server: FastifyZodProvider) => {
actorId: req.permission.id, actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
id: req.query.slackIntegrationId id: req.query.id
}); });
await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
@@ -91,7 +102,7 @@ export const registerSlackRouter = async (server: FastifyZodProvider) => {
event: { event: {
type: EventType.ATTEMPT_REINSTALL_SLACK_INTEGRATION, type: EventType.ATTEMPT_REINSTALL_SLACK_INTEGRATION,
metadata: { metadata: {
id: req.query.slackIntegrationId id: req.query.id
} }
} }
}); });
@@ -113,12 +124,7 @@ export const registerSlackRouter = async (server: FastifyZodProvider) => {
} }
], ],
response: { response: {
200: SlackIntegrationsSchema.pick({ 200: sanitizedSlackIntegrationSchema.array()
id: true,
slug: true,
description: true,
teamName: true
}).array()
} }
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
@@ -136,7 +142,7 @@ export const registerSlackRouter = async (server: FastifyZodProvider) => {
server.route({ server.route({
method: "DELETE", method: "DELETE",
url: "/:slackIntegrationId", url: "/:id",
config: { config: {
rateLimit: writeLimit rateLimit: writeLimit
}, },
@@ -147,15 +153,10 @@ export const registerSlackRouter = async (server: FastifyZodProvider) => {
} }
], ],
params: z.object({ params: z.object({
slackIntegrationId: z.string() id: z.string()
}), }),
response: { response: {
200: SlackIntegrationsSchema.pick({ 200: sanitizedSlackIntegrationSchema
id: true,
slug: true,
description: true,
teamName: true
})
} }
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
@@ -165,7 +166,7 @@ export const registerSlackRouter = async (server: FastifyZodProvider) => {
actorId: req.permission.id, actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
id: req.params.slackIntegrationId id: req.params.id
}); });
await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
@@ -185,7 +186,7 @@ export const registerSlackRouter = async (server: FastifyZodProvider) => {
server.route({ server.route({
method: "GET", method: "GET",
url: "/:slackIntegrationId", url: "/:id",
config: { config: {
rateLimit: readLimit rateLimit: readLimit
}, },
@@ -196,15 +197,10 @@ export const registerSlackRouter = async (server: FastifyZodProvider) => {
} }
], ],
params: z.object({ params: z.object({
slackIntegrationId: z.string() id: z.string()
}), }),
response: { response: {
200: SlackIntegrationsSchema.pick({ 200: sanitizedSlackIntegrationSchema
id: true,
slug: true,
description: true,
teamName: true
})
} }
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
@@ -214,7 +210,7 @@ export const registerSlackRouter = async (server: FastifyZodProvider) => {
actorId: req.permission.id, actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
id: req.params.slackIntegrationId id: req.params.id
}); });
await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
@@ -234,7 +230,7 @@ export const registerSlackRouter = async (server: FastifyZodProvider) => {
server.route({ server.route({
method: "GET", method: "GET",
url: "/:slackIntegrationId/channels", url: "/:id/channels",
config: { config: {
rateLimit: readLimit rateLimit: readLimit
}, },
@@ -245,7 +241,7 @@ export const registerSlackRouter = async (server: FastifyZodProvider) => {
} }
], ],
params: z.object({ params: z.object({
slackIntegrationId: z.string() id: z.string()
}), }),
response: { response: {
200: z 200: z
@@ -263,7 +259,7 @@ export const registerSlackRouter = async (server: FastifyZodProvider) => {
actorId: req.permission.id, actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
id: req.params.slackIntegrationId id: req.params.id
}); });
return slackChannels; return slackChannels;
@@ -272,7 +268,7 @@ export const registerSlackRouter = async (server: FastifyZodProvider) => {
server.route({ server.route({
method: "PATCH", method: "PATCH",
url: "/:slackIntegrationId", url: "/:id",
config: { config: {
rateLimit: readLimit rateLimit: readLimit
}, },
@@ -283,19 +279,14 @@ export const registerSlackRouter = async (server: FastifyZodProvider) => {
} }
], ],
params: z.object({ params: z.object({
slackIntegrationId: z.string() id: z.string()
}), }),
body: z.object({ body: z.object({
slug: z.string().optional(), slug: z.string().optional(),
description: z.string().optional() description: z.string().optional()
}), }),
response: { response: {
200: SlackIntegrationsSchema.pick({ 200: sanitizedSlackIntegrationSchema
id: true,
slug: true,
description: true,
teamName: true
})
} }
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
@@ -305,7 +296,7 @@ export const registerSlackRouter = async (server: FastifyZodProvider) => {
actorId: req.permission.id, actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
id: req.params.slackIntegrationId, id: req.params.id,
...req.body ...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">; projectKeyDAL: Pick<TProjectKeyDALFactory, "create" | "findLatestProjectKey" | "delete" | "find" | "insertMany">;
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "create" | "findProjectGhostUser" | "findOne">; projectMembershipDAL: Pick<TProjectMembershipDALFactory, "create" | "findProjectGhostUser" | "findOne">;
projectSlackConfigDAL: Pick<TProjectSlackConfigDALFactory, "findOne" | "transaction" | "updateById" | "create">; projectSlackConfigDAL: Pick<TProjectSlackConfigDALFactory, "findOne" | "transaction" | "updateById" | "create">;
slackIntegrationDAL: Pick<TSlackIntegrationDALFactory, "findById">; slackIntegrationDAL: Pick<TSlackIntegrationDALFactory, "findById" | "findByIdWithWorkflowIntegrationDetails">;
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "create">; projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "create">;
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "find">; certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "find">;
certificateDAL: Pick<TCertificateDALFactory, "find" | "countCertificatesInProject">; 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) { if (!slackIntegration) {
throw new NotFoundError({ throw new NotFoundError({
message: "Slack integration not found" message: "Slack integration not found"
@@ -1,11 +1,56 @@
import { Knex } from "knex";
import { TDbClient } from "@app/db"; import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas"; import { TableName, TSlackIntegrations, TWorkflowIntegrations } from "@app/db/schemas";
import { ormify } from "@app/lib/knex"; import { DatabaseError } from "@app/lib/errors";
import { ormify, selectAllTableCols } from "@app/lib/knex";
export type TSlackIntegrationDALFactory = ReturnType<typeof slackIntegrationDALFactory>; export type TSlackIntegrationDALFactory = ReturnType<typeof slackIntegrationDALFactory>;
export const slackIntegrationDALFactory = (db: TDbClient) => { export const slackIntegrationDALFactory = (db: TDbClient) => {
const slackIntegrationOrm = ormify(db, TableName.SlackIntegrations); 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 };
}; };
+70 -24
View File
@@ -8,6 +8,8 @@ import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { TKmsServiceFactory } from "../kms/kms-service"; import { TKmsServiceFactory } from "../kms/kms-service";
import { KmsDataKey } from "../kms/kms-types"; 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 { TAdminSlackConfigDALFactory } from "./admin-slack-config-dal";
import { fetchSlackChannels, getAdminSlackCredentials } from "./slack-fns"; import { fetchSlackChannels, getAdminSlackCredentials } from "./slack-fns";
import { TSlackIntegrationDALFactory } from "./slack-integration-dal"; import { TSlackIntegrationDALFactory } from "./slack-integration-dal";
@@ -24,10 +26,18 @@ import {
} from "./slack-types"; } from "./slack-types";
type TSlackServiceFactoryDep = { type TSlackServiceFactoryDep = {
slackIntegrationDAL: Pick<TSlackIntegrationDALFactory, "find" | "findById" | "deleteById" | "updateById" | "create">; slackIntegrationDAL: Pick<
TSlackIntegrationDALFactory,
| "deleteById"
| "updateById"
| "create"
| "findByIdWithWorkflowIntegrationDetails"
| "findWithWorkflowIntegrationDetails"
>;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getOrgPermission">; permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getOrgPermission">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey" | "encryptWithRootKey" | "decryptWithRootKey">; kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey" | "encryptWithRootKey" | "decryptWithRootKey">;
adminSlackConfigDAL: Pick<TAdminSlackConfigDALFactory, "findById">; adminSlackConfigDAL: Pick<TAdminSlackConfigDALFactory, "findById">;
workflowIntegrationDAL: Pick<TWorkflowIntegrationDALFactory, "transaction" | "create" | "updateById" | "deleteById">;
}; };
export type TSlackServiceFactory = ReturnType<typeof slackServiceFactory>; export type TSlackServiceFactory = ReturnType<typeof slackServiceFactory>;
@@ -36,7 +46,8 @@ export const slackServiceFactory = ({
permissionService, permissionService,
slackIntegrationDAL, slackIntegrationDAL,
kmsService, kmsService,
adminSlackConfigDAL adminSlackConfigDAL,
workflowIntegrationDAL
}: TSlackServiceFactoryDep) => { }: TSlackServiceFactoryDep) => {
const completeSlackIntegration = async ({ const completeSlackIntegration = async ({
orgId, orgId,
@@ -59,17 +70,31 @@ export const slackServiceFactory = ({
plainText: Buffer.from(botAccessToken, "utf8") plainText: Buffer.from(botAccessToken, "utf8")
}); });
await slackIntegrationDAL.create({ await workflowIntegrationDAL.transaction(async (tx) => {
orgId, const workflowIntegration = await workflowIntegrationDAL.create(
slug, {
description, description,
teamId, orgId,
teamName, slug,
slackUserId, integration: WorkflowIntegration.SLACK
slackAppId, },
slackBotId, tx
slackBotUserId, );
encryptedBotAccessToken
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, slackBotId,
slackBotUserId slackBotUserId
}: TReinstallSlackIntegrationDTO) => { }: 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({ const { encryptor: orgDataKeyEncryptor } = await kmsService.createCipherPairWithDataKey({
orgId: slackIntegration.orgId, orgId: slackIntegration.orgId,
@@ -228,7 +259,7 @@ export const slackServiceFactory = ({
const getReinstallUrl = async ({ actorId, actor, actorOrgId, actorAuthMethod, id }: TGetReinstallUrlDTO) => { const getReinstallUrl = async ({ actorId, actor, actorOrgId, actorAuthMethod, id }: TGetReinstallUrlDTO) => {
const appCfg = getConfig(); const appCfg = getConfig();
const slackIntegration = await slackIntegrationDAL.findById(id); const slackIntegration = await slackIntegrationDAL.findByIdWithWorkflowIntegrationDetails(id);
if (!slackIntegration) { if (!slackIntegration) {
throw new NotFoundError({ throw new NotFoundError({
@@ -275,7 +306,7 @@ export const slackServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Settings); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Settings);
const slackIntegrations = await slackIntegrationDAL.find({ const slackIntegrations = await slackIntegrationDAL.findWithWorkflowIntegrationDetails({
orgId: actorOrgId orgId: actorOrgId
}); });
@@ -289,7 +320,7 @@ export const slackServiceFactory = ({
actorAuthMethod, actorAuthMethod,
id id
}: TGetSlackIntegrationByIdDTO) => { }: TGetSlackIntegrationByIdDTO) => {
const slackIntegration = await slackIntegrationDAL.findById(id); const slackIntegration = await slackIntegrationDAL.findByIdWithWorkflowIntegrationDetails(id);
if (!slackIntegration) { if (!slackIntegration) {
throw new NotFoundError({ throw new NotFoundError({
message: "Slack integration not found." message: "Slack integration not found."
@@ -316,7 +347,7 @@ export const slackServiceFactory = ({
actorAuthMethod, actorAuthMethod,
id id
}: TGetSlackIntegrationChannelsDTO) => { }: TGetSlackIntegrationChannelsDTO) => {
const slackIntegration = await slackIntegrationDAL.findById(id); const slackIntegration = await slackIntegrationDAL.findByIdWithWorkflowIntegrationDetails(id);
if (!slackIntegration) { if (!slackIntegration) {
throw new NotFoundError({ throw new NotFoundError({
message: "Slack integration not found." message: "Slack integration not found."
@@ -354,7 +385,7 @@ export const slackServiceFactory = ({
slug, slug,
description description
}: TUpdateSlackIntegrationDTO) => { }: TUpdateSlackIntegrationDTO) => {
const slackIntegration = await slackIntegrationDAL.findById(id); const slackIntegration = await slackIntegrationDAL.findByIdWithWorkflowIntegrationDetails(id);
if (!slackIntegration) { if (!slackIntegration) {
throw new NotFoundError({ throw new NotFoundError({
message: "Slack integration not found" message: "Slack integration not found"
@@ -371,9 +402,22 @@ export const slackServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings);
return slackIntegrationDAL.updateById(slackIntegration.id, { return workflowIntegrationDAL.transaction(async (tx) => {
slug, await workflowIntegrationDAL.updateById(
description slackIntegration.id,
{
slug,
description
},
tx
);
const updatedIntegration = await slackIntegrationDAL.findByIdWithWorkflowIntegrationDetails(
slackIntegration.id,
tx
);
return updatedIntegration!;
}); });
}; };
@@ -384,7 +428,7 @@ export const slackServiceFactory = ({
actorAuthMethod, actorAuthMethod,
id id
}: TDeleteSlackIntegrationDTO) => { }: TDeleteSlackIntegrationDTO) => {
const slackIntegration = await slackIntegrationDAL.findById(id); const slackIntegration = await slackIntegrationDAL.findByIdWithWorkflowIntegrationDetails(id);
if (!slackIntegration) { if (!slackIntegration) {
throw new NotFoundError({ throw new NotFoundError({
message: "Slack integration not found" message: "Slack integration not found"
@@ -401,7 +445,9 @@ export const slackServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Settings); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Settings);
return slackIntegrationDAL.deleteById(id); await workflowIntegrationDAL.deleteById(id);
return slackIntegration;
}; };
return { 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, fetchSlackReinstallUrl,
useGetSlackIntegrationById, useGetSlackIntegrationById,
useGetSlackIntegrationChannels, useGetSlackIntegrationChannels,
useGetSlackIntegrations useGetSlackIntegrations,
useGetWorkflowIntegrations
} from "./queries"; } from "./queries";
@@ -37,7 +37,7 @@ export const useDeleteSlackIntegration = () => {
}, },
onSuccess: (_, { orgId, id }) => { onSuccess: (_, { orgId, id }) => {
queryClient.invalidateQueries(workflowIntegrationKeys.getSlackIntegration(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 { apiRequest } from "@app/config/request";
import { SlackIntegration, SlackIntegrationChannel } from "./types"; import { SlackIntegration, SlackIntegrationChannel, WorkflowIntegration } from "./types";
export const workflowIntegrationKeys = { export const workflowIntegrationKeys = {
getIntegrations: (orgId?: string) => [{ orgId }, "workflow-integrations"],
getSlackIntegrations: (orgId?: string) => [{ orgId }, "slack-workflow-integrations"], getSlackIntegrations: (orgId?: string) => [{ orgId }, "slack-workflow-integrations"],
getSlackIntegration: (id?: string) => [{ id }, "slack-workflow-integration"], getSlackIntegration: (id?: string) => [{ id }, "slack-workflow-integration"],
getSlackIntegrationChannels: (id?: string) => [{ id }, "slack-workflow-integration-channels"] getSlackIntegrationChannels: (id?: string) => [{ id }, "slack-workflow-integration-channels"]
@@ -27,14 +28,10 @@ export const fetchSlackInstallUrl = async ({
return data; return data;
}; };
export const fetchSlackReinstallUrl = async ({ export const fetchSlackReinstallUrl = async ({ id }: { id: string }) => {
slackIntegrationId
}: {
slackIntegrationId: string;
}) => {
const { data } = await apiRequest.get<string>("/api/v1/workflow-integrations/slack/reinstall", { const { data } = await apiRequest.get<string>("/api/v1/workflow-integrations/slack/reinstall", {
params: { params: {
slackIntegrationId id
} }
}); });
@@ -63,6 +60,12 @@ export const fetchSlackIntegrationChannels = async (id?: string) => {
return data; return data;
}; };
export const fetchWorkflowIntegrations = async () => {
const { data } = await apiRequest.get<WorkflowIntegration[]>("/api/v1/workflow-integrations");
return data;
};
export const useGetSlackIntegrations = (orgId?: string) => export const useGetSlackIntegrations = (orgId?: string) =>
useQuery({ useQuery({
queryKey: workflowIntegrationKeys.getSlackIntegrations(orgId), queryKey: workflowIntegrationKeys.getSlackIntegrations(orgId),
@@ -83,3 +86,10 @@ export const useGetSlackIntegrationChannels = (id?: string) =>
queryFn: () => fetchSlackIntegrationChannels(id), queryFn: () => fetchSlackIntegrationChannels(id),
enabled: Boolean(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 { export enum WorkflowIntegrationPlatform {
SLACK = "Slack" SLACK = "slack"
} }
export type WorkflowIntegration = {
id: string;
slug: string;
description: string;
integration: WorkflowIntegrationPlatform;
};
export type SlackIntegration = { export type SlackIntegration = {
id: string; id: string;
slug: string; slug: string;
@@ -11,9 +11,12 @@ type Props = {
}; };
export const IntegrationFormDetails = ({ isOpen, id, onOpenChange, workflowPlatform }: Props) => { export const IntegrationFormDetails = ({ isOpen, id, onOpenChange, workflowPlatform }: Props) => {
const modalTitle =
workflowPlatform === WorkflowIntegrationPlatform.SLACK ? "Slack integration" : "Integration";
return ( return (
<Modal isOpen={isOpen} onOpenChange={onOpenChange}> <Modal isOpen={isOpen} onOpenChange={onOpenChange}>
<ModalContent title={`${WorkflowIntegrationPlatform.SLACK} integration`}> <ModalContent title={modalTitle}>
{workflowPlatform === WorkflowIntegrationPlatform.SLACK && ( {workflowPlatform === WorkflowIntegrationPlatform.SLACK && (
<SlackIntegrationForm id={id} onClose={() => onOpenChange(false)} /> <SlackIntegrationForm id={id} onClose={() => onOpenChange(false)} />
)} )}
@@ -29,7 +29,7 @@ import { usePopUp } from "@app/hooks";
import { import {
fetchSlackReinstallUrl, fetchSlackReinstallUrl,
useDeleteSlackIntegration, useDeleteSlackIntegration,
useGetSlackIntegrations useGetWorkflowIntegrations
} from "@app/hooks/api"; } from "@app/hooks/api";
import { WorkflowIntegrationPlatform } from "@app/hooks/api/workflowIntegrations/types"; import { WorkflowIntegrationPlatform } from "@app/hooks/api/workflowIntegrations/types";
@@ -46,8 +46,9 @@ export const OrgWorkflowIntegrationTab = withPermission(
const { currentOrg } = useOrganization(); const { currentOrg } = useOrganization();
const router = useRouter(); const router = useRouter();
const { data: slackIntegrations, isLoading: isSlackIntegrationsLoading } = const { data: workflowIntegrations, isLoading: isWorkflowIntegrationsLoading } =
useGetSlackIntegrations(currentOrg?.id); useGetWorkflowIntegrations(currentOrg?.id);
const { mutateAsync: deleteSlackIntegration } = useDeleteSlackIntegration(); const { mutateAsync: deleteSlackIntegration } = useDeleteSlackIntegration();
const handleRemoveIntegration = async () => { const handleRemoveIntegration = async () => {
@@ -74,7 +75,7 @@ export const OrgWorkflowIntegrationTab = withPermission(
if (platform === WorkflowIntegrationPlatform.SLACK) { if (platform === WorkflowIntegrationPlatform.SLACK) {
try { try {
const slackReinstallUrl = await fetchSlackReinstallUrl({ const slackReinstallUrl = await fetchSlackReinstallUrl({
slackIntegrationId: id id
}); });
if (slackReinstallUrl) { if (slackReinstallUrl) {
@@ -91,8 +92,6 @@ export const OrgWorkflowIntegrationTab = withPermission(
} }
}; };
const isIntegrationsLoading = isSlackIntegrationsLoading;
return ( return (
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"> <div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="flex justify-between"> <div className="flex justify-between">
@@ -123,23 +122,25 @@ export const OrgWorkflowIntegrationTab = withPermission(
</Tr> </Tr>
</THead> </THead>
<TBody> <TBody>
{isIntegrationsLoading && ( {isWorkflowIntegrationsLoading && (
<TableSkeleton columns={2} innerKey="integrations-loading" /> <TableSkeleton columns={2} innerKey="integrations-loading" />
)} )}
{!isIntegrationsLoading && slackIntegrations && slackIntegrations.length === 0 && ( {!isWorkflowIntegrationsLoading &&
<Tr> workflowIntegrations &&
<Td colSpan={5}> workflowIntegrations.length === 0 && (
<EmptyState title="No workflow integrations found" icon={faGear} /> <Tr>
</Td> <Td colSpan={5}>
</Tr> <EmptyState title="No workflow integrations found" icon={faGear} />
)} </Td>
{slackIntegrations?.map((slackIntegration) => ( </Tr>
<Tr key={slackIntegration.id}> )}
{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"> <Td className="flex max-w-xs items-center overflow-hidden text-ellipsis hover:overflow-auto hover:break-all">
<FontAwesomeIcon icon={faSlack} /> <FontAwesomeIcon icon={faSlack} />
<div className="ml-2">SLACK</div> <div className="ml-2">{workflowIntegration.integration.toUpperCase()}</div>
</Td> </Td>
<Td>{slackIntegration.slug}</Td> <Td>{workflowIntegration.slug}</Td>
<Td> <Td>
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg"> <DropdownMenuTrigger asChild className="rounded-lg">
@@ -153,8 +154,8 @@ export const OrgWorkflowIntegrationTab = withPermission(
e.stopPropagation(); e.stopPropagation();
handlePopUpOpen("integrationDetails", { handlePopUpOpen("integrationDetails", {
id: slackIntegration.id, id: workflowIntegration.id,
platform: WorkflowIntegrationPlatform.SLACK platform: workflowIntegration.integration
}); });
}} }}
> >
@@ -174,8 +175,8 @@ export const OrgWorkflowIntegrationTab = withPermission(
e.stopPropagation(); e.stopPropagation();
triggerReinstall( triggerReinstall(
WorkflowIntegrationPlatform.SLACK, workflowIntegration.integration,
slackIntegration.id workflowIntegration.id
); );
}} }}
> >
@@ -197,9 +198,9 @@ export const OrgWorkflowIntegrationTab = withPermission(
e.stopPropagation(); e.stopPropagation();
handlePopUpOpen("removeIntegration", { handlePopUpOpen("removeIntegration", {
id: slackIntegration.id, id: workflowIntegration.id,
slug: slackIntegration.slug, slug: workflowIntegration.slug,
platform: WorkflowIntegrationPlatform.SLACK platform: workflowIntegration.integration
}); });
}} }}
> >