feat: admin slack configuration

This commit is contained in:
Sheen Capadngan
2024-09-04 23:06:30 +08:00
parent 71b8c59050
commit 956719f797
13 changed files with 465 additions and 15 deletions
+8 -2
View File
@@ -182,6 +182,7 @@ import { secretVersionV2BridgeDALFactory } from "@app/services/secret-v2-bridge/
import { secretVersionV2TagBridgeDALFactory } from "@app/services/secret-v2-bridge/secret-version-tag-dal"; import { secretVersionV2TagBridgeDALFactory } from "@app/services/secret-v2-bridge/secret-version-tag-dal";
import { serviceTokenDALFactory } from "@app/services/service-token/service-token-dal"; import { serviceTokenDALFactory } from "@app/services/service-token/service-token-dal";
import { serviceTokenServiceFactory } from "@app/services/service-token/service-token-service"; import { serviceTokenServiceFactory } from "@app/services/service-token/service-token-service";
import { adminSlackConfigDALFactory } from "@app/services/slack/admin-slack-config-dal";
import { projectSlackConfigDALFactory } from "@app/services/slack/project-slack-config-dal"; import { projectSlackConfigDALFactory } from "@app/services/slack/project-slack-config-dal";
import { slackIntegrationDALFactory } from "@app/services/slack/slack-integration-dal"; import { slackIntegrationDALFactory } from "@app/services/slack/slack-integration-dal";
import { slackServiceFactory } from "@app/services/slack/slack-service"; import { slackServiceFactory } from "@app/services/slack/slack-service";
@@ -327,6 +328,7 @@ export const registerRoutes = async (
const slackIntegrationDAL = slackIntegrationDALFactory(db); const slackIntegrationDAL = slackIntegrationDALFactory(db);
const projectSlackConfigDAL = projectSlackConfigDALFactory(db); const projectSlackConfigDAL = projectSlackConfigDALFactory(db);
const adminSlackConfigDAL = adminSlackConfigDALFactory(db);
const permissionService = permissionServiceFactory({ const permissionService = permissionServiceFactory({
permissionDAL, permissionDAL,
@@ -526,8 +528,11 @@ export const registerRoutes = async (
serverCfgDAL: superAdminDAL, serverCfgDAL: superAdminDAL,
orgService, orgService,
keyStore, keyStore,
licenseService licenseService,
adminSlackConfigDAL,
kmsService
}); });
const orgAdminService = orgAdminServiceFactory({ const orgAdminService = orgAdminServiceFactory({
projectDAL, projectDAL,
permissionService, permissionService,
@@ -1162,7 +1167,8 @@ export const registerRoutes = async (
const slackService = slackServiceFactory({ const slackService = slackServiceFactory({
permissionService, permissionService,
kmsService, kmsService,
slackIntegrationDAL slackIntegrationDAL,
adminSlackConfigDAL
}); });
await superAdminService.initServerCfg(); await superAdminService.initServerCfg();
@@ -123,6 +123,87 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
} }
}); });
server.route({
method: "GET",
url: "/integrations/slack/bot-creation-url",
config: {
rateLimit: readLimit
},
schema: {
response: {
200: z.string()
}
},
onRequest: (req, res, done) => {
verifyAuth([AuthMode.JWT])(req, res, () => {
verifySuperAdmin(req, res, done);
});
},
handler: async () => {
const url = await server.services.superAdmin.getCustomSlackBotCreationUrl();
return url;
}
});
server.route({
method: "GET",
url: "/integrations/slack/config",
config: {
rateLimit: readLimit
},
schema: {
response: {
200: z.object({
clientId: z.string(),
clientSecret: z.string()
})
}
},
onRequest: (req, res, done) => {
verifyAuth([AuthMode.JWT])(req, res, () => {
verifySuperAdmin(req, res, done);
});
},
handler: async () => {
const adminSlackConfig = await server.services.superAdmin.getAdminSlackConfig();
return adminSlackConfig;
}
});
server.route({
method: "PUT",
url: "/integrations/slack/config",
config: {
rateLimit: writeLimit
},
schema: {
body: z.object({
clientId: z.string(),
clientSecret: z.string()
}),
response: {
200: z.object({
clientId: z.string(),
clientSecret: z.string()
})
}
},
onRequest: (req, res, done) => {
verifyAuth([AuthMode.JWT])(req, res, () => {
verifySuperAdmin(req, res, done);
});
},
handler: async (req) => {
const adminSlackConfig = await server.services.superAdmin.updateAdminSlackConfig({
...req.body
});
return adminSlackConfig;
}
});
server.route({ server.route({
method: "DELETE", method: "DELETE",
url: "/user-management/users/:userId", url: "/user-management/users/:userId",
+19
View File
@@ -208,6 +208,23 @@ export const kmsServiceFactory = ({
return org.kmsDefaultKeyId; return org.kmsDefaultKeyId;
}; };
const encryptWithRootKey = async () => {
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
return ({ plainText }: { plainText: Buffer }) => {
const encryptedPlainTextBlob = cipher.encrypt(plainText, ROOT_ENCRYPTION_KEY);
return Promise.resolve({ cipherTextBlob: encryptedPlainTextBlob });
};
};
const decryptWithRootKey = async () => {
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
return ({ cipherTextBlob }: { cipherTextBlob: Buffer }) => {
const decryptedBlob = cipher.decrypt(cipherTextBlob, ROOT_ENCRYPTION_KEY);
return Promise.resolve(decryptedBlob);
};
};
const decryptWithKmsKey = async ({ const decryptWithKmsKey = async ({
kmsId, kmsId,
depth = 0 depth = 0
@@ -808,6 +825,8 @@ export const kmsServiceFactory = ({
decryptWithKmsKey, decryptWithKmsKey,
encryptWithInputKey, encryptWithInputKey,
decryptWithInputKey, decryptWithInputKey,
encryptWithRootKey,
decryptWithRootKey,
getOrgKmsKeyId, getOrgKmsKeyId,
getProjectSecretManagerKmsKeyId, getProjectSecretManagerKmsKeyId,
updateProjectSecretManagerKmsKey, updateProjectSecretManagerKmsKey,
+70
View File
@@ -1,13 +1,57 @@
import { Block, WebClient } from "@slack/web-api"; import { Block, WebClient } from "@slack/web-api";
import { getConfig } from "@app/lib/config/env";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
import { TKmsServiceFactory } from "../kms/kms-service"; import { TKmsServiceFactory } from "../kms/kms-service";
import { KmsDataKey } from "../kms/kms-types"; import { KmsDataKey } from "../kms/kms-types";
import { TProjectDALFactory } from "../project/project-dal"; import { TProjectDALFactory } from "../project/project-dal";
import { TAdminSlackConfigDALFactory } from "./admin-slack-config-dal";
import { TProjectSlackConfigDALFactory } from "./project-slack-config-dal"; import { TProjectSlackConfigDALFactory } from "./project-slack-config-dal";
import { SlackTriggerFeature } from "./slack-types"; import { SlackTriggerFeature } from "./slack-types";
const ADMIN_CONFIG_DB_UUID = "00000000-0000-0000-0000-000000000000";
export const getCustomSlackBotManifest = () => {
const appCfg = getConfig();
return {
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: [`${appCfg.SITE_URL}/api/v1/workflow-integrations/slack/oauth_redirect`],
scopes: {
bot: ["chat:write.public", "chat:write", "channels:read", "groups:read", "im:read", "mpim:read"]
}
},
settings: {
org_deploy_enabled: false,
socket_mode_enabled: false,
token_rotation_enabled: false
}
};
};
export const fetchSlackChannels = async (botKey: string) => { export const fetchSlackChannels = async (botKey: string) => {
const slackChannels: { const slackChannels: {
name: string; name: string;
@@ -98,3 +142,29 @@ export const triggerSlackNotification = async ({
.catch((err) => void logger.error(err)); .catch((err) => void logger.error(err));
} }
}; };
export const getAdminSlackCredentials = async ({
adminSlackConfigDAL,
kmsService
}: {
adminSlackConfigDAL: Pick<TAdminSlackConfigDALFactory, "findById">;
kmsService: Pick<TKmsServiceFactory, "encryptWithRootKey" | "decryptWithRootKey">;
}) => {
const adminSlackConfig = await adminSlackConfigDAL.findById(ADMIN_CONFIG_DB_UUID);
let clientId = "";
let clientSecret = "";
const decrypt = await kmsService.decryptWithRootKey();
if (adminSlackConfig.encryptedClientId) {
clientId = (await decrypt({ cipherTextBlob: adminSlackConfig.encryptedClientId })).toString();
}
if (adminSlackConfig.encryptedClientSecret) {
clientSecret = (await decrypt({ cipherTextBlob: adminSlackConfig.encryptedClientSecret })).toString();
}
return {
clientId,
clientSecret
};
};
+24 -6
View File
@@ -8,7 +8,8 @@ import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { TKmsServiceFactory } from "../kms/kms-service"; import { TKmsServiceFactory } from "../kms/kms-service";
import { KmsDataKey } from "../kms/kms-types"; import { KmsDataKey } from "../kms/kms-types";
import { fetchSlackChannels } from "./slack-fns"; import { TAdminSlackConfigDALFactory } from "./admin-slack-config-dal";
import { fetchSlackChannels, getAdminSlackCredentials } from "./slack-fns";
import { TSlackIntegrationDALFactory } from "./slack-integration-dal"; import { TSlackIntegrationDALFactory } from "./slack-integration-dal";
import { import {
TCompleteSlackIntegrationDTO, TCompleteSlackIntegrationDTO,
@@ -25,7 +26,8 @@ import {
type TSlackServiceFactoryDep = { type TSlackServiceFactoryDep = {
slackIntegrationDAL: Pick<TSlackIntegrationDALFactory, "find" | "findById" | "deleteById" | "updateById" | "create">; slackIntegrationDAL: Pick<TSlackIntegrationDALFactory, "find" | "findById" | "deleteById" | "updateById" | "create">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getOrgPermission">; permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getOrgPermission">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">; kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey" | "encryptWithRootKey" | "decryptWithRootKey">;
adminSlackConfigDAL: Pick<TAdminSlackConfigDALFactory, "findById">;
}; };
export type TSlackServiceFactory = ReturnType<typeof slackServiceFactory>; export type TSlackServiceFactory = ReturnType<typeof slackServiceFactory>;
@@ -33,7 +35,8 @@ export type TSlackServiceFactory = ReturnType<typeof slackServiceFactory>;
export const slackServiceFactory = ({ export const slackServiceFactory = ({
permissionService, permissionService,
slackIntegrationDAL, slackIntegrationDAL,
kmsService kmsService,
adminSlackConfigDAL
}: TSlackServiceFactoryDep) => { }: TSlackServiceFactoryDep) => {
const completeSlackIntegration = async ({ const completeSlackIntegration = async ({
orgId, orgId,
@@ -104,16 +107,31 @@ export const slackServiceFactory = ({
const getSlackInstaller = async () => { const getSlackInstaller = async () => {
const appCfg = getConfig(); const appCfg = getConfig();
const adminSlackCredentials = await getAdminSlackCredentials({
kmsService,
adminSlackConfigDAL
});
if (!appCfg.SLACK_CLIENT_ID || !appCfg.SLACK_CLIENT_SECRET) { let slackClientId = "";
let slackClientSecret = "";
if (adminSlackCredentials.clientId && adminSlackCredentials.clientSecret) {
slackClientId = adminSlackCredentials.clientId;
slackClientSecret = adminSlackCredentials.clientSecret;
} else {
slackClientId = appCfg.SLACK_CLIENT_ID as string;
slackClientSecret = appCfg.SLACK_CLIENT_SECRET as string;
}
if (!slackClientId || !slackClientSecret) {
throw new BadRequestError({ throw new BadRequestError({
message: "Invalid slack configuration" message: "Invalid slack configuration"
}); });
} }
return new InstallProvider({ return new InstallProvider({
clientId: appCfg.SLACK_CLIENT_ID, clientId: slackClientId,
clientSecret: appCfg.SLACK_CLIENT_SECRET, clientSecret: slackClientSecret,
stateSecret: appCfg.AUTH_SECRET, stateSecret: appCfg.AUTH_SECRET,
legacyStateVerification: true, legacyStateVerification: true,
installationStore: { installationStore: {
@@ -10,15 +10,20 @@ import { BadRequestError } from "@app/lib/errors";
import { TAuthLoginFactory } from "../auth/auth-login-service"; import { TAuthLoginFactory } from "../auth/auth-login-service";
import { AuthMethod } from "../auth/auth-type"; import { AuthMethod } from "../auth/auth-type";
import { TKmsServiceFactory } from "../kms/kms-service";
import { TOrgServiceFactory } from "../org/org-service"; import { TOrgServiceFactory } from "../org/org-service";
import { TAdminSlackConfigDALFactory } from "../slack/admin-slack-config-dal";
import { getAdminSlackCredentials, getCustomSlackBotManifest } from "../slack/slack-fns";
import { TUserDALFactory } from "../user/user-dal"; import { TUserDALFactory } from "../user/user-dal";
import { TSuperAdminDALFactory } from "./super-admin-dal"; import { TSuperAdminDALFactory } from "./super-admin-dal";
import { LoginMethod, TAdminGetUsersDTO, TAdminSignUpDTO } from "./super-admin-types"; import { LoginMethod, TAdminGetUsersDTO, TAdminSignUpDTO, TUpdateAdminSlackConfigDTO } from "./super-admin-types";
type TSuperAdminServiceFactoryDep = { type TSuperAdminServiceFactoryDep = {
serverCfgDAL: TSuperAdminDALFactory; serverCfgDAL: TSuperAdminDALFactory;
userDAL: TUserDALFactory; userDAL: TUserDALFactory;
adminSlackConfigDAL: Pick<TAdminSlackConfigDALFactory, "create" | "updateById" | "transaction" | "findById">;
authService: Pick<TAuthLoginFactory, "generateUserTokens">; authService: Pick<TAuthLoginFactory, "generateUserTokens">;
kmsService: Pick<TKmsServiceFactory, "encryptWithRootKey" | "decryptWithRootKey">;
orgService: Pick<TOrgServiceFactory, "createOrganization">; orgService: Pick<TOrgServiceFactory, "createOrganization">;
keyStore: Pick<TKeyStoreFactory, "getItem" | "setItemWithExpiry" | "deleteItem">; keyStore: Pick<TKeyStoreFactory, "getItem" | "setItemWithExpiry" | "deleteItem">;
licenseService: Pick<TLicenseServiceFactory, "onPremFeatures">; licenseService: Pick<TLicenseServiceFactory, "onPremFeatures">;
@@ -38,7 +43,9 @@ export const superAdminServiceFactory = ({
userDAL, userDAL,
authService, authService,
orgService, orgService,
adminSlackConfigDAL,
keyStore, keyStore,
kmsService,
licenseService licenseService
}: TSuperAdminServiceFactoryDep) => { }: TSuperAdminServiceFactoryDep) => {
const initServerCfg = async () => { const initServerCfg = async () => {
@@ -232,11 +239,63 @@ export const superAdminServiceFactory = ({
return user; return user;
}; };
const getCustomSlackBotCreationUrl = async () => {
return `https://api.slack.com/apps?new_app=1&manifest_json=${encodeURIComponent(
JSON.stringify(getCustomSlackBotManifest())
)}`;
};
const updateAdminSlackConfig = async ({ clientId, clientSecret }: TUpdateAdminSlackConfigDTO) => {
return adminSlackConfigDAL.transaction(async (tx) => {
const adminSlackConfig = await adminSlackConfigDAL.findById(ADMIN_CONFIG_DB_UUID, tx);
const encrypt = await kmsService.encryptWithRootKey();
const { cipherTextBlob: encryptedClientId } = await encrypt({ plainText: Buffer.from(clientId) });
const { cipherTextBlob: encryptedClientSecret } = await encrypt({ plainText: Buffer.from(clientSecret) });
if (adminSlackConfig) {
await adminSlackConfigDAL.updateById(
ADMIN_CONFIG_DB_UUID,
{
encryptedClientId,
encryptedClientSecret
},
tx
);
} else {
await adminSlackConfigDAL.create(
{
// @ts-expect-error id is kept as fixed for idempotence and to avoid race condition
id: ADMIN_CONFIG_DB_UUID,
encryptedClientId,
encryptedClientSecret
},
tx
);
}
return {
clientId,
clientSecret
};
});
};
const getAdminSlackConfig = async () => {
return getAdminSlackCredentials({
kmsService,
adminSlackConfigDAL
});
};
return { return {
initServerCfg, initServerCfg,
updateServerCfg, updateServerCfg,
adminSignUp, adminSignUp,
getUsers, getUsers,
deleteUser deleteUser,
getCustomSlackBotCreationUrl,
updateAdminSlackConfig,
getAdminSlackConfig
}; };
}; };
@@ -31,3 +31,8 @@ export enum LoginMethod {
LDAP = "ldap", LDAP = "ldap",
OIDC = "oidc" OIDC = "oidc"
} }
export type TUpdateAdminSlackConfigDTO = {
clientId: string;
clientSecret: string;
};
+12 -2
View File
@@ -1,2 +1,12 @@
export { useAdminDeleteUser, useCreateAdminUser, useUpdateServerConfig } from "./mutation"; export {
export { useAdminGetUsers, useGetServerConfig } from "./queries"; useAdminDeleteUser,
useCreateAdminUser,
useUpdateAdminSlackConfig,
useUpdateServerConfig
} from "./mutation";
export {
useAdminGetUsers,
useGetAdminSlackConfig,
useGetCustomSlackAppCreationUrl,
useGetServerConfig
} from "./queries";
+23 -1
View File
@@ -5,7 +5,12 @@ import { apiRequest } from "@app/config/request";
import { organizationKeys } from "../organization/queries"; import { organizationKeys } from "../organization/queries";
import { User } from "../users/types"; import { User } from "../users/types";
import { adminQueryKeys, adminStandaloneKeys } from "./queries"; import { adminQueryKeys, adminStandaloneKeys } from "./queries";
import { TCreateAdminUserDTO, TServerConfig } from "./types"; import {
AdminSlackConfig,
TCreateAdminUserDTO,
TServerConfig,
TUpdateAdminSlackConfigDTO
} from "./types";
export const useCreateAdminUser = () => { export const useCreateAdminUser = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
@@ -59,3 +64,20 @@ export const useAdminDeleteUser = () => {
} }
}); });
}; };
export const useUpdateAdminSlackConfig = () => {
const queryClient = useQueryClient();
return useMutation<AdminSlackConfig, {}, TUpdateAdminSlackConfigDTO>({
mutationFn: async (dto) => {
const { data } = await apiRequest.put<AdminSlackConfig>(
"/api/v1/admin/integrations/slack/config",
dto
);
return data;
},
onSuccess: () => {
queryClient.invalidateQueries(adminQueryKeys.getAdminSlackConfig());
}
});
};
+28 -2
View File
@@ -3,7 +3,7 @@ import { useInfiniteQuery, useQuery, UseQueryOptions } from "@tanstack/react-que
import { apiRequest } from "@app/config/request"; import { apiRequest } from "@app/config/request";
import { User } from "../types"; import { User } from "../types";
import { AdminGetUsersFilters, TServerConfig } from "./types"; import { AdminGetUsersFilters, AdminSlackConfig, TServerConfig } from "./types";
export const adminStandaloneKeys = { export const adminStandaloneKeys = {
getUsers: "get-users" getUsers: "get-users"
@@ -11,7 +11,9 @@ export const adminStandaloneKeys = {
export const adminQueryKeys = { export const adminQueryKeys = {
serverConfig: () => ["server-config"] as const, serverConfig: () => ["server-config"] as const,
getUsers: (filters: AdminGetUsersFilters) => [adminStandaloneKeys.getUsers, { filters }] as const getUsers: (filters: AdminGetUsersFilters) => [adminStandaloneKeys.getUsers, { filters }] as const,
getCustomSlackAppCreationUrl: () => ["custom-slack-app-creation-url"] as const,
getAdminSlackConfig: () => ["admin-slack-config"] as const
}; };
const fetchServerConfig = async () => { const fetchServerConfig = async () => {
@@ -59,3 +61,27 @@ export const useAdminGetUsers = (filters: AdminGetUsersFilters) => {
lastPage.length !== 0 ? pages.length * filters.limit : undefined lastPage.length !== 0 ? pages.length * filters.limit : undefined
}); });
}; };
export const useGetCustomSlackAppCreationUrl = () =>
useQuery({
queryKey: adminQueryKeys.getCustomSlackAppCreationUrl(),
queryFn: async () => {
const { data } = await apiRequest.get<string>(
"/api/v1/admin/integrations/slack/bot-creation-url"
);
return data;
}
});
export const useGetAdminSlackConfig = () =>
useQuery({
queryKey: adminQueryKeys.getAdminSlackConfig(),
queryFn: async () => {
const { data } = await apiRequest.get<AdminSlackConfig>(
"/api/v1/admin/integrations/slack/config"
);
return data;
}
});
+10
View File
@@ -38,7 +38,17 @@ export type TCreateAdminUserDTO = {
salt: string; salt: string;
}; };
export type TUpdateAdminSlackConfigDTO = {
clientId: string;
clientSecret: string;
};
export type AdminGetUsersFilters = { export type AdminGetUsersFilters = {
limit: number; limit: number;
searchTerm: string; searchTerm: string;
}; };
export type AdminSlackConfig = {
clientId: string;
clientSecret: string;
};
@@ -25,6 +25,7 @@ import { useOrganization, useServerConfig, useUser } from "@app/context";
import { useGetOrganizations, useUpdateServerConfig } from "@app/hooks/api"; import { useGetOrganizations, useUpdateServerConfig } from "@app/hooks/api";
import { AuthPanel } from "./AuthPanel"; import { AuthPanel } from "./AuthPanel";
import { IntegrationPanel } from "./IntegrationPanel";
import { RateLimitPanel } from "./RateLimitPanel"; import { RateLimitPanel } from "./RateLimitPanel";
import { UserPanel } from "./UserPanel"; import { UserPanel } from "./UserPanel";
@@ -32,6 +33,7 @@ enum TabSections {
Settings = "settings", Settings = "settings",
Auth = "auth", Auth = "auth",
RateLimit = "rate-limit", RateLimit = "rate-limit",
Integrations = "integrations",
Users = "users" Users = "users"
} }
@@ -137,6 +139,7 @@ export const AdminDashboardPage = () => {
<Tab value={TabSections.Settings}>General</Tab> <Tab value={TabSections.Settings}>General</Tab>
<Tab value={TabSections.Auth}>Authentication</Tab> <Tab value={TabSections.Auth}>Authentication</Tab>
<Tab value={TabSections.RateLimit}>Rate Limit</Tab> <Tab value={TabSections.RateLimit}>Rate Limit</Tab>
<Tab value={TabSections.Integrations}>Integrations</Tab>
<Tab value={TabSections.Users}>Users</Tab> <Tab value={TabSections.Users}>Users</Tab>
</div> </div>
</TabList> </TabList>
@@ -323,6 +326,9 @@ export const AdminDashboardPage = () => {
<TabPanel value={TabSections.RateLimit}> <TabPanel value={TabSections.RateLimit}>
<RateLimitPanel /> <RateLimitPanel />
</TabPanel> </TabPanel>
<TabPanel value={TabSections.Integrations}>
<IntegrationPanel />
</TabPanel>
<TabPanel value={TabSections.Users}> <TabPanel value={TabSections.Users}>
<UserPanel /> <UserPanel />
</TabPanel> </TabPanel>
@@ -0,0 +1,118 @@
import { useEffect } from "react";
import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import { Button, FormControl, Input } from "@app/components/v2";
import {
useGetAdminSlackConfig,
useGetCustomSlackAppCreationUrl,
useUpdateAdminSlackConfig
} from "@app/hooks/api";
const slackFormSchema = z.object({
clientId: z.string(),
clientSecret: z.string()
});
type TSlackForm = z.infer<typeof slackFormSchema>;
export const IntegrationPanel = () => {
const {
control,
handleSubmit,
setValue,
formState: { isSubmitting, isDirty }
} = useForm<TSlackForm>({
resolver: zodResolver(slackFormSchema)
});
const { data: customSlackAppCreationUrl } = useGetCustomSlackAppCreationUrl();
const { data: adminSlackConfig } = useGetAdminSlackConfig();
const { mutateAsync: updateAdminSlackConfig } = useUpdateAdminSlackConfig();
useEffect(() => {
if (adminSlackConfig) {
setValue("clientId", adminSlackConfig.clientId);
setValue("clientSecret", adminSlackConfig.clientSecret);
}
}, [adminSlackConfig]);
const onSlackFormSubmit = async (data: TSlackForm) => {
await updateAdminSlackConfig(data);
createNotification({
text: "Updated admin slack configuration",
type: "success"
});
};
return (
<form
className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"
onSubmit={handleSubmit(onSlackFormSubmit)}
>
<div className="flex flex-col justify-start">
<div className="mb-2 text-xl font-semibold text-mineshaft-100">Slack Integration</div>
<div className="mb-4 max-w-lg text-sm text-mineshaft-300">
Step 1: Create your Infisical Slack App
</div>
<div className="mb-6">
<Button colorSchema="secondary" onClick={() => window.open(customSlackAppCreationUrl)}>
Create Slack 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 || ""}
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 || ""}
onChange={(e) => field.onChange(e.target.value)}
/>
</FormControl>
)}
/>
</div>
<Button
className="mt-2"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting || !isDirty}
>
Save
</Button>
</form>
);
};