mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 18:27:36 +00:00
Merge pull request #4756 from Infisical/feature/slack-secret-sync-error-notification
Feature: slack secret sync error notification
This commit is contained in:
+31
@@ -0,0 +1,31 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
if (!(await knex.schema.hasColumn(TableName.ProjectSlackConfigs, "isSecretSyncErrorNotificationEnabled"))) {
|
||||||
|
await knex.schema.alterTable(TableName.ProjectSlackConfigs, (table) => {
|
||||||
|
table.boolean("isSecretSyncErrorNotificationEnabled").notNullable().defaultTo(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(await knex.schema.hasColumn(TableName.ProjectSlackConfigs, "secretSyncErrorChannels"))) {
|
||||||
|
await knex.schema.alterTable(TableName.ProjectSlackConfigs, (table) => {
|
||||||
|
table.text("secretSyncErrorChannels").notNullable().defaultTo("");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
if (await knex.schema.hasColumn(TableName.ProjectSlackConfigs, "isSecretSyncErrorNotificationEnabled")) {
|
||||||
|
await knex.schema.alterTable(TableName.ProjectSlackConfigs, (table) => {
|
||||||
|
table.dropColumn("isSecretSyncErrorNotificationEnabled");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await knex.schema.hasColumn(TableName.ProjectSlackConfigs, "secretSyncErrorChannels")) {
|
||||||
|
await knex.schema.alterTable(TableName.ProjectSlackConfigs, (table) => {
|
||||||
|
table.dropColumn("secretSyncErrorChannels");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,7 +16,9 @@ export const ProjectSlackConfigsSchema = z.object({
|
|||||||
isSecretRequestNotificationEnabled: z.boolean().default(false),
|
isSecretRequestNotificationEnabled: z.boolean().default(false),
|
||||||
secretRequestChannels: z.string().default(""),
|
secretRequestChannels: z.string().default(""),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date()
|
updatedAt: z.date(),
|
||||||
|
isSecretSyncErrorNotificationEnabled: z.boolean().default(false),
|
||||||
|
secretSyncErrorChannels: z.string().default("")
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TProjectSlackConfigs = z.infer<typeof ProjectSlackConfigsSchema>;
|
export type TProjectSlackConfigs = z.infer<typeof ProjectSlackConfigsSchema>;
|
||||||
|
|||||||
@@ -243,7 +243,8 @@ export const accessApprovalRequestServiceFactory = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const requesterFullName = `${requestedByUser.firstName} ${requestedByUser.lastName}`;
|
const requesterFullName = `${requestedByUser.firstName} ${requestedByUser.lastName}`;
|
||||||
const approvalPath = `/projects/secret-management/${project.id}/approval`;
|
const projectPath = `/projects/secret-management/${project.id}`;
|
||||||
|
const approvalPath = `${projectPath}/approval`;
|
||||||
const approvalUrl = `${cfg.SITE_URL}${approvalPath}`;
|
const approvalUrl = `${cfg.SITE_URL}${approvalPath}`;
|
||||||
|
|
||||||
await triggerWorkflowIntegrationNotification({
|
await triggerWorkflowIntegrationNotification({
|
||||||
@@ -252,6 +253,7 @@ export const accessApprovalRequestServiceFactory = ({
|
|||||||
type: TriggerFeature.ACCESS_REQUEST,
|
type: TriggerFeature.ACCESS_REQUEST,
|
||||||
payload: {
|
payload: {
|
||||||
projectName: project.name,
|
projectName: project.name,
|
||||||
|
projectPath,
|
||||||
requesterFullName,
|
requesterFullName,
|
||||||
isTemporary,
|
isTemporary,
|
||||||
requesterEmail: requestedByUser.email as string,
|
requesterEmail: requestedByUser.email as string,
|
||||||
@@ -397,7 +399,8 @@ export const accessApprovalRequestServiceFactory = ({
|
|||||||
|
|
||||||
const requesterFullName = `${requestedByUser.firstName} ${requestedByUser.lastName}`;
|
const requesterFullName = `${requestedByUser.firstName} ${requestedByUser.lastName}`;
|
||||||
const editorFullName = `${editedByUser.firstName} ${editedByUser.lastName}`;
|
const editorFullName = `${editedByUser.firstName} ${editedByUser.lastName}`;
|
||||||
const approvalPath = `/projects/secret-management/${project.id}/approval`;
|
const projectPath = `/projects/secret-management/${project.id}`;
|
||||||
|
const approvalPath = `${projectPath}/approval`;
|
||||||
const approvalUrl = `${cfg.SITE_URL}${approvalPath}`;
|
const approvalUrl = `${cfg.SITE_URL}${approvalPath}`;
|
||||||
|
|
||||||
await triggerWorkflowIntegrationNotification({
|
await triggerWorkflowIntegrationNotification({
|
||||||
@@ -415,7 +418,8 @@ export const accessApprovalRequestServiceFactory = ({
|
|||||||
approvalUrl,
|
approvalUrl,
|
||||||
editNote,
|
editNote,
|
||||||
editorEmail: editedByUser.email as string,
|
editorEmail: editedByUser.email as string,
|
||||||
editorFullName
|
editorFullName,
|
||||||
|
projectPath
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
projectId: project.id
|
projectId: project.id
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export const sendApprovalEmailsFn = async ({
|
|||||||
type: NotificationType.SECRET_CHANGE_REQUEST,
|
type: NotificationType.SECRET_CHANGE_REQUEST,
|
||||||
title: "Secret Change Request",
|
title: "Secret Change Request",
|
||||||
body: `You have a new secret change request pending your review for the project **${project.name}** in the organization **${project.organization.name}**.`,
|
body: `You have a new secret change request pending your review for the project **${project.name}** in the organization **${project.organization.name}**.`,
|
||||||
link: `/projects/secret-management/${project.id}/approval?requestId=${secretApprovalRequest.id}`
|
link: `/projects/secret-management/${project.id}/approval`
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -51,7 +51,7 @@ export const sendApprovalEmailsFn = async ({
|
|||||||
firstName: reviewerUser.firstName,
|
firstName: reviewerUser.firstName,
|
||||||
projectName: project.name,
|
projectName: project.name,
|
||||||
organizationName: project.organization.name,
|
organizationName: project.organization.name,
|
||||||
approvalUrl: `${cfg.SITE_URL}/projects/secret-management/${project.id}/approval?requestId=${secretApprovalRequest.id}`
|
approvalUrl: `${cfg.SITE_URL}/projects/secret-management/${project.id}/approval}`
|
||||||
},
|
},
|
||||||
template: SmtpTemplates.SecretApprovalRequestNeedsReview
|
template: SmtpTemplates.SecretApprovalRequestNeedsReview
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1416,6 +1416,11 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
const env = await projectEnvDAL.findOne({ id: policy.envId });
|
const env = await projectEnvDAL.findOne({ id: policy.envId });
|
||||||
const user = await userDAL.findById(actorId);
|
const user = await userDAL.findById(actorId);
|
||||||
|
|
||||||
|
const projectPath = `/projects/secret-management/${projectId}`;
|
||||||
|
const approvalPath = `${projectPath}/approval`;
|
||||||
|
const cfg = getConfig();
|
||||||
|
const approvalUrl = `${cfg.SITE_URL}${approvalPath}`;
|
||||||
|
|
||||||
await triggerWorkflowIntegrationNotification({
|
await triggerWorkflowIntegrationNotification({
|
||||||
input: {
|
input: {
|
||||||
projectId,
|
projectId,
|
||||||
@@ -1427,7 +1432,8 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
secretPath,
|
secretPath,
|
||||||
projectId,
|
projectId,
|
||||||
requestId: secretApprovalRequest.id,
|
requestId: secretApprovalRequest.id,
|
||||||
secretKeys: [...new Set(Object.values(data).flatMap((arr) => arr?.map((item) => item.secretName) ?? []))]
|
secretKeys: [...new Set(Object.values(data).flatMap((arr) => arr?.map((item) => item.secretName) ?? []))],
|
||||||
|
approvalUrl
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -1786,6 +1792,11 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
const user = await userDAL.findById(actorId);
|
const user = await userDAL.findById(actorId);
|
||||||
const env = await projectEnvDAL.findOne({ id: policy.envId });
|
const env = await projectEnvDAL.findOne({ id: policy.envId });
|
||||||
|
|
||||||
|
const projectPath = `/projects/secret-management/${project.id}`;
|
||||||
|
const approvalPath = `${projectPath}/approval`;
|
||||||
|
const cfg = getConfig();
|
||||||
|
const approvalUrl = `${cfg.SITE_URL}${approvalPath}`;
|
||||||
|
|
||||||
await triggerWorkflowIntegrationNotification({
|
await triggerWorkflowIntegrationNotification({
|
||||||
input: {
|
input: {
|
||||||
projectId,
|
projectId,
|
||||||
@@ -1797,7 +1808,8 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
secretPath,
|
secretPath,
|
||||||
projectId,
|
projectId,
|
||||||
requestId: secretApprovalRequest.id,
|
requestId: secretApprovalRequest.id,
|
||||||
secretKeys: [...new Set(Object.values(data).flatMap((arr) => arr?.map((item) => item.secretKey) ?? []))]
|
secretKeys: [...new Set(Object.values(data).flatMap((arr) => arr?.map((item) => item.secretKey) ?? []))],
|
||||||
|
approvalUrl
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
import { validateMicrosoftTeamsChannelsSchema } from "@app/services/microsoft-teams/microsoft-teams-fns";
|
||||||
|
import { TMicrosoftTeamsServiceFactory } from "@app/services/microsoft-teams/microsoft-teams-service";
|
||||||
|
import {
|
||||||
|
TProjectMicrosoftTeamsConfigDALFactory,
|
||||||
|
TProjectMicrosoftTeamsConfigWithIntegrations
|
||||||
|
} from "@app/services/microsoft-teams/project-microsoft-teams-config-dal";
|
||||||
|
|
||||||
|
import { logger } from "../../logger";
|
||||||
|
import { TNotification, TriggerFeature } from "../types";
|
||||||
|
|
||||||
|
const handleMicrosoftTeamsNotification = async ({
|
||||||
|
microsoftTeamsConfig,
|
||||||
|
notification,
|
||||||
|
orgId,
|
||||||
|
microsoftTeamsService
|
||||||
|
}: {
|
||||||
|
microsoftTeamsConfig: TProjectMicrosoftTeamsConfigWithIntegrations;
|
||||||
|
notification: TNotification;
|
||||||
|
orgId: string;
|
||||||
|
microsoftTeamsService: Pick<TMicrosoftTeamsServiceFactory, "sendNotification">;
|
||||||
|
}): Promise<void> => {
|
||||||
|
let targetChannels: unknown;
|
||||||
|
let isEnabled = false;
|
||||||
|
|
||||||
|
switch (notification.type) {
|
||||||
|
case TriggerFeature.ACCESS_REQUEST:
|
||||||
|
case TriggerFeature.ACCESS_REQUEST_UPDATED:
|
||||||
|
targetChannels = microsoftTeamsConfig.accessRequestChannels;
|
||||||
|
isEnabled = microsoftTeamsConfig.isAccessRequestNotificationEnabled;
|
||||||
|
break;
|
||||||
|
case TriggerFeature.SECRET_APPROVAL:
|
||||||
|
targetChannels = microsoftTeamsConfig.secretRequestChannels;
|
||||||
|
isEnabled = microsoftTeamsConfig.isSecretRequestNotificationEnabled;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isEnabled && targetChannels) {
|
||||||
|
const { success, data, error: validationError } = validateMicrosoftTeamsChannelsSchema.safeParse(targetChannels);
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
logger.error(validationError, "Invalid Microsoft Teams channel configuration");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
await microsoftTeamsService
|
||||||
|
.sendNotification({
|
||||||
|
notification,
|
||||||
|
target: data,
|
||||||
|
tenantId: microsoftTeamsConfig.tenantId,
|
||||||
|
microsoftTeamsIntegrationId: microsoftTeamsConfig.id,
|
||||||
|
orgId
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
logger.error(
|
||||||
|
error,
|
||||||
|
`Error sending Microsoft Teams notification. Notification type: ${notification.type}, Tenant ID: ${microsoftTeamsConfig.tenantId}, Project ID: ${microsoftTeamsConfig.projectId}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const triggerMicrosoftTeamsNotification = async ({
|
||||||
|
projectId,
|
||||||
|
notification,
|
||||||
|
orgId,
|
||||||
|
projectMicrosoftTeamsConfigDAL,
|
||||||
|
microsoftTeamsService
|
||||||
|
}: {
|
||||||
|
projectId: string;
|
||||||
|
notification: TNotification;
|
||||||
|
orgId: string;
|
||||||
|
projectMicrosoftTeamsConfigDAL: Pick<TProjectMicrosoftTeamsConfigDALFactory, "getIntegrationDetailsByProject">;
|
||||||
|
microsoftTeamsService: Pick<TMicrosoftTeamsServiceFactory, "sendNotification">;
|
||||||
|
}): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const config = await projectMicrosoftTeamsConfigDAL.getIntegrationDetailsByProject(projectId);
|
||||||
|
if (config) {
|
||||||
|
await handleMicrosoftTeamsNotification({
|
||||||
|
microsoftTeamsConfig: config,
|
||||||
|
notification,
|
||||||
|
orgId,
|
||||||
|
microsoftTeamsService
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error, `Error handling Microsoft Teams notification. Project ID: ${projectId}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
|
import {
|
||||||
|
TProjectSlackConfigDALFactory,
|
||||||
|
TProjectSlackConfigWithIntegrations
|
||||||
|
} from "@app/services/slack/project-slack-config-dal";
|
||||||
|
import { sendSlackNotification } from "@app/services/slack/slack-fns";
|
||||||
|
|
||||||
|
import { logger } from "../../logger";
|
||||||
|
import { TNotification, TriggerFeature } from "../types";
|
||||||
|
|
||||||
|
const handleSlackNotification = async ({
|
||||||
|
slackConfig,
|
||||||
|
notification,
|
||||||
|
orgId,
|
||||||
|
kmsService
|
||||||
|
}: {
|
||||||
|
slackConfig: TProjectSlackConfigWithIntegrations;
|
||||||
|
notification: TNotification;
|
||||||
|
orgId: string;
|
||||||
|
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||||
|
}): Promise<void> => {
|
||||||
|
let targetChannelIds: string[] = [];
|
||||||
|
let isEnabled = false;
|
||||||
|
|
||||||
|
switch (notification.type) {
|
||||||
|
case TriggerFeature.ACCESS_REQUEST:
|
||||||
|
case TriggerFeature.ACCESS_REQUEST_UPDATED:
|
||||||
|
targetChannelIds = slackConfig.accessRequestChannels?.split(", ").filter(Boolean) || [];
|
||||||
|
isEnabled = slackConfig.isAccessRequestNotificationEnabled;
|
||||||
|
break;
|
||||||
|
case TriggerFeature.SECRET_APPROVAL:
|
||||||
|
targetChannelIds = slackConfig.secretRequestChannels?.split(", ").filter(Boolean) || [];
|
||||||
|
isEnabled = slackConfig.isSecretRequestNotificationEnabled;
|
||||||
|
break;
|
||||||
|
case TriggerFeature.SECRET_SYNC_ERROR:
|
||||||
|
targetChannelIds = slackConfig.secretSyncErrorChannels?.split(", ").filter(Boolean) || [];
|
||||||
|
isEnabled = slackConfig.isSecretSyncErrorNotificationEnabled;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetChannelIds.length && isEnabled) {
|
||||||
|
await sendSlackNotification({
|
||||||
|
orgId,
|
||||||
|
notification,
|
||||||
|
kmsService,
|
||||||
|
targetChannelIds,
|
||||||
|
slackIntegration: slackConfig
|
||||||
|
}).catch((error) => {
|
||||||
|
logger.error(
|
||||||
|
error,
|
||||||
|
`Error sending Slack notification. Notification type: ${notification.type}, Target channel IDs: ${targetChannelIds.join(", ")}, Project ID: ${slackConfig.projectId}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const triggerSlackNotification = async ({
|
||||||
|
projectId,
|
||||||
|
notification,
|
||||||
|
orgId,
|
||||||
|
projectSlackConfigDAL,
|
||||||
|
kmsService
|
||||||
|
}: {
|
||||||
|
projectId: string;
|
||||||
|
notification: TNotification;
|
||||||
|
orgId: string;
|
||||||
|
projectSlackConfigDAL: Pick<TProjectSlackConfigDALFactory, "getIntegrationDetailsByProject">;
|
||||||
|
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||||
|
}): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const config = await projectSlackConfigDAL.getIntegrationDetailsByProject(projectId);
|
||||||
|
if (config) {
|
||||||
|
await handleSlackNotification({ slackConfig: config, notification, orgId, kmsService });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error, `Error handling Slack notification. Project ID: ${projectId}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,8 +1,7 @@
|
|||||||
import { validateMicrosoftTeamsChannelsSchema } from "@app/services/microsoft-teams/microsoft-teams-fns";
|
|
||||||
import { sendSlackNotification } from "@app/services/slack/slack-fns";
|
|
||||||
|
|
||||||
import { logger } from "../logger";
|
import { logger } from "../logger";
|
||||||
import { TriggerFeature, TTriggerWorkflowNotificationDTO } from "./types";
|
import { triggerMicrosoftTeamsNotification } from "./notification-handlers/microsoft-teams";
|
||||||
|
import { triggerSlackNotification } from "./notification-handlers/slack";
|
||||||
|
import { TTriggerWorkflowNotificationDTO } from "./types";
|
||||||
|
|
||||||
export const triggerWorkflowIntegrationNotification = async (dto: TTriggerWorkflowNotificationDTO) => {
|
export const triggerWorkflowIntegrationNotification = async (dto: TTriggerWorkflowNotificationDTO) => {
|
||||||
try {
|
try {
|
||||||
@@ -16,88 +15,25 @@ export const triggerWorkflowIntegrationNotification = async (dto: TTriggerWorkfl
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const microsoftTeamsConfig = await projectMicrosoftTeamsConfigDAL.getIntegrationDetailsByProject(projectId);
|
const handlerPromises = [
|
||||||
const slackConfig = await projectSlackConfigDAL.getIntegrationDetailsByProject(projectId);
|
triggerSlackNotification({
|
||||||
|
projectId,
|
||||||
|
notification,
|
||||||
|
orgId: project.orgId,
|
||||||
|
projectSlackConfigDAL,
|
||||||
|
kmsService
|
||||||
|
}),
|
||||||
|
|
||||||
if (slackConfig) {
|
triggerMicrosoftTeamsNotification({
|
||||||
if (
|
projectId,
|
||||||
notification.type === TriggerFeature.ACCESS_REQUEST ||
|
notification,
|
||||||
notification.type === TriggerFeature.ACCESS_REQUEST_UPDATED
|
orgId: project.orgId,
|
||||||
) {
|
projectMicrosoftTeamsConfigDAL,
|
||||||
const targetChannelIds = slackConfig.accessRequestChannels?.split(", ") || [];
|
microsoftTeamsService
|
||||||
if (targetChannelIds.length && slackConfig.isAccessRequestNotificationEnabled) {
|
})
|
||||||
await sendSlackNotification({
|
];
|
||||||
orgId: project.orgId,
|
|
||||||
notification,
|
|
||||||
kmsService,
|
|
||||||
targetChannelIds,
|
|
||||||
slackIntegration: slackConfig
|
|
||||||
}).catch((error) => {
|
|
||||||
logger.error(error, "Error sending Slack notification");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else if (notification.type === TriggerFeature.SECRET_APPROVAL) {
|
|
||||||
const targetChannelIds = slackConfig.secretRequestChannels?.split(", ") || [];
|
|
||||||
if (targetChannelIds.length && slackConfig.isSecretRequestNotificationEnabled) {
|
|
||||||
await sendSlackNotification({
|
|
||||||
orgId: project.orgId,
|
|
||||||
notification,
|
|
||||||
kmsService,
|
|
||||||
targetChannelIds,
|
|
||||||
slackIntegration: slackConfig
|
|
||||||
}).catch((error) => {
|
|
||||||
logger.error(error, "Error sending Slack notification");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (microsoftTeamsConfig) {
|
await Promise.allSettled(handlerPromises);
|
||||||
if (
|
|
||||||
notification.type === TriggerFeature.ACCESS_REQUEST ||
|
|
||||||
notification.type === TriggerFeature.ACCESS_REQUEST_UPDATED
|
|
||||||
) {
|
|
||||||
if (microsoftTeamsConfig.isAccessRequestNotificationEnabled && microsoftTeamsConfig.accessRequestChannels) {
|
|
||||||
const { success, data } = validateMicrosoftTeamsChannelsSchema.safeParse(
|
|
||||||
microsoftTeamsConfig.accessRequestChannels
|
|
||||||
);
|
|
||||||
|
|
||||||
if (success && data) {
|
|
||||||
await microsoftTeamsService
|
|
||||||
.sendNotification({
|
|
||||||
notification,
|
|
||||||
target: data,
|
|
||||||
tenantId: microsoftTeamsConfig.tenantId,
|
|
||||||
microsoftTeamsIntegrationId: microsoftTeamsConfig.id,
|
|
||||||
orgId: project.orgId
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
logger.error(error, "Error sending Microsoft Teams notification");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (notification.type === TriggerFeature.SECRET_APPROVAL) {
|
|
||||||
if (microsoftTeamsConfig.isSecretRequestNotificationEnabled && microsoftTeamsConfig.secretRequestChannels) {
|
|
||||||
const { success, data } = validateMicrosoftTeamsChannelsSchema.safeParse(
|
|
||||||
microsoftTeamsConfig.secretRequestChannels
|
|
||||||
);
|
|
||||||
|
|
||||||
if (success && data) {
|
|
||||||
await microsoftTeamsService
|
|
||||||
.sendNotification({
|
|
||||||
notification,
|
|
||||||
target: data,
|
|
||||||
tenantId: microsoftTeamsConfig.tenantId,
|
|
||||||
microsoftTeamsIntegrationId: microsoftTeamsConfig.id,
|
|
||||||
orgId: project.orgId
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
logger.error(error, "Error sending Microsoft Teams notification");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(error, "Error triggering workflow integration notification");
|
logger.error(error, "Error triggering workflow integration notification");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ import { TProjectSlackConfigDALFactory } from "@app/services/slack/project-slack
|
|||||||
export enum TriggerFeature {
|
export enum TriggerFeature {
|
||||||
SECRET_APPROVAL = "secret-approval",
|
SECRET_APPROVAL = "secret-approval",
|
||||||
ACCESS_REQUEST = "access-request",
|
ACCESS_REQUEST = "access-request",
|
||||||
ACCESS_REQUEST_UPDATED = "access-request-updated"
|
ACCESS_REQUEST_UPDATED = "access-request-updated",
|
||||||
|
SECRET_SYNC_ERROR = "secret-sync-error"
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TNotification =
|
export type TNotification =
|
||||||
@@ -20,6 +21,7 @@ export type TNotification =
|
|||||||
requestId: string;
|
requestId: string;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
secretKeys: string[];
|
secretKeys: string[];
|
||||||
|
approvalUrl: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
@@ -31,6 +33,7 @@ export type TNotification =
|
|||||||
secretPath: string;
|
secretPath: string;
|
||||||
environment: string;
|
environment: string;
|
||||||
projectName: string;
|
projectName: string;
|
||||||
|
projectPath: string;
|
||||||
permissions: string[];
|
permissions: string[];
|
||||||
approvalUrl: string;
|
approvalUrl: string;
|
||||||
note?: string;
|
note?: string;
|
||||||
@@ -50,6 +53,21 @@ export type TNotification =
|
|||||||
editNote?: string;
|
editNote?: string;
|
||||||
editorFullName?: string;
|
editorFullName?: string;
|
||||||
editorEmail?: string;
|
editorEmail?: string;
|
||||||
|
projectPath: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: TriggerFeature.SECRET_SYNC_ERROR;
|
||||||
|
payload: {
|
||||||
|
syncName: string;
|
||||||
|
syncActionLabel: string;
|
||||||
|
syncDestination: string;
|
||||||
|
failureMessage: string;
|
||||||
|
syncUrl: string;
|
||||||
|
environment: string;
|
||||||
|
secretPath: string;
|
||||||
|
projectName: string;
|
||||||
|
projectPath: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1251,7 +1251,10 @@ export const registerRoutes = async (
|
|||||||
licenseService,
|
licenseService,
|
||||||
gatewayService,
|
gatewayService,
|
||||||
gatewayV2Service,
|
gatewayV2Service,
|
||||||
notificationService
|
notificationService,
|
||||||
|
projectSlackConfigDAL,
|
||||||
|
projectMicrosoftTeamsConfigDAL,
|
||||||
|
microsoftTeamsService
|
||||||
});
|
});
|
||||||
|
|
||||||
const secretQueueService = secretQueueFactory({
|
const secretQueueService = secretQueueFactory({
|
||||||
|
|||||||
@@ -614,8 +614,10 @@ export const registerDeprecatedProjectRouter = async (server: FastifyZodProvider
|
|||||||
integrationId: z.string(),
|
integrationId: z.string(),
|
||||||
accessRequestChannels: validateSlackChannelsField,
|
accessRequestChannels: validateSlackChannelsField,
|
||||||
secretRequestChannels: validateSlackChannelsField,
|
secretRequestChannels: validateSlackChannelsField,
|
||||||
|
secretSyncErrorChannels: validateSlackChannelsField,
|
||||||
isAccessRequestNotificationEnabled: z.boolean(),
|
isAccessRequestNotificationEnabled: z.boolean(),
|
||||||
isSecretRequestNotificationEnabled: z.boolean()
|
isSecretRequestNotificationEnabled: z.boolean(),
|
||||||
|
isSecretSyncErrorNotificationEnabled: z.boolean()
|
||||||
}),
|
}),
|
||||||
z.object({
|
z.object({
|
||||||
integration: z.literal(WorkflowIntegration.MICROSOFT_TEAMS),
|
integration: z.literal(WorkflowIntegration.MICROSOFT_TEAMS),
|
||||||
@@ -633,7 +635,9 @@ export const registerDeprecatedProjectRouter = async (server: FastifyZodProvider
|
|||||||
isAccessRequestNotificationEnabled: true,
|
isAccessRequestNotificationEnabled: true,
|
||||||
accessRequestChannels: true,
|
accessRequestChannels: true,
|
||||||
isSecretRequestNotificationEnabled: true,
|
isSecretRequestNotificationEnabled: true,
|
||||||
secretRequestChannels: true
|
secretRequestChannels: true,
|
||||||
|
isSecretSyncErrorNotificationEnabled: true,
|
||||||
|
secretSyncErrorChannels: true
|
||||||
}).merge(
|
}).merge(
|
||||||
z.object({
|
z.object({
|
||||||
integration: z.literal(WorkflowIntegration.SLACK),
|
integration: z.literal(WorkflowIntegration.SLACK),
|
||||||
|
|||||||
@@ -769,7 +769,9 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
isAccessRequestNotificationEnabled: true,
|
isAccessRequestNotificationEnabled: true,
|
||||||
accessRequestChannels: true,
|
accessRequestChannels: true,
|
||||||
isSecretRequestNotificationEnabled: true,
|
isSecretRequestNotificationEnabled: true,
|
||||||
secretRequestChannels: true
|
secretRequestChannels: true,
|
||||||
|
isSecretSyncErrorNotificationEnabled: true,
|
||||||
|
secretSyncErrorChannels: true
|
||||||
}).merge(
|
}).merge(
|
||||||
z.object({
|
z.object({
|
||||||
integration: z.literal(WorkflowIntegration.SLACK),
|
integration: z.literal(WorkflowIntegration.SLACK),
|
||||||
@@ -873,7 +875,9 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
accessRequestChannels: validateSlackChannelsField,
|
accessRequestChannels: validateSlackChannelsField,
|
||||||
secretRequestChannels: validateSlackChannelsField,
|
secretRequestChannels: validateSlackChannelsField,
|
||||||
isAccessRequestNotificationEnabled: z.boolean(),
|
isAccessRequestNotificationEnabled: z.boolean(),
|
||||||
isSecretRequestNotificationEnabled: z.boolean()
|
isSecretRequestNotificationEnabled: z.boolean(),
|
||||||
|
secretSyncErrorChannels: validateSlackChannelsField,
|
||||||
|
isSecretSyncErrorNotificationEnabled: z.boolean()
|
||||||
}),
|
}),
|
||||||
z.object({
|
z.object({
|
||||||
integration: z.literal(WorkflowIntegration.MICROSOFT_TEAMS),
|
integration: z.literal(WorkflowIntegration.MICROSOFT_TEAMS),
|
||||||
@@ -891,7 +895,9 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
isAccessRequestNotificationEnabled: true,
|
isAccessRequestNotificationEnabled: true,
|
||||||
accessRequestChannels: true,
|
accessRequestChannels: true,
|
||||||
isSecretRequestNotificationEnabled: true,
|
isSecretRequestNotificationEnabled: true,
|
||||||
secretRequestChannels: true
|
secretRequestChannels: true,
|
||||||
|
isSecretSyncErrorNotificationEnabled: true,
|
||||||
|
secretSyncErrorChannels: true
|
||||||
}).merge(
|
}).merge(
|
||||||
z.object({
|
z.object({
|
||||||
integration: z.literal(WorkflowIntegration.SLACK),
|
integration: z.literal(WorkflowIntegration.SLACK),
|
||||||
|
|||||||
@@ -1,16 +1,20 @@
|
|||||||
import { Knex } from "knex";
|
import { Knex } from "knex";
|
||||||
|
|
||||||
import { TDbClient } from "@app/db";
|
import { TDbClient } from "@app/db";
|
||||||
import { TableName } from "@app/db/schemas";
|
import { TableName, TMicrosoftTeamsIntegrations } from "@app/db/schemas";
|
||||||
|
import { TProjectMicrosoftTeamsConfigs } from "@app/db/schemas/project-microsoft-teams-configs";
|
||||||
import { ormify, selectAllTableCols } from "@app/lib/knex";
|
import { ormify, selectAllTableCols } from "@app/lib/knex";
|
||||||
|
|
||||||
export type TProjectMicrosoftTeamsConfigDALFactory = ReturnType<typeof projectMicrosoftTeamsConfigDALFactory>;
|
export type TProjectMicrosoftTeamsConfigDALFactory = ReturnType<typeof projectMicrosoftTeamsConfigDALFactory>;
|
||||||
|
export type TProjectMicrosoftTeamsConfigWithIntegrations = TProjectMicrosoftTeamsConfigs & TMicrosoftTeamsIntegrations;
|
||||||
|
|
||||||
export const projectMicrosoftTeamsConfigDALFactory = (db: TDbClient) => {
|
export const projectMicrosoftTeamsConfigDALFactory = (db: TDbClient) => {
|
||||||
const projectMicrosoftTeamsConfigOrm = ormify(db, TableName.ProjectMicrosoftTeamsConfigs);
|
const projectMicrosoftTeamsConfigOrm = ormify(db, TableName.ProjectMicrosoftTeamsConfigs);
|
||||||
|
|
||||||
const getIntegrationDetailsByProject = (projectId: string, tx?: Knex) => {
|
const getIntegrationDetailsByProject = (projectId: string, tx?: Knex) => {
|
||||||
return (tx || db.replicaNode())(TableName.ProjectMicrosoftTeamsConfigs)
|
return (tx || db.replicaNode())<TProjectMicrosoftTeamsConfigWithIntegrations>(
|
||||||
|
TableName.ProjectMicrosoftTeamsConfigs
|
||||||
|
)
|
||||||
.join(
|
.join(
|
||||||
TableName.MicrosoftTeamsIntegrations,
|
TableName.MicrosoftTeamsIntegrations,
|
||||||
`${TableName.ProjectMicrosoftTeamsConfigs}.microsoftTeamsIntegrationId`,
|
`${TableName.ProjectMicrosoftTeamsConfigs}.microsoftTeamsIntegrationId`,
|
||||||
|
|||||||
@@ -1605,8 +1605,14 @@ export const projectServiceFactory = ({
|
|||||||
isAccessRequestNotificationEnabled,
|
isAccessRequestNotificationEnabled,
|
||||||
accessRequestChannels,
|
accessRequestChannels,
|
||||||
isSecretRequestNotificationEnabled,
|
isSecretRequestNotificationEnabled,
|
||||||
secretRequestChannels
|
secretRequestChannels,
|
||||||
}: TUpdateProjectWorkflowIntegration) => {
|
secretSyncErrorChannels,
|
||||||
|
isSecretSyncErrorNotificationEnabled
|
||||||
|
}: TUpdateProjectWorkflowIntegration & {
|
||||||
|
// workaround intersection type while we don't have the microsoft teams integration for failed secret syncs
|
||||||
|
isSecretSyncErrorNotificationEnabled?: boolean;
|
||||||
|
secretSyncErrorChannels?: string;
|
||||||
|
}) => {
|
||||||
const project = await projectDAL.findById(projectId);
|
const project = await projectDAL.findById(projectId);
|
||||||
if (!project) {
|
if (!project) {
|
||||||
throw new NotFoundError({
|
throw new NotFoundError({
|
||||||
@@ -1628,6 +1634,7 @@ export const projectServiceFactory = ({
|
|||||||
|
|
||||||
const sanitizedAccessRequestChannels = validateSlackChannelsField.parse(accessRequestChannels);
|
const sanitizedAccessRequestChannels = validateSlackChannelsField.parse(accessRequestChannels);
|
||||||
const sanitizedSecretRequestChannels = validateSlackChannelsField.parse(secretRequestChannels);
|
const sanitizedSecretRequestChannels = validateSlackChannelsField.parse(secretRequestChannels);
|
||||||
|
const sanitizedSecretSyncErrorChannels = validateSlackChannelsField.parse(secretSyncErrorChannels);
|
||||||
|
|
||||||
const slackIntegration = await slackIntegrationDAL.findByIdWithWorkflowIntegrationDetails(integrationId);
|
const slackIntegration = await slackIntegrationDAL.findByIdWithWorkflowIntegrationDetails(integrationId);
|
||||||
|
|
||||||
@@ -1665,7 +1672,9 @@ export const projectServiceFactory = ({
|
|||||||
isAccessRequestNotificationEnabled,
|
isAccessRequestNotificationEnabled,
|
||||||
accessRequestChannels: sanitizedAccessRequestChannels,
|
accessRequestChannels: sanitizedAccessRequestChannels,
|
||||||
isSecretRequestNotificationEnabled,
|
isSecretRequestNotificationEnabled,
|
||||||
secretRequestChannels: sanitizedSecretRequestChannels
|
secretRequestChannels: sanitizedSecretRequestChannels,
|
||||||
|
isSecretSyncErrorNotificationEnabled,
|
||||||
|
secretSyncErrorChannels: sanitizedSecretSyncErrorChannels
|
||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
@@ -1678,7 +1687,9 @@ export const projectServiceFactory = ({
|
|||||||
isAccessRequestNotificationEnabled,
|
isAccessRequestNotificationEnabled,
|
||||||
accessRequestChannels: sanitizedAccessRequestChannels,
|
accessRequestChannels: sanitizedAccessRequestChannels,
|
||||||
isSecretRequestNotificationEnabled,
|
isSecretRequestNotificationEnabled,
|
||||||
secretRequestChannels: sanitizedSecretRequestChannels
|
secretRequestChannels: sanitizedSecretRequestChannels,
|
||||||
|
isSecretSyncErrorNotificationEnabled,
|
||||||
|
secretSyncErrorChannels: sanitizedSecretSyncErrorChannels
|
||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
@@ -1688,6 +1699,7 @@ export const projectServiceFactory = ({
|
|||||||
...updatedWorkflowIntegration,
|
...updatedWorkflowIntegration,
|
||||||
accessRequestChannels: sanitizedAccessRequestChannels,
|
accessRequestChannels: sanitizedAccessRequestChannels,
|
||||||
secretRequestChannels: sanitizedSecretRequestChannels,
|
secretRequestChannels: sanitizedSecretRequestChannels,
|
||||||
|
secretSyncErrorChannels: sanitizedSecretSyncErrorChannels,
|
||||||
integrationId: slackIntegration.id,
|
integrationId: slackIntegration.id,
|
||||||
integration: WorkflowIntegration.SLACK
|
integration: WorkflowIntegration.SLACK
|
||||||
} as const;
|
} as const;
|
||||||
|
|||||||
@@ -185,8 +185,10 @@ export type TUpdateProjectWorkflowIntegration = (
|
|||||||
integration: WorkflowIntegration.SLACK;
|
integration: WorkflowIntegration.SLACK;
|
||||||
isAccessRequestNotificationEnabled: boolean;
|
isAccessRequestNotificationEnabled: boolean;
|
||||||
isSecretRequestNotificationEnabled: boolean;
|
isSecretRequestNotificationEnabled: boolean;
|
||||||
|
isSecretSyncErrorNotificationEnabled: boolean;
|
||||||
accessRequestChannels?: string;
|
accessRequestChannels?: string;
|
||||||
secretRequestChannels?: string;
|
secretRequestChannels?: string;
|
||||||
|
secretSyncErrorChannels?: string;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
integrationId: string;
|
integrationId: string;
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import { TLicenseServiceFactory } from "@app/ee/services/license/license-service
|
|||||||
import { KeyStorePrefixes, TKeyStoreFactory } from "@app/keystore/keystore";
|
import { KeyStorePrefixes, TKeyStoreFactory } from "@app/keystore/keystore";
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
|
import { triggerWorkflowIntegrationNotification } from "@app/lib/workflow-integrations/trigger-notification";
|
||||||
|
import { TriggerFeature } from "@app/lib/workflow-integrations/types";
|
||||||
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
|
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
|
||||||
import { SecretNameSchema } from "@app/server/lib/schemas";
|
import { SecretNameSchema } from "@app/server/lib/schemas";
|
||||||
import { decryptAppConnectionCredentials } from "@app/services/app-connection/app-connection-fns";
|
import { decryptAppConnectionCredentials } from "@app/services/app-connection/app-connection-fns";
|
||||||
@@ -62,8 +64,11 @@ import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
|
|||||||
|
|
||||||
import { TAppConnectionDALFactory } from "../app-connection/app-connection-dal";
|
import { TAppConnectionDALFactory } from "../app-connection/app-connection-dal";
|
||||||
import { TFolderCommitServiceFactory } from "../folder-commit/folder-commit-service";
|
import { TFolderCommitServiceFactory } from "../folder-commit/folder-commit-service";
|
||||||
|
import { TMicrosoftTeamsServiceFactory } from "../microsoft-teams/microsoft-teams-service";
|
||||||
|
import { TProjectMicrosoftTeamsConfigDALFactory } from "../microsoft-teams/project-microsoft-teams-config-dal";
|
||||||
import { TNotificationServiceFactory } from "../notification/notification-service";
|
import { TNotificationServiceFactory } from "../notification/notification-service";
|
||||||
import { NotificationType } from "../notification/notification-types";
|
import { NotificationType } from "../notification/notification-types";
|
||||||
|
import { TProjectSlackConfigDALFactory } from "../slack/project-slack-config-dal";
|
||||||
|
|
||||||
export type TSecretSyncQueueFactory = ReturnType<typeof secretSyncQueueFactory>;
|
export type TSecretSyncQueueFactory = ReturnType<typeof secretSyncQueueFactory>;
|
||||||
|
|
||||||
@@ -104,6 +109,9 @@ type TSecretSyncQueueFactoryDep = {
|
|||||||
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">;
|
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">;
|
||||||
gatewayV2Service: Pick<TGatewayV2ServiceFactory, "getPlatformConnectionDetailsByGatewayId">;
|
gatewayV2Service: Pick<TGatewayV2ServiceFactory, "getPlatformConnectionDetailsByGatewayId">;
|
||||||
notificationService: Pick<TNotificationServiceFactory, "createUserNotifications">;
|
notificationService: Pick<TNotificationServiceFactory, "createUserNotifications">;
|
||||||
|
projectSlackConfigDAL: Pick<TProjectSlackConfigDALFactory, "getIntegrationDetailsByProject">;
|
||||||
|
projectMicrosoftTeamsConfigDAL: Pick<TProjectMicrosoftTeamsConfigDALFactory, "getIntegrationDetailsByProject">;
|
||||||
|
microsoftTeamsService: Pick<TMicrosoftTeamsServiceFactory, "sendNotification">;
|
||||||
};
|
};
|
||||||
|
|
||||||
type SecretSyncActionJob = Job<
|
type SecretSyncActionJob = Job<
|
||||||
@@ -147,7 +155,10 @@ export const secretSyncQueueFactory = ({
|
|||||||
licenseService,
|
licenseService,
|
||||||
gatewayService,
|
gatewayService,
|
||||||
gatewayV2Service,
|
gatewayV2Service,
|
||||||
notificationService
|
notificationService,
|
||||||
|
projectSlackConfigDAL,
|
||||||
|
projectMicrosoftTeamsConfigDAL,
|
||||||
|
microsoftTeamsService
|
||||||
}: TSecretSyncQueueFactoryDep) => {
|
}: TSecretSyncQueueFactoryDep) => {
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
|
|
||||||
@@ -921,34 +932,65 @@ export const secretSyncQueueFactory = ({
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
const syncPath = `/projects/secret-management/${projectId}/integrations/secret-syncs/${destination}/${secretSync.id}`;
|
const baseProjectPath = `/projects/secret-management/${projectId}`;
|
||||||
|
const overviewPath = `${baseProjectPath}/overview`;
|
||||||
|
const syncPath = `${baseProjectPath}/integrations/secret-syncs/${destination}/${secretSync.id}`;
|
||||||
|
|
||||||
await notificationService.createUserNotifications(
|
const notifications = [
|
||||||
projectAdmins.map((admin) => ({
|
triggerWorkflowIntegrationNotification({
|
||||||
userId: admin.userId,
|
input: {
|
||||||
orgId: project.orgId,
|
notification: {
|
||||||
type: NotificationType.SECRET_SYNC_FAILED,
|
type: TriggerFeature.SECRET_SYNC_ERROR,
|
||||||
title: `Secret Sync Failed to ${actionLabel} Secrets`,
|
payload: {
|
||||||
body: `Your **${syncDestination}** sync **${name}** failed to complete${failureMessage ? `: \`${failureMessage}\`` : ""}`,
|
syncName: name,
|
||||||
link: syncPath
|
syncDestination,
|
||||||
}))
|
failureMessage: failureMessage || "An unknown error occurred",
|
||||||
);
|
syncUrl: `${appCfg.SITE_URL}${syncPath}`,
|
||||||
|
syncActionLabel: actionLabel,
|
||||||
|
environment: environment?.name || "-",
|
||||||
|
secretPath: folder?.path || "-",
|
||||||
|
projectName: project.name,
|
||||||
|
projectPath: overviewPath
|
||||||
|
}
|
||||||
|
},
|
||||||
|
projectId
|
||||||
|
},
|
||||||
|
dependencies: {
|
||||||
|
projectDAL,
|
||||||
|
projectSlackConfigDAL,
|
||||||
|
kmsService,
|
||||||
|
microsoftTeamsService,
|
||||||
|
projectMicrosoftTeamsConfigDAL
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
notificationService.createUserNotifications(
|
||||||
|
projectAdmins.map((admin) => ({
|
||||||
|
userId: admin.userId,
|
||||||
|
orgId: project.orgId,
|
||||||
|
type: NotificationType.SECRET_SYNC_FAILED,
|
||||||
|
title: `Secret Sync Failed to ${actionLabel} Secrets`,
|
||||||
|
body: `Your **${syncDestination}** sync **${name}** failed to complete${failureMessage ? `: \`${failureMessage}\`` : ""}`,
|
||||||
|
link: syncPath
|
||||||
|
}))
|
||||||
|
),
|
||||||
|
smtpService.sendMail({
|
||||||
|
recipients: projectAdmins.map((member) => member.user.email!).filter(Boolean),
|
||||||
|
template: SmtpTemplates.SecretSyncFailed,
|
||||||
|
subjectLine: `Secret Sync Failed to ${actionLabel} Secrets`,
|
||||||
|
substitutions: {
|
||||||
|
syncName: name,
|
||||||
|
syncDestination,
|
||||||
|
content: `Your ${syncDestination} Sync named "${name}" failed while attempting to ${action.toLowerCase()} secrets.`,
|
||||||
|
failureMessage,
|
||||||
|
secretPath: folder?.path,
|
||||||
|
environment: environment?.name,
|
||||||
|
projectName: project.name,
|
||||||
|
syncUrl: `${appCfg.SITE_URL}${syncPath}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
await smtpService.sendMail({
|
await Promise.allSettled(notifications);
|
||||||
recipients: projectAdmins.map((member) => member.user.email!).filter(Boolean),
|
|
||||||
template: SmtpTemplates.SecretSyncFailed,
|
|
||||||
subjectLine: `Secret Sync Failed to ${actionLabel} Secrets`,
|
|
||||||
substitutions: {
|
|
||||||
syncName: name,
|
|
||||||
syncDestination,
|
|
||||||
content: `Your ${syncDestination} Sync named "${name}" failed while attempting to ${action.toLowerCase()} secrets.`,
|
|
||||||
failureMessage,
|
|
||||||
secretPath: folder?.path,
|
|
||||||
environment: environment?.name,
|
|
||||||
projectName: project.name,
|
|
||||||
syncUrl: `${appCfg.SITE_URL}${syncPath}`
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const queueSecretSyncsSyncSecretsByPath = async ({
|
const queueSecretSyncsSyncSecretsByPath = async ({
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
import { Knex } from "knex";
|
import { Knex } from "knex";
|
||||||
|
|
||||||
import { TDbClient } from "@app/db";
|
import { TDbClient } from "@app/db";
|
||||||
import { TableName } from "@app/db/schemas";
|
import { TableName, TProjectSlackConfigs, TSlackIntegrations } from "@app/db/schemas";
|
||||||
import { ormify, selectAllTableCols } from "@app/lib/knex";
|
import { ormify, selectAllTableCols } from "@app/lib/knex";
|
||||||
|
|
||||||
export type TProjectSlackConfigDALFactory = ReturnType<typeof projectSlackConfigDALFactory>;
|
export type TProjectSlackConfigDALFactory = ReturnType<typeof projectSlackConfigDALFactory>;
|
||||||
|
export type TProjectSlackConfigWithIntegrations = TProjectSlackConfigs & TSlackIntegrations;
|
||||||
|
|
||||||
export const projectSlackConfigDALFactory = (db: TDbClient) => {
|
export const projectSlackConfigDALFactory = (db: TDbClient) => {
|
||||||
const projectSlackConfigOrm = ormify(db, TableName.ProjectSlackConfigs);
|
const projectSlackConfigOrm = ormify(db, TableName.ProjectSlackConfigs);
|
||||||
|
|
||||||
const getIntegrationDetailsByProject = (projectId: string, tx?: Knex) => {
|
const getIntegrationDetailsByProject = (projectId: string, tx?: Knex) => {
|
||||||
return (tx || db.replicaNode())(TableName.ProjectSlackConfigs)
|
return (tx || db.replicaNode())<TProjectSlackConfigWithIntegrations>(TableName.ProjectSlackConfigs)
|
||||||
.join(
|
.join(
|
||||||
TableName.SlackIntegrations,
|
TableName.SlackIntegrations,
|
||||||
`${TableName.ProjectSlackConfigs}.slackIntegrationId`,
|
`${TableName.ProjectSlackConfigs}.slackIntegrationId`,
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ import { TNotification, TriggerFeature } from "@app/lib/workflow-integrations/ty
|
|||||||
import { KmsDataKey } from "../kms/kms-types";
|
import { KmsDataKey } from "../kms/kms-types";
|
||||||
import { TSendSlackNotificationDTO } from "./slack-types";
|
import { TSendSlackNotificationDTO } from "./slack-types";
|
||||||
|
|
||||||
|
const COMPANY_BRAND_COLOR = "#e0ed34";
|
||||||
|
const ERROR_COLOR = "#e74c3c";
|
||||||
|
|
||||||
export const fetchSlackChannels = async (botKey: string) => {
|
export const fetchSlackChannels = async (botKey: string) => {
|
||||||
const slackChannels: {
|
const slackChannels: {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -48,13 +51,9 @@ const buildSlackPayload = (notification: TNotification) => {
|
|||||||
const messageBody = `A secret approval request has been opened by ${payload.userEmail}.
|
const messageBody = `A secret approval request has been opened by ${payload.userEmail}.
|
||||||
*Environment*: ${payload.environment}
|
*Environment*: ${payload.environment}
|
||||||
*Secret path*: ${payload.secretPath || "/"}
|
*Secret path*: ${payload.secretPath || "/"}
|
||||||
*Secret Key${payload.secretKeys.length > 1 ? "s" : ""}*: ${payload.secretKeys.join(", ")}
|
*Secret Key${payload.secretKeys.length > 1 ? "s" : ""}*: ${payload.secretKeys.join(", ")}`;
|
||||||
|
|
||||||
View the complete details <${appCfg.SITE_URL}/projects/secret-management/${payload.projectId}/approval?requestId=${
|
const headerBlocks = [
|
||||||
payload.requestId
|
|
||||||
}|here>.`;
|
|
||||||
|
|
||||||
const payloadBlocks = [
|
|
||||||
{
|
{
|
||||||
type: "header",
|
type: "header",
|
||||||
text: {
|
text: {
|
||||||
@@ -62,37 +61,52 @@ View the complete details <${appCfg.SITE_URL}/projects/secret-management/${paylo
|
|||||||
text: "Secret approval request",
|
text: "Secret approval request",
|
||||||
emoji: true
|
emoji: true
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const payloadBlocks = [
|
||||||
{
|
{
|
||||||
type: "section",
|
type: "section",
|
||||||
text: {
|
text: {
|
||||||
type: "mrkdwn",
|
type: "mrkdwn",
|
||||||
text: messageBody
|
text: messageBody
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "actions",
|
||||||
|
elements: [
|
||||||
|
{
|
||||||
|
type: "button",
|
||||||
|
text: {
|
||||||
|
type: "plain_text",
|
||||||
|
text: "View request",
|
||||||
|
emoji: true
|
||||||
|
},
|
||||||
|
style: "primary",
|
||||||
|
url: payload.approvalUrl
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
headerBlocks,
|
||||||
payloadMessage: messageBody,
|
payloadMessage: messageBody,
|
||||||
payloadBlocks
|
payloadBlocks,
|
||||||
|
color: COMPANY_BRAND_COLOR
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case TriggerFeature.ACCESS_REQUEST: {
|
case TriggerFeature.ACCESS_REQUEST: {
|
||||||
const { payload } = notification;
|
const { payload } = notification;
|
||||||
const messageBody = `${payload.requesterFullName} (${payload.requesterEmail}) has requested ${
|
const projectUrl = `${appCfg.SITE_URL}${payload.projectPath}/overview`;
|
||||||
payload.isTemporary ? "temporary" : "permanent"
|
const accessType = payload.isTemporary ? "temporary" : "permanent";
|
||||||
} access to ${payload.secretPath} in the ${payload.environment} environment of ${payload.projectName}.
|
const permissionsFormatted = payload.permissions.map((p) => `*${p}*`).join(", ");
|
||||||
|
|
||||||
The following permissions are requested: ${payload.permissions.join(", ")}
|
|
||||||
|
|
||||||
View the request and approve or deny it <${payload.approvalUrl}|here>.${
|
const messageBody = `${payload.requesterFullName} (${payload.requesterEmail}) has requested ${accessType} access to ${payload.secretPath} in the ${payload.environment} environment of ${payload.projectName}.\n\nThe following permissions are requested: ${payload.permissions.join(", ")}${
|
||||||
payload.note
|
payload.note ? `\n\nUser note: ${payload.note}` : ""
|
||||||
? `
|
|
||||||
User Note: ${payload.note}`
|
|
||||||
: ""
|
|
||||||
}`;
|
}`;
|
||||||
|
|
||||||
const payloadBlocks = [
|
const headerBlocks = [
|
||||||
{
|
{
|
||||||
type: "header",
|
type: "header",
|
||||||
text: {
|
text: {
|
||||||
@@ -100,37 +114,54 @@ User Note: ${payload.note}`
|
|||||||
text: "New access approval request pending for review",
|
text: "New access approval request pending for review",
|
||||||
emoji: true
|
emoji: true
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const payloadBlocks = [
|
||||||
{
|
{
|
||||||
type: "section",
|
type: "section",
|
||||||
text: {
|
text: {
|
||||||
type: "mrkdwn",
|
type: "mrkdwn",
|
||||||
text: messageBody
|
text: `*${payload.requesterFullName}* (${payload.requesterEmail}) has requested *${accessType}* access to *${payload.secretPath}* in the *${payload.environment}* environment of *<${projectUrl}|${payload.projectName}>*.\n\nThe following permissions are requested: ${permissionsFormatted}${
|
||||||
|
payload.note ? `\n\n*User note:* ${payload.note}` : ""
|
||||||
|
}`
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "actions",
|
||||||
|
elements: [
|
||||||
|
{
|
||||||
|
type: "button",
|
||||||
|
text: {
|
||||||
|
type: "plain_text",
|
||||||
|
text: "View request",
|
||||||
|
emoji: true
|
||||||
|
},
|
||||||
|
style: "primary",
|
||||||
|
url: payload.approvalUrl
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
headerBlocks,
|
||||||
payloadMessage: messageBody,
|
payloadMessage: messageBody,
|
||||||
payloadBlocks
|
payloadBlocks,
|
||||||
|
color: COMPANY_BRAND_COLOR
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case TriggerFeature.ACCESS_REQUEST_UPDATED: {
|
case TriggerFeature.ACCESS_REQUEST_UPDATED: {
|
||||||
const { payload } = notification;
|
const { payload } = notification;
|
||||||
const messageBody = `${payload.editorFullName} (${payload.editorEmail}) has updated the ${
|
const projectUrl = `${appCfg.SITE_URL}${payload.projectPath}/overview`;
|
||||||
payload.isTemporary ? "temporary" : "permanent"
|
const accessType = payload.isTemporary ? "temporary" : "permanent";
|
||||||
} access request from ${payload.requesterFullName} (${payload.requesterEmail}) to ${payload.secretPath} in the ${payload.environment} environment of ${payload.projectName}.
|
const permissionsFormatted = payload.permissions.map((p) => `*${p}*`).join(", ");
|
||||||
|
|
||||||
The following permissions are requested: ${payload.permissions.join(", ")}
|
|
||||||
|
|
||||||
View the request and approve or deny it <${payload.approvalUrl}|here>.${
|
const messageBody = `${payload.editorFullName} (${payload.editorEmail}) has updated the ${accessType} access request from ${payload.requesterFullName} (${payload.requesterEmail}) to ${payload.secretPath} in the ${payload.environment} environment of ${payload.projectName}.\n\nThe following permissions are requested: ${payload.permissions.join(", ")}${
|
||||||
payload.editNote
|
payload.editNote ? `\n\nEditor Note: ${payload.editNote}` : ""
|
||||||
? `
|
|
||||||
Editor Note: ${payload.editNote}`
|
|
||||||
: ""
|
|
||||||
}`;
|
}`;
|
||||||
|
|
||||||
const payloadBlocks = [
|
const headerBlocks = [
|
||||||
{
|
{
|
||||||
type: "header",
|
type: "header",
|
||||||
text: {
|
text: {
|
||||||
@@ -138,19 +169,89 @@ Editor Note: ${payload.editNote}`
|
|||||||
text: "Updated access approval request pending for review",
|
text: "Updated access approval request pending for review",
|
||||||
emoji: true
|
emoji: true
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const payloadBlocks = [
|
||||||
{
|
{
|
||||||
type: "section",
|
type: "section",
|
||||||
text: {
|
text: {
|
||||||
type: "mrkdwn",
|
type: "mrkdwn",
|
||||||
text: messageBody
|
text: `*${payload.editorFullName}* (${payload.editorEmail}) has updated the *${accessType}* access request from *${payload.requesterFullName}* (${payload.requesterEmail}) to *${payload.secretPath}* in the *${payload.environment}* environment of *<${projectUrl}|${payload.projectName}>*.\n\nThe following permissions are requested: ${permissionsFormatted}${
|
||||||
|
payload.editNote ? `\n\n*Editor Note:* ${payload.editNote}` : ""
|
||||||
|
}`
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "actions",
|
||||||
|
elements: [
|
||||||
|
{
|
||||||
|
type: "button",
|
||||||
|
text: {
|
||||||
|
type: "plain_text",
|
||||||
|
text: "View request",
|
||||||
|
emoji: true
|
||||||
|
},
|
||||||
|
style: "primary",
|
||||||
|
url: payload.approvalUrl
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
return {
|
||||||
|
headerBlocks,
|
||||||
|
payloadMessage: messageBody,
|
||||||
|
payloadBlocks,
|
||||||
|
color: COMPANY_BRAND_COLOR
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case TriggerFeature.SECRET_SYNC_ERROR: {
|
||||||
|
const { payload } = notification;
|
||||||
|
const projectUrl = `${appCfg.SITE_URL}${payload.projectPath}`;
|
||||||
|
const messageBody = `Secret sync ${payload.syncName} for ${payload.syncDestination} failed on ${payload.syncActionLabel}\n\n\nEnvironment: ${payload.environment}\n\n\nSecret Path: ${payload.secretPath}\n\n\nProject: ${payload.projectName} (${projectUrl})\n\n\nReason:\n${payload.failureMessage}`;
|
||||||
|
|
||||||
|
const headerBlocks = [
|
||||||
|
{
|
||||||
|
type: "header",
|
||||||
|
text: {
|
||||||
|
type: "plain_text",
|
||||||
|
text: `Secret sync ${payload.syncName} for ${payload.syncDestination} failed on ${payload.syncActionLabel}`,
|
||||||
|
emoji: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const payloadBlocks = [
|
||||||
|
{
|
||||||
|
type: "section",
|
||||||
|
text: {
|
||||||
|
type: "mrkdwn",
|
||||||
|
text: `*Environment:* ${payload.environment}\n\n*Secret Path:* ${payload.secretPath}\n\n*Project:* <${projectUrl}|${payload.projectName}>\n\n*Reason:* ${payload.failureMessage}`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "actions",
|
||||||
|
elements: [
|
||||||
|
{
|
||||||
|
type: "button",
|
||||||
|
text: {
|
||||||
|
type: "plain_text",
|
||||||
|
text: "Open secret sync",
|
||||||
|
emoji: true
|
||||||
|
},
|
||||||
|
style: "primary",
|
||||||
|
url: payload.syncUrl
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
payloadMessage: messageBody,
|
payloadMessage: messageBody,
|
||||||
payloadBlocks
|
headerBlocks,
|
||||||
|
payloadBlocks,
|
||||||
|
color: ERROR_COLOR
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
@@ -177,15 +278,22 @@ export const sendSlackNotification = async ({
|
|||||||
}).toString("utf8");
|
}).toString("utf8");
|
||||||
const slackWebClient = new WebClient(botKey);
|
const slackWebClient = new WebClient(botKey);
|
||||||
|
|
||||||
const { payloadMessage, payloadBlocks } = buildSlackPayload(notification);
|
const { payloadMessage, payloadBlocks, color, headerBlocks } = buildSlackPayload(notification);
|
||||||
|
|
||||||
for await (const conversationId of targetChannelIds) {
|
for await (const conversationId of targetChannelIds) {
|
||||||
// we send both text and blocks for compatibility with barebone clients
|
// we send both text and blocks for compatibility with barebone clients
|
||||||
|
|
||||||
await slackWebClient.chat
|
await slackWebClient.chat
|
||||||
.postMessage({
|
.postMessage({
|
||||||
channel: conversationId,
|
channel: conversationId,
|
||||||
text: payloadMessage,
|
text: payloadMessage,
|
||||||
blocks: payloadBlocks
|
blocks: headerBlocks,
|
||||||
|
attachments: [
|
||||||
|
{
|
||||||
|
color,
|
||||||
|
blocks: payloadBlocks
|
||||||
|
}
|
||||||
|
]
|
||||||
})
|
})
|
||||||
.catch((err) => logger.error(err));
|
.catch((err) => logger.error(err));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,6 +86,8 @@ export type ProjectWorkflowIntegrationConfig =
|
|||||||
accessRequestChannels: string;
|
accessRequestChannels: string;
|
||||||
isSecretRequestNotificationEnabled: boolean;
|
isSecretRequestNotificationEnabled: boolean;
|
||||||
secretRequestChannels: string;
|
secretRequestChannels: string;
|
||||||
|
isSecretSyncErrorNotificationEnabled: boolean;
|
||||||
|
secretSyncErrorChannels: string;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -112,6 +114,8 @@ export type TUpdateProjectWorkflowIntegrationConfigDTO =
|
|||||||
accessRequestChannels: string;
|
accessRequestChannels: string;
|
||||||
isSecretRequestNotificationEnabled: boolean;
|
isSecretRequestNotificationEnabled: boolean;
|
||||||
secretRequestChannels: string;
|
secretRequestChannels: string;
|
||||||
|
isSecretSyncErrorNotificationEnabled: boolean;
|
||||||
|
secretSyncErrorChannels: string;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
integration: WorkflowIntegrationPlatform.MICROSOFT_TEAMS;
|
integration: WorkflowIntegrationPlatform.MICROSOFT_TEAMS;
|
||||||
|
|||||||
+1
-1
@@ -111,7 +111,7 @@ export const WorkflowIntegrationTab = () => {
|
|||||||
<Td>Provider</Td>
|
<Td>Provider</Td>
|
||||||
<Td>Access Request Notifications Destination</Td>
|
<Td>Access Request Notifications Destination</Td>
|
||||||
<Td>Secret Request Notifications Destination</Td>
|
<Td>Secret Request Notifications Destination</Td>
|
||||||
<Td />
|
<Td>Secret Sync Error Notifications Destination</Td>
|
||||||
</Tr>
|
</Tr>
|
||||||
</THead>
|
</THead>
|
||||||
<TBody>
|
<TBody>
|
||||||
|
|||||||
+3
-1
@@ -99,7 +99,9 @@ export const MicrosoftTeamsConfigRow = ({
|
|||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</Td>
|
</Td>
|
||||||
|
<Td>
|
||||||
|
<Badge variant="danger">Disabled</Badge>
|
||||||
|
</Td>
|
||||||
<Td>
|
<Td>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild className="rounded-lg">
|
<DropdownMenuTrigger asChild className="rounded-lg">
|
||||||
|
|||||||
+19
-1
@@ -96,7 +96,25 @@ export const SlackConfigRow = ({ handlePopUpOpen, isSlackConfigLoading, slackCon
|
|||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</Td>
|
</Td>
|
||||||
|
<Td>
|
||||||
|
{slackConfig.isSecretSyncErrorNotificationEnabled &&
|
||||||
|
!isLoadingConfig &&
|
||||||
|
slackConfig.secretSyncErrorChannels.length > 0 ? (
|
||||||
|
<p>
|
||||||
|
{slackConfig.secretSyncErrorChannels
|
||||||
|
.split(", ")
|
||||||
|
.map((channel) => slackChannelIdToName[channel])
|
||||||
|
.join(", ")}
|
||||||
|
</p>
|
||||||
|
) : isLoadingConfig ? (
|
||||||
|
<Spinner size="xs" />
|
||||||
|
) : (
|
||||||
|
<Badge variant="neutral">
|
||||||
|
<BanIcon />
|
||||||
|
Disabled
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</Td>
|
||||||
<Td>
|
<Td>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild className="rounded-lg">
|
<DropdownMenuTrigger asChild className="rounded-lg">
|
||||||
|
|||||||
+144
-114
@@ -37,11 +37,76 @@ const formSchema = z.object({
|
|||||||
isSecretRequestNotificationEnabled: z.boolean(),
|
isSecretRequestNotificationEnabled: z.boolean(),
|
||||||
secretRequestChannels: z.string().array(),
|
secretRequestChannels: z.string().array(),
|
||||||
isAccessRequestNotificationEnabled: z.boolean(),
|
isAccessRequestNotificationEnabled: z.boolean(),
|
||||||
accessRequestChannels: z.string().array()
|
accessRequestChannels: z.string().array(),
|
||||||
|
isSecretSyncErrorNotificationEnabled: z.boolean(),
|
||||||
|
secretSyncErrorChannels: z.string().array()
|
||||||
});
|
});
|
||||||
|
|
||||||
type TSlackConfigForm = z.infer<typeof formSchema>;
|
type TSlackConfigForm = z.infer<typeof formSchema>;
|
||||||
|
|
||||||
|
type TChannelSelectorProps = {
|
||||||
|
value: string[];
|
||||||
|
onChange: (value: string[]) => void;
|
||||||
|
error?: { message?: string };
|
||||||
|
slackChannelIdToName: Record<string, string>;
|
||||||
|
sortedSlackChannels?: { id: string; name: string }[];
|
||||||
|
keyPrefix: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ChannelSelector = ({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
error,
|
||||||
|
slackChannelIdToName,
|
||||||
|
sortedSlackChannels,
|
||||||
|
keyPrefix
|
||||||
|
}: TChannelSelectorProps) => (
|
||||||
|
<FormControl label="Slack channels" isError={Boolean(error)} errorText={error?.message}>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Input
|
||||||
|
isReadOnly
|
||||||
|
value={value
|
||||||
|
?.filter(Boolean)
|
||||||
|
.map((entry) => slackChannelIdToName[entry])
|
||||||
|
.join(", ")}
|
||||||
|
className="text-left"
|
||||||
|
/>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent
|
||||||
|
style={{
|
||||||
|
width: "var(--radix-dropdown-menu-trigger-width)",
|
||||||
|
maxHeight: "350px",
|
||||||
|
overflowY: "auto"
|
||||||
|
}}
|
||||||
|
side="bottom"
|
||||||
|
align="start"
|
||||||
|
>
|
||||||
|
{sortedSlackChannels?.map((slackChannel) => {
|
||||||
|
const isChecked = value?.includes(slackChannel.id);
|
||||||
|
return (
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={(evt) => {
|
||||||
|
evt.preventDefault();
|
||||||
|
onChange(
|
||||||
|
isChecked
|
||||||
|
? value?.filter((el: string) => el !== slackChannel.id)
|
||||||
|
: [...(value || []), slackChannel.id]
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
key={`${keyPrefix}-slack-channel-${slackChannel.id}`}
|
||||||
|
iconPos="right"
|
||||||
|
icon={isChecked && <FontAwesomeIcon icon={faCheckCircle} />}
|
||||||
|
>
|
||||||
|
{slackChannel.name}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</FormControl>
|
||||||
|
);
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
};
|
};
|
||||||
@@ -71,7 +136,9 @@ export const SlackIntegrationForm = ({ onClose }: Props) => {
|
|||||||
isAccessRequestNotificationEnabled: false,
|
isAccessRequestNotificationEnabled: false,
|
||||||
accessRequestChannels: [],
|
accessRequestChannels: [],
|
||||||
isSecretRequestNotificationEnabled: false,
|
isSecretRequestNotificationEnabled: false,
|
||||||
secretRequestChannels: []
|
secretRequestChannels: [],
|
||||||
|
isSecretSyncErrorNotificationEnabled: false,
|
||||||
|
secretSyncErrorChannels: []
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -86,7 +153,8 @@ export const SlackIntegrationForm = ({ onClose }: Props) => {
|
|||||||
integration: WorkflowIntegrationPlatform.SLACK,
|
integration: WorkflowIntegrationPlatform.SLACK,
|
||||||
integrationId: data.slackIntegrationId,
|
integrationId: data.slackIntegrationId,
|
||||||
accessRequestChannels: data.accessRequestChannels.filter(Boolean).join(", "),
|
accessRequestChannels: data.accessRequestChannels.filter(Boolean).join(", "),
|
||||||
secretRequestChannels: data.secretRequestChannels.filter(Boolean).join(", ")
|
secretRequestChannels: data.secretRequestChannels.filter(Boolean).join(", "),
|
||||||
|
secretSyncErrorChannels: data.secretSyncErrorChannels.filter(Boolean).join(", ")
|
||||||
});
|
});
|
||||||
|
|
||||||
createNotification({
|
createNotification({
|
||||||
@@ -100,6 +168,7 @@ export const SlackIntegrationForm = ({ onClose }: Props) => {
|
|||||||
const secretRequestNotifState = watch("isSecretRequestNotificationEnabled");
|
const secretRequestNotifState = watch("isSecretRequestNotificationEnabled");
|
||||||
const selectedSlackIntegrationId = watch("slackIntegrationId");
|
const selectedSlackIntegrationId = watch("slackIntegrationId");
|
||||||
const accessRequestNotifState = watch("isAccessRequestNotificationEnabled");
|
const accessRequestNotifState = watch("isAccessRequestNotificationEnabled");
|
||||||
|
const secretSyncErrorNotifState = watch("isSecretSyncErrorNotificationEnabled");
|
||||||
|
|
||||||
const { data: slackChannels } = useGetSlackIntegrationChannels(selectedSlackIntegrationId);
|
const { data: slackChannels } = useGetSlackIntegrationChannels(selectedSlackIntegrationId);
|
||||||
const slackChannelIdToName = Object.fromEntries(
|
const slackChannelIdToName = Object.fromEntries(
|
||||||
@@ -110,7 +179,7 @@ export const SlackIntegrationForm = ({ onClose }: Props) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (slackConfig) {
|
if (slackConfig && slackConfig.integration === WorkflowIntegrationPlatform.SLACK) {
|
||||||
setValue("slackIntegrationId", slackConfig.integrationId);
|
setValue("slackIntegrationId", slackConfig.integrationId);
|
||||||
setValue(
|
setValue(
|
||||||
"isSecretRequestNotificationEnabled",
|
"isSecretRequestNotificationEnabled",
|
||||||
@@ -120,22 +189,30 @@ export const SlackIntegrationForm = ({ onClose }: Props) => {
|
|||||||
"isAccessRequestNotificationEnabled",
|
"isAccessRequestNotificationEnabled",
|
||||||
slackConfig.isAccessRequestNotificationEnabled
|
slackConfig.isAccessRequestNotificationEnabled
|
||||||
);
|
);
|
||||||
|
setValue(
|
||||||
|
"isSecretSyncErrorNotificationEnabled",
|
||||||
|
slackConfig.isSecretSyncErrorNotificationEnabled
|
||||||
|
);
|
||||||
|
|
||||||
if (slackConfig.integration === WorkflowIntegrationPlatform.SLACK) {
|
if (slackChannels) {
|
||||||
if (slackChannels) {
|
setValue(
|
||||||
setValue(
|
"secretRequestChannels",
|
||||||
"secretRequestChannels",
|
(slackConfig.secretRequestChannels || "")
|
||||||
slackConfig.secretRequestChannels
|
.split(", ")
|
||||||
.split(", ")
|
.filter((channel) => channel in slackChannelIdToName)
|
||||||
.filter((channel) => channel in slackChannelIdToName)
|
);
|
||||||
);
|
setValue(
|
||||||
setValue(
|
"accessRequestChannels",
|
||||||
"accessRequestChannels",
|
(slackConfig.accessRequestChannels || "")
|
||||||
slackConfig.accessRequestChannels
|
.split(", ")
|
||||||
.split(", ")
|
.filter((channel) => channel in slackChannelIdToName)
|
||||||
.filter((channel) => channel in slackChannelIdToName)
|
);
|
||||||
);
|
setValue(
|
||||||
}
|
"secretSyncErrorChannels",
|
||||||
|
(slackConfig.secretSyncErrorChannels || "")
|
||||||
|
.split(", ")
|
||||||
|
.filter((channel) => channel in slackChannelIdToName)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [slackConfig, slackChannels]);
|
}, [slackConfig, slackChannels]);
|
||||||
@@ -208,54 +285,14 @@ export const SlackIntegrationForm = ({ onClose }: Props) => {
|
|||||||
control={control}
|
control={control}
|
||||||
name="secretRequestChannels"
|
name="secretRequestChannels"
|
||||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
<FormControl
|
<ChannelSelector
|
||||||
label="Slack channels"
|
value={value}
|
||||||
isError={Boolean(error)}
|
onChange={onChange}
|
||||||
errorText={error?.message}
|
error={error}
|
||||||
>
|
slackChannelIdToName={slackChannelIdToName}
|
||||||
<DropdownMenu>
|
sortedSlackChannels={sortedSlackChannels}
|
||||||
<DropdownMenuTrigger asChild>
|
keyPrefix="secret-requests"
|
||||||
<Input
|
/>
|
||||||
isReadOnly
|
|
||||||
value={value
|
|
||||||
?.filter(Boolean)
|
|
||||||
.map((entry) => slackChannelIdToName[entry])
|
|
||||||
.join(", ")}
|
|
||||||
className="text-left"
|
|
||||||
/>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent
|
|
||||||
style={{
|
|
||||||
width: "var(--radix-dropdown-menu-trigger-width)",
|
|
||||||
maxHeight: "350px",
|
|
||||||
overflowY: "auto"
|
|
||||||
}}
|
|
||||||
side="bottom"
|
|
||||||
align="start"
|
|
||||||
>
|
|
||||||
{sortedSlackChannels?.map((slackChannel) => {
|
|
||||||
const isChecked = value?.includes(slackChannel.id);
|
|
||||||
return (
|
|
||||||
<DropdownMenuItem
|
|
||||||
onClick={(evt) => {
|
|
||||||
evt.preventDefault();
|
|
||||||
onChange(
|
|
||||||
isChecked
|
|
||||||
? value?.filter((el: string) => el !== slackChannel.id)
|
|
||||||
: [...(value || []), slackChannel.id]
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
key={`secret-requests-slack-channel-${slackChannel.id}`}
|
|
||||||
iconPos="right"
|
|
||||||
icon={isChecked && <FontAwesomeIcon icon={faCheckCircle} />}
|
|
||||||
>
|
|
||||||
{slackChannel.name}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
</FormControl>
|
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -281,54 +318,47 @@ export const SlackIntegrationForm = ({ onClose }: Props) => {
|
|||||||
control={control}
|
control={control}
|
||||||
name="accessRequestChannels"
|
name="accessRequestChannels"
|
||||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
<FormControl
|
<ChannelSelector
|
||||||
label="Slack channels"
|
value={value}
|
||||||
isError={Boolean(error)}
|
onChange={onChange}
|
||||||
errorText={error?.message}
|
error={error}
|
||||||
>
|
slackChannelIdToName={slackChannelIdToName}
|
||||||
<DropdownMenu>
|
sortedSlackChannels={sortedSlackChannels}
|
||||||
<DropdownMenuTrigger asChild>
|
keyPrefix="access-requests"
|
||||||
<Input
|
/>
|
||||||
isReadOnly
|
)}
|
||||||
value={value
|
/>
|
||||||
?.filter(Boolean)
|
)}
|
||||||
.map((entry) => slackChannelIdToName[entry])
|
<Controller
|
||||||
.join(", ")}
|
control={control}
|
||||||
className="text-left"
|
name="isSecretSyncErrorNotificationEnabled"
|
||||||
/>
|
render={({ field, fieldState: { error } }) => {
|
||||||
</DropdownMenuTrigger>
|
return (
|
||||||
<DropdownMenuContent
|
<FormControl isError={Boolean(error)} errorText={error?.message} className="mb-2">
|
||||||
style={{
|
<Switch
|
||||||
width: "var(--radix-dropdown-menu-trigger-width)",
|
id="secret-sync-error-notification"
|
||||||
maxHeight: "350px",
|
onCheckedChange={(value) => field.onChange(value)}
|
||||||
overflowY: "auto"
|
isChecked={field.value}
|
||||||
}}
|
>
|
||||||
side="bottom"
|
<p className="w-full">Secret Sync Errors</p>
|
||||||
align="start"
|
</Switch>
|
||||||
>
|
|
||||||
{sortedSlackChannels?.map((slackChannel) => {
|
|
||||||
const isChecked = value?.includes(slackChannel.id);
|
|
||||||
return (
|
|
||||||
<DropdownMenuItem
|
|
||||||
onClick={(evt) => {
|
|
||||||
evt.preventDefault();
|
|
||||||
onChange(
|
|
||||||
isChecked
|
|
||||||
? value?.filter((el: string) => el !== slackChannel.id)
|
|
||||||
: [...(value || []), slackChannel.id]
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
key={`access-requests-slack-channel-${slackChannel.id}`}
|
|
||||||
iconPos="right"
|
|
||||||
icon={isChecked && <FontAwesomeIcon icon={faCheckCircle} />}
|
|
||||||
>
|
|
||||||
{slackChannel.name}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{secretSyncErrorNotifState && (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="secretSyncErrorChannels"
|
||||||
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
|
<ChannelSelector
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
error={error}
|
||||||
|
slackChannelIdToName={slackChannelIdToName}
|
||||||
|
sortedSlackChannels={sortedSlackChannels}
|
||||||
|
keyPrefix="secret-sync-errors"
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user