diff --git a/frontend-v2/src/const/routes.ts b/frontend-v2/src/const/routes.ts index 913b3f60d..7d987d72c 100644 --- a/frontend-v2/src/const/routes.ts +++ b/frontend-v2/src/const/routes.ts @@ -39,5 +39,8 @@ export const ROUTE_PATHS = Object.freeze({ SecretDashboardPage: setRoute( "/secret-manager/$projectId/secrets/$envSlug", "/_authenticate/_ctx-org-details/secret-manager/$projectId/_layout-secret-manager/secrets/$envSlug/" - ) + ), + CertAuthDetailsByIDPage: setRoute("cert-auth", "cert-auth"), + CertCertificatesPage: setRoute("cert-list", "cert-list"), + CertPkiCollectionDetailsByIDPage: setRoute("cert-pki-collection", "cert-pki-collection") }); diff --git a/frontend-v2/src/pages/cert-manager/CertAuthDetailsByIDPage/CertAuthDetailsByIDPage.tsx b/frontend-v2/src/pages/cert-manager/CertAuthDetailsByIDPage/CertAuthDetailsByIDPage.tsx new file mode 100644 index 000000000..2552a57c6 --- /dev/null +++ b/frontend-v2/src/pages/cert-manager/CertAuthDetailsByIDPage/CertAuthDetailsByIDPage.tsx @@ -0,0 +1,168 @@ +import { Helmet } from "react-helmet"; +import { faChevronLeft, faEllipsis } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { useNavigate, useParams } from "@tanstack/react-router"; +import { twMerge } from "tailwind-merge"; + +import { createNotification } from "@app/components/notifications"; +import { ProjectPermissionCan } from "@app/components/permissions"; +import { + Button, + DeleteActionModal, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, + Tooltip +} from "@app/components/v2"; +import { ROUTE_PATHS } from "@app/const/routes"; +import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; +import { useDeleteCa, useGetCaById } from "@app/hooks/api"; +import { ProjectType } from "@app/hooks/api/workspace/types"; +import { usePopUp } from "@app/hooks/usePopUp"; + +import { CaInstallCertModal } from "../CertificatesPage/components/CaTab/components/CaInstallCertModal"; +import { CaModal } from "../CertificatesPage/components/CaTab/components/CaModal"; +import { CertificateTemplatesSection } from "../CertificatesPage/components/CertificatesTab/components/CertificateTemplatesSection"; +import { + CaCertificatesSection, + CaCrlsSection, + CaDetailsSection, + CaRenewalModal +} from "./components"; + +const Page = () => { + const navigate = useNavigate(); + const params = useParams({ + from: ROUTE_PATHS.CertAuthDetailsByIDPage.id + }); + const caId = params.caId as string; + const { data } = useGetCaById(caId); + + const { currentWorkspace } = useWorkspace(); + const projectId = currentWorkspace?.id || ""; + + const { mutateAsync: deleteCa } = useDeleteCa(); + + const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ + "ca", + "deleteCa", + "installCaCert", + "renewCa" + ] as const); + + const onRemoveCaSubmit = async (caIdToDelete: string) => { + try { + if (!currentWorkspace?.slug) return; + + await deleteCa({ caId: caIdToDelete, projectSlug: currentWorkspace.slug }); + + createNotification({ + text: "Successfully deleted CA", + type: "success" + }); + + handlePopUpClose("deleteCa"); + navigate({ to: `/${ProjectType.CertificateManager}/${projectId}/certificates` as const }); + } catch { + createNotification({ + text: "Failed to delete CA", + type: "error" + }); + } + }; + + return ( +
+ {data && ( +
+ +
+

{data.friendlyName}

+ + +
+ + + +
+
+ + + {(isAllowed) => ( + + handlePopUpOpen("deleteCa", { + caId: data.id, + dn: data.dn + }) + } + disabled={!isAllowed} + > + Delete CA + + )} + + +
+
+
+
+ +
+
+ + + +
+
+
+ )} + + + + handlePopUpToggle("deleteCa", isOpen)} + deleteKey="confirm" + onDeleteApproved={() => onRemoveCaSubmit((popUp?.deleteCa?.data as { caId: string })?.caId)} + /> +
+ ); +}; + +export const CertAuthDetailsByIDPage = () => { + return ( + <> + + Certificate Authority + + + + + ); +}; diff --git a/frontend-v2/src/pages/cert-manager/CertAuthDetailsByIDPage/components/CaCertificatesSection/CaCertificatesSection.tsx b/frontend-v2/src/pages/cert-manager/CertAuthDetailsByIDPage/components/CaCertificatesSection/CaCertificatesSection.tsx new file mode 100644 index 000000000..23bfd269b --- /dev/null +++ b/frontend-v2/src/pages/cert-manager/CertAuthDetailsByIDPage/components/CaCertificatesSection/CaCertificatesSection.tsx @@ -0,0 +1,28 @@ +import { CaCertificatesTable } from "./CaCertificatesTable"; + +type Props = { + caId: string; +}; + +export const CaCertificatesSection = ({ caId }: Props) => { + return ( +
+
+

CA Certificates

+ {/* { + // handlePopUpOpen("addIdentityToProject"); + }} + > + + */} +
+
+ +
+
+ ); +}; diff --git a/frontend-v2/src/pages/cert-manager/CertAuthDetailsByIDPage/components/CaCertificatesSection/CaCertificatesTable.tsx b/frontend-v2/src/pages/cert-manager/CertAuthDetailsByIDPage/components/CaCertificatesSection/CaCertificatesTable.tsx new file mode 100644 index 000000000..e0d7c1740 --- /dev/null +++ b/frontend-v2/src/pages/cert-manager/CertAuthDetailsByIDPage/components/CaCertificatesSection/CaCertificatesTable.tsx @@ -0,0 +1,133 @@ +import { faCertificate, faEllipsis } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import * as x509 from "@peculiar/x509"; +import { format } from "date-fns"; +import FileSaver from "file-saver"; +import { twMerge } from "tailwind-merge"; + +import { ProjectPermissionCan } from "@app/components/permissions"; +import { + Badge, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, + EmptyState, + Table, + TableContainer, + TableSkeleton, + TBody, + Td, + Th, + THead, + Tr +} from "@app/components/v2"; +import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context"; +import { useGetCaCerts } from "@app/hooks/api"; + +type Props = { + caId: string; +}; + +export const CaCertificatesTable = ({ caId }: Props) => { + const { data: caCerts, isPending } = useGetCaCerts(caId); + + const downloadTxtFile = (filename: string, content: string) => { + const blob = new Blob([content], { type: "text/plain;charset=utf-8" }); + FileSaver.saveAs(blob, filename); + }; + + return ( + + + + + + + + + + + {isPending && } + {!isPending && + caCerts?.map((caCert, index) => { + const isLastItem = index === caCerts.length - 1; + const caCertObj = new x509.X509Certificate(caCert.certificate); + return ( + + + + + + + ); + })} + +
CA Certificate #Not BeforeNot After +
+
+ CA Certificate {caCert.version} + {isLastItem && ( + + Current + + )} +
+
{format(new Date(caCertObj.notBefore), "yyyy-MM-dd")}{format(new Date(caCertObj.notAfter), "yyyy-MM-dd")} + + +
+ +
+
+ + + {(isAllowed) => ( + { + e.stopPropagation(); + downloadTxtFile("cert.pem", caCert.certificate); + }} + disabled={!isAllowed} + > + Download CA Certificate + + )} + + + {(isAllowed) => ( + { + e.stopPropagation(); + downloadTxtFile("chain.pem", caCert.certificateChain); + }} + disabled={!isAllowed} + > + Download CA Certificate Chain + + )} + + +
+
+ {!isPending && !caCerts?.length && ( + + )} +
+ ); +}; diff --git a/frontend-v2/src/pages/cert-manager/CertAuthDetailsByIDPage/components/CaCertificatesSection/index.tsx b/frontend-v2/src/pages/cert-manager/CertAuthDetailsByIDPage/components/CaCertificatesSection/index.tsx new file mode 100644 index 000000000..14f5dbbb9 --- /dev/null +++ b/frontend-v2/src/pages/cert-manager/CertAuthDetailsByIDPage/components/CaCertificatesSection/index.tsx @@ -0,0 +1 @@ +export { CaCertificatesSection } from "./CaCertificatesSection"; diff --git a/frontend-v2/src/pages/cert-manager/CertAuthDetailsByIDPage/components/CaCrlsSection/CaCrlsSection.tsx b/frontend-v2/src/pages/cert-manager/CertAuthDetailsByIDPage/components/CaCrlsSection/CaCrlsSection.tsx new file mode 100644 index 000000000..6778a77a3 --- /dev/null +++ b/frontend-v2/src/pages/cert-manager/CertAuthDetailsByIDPage/components/CaCrlsSection/CaCrlsSection.tsx @@ -0,0 +1,20 @@ +import { CaCrlsTable } from "./CaCrlsTable"; + +type Props = { + caId: string; +}; + +export const CaCrlsSection = ({ caId }: Props) => { + return ( +
+
+

+ CA Certificate Revocation Lists (CRLs) +

+
+
+ +
+
+ ); +}; diff --git a/frontend-v2/src/pages/cert-manager/CertAuthDetailsByIDPage/components/CaCrlsSection/CaCrlsTable.tsx b/frontend-v2/src/pages/cert-manager/CertAuthDetailsByIDPage/components/CaCrlsSection/CaCrlsTable.tsx new file mode 100644 index 000000000..dfe87d027 --- /dev/null +++ b/frontend-v2/src/pages/cert-manager/CertAuthDetailsByIDPage/components/CaCrlsSection/CaCrlsTable.tsx @@ -0,0 +1,88 @@ +import { faCertificate, faFileDownload } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +// import * as x509 from "@peculiar/x509"; +// import { format } from "date-fns"; +import FileSaver from "file-saver"; + +import { + EmptyState, + IconButton, + Table, + TableContainer, + TableSkeleton, + TBody, + Td, + Th, + THead, + Tooltip, + Tr +} from "@app/components/v2"; +import { useGetCaCrls } from "@app/hooks/api"; + +type Props = { + caId: string; +}; + +export const CaCrlsTable = ({ caId }: Props) => { + const { data: caCrls, isPending } = useGetCaCrls(caId); + + const downloadTxtFile = (filename: string, content: string) => { + const blob = new Blob([content], { type: "text/plain;charset=utf-8" }); + FileSaver.saveAs(blob, filename); + }; + + return ( + + + + + + {/* */} + {/* */} + + + + {isPending && } + {!isPending && + caCrls?.map(({ id, crl }) => { + // const caCrlObj = new x509.X509Crl(crl); + return ( + + + {/* */} + {/* */} + + + ); + })} + +
Distribution Point URLThis UpdateNext Update +
+
+ {`${window.origin}/api/v1/pki/crl/${id}`} +
+
{format(new Date(caCrlObj.thisUpdate), "yyyy-MM-dd")} + {caCrlObj.nextUpdate + ? format(new Date(caCrlObj.nextUpdate), "yyyy-MM-dd") + : "-"} + + + { + e.stopPropagation(); + downloadTxtFile("crl.pem", crl); + }} + > + + + +
+ {!isPending && !caCrls?.length && ( + + )} +
+ ); +}; diff --git a/frontend-v2/src/pages/cert-manager/CertAuthDetailsByIDPage/components/CaCrlsSection/index.tsx b/frontend-v2/src/pages/cert-manager/CertAuthDetailsByIDPage/components/CaCrlsSection/index.tsx new file mode 100644 index 000000000..a46d94d7b --- /dev/null +++ b/frontend-v2/src/pages/cert-manager/CertAuthDetailsByIDPage/components/CaCrlsSection/index.tsx @@ -0,0 +1 @@ +export { CaCrlsSection } from "./CaCrlsSection"; diff --git a/frontend-v2/src/pages/cert-manager/CertAuthDetailsByIDPage/components/CaDetailsSection.tsx b/frontend-v2/src/pages/cert-manager/CertAuthDetailsByIDPage/components/CaDetailsSection.tsx new file mode 100644 index 000000000..06fa5d1d9 --- /dev/null +++ b/frontend-v2/src/pages/cert-manager/CertAuthDetailsByIDPage/components/CaDetailsSection.tsx @@ -0,0 +1,208 @@ +import { faCheck, faCopy, faPencil } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { format } from "date-fns"; + +import { ProjectPermissionCan } from "@app/components/permissions"; +import { Button, IconButton, Tooltip } from "@app/components/v2"; +import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context"; +import { useTimedReset } from "@app/hooks"; +import { CaStatus, CaType, useGetCaById } from "@app/hooks/api"; +import { caStatusToNameMap, caTypeToNameMap } from "@app/hooks/api/ca/constants"; +import { certKeyAlgorithmToNameMap } from "@app/hooks/api/certificates/constants"; +import { UsePopUpState } from "@app/hooks/usePopUp"; + +type Props = { + caId: string; + handlePopUpOpen: ( + popUpName: keyof UsePopUpState<["ca", "renewCa", "installCaCert"]>, + data?: object + ) => void; +}; + +export const CaDetailsSection = ({ caId, handlePopUpOpen }: Props) => { + const [copyTextId, isCopyingId, setCopyTextId] = useTimedReset({ + initialState: "Copy ID to clipboard" + }); + const [copyTextParentId, isCopyingParentId, setCopyTextParentId] = useTimedReset({ + initialState: "Copy ID to clipboard" + }); + + const { data: ca } = useGetCaById(caId); + + return ca ? ( +
+
+

