mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-21 16:28:12 +00:00
Add Certificate Syncs
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
import { Controller, useFormContext } from "react-hook-form";
|
||||
|
||||
import { FormControl, Input } from "@app/components/v2";
|
||||
import { PkiSync } from "@app/hooks/api/pkiSyncs";
|
||||
|
||||
import { PkiSyncConnectionField } from "./PkiSyncConnectionField";
|
||||
import { TPkiSyncForm } from "./schemas";
|
||||
|
||||
export const AzureKeyVaultPkiSyncFields = () => {
|
||||
const { control, setValue } = useFormContext<
|
||||
TPkiSyncForm & { destination: PkiSync.AzureKeyVault }
|
||||
>();
|
||||
|
||||
return (
|
||||
<>
|
||||
<PkiSyncConnectionField
|
||||
onChange={() => {
|
||||
setValue("destinationConfig.vaultBaseUrl", "");
|
||||
}}
|
||||
/>
|
||||
<Controller
|
||||
name="destinationConfig.vaultBaseUrl"
|
||||
control={control}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
label="Vault Base URL"
|
||||
tooltipText="Enter your Azure Key Vault URL. This is the base URL for your Azure Key Vault, e.g. https://example.vault.azure.net."
|
||||
>
|
||||
<Input {...field} placeholder="https://example.vault.azure.net" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,243 @@
|
||||
import { useState } from "react";
|
||||
import { Controller, FormProvider, useForm } from "react-hook-form";
|
||||
import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Tab } from "@headlessui/react";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Button, FormControl, Switch } from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { PKI_SYNC_MAP } from "@app/helpers/pkiSyncs";
|
||||
import { PkiSync, TPkiSync, useCreatePkiSync, usePkiSyncOption } from "@app/hooks/api/pkiSyncs";
|
||||
|
||||
import { PkiSyncDestinationFields } from "./PkiSyncDestinationFields";
|
||||
import { PkiSyncDetailsFields } from "./PkiSyncDetailsFields";
|
||||
import { PkiSyncOptionsFields } from "./PkiSyncOptionsFields";
|
||||
import { PkiSyncReviewFields } from "./PkiSyncReviewFields";
|
||||
import { PkiSyncSourceFields } from "./PkiSyncSourceFields";
|
||||
import { PkiSyncFormSchema, TPkiSyncForm } from "./schemas";
|
||||
|
||||
type Props = {
|
||||
onComplete: (pkiSync: TPkiSync) => void;
|
||||
destination: PkiSync;
|
||||
onCancel: () => void;
|
||||
};
|
||||
|
||||
const FORM_TABS: { name: string; key: string; fields: (keyof TPkiSyncForm)[] }[] = [
|
||||
{ name: "Source", key: "source", fields: ["subscriberId"] },
|
||||
{ name: "Destination", key: "destination", fields: ["connection", "destinationConfig"] },
|
||||
{ name: "Sync Options", key: "options", fields: ["syncOptions"] },
|
||||
{ name: "Details", key: "details", fields: ["name", "description"] },
|
||||
{ name: "Review", key: "review", fields: [] }
|
||||
];
|
||||
|
||||
export const CreatePkiSyncForm = ({ destination, onComplete, onCancel }: Props) => {
|
||||
const createPkiSync = useCreatePkiSync();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { name: destinationName } = PKI_SYNC_MAP[destination];
|
||||
|
||||
const [showConfirmation, setShowConfirmation] = useState(false);
|
||||
|
||||
const [selectedTabIndex, setSelectedTabIndex] = useState(0);
|
||||
|
||||
const { syncOption } = usePkiSyncOption(destination);
|
||||
|
||||
const formMethods = useForm<TPkiSyncForm>({
|
||||
resolver: zodResolver(PkiSyncFormSchema),
|
||||
defaultValues: {
|
||||
destination,
|
||||
isAutoSyncEnabled: true,
|
||||
syncOptions: {
|
||||
canImportCertificates: syncOption?.canImportCertificates ?? true,
|
||||
canRemoveCertificates: syncOption?.canRemoveCertificates ?? true
|
||||
}
|
||||
} as Partial<TPkiSyncForm>,
|
||||
reValidateMode: "onChange"
|
||||
});
|
||||
|
||||
const onSubmit = async ({ connection, ...formData }: TPkiSyncForm) => {
|
||||
try {
|
||||
const pkiSync = await createPkiSync.mutateAsync({
|
||||
...formData,
|
||||
connectionId: connection.id,
|
||||
projectId: currentWorkspace.id
|
||||
});
|
||||
|
||||
createNotification({
|
||||
text: `Successfully added ${destinationName} Certificate Sync`,
|
||||
type: "success"
|
||||
});
|
||||
onComplete(pkiSync);
|
||||
} catch (err: any) {
|
||||
console.error(err);
|
||||
setShowConfirmation(false);
|
||||
createNotification({
|
||||
title: `Failed to add ${destinationName} Certificate Sync`,
|
||||
text: err.message,
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handlePrev = () => {
|
||||
if (selectedTabIndex === 0) {
|
||||
onCancel();
|
||||
return;
|
||||
}
|
||||
|
||||
setSelectedTabIndex((prev) => prev - 1);
|
||||
};
|
||||
|
||||
const { handleSubmit, trigger, control } = formMethods;
|
||||
|
||||
const isStepValid = async (index: number) => trigger(FORM_TABS[index].fields);
|
||||
|
||||
const isFinalStep = selectedTabIndex === FORM_TABS.length - 1;
|
||||
|
||||
const handleNext = async () => {
|
||||
if (isFinalStep) {
|
||||
setShowConfirmation(true);
|
||||
return;
|
||||
}
|
||||
|
||||
const isValid = await isStepValid(selectedTabIndex);
|
||||
|
||||
if (!isValid) return;
|
||||
|
||||
setSelectedTabIndex((prev) => prev + 1);
|
||||
};
|
||||
|
||||
const isTabEnabled = async (index: number) => {
|
||||
let isEnabled = true;
|
||||
for (let i = index - 1; i >= 0; i -= 1) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
isEnabled = isEnabled && (await isStepValid(i));
|
||||
}
|
||||
|
||||
return isEnabled;
|
||||
};
|
||||
|
||||
if (showConfirmation)
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col rounded-sm border border-l-[2px] border-mineshaft-600 border-l-primary bg-mineshaft-700/80 px-4 py-3">
|
||||
<div className="mb-1 flex items-center text-sm">
|
||||
<FontAwesomeIcon icon={faInfoCircle} size="sm" className="mr-1.5 text-primary" />
|
||||
Certificate Sync Behavior
|
||||
</div>
|
||||
<p className="mt-1 text-sm text-bunker-200">
|
||||
Certificate Syncs are the source of truth for connected third-party services. Any
|
||||
certificate, including associated data, not present or imported in Infisical before
|
||||
syncing will be overwritten, and changes made directly in the connected service outside
|
||||
of infisical may also be overwritten by future syncs.
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-4 flex gap-4">
|
||||
<Button
|
||||
isDisabled={createPkiSync.isPending}
|
||||
isLoading={createPkiSync.isPending}
|
||||
onClick={handleSubmit(onSubmit)}
|
||||
colorSchema="secondary"
|
||||
>
|
||||
I Understand
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
isDisabled={createPkiSync.isPending}
|
||||
variant="plain"
|
||||
onClick={() => setShowConfirmation(false)}
|
||||
colorSchema="secondary"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<form className={twMerge(isFinalStep && "max-h-[70vh] overflow-y-auto")}>
|
||||
<FormProvider {...formMethods}>
|
||||
<Tab.Group selectedIndex={selectedTabIndex} onChange={setSelectedTabIndex}>
|
||||
<Tab.List className="-pb-1 mb-6 w-full border-b-2 border-mineshaft-600">
|
||||
{FORM_TABS.map((tab, index) => (
|
||||
<Tab
|
||||
onClick={async (e) => {
|
||||
e.preventDefault();
|
||||
const isEnabled = await isTabEnabled(index);
|
||||
setSelectedTabIndex((prev) => (isEnabled ? index : prev));
|
||||
}}
|
||||
className={({ selected }) =>
|
||||
`w-30 -mb-[0.14rem] ${index > selectedTabIndex ? "opacity-30" : ""} px-4 py-2 text-sm font-medium outline-none disabled:opacity-60 ${
|
||||
selected
|
||||
? "border-b-2 border-mineshaft-300 text-mineshaft-200"
|
||||
: "text-bunker-300"
|
||||
}`
|
||||
}
|
||||
key={tab.key}
|
||||
>
|
||||
{index + 1}. {tab.name}
|
||||
</Tab>
|
||||
))}
|
||||
</Tab.List>
|
||||
<Tab.Panels>
|
||||
<Tab.Panel>
|
||||
<PkiSyncSourceFields />
|
||||
</Tab.Panel>
|
||||
<Tab.Panel>
|
||||
<PkiSyncDestinationFields />
|
||||
</Tab.Panel>
|
||||
<Tab.Panel>
|
||||
<PkiSyncOptionsFields />
|
||||
<Controller
|
||||
control={control}
|
||||
name="isAutoSyncEnabled"
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl
|
||||
helperText={
|
||||
value
|
||||
? "Certificates will automatically be synced when changes occur in the source subscriber."
|
||||
: "Certificates will not automatically be synced when changes occur in the source subscriber. You can still trigger syncs manually."
|
||||
}
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Switch
|
||||
className="bg-mineshaft-400/80 shadow-inner data-[state=checked]:bg-green/80"
|
||||
id="auto-sync-enabled"
|
||||
thumbClassName="bg-mineshaft-800"
|
||||
onCheckedChange={onChange}
|
||||
isChecked={value}
|
||||
>
|
||||
<p className="w-[8.4rem]">Auto-Sync {value ? "Enabled" : "Disabled"}</p>
|
||||
</Switch>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Tab.Panel>
|
||||
<Tab.Panel>
|
||||
<PkiSyncDetailsFields />
|
||||
</Tab.Panel>
|
||||
<Tab.Panel>
|
||||
<PkiSyncReviewFields />
|
||||
</Tab.Panel>
|
||||
</Tab.Panels>
|
||||
</Tab.Group>
|
||||
</FormProvider>
|
||||
|
||||
<div className="flex w-full flex-row-reverse justify-between gap-4 pt-4">
|
||||
<Button onClick={handleNext} colorSchema="secondary">
|
||||
{isFinalStep ? "Create Sync" : "Next"}
|
||||
</Button>
|
||||
{selectedTabIndex > 0 && (
|
||||
<Button onClick={handlePrev} colorSchema="secondary">
|
||||
Back
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,107 @@
|
||||
import { ReactNode } from "react";
|
||||
import { FormProvider, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { PkiSyncEditFields } from "@app/components/pki-syncs/types";
|
||||
import { Button, ModalClose } from "@app/components/v2";
|
||||
import { PKI_SYNC_MAP } from "@app/helpers/pkiSyncs";
|
||||
import { TPkiSync, useUpdatePkiSync } from "@app/hooks/api/pkiSyncs";
|
||||
|
||||
import { PkiSyncDestinationFields } from "./PkiSyncDestinationFields";
|
||||
import { PkiSyncDetailsFields } from "./PkiSyncDetailsFields";
|
||||
import { PkiSyncOptionsFields } from "./PkiSyncOptionsFields";
|
||||
import { PkiSyncSourceFields } from "./PkiSyncSourceFields";
|
||||
import { TPkiSyncForm, UpdatePkiSyncFormSchema } from "./schemas";
|
||||
|
||||
type Props = {
|
||||
onComplete: (pkiSync: TPkiSync) => void;
|
||||
pkiSync: TPkiSync;
|
||||
fields: PkiSyncEditFields;
|
||||
};
|
||||
|
||||
export const EditPkiSyncForm = ({ pkiSync, fields, onComplete }: Props) => {
|
||||
const updatePkiSync = useUpdatePkiSync();
|
||||
const { name: destinationName } = PKI_SYNC_MAP[pkiSync.destination];
|
||||
|
||||
const formMethods = useForm<TPkiSyncForm>({
|
||||
resolver: zodResolver(UpdatePkiSyncFormSchema),
|
||||
defaultValues: {
|
||||
...pkiSync,
|
||||
description: pkiSync.description ?? "",
|
||||
connection: {
|
||||
id: pkiSync.connectionId,
|
||||
name: pkiSync.appConnectionName
|
||||
}
|
||||
} as Partial<TPkiSyncForm>,
|
||||
reValidateMode: "onChange"
|
||||
});
|
||||
|
||||
const onSubmit = async ({ connection, ...formData }: TPkiSyncForm) => {
|
||||
try {
|
||||
const updatedPkiSync = await updatePkiSync.mutateAsync({
|
||||
syncId: pkiSync.id,
|
||||
...formData,
|
||||
connectionId: connection.id,
|
||||
projectId: pkiSync.projectId
|
||||
});
|
||||
|
||||
createNotification({
|
||||
text: `Successfully updated ${destinationName} PKI Sync`,
|
||||
type: "success"
|
||||
});
|
||||
onComplete(updatedPkiSync);
|
||||
} catch (err: any) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
title: `Failed to update ${destinationName} PKI Sync`,
|
||||
text: err.message,
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
let Component: ReactNode;
|
||||
|
||||
switch (fields) {
|
||||
case PkiSyncEditFields.Destination:
|
||||
Component = <PkiSyncDestinationFields />;
|
||||
break;
|
||||
case PkiSyncEditFields.Options:
|
||||
Component = <PkiSyncOptionsFields />;
|
||||
break;
|
||||
case PkiSyncEditFields.Source:
|
||||
Component = <PkiSyncSourceFields />;
|
||||
break;
|
||||
case PkiSyncEditFields.Details:
|
||||
default:
|
||||
Component = <PkiSyncDetailsFields />;
|
||||
break;
|
||||
}
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
formState: { isSubmitting, isDirty }
|
||||
} = formMethods;
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<FormProvider {...formMethods}>{Component}</FormProvider>
|
||||
<div className="flex w-full justify-between gap-4 pt-4">
|
||||
<ModalClose asChild>
|
||||
<Button colorSchema="secondary" variant="plain">
|
||||
Cancel
|
||||
</Button>
|
||||
</ModalClose>
|
||||
<Button
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={!isDirty || isSubmitting}
|
||||
type="submit"
|
||||
colorSchema="secondary"
|
||||
>
|
||||
Update PKI Sync
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,86 @@
|
||||
import { Controller, useFormContext } from "react-hook-form";
|
||||
import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Link } from "@tanstack/react-router";
|
||||
|
||||
import { FilterableSelect, FormControl } from "@app/components/v2";
|
||||
import { OrgPermissionSubjects, useOrgPermission } from "@app/context";
|
||||
import { OrgPermissionAppConnectionActions } from "@app/context/OrgPermissionContext/types";
|
||||
import { APP_CONNECTION_MAP } from "@app/helpers/appConnections";
|
||||
import { PKI_SYNC_CONNECTION_MAP } from "@app/helpers/pkiSyncs";
|
||||
import { useListAvailableAppConnections } from "@app/hooks/api/appConnections";
|
||||
|
||||
import { TPkiSyncForm } from "./schemas";
|
||||
|
||||
type Props = {
|
||||
onChange?: VoidFunction;
|
||||
};
|
||||
|
||||
export const PkiSyncConnectionField = ({ onChange: callback }: Props) => {
|
||||
const { permission } = useOrgPermission();
|
||||
const { control, watch } = useFormContext<TPkiSyncForm>();
|
||||
|
||||
const destination = watch("destination");
|
||||
const app = PKI_SYNC_CONNECTION_MAP[destination];
|
||||
|
||||
const { data: availableConnections, isPending } = useListAvailableAppConnections(app);
|
||||
|
||||
const connectionName = APP_CONNECTION_MAP[app].name;
|
||||
|
||||
const canCreateConnection = permission.can(
|
||||
OrgPermissionAppConnectionActions.Create,
|
||||
OrgPermissionSubjects.AppConnections
|
||||
);
|
||||
|
||||
const appName = APP_CONNECTION_MAP[PKI_SYNC_CONNECTION_MAP[destination]].name;
|
||||
|
||||
return (
|
||||
<>
|
||||
<p className="mb-4 text-sm text-bunker-300">
|
||||
Specify the {appName} Connection to use to connect to {connectionName} and configure
|
||||
destination parameters.
|
||||
</p>
|
||||
<Controller
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
tooltipText="App Connections can be created from the Organization Settings page."
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
label={`${connectionName} Connection`}
|
||||
>
|
||||
<FilterableSelect
|
||||
value={value}
|
||||
onChange={(newValue) => {
|
||||
onChange(newValue);
|
||||
if (callback) callback();
|
||||
}}
|
||||
isLoading={isPending}
|
||||
options={availableConnections}
|
||||
placeholder="Select connection..."
|
||||
getOptionLabel={(option) => option.name}
|
||||
getOptionValue={(option) => option.id}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
control={control}
|
||||
name="connection"
|
||||
/>
|
||||
{availableConnections?.length === 0 && (
|
||||
<p className="-mt-2.5 mb-2.5 text-xs text-yellow">
|
||||
<FontAwesomeIcon className="mr-1" size="xs" icon={faInfoCircle} />
|
||||
{canCreateConnection ? (
|
||||
<>
|
||||
You do not have access to any {appName} Connections. Create one from the{" "}
|
||||
<Link to="/organization/app-connections" className="underline">
|
||||
App Connections
|
||||
</Link>{" "}
|
||||
page.
|
||||
</>
|
||||
) : (
|
||||
`You do not have access to any ${appName} Connections. Contact an admin to create one.`
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
import { useFormContext } from "react-hook-form";
|
||||
|
||||
import { PkiSync } from "@app/hooks/api/pkiSyncs";
|
||||
|
||||
import { AzureKeyVaultPkiSyncFields } from "./AzureKeyVaultPkiSyncFields";
|
||||
import { TPkiSyncForm } from "./schemas";
|
||||
|
||||
export const PkiSyncDestinationFields = () => {
|
||||
const { watch } = useFormContext<TPkiSyncForm>();
|
||||
|
||||
const destination = watch("destination");
|
||||
|
||||
switch (destination) {
|
||||
case PkiSync.AzureKeyVault:
|
||||
return <AzureKeyVaultPkiSyncFields />;
|
||||
default:
|
||||
throw new Error(`Unhandled Destination Config Field: ${destination}`);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,51 @@
|
||||
import { Controller, useFormContext } from "react-hook-form";
|
||||
|
||||
import { FormControl, Input, TextArea } from "@app/components/v2";
|
||||
|
||||
import { TPkiSyncForm } from "./schemas";
|
||||
|
||||
export const PkiSyncDetailsFields = () => {
|
||||
const { control } = useFormContext<TPkiSyncForm>();
|
||||
|
||||
return (
|
||||
<>
|
||||
<p className="mb-4 text-sm text-bunker-300">
|
||||
Provide a name and description for this Certificate Sync.
|
||||
</p>
|
||||
<Controller
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
helperText="Must be slug-friendly"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
label="Name"
|
||||
>
|
||||
<Input value={value} onChange={onChange} placeholder="my-certificate-sync" />
|
||||
</FormControl>
|
||||
)}
|
||||
control={control}
|
||||
name="name"
|
||||
/>
|
||||
<Controller
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error)}
|
||||
isOptional
|
||||
errorText={error?.message}
|
||||
label="Description"
|
||||
>
|
||||
<TextArea
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder="Describe the purpose of this sync..."
|
||||
className="!resize-none"
|
||||
rows={4}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
control={control}
|
||||
name="description"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,89 @@
|
||||
import { Controller, useFormContext } from "react-hook-form";
|
||||
import { faQuestionCircle } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { FormControl, Switch, Tooltip } from "@app/components/v2";
|
||||
|
||||
import { TPkiSyncForm } from "../schemas";
|
||||
|
||||
export const PkiSyncOptionsFields = () => {
|
||||
const { control } = useFormContext<TPkiSyncForm>();
|
||||
|
||||
return (
|
||||
<>
|
||||
<p className="mb-4 text-sm text-bunker-300">Configure how certificates should be synced.</p>
|
||||
<Controller
|
||||
control={control}
|
||||
name="syncOptions.canImportCertificates"
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl isError={Boolean(error)} errorText={error?.message}>
|
||||
<Switch
|
||||
className="bg-mineshaft-400/80 shadow-inner data-[state=checked]:bg-green/80"
|
||||
id="can-import-certificates"
|
||||
thumbClassName="bg-mineshaft-800"
|
||||
onCheckedChange={onChange}
|
||||
isChecked={value}
|
||||
>
|
||||
<p>
|
||||
Auto Import Certificates{" "}
|
||||
<Tooltip
|
||||
className="max-w-md"
|
||||
content={
|
||||
<>
|
||||
<p>
|
||||
When enabled, Infisical will automatically import certificates from the PKI
|
||||
subscriber to the destination during sync operations.
|
||||
</p>
|
||||
<p className="mt-4">
|
||||
This allows you to automatically populate your destination with certificates
|
||||
issued by your Certificate Authority.
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<FontAwesomeIcon icon={faQuestionCircle} size="sm" className="ml-1" />
|
||||
</Tooltip>
|
||||
</p>
|
||||
</Switch>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="syncOptions.canRemoveCertificates"
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl isError={Boolean(error)} errorText={error?.message}>
|
||||
<Switch
|
||||
className="bg-mineshaft-400/80 shadow-inner data-[state=checked]:bg-green/80"
|
||||
id="can-remove-certificates"
|
||||
thumbClassName="bg-mineshaft-800"
|
||||
onCheckedChange={onChange}
|
||||
isChecked={value}
|
||||
>
|
||||
<p>
|
||||
Disable Certificate Removal{" "}
|
||||
<Tooltip
|
||||
className="max-w-md"
|
||||
content={
|
||||
<>
|
||||
<p>
|
||||
When enabled, Infisical will <span className="font-semibold">not</span>{" "}
|
||||
remove certificates from the destination during a sync.
|
||||
</p>
|
||||
<p className="mt-4">
|
||||
Enable this option if you intend to manage some certificates manually
|
||||
outside of Infisical.
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<FontAwesomeIcon icon={faQuestionCircle} size="sm" className="ml-1" />
|
||||
</Tooltip>
|
||||
</p>
|
||||
</Switch>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { PkiSyncOptionsFields } from "./PkiSyncOptionsFields";
|
||||
@@ -0,0 +1,100 @@
|
||||
import { useFormContext } from "react-hook-form";
|
||||
|
||||
import { Badge, GenericFieldLabel } from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { PKI_SYNC_MAP } from "@app/helpers/pkiSyncs";
|
||||
import { useListWorkspacePkiSubscribers } from "@app/hooks/api/workspace";
|
||||
|
||||
import { TPkiSyncForm } from "./schemas";
|
||||
|
||||
export const PkiSyncReviewFields = () => {
|
||||
const { watch } = useFormContext<TPkiSyncForm>();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
|
||||
const { data: pkiSubscribers = [] } = useListWorkspacePkiSubscribers(currentWorkspace?.id || "");
|
||||
|
||||
const getSubscriberName = (subscriberId?: string) => {
|
||||
const subscriber = pkiSubscribers.find((sub) => sub.id === subscriberId);
|
||||
return subscriber?.name || "Unknown";
|
||||
};
|
||||
|
||||
const {
|
||||
name,
|
||||
description,
|
||||
connection,
|
||||
subscriberId,
|
||||
syncOptions,
|
||||
destination,
|
||||
destinationConfig,
|
||||
isAutoSyncEnabled
|
||||
} = watch();
|
||||
|
||||
const destinationName = PKI_SYNC_MAP[destination].name;
|
||||
|
||||
return (
|
||||
<div className="mb-4 flex flex-col gap-6">
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="w-full border-b border-mineshaft-600">
|
||||
<span className="text-sm text-mineshaft-300">Source</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-x-8 gap-y-2">
|
||||
<GenericFieldLabel label="PKI Subscriber">
|
||||
{getSubscriberName(subscriberId)}
|
||||
</GenericFieldLabel>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="w-full border-b border-mineshaft-600">
|
||||
<span className="text-sm text-mineshaft-300">Destination</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-x-8 gap-y-2">
|
||||
<GenericFieldLabel label="Connection">{connection?.name}</GenericFieldLabel>
|
||||
<GenericFieldLabel label="Service">{destinationName}</GenericFieldLabel>
|
||||
{destinationConfig && "vaultBaseUrl" in destinationConfig && (
|
||||
<GenericFieldLabel label="Vault URL">
|
||||
{destinationConfig.vaultBaseUrl}
|
||||
</GenericFieldLabel>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="w-full border-b border-mineshaft-600">
|
||||
<span className="text-sm text-mineshaft-300">Sync Options</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-x-8 gap-y-2">
|
||||
<GenericFieldLabel label="Auto-Sync">
|
||||
<Badge variant={isAutoSyncEnabled ? "success" : "danger"}>
|
||||
{isAutoSyncEnabled ? "Enabled" : "Disabled"}
|
||||
</Badge>
|
||||
</GenericFieldLabel>
|
||||
<GenericFieldLabel label="Upload Certificates">
|
||||
<Badge variant="success">Always Enabled</Badge>
|
||||
</GenericFieldLabel>
|
||||
{syncOptions?.canImportCertificates !== undefined && (
|
||||
<GenericFieldLabel label="Import Certificates">
|
||||
<Badge variant={syncOptions.canImportCertificates ? "success" : "danger"}>
|
||||
{syncOptions.canImportCertificates ? "Enabled" : "Disabled"}
|
||||
</Badge>
|
||||
</GenericFieldLabel>
|
||||
)}
|
||||
{syncOptions?.canRemoveCertificates !== undefined && (
|
||||
<GenericFieldLabel label="Remove Certificates">
|
||||
<Badge variant={syncOptions.canRemoveCertificates ? "success" : "danger"}>
|
||||
{syncOptions.canRemoveCertificates ? "Enabled" : "Disabled"}
|
||||
</Badge>
|
||||
</GenericFieldLabel>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="w-full border-b border-mineshaft-600">
|
||||
<span className="text-sm text-mineshaft-300">Details</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-x-8 gap-y-2">
|
||||
<GenericFieldLabel label="Name">{name}</GenericFieldLabel>
|
||||
<GenericFieldLabel label="Description">{description}</GenericFieldLabel>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,53 @@
|
||||
import { Controller, useFormContext } from "react-hook-form";
|
||||
|
||||
import { FilterableSelect, FormControl } from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { useListWorkspacePkiSubscribers } from "@app/hooks/api/workspace";
|
||||
|
||||
import { TPkiSyncForm } from "./schemas";
|
||||
|
||||
export const PkiSyncSourceFields = () => {
|
||||
const { control } = useFormContext<TPkiSyncForm>();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
|
||||
const { data: pkiSubscribers = [], isLoading } = useListWorkspacePkiSubscribers(
|
||||
currentWorkspace?.id || ""
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<p className="mb-4 text-sm text-bunker-300">
|
||||
Specify the PKI subscriber where you would like to sync certificates from.
|
||||
</p>
|
||||
|
||||
<Controller
|
||||
control={control}
|
||||
name="subscriberId"
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl label="PKI Subscriber" isError={Boolean(error)} errorText={error?.message}>
|
||||
<FilterableSelect
|
||||
value={pkiSubscribers.find((sub) => sub.id === value)}
|
||||
onChange={(selectedSubscriber) => {
|
||||
if (Array.isArray(selectedSubscriber)) return;
|
||||
if (
|
||||
selectedSubscriber &&
|
||||
!Array.isArray(selectedSubscriber) &&
|
||||
"id" in selectedSubscriber
|
||||
) {
|
||||
onChange(selectedSubscriber.id);
|
||||
} else {
|
||||
onChange(undefined);
|
||||
}
|
||||
}}
|
||||
options={pkiSubscribers}
|
||||
isLoading={isLoading}
|
||||
placeholder="Select PKI subscriber..."
|
||||
getOptionLabel={(option) => option?.name || ""}
|
||||
getOptionValue={(option) => option?.id || ""}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
export { CreatePkiSyncForm } from "./CreatePkiSyncForm";
|
||||
export { EditPkiSyncForm } from "./EditPkiSyncForm";
|
||||
export { PkiSyncDestinationFields } from "./PkiSyncDestinationFields";
|
||||
export { PkiSyncDetailsFields } from "./PkiSyncDetailsFields";
|
||||
export { PkiSyncOptionsFields } from "./PkiSyncOptionsFields/PkiSyncOptionsFields";
|
||||
export { PkiSyncReviewFields } from "./PkiSyncReviewFields";
|
||||
export { PkiSyncSourceFields } from "./PkiSyncSourceFields";
|
||||
export { PkiSyncFormSchema, type TPkiSyncForm } from "./schemas";
|
||||
@@ -0,0 +1,35 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { PkiSync } from "@app/hooks/api/pkiSyncs";
|
||||
|
||||
export const PkiSyncFormSchema = z.object({
|
||||
name: z.string().trim().min(1, "Name is required"),
|
||||
description: z.string().optional(),
|
||||
destination: z.nativeEnum(PkiSync),
|
||||
isAutoSyncEnabled: z.boolean().default(true),
|
||||
subscriberId: z.string().min(1, "PKI Subscriber is required"),
|
||||
connection: z.object({
|
||||
id: z.string(),
|
||||
name: z.string()
|
||||
}),
|
||||
destinationConfig: z.object({
|
||||
vaultBaseUrl: z.string().url("Valid URL is required")
|
||||
}),
|
||||
syncOptions: z.object({
|
||||
canImportCertificates: z.boolean().default(true),
|
||||
canRemoveCertificates: z.boolean().default(true)
|
||||
})
|
||||
});
|
||||
|
||||
export type TPkiSyncForm = z.infer<typeof PkiSyncFormSchema>;
|
||||
|
||||
export const UpdatePkiSyncFormSchema = PkiSyncFormSchema.partial().merge(
|
||||
z.object({
|
||||
name: z.string().trim().min(1, "Name is required"),
|
||||
destination: z.nativeEnum(PkiSync),
|
||||
connection: z.object({
|
||||
id: z.string(),
|
||||
name: z.string()
|
||||
})
|
||||
})
|
||||
);
|
||||
Reference in New Issue
Block a user