import { useEffect, useState } 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 { Accordion, AccordionContent, AccordionItem, AccordionTrigger, Button, Checkbox, FormControl, Input, Modal, ModalContent, Select, SelectItem, Tab, TabList, TabPanel, Tabs } from "@app/components/v2"; import { useProject } from "@app/context"; import { CaType, useCreatePkiSubscriber, useGetAzureAdcsTemplates, useGetPkiSubscriber, useListCasByProjectId, useListWorkspacePkiSubscribers, useUpdatePkiSubscriber } from "@app/hooks/api"; import { EXTENDED_KEY_USAGES_OPTIONS, KEY_USAGES_OPTIONS } from "@app/hooks/api/certificates/constants"; import { CertExtendedKeyUsage, CertKeyUsage } from "@app/hooks/api/certificates/enums"; import { UsePopUpState } from "@app/hooks/usePopUp"; import { convertTimeUnitValueToDays, TimeUnit } from "@app/lib/fn/date"; type Props = { popUp: UsePopUpState<["pkiSubscriber"]>; handlePopUpToggle: (popUpName: keyof UsePopUpState<["pkiSubscriber"]>, state?: boolean) => void; }; enum FormTab { Configuration = "configuration", Advanced = "advanced" } const schema = z .object({ name: z.string().trim().min(1, "Name is required"), caId: z.string().min(1, "Issuing CA is required"), commonName: z.string().trim().min(1, "Common Name is required"), subjectAlternativeNames: z.string(), ttl: z.string().trim().optional(), keyUsages: z.object({ [CertKeyUsage.DIGITAL_SIGNATURE]: z.boolean().optional(), [CertKeyUsage.KEY_ENCIPHERMENT]: z.boolean().optional(), [CertKeyUsage.NON_REPUDIATION]: z.boolean().optional(), [CertKeyUsage.DATA_ENCIPHERMENT]: z.boolean().optional(), [CertKeyUsage.KEY_AGREEMENT]: z.boolean().optional(), [CertKeyUsage.KEY_CERT_SIGN]: z.boolean().optional(), [CertKeyUsage.CRL_SIGN]: z.boolean().optional(), [CertKeyUsage.ENCIPHER_ONLY]: z.boolean().optional(), [CertKeyUsage.DECIPHER_ONLY]: z.boolean().optional() }), extendedKeyUsages: z.object({ [CertExtendedKeyUsage.CLIENT_AUTH]: z.boolean().optional(), [CertExtendedKeyUsage.CODE_SIGNING]: z.boolean().optional(), [CertExtendedKeyUsage.EMAIL_PROTECTION]: z.boolean().optional(), [CertExtendedKeyUsage.OCSP_SIGNING]: z.boolean().optional(), [CertExtendedKeyUsage.SERVER_AUTH]: z.boolean().optional(), [CertExtendedKeyUsage.TIMESTAMPING]: z.boolean().optional() }), enableAutoRenewal: z.boolean().optional().default(false), renewalBefore: z.number().min(1).optional(), renewalUnit: z.nativeEnum(TimeUnit).optional(), // Properties for Azure ADCS only azureTemplateType: z.string().optional(), organization: z .string() .trim() .max(64, "Organization cannot exceed 64 characters") .regex( /^[^,=+<>#;\\"/\r\n\t]*$/, 'Organization contains invalid characters: , = + < > # ; \\ " / \\r \\n \\t' ) .regex( /^[^\s\-_.]+.*[^\s\-_.]+$|^[^\s\-_.]{1}$/, "Organization cannot start or end with spaces, hyphens, underscores, or periods" ) .optional() .or(z.literal("")), organizationalUnit: z .string() .trim() .max(64, "Organizational Unit cannot exceed 64 characters") .regex( /^[^,=+<>#;\\"/\r\n\t]*$/, 'Organizational Unit contains invalid characters: , = + < > # ; \\ " / \\r \\n \\t' ) .regex( /^[^\s\-_.]+.*[^\s\-_.]+$|^[^\s\-_.]{1}$/, "Organizational Unit cannot start or end with spaces, hyphens, underscores, or periods" ) .optional() .or(z.literal("")), country: z .string() .trim() .length(2, "Country must be exactly 2 characters") .regex(/^[A-Z]{2}$/, "Country must be exactly 2 uppercase letters") .optional() .or(z.literal("")), state: z .string() .trim() .max(64, "State cannot exceed 64 characters") .regex( /^[^,=+<>#;\\"/\r\n\t]*$/, 'State contains invalid characters: , = + < > # ; \\ " / \\r \\n \\t' ) .regex( /^[^\s\-_.]+.*[^\s\-_.]+$|^[^\s\-_.]{1}$/, "State cannot start or end with spaces, hyphens, underscores, or periods" ) .optional() .or(z.literal("")), locality: z .string() .trim() .max(64, "Locality cannot exceed 64 characters") .regex( /^[^,=+<>#;\\"/\r\n\t]*$/, 'Locality contains invalid characters: , = + < > # ; \\ " / \\r \\n \\t' ) .regex( /^[^\s\-_.]+.*[^\s\-_.]+$|^[^\s\-_.]{1}$/, "Locality cannot start or end with spaces, hyphens, underscores, or periods" ) .optional() .or(z.literal("")), emailAddress: z .string() .trim() .email("Email Address must be a valid email format") .min(6, "Email Address must be at least 6 characters") .max(64, "Email Address cannot exceed 64 characters") .optional() .or(z.literal("")) }) .required(); export type FormData = z.infer; export const PkiSubscriberModal = ({ popUp, handlePopUpToggle }: Props) => { const { currentProject } = useProject(); const projectId = currentProject.id; const { data: subscribers } = useListWorkspacePkiSubscribers(projectId); const { data: cas } = useListCasByProjectId(projectId); const [tabValue, setTabValue] = useState(FormTab.Configuration); const { data: pkiSubscriber } = useGetPkiSubscriber({ subscriberName: (popUp?.pkiSubscriber?.data as { subscriberName: string })?.subscriberName || "", projectId }); const { mutateAsync: createMutateAsync } = useCreatePkiSubscriber(); const { mutateAsync: updateMutateAsync } = useUpdatePkiSubscriber(); const { control, handleSubmit, reset, setValue, watch, formState: { isSubmitting } } = useForm({ resolver: zodResolver(schema), defaultValues: { name: "", caId: "", commonName: "", subjectAlternativeNames: "", ttl: "", keyUsages: { [CertKeyUsage.DIGITAL_SIGNATURE]: true, [CertKeyUsage.KEY_ENCIPHERMENT]: true }, extendedKeyUsages: {}, enableAutoRenewal: false, renewalBefore: 7, renewalUnit: TimeUnit.DAY, azureTemplateType: "", organization: "", organizationalUnit: "", country: "", state: "", locality: "", emailAddress: "" } }); const selectedCaId = watch("caId"); const selectedCa = cas?.find((ca) => ca.id === selectedCaId); const selectedAutoRenewalState = watch("enableAutoRenewal"); // Fetch Azure ADCS templates when Azure CA is selected const { data: azureTemplates } = useGetAzureAdcsTemplates({ caId: selectedCa?.type === CaType.AZURE_AD_CS ? selectedCaId : "", projectId }); // Initialize form with ALL subscriber data including template useEffect(() => { if (pkiSubscriber) { // Set all values directly and immediately setValue("name", pkiSubscriber.name); setValue("caId", pkiSubscriber.caId || ""); setValue("commonName", pkiSubscriber.commonName); setValue("subjectAlternativeNames", pkiSubscriber.subjectAlternativeNames.join(", ")); setValue("ttl", pkiSubscriber.ttl || ""); setValue( "keyUsages", Object.fromEntries((pkiSubscriber.keyUsages || []).map((name) => [name, true])) ); setValue( "extendedKeyUsages", Object.fromEntries((pkiSubscriber.extendedKeyUsages || []).map((name) => [name, true])) ); setValue("enableAutoRenewal", pkiSubscriber.enableAutoRenewal || false); setValue("renewalBefore", pkiSubscriber.autoRenewalPeriodInDays || 7); setValue("renewalUnit", TimeUnit.DAY); // Set Azure template immediately setValue("azureTemplateType", pkiSubscriber.properties?.azureTemplateType || ""); // Set all Additional Subject Fields immediately setValue("organization", pkiSubscriber.properties?.organization || ""); setValue("organizationalUnit", pkiSubscriber.properties?.organizationalUnit || ""); setValue("country", pkiSubscriber.properties?.country || ""); setValue("state", pkiSubscriber.properties?.state || ""); setValue("locality", pkiSubscriber.properties?.locality || ""); setValue("emailAddress", pkiSubscriber.properties?.emailAddress || ""); } }, [pkiSubscriber, setValue]); useEffect(() => { if (cas?.length && !pkiSubscriber) { // Only auto-select first CA when creating new subscriber, not when updating setValue("caId", cas[0].id); } }, [cas, setValue, pkiSubscriber]); const onFormSubmit = async ({ name, caId, commonName, subjectAlternativeNames, ttl, keyUsages, extendedKeyUsages, enableAutoRenewal, renewalBefore, renewalUnit, azureTemplateType, organization, organizationalUnit, country, state, locality, emailAddress }: FormData) => { try { if (!projectId) return; if (!caId) { createNotification({ text: "Please select an Issuing CA", type: "error" }); return; } // Check if there is already a different subscriber with the same name const existingNames = subscribers?.filter((s) => s.id !== pkiSubscriber?.id).map((s) => s.name) || []; if (existingNames.includes(name.trim())) { createNotification({ text: "A subscriber with this name already exists.", type: "error" }); return; } // Validate Azure template for Azure ADCS CA if (selectedCa?.type === CaType.AZURE_AD_CS && !azureTemplateType) { createNotification({ text: "Please select an Azure certificate template", type: "error" }); return; } const keyUsagesList = selectedCa?.type === CaType.AZURE_AD_CS ? [] : Object.entries(keyUsages) .filter(([, value]) => value) .map(([key]) => key as CertKeyUsage); const extendedKeyUsagesList = selectedCa?.type === CaType.AZURE_AD_CS ? [] : Object.entries(extendedKeyUsages) .filter(([, value]) => value) .map(([key]) => key as CertExtendedKeyUsage); const subjectAlternativeNamesList = subjectAlternativeNames .split(",") .map((san) => san.trim()) .filter(Boolean); const autoRenewalPeriodInDays = enableAutoRenewal ? convertTimeUnitValueToDays(renewalUnit, renewalBefore) : undefined; // Build properties object const properties = { ...(selectedCa?.type === CaType.AZURE_AD_CS && azureTemplateType && { azureTemplateType }), ...(organization && { organization }), ...(organizationalUnit && { organizationalUnit }), ...(country && { country }), ...(state && { state }), ...(locality && { locality }), ...(emailAddress && { emailAddress }) }; if (pkiSubscriber) { await updateMutateAsync({ subscriberName: pkiSubscriber.name, projectId, name, caId, commonName, subjectAlternativeNames: subjectAlternativeNamesList, ttl, keyUsages: keyUsagesList, extendedKeyUsages: extendedKeyUsagesList, enableAutoRenewal, autoRenewalPeriodInDays, properties: Object.keys(properties).length > 0 ? properties : undefined }); } else { await createMutateAsync({ projectId, name, caId, commonName, subjectAlternativeNames: subjectAlternativeNamesList, ttl, keyUsages: keyUsagesList, extendedKeyUsages: extendedKeyUsagesList, enableAutoRenewal, autoRenewalPeriodInDays, properties: Object.keys(properties).length > 0 ? properties : undefined }); } reset(); handlePopUpToggle("pkiSubscriber", false); createNotification({ text: `Successfully ${pkiSubscriber ? "updated" : "added"} PKI subscriber`, type: "success" }); } catch (err) { console.error(err); createNotification({ text: `Failed to ${pkiSubscriber ? "update" : "add"} PKI subscriber`, type: "error" }); } }; return ( { reset(); handlePopUpToggle("pkiSubscriber", isOpen); }} >
setTabValue(value as FormTab)}> Configuration Advanced {pkiSubscriber && ( )} ( )} /> ( )} /> {selectedCa?.type === CaType.AZURE_AD_CS && ( ( )} /> )} ( )} /> ( )} /> {/* Additional Subject Fields - Only for Azure ADCS CAs */} {selectedCa?.type === CaType.AZURE_AD_CS && (
Additional Subject Fields
( )} /> ( )} />
( )} /> ( )} />
( )} /> ( )} />
)} {selectedCa?.type !== CaType.ACME && ( ( )} /> )} {selectedCa?.type !== CaType.ACME && selectedCa?.type !== CaType.AZURE_AD_CS && (
Key Usage
{ return (
{KEY_USAGES_OPTIONS.map(({ label, value: optionValue }) => { return ( { onChange({ ...value, [optionValue]: state }); }} > {label} ); })}
); }} /> { return (
{EXTENDED_KEY_USAGES_OPTIONS.map( ({ label, value: optionValue }) => { return ( { onChange({ ...value, [optionValue]: state }); }} > {label} ); } )}
); }} />
)}
( Enable Certificate Auto Renewal )} /> {selectedAutoRenewalState && (
( onChange(Number(e.target.value))} /> )} /> ( )} />
)}
); };