From ecca51b1d60f6294985218b30920fd086009582c Mon Sep 17 00:00:00 2001 From: Victor Santos Date: Fri, 24 Oct 2025 18:04:51 -0300 Subject: [PATCH] feat(slack-integration): add support for secret sync error notifications and enhance notification handling --- ...re-slack-secret-sync-error-notification.ts | 29 +++++ .../src/db/schemas/project-slack-configs.ts | 4 +- .../notification-handlers/microsoft-teams.ts | 92 ++++++++++++++++ .../notification-handlers/slack.ts | 80 ++++++++++++++ .../trigger-notification.ts | 104 ++++-------------- .../src/lib/workflow-integrations/types.ts | 13 ++- .../project-microsoft-teams-config-dal.ts | 8 +- .../services/secret-sync/secret-sync-queue.ts | 88 ++++++++++----- .../slack/project-slack-config-dal.ts | 5 +- backend/src/services/slack/slack-fns.ts | 65 ++++++++++- 10 files changed, 367 insertions(+), 121 deletions(-) create mode 100644 backend/src/db/migrations/20251024184713_feature-slack-secret-sync-error-notification.ts create mode 100644 backend/src/lib/workflow-integrations/notification-handlers/microsoft-teams.ts create mode 100644 backend/src/lib/workflow-integrations/notification-handlers/slack.ts diff --git a/backend/src/db/migrations/20251024184713_feature-slack-secret-sync-error-notification.ts b/backend/src/db/migrations/20251024184713_feature-slack-secret-sync-error-notification.ts new file mode 100644 index 000000000..4b5bf54d3 --- /dev/null +++ b/backend/src/db/migrations/20251024184713_feature-slack-secret-sync-error-notification.ts @@ -0,0 +1,29 @@ +import { Knex } from "knex"; + +export async function up(knex: Knex): Promise { + if (!(await knex.schema.hasColumn("project_slack_configs", "isSecretSyncErrorNotificationEnabled"))) { + await knex.schema.alterTable("project_slack_configs", (table) => { + table.boolean("isSecretSyncErrorNotificationEnabled").notNullable().defaultTo(false); + }); + } + + if (!(await knex.schema.hasColumn("project_slack_configs", "secretSyncErrorChannels"))) { + await knex.schema.alterTable("project_slack_configs", (table) => { + table.text("secretSyncErrorChannels").notNullable().defaultTo(""); + }); + } +} + +export async function down(knex: Knex): Promise { + if (await knex.schema.hasColumn("project_slack_configs", "isSecretSyncErrorNotificationEnabled")) { + await knex.schema.alterTable("project_slack_configs", (table) => { + table.dropColumn("isSecretSyncErrorNotificationEnabled"); + }); + } + + if (await knex.schema.hasColumn("project_slack_configs", "secretSyncErrorChannels")) { + await knex.schema.alterTable("project_slack_configs", (table) => { + table.dropColumn("secretSyncErrorChannels"); + }); + } +} diff --git a/backend/src/db/schemas/project-slack-configs.ts b/backend/src/db/schemas/project-slack-configs.ts index 0a46e5aae..48674ec8f 100644 --- a/backend/src/db/schemas/project-slack-configs.ts +++ b/backend/src/db/schemas/project-slack-configs.ts @@ -16,7 +16,9 @@ export const ProjectSlackConfigsSchema = z.object({ isSecretRequestNotificationEnabled: z.boolean().default(false), secretRequestChannels: z.string().default(""), createdAt: z.date(), - updatedAt: z.date() + updatedAt: z.date(), + isSecretSyncErrorNotificationEnabled: z.boolean().default(false), + secretSyncErrorChannels: z.string().default("") }); export type TProjectSlackConfigs = z.infer; diff --git a/backend/src/lib/workflow-integrations/notification-handlers/microsoft-teams.ts b/backend/src/lib/workflow-integrations/notification-handlers/microsoft-teams.ts new file mode 100644 index 000000000..0cfe28491 --- /dev/null +++ b/backend/src/lib/workflow-integrations/notification-handlers/microsoft-teams.ts @@ -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; +}): Promise => { + 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; + microsoftTeamsService: Pick; +}): Promise => { + 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}`); + } +}; diff --git a/backend/src/lib/workflow-integrations/notification-handlers/slack.ts b/backend/src/lib/workflow-integrations/notification-handlers/slack.ts new file mode 100644 index 000000000..66fd58b3a --- /dev/null +++ b/backend/src/lib/workflow-integrations/notification-handlers/slack.ts @@ -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; +}): Promise => { + let targetChannelIds: string[] = []; + let isEnabled = false; + + switch (notification.type) { + case TriggerFeature.ACCESS_REQUEST: + case TriggerFeature.ACCESS_REQUEST_UPDATED: + targetChannelIds = slackConfig.accessRequestChannels?.split(", ") || []; + isEnabled = slackConfig.isAccessRequestNotificationEnabled; + break; + case TriggerFeature.SECRET_APPROVAL: + targetChannelIds = slackConfig.secretRequestChannels?.split(", ") || []; + isEnabled = slackConfig.isSecretRequestNotificationEnabled; + break; + case TriggerFeature.SECRET_SYNC_ERROR: + targetChannelIds = slackConfig.secretSyncErrorChannels?.split(", ") || []; + 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; + kmsService: Pick; +}): Promise => { + 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}`); + } +}; diff --git a/backend/src/lib/workflow-integrations/trigger-notification.ts b/backend/src/lib/workflow-integrations/trigger-notification.ts index 355761f73..2dc6f61fe 100644 --- a/backend/src/lib/workflow-integrations/trigger-notification.ts +++ b/backend/src/lib/workflow-integrations/trigger-notification.ts @@ -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 { 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) => { try { @@ -16,88 +15,25 @@ export const triggerWorkflowIntegrationNotification = async (dto: TTriggerWorkfl return; } - const microsoftTeamsConfig = await projectMicrosoftTeamsConfigDAL.getIntegrationDetailsByProject(projectId); - const slackConfig = await projectSlackConfigDAL.getIntegrationDetailsByProject(projectId); + const handlerPromises = [ + triggerSlackNotification({ + projectId, + notification, + orgId: project.orgId, + projectSlackConfigDAL, + kmsService + }), - if (slackConfig) { - if ( - notification.type === TriggerFeature.ACCESS_REQUEST || - notification.type === TriggerFeature.ACCESS_REQUEST_UPDATED - ) { - const targetChannelIds = slackConfig.accessRequestChannels?.split(", ") || []; - 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"); - }); - } - } - } + triggerMicrosoftTeamsNotification({ + projectId, + notification, + orgId: project.orgId, + projectMicrosoftTeamsConfigDAL, + microsoftTeamsService + }) + ]; - if (microsoftTeamsConfig) { - 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"); - }); - } - } - } - } + await Promise.allSettled(handlerPromises); } catch (error) { logger.error(error, "Error triggering workflow integration notification"); } diff --git a/backend/src/lib/workflow-integrations/types.ts b/backend/src/lib/workflow-integrations/types.ts index f8f55eadd..08f75d89c 100644 --- a/backend/src/lib/workflow-integrations/types.ts +++ b/backend/src/lib/workflow-integrations/types.ts @@ -7,7 +7,8 @@ import { TProjectSlackConfigDALFactory } from "@app/services/slack/project-slack export enum TriggerFeature { SECRET_APPROVAL = "secret-approval", 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 = @@ -51,6 +52,16 @@ export type TNotification = editorFullName?: string; editorEmail?: string; }; + } + | { + type: TriggerFeature.SECRET_SYNC_ERROR; + payload: { + syncName: string; + syncActionLabel: string; + syncDestination: string; + failureMessage: string; + syncUrl: string; + }; }; export type TTriggerWorkflowNotificationDTO = { diff --git a/backend/src/services/microsoft-teams/project-microsoft-teams-config-dal.ts b/backend/src/services/microsoft-teams/project-microsoft-teams-config-dal.ts index 918b96e89..b1f9fec02 100644 --- a/backend/src/services/microsoft-teams/project-microsoft-teams-config-dal.ts +++ b/backend/src/services/microsoft-teams/project-microsoft-teams-config-dal.ts @@ -1,16 +1,20 @@ import { Knex } from "knex"; 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"; export type TProjectMicrosoftTeamsConfigDALFactory = ReturnType; +export type TProjectMicrosoftTeamsConfigWithIntegrations = TProjectMicrosoftTeamsConfigs & TMicrosoftTeamsIntegrations; export const projectMicrosoftTeamsConfigDALFactory = (db: TDbClient) => { const projectMicrosoftTeamsConfigOrm = ormify(db, TableName.ProjectMicrosoftTeamsConfigs); const getIntegrationDetailsByProject = (projectId: string, tx?: Knex) => { - return (tx || db.replicaNode())(TableName.ProjectMicrosoftTeamsConfigs) + return (tx || db.replicaNode())( + TableName.ProjectMicrosoftTeamsConfigs + ) .join( TableName.MicrosoftTeamsIntegrations, `${TableName.ProjectMicrosoftTeamsConfigs}.microsoftTeamsIntegrationId`, diff --git a/backend/src/services/secret-sync/secret-sync-queue.ts b/backend/src/services/secret-sync/secret-sync-queue.ts index e83a0033f..acb2ea4d1 100644 --- a/backend/src/services/secret-sync/secret-sync-queue.ts +++ b/backend/src/services/secret-sync/secret-sync-queue.ts @@ -10,6 +10,8 @@ import { TLicenseServiceFactory } from "@app/ee/services/license/license-service import { KeyStorePrefixes, TKeyStoreFactory } from "@app/keystore/keystore"; import { getConfig } from "@app/lib/config/env"; 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 { SecretNameSchema } from "@app/server/lib/schemas"; 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 { 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 { NotificationType } from "../notification/notification-types"; +import { TProjectSlackConfigDALFactory } from "../slack/project-slack-config-dal"; export type TSecretSyncQueueFactory = ReturnType; @@ -104,6 +109,9 @@ type TSecretSyncQueueFactoryDep = { gatewayService: Pick; gatewayV2Service: Pick; notificationService: Pick; + projectSlackConfigDAL: Pick; + projectMicrosoftTeamsConfigDAL: Pick; + microsoftTeamsService: Pick; }; type SecretSyncActionJob = Job< @@ -147,7 +155,10 @@ export const secretSyncQueueFactory = ({ licenseService, gatewayService, gatewayV2Service, - notificationService + notificationService, + projectSlackConfigDAL, + projectMicrosoftTeamsConfigDAL, + microsoftTeamsService }: TSecretSyncQueueFactoryDep) => { const appCfg = getConfig(); @@ -923,32 +934,57 @@ export const secretSyncQueueFactory = ({ const syncPath = `/projects/secret-management/${projectId}/integrations/secret-syncs/${destination}/${secretSync.id}`; - await 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 - })) - ); + const notifications = [ + triggerWorkflowIntegrationNotification({ + input: { + notification: { + type: TriggerFeature.SECRET_SYNC_ERROR, + payload: { + syncName: name, + syncDestination, + failureMessage: failureMessage || "An unknown error occurred", + syncUrl: `${appCfg.SITE_URL}${syncPath}`, + syncActionLabel: actionLabel + } + }, + projectId: project.id + }, + 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({ - 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 Promise.allSettled(notifications); }; const queueSecretSyncsSyncSecretsByPath = async ({ diff --git a/backend/src/services/slack/project-slack-config-dal.ts b/backend/src/services/slack/project-slack-config-dal.ts index 276442b1b..4b5b2146e 100644 --- a/backend/src/services/slack/project-slack-config-dal.ts +++ b/backend/src/services/slack/project-slack-config-dal.ts @@ -1,16 +1,17 @@ import { Knex } from "knex"; 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"; export type TProjectSlackConfigDALFactory = ReturnType; +export type TProjectSlackConfigWithIntegrations = TProjectSlackConfigs & TSlackIntegrations; export const projectSlackConfigDALFactory = (db: TDbClient) => { const projectSlackConfigOrm = ormify(db, TableName.ProjectSlackConfigs); const getIntegrationDetailsByProject = (projectId: string, tx?: Knex) => { - return (tx || db.replicaNode())(TableName.ProjectSlackConfigs) + return (tx || db.replicaNode())(TableName.ProjectSlackConfigs) .join( TableName.SlackIntegrations, `${TableName.ProjectSlackConfigs}.slackIntegrationId`, diff --git a/backend/src/services/slack/slack-fns.ts b/backend/src/services/slack/slack-fns.ts index 88db1a84d..92684a707 100644 --- a/backend/src/services/slack/slack-fns.ts +++ b/backend/src/services/slack/slack-fns.ts @@ -8,6 +8,9 @@ import { TNotification, TriggerFeature } from "@app/lib/workflow-integrations/ty import { KmsDataKey } from "../kms/kms-types"; import { TSendSlackNotificationDTO } from "./slack-types"; +const COMPANY_BRAND_COLOR = "#e0ed34"; +const ERROR_COLOR = "#e74c3c"; + export const fetchSlackChannels = async (botKey: string) => { const slackChannels: { name: string; @@ -74,7 +77,8 @@ View the complete details <${appCfg.SITE_URL}/projects/secret-management/${paylo return { payloadMessage: messageBody, - payloadBlocks + payloadBlocks, + color: COMPANY_BRAND_COLOR }; } case TriggerFeature.ACCESS_REQUEST: { @@ -112,7 +116,8 @@ User Note: ${payload.note}` return { payloadMessage: messageBody, - payloadBlocks + payloadBlocks, + color: COMPANY_BRAND_COLOR }; } case TriggerFeature.ACCESS_REQUEST_UPDATED: { @@ -150,7 +155,52 @@ Editor Note: ${payload.editNote}` return { payloadMessage: messageBody, - payloadBlocks + payloadBlocks, + color: COMPANY_BRAND_COLOR + }; + } + case TriggerFeature.SECRET_SYNC_ERROR: { + const { payload } = notification; + const messageBody = `${payload.syncName} for ${payload.syncDestination} failed on ${payload.syncActionLabel} + +Sync Error: ${payload.failureMessage}`; + + const payloadBlocks = [ + { + type: "header", + text: { + type: "plain_text", + text: `${payload.syncName} for ${payload.syncDestination} failed on ${payload.syncActionLabel}`, + emoji: true + } + }, + { + type: "section", + text: { + type: "mrkdwn", + text: `*Sync Error:* ${payload.failureMessage}` + } + }, + { + type: "actions", + elements: [ + { + type: "button", + text: { + type: "plain_text", + text: `Open ${payload.syncName}`, + emoji: true + }, + url: payload.syncUrl + } + ] + } + ]; + + return { + payloadMessage: messageBody, + payloadBlocks, + color: ERROR_COLOR }; } default: { @@ -177,7 +227,7 @@ export const sendSlackNotification = async ({ }).toString("utf8"); const slackWebClient = new WebClient(botKey); - const { payloadMessage, payloadBlocks } = buildSlackPayload(notification); + const { payloadMessage, payloadBlocks, color } = buildSlackPayload(notification); for await (const conversationId of targetChannelIds) { // we send both text and blocks for compatibility with barebone clients @@ -185,7 +235,12 @@ export const sendSlackNotification = async ({ .postMessage({ channel: conversationId, text: payloadMessage, - blocks: payloadBlocks + attachments: [ + { + color, + blocks: payloadBlocks + } + ] }) .catch((err) => logger.error(err)); }