CA Details

+ + {(isAllowed) => { + return ( + + { + e.stopPropagation(); + handlePopUpOpen("ca", { + caId: ca.id + }); + }} + > + + + + ); + }} + +
+
+
+

CA Type

+

{caTypeToNameMap[ca.type]}

+
+
+

CA ID

+
+

{ca.id}

+
+ + { + navigator.clipboard.writeText(ca.id); + setCopyTextId("Copied"); + }} + > + + + +
+
+
+ {ca.type === CaType.INTERMEDIATE && ca.status !== CaStatus.PENDING_CERTIFICATE && ( +
+

Parent CA ID

+
+

+ {ca.parentCaId ? ca.parentCaId : "N/A - External Parent CA"} +

+ {ca.parentCaId && ( +
+ + { + navigator.clipboard.writeText(ca.parentCaId as string); + setCopyTextParentId("Copied"); + }} + > + + + +
+ )} +
+
+ )} +
+

Friendly Name

+

{ca.friendlyName}

+
+
+

Status

+

{caStatusToNameMap[ca.status]}

+
+
+

Key Algorithm

+

{certKeyAlgorithmToNameMap[ca.keyAlgorithm]}

+
+
+

Max Path Length

+

{ca.maxPathLength ?? "-"}

+
+
+

Not Before

