Make progress on ca cert versioning

This commit is contained in:
Tuan Dang
2024-08-05 11:00:22 -07:00
parent a0490d0fde
commit 587a4a1120
28 changed files with 1490 additions and 74 deletions
+1
View File
@@ -1,5 +1,6 @@
export * from "./Accordion";
export * from "./Alert";
export * from "./Badge";
export * from "./Button";
export * from "./Card";
export * from "./Checkbox";
+12 -1
View File
@@ -1,4 +1,4 @@
import { CaStatus,CaType } from "./enums";
import { CaStatus, CaType } from "./enums";
export const caTypeToNameMap: { [K in CaType]: string } = {
[CaType.ROOT]: "Root",
@@ -10,3 +10,14 @@ export const caStatusToNameMap: { [K in CaStatus]: string } = {
[CaStatus.DISABLED]: "Disabled",
[CaStatus.PENDING_CERTIFICATE]: "Pending Certificate"
};
export const getStatusBadgeVariant = (status: CaStatus) => {
switch (status) {
case CaStatus.ACTIVE:
return "success";
case CaStatus.DISABLED:
return "danger";
default:
return "primary";
}
};
+4
View File
@@ -8,3 +8,7 @@ export enum CaStatus {
DISABLED = "disabled",
PENDING_CERTIFICATE = "pending-certificate"
}
export enum CaRenewalType {
EXISTING = "existing"
}
+4 -4
View File
@@ -1,10 +1,10 @@
export { CaStatus, CaType } from "./enums";
export { CaRenewalType,CaStatus, CaType } from "./enums";
export {
useCreateCa,
useCreateCertificate,
useDeleteCa,
useImportCaCertificate,
useRenewCa,
useSignIntermediate,
useUpdateCa
} from "./mutations";
export { useGetCaById, useGetCaCert, useGetCaCrl,useGetCaCsr } from "./queries";
useUpdateCa} from "./mutations";
export { useGetCaById, useGetCaCert, useGetCaCerts, useGetCaCrl, useGetCaCsr } from "./queries";
+27 -3
View File
@@ -3,6 +3,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { workspaceKeys } from "../workspace/queries";
import { caKeys } from "./queries";
import {
TCertificateAuthority,
TCreateCaDTO,
@@ -11,10 +12,11 @@ import {
TDeleteCaDTO,
TImportCaCertificateDTO,
TImportCaCertificateResponse,
TRenewCaDTO,
TRenewCaResponse,
TSignIntermediateDTO,
TSignIntermediateResponse,
TUpdateCaDTO
} from "./types";
TUpdateCaDTO} from "./types";
export const useCreateCa = () => {
const queryClient = useQueryClient();
@@ -84,8 +86,10 @@ export const useImportCaCertificate = () => {
);
return data;
},
onSuccess: (_, { projectSlug }) => {
onSuccess: (_, { caId, projectSlug }) => {
queryClient.invalidateQueries(workspaceKeys.getWorkspaceCas({ projectSlug }));
queryClient.invalidateQueries(caKeys.getCaCerts(caId));
queryClient.invalidateQueries(caKeys.getCaCert(caId));
}
});
};
@@ -106,3 +110,23 @@ export const useCreateCertificate = () => {
}
});
};
export const useRenewCa = () => {
const queryClient = useQueryClient();
return useMutation<TRenewCaResponse, {}, TRenewCaDTO>({
mutationFn: async (body) => {
const { data } = await apiRequest.post<TRenewCaResponse>(
`/api/v1/pki/ca/${body.caId}/renew`,
body
);
return data;
},
onSuccess: (_, { caId, projectSlug }) => {
queryClient.invalidateQueries(workspaceKeys.getWorkspaceCas({ projectSlug }));
queryClient.invalidateQueries(caKeys.getCaCert(caId));
queryClient.invalidateQueries(caKeys.getCaCerts(caId));
queryClient.invalidateQueries(caKeys.getCaCsr(caId));
queryClient.invalidateQueries(caKeys.getCaCrl(caId));
}
});
};
+20 -1
View File
@@ -6,6 +6,7 @@ import { TCertificateAuthority } from "./types";
export const caKeys = {
getCaById: (caId: string) => [{ caId }, "ca"],
getCaCerts: (caId: string) => [{ caId }, "ca-cert"],
getCaCert: (caId: string) => [{ caId }, "ca-cert"],
getCaCsr: (caId: string) => [{ caId }, "ca-csr"],
getCaCrl: (caId: string) => [{ caId }, "ca-crl"]
@@ -24,6 +25,24 @@ export const useGetCaById = (caId: string) => {
});
};
export const useGetCaCerts = (caId: string) => {
return useQuery({
queryKey: caKeys.getCaCerts(caId),
queryFn: async () => {
const { data } = await apiRequest.get<
{
certificate: string;
certificateChain: string;
serialNumber: string;
version: number;
}[]
>(`/api/v1/pki/ca/${caId}/ca-certificates`); // TODO: consider updating endpoint structure
return data;
},
enabled: Boolean(caId)
});
};
export const useGetCaCert = (caId: string) => {
return useQuery({
queryKey: caKeys.getCaCert(caId),
@@ -32,7 +51,7 @@ export const useGetCaCert = (caId: string) => {
certificate: string;
certificateChain: string;
serialNumber: string;
}>(`/api/v1/pki/ca/${caId}/certificate`);
}>(`/api/v1/pki/ca/${caId}/certificate`); // TODO: consider updating endpoint structure
return data;
},
enabled: Boolean(caId)
+14 -1
View File
@@ -1,5 +1,5 @@
import { CertKeyAlgorithm } from "../certificates/enums";
import { CaStatus, CaType } from "./enums";
import { CaRenewalType,CaStatus, CaType } from "./enums";
export type TCertificateAuthority = {
id: string;
@@ -94,3 +94,16 @@ export type TCreateCertificateResponse = {
privateKey: string;
serialNumber: string;
};
export type TRenewCaDTO = {
projectSlug: string;
caId: string;
type: CaRenewalType;
notAfter: string;
};
export type TRenewCaResponse = {
certificate: string;
certificateChain: string;
serialNumber: string;
};
@@ -0,0 +1,18 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import Head from "next/head";
import { CaPage } from "@app/views/Project/CaPage";
export default function Ca() {
return (
<>
<Head>
<title>Certificate Authority</title>
<link rel="icon" href="/infisical.ico" />
</Head>
<CaPage />
</>
);
}
Ca.requireAuth = true;
@@ -0,0 +1,147 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { useRouter } from "next/router";
import { faChevronLeft, faEllipsis } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
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 { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { withProjectPermission } from "@app/hoc";
import { useDeleteCa,useGetCaById } from "@app/hooks/api";
import { usePopUp } from "@app/hooks/usePopUp";
import { CaModal } from "@app/views/Project/CertificatesPage/components/CaTab/components/CaModal";
import { CaInstallCertModal } from "../CertificatesPage/components/CaTab/components/CaInstallCertModal";
import { TabSections } from "../Types";
import { CaCertificatesSection, CaDetailsSection, CaRenewalModal } from "./components";
export const CaPage = withProjectPermission(
() => {
const router = useRouter();
const caId = router.query.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 });
await createNotification({
text: "Successfully deleted CA",
type: "success"
});
handlePopUpClose("deleteCa");
router.push(`/project/${projectId}/certificates`);
} catch (err) {
console.error(err);
createNotification({
text: "Failed to delete CA",
type: "error"
});
}
};
return (
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
{data && (
<div className="mx-auto mb-6 w-full max-w-7xl py-6 px-6">
<Button
variant="link"
type="submit"
leftIcon={<FontAwesomeIcon icon={faChevronLeft} />}
onClick={() =>
router.push(`/project/${projectId}/members?selectedTab=${TabSections.Roles}`)
}
className="mb-4"
>
Certificate Authorities
</Button>
<div className="mb-4 flex items-center justify-between">
<p className="text-3xl font-semibold text-white">{data.friendlyName}</p>
<DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg">
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
<Tooltip content="More options">
<FontAwesomeIcon size="sm" icon={faEllipsis} />
</Tooltip>
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1">
<ProjectPermissionCan
I={ProjectPermissionActions.Delete}
a={ProjectPermissionSub.CertificateAuthorities}
>
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
isAllowed
? "hover:!bg-red-500 hover:!text-white"
: "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={() =>
handlePopUpOpen("deleteCa", {
caId: data.id,
dn: data.dn
})
}
disabled={!isAllowed}
>
Delete CA
</DropdownMenuItem>
)}
</ProjectPermissionCan>
</DropdownMenuContent>
</DropdownMenu>
</div>
<div className="flex">
<div className="mr-4 w-96">
<CaDetailsSection caId={caId} handlePopUpOpen={handlePopUpOpen} />
</div>
<CaCertificatesSection caId={caId} />
</div>
</div>
)}
<CaModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<CaRenewalModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<CaInstallCertModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<DeleteActionModal
isOpen={popUp.deleteCa.isOpen}
title={`Are you sure want to remove the CA ${
(popUp?.deleteCa?.data as { dn: string })?.dn || ""
} from the project?`}
subTitle="This action will delete other CAs and certificates below it in your CA hierarchy."
onChange={(isOpen) => handlePopUpToggle("deleteCa", isOpen)}
deleteKey="confirm"
onDeleteApproved={() =>
onRemoveCaSubmit((popUp?.deleteCa?.data as { caId: string })?.caId)
}
/>
</div>
);
},
{ action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.CertificateAuthorities }
);
@@ -0,0 +1,31 @@
// import { faPlus } from "@fortawesome/free-solid-svg-icons";
// import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
// import { IconButton } from "@app/components/v2";
import { CaCertificatesTable } from "./CaCertificatesTable";
type Props = {
caId: string;
};
export const CaCertificatesSection = ({ caId }: Props) => {
return (
<div className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
<h3 className="text-lg font-semibold text-mineshaft-100">CA Certificates</h3>
{/* <IconButton
ariaLabel="copy icon"
variant="plain"
className="group relative"
onClick={() => {
// handlePopUpOpen("addIdentityToProject");
}}
>
<FontAwesomeIcon icon={faPlus} />
</IconButton> */}
</div>
<div className="py-4">
<CaCertificatesTable caId={caId} />
</div>
</div>
);
};
@@ -0,0 +1,131 @@
import { faCertificate, faEllipsis } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge";
import { ProjectPermissionCan } from "@app/components/permissions";
import {
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;
};
// TODO: not before
// created at
// expires on
export const CaCertificatesTable = ({ caId }: Props) => {
const { data: caCerts, isLoading } = useGetCaCerts(caId);
console.log("CaCertificatesTable data: ", caCerts);
return (
<TableContainer>
<Table>
<THead>
<Tr>
<Th>Name</Th>
<Th>Created At</Th>
<Th>Valid Until</Th>
<Th className="w-5" />
</Tr>
</THead>
<TBody>
{isLoading && <TableSkeleton columns={4} innerKey="ca-certificates" />}
{!isLoading &&
caCerts?.map((caCert) => {
// console.log("caCert index: ", index);
// const isLastItem = index === caCerts.length - 1;
return (
<Tr key={`ca-cert=${caCert.serialNumber}`}>
<Td>
<div className="flex items-center">
Certificate {caCert.version}
{/* <Badge variant="success" className="ml-4">
Current
</Badge> */}
</div>
</Td>
<Td>Test</Td>
<Td>Test</Td>
<Td>
<DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg">
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
<FontAwesomeIcon size="sm" icon={faEllipsis} />
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1">
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={ProjectPermissionSub.Identity}
>
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={(e) => {
e.stopPropagation();
// TODO
// router.push(`/org/${orgId}/identities/${id}`);
}}
disabled={!isAllowed}
>
Download CA Certificate
</DropdownMenuItem>
)}
</ProjectPermissionCan>
<ProjectPermissionCan
I={ProjectPermissionActions.Delete}
a={ProjectPermissionSub.Identity}
>
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={(e) => {
e.stopPropagation();
// TODO
// handlePopUpOpen("deleteIdentity", {
// identityId: id,
// name
// });
}}
disabled={!isAllowed}
>
Download CA Certificate Chain
</DropdownMenuItem>
)}
</ProjectPermissionCan>
</DropdownMenuContent>
</DropdownMenu>
</Td>
</Tr>
);
})}
</TBody>
</Table>
{!isLoading && !caCerts?.length && (
<EmptyState
title="This CA does not have any CA certificates installed"
icon={faCertificate}
/>
)}
</TableContainer>
);
};
@@ -0,0 +1 @@
export { CaCertificatesSection } from "./CaCertificatesSection";
@@ -0,0 +1,141 @@
import { faCheck, faCopy } 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,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?: {}
) => void;
};
export const CaDetailsSection = ({ caId, handlePopUpOpen }: Props) => {
const [copyTextId, isCopyingId, setCopyTextId] = useTimedReset<string>({
initialState: "Copy ID to clipboard"
});
const { data: ca } = useGetCaById(caId);
return ca ? (
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
<h3 className="text-lg font-semibold text-mineshaft-100">CA Details</h3>
</div>
<div className="pt-4">
<div className="mb-4">
<p className="text-sm font-semibold text-mineshaft-300">CA ID</p>
<div className="group flex align-top">
<p className="text-sm text-mineshaft-300">{ca.id}</p>
<div className="opacity-0 transition-opacity duration-300 group-hover:opacity-100">
<Tooltip content={copyTextId}>
<IconButton
ariaLabel="copy icon"
variant="plain"
className="group relative ml-2"
onClick={() => {
navigator.clipboard.writeText(ca.id);
setCopyTextId("Copied");
}}
>
<FontAwesomeIcon icon={isCopyingId ? faCheck : faCopy} />
</IconButton>
</Tooltip>
</div>
</div>
</div>
<div className="mb-4">
<p className="text-sm font-semibold text-mineshaft-300">Friendly Name</p>
<p className="text-sm text-mineshaft-300">{ca.friendlyName}</p>
</div>
<div className="mb-4">
<p className="text-sm font-semibold text-mineshaft-300">CA Type</p>
<p className="text-sm text-mineshaft-300">{caTypeToNameMap[ca.type]}</p>
</div>
<div className="mb-4">
<p className="text-sm font-semibold text-mineshaft-300">Status</p>
<p className="text-sm text-mineshaft-300">{caStatusToNameMap[ca.status]}</p>
</div>
<div className="mb-4">
<p className="text-sm font-semibold text-mineshaft-300">Key Algorithm</p>
<p className="text-sm text-mineshaft-300">{certKeyAlgorithmToNameMap[ca.keyAlgorithm]}</p>
</div>
<div className="mb-4">
<p className="text-sm font-semibold text-mineshaft-300">Max Path Length</p>
<p className="text-sm text-mineshaft-300">{ca.maxPathLength ?? "-"}</p>
</div>
<div className="mb-4">
<p className="text-sm font-semibold text-mineshaft-300">Not Before</p>
<p className="text-sm text-mineshaft-300">
{ca.notBefore ? format(new Date(ca.notBefore), "yyyy-MM-dd") : "-"}
</p>
</div>
<div className="mb-4">
<p className="text-sm font-semibold text-mineshaft-300">Not After</p>
<p className="text-sm text-mineshaft-300">
{ca.notAfter ? format(new Date(ca.notAfter), "yyyy-MM-dd") : "-"}
</p>
</div>
{ca.status === CaStatus.ACTIVE && (
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={ProjectPermissionSub.CertificateAuthorities}
>
{(isAllowed) => {
return (
<Button
isDisabled={!isAllowed}
className="mt-4 w-full"
colorSchema="primary"
type="submit"
onClick={() => {
handlePopUpOpen("renewCa", {
caId
});
}}
>
Renew CA
</Button>
);
}}
</ProjectPermissionCan>
)}
{ca.status === CaStatus.PENDING_CERTIFICATE && (
<ProjectPermissionCan
I={ProjectPermissionActions.Create}
a={ProjectPermissionSub.CertificateAuthorities}
>
{(isAllowed) => {
return (
<Button
isDisabled={!isAllowed}
className="mt-4 w-full"
colorSchema="primary"
type="submit"
onClick={() => {
handlePopUpOpen("installCaCert", {
caId
});
}}
>
Install CA Certificate
</Button>
);
}}
</ProjectPermissionCan>
)}
</div>
</div>
) : (
<div />
);
};
@@ -0,0 +1,215 @@
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,
// CaType,
CaStatus,
useGetCaById,
useRenewCa} 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 getMiddleDate = (date1: Date, date2: Date): Date => {
// const timestamp1 = date1.getTime();
// const timestamp2 = date2.getTime();
// const middleTimestamp = (timestamp1 + timestamp2) / 2;
// return new Date(middleTimestamp);
// };
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<typeof schema>;
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<FormData>({
resolver: zodResolver(schema),
defaultValues: {
type: CaRenewalType.EXISTING,
notAfter: "" // TODO: set sensible default
}
});
useEffect(() => {
if (ca && ca.status === CaStatus.ACTIVE) {
// if (ca.type === CaType.ROOT) {
// // extend Root CA validity by the same amount of time
// 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]);
// } else if (ca.type === CaType.INTERMEDIATE && parentCa) {
// }
// extend Root CA validity by the same amount of time
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]);
}
// if (ca && parentCa) {
// // intermediate CA
// } else if (ca && ca.notAfter && !parentCa) {
// // root CA
// const timeDifference = new Date(ca.).getTime() - startDate.getTime();
// }
}, [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);
const error = err as any;
const text = error?.response?.data?.message ?? "Failed to renew CA";
createNotification({
text,
type: "error"
});
}
};
return (
<Modal
isOpen={popUp?.renewCa?.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle("renewCa", isOpen);
reset();
}}
>
<ModalContent title="Renew CA">
<form onSubmit={handleSubmit(onFormSubmit)}>
<Controller
control={control}
name="type"
defaultValue={CaRenewalType.EXISTING}
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="CA Renewal Method"
errorText={error?.message}
isError={Boolean(error)}
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
>
{caRenewalTypes.map(({ label, value }) => (
<SelectItem value={String(value || "")} key={label}>
{label}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
<Controller
control={control}
defaultValue=""
name="notAfter"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Valid Until"
isError={Boolean(error)}
errorText={error?.message}
isRequired
>
<Input {...field} placeholder="YYYY-MM-DD" />
</FormControl>
)}
/>
<div className="flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
Create
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpToggle("renewCa", false)}
>
Cancel
</Button>
</div>
</form>
</ModalContent>
</Modal>
);
};
@@ -0,0 +1,3 @@
export { CaCertificatesSection } from "./CaCertificatesSection/CaCertificatesSection";
export { CaDetailsSection } from "./CaDetailsSection";
export { CaRenewalModal } from "./CaRenewalModal";
@@ -0,0 +1 @@
export { CaPage } from "./CaPage";
@@ -1,3 +1,4 @@
import { useRouter } from "next/router";
import {
faBan,
faCertificate,
@@ -12,6 +13,7 @@ import { twMerge } from "tailwind-merge";
import { ProjectPermissionCan } from "@app/components/permissions";
import {
Badge,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
@@ -31,9 +33,14 @@ import {
ProjectPermissionActions,
ProjectPermissionSub,
useSubscription,
useWorkspace} from "@app/context";
useWorkspace
} from "@app/context";
import { CaStatus, useListWorkspaceCas } from "@app/hooks/api";
import { caStatusToNameMap, caTypeToNameMap } from "@app/hooks/api/ca/constants";
import {
caStatusToNameMap,
caTypeToNameMap,
getStatusBadgeVariant
} from "@app/hooks/api/ca/constants";
import { UsePopUpState } from "@app/hooks/usePopUp";
type Props = {
@@ -51,11 +58,13 @@ type Props = {
};
export const CaTable = ({ handlePopUpOpen }: Props) => {
const router = useRouter();
const { subscription } = useSubscription();
const { currentWorkspace } = useWorkspace();
const { data, isLoading } = useListWorkspaceCas({
projectSlug: currentWorkspace?.slug ?? ""
});
return (
<div>
<TableContainer>
@@ -76,11 +85,26 @@ export const CaTable = ({ handlePopUpOpen }: Props) => {
data.length > 0 &&
data.map((ca) => {
return (
<Tr className="h-10" key={`ca-${ca.id}`}>
<Tr
className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
key={`ca-${ca.id}`}
onClick={() => router.push(`/project/${currentWorkspace?.id}/ca/${ca.id}`)}
>
<Td>{ca.friendlyName}</Td>
<Td>{caStatusToNameMap[ca.status]}</Td>
<Td>
<Badge variant={getStatusBadgeVariant(ca.status)}>
{caStatusToNameMap[ca.status]}
</Badge>
</Td>
<Td>{caTypeToNameMap[ca.type]}</Td>
<Td>{ca.notAfter ? format(new Date(ca.notAfter), "yyyy-MM-dd") : "-"}</Td>
<Td>
<div className="flex items-center ">
<p>{ca.notAfter ? format(new Date(ca.notAfter), "yyyy-MM-dd") : "-"}</p>
{/* <Badge variant="danger" className="ml-4">
Expires Soon
</Badge> */}
</div>
</Td>
<Td className="flex justify-end">
<DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg">
@@ -102,7 +126,8 @@ export const CaTable = ({ handlePopUpOpen }: Props) => {
!isAllowed &&
"pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={async () => {
onClick={(e) => {
e.stopPropagation();
handlePopUpOpen("installCaCert", {
caId: ca.id
});
@@ -110,7 +135,7 @@ export const CaTable = ({ handlePopUpOpen }: Props) => {
disabled={!isAllowed}
icon={<FontAwesomeIcon icon={faCertificate} />}
>
Install Certificate
Install CA Certificate
</DropdownMenuItem>
)}
</ProjectPermissionCan>
@@ -126,7 +151,8 @@ export const CaTable = ({ handlePopUpOpen }: Props) => {
!isAllowed &&
"pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={async () => {
onClick={(e) => {
e.stopPropagation();
handlePopUpOpen("caCert", {
caId: ca.id
});
@@ -150,7 +176,8 @@ export const CaTable = ({ handlePopUpOpen }: Props) => {
!isAllowed &&
"pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={async () => {
onClick={(e) => {
e.stopPropagation();
if (!subscription?.caCrl) {
handlePopUpOpen("upgradePlan", {
description:
@@ -179,11 +206,12 @@ export const CaTable = ({ handlePopUpOpen }: Props) => {
className={twMerge(
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={async () =>
onClick={(e) => {
e.stopPropagation();
handlePopUpOpen("ca", {
caId: ca.id
})
}
});
}}
disabled={!isAllowed}
icon={<FontAwesomeIcon icon={faEye} />}
>
@@ -202,15 +230,16 @@ export const CaTable = ({ handlePopUpOpen }: Props) => {
!isAllowed &&
"pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={async () =>
onClick={(e) => {
e.stopPropagation();
handlePopUpOpen("caStatus", {
caId: ca.id,
status:
ca.status === CaStatus.ACTIVE
? CaStatus.DISABLED
: CaStatus.ACTIVE
})
}
});
}}
disabled={!isAllowed}
icon={<FontAwesomeIcon icon={faBan} />}
>
@@ -228,12 +257,13 @@ export const CaTable = ({ handlePopUpOpen }: Props) => {
className={twMerge(
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={async () =>
onClick={(e) => {
e.stopPropagation();
handlePopUpOpen("deleteCa", {
caId: ca.id,
dn: ca.dn
})
}
});
}}
disabled={!isAllowed}
icon={<FontAwesomeIcon icon={faTrash} />}
>