Merge remote-tracking branch 'origin' into pki-v3-docs

This commit is contained in:
Tuan Dang
2025-11-06 16:56:40 -08:00
9 changed files with 102 additions and 43 deletions

View File

@@ -159,8 +159,8 @@ export type TCreateCertificateDTO = {
ttl: string; // string compatible with ms
notBefore?: string;
notAfter?: string;
keyUsages: CertKeyUsage[];
extendedKeyUsages: CertExtendedKeyUsage[];
keyUsages: string[];
extendedKeyUsages: string[];
};
export type TCreateCertificateResponse = {

View File

@@ -90,7 +90,12 @@ export const useCreateCertTemplateV2 = () => {
return data.certificateTemplate;
},
onSuccess: (_, { projectId }) => {
queryClient.invalidateQueries({ queryKey: certTemplateKeys.listTemplates({ projectId }) });
queryClient.invalidateQueries({
predicate: (query) => {
const [firstKey, queryProjectId] = query.queryKey;
return firstKey === "list-template" && queryProjectId === projectId;
}
});
}
});
};
@@ -107,7 +112,12 @@ export const useUpdateCertTemplateV2 = () => {
return data.certificateTemplate;
},
onSuccess: (_, { projectId }) => {
queryClient.invalidateQueries({ queryKey: certTemplateKeys.listTemplates({ projectId }) });
queryClient.invalidateQueries({
predicate: (query) => {
const [firstKey, queryProjectId] = query.queryKey;
return firstKey === "list-template" && queryProjectId === projectId;
}
});
}
});
};
@@ -127,7 +137,12 @@ export const useDeleteCertTemplateV2 = () => {
return data.certificateTemplate;
},
onSuccess: (_, { projectId }) => {
queryClient.invalidateQueries({ queryKey: certTemplateKeys.listTemplates({ projectId }) });
queryClient.invalidateQueries({
predicate: (query) => {
const [firstKey, queryProjectId] = query.queryKey;
return firstKey === "list-template" && queryProjectId === projectId;
}
});
}
});
};

View File