+

+ {ca.notBefore ? format(new Date(ca.notBefore), "yyyy-MM-dd") : "-"} +

+
+
+

Not After

+

+ {ca.notAfter ? format(new Date(ca.notAfter), "yyyy-MM-dd") : "-"} +

+
+
+

Template Issuance Required

+

+ {ca.requireTemplateForIssuance ? "True" : "False"} +

+
+ {ca.status === CaStatus.ACTIVE && ( + + {(isAllowed) => { + return ( + + ); + }} + + )} + {ca.status === CaStatus.PENDING_CERTIFICATE && ( + + {(isAllowed) => { + return ( + + ); + }} + + )} +
+
+ ) : ( +
+ ); +}; diff --git a/frontend-v2/src/pages/cert-manager/CertAuthDetailsByIDPage/components/CaRenewalModal.tsx b/frontend-v2/src/pages/cert-manager/CertAuthDetailsByIDPage/components/CaRenewalModal.tsx new file mode 100644 index 000000000..6f9cf3a1c --- /dev/null +++ b/frontend-v2/src/pages/cert-manager/CertAuthDetailsByIDPage/components/CaRenewalModal.tsx @@ -0,0 +1,182 @@ +// import { useEffect } 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 { + Button, + FormControl, + Input, + Modal, + ModalContent, + Select, + SelectItem +} from "@app/components/v2"; +import { useWorkspace } from "@app/context"; +import { + CaRenewalType, + useRenewCa + // useGetCaById, + // CaType, + // CaStatus +} from "@app/hooks/api/ca"; +import { UsePopUpState } from "@app/hooks/usePopUp"; + +const caRenewalTypes = [{ label: "Renew with same key pair", value: CaRenewalType.EXISTING }]; + +const isValidDate = (dateString: string) => { + const date = new Date(dateString); + return !Number.isNaN(date.getTime()); +}; + +const schema = z + .object({ + type: z.enum([CaRenewalType.EXISTING]), + notAfter: z.string().trim().refine(isValidDate, { message: "Invalid date format" }) + }) + .required(); + +export type FormData = z.infer; + +type Props = { + popUp: UsePopUpState<["renewCa"]>; + handlePopUpToggle: (popUpName: keyof UsePopUpState<["renewCa"]>, state?: boolean) => void; +}; + +export const CaRenewalModal = ({ popUp, handlePopUpToggle }: Props) => { + const { currentWorkspace } = useWorkspace(); + const projectSlug = currentWorkspace?.slug || ""; + + const popUpData = popUp?.renewCa?.data as { + caId: string; + }; + + // const { data: ca } = useGetCaById(popUpData?.caId || ""); + // const { data: parentCa } = useGetCaById(ca?.parentCaId || ""); + const { mutateAsync: renewCa } = useRenewCa(); + + const { + control, + handleSubmit, + reset, + formState: { isSubmitting } + // setValue + } = useForm({ + resolver: zodResolver(schema), + defaultValues: { + type: CaRenewalType.EXISTING, + notAfter: "" // TODO: consider setting a default value + } + }); + + // useEffect(() => { + // if (ca && ca.status === CaStatus.ACTIVE) { + // const notBeforeDate = new Date(ca.notBefore as string); + // const notAfterDate = new Date(ca.notAfter as string); + + // const newNotAfterDate = new Date( + // notAfterDate.getTime() + notAfterDate.getTime() - notBeforeDate.getTime() + // ); + + // setValue("notAfter", newNotAfterDate.toISOString().split("T")[0]); + // } + // }, [ca, parentCa]); + + const onFormSubmit = async ({ type, notAfter }: FormData) => { + try { + if (!projectSlug || !popUpData.caId) return; + + await renewCa({ + projectSlug, + caId: popUpData.caId, + notAfter, + type + }); + + handlePopUpToggle("renewCa", false); + + createNotification({ + text: "Successfully renewed CA", + type: "success" + }); + + reset(); + } catch (err) { + console.error(err); + } + }; + + return ( + { + handlePopUpToggle("renewCa", isOpen); + reset(); + }} + > + +
+ ( + + + + )} + /> + ( + + + + )} + /> +
+ + +
+ +
+
+ ); +}; diff --git a/frontend-v2/src/pages/cert-manager/CertAuthDetailsByIDPage/components/index.tsx b/frontend-v2/src/pages/cert-manager/CertAuthDetailsByIDPage/components/index.tsx new file mode 100644 index 000000000..95a4868f2 --- /dev/null +++ b/frontend-v2/src/pages/cert-manager/CertAuthDetailsByIDPage/components/index.tsx @@ -0,0 +1,4 @@ +export { CaCertificatesSection } from "./CaCertificatesSection/CaCertificatesSection"; +export { CaCrlsSection } from "./CaCrlsSection"; +export { CaDetailsSection } from "./CaDetailsSection"; +export { CaRenewalModal } from "./CaRenewalModal"; diff --git a/frontend-v2/src/pages/cert-manager/CertAuthDetailsByIDPage/route.tsx b/frontend-v2/src/pages/cert-manager/CertAuthDetailsByIDPage/route.tsx new file mode 100644 index 000000000..17f0f27e2 --- /dev/null +++ b/frontend-v2/src/pages/cert-manager/CertAuthDetailsByIDPage/route.tsx @@ -0,0 +1,7 @@ +import { createFileRoute } from "@tanstack/react-router"; + +import { CertAuthDetailsByIDPage } from "./CertAuthDetailsByIDPage"; + +export const Route = createFileRoute("")({ + component: CertAuthDetailsByIDPage +}); diff --git a/frontend-v2/src/pages/cert-manager/CertificatesPage/CertificatesPage.tsx b/frontend-v2/src/pages/cert-manager/CertificatesPage/CertificatesPage.tsx new file mode 100644 index 000000000..e18782cdc --- /dev/null +++ b/frontend-v2/src/pages/cert-manager/CertificatesPage/CertificatesPage.tsx @@ -0,0 +1,67 @@ +import { Helmet } from "react-helmet"; +import { useTranslation } from "react-i18next"; + +import { ProjectPermissionCan } from "@app/components/permissions"; +import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2"; +import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context"; + +import { CaTab, CertificatesTab, PkiAlertsTab } from "./components"; + +enum TabSections { + Ca = "certificate-authorities", + Certificates = "certificates", + Alerting = "alerting" +} + +export const CertificatesPage = () => { + const { t } = useTranslation(); + return ( +
+ + {t("common.head-title", { title: "Certificates" })} + + + +
+

Internal PKI

+ + + Certificates + Certificate Authorities + Alerting + + + + + + + + + + + + + + + + + +
+
+ ); +}; diff --git a/frontend-v2/src/routes/_authenticate/_ctx-org-details/secret-manager.$projectId/_layout-secret-manager/access/-components/GroupsTab/GroupsTab.tsx b/frontend-v2/src/pages/cert-manager/CertificatesPage/components/CaTab/CaTab.tsx similarity index 67% rename from frontend-v2/src/routes/_authenticate/_ctx-org-details/secret-manager.$projectId/_layout-secret-manager/access/-components/GroupsTab/GroupsTab.tsx rename to frontend-v2/src/pages/cert-manager/CertificatesPage/components/CaTab/CaTab.tsx index 19fb35773..aadca4dfe 100644 --- a/frontend-v2/src/routes/_authenticate/_ctx-org-details/secret-manager.$projectId/_layout-secret-manager/access/-components/GroupsTab/GroupsTab.tsx +++ b/frontend-v2/src/pages/cert-manager/CertificatesPage/components/CaTab/CaTab.tsx @@ -1,18 +1,17 @@ import { motion } from "framer-motion"; -import { GroupsSection } from "./components"; +import { CaSection } from "./components"; -export const GroupsTab = () => { +export const CaTab = () => { return ( - + ); }; - diff --git a/frontend-v2/src/pages/cert-manager/CertificatesPage/components/CaTab/components/CaCertModal.tsx b/frontend-v2/src/pages/cert-manager/CertificatesPage/components/CaTab/components/CaCertModal.tsx new file mode 100644 index 000000000..97970d94f --- /dev/null +++ b/frontend-v2/src/pages/cert-manager/CertificatesPage/components/CaTab/components/CaCertModal.tsx @@ -0,0 +1,34 @@ +import { Modal, ModalContent } from "@app/components/v2"; +import { useGetCaCert } from "@app/hooks/api"; +import { UsePopUpState } from "@app/hooks/usePopUp"; + +import { CertificateContent } from "../../CertificatesTab/components/CertificateContent"; + +type Props = { + popUp: UsePopUpState<["caCert"]>; + handlePopUpToggle: (popUpName: keyof UsePopUpState<["caCert"]>, state?: boolean) => void; +}; + +export const CaCertModal = ({ popUp, handlePopUpToggle }: Props) => { + const { data } = useGetCaCert((popUp?.caCert?.data as { caId: string })?.caId || ""); + return ( + { + handlePopUpToggle("caCert", isOpen); + }} + > + + {data ? ( + + ) : ( +
+ )} + + + ); +}; diff --git a/frontend-v2/src/pages/cert-manager/CertificatesPage/components/CaTab/components/CaInstallCertModal/CaInstallCertModal.tsx b/frontend-v2/src/pages/cert-manager/CertificatesPage/components/CaTab/components/CaInstallCertModal/CaInstallCertModal.tsx new file mode 100644 index 000000000..1095e0d3e --- /dev/null +++ b/frontend-v2/src/pages/cert-manager/CertificatesPage/components/CaTab/components/CaInstallCertModal/CaInstallCertModal.tsx @@ -0,0 +1,75 @@ +import { useEffect, useState } from "react"; + +import { FormControl, Modal, ModalContent, Select, SelectItem } from "@app/components/v2"; +import { UsePopUpState } from "@app/hooks/usePopUp"; + +import { ExternalCaInstallForm } from "./ExternalCaInstallForm"; +import { InternalCaInstallForm } from "./InternalCaInstallForm"; + +type Props = { + popUp: UsePopUpState<["installCaCert"]>; + handlePopUpToggle: (popUpName: keyof UsePopUpState<["installCaCert"]>, state?: boolean) => void; +}; + +enum ParentCaType { + Internal = "internal", + External = "external" +} + +export const CaInstallCertModal = ({ popUp, handlePopUpToggle }: Props) => { + const popupData = popUp?.installCaCert?.data; + const caId = popupData?.caId || ""; + const isParentCaExternal = popupData?.isParentCaExternal || false; + const [parentCaType, setParentCaType] = useState(ParentCaType.Internal); + + useEffect(() => { + if (popupData?.isParentCaExternal) { + setParentCaType(ParentCaType.External); + } + }, [popupData]); + + const renderForm = (parentCaTypeInput: ParentCaType) => { + switch (parentCaTypeInput) { + case ParentCaType.Internal: + return ; + default: + return ; + } + }; + + return ( + { + handlePopUpToggle("installCaCert", isOpen); + }} + > + + + + + {renderForm(parentCaType)} + + + ); +}; diff --git a/frontend-v2/src/pages/cert-manager/CertificatesPage/components/CaTab/components/CaInstallCertModal/ExternalCaInstallForm.tsx b/frontend-v2/src/pages/cert-manager/CertificatesPage/components/CaTab/components/CaInstallCertModal/ExternalCaInstallForm.tsx new file mode 100644 index 000000000..8b3ddd0cf --- /dev/null +++ b/frontend-v2/src/pages/cert-manager/CertificatesPage/components/CaTab/components/CaInstallCertModal/ExternalCaInstallForm.tsx @@ -0,0 +1,172 @@ +import { useEffect } from "react"; +import { Controller, useForm } from "react-hook-form"; +import { faCheck, faCopy, faDownload } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { zodResolver } from "@hookform/resolvers/zod"; +import FileSaver from "file-saver"; +import { z } from "zod"; + +import { createNotification } from "@app/components/notifications"; +import { Button, FormControl, IconButton, TextArea, Tooltip } from "@app/components/v2"; +import { useWorkspace } from "@app/context"; +import { useTimedReset } from "@app/hooks"; +import { useGetCaCsr, useImportCaCertificate } from "@app/hooks/api"; +import { UsePopUpState } from "@app/hooks/usePopUp"; + +const schema = z.object({ + certificate: z.string().min(1), + certificateChain: z.string().min(1) +}); + +export type FormData = z.infer; + +type Props = { + caId: string; + handlePopUpToggle: (popUpName: keyof UsePopUpState<["installCaCert"]>, state?: boolean) => void; +}; + +export const ExternalCaInstallForm = ({ caId, handlePopUpToggle }: Props) => { + const { currentWorkspace } = useWorkspace(); + const [copyTextCaCsr, isCopyingCaCsr, setCopyTextCaCsr] = useTimedReset({ + initialState: "Copy to clipboard" + }); + + const { + control, + handleSubmit, + reset, + formState: { isSubmitting } + } = useForm({ + resolver: zodResolver(schema) + }); + + const { data: csr } = useGetCaCsr(caId); + const { mutateAsync: importCaCertificate } = useImportCaCertificate(); + + useEffect(() => { + reset(); + }, []); + + const onFormSubmit = async ({ certificate, certificateChain }: FormData) => { + try { + if (!csr || !caId || !currentWorkspace?.slug) return; + + await importCaCertificate({ + caId, + projectSlug: currentWorkspace?.slug, + certificate, + certificateChain + }); + + reset(); + + createNotification({ + text: "Successfully installed certificate for CA", + type: "success" + }); + handlePopUpToggle("installCaCert", false); + } catch (err) { + createNotification({ + text: "Failed to install certificate for CA", + type: "error" + }); + } + }; + + const downloadTxtFile = (filename: string, content: string) => { + const blob = new Blob([content], { type: "text/plain;charset=utf-8" }); + FileSaver.saveAs(blob, filename); + }; + + return ( +
+ {csr && ( + <> +
+

CSR for this CA

+
+ + { + navigator.clipboard.writeText(csr); + setCopyTextCaCsr("Copied"); + }} + > + + + + + { + downloadTxtFile("csr.pem", csr); + }} + > + + + +
+
+
+

{csr}

+
+ + )} + ( + +