Transfer cert endpoints to work with serial numbers

This commit is contained in:
Tuan Dang
2024-05-29 16:12:46 -07:00
parent f9847f48b0
commit a2a786f392
23 changed files with 346 additions and 139 deletions
+8 -7
View File
@@ -13,7 +13,8 @@ import {
TImportCaCertificateResponse,
TSignIntermediateDTO,
TSignIntermediateResponse,
TUpdateCaDTO} from "./types";
TUpdateCaDTO
} from "./types";
export const useCreateCa = () => {
const queryClient = useQueryClient();
@@ -21,7 +22,7 @@ export const useCreateCa = () => {
mutationFn: async (body) => {
const {
data: { ca }
} = await apiRequest.post<{ ca: TCertificateAuthority }>("/api/v1/ca/", body);
} = await apiRequest.post<{ ca: TCertificateAuthority }>("/api/v1/pki/ca/", body);
return ca;
},
onSuccess: (_, { projectSlug }) => {
@@ -36,7 +37,7 @@ export const useUpdateCa = () => {
mutationFn: async ({ caId, projectSlug, ...body }) => {
const {
data: { ca }
} = await apiRequest.patch<{ ca: TCertificateAuthority }>(`/api/v1/ca/${caId}`, body);
} = await apiRequest.patch<{ ca: TCertificateAuthority }>(`/api/v1/pki/ca/${caId}`, body);
return ca;
},
onSuccess: (_, { projectSlug }) => {
@@ -51,7 +52,7 @@ export const useDeleteCa = () => {
mutationFn: async ({ caId }) => {
const {
data: { ca }
} = await apiRequest.delete<{ ca: TCertificateAuthority }>(`/api/v1/ca/${caId}`);
} = await apiRequest.delete<{ ca: TCertificateAuthority }>(`/api/v1/pki/ca/${caId}`);
return ca;
},
onSuccess: (_, { projectSlug }) => {
@@ -65,7 +66,7 @@ export const useSignIntermediate = () => {
return useMutation<TSignIntermediateResponse, {}, TSignIntermediateDTO>({
mutationFn: async (body) => {
const { data } = await apiRequest.post<TSignIntermediateResponse>(
`/api/v1/ca/${body.caId}/sign-intermediate`,
`/api/v1/pki/ca/${body.caId}/sign-intermediate`,
body
);
return data;
@@ -78,7 +79,7 @@ export const useImportCaCertificate = () => {
return useMutation<TImportCaCertificateResponse, {}, TImportCaCertificateDTO>({
mutationFn: async ({ caId, ...body }) => {
const { data } = await apiRequest.post<TImportCaCertificateResponse>(
`/api/v1/ca/${caId}/import-certificate`,
`/api/v1/pki/ca/${caId}/import-certificate`,
body
);
return data;
@@ -95,7 +96,7 @@ export const useCreateCertificate = () => {
return useMutation<TCreateCertificateResponse, {}, TCreateCertificateDTO>({
mutationFn: async ({ caId, ...body }) => {
const { data } = await apiRequest.post<TCreateCertificateResponse>(
`/api/v1/ca/${caId}/issue-certificate`,
`/api/v1/pki/ca/${caId}/issue-certificate`,
body
);
return data;
+3 -3
View File
@@ -16,7 +16,7 @@ export const useGetCaById = (caId: string) => {
queryFn: async () => {
const {
data: { ca }
} = await apiRequest.get<{ ca: TCertificateAuthority }>(`/api/v1/ca/${caId}`);
} = await apiRequest.get<{ ca: TCertificateAuthority }>(`/api/v1/pki/ca/${caId}`);
return ca;
},
enabled: Boolean(caId)
@@ -31,7 +31,7 @@ export const useGetCaCert = (caId: string) => {
certificate: string;
certificateChain: string;
serialNumber: string;
}>(`/api/v1/ca/${caId}/certificate`);
}>(`/api/v1/pki/ca/${caId}/certificate`);
return data;
},
enabled: Boolean(caId)
@@ -46,7 +46,7 @@ export const useGetCaCsr = (caId: string) => {
data: { csr }
} = await apiRequest.get<{
csr: string;
}>(`/api/v1/ca/${caId}/csr`);
}>(`/api/v1/pki/ca/${caId}/csr`);
return csr;
},
enabled: Boolean(caId)
@@ -0,0 +1,6 @@
import { CertStatus } from "./enums";
export const certStatusToNameMap: { [K in CertStatus]: string } = {
[CertStatus.ACTIVE]: "Active",
[CertStatus.REVOKED]: "Revoked"
};
@@ -0,0 +1,4 @@
export enum CertStatus {
ACTIVE = "active",
REVOKED = "revoked"
}
@@ -1,2 +1,2 @@
export { useDeleteCert } from "./mutations";
export { useGetCertById, useGetCertCert } from "./queries";
export { useDeleteCert, useRevokeCert } from "./mutations";
export { useGetCert, useGetCertCert } from "./queries";
@@ -3,20 +3,34 @@ import { useMutation, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { workspaceKeys } from "../workspace/queries";
import { TCertificate } from "./types";
export type TDeleteCaDTO = {
projectSlug: string;
certId: string;
};
import { TCertificate, TDeleteCertDTO, TRevokeCertDTO } from "./types";
export const useDeleteCert = () => {
const queryClient = useQueryClient();
return useMutation<TCertificate, {}, TDeleteCaDTO>({
mutationFn: async ({ certId }) => {
return useMutation<TCertificate, {}, TDeleteCertDTO>({
mutationFn: async ({ serialNumber }) => {
const {
data: { certificate }
} = await apiRequest.delete<{ certificate: TCertificate }>(`/api/v1/certificates/${certId}`);
} = await apiRequest.delete<{ certificate: TCertificate }>(
`/api/v1/pki/certificates/${serialNumber}`
);
return certificate;
},
onSuccess: (_, { projectSlug }) => {
queryClient.invalidateQueries(workspaceKeys.getWorkspaceCertificates(projectSlug));
}
});
};
export const useRevokeCert = () => {
const queryClient = useQueryClient();
return useMutation<TCertificate, {}, TRevokeCertDTO>({
mutationFn: async ({ serialNumber }) => {
const {
data: { certificate }
} = await apiRequest.post<{ certificate: TCertificate }>(
`/api/v1/pki/certificates/${serialNumber}/revoke`
);
return certificate;
},
onSuccess: (_, { projectSlug }) => {
+12 -10
View File
@@ -5,34 +5,36 @@ import { apiRequest } from "@app/config/request";
import { TCertificate } from "./types";
export const certKeys = {
getCertById: (certId: string) => [{ certId }, "cert"],
getCertCert: (certId: string) => [{ certId }, "certCert"]
getCertById: (serialNumber: string) => [{ serialNumber }, "cert"],
getCertCert: (serialNumber: string) => [{ serialNumber }, "certCert"]
};
export const useGetCertById = (certId: string) => {
export const useGetCert = (serialNumber: string) => {
return useQuery({
queryKey: certKeys.getCertById(certId),
queryKey: certKeys.getCertById(serialNumber),
queryFn: async () => {
const {
data: { certificate }
} = await apiRequest.get<{ certificate: TCertificate }>(`/api/v1/certificates/${certId}`);
} = await apiRequest.get<{ certificate: TCertificate }>(
`/api/v1/pki/certificates/${serialNumber}`
);
return certificate;
},
enabled: Boolean(certId)
enabled: Boolean(serialNumber)
});
};
export const useGetCertCert = (certId: string) => {
export const useGetCertCert = (serialNumber: string) => {
return useQuery({
queryKey: certKeys.getCertCert(certId),
queryKey: certKeys.getCertCert(serialNumber),
queryFn: async () => {
const { data } = await apiRequest.get<{
certificate: string;
certificateChain: string;
serialNumber: string;
}>(`/api/v1/certificates/${certId}/certificate`);
}>(`/api/v1/pki/certificates/${serialNumber}/certificate`);
return data;
},
enabled: Boolean(certId)
enabled: Boolean(serialNumber)
});
};
@@ -1,7 +1,21 @@
import { CertStatus } from "./enums";
export type TCertificate = {
id: string;
caId: string;
status: CertStatus;
commonName: string;
serialNumber: string;
notBefore: string;
notAfter: string;
};
export type TDeleteCertDTO = {
projectSlug: string;
serialNumber: string;
};
export type TRevokeCertDTO = {
projectSlug: string;
serialNumber: string;
};
@@ -11,7 +11,7 @@ type Props = {
export const CertificateCertModal = ({ popUp, handlePopUpToggle }: Props) => {
const { data } = useGetCertCert(
(popUp?.certificateCert?.data as { certId: string })?.certId || ""
(popUp?.certificateCert?.data as { serialNumber: string })?.serialNumber || ""
);
return (
@@ -111,41 +111,45 @@ export const CertificateContent = ({
<div className="mb-8 flex items-center justify-between rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
<p className="mr-4 whitespace-pre-wrap break-all">{certificate}</p>
</div>
<div className="mb-4 flex items-center justify-between">
<h2>Certificate Chain</h2>
<div className="flex">
<IconButton
ariaLabel="copy icon"
colorSchema="secondary"
className="group relative"
onClick={() => {
navigator.clipboard.writeText(certificateChain);
setIsCertificateChainCopied.on();
}}
>
<FontAwesomeIcon icon={isCertificateChainCopied ? faCheck : faCopy} />
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
Copy
</span>
</IconButton>
<IconButton
ariaLabel="copy icon"
colorSchema="secondary"
className="group relative ml-2"
onClick={() => {
downloadTxtFile("certificate_chain.txt", certificate);
}}
>
<FontAwesomeIcon icon={faDownload} />
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
Download
</span>
</IconButton>
</div>
</div>
<div className="mb-8 flex items-center justify-between rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
<p className="mr-4 whitespace-pre-wrap break-all">{certificateChain}</p>
</div>
{certificateChain && (
<>
<div className="mb-4 flex items-center justify-between">
<h2>Certificate Chain</h2>
<div className="flex">
<IconButton
ariaLabel="copy icon"
colorSchema="secondary"
className="group relative"
onClick={() => {
navigator.clipboard.writeText(certificateChain);
setIsCertificateChainCopied.on();
}}
>
<FontAwesomeIcon icon={isCertificateChainCopied ? faCheck : faCopy} />
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
Copy
</span>
</IconButton>
<IconButton
ariaLabel="copy icon"
colorSchema="secondary"
className="group relative ml-2"
onClick={() => {
downloadTxtFile("certificate_chain.txt", certificateChain);
}}
>
<FontAwesomeIcon icon={faDownload} />
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
Download
</span>
</IconButton>
</div>
</div>
<div className="mb-8 flex items-center justify-between rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
<p className="mr-4 whitespace-pre-wrap break-all">{certificateChain}</p>
</div>
</>
)}
{privateKey && (
<>
<div className="mb-4 flex items-center justify-between">
@@ -15,12 +15,7 @@ import {
SelectItem
} from "@app/components/v2";
import { useWorkspace } from "@app/context";
import {
CaStatus,
useCreateCertificate,
useGetCertById,
useListWorkspaceCas
} from "@app/hooks/api";
import { CaStatus, useCreateCertificate, useGetCert, useListWorkspaceCas } from "@app/hooks/api";
import { caTypeToNameMap } from "@app/hooks/api/ca/constants";
import { UsePopUpState } from "@app/hooks/usePopUp";
@@ -60,8 +55,8 @@ type TCertificateDetails = {
export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
const [certificateDetails, setCertificateDetails] = useState<TCertificateDetails | null>(null);
const { currentWorkspace } = useWorkspace();
const { data: cert } = useGetCertById(
(popUp?.certificate?.data as { certId: string })?.certId || ""
const { data: cert } = useGetCert(
(popUp?.certificate?.data as { serialNumber: string })?.serialNumber || ""
);
const { data: cas } = useListWorkspaceCas({
@@ -2,9 +2,10 @@ import { faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions";
import { Button, DeleteActionModal } from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { useDeleteCert } from "@app/hooks/api";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { useDeleteCert, useRevokeCert } from "@app/hooks/api";
import { usePopUp } from "@app/hooks/usePopUp";
import { CertificateCertModal } from "./CertificateCertModal";
@@ -14,18 +15,20 @@ import { CertificatesTable } from "./CertificatesTable";
export const CertificatesSection = () => {
const { currentWorkspace } = useWorkspace();
const { mutateAsync: deleteCert } = useDeleteCert();
const { mutateAsync: revokeCert } = useRevokeCert();
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"certificate",
"certificateCert",
"deleteCertificate"
"deleteCertificate",
"revokeCertificate"
] as const);
const onRemoveCertificateSubmit = async (certId: string) => {
const onRemoveCertificateSubmit = async (serialNumber: string) => {
try {
if (!currentWorkspace?.slug) return;
await deleteCert({ certId, projectSlug: currentWorkspace.slug });
await deleteCert({ serialNumber, projectSlug: currentWorkspace.slug });
await createNotification({
text: "Successfully deleted certificate",
@@ -42,23 +45,47 @@ export const CertificatesSection = () => {
}
};
const onRevokeCertificateSubmit = async (serialNumber: string) => {
try {
if (!currentWorkspace?.slug) return;
await revokeCert({ serialNumber, projectSlug: currentWorkspace.slug });
await createNotification({
text: "Successfully revoked certificate",
type: "success"
});
handlePopUpClose("revokeCertificate");
} catch (err) {
console.error(err);
createNotification({
text: "Failed to revoke certificate",
type: "error"
});
}
};
return (
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex justify-between">
<p className="text-xl font-semibold text-mineshaft-100">Certificates</p>
{/* <OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Member}>
{(isAllowed) => ( */}
<Button
colorSchema="primary"
type="submit"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => handlePopUpOpen("certificate")}
// isDisabled={!isAllowed}
<ProjectPermissionCan
I={ProjectPermissionActions.Create}
a={ProjectPermissionSub.Certificates}
>
Issue Certificate
</Button>
{/* )} */}
{/* </OrgPermissionCan> */}
{(isAllowed) => (
<Button
colorSchema="primary"
type="submit"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => handlePopUpOpen("certificate")}
isDisabled={!isAllowed}
>
Issue Certificate
</Button>
)}
</ProjectPermissionCan>
</div>
<CertificatesTable handlePopUpOpen={handlePopUpOpen} />
<CertificateModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
@@ -71,7 +98,23 @@ export const CertificatesSection = () => {
onChange={(isOpen) => handlePopUpToggle("deleteCertificate", isOpen)}
deleteKey="confirm"
onDeleteApproved={() =>
onRemoveCertificateSubmit((popUp?.deleteCertificate?.data as { certId: string })?.certId)
onRemoveCertificateSubmit(
(popUp?.deleteCertificate?.data as { serialNumber: string })?.serialNumber
)
}
/>
<DeleteActionModal
isOpen={popUp.revokeCertificate.isOpen}
title={`Are you sure want to revoke the certificate ${
(popUp?.revokeCertificate?.data as { commonName: string })?.commonName || ""
} from the project?`}
subTitle="This action is irreversible and will add the certificate to the CRL"
onChange={(isOpen) => handlePopUpToggle("revokeCertificate", isOpen)}
deleteKey="confirm"
onDeleteApproved={() =>
onRevokeCertificateSubmit(
(popUp?.revokeCertificate?.data as { serialNumber: string }).serialNumber
)
}
/>
</div>
@@ -1,4 +1,5 @@
import {
faBan,
faCertificate,
faEllipsis,
faEye,
@@ -24,16 +25,20 @@ import {
Th,
THead,
Tooltip,
Tr} from "@app/components/v2";
Tr
} from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { useListWorkspaceCertificates } from "@app/hooks/api";
import { certStatusToNameMap } from "@app/hooks/api/certificates/constants";
import { UsePopUpState } from "@app/hooks/usePopUp";
type Props = {
handlePopUpOpen: (
popUpName: keyof UsePopUpState<["certificate", "deleteCertificate", "certificateCert"]>,
popUpName: keyof UsePopUpState<
["certificate", "deleteCertificate", "revokeCertificate", "certificateCert"]
>,
data?: {
certId?: string;
serialNumber?: string;
commonName?: string;
}
) => void;
@@ -48,8 +53,8 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
<Table>
<THead>
<Tr>
<Th>Certificate ID</Th>
<Th>Common Name</Th>
<Th>Status</Th>
<Th>Valid Until</Th>
<Th />
</Tr>
@@ -62,8 +67,8 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
data.map((certificate) => {
return (
<Tr className="h-10" key={`certificate-${certificate.id}`}>
<Td>{certificate.id}</Td>
<Td>{certificate.commonName}</Td>
<Td>{certStatusToNameMap[certificate.status]}</Td>
<Td>
{certificate.notAfter
? format(new Date(certificate.notAfter), "yyyy-MM-dd")
@@ -90,7 +95,7 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
)}
onClick={async () =>
handlePopUpOpen("certificateCert", {
certId: certificate.id
serialNumber: certificate.serialNumber
})
}
disabled={!isAllowed}
@@ -111,7 +116,7 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
)}
onClick={async () =>
handlePopUpOpen("certificate", {
certId: certificate.id
serialNumber: certificate.serialNumber
})
}
disabled={!isAllowed}
@@ -121,6 +126,27 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
</DropdownMenuItem>
)}
</ProjectPermissionCan>
<ProjectPermissionCan
I={ProjectPermissionActions.Delete}
a={ProjectPermissionSub.Certificates}
>
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={async () =>
handlePopUpOpen("revokeCertificate", {
serialNumber: certificate.serialNumber
})
}
disabled={!isAllowed}
icon={<FontAwesomeIcon icon={faBan} />}
>
Revoke Certificate
</DropdownMenuItem>
)}
</ProjectPermissionCan>
<ProjectPermissionCan
I={ProjectPermissionActions.Delete}
a={ProjectPermissionSub.Certificates}
@@ -132,7 +158,7 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
)}
onClick={async () =>
handlePopUpOpen("deleteCertificate", {
certId: certificate.id,
serialNumber: certificate.serialNumber,
commonName: certificate.commonName
})
}