mirror of
https://github.com/Infisical/infisical.git
synced 2026-01-09 15:38:03 -05:00
feat: adds govslack support
This commit is contained in:
@@ -0,0 +1,54 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
if (await knex.schema.hasTable(TableName.SuperAdmin)) {
|
||||||
|
const hasGovSlackClientId = await knex.schema.hasColumn(TableName.SuperAdmin, "encryptedGovSlackClientId");
|
||||||
|
const hasGovSlackClientSecret = await knex.schema.hasColumn(TableName.SuperAdmin, "encryptedGovSlackClientSecret");
|
||||||
|
|
||||||
|
await knex.schema.alterTable(TableName.SuperAdmin, (t) => {
|
||||||
|
if (!hasGovSlackClientId) {
|
||||||
|
t.binary("encryptedGovSlackClientId").nullable();
|
||||||
|
}
|
||||||
|
if (!hasGovSlackClientSecret) {
|
||||||
|
t.binary("encryptedGovSlackClientSecret").nullable();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await knex.schema.hasTable(TableName.SlackIntegrations)) {
|
||||||
|
const hasIsGovSlack = await knex.schema.hasColumn(TableName.SlackIntegrations, "isGovSlack");
|
||||||
|
|
||||||
|
if (!hasIsGovSlack) {
|
||||||
|
await knex.schema.alterTable(TableName.SlackIntegrations, (t) => {
|
||||||
|
t.boolean("isGovSlack").defaultTo(false).notNullable();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
if (await knex.schema.hasTable(TableName.SuperAdmin)) {
|
||||||
|
const hasGovSlackClientId = await knex.schema.hasColumn(TableName.SuperAdmin, "encryptedGovSlackClientId");
|
||||||
|
const hasGovSlackClientSecret = await knex.schema.hasColumn(TableName.SuperAdmin, "encryptedGovSlackClientSecret");
|
||||||
|
|
||||||
|
await knex.schema.alterTable(TableName.SuperAdmin, (t) => {
|
||||||
|
if (hasGovSlackClientId) {
|
||||||
|
t.dropColumn("encryptedGovSlackClientId");
|
||||||
|
}
|
||||||
|
if (hasGovSlackClientSecret) {
|
||||||
|
t.dropColumn("encryptedGovSlackClientSecret");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (await knex.schema.hasTable(TableName.SlackIntegrations)) {
|
||||||
|
const hasIsGovSlack = await knex.schema.hasColumn(TableName.SlackIntegrations, "isGovSlack");
|
||||||
|
|
||||||
|
if (hasIsGovSlack) {
|
||||||
|
await knex.schema.alterTable(TableName.SlackIntegrations, (t) => {
|
||||||
|
t.dropColumn("isGovSlack");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,7 +19,8 @@ export const SlackIntegrationsSchema = z.object({
|
|||||||
slackBotId: z.string(),
|
slackBotId: z.string(),
|
||||||
slackBotUserId: z.string(),
|
slackBotUserId: z.string(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date()
|
updatedAt: z.date(),
|
||||||
|
isGovSlack: z.boolean().default(false)
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TSlackIntegrations = z.infer<typeof SlackIntegrationsSchema>;
|
export type TSlackIntegrations = z.infer<typeof SlackIntegrationsSchema>;
|
||||||
|
|||||||
@@ -36,7 +36,9 @@ export const SuperAdminSchema = z.object({
|
|||||||
encryptedGitHubAppConnectionId: zodBuffer.nullable().optional(),
|
encryptedGitHubAppConnectionId: zodBuffer.nullable().optional(),
|
||||||
encryptedGitHubAppConnectionPrivateKey: zodBuffer.nullable().optional(),
|
encryptedGitHubAppConnectionPrivateKey: zodBuffer.nullable().optional(),
|
||||||
encryptedEnvOverrides: zodBuffer.nullable().optional(),
|
encryptedEnvOverrides: zodBuffer.nullable().optional(),
|
||||||
fipsEnabled: z.boolean().default(false)
|
fipsEnabled: z.boolean().default(false),
|
||||||
|
encryptedGovSlackClientId: zodBuffer.nullable().optional(),
|
||||||
|
encryptedGovSlackClientSecret: zodBuffer.nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TSuperAdmin = z.infer<typeof SuperAdminSchema>;
|
export type TSuperAdmin = z.infer<typeof SuperAdminSchema>;
|
||||||
|
|||||||
@@ -246,8 +246,8 @@ const envSchema = z
|
|||||||
),
|
),
|
||||||
WORKFLOW_SLACK_CLIENT_ID: zpStr(z.string().optional()),
|
WORKFLOW_SLACK_CLIENT_ID: zpStr(z.string().optional()),
|
||||||
WORKFLOW_SLACK_CLIENT_SECRET: zpStr(z.string().optional()),
|
WORKFLOW_SLACK_CLIENT_SECRET: zpStr(z.string().optional()),
|
||||||
WORKFLOW_SLACK_GOV_ENABLED: zodStrBool.default("false"),
|
WORKFLOW_GOV_SLACK_CLIENT_ID: zpStr(z.string().optional()),
|
||||||
WORKFLOW_SLACK_GOV_BASE_URL: zpStr(z.string().optional().default("https://slack-gov.com")),
|
WORKFLOW_GOV_SLACK_CLIENT_SECRET: zpStr(z.string().optional()),
|
||||||
ENABLE_MSSQL_SECRET_ROTATION_ENCRYPT: zodStrBool.default("true"),
|
ENABLE_MSSQL_SECRET_ROTATION_ENCRYPT: zodStrBool.default("true"),
|
||||||
|
|
||||||
// Special Detection Feature
|
// Special Detection Feature
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
|
|||||||
updatedAt: true,
|
updatedAt: true,
|
||||||
encryptedSlackClientId: true,
|
encryptedSlackClientId: true,
|
||||||
encryptedSlackClientSecret: true,
|
encryptedSlackClientSecret: true,
|
||||||
|
encryptedGovSlackClientId: true,
|
||||||
|
encryptedGovSlackClientSecret: true,
|
||||||
encryptedMicrosoftTeamsAppId: true,
|
encryptedMicrosoftTeamsAppId: true,
|
||||||
encryptedMicrosoftTeamsClientSecret: true,
|
encryptedMicrosoftTeamsClientSecret: true,
|
||||||
encryptedMicrosoftTeamsBotId: true,
|
encryptedMicrosoftTeamsBotId: true,
|
||||||
@@ -107,6 +109,8 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
|
|||||||
}),
|
}),
|
||||||
slackClientId: z.string().optional(),
|
slackClientId: z.string().optional(),
|
||||||
slackClientSecret: z.string().optional(),
|
slackClientSecret: z.string().optional(),
|
||||||
|
govSlackClientId: z.string().optional(),
|
||||||
|
govSlackClientSecret: z.string().optional(),
|
||||||
microsoftTeamsAppId: z.string().optional(),
|
microsoftTeamsAppId: z.string().optional(),
|
||||||
microsoftTeamsClientSecret: z.string().optional(),
|
microsoftTeamsClientSecret: z.string().optional(),
|
||||||
microsoftTeamsBotId: z.string().optional(),
|
microsoftTeamsBotId: z.string().optional(),
|
||||||
@@ -374,8 +378,11 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
|
|||||||
200: z.object({
|
200: z.object({
|
||||||
slack: z.object({
|
slack: z.object({
|
||||||
clientId: z.string(),
|
clientId: z.string(),
|
||||||
clientSecret: z.string(),
|
clientSecret: z.string()
|
||||||
govEnabled: z.boolean()
|
}),
|
||||||
|
govSlack: z.object({
|
||||||
|
clientId: z.string(),
|
||||||
|
clientSecret: z.string()
|
||||||
}),
|
}),
|
||||||
microsoftTeams: z.object({
|
microsoftTeams: z.object({
|
||||||
appId: z.string(),
|
appId: z.string(),
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import { slugSchema } from "@app/server/lib/schemas";
|
|||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
|
import { booleanSchema } from "../sanitizedSchemas";
|
||||||
|
|
||||||
const sanitizedSlackIntegrationSchema = WorkflowIntegrationsSchema.pick({
|
const sanitizedSlackIntegrationSchema = WorkflowIntegrationsSchema.pick({
|
||||||
id: true,
|
id: true,
|
||||||
description: true,
|
description: true,
|
||||||
@@ -36,7 +38,8 @@ export const registerSlackRouter = async (server: FastifyZodProvider) => {
|
|||||||
],
|
],
|
||||||
querystring: z.object({
|
querystring: z.object({
|
||||||
slug: slugSchema({ max: 64 }),
|
slug: slugSchema({ max: 64 }),
|
||||||
description: z.string().optional()
|
description: z.string().optional(),
|
||||||
|
isGovSlack: booleanSchema.default(false).optional()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.string()
|
200: z.string()
|
||||||
@@ -337,4 +340,24 @@ export const registerSlackRouter = async (server: FastifyZodProvider) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/oauth_redirect_gov",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
handler: async (req, res) => {
|
||||||
|
const installer = await server.services.slack.getSlackInstaller(true);
|
||||||
|
|
||||||
|
return installer.handleCallback(req.raw, res.raw, {
|
||||||
|
failureAsync: async () => {
|
||||||
|
return res.redirect(appCfg.SITE_URL as string);
|
||||||
|
},
|
||||||
|
successAsync: async () => {
|
||||||
|
return res.redirect(`${appCfg.SITE_URL}/organization/settings?selectedTab=workflow-integrations`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
1
backend/src/services/slack/slack-constants.ts
Normal file
1
backend/src/services/slack/slack-constants.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export const SLACK_GOV_BASE_URL = "https://slack-gov.com";
|
||||||
@@ -6,13 +6,13 @@ import { logger } from "@app/lib/logger";
|
|||||||
import { TNotification, TriggerFeature } from "@app/lib/workflow-integrations/types";
|
import { TNotification, TriggerFeature } from "@app/lib/workflow-integrations/types";
|
||||||
|
|
||||||
import { KmsDataKey } from "../kms/kms-types";
|
import { KmsDataKey } from "../kms/kms-types";
|
||||||
|
import { SLACK_GOV_BASE_URL } from "./slack-constants";
|
||||||
import { TSendSlackNotificationDTO } from "./slack-types";
|
import { TSendSlackNotificationDTO } from "./slack-types";
|
||||||
|
|
||||||
const COMPANY_BRAND_COLOR = "#e0ed34";
|
const COMPANY_BRAND_COLOR = "#e0ed34";
|
||||||
const ERROR_COLOR = "#e74c3c";
|
const ERROR_COLOR = "#e74c3c";
|
||||||
|
|
||||||
export const fetchSlackChannels = async (botKey: string) => {
|
export const fetchSlackChannels = async (botKey: string, isGovSlack = false) => {
|
||||||
const appCfg = getConfig();
|
|
||||||
const slackChannels: {
|
const slackChannels: {
|
||||||
name: string;
|
name: string;
|
||||||
id: string;
|
id: string;
|
||||||
@@ -20,9 +20,8 @@ export const fetchSlackChannels = async (botKey: string) => {
|
|||||||
|
|
||||||
const webClientOptions: WebClientOptions = {};
|
const webClientOptions: WebClientOptions = {};
|
||||||
|
|
||||||
if (appCfg.WORKFLOW_SLACK_GOV_ENABLED) {
|
if (isGovSlack) {
|
||||||
const govBaseUrl = appCfg.WORKFLOW_SLACK_GOV_BASE_URL;
|
webClientOptions.slackApiUrl = `${SLACK_GOV_BASE_URL}/api`;
|
||||||
webClientOptions.slackApiUrl = `${govBaseUrl}/api`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const slackWebClient = new WebClient(botKey, webClientOptions);
|
const slackWebClient = new WebClient(botKey, webClientOptions);
|
||||||
@@ -277,7 +276,6 @@ export const sendSlackNotification = async ({
|
|||||||
targetChannelIds,
|
targetChannelIds,
|
||||||
slackIntegration
|
slackIntegration
|
||||||
}: TSendSlackNotificationDTO) => {
|
}: TSendSlackNotificationDTO) => {
|
||||||
const appCfg = getConfig();
|
|
||||||
const { decryptor: orgDataKeyDecryptor } = await kmsService.createCipherPairWithDataKey({
|
const { decryptor: orgDataKeyDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
type: KmsDataKey.Organization,
|
type: KmsDataKey.Organization,
|
||||||
orgId
|
orgId
|
||||||
@@ -288,9 +286,8 @@ export const sendSlackNotification = async ({
|
|||||||
|
|
||||||
const webClientOptions: WebClientOptions = {};
|
const webClientOptions: WebClientOptions = {};
|
||||||
|
|
||||||
if (appCfg.WORKFLOW_SLACK_GOV_ENABLED) {
|
if (slackIntegration.isGovSlack) {
|
||||||
const govBaseUrl = appCfg.WORKFLOW_SLACK_GOV_BASE_URL;
|
webClientOptions.slackApiUrl = `${SLACK_GOV_BASE_URL}/api`;
|
||||||
webClientOptions.slackApiUrl = `${govBaseUrl}/api`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const slackWebClient = new WebClient(botKey, webClientOptions);
|
const slackWebClient = new WebClient(botKey, webClientOptions);
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { KmsDataKey } from "../kms/kms-types";
|
|||||||
import { getServerCfg } from "../super-admin/super-admin-service";
|
import { getServerCfg } from "../super-admin/super-admin-service";
|
||||||
import { TWorkflowIntegrationDALFactory } from "../workflow-integration/workflow-integration-dal";
|
import { TWorkflowIntegrationDALFactory } from "../workflow-integration/workflow-integration-dal";
|
||||||
import { WorkflowIntegration } from "../workflow-integration/workflow-integration-types";
|
import { WorkflowIntegration } from "../workflow-integration/workflow-integration-types";
|
||||||
|
import { SLACK_GOV_BASE_URL } from "./slack-constants";
|
||||||
import { fetchSlackChannels } from "./slack-fns";
|
import { fetchSlackChannels } from "./slack-fns";
|
||||||
import { TSlackIntegrationDALFactory } from "./slack-integration-dal";
|
import { TSlackIntegrationDALFactory } from "./slack-integration-dal";
|
||||||
import {
|
import {
|
||||||
@@ -58,7 +59,8 @@ export const slackServiceFactory = ({
|
|||||||
slackAppId,
|
slackAppId,
|
||||||
botAccessToken,
|
botAccessToken,
|
||||||
slackBotId,
|
slackBotId,
|
||||||
slackBotUserId
|
slackBotUserId,
|
||||||
|
isGovSlack = false
|
||||||
}: TCompleteSlackIntegrationDTO) => {
|
}: TCompleteSlackIntegrationDTO) => {
|
||||||
const { encryptor: orgDataKeyEncryptor } = await kmsService.createCipherPairWithDataKey({
|
const { encryptor: orgDataKeyEncryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
orgId,
|
orgId,
|
||||||
@@ -90,7 +92,8 @@ export const slackServiceFactory = ({
|
|||||||
slackAppId,
|
slackAppId,
|
||||||
slackBotId,
|
slackBotId,
|
||||||
slackBotUserId,
|
slackBotUserId,
|
||||||
encryptedBotAccessToken
|
encryptedBotAccessToken,
|
||||||
|
isGovSlack
|
||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
@@ -135,29 +138,44 @@ export const slackServiceFactory = ({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const getSlackInstaller = async () => {
|
const getSlackInstaller = async (isGovSlack = false) => {
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
const serverCfg = await getServerCfg();
|
const serverCfg = await getServerCfg();
|
||||||
|
|
||||||
let slackClientId = appCfg.WORKFLOW_SLACK_CLIENT_ID as string;
|
let slackClientId = "";
|
||||||
let slackClientSecret = appCfg.WORKFLOW_SLACK_CLIENT_SECRET as string;
|
let slackClientSecret = "";
|
||||||
|
|
||||||
const decrypt = kmsService.decryptWithRootKey();
|
const decrypt = kmsService.decryptWithRootKey();
|
||||||
|
|
||||||
if (serverCfg.encryptedSlackClientId) {
|
if (isGovSlack) {
|
||||||
slackClientId = decrypt(Buffer.from(serverCfg.encryptedSlackClientId)).toString();
|
slackClientId = appCfg.WORKFLOW_GOV_SLACK_CLIENT_ID as string;
|
||||||
}
|
slackClientSecret = appCfg.WORKFLOW_GOV_SLACK_CLIENT_SECRET as string;
|
||||||
|
|
||||||
if (serverCfg.encryptedSlackClientSecret) {
|
if (serverCfg.encryptedGovSlackClientId) {
|
||||||
slackClientSecret = decrypt(Buffer.from(serverCfg.encryptedSlackClientSecret)).toString();
|
slackClientId = decrypt(Buffer.from(serverCfg.encryptedGovSlackClientId)).toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (serverCfg.encryptedGovSlackClientSecret) {
|
||||||
|
slackClientSecret = decrypt(Buffer.from(serverCfg.encryptedGovSlackClientSecret)).toString();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
slackClientId = appCfg.WORKFLOW_SLACK_CLIENT_ID as string;
|
||||||
|
slackClientSecret = appCfg.WORKFLOW_SLACK_CLIENT_SECRET as string;
|
||||||
|
|
||||||
|
if (serverCfg.encryptedSlackClientId) {
|
||||||
|
slackClientId = decrypt(Buffer.from(serverCfg.encryptedSlackClientId)).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (serverCfg.encryptedSlackClientSecret) {
|
||||||
|
slackClientSecret = decrypt(Buffer.from(serverCfg.encryptedSlackClientSecret)).toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
if (!slackClientId || !slackClientSecret) {
|
if (!slackClientId || !slackClientSecret) {
|
||||||
throw new BadRequestError({
|
throw new BadRequestError({
|
||||||
message: `Invalid Slack configuration. ${
|
message: `Invalid ${isGovSlack ? "GovSlack" : "Slack"} configuration. ${
|
||||||
appCfg.isCloud
|
appCfg.isCloud
|
||||||
? "Please contact the Infisical team."
|
? "Please contact the Infisical team."
|
||||||
: "Contact your instance admin to setup Slack integration in the Admin settings. Your configuration is missing Slack client ID and secret."
|
: `Contact your instance admin to setup Slack integration in the Admin settings. Your configuration is missing ${isGovSlack ? "GovSlack" : "Slack"} client ID and secret.`
|
||||||
}`
|
}`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -180,6 +198,7 @@ export const slackServiceFactory = ({
|
|||||||
orgId: string;
|
orgId: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
|
isGovSlack?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
if (metadata.id) {
|
if (metadata.id) {
|
||||||
@@ -205,7 +224,8 @@ export const slackServiceFactory = ({
|
|||||||
slackAppId: installation.appId || "",
|
slackAppId: installation.appId || "",
|
||||||
botAccessToken: installation.bot?.token || "",
|
botAccessToken: installation.bot?.token || "",
|
||||||
slackBotId: installation.bot?.id || "",
|
slackBotId: installation.bot?.id || "",
|
||||||
slackBotUserId: installation.bot?.userId || ""
|
slackBotUserId: installation.bot?.userId || "",
|
||||||
|
isGovSlack: metadata.isGovSlack
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
// for our use-case we don't need to implement this because this will only be used
|
// for our use-case we don't need to implement this because this will only be used
|
||||||
@@ -220,11 +240,10 @@ export const slackServiceFactory = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (appCfg.WORKFLOW_SLACK_GOV_ENABLED) {
|
if (isGovSlack) {
|
||||||
const govBaseUrl = appCfg.WORKFLOW_SLACK_GOV_BASE_URL;
|
installProviderOptions.authorizationUrl = `${SLACK_GOV_BASE_URL}/oauth/v2/authorize`;
|
||||||
installProviderOptions.authorizationUrl = `${govBaseUrl}/oauth/v2/authorize`;
|
|
||||||
installProviderOptions.clientOptions = {
|
installProviderOptions.clientOptions = {
|
||||||
slackApiUrl: `${govBaseUrl}/api`
|
slackApiUrl: `${SLACK_GOV_BASE_URL}/api`
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -237,7 +256,8 @@ export const slackServiceFactory = ({
|
|||||||
actorOrgId,
|
actorOrgId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
slug,
|
slug,
|
||||||
description
|
description,
|
||||||
|
isGovSlack = false
|
||||||
}: TGetSlackInstallUrlDTO) => {
|
}: TGetSlackInstallUrlDTO) => {
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
|
|
||||||
@@ -252,15 +272,16 @@ export const slackServiceFactory = ({
|
|||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Settings);
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Settings);
|
||||||
|
|
||||||
const installer = await getSlackInstaller();
|
const installer = await getSlackInstaller(isGovSlack);
|
||||||
const url = await installer.generateInstallUrl({
|
const url = await installer.generateInstallUrl({
|
||||||
scopes: ["chat:write.public", "chat:write", "channels:read", "groups:read"],
|
scopes: ["chat:write.public", "chat:write", "channels:read", "groups:read"],
|
||||||
metadata: JSON.stringify({
|
metadata: JSON.stringify({
|
||||||
slug,
|
slug,
|
||||||
description,
|
description,
|
||||||
orgId: actorOrgId
|
orgId: actorOrgId,
|
||||||
|
isGovSlack
|
||||||
}),
|
}),
|
||||||
redirectUri: `${appCfg.SITE_URL}/api/v1/workflow-integrations/slack/oauth_redirect`
|
redirectUri: `${appCfg.SITE_URL}/api/v1/workflow-integrations/slack/oauth_redirect${isGovSlack ? "_gov" : ""}`
|
||||||
});
|
});
|
||||||
|
|
||||||
return url;
|
return url;
|
||||||
@@ -287,14 +308,15 @@ export const slackServiceFactory = ({
|
|||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Settings);
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Settings);
|
||||||
|
|
||||||
const installer = await getSlackInstaller();
|
const installer = await getSlackInstaller(slackIntegration.isGovSlack);
|
||||||
const url = await installer.generateInstallUrl({
|
const url = await installer.generateInstallUrl({
|
||||||
scopes: ["chat:write.public", "chat:write", "channels:read", "groups:read"],
|
scopes: ["chat:write.public", "chat:write", "channels:read", "groups:read"],
|
||||||
metadata: JSON.stringify({
|
metadata: JSON.stringify({
|
||||||
id,
|
id,
|
||||||
orgId: slackIntegration.orgId
|
orgId: slackIntegration.orgId,
|
||||||
|
isGovSlack: slackIntegration.isGovSlack
|
||||||
}),
|
}),
|
||||||
redirectUri: `${appCfg.SITE_URL}/api/v1/workflow-integrations/slack/oauth_redirect`
|
redirectUri: `${appCfg.SITE_URL}/api/v1/workflow-integrations/slack/oauth_redirect${slackIntegration.isGovSlack ? "_gov" : ""}`
|
||||||
});
|
});
|
||||||
|
|
||||||
return url;
|
return url;
|
||||||
@@ -386,7 +408,7 @@ export const slackServiceFactory = ({
|
|||||||
cipherTextBlob: slackIntegration.encryptedBotAccessToken
|
cipherTextBlob: slackIntegration.encryptedBotAccessToken
|
||||||
}).toString("utf8");
|
}).toString("utf8");
|
||||||
|
|
||||||
return fetchSlackChannels(botKey);
|
return fetchSlackChannels(botKey, slackIntegration.isGovSlack);
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateSlackIntegration = async ({
|
const updateSlackIntegration = async ({
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { TKmsServiceFactory } from "../kms/kms-service";
|
|||||||
export type TGetSlackInstallUrlDTO = {
|
export type TGetSlackInstallUrlDTO = {
|
||||||
slug: string;
|
slug: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
|
isGovSlack?: boolean;
|
||||||
} & Omit<TOrgPermission, "orgId">;
|
} & Omit<TOrgPermission, "orgId">;
|
||||||
|
|
||||||
export type TGetReinstallUrlDTO = {
|
export type TGetReinstallUrlDTO = {
|
||||||
@@ -39,6 +40,7 @@ export type TCompleteSlackIntegrationDTO = {
|
|||||||
botAccessToken: string;
|
botAccessToken: string;
|
||||||
slackBotId: string;
|
slackBotId: string;
|
||||||
slackBotUserId: string;
|
slackBotUserId: string;
|
||||||
|
isGovSlack?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TReinstallSlackIntegrationDTO = {
|
export type TReinstallSlackIntegrationDTO = {
|
||||||
|
|||||||
@@ -101,6 +101,10 @@ let adminIntegrationsConfig: TAdminIntegrationConfig = {
|
|||||||
clientSecret: "",
|
clientSecret: "",
|
||||||
clientId: ""
|
clientId: ""
|
||||||
},
|
},
|
||||||
|
govSlack: {
|
||||||
|
clientSecret: "",
|
||||||
|
clientId: ""
|
||||||
|
},
|
||||||
microsoftTeams: {
|
microsoftTeams: {
|
||||||
appId: "",
|
appId: "",
|
||||||
clientSecret: "",
|
clientSecret: "",
|
||||||
@@ -207,6 +211,13 @@ export const superAdminServiceFactory = ({
|
|||||||
? decrypt(serverCfg.encryptedSlackClientSecret).toString()
|
? decrypt(serverCfg.encryptedSlackClientSecret).toString()
|
||||||
: "";
|
: "";
|
||||||
|
|
||||||
|
const govSlackClientId = serverCfg.encryptedGovSlackClientId
|
||||||
|
? decrypt(serverCfg.encryptedGovSlackClientId).toString()
|
||||||
|
: "";
|
||||||
|
const govSlackClientSecret = serverCfg.encryptedGovSlackClientSecret
|
||||||
|
? decrypt(serverCfg.encryptedGovSlackClientSecret).toString()
|
||||||
|
: "";
|
||||||
|
|
||||||
const microsoftAppId = serverCfg.encryptedMicrosoftTeamsAppId
|
const microsoftAppId = serverCfg.encryptedMicrosoftTeamsAppId
|
||||||
? decrypt(serverCfg.encryptedMicrosoftTeamsAppId).toString()
|
? decrypt(serverCfg.encryptedMicrosoftTeamsAppId).toString()
|
||||||
: "";
|
: "";
|
||||||
@@ -235,13 +246,14 @@ export const superAdminServiceFactory = ({
|
|||||||
? decrypt(serverCfg.encryptedGitHubAppConnectionPrivateKey).toString()
|
? decrypt(serverCfg.encryptedGitHubAppConnectionPrivateKey).toString()
|
||||||
: "";
|
: "";
|
||||||
|
|
||||||
const appCfg = getConfig();
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
slack: {
|
slack: {
|
||||||
clientSecret: slackClientSecret,
|
clientSecret: slackClientSecret,
|
||||||
clientId: slackClientId,
|
clientId: slackClientId
|
||||||
govEnabled: appCfg.WORKFLOW_SLACK_GOV_ENABLED
|
},
|
||||||
|
govSlack: {
|
||||||
|
clientSecret: govSlackClientSecret,
|
||||||
|
clientId: govSlackClientId
|
||||||
},
|
},
|
||||||
microsoftTeams: {
|
microsoftTeams: {
|
||||||
appId: microsoftAppId,
|
appId: microsoftAppId,
|
||||||
@@ -308,6 +320,8 @@ export const superAdminServiceFactory = ({
|
|||||||
data: TSuperAdminUpdate & {
|
data: TSuperAdminUpdate & {
|
||||||
slackClientId?: string;
|
slackClientId?: string;
|
||||||
slackClientSecret?: string;
|
slackClientSecret?: string;
|
||||||
|
govSlackClientId?: string;
|
||||||
|
govSlackClientSecret?: string;
|
||||||
microsoftTeamsAppId?: string;
|
microsoftTeamsAppId?: string;
|
||||||
microsoftTeamsClientSecret?: string;
|
microsoftTeamsClientSecret?: string;
|
||||||
microsoftTeamsBotId?: string;
|
microsoftTeamsBotId?: string;
|
||||||
@@ -384,6 +398,20 @@ export const superAdminServiceFactory = ({
|
|||||||
updatedData.slackClientSecret = undefined;
|
updatedData.slackClientSecret = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (data.govSlackClientId) {
|
||||||
|
const encryptedClientId = encryptWithRoot(Buffer.from(data.govSlackClientId));
|
||||||
|
|
||||||
|
updatedData.encryptedGovSlackClientId = encryptedClientId;
|
||||||
|
updatedData.govSlackClientId = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.govSlackClientSecret) {
|
||||||
|
const encryptedClientSecret = encryptWithRoot(Buffer.from(data.govSlackClientSecret));
|
||||||
|
|
||||||
|
updatedData.encryptedGovSlackClientSecret = encryptedClientSecret;
|
||||||
|
updatedData.govSlackClientSecret = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
let microsoftTeamsSettingsUpdated = false;
|
let microsoftTeamsSettingsUpdated = false;
|
||||||
if (data.microsoftTeamsAppId) {
|
if (data.microsoftTeamsAppId) {
|
||||||
const encryptedClientId = encryptWithRoot(Buffer.from(data.microsoftTeamsAppId));
|
const encryptedClientId = encryptWithRoot(Buffer.from(data.microsoftTeamsAppId));
|
||||||
|
|||||||
@@ -64,6 +64,10 @@ export type TAdminIntegrationConfig = {
|
|||||||
clientSecret: string;
|
clientSecret: string;
|
||||||
clientId: string;
|
clientId: string;
|
||||||
};
|
};
|
||||||
|
govSlack: {
|
||||||
|
clientSecret: string;
|
||||||
|
clientId: string;
|
||||||
|
};
|
||||||
microsoftTeams: {
|
microsoftTeams: {
|
||||||
appId: string;
|
appId: string;
|
||||||
clientSecret: string;
|
clientSecret: string;
|
||||||
|
|||||||
@@ -78,6 +78,8 @@ export type TServerConfig = {
|
|||||||
export type TUpdateServerConfigDTO = {
|
export type TUpdateServerConfigDTO = {
|
||||||
slackClientId?: string;
|
slackClientId?: string;
|
||||||
slackClientSecret?: string;
|
slackClientSecret?: string;
|
||||||
|
govSlackClientId?: string;
|
||||||
|
govSlackClientSecret?: string;
|
||||||
microsoftTeamsAppId?: string;
|
microsoftTeamsAppId?: string;
|
||||||
microsoftTeamsClientSecret?: string;
|
microsoftTeamsClientSecret?: string;
|
||||||
microsoftTeamsBotId?: string;
|
microsoftTeamsBotId?: string;
|
||||||
@@ -119,7 +121,10 @@ export type AdminIntegrationsConfig = {
|
|||||||
slack: {
|
slack: {
|
||||||
clientId: string;
|
clientId: string;
|
||||||
clientSecret: string;
|
clientSecret: string;
|
||||||
govEnabled: boolean;
|
};
|
||||||
|
govSlack: {
|
||||||
|
clientId: string;
|
||||||
|
clientSecret: string;
|
||||||
};
|
};
|
||||||
microsoftTeams: {
|
microsoftTeams: {
|
||||||
appId: string;
|
appId: string;
|
||||||
|
|||||||
@@ -29,15 +29,18 @@ export const workflowIntegrationKeys = {
|
|||||||
|
|
||||||
export const fetchSlackInstallUrl = async ({
|
export const fetchSlackInstallUrl = async ({
|
||||||
slug,
|
slug,
|
||||||
description
|
description,
|
||||||
|
isGovSlack
|
||||||
}: {
|
}: {
|
||||||
slug: string;
|
slug: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
|
isGovSlack?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const { data } = await apiRequest.get<string>("/api/v1/workflow-integrations/slack/install", {
|
const { data } = await apiRequest.get<string>("/api/v1/workflow-integrations/slack/install", {
|
||||||
params: {
|
params: {
|
||||||
slug,
|
slug,
|
||||||
description
|
description,
|
||||||
|
isGovSlack
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,189 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import { BsSlack } from "react-icons/bs";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import {
|
||||||
|
Accordion,
|
||||||
|
AccordionContent,
|
||||||
|
AccordionItem,
|
||||||
|
AccordionTrigger,
|
||||||
|
Button,
|
||||||
|
FormControl,
|
||||||
|
Input
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { useToggle } from "@app/hooks";
|
||||||
|
import { useUpdateServerConfig } from "@app/hooks/api";
|
||||||
|
import { AdminIntegrationsConfig } from "@app/hooks/api/admin/types";
|
||||||
|
|
||||||
|
const getCustomSlackAppCreationUrl = () =>
|
||||||
|
`https://api.slack-gov.com/apps?new_app=1&manifest_json=${encodeURIComponent(
|
||||||
|
JSON.stringify({
|
||||||
|
display_information: {
|
||||||
|
name: "Infisical",
|
||||||
|
description: "Get real-time Infisical updates in Slack",
|
||||||
|
background_color: "#c2d62b",
|
||||||
|
long_description: `This Slack application is designed specifically for use with your self-hosted Infisical instance, allowing seamless integration between your Infisical projects and your Slack workspace. With this integration, your team can stay up-to-date with the latest events, changes, and notifications directly inside Slack.
|
||||||
|
- Notifications: Receive real-time updates and alerts about critical events in your Infisical projects. Whether it's a new project being created, updates to secrets, or changes to your team's configuration, you will be promptly notified within the designated Slack channels of your choice.
|
||||||
|
- Customization: Tailor the notifications to your team's specific needs by configuring which types of events trigger alerts and in which channels they are sent.
|
||||||
|
- Collaboration: Keep your entire team in the loop with notifications that help facilitate more efficient collaboration by ensuring that everyone is aware of important developments in your Infisical projects.
|
||||||
|
|
||||||
|
By integrating Infisical with Slack, you can enhance your workflow by combining the power of secure secrets management with the communication capabilities of Slack.`
|
||||||
|
},
|
||||||
|
features: {
|
||||||
|
app_home: {
|
||||||
|
home_tab_enabled: false,
|
||||||
|
messages_tab_enabled: false,
|
||||||
|
messages_tab_read_only_enabled: true
|
||||||
|
},
|
||||||
|
bot_user: {
|
||||||
|
display_name: "Infisical",
|
||||||
|
always_online: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
oauth_config: {
|
||||||
|
redirect_urls: [`${window.origin}/api/v1/workflow-integrations/slack/oauth_redirect_gov`],
|
||||||
|
scopes: {
|
||||||
|
bot: ["chat:write.public", "chat:write", "channels:read", "groups:read"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
org_deploy_enabled: false,
|
||||||
|
socket_mode_enabled: false,
|
||||||
|
token_rotation_enabled: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)}`;
|
||||||
|
|
||||||
|
const slackFormSchema = z.object({
|
||||||
|
clientId: z.string(),
|
||||||
|
clientSecret: z.string()
|
||||||
|
});
|
||||||
|
|
||||||
|
type TSlackForm = z.infer<typeof slackFormSchema>;
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
adminIntegrationsConfig?: AdminIntegrationsConfig;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const GovSlackIntegrationForm = ({ adminIntegrationsConfig }: Props) => {
|
||||||
|
const { mutateAsync: updateAdminServerConfig } = useUpdateServerConfig();
|
||||||
|
const [isSlackClientIdFocused, setIsSlackClientIdFocused] = useToggle();
|
||||||
|
const [isSlackClientSecretFocused, setIsSlackClientSecretFocused] = useToggle();
|
||||||
|
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
handleSubmit,
|
||||||
|
setValue,
|
||||||
|
formState: { isSubmitting, isDirty }
|
||||||
|
} = useForm<TSlackForm>({
|
||||||
|
resolver: zodResolver(slackFormSchema)
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSubmit = async (data: TSlackForm) => {
|
||||||
|
await updateAdminServerConfig({
|
||||||
|
govSlackClientId: data.clientId,
|
||||||
|
govSlackClientSecret: data.clientSecret
|
||||||
|
});
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
text: "Updated admin gov slack configuration",
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (adminIntegrationsConfig) {
|
||||||
|
setValue("clientId", adminIntegrationsConfig.govSlack.clientId);
|
||||||
|
setValue("clientSecret", adminIntegrationsConfig.govSlack.clientSecret);
|
||||||
|
}
|
||||||
|
}, [adminIntegrationsConfig]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
|
<Accordion type="single" collapsible className="w-full">
|
||||||
|
<AccordionItem value="slack-integration" className="data-[state=open]:border-none">
|
||||||
|
<AccordionTrigger className="flex h-fit w-full justify-start rounded-md border border-mineshaft-500 bg-mineshaft-700 px-4 py-6 text-sm transition-colors data-[state=open]:rounded-b-none">
|
||||||
|
<div className="text-md group order-1 ml-3 flex items-center gap-2">
|
||||||
|
<BsSlack className="text-lg group-hover:text-primary-400" />
|
||||||
|
<div className="text-[15px] font-medium">GovSlack</div>
|
||||||
|
</div>
|
||||||
|
</AccordionTrigger>
|
||||||
|
<AccordionContent childrenClassName="px-0 py-0">
|
||||||
|
<div className="flex w-full flex-col justify-start rounded-md rounded-t-none border border-t-0 border-mineshaft-500 bg-mineshaft-700 px-4 py-4">
|
||||||
|
<div className="mb-4 max-w-lg text-sm text-mineshaft-300">
|
||||||
|
Step 1: Create your Infisical GovSlack App
|
||||||
|
</div>
|
||||||
|
<div className="mb-6">
|
||||||
|
<Button
|
||||||
|
colorSchema="secondary"
|
||||||
|
onClick={() => window.open(getCustomSlackAppCreationUrl())}
|
||||||
|
>
|
||||||
|
Create GovSlack App
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="mb-4 max-w-lg text-sm text-mineshaft-300">
|
||||||
|
Step 2: Configure your instance-wide settings to enable integration with Slack. Copy
|
||||||
|
the values from the App Credentials page of your custom Slack App.
|
||||||
|
</div>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="clientId"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Client ID"
|
||||||
|
className="w-96"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
value={field.value || ""}
|
||||||
|
type={isSlackClientIdFocused ? "text" : "password"}
|
||||||
|
onFocus={() => setIsSlackClientIdFocused.on()}
|
||||||
|
onBlur={() => setIsSlackClientIdFocused.off()}
|
||||||
|
onChange={(e) => field.onChange(e.target.value)}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="clientSecret"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Client Secret"
|
||||||
|
className="w-96"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
value={field.value || ""}
|
||||||
|
type={isSlackClientSecretFocused ? "text" : "password"}
|
||||||
|
onFocus={() => setIsSlackClientSecretFocused.on()}
|
||||||
|
onBlur={() => setIsSlackClientSecretFocused.off()}
|
||||||
|
onChange={(e) => field.onChange(e.target.value)}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
className="mt-2"
|
||||||
|
type="submit"
|
||||||
|
isLoading={isSubmitting}
|
||||||
|
isDisabled={isSubmitting || !isDirty}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
|
</Accordion>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -5,6 +5,7 @@ import { ROUTE_PATHS } from "@app/const/routes";
|
|||||||
import { useGetAdminIntegrationsConfig } from "@app/hooks/api";
|
import { useGetAdminIntegrationsConfig } from "@app/hooks/api";
|
||||||
import { AdminIntegrationsConfig } from "@app/hooks/api/admin/types";
|
import { AdminIntegrationsConfig } from "@app/hooks/api/admin/types";
|
||||||
|
|
||||||
|
import { GovSlackIntegrationForm } from "./GovSlackIntegrationForm";
|
||||||
import { MicrosoftTeamsIntegrationForm } from "./MicrosoftTeamsIntegrationForm";
|
import { MicrosoftTeamsIntegrationForm } from "./MicrosoftTeamsIntegrationForm";
|
||||||
import { SlackIntegrationForm } from "./SlackIntegrationForm";
|
import { SlackIntegrationForm } from "./SlackIntegrationForm";
|
||||||
|
|
||||||
@@ -19,6 +20,7 @@ interface WorkflowTabProps {
|
|||||||
const WorkflowTab = ({ adminIntegrationsConfig }: WorkflowTabProps) => (
|
const WorkflowTab = ({ adminIntegrationsConfig }: WorkflowTabProps) => (
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<SlackIntegrationForm adminIntegrationsConfig={adminIntegrationsConfig} />
|
<SlackIntegrationForm adminIntegrationsConfig={adminIntegrationsConfig} />
|
||||||
|
<GovSlackIntegrationForm adminIntegrationsConfig={adminIntegrationsConfig} />
|
||||||
<MicrosoftTeamsIntegrationForm adminIntegrationsConfig={adminIntegrationsConfig} />
|
<MicrosoftTeamsIntegrationForm adminIntegrationsConfig={adminIntegrationsConfig} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -18,9 +18,8 @@ import { useToggle } from "@app/hooks";
|
|||||||
import { useUpdateServerConfig } from "@app/hooks/api";
|
import { useUpdateServerConfig } from "@app/hooks/api";
|
||||||
import { AdminIntegrationsConfig } from "@app/hooks/api/admin/types";
|
import { AdminIntegrationsConfig } from "@app/hooks/api/admin/types";
|
||||||
|
|
||||||
const getCustomSlackAppCreationUrl = (govEnabled: boolean) => {
|
const getCustomSlackAppCreationUrl = () =>
|
||||||
const baseUrl = govEnabled ? "https://api.slack-gov.com" : "https://api.slack.com";
|
`https://api.slack.com/apps?new_app=1&manifest_json=${encodeURIComponent(
|
||||||
return `${baseUrl}/apps?new_app=1&manifest_json=${encodeURIComponent(
|
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
display_information: {
|
display_information: {
|
||||||
name: "Infisical",
|
name: "Infisical",
|
||||||
@@ -57,7 +56,6 @@ const getCustomSlackAppCreationUrl = (govEnabled: boolean) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
)}`;
|
)}`;
|
||||||
};
|
|
||||||
|
|
||||||
const slackFormSchema = z.object({
|
const slackFormSchema = z.object({
|
||||||
clientId: z.string(),
|
clientId: z.string(),
|
||||||
@@ -121,13 +119,7 @@ export const SlackIntegrationForm = ({ adminIntegrationsConfig }: Props) => {
|
|||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<Button
|
<Button
|
||||||
colorSchema="secondary"
|
colorSchema="secondary"
|
||||||
onClick={() =>
|
onClick={() => window.open(getCustomSlackAppCreationUrl())}
|
||||||
window.open(
|
|
||||||
getCustomSlackAppCreationUrl(
|
|
||||||
adminIntegrationsConfig?.slack.govEnabled ?? false
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
Create Slack App
|
Create Slack App
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -5,11 +5,12 @@ import axios from "axios";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import { Button, FormControl, Input } from "@app/components/v2";
|
import { Button, FormControl, Input, Select, SelectItem } from "@app/components/v2";
|
||||||
import { useOrganization } from "@app/context";
|
import { useOrganization } from "@app/context";
|
||||||
import { useToggle } from "@app/hooks";
|
import { useToggle } from "@app/hooks";
|
||||||
import {
|
import {
|
||||||
fetchSlackInstallUrl,
|
fetchSlackInstallUrl,
|
||||||
|
useGetAdminIntegrationsConfig,
|
||||||
useGetSlackIntegrationById,
|
useGetSlackIntegrationById,
|
||||||
useUpdateSlackIntegration
|
useUpdateSlackIntegration
|
||||||
} from "@app/hooks/api";
|
} from "@app/hooks/api";
|
||||||
@@ -22,7 +23,8 @@ type Props = {
|
|||||||
|
|
||||||
const slackFormSchema = z.object({
|
const slackFormSchema = z.object({
|
||||||
slug: slugSchema({ min: 1, field: "Alias" }),
|
slug: slugSchema({ min: 1, field: "Alias" }),
|
||||||
description: z.string().optional()
|
description: z.string().optional(),
|
||||||
|
integrationType: z.enum(["slack", "govSlack"]).default("slack")
|
||||||
});
|
});
|
||||||
|
|
||||||
type TSlackFormData = z.infer<typeof slackFormSchema>;
|
type TSlackFormData = z.infer<typeof slackFormSchema>;
|
||||||
@@ -41,6 +43,7 @@ export const SlackIntegrationForm = ({ id, onClose }: Props) => {
|
|||||||
const { currentOrg } = useOrganization();
|
const { currentOrg } = useOrganization();
|
||||||
const { data: slackIntegration } = useGetSlackIntegrationById(id);
|
const { data: slackIntegration } = useGetSlackIntegrationById(id);
|
||||||
const { mutateAsync: updateSlackIntegration } = useUpdateSlackIntegration();
|
const { mutateAsync: updateSlackIntegration } = useUpdateSlackIntegration();
|
||||||
|
const { data: adminIntegrationsConfig } = useGetAdminIntegrationsConfig();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (slackIntegration) {
|
if (slackIntegration) {
|
||||||
@@ -49,14 +52,16 @@ export const SlackIntegrationForm = ({ id, onClose }: Props) => {
|
|||||||
}
|
}
|
||||||
}, [slackIntegration]);
|
}, [slackIntegration]);
|
||||||
|
|
||||||
const triggerSlackInstall = async (slug: string, description?: string) => {
|
const triggerSlackInstall = async (slug: string, description?: string, isGovSlack?: boolean) => {
|
||||||
setIsConnectLoading.on();
|
setIsConnectLoading.on();
|
||||||
try {
|
try {
|
||||||
const slackInstallUrl = await fetchSlackInstallUrl({
|
const slackInstallUrl = await fetchSlackInstallUrl({
|
||||||
slug,
|
slug,
|
||||||
description
|
description,
|
||||||
|
isGovSlack
|
||||||
});
|
});
|
||||||
if (slackInstallUrl) {
|
if (slackInstallUrl) {
|
||||||
|
console.log("slackInstallUrl", slackInstallUrl);
|
||||||
window.location.assign(slackInstallUrl);
|
window.location.assign(slackInstallUrl);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -71,7 +76,7 @@ export const SlackIntegrationForm = ({ id, onClose }: Props) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSlackFormSubmit = async ({ slug, description }: TSlackFormData) => {
|
const handleSlackFormSubmit = async ({ slug, description, integrationType }: TSlackFormData) => {
|
||||||
if (id && slackIntegration) {
|
if (id && slackIntegration) {
|
||||||
if (!currentOrg) {
|
if (!currentOrg) {
|
||||||
return;
|
return;
|
||||||
@@ -90,12 +95,38 @@ export const SlackIntegrationForm = ({ id, onClose }: Props) => {
|
|||||||
type: "success"
|
type: "success"
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
await triggerSlackInstall(slug, description);
|
await triggerSlackInstall(slug, description, integrationType === "govSlack");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isGovSlackAvailable =
|
||||||
|
adminIntegrationsConfig?.govSlack?.clientId && adminIntegrationsConfig?.govSlack?.clientSecret;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit(handleSlackFormSubmit)} autoComplete="off">
|
<form onSubmit={handleSubmit(handleSlackFormSubmit)} autoComplete="off">
|
||||||
|
{!slackIntegration && (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="integrationType"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Integration Type"
|
||||||
|
isRequired
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
{...field}
|
||||||
|
onValueChange={(value) => field.onChange(value)}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
<SelectItem value="slack">Slack</SelectItem>
|
||||||
|
{isGovSlackAvailable && <SelectItem value="govSlack">GovSlack</SelectItem>}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="slug"
|
name="slug"
|
||||||
|
|||||||
Reference in New Issue
Block a user