Files
infisical/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/CaModal.tsx
T

500 lines
15 KiB
TypeScript

import { useEffect } from "react";
import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { format } from "date-fns";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import {
Button,
FormControl,
Input,
Modal,
ModalContent,
Select,
SelectItem,
Switch
// DatePicker
} from "@app/components/v2";
import { useWorkspace } from "@app/context";
import {
CaStatus,
CaType,
InternalCaType,
useCreateCa,
useGetCa,
useUpdateCa
} from "@app/hooks/api/ca";
import { certKeyAlgorithms } from "@app/hooks/api/certificates/constants";
import { CertKeyAlgorithm } from "@app/hooks/api/certificates/enums";
import { UsePopUpState } from "@app/hooks/usePopUp";
import { slugSchema } from "@app/lib/schemas";
const isValidDate = (dateString: string) => {
const date = new Date(dateString);
return !Number.isNaN(date.getTime());
};
const getDateTenYearsFromToday = () => {
const date = new Date();
date.setFullYear(date.getFullYear() + 10);
return format(date, "yyyy-MM-dd");
};
const schema = z
.object({
type: z.nativeEnum(CaType),
name: slugSchema({
field: "Name"
}),
enableDirectIssuance: z.boolean(),
status: z.nativeEnum(CaStatus),
configuration: z
.object({
type: z.enum([InternalCaType.ROOT, InternalCaType.INTERMEDIATE]),
organization: z.string(),
ou: z.string(),
country: z.string(),
province: z.string(),
locality: z.string(),
commonName: z.string(),
notAfter: z.string().trim().refine(isValidDate, { message: "Invalid date format" }),
maxPathLength: z.string(),
keyAlgorithm: z.enum([
CertKeyAlgorithm.RSA_2048,
CertKeyAlgorithm.RSA_4096,
CertKeyAlgorithm.ECDSA_P256,
CertKeyAlgorithm.ECDSA_P384
])
})
.required()
})
.required();
export type FormData = z.infer<typeof schema>;
type Props = {
popUp: UsePopUpState<["ca"]>;
handlePopUpToggle: (popUpName: keyof UsePopUpState<["ca"]>, state?: boolean) => void;
};
const caTypes = [
{ label: "Root", value: InternalCaType.ROOT },
{ label: "Intermediate", value: InternalCaType.INTERMEDIATE }
];
export const CaModal = ({ popUp, handlePopUpToggle }: Props) => {
const { currentWorkspace } = useWorkspace();
const { data: ca } = useGetCa({
caName: (popUp?.ca?.data as { name: string })?.name || "",
projectId: currentWorkspace?.id || "",
type: CaType.INTERNAL
});
const { mutateAsync: createMutateAsync } = useCreateCa();
const { mutateAsync: updateMutateAsync } = useUpdateCa();
const {
control,
handleSubmit,
reset,
formState: { isSubmitting },
watch
} = useForm<FormData>({
resolver: zodResolver(schema),
defaultValues: {
type: CaType.INTERNAL,
name: "",
status: CaStatus.ACTIVE,
enableDirectIssuance: true,
configuration: {
type: InternalCaType.ROOT,
organization: "",
ou: "",
country: "",
province: "",
locality: "",
commonName: "",
notAfter: getDateTenYearsFromToday(),
maxPathLength: "-1",
keyAlgorithm: CertKeyAlgorithm.RSA_2048
}
}
});
const caType = watch("configuration.type");
useEffect(() => {
if (ca && ca.type === CaType.INTERNAL) {
reset({
type: ca.type,
name: ca.name,
status: ca.status,
enableDirectIssuance: ca.enableDirectIssuance,
configuration: {
type: ca.configuration.type,
organization: ca.configuration.organization,
ou: ca.configuration.ou,
country: ca.configuration.country,
province: ca.configuration.province,
locality: ca.configuration.locality,
commonName: ca.configuration.commonName,
notAfter: ca.configuration.notAfter
? format(new Date(ca.configuration.notAfter), "yyyy-MM-dd")
: "",
maxPathLength: ca.configuration.maxPathLength
? String(ca.configuration.maxPathLength)
: "",
keyAlgorithm: ca.configuration.keyAlgorithm
}
});
} else {
reset({
type: CaType.INTERNAL,
name: "",
status: CaStatus.ACTIVE,
enableDirectIssuance: true,
configuration: {
type: InternalCaType.ROOT,
organization: "",
ou: "",
country: "",
province: "",
locality: "",
commonName: "",
notAfter: getDateTenYearsFromToday(),
maxPathLength: "-1",
keyAlgorithm: CertKeyAlgorithm.RSA_2048
}
});
}
}, [ca]);
const onFormSubmit = async ({
type,
name,
enableDirectIssuance,
status,
configuration
}: FormData) => {
try {
if (!currentWorkspace?.slug) return;
if (ca) {
// update
await updateMutateAsync({
caName: ca.name,
projectId: currentWorkspace.id,
name,
type: CaType.INTERNAL,
status,
enableDirectIssuance
});
} else {
// create
await createMutateAsync({
projectId: currentWorkspace.id,
name,
type,
status,
enableDirectIssuance,
configuration: {
...configuration,
maxPathLength: Number(configuration.maxPathLength)
}
});
}
reset();
handlePopUpToggle("ca", false);
createNotification({
text: `Successfully ${ca ? "updated" : "created"} CA`,
type: "success"
});
} catch (err) {
console.error(err);
createNotification({
text: "Failed to create CA",
type: "error"
});
}
};
return (
<Modal
isOpen={popUp?.ca?.isOpen}
onOpenChange={(isOpen) => {
reset();
handlePopUpToggle("ca", isOpen);
}}
>
<ModalContent title={`${ca ? "View" : "Create"} Private CA`}>
<form onSubmit={handleSubmit(onFormSubmit)}>
{ca && (
<FormControl label="CA ID">
<Input value={ca.id} isDisabled className="bg-white/[0.07]" />
</FormControl>
)}
<Controller
control={control}
name="configuration.type"
defaultValue={InternalCaType.ROOT}
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl label="CA Type" errorText={error?.message} isError={Boolean(error)}>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
isDisabled={Boolean(ca)}
>
{caTypes.map(({ label, value }) => (
<SelectItem value={String(value || "")} key={label}>
{label}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
{caType === InternalCaType.ROOT && (
<>
{/* <Controller
name="notAfter"
control={control}
defaultValue={getDefaultNotAfterDate()}
render={({ field: { onChange, ...field }, fieldState: { error } }) => {
return (
<FormControl
label="Validity"
errorText={error?.message}
isError={Boolean(error)}
className="mr-4"
>
<DatePicker
value={field.value || undefined}
onChange={(date) => {
onChange(date);
setIsStartDatePickerOpen(false);
}}
popUpProps={{
open: isStartDatePickerOpen,
onOpenChange: setIsStartDatePickerOpen
}}
popUpContentProps={{}}
/>
</FormControl>
);
}}
/> */}
<Controller
control={control}
defaultValue=""
name="configuration.notAfter"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Valid Until"
isError={Boolean(error)}
errorText={error?.message}
isRequired
>
<Input {...field} placeholder="YYYY-MM-DD" isDisabled={Boolean(ca)} />
</FormControl>
)}
/>
<Controller
control={control}
name="configuration.maxPathLength"
defaultValue="-1"
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Path Length"
errorText={error?.message}
isError={Boolean(error)}
className="mt-4"
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
isDisabled={Boolean(ca)}
>
{[-1, 0, 1, 2, 3, 4].map((value) => (
<SelectItem value={String(value)} key={`ca-path-length-${value}`}>
{`${value}`}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
</>
)}
<Controller
control={control}
name="configuration.keyAlgorithm"
defaultValue={CertKeyAlgorithm.RSA_2048}
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Key Algorithm"
errorText={error?.message}
isError={Boolean(error)}
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
isDisabled={Boolean(ca)}
>
{certKeyAlgorithms.map(({ label, value }) => (
<SelectItem value={String(value || "")} key={label}>
{label}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
<Controller
control={control}
defaultValue=""
name="name"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Name"
isError={Boolean(error)}
errorText={error?.message}
isRequired
>
<Input {...field} placeholder="my-internal-ca" isDisabled={Boolean(ca)} />
</FormControl>
)}
/>
<Controller
control={control}
defaultValue=""
name="configuration.organization"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Organization (O)"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="Acme Corp" isDisabled={Boolean(ca)} />
</FormControl>
)}
/>
<Controller
control={control}
defaultValue=""
name="configuration.ou"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Organization Unit (OU)"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="Engineering" isDisabled={Boolean(ca)} />
</FormControl>
)}
/>
<Controller
control={control}
defaultValue=""
name="configuration.country"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Country Name (C)"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="United States (US)" isDisabled={Boolean(ca)} />
</FormControl>
)}
/>
<Controller
control={control}
defaultValue=""
name="configuration.province"
render={({ field, fieldState: { error } }) => (
<FormControl
label="State or Province Name"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="California" isDisabled={Boolean(ca)} />
</FormControl>
)}
/>
<Controller
control={control}
defaultValue=""
name="configuration.locality"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Locality Name"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="San Francisco" isDisabled={Boolean(ca)} />
</FormControl>
)}
/>
<Controller
control={control}
defaultValue=""
name="configuration.commonName"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Common Name (CN)"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="Example CA" isDisabled={Boolean(ca)} />
</FormControl>
)}
/>
<Controller
control={control}
name="enableDirectIssuance"
render={({ field, fieldState: { error } }) => {
return (
<FormControl isError={Boolean(error)} errorText={error?.message} className="my-8">
<Switch
id="enable-direct-issuance"
onCheckedChange={(value) => field.onChange(value)}
isChecked={field.value}
>
<p className="w-full">Enable Direct Issuance</p>
</Switch>
</FormControl>
);
}}
/>
<div className="flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
{popUp?.ca?.data ? "Update" : "Create"}
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpToggle("ca", false)}
>
Cancel
</Button>
</div>
</form>
</ModalContent>
</Modal>
);
};