mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-15 00:52:09 +00:00
Merge branch 'main' into ENG-2685
This commit is contained in:
@@ -31,6 +31,25 @@ const codeInputProps = {
|
||||
}
|
||||
} as const;
|
||||
|
||||
const codeInputPropsPhone = {
|
||||
inputStyle: {
|
||||
fontFamily: "monospace",
|
||||
margin: "4px",
|
||||
MozAppearance: "textfield",
|
||||
width: "40px",
|
||||
borderRadius: "5px",
|
||||
fontSize: "24px",
|
||||
height: "40px",
|
||||
paddingLeft: "7",
|
||||
backgroundColor: "#0d1117",
|
||||
color: "white",
|
||||
border: "1px solid #2d2f33",
|
||||
textAlign: "center",
|
||||
outlineColor: "#8ca542",
|
||||
borderColor: "#2d2f33"
|
||||
}
|
||||
} as const;
|
||||
|
||||
type Props = {
|
||||
successCallback: () => void | Promise<void>;
|
||||
closeMfa?: () => void;
|
||||
@@ -172,6 +191,24 @@ export const Mfa = ({ successCallback, closeMfa, hideLogo, email, method }: Prop
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="mx-auto mt-4 block w-max min-w-[18rem] md:hidden">
|
||||
{method === MfaMethod.EMAIL && (
|
||||
<ReactCodeInput
|
||||
name=""
|
||||
inputMode="tel"
|
||||
type="text"
|
||||
fields={6}
|
||||
onChange={setMfaCode}
|
||||
className="mb-2 mt-2"
|
||||
{...codeInputPropsPhone}
|
||||
/>
|
||||
)}
|
||||
{method === MfaMethod.TOTP && (
|
||||
<div className="mb-4 mt-2">
|
||||
<Input value={mfaCode} onChange={(e) => setMfaCode(e.target.value)} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{typeof triesLeft === "number" && (
|
||||
<Error text={`Invalid code. You have ${triesLeft} attempt(s) remaining.`} />
|
||||
)}
|
||||
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
import { CredentialDisplay } from "@app/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/shared/CredentialDisplay";
|
||||
import { TAzureClientSecretRotationGeneratedCredentialsResponse } from "@app/hooks/api/secretRotationsV2/types/azure-client-secret-rotation";
|
||||
|
||||
import { ViewRotationGeneratedCredentialsDisplay } from "./shared";
|
||||
|
||||
type Props = {
|
||||
generatedCredentialsResponse: TAzureClientSecretRotationGeneratedCredentialsResponse;
|
||||
};
|
||||
|
||||
export const ViewAzureClientSecretRotationGeneratedCredentials = ({
|
||||
generatedCredentialsResponse: { generatedCredentials, activeIndex }
|
||||
}: Props) => {
|
||||
const inactiveIndex = activeIndex === 0 ? 1 : 0;
|
||||
|
||||
const activeCredentials = generatedCredentials[activeIndex];
|
||||
const inactiveCredentials = generatedCredentials[inactiveIndex];
|
||||
|
||||
return (
|
||||
<ViewRotationGeneratedCredentialsDisplay
|
||||
activeCredentials={
|
||||
<>
|
||||
<CredentialDisplay label="Client ID">{activeCredentials?.clientId}</CredentialDisplay>
|
||||
<CredentialDisplay isSensitive label="Client Secret">
|
||||
{activeCredentials?.clientSecret}
|
||||
</CredentialDisplay>
|
||||
</>
|
||||
}
|
||||
inactiveCredentials={
|
||||
<>
|
||||
<CredentialDisplay label="Client ID">{inactiveCredentials?.clientId}</CredentialDisplay>
|
||||
<CredentialDisplay isSensitive label="Client Secret">
|
||||
{inactiveCredentials?.clientSecret}
|
||||
</CredentialDisplay>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
+8
@@ -4,6 +4,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { format } from "date-fns";
|
||||
|
||||
import { ViewAuth0ClientSecretRotationGeneratedCredentials } from "@app/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/ViewAuth0ClientSecretRotationGeneratedCredentials";
|
||||
import { ViewAzureClientSecretRotationGeneratedCredentials } from "@app/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/ViewAzureClientSecretRotationGeneratedCredentials";
|
||||
import { ViewLdapPasswordRotationGeneratedCredentials } from "@app/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/ViewLdapPasswordRotationGeneratedCredentials";
|
||||
import { Modal, ModalContent, Spinner } from "@app/components/v2";
|
||||
import { NoticeBannerV2 } from "@app/components/v2/NoticeBannerV2/NoticeBannerV2";
|
||||
@@ -75,6 +76,13 @@ const Content = ({ secretRotation }: ContentProps) => {
|
||||
/>
|
||||
);
|
||||
break;
|
||||
case SecretRotation.AzureClientSecret:
|
||||
Component = (
|
||||
<ViewAzureClientSecretRotationGeneratedCredentials
|
||||
generatedCredentialsResponse={generatedCredentialsResponse}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
case SecretRotation.LdapPassword:
|
||||
Component = (
|
||||
<ViewLdapPasswordRotationGeneratedCredentials
|
||||
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
import { Controller, useFormContext } from "react-hook-form";
|
||||
import { SingleValue } from "react-select";
|
||||
import { faCircleInfo } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { TSecretRotationV2Form } from "@app/components/secret-rotations-v2/forms/schemas";
|
||||
import { FilterableSelect, FormControl, Tooltip } from "@app/components/v2";
|
||||
import { useAzureConnectionListClients } from "@app/hooks/api/appConnections/azure";
|
||||
import { TAzureClient } from "@app/hooks/api/appConnections/azure/types";
|
||||
import { SecretRotation } from "@app/hooks/api/secretRotationsV2";
|
||||
|
||||
export const AzureClientSecretRotationParametersFields = () => {
|
||||
const { control, watch, setValue } = useFormContext<
|
||||
TSecretRotationV2Form & {
|
||||
type: SecretRotation.AzureClientSecret;
|
||||
}
|
||||
>();
|
||||
|
||||
const connectionId = watch("connection.id");
|
||||
|
||||
const { data: clients, isPending: isClientsPending } = useAzureConnectionListClients(
|
||||
connectionId,
|
||||
{ enabled: Boolean(connectionId) }
|
||||
);
|
||||
|
||||
return (
|
||||
<Controller
|
||||
name="parameters.objectId"
|
||||
control={control}
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
label="Application"
|
||||
helperText={
|
||||
<Tooltip
|
||||
className="max-w-md"
|
||||
content={
|
||||
<>
|
||||
Ensure that your connection has the{" "}
|
||||
<span className="font-semibold">
|
||||
Application.ReadWrite.All, Directory.ReadWrite.All,
|
||||
Application.ReadWrite.OwnedBy, user_impersonation and User.Read
|
||||
</span>{" "}
|
||||
permissions and the application exists in Azure.
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div>
|
||||
<span>Don't see the application you're looking for?</span>{" "}
|
||||
<FontAwesomeIcon icon={faCircleInfo} className="text-mineshaft-400" />
|
||||
</div>
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
<FilterableSelect
|
||||
menuPlacement="top"
|
||||
isLoading={isClientsPending && Boolean(connectionId)}
|
||||
isDisabled={!connectionId}
|
||||
value={clients?.find((client) => client.id === value) ?? null}
|
||||
onChange={(option) => {
|
||||
onChange((option as SingleValue<TAzureClient>)?.id ?? null);
|
||||
setValue("parameters.appName", (option as SingleValue<TAzureClient>)?.name ?? "");
|
||||
setValue("parameters.clientId", (option as SingleValue<TAzureClient>)?.appId ?? "");
|
||||
}}
|
||||
options={clients}
|
||||
placeholder="Select an application..."
|
||||
getOptionLabel={(option) => option.name}
|
||||
getOptionValue={(option) => option.id}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
+2
@@ -5,6 +5,7 @@ import { SecretRotation } from "@app/hooks/api/secretRotationsV2";
|
||||
import { TSecretRotationV2Form } from "../schemas";
|
||||
import { Auth0ClientSecretRotationParametersFields } from "./Auth0ClientSecretRotationParametersFields";
|
||||
import { AwsIamUserSecretRotationParametersFields } from "./AwsIamUserSecretRotationParametersFields";
|
||||
import { AzureClientSecretRotationParametersFields } from "./AzureClientSecretRotationParametersFields";
|
||||
import { LdapPasswordRotationParametersFields } from "./LdapPasswordRotationParametersFields";
|
||||
import { SqlCredentialsRotationParametersFields } from "./shared";
|
||||
|
||||
@@ -12,6 +13,7 @@ const COMPONENT_MAP: Record<SecretRotation, React.FC> = {
|
||||
[SecretRotation.PostgresCredentials]: SqlCredentialsRotationParametersFields,
|
||||
[SecretRotation.MsSqlCredentials]: SqlCredentialsRotationParametersFields,
|
||||
[SecretRotation.Auth0ClientSecret]: Auth0ClientSecretRotationParametersFields,
|
||||
[SecretRotation.AzureClientSecret]: AzureClientSecretRotationParametersFields,
|
||||
[SecretRotation.LdapPassword]: LdapPasswordRotationParametersFields,
|
||||
[SecretRotation.AwsIamUserSecret]: AwsIamUserSecretRotationParametersFields
|
||||
};
|
||||
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
import { useFormContext } from "react-hook-form";
|
||||
|
||||
import { TSecretRotationV2Form } from "@app/components/secret-rotations-v2/forms/schemas";
|
||||
import { GenericFieldLabel } from "@app/components/v2";
|
||||
import { SecretRotation } from "@app/hooks/api/secretRotationsV2";
|
||||
|
||||
import { SecretRotationReviewSection } from "./shared";
|
||||
|
||||
export const AzureClientSecretRotationReviewFields = () => {
|
||||
const { watch } = useFormContext<
|
||||
TSecretRotationV2Form & {
|
||||
type: SecretRotation.AzureClientSecret;
|
||||
}
|
||||
>();
|
||||
|
||||
const [parameters, { clientId, clientSecret }] = watch(["parameters", "secretsMapping"]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SecretRotationReviewSection label="Parameters">
|
||||
<GenericFieldLabel label="App Name">{parameters.appName}</GenericFieldLabel>
|
||||
<GenericFieldLabel label="App ID">{parameters.objectId}</GenericFieldLabel>
|
||||
</SecretRotationReviewSection>
|
||||
<SecretRotationReviewSection label="Secrets Mapping">
|
||||
<GenericFieldLabel label="Client ID">{clientId}</GenericFieldLabel>
|
||||
<GenericFieldLabel label="Client Secret">{clientSecret}</GenericFieldLabel>
|
||||
</SecretRotationReviewSection>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+2
@@ -8,6 +8,7 @@ import { SecretRotation } from "@app/hooks/api/secretRotationsV2";
|
||||
|
||||
import { Auth0ClientSecretRotationReviewFields } from "./Auth0ClientSecretRotationReviewFields";
|
||||
import { AwsIamUserSecretRotationReviewFields } from "./AwsIamUserSecretRotationReviewFields";
|
||||
import { AzureClientSecretRotationReviewFields } from "./AzureClientSecretRotationReviewFields";
|
||||
import { LdapPasswordRotationReviewFields } from "./LdapPasswordRotationReviewFields";
|
||||
import { SqlCredentialsRotationReviewFields } from "./shared";
|
||||
|
||||
@@ -15,6 +16,7 @@ const COMPONENT_MAP: Record<SecretRotation, React.FC> = {
|
||||
[SecretRotation.PostgresCredentials]: SqlCredentialsRotationReviewFields,
|
||||
[SecretRotation.MsSqlCredentials]: SqlCredentialsRotationReviewFields,
|
||||
[SecretRotation.Auth0ClientSecret]: Auth0ClientSecretRotationReviewFields,
|
||||
[SecretRotation.AzureClientSecret]: AzureClientSecretRotationReviewFields,
|
||||
[SecretRotation.LdapPassword]: LdapPasswordRotationReviewFields,
|
||||
[SecretRotation.AwsIamUserSecret]: AwsIamUserSecretRotationReviewFields
|
||||
};
|
||||
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
import { Controller, useFormContext } from "react-hook-form";
|
||||
|
||||
import { TSecretRotationV2Form } from "@app/components/secret-rotations-v2/forms/schemas";
|
||||
import { FormControl, Input } from "@app/components/v2";
|
||||
import { SecretRotation, useSecretRotationV2Option } from "@app/hooks/api/secretRotationsV2";
|
||||
|
||||
import { SecretsMappingTable } from "./shared";
|
||||
|
||||
export const AzureClientSecretRotationSecretsMappingFields = () => {
|
||||
const { control } = useFormContext<
|
||||
TSecretRotationV2Form & {
|
||||
type: SecretRotation.AzureClientSecret;
|
||||
}
|
||||
>();
|
||||
|
||||
const { rotationOption } = useSecretRotationV2Option(SecretRotation.AzureClientSecret);
|
||||
|
||||
const items = [
|
||||
{
|
||||
name: "Client ID",
|
||||
input: (
|
||||
<Controller
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl isError={Boolean(error)} errorText={error?.message}>
|
||||
<Input
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder={rotationOption?.template.secretsMapping.clientId}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
control={control}
|
||||
name="secretsMapping.clientId"
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: "Client Secret",
|
||||
input: (
|
||||
<Controller
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl isError={Boolean(error)} errorText={error?.message}>
|
||||
<Input
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder={rotationOption?.template.secretsMapping.clientSecret}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
control={control}
|
||||
name="secretsMapping.clientSecret"
|
||||
/>
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
return <SecretsMappingTable items={items} />;
|
||||
};
|
||||
+2
@@ -5,6 +5,7 @@ import { SecretRotation } from "@app/hooks/api/secretRotationsV2";
|
||||
import { TSecretRotationV2Form } from "../schemas";
|
||||
import { Auth0ClientSecretRotationSecretsMappingFields } from "./Auth0ClientSecretRotationSecretsMappingFields";
|
||||
import { AwsIamUserSecretRotationSecretsMappingFields } from "./AwsIamUserSecretRotationSecretsMappingFields";
|
||||
import { AzureClientSecretRotationSecretsMappingFields } from "./AzureClientSecretRotationSecretsMappingFields";
|
||||
import { LdapPasswordRotationSecretsMappingFields } from "./LdapPasswordRotationSecretsMappingFields";
|
||||
import { SqlCredentialsRotationSecretsMappingFields } from "./shared";
|
||||
|
||||
@@ -12,6 +13,7 @@ const COMPONENT_MAP: Record<SecretRotation, React.FC> = {
|
||||
[SecretRotation.PostgresCredentials]: SqlCredentialsRotationSecretsMappingFields,
|
||||
[SecretRotation.MsSqlCredentials]: SqlCredentialsRotationSecretsMappingFields,
|
||||
[SecretRotation.Auth0ClientSecret]: Auth0ClientSecretRotationSecretsMappingFields,
|
||||
[SecretRotation.AzureClientSecret]: AzureClientSecretRotationSecretsMappingFields,
|
||||
[SecretRotation.LdapPassword]: LdapPasswordRotationSecretsMappingFields,
|
||||
[SecretRotation.AwsIamUserSecret]: AwsIamUserSecretRotationSecretsMappingFields
|
||||
};
|
||||
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { BaseSecretRotationSchema } from "@app/components/secret-rotations-v2/forms/schemas/base-secret-rotation-v2-schema";
|
||||
import { SecretRotation } from "@app/hooks/api/secretRotationsV2";
|
||||
|
||||
export const AzureClientSecretRotationSchema = z
|
||||
.object({
|
||||
type: z.literal(SecretRotation.AzureClientSecret),
|
||||
parameters: z.object({
|
||||
objectId: z.string().trim().min(1, "Object ID required"),
|
||||
appName: z.string().trim().min(1, "App Name required"),
|
||||
clientId: z.string().trim().min(1, "Client ID required")
|
||||
}),
|
||||
secretsMapping: z.object({
|
||||
clientId: z.string().trim().min(1, "Client ID required"),
|
||||
clientSecret: z.string().trim().min(1, "Client Secret required")
|
||||
})
|
||||
})
|
||||
.merge(BaseSecretRotationSchema);
|
||||
@@ -2,14 +2,16 @@ import { z } from "zod";
|
||||
|
||||
import { Auth0ClientSecretRotationSchema } from "@app/components/secret-rotations-v2/forms/schemas/auth0-client-secret-rotation-schema";
|
||||
import { AwsIamUserSecretRotationSchema } from "@app/components/secret-rotations-v2/forms/schemas/aws-iam-user-secret-rotation-schema";
|
||||
import { AzureClientSecretRotationSchema } from "@app/components/secret-rotations-v2/forms/schemas/azure-client-secret-rotation-schema";
|
||||
import { LdapPasswordRotationSchema } from "@app/components/secret-rotations-v2/forms/schemas/ldap-password-rotation-schema";
|
||||
import { MsSqlCredentialsRotationSchema } from "@app/components/secret-rotations-v2/forms/schemas/mssql-credentials-rotation-schema";
|
||||
import { PostgresCredentialsRotationSchema } from "@app/components/secret-rotations-v2/forms/schemas/postgres-credentials-rotation-schema";
|
||||
|
||||
const SecretRotationUnionSchema = z.discriminatedUnion("type", [
|
||||
Auth0ClientSecretRotationSchema,
|
||||
AzureClientSecretRotationSchema,
|
||||
PostgresCredentialsRotationSchema,
|
||||
MsSqlCredentialsRotationSchema,
|
||||
Auth0ClientSecretRotationSchema,
|
||||
LdapPasswordRotationSchema,
|
||||
AwsIamUserSecretRotationSchema
|
||||
]);
|
||||
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
import { Controller, useFormContext, useWatch } from "react-hook-form";
|
||||
import { SingleValue } from "react-select";
|
||||
import { faCircleInfo } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { SecretSyncConnectionField } from "@app/components/secret-syncs/forms/SecretSyncConnectionField";
|
||||
import { FilterableSelect, FormControl, Input, Tooltip } from "@app/components/v2";
|
||||
import { useHCVaultConnectionListMounts } from "@app/hooks/api/appConnections/hc-vault";
|
||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||
|
||||
import { TSecretSyncForm } from "../schemas";
|
||||
|
||||
export const HCVaultSyncFields = () => {
|
||||
const { control, setValue } = useFormContext<
|
||||
TSecretSyncForm & { destination: SecretSync.HCVault }
|
||||
>();
|
||||
|
||||
const connectionId = useWatch({ name: "connection.id", control });
|
||||
|
||||
const { data: mounts, isLoading: isMountsLoading } = useHCVaultConnectionListMounts(
|
||||
connectionId,
|
||||
{
|
||||
enabled: Boolean(connectionId)
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SecretSyncConnectionField
|
||||
onChange={() => {
|
||||
setValue("destinationConfig.mount", "");
|
||||
setValue("destinationConfig.path", "");
|
||||
}}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
name="destinationConfig.mount"
|
||||
control={control}
|
||||
render={({ field: { onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
label="Secrets Engine Mount"
|
||||
helperText={
|
||||
<Tooltip
|
||||
className="max-w-md"
|
||||
content="Ensure the Secrets Engine mount exists and that your App Role / Access Token has permission to access it. Infisical only supports version 2 KV Secrets Engine mounts. If you're using Hashicorp Cloud Platform, ensure that you correctly defined your 'namespace' when creating the App Connection."
|
||||
>
|
||||
<div>
|
||||
<span>Don't see the mount you're looking for?</span>{" "}
|
||||
<FontAwesomeIcon icon={faCircleInfo} className="text-mineshaft-400" />
|
||||
</div>
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
<FilterableSelect
|
||||
menuPlacement="top"
|
||||
isLoading={isMountsLoading && Boolean(connectionId)}
|
||||
isDisabled={!connectionId}
|
||||
onChange={(option) =>
|
||||
onChange((option as SingleValue<{ value: string }>)?.value ?? null)
|
||||
}
|
||||
options={mounts?.map((v) => ({ label: v, value: v }))}
|
||||
placeholder="Select a Secrets Engine Mount..."
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="destinationConfig.path"
|
||||
control={control}
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
tooltipClassName="max-w-sm"
|
||||
tooltipText="The Secrets Engine mount path where secrets should be synced to. If the path does not exist, it will be created."
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
label="Path"
|
||||
>
|
||||
<Input value={value} onChange={onChange} placeholder="dev/example" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+3
@@ -11,6 +11,7 @@ import { CamundaSyncFields } from "./CamundaSyncFields";
|
||||
import { DatabricksSyncFields } from "./DatabricksSyncFields";
|
||||
import { GcpSyncFields } from "./GcpSyncFields";
|
||||
import { GitHubSyncFields } from "./GitHubSyncFields";
|
||||
import { HCVaultSyncFields } from "./HCVaultSyncFields";
|
||||
import { HumanitecSyncFields } from "./HumanitecSyncFields";
|
||||
import { TeamCitySyncFields } from "./TeamCitySyncFields";
|
||||
import { TerraformCloudSyncFields } from "./TerraformCloudSyncFields";
|
||||
@@ -47,6 +48,8 @@ export const SecretSyncDestinationFields = () => {
|
||||
return <VercelSyncFields />;
|
||||
case SecretSync.Windmill:
|
||||
return <WindmillSyncFields />;
|
||||
case SecretSync.HCVault:
|
||||
return <HCVaultSyncFields />;
|
||||
case SecretSync.TeamCity:
|
||||
return <TeamCitySyncFields />;
|
||||
default:
|
||||
|
||||
+1
@@ -43,6 +43,7 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => {
|
||||
case SecretSync.Camunda:
|
||||
case SecretSync.Vercel:
|
||||
case SecretSync.Windmill:
|
||||
case SecretSync.HCVault:
|
||||
case SecretSync.TeamCity:
|
||||
AdditionalSyncOptionsFieldsComponent = null;
|
||||
break;
|
||||
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
import { useFormContext } from "react-hook-form";
|
||||
|
||||
import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas";
|
||||
import { GenericFieldLabel } from "@app/components/v2";
|
||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||
|
||||
export const HCVaultSyncReviewFields = () => {
|
||||
const { watch } = useFormContext<TSecretSyncForm & { destination: SecretSync.HCVault }>();
|
||||
const mount = watch("destinationConfig.mount");
|
||||
const path = watch("destinationConfig.path");
|
||||
|
||||
return (
|
||||
<>
|
||||
<GenericFieldLabel label="Secrets Engine Mount">{mount}</GenericFieldLabel>
|
||||
<GenericFieldLabel label="Path">{path}</GenericFieldLabel>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+4
@@ -21,6 +21,7 @@ import { CamundaSyncReviewFields } from "./CamundaSyncReviewFields";
|
||||
import { DatabricksSyncReviewFields } from "./DatabricksSyncReviewFields";
|
||||
import { GcpSyncReviewFields } from "./GcpSyncReviewFields";
|
||||
import { GitHubSyncReviewFields } from "./GitHubSyncReviewFields";
|
||||
import { HCVaultSyncReviewFields } from "./HCVaultSyncReviewFields";
|
||||
import { HumanitecSyncReviewFields } from "./HumanitecSyncReviewFields";
|
||||
import { TeamCitySyncReviewFields } from "./TeamCitySyncReviewFields";
|
||||
import { TerraformCloudSyncReviewFields } from "./TerraformCloudSyncReviewFields";
|
||||
@@ -89,6 +90,9 @@ export const SecretSyncReviewFields = () => {
|
||||
case SecretSync.Windmill:
|
||||
DestinationFieldsComponent = <WindmillSyncReviewFields />;
|
||||
break;
|
||||
case SecretSync.HCVault:
|
||||
DestinationFieldsComponent = <HCVaultSyncReviewFields />;
|
||||
break;
|
||||
case SecretSync.TeamCity:
|
||||
DestinationFieldsComponent = <TeamCitySyncReviewFields />;
|
||||
break;
|
||||
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { BaseSecretSyncSchema } from "@app/components/secret-syncs/forms/schemas/base-secret-sync-schema";
|
||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||
|
||||
export const HCVaultSyncDestinationSchema = BaseSecretSyncSchema().merge(
|
||||
z.object({
|
||||
destination: z.literal(SecretSync.HCVault),
|
||||
destinationConfig: z.object({
|
||||
mount: z.string().trim().min(1, "Secrets Engine Mount required"),
|
||||
path: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Path required")
|
||||
.transform((val) => val.trim().replace(/^\/+|\/+$/g, "")) // removes leading/trailing slashes
|
||||
.refine((val) => /^([a-zA-Z0-9._-]+\/)*[a-zA-Z0-9._-]+$/.test(val), {
|
||||
message:
|
||||
"Invalid Vault path format. Use alphanumerics, dots, dashes, underscores, and single slashes between segments."
|
||||
})
|
||||
})
|
||||
})
|
||||
);
|
||||
@@ -8,6 +8,7 @@ import { CamundaSyncDestinationSchema } from "./camunda-sync-destination-schema"
|
||||
import { DatabricksSyncDestinationSchema } from "./databricks-sync-destination-schema";
|
||||
import { GcpSyncDestinationSchema } from "./gcp-sync-destination-schema";
|
||||
import { GitHubSyncDestinationSchema } from "./github-sync-destination-schema";
|
||||
import { HCVaultSyncDestinationSchema } from "./hc-vault-sync-destination-schema";
|
||||
import { HumanitecSyncDestinationSchema } from "./humanitec-sync-destination-schema";
|
||||
import { TeamCitySyncDestinationSchema } from "./teamcity-sync-destination-schema";
|
||||
import { TerraformCloudSyncDestinationSchema } from "./terraform-cloud-destination-schema";
|
||||
@@ -27,6 +28,7 @@ const SecretSyncUnionSchema = z.discriminatedUnion("destination", [
|
||||
CamundaSyncDestinationSchema,
|
||||
VercelSyncDestinationSchema,
|
||||
WindmillSyncDestinationSchema,
|
||||
HCVaultSyncDestinationSchema,
|
||||
TeamCitySyncDestinationSchema
|
||||
]);
|
||||
|
||||
|
||||
@@ -47,8 +47,10 @@ AccordionTrigger.displayName = "AccordionTrigger";
|
||||
|
||||
export const AccordionContent = forwardRef<
|
||||
HTMLDivElement,
|
||||
AccordionPrimitive.AccordionContentProps
|
||||
>(({ children, className, ...props }, forwardedRef) => (
|
||||
AccordionPrimitive.AccordionContentProps & {
|
||||
childrenClassName?: string;
|
||||
}
|
||||
>(({ children, className, childrenClassName, ...props }, forwardedRef) => (
|
||||
<AccordionPrimitive.Content
|
||||
className={twMerge(
|
||||
"overflow-hidden data-[state=closed]:animate-slideUp data-[state=open]:animate-slideDown",
|
||||
@@ -57,7 +59,7 @@ export const AccordionContent = forwardRef<
|
||||
{...props}
|
||||
ref={forwardedRef}
|
||||
>
|
||||
<div className="px-4 py-2 text-sm">{children}</div>
|
||||
<div className={twMerge("px-4 py-2 text-sm", childrenClassName)}>{children}</div>
|
||||
</AccordionPrimitive.Content>
|
||||
));
|
||||
|
||||
|
||||
@@ -17,6 +17,12 @@ export const ROUTE_PATHS = Object.freeze({
|
||||
PasswordSetupPage: setRoute("/password-setup", "/_authenticate/password-setup")
|
||||
},
|
||||
Organization: {
|
||||
Settings: {
|
||||
OauthCallbackPage: setRoute(
|
||||
"/organization/settings/oauth/callback",
|
||||
"/_authenticate/_inject-org-details/_org-layout/organization/settings/oauth/callback"
|
||||
)
|
||||
},
|
||||
SecretScanning: setRoute(
|
||||
"/organization/secret-scanning",
|
||||
"/_authenticate/_inject-org-details/_org-layout/organization/secret-scanning"
|
||||
@@ -31,7 +37,7 @@ export const ROUTE_PATHS = Object.freeze({
|
||||
),
|
||||
SettingsPage: setRoute(
|
||||
"/organization/settings",
|
||||
"/_authenticate/_inject-org-details/_org-layout/organization/settings"
|
||||
"/_authenticate/_inject-org-details/_org-layout/organization/settings/"
|
||||
),
|
||||
GroupDetailsByIDPage: setRoute(
|
||||
"/organization/groups/$groupId",
|
||||
@@ -298,6 +304,10 @@ export const ROUTE_PATHS = Object.freeze({
|
||||
SshCaByIDPage: setRoute(
|
||||
"/ssh/$projectId/ca/$caId",
|
||||
"/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/ca/$caId"
|
||||
),
|
||||
SshHostGroupDetailsByIDPage: setRoute(
|
||||
"/ssh/$projectId/ssh-host-groups/$sshHostGroupId",
|
||||
"/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/ssh-host-groups/$sshHostGroupId"
|
||||
)
|
||||
},
|
||||
Public: {
|
||||
|
||||
@@ -183,6 +183,7 @@ export enum ProjectPermissionSub {
|
||||
SshCertificateTemplates = "ssh-certificate-templates",
|
||||
SshCertificates = "ssh-certificates",
|
||||
SshHosts = "ssh-hosts",
|
||||
SshHostGroups = "ssh-host-groups",
|
||||
PkiAlerts = "pki-alerts",
|
||||
PkiCollections = "pki-collections",
|
||||
Kms = "kms",
|
||||
@@ -280,6 +281,7 @@ export type ProjectPermissionSet =
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.SshCertificateAuthorities]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.SshCertificateTemplates]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.SshCertificates]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.SshHostGroups]
|
||||
| [ProjectPermissionSshHostActions, ProjectPermissionSub.SshHosts]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.PkiCollections]
|
||||
|
||||
@@ -13,11 +13,13 @@ import {
|
||||
Auth0ConnectionMethod,
|
||||
AwsConnectionMethod,
|
||||
AzureAppConfigurationConnectionMethod,
|
||||
AzureClientSecretsConnectionMethod,
|
||||
AzureKeyVaultConnectionMethod,
|
||||
CamundaConnectionMethod,
|
||||
DatabricksConnectionMethod,
|
||||
GcpConnectionMethod,
|
||||
GitHubConnectionMethod,
|
||||
HCVaultConnectionMethod,
|
||||
HumanitecConnectionMethod,
|
||||
LdapConnectionMethod,
|
||||
MsSqlConnectionMethod,
|
||||
@@ -44,6 +46,10 @@ export const APP_CONNECTION_MAP: Record<
|
||||
name: "Azure App Configuration",
|
||||
image: "Microsoft Azure.png"
|
||||
},
|
||||
[AppConnection.AzureClientSecrets]: {
|
||||
name: "Azure Client Secrets",
|
||||
image: "Microsoft Azure.png"
|
||||
},
|
||||
[AppConnection.Databricks]: { name: "Databricks", image: "Databricks.png" },
|
||||
[AppConnection.Humanitec]: { name: "Humanitec", image: "Humanitec.png" },
|
||||
[AppConnection.TerraformCloud]: { name: "Terraform Cloud", image: "Terraform Cloud.png" },
|
||||
@@ -53,6 +59,7 @@ export const APP_CONNECTION_MAP: Record<
|
||||
[AppConnection.Camunda]: { name: "Camunda", image: "Camunda.png" },
|
||||
[AppConnection.Windmill]: { name: "Windmill", image: "Windmill.png" },
|
||||
[AppConnection.Auth0]: { name: "Auth0", image: "Auth0.png", size: 40 },
|
||||
[AppConnection.HCVault]: { name: "Hashicorp Vault", image: "Vault.png", size: 65 },
|
||||
[AppConnection.LDAP]: { name: "LDAP", image: "LDAP.png", size: 65 },
|
||||
[AppConnection.TeamCity]: { name: "TeamCity", image: "TeamCity.png" }
|
||||
};
|
||||
@@ -63,6 +70,7 @@ export const getAppConnectionMethodDetails = (method: TAppConnection["method"])
|
||||
return { name: "GitHub App", icon: faGithub };
|
||||
case AzureKeyVaultConnectionMethod.OAuth:
|
||||
case AzureAppConfigurationConnectionMethod.OAuth:
|
||||
case AzureClientSecretsConnectionMethod.OAuth:
|
||||
case GitHubConnectionMethod.OAuth:
|
||||
return { name: "OAuth", icon: faPassport };
|
||||
case AwsConnectionMethod.AccessKey:
|
||||
@@ -82,11 +90,14 @@ export const getAppConnectionMethodDetails = (method: TAppConnection["method"])
|
||||
case PostgresConnectionMethod.UsernameAndPassword:
|
||||
case MsSqlConnectionMethod.UsernameAndPassword:
|
||||
return { name: "Username & Password", icon: faLock };
|
||||
case HCVaultConnectionMethod.AccessToken:
|
||||
case TeamCityConnectionMethod.AccessToken:
|
||||
case WindmillConnectionMethod.AccessToken:
|
||||
return { name: "Access Token", icon: faKey };
|
||||
case Auth0ConnectionMethod.ClientCredentials:
|
||||
return { name: "Client Credentials", icon: faServer };
|
||||
case HCVaultConnectionMethod.AppRole:
|
||||
return { name: "App Role", icon: faUser };
|
||||
case LdapConnectionMethod.SimpleBind:
|
||||
return { name: "Simple Bind", icon: faLink };
|
||||
default:
|
||||
|
||||
@@ -20,6 +20,11 @@ export const SECRET_ROTATION_MAP: Record<
|
||||
image: "Auth0.png",
|
||||
size: 35
|
||||
},
|
||||
[SecretRotation.AzureClientSecret]: {
|
||||
name: "Azure Client Secret",
|
||||
image: "Microsoft Azure.png",
|
||||
size: 65
|
||||
},
|
||||
[SecretRotation.LdapPassword]: {
|
||||
name: "LDAP Password",
|
||||
image: "LDAP.png",
|
||||
@@ -36,6 +41,7 @@ export const SECRET_ROTATION_CONNECTION_MAP: Record<SecretRotation, AppConnectio
|
||||
[SecretRotation.PostgresCredentials]: AppConnection.Postgres,
|
||||
[SecretRotation.MsSqlCredentials]: AppConnection.MsSql,
|
||||
[SecretRotation.Auth0ClientSecret]: AppConnection.Auth0,
|
||||
[SecretRotation.AzureClientSecret]: AppConnection.AzureClientSecrets,
|
||||
[SecretRotation.LdapPassword]: AppConnection.LDAP,
|
||||
[SecretRotation.AwsIamUserSecret]: AppConnection.AWS
|
||||
};
|
||||
@@ -45,6 +51,7 @@ export const IS_ROTATION_DUAL_CREDENTIALS: Record<SecretRotation, boolean> = {
|
||||
[SecretRotation.PostgresCredentials]: true,
|
||||
[SecretRotation.MsSqlCredentials]: true,
|
||||
[SecretRotation.Auth0ClientSecret]: false,
|
||||
[SecretRotation.AzureClientSecret]: true,
|
||||
[SecretRotation.LdapPassword]: false,
|
||||
[SecretRotation.AwsIamUserSecret]: true
|
||||
};
|
||||
|
||||
@@ -40,6 +40,10 @@ export const SECRET_SYNC_MAP: Record<SecretSync, { name: string; image: string }
|
||||
name: "Windmill",
|
||||
image: "Windmill.png"
|
||||
},
|
||||
[SecretSync.HCVault]: {
|
||||
name: "Hashicorp Vault",
|
||||
image: "Vault.png"
|
||||
},
|
||||
[SecretSync.TeamCity]: {
|
||||
name: "TeamCity",
|
||||
image: "TeamCity.png"
|
||||
@@ -59,6 +63,7 @@ export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
|
||||
[SecretSync.Camunda]: AppConnection.Camunda,
|
||||
[SecretSync.Vercel]: AppConnection.Vercel,
|
||||
[SecretSync.Windmill]: AppConnection.Windmill,
|
||||
[SecretSync.HCVault]: AppConnection.HCVault,
|
||||
[SecretSync.TeamCity]: AppConnection.TeamCity
|
||||
};
|
||||
|
||||
|
||||
@@ -4,13 +4,12 @@ export {
|
||||
useAdminRemoveIdentitySuperAdminAccess,
|
||||
useCreateAdminUser,
|
||||
useRemoveUserServerAdminAccess,
|
||||
useUpdateAdminSlackConfig,
|
||||
useUpdateServerConfig,
|
||||
useUpdateServerEncryptionStrategy
|
||||
} from "./mutation";
|
||||
export {
|
||||
useAdminGetUsers,
|
||||
useGetAdminSlackConfig,
|
||||
useGetAdminIntegrationsConfig,
|
||||
useGetServerConfig,
|
||||
useGetServerRootKmsEncryptionDetails
|
||||
} from "./queries";
|
||||
|
||||
@@ -6,11 +6,10 @@ import { organizationKeys } from "../organization/queries";
|
||||
import { User } from "../users/types";
|
||||
import { adminQueryKeys, adminStandaloneKeys } from "./queries";
|
||||
import {
|
||||
AdminSlackConfig,
|
||||
RootKeyEncryptionStrategy,
|
||||
TCreateAdminUserDTO,
|
||||
TServerConfig,
|
||||
TUpdateAdminSlackConfigDTO
|
||||
TUpdateServerConfigDTO
|
||||
} from "./types";
|
||||
|
||||
export const useCreateAdminUser = () => {
|
||||
@@ -34,11 +33,7 @@ export const useCreateAdminUser = () => {
|
||||
export const useUpdateServerConfig = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<
|
||||
TServerConfig,
|
||||
object,
|
||||
Partial<TServerConfig & { slackClientId: string; slackClientSecret: string }>
|
||||
>({
|
||||
return useMutation<TServerConfig, object, TUpdateServerConfigDTO>({
|
||||
mutationFn: async (opt) => {
|
||||
const { data } = await apiRequest.patch<{ config: TServerConfig }>(
|
||||
"/api/v1/admin/config",
|
||||
@@ -48,6 +43,7 @@ export const useUpdateServerConfig = () => {
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
queryClient.setQueryData(adminQueryKeys.serverConfig(), data);
|
||||
queryClient.invalidateQueries({ queryKey: adminQueryKeys.getAdminIntegrationsConfig() });
|
||||
queryClient.invalidateQueries({ queryKey: adminQueryKeys.serverConfig() });
|
||||
queryClient.invalidateQueries({ queryKey: organizationKeys.getUserOrganizations });
|
||||
}
|
||||
@@ -119,23 +115,6 @@ export const useAdminGrantServerAdminAccess = () => {
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateAdminSlackConfig = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<AdminSlackConfig, object, TUpdateAdminSlackConfigDTO>({
|
||||
mutationFn: async (dto) => {
|
||||
const { data } = await apiRequest.put<AdminSlackConfig>(
|
||||
"/api/v1/admin/integrations/slack/config",
|
||||
dto
|
||||
);
|
||||
|
||||
return data;
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: adminQueryKeys.getAdminSlackConfig() });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateServerEncryptionStrategy = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
|
||||
@@ -7,7 +7,7 @@ import { User } from "../types";
|
||||
import {
|
||||
AdminGetIdentitiesFilters,
|
||||
AdminGetUsersFilters,
|
||||
AdminSlackConfig,
|
||||
AdminIntegrationsConfig,
|
||||
TGetServerRootKmsEncryptionDetails,
|
||||
TServerConfig
|
||||
} from "./types";
|
||||
@@ -22,7 +22,7 @@ export const adminQueryKeys = {
|
||||
getUsers: (filters: AdminGetUsersFilters) => [adminStandaloneKeys.getUsers, { filters }] as const,
|
||||
getIdentities: (filters: AdminGetIdentitiesFilters) =>
|
||||
[adminStandaloneKeys.getIdentities, { filters }] as const,
|
||||
getAdminSlackConfig: () => ["admin-slack-config"] as const,
|
||||
getAdminIntegrationsConfig: () => ["admin-integrations-config"] as const,
|
||||
getServerEncryptionStrategies: () => ["server-encryption-strategies"] as const
|
||||
};
|
||||
|
||||
@@ -95,13 +95,11 @@ export const useAdminGetIdentities = (filters: AdminGetIdentitiesFilters) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetAdminSlackConfig = () => {
|
||||
export const useGetAdminIntegrationsConfig = () => {
|
||||
return useQuery({
|
||||
queryKey: adminQueryKeys.getAdminSlackConfig(),
|
||||
queryKey: adminQueryKeys.getAdminIntegrationsConfig(),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<AdminSlackConfig>(
|
||||
"/api/v1/admin/integrations/slack/config"
|
||||
);
|
||||
const { data } = await apiRequest.get<AdminIntegrationsConfig>("/api/v1/admin/integrations");
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -26,6 +26,14 @@ export type TServerConfig = {
|
||||
pageFrameContent?: string;
|
||||
};
|
||||
|
||||
export type TUpdateServerConfigDTO = {
|
||||
slackClientId?: string;
|
||||
slackClientSecret?: string;
|
||||
microsoftTeamsAppId?: string;
|
||||
microsoftTeamsClientSecret?: string;
|
||||
microsoftTeamsBotId?: string;
|
||||
} & Partial<TServerConfig>;
|
||||
|
||||
export type TCreateAdminUserDTO = {
|
||||
email: string;
|
||||
password: string;
|
||||
@@ -42,11 +50,6 @@ export type TCreateAdminUserDTO = {
|
||||
salt: string;
|
||||
};
|
||||
|
||||
export type TUpdateAdminSlackConfigDTO = {
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
};
|
||||
|
||||
export type AdminGetUsersFilters = {
|
||||
limit: number;
|
||||
searchTerm: string;
|
||||
@@ -58,9 +61,16 @@ export type AdminGetIdentitiesFilters = {
|
||||
searchTerm: string;
|
||||
};
|
||||
|
||||
export type AdminSlackConfig = {
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
export type AdminIntegrationsConfig = {
|
||||
slack: {
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
};
|
||||
microsoftTeams: {
|
||||
appId: string;
|
||||
clientSecret: string;
|
||||
botId: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type TGetServerRootKmsEncryptionDetails = {
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./queries";
|
||||
@@ -0,0 +1,37 @@
|
||||
import { useQuery, UseQueryOptions } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { appConnectionKeys } from "../queries";
|
||||
import { TAzureClient } from "./types";
|
||||
|
||||
const azureConnectionKeys = {
|
||||
all: [...appConnectionKeys.all, "azure"] as const,
|
||||
listClients: (connectionId: string) =>
|
||||
[...azureConnectionKeys.all, "clients", connectionId] as const
|
||||
};
|
||||
|
||||
export const useAzureConnectionListClients = (
|
||||
connectionId: string,
|
||||
options?: Omit<
|
||||
UseQueryOptions<
|
||||
TAzureClient[],
|
||||
unknown,
|
||||
TAzureClient[],
|
||||
ReturnType<typeof azureConnectionKeys.listClients>
|
||||
>,
|
||||
"queryKey" | "queryFn"
|
||||
>
|
||||
) => {
|
||||
return useQuery({
|
||||
queryKey: azureConnectionKeys.listClients(connectionId),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<{ clients: TAzureClient[] }>(
|
||||
`/api/v1/app-connections/azure-client-secrets/${connectionId}/clients`
|
||||
);
|
||||
|
||||
return data.clients;
|
||||
},
|
||||
...options
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
export type TAzureClient = {
|
||||
name: string;
|
||||
appId: string;
|
||||
id: string;
|
||||
};
|
||||
@@ -4,6 +4,7 @@ export enum AppConnection {
|
||||
GCP = "gcp",
|
||||
AzureKeyVault = "azure-key-vault",
|
||||
AzureAppConfiguration = "azure-app-configuration",
|
||||
AzureClientSecrets = "azure-client-secrets",
|
||||
Databricks = "databricks",
|
||||
Humanitec = "humanitec",
|
||||
TerraformCloud = "terraform-cloud",
|
||||
@@ -13,6 +14,7 @@ export enum AppConnection {
|
||||
Camunda = "camunda",
|
||||
Windmill = "windmill",
|
||||
Auth0 = "auth0",
|
||||
HCVault = "hashicorp-vault",
|
||||
LDAP = "ldap",
|
||||
TeamCity = "teamcity"
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./queries";
|
||||
@@ -0,0 +1,36 @@
|
||||
import { useQuery, UseQueryOptions } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { appConnectionKeys } from "../queries";
|
||||
|
||||
const hcVaultConnectionKeys = {
|
||||
all: [...appConnectionKeys.all, "hcvault"] as const,
|
||||
listMounts: (connectionId: string) =>
|
||||
[...hcVaultConnectionKeys.all, "mounts", connectionId] as const
|
||||
};
|
||||
|
||||
export const useHCVaultConnectionListMounts = (
|
||||
connectionId: string,
|
||||
options?: Omit<
|
||||
UseQueryOptions<
|
||||
string[],
|
||||
unknown,
|
||||
string[],
|
||||
ReturnType<typeof hcVaultConnectionKeys.listMounts>
|
||||
>,
|
||||
"queryKey" | "queryFn"
|
||||
>
|
||||
) => {
|
||||
return useQuery({
|
||||
queryKey: hcVaultConnectionKeys.listMounts(connectionId),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<string[]>(
|
||||
`/api/v1/app-connections/hashicorp-vault/${connectionId}/mounts`
|
||||
);
|
||||
|
||||
return data;
|
||||
},
|
||||
...options
|
||||
});
|
||||
};
|
||||
@@ -31,6 +31,11 @@ export type TAzureAppConfigurationConnectionOption = TAppConnectionOptionBase &
|
||||
oauthClientId?: string;
|
||||
};
|
||||
|
||||
export type TAzureClientSecretsConnectionOption = TAppConnectionOptionBase & {
|
||||
app: AppConnection.AzureClientSecrets;
|
||||
oauthClientId?: string;
|
||||
};
|
||||
|
||||
export type TDatabricksConnectionOption = TAppConnectionOptionBase & {
|
||||
app: AppConnection.Databricks;
|
||||
};
|
||||
@@ -67,6 +72,10 @@ export type TAuth0ConnectionOption = TAppConnectionOptionBase & {
|
||||
app: AppConnection.Auth0;
|
||||
};
|
||||
|
||||
export type THCVaultConnectionOption = TAppConnectionOptionBase & {
|
||||
app: AppConnection.HCVault;
|
||||
};
|
||||
|
||||
export type TLdapConnectionOption = TAppConnectionOptionBase & {
|
||||
app: AppConnection.LDAP;
|
||||
};
|
||||
@@ -81,6 +90,7 @@ export type TAppConnectionOption =
|
||||
| TGcpConnectionOption
|
||||
| TAzureAppConfigurationConnectionOption
|
||||
| TAzureKeyVaultConnectionOption
|
||||
| TAzureClientSecretsConnectionOption
|
||||
| TDatabricksConnectionOption
|
||||
| THumanitecConnectionOption
|
||||
| TTerraformCloudConnectionOption
|
||||
@@ -90,6 +100,7 @@ export type TAppConnectionOption =
|
||||
| TCamundaConnectionOption
|
||||
| TWindmillConnectionOption
|
||||
| TAuth0ConnectionOption
|
||||
| THCVaultConnectionOption
|
||||
| TTeamCityConnectionOption;
|
||||
|
||||
export type TAppConnectionOptionMap = {
|
||||
@@ -98,6 +109,7 @@ export type TAppConnectionOptionMap = {
|
||||
[AppConnection.GCP]: TGcpConnectionOption;
|
||||
[AppConnection.AzureKeyVault]: TAzureKeyVaultConnectionOption;
|
||||
[AppConnection.AzureAppConfiguration]: TAzureAppConfigurationConnectionOption;
|
||||
[AppConnection.AzureClientSecrets]: TAzureClientSecretsConnectionOption;
|
||||
[AppConnection.Databricks]: TDatabricksConnectionOption;
|
||||
[AppConnection.Humanitec]: THumanitecConnectionOption;
|
||||
[AppConnection.TerraformCloud]: TTerraformCloudConnectionOption;
|
||||
@@ -107,6 +119,7 @@ export type TAppConnectionOptionMap = {
|
||||
[AppConnection.Camunda]: TCamundaConnectionOption;
|
||||
[AppConnection.Windmill]: TWindmillConnectionOption;
|
||||
[AppConnection.Auth0]: TAuth0ConnectionOption;
|
||||
[AppConnection.HCVault]: THCVaultConnectionOption;
|
||||
[AppConnection.LDAP]: TLdapConnectionOption;
|
||||
[AppConnection.TeamCity]: TTeamCityConnectionOption;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||
import { TRootAppConnection } from "@app/hooks/api/appConnections/types/root-connection";
|
||||
|
||||
export enum AzureClientSecretsConnectionMethod {
|
||||
OAuth = "oauth"
|
||||
}
|
||||
|
||||
export type TAzureClientSecretsConnection = TRootAppConnection & {
|
||||
app: AppConnection.AzureClientSecrets;
|
||||
} & {
|
||||
method: AzureClientSecretsConnectionMethod.OAuth;
|
||||
credentials: {
|
||||
code: string;
|
||||
tenantId: string;
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||
import { TRootAppConnection } from "@app/hooks/api/appConnections/types/root-connection";
|
||||
|
||||
export enum HCVaultConnectionMethod {
|
||||
AccessToken = "access-token",
|
||||
AppRole = "app-role"
|
||||
}
|
||||
|
||||
export type THCVaultConnection = TRootAppConnection & { app: AppConnection.HCVault } & (
|
||||
| {
|
||||
method: HCVaultConnectionMethod.AccessToken;
|
||||
credentials: {
|
||||
instanceUrl: string;
|
||||
namespace?: string;
|
||||
accessToken: string;
|
||||
};
|
||||
}
|
||||
| {
|
||||
method: HCVaultConnectionMethod.AppRole;
|
||||
credentials: {
|
||||
instanceUrl: string;
|
||||
namespace?: string;
|
||||
roleId: string;
|
||||
secretId: string;
|
||||
};
|
||||
}
|
||||
);
|
||||
@@ -3,11 +3,13 @@ import { TAppConnectionOption } from "./app-options";
|
||||
import { TAuth0Connection } from "./auth0-connection";
|
||||
import { TAwsConnection } from "./aws-connection";
|
||||
import { TAzureAppConfigurationConnection } from "./azure-app-configuration-connection";
|
||||
import { TAzureClientSecretsConnection } from "./azure-client-secrets-connection";
|
||||
import { TAzureKeyVaultConnection } from "./azure-key-vault-connection";
|
||||
import { TCamundaConnection } from "./camunda-connection";
|
||||
import { TDatabricksConnection } from "./databricks-connection";
|
||||
import { TGcpConnection } from "./gcp-connection";
|
||||
import { TGitHubConnection } from "./github-connection";
|
||||
import { THCVaultConnection } from "./hc-vault-connection";
|
||||
import { THumanitecConnection } from "./humanitec-connection";
|
||||
import { TLdapConnection } from "./ldap-connection";
|
||||
import { TMsSqlConnection } from "./mssql-connection";
|
||||
@@ -20,11 +22,13 @@ import { TWindmillConnection } from "./windmill-connection";
|
||||
export * from "./auth0-connection";
|
||||
export * from "./aws-connection";
|
||||
export * from "./azure-app-configuration-connection";
|
||||
export * from "./azure-client-secrets-connection";
|
||||
export * from "./azure-key-vault-connection";
|
||||
export * from "./camunda-connection";
|
||||
export * from "./databricks-connection";
|
||||
export * from "./gcp-connection";
|
||||
export * from "./github-connection";
|
||||
export * from "./hc-vault-connection";
|
||||
export * from "./humanitec-connection";
|
||||
export * from "./ldap-connection";
|
||||
export * from "./mssql-connection";
|
||||
@@ -40,6 +44,7 @@ export type TAppConnection =
|
||||
| TGcpConnection
|
||||
| TAzureKeyVaultConnection
|
||||
| TAzureAppConfigurationConnection
|
||||
| TAzureClientSecretsConnection
|
||||
| TDatabricksConnection
|
||||
| THumanitecConnection
|
||||
| TTerraformCloudConnection
|
||||
@@ -49,6 +54,7 @@ export type TAppConnection =
|
||||
| TCamundaConnection
|
||||
| TWindmillConnection
|
||||
| TAuth0Connection
|
||||
| THCVaultConnection
|
||||
| TLdapConnection
|
||||
| TTeamCityConnection;
|
||||
|
||||
@@ -83,6 +89,7 @@ export type TAppConnectionMap = {
|
||||
[AppConnection.GCP]: TGcpConnection;
|
||||
[AppConnection.AzureKeyVault]: TAzureKeyVaultConnection;
|
||||
[AppConnection.AzureAppConfiguration]: TAzureAppConfigurationConnection;
|
||||
[AppConnection.AzureClientSecrets]: TAzureClientSecretsConnection;
|
||||
[AppConnection.Databricks]: TDatabricksConnection;
|
||||
[AppConnection.Humanitec]: THumanitecConnection;
|
||||
[AppConnection.TerraformCloud]: TTerraformCloudConnection;
|
||||
@@ -92,6 +99,7 @@ export type TAppConnectionMap = {
|
||||
[AppConnection.Camunda]: TCamundaConnection;
|
||||
[AppConnection.Windmill]: TWindmillConnection;
|
||||
[AppConnection.Auth0]: TAuth0Connection;
|
||||
[AppConnection.HCVault]: THCVaultConnection;
|
||||
[AppConnection.LDAP]: TLdapConnection;
|
||||
[AppConnection.TeamCity]: TTeamCityConnection;
|
||||
};
|
||||
|
||||
@@ -96,8 +96,10 @@ export const eventToNameMap: { [K in EventType]: string } = {
|
||||
"Create certificate template EST configuration",
|
||||
[EventType.UPDATE_CERTIFICATE_TEMPLATE_EST_CONFIG]:
|
||||
"Update certificate template EST configuration",
|
||||
[EventType.UPDATE_PROJECT_SLACK_CONFIG]: "Update project slack configuration",
|
||||
[EventType.GET_PROJECT_SLACK_CONFIG]: "Get project slack configuration",
|
||||
[EventType.UPDATE_PROJECT_WORKFLOW_INTEGRATION_CONFIG]:
|
||||
"Update project workflow integration configuration",
|
||||
[EventType.GET_PROJECT_WORKFLOW_INTEGRATION_CONFIG]:
|
||||
"Get project workflow integration configuration",
|
||||
[EventType.INTEGRATION_SYNCED]: "Integration sync",
|
||||
[EventType.CREATE_SHARED_SECRET]: "Create shared secret",
|
||||
[EventType.DELETE_SHARED_SECRET]: "Delete shared secret",
|
||||
@@ -165,7 +167,22 @@ export const eventToNameMap: { [K in EventType]: string } = {
|
||||
[EventType.CREATE_SECRET_ROTATION]: "Create Secret Rotation",
|
||||
[EventType.UPDATE_SECRET_ROTATION]: "Update Secret Rotation",
|
||||
[EventType.DELETE_SECRET_ROTATION]: "Delete Secret Rotation",
|
||||
[EventType.SECRET_ROTATION_ROTATE_SECRETS]: "Secret Rotation secrets rotated"
|
||||
[EventType.SECRET_ROTATION_ROTATE_SECRETS]: "Secret Rotation secrets rotated",
|
||||
|
||||
[EventType.GET_PROJECT_SLACK_CONFIG]: "Get Project Slack Config",
|
||||
[EventType.UPDATE_PROJECT_SLACK_CONFIG]: "Update Project Slack Config",
|
||||
|
||||
[EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_CREATE]:
|
||||
"Create Microsoft Teams Workflow Integration",
|
||||
[EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_DELETE]:
|
||||
"Delete Microsoft Teams Workflow Integration",
|
||||
[EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_UPDATE]:
|
||||
"Update Microsoft Teams Workflow Integration",
|
||||
[EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_CHECK_INSTALLATION_STATUS]:
|
||||
"Microsoft Teams Workflow Integration Check Installation Status",
|
||||
[EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_GET_TEAMS]: "Get Microsoft Teams tenant teams",
|
||||
[EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_GET]: "Get Microsoft Teams Workflow Integration",
|
||||
[EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_LIST]: "List Microsoft Teams Workflow Integration"
|
||||
};
|
||||
|
||||
export const userAgentTTypeoNameMap: { [K in UserAgentType]: string } = {
|
||||
|
||||
@@ -100,8 +100,8 @@ export enum EventType {
|
||||
CREATE_CERTIFICATE_TEMPLATE_EST_CONFIG = "create-certificate-template-est-config",
|
||||
UPDATE_CERTIFICATE_TEMPLATE_EST_CONFIG = "update-certificate-template-est-config",
|
||||
GET_CERTIFICATE_TEMPLATE_EST_CONFIG = "get-certificate-template-est-config",
|
||||
UPDATE_PROJECT_SLACK_CONFIG = "update-project-slack-config",
|
||||
GET_PROJECT_SLACK_CONFIG = "get-project-slack-config",
|
||||
UPDATE_PROJECT_WORKFLOW_INTEGRATION_CONFIG = "update-project-workflow-integration-config",
|
||||
GET_PROJECT_WORKFLOW_INTEGRATION_CONFIG = "get-project-workflow-integration-config",
|
||||
INTEGRATION_SYNCED = "integration-synced",
|
||||
CREATE_SHARED_SECRET = "create-shared-secret",
|
||||
DELETE_SHARED_SECRET = "delete-shared-secret",
|
||||
@@ -165,5 +165,16 @@ export enum EventType {
|
||||
CREATE_SECRET_ROTATION = "create-secret-rotation",
|
||||
UPDATE_SECRET_ROTATION = "update-secret-rotation",
|
||||
DELETE_SECRET_ROTATION = "delete-secret-rotation",
|
||||
SECRET_ROTATION_ROTATE_SECRETS = "secret-rotation-rotate-secrets"
|
||||
SECRET_ROTATION_ROTATE_SECRETS = "secret-rotation-rotate-secrets",
|
||||
|
||||
GET_PROJECT_SLACK_CONFIG = "get-project-slack-config",
|
||||
UPDATE_PROJECT_SLACK_CONFIG = "update-project-slack-config",
|
||||
|
||||
MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_CREATE = "microsoft-teams-workflow-integration-create",
|
||||
MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_DELETE = "microsoft-teams-workflow-integration-delete",
|
||||
MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_UPDATE = "microsoft-teams-workflow-integration-update",
|
||||
MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_CHECK_INSTALLATION_STATUS = "microsoft-teams-workflow-integration-check-installation-status",
|
||||
MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_GET_TEAMS = "microsoft-teams-workflow-integration-get-teams",
|
||||
MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_GET = "microsoft-teams-workflow-integration-get",
|
||||
MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_LIST = "microsoft-teams-workflow-integration-list"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { CaStatus } from "../ca";
|
||||
import { IdentityTrustedIp } from "../identities/types";
|
||||
import { PkiItemType } from "../pkiCollections/constants";
|
||||
import { WorkflowIntegration } from "../workflowIntegrations/types";
|
||||
import { ActorType, EventType, UserAgentType } from "./enums";
|
||||
|
||||
export type TGetAuditLogsFilter = {
|
||||
@@ -804,11 +805,12 @@ interface GetCertificateTemplateEstConfig {
|
||||
};
|
||||
}
|
||||
|
||||
interface UpdateProjectSlackConfig {
|
||||
type: EventType.UPDATE_PROJECT_SLACK_CONFIG;
|
||||
interface UpdateProjectWorkflowIntegrationConfig {
|
||||
type: EventType.UPDATE_PROJECT_WORKFLOW_INTEGRATION_CONFIG;
|
||||
metadata: {
|
||||
id: string;
|
||||
slackIntegrationId: string;
|
||||
integrationId: string;
|
||||
integration: WorkflowIntegration;
|
||||
isAccessRequestNotificationEnabled: boolean;
|
||||
accessRequestChannels: string;
|
||||
isSecretRequestNotificationEnabled: boolean;
|
||||
@@ -816,10 +818,11 @@ interface UpdateProjectSlackConfig {
|
||||
};
|
||||
}
|
||||
|
||||
interface GetProjectSlackConfig {
|
||||
type: EventType.GET_PROJECT_SLACK_CONFIG;
|
||||
interface GetProjectWorkflowIntegrationConfig {
|
||||
type: EventType.GET_PROJECT_WORKFLOW_INTEGRATION_CONFIG;
|
||||
metadata: {
|
||||
id: string;
|
||||
integration: WorkflowIntegration;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -918,8 +921,8 @@ export type Event =
|
||||
| UpdateCertificateTemplateEstConfig
|
||||
| CreateCertificateTemplateEstConfig
|
||||
| GetCertificateTemplateEstConfig
|
||||
| UpdateProjectSlackConfig
|
||||
| GetProjectSlackConfig
|
||||
| UpdateProjectWorkflowIntegrationConfig
|
||||
| GetProjectWorkflowIntegrationConfig
|
||||
| IntegrationSyncedEvent;
|
||||
|
||||
export type AuditLog = {
|
||||
|
||||
@@ -107,6 +107,7 @@ export type CompleteAccountSignupDTO = CompleteAccountDTO & {
|
||||
providerAuthToken?: string;
|
||||
attributionSource?: string;
|
||||
organizationName: string;
|
||||
useDefaultOrg?: boolean;
|
||||
};
|
||||
|
||||
export type VerifySignupInviteDTO = {
|
||||
|
||||
@@ -44,6 +44,7 @@ export * from "./serviceTokens";
|
||||
export * from "./sshCa";
|
||||
export * from "./sshCertificateTemplates";
|
||||
export * from "./sshHost";
|
||||
export * from "./sshHostGroup";
|
||||
export * from "./ssoConfig";
|
||||
export * from "./subscriptions";
|
||||
export * from "./tags";
|
||||
|
||||
@@ -111,7 +111,8 @@ export const useUpdateOrg = () => {
|
||||
enforceMfa,
|
||||
selectedMfaMethod,
|
||||
allowSecretSharingOutsideOrganization,
|
||||
bypassOrgAuthEnabled
|
||||
bypassOrgAuthEnabled,
|
||||
userTokenExpiration
|
||||
}) => {
|
||||
return apiRequest.patch(`/api/v1/organization/${orgId}`, {
|
||||
name,
|
||||
@@ -122,7 +123,8 @@ export const useUpdateOrg = () => {
|
||||
enforceMfa,
|
||||
selectedMfaMethod,
|
||||
allowSecretSharingOutsideOrganization,
|
||||
bypassOrgAuthEnabled
|
||||
bypassOrgAuthEnabled,
|
||||
userTokenExpiration
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
|
||||
@@ -18,6 +18,7 @@ export type Organization = {
|
||||
selectedMfaMethod?: MfaMethod;
|
||||
shouldUseNewPrivilegeSystem: boolean;
|
||||
allowSecretSharingOutsideOrganization?: boolean;
|
||||
userTokenExpiration?: string;
|
||||
userRole: string;
|
||||
};
|
||||
|
||||
@@ -32,6 +33,7 @@ export type UpdateOrgDTO = {
|
||||
selectedMfaMethod?: MfaMethod;
|
||||
allowSecretSharingOutsideOrganization?: boolean;
|
||||
bypassOrgAuthEnabled?: boolean;
|
||||
userTokenExpiration?: string;
|
||||
};
|
||||
|
||||
export type BillingDetails = {
|
||||
|
||||
@@ -2,6 +2,7 @@ export enum SecretRotation {
|
||||
PostgresCredentials = "postgres-credentials",
|
||||
MsSqlCredentials = "mssql-credentials",
|
||||
Auth0ClientSecret = "auth0-client-secret",
|
||||
AzureClientSecret = "azure-client-secret",
|
||||
LdapPassword = "ldap-password",
|
||||
AwsIamUserSecret = "aws-iam-user-secret"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||
import { SecretRotation } from "@app/hooks/api/secretRotationsV2";
|
||||
import {
|
||||
TSecretRotationV2Base,
|
||||
TSecretRotationV2GeneratedCredentialsResponseBase
|
||||
} from "@app/hooks/api/secretRotationsV2/types/shared";
|
||||
|
||||
export type TAzureClientSecretRotation = TSecretRotationV2Base & {
|
||||
type: SecretRotation.AzureClientSecret;
|
||||
parameters: {
|
||||
objectId: string;
|
||||
appName: string;
|
||||
};
|
||||
secretsMapping: {
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type TAzureClientSecretRotationGeneratedCredentials = {
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
};
|
||||
|
||||
export type TAzureClientSecretRotationGeneratedCredentialsResponse =
|
||||
TSecretRotationV2GeneratedCredentialsResponseBase<
|
||||
SecretRotation.AzureClientSecret,
|
||||
TAzureClientSecretRotationGeneratedCredentials
|
||||
>;
|
||||
|
||||
export type TAzureClientSecretRotationOption = {
|
||||
name: string;
|
||||
type: SecretRotation.AzureClientSecret;
|
||||
connection: AppConnection.AzureClientSecrets;
|
||||
template: {
|
||||
secretsMapping: TAzureClientSecretRotation["secretsMapping"];
|
||||
};
|
||||
};
|
||||
@@ -9,6 +9,11 @@ import {
|
||||
TAwsIamUserSecretRotationGeneratedCredentialsResponse,
|
||||
TAwsIamUserSecretRotationOption
|
||||
} from "@app/hooks/api/secretRotationsV2/types/aws-iam-user-secret-rotation";
|
||||
import {
|
||||
TAzureClientSecretRotation,
|
||||
TAzureClientSecretRotationGeneratedCredentialsResponse,
|
||||
TAzureClientSecretRotationOption
|
||||
} from "@app/hooks/api/secretRotationsV2/types/azure-client-secret-rotation";
|
||||
import {
|
||||
TLdapPasswordRotation,
|
||||
TLdapPasswordRotationGeneratedCredentialsResponse,
|
||||
@@ -30,6 +35,7 @@ export type TSecretRotationV2 = (
|
||||
| TPostgresCredentialsRotation
|
||||
| TMsSqlCredentialsRotation
|
||||
| TAuth0ClientSecretRotation
|
||||
| TAzureClientSecretRotation
|
||||
| TLdapPasswordRotation
|
||||
| TAwsIamUserSecretRotation
|
||||
) & {
|
||||
@@ -39,6 +45,7 @@ export type TSecretRotationV2 = (
|
||||
export type TSecretRotationV2Option =
|
||||
| TSqlCredentialsRotationOption
|
||||
| TAuth0ClientSecretRotationOption
|
||||
| TAzureClientSecretRotationOption
|
||||
| TLdapPasswordRotationOption
|
||||
| TAwsIamUserSecretRotationOption;
|
||||
|
||||
@@ -50,6 +57,7 @@ export type TViewSecretRotationGeneratedCredentialsResponse =
|
||||
| TPostgresCredentialsRotationGeneratedCredentialsResponse
|
||||
| TMsSqlCredentialsRotationGeneratedCredentialsResponse
|
||||
| TAuth0ClientSecretRotationGeneratedCredentialsResponse
|
||||
| TAzureClientSecretRotationGeneratedCredentialsResponse
|
||||
| TLdapPasswordRotationGeneratedCredentialsResponse
|
||||
| TAwsIamUserSecretRotationGeneratedCredentialsResponse;
|
||||
|
||||
@@ -98,6 +106,7 @@ export type TSecretRotationOptionMap = {
|
||||
[SecretRotation.PostgresCredentials]: TSqlCredentialsRotationOption;
|
||||
[SecretRotation.MsSqlCredentials]: TSqlCredentialsRotationOption;
|
||||
[SecretRotation.Auth0ClientSecret]: TAuth0ClientSecretRotationOption;
|
||||
[SecretRotation.AzureClientSecret]: TAzureClientSecretRotationOption;
|
||||
[SecretRotation.LdapPassword]: TLdapPasswordRotationOption;
|
||||
[SecretRotation.AwsIamUserSecret]: TAwsIamUserSecretRotationOption;
|
||||
};
|
||||
@@ -106,6 +115,7 @@ export type TSecretRotationGeneratedCredentialsResponseMap = {
|
||||
[SecretRotation.PostgresCredentials]: TPostgresCredentialsRotationGeneratedCredentialsResponse;
|
||||
[SecretRotation.MsSqlCredentials]: TMsSqlCredentialsRotationGeneratedCredentialsResponse;
|
||||
[SecretRotation.Auth0ClientSecret]: TAuth0ClientSecretRotationGeneratedCredentialsResponse;
|
||||
[SecretRotation.AzureClientSecret]: TAzureClientSecretRotationGeneratedCredentialsResponse;
|
||||
[SecretRotation.LdapPassword]: TLdapPasswordRotationGeneratedCredentialsResponse;
|
||||
[SecretRotation.AwsIamUserSecret]: TAwsIamUserSecretRotationGeneratedCredentialsResponse;
|
||||
};
|
||||
|
||||
@@ -11,6 +11,7 @@ export enum SecretSync {
|
||||
Camunda = "camunda",
|
||||
Vercel = "vercel",
|
||||
Windmill = "windmill",
|
||||
HCVault = "hashicorp-vault",
|
||||
TeamCity = "teamcity"
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||
import { TRootSecretSync } from "@app/hooks/api/secretSyncs/types/root-sync";
|
||||
|
||||
export type THCVaultSync = TRootSecretSync & {
|
||||
destination: SecretSync.HCVault;
|
||||
destinationConfig: {
|
||||
mount: string;
|
||||
path: string;
|
||||
};
|
||||
connection: {
|
||||
app: AppConnection.HCVault;
|
||||
name: string;
|
||||
id: string;
|
||||
};
|
||||
};
|
||||
@@ -9,6 +9,7 @@ import { TCamundaSync } from "./camunda-sync";
|
||||
import { TDatabricksSync } from "./databricks-sync";
|
||||
import { TGcpSync } from "./gcp-sync";
|
||||
import { TGitHubSync } from "./github-sync";
|
||||
import { THCVaultSync } from "./hc-vault-sync";
|
||||
import { THumanitecSync } from "./humanitec-sync";
|
||||
import { TTeamCitySync } from "./teamcity-sync";
|
||||
import { TTerraformCloudSync } from "./terraform-cloud-sync";
|
||||
@@ -34,6 +35,7 @@ export type TSecretSync =
|
||||
| TCamundaSync
|
||||
| TVercelSync
|
||||
| TWindmillSync
|
||||
| THCVaultSync
|
||||
| TTeamCitySync;
|
||||
|
||||
export type TListSecretSyncs = { secretSyncs: TSecretSync[] };
|
||||
|
||||
@@ -1,3 +1,16 @@
|
||||
export enum LoginMappingSource {
|
||||
HOST = "host",
|
||||
HOST_GROUP = "hostGroup"
|
||||
}
|
||||
|
||||
export type TLoginMapping = {
|
||||
loginUser: string;
|
||||
allowedPrincipals: {
|
||||
usernames: string[];
|
||||
};
|
||||
source: LoginMappingSource;
|
||||
};
|
||||
|
||||
export type TSshHost = {
|
||||
id: string;
|
||||
projectId: string;
|
||||
@@ -5,26 +18,15 @@ export type TSshHost = {
|
||||
alias: string | null;
|
||||
userCertTtl: string;
|
||||
hostCertTtl: string;
|
||||
loginMappings: {
|
||||
loginUser: string;
|
||||
allowedPrincipals: {
|
||||
usernames: string[];
|
||||
};
|
||||
}[];
|
||||
loginMappings: TLoginMapping[];
|
||||
};
|
||||
|
||||
export type TCreateSshHostDTO = {
|
||||
projectId: string;
|
||||
hostname: string;
|
||||
alias?: string;
|
||||
userCertTtl?: string;
|
||||
hostCertTtl?: string;
|
||||
loginMappings: {
|
||||
loginUser: string;
|
||||
allowedPrincipals: {
|
||||
usernames: string[];
|
||||
};
|
||||
}[];
|
||||
loginMappings: Omit<TLoginMapping, "source">[];
|
||||
};
|
||||
|
||||
export type TUpdateSshHostDTO = {
|
||||
@@ -33,12 +35,7 @@ export type TUpdateSshHostDTO = {
|
||||
alias?: string;
|
||||
userCertTtl?: string;
|
||||
hostCertTtl?: string;
|
||||
loginMappings?: {
|
||||
loginUser: string;
|
||||
allowedPrincipals: {
|
||||
usernames: string[];
|
||||
};
|
||||
}[];
|
||||
loginMappings?: Omit<TLoginMapping, "source">[];
|
||||
};
|
||||
|
||||
export type TDeleteSshHostDTO = {
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
export {
|
||||
useAddHostToSshHostGroup,
|
||||
useCreateSshHostGroup,
|
||||
useDeleteSshHostGroup,
|
||||
useRemoveHostFromSshHostGroup,
|
||||
useUpdateSshHostGroup
|
||||
} from "./mutations";
|
||||
export { useGetSshHostGroupById, useListSshHostGroupHosts } from "./queries";
|
||||
@@ -0,0 +1,105 @@
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { workspaceKeys } from "../workspace/query-keys";
|
||||
import { sshHostGroupKeys } from "./queries";
|
||||
import {
|
||||
TCreateSshHostGroupDTO,
|
||||
TDeleteSshHostGroupDTO,
|
||||
TSshHostGroup,
|
||||
TUpdateSshHostGroupDTO
|
||||
} from "./types";
|
||||
|
||||
export const useCreateSshHostGroup = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<TSshHostGroup, object, TCreateSshHostGroupDTO>({
|
||||
mutationFn: async (body) => {
|
||||
const { data: hostGroup } = await apiRequest.post("/api/v1/ssh/host-groups", body);
|
||||
return hostGroup;
|
||||
},
|
||||
onSuccess: ({ projectId, id }) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: workspaceKeys.getWorkspaceSshHostGroups(projectId)
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: sshHostGroupKeys.getSshHostGroupById(id)
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateSshHostGroup = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<TSshHostGroup, object, TUpdateSshHostGroupDTO>({
|
||||
mutationFn: async ({ sshHostGroupId, ...body }) => {
|
||||
const { data: hostGroup } = await apiRequest.patch(
|
||||
`/api/v1/ssh/host-groups/${sshHostGroupId}`,
|
||||
body
|
||||
);
|
||||
return hostGroup;
|
||||
},
|
||||
onSuccess: ({ projectId }, { sshHostGroupId }) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: workspaceKeys.getWorkspaceSshHostGroups(projectId)
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: workspaceKeys.getWorkspaceSshHosts(projectId)
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: sshHostGroupKeys.getSshHostGroupById(sshHostGroupId)
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeleteSshHostGroup = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<TSshHostGroup, object, TDeleteSshHostGroupDTO>({
|
||||
mutationFn: async ({ sshHostGroupId }) => {
|
||||
const { data: hostGroup } = await apiRequest.delete(
|
||||
`/api/v1/ssh/host-groups/${sshHostGroupId}`
|
||||
);
|
||||
return hostGroup;
|
||||
},
|
||||
onSuccess: ({ projectId }, { sshHostGroupId }) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: workspaceKeys.getWorkspaceSshHostGroups(projectId)
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: workspaceKeys.getWorkspaceSshHosts(projectId)
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: sshHostGroupKeys.getSshHostGroupById(sshHostGroupId)
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useAddHostToSshHostGroup = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<void, object, { sshHostGroupId: string; sshHostId: string }>({
|
||||
mutationFn: async ({ sshHostGroupId, sshHostId }) => {
|
||||
await apiRequest.post(`/api/v1/ssh/host-groups/${sshHostGroupId}/hosts/${sshHostId}`);
|
||||
},
|
||||
onSuccess: (_, { sshHostGroupId }) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: sshHostGroupKeys.forSshHostGroupHosts(sshHostGroupId)
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useRemoveHostFromSshHostGroup = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<void, object, { sshHostGroupId: string; sshHostId: string }>({
|
||||
mutationFn: async ({ sshHostGroupId, sshHostId }) => {
|
||||
await apiRequest.delete(`/api/v1/ssh/host-groups/${sshHostGroupId}/hosts/${sshHostId}`);
|
||||
},
|
||||
onSuccess: (_, { sshHostGroupId }) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: sshHostGroupKeys.forSshHostGroupHosts(sshHostGroupId)
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,60 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { EHostGroupMembershipFilter, TListSshHostGroupHostsResponse, TSshHostGroup } from "./types";
|
||||
|
||||
export const sshHostGroupKeys = {
|
||||
getSshHostGroupById: (sshHostGroupId: string) => [{ sshHostGroupId }, "ssh-host-group"],
|
||||
allSshHostGroupHosts: () => ["ssh-host-group-hosts"] as const,
|
||||
forSshHostGroupHosts: (sshHostGroupId: string) =>
|
||||
[...sshHostGroupKeys.allSshHostGroupHosts(), sshHostGroupId] as const,
|
||||
specificSshHostGroupHosts: ({
|
||||
sshHostGroupId,
|
||||
filter
|
||||
}: {
|
||||
sshHostGroupId: string;
|
||||
filter?: EHostGroupMembershipFilter;
|
||||
}) => [...sshHostGroupKeys.forSshHostGroupHosts(sshHostGroupId), { filter }] as const
|
||||
};
|
||||
|
||||
export const useGetSshHostGroupById = (sshHostGroupId: string) => {
|
||||
return useQuery({
|
||||
queryKey: sshHostGroupKeys.getSshHostGroupById(sshHostGroupId),
|
||||
queryFn: async () => {
|
||||
const { data: sshHostGroup } = await apiRequest.get<TSshHostGroup>(
|
||||
`/api/v1/ssh/host-groups/${sshHostGroupId}`
|
||||
);
|
||||
return sshHostGroup;
|
||||
},
|
||||
enabled: Boolean(sshHostGroupId)
|
||||
});
|
||||
};
|
||||
|
||||
export const useListSshHostGroupHosts = ({
|
||||
sshHostGroupId,
|
||||
filter
|
||||
}: {
|
||||
sshHostGroupId: string;
|
||||
filter?: EHostGroupMembershipFilter;
|
||||
}) => {
|
||||
return useQuery({
|
||||
queryKey: sshHostGroupKeys.specificSshHostGroupHosts({ sshHostGroupId, filter }),
|
||||
queryFn: async () => {
|
||||
const params = new URLSearchParams({
|
||||
...(filter ? { filter } : {})
|
||||
});
|
||||
|
||||
const { data } = await apiRequest.get<TListSshHostGroupHostsResponse>(
|
||||
`/api/v1/ssh/host-groups/${sshHostGroupId}/hosts`,
|
||||
{
|
||||
params
|
||||
}
|
||||
);
|
||||
return data;
|
||||
},
|
||||
enabled: Boolean(sshHostGroupId),
|
||||
staleTime: 0,
|
||||
gcTime: 0
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
import { TLoginMapping, TSshHost } from "../sshHost/types";
|
||||
|
||||
export type TSshHostGroup = {
|
||||
id: string;
|
||||
projectId: string;
|
||||
name: string;
|
||||
loginMappings: Omit<TLoginMapping, "source">[];
|
||||
};
|
||||
|
||||
export type TCreateSshHostGroupDTO = {
|
||||
projectId: string;
|
||||
name: string;
|
||||
loginMappings: Omit<TLoginMapping, "source">[];
|
||||
};
|
||||
|
||||
export type TUpdateSshHostGroupDTO = {
|
||||
sshHostGroupId: string;
|
||||
name?: string;
|
||||
loginMappings?: Omit<TLoginMapping, "source">[];
|
||||
};
|
||||
|
||||
export type TDeleteSshHostGroupDTO = {
|
||||
sshHostGroupId: string;
|
||||
};
|
||||
|
||||
export type TListSshHostGroupHostsResponse = {
|
||||
hosts: (TSshHost & { joinedGroupAt: string; isPartOfGroup: boolean })[];
|
||||
totalCount: number;
|
||||
};
|
||||
|
||||
export enum EHostGroupMembershipFilter {
|
||||
GROUP_MEMBERS = "group-members",
|
||||
NON_GROUP_MEMBERS = "non-group-members"
|
||||
}
|
||||
@@ -24,6 +24,7 @@ export type SubscriptionPlan = {
|
||||
workspacesUsed: number;
|
||||
environmentLimit: number;
|
||||
samlSSO: boolean;
|
||||
sshHostGroups: boolean;
|
||||
secretAccessInsights: boolean;
|
||||
hsm: boolean;
|
||||
oidcSSO: boolean;
|
||||
|
||||
@@ -1,13 +1,2 @@
|
||||
export {
|
||||
useDeleteSlackIntegration,
|
||||
useUpdateProjectSlackConfig,
|
||||
useUpdateSlackIntegration
|
||||
} from "./mutation";
|
||||
export {
|
||||
fetchSlackInstallUrl,
|
||||
fetchSlackReinstallUrl,
|
||||
useGetSlackIntegrationById,
|
||||
useGetSlackIntegrationChannels,
|
||||
useGetSlackIntegrations,
|
||||
useGetWorkflowIntegrations
|
||||
} from "./queries";
|
||||
export * from "./mutation";
|
||||
export * from "./queries";
|
||||
|
||||
@@ -5,8 +5,13 @@ import { apiRequest } from "@app/config/request";
|
||||
import { workspaceKeys } from "../workspace/query-keys";
|
||||
import { workflowIntegrationKeys } from "./queries";
|
||||
import {
|
||||
TCheckMicrosoftTeamsIntegrationInstallationStatusDTO,
|
||||
TCreateMicrosoftTeamsIntegrationDTO,
|
||||
TDeleteMicrosoftTeamsIntegrationDTO,
|
||||
TDeleteProjectWorkflowIntegrationDTO,
|
||||
TDeleteSlackIntegrationDTO,
|
||||
TUpdateProjectSlackConfigDTO,
|
||||
TUpdateMicrosoftTeamsIntegrationDTO,
|
||||
TUpdateProjectWorkflowIntegrationConfigDTO,
|
||||
TUpdateSlackIntegrationDTO
|
||||
} from "./types";
|
||||
|
||||
@@ -28,6 +33,47 @@ export const useUpdateSlackIntegration = () => {
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateMicrosoftTeamsIntegration = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<object, object, TUpdateMicrosoftTeamsIntegrationDTO>({
|
||||
mutationFn: async (dto) => {
|
||||
const { data } = await apiRequest.patch(
|
||||
`/api/v1/workflow-integrations/microsoft-teams/${dto.id}`,
|
||||
dto
|
||||
);
|
||||
|
||||
return data;
|
||||
},
|
||||
onSuccess: (_, { orgId, id }) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: workflowIntegrationKeys.getMicrosoftTeamsIntegration(id)
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: workflowIntegrationKeys.getMicrosoftTeamsIntegrations(orgId)
|
||||
});
|
||||
queryClient.invalidateQueries({ queryKey: workflowIntegrationKeys.getIntegrations(orgId) });
|
||||
}
|
||||
});
|
||||
};
|
||||
export const useCreateMicrosoftTeamsIntegration = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<object, object, TCreateMicrosoftTeamsIntegrationDTO>({
|
||||
mutationFn: async (dto) => {
|
||||
const { data } = await apiRequest.post("/api/v1/workflow-integrations/microsoft-teams", dto);
|
||||
|
||||
return data;
|
||||
},
|
||||
onSuccess: (_, { orgId }) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: workflowIntegrationKeys.getMicrosoftTeamsIntegrations(orgId)
|
||||
});
|
||||
queryClient.invalidateQueries({ queryKey: workflowIntegrationKeys.getIntegrations(orgId) });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeleteSlackIntegration = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
@@ -44,21 +90,86 @@ export const useDeleteSlackIntegration = () => {
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateProjectSlackConfig = () => {
|
||||
export const useDeleteMicrosoftTeamsIntegration = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<object, object, TDeleteMicrosoftTeamsIntegrationDTO>({
|
||||
mutationFn: async (dto) => {
|
||||
const { data } = await apiRequest.delete(
|
||||
`/api/v1/workflow-integrations/microsoft-teams/${dto.id}`
|
||||
);
|
||||
|
||||
return data;
|
||||
},
|
||||
onSuccess: (_, { orgId, id }) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: workflowIntegrationKeys.getMicrosoftTeamsIntegration(id)
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: workflowIntegrationKeys.getMicrosoftTeamsIntegrations(orgId)
|
||||
});
|
||||
queryClient.invalidateQueries({ queryKey: workflowIntegrationKeys.getIntegrations(orgId) });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateProjectWorkflowIntegrationConfig = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async (dto: TUpdateProjectSlackConfigDTO) => {
|
||||
mutationFn: async (dto: TUpdateProjectWorkflowIntegrationConfigDTO) => {
|
||||
const { data } = await apiRequest.put(
|
||||
`/api/v1/workspace/${dto.workspaceId}/slack-config`,
|
||||
`/api/v1/workspace/${dto.workspaceId}/workflow-integration`,
|
||||
dto
|
||||
);
|
||||
|
||||
return data;
|
||||
},
|
||||
onSuccess: (_, { workspaceId }) => {
|
||||
onSuccess: (_, { workspaceId, integration }) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: workspaceKeys.getWorkspaceSlackConfig(workspaceId)
|
||||
queryKey: workspaceKeys.getWorkspaceWorkflowIntegrationConfig(workspaceId, integration)
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeleteProjectWorkflowIntegration = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (dto: TDeleteProjectWorkflowIntegrationDTO) => {
|
||||
const { data } = await apiRequest.delete(
|
||||
`/api/v1/workspace/${dto.projectId}/workflow-integration/${dto.integration}/${dto.integrationId}`
|
||||
);
|
||||
|
||||
return data;
|
||||
},
|
||||
onSuccess: (_, { projectId, integration }) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: workspaceKeys.getWorkspaceWorkflowIntegrationConfig(projectId, integration)
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useCheckMicrosoftTeamsIntegrationInstallationStatus = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (dto: TCheckMicrosoftTeamsIntegrationInstallationStatusDTO) => {
|
||||
const { data } = await apiRequest.post(
|
||||
`/api/v1/workflow-integrations/microsoft-teams/${dto.workflowIntegrationId}/installation-status`
|
||||
);
|
||||
|
||||
return data;
|
||||
},
|
||||
onSuccess: (_, { workflowIntegrationId, orgId }) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: workflowIntegrationKeys.getMicrosoftTeamsIntegration(workflowIntegrationId)
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: workflowIntegrationKeys.getMicrosoftTeamsIntegrations(orgId)
|
||||
});
|
||||
queryClient.invalidateQueries({ queryKey: workflowIntegrationKeys.getIntegrations(orgId) });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -2,13 +2,29 @@ import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { SlackIntegration, SlackIntegrationChannel, WorkflowIntegration } from "./types";
|
||||
import {
|
||||
MicrosoftTeamsIntegration,
|
||||
MicrosoftTeamsIntegrationTeam,
|
||||
SlackIntegration,
|
||||
SlackIntegrationChannel,
|
||||
WorkflowIntegration
|
||||
} from "./types";
|
||||
|
||||
export const workflowIntegrationKeys = {
|
||||
getIntegrations: (orgId?: string) => [{ orgId }, "workflow-integrations"],
|
||||
getSlackIntegrations: (orgId?: string) => [{ orgId }, "slack-workflow-integrations"],
|
||||
getSlackIntegration: (id?: string) => [{ id }, "slack-workflow-integration"],
|
||||
getSlackIntegrationChannels: (id?: string) => [{ id }, "slack-workflow-integration-channels"]
|
||||
getMicrosoftTeamsIntegrations: (orgId?: string) => [
|
||||
{ orgId },
|
||||
"microsoft-teams-workflow-integrations"
|
||||
],
|
||||
getMicrosoftTeamsIntegration: (id?: string) => [{ id }, "microsoft-teams-workflow-integration"],
|
||||
getMicrosoftTeamsIntegrationTeams: (id?: string) => [
|
||||
{ id },
|
||||
"microsoft-teams-workflow-integration-teams"
|
||||
],
|
||||
getSlackIntegrationChannels: (id?: string) => [{ id }, "slack-workflow-integration-channels"],
|
||||
getMicrosoftTeamsClientId: () => ["microsoft-teams-client-id"]
|
||||
};
|
||||
|
||||
export const fetchSlackInstallUrl = async ({
|
||||
@@ -66,13 +82,58 @@ export const fetchWorkflowIntegrations = async () => {
|
||||
return data;
|
||||
};
|
||||
|
||||
export const useGetSlackIntegrations = (orgId?: string) =>
|
||||
export const fetchMicrosoftTeamsIntegrations = async () => {
|
||||
const { data } = await apiRequest.get<MicrosoftTeamsIntegration[]>(
|
||||
"/api/v1/workflow-integrations/microsoft-teams"
|
||||
);
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
export const fetchMicrosoftTeamsIntegrationById = async (id?: string) => {
|
||||
const { data } = await apiRequest.get<MicrosoftTeamsIntegration>(
|
||||
`/api/v1/workflow-integrations/microsoft-teams/${id}`
|
||||
);
|
||||
|
||||
return data;
|
||||
};
|
||||
export const fetchMicrosoftTeamsIntegrationTeams = async (id?: string) => {
|
||||
const { data } = await apiRequest.get<MicrosoftTeamsIntegrationTeam[]>(
|
||||
`/api/v1/workflow-integrations/microsoft-teams/${id}/teams`
|
||||
);
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
export const fetchMicrosoftTeamsClientId = async () => {
|
||||
const { data } = await apiRequest.get<{ clientId: string }>(
|
||||
"/api/v1/workflow-integrations/microsoft-teams/client-id"
|
||||
);
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
export const useGetMicrosoftTeamsIntegrations = (orgId?: string) =>
|
||||
useQuery({
|
||||
queryKey: workflowIntegrationKeys.getSlackIntegrations(orgId),
|
||||
queryFn: () => fetchSlackIntegrations(),
|
||||
queryKey: workflowIntegrationKeys.getMicrosoftTeamsIntegrations(orgId),
|
||||
queryFn: () => fetchMicrosoftTeamsIntegrations(),
|
||||
enabled: Boolean(orgId)
|
||||
});
|
||||
|
||||
export const useGetMicrosoftTeamsIntegrationById = (id?: string) =>
|
||||
useQuery({
|
||||
queryKey: workflowIntegrationKeys.getMicrosoftTeamsIntegration(id),
|
||||
queryFn: () => fetchMicrosoftTeamsIntegrationById(id),
|
||||
enabled: Boolean(id)
|
||||
});
|
||||
|
||||
export const useGetMicrosoftTeamsIntegrationTeams = (id?: string) =>
|
||||
useQuery({
|
||||
queryKey: workflowIntegrationKeys.getMicrosoftTeamsIntegrationTeams(id),
|
||||
queryFn: () => fetchMicrosoftTeamsIntegrationTeams(id),
|
||||
enabled: Boolean(id)
|
||||
});
|
||||
|
||||
export const useGetSlackIntegrationById = (id?: string) =>
|
||||
useQuery({
|
||||
queryKey: workflowIntegrationKeys.getSlackIntegration(id),
|
||||
@@ -93,3 +154,10 @@ export const useGetWorkflowIntegrations = (id?: string) =>
|
||||
queryFn: () => fetchWorkflowIntegrations(),
|
||||
enabled: Boolean(id)
|
||||
});
|
||||
|
||||
export const useGetMicrosoftTeamsClientId = () =>
|
||||
useQuery({
|
||||
queryKey: workflowIntegrationKeys.getMicrosoftTeamsClientId(),
|
||||
queryFn: () => fetchMicrosoftTeamsClientId(),
|
||||
enabled: true
|
||||
});
|
||||
|
||||
@@ -1,14 +1,25 @@
|
||||
export enum WorkflowIntegrationPlatform {
|
||||
SLACK = "slack"
|
||||
SLACK = "slack",
|
||||
MICROSOFT_TEAMS = "microsoft-teams"
|
||||
}
|
||||
|
||||
export type WorkflowIntegration = {
|
||||
id: string;
|
||||
slug: string;
|
||||
description: string;
|
||||
status: WorkflowIntegrationStatus;
|
||||
integration: WorkflowIntegrationPlatform;
|
||||
};
|
||||
|
||||
export type MicrosoftTeamsIntegrationTeam = {
|
||||
teamId: string;
|
||||
teamName: string;
|
||||
channels: {
|
||||
channelId: string;
|
||||
channelName: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
export type SlackIntegration = {
|
||||
id: string;
|
||||
slug: string;
|
||||
@@ -16,6 +27,18 @@ export type SlackIntegration = {
|
||||
teamName: string;
|
||||
};
|
||||
|
||||
export enum WorkflowIntegrationStatus {
|
||||
Pending = "pending",
|
||||
Installed = "installed"
|
||||
}
|
||||
|
||||
export type MicrosoftTeamsIntegration = {
|
||||
id: string;
|
||||
slug: string;
|
||||
description: string;
|
||||
tenantId: string;
|
||||
};
|
||||
|
||||
export type SlackIntegrationChannel = {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -28,25 +51,90 @@ export type TUpdateSlackIntegrationDTO = {
|
||||
description?: string;
|
||||
};
|
||||
|
||||
export type TUpdateMicrosoftTeamsIntegrationDTO = {
|
||||
id: string;
|
||||
orgId: string;
|
||||
slug?: string;
|
||||
description?: string;
|
||||
};
|
||||
|
||||
export type TCreateMicrosoftTeamsIntegrationDTO = {
|
||||
code: string;
|
||||
tenantId: string;
|
||||
slug: string;
|
||||
description?: string;
|
||||
redirectUri: string;
|
||||
orgId: string;
|
||||
};
|
||||
|
||||
export type TDeleteSlackIntegrationDTO = {
|
||||
id: string;
|
||||
orgId: string;
|
||||
};
|
||||
|
||||
export type ProjectSlackConfig = {
|
||||
export type TDeleteMicrosoftTeamsIntegrationDTO = {
|
||||
id: string;
|
||||
slackIntegrationId: string;
|
||||
isAccessRequestNotificationEnabled: boolean;
|
||||
accessRequestChannels: string;
|
||||
isSecretRequestNotificationEnabled: boolean;
|
||||
secretRequestChannels: string;
|
||||
orgId: string;
|
||||
};
|
||||
|
||||
export type TUpdateProjectSlackConfigDTO = {
|
||||
workspaceId: string;
|
||||
slackIntegrationId: string;
|
||||
isAccessRequestNotificationEnabled: boolean;
|
||||
accessRequestChannels: string;
|
||||
isSecretRequestNotificationEnabled: boolean;
|
||||
secretRequestChannels: string;
|
||||
export type ProjectWorkflowIntegrationConfig =
|
||||
| {
|
||||
id: string;
|
||||
integration: WorkflowIntegrationPlatform.SLACK;
|
||||
integrationId: string;
|
||||
isAccessRequestNotificationEnabled: boolean;
|
||||
accessRequestChannels: string;
|
||||
isSecretRequestNotificationEnabled: boolean;
|
||||
secretRequestChannels: string;
|
||||
}
|
||||
| {
|
||||
id: string;
|
||||
integration: WorkflowIntegrationPlatform.MICROSOFT_TEAMS;
|
||||
integrationId: string;
|
||||
isAccessRequestNotificationEnabled: boolean;
|
||||
isSecretRequestNotificationEnabled: boolean;
|
||||
accessRequestChannels: {
|
||||
teamId: string;
|
||||
channelIds: string[];
|
||||
};
|
||||
secretRequestChannels: {
|
||||
teamId: string;
|
||||
channelIds: string[];
|
||||
};
|
||||
};
|
||||
|
||||
export type TUpdateProjectWorkflowIntegrationConfigDTO =
|
||||
| {
|
||||
integration: WorkflowIntegrationPlatform.SLACK;
|
||||
workspaceId: string;
|
||||
integrationId: string;
|
||||
isAccessRequestNotificationEnabled: boolean;
|
||||
accessRequestChannels: string;
|
||||
isSecretRequestNotificationEnabled: boolean;
|
||||
secretRequestChannels: string;
|
||||
}
|
||||
| {
|
||||
integration: WorkflowIntegrationPlatform.MICROSOFT_TEAMS;
|
||||
workspaceId: string;
|
||||
integrationId: string;
|
||||
isAccessRequestNotificationEnabled: boolean;
|
||||
isSecretRequestNotificationEnabled: boolean;
|
||||
accessRequestChannels?: {
|
||||
teamId: string;
|
||||
channelIds: string[];
|
||||
};
|
||||
secretRequestChannels?: {
|
||||
teamId: string;
|
||||
channelIds: string[];
|
||||
};
|
||||
};
|
||||
export type TDeleteProjectWorkflowIntegrationDTO = {
|
||||
projectId: string;
|
||||
integration: WorkflowIntegrationPlatform;
|
||||
integrationId: string;
|
||||
};
|
||||
|
||||
export type TCheckMicrosoftTeamsIntegrationInstallationStatusDTO = {
|
||||
workflowIntegrationId: string;
|
||||
orgId: string;
|
||||
};
|
||||
|
||||
@@ -26,9 +26,9 @@ export {
|
||||
useGetWorkspaceIndexStatus,
|
||||
useGetWorkspaceIntegrations,
|
||||
useGetWorkspaceSecrets,
|
||||
useGetWorkspaceSlackConfig,
|
||||
useGetWorkspaceUserDetails,
|
||||
useGetWorkspaceUsers,
|
||||
useGetWorkspaceWorkflowIntegrationConfig,
|
||||
useListWorkspaceCas,
|
||||
useListWorkspaceCertificates,
|
||||
useListWorkspaceCertificateTemplates,
|
||||
@@ -38,6 +38,7 @@ export {
|
||||
useListWorkspaceSshCas,
|
||||
useListWorkspaceSshCertificates,
|
||||
useListWorkspaceSshCertificateTemplates,
|
||||
useListWorkspaceSshHostGroups,
|
||||
useListWorkspaceSshHosts,
|
||||
useNameWorkspaceSecrets,
|
||||
useSearchProjects,
|
||||
|
||||
@@ -18,9 +18,13 @@ import { EncryptedSecret } from "../secrets/types";
|
||||
import { TSshCertificate, TSshCertificateAuthority } from "../sshCa/types";
|
||||
import { TSshCertificateTemplate } from "../sshCertificateTemplates/types";
|
||||
import { TSshHost } from "../sshHost/types";
|
||||
import { TSshHostGroup } from "../sshHostGroup/types";
|
||||
import { userKeys } from "../users/query-keys";
|
||||
import { TWorkspaceUser } from "../users/types";
|
||||
import { ProjectSlackConfig } from "../workflowIntegrations/types";
|
||||
import {
|
||||
ProjectWorkflowIntegrationConfig,
|
||||
WorkflowIntegrationPlatform
|
||||
} from "../workflowIntegrations/types";
|
||||
import { workspaceKeys } from "./query-keys";
|
||||
import {
|
||||
CreateEnvironmentDTO,
|
||||
@@ -870,6 +874,21 @@ export const useListWorkspaceSshHosts = (projectId: string) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const useListWorkspaceSshHostGroups = (projectId: string) => {
|
||||
return useQuery({
|
||||
queryKey: workspaceKeys.getWorkspaceSshHostGroups(projectId),
|
||||
queryFn: async () => {
|
||||
const {
|
||||
data: { groups }
|
||||
} = await apiRequest.get<{ groups: (TSshHostGroup & { hostCount: number })[] }>(
|
||||
`/api/v2/workspace/${projectId}/ssh-host-groups`
|
||||
);
|
||||
return groups;
|
||||
},
|
||||
enabled: Boolean(projectId)
|
||||
});
|
||||
};
|
||||
|
||||
export const useListWorkspaceSshCertificateTemplates = (projectId: string) => {
|
||||
return useQuery({
|
||||
queryKey: workspaceKeys.getWorkspaceSshCertificateTemplates(projectId),
|
||||
@@ -883,13 +902,27 @@ export const useListWorkspaceSshCertificateTemplates = (projectId: string) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetWorkspaceSlackConfig = ({ workspaceId }: { workspaceId: string }) => {
|
||||
export const useGetWorkspaceWorkflowIntegrationConfig = ({
|
||||
workspaceId,
|
||||
integration
|
||||
}: {
|
||||
workspaceId: string;
|
||||
integration: WorkflowIntegrationPlatform;
|
||||
}) => {
|
||||
return useQuery({
|
||||
queryKey: workspaceKeys.getWorkspaceSlackConfig(workspaceId),
|
||||
queryKey: workspaceKeys.getWorkspaceWorkflowIntegrationConfig(workspaceId, integration),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<ProjectSlackConfig>(
|
||||
`/api/v1/workspace/${workspaceId}/slack-config`
|
||||
);
|
||||
const { data } = await apiRequest
|
||||
.get<ProjectWorkflowIntegrationConfig>(
|
||||
`/api/v1/workspace/${workspaceId}/workflow-integration-config/${integration}`
|
||||
)
|
||||
.catch((err) => {
|
||||
if (err.response.status === 404) {
|
||||
return { data: null };
|
||||
}
|
||||
|
||||
throw err;
|
||||
});
|
||||
|
||||
return data;
|
||||
},
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { TListProjectIdentitiesDTO, TSearchProjectsDTO } from "@app/hooks/api/workspace/types";
|
||||
|
||||
import type { CaStatus } from "../ca";
|
||||
import { WorkflowIntegrationPlatform } from "../workflowIntegrations/types";
|
||||
|
||||
export const workspaceKeys = {
|
||||
getWorkspaceById: (workspaceId: string) => ["workspaces", { workspaceId }] as const,
|
||||
@@ -54,12 +55,16 @@ export const workspaceKeys = {
|
||||
[{ workspaceId }, "workspace-pki-collections"] as const,
|
||||
getWorkspaceCertificateTemplates: (workspaceId: string) =>
|
||||
[{ workspaceId }, "workspace-certificate-templates"] as const,
|
||||
getWorkspaceSlackConfig: (workspaceId: string) =>
|
||||
[{ workspaceId }, "workspace-slack-config"] as const,
|
||||
getWorkspaceWorkflowIntegrationConfig: (
|
||||
workspaceId: string,
|
||||
integration: WorkflowIntegrationPlatform
|
||||
) => [{ workspaceId, integration }, "workspace-workflow-integration-config"] as const,
|
||||
getWorkspaceSshCas: (projectId: string) => [{ projectId }, "workspace-ssh-cas"] as const,
|
||||
allWorkspaceSshCertificates: (projectId: string) =>
|
||||
[{ projectId }, "workspace-ssh-certificates"] as const,
|
||||
getWorkspaceSshHosts: (projectId: string) => [{ projectId }, "workspace-ssh-hosts"] as const,
|
||||
getWorkspaceSshHostGroups: (projectId: string) =>
|
||||
[{ projectId }, "workspace-ssh-host-groups"] as const,
|
||||
specificWorkspaceSshCertificates: ({
|
||||
offset,
|
||||
limit,
|
||||
|
||||
@@ -197,4 +197,4 @@ html {
|
||||
position: absolute;
|
||||
top: 0.5rem;
|
||||
@apply text-sm text-gray-500 opacity-50;
|
||||
}
|
||||
}
|
||||
@@ -235,7 +235,7 @@ export const OverviewPage = () => {
|
||||
Default organization
|
||||
</div>
|
||||
<div className="mb-4 max-w-sm text-sm text-mineshaft-400">
|
||||
Select the default organization you want to set for SAML/LDAP/OIDC based
|
||||
Select the default organization you want to set for SAML/LDAP/OIDC/Github
|
||||
logins. When selected, user logins will be automatically scoped to the
|
||||
selected organization.
|
||||
</div>
|
||||
|
||||
@@ -1,166 +1,24 @@
|
||||
import { useEffect } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
import { useGetAdminIntegrationsConfig } from "@app/hooks/api";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Button, FormControl, Input } from "@app/components/v2";
|
||||
import { useToggle } from "@app/hooks";
|
||||
import { useGetAdminSlackConfig, useUpdateServerConfig } from "@app/hooks/api";
|
||||
|
||||
const slackFormSchema = z.object({
|
||||
clientId: z.string(),
|
||||
clientSecret: z.string()
|
||||
});
|
||||
|
||||
type TSlackForm = z.infer<typeof slackFormSchema>;
|
||||
|
||||
const getCustomSlackAppCreationUrl = () =>
|
||||
`https://api.slack.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`],
|
||||
scopes: {
|
||||
bot: ["chat:write.public", "chat:write", "channels:read", "groups:read"]
|
||||
}
|
||||
},
|
||||
settings: {
|
||||
org_deploy_enabled: false,
|
||||
socket_mode_enabled: false,
|
||||
token_rotation_enabled: false
|
||||
}
|
||||
})
|
||||
)}`;
|
||||
import { MicrosoftTeamsIntegrationForm } from "./MicrosoftTeamsIntegrationForm";
|
||||
import { SlackIntegrationForm } from "./SlackIntegrationForm";
|
||||
|
||||
export const IntegrationPanel = () => {
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
setValue,
|
||||
formState: { isSubmitting, isDirty }
|
||||
} = useForm<TSlackForm>({
|
||||
resolver: zodResolver(slackFormSchema)
|
||||
});
|
||||
|
||||
const { data: adminSlackConfig } = useGetAdminSlackConfig();
|
||||
const { mutateAsync: updateAdminServerConfig } = useUpdateServerConfig();
|
||||
const [isSlackClientIdFocused, setIsSlackClientIdFocused] = useToggle();
|
||||
const [isSlackClientSecretFocused, setIsSlackClientSecretFocused] = useToggle();
|
||||
|
||||
useEffect(() => {
|
||||
if (adminSlackConfig) {
|
||||
setValue("clientId", adminSlackConfig.clientId);
|
||||
setValue("clientSecret", adminSlackConfig.clientSecret);
|
||||
}
|
||||
}, [adminSlackConfig]);
|
||||
|
||||
const onSlackFormSubmit = async (data: TSlackForm) => {
|
||||
await updateAdminServerConfig({
|
||||
slackClientId: data.clientId,
|
||||
slackClientSecret: data.clientSecret
|
||||
});
|
||||
|
||||
createNotification({
|
||||
text: "Updated admin slack configuration",
|
||||
type: "success"
|
||||
});
|
||||
};
|
||||
const { data: adminIntegrationsConfig } = useGetAdminIntegrationsConfig();
|
||||
|
||||
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 className="mb-6 min-h-64 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-4">
|
||||
<div className="text-xl font-semibold text-mineshaft-100">Integrations</div>
|
||||
<div className="text-sm text-mineshaft-300">
|
||||
Configure your instance-wide settings to enable integration with Slack and Microsoft
|
||||
Teams.
|
||||
</div>
|
||||
<div className="mb-6">
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
onClick={() => window.open(getCustomSlackAppCreationUrl())}
|
||||
>
|
||||
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 || ""}
|
||||
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>
|
||||
</form>
|
||||
<div className="flex flex-col gap-2">
|
||||
<SlackIntegrationForm adminIntegrationsConfig={adminIntegrationsConfig} />
|
||||
<MicrosoftTeamsIntegrationForm adminIntegrationsConfig={adminIntegrationsConfig} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
import { useEffect } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { BsMicrosoftTeams } 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 microsoftTeamsFormSchema = z.object({
|
||||
appId: z.string(),
|
||||
clientSecret: z.string(),
|
||||
botId: z.string()
|
||||
});
|
||||
|
||||
type TMicrosoftTeamsForm = z.infer<typeof microsoftTeamsFormSchema>;
|
||||
|
||||
type Props = {
|
||||
adminIntegrationsConfig?: AdminIntegrationsConfig;
|
||||
};
|
||||
|
||||
export const MicrosoftTeamsIntegrationForm = ({ adminIntegrationsConfig }: Props) => {
|
||||
const { mutateAsync: updateAdminServerConfig } = useUpdateServerConfig();
|
||||
const [isMicrosoftTeamsAppIdFocused, setIsMicrosoftTeamsAppIdFocused] = useToggle();
|
||||
const [isMicrosoftTeamsClientSecretFocused, setIsMicrosoftTeamsClientSecretFocused] = useToggle();
|
||||
const [isMicrosoftTeamsBotIdFocused, setIsMicrosoftTeamsBotIdFocused] = useToggle();
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
setValue,
|
||||
formState: { isSubmitting, isDirty }
|
||||
} = useForm<TMicrosoftTeamsForm>({
|
||||
resolver: zodResolver(microsoftTeamsFormSchema)
|
||||
});
|
||||
|
||||
const onSubmit = async (data: TMicrosoftTeamsForm) => {
|
||||
await updateAdminServerConfig({
|
||||
microsoftTeamsAppId: data.appId,
|
||||
microsoftTeamsClientSecret: data.clientSecret,
|
||||
microsoftTeamsBotId: data.botId
|
||||
});
|
||||
|
||||
createNotification({
|
||||
text: "Updated admin Microsoft Teams configuration",
|
||||
type: "success"
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (adminIntegrationsConfig) {
|
||||
setValue("appId", adminIntegrationsConfig.microsoftTeams.appId);
|
||||
setValue("clientSecret", adminIntegrationsConfig.microsoftTeams.clientSecret);
|
||||
setValue("botId", adminIntegrationsConfig.microsoftTeams.botId);
|
||||
}
|
||||
}, [adminIntegrationsConfig]);
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<Accordion type="single" collapsible className="w-full">
|
||||
<AccordionItem
|
||||
value="microsoft-teams-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">
|
||||
<BsMicrosoftTeams className="text-lg group-hover:text-primary-400" />
|
||||
<div className="text-[15px] font-semibold">Microsoft Teams Integration</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-2 max-w-lg text-sm text-mineshaft-300">
|
||||
Step 1: Create and configure Microsoft Teams bot and Azure Resources. Please refer
|
||||
to the documentation below for more information.
|
||||
</div>
|
||||
<div className="mb-6">
|
||||
<a
|
||||
href="https://infisical.com/docs/documentation/platform/workflow-integrations/microsoft-teams-integration"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Button colorSchema="secondary">Documentation</Button>
|
||||
</a>
|
||||
</div>
|
||||
<div className="mb-4 max-w-lg text-sm text-mineshaft-300">
|
||||
Step 2: Configure your instance-wide settings to enable integration with Microsoft
|
||||
Teams. Copy the App ID and Client Secret from your Microsoft Teams bot's App
|
||||
Registration page. The Client Secret is the password for the bot.
|
||||
</div>
|
||||
<Controller
|
||||
control={control}
|
||||
name="appId"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Application (Client) ID"
|
||||
className="w-96"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value || ""}
|
||||
type={isMicrosoftTeamsAppIdFocused ? "text" : "password"}
|
||||
onFocus={() => setIsMicrosoftTeamsAppIdFocused.on()}
|
||||
onBlur={() => setIsMicrosoftTeamsAppIdFocused.off()}
|
||||
onChange={(e) => field.onChange(e.target.value)}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="clientSecret"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Client Secret"
|
||||
tooltipText="You can find your Client Secret in the Certificates & Secrets section of your Microsoft Teams bot's App Registration."
|
||||
className="w-96"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value || ""}
|
||||
type={isMicrosoftTeamsClientSecretFocused ? "text" : "password"}
|
||||
onFocus={() => setIsMicrosoftTeamsClientSecretFocused.on()}
|
||||
onBlur={() => setIsMicrosoftTeamsClientSecretFocused.off()}
|
||||
onChange={(e) => field.onChange(e.target.value)}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
control={control}
|
||||
name="botId"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Microsoft Teams App ID"
|
||||
tooltipText="You can find the Microsoft Teams App ID in the overview of your Microsoft Teams App inside the Microsoft Teams Developer Portal."
|
||||
className="w-96"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value || ""}
|
||||
type={isMicrosoftTeamsBotIdFocused ? "text" : "password"}
|
||||
onFocus={() => setIsMicrosoftTeamsBotIdFocused.on()}
|
||||
onBlur={() => setIsMicrosoftTeamsBotIdFocused.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>
|
||||
);
|
||||
};
|
||||
@@ -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.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`],
|
||||
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 SlackIntegrationForm = ({ 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({
|
||||
slackClientId: data.clientId,
|
||||
slackClientSecret: data.clientSecret
|
||||
});
|
||||
|
||||
createNotification({
|
||||
text: "Updated admin slack configuration",
|
||||
type: "success"
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (adminIntegrationsConfig) {
|
||||
setValue("clientId", adminIntegrationsConfig.slack.clientId);
|
||||
setValue("clientSecret", adminIntegrationsConfig.slack.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-semibold">Slack Integration</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 Slack App
|
||||
</div>
|
||||
<div className="mb-6">
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
onClick={() => window.open(getCustomSlackAppCreationUrl())}
|
||||
>
|
||||
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 || ""}
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -13,6 +13,7 @@ export const SignupSsoPage = () => {
|
||||
const { t } = useTranslation();
|
||||
const search = useSearch({ from: ROUTE_PATHS.Auth.SignUpSsoPage.id });
|
||||
const token = search.token as string;
|
||||
const defaultOrgAllowed = search.defaultOrgAllowed as boolean | undefined;
|
||||
|
||||
const [step, setStep] = useState(0);
|
||||
const [password, setPassword] = useState("");
|
||||
@@ -57,6 +58,7 @@ export const SignupSsoPage = () => {
|
||||
password={password}
|
||||
setPassword={setPassword}
|
||||
providerAuthToken={token}
|
||||
forceDefaultOrg={defaultOrgAllowed}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
|
||||
+8
-5
@@ -30,6 +30,7 @@ type Props = {
|
||||
name: string;
|
||||
providerOrganizationName: string;
|
||||
providerAuthToken?: string;
|
||||
forceDefaultOrg?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -51,7 +52,8 @@ export const UserInfoSSOStep = ({
|
||||
providerOrganizationName,
|
||||
password,
|
||||
setPassword,
|
||||
providerAuthToken
|
||||
providerAuthToken,
|
||||
forceDefaultOrg
|
||||
}: Props) => {
|
||||
const [nameError, setNameError] = useState(false);
|
||||
const [organizationName, setOrganizationName] = useState("");
|
||||
@@ -84,7 +86,7 @@ export const UserInfoSSOStep = ({
|
||||
} else {
|
||||
setNameError(false);
|
||||
}
|
||||
if (!organizationName) {
|
||||
if (!organizationName && !forceDefaultOrg) {
|
||||
setOrganizationNameError(true);
|
||||
errorCheck = true;
|
||||
} else {
|
||||
@@ -160,7 +162,8 @@ export const UserInfoSSOStep = ({
|
||||
salt: result.salt,
|
||||
verifier: result.verifier,
|
||||
organizationName,
|
||||
attributionSource
|
||||
attributionSource,
|
||||
useDefaultOrg: forceDefaultOrg
|
||||
});
|
||||
|
||||
// unset signup JWT token and set JWT token
|
||||
@@ -267,7 +270,7 @@ export const UserInfoSSOStep = ({
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{providerOrganizationName === undefined && (
|
||||
{!forceDefaultOrg && providerOrganizationName === undefined && (
|
||||
<div className="relative z-0 flex w-full min-w-[20rem] flex-col items-center justify-end rounded-lg py-2 lg:w-1/6">
|
||||
<p className="mb-1 ml-1 w-full text-left text-sm font-medium text-bunker-300">
|
||||
Organization Name
|
||||
@@ -279,7 +282,7 @@ export const UserInfoSSOStep = ({
|
||||
isRequired
|
||||
className="h-12"
|
||||
maxLength={64}
|
||||
disabled
|
||||
isDisabled={forceDefaultOrg}
|
||||
/>
|
||||
{organizationNameError && (
|
||||
<p className="ml-1 mt-1 w-full text-left text-xs text-red-600">
|
||||
|
||||
@@ -5,7 +5,8 @@ import { z } from "zod";
|
||||
import { SignupSsoPage } from "./SignUpSsoPage";
|
||||
|
||||
const SignupSSOPageQueryParamsSchema = z.object({
|
||||
token: z.string()
|
||||
token: z.string(),
|
||||
defaultOrgAllowed: z.boolean().optional()
|
||||
});
|
||||
|
||||
export const Route = createFileRoute("/_restrict-login-signup/signup/sso")({
|
||||
|
||||
@@ -97,7 +97,11 @@ export const Route = createFileRoute("/_restrict-login-signup")({
|
||||
}
|
||||
|
||||
if (!data.organizationId) {
|
||||
if (location.pathname.endsWith("select-organization")) return;
|
||||
if (
|
||||
location.pathname.endsWith("select-organization") ||
|
||||
location.pathname.endsWith("verify-email")
|
||||
)
|
||||
return;
|
||||
throw redirect({ to: "/login/select-organization" });
|
||||
}
|
||||
throw redirect({
|
||||
|
||||
+10
@@ -12,11 +12,13 @@ import { AppConnectionHeader } from "../AppConnectionHeader";
|
||||
import { Auth0ConnectionForm } from "./Auth0ConnectionForm";
|
||||
import { AwsConnectionForm } from "./AwsConnectionForm";
|
||||
import { AzureAppConfigurationConnectionForm } from "./AzureAppConfigurationConnectionForm";
|
||||
import { AzureClientSecretsConnectionForm } from "./AzureClientSecretsConnectionForm";
|
||||
import { AzureKeyVaultConnectionForm } from "./AzureKeyVaultConnectionForm";
|
||||
import { CamundaConnectionForm } from "./CamundaConnectionForm";
|
||||
import { DatabricksConnectionForm } from "./DatabricksConnectionForm";
|
||||
import { GcpConnectionForm } from "./GcpConnectionForm";
|
||||
import { GitHubConnectionForm } from "./GitHubConnectionForm";
|
||||
import { HCVaultConnectionForm } from "./HCVaultConnectionForm";
|
||||
import { HumanitecConnectionForm } from "./HumanitecConnectionForm";
|
||||
import { LdapConnectionForm } from "./LdapConnectionForm";
|
||||
import { MsSqlConnectionForm } from "./MsSqlConnectionForm";
|
||||
@@ -87,10 +89,14 @@ const CreateForm = ({ app, onComplete }: CreateFormProps) => {
|
||||
return <MsSqlConnectionForm onSubmit={onSubmit} />;
|
||||
case AppConnection.Camunda:
|
||||
return <CamundaConnectionForm onSubmit={onSubmit} />;
|
||||
case AppConnection.AzureClientSecrets:
|
||||
return <AzureClientSecretsConnectionForm />;
|
||||
case AppConnection.Windmill:
|
||||
return <WindmillConnectionForm onSubmit={onSubmit} />;
|
||||
case AppConnection.Auth0:
|
||||
return <Auth0ConnectionForm onSubmit={onSubmit} />;
|
||||
case AppConnection.HCVault:
|
||||
return <HCVaultConnectionForm onSubmit={onSubmit} />;
|
||||
case AppConnection.LDAP:
|
||||
return <LdapConnectionForm onSubmit={onSubmit} />;
|
||||
case AppConnection.TeamCity:
|
||||
@@ -155,10 +161,14 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => {
|
||||
return <MsSqlConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
|
||||
case AppConnection.Camunda:
|
||||
return <CamundaConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
|
||||
case AppConnection.AzureClientSecrets:
|
||||
return <AzureClientSecretsConnectionForm appConnection={appConnection} />;
|
||||
case AppConnection.Windmill:
|
||||
return <WindmillConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
|
||||
case AppConnection.Auth0:
|
||||
return <Auth0ConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
|
||||
case AppConnection.HCVault:
|
||||
return <HCVaultConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
|
||||
case AppConnection.LDAP:
|
||||
return <LdapConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
|
||||
case AppConnection.TeamCity:
|
||||
|
||||
+169
@@ -0,0 +1,169 @@
|
||||
import crypto from "crypto";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Controller, FormProvider, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { Button, FormControl, Input, ModalClose, Select, SelectItem } from "@app/components/v2";
|
||||
import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections";
|
||||
import { isInfisicalCloud } from "@app/helpers/platform";
|
||||
import {
|
||||
AzureClientSecretsConnectionMethod,
|
||||
TAzureClientSecretsConnection,
|
||||
useGetAppConnectionOption
|
||||
} from "@app/hooks/api/appConnections";
|
||||
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||
|
||||
import {
|
||||
genericAppConnectionFieldsSchema,
|
||||
GenericAppConnectionsFields
|
||||
} from "./GenericAppConnectionFields";
|
||||
|
||||
type Props = {
|
||||
appConnection?: TAzureClientSecretsConnection;
|
||||
};
|
||||
|
||||
const formSchema = genericAppConnectionFieldsSchema.extend({
|
||||
app: z.literal(AppConnection.AzureClientSecrets),
|
||||
method: z.nativeEnum(AzureClientSecretsConnectionMethod),
|
||||
tenantId: z.string().trim().min(1, "Tenant ID is required")
|
||||
});
|
||||
|
||||
type FormData = z.infer<typeof formSchema>;
|
||||
|
||||
export const AzureClientSecretsConnectionForm = ({ appConnection }: Props) => {
|
||||
const isUpdate = Boolean(appConnection);
|
||||
const [isRedirecting, setIsRedirecting] = useState(false);
|
||||
|
||||
const {
|
||||
option: { oauthClientId },
|
||||
isLoading
|
||||
} = useGetAppConnectionOption(AppConnection.AzureClientSecrets);
|
||||
|
||||
const form = useForm<FormData>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: appConnection
|
||||
? {
|
||||
...appConnection,
|
||||
tenantId: appConnection.credentials.tenantId
|
||||
}
|
||||
: {
|
||||
app: AppConnection.AzureClientSecrets,
|
||||
method: AzureClientSecretsConnectionMethod.OAuth
|
||||
}
|
||||
});
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
control,
|
||||
watch,
|
||||
formState: { isSubmitting, isDirty }
|
||||
} = form;
|
||||
|
||||
const selectedMethod = watch("method");
|
||||
|
||||
const onSubmit = (formData: FormData) => {
|
||||
setIsRedirecting(true);
|
||||
const state = crypto.randomBytes(16).toString("hex");
|
||||
localStorage.setItem("latestCSRFToken", state);
|
||||
localStorage.setItem(
|
||||
"azureClientSecretsConnectionFormData",
|
||||
JSON.stringify({ ...formData, connectionId: appConnection?.id })
|
||||
);
|
||||
|
||||
switch (formData.method) {
|
||||
case AzureClientSecretsConnectionMethod.OAuth:
|
||||
window.location.assign(
|
||||
`https://login.microsoftonline.com/${formData.tenantId || "common"}/oauth2/v2.0/authorize?client_id=${oauthClientId}&response_type=code&redirect_uri=${window.location.origin}/organization/app-connections/azure/oauth/callback&response_mode=query&scope=https://azconfig.io/.default%20openid%20offline_access&state=${state}<:>azure-client-secrets`
|
||||
);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unhandled Azure Connection method: ${(formData as FormData).method}`);
|
||||
}
|
||||
};
|
||||
|
||||
const isMissingConfig = !oauthClientId;
|
||||
|
||||
const methodDetails = getAppConnectionMethodDetails(selectedMethod);
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
{!isUpdate && <GenericAppConnectionsFields />}
|
||||
|
||||
<Controller
|
||||
name="tenantId"
|
||||
control={control}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
tooltipText="The Directory (tenant) ID."
|
||||
isError={Boolean(error?.message)}
|
||||
label="Tenant ID"
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="e4f34ea5-ad23-4291-8585-66d20d603cc8" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
name="method"
|
||||
control={control}
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
tooltipText={`The method you would like to use to connect with ${
|
||||
APP_CONNECTION_MAP[AppConnection.AzureClientSecrets].name
|
||||
}. This field cannot be changed after creation.`}
|
||||
errorText={
|
||||
!isLoading && isMissingConfig
|
||||
? `Environment variables have not been configured. ${
|
||||
isInfisicalCloud()
|
||||
? "Please contact Infisical."
|
||||
: `See documentation to configure Azure ${methodDetails.name} Connections.`
|
||||
}`
|
||||
: error?.message
|
||||
}
|
||||
isError={Boolean(error?.message) || isMissingConfig}
|
||||
label="Method"
|
||||
>
|
||||
<Select
|
||||
isDisabled={isUpdate}
|
||||
value={value}
|
||||
onValueChange={(val) => onChange(val)}
|
||||
className="w-full border border-mineshaft-500"
|
||||
position="popper"
|
||||
dropdownContainerClassName="max-w-none"
|
||||
>
|
||||
{Object.values(AzureClientSecretsConnectionMethod).map((method) => {
|
||||
return (
|
||||
<SelectItem value={method} key={method}>
|
||||
{getAppConnectionMethodDetails(method).name}
|
||||
</SelectItem>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<div className="mt-8 flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
type="submit"
|
||||
colorSchema="secondary"
|
||||
isLoading={isSubmitting || isRedirecting}
|
||||
isDisabled={isSubmitting || (!isUpdate && !isDirty) || isMissingConfig || isRedirecting}
|
||||
>
|
||||
{isUpdate ? "Reconnect to Azure" : "Connect to Azure"}
|
||||
</Button>
|
||||
<ModalClose asChild>
|
||||
<Button colorSchema="secondary" variant="plain">
|
||||
Cancel
|
||||
</Button>
|
||||
</ModalClose>
|
||||
</div>
|
||||
</form>
|
||||
</FormProvider>
|
||||
);
|
||||
};
|
||||
+224
@@ -0,0 +1,224 @@
|
||||
import { Controller, FormProvider, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import {
|
||||
Button,
|
||||
FormControl,
|
||||
Input,
|
||||
ModalClose,
|
||||
SecretInput,
|
||||
Select,
|
||||
SelectItem
|
||||
} from "@app/components/v2";
|
||||
import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections";
|
||||
import { HCVaultConnectionMethod, THCVaultConnection } from "@app/hooks/api/appConnections";
|
||||
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||
|
||||
import {
|
||||
genericAppConnectionFieldsSchema,
|
||||
GenericAppConnectionsFields
|
||||
} from "./GenericAppConnectionFields";
|
||||
|
||||
type Props = {
|
||||
appConnection?: THCVaultConnection;
|
||||
onSubmit: (formData: FormData) => Promise<void>;
|
||||
};
|
||||
|
||||
const rootSchema = genericAppConnectionFieldsSchema.extend({
|
||||
app: z.literal(AppConnection.HCVault)
|
||||
});
|
||||
|
||||
const InstanceUrlSchema = z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Instance URL required")
|
||||
.url("Invalid Instance URL");
|
||||
|
||||
const NamespaceSchema = z.string().trim().optional();
|
||||
|
||||
const formSchema = z.discriminatedUnion("method", [
|
||||
rootSchema.extend({
|
||||
method: z.literal(HCVaultConnectionMethod.AccessToken),
|
||||
credentials: z.object({
|
||||
instanceUrl: InstanceUrlSchema,
|
||||
namespace: NamespaceSchema,
|
||||
accessToken: z.string().trim().min(1, "Access Token required")
|
||||
})
|
||||
}),
|
||||
rootSchema.extend({
|
||||
method: z.literal(HCVaultConnectionMethod.AppRole),
|
||||
credentials: z.object({
|
||||
instanceUrl: InstanceUrlSchema,
|
||||
namespace: NamespaceSchema,
|
||||
roleId: z.string().trim().min(1, "Role ID required"),
|
||||
secretId: z.string().trim().min(1, "Secret ID required")
|
||||
})
|
||||
})
|
||||
]);
|
||||
|
||||
type FormData = z.infer<typeof formSchema>;
|
||||
|
||||
export const HCVaultConnectionForm = ({ appConnection, onSubmit }: Props) => {
|
||||
const isUpdate = Boolean(appConnection);
|
||||
|
||||
const form = useForm<FormData>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: appConnection ?? {
|
||||
app: AppConnection.HCVault,
|
||||
method: HCVaultConnectionMethod.AppRole
|
||||
}
|
||||
});
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
control,
|
||||
watch,
|
||||
formState: { isSubmitting, isDirty }
|
||||
} = form;
|
||||
|
||||
const selectedMethod = watch("method");
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
{!isUpdate && <GenericAppConnectionsFields />}
|
||||
<Controller
|
||||
name="method"
|
||||
control={control}
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
tooltipText={`The method you would like to use to connect with ${
|
||||
APP_CONNECTION_MAP[AppConnection.HCVault].name
|
||||
}. This field cannot be changed after creation.`}
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error?.message)}
|
||||
label="Method"
|
||||
>
|
||||
<Select
|
||||
isDisabled={isUpdate}
|
||||
value={value}
|
||||
onValueChange={(val) => onChange(val)}
|
||||
className="w-full border border-mineshaft-500"
|
||||
position="popper"
|
||||
dropdownContainerClassName="max-w-none"
|
||||
>
|
||||
{Object.values(HCVaultConnectionMethod).map((method) => {
|
||||
return (
|
||||
<SelectItem value={method} key={method}>
|
||||
{getAppConnectionMethodDetails(method).name}{" "}
|
||||
{method === HCVaultConnectionMethod.AppRole ? " (Recommended)" : ""}
|
||||
</SelectItem>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="credentials.instanceUrl"
|
||||
control={control}
|
||||
shouldUnregister
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error?.message)}
|
||||
label="Instance URL"
|
||||
tooltipClassName="max-w-sm"
|
||||
tooltipText="The URL at which your Hashicorp Vault instance is hosted."
|
||||
>
|
||||
<Input {...field} placeholder="https://vault.example.com" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="credentials.namespace"
|
||||
control={control}
|
||||
shouldUnregister
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error?.message)}
|
||||
label="Namespace"
|
||||
isOptional
|
||||
tooltipClassName="max-w-sm"
|
||||
tooltipText="On self-hosted and enterprise clusters there may not be namespaces."
|
||||
>
|
||||
<Input {...field} placeholder="admin" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{selectedMethod === HCVaultConnectionMethod.AccessToken ? (
|
||||
<Controller
|
||||
name="credentials.accessToken"
|
||||
control={control}
|
||||
shouldUnregister
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error?.message)}
|
||||
label="Access Token"
|
||||
>
|
||||
<SecretInput
|
||||
{...field}
|
||||
containerClassName="text-gray-400 group-focus-within:!border-primary-400/50 border border-mineshaft-500 bg-mineshaft-900 px-2.5 py-1.5"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<Controller
|
||||
name="credentials.roleId"
|
||||
control={control}
|
||||
shouldUnregister
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error?.message)}
|
||||
label="Role ID"
|
||||
>
|
||||
<Input {...field} placeholder="00000000-0000-0000-0000-000000000000" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="credentials.secretId"
|
||||
control={control}
|
||||
shouldUnregister
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error?.message)}
|
||||
label="Secret ID"
|
||||
>
|
||||
<SecretInput
|
||||
{...field}
|
||||
containerClassName="text-gray-400 group-focus-within:!border-primary-400/50 border border-mineshaft-500 bg-mineshaft-900 px-2.5 py-1.5"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<div className="mt-8 flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
type="submit"
|
||||
colorSchema="secondary"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting || !isDirty}
|
||||
>
|
||||
{isUpdate ? "Update Credentials" : "Connect to Hashicorp Vault"}
|
||||
</Button>
|
||||
<ModalClose asChild>
|
||||
<Button colorSchema="secondary" variant="plain">
|
||||
Cancel
|
||||
</Button>
|
||||
</ModalClose>
|
||||
</div>
|
||||
</form>
|
||||
</FormProvider>
|
||||
);
|
||||
};
|
||||
+61
-1
@@ -7,9 +7,11 @@ import { ROUTE_PATHS } from "@app/const/routes";
|
||||
import { APP_CONNECTION_MAP } from "@app/helpers/appConnections";
|
||||
import {
|
||||
AzureAppConfigurationConnectionMethod,
|
||||
AzureClientSecretsConnectionMethod,
|
||||
AzureKeyVaultConnectionMethod,
|
||||
GitHubConnectionMethod,
|
||||
TAzureAppConfigurationConnection,
|
||||
TAzureClientSecretsConnection,
|
||||
TAzureKeyVaultConnection,
|
||||
TGitHubConnection,
|
||||
useCreateAppConnection,
|
||||
@@ -32,18 +34,26 @@ type AzureAppConfigurationFormData = BaseFormData &
|
||||
Pick<TAzureAppConfigurationConnection, "name" | "method" | "description"> &
|
||||
Pick<TAzureAppConfigurationConnection["credentials"], "tenantId">;
|
||||
|
||||
type AzureClientSecretsFormData = BaseFormData &
|
||||
Pick<TAzureClientSecretsConnection, "name" | "method" | "description"> &
|
||||
Pick<TAzureClientSecretsConnection["credentials"], "tenantId">;
|
||||
|
||||
type FormDataMap = {
|
||||
[AppConnection.GitHub]: GithubFormData & { app: AppConnection.GitHub };
|
||||
[AppConnection.AzureKeyVault]: AzureKeyVaultFormData & { app: AppConnection.AzureKeyVault };
|
||||
[AppConnection.AzureAppConfiguration]: AzureAppConfigurationFormData & {
|
||||
app: AppConnection.AzureAppConfiguration;
|
||||
};
|
||||
[AppConnection.AzureClientSecrets]: AzureClientSecretsFormData & {
|
||||
app: AppConnection.AzureClientSecrets;
|
||||
};
|
||||
};
|
||||
|
||||
const formDataStorageFieldMap: Partial<Record<AppConnection, string>> = {
|
||||
[AppConnection.GitHub]: "githubConnectionFormData",
|
||||
[AppConnection.AzureKeyVault]: "azureKeyVaultConnectionFormData",
|
||||
[AppConnection.AzureAppConfiguration]: "azureAppConfigurationConnectionFormData"
|
||||
[AppConnection.AzureAppConfiguration]: "azureAppConfigurationConnectionFormData",
|
||||
[AppConnection.AzureClientSecrets]: "azureClientSecretsConnectionFormData"
|
||||
};
|
||||
|
||||
export const OAuthCallbackPage = () => {
|
||||
@@ -194,6 +204,54 @@ export const OAuthCallbackPage = () => {
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleAzureClientSecrets = useCallback(async () => {
|
||||
const formData = getFormData(AppConnection.AzureClientSecrets);
|
||||
if (formData === null) return null;
|
||||
|
||||
clearState(AppConnection.AzureClientSecrets);
|
||||
|
||||
const { connectionId, name, description, returnUrl } = formData;
|
||||
|
||||
try {
|
||||
if (connectionId) {
|
||||
await updateAppConnection.mutateAsync({
|
||||
app: AppConnection.AzureClientSecrets,
|
||||
connectionId,
|
||||
credentials: {
|
||||
code: code as string,
|
||||
tenantId: formData.tenantId
|
||||
}
|
||||
});
|
||||
} else {
|
||||
await createAppConnection.mutateAsync({
|
||||
app: AppConnection.AzureClientSecrets,
|
||||
name,
|
||||
description,
|
||||
method: AzureClientSecretsConnectionMethod.OAuth,
|
||||
credentials: {
|
||||
code: code as string,
|
||||
tenantId: formData.tenantId
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (err: any) {
|
||||
createNotification({
|
||||
title: `Failed to ${connectionId ? "update" : "add"} Azure Client Secrets Connection`,
|
||||
text: err?.message,
|
||||
type: "error"
|
||||
});
|
||||
navigate({
|
||||
to: returnUrl ?? "/organization/app-connections"
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
connectionId,
|
||||
returnUrl,
|
||||
appConnectionName: formData.app
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleGithub = useCallback(async () => {
|
||||
const formData = getFormData(AppConnection.GitHub);
|
||||
if (formData === null) return null;
|
||||
@@ -280,6 +338,8 @@ export const OAuthCallbackPage = () => {
|
||||
data = await handleAzureKeyVault();
|
||||
} else if (appConnection === AppConnection.AzureAppConfiguration) {
|
||||
data = await handleAzureAppConfiguration();
|
||||
} else if (appConnection === AppConnection.AzureClientSecrets) {
|
||||
data = await handleAzureClientSecrets();
|
||||
}
|
||||
|
||||
if (data) {
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useNavigate, useSearch } from "@tanstack/react-router";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { ContentLoader } from "@app/components/v2";
|
||||
import { ROUTE_PATHS } from "@app/const/routes";
|
||||
import { useOrganization } from "@app/context";
|
||||
import { useCreateMicrosoftTeamsIntegration } from "@app/hooks/api";
|
||||
|
||||
const stateSchema = z.object({
|
||||
redirectUri: z.string(),
|
||||
tenantId: z.string(),
|
||||
slug: z.string(),
|
||||
description: z.string().optional(),
|
||||
csrfToken: z.string(),
|
||||
clientId: z.string()
|
||||
});
|
||||
|
||||
export const OAuthCallbackPage = () => {
|
||||
const navigate = useNavigate();
|
||||
const { currentOrg } = useOrganization();
|
||||
const [isReady, setIsReady] = useState(false);
|
||||
|
||||
const search = useSearch({
|
||||
from: ROUTE_PATHS.Organization.Settings.OauthCallbackPage.id
|
||||
});
|
||||
|
||||
const createMicrosoftTeamsWorkflowIntegration = useCreateMicrosoftTeamsIntegration();
|
||||
|
||||
const { state: rawState, code } = search;
|
||||
|
||||
const state = stateSchema.parse(rawState);
|
||||
|
||||
const clearState = () => {
|
||||
if (state.csrfToken !== localStorage.getItem("latestCSRFToken")) {
|
||||
throw new Error("Invalid CSRF token");
|
||||
}
|
||||
|
||||
localStorage.removeItem("latestCSRFToken");
|
||||
};
|
||||
|
||||
const handleMicrosoftTeams = useCallback(async () => {
|
||||
clearState();
|
||||
|
||||
if (!code) {
|
||||
throw new Error("No code provided");
|
||||
}
|
||||
|
||||
await createMicrosoftTeamsWorkflowIntegration.mutateAsync({
|
||||
orgId: currentOrg.id,
|
||||
tenantId: state.tenantId,
|
||||
code,
|
||||
slug: state.slug,
|
||||
description: state.description ?? "",
|
||||
redirectUri: state.redirectUri
|
||||
});
|
||||
|
||||
navigate({
|
||||
to: ROUTE_PATHS.Organization.SettingsPage.path
|
||||
});
|
||||
}, []);
|
||||
|
||||
// Ensure that the localstorage is ready for use, to avoid the form data being malformed
|
||||
useEffect(() => {
|
||||
if (!isReady) {
|
||||
setIsReady(!!localStorage.length);
|
||||
}
|
||||
}, [localStorage.length]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isReady) return;
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
await handleMicrosoftTeams();
|
||||
|
||||
createNotification({
|
||||
text: "Successfully created Microsoft Teams workflow integration",
|
||||
type: "success"
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
text: "Failed to create Microsoft Teams workflow integration",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
})();
|
||||
}, [isReady]);
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<ContentLoader text="Please wait! Authentication in process." />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { zodValidator } from "@tanstack/zod-adapter";
|
||||
import { z } from "zod";
|
||||
|
||||
import { OAuthCallbackPage } from "./OauthCallbackPage";
|
||||
|
||||
const SettingsOAuthCallbackPageQueryParamsSchema = z.object({
|
||||
state: z
|
||||
.object({
|
||||
clientId: z.string(),
|
||||
tenantId: z.string(),
|
||||
slug: z.string(),
|
||||
description: z.string().optional(),
|
||||
redirectUri: z.string(),
|
||||
csrfToken: z.string()
|
||||
})
|
||||
.nullable()
|
||||
.catch(null),
|
||||
code: z.string().catch("")
|
||||
});
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/organization/settings/oauth/callback"
|
||||
)({
|
||||
component: OAuthCallbackPage,
|
||||
validateSearch: zodValidator(SettingsOAuthCallbackPageQueryParamsSchema)
|
||||
});
|
||||
@@ -23,6 +23,7 @@ import { OrgLDAPSection } from "./OrgLDAPSection";
|
||||
import { OrgOIDCSection } from "./OrgOIDCSection";
|
||||
import { OrgScimSection } from "./OrgSCIMSection";
|
||||
import { OrgSSOSection } from "./OrgSSOSection";
|
||||
import { OrgUserAccessTokenLimitSection } from "./OrgUserAccessTokenLimitSection";
|
||||
import { SSOModal } from "./SSOModal";
|
||||
|
||||
export const OrgAuthTab = withPermission(
|
||||
@@ -167,6 +168,7 @@ export const OrgAuthTab = withPermission(
|
||||
return (
|
||||
<>
|
||||
<OrgGenericAuthSection />
|
||||
<OrgUserAccessTokenLimitSection />
|
||||
{shouldShowCreateIdentityProviderView ? (
|
||||
createIdentityProviderView
|
||||
) : (
|
||||
|
||||
+171
@@ -0,0 +1,171 @@
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import { Button, FormControl, Input, Select, SelectItem } from "@app/components/v2";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
|
||||
import { useUpdateOrg } from "@app/hooks/api";
|
||||
|
||||
const formSchema = z.object({
|
||||
expirationValue: z.number().min(1, "Value must be at least 1"),
|
||||
expirationUnit: z.enum(["m", "h", "d", "w"], {
|
||||
invalid_type_error: "Please select a valid time unit"
|
||||
})
|
||||
});
|
||||
|
||||
type TForm = z.infer<typeof formSchema>;
|
||||
|
||||
// Function to parse duration string like "30d" into value and unit
|
||||
const parseDuration = (duration: string): { value: number; unit: string } => {
|
||||
const match = duration.match(/^(\d+)([mhdw])$/);
|
||||
if (match) {
|
||||
return {
|
||||
value: parseInt(match[1], 10),
|
||||
unit: match[2]
|
||||
};
|
||||
}
|
||||
// Default to 30 days if invalid format
|
||||
return { value: 30, unit: "d" };
|
||||
};
|
||||
|
||||
// Function to format value and unit back to duration string
|
||||
const formatDuration = (value: number, unit: string): string => {
|
||||
return `${value}${unit}`;
|
||||
};
|
||||
|
||||
export const OrgUserAccessTokenLimitSection = () => {
|
||||
const { mutateAsync: updateUserTokenExpiration } = useUpdateOrg();
|
||||
const { currentOrg } = useOrganization();
|
||||
|
||||
// Parse the current duration or use default
|
||||
const currentDuration = parseDuration(currentOrg?.userTokenExpiration || "30d");
|
||||
|
||||
const {
|
||||
control,
|
||||
formState: { isSubmitting, isDirty },
|
||||
handleSubmit
|
||||
} = useForm<TForm>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
expirationValue: currentDuration.value,
|
||||
expirationUnit: currentDuration.unit as "m" | "h" | "d" | "w"
|
||||
}
|
||||
});
|
||||
|
||||
if (!currentOrg) return null;
|
||||
|
||||
const handleUserTokenExpirationSubmit = async (formData: TForm) => {
|
||||
try {
|
||||
const userTokenExpiration = formatDuration(formData.expirationValue, formData.expirationUnit);
|
||||
|
||||
await updateUserTokenExpiration({
|
||||
userTokenExpiration,
|
||||
orgId: currentOrg.id
|
||||
});
|
||||
|
||||
createNotification({
|
||||
text: "Successfully updated user token expiration",
|
||||
type: "success"
|
||||
});
|
||||
} catch {
|
||||
createNotification({
|
||||
text: "Failed updating user token expiration",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Units for the dropdown with readable labels
|
||||
const timeUnits = [
|
||||
{ value: "m", label: "Minutes" },
|
||||
{ value: "h", label: "Hours" },
|
||||
{ value: "d", label: "Days" },
|
||||
{ value: "w", label: "Weeks" }
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<p className="text-xl font-semibold">User Token Expiration</p>
|
||||
</div>
|
||||
<p className="mb-4 mt-2 text-sm text-gray-400">
|
||||
This defines the maximum time a user token will be valid. After this time, the user will
|
||||
need to re-authenticate.
|
||||
</p>
|
||||
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Settings}>
|
||||
{(isAllowed) => (
|
||||
<form onSubmit={handleSubmit(handleUserTokenExpirationSubmit)} autoComplete="off">
|
||||
<div className="flex max-w-md gap-4">
|
||||
<div className="flex-1">
|
||||
<Controller
|
||||
control={control}
|
||||
name="expirationValue"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
label="Expiration value"
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
type="number"
|
||||
min={1}
|
||||
step={1}
|
||||
value={field.value}
|
||||
onChange={(e) => field.onChange(parseInt(e.target.value, 10))}
|
||||
disabled={!isAllowed}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<Controller
|
||||
control={control}
|
||||
name="expirationUnit"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
label="Time unit"
|
||||
>
|
||||
<Select
|
||||
value={field.value}
|
||||
className="pr-2"
|
||||
onValueChange={field.onChange}
|
||||
placeholder="Select time unit"
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
{timeUnits.map(({ value, label }) => (
|
||||
<SelectItem
|
||||
key={value}
|
||||
value={value}
|
||||
className="relative py-2 pl-6 pr-8 text-sm hover:bg-mineshaft-700"
|
||||
>
|
||||
<div className="ml-3 font-medium">{label}</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
disabled={!isDirty}
|
||||
className="mt-4"
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</form>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+25
-3
@@ -1,4 +1,5 @@
|
||||
import { useState } from "react";
|
||||
import { BsMicrosoftTeams } from "react-icons/bs";
|
||||
import { faSlack } from "@fortawesome/free-brands-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
@@ -6,6 +7,7 @@ import { AnimatePresence, motion } from "framer-motion";
|
||||
import { Modal, ModalContent } from "@app/components/v2";
|
||||
import { WorkflowIntegrationPlatform } from "@app/hooks/api/workflowIntegrations/types";
|
||||
|
||||
import { MicrosoftTeamsIntegrationForm } from "./MicrosoftTeamsIntegrationForm";
|
||||
import { SlackIntegrationForm } from "./SlackIntegrationForm";
|
||||
|
||||
type Props = {
|
||||
@@ -20,9 +22,14 @@ enum WizardSteps {
|
||||
|
||||
const PLATFORM_LIST = [
|
||||
{
|
||||
icon: faSlack,
|
||||
icon: <FontAwesomeIcon icon={faSlack} size="lg" />,
|
||||
platform: WorkflowIntegrationPlatform.SLACK,
|
||||
title: "Slack"
|
||||
},
|
||||
{
|
||||
icon: <BsMicrosoftTeams className="text-lg" />,
|
||||
platform: WorkflowIntegrationPlatform.MICROSOFT_TEAMS,
|
||||
title: "Microsoft Teams"
|
||||
}
|
||||
];
|
||||
|
||||
@@ -38,7 +45,10 @@ export const AddWorkflowIntegrationForm = ({ isOpen, onToggle }: Props) => {
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onOpenChange={(state) => handleFormReset(state)}>
|
||||
<ModalContent title={`Add a ${selectedPlatform ?? "workflow"} integration`} className="my-4">
|
||||
<ModalContent
|
||||
title={`Add a ${selectedPlatform?.replace("-", " ").replace(/\b\w/g, (char) => char.toUpperCase()) ?? "workflow"} integration`}
|
||||
className="my-4"
|
||||
>
|
||||
<AnimatePresence mode="wait">
|
||||
{wizardStep === WizardSteps.SelectPlatform && (
|
||||
<motion.div
|
||||
@@ -67,7 +77,7 @@ export const AddWorkflowIntegrationForm = ({ isOpen, onToggle }: Props) => {
|
||||
}
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={icon} size="lg" />
|
||||
<div>{icon}</div>
|
||||
<div className="whitespace-pre-wrap text-center text-sm">{title}</div>
|
||||
</div>
|
||||
))}
|
||||
@@ -86,6 +96,18 @@ export const AddWorkflowIntegrationForm = ({ isOpen, onToggle }: Props) => {
|
||||
<SlackIntegrationForm onClose={() => onToggle(false)} />
|
||||
</motion.div>
|
||||
)}
|
||||
{wizardStep === WizardSteps.PlatformInputs &&
|
||||
selectedPlatform === WorkflowIntegrationPlatform.MICROSOFT_TEAMS && (
|
||||
<motion.div
|
||||
key="microsoft-teams-platform"
|
||||
transition={{ duration: 0.1 }}
|
||||
initial={{ opacity: 0, translateX: 30 }}
|
||||
animate={{ opacity: 1, translateX: 0 }}
|
||||
exit={{ opacity: 0, translateX: -30 }}
|
||||
>
|
||||
<MicrosoftTeamsIntegrationForm onClose={() => onToggle(false)} />
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
|
||||
+4
@@ -1,6 +1,7 @@
|
||||
import { Modal, ModalContent } from "@app/components/v2";
|
||||
import { WorkflowIntegrationPlatform } from "@app/hooks/api/workflowIntegrations/types";
|
||||
|
||||
import { MicrosoftTeamsIntegrationForm } from "./MicrosoftTeamsIntegrationForm";
|
||||
import { SlackIntegrationForm } from "./SlackIntegrationForm";
|
||||
|
||||
type Props = {
|
||||
@@ -20,6 +21,9 @@ export const IntegrationFormDetails = ({ isOpen, id, onOpenChange, workflowPlatf
|
||||
{workflowPlatform === WorkflowIntegrationPlatform.SLACK && (
|
||||
<SlackIntegrationForm id={id} onClose={() => onOpenChange(false)} />
|
||||
)}
|
||||
{workflowPlatform === WorkflowIntegrationPlatform.MICROSOFT_TEAMS && (
|
||||
<MicrosoftTeamsIntegrationForm id={id} onClose={() => onOpenChange(false)} />
|
||||
)}
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
+192
@@ -0,0 +1,192 @@
|
||||
import crypto from "crypto";
|
||||
|
||||
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 { useOrganization } from "@app/context";
|
||||
import {
|
||||
useGetMicrosoftTeamsClientId,
|
||||
useGetMicrosoftTeamsIntegrationById,
|
||||
useUpdateMicrosoftTeamsIntegration
|
||||
} from "@app/hooks/api";
|
||||
import { slugSchema } from "@app/lib/schemas";
|
||||
|
||||
type Props = {
|
||||
id?: string;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
const microsoftTeamsFormSchema = z.object({
|
||||
slug: slugSchema({ min: 1, field: "Alias" }),
|
||||
tenantId: z
|
||||
.string()
|
||||
.min(1, { message: "Tenant ID is required" })
|
||||
.trim()
|
||||
.uuid("Tenant ID must be a valid UUID"),
|
||||
description: z.string().optional()
|
||||
});
|
||||
|
||||
type TMicrosoftTeamsFormData = z.infer<typeof microsoftTeamsFormSchema>;
|
||||
|
||||
export const MicrosoftTeamsIntegrationForm = ({ id, onClose }: Props) => {
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
setValue,
|
||||
formState: { isSubmitting, isDirty }
|
||||
} = useForm<TMicrosoftTeamsFormData>({
|
||||
resolver: zodResolver(microsoftTeamsFormSchema)
|
||||
});
|
||||
|
||||
const { currentOrg } = useOrganization();
|
||||
const { data: microsoftTeamsIntegration } = useGetMicrosoftTeamsIntegrationById(id);
|
||||
const { mutateAsync: updateMicrosoftTeamsIntegration } = useUpdateMicrosoftTeamsIntegration();
|
||||
const { data: microsoftTeamsClientId } = useGetMicrosoftTeamsClientId();
|
||||
|
||||
useEffect(() => {
|
||||
if (microsoftTeamsIntegration) {
|
||||
setValue("slug", microsoftTeamsIntegration.slug);
|
||||
setValue("description", microsoftTeamsIntegration.description ?? "");
|
||||
setValue("tenantId", microsoftTeamsIntegration.tenantId);
|
||||
}
|
||||
}, [microsoftTeamsIntegration]);
|
||||
|
||||
const handleMicrosoftTeamsFormSubmit = async ({
|
||||
slug,
|
||||
description,
|
||||
tenantId
|
||||
}: TMicrosoftTeamsFormData) => {
|
||||
if (!microsoftTeamsClientId) {
|
||||
createNotification({
|
||||
text: "Microsoft Teams client ID is not set. Please contact your instance administrator.",
|
||||
type: "error"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (id && microsoftTeamsIntegration) {
|
||||
if (!currentOrg) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (tenantId !== microsoftTeamsIntegration.tenantId) {
|
||||
createNotification({
|
||||
text: "Tenant ID cannot be changed",
|
||||
type: "error"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await updateMicrosoftTeamsIntegration({
|
||||
id,
|
||||
orgId: currentOrg.id,
|
||||
slug,
|
||||
description
|
||||
});
|
||||
|
||||
createNotification({
|
||||
text: "Successfully updated Microsoft Teams integration",
|
||||
type: "success"
|
||||
});
|
||||
|
||||
onClose();
|
||||
} else {
|
||||
const csrfToken = crypto.randomBytes(32).toString("hex");
|
||||
localStorage.setItem("latestCSRFToken", csrfToken);
|
||||
|
||||
const state = {
|
||||
redirectUri: `${window.location.origin}/organization/settings/oauth/callback`,
|
||||
tenantId,
|
||||
slug,
|
||||
description,
|
||||
csrfToken,
|
||||
clientId: microsoftTeamsClientId.clientId
|
||||
};
|
||||
|
||||
const url = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/authorize?
|
||||
client_id=${microsoftTeamsClientId.clientId}
|
||||
&redirect_uri=${state.redirectUri}
|
||||
&response_type=code
|
||||
&response_mode=query
|
||||
&scope=https://graph.microsoft.com/.default
|
||||
&state=${encodeURIComponent(JSON.stringify(state))}
|
||||
&prompt=consent
|
||||
&admin_consent=true`;
|
||||
|
||||
window.location.href = url;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(handleMicrosoftTeamsFormSubmit)} autoComplete="off">
|
||||
<div className="mb-4 text-xs text-mineshaft-200">
|
||||
For seamless installations, ensure that the Infisical bot is already installed in your
|
||||
Microsoft Teams tenant. For more information, please refer to the{" "}
|
||||
<a
|
||||
className="text-primary-500"
|
||||
href="https://infisical.com/docs/documentation/platform/workflow-integrations/microsoft-teams-integration"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Microsoft Teams Workflow Integration Documentation
|
||||
</a>
|
||||
, which will guide you through the download and installation process.
|
||||
</div>
|
||||
<Controller
|
||||
control={control}
|
||||
name="slug"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl label="Alias" isRequired errorText={error?.message} isError={Boolean(error)}>
|
||||
<Input placeholder="" {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{!microsoftTeamsIntegration && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="tenantId"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isRequired
|
||||
label="Tenant ID"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
>
|
||||
<Input placeholder="" {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<Controller
|
||||
control={control}
|
||||
name="description"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl label="Description" errorText={error?.message} isError={Boolean(error)}>
|
||||
<Input placeholder="" {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{microsoftTeamsIntegration && (
|
||||
<FormControl label="Connected Microsoft Teams Tenant">
|
||||
<Input
|
||||
value={microsoftTeamsIntegration.tenantId}
|
||||
isReadOnly
|
||||
className="bg-white/[0.07]"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
<div className="mt-6 flex items-center space-x-4">
|
||||
<Button type="submit" isLoading={isSubmitting} isDisabled={!isDirty || isSubmitting}>
|
||||
{id && microsoftTeamsIntegration ? "Save" : "Create Microsoft Teams Integration"}
|
||||
</Button>
|
||||
<Button variant="outline_bg" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
+123
-44
@@ -1,5 +1,5 @@
|
||||
import { faSlack } from "@fortawesome/free-brands-svg-icons";
|
||||
import { faEllipsis, faGear, faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { BsMicrosoftTeams, BsSlack } from "react-icons/bs";
|
||||
import { faEllipsis, faGear, faInfoCircle, faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import axios from "axios";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
@@ -7,6 +7,7 @@ import { twMerge } from "tailwind-merge";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
DeleteActionModal,
|
||||
DropdownMenu,
|
||||
@@ -20,6 +21,7 @@ import {
|
||||
TBody,
|
||||
Td,
|
||||
THead,
|
||||
Tooltip,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
|
||||
@@ -27,14 +29,31 @@ import { withPermission } from "@app/hoc";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import {
|
||||
fetchSlackReinstallUrl,
|
||||
useCheckMicrosoftTeamsIntegrationInstallationStatus,
|
||||
useDeleteMicrosoftTeamsIntegration,
|
||||
useDeleteSlackIntegration,
|
||||
useGetWorkflowIntegrations
|
||||
} from "@app/hooks/api";
|
||||
import { WorkflowIntegrationPlatform } from "@app/hooks/api/workflowIntegrations/types";
|
||||
import {
|
||||
WorkflowIntegrationPlatform,
|
||||
WorkflowIntegrationStatus
|
||||
} from "@app/hooks/api/workflowIntegrations/types";
|
||||
|
||||
import { AddWorkflowIntegrationForm } from "./AddWorkflowIntegrationForm";
|
||||
import { IntegrationFormDetails } from "./IntegrationFormDetails";
|
||||
|
||||
const renderStatus = (status: WorkflowIntegrationStatus) => {
|
||||
if (status === WorkflowIntegrationStatus.Installed) {
|
||||
return <Badge variant="success">Installed</Badge>;
|
||||
}
|
||||
|
||||
if (status === WorkflowIntegrationStatus.Pending) {
|
||||
return <Badge variant="primary">Pending</Badge>;
|
||||
}
|
||||
|
||||
return <Badge variant="danger">Failed</Badge>;
|
||||
};
|
||||
|
||||
export const OrgWorkflowIntegrationTab = withPermission(
|
||||
() => {
|
||||
const { popUp, handlePopUpOpen, handlePopUpToggle, handlePopUpClose } = usePopUp([
|
||||
@@ -48,6 +67,9 @@ export const OrgWorkflowIntegrationTab = withPermission(
|
||||
useGetWorkflowIntegrations(currentOrg?.id);
|
||||
|
||||
const { mutateAsync: deleteSlackIntegration } = useDeleteSlackIntegration();
|
||||
const { mutateAsync: deleteMicrosoftTeamsIntegration } = useDeleteMicrosoftTeamsIntegration();
|
||||
const { mutateAsync: checkMicrosoftTeamsInstallationStatus } =
|
||||
useCheckMicrosoftTeamsIntegrationInstallationStatus();
|
||||
|
||||
const handleRemoveIntegration = async () => {
|
||||
if (!currentOrg) {
|
||||
@@ -62,6 +84,13 @@ export const OrgWorkflowIntegrationTab = withPermission(
|
||||
});
|
||||
}
|
||||
|
||||
if (platform === WorkflowIntegrationPlatform.MICROSOFT_TEAMS) {
|
||||
await deleteMicrosoftTeamsIntegration({
|
||||
id,
|
||||
orgId: currentOrg?.id
|
||||
});
|
||||
}
|
||||
|
||||
handlePopUpClose("removeIntegration");
|
||||
createNotification({
|
||||
text: "Successfully deleted integration",
|
||||
@@ -69,27 +98,37 @@ export const OrgWorkflowIntegrationTab = withPermission(
|
||||
});
|
||||
};
|
||||
|
||||
const triggerReinstall = async (platform: WorkflowIntegrationPlatform, id: string) => {
|
||||
if (platform === WorkflowIntegrationPlatform.SLACK) {
|
||||
try {
|
||||
const slackReinstallUrl = await fetchSlackReinstallUrl({
|
||||
id
|
||||
});
|
||||
const triggerSlackReinstall = async (id: string) => {
|
||||
try {
|
||||
const slackReinstallUrl = await fetchSlackReinstallUrl({
|
||||
id
|
||||
});
|
||||
|
||||
if (slackReinstallUrl) {
|
||||
window.location.href = slackReinstallUrl;
|
||||
}
|
||||
} catch (err) {
|
||||
if (axios.isAxiosError(err)) {
|
||||
createNotification({
|
||||
text: (err.response?.data as { message: string })?.message,
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
if (slackReinstallUrl) {
|
||||
window.location.href = slackReinstallUrl;
|
||||
}
|
||||
} catch (err) {
|
||||
if (axios.isAxiosError(err)) {
|
||||
createNotification({
|
||||
text: (err.response?.data as { message: string })?.message,
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const triggerMicrosoftTeamsInstallationStatusCheck = async (id: string) => {
|
||||
await checkMicrosoftTeamsInstallationStatus({
|
||||
workflowIntegrationId: id,
|
||||
orgId: currentOrg?.id
|
||||
});
|
||||
|
||||
createNotification({
|
||||
text: "The Microsoft Teams bot is successfully installed in your Microsoft Teams tenant",
|
||||
type: "success"
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="flex justify-between">
|
||||
@@ -117,6 +156,7 @@ export const OrgWorkflowIntegrationTab = withPermission(
|
||||
<Tr>
|
||||
<Td>Provider</Td>
|
||||
<Td>Alias</Td>
|
||||
<Td>Status</Td>
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
@@ -134,11 +174,23 @@ export const OrgWorkflowIntegrationTab = withPermission(
|
||||
)}
|
||||
{workflowIntegrations?.map((workflowIntegration) => (
|
||||
<Tr key={workflowIntegration.id}>
|
||||
<Td className="flex max-w-xs items-center overflow-hidden text-ellipsis hover:overflow-auto hover:break-all">
|
||||
<FontAwesomeIcon icon={faSlack} />
|
||||
<div className="ml-2">{workflowIntegration.integration.toUpperCase()}</div>
|
||||
<Td className="flex max-w-xs items-center gap-2 overflow-hidden text-ellipsis hover:overflow-auto hover:break-all">
|
||||
{workflowIntegration.integration === WorkflowIntegrationPlatform.SLACK ? (
|
||||
<BsSlack />
|
||||
) : (
|
||||
<BsMicrosoftTeams />
|
||||
)}
|
||||
<div className="capitalize">
|
||||
{workflowIntegration.integration.replaceAll("-", " ")}
|
||||
</div>
|
||||
{workflowIntegration.description && (
|
||||
<Tooltip content={workflowIntegration.description}>
|
||||
<FontAwesomeIcon icon={faInfoCircle} className="text-gray-400" size="sm" />
|
||||
</Tooltip>
|
||||
)}
|
||||
</Td>
|
||||
<Td>{workflowIntegration.slug}</Td>
|
||||
<Td>{renderStatus(workflowIntegration.status)}</Td>
|
||||
<Td>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild className="rounded-lg">
|
||||
@@ -159,29 +211,56 @@ export const OrgWorkflowIntegrationTab = withPermission(
|
||||
>
|
||||
More details
|
||||
</DropdownMenuItem>
|
||||
<OrgPermissionCan
|
||||
I={OrgPermissionActions.Create}
|
||||
an={OrgPermissionSubjects.Settings}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
disabled={!isAllowed}
|
||||
className={twMerge(
|
||||
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
|
||||
triggerReinstall(
|
||||
workflowIntegration.integration,
|
||||
workflowIntegration.id
|
||||
);
|
||||
}}
|
||||
>
|
||||
Reinstall
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
{workflowIntegration.integration === WorkflowIntegrationPlatform.SLACK && (
|
||||
<OrgPermissionCan
|
||||
I={OrgPermissionActions.Create}
|
||||
an={OrgPermissionSubjects.Settings}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
disabled={!isAllowed}
|
||||
className={twMerge(
|
||||
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
onClick={async (e) => {
|
||||
e.stopPropagation();
|
||||
|
||||
await triggerSlackReinstall(workflowIntegration.id);
|
||||
}}
|
||||
>
|
||||
Reinstall
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
)}
|
||||
|
||||
{workflowIntegration.integration ===
|
||||
WorkflowIntegrationPlatform.MICROSOFT_TEAMS && (
|
||||
<OrgPermissionCan
|
||||
I={OrgPermissionActions.Edit}
|
||||
an={OrgPermissionSubjects.Settings}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
disabled={!isAllowed}
|
||||
className={twMerge(
|
||||
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
onClick={async (e) => {
|
||||
e.stopPropagation();
|
||||
|
||||
await triggerMicrosoftTeamsInstallationStatusCheck(
|
||||
workflowIntegration.id
|
||||
);
|
||||
}}
|
||||
>
|
||||
Check Installation Status
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
)}
|
||||
|
||||
<OrgPermissionCan
|
||||
I={OrgPermissionActions.Delete}
|
||||
an={OrgPermissionSubjects.Settings}
|
||||
|
||||
@@ -11,7 +11,7 @@ const SettingsPageQueryParams = z.object({
|
||||
});
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/organization/settings"
|
||||
"/_authenticate/_inject-org-details/_org-layout/organization/settings/"
|
||||
)({
|
||||
component: SettingsPage,
|
||||
validateSearch: zodValidator(SettingsPageQueryParams),
|
||||
|
||||
+12
-1
@@ -243,6 +243,7 @@ export const projectRoleFormSchema = z.object({
|
||||
})
|
||||
.array()
|
||||
.default([]),
|
||||
[ProjectPermissionSub.SshHostGroups]: GeneralPolicyActionSchema.array().default([]),
|
||||
[ProjectPermissionSub.SecretApproval]: GeneralPolicyActionSchema.array().default([]),
|
||||
[ProjectPermissionSub.SecretRollback]: SecretRollbackPolicyActionSchema.array().default([]),
|
||||
[ProjectPermissionSub.Project]: WorkspacePolicyActionSchema.array().default([]),
|
||||
@@ -388,7 +389,8 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
|
||||
ProjectPermissionSub.Kms,
|
||||
ProjectPermissionSub.SshCertificateTemplates,
|
||||
ProjectPermissionSub.SshCertificateAuthorities,
|
||||
ProjectPermissionSub.SshCertificates
|
||||
ProjectPermissionSub.SshCertificates,
|
||||
ProjectPermissionSub.SshHostGroups
|
||||
].includes(subject)
|
||||
) {
|
||||
// from above statement we are sure it won't be undefined
|
||||
@@ -1092,6 +1094,15 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = {
|
||||
{ label: "Issue Host Certificate", value: ProjectPermissionSshHostActions.IssueHostCert }
|
||||
]
|
||||
},
|
||||
[ProjectPermissionSub.SshHostGroups]: {
|
||||
title: "SSH Host Groups",
|
||||
actions: [
|
||||
{ label: "Read", value: "read" },
|
||||
{ label: "Create", value: "create" },
|
||||
{ label: "Modify", value: "edit" },
|
||||
{ label: "Remove", value: "delete" }
|
||||
]
|
||||
},
|
||||
[ProjectPermissionSub.PkiCollections]: {
|
||||
title: "PKI Collections",
|
||||
actions: [
|
||||
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
import { THCVaultSync } from "@app/hooks/api/secretSyncs/types/hc-vault-sync";
|
||||
|
||||
import { getSecretSyncDestinationColValues } from "../helpers";
|
||||
import { SecretSyncTableCell } from "../SecretSyncTableCell";
|
||||
|
||||
type Props = {
|
||||
secretSync: THCVaultSync;
|
||||
};
|
||||
|
||||
export const HCVaultSyncDestinationCol = ({ secretSync }: Props) => {
|
||||
const { primaryText, secondaryText } = getSecretSyncDestinationColValues(secretSync);
|
||||
|
||||
return <SecretSyncTableCell primaryText={primaryText} secondaryText={secondaryText} />;
|
||||
};
|
||||
+3
@@ -8,6 +8,7 @@ import { CamundaSyncDestinationCol } from "./CamundaSyncDestinationCol";
|
||||
import { DatabricksSyncDestinationCol } from "./DatabricksSyncDestinationCol";
|
||||
import { GcpSyncDestinationCol } from "./GcpSyncDestinationCol";
|
||||
import { GitHubSyncDestinationCol } from "./GitHubSyncDestinationCol";
|
||||
import { HCVaultSyncDestinationCol } from "./HCVaultSyncDestinationCol";
|
||||
import { HumanitecSyncDestinationCol } from "./HumanitecSyncDestinationCol";
|
||||
import { TeamCitySyncDestinationCol } from "./TeamCitySyncDestinationCol";
|
||||
import { TerraformCloudSyncDestinationCol } from "./TerraformCloudSyncDestinationCol";
|
||||
@@ -44,6 +45,8 @@ export const SecretSyncDestinationCol = ({ secretSync }: Props) => {
|
||||
return <VercelSyncDestinationCol secretSync={secretSync} />;
|
||||
case SecretSync.Windmill:
|
||||
return <WindmillSyncDestinationCol secretSync={secretSync} />;
|
||||
case SecretSync.HCVault:
|
||||
return <HCVaultSyncDestinationCol secretSync={secretSync} />;
|
||||
case SecretSync.TeamCity:
|
||||
return <TeamCitySyncDestinationCol secretSync={secretSync} />;
|
||||
default:
|
||||
|
||||
+4
@@ -94,6 +94,10 @@ export const getSecretSyncDestinationColValues = (secretSync: TSecretSync) => {
|
||||
primaryText = destinationConfig.workspace;
|
||||
secondaryText = destinationConfig.path;
|
||||
break;
|
||||
case SecretSync.HCVault:
|
||||
primaryText = destinationConfig.mount;
|
||||
secondaryText = destinationConfig.path;
|
||||
break;
|
||||
case SecretSync.TeamCity:
|
||||
primaryText = destinationConfig.project;
|
||||
secondaryText = destinationConfig.buildConfig;
|
||||
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
import { GenericFieldLabel } from "@app/components/secret-syncs";
|
||||
import { THCVaultSync } from "@app/hooks/api/secretSyncs/types/hc-vault-sync";
|
||||
|
||||
type Props = {
|
||||
secretSync: THCVaultSync;
|
||||
};
|
||||
|
||||
export const HCVaultSyncDestinationSection = ({ secretSync }: Props) => {
|
||||
const {
|
||||
destinationConfig: { path, mount }
|
||||
} = secretSync;
|
||||
|
||||
return (
|
||||
<>
|
||||
<GenericFieldLabel label="Secrets Engine Mount">{mount}</GenericFieldLabel>
|
||||
<GenericFieldLabel label="Path">{path}</GenericFieldLabel>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+5
-1
@@ -18,6 +18,7 @@ import { CamundaSyncDestinationSection } from "./CamundaSyncDestinationSection";
|
||||
import { DatabricksSyncDestinationSection } from "./DatabricksSyncDestinationSection";
|
||||
import { GcpSyncDestinationSection } from "./GcpSyncDestinationSection";
|
||||
import { GitHubSyncDestinationSection } from "./GitHubSyncDestinationSection";
|
||||
import { HCVaultSyncDestinationSection } from "./HCVaultSyncDestinationSection";
|
||||
import { HumanitecSyncDestinationSection } from "./HumanitecSyncDestinationSection";
|
||||
import { TeamCitySyncDestinationSection } from "./TeamCitySyncDestinationSection";
|
||||
import { TerraformCloudSyncDestinationSection } from "./TerraformCloudSyncDestinationSection";
|
||||
@@ -35,7 +36,7 @@ export const SecretSyncDestinationSection = ({ secretSync, onEditDestination }:
|
||||
const app = APP_CONNECTION_MAP[connection.app].name;
|
||||
|
||||
let DestinationComponents: ReactNode;
|
||||
switch (secretSync.destination) {
|
||||
switch (destination) {
|
||||
case SecretSync.AWSParameterStore:
|
||||
DestinationComponents = <AwsParameterStoreSyncDestinationSection secretSync={secretSync} />;
|
||||
break;
|
||||
@@ -74,6 +75,9 @@ export const SecretSyncDestinationSection = ({ secretSync, onEditDestination }:
|
||||
case SecretSync.Windmill:
|
||||
DestinationComponents = <WindmillSyncDestinationSection secretSync={secretSync} />;
|
||||
break;
|
||||
case SecretSync.HCVault:
|
||||
DestinationComponents = <HCVaultSyncDestinationSection secretSync={secretSync} />;
|
||||
break;
|
||||
case SecretSync.TeamCity:
|
||||
DestinationComponents = <TeamCitySyncDestinationSection secretSync={secretSync} />;
|
||||
break;
|
||||
|
||||
+1
@@ -52,6 +52,7 @@ export const SecretSyncOptionsSection = ({ secretSync, onEditOptions }: Props) =
|
||||
case SecretSync.Camunda:
|
||||
case SecretSync.Vercel:
|
||||
case SecretSync.Windmill:
|
||||
case SecretSync.HCVault:
|
||||
case SecretSync.TeamCity:
|
||||
AdditionalSyncOptionsComponent = null;
|
||||
break;
|
||||
|
||||
+125
-313
@@ -1,351 +1,163 @@
|
||||
import { useEffect } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { faCheckCircle } from "@fortawesome/free-solid-svg-icons";
|
||||
import { BsMicrosoftTeams, BsSlack } from "react-icons/bs";
|
||||
import { faGear, faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Link } from "@tanstack/react-router";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
Button,
|
||||
ContentLoader,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
DeleteActionModal,
|
||||
EmptyState,
|
||||
FormControl,
|
||||
Input,
|
||||
Select,
|
||||
SelectItem,
|
||||
Switch
|
||||
Table,
|
||||
TableContainer,
|
||||
TBody,
|
||||
Td,
|
||||
THead,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects, useWorkspace } from "@app/context";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import {
|
||||
useGetSlackIntegrationChannels,
|
||||
useGetSlackIntegrations,
|
||||
useGetWorkspaceSlackConfig,
|
||||
useUpdateProjectSlackConfig
|
||||
useDeleteProjectWorkflowIntegration,
|
||||
useGetWorkspaceWorkflowIntegrationConfig
|
||||
} from "@app/hooks/api";
|
||||
import { WorkflowIntegrationPlatform } from "@app/hooks/api/workflowIntegrations/types";
|
||||
|
||||
const formSchema = z.object({
|
||||
slackIntegrationId: z.string(),
|
||||
isSecretRequestNotificationEnabled: z.boolean(),
|
||||
secretRequestChannels: z.string().array(),
|
||||
isAccessRequestNotificationEnabled: z.boolean(),
|
||||
accessRequestChannels: z.string().array()
|
||||
});
|
||||
import { AddWorkflowIntegrationModal } from "./components/AddWorkflowIntegrationModal";
|
||||
import { EditWorkflowIntegrationModal } from "./components/EditWorkflowIntegrationModal";
|
||||
import { MicrosoftTeamsConfigRow } from "./components/MicrosoftTeamsConfigRow";
|
||||
import { SlackConfigRow } from "./components/SlackConfigRow";
|
||||
|
||||
type TSlackConfigForm = z.infer<typeof formSchema>;
|
||||
export const renderProvider = (integration: WorkflowIntegrationPlatform) => {
|
||||
if (integration === WorkflowIntegrationPlatform.SLACK) {
|
||||
return <BsSlack />;
|
||||
}
|
||||
|
||||
if (integration === WorkflowIntegrationPlatform.MICROSOFT_TEAMS) {
|
||||
return <BsMicrosoftTeams />;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const WorkflowIntegrationTab = () => {
|
||||
const { popUp, handlePopUpOpen, handlePopUpToggle, handlePopUpClose } = usePopUp([
|
||||
"addWorkflowIntegration",
|
||||
"removeIntegration",
|
||||
"editIntegration"
|
||||
] as const);
|
||||
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { data: slackConfig, isPending: isSlackConfigLoading } = useGetWorkspaceSlackConfig({
|
||||
workspaceId: currentWorkspace?.id ?? ""
|
||||
});
|
||||
const { data: slackIntegrations } = useGetSlackIntegrations(currentWorkspace?.orgId);
|
||||
const { mutateAsync: updateProjectSlackConfig } = useUpdateProjectSlackConfig();
|
||||
const { data: slackConfig, isPending: isSlackConfigLoading } =
|
||||
useGetWorkspaceWorkflowIntegrationConfig({
|
||||
workspaceId: currentWorkspace?.id ?? "",
|
||||
integration: WorkflowIntegrationPlatform.SLACK
|
||||
});
|
||||
|
||||
const {
|
||||
control,
|
||||
watch,
|
||||
handleSubmit,
|
||||
setValue,
|
||||
formState: { isDirty, isSubmitting }
|
||||
} = useForm<TSlackConfigForm>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
isAccessRequestNotificationEnabled: false,
|
||||
accessRequestChannels: [],
|
||||
isSecretRequestNotificationEnabled: false,
|
||||
secretRequestChannels: []
|
||||
}
|
||||
});
|
||||
const { data: microsoftTeamsConfig, isPending: isMicrosoftTeamsConfigLoading } =
|
||||
useGetWorkspaceWorkflowIntegrationConfig({
|
||||
workspaceId: currentWorkspace?.id ?? "",
|
||||
integration: WorkflowIntegrationPlatform.MICROSOFT_TEAMS
|
||||
});
|
||||
|
||||
const secretRequestNotifState = watch("isSecretRequestNotificationEnabled");
|
||||
const selectedSlackIntegrationId = watch("slackIntegrationId");
|
||||
const accessRequestNotifState = watch("isAccessRequestNotificationEnabled");
|
||||
const { mutateAsync: deleteIntegration } = useDeleteProjectWorkflowIntegration();
|
||||
|
||||
const { data: slackChannels } = useGetSlackIntegrationChannels(selectedSlackIntegrationId);
|
||||
const slackChannelIdToName = Object.fromEntries(
|
||||
(slackChannels || []).map((channel) => [channel.id, channel.name])
|
||||
);
|
||||
const sortedSlackChannels = slackChannels?.sort((a, b) =>
|
||||
a.name.toLowerCase().localeCompare(b.name.toLowerCase())
|
||||
);
|
||||
|
||||
const handleIntegrationSave = async (data: TSlackConfigForm) => {
|
||||
if (!currentWorkspace) {
|
||||
const handleRemoveIntegration = async (
|
||||
integrationType: WorkflowIntegrationPlatform,
|
||||
integrationId: string
|
||||
) => {
|
||||
if (!currentWorkspace.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
await updateProjectSlackConfig({
|
||||
workspaceId: currentWorkspace.id,
|
||||
...data,
|
||||
accessRequestChannels: data.accessRequestChannels.filter(Boolean).join(", "),
|
||||
secretRequestChannels: data.secretRequestChannels.filter(Boolean).join(", ")
|
||||
await deleteIntegration({
|
||||
projectId: currentWorkspace?.id ?? "",
|
||||
integration: integrationType,
|
||||
integrationId
|
||||
});
|
||||
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: "Successfully updated slack integration"
|
||||
text: `Successfully removed ${integrationType.replace("-", " ").replace(/\b\w/g, (char) => char.toUpperCase())} integration`
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (slackConfig) {
|
||||
setValue("slackIntegrationId", slackConfig.slackIntegrationId);
|
||||
setValue(
|
||||
"isSecretRequestNotificationEnabled",
|
||||
slackConfig.isSecretRequestNotificationEnabled
|
||||
);
|
||||
setValue(
|
||||
"isAccessRequestNotificationEnabled",
|
||||
slackConfig.isAccessRequestNotificationEnabled
|
||||
);
|
||||
|
||||
if (slackChannels) {
|
||||
setValue(
|
||||
"secretRequestChannels",
|
||||
slackConfig.secretRequestChannels
|
||||
.split(", ")
|
||||
.filter((channel) => channel in slackChannelIdToName)
|
||||
);
|
||||
setValue(
|
||||
"accessRequestChannels",
|
||||
slackConfig.accessRequestChannels
|
||||
.split(", ")
|
||||
.filter((channel) => channel in slackChannelIdToName)
|
||||
);
|
||||
}
|
||||
}
|
||||
}, [slackConfig, slackChannels]);
|
||||
|
||||
if (isSlackConfigLoading) {
|
||||
return <ContentLoader />;
|
||||
}
|
||||
|
||||
return !slackIntegrations?.length ? (
|
||||
<EmptyState title="You do not have any integrations configured.">
|
||||
<Link
|
||||
to="/organization/settings"
|
||||
search={{
|
||||
selectedTab: "workflow-integrations"
|
||||
}}
|
||||
>
|
||||
<div className="mt-2 underline decoration-primary-800 underline-offset-4 duration-200 hover:cursor-pointer hover:text-mineshaft-100 hover:decoration-primary-600">
|
||||
Create one now
|
||||
</div>
|
||||
</Link>
|
||||
</EmptyState>
|
||||
) : (
|
||||
return (
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="flex justify-between">
|
||||
<h2 className="mb-2 flex-1 text-xl font-semibold text-mineshaft-100">Slack Integration</h2>
|
||||
<p className="text-xl font-semibold text-mineshaft-100">Workflow Integrations</p>
|
||||
<OrgPermissionCan I={OrgPermissionActions.Create} an={OrgPermissionSubjects.Settings}>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
onClick={() => {
|
||||
handlePopUpOpen("addWorkflowIntegration");
|
||||
}}
|
||||
isDisabled={!isAllowed}
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</div>
|
||||
<p className="mb-4 text-gray-400">
|
||||
This integration allows you to send notifications to your Slack workspace in response to
|
||||
events in your project.
|
||||
Connect Infisical to other platforms for notification and workflow integrations.
|
||||
</p>
|
||||
<form onSubmit={handleSubmit(handleIntegrationSave)}>
|
||||
<div className="max-w-md">
|
||||
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Settings}>
|
||||
{(isAllowed) => (
|
||||
<Controller
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl errorText={error?.message} isError={Boolean(error)}>
|
||||
<Select
|
||||
{...field}
|
||||
isDisabled={!isAllowed}
|
||||
placeholder="None"
|
||||
onValueChange={onChange}
|
||||
defaultValue={slackConfig?.slackIntegrationId}
|
||||
className="w-3/4 bg-mineshaft-600"
|
||||
>
|
||||
{slackIntegrations?.map((slackIntegration) => (
|
||||
<SelectItem
|
||||
value={slackIntegration.id}
|
||||
key={`slack-integration-${slackIntegration.id}`}
|
||||
>
|
||||
{slackIntegration.slug}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
control={control}
|
||||
name="slackIntegrationId"
|
||||
<TableContainer>
|
||||
{!!slackConfig || !!microsoftTeamsConfig ? (
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Td>Provider</Td>
|
||||
<Td>Access Request Notifications Destination</Td>
|
||||
<Td>Secret Request Notifications Destination</Td>
|
||||
<Td />
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
<SlackConfigRow
|
||||
handlePopUpOpen={handlePopUpOpen}
|
||||
isSlackConfigLoading={isSlackConfigLoading}
|
||||
slackConfig={slackConfig}
|
||||
/>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
{selectedSlackIntegrationId && (
|
||||
<>
|
||||
<h2 className="mb-2 flex-1 text-lg font-semibold text-mineshaft-100">Events</h2>
|
||||
<Controller
|
||||
control={control}
|
||||
name="isSecretRequestNotificationEnabled"
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
className="mb-2 mt-3"
|
||||
>
|
||||
<Switch
|
||||
id="secret-approval-notification"
|
||||
onCheckedChange={(value) => field.onChange(value)}
|
||||
isChecked={field.value}
|
||||
>
|
||||
<p className="w-full">Secret Approval Requests</p>
|
||||
</Switch>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
<MicrosoftTeamsConfigRow
|
||||
handlePopUpOpen={handlePopUpOpen}
|
||||
isMicrosoftTeamsConfigLoading={isMicrosoftTeamsConfigLoading}
|
||||
microsoftTeamsConfig={microsoftTeamsConfig}
|
||||
/>
|
||||
</TBody>
|
||||
</Table>
|
||||
) : (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<EmptyState
|
||||
title="No project workflow integrations configured. Add a new workflow integration to get started"
|
||||
icon={faGear}
|
||||
/>
|
||||
{secretRequestNotifState && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="secretRequestChannels"
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Slack channels"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Input
|
||||
isReadOnly
|
||||
value={value
|
||||
?.filter(Boolean)
|
||||
.map((entry) => slackChannelIdToName[entry])
|
||||
.join(", ")}
|
||||
className="text-left"
|
||||
/>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
style={{
|
||||
width: "var(--radix-dropdown-menu-trigger-width)",
|
||||
maxHeight: "350px",
|
||||
overflowY: "auto"
|
||||
}}
|
||||
side="bottom"
|
||||
align="start"
|
||||
>
|
||||
{sortedSlackChannels?.map((slackChannel) => {
|
||||
const isChecked = value?.includes(slackChannel.id);
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
onClick={(evt) => {
|
||||
evt.preventDefault();
|
||||
onChange(
|
||||
isChecked
|
||||
? value?.filter((el: string) => el !== slackChannel.id)
|
||||
: [...(value || []), slackChannel.id]
|
||||
);
|
||||
}}
|
||||
key={`secret-requests-slack-channel-${slackChannel.id}`}
|
||||
iconPos="right"
|
||||
icon={isChecked && <FontAwesomeIcon icon={faCheckCircle} />}
|
||||
>
|
||||
{slackChannel.name}
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<Controller
|
||||
control={control}
|
||||
name="isAccessRequestNotificationEnabled"
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl isError={Boolean(error)} errorText={error?.message} className="mb-2">
|
||||
<Switch
|
||||
id="access-request-notification"
|
||||
onCheckedChange={(value) => field.onChange(value)}
|
||||
isChecked={field.value}
|
||||
>
|
||||
<p className="w-full">Access Requests</p>
|
||||
</Switch>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
{accessRequestNotifState && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="accessRequestChannels"
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Slack channels"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Input
|
||||
isReadOnly
|
||||
value={value
|
||||
?.filter(Boolean)
|
||||
.map((entry) => slackChannelIdToName[entry])
|
||||
.join(", ")}
|
||||
className="text-left"
|
||||
/>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
style={{
|
||||
width: "var(--radix-dropdown-menu-trigger-width)",
|
||||
maxHeight: "350px",
|
||||
overflowY: "auto"
|
||||
}}
|
||||
side="bottom"
|
||||
align="start"
|
||||
>
|
||||
{sortedSlackChannels?.map((slackChannel) => {
|
||||
const isChecked = value?.includes(slackChannel.id);
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
onClick={(evt) => {
|
||||
evt.preventDefault();
|
||||
onChange(
|
||||
isChecked
|
||||
? value?.filter((el: string) => el !== slackChannel.id)
|
||||
: [...(value || []), slackChannel.id]
|
||||
);
|
||||
}}
|
||||
key={`access-requests-slack-channel-${slackChannel.id}`}
|
||||
iconPos="right"
|
||||
icon={isChecked && <FontAwesomeIcon icon={faCheckCircle} />}
|
||||
>
|
||||
{slackChannel.name}
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
className="mt-4"
|
||||
type="submit"
|
||||
isDisabled={!isDirty}
|
||||
isLoading={isSubmitting}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
</TableContainer>
|
||||
<AddWorkflowIntegrationModal
|
||||
isOpen={popUp.addWorkflowIntegration.isOpen}
|
||||
onToggle={(state) => handlePopUpToggle("addWorkflowIntegration", state)}
|
||||
/>
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.removeIntegration.isOpen}
|
||||
title="Are you sure want to remove this integration?"
|
||||
onChange={(isOpen) => handlePopUpToggle("removeIntegration", isOpen)}
|
||||
deleteKey="confirm"
|
||||
onDeleteApproved={async () => {
|
||||
await handleRemoveIntegration(
|
||||
popUp.removeIntegration.data?.integration,
|
||||
popUp.removeIntegration.data?.integrationId
|
||||
);
|
||||
handlePopUpClose("removeIntegration");
|
||||
}}
|
||||
/>
|
||||
<EditWorkflowIntegrationModal
|
||||
isOpen={popUp.editIntegration.isOpen}
|
||||
onClose={() => handlePopUpClose("editIntegration")}
|
||||
integration={popUp.editIntegration.data?.integration}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
+153
@@ -0,0 +1,153 @@
|
||||
import { useState } from "react";
|
||||
import { BsMicrosoftTeams } from "react-icons/bs";
|
||||
import { faSlack } from "@fortawesome/free-brands-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { Modal, ModalContent } from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { useGetWorkspaceWorkflowIntegrationConfig } from "@app/hooks/api";
|
||||
import { WorkflowIntegrationPlatform } from "@app/hooks/api/workflowIntegrations/types";
|
||||
|
||||
import { MicrosoftTeamsIntegrationForm } from "./MicrosoftTeamsIntegrationForm";
|
||||
import { SlackIntegrationForm } from "./SlackIntegrationForm";
|
||||
|
||||
type Props = {
|
||||
isOpen?: boolean;
|
||||
onToggle: (isOpen: boolean) => void;
|
||||
};
|
||||
|
||||
enum WizardSteps {
|
||||
SelectPlatform = "select-platform",
|
||||
PlatformInputs = "platform-inputs"
|
||||
}
|
||||
|
||||
const PLATFORM_LIST = [
|
||||
{
|
||||
icon: <FontAwesomeIcon icon={faSlack} size="lg" />,
|
||||
platform: WorkflowIntegrationPlatform.SLACK,
|
||||
title: "Slack"
|
||||
},
|
||||
{
|
||||
icon: <BsMicrosoftTeams className="text-lg" />,
|
||||
platform: WorkflowIntegrationPlatform.MICROSOFT_TEAMS,
|
||||
title: "Microsoft Teams"
|
||||
}
|
||||
];
|
||||
|
||||
export const AddWorkflowIntegrationModal = ({ isOpen, onToggle }: Props) => {
|
||||
const [wizardStep, setWizardStep] = useState(WizardSteps.SelectPlatform);
|
||||
const [selectedPlatform, setSelectedPlatform] = useState<string | null>(null);
|
||||
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { data: microsoftTeamsConfig } = useGetWorkspaceWorkflowIntegrationConfig({
|
||||
workspaceId: currentWorkspace?.id ?? "",
|
||||
integration: WorkflowIntegrationPlatform.MICROSOFT_TEAMS
|
||||
});
|
||||
|
||||
const { data: slackConfig } = useGetWorkspaceWorkflowIntegrationConfig({
|
||||
workspaceId: currentWorkspace?.id ?? "",
|
||||
integration: WorkflowIntegrationPlatform.SLACK
|
||||
});
|
||||
|
||||
const microsoftTeamsConfigured = !!microsoftTeamsConfig;
|
||||
const slackConfigured = !!slackConfig;
|
||||
|
||||
const handleFormReset = (state: boolean = false) => {
|
||||
onToggle(state);
|
||||
setWizardStep(WizardSteps.SelectPlatform);
|
||||
setSelectedPlatform(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onOpenChange={(state) => handleFormReset(state)}>
|
||||
<ModalContent
|
||||
title={`Add a ${selectedPlatform?.replace("-", " ").replace(/\b\w/g, (char) => char.toUpperCase()) ?? "workflow"} integration`}
|
||||
className="my-4"
|
||||
>
|
||||
<AnimatePresence mode="wait">
|
||||
{wizardStep === WizardSteps.SelectPlatform && (
|
||||
<motion.div
|
||||
key="select-type-step"
|
||||
transition={{ duration: 0.1 }}
|
||||
initial={{ opacity: 0, translateX: 30 }}
|
||||
animate={{ opacity: 1, translateX: 0 }}
|
||||
exit={{ opacity: 0, translateX: -30 }}
|
||||
>
|
||||
<div className="mb-4 text-mineshaft-300">Select a workflow integration</div>
|
||||
<div className="flex items-center space-x-4">
|
||||
{PLATFORM_LIST.map(({ icon, platform, title }) => {
|
||||
const isConfigured =
|
||||
(platform === WorkflowIntegrationPlatform.MICROSOFT_TEAMS &&
|
||||
microsoftTeamsConfigured) ||
|
||||
(platform === WorkflowIntegrationPlatform.SLACK && slackConfigured);
|
||||
|
||||
return (
|
||||
<div className="relative" key={platform}>
|
||||
<div
|
||||
className={twMerge(
|
||||
"flex h-32 w-36 cursor-pointer flex-col items-center space-y-4 rounded border border-mineshaft-500 bg-bunker-600 p-6 transition-all hover:border-primary/70 hover:bg-primary/10 hover:text-white",
|
||||
isConfigured && "border-primary/50 opacity-60"
|
||||
)}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => {
|
||||
setSelectedPlatform(platform);
|
||||
setWizardStep(WizardSteps.PlatformInputs);
|
||||
}}
|
||||
onKeyDown={(evt) => {
|
||||
if (evt.key === "Enter") {
|
||||
setSelectedPlatform(platform);
|
||||
setWizardStep(WizardSteps.PlatformInputs);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div>{icon}</div>
|
||||
<div className="whitespace-pre-wrap text-center text-sm">{title}</div>
|
||||
</div>
|
||||
{isConfigured && (
|
||||
<div className="absolute bottom-0 left-0 right-0 z-30 h-full">
|
||||
<div className="relative h-full">
|
||||
<div className="absolute bottom-0 right-0 w-full flex-row items-center overflow-hidden whitespace-nowrap rounded-b-md bg-primary px-2 py-0.5 text-center text-xs text-black">
|
||||
Already Configured
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{wizardStep === WizardSteps.PlatformInputs &&
|
||||
selectedPlatform === WorkflowIntegrationPlatform.SLACK && (
|
||||
<motion.div
|
||||
key="platform-inputs-step-slack"
|
||||
transition={{ duration: 0.1 }}
|
||||
initial={{ opacity: 0, translateX: 30 }}
|
||||
animate={{ opacity: 1, translateX: 0 }}
|
||||
exit={{ opacity: 0, translateX: -30 }}
|
||||
>
|
||||
<SlackIntegrationForm onClose={() => handleFormReset(false)} />
|
||||
</motion.div>
|
||||
)}
|
||||
{wizardStep === WizardSteps.PlatformInputs &&
|
||||
selectedPlatform === WorkflowIntegrationPlatform.MICROSOFT_TEAMS && (
|
||||
<motion.div
|
||||
key="platform-inputs-step-ms-teams"
|
||||
transition={{ duration: 0.1 }}
|
||||
initial={{ opacity: 0, translateX: 30 }}
|
||||
animate={{ opacity: 1, translateX: 0 }}
|
||||
exit={{ opacity: 0, translateX: -30 }}
|
||||
>
|
||||
<MicrosoftTeamsIntegrationForm onClose={() => handleFormReset(false)} />
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
import { Modal, ModalContent } from "@app/components/v2";
|
||||
import { WorkflowIntegrationPlatform } from "@app/hooks/api/workflowIntegrations/types";
|
||||
|
||||
import { MicrosoftTeamsIntegrationForm } from "./MicrosoftTeamsIntegrationForm";
|
||||
import { SlackIntegrationForm } from "./SlackIntegrationForm";
|
||||
|
||||
type Props = {
|
||||
isOpen?: boolean;
|
||||
onClose: () => void;
|
||||
|
||||
integration: WorkflowIntegrationPlatform;
|
||||
};
|
||||
|
||||
export const EditWorkflowIntegrationModal = ({ isOpen, onClose, integration }: Props) => {
|
||||
const handleFormReset = () => {
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onOpenChange={handleFormReset}>
|
||||
<ModalContent
|
||||
title={`Edit ${integration?.replace("-", " ").replace(/\b\w/g, (char) => char.toUpperCase()) ?? "workflow"} integration`}
|
||||
>
|
||||
{integration === WorkflowIntegrationPlatform.SLACK && (
|
||||
<SlackIntegrationForm onClose={handleFormReset} />
|
||||
)}
|
||||
{integration === WorkflowIntegrationPlatform.MICROSOFT_TEAMS && (
|
||||
<MicrosoftTeamsIntegrationForm onClose={handleFormReset} />
|
||||
)}
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
+148
@@ -0,0 +1,148 @@
|
||||
/* eslint-disable no-nested-ternary */
|
||||
import { BsMicrosoftTeams } from "react-icons/bs";
|
||||
import { faEllipsis } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
Badge,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
Spinner,
|
||||
Td,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
|
||||
import { useGetMicrosoftTeamsIntegrationTeams } from "@app/hooks/api";
|
||||
import {
|
||||
ProjectWorkflowIntegrationConfig,
|
||||
WorkflowIntegrationPlatform
|
||||
} from "@app/hooks/api/workflowIntegrations/types";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
type Props = {
|
||||
microsoftTeamsConfig?: ProjectWorkflowIntegrationConfig | null;
|
||||
isMicrosoftTeamsConfigLoading: boolean;
|
||||
handlePopUpOpen: (
|
||||
popUpName: keyof UsePopUpState<["removeIntegration", "editIntegration"]>,
|
||||
data?: {
|
||||
integrationId?: string;
|
||||
integration: WorkflowIntegrationPlatform;
|
||||
}
|
||||
) => void;
|
||||
};
|
||||
|
||||
export const MicrosoftTeamsConfigRow = ({
|
||||
handlePopUpOpen,
|
||||
isMicrosoftTeamsConfigLoading,
|
||||
microsoftTeamsConfig
|
||||
}: Props) => {
|
||||
const { data: microsoftTeamsChannels, isPending: isMicrosoftTeamsChannelsLoading } =
|
||||
useGetMicrosoftTeamsIntegrationTeams(microsoftTeamsConfig?.integrationId);
|
||||
const microsoftTeamsChannelIdToName = Object.fromEntries(
|
||||
microsoftTeamsChannels?.flatMap((team) =>
|
||||
team.channels.map((channel) => [channel.channelId, channel.channelName])
|
||||
) ?? []
|
||||
);
|
||||
|
||||
if (microsoftTeamsConfig?.integration !== WorkflowIntegrationPlatform.MICROSOFT_TEAMS) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isLoadingConfig = isMicrosoftTeamsChannelsLoading || isMicrosoftTeamsConfigLoading;
|
||||
|
||||
return (
|
||||
<Tr>
|
||||
<Td className="flex max-w-xs items-center overflow-hidden text-ellipsis">
|
||||
<div className="flex items-center gap-2">
|
||||
<BsMicrosoftTeams />
|
||||
Microsoft Teams
|
||||
</div>
|
||||
</Td>
|
||||
<Td>
|
||||
{microsoftTeamsConfig.isAccessRequestNotificationEnabled &&
|
||||
!isLoadingConfig &&
|
||||
microsoftTeamsConfig.accessRequestChannels?.channelIds?.length > 0 ? (
|
||||
<Badge>
|
||||
{microsoftTeamsConfig.accessRequestChannels.channelIds
|
||||
.map((channel) => microsoftTeamsChannelIdToName[channel])
|
||||
.join(", ")}
|
||||
</Badge>
|
||||
) : isLoadingConfig ? (
|
||||
<Spinner size="xs" />
|
||||
) : (
|
||||
<Badge variant="danger">Disabled</Badge>
|
||||
)}
|
||||
</Td>
|
||||
<Td>
|
||||
{microsoftTeamsConfig.isSecretRequestNotificationEnabled &&
|
||||
!isLoadingConfig &&
|
||||
microsoftTeamsConfig.secretRequestChannels?.channelIds?.length > 0 ? (
|
||||
<Badge>
|
||||
{microsoftTeamsConfig.secretRequestChannels.channelIds
|
||||
.map((channel) => microsoftTeamsChannelIdToName[channel])
|
||||
.join(", ")}
|
||||
</Badge>
|
||||
) : isLoadingConfig ? (
|
||||
<Spinner size="xs" />
|
||||
) : (
|
||||
<Badge variant="danger">Disabled</Badge>
|
||||
)}
|
||||
</Td>
|
||||
|
||||
<Td>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild className="rounded-lg">
|
||||
<div className="flex justify-end hover:text-primary-400 data-[state=open]:text-primary-400">
|
||||
<FontAwesomeIcon size="sm" icon={faEllipsis} />
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="p-1">
|
||||
<OrgPermissionCan I={OrgPermissionActions.Edit} an={OrgPermissionSubjects.Settings}>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
disabled={!isAllowed}
|
||||
className={twMerge(
|
||||
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
|
||||
handlePopUpOpen("editIntegration", {
|
||||
integration: WorkflowIntegrationPlatform.MICROSOFT_TEAMS
|
||||
});
|
||||
}}
|
||||
>
|
||||
Edit
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
<OrgPermissionCan I={OrgPermissionActions.Delete} an={OrgPermissionSubjects.Settings}>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
disabled={!isAllowed}
|
||||
className={twMerge(
|
||||
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
|
||||
handlePopUpOpen("removeIntegration", {
|
||||
integrationId: microsoftTeamsConfig.integrationId,
|
||||
integration: WorkflowIntegrationPlatform.MICROSOFT_TEAMS
|
||||
});
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
};
|
||||
+556
@@ -0,0 +1,556 @@
|
||||
import { useEffect } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { faCheckCircle } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
Button,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
FormControl,
|
||||
Input,
|
||||
Select,
|
||||
SelectItem,
|
||||
Switch
|
||||
} from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub
|
||||
} from "@app/context/ProjectPermissionContext";
|
||||
import {
|
||||
useGetMicrosoftTeamsIntegrations,
|
||||
useGetMicrosoftTeamsIntegrationTeams,
|
||||
useGetWorkspaceWorkflowIntegrationConfig,
|
||||
useUpdateProjectWorkflowIntegrationConfig
|
||||
} from "@app/hooks/api";
|
||||
import { WorkflowIntegrationPlatform } from "@app/hooks/api/workflowIntegrations/types";
|
||||
|
||||
const formSchema = z
|
||||
.object({
|
||||
microsoftTeamsIntegrationId: z.string(),
|
||||
isSecretRequestNotificationEnabled: z.boolean(),
|
||||
isAccessRequestNotificationEnabled: z.boolean(),
|
||||
secretRequestChannels: z
|
||||
.object({
|
||||
teamId: z.string(),
|
||||
channelIds: z.string().array()
|
||||
})
|
||||
.optional(),
|
||||
accessRequestChannels: z
|
||||
.object({
|
||||
teamId: z.string(),
|
||||
channelIds: z.string().array()
|
||||
})
|
||||
.optional()
|
||||
})
|
||||
.superRefine((data, ctx) => {
|
||||
if (data.isSecretRequestNotificationEnabled) {
|
||||
if (!data?.secretRequestChannels?.teamId) {
|
||||
ctx.addIssue({
|
||||
path: ["secretRequestChannels", "teamId"],
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "Team is required"
|
||||
});
|
||||
}
|
||||
|
||||
if (!data?.secretRequestChannels?.channelIds?.length) {
|
||||
ctx.addIssue({
|
||||
path: ["secretRequestChannels", "channelIds"],
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "At least one channel is required"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (data.isAccessRequestNotificationEnabled) {
|
||||
if (!data?.accessRequestChannels?.teamId) {
|
||||
ctx.addIssue({
|
||||
path: ["accessRequestChannels", "teamId"],
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "Team is required"
|
||||
});
|
||||
}
|
||||
|
||||
if (!data?.accessRequestChannels?.channelIds?.length) {
|
||||
ctx.addIssue({
|
||||
path: ["accessRequestChannels", "channelIds"],
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "At least one channel is required"
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
type TMicrosoftTeamsConfigForm = z.infer<typeof formSchema>;
|
||||
|
||||
type Props = {
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
export const MicrosoftTeamsIntegrationForm = ({ onClose }: Props) => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { data: microsoftTeamsConfig } = useGetWorkspaceWorkflowIntegrationConfig({
|
||||
workspaceId: currentWorkspace?.id ?? "",
|
||||
integration: WorkflowIntegrationPlatform.MICROSOFT_TEAMS
|
||||
});
|
||||
const { data: microsoftTeamsIntegrations } = useGetMicrosoftTeamsIntegrations(
|
||||
currentWorkspace?.orgId
|
||||
);
|
||||
|
||||
const { mutateAsync: updateProjectMicrosoftTeamsConfig } =
|
||||
useUpdateProjectWorkflowIntegrationConfig();
|
||||
|
||||
const {
|
||||
control,
|
||||
watch,
|
||||
handleSubmit,
|
||||
setValue,
|
||||
formState: { isDirty, isSubmitting }
|
||||
} = useForm<TMicrosoftTeamsConfigForm>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
isAccessRequestNotificationEnabled: false,
|
||||
isSecretRequestNotificationEnabled: false,
|
||||
accessRequestChannels: {
|
||||
teamId: "",
|
||||
channelIds: []
|
||||
},
|
||||
secretRequestChannels: {
|
||||
teamId: "",
|
||||
channelIds: []
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const handleIntegrationSave = async (data: TMicrosoftTeamsConfigForm) => {
|
||||
try {
|
||||
if (!currentWorkspace) {
|
||||
return;
|
||||
}
|
||||
|
||||
await updateProjectMicrosoftTeamsConfig({
|
||||
workspaceId: currentWorkspace.id,
|
||||
isAccessRequestNotificationEnabled: data.isAccessRequestNotificationEnabled,
|
||||
isSecretRequestNotificationEnabled: data.isSecretRequestNotificationEnabled,
|
||||
...(data.isAccessRequestNotificationEnabled && {
|
||||
accessRequestChannels: data.accessRequestChannels
|
||||
}),
|
||||
...(data.isSecretRequestNotificationEnabled && {
|
||||
secretRequestChannels: data.secretRequestChannels
|
||||
}),
|
||||
integration: WorkflowIntegrationPlatform.MICROSOFT_TEAMS,
|
||||
integrationId: data.microsoftTeamsIntegrationId
|
||||
});
|
||||
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: "Successfully created microsoft teams integration"
|
||||
});
|
||||
|
||||
onClose();
|
||||
} catch {
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: "Failed to create microsoft teams integration"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const selectedAccessRequestTeamId = watch("accessRequestChannels.teamId");
|
||||
const selectedSecretRequestTeamId = watch("secretRequestChannels.teamId");
|
||||
|
||||
const selectedMicrosoftTeamsIntegrationId = watch("microsoftTeamsIntegrationId");
|
||||
|
||||
const accessRequestNotificationsEnabled = watch("isAccessRequestNotificationEnabled");
|
||||
const secretRequestNotificationsEnabled = watch("isSecretRequestNotificationEnabled");
|
||||
|
||||
const {
|
||||
data: microsoftTeamsIntegrationTeams,
|
||||
isPending: isLoadingMicrosoftTeamsIntegrationTeams
|
||||
} = useGetMicrosoftTeamsIntegrationTeams(selectedMicrosoftTeamsIntegrationId);
|
||||
|
||||
const sortedMicrosoftTeamsIntegrationTeams = microsoftTeamsIntegrationTeams?.sort((a, b) =>
|
||||
a.teamName.toLowerCase().localeCompare(b.teamName.toLowerCase())
|
||||
);
|
||||
|
||||
const selectableAccessRequestChannelIds = sortedMicrosoftTeamsIntegrationTeams
|
||||
?.filter((team) => team.teamId === selectedAccessRequestTeamId)
|
||||
.map((team) => team.channels)
|
||||
.flat();
|
||||
const selectableSecretRequestChannelIds = sortedMicrosoftTeamsIntegrationTeams
|
||||
?.filter((team) => team.teamId === selectedSecretRequestTeamId)
|
||||
.map((team) => team.channels)
|
||||
.flat();
|
||||
|
||||
const channelIdToName = [
|
||||
...(selectableAccessRequestChannelIds || []),
|
||||
...(selectableSecretRequestChannelIds || [])
|
||||
].reduce(
|
||||
(acc, channel) => {
|
||||
acc[channel.channelId] = channel.channelName;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, string>
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (microsoftTeamsConfig) {
|
||||
setValue("microsoftTeamsIntegrationId", microsoftTeamsConfig.integrationId);
|
||||
setValue(
|
||||
"isSecretRequestNotificationEnabled",
|
||||
microsoftTeamsConfig.isSecretRequestNotificationEnabled
|
||||
);
|
||||
setValue(
|
||||
"isAccessRequestNotificationEnabled",
|
||||
microsoftTeamsConfig.isAccessRequestNotificationEnabled
|
||||
);
|
||||
|
||||
if (microsoftTeamsConfig.integration === WorkflowIntegrationPlatform.MICROSOFT_TEAMS) {
|
||||
if (microsoftTeamsConfig.secretRequestChannels) {
|
||||
if (Object.entries(microsoftTeamsConfig.accessRequestChannels).length) {
|
||||
setValue("accessRequestChannels", microsoftTeamsConfig.accessRequestChannels);
|
||||
}
|
||||
if (Object.entries(microsoftTeamsConfig.secretRequestChannels).length) {
|
||||
setValue("secretRequestChannels", microsoftTeamsConfig.secretRequestChannels);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [microsoftTeamsConfig]);
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(handleIntegrationSave)}>
|
||||
<div className="flex w-full flex-col justify-start">
|
||||
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Settings}>
|
||||
{(isAllowed) => (
|
||||
<Controller
|
||||
control={control}
|
||||
name="microsoftTeamsIntegrationId"
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Microsoft Teams Integration"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
>
|
||||
<Select
|
||||
{...field}
|
||||
isDisabled={!isAllowed}
|
||||
placeholder="None"
|
||||
onValueChange={(v) => {
|
||||
onChange(v);
|
||||
|
||||
setValue("isAccessRequestNotificationEnabled", false);
|
||||
setValue("isSecretRequestNotificationEnabled", false);
|
||||
setValue("accessRequestChannels", {
|
||||
teamId: "",
|
||||
channelIds: []
|
||||
});
|
||||
setValue("secretRequestChannels", {
|
||||
teamId: "",
|
||||
channelIds: []
|
||||
});
|
||||
}}
|
||||
className="w-full"
|
||||
defaultValue={microsoftTeamsConfig?.integrationId}
|
||||
>
|
||||
{microsoftTeamsIntegrations?.map((microsoftTeamsIntegration) => (
|
||||
<SelectItem
|
||||
value={microsoftTeamsIntegration.id}
|
||||
key={`microsoft-teams-integration-${microsoftTeamsIntegration.id}`}
|
||||
>
|
||||
{microsoftTeamsIntegration.slug}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
{selectedMicrosoftTeamsIntegrationId && (
|
||||
<>
|
||||
<h2 className="mb-2 flex-1 text-sm text-mineshaft-400">Configure Events</h2>
|
||||
<Controller
|
||||
control={control}
|
||||
name="isSecretRequestNotificationEnabled"
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
className="mb-2 mt-3"
|
||||
>
|
||||
<Switch
|
||||
id="secret-approval-notification"
|
||||
onCheckedChange={(value) => field.onChange(value)}
|
||||
isChecked={field.value}
|
||||
>
|
||||
<p className="w-full">Secret Approval Requests</p>
|
||||
</Switch>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
{secretRequestNotificationsEnabled && (
|
||||
<>
|
||||
<Controller
|
||||
control={control}
|
||||
name="secretRequestChannels.teamId"
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Secret Approval Requests Notifications Team"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Select
|
||||
{...field}
|
||||
placeholder={
|
||||
isLoadingMicrosoftTeamsIntegrationTeams ? "Loading..." : "Select a team..."
|
||||
}
|
||||
className="w-full"
|
||||
onValueChange={(value) => onChange(value)}
|
||||
isLoading={isLoadingMicrosoftTeamsIntegrationTeams}
|
||||
>
|
||||
{!isLoadingMicrosoftTeamsIntegrationTeams &&
|
||||
sortedMicrosoftTeamsIntegrationTeams?.map((team) => (
|
||||
<SelectItem
|
||||
key={`secret-requests-microsoft-teams-team-${team.teamId}`}
|
||||
value={team.teamId}
|
||||
>
|
||||
{team.teamName}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="secretRequestChannels.channelIds"
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Microsoft Teams channels"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
className={
|
||||
isLoadingMicrosoftTeamsIntegrationTeams || !selectedSecretRequestTeamId
|
||||
? "opacity-50"
|
||||
: ""
|
||||
}
|
||||
asChild
|
||||
disabled={
|
||||
isLoadingMicrosoftTeamsIntegrationTeams || !selectedSecretRequestTeamId
|
||||
}
|
||||
>
|
||||
{selectedSecretRequestTeamId ? (
|
||||
<Input
|
||||
isReadOnly
|
||||
value={
|
||||
isLoadingMicrosoftTeamsIntegrationTeams
|
||||
? "Loading..."
|
||||
: value
|
||||
?.filter(Boolean)
|
||||
.map((entry) => channelIdToName[entry])
|
||||
.join(", ")
|
||||
}
|
||||
className="text-left"
|
||||
/>
|
||||
) : (
|
||||
<Input
|
||||
isReadOnly
|
||||
value="Select a team..."
|
||||
className="py-2 text-left text-sm"
|
||||
/>
|
||||
)}
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
style={{
|
||||
width: "var(--radix-dropdown-menu-trigger-width)",
|
||||
maxHeight: "350px",
|
||||
overflowY: "auto"
|
||||
}}
|
||||
side="bottom"
|
||||
align="start"
|
||||
>
|
||||
{selectableSecretRequestChannelIds?.map((channel) => {
|
||||
const isChecked = value?.includes(channel.channelId);
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
onClick={(evt) => {
|
||||
evt.preventDefault();
|
||||
onChange(
|
||||
isChecked
|
||||
? value?.filter((el: string) => el !== channel.channelId)
|
||||
: [...(value || []), channel.channelId]
|
||||
);
|
||||
}}
|
||||
key={`secret-requests-microsoft-teams-channel-${channel.channelId}`}
|
||||
iconPos="right"
|
||||
icon={isChecked && <FontAwesomeIcon icon={faCheckCircle} />}
|
||||
>
|
||||
{channel.channelName}
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<Controller
|
||||
control={control}
|
||||
name="isAccessRequestNotificationEnabled"
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl isError={Boolean(error)} errorText={error?.message} className="mb-2">
|
||||
<Switch
|
||||
id="access-request-notification"
|
||||
onCheckedChange={(value) => field.onChange(value)}
|
||||
isChecked={field.value}
|
||||
>
|
||||
<p className="w-full">Access Requests</p>
|
||||
</Switch>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
{accessRequestNotificationsEnabled && (
|
||||
<>
|
||||
<Controller
|
||||
control={control}
|
||||
name="accessRequestChannels.teamId"
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Access Requests Notifications Team"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Select
|
||||
{...field}
|
||||
placeholder={
|
||||
isLoadingMicrosoftTeamsIntegrationTeams ? "Loading..." : "Select a team..."
|
||||
}
|
||||
className="w-full"
|
||||
onValueChange={(value) => onChange(value)}
|
||||
isLoading={isLoadingMicrosoftTeamsIntegrationTeams}
|
||||
>
|
||||
{!isLoadingMicrosoftTeamsIntegrationTeams &&
|
||||
sortedMicrosoftTeamsIntegrationTeams?.map((team) => (
|
||||
<SelectItem
|
||||
key={`access-requests-microsoft-teams-team-${team.teamId}`}
|
||||
value={team.teamId}
|
||||
>
|
||||
{team.teamName}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
control={control}
|
||||
name="accessRequestChannels.channelIds"
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Microsoft Teams channels"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
className={
|
||||
isLoadingMicrosoftTeamsIntegrationTeams || !selectedAccessRequestTeamId
|
||||
? "opacity-50"
|
||||
: ""
|
||||
}
|
||||
asChild
|
||||
disabled={
|
||||
isLoadingMicrosoftTeamsIntegrationTeams || !selectedAccessRequestTeamId
|
||||
}
|
||||
>
|
||||
{selectedAccessRequestTeamId ? (
|
||||
<Input
|
||||
isReadOnly
|
||||
value={
|
||||
isLoadingMicrosoftTeamsIntegrationTeams
|
||||
? "Loading..."
|
||||
: value
|
||||
?.filter(Boolean)
|
||||
.map((entry) => channelIdToName[entry])
|
||||
.join(", ")
|
||||
}
|
||||
className="text-left"
|
||||
/>
|
||||
) : (
|
||||
<Input
|
||||
isReadOnly
|
||||
value="Select a team..."
|
||||
className="py-2 text-left text-sm"
|
||||
/>
|
||||
)}
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
style={{
|
||||
width: "var(--radix-dropdown-menu-trigger-width)",
|
||||
maxHeight: "350px",
|
||||
overflowY: "auto"
|
||||
}}
|
||||
side="bottom"
|
||||
align="start"
|
||||
>
|
||||
{selectableAccessRequestChannelIds?.map((channel) => {
|
||||
const isChecked = value?.includes(channel.channelId);
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
onClick={(evt) => {
|
||||
evt.preventDefault();
|
||||
onChange(
|
||||
isChecked
|
||||
? value?.filter((el: string) => el !== channel.channelId)
|
||||
: [...(value || []), channel.channelId]
|
||||
);
|
||||
}}
|
||||
key={`access-requests-slack-channel-${channel.channelId}`}
|
||||
iconPos="right"
|
||||
icon={isChecked && <FontAwesomeIcon icon={faCheckCircle} />}
|
||||
>
|
||||
{channel.channelName}
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
className="mt-4"
|
||||
type="submit"
|
||||
disabled={!isDirty}
|
||||
isDisabled={!isDirty}
|
||||
isLoading={isSubmitting}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</form>
|
||||
);
|
||||
};
|
||||
+145
@@ -0,0 +1,145 @@
|
||||
/* eslint-disable no-nested-ternary */
|
||||
import { BsSlack } from "react-icons/bs";
|
||||
import { faEllipsis } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
Badge,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
Spinner,
|
||||
Td,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
|
||||
import { useGetSlackIntegrationChannels } from "@app/hooks/api";
|
||||
import {
|
||||
ProjectWorkflowIntegrationConfig,
|
||||
WorkflowIntegrationPlatform
|
||||
} from "@app/hooks/api/workflowIntegrations/types";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
type Props = {
|
||||
slackConfig?: ProjectWorkflowIntegrationConfig | null;
|
||||
isSlackConfigLoading: boolean;
|
||||
handlePopUpOpen: (
|
||||
popUpName: keyof UsePopUpState<["removeIntegration", "editIntegration"]>,
|
||||
data?: {
|
||||
integrationId?: string;
|
||||
integration: WorkflowIntegrationPlatform;
|
||||
}
|
||||
) => void;
|
||||
};
|
||||
|
||||
export const SlackConfigRow = ({ handlePopUpOpen, isSlackConfigLoading, slackConfig }: Props) => {
|
||||
const { data: slackChannels, isPending: isSlackChannelsLoading } = useGetSlackIntegrationChannels(
|
||||
slackConfig?.integrationId
|
||||
);
|
||||
const slackChannelIdToName = Object.fromEntries(
|
||||
(slackChannels || []).map((channel) => [channel.id, channel.name])
|
||||
);
|
||||
|
||||
if (slackConfig?.integration !== WorkflowIntegrationPlatform.SLACK) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isLoadingConfig = isSlackChannelsLoading || isSlackConfigLoading;
|
||||
|
||||
return (
|
||||
<Tr>
|
||||
<Td className="flex max-w-xs items-center overflow-hidden text-ellipsis">
|
||||
<div className="flex items-center gap-2">
|
||||
<BsSlack />
|
||||
Slack
|
||||
</div>
|
||||
</Td>
|
||||
<Td>
|
||||
{slackConfig.isAccessRequestNotificationEnabled &&
|
||||
!isLoadingConfig &&
|
||||
slackConfig.accessRequestChannels.length > 0 ? (
|
||||
<Badge>
|
||||
{slackConfig.accessRequestChannels
|
||||
.split(", ")
|
||||
.map((channel) => slackChannelIdToName[channel])
|
||||
.join(", ")}
|
||||
</Badge>
|
||||
) : isLoadingConfig ? (
|
||||
<Spinner size="xs" />
|
||||
) : (
|
||||
<Badge variant="danger">Disabled</Badge>
|
||||
)}
|
||||
</Td>
|
||||
<Td>
|
||||
{slackConfig.isSecretRequestNotificationEnabled &&
|
||||
!isLoadingConfig &&
|
||||
slackConfig.secretRequestChannels.length > 0 ? (
|
||||
<Badge>
|
||||
{slackConfig.secretRequestChannels
|
||||
.split(", ")
|
||||
.map((channel) => slackChannelIdToName[channel])
|
||||
.join(", ")}
|
||||
</Badge>
|
||||
) : isLoadingConfig ? (
|
||||
<Spinner size="xs" />
|
||||
) : (
|
||||
<Badge variant="danger">Disabled</Badge>
|
||||
)}
|
||||
</Td>
|
||||
|
||||
<Td>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild className="rounded-lg">
|
||||
<div className="flex justify-end hover:text-primary-400 data-[state=open]:text-primary-400">
|
||||
<FontAwesomeIcon size="sm" icon={faEllipsis} />
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="p-1">
|
||||
<OrgPermissionCan I={OrgPermissionActions.Edit} an={OrgPermissionSubjects.Settings}>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
disabled={!isAllowed}
|
||||
className={twMerge(
|
||||
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
|
||||
handlePopUpOpen("editIntegration", {
|
||||
integration: WorkflowIntegrationPlatform.SLACK
|
||||
});
|
||||
}}
|
||||
>
|
||||
Edit
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
<OrgPermissionCan I={OrgPermissionActions.Delete} an={OrgPermissionSubjects.Settings}>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
disabled={!isAllowed}
|
||||
className={twMerge(
|
||||
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
|
||||
handlePopUpOpen("removeIntegration", {
|
||||
integrationId: slackConfig.integrationId,
|
||||
integration: WorkflowIntegrationPlatform.SLACK
|
||||
});
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
};
|
||||
+355
@@ -0,0 +1,355 @@
|
||||
import { useEffect } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { faCheckCircle } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
Button,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
FormControl,
|
||||
Input,
|
||||
Select,
|
||||
SelectItem,
|
||||
Switch
|
||||
} from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub
|
||||
} from "@app/context/ProjectPermissionContext";
|
||||
import {
|
||||
useGetSlackIntegrationChannels,
|
||||
useGetWorkflowIntegrations,
|
||||
useGetWorkspaceWorkflowIntegrationConfig,
|
||||
useUpdateProjectWorkflowIntegrationConfig
|
||||
} from "@app/hooks/api";
|
||||
import { WorkflowIntegrationPlatform } from "@app/hooks/api/workflowIntegrations/types";
|
||||
|
||||
const formSchema = z.object({
|
||||
slackIntegrationId: z.string(),
|
||||
isSecretRequestNotificationEnabled: z.boolean(),
|
||||
secretRequestChannels: z.string().array(),
|
||||
isAccessRequestNotificationEnabled: z.boolean(),
|
||||
accessRequestChannels: z.string().array()
|
||||
});
|
||||
|
||||
type TSlackConfigForm = z.infer<typeof formSchema>;
|
||||
|
||||
type Props = {
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
export const SlackIntegrationForm = ({ onClose }: Props) => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { data: slackConfig } = useGetWorkspaceWorkflowIntegrationConfig({
|
||||
workspaceId: currentWorkspace?.id ?? "",
|
||||
integration: WorkflowIntegrationPlatform.SLACK
|
||||
});
|
||||
const { data: workflowIntegrations } = useGetWorkflowIntegrations(currentWorkspace?.orgId);
|
||||
const { mutateAsync: updateProjectSlackConfig } = useUpdateProjectWorkflowIntegrationConfig();
|
||||
|
||||
const slackIntegrations = workflowIntegrations?.filter(
|
||||
(integration) => integration.integration === WorkflowIntegrationPlatform.SLACK
|
||||
);
|
||||
|
||||
const {
|
||||
control,
|
||||
watch,
|
||||
handleSubmit,
|
||||
setValue,
|
||||
formState: { isDirty, isSubmitting }
|
||||
} = useForm<TSlackConfigForm>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
isAccessRequestNotificationEnabled: false,
|
||||
accessRequestChannels: [],
|
||||
isSecretRequestNotificationEnabled: false,
|
||||
secretRequestChannels: []
|
||||
}
|
||||
});
|
||||
|
||||
const handleIntegrationSave = async (data: TSlackConfigForm) => {
|
||||
try {
|
||||
if (!currentWorkspace) {
|
||||
return;
|
||||
}
|
||||
|
||||
await updateProjectSlackConfig({
|
||||
...data,
|
||||
workspaceId: currentWorkspace.id,
|
||||
integration: WorkflowIntegrationPlatform.SLACK,
|
||||
integrationId: data.slackIntegrationId,
|
||||
accessRequestChannels: data.accessRequestChannels.filter(Boolean).join(", "),
|
||||
secretRequestChannels: data.secretRequestChannels.filter(Boolean).join(", ")
|
||||
});
|
||||
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: "Successfully created slack integration"
|
||||
});
|
||||
|
||||
onClose();
|
||||
} catch {
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: "Failed to create slack integration"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const secretRequestNotifState = watch("isSecretRequestNotificationEnabled");
|
||||
const selectedSlackIntegrationId = watch("slackIntegrationId");
|
||||
const accessRequestNotifState = watch("isAccessRequestNotificationEnabled");
|
||||
|
||||
const { data: slackChannels } = useGetSlackIntegrationChannels(selectedSlackIntegrationId);
|
||||
const slackChannelIdToName = Object.fromEntries(
|
||||
(slackChannels || []).map((channel) => [channel.id, channel.name])
|
||||
);
|
||||
const sortedSlackChannels = slackChannels?.sort((a, b) =>
|
||||
a.name.toLowerCase().localeCompare(b.name.toLowerCase())
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (slackConfig) {
|
||||
setValue("slackIntegrationId", slackConfig.integrationId);
|
||||
setValue(
|
||||
"isSecretRequestNotificationEnabled",
|
||||
slackConfig.isSecretRequestNotificationEnabled
|
||||
);
|
||||
setValue(
|
||||
"isAccessRequestNotificationEnabled",
|
||||
slackConfig.isAccessRequestNotificationEnabled
|
||||
);
|
||||
|
||||
if (slackConfig.integration === WorkflowIntegrationPlatform.SLACK) {
|
||||
if (slackChannels) {
|
||||
setValue(
|
||||
"secretRequestChannels",
|
||||
slackConfig.secretRequestChannels
|
||||
.split(", ")
|
||||
.filter((channel) => channel in slackChannelIdToName)
|
||||
);
|
||||
setValue(
|
||||
"accessRequestChannels",
|
||||
slackConfig.accessRequestChannels
|
||||
.split(", ")
|
||||
.filter((channel) => channel in slackChannelIdToName)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [slackConfig, slackChannels]);
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(handleIntegrationSave)}>
|
||||
<div className="flex w-full flex-col justify-start">
|
||||
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Settings}>
|
||||
{(isAllowed) => (
|
||||
<Controller
|
||||
control={control}
|
||||
name="slackIntegrationId"
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Slack Integration"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
>
|
||||
<Select
|
||||
{...field}
|
||||
isDisabled={!isAllowed}
|
||||
placeholder="None"
|
||||
onValueChange={(val) => {
|
||||
onChange(val);
|
||||
}}
|
||||
className="w-full"
|
||||
defaultValue={slackConfig?.integrationId}
|
||||
>
|
||||
{slackIntegrations?.map((slackIntegration) => (
|
||||
<SelectItem
|
||||
value={slackIntegration.id}
|
||||
key={`slack-integration-${slackIntegration.id}`}
|
||||
>
|
||||
{slackIntegration.slug}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
{selectedSlackIntegrationId && (
|
||||
<>
|
||||
<h2 className="mb-2 flex-1 text-sm text-mineshaft-400">Configure Events</h2>
|
||||
<Controller
|
||||
control={control}
|
||||
name="isSecretRequestNotificationEnabled"
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
className="mb-2 mt-3"
|
||||
>
|
||||
<Switch
|
||||
id="secret-approval-notification"
|
||||
onCheckedChange={(value) => field.onChange(value)}
|
||||
isChecked={field.value}
|
||||
>
|
||||
<p className="w-full">Secret Approval Requests</p>
|
||||
</Switch>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
{secretRequestNotifState && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="secretRequestChannels"
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Slack channels"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Input
|
||||
isReadOnly
|
||||
value={value
|
||||
?.filter(Boolean)
|
||||
.map((entry) => slackChannelIdToName[entry])
|
||||
.join(", ")}
|
||||
className="text-left"
|
||||
/>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
style={{
|
||||
width: "var(--radix-dropdown-menu-trigger-width)",
|
||||
maxHeight: "350px",
|
||||
overflowY: "auto"
|
||||
}}
|
||||
side="bottom"
|
||||
align="start"
|
||||
>
|
||||
{sortedSlackChannels?.map((slackChannel) => {
|
||||
const isChecked = value?.includes(slackChannel.id);
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
onClick={(evt) => {
|
||||
evt.preventDefault();
|
||||
onChange(
|
||||
isChecked
|
||||
? value?.filter((el: string) => el !== slackChannel.id)
|
||||
: [...(value || []), slackChannel.id]
|
||||
);
|
||||
}}
|
||||
key={`secret-requests-slack-channel-${slackChannel.id}`}
|
||||
iconPos="right"
|
||||
icon={isChecked && <FontAwesomeIcon icon={faCheckCircle} />}
|
||||
>
|
||||
{slackChannel.name}
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<Controller
|
||||
control={control}
|
||||
name="isAccessRequestNotificationEnabled"
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl isError={Boolean(error)} errorText={error?.message} className="mb-2">
|
||||
<Switch
|
||||
id="access-request-notification"
|
||||
onCheckedChange={(value) => field.onChange(value)}
|
||||
isChecked={field.value}
|
||||
>
|
||||
<p className="w-full">Access Requests</p>
|
||||
</Switch>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
{accessRequestNotifState && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="accessRequestChannels"
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Slack channels"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Input
|
||||
isReadOnly
|
||||
value={value
|
||||
?.filter(Boolean)
|
||||
.map((entry) => slackChannelIdToName[entry])
|
||||
.join(", ")}
|
||||
className="text-left"
|
||||
/>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
style={{
|
||||
width: "var(--radix-dropdown-menu-trigger-width)",
|
||||
maxHeight: "350px",
|
||||
overflowY: "auto"
|
||||
}}
|
||||
side="bottom"
|
||||
align="start"
|
||||
>
|
||||
{sortedSlackChannels?.map((slackChannel) => {
|
||||
const isChecked = value?.includes(slackChannel.id);
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
onClick={(evt) => {
|
||||
evt.preventDefault();
|
||||
onChange(
|
||||
isChecked
|
||||
? value?.filter((el: string) => el !== slackChannel.id)
|
||||
: [...(value || []), slackChannel.id]
|
||||
);
|
||||
}}
|
||||
key={`access-requests-slack-channel-${slackChannel.id}`}
|
||||
iconPos="right"
|
||||
icon={isChecked && <FontAwesomeIcon icon={faCheckCircle} />}
|
||||
>
|
||||
{slackChannel.name}
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
className="mt-4"
|
||||
type="submit"
|
||||
isDisabled={!isDirty}
|
||||
isLoading={isSubmitting}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</form>
|
||||
);
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user