mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
feat: finished org-level integration management flow
This commit is contained in:
8
backend/src/@types/knex.d.ts
vendored
8
backend/src/@types/knex.d.ts
vendored
@@ -196,6 +196,9 @@ import {
|
||||
TProjectRolesUpdate,
|
||||
TProjects,
|
||||
TProjectsInsert,
|
||||
TProjectSlackConfigs,
|
||||
TProjectSlackConfigsInsert,
|
||||
TProjectSlackConfigsUpdate,
|
||||
TProjectsUpdate,
|
||||
TProjectUserAdditionalPrivilege,
|
||||
TProjectUserAdditionalPrivilegeInsert,
|
||||
@@ -787,6 +790,11 @@ declare module "knex/types/tables" {
|
||||
TSlackIntegrationsInsert,
|
||||
TSlackIntegrationsUpdate
|
||||
>;
|
||||
[TableName.ProjectSlackConfigs]: KnexOriginal.CompositeTableType<
|
||||
TProjectSlackConfigs,
|
||||
TProjectSlackConfigsInsert,
|
||||
TProjectSlackConfigsUpdate
|
||||
>;
|
||||
[TableName.AdminSlackConfig]: KnexOriginal.CompositeTableType<
|
||||
TAdminSlackConfigs,
|
||||
TAdminSlackConfigsInsert,
|
||||
|
||||
@@ -7,8 +7,10 @@ export async function up(knex: Knex): Promise<void> {
|
||||
if (!(await knex.schema.hasTable(TableName.SlackIntegrations))) {
|
||||
await knex.schema.createTable(TableName.SlackIntegrations, (tb) => {
|
||||
tb.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
tb.string("projectId").notNullable().unique();
|
||||
tb.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
|
||||
tb.string("slug").notNullable();
|
||||
tb.uuid("orgId").notNullable();
|
||||
tb.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
|
||||
tb.string("description");
|
||||
tb.string("teamId").notNullable();
|
||||
tb.string("teamName").notNullable();
|
||||
tb.string("slackUserId").notNullable();
|
||||
@@ -16,6 +18,20 @@ 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);
|
||||
});
|
||||
|
||||
await createOnUpdateTrigger(knex, TableName.SlackIntegrations);
|
||||
}
|
||||
|
||||
if (!(await knex.schema.hasTable(TableName.ProjectSlackConfigs))) {
|
||||
await knex.schema.createTable(TableName.ProjectSlackConfigs, (tb) => {
|
||||
tb.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
tb.string("projectId").notNullable().unique();
|
||||
tb.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
|
||||
tb.uuid("slackIntegrationId").notNullable();
|
||||
tb.foreign("slackIntegrationId").references("id").inTable(TableName.SlackIntegrations).onDelete("CASCADE");
|
||||
tb.boolean("isAccessRequestNotificationEnabled").notNullable().defaultTo(false);
|
||||
tb.string("accessRequestChannels").notNullable().defaultTo("");
|
||||
tb.boolean("isSecretRequestNotificationEnabled").notNullable().defaultTo(false);
|
||||
@@ -23,7 +39,7 @@ export async function up(knex: Knex): Promise<void> {
|
||||
tb.timestamps(true, true, true);
|
||||
});
|
||||
|
||||
await createOnUpdateTrigger(knex, TableName.SlackIntegrations);
|
||||
await createOnUpdateTrigger(knex, TableName.ProjectSlackConfigs);
|
||||
}
|
||||
|
||||
if (!(await knex.schema.hasTable(TableName.AdminSlackConfig))) {
|
||||
@@ -39,9 +55,12 @@ export async function up(knex: Knex): Promise<void> {
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.dropTableIfExists(TableName.SlackIntegrations);
|
||||
await dropOnUpdateTrigger(knex, TableName.SlackIntegrations);
|
||||
await knex.schema.dropTableIfExists(TableName.ProjectSlackConfigs);
|
||||
await dropOnUpdateTrigger(knex, TableName.ProjectSlackConfigs);
|
||||
|
||||
await knex.schema.dropTableIfExists(TableName.AdminSlackConfig);
|
||||
await dropOnUpdateTrigger(knex, TableName.AdminSlackConfig);
|
||||
|
||||
await knex.schema.dropTableIfExists(TableName.SlackIntegrations);
|
||||
await dropOnUpdateTrigger(knex, TableName.SlackIntegrations);
|
||||
}
|
||||
|
||||
@@ -63,6 +63,7 @@ export * from "./project-environments";
|
||||
export * from "./project-keys";
|
||||
export * from "./project-memberships";
|
||||
export * from "./project-roles";
|
||||
export * from "./project-slack-configs";
|
||||
export * from "./project-user-additional-privilege";
|
||||
export * from "./project-user-membership-roles";
|
||||
export * from "./projects";
|
||||
|
||||
@@ -116,6 +116,7 @@ export enum TableName {
|
||||
// @depreciated
|
||||
KmsKeyVersion = "kms_key_versions",
|
||||
SlackIntegrations = "slack_integrations",
|
||||
ProjectSlackConfigs = "project_slack_configs",
|
||||
AdminSlackConfig = "admin_slack_configs"
|
||||
}
|
||||
|
||||
|
||||
24
backend/src/db/schemas/project-slack-configs.ts
Normal file
24
backend/src/db/schemas/project-slack-configs.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
// 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 ProjectSlackConfigsSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
projectId: z.string(),
|
||||
slackIntegrationId: z.string().uuid(),
|
||||
isAccessRequestNotificationEnabled: z.boolean().default(false),
|
||||
accessRequestChannels: z.string().default(""),
|
||||
isSecretRequestNotificationEnabled: z.boolean().default(false),
|
||||
secretRequestChannels: z.string().default(""),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
});
|
||||
|
||||
export type TProjectSlackConfigs = z.infer<typeof ProjectSlackConfigsSchema>;
|
||||
export type TProjectSlackConfigsInsert = Omit<z.input<typeof ProjectSlackConfigsSchema>, TImmutableDBKeys>;
|
||||
export type TProjectSlackConfigsUpdate = Partial<Omit<z.input<typeof ProjectSlackConfigsSchema>, TImmutableDBKeys>>;
|
||||
@@ -11,7 +11,9 @@ import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const SlackIntegrationsSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
projectId: z.string(),
|
||||
slug: z.string(),
|
||||
orgId: z.string().uuid(),
|
||||
description: z.string().nullable().optional(),
|
||||
teamId: z.string(),
|
||||
teamName: z.string(),
|
||||
slackUserId: z.string(),
|
||||
@@ -19,10 +21,6 @@ export const SlackIntegrationsSchema = z.object({
|
||||
encryptedBotAccessToken: zodBuffer,
|
||||
slackBotId: z.string(),
|
||||
slackBotUserId: z.string(),
|
||||
isAccessRequestNotificationEnabled: z.boolean().default(false),
|
||||
accessRequestChannels: z.string().default(""),
|
||||
isSecretRequestNotificationEnabled: z.boolean().default(false),
|
||||
secretRequestChannels: z.string().default(""),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
});
|
||||
|
||||
@@ -1453,18 +1453,17 @@ interface GetCertificateTemplateEstConfig {
|
||||
interface AttemptCreateSlackIntegration {
|
||||
type: EventType.ATTEMPT_CREATE_SLACK_INTEGRATION;
|
||||
metadata: {
|
||||
projectId?: string;
|
||||
}; // no metadata
|
||||
slug: string;
|
||||
description?: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface UpdateSlackIntegration {
|
||||
type: EventType.UPDATE_SLACK_INTEGRATION;
|
||||
metadata: {
|
||||
id: string;
|
||||
isAccessRequestNotificationEnabled: boolean;
|
||||
accessRequestChannels: string;
|
||||
isSecretRequestNotificationEnabled: boolean;
|
||||
secretRequestChannels: string;
|
||||
slug: string;
|
||||
description?: string;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -61,7 +61,13 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
|
||||
await server.register(registerUserActionRouter, { prefix: "/user-action" });
|
||||
await server.register(registerSecretImportRouter, { prefix: "/secret-imports" });
|
||||
await server.register(registerSecretFolderRouter, { prefix: "/folders" });
|
||||
await server.register(registerSlackRouter, { prefix: "/slack" });
|
||||
|
||||
await server.register(
|
||||
async (workflowIntegrationRouter) => {
|
||||
await workflowIntegrationRouter.register(registerSlackRouter, { prefix: "/slack" });
|
||||
},
|
||||
{ prefix: "/workflow-integrations" }
|
||||
);
|
||||
|
||||
await server.register(
|
||||
async (projectRouter) => {
|
||||
|
||||
@@ -6,7 +6,6 @@ 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";
|
||||
import { validateSlackChannelsField } from "@app/services/slack/slack-auth-validators";
|
||||
|
||||
export const registerSlackRouter = async (server: FastifyZodProvider) => {
|
||||
const appCfg = getConfig();
|
||||
@@ -24,7 +23,8 @@ export const registerSlackRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
],
|
||||
querystring: z.object({
|
||||
projectId: z.string()
|
||||
slug: z.string(),
|
||||
description: z.string().optional()
|
||||
}),
|
||||
response: {
|
||||
200: z.string()
|
||||
@@ -37,15 +37,18 @@ export const registerSlackRouter = async (server: FastifyZodProvider) => {
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
projectId: req.query.projectId
|
||||
...req.query
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: req.query.projectId,
|
||||
orgId: req.permission.orgId,
|
||||
event: {
|
||||
type: EventType.ATTEMPT_CREATE_SLACK_INTEGRATION,
|
||||
metadata: {}
|
||||
metadata: {
|
||||
slug: req.query.slug,
|
||||
description: req.query.description
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -65,104 +68,25 @@ export const registerSlackRouter = async (server: FastifyZodProvider) => {
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
querystring: z.object({
|
||||
projectId: z.string()
|
||||
}),
|
||||
response: {
|
||||
200: SlackIntegrationsSchema.pick({
|
||||
id: true,
|
||||
teamName: true,
|
||||
isAccessRequestNotificationEnabled: true,
|
||||
accessRequestChannels: true,
|
||||
isSecretRequestNotificationEnabled: true,
|
||||
secretRequestChannels: true
|
||||
})
|
||||
slug: true,
|
||||
description: true,
|
||||
teamName: true
|
||||
}).array()
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const slackIntegration = await server.services.slack.getSlackIntegrationByProjectId({
|
||||
const slackIntegrations = await server.services.slack.getSlackIntegrationsByOrg({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
projectId: req.query.projectId
|
||||
actorOrgId: req.permission.orgId
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: req.query.projectId,
|
||||
event: {
|
||||
type: EventType.GET_SLACK_INTEGRATION,
|
||||
metadata: {
|
||||
id: slackIntegration?.id
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return slackIntegration;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "PATCH",
|
||||
url: "/:slackIntegrationId",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
params: z.object({
|
||||
slackIntegrationId: z.string()
|
||||
}),
|
||||
body: z.object({
|
||||
isAccessRequestNotificationEnabled: z.boolean().optional(),
|
||||
accessRequestChannels: validateSlackChannelsField.optional(),
|
||||
isSecretRequestNotificationEnabled: z.boolean().optional(),
|
||||
secretRequestChannels: validateSlackChannelsField.optional()
|
||||
}),
|
||||
response: {
|
||||
200: SlackIntegrationsSchema.pick({
|
||||
id: true,
|
||||
teamName: true,
|
||||
isAccessRequestNotificationEnabled: true,
|
||||
accessRequestChannels: true,
|
||||
isSecretRequestNotificationEnabled: true,
|
||||
secretRequestChannels: true
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const updatedSlackIntegration = await server.services.slack.updateSlackIntegration({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
id: req.params.slackIntegrationId,
|
||||
...req.body
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: updatedSlackIntegration.projectId,
|
||||
event: {
|
||||
type: EventType.UPDATE_SLACK_INTEGRATION,
|
||||
metadata: {
|
||||
id: updatedSlackIntegration.id,
|
||||
isAccessRequestNotificationEnabled: updatedSlackIntegration.isAccessRequestNotificationEnabled,
|
||||
accessRequestChannels: updatedSlackIntegration.accessRequestChannels,
|
||||
isSecretRequestNotificationEnabled: updatedSlackIntegration.isSecretRequestNotificationEnabled,
|
||||
secretRequestChannels: updatedSlackIntegration.secretRequestChannels
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return updatedSlackIntegration;
|
||||
return slackIntegrations;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -184,11 +108,9 @@ export const registerSlackRouter = async (server: FastifyZodProvider) => {
|
||||
response: {
|
||||
200: SlackIntegrationsSchema.pick({
|
||||
id: true,
|
||||
teamName: true,
|
||||
isAccessRequestNotificationEnabled: true,
|
||||
accessRequestChannels: true,
|
||||
isSecretRequestNotificationEnabled: true,
|
||||
secretRequestChannels: true
|
||||
slug: true,
|
||||
description: true,
|
||||
teamName: true
|
||||
})
|
||||
}
|
||||
},
|
||||
@@ -204,7 +126,7 @@ export const registerSlackRouter = async (server: FastifyZodProvider) => {
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: deletedSlackIntegration.projectId,
|
||||
orgId: deletedSlackIntegration.orgId,
|
||||
event: {
|
||||
type: EventType.DELETE_SLACK_INTEGRATION,
|
||||
metadata: {
|
||||
@@ -217,6 +139,111 @@ export const registerSlackRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:slackIntegrationId",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
params: z.object({
|
||||
slackIntegrationId: z.string()
|
||||
}),
|
||||
response: {
|
||||
200: SlackIntegrationsSchema.pick({
|
||||
id: true,
|
||||
slug: true,
|
||||
description: true,
|
||||
teamName: true
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const slackIntegration = await server.services.slack.getSlackIntegrationById({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
id: req.params.slackIntegrationId
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: slackIntegration.orgId,
|
||||
event: {
|
||||
type: EventType.GET_SLACK_INTEGRATION,
|
||||
metadata: {
|
||||
id: slackIntegration.id
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return slackIntegration;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "PATCH",
|
||||
url: "/:slackIntegrationId",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
params: z.object({
|
||||
slackIntegrationId: 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
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const slackIntegration = await server.services.slack.updateSlackIntegration({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
id: req.params.slackIntegrationId,
|
||||
...req.body
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: slackIntegration.orgId,
|
||||
event: {
|
||||
type: EventType.UPDATE_SLACK_INTEGRATION,
|
||||
metadata: {
|
||||
id: slackIntegration.id,
|
||||
slug: slackIntegration.slug,
|
||||
description: slackIntegration.description as string
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return slackIntegration;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/oauth_redirect",
|
||||
@@ -232,10 +259,10 @@ export const registerSlackRouter = async (server: FastifyZodProvider) => {
|
||||
},
|
||||
successAsync: async (installation) => {
|
||||
const metadata = JSON.parse(installation.metadata || "") as {
|
||||
projectId: string;
|
||||
orgId: string;
|
||||
};
|
||||
|
||||
return res.redirect(`${appCfg.SITE_URL}/project/${metadata.projectId}/settings`);
|
||||
return res.redirect(`${appCfg.SITE_URL}/org/${metadata.orgId}/settings`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,43 +1,40 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import { InstallProvider } from "@slack/oauth";
|
||||
|
||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
|
||||
import { TKmsServiceFactory } from "../kms/kms-service";
|
||||
import { KmsDataKey } from "../kms/kms-types";
|
||||
import { TProjectDALFactory } from "../project/project-dal";
|
||||
import { TSlackIntegrationDALFactory } from "./slack-integration-dal";
|
||||
import {
|
||||
TCompleteSlackIntegrationDTO,
|
||||
TDeleteSlackIntegrationDTO,
|
||||
TGetSlackInstallUrlDTO,
|
||||
TGetSlackIntegrationByProjectIdDTO,
|
||||
TGetSlackIntegrationByIdDTO,
|
||||
TGetSlackIntegrationByOrgDTO,
|
||||
TUpdateSlackIntegrationDTO
|
||||
} from "./slack-types";
|
||||
|
||||
type TSlackServiceFactoryDep = {
|
||||
slackIntegrationDAL: Pick<
|
||||
TSlackIntegrationDALFactory,
|
||||
"create" | "findOne" | "findById" | "updateById" | "deleteById" | "transaction"
|
||||
>;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findById">;
|
||||
slackIntegrationDAL: Pick<TSlackIntegrationDALFactory, "find" | "findById" | "deleteById" | "updateById" | "create">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getOrgPermission">;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
};
|
||||
|
||||
export type TSlackServiceFactory = ReturnType<typeof slackServiceFactory>;
|
||||
|
||||
export const slackServiceFactory = ({
|
||||
projectDAL,
|
||||
permissionService,
|
||||
slackIntegrationDAL,
|
||||
kmsService
|
||||
}: TSlackServiceFactoryDep) => {
|
||||
const completeSlackIntegration = async ({
|
||||
projectId,
|
||||
orgId,
|
||||
slug,
|
||||
description,
|
||||
teamId,
|
||||
teamName,
|
||||
slackUserId,
|
||||
@@ -46,59 +43,26 @@ export const slackServiceFactory = ({
|
||||
slackBotId,
|
||||
slackBotUserId
|
||||
}: TCompleteSlackIntegrationDTO) => {
|
||||
const project = await projectDAL.findById(projectId);
|
||||
if (!project) {
|
||||
throw new NotFoundError({
|
||||
message: "Project not found"
|
||||
});
|
||||
}
|
||||
|
||||
const { encryptor: orgDataKeyEncryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.Organization,
|
||||
orgId: project.orgId
|
||||
orgId,
|
||||
type: KmsDataKey.Organization
|
||||
});
|
||||
|
||||
const { cipherTextBlob: encryptedBotAccessToken } = orgDataKeyEncryptor({
|
||||
plainText: Buffer.from(botAccessToken, "utf8")
|
||||
});
|
||||
|
||||
await slackIntegrationDAL.transaction(async (tx) => {
|
||||
const slackIntegration = await slackIntegrationDAL.findOne(
|
||||
{
|
||||
projectId
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
if (slackIntegration) {
|
||||
return slackIntegrationDAL.updateById(
|
||||
slackIntegration.id,
|
||||
{
|
||||
teamId,
|
||||
teamName,
|
||||
slackUserId,
|
||||
slackAppId,
|
||||
slackBotId,
|
||||
slackBotUserId,
|
||||
encryptedBotAccessToken
|
||||
},
|
||||
tx
|
||||
);
|
||||
}
|
||||
|
||||
return slackIntegrationDAL.create(
|
||||
{
|
||||
projectId,
|
||||
teamId,
|
||||
teamName,
|
||||
slackUserId,
|
||||
slackAppId,
|
||||
slackBotId,
|
||||
slackBotUserId,
|
||||
encryptedBotAccessToken
|
||||
},
|
||||
tx
|
||||
);
|
||||
await slackIntegrationDAL.create({
|
||||
orgId,
|
||||
slug,
|
||||
description,
|
||||
teamId,
|
||||
teamName,
|
||||
slackUserId,
|
||||
slackAppId,
|
||||
slackBotId,
|
||||
slackBotUserId,
|
||||
encryptedBotAccessToken
|
||||
});
|
||||
};
|
||||
|
||||
@@ -125,11 +89,15 @@ export const slackServiceFactory = ({
|
||||
}
|
||||
|
||||
const metadata = JSON.parse(installation.metadata || "") as {
|
||||
projectId: string;
|
||||
orgId: string;
|
||||
slug: string;
|
||||
description?: string;
|
||||
};
|
||||
|
||||
return completeSlackIntegration({
|
||||
projectId: metadata.projectId,
|
||||
orgId: metadata.orgId,
|
||||
slug: metadata.slug,
|
||||
description: metadata.description,
|
||||
teamId: installation.team?.id || "",
|
||||
teamName: installation.team?.name || "",
|
||||
slackUserId: installation.user.id,
|
||||
@@ -152,56 +120,87 @@ export const slackServiceFactory = ({
|
||||
});
|
||||
};
|
||||
|
||||
const getInstallUrl = async ({ actorId, actor, actorOrgId, actorAuthMethod, projectId }: TGetSlackInstallUrlDTO) => {
|
||||
const getInstallUrl = async ({
|
||||
actorId,
|
||||
actor,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
slug,
|
||||
description
|
||||
}: TGetSlackInstallUrlDTO) => {
|
||||
const appCfg = getConfig();
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Settings);
|
||||
const project = await projectDAL.findById(projectId);
|
||||
if (!project) {
|
||||
throw new NotFoundError({
|
||||
message: "Project not found"
|
||||
});
|
||||
}
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Settings);
|
||||
|
||||
const installer = await getSlackInstaller();
|
||||
const url = await installer.generateInstallUrl({
|
||||
scopes: ["chat:write.public", "chat:write", "channels:read", "groups:read", "im:read", "mpim:read"],
|
||||
metadata: JSON.stringify({
|
||||
projectId: project.id
|
||||
slug,
|
||||
description,
|
||||
orgId: actorOrgId
|
||||
}),
|
||||
redirectUri: `${appCfg.SITE_URL}/api/v1/slack/oauth_redirect`
|
||||
redirectUri: `${appCfg.SITE_URL}/api/v1/workflow-integrations/slack/oauth_redirect`
|
||||
});
|
||||
|
||||
return url;
|
||||
};
|
||||
|
||||
const getSlackIntegrationByProjectId = async ({
|
||||
const getSlackIntegrationsByOrg = async ({
|
||||
actorId,
|
||||
actor,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
projectId
|
||||
}: TGetSlackIntegrationByProjectIdDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actorAuthMethod
|
||||
}: TGetSlackIntegrationByOrgDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Settings);
|
||||
const slackIntegration = await slackIntegrationDAL.findOne({
|
||||
projectId
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Settings);
|
||||
|
||||
const slackIntegrations = await slackIntegrationDAL.find({
|
||||
orgId: actorOrgId
|
||||
});
|
||||
|
||||
return slackIntegrations;
|
||||
};
|
||||
|
||||
const getSlackIntegrationById = async ({
|
||||
actorId,
|
||||
actor,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
id
|
||||
}: TGetSlackIntegrationByIdDTO) => {
|
||||
const slackIntegration = await slackIntegrationDAL.findById(id);
|
||||
if (!slackIntegration) {
|
||||
throw new NotFoundError({
|
||||
message: "Slack integration not found."
|
||||
});
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
slackIntegration.orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Settings);
|
||||
|
||||
return slackIntegration;
|
||||
};
|
||||
|
||||
@@ -211,10 +210,8 @@ export const slackServiceFactory = ({
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
id,
|
||||
isAccessRequestNotificationEnabled,
|
||||
accessRequestChannels,
|
||||
isSecretRequestNotificationEnabled,
|
||||
secretRequestChannels
|
||||
slug,
|
||||
description
|
||||
}: TUpdateSlackIntegrationDTO) => {
|
||||
const slackIntegration = await slackIntegrationDAL.findById(id);
|
||||
if (!slackIntegration) {
|
||||
@@ -222,21 +219,20 @@ export const slackServiceFactory = ({
|
||||
message: "Slack integration not found"
|
||||
});
|
||||
}
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
slackIntegration.projectId,
|
||||
slackIntegration.orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Settings);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings);
|
||||
|
||||
return slackIntegrationDAL.updateById(slackIntegration.id, {
|
||||
isAccessRequestNotificationEnabled,
|
||||
accessRequestChannels,
|
||||
isSecretRequestNotificationEnabled,
|
||||
secretRequestChannels
|
||||
slug,
|
||||
description
|
||||
});
|
||||
};
|
||||
|
||||
@@ -253,22 +249,24 @@ export const slackServiceFactory = ({
|
||||
message: "Slack integration not found"
|
||||
});
|
||||
}
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
slackIntegration.projectId,
|
||||
slackIntegration.orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Settings);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Settings);
|
||||
|
||||
return slackIntegrationDAL.deleteById(id);
|
||||
};
|
||||
|
||||
return {
|
||||
getInstallUrl,
|
||||
getSlackIntegrationByProjectId,
|
||||
getSlackIntegrationsByOrg,
|
||||
getSlackIntegrationById,
|
||||
completeSlackIntegration,
|
||||
getSlackInstaller,
|
||||
updateSlackIntegration,
|
||||
|
||||
@@ -1,23 +1,27 @@
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
import { TOrgPermission } from "@app/lib/types";
|
||||
|
||||
export type TGetSlackInstallUrlDTO = TProjectPermission;
|
||||
export type TGetSlackInstallUrlDTO = {
|
||||
slug: string;
|
||||
description?: string;
|
||||
} & Omit<TOrgPermission, "orgId">;
|
||||
|
||||
export type TGetSlackIntegrationByProjectIdDTO = TProjectPermission;
|
||||
export type TGetSlackIntegrationByOrgDTO = Omit<TOrgPermission, "orgId">;
|
||||
|
||||
export type TUpdateSlackIntegrationDTO = {
|
||||
id: string;
|
||||
isAccessRequestNotificationEnabled?: boolean;
|
||||
accessRequestChannels?: string;
|
||||
isSecretRequestNotificationEnabled?: boolean;
|
||||
secretRequestChannels?: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
export type TGetSlackIntegrationByIdDTO = { id: string } & Omit<TOrgPermission, "orgId">;
|
||||
|
||||
export type TUpdateSlackIntegrationDTO = { id: string; slug?: string; description?: string } & Omit<
|
||||
TOrgPermission,
|
||||
"orgId"
|
||||
>;
|
||||
|
||||
export type TDeleteSlackIntegrationDTO = {
|
||||
id: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
} & Omit<TOrgPermission, "orgId">;
|
||||
|
||||
export type TCompleteSlackIntegrationDTO = {
|
||||
projectId: string;
|
||||
orgId: string;
|
||||
slug: string;
|
||||
description?: string;
|
||||
teamId: string;
|
||||
teamName: string;
|
||||
slackUserId: string;
|
||||
|
||||
@@ -45,4 +45,5 @@ export * from "./tags";
|
||||
export * from "./trustedIps";
|
||||
export * from "./users";
|
||||
export * from "./webhooks";
|
||||
export * from "./workflowIntegrations";
|
||||
export * from "./workspace";
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
export { useDeleteSlackIntegration, useUpdateSlackIntegration } from "./mutation";
|
||||
export { fetchSlackInstallUrl, useGetSlackIntegrationByProject } from "./queries";
|
||||
export { useGetSlackIntegrationByProject } from "./queries";
|
||||
|
||||
@@ -11,16 +11,6 @@ export const slackKeys = {
|
||||
]
|
||||
};
|
||||
|
||||
export const fetchSlackInstallUrl = async (workspaceId?: string) => {
|
||||
const { data } = await apiRequest.get<string>("/api/v1/slack/install", {
|
||||
params: {
|
||||
projectId: workspaceId
|
||||
}
|
||||
});
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
export const fetchSlackIntegrationByProject = async (workspaceId?: string) => {
|
||||
const { data } = await apiRequest.get<ProjectSlackIntegration>("/api/v1/slack", {
|
||||
params: {
|
||||
|
||||
6
frontend/src/hooks/api/workflowIntegrations/index.ts
Normal file
6
frontend/src/hooks/api/workflowIntegrations/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export { useDeleteSlackIntegration, useUpdateSlackIntegration } from "./mutation";
|
||||
export {
|
||||
fetchSlackInstallUrl,
|
||||
useGetSlackIntegrationById,
|
||||
useGetSlackIntegrations
|
||||
} from "./queries";
|
||||
38
frontend/src/hooks/api/workflowIntegrations/mutation.tsx
Normal file
38
frontend/src/hooks/api/workflowIntegrations/mutation.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { workflowIntegrationKeys } from "./queries";
|
||||
import { TDeleteSlackIntegrationDTO, TUpdateSlackIntegrationDTO } from "./types";
|
||||
|
||||
export const useUpdateSlackIntegration = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<{}, {}, TUpdateSlackIntegrationDTO>({
|
||||
mutationFn: async (dto) => {
|
||||
const { data } = await apiRequest.patch(`/api/v1/workflow-integrations/slack/${dto.id}`, dto);
|
||||
|
||||
return data;
|
||||
},
|
||||
onSuccess: (_, { orgId, id }) => {
|
||||
queryClient.invalidateQueries(workflowIntegrationKeys.getSlackWorkflowIntegration(id));
|
||||
queryClient.invalidateQueries(workflowIntegrationKeys.getSlackWorkflowIntegrations(orgId));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeleteSlackIntegration = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<{}, {}, TDeleteSlackIntegrationDTO>({
|
||||
mutationFn: async (dto) => {
|
||||
const { data } = await apiRequest.delete(`/api/v1/workflow-integrations/slack/${dto.id}`);
|
||||
|
||||
return data;
|
||||
},
|
||||
onSuccess: (_, { orgId, id }) => {
|
||||
queryClient.invalidateQueries(workflowIntegrationKeys.getSlackWorkflowIntegration(id));
|
||||
queryClient.invalidateQueries(workflowIntegrationKeys.getSlackWorkflowIntegrations(orgId));
|
||||
}
|
||||
});
|
||||
};
|
||||
55
frontend/src/hooks/api/workflowIntegrations/queries.tsx
Normal file
55
frontend/src/hooks/api/workflowIntegrations/queries.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { SlackIntegration } from "./types";
|
||||
|
||||
export const workflowIntegrationKeys = {
|
||||
getSlackWorkflowIntegrations: (orgId?: string) => [{ orgId }, "slack-workflow-integrations"],
|
||||
getSlackWorkflowIntegration: (id?: string) => [{ id }, "slack-workflow-integration"]
|
||||
};
|
||||
|
||||
export const fetchSlackInstallUrl = async ({
|
||||
slug,
|
||||
description
|
||||
}: {
|
||||
slug: string;
|
||||
description?: string;
|
||||
}) => {
|
||||
const { data } = await apiRequest.get<string>("/api/v1/workflow-integrations/slack/install", {
|
||||
params: {
|
||||
slug,
|
||||
description
|
||||
}
|
||||
});
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
export const fetchSlackIntegrations = async () => {
|
||||
const { data } = await apiRequest.get<SlackIntegration[]>("/api/v1/workflow-integrations/slack");
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
export const fetchSlackIntegrationById = async (id?: string) => {
|
||||
const { data } = await apiRequest.get<SlackIntegration>(
|
||||
`/api/v1/workflow-integrations/slack/${id}`
|
||||
);
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
export const useGetSlackIntegrations = (orgId?: string) =>
|
||||
useQuery({
|
||||
queryKey: workflowIntegrationKeys.getSlackWorkflowIntegrations(orgId),
|
||||
queryFn: () => fetchSlackIntegrations(),
|
||||
enabled: Boolean(orgId)
|
||||
});
|
||||
|
||||
export const useGetSlackIntegrationById = (id?: string) =>
|
||||
useQuery({
|
||||
queryKey: workflowIntegrationKeys.getSlackWorkflowIntegration(id),
|
||||
queryFn: () => fetchSlackIntegrationById(id),
|
||||
enabled: Boolean(id)
|
||||
});
|
||||
22
frontend/src/hooks/api/workflowIntegrations/types.ts
Normal file
22
frontend/src/hooks/api/workflowIntegrations/types.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
export enum WorkflowIntegrationPlatform {
|
||||
SLACK = "slack"
|
||||
}
|
||||
|
||||
export type SlackIntegration = {
|
||||
id: string;
|
||||
slug: string;
|
||||
description: string;
|
||||
teamName: string;
|
||||
};
|
||||
|
||||
export type TUpdateSlackIntegrationDTO = {
|
||||
id: string;
|
||||
orgId: string;
|
||||
slug?: string;
|
||||
description?: string;
|
||||
};
|
||||
|
||||
export type TDeleteSlackIntegrationDTO = {
|
||||
id: string;
|
||||
orgId: string;
|
||||
};
|
||||
@@ -5,11 +5,13 @@ import { AuditLogStreamsTab } from "../AuditLogStreamTab";
|
||||
import { OrgAuthTab } from "../OrgAuthTab";
|
||||
import { OrgEncryptionTab } from "../OrgEncryptionTab";
|
||||
import { OrgGeneralTab } from "../OrgGeneralTab";
|
||||
import { OrgWorkflowIntegrationTab } from "../OrgWorkflowIntegrationTab/OrgWorkflowIntegrationTab";
|
||||
|
||||
const tabs = [
|
||||
{ name: "General", key: "tab-org-general" },
|
||||
{ name: "Security", key: "tab-org-security" },
|
||||
{ name: "Encryption", key: "tab-org-encryption" },
|
||||
{ name: "Workflow Integrations", key: "workflow-integrations" },
|
||||
{ name: "Audit Log Streams", key: "tag-audit-log-streams" }
|
||||
];
|
||||
export const OrgTabGroup = () => {
|
||||
@@ -41,6 +43,9 @@ export const OrgTabGroup = () => {
|
||||
<Tab.Panel>
|
||||
<OrgEncryptionTab />
|
||||
</Tab.Panel>
|
||||
<Tab.Panel>
|
||||
<OrgWorkflowIntegrationTab />
|
||||
</Tab.Panel>
|
||||
<Tab.Panel>
|
||||
<AuditLogStreamsTab />
|
||||
</Tab.Panel>
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
import { useState } from "react";
|
||||
import { faSlack } from "@fortawesome/free-brands-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
|
||||
import { Modal, ModalContent } from "@app/components/v2";
|
||||
import { WorkflowIntegrationPlatform } from "@app/hooks/api/workflowIntegrations/types";
|
||||
|
||||
import { SlackIntegrationForm } from "./SlackIntegrationForm";
|
||||
|
||||
type Props = {
|
||||
isOpen?: boolean;
|
||||
onToggle: (isOpen: boolean) => void;
|
||||
};
|
||||
|
||||
enum WizardSteps {
|
||||
SelectPlatform = "select-platform",
|
||||
PlatformInputs = "platform-inputs"
|
||||
}
|
||||
|
||||
const PLATFORM_LIST = [
|
||||
{
|
||||
icon: faSlack,
|
||||
platform: WorkflowIntegrationPlatform.SLACK,
|
||||
title: "Slack"
|
||||
}
|
||||
];
|
||||
|
||||
export const AddWorkflowIntegrationForm = ({ isOpen, onToggle }: Props) => {
|
||||
const [wizardStep, setWizardStep] = useState(WizardSteps.SelectPlatform);
|
||||
const [selectedPlatform, setSelectedPlatform] = useState<string | null>(null);
|
||||
|
||||
const handleFormReset = (state: boolean = false) => {
|
||||
onToggle(state);
|
||||
setWizardStep(WizardSteps.SelectPlatform);
|
||||
setSelectedPlatform(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onOpenChange={(state) => handleFormReset(state)}>
|
||||
<ModalContent
|
||||
title="Add a workflow integration"
|
||||
subTitle="Configure a workflow integration"
|
||||
className="my-4"
|
||||
>
|
||||
<AnimatePresence exitBeforeEnter>
|
||||
{wizardStep === WizardSteps.SelectPlatform && (
|
||||
<motion.div
|
||||
key="select-type-step"
|
||||
transition={{ duration: 0.1 }}
|
||||
initial={{ opacity: 0, translateX: 30 }}
|
||||
animate={{ opacity: 1, translateX: 0 }}
|
||||
exit={{ opacity: 0, translateX: -30 }}
|
||||
>
|
||||
<div className="mb-4 text-mineshaft-300">Select a platform</div>
|
||||
<div className="flex items-center space-x-4">
|
||||
{PLATFORM_LIST.map(({ icon, platform, title }) => (
|
||||
<div
|
||||
key={platform}
|
||||
className="flex h-28 w-32 cursor-pointer flex-col items-center space-y-4 rounded border border-mineshaft-500 bg-bunker-600 p-6 transition-all hover:border-primary/70 hover:bg-primary/10 hover:text-white"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => {
|
||||
setSelectedPlatform(platform);
|
||||
setWizardStep(WizardSteps.PlatformInputs);
|
||||
}}
|
||||
onKeyDown={(evt) => {
|
||||
if (evt.key === "Enter") {
|
||||
setSelectedPlatform(platform);
|
||||
setWizardStep(WizardSteps.PlatformInputs);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={icon} size="lg" />
|
||||
<div className="whitespace-pre-wrap text-center text-sm">{title}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
{wizardStep === WizardSteps.PlatformInputs &&
|
||||
selectedPlatform === WorkflowIntegrationPlatform.SLACK && (
|
||||
<motion.div
|
||||
key="slack-platform"
|
||||
transition={{ duration: 0.1 }}
|
||||
initial={{ opacity: 0, translateX: 30 }}
|
||||
animate={{ opacity: 1, translateX: 0 }}
|
||||
exit={{ opacity: 0, translateX: -30 }}
|
||||
>
|
||||
<SlackIntegrationForm onClose={() => onToggle(false)} />
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Modal, ModalContent } from "@app/components/v2";
|
||||
import { WorkflowIntegrationPlatform } from "@app/hooks/api/workflowIntegrations/types";
|
||||
|
||||
import { SlackIntegrationForm } from "./SlackIntegrationForm";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
id: string;
|
||||
workflowPlatform: WorkflowIntegrationPlatform;
|
||||
onOpenChange: (state: boolean) => void;
|
||||
};
|
||||
|
||||
export const IntegrationFormDetails = ({ isOpen, id, onOpenChange, workflowPlatform }: Props) => {
|
||||
return (
|
||||
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
||||
<ModalContent title="Workflow integration details">
|
||||
{workflowPlatform === WorkflowIntegrationPlatform.SLACK && (
|
||||
<SlackIntegrationForm id={id} onClose={() => onOpenChange(false)} />
|
||||
)}
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,188 @@
|
||||
import { faSlack } from "@fortawesome/free-brands-svg-icons";
|
||||
import { faEllipsis, faGear, faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
Button,
|
||||
DeleteActionModal,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
EmptyState,
|
||||
Table,
|
||||
TableContainer,
|
||||
TableSkeleton,
|
||||
TBody,
|
||||
Td,
|
||||
THead,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
|
||||
import { withPermission } from "@app/hoc";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { useDeleteSlackIntegration, useGetSlackIntegrations } from "@app/hooks/api";
|
||||
import { WorkflowIntegrationPlatform } from "@app/hooks/api/workflowIntegrations/types";
|
||||
|
||||
import { AddWorkflowIntegrationForm } from "./AddWorkflowIntegrationForm";
|
||||
import { IntegrationFormDetails } from "./IntegrationFormDetails";
|
||||
|
||||
export const OrgWorkflowIntegrationTab = withPermission(
|
||||
() => {
|
||||
const { popUp, handlePopUpOpen, handlePopUpToggle, handlePopUpClose } = usePopUp([
|
||||
"addWorkflowIntegration",
|
||||
"integrationDetails",
|
||||
"removeIntegration"
|
||||
] as const);
|
||||
|
||||
const { currentOrg } = useOrganization();
|
||||
const { data: slackIntegrations, isLoading: isSlackIntegrationsLoading } =
|
||||
useGetSlackIntegrations(currentOrg?.id);
|
||||
const { mutateAsync: deleteSlackIntegration } = useDeleteSlackIntegration();
|
||||
|
||||
const handleRemoveIntegration = async () => {
|
||||
if (!currentOrg) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { platform, id } = popUp.removeIntegration.data;
|
||||
if (platform === WorkflowIntegrationPlatform.SLACK) {
|
||||
await deleteSlackIntegration({
|
||||
id,
|
||||
orgId: currentOrg?.id
|
||||
});
|
||||
}
|
||||
|
||||
handlePopUpClose("removeIntegration");
|
||||
createNotification({
|
||||
text: "Successfully deleted integration",
|
||||
type: "success"
|
||||
});
|
||||
};
|
||||
|
||||
const isIntegrationsLoading = isSlackIntegrationsLoading;
|
||||
|
||||
return (
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="flex justify-between">
|
||||
<p className="text-xl font-semibold text-mineshaft-100">Workflow Integrations</p>
|
||||
<OrgPermissionCan I={OrgPermissionActions.Create} an={OrgPermissionSubjects.Settings}>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
onClick={() => {
|
||||
handlePopUpOpen("addWorkflowIntegration");
|
||||
}}
|
||||
isDisabled={!isAllowed}
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</div>
|
||||
<p className="mb-4 text-gray-400">
|
||||
Connect Infisical to other platforms for notification and workflow integrations.
|
||||
</p>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Td>Provider</Td>
|
||||
<Td>Alias</Td>
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{isIntegrationsLoading && (
|
||||
<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}>
|
||||
<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>
|
||||
</Td>
|
||||
<Td>{slackIntegration.slug}</Td>
|
||||
<Td>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild className="rounded-lg">
|
||||
<div className="flex justify-end hover:text-primary-400 data-[state=open]:text-primary-400">
|
||||
<FontAwesomeIcon size="sm" icon={faEllipsis} />
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="p-1">
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
|
||||
handlePopUpOpen("integrationDetails", {
|
||||
id: slackIntegration.id,
|
||||
platform: WorkflowIntegrationPlatform.SLACK
|
||||
});
|
||||
}}
|
||||
>
|
||||
More details
|
||||
</DropdownMenuItem>
|
||||
<OrgPermissionCan
|
||||
I={OrgPermissionActions.Delete}
|
||||
an={OrgPermissionSubjects.Settings}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
disabled={!isAllowed}
|
||||
className={twMerge(
|
||||
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
|
||||
handlePopUpOpen("removeIntegration", {
|
||||
id: slackIntegration.id,
|
||||
slug: slackIntegration.slug,
|
||||
platform: WorkflowIntegrationPlatform.SLACK
|
||||
});
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</TBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
<AddWorkflowIntegrationForm
|
||||
isOpen={popUp.addWorkflowIntegration.isOpen}
|
||||
onToggle={(state) => handlePopUpToggle("addWorkflowIntegration", state)}
|
||||
/>
|
||||
<IntegrationFormDetails
|
||||
isOpen={popUp.integrationDetails?.isOpen}
|
||||
workflowPlatform={popUp.integrationDetails?.data?.platform}
|
||||
id={popUp.integrationDetails?.data?.id}
|
||||
onOpenChange={(state) => handlePopUpToggle("integrationDetails", state)}
|
||||
/>
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.removeIntegration.isOpen}
|
||||
title={`Are you sure want to remove ${popUp?.removeIntegration?.data?.slug}?`}
|
||||
onChange={(isOpen) => handlePopUpToggle("removeIntegration", isOpen)}
|
||||
deleteKey="confirm"
|
||||
onDeleteApproved={handleRemoveIntegration}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
{ action: OrgPermissionActions.Read, subject: OrgPermissionSubjects.Settings }
|
||||
);
|
||||
@@ -0,0 +1,121 @@
|
||||
import { useEffect } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { useRouter } from "next/router";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Button, FormControl, Input } from "@app/components/v2";
|
||||
import { useOrganization } from "@app/context";
|
||||
import { useToggle } from "@app/hooks";
|
||||
import {
|
||||
fetchSlackInstallUrl,
|
||||
useGetSlackIntegrationById,
|
||||
useUpdateSlackIntegration
|
||||
} from "@app/hooks/api";
|
||||
|
||||
type Props = {
|
||||
id?: string;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
const slackFormSchema = z.object({
|
||||
slug: z.string(),
|
||||
description: z.string().optional()
|
||||
});
|
||||
|
||||
type TSlackFormData = z.infer<typeof slackFormSchema>;
|
||||
|
||||
export const SlackIntegrationForm = ({ id, onClose }: Props) => {
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
setValue,
|
||||
formState: { isSubmitting, isDirty }
|
||||
} = useForm<TSlackFormData>({
|
||||
resolver: zodResolver(slackFormSchema)
|
||||
});
|
||||
|
||||
const router = useRouter();
|
||||
const [isConnectLoading, setIsConnectLoading] = useToggle(false);
|
||||
const { currentOrg } = useOrganization();
|
||||
const { data: slackIntegration } = useGetSlackIntegrationById(id);
|
||||
const { mutateAsync: updateSlackIntegration } = useUpdateSlackIntegration();
|
||||
|
||||
useEffect(() => {
|
||||
if (slackIntegration) {
|
||||
setValue("slug", slackIntegration.slug);
|
||||
setValue("description", slackIntegration.description ?? "");
|
||||
}
|
||||
}, [slackIntegration]);
|
||||
|
||||
const triggerSlackInstall = async (slug: string, description?: string) => {
|
||||
setIsConnectLoading.on();
|
||||
const slackInstallUrl = await fetchSlackInstallUrl({
|
||||
slug,
|
||||
description
|
||||
});
|
||||
if (slackInstallUrl) {
|
||||
router.push(slackInstallUrl);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSlackFormSubmit = async ({ slug, description }: TSlackFormData) => {
|
||||
if (id && slackIntegration) {
|
||||
if (!currentOrg) {
|
||||
return;
|
||||
}
|
||||
|
||||
await updateSlackIntegration({
|
||||
id,
|
||||
orgId: currentOrg?.id,
|
||||
slug,
|
||||
description
|
||||
});
|
||||
|
||||
onClose();
|
||||
createNotification({
|
||||
text: "Successfully updated Slack integration",
|
||||
type: "success"
|
||||
});
|
||||
} else {
|
||||
await triggerSlackInstall(slug, description);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(handleSlackFormSubmit)} autoComplete="off">
|
||||
<Controller
|
||||
control={control}
|
||||
name="slug"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl label="Alias" errorText={error?.message} isError={Boolean(error)}>
|
||||
<Input placeholder="" {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="description"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl label="Description" errorText={error?.message} isError={Boolean(error)}>
|
||||
<Input placeholder="" {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{slackIntegration && (
|
||||
<FormControl label="Slack workspace">
|
||||
<Input value={slackIntegration?.teamName} isReadOnly className="bg-white/[0.07]" />
|
||||
</FormControl>
|
||||
)}
|
||||
<div className="mt-6 flex items-center space-x-4">
|
||||
<Button type="submit" isLoading={isSubmitting || isConnectLoading} isDisabled={!isDirty}>
|
||||
{slackIntegration ? "Save" : "Connect"}
|
||||
</Button>
|
||||
<Button variant="outline_bg" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user