@@ -94,9 +94,9 @@ const createSchema = (shouldShowSubjectSection: boolean) => {
export type FormData = z.infer<ReturnType<typeof createSchema>>;
type Props = {
popUp: UsePopUpState<["certificateIssuance"]>;
popUp: UsePopUpState<["issueCertificate"]>;
handlePopUpToggle: (
popUpName: keyof UsePopUpState<["certificateIssuance"]>,
popUpName: keyof UsePopUpState<["issueCertificate"]>,
state?: boolean
) => void;
profileId?: string;
@@ -115,7 +115,7 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
const { currentProject } = useProject();
const inputSerialNumber =
(popUp?.certificateIssuance?.data as { serialNumber: string })?.serialNumber || "";
(popUp?.issueCertificate?.data as { serialNumber: string })?.serialNumber || "";
const sanitizedSerialNumber = inputSerialNumber.replace(/[^a-fA-F0-9:]/g, "");
const { data: cert } = useGetCert(sanitizedSerialNumber);
@@ -181,7 +181,7 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
} = useCertificateTemplate(
templateData,
actualSelectedProfile,
popUp?.certificateIssuance?.isOpen || false,
popUp?.issueCertificate?.isOpen || false,
setValue,
watch
);
@@ -227,10 +227,10 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
}, [cert, reset]);
useEffect(() => {
if (popUp?.certificateIssuance?.isOpen && profileId && !cert) {
if (popUp?.issueCertificate?.isOpen && profileId && !cert) {
setValue("profileId", profileId);
}
}, [popUp?.certificateIssuance?.isOpen, profileId, cert, setValue]);
}, [popUp?.issueCertificate?.isOpen, profileId, cert, setValue]);
const onFormSubmit = useCallback(
async ({
@@ -332,9 +332,9 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
return (
<Modal
isOpen={popUp?.certificateIssuance?.isOpen}
isOpen={popUp?.issueCertificate?.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle("certificateIssuance", isOpen);
handlePopUpToggle("issueCertificate", isOpen);
if (!isOpen) {
resetAllState();
}
@@ -503,7 +503,7 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
colorSchema="secondary"
variant="plain"
onClick={() => {
handlePopUpToggle("certificateIssuance", false);
handlePopUpToggle("issueCertificate", false);
}}
>
Cancel

View File

@@ -75,6 +75,7 @@ export type FormData = z.infer<typeof schema>;
type Props = {
popUp: UsePopUpState<["certificate"]>;
handlePopUpToggle: (popUpName: keyof UsePopUpState<["certificate"]>, state?: boolean) => void;
preselectedTemplate?: { id: string; name: string };
};
type TCertificateDetails = {
@@ -86,7 +87,7 @@ type TCertificateDetails = {
const CERT_TEMPLATE_NONE_VALUE = "none";
export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
export const CertificateModal = ({ popUp, handlePopUpToggle, preselectedTemplate }: Props) => {
const [certificateDetails, setCertificateDetails] = useState<TCertificateDetails | null>(null);
const { currentProject } = useProject();
const { data: cert } = useGetCert(
@@ -147,13 +148,15 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
(cert.extendedKeyUsages || []).map((name) => [name, true])
)
});
} else {
} else if (popUp?.certificate?.isOpen) {
const templateId = preselectedTemplate?.id || CERT_TEMPLATE_NONE_VALUE;
reset({
caId: "",
commonName: "",
subjectAltNames: "",
ttl: "",
certificateTemplateId: CERT_TEMPLATE_NONE_VALUE,
certificateTemplateId: templateId,
keyUsages: {
[CertKeyUsage.DIGITAL_SIGNATURE]: true,
[CertKeyUsage.KEY_ENCIPHERMENT]: true
@@ -161,7 +164,7 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
extendedKeyUsages: {}
});
}
}, [cert]);
}, [cert, preselectedTemplate, popUp?.certificate?.isOpen]);
useEffect(() => {
if (!cert && selectedCertTemplate) {
@@ -198,10 +201,14 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
ttl,
keyUsages: Object.entries(keyUsages)
.filter(([, value]) => value)
.map(([key]) => key as CertKeyUsage),
.map(([key]) =>
key === CertKeyUsage.CRL_SIGN
? "cRLSign"
: key.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase())
),
extendedKeyUsages: Object.entries(extendedKeyUsages)
.filter(([, value]) => value)
.map(([key]) => key as CertExtendedKeyUsage)
.map(([key]) => key.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase()))
});
reset();
@@ -269,15 +276,25 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
isRequired
>
<Select
defaultValue={field.value}
{...field}
value={field.value}
onValueChange={(e) => onChange(e)}
className="w-full"
isDisabled={Boolean(cert)}
isDisabled={Boolean(cert) || Boolean(preselectedTemplate)}
>
<SelectItem value={CERT_TEMPLATE_NONE_VALUE} key="cert-template-none">
None
</SelectItem>
{preselectedTemplate &&
!templatesData?.certificateTemplates?.find(
(t) => t.id === preselectedTemplate.id
) && (
<SelectItem
value={preselectedTemplate.id}
key={`cert-template-preselected-${preselectedTemplate.id}`}
>
{preselectedTemplate.name}
</SelectItem>
)}
{(templatesData?.certificateTemplates || []).map(({ id, name }) => (
<SelectItem value={id} key={`cert-template-${id}`}>
{name}

View File

@@ -17,7 +17,6 @@ import { CertificateImportModal } from "./CertificateImportModal";
import { CertificateIssuanceModal } from "./CertificateIssuanceModal";
import { CertificateManagePkiSyncsModal } from "./CertificateManagePkiSyncsModal";
import { CertificateManageRenewalModal } from "./CertificateManageRenewalModal";
import { CertificateModal } from "./CertificateModal";
import { CertificateRenewalModal } from "./CertificateRenewalModal";
import { CertificateRevocationModal } from "./CertificateRevocationModal";
import { CertificatesTable } from "./CertificatesTable";
@@ -26,12 +25,8 @@ export const CertificatesSection = () => {
const { currentProject } = useProject();
const { mutateAsync: deleteCert } = useDeleteCert();
// TODO: Use subscription.pkiLegacyTemplates to block legacy templates creation
const isLegacyTemplatesEnabled = true;
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"certificateIssuance",
"certificate",
"issueCertificate",
"certificateImport",
"certificateCert",
"deleteCertificate",
@@ -76,9 +71,7 @@ export const CertificatesSection = () => {
colorSchema="primary"
type="submit"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() =>
handlePopUpOpen(isLegacyTemplatesEnabled ? "certificate" : "certificateIssuance")
}
onClick={() => handlePopUpOpen("issueCertificate")}
isDisabled={!isAllowed}
>
Issue
@@ -88,11 +81,7 @@ export const CertificatesSection = () => {
</ProjectPermissionCan>
</div>
<CertificatesTable handlePopUpOpen={handlePopUpOpen} />
{isLegacyTemplatesEnabled ? (
<CertificateModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
) : (
<CertificateIssuanceModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
)}
<CertificateIssuanceModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<CertificateImportModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<CertificateCertModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<CertificateManageRenewalModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />

View File

@@ -64,7 +64,7 @@ type Props = {
handlePopUpOpen: (
popUpName: keyof UsePopUpState<
[
"certificate",
"issueCertificate",
"deleteCertificate",
"revokeCertificate",
"certificateCert",
@@ -297,7 +297,7 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={async () =>
handlePopUpOpen("certificate", {
handlePopUpOpen("issueCertificate", {
serialNumber: certificate.serialNumber
})
}

View File

@@ -5,6 +5,7 @@ import {
faCertificate,
faCog,
faEllipsis,
faFileContract,
faPencil,
faPlus,
faTrash
@@ -40,6 +41,7 @@ import {
Tr
} from "@app/components/v2";
import {
ProjectPermissionCertificateActions,
ProjectPermissionPkiTemplateActions,
ProjectPermissionSub,
useProject,
@@ -50,6 +52,7 @@ import { useDeleteCertTemplateV2 } from "@app/hooks/api";
import { useListCertificateTemplates } from "@app/hooks/api/certificateTemplates/queries";
import { ProjectType } from "@app/hooks/api/projects/types";
import { CertificateModal } from "../CertificatesPage/components/CertificateModal";
import { CertificateTemplateEnrollmentModal } from "../CertificatesPage/components/CertificateTemplateEnrollmentModal";
import { PkiTemplateForm } from "./components/PkiTemplateForm";
@@ -64,7 +67,8 @@ export const PkiTemplateListPage = () => {
"certificateTemplate",
"deleteTemplate",
"enrollmentOptions",
"estUpgradePlan"
"estUpgradePlan",
"certificateFromTemplate"
] as const);
const { subscription } = useSubscription();
@@ -160,6 +164,27 @@ export const PkiTemplateListPage = () => {
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1">
<ProjectPermissionCan
I={ProjectPermissionCertificateActions.Create}
a={ProjectPermissionSub.Certificates}
>
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
!isAllowed &&
"pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={(e) => {
e.stopPropagation();
handlePopUpOpen("certificateFromTemplate", template);
}}
disabled={!isAllowed}
icon={<FontAwesomeIcon icon={faFileContract} />}
>
Issue Certificate
</DropdownMenuItem>
)}
</ProjectPermissionCan>
<ProjectPermissionCan
I={ProjectPermissionPkiTemplateActions.Edit}
a={ProjectPermissionSub.CertificateTemplates}
@@ -284,6 +309,16 @@ export const PkiTemplateListPage = () => {
</ModalContent>
</Modal>
<CertificateTemplateEnrollmentModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<CertificateModal
popUp={{
certificate: {
isOpen: popUp.certificateFromTemplate.isOpen,
data: popUp.certificateFromTemplate.data
}
}}
handlePopUpToggle={(_, state) => handlePopUpToggle("certificateFromTemplate", state)}
preselectedTemplate={popUp.certificateFromTemplate.data}
/>
</div>
<UpgradePlanModal
isOpen={popUp.estUpgradePlan.isOpen}

View File

@@ -44,7 +44,7 @@ export const ProfileRow = ({ profile, onEditProfile, onDeleteProfile }: Props) =
const { data: caData } = useGetCaById(profile.caId);
const { popUp, handlePopUpToggle } = usePopUp(["certificateIssuance"] as const);
const { popUp, handlePopUpToggle } = usePopUp(["issueCertificate"] as const);
const [isIdCopied, setIsIdCopied] = useToggle(false);
@@ -147,7 +147,7 @@ export const ProfileRow = ({ profile, onEditProfile, onDeleteProfile }: Props) =
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
handlePopUpToggle("certificateIssuance");
handlePopUpToggle("issueCertificate");
}}
icon={<FontAwesomeIcon icon={faPlus} />}
>

View File

@@ -251,7 +251,10 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
const { control, handleSubmit, reset, watch, setValue, formState } = useForm<FormData>({
resolver: zodResolver(templateSchema),
defaultValues: getDefaultValues()
defaultValues: getDefaultValues(),
mode: "onChange",
reValidateMode: "onChange",
criteriaMode: "all"
});
useEffect(() => {