feat: adds govslack support

This commit is contained in:
Piyush Gupta
2025-12-16 19:57:41 +05:30
parent 337e73cafa
commit 21090eaaf9
18 changed files with 429 additions and 66 deletions

View File

@@ -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");
});
}
}
}

View File

@@ -19,7 +19,8 @@ export const SlackIntegrationsSchema = z.object({
slackBotId: z.string(),
slackBotUserId: z.string(),
createdAt: z.date(),
updatedAt: z.date()
updatedAt: z.date(),
isGovSlack: z.boolean().default(false)
});
export type TSlackIntegrations = z.infer<typeof SlackIntegrationsSchema>;

View File

@@ -36,7 +36,9 @@ export const SuperAdminSchema = z.object({
encryptedGitHubAppConnectionId: zodBuffer.nullable().optional(),
encryptedGitHubAppConnectionPrivateKey: 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>;

View File

@@ -246,8 +246,8 @@ const envSchema = z
),
WORKFLOW_SLACK_CLIENT_ID: zpStr(z.string().optional()),
WORKFLOW_SLACK_CLIENT_SECRET: zpStr(z.string().optional()),
WORKFLOW_SLACK_GOV_ENABLED: zodStrBool.default("false"),
WORKFLOW_SLACK_GOV_BASE_URL: zpStr(z.string().optional().default("https://slack-gov.com")),
WORKFLOW_GOV_SLACK_CLIENT_ID: zpStr(z.string().optional()),
WORKFLOW_GOV_SLACK_CLIENT_SECRET: zpStr(z.string().optional()),
ENABLE_MSSQL_SECRET_ROTATION_ENCRYPT: zodStrBool.default("true"),
// Special Detection Feature

View File

@@ -41,6 +41,8 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
updatedAt: true,
encryptedSlackClientId: true,
encryptedSlackClientSecret: true,
encryptedGovSlackClientId: true,
encryptedGovSlackClientSecret: true,
encryptedMicrosoftTeamsAppId: true,
encryptedMicrosoftTeamsClientSecret: true,
encryptedMicrosoftTeamsBotId: true,
@@ -107,6 +109,8 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
}),
slackClientId: z.string().optional(),
slackClientSecret: z.string().optional(),
govSlackClientId: z.string().optional(),
govSlackClientSecret: z.string().optional(),
microsoftTeamsAppId: z.string().optional(),
microsoftTeamsClientSecret: z.string().optional(),
microsoftTeamsBotId: z.string().optional(),
@@ -374,8 +378,11 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
200: z.object({
slack: z.object({
clientId: z.string(),
clientSecret: z.string(),
govEnabled: z.boolean()
clientSecret: z.string()
}),
govSlack: z.object({
clientId: z.string(),
clientSecret: z.string()
}),
microsoftTeams: z.object({
appId: z.string(),

View File

@@ -8,6 +8,8 @@ import { slugSchema } from "@app/server/lib/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import { booleanSchema } from "../sanitizedSchemas";
const sanitizedSlackIntegrationSchema = WorkflowIntegrationsSchema.pick({
id: true,
description: true,
@@ -36,7 +38,8 @@ export const registerSlackRouter = async (server: FastifyZodProvider) => {
],
querystring: z.object({
slug: slugSchema({ max: 64 }),
description: z.string().optional()
description: z.string().optional(),
isGovSlack: booleanSchema.default(false).optional()
}),
response: {
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`);
}
});
}
});
};

View File

@@ -0,0 +1 @@
export const SLACK_GOV_BASE_URL = "https://slack-gov.com";

View File

@@ -6,13 +6,13 @@ import { logger } from "@app/lib/logger";
import { TNotification, TriggerFeature } from "@app/lib/workflow-integrations/types";
import { KmsDataKey } from "../kms/kms-types";
import { SLACK_GOV_BASE_URL } from "./slack-constants";
import { TSendSlackNotificationDTO } from "./slack-types";
const COMPANY_BRAND_COLOR = "#e0ed34";
const ERROR_COLOR = "#e74c3c";
export const fetchSlackChannels = async (botKey: string) => {
const appCfg = getConfig();
export const fetchSlackChannels = async (botKey: string, isGovSlack = false) => {
const slackChannels: {
name: string;
id: string;
@@ -20,9 +20,8 @@ export const fetchSlackChannels = async (botKey: string) => {
const webClientOptions: WebClientOptions = {};
if (appCfg.WORKFLOW_SLACK_GOV_ENABLED) {
const govBaseUrl = appCfg.WORKFLOW_SLACK_GOV_BASE_URL;
webClientOptions.slackApiUrl = `${govBaseUrl}/api`;
if (isGovSlack) {
webClientOptions.slackApiUrl = `${SLACK_GOV_BASE_URL}/api`;
}
const slackWebClient = new WebClient(botKey, webClientOptions);
@@ -277,7 +276,6 @@ export const sendSlackNotification = async ({
targetChannelIds,
slackIntegration
}: TSendSlackNotificationDTO) => {
const appCfg = getConfig();
const { decryptor: orgDataKeyDecryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization,
orgId
@@ -288,9 +286,8 @@ export const sendSlackNotification = async ({
const webClientOptions: WebClientOptions = {};
if (appCfg.WORKFLOW_SLACK_GOV_ENABLED) {
const govBaseUrl = appCfg.WORKFLOW_SLACK_GOV_BASE_URL;
webClientOptions.slackApiUrl = `${govBaseUrl}/api`;
if (slackIntegration.isGovSlack) {
webClientOptions.slackApiUrl = `${SLACK_GOV_BASE_URL}/api`;
}
const slackWebClient = new WebClient(botKey, webClientOptions);

View File

@@ -12,6 +12,7 @@ import { KmsDataKey } from "../kms/kms-types";
import { getServerCfg } from "../super-admin/super-admin-service";
import { TWorkflowIntegrationDALFactory } from "../workflow-integration/workflow-integration-dal";
import { WorkflowIntegration } from "../workflow-integration/workflow-integration-types";
import { SLACK_GOV_BASE_URL } from "./slack-constants";
import { fetchSlackChannels } from "./slack-fns";
import { TSlackIntegrationDALFactory } from "./slack-integration-dal";
import {
@@ -58,7 +59,8 @@ export const slackServiceFactory = ({
slackAppId,
botAccessToken,
slackBotId,
slackBotUserId
slackBotUserId,
isGovSlack = false
}: TCompleteSlackIntegrationDTO) => {
const { encryptor: orgDataKeyEncryptor } = await kmsService.createCipherPairWithDataKey({
orgId,
@@ -90,7 +92,8 @@ export const slackServiceFactory = ({
slackAppId,
slackBotId,
slackBotUserId,
encryptedBotAccessToken
encryptedBotAccessToken,
isGovSlack
},
tx
);
@@ -135,15 +138,30 @@ export const slackServiceFactory = ({
});
};
const getSlackInstaller = async () => {
const getSlackInstaller = async (isGovSlack = false) => {
const appCfg = getConfig();
const serverCfg = await getServerCfg();
let slackClientId = appCfg.WORKFLOW_SLACK_CLIENT_ID as string;
let slackClientSecret = appCfg.WORKFLOW_SLACK_CLIENT_SECRET as string;
let slackClientId = "";
let slackClientSecret = "";
const decrypt = kmsService.decryptWithRootKey();
if (isGovSlack) {
slackClientId = appCfg.WORKFLOW_GOV_SLACK_CLIENT_ID as string;
slackClientSecret = appCfg.WORKFLOW_GOV_SLACK_CLIENT_SECRET as string;
if (serverCfg.encryptedGovSlackClientId) {
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();
}
@@ -151,13 +169,13 @@ export const slackServiceFactory = ({
if (serverCfg.encryptedSlackClientSecret) {
slackClientSecret = decrypt(Buffer.from(serverCfg.encryptedSlackClientSecret)).toString();
}
}
if (!slackClientId || !slackClientSecret) {
throw new BadRequestError({
message: `Invalid Slack configuration. ${
message: `Invalid ${isGovSlack ? "GovSlack" : "Slack"} configuration. ${
appCfg.isCloud
? "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;
slug: string;
description?: string;
isGovSlack?: boolean;
};
if (metadata.id) {
@@ -205,7 +224,8 @@ export const slackServiceFactory = ({
slackAppId: installation.appId || "",
botAccessToken: installation.bot?.token || "",
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
@@ -220,11 +240,10 @@ export const slackServiceFactory = ({
}
};
if (appCfg.WORKFLOW_SLACK_GOV_ENABLED) {
const govBaseUrl = appCfg.WORKFLOW_SLACK_GOV_BASE_URL;
installProviderOptions.authorizationUrl = `${govBaseUrl}/oauth/v2/authorize`;
if (isGovSlack) {
installProviderOptions.authorizationUrl = `${SLACK_GOV_BASE_URL}/oauth/v2/authorize`;
installProviderOptions.clientOptions = {
slackApiUrl: `${govBaseUrl}/api`
slackApiUrl: `${SLACK_GOV_BASE_URL}/api`
};
}
@@ -237,7 +256,8 @@ export const slackServiceFactory = ({
actorOrgId,
actorAuthMethod,
slug,
description
description,
isGovSlack = false
}: TGetSlackInstallUrlDTO) => {
const appCfg = getConfig();
@@ -252,15 +272,16 @@ export const slackServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Settings);
const installer = await getSlackInstaller();
const installer = await getSlackInstaller(isGovSlack);
const url = await installer.generateInstallUrl({
scopes: ["chat:write.public", "chat:write", "channels:read", "groups:read"],
metadata: JSON.stringify({
slug,
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;
@@ -287,14 +308,15 @@ export const slackServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Settings);
const installer = await getSlackInstaller();
const installer = await getSlackInstaller(slackIntegration.isGovSlack);
const url = await installer.generateInstallUrl({
scopes: ["chat:write.public", "chat:write", "channels:read", "groups:read"],
metadata: JSON.stringify({
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;
@@ -386,7 +408,7 @@ export const slackServiceFactory = ({
cipherTextBlob: slackIntegration.encryptedBotAccessToken
}).toString("utf8");
return fetchSlackChannels(botKey);
return fetchSlackChannels(botKey, slackIntegration.isGovSlack);
};
const updateSlackIntegration = async ({

View File

@@ -7,6 +7,7 @@ import { TKmsServiceFactory } from "../kms/kms-service";
export type TGetSlackInstallUrlDTO = {
slug: string;
description?: string;
isGovSlack?: boolean;
} & Omit<TOrgPermission, "orgId">;
export type TGetReinstallUrlDTO = {
@@ -39,6 +40,7 @@ export type TCompleteSlackIntegrationDTO = {
botAccessToken: string;
slackBotId: string;
slackBotUserId: string;
isGovSlack?: boolean;
};
export type TReinstallSlackIntegrationDTO = {

View File

@@ -101,6 +101,10 @@ let adminIntegrationsConfig: TAdminIntegrationConfig = {
clientSecret: "",
clientId: ""
},
govSlack: {
clientSecret: "",
clientId: ""
},
microsoftTeams: {
appId: "",
clientSecret: "",
@@ -207,6 +211,13 @@ export const superAdminServiceFactory = ({
? decrypt(serverCfg.encryptedSlackClientSecret).toString()
: "";
const govSlackClientId = serverCfg.encryptedGovSlackClientId
? decrypt(serverCfg.encryptedGovSlackClientId).toString()
: "";
const govSlackClientSecret = serverCfg.encryptedGovSlackClientSecret
? decrypt(serverCfg.encryptedGovSlackClientSecret).toString()
: "";
const microsoftAppId = serverCfg.encryptedMicrosoftTeamsAppId
? decrypt(serverCfg.encryptedMicrosoftTeamsAppId).toString()
: "";
@@ -235,13 +246,14 @@ export const superAdminServiceFactory = ({
? decrypt(serverCfg.encryptedGitHubAppConnectionPrivateKey).toString()
: "";
const appCfg = getConfig();
return {
slack: {
clientSecret: slackClientSecret,
clientId: slackClientId,
govEnabled: appCfg.WORKFLOW_SLACK_GOV_ENABLED
clientId: slackClientId
},
govSlack: {
clientSecret: govSlackClientSecret,
clientId: govSlackClientId
},
microsoftTeams: {
appId: microsoftAppId,
@@ -308,6 +320,8 @@ export const superAdminServiceFactory = ({
data: TSuperAdminUpdate & {
slackClientId?: string;
slackClientSecret?: string;
govSlackClientId?: string;
govSlackClientSecret?: string;
microsoftTeamsAppId?: string;
microsoftTeamsClientSecret?: string;
microsoftTeamsBotId?: string;
@@ -384,6 +398,20 @@ export const superAdminServiceFactory = ({
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;
if (data.microsoftTeamsAppId) {
const encryptedClientId = encryptWithRoot(Buffer.from(data.microsoftTeamsAppId));

View File

@@ -64,6 +64,10 @@ export type TAdminIntegrationConfig = {
clientSecret: string;
clientId: string;
};
govSlack: {
clientSecret: string;
clientId: string;
};
microsoftTeams: {
appId: string;
clientSecret: string;

View File

@@ -78,6 +78,8 @@ export type TServerConfig = {
export type TUpdateServerConfigDTO = {
slackClientId?: string;
slackClientSecret?: string;
govSlackClientId?: string;
govSlackClientSecret?: string;
microsoftTeamsAppId?: string;
microsoftTeamsClientSecret?: string;
microsoftTeamsBotId?: string;
@@ -119,7 +121,10 @@ export type AdminIntegrationsConfig = {
slack: {
clientId: string;
clientSecret: string;
govEnabled: boolean;
};
govSlack: {
clientId: string;
clientSecret: string;
};
microsoftTeams: {
appId: string;

View File

@@ -29,15 +29,18 @@ export const workflowIntegrationKeys = {
export const fetchSlackInstallUrl = async ({
slug,
description
description,
isGovSlack
}: {
slug: string;
description?: string;
isGovSlack?: boolean;
}) => {
const { data } = await apiRequest.get<string>("/api/v1/workflow-integrations/slack/install", {
params: {
slug,
description
description,
isGovSlack
}
});

View File

@@ -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>
);
};

View File

@@ -5,6 +5,7 @@ import { ROUTE_PATHS } from "@app/const/routes";
import { useGetAdminIntegrationsConfig } from "@app/hooks/api";
import { AdminIntegrationsConfig } from "@app/hooks/api/admin/types";
import { GovSlackIntegrationForm } from "./GovSlackIntegrationForm";
import { MicrosoftTeamsIntegrationForm } from "./MicrosoftTeamsIntegrationForm";
import { SlackIntegrationForm } from "./SlackIntegrationForm";
@@ -19,6 +20,7 @@ interface WorkflowTabProps {
const WorkflowTab = ({ adminIntegrationsConfig }: WorkflowTabProps) => (
<div className="flex flex-col gap-2">
<SlackIntegrationForm adminIntegrationsConfig={adminIntegrationsConfig} />
<GovSlackIntegrationForm adminIntegrationsConfig={adminIntegrationsConfig} />
<MicrosoftTeamsIntegrationForm adminIntegrationsConfig={adminIntegrationsConfig} />
</div>
);

View File

@@ -18,9 +18,8 @@ import { useToggle } from "@app/hooks";
import { useUpdateServerConfig } from "@app/hooks/api";
import { AdminIntegrationsConfig } from "@app/hooks/api/admin/types";
const getCustomSlackAppCreationUrl = (govEnabled: boolean) => {
const baseUrl = govEnabled ? "https://api.slack-gov.com" : "https://api.slack.com";
return `${baseUrl}/apps?new_app=1&manifest_json=${encodeURIComponent(
const getCustomSlackAppCreationUrl = () =>
`https://api.slack.com/apps?new_app=1&manifest_json=${encodeURIComponent(
JSON.stringify({
display_information: {
name: "Infisical",
@@ -57,7 +56,6 @@ const getCustomSlackAppCreationUrl = (govEnabled: boolean) => {
}
})
)}`;
};
const slackFormSchema = z.object({
clientId: z.string(),
@@ -121,13 +119,7 @@ export const SlackIntegrationForm = ({ adminIntegrationsConfig }: Props) => {
<div className="mb-6">
<Button
colorSchema="secondary"
onClick={() =>
window.open(
getCustomSlackAppCreationUrl(
adminIntegrationsConfig?.slack.govEnabled ?? false
)
)
}
onClick={() => window.open(getCustomSlackAppCreationUrl())}
>
Create Slack App
</Button>

View File

@@ -5,11 +5,12 @@ import axios from "axios";
import { z } from "zod";
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 { useToggle } from "@app/hooks";
import {
fetchSlackInstallUrl,
useGetAdminIntegrationsConfig,
useGetSlackIntegrationById,
useUpdateSlackIntegration
} from "@app/hooks/api";
@@ -22,7 +23,8 @@ type Props = {
const slackFormSchema = z.object({
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>;
@@ -41,6 +43,7 @@ export const SlackIntegrationForm = ({ id, onClose }: Props) => {
const { currentOrg } = useOrganization();
const { data: slackIntegration } = useGetSlackIntegrationById(id);
const { mutateAsync: updateSlackIntegration } = useUpdateSlackIntegration();
const { data: adminIntegrationsConfig } = useGetAdminIntegrationsConfig();
useEffect(() => {
if (slackIntegration) {
@@ -49,14 +52,16 @@ export const SlackIntegrationForm = ({ id, onClose }: Props) => {
}
}, [slackIntegration]);
const triggerSlackInstall = async (slug: string, description?: string) => {
const triggerSlackInstall = async (slug: string, description?: string, isGovSlack?: boolean) => {
setIsConnectLoading.on();
try {
const slackInstallUrl = await fetchSlackInstallUrl({
slug,
description
description,
isGovSlack
});
if (slackInstallUrl) {
console.log("slackInstallUrl", slackInstallUrl);
window.location.assign(slackInstallUrl);
}
} 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 (!currentOrg) {
return;
@@ -90,12 +95,38 @@ export const SlackIntegrationForm = ({ id, onClose }: Props) => {
type: "success"
});
} else {
await triggerSlackInstall(slug, description);
await triggerSlackInstall(slug, description, integrationType === "govSlack");
}
};
const isGovSlackAvailable =
adminIntegrationsConfig?.govSlack?.clientId && adminIntegrationsConfig?.govSlack?.clientSecret;
return (
<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
control={control}
name="slug"