Continue cert mgmt mvp

This commit is contained in:
Tuan Dang
2024-05-28 17:00:48 -07:00
parent 26ea949a4e
commit b8516da90f
57 changed files with 3339 additions and 467 deletions
@@ -24,7 +24,9 @@ export enum ProjectPermissionSub {
SecretRollback = "secret-rollback",
SecretApproval = "secret-approval",
SecretRotation = "secret-rotation",
Identity = "identity"
Identity = "identity",
CertificateAuthorities = "certificate-authorities",
Certificates = "certificates"
}
type SubjectFields = {
@@ -51,6 +53,8 @@ export type ProjectPermissionSet =
| [ProjectPermissionActions, ProjectPermissionSub.ServiceTokens]
| [ProjectPermissionActions, ProjectPermissionSub.SecretApproval]
| [ProjectPermissionActions, ProjectPermissionSub.SecretRotation]
| [ProjectPermissionActions, ProjectPermissionSub.CertificateAuthorities]
| [ProjectPermissionActions, ProjectPermissionSub.Certificates]
| [ProjectPermissionActions.Delete, ProjectPermissionSub.Workspace]
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Workspace]
| [ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback]
+10 -4
View File
@@ -1,6 +1,12 @@
import { CertificateAuthorityType } from "./enums";
import { CaStatus,CaType } from "./enums";
export const caTypeToNameMap: { [K in CertificateAuthorityType]: string } = {
[CertificateAuthorityType.ROOT]: "Root",
[CertificateAuthorityType.INTERMEDIATE]: "Intermediate"
export const caTypeToNameMap: { [K in CaType]: string } = {
[CaType.ROOT]: "Root",
[CaType.INTERMEDIATE]: "Intermediate"
};
export const caStatusToNameMap: { [K in CaStatus]: string } = {
[CaStatus.ACTIVE]: "Active",
[CaStatus.DISABLED]: "Disabled",
[CaStatus.PENDING_CERTIFICATE]: "Pending Certificate"
};
+7 -1
View File
@@ -1,4 +1,10 @@
export enum CertificateAuthorityType {
export enum CaType {
ROOT = "root",
INTERMEDIATE = "intermediate"
}
export enum CaStatus {
ACTIVE = "active",
DISABLED = "disabled",
PENDING_CERTIFICATE = "pending-certificate"
}
+9 -2
View File
@@ -1,2 +1,9 @@
export { CertificateAuthorityType } from "./enums";
export { useCreateCa } from "./mutations";
export { CaStatus,CaType } from "./enums";
export {
useCreateCa,
useCreateCertificate,
useDeleteCa,
useImportCaCertificate,
useSignIntermediate,
useUpdateCa} from "./mutations";
export { useGetCaById, useGetCaCert, useGetCaCsr } from "./queries";
+95 -11
View File
@@ -1,23 +1,107 @@
import {
useMutation
// useQueryClient
} from "@tanstack/react-query";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { TCertificateAuthority, TCreateCaDTO } from "./types";
import { workspaceKeys } from "../workspace/queries";
import {
TCertificateAuthority,
TCreateCaDTO,
TCreateCertificateDTO,
TCreateCertificateResponse,
TDeleteCaDTO,
TImportCaCertificateDTO,
TImportCaCertificateResponse,
TSignIntermediateDTO,
TSignIntermediateResponse,
TUpdateCaDTO} from "./types";
export const useCreateCa = () => {
// const queryClient = useQueryClient();
const queryClient = useQueryClient();
return useMutation<TCertificateAuthority, {}, TCreateCaDTO>({
mutationFn: async (body) => {
const {
data: { identity }
} = await apiRequest.post("/api/v1/ca/", body);
return identity;
data: { ca }
} = await apiRequest.post<{ ca: TCertificateAuthority }>("/api/v1/ca/", body);
return ca;
},
onSuccess: () => {
// queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId));
onSuccess: (_, { projectSlug }) => {
queryClient.invalidateQueries(workspaceKeys.getWorkspaceCas({ projectSlug }));
}
});
};
export const useUpdateCa = () => {
const queryClient = useQueryClient();
return useMutation<TCertificateAuthority, {}, TUpdateCaDTO>({
mutationFn: async ({ caId, projectSlug, ...body }) => {
const {
data: { ca }
} = await apiRequest.patch<{ ca: TCertificateAuthority }>(`/api/v1/ca/${caId}`, body);
return ca;
},
onSuccess: (_, { projectSlug }) => {
queryClient.invalidateQueries(workspaceKeys.getWorkspaceCas({ projectSlug }));
}
});
};
export const useDeleteCa = () => {
const queryClient = useQueryClient();
return useMutation<TCertificateAuthority, {}, TDeleteCaDTO>({
mutationFn: async ({ caId }) => {
const {
data: { ca }
} = await apiRequest.delete<{ ca: TCertificateAuthority }>(`/api/v1/ca/${caId}`);
return ca;
},
onSuccess: (_, { projectSlug }) => {
queryClient.invalidateQueries(workspaceKeys.getWorkspaceCas({ projectSlug }));
}
});
};
export const useSignIntermediate = () => {
// TODO: consider renaming
return useMutation<TSignIntermediateResponse, {}, TSignIntermediateDTO>({
mutationFn: async (body) => {
const { data } = await apiRequest.post<TSignIntermediateResponse>(
`/api/v1/ca/${body.caId}/sign-intermediate`,
body
);
return data;
}
});
};
export const useImportCaCertificate = () => {
const queryClient = useQueryClient();
return useMutation<TImportCaCertificateResponse, {}, TImportCaCertificateDTO>({
mutationFn: async ({ caId, ...body }) => {
const { data } = await apiRequest.post<TImportCaCertificateResponse>(
`/api/v1/ca/${caId}/import-certificate`,
body
);
return data;
},
onSuccess: (_, { projectSlug }) => {
queryClient.invalidateQueries(workspaceKeys.getWorkspaceCas({ projectSlug }));
}
});
};
// consider rename to issue certificate
export const useCreateCertificate = () => {
const queryClient = useQueryClient();
return useMutation<TCreateCertificateResponse, {}, TCreateCertificateDTO>({
mutationFn: async ({ caId, ...body }) => {
const { data } = await apiRequest.post<TCreateCertificateResponse>(
`/api/v1/ca/${caId}/issue-certificate`,
body
);
return data;
},
onSuccess: (_, { projectSlug }) => {
queryClient.invalidateQueries(workspaceKeys.getWorkspaceCertificates(projectSlug));
}
});
};
+54
View File
@@ -0,0 +1,54 @@
import { useQuery } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { TCertificateAuthority } from "./types";
export const caKeys = {
getCaById: (caId: string) => [{ caId }, "ca"],
getCaCert: (caId: string) => [{ caId }, "ca-cert"],
getCaCsr: (caId: string) => [{ caId }, "ca-csr"]
};
export const useGetCaById = (caId: string) => {
return useQuery({
queryKey: caKeys.getCaById(caId),
queryFn: async () => {
const {
data: { ca }
} = await apiRequest.get<{ ca: TCertificateAuthority }>(`/api/v1/ca/${caId}`);
return ca;
},
enabled: Boolean(caId)
});
};
export const useGetCaCert = (caId: string) => {
return useQuery({
queryKey: caKeys.getCaCert(caId),
queryFn: async () => {
const { data } = await apiRequest.get<{
certificate: string;
certificateChain: string;
serialNumber: string;
}>(`/api/v1/ca/${caId}/certificate`);
return data;
},
enabled: Boolean(caId)
});
};
export const useGetCaCsr = (caId: string) => {
return useQuery({
queryKey: caKeys.getCaCsr(caId),
queryFn: async () => {
const {
data: { csr }
} = await apiRequest.get<{
csr: string;
}>(`/api/v1/ca/${caId}/csr`);
return csr;
},
enabled: Boolean(caId)
});
};
+70 -3
View File
@@ -1,12 +1,21 @@
import { CertificateAuthorityType } from "./enums";
import { CaStatus,CaType } from "./enums";
export type TCertificateAuthority = {
id: string;
parentCaId?: string;
projectId: string;
type: CertificateAuthorityType;
dn: string;
type: CaType;
status: CaStatus;
organization: string;
ou: string;
country: string;
province: string;
locality: string;
commonName: string;
dn: string;
maxPathLength?: number;
notAfter?: string;
notBefore?: string;
createdAt: string;
updatedAt: string;
};
@@ -20,4 +29,62 @@ export type TCreateCaDTO = {
province: string;
locality: string;
commonName: string;
notAfter?: string;
maxPathLength: number;
};
export type TUpdateCaDTO = {
projectSlug: string;
caId: string;
status?: CaStatus;
};
export type TDeleteCaDTO = {
projectSlug: string;
caId: string;
};
export type TSignIntermediateDTO = {
caId: string;
csr: string;
maxPathLength: number;
notBefore?: string;
notAfter?: string;
};
export type TSignIntermediateResponse = {
certificate: string;
certificateChain: string;
issuingCaCertificate: string;
serialNumber: string;
};
export type TImportCaCertificateDTO = {
caId: string;
projectSlug: string;
certificate: string;
certificateChain: string;
};
export type TImportCaCertificateResponse = {
message: string;
caId: string;
};
// TODO: add TTL
export type TCreateCertificateDTO = {
projectSlug: string;
caId: string;
commonName: string;
ttl?: number;
notBefore?: string;
notAfter?: string;
};
export type TCreateCertificateResponse = {
certificate: string;
issuingCertificate: string;
certificateChain: string;
sk: string;
serialNumber: string;
};
@@ -0,0 +1,2 @@
export { useDeleteCert } from "./mutations";
export { useGetCertById, useGetCertCert } from "./queries";
@@ -0,0 +1,26 @@
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;
};
export const useDeleteCert = () => {
const queryClient = useQueryClient();
return useMutation<TCertificate, {}, TDeleteCaDTO>({
mutationFn: async ({ certId }) => {
const {
data: { certificate }
} = await apiRequest.delete<{ certificate: TCertificate }>(`/api/v1/certificates/${certId}`);
return certificate;
},
onSuccess: (_, { projectSlug }) => {
queryClient.invalidateQueries(workspaceKeys.getWorkspaceCertificates(projectSlug));
}
});
};
@@ -0,0 +1,40 @@
import { useQuery } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { TCertificate } from "./types";
export const certKeys = {
getCertById: (certId: string) => [{ certId }, "cert"],
getCertCert: (certId: string) => [{ certId }, "certCert"]
};
export const useGetCertById = (certId: string) => {
return useQuery({
queryKey: certKeys.getCertById(certId),
queryFn: async () => {
const {
data: { certificate }
} = await apiRequest.get<{ certificate: TCertificate }>(`/api/v1/certificates/${certId}`);
return certificate;
},
enabled: Boolean(certId)
});
};
export const useGetCertCert = (certId: string) => {
return useQuery({
queryKey: certKeys.getCertCert(certId),
queryFn: async () => {
const { data } = await apiRequest.get<{
certificate: string;
certificateChain: string;
issuingCaCertificate: string;
privateKey: string;
serialNumber: string;
}>(`/api/v1/certificates/${certId}/certificate`);
return data;
},
enabled: Boolean(certId)
});
};
@@ -0,0 +1,7 @@
export type TCertificate = {
id: string;
caId: string;
commonName: string;
notBefore: string;
notAfter: string;
};
+1
View File
@@ -6,6 +6,7 @@ export * from "./auditLogStreams";
export * from "./auth";
export * from "./bots";
export * from "./ca";
export * from "./certificates";
export * from "./dynamicSecret";
export * from "./dynamicSecretLease";
export * from "./groups";
@@ -22,6 +22,7 @@ export {
useGetWorkspaceSecrets,
useGetWorkspaceUsers,
useListWorkspaceCas,
useListWorkspaceCertificates,
useListWorkspaceGroups,
useNameWorkspaceSecrets,
useRenameWorkspace,
+42 -4
View File
@@ -2,7 +2,9 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { CaStatus } from "../ca/enums";
import { TCertificateAuthority } from "../ca/types";
import { TCertificate } from "../certificates/types";
import { TGroupMembership } from "../groups/types";
import { IdentityMembership } from "../identities/types";
import { IntegrationAuth } from "../integrationAuth/types";
@@ -41,7 +43,12 @@ export const workspaceKeys = {
[{ workspaceId }, "workspace-identity-memberships"] as const,
getWorkspaceGroupMemberships: (workspaceId: string) =>
[{ workspaceId }, "workspace-groups"] as const,
getWorkspaceCas: (workspaceId: string) => [{ workspaceId }, "workspace-cas"] as const
getWorkspaceCas: ({ projectSlug }: { projectSlug: string }) =>
[{ projectSlug }, "workspace-cas"] as const,
specificWorkspaceCas: ({ projectSlug, status }: { projectSlug: string; status?: CaStatus }) =>
[...workspaceKeys.getWorkspaceCas({ projectSlug }), { status }] as const,
getWorkspaceCertificates: (projectSlug: string) =>
[{ projectSlug }, "workspace-certificates"] as const
};
const fetchWorkspaceById = async (workspaceId: string) => {
@@ -472,17 +479,48 @@ export const useListWorkspaceGroups = (projectSlug: string) => {
});
};
export const useListWorkspaceCas = (projectSlug: string) => {
export const useListWorkspaceCas = ({
projectSlug,
status
}: {
projectSlug: string;
status?: CaStatus;
}) => {
return useQuery({
queryKey: workspaceKeys.getWorkspaceCas(projectSlug),
queryKey: workspaceKeys.specificWorkspaceCas({
projectSlug,
status
}),
queryFn: async () => {
const params = new URLSearchParams({
...(status && { status })
});
const {
data: { cas }
} = await apiRequest.get<{ cas: TCertificateAuthority[] }>(
`/api/v2/workspace/${projectSlug}/cas`
`/api/v2/workspace/${projectSlug}/cas`,
{
params
}
);
return cas;
},
enabled: Boolean(projectSlug)
});
};
export const useListWorkspaceCertificates = (projectSlug: string) => {
return useQuery({
queryKey: workspaceKeys.getWorkspaceCertificates(projectSlug),
queryFn: async () => {
const {
data: { certificates }
} = await apiRequest.get<{ certificates: TCertificate[] }>(
`/api/v2/workspace/${projectSlug}/certificates`
);
return certificates;
},
enabled: Boolean(projectSlug)
});
};
@@ -9,11 +9,10 @@ const Certificates = () => {
return (
<div className="h-full bg-bunker-800">
<Head>
<title>{t("common.head-title", { title: t("settings.project.title") })}</title>
<title>{t("common.head-title", { title: "Certificates" })}</title>
<link rel="icon" href="/infisical.ico" />
<meta property="og:image" content="/images/message.png" />
</Head>
<div>Test CA page</div>
<CertificatesPage />
</div>
);
@@ -2,15 +2,7 @@ import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { withProjectPermission } from "@app/hoc";
import { CaTab } from "./components";
// import { CaTable } from "./components";
// TODO: fix permission
/**
* TODO 1: CA section
* TODO 2: Certificates section
*/
import { CaTab, CertificatesTab } from "./components";
enum TabSections {
Ca = "certificate-authorities",
@@ -23,18 +15,17 @@ export const CertificatesPage = withProjectPermission(
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto mb-6 w-full max-w-7xl py-6 px-6">
<p className="mr-4 mb-4 text-3xl font-semibold text-white">Certificates</p>
<Tabs defaultValue={TabSections.Ca}>
<Tabs defaultValue={TabSections.Certificates}>
<TabList>
<Tab value={TabSections.Ca}>Certificate Authorities</Tab>
<Tab value={TabSections.Certificates}>Certificates</Tab>
<Tab value={TabSections.Ca}>Certificate Authorities</Tab>
</TabList>
<TabPanel value={TabSections.Certificates}>
<CertificatesTab />
</TabPanel>
<TabPanel value={TabSections.Ca}>
<CaTab />
</TabPanel>
<TabPanel value={TabSections.Certificates}>
<div>Certs</div>
{/* <OrgGroupsTab /> */}
</TabPanel>
</Tabs>
</div>
</div>
@@ -0,0 +1,109 @@
import { useEffect } from "react";
import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { IconButton,Modal, ModalContent } from "@app/components/v2";
import { useToggle } from "@app/hooks";
import { useGetCaCert } from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
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 || "");
const [isSerialNumberCopied, setIsSerialNumberCopied] = useToggle(false);
const [isCertificateCopied, setIsCertificateCopied] = useToggle(false);
const [isCertificateChainCopied, setIsCertificateChainCopied] = useToggle(false);
useEffect(() => {
let timer: NodeJS.Timeout;
if (isSerialNumberCopied) {
timer = setTimeout(() => setIsSerialNumberCopied.off(), 2000);
}
if (isCertificateCopied) {
timer = setTimeout(() => setIsCertificateCopied.off(), 2000);
}
if (isCertificateChainCopied) {
timer = setTimeout(() => setIsCertificateChainCopied.off(), 2000);
}
return () => clearTimeout(timer);
}, [isSerialNumberCopied, isCertificateCopied, isCertificateChainCopied]);
return (
<Modal
isOpen={popUp?.caCert?.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle("caCert", isOpen);
}}
>
<ModalContent title="CA Certificate">
{data ? (
<div>
<h2 className="mb-4">Serial Number</h2>
<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 break-all">{data.serialNumber}</p>
<IconButton
ariaLabel="copy icon"
colorSchema="secondary"
className="group relative"
onClick={() => {
navigator.clipboard.writeText(data.serialNumber);
setIsSerialNumberCopied.on();
}}
>
<FontAwesomeIcon icon={isSerialNumberCopied ? 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">
Click to copy
</span>
</IconButton>
</div>
<h2 className="mb-4">Certificate Body</h2>
<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">{data.certificate}</p>
<IconButton
ariaLabel="copy icon"
colorSchema="secondary"
className="group relative"
onClick={() => {
navigator.clipboard.writeText(data.certificate);
setIsCertificateCopied.on();
}}
>
<FontAwesomeIcon icon={isCertificateCopied ? 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">
Click to copy
</span>
</IconButton>
</div>
<h2 className="mb-4">Certificate Chain</h2>
<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">{data.certificateChain}</p>
<IconButton
ariaLabel="copy icon"
colorSchema="secondary"
className="group relative"
onClick={() => {
navigator.clipboard.writeText(data.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">
Click to copy
</span>
</IconButton>
</div>
</div>
) : (
<div />
)}
</ModalContent>
</Modal>
);
};
@@ -0,0 +1,328 @@
import { useEffect, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { format } from "date-fns";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import {
// DatePicker,
Button,
FormControl,
Input,
Modal,
ModalContent,
Select,
SelectItem
} from "@app/components/v2";
import { useWorkspace } from "@app/context";
import {
CaStatus,
useGetCaById,
useGetCaCsr,
useImportCaCertificate,
useListWorkspaceCas,
useSignIntermediate
} from "@app/hooks/api";
import { caTypeToNameMap } from "@app/hooks/api/ca/constants";
import { UsePopUpState } from "@app/hooks/usePopUp";
const isValidDate = (dateString: string) => {
const date = new Date(dateString);
return !Number.isNaN(date.getTime());
};
const getMiddleDate = (date1: Date, date2: Date) => {
const timestamp1 = date1.getTime();
const timestamp2 = date2.getTime();
const middleTimestamp = (timestamp1 + timestamp2) / 2;
return new Date(middleTimestamp);
};
const schema = z.object({
parentCaId: z.string(),
notAfter: z.string().trim().refine(isValidDate, { message: "Invalid date format" }),
maxPathLength: z.string()
});
export type FormData = z.infer<typeof schema>;
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 [parentCaType] = useState<ParentCaType>(ParentCaType.Internal);
const { currentWorkspace } = useWorkspace();
const caId = (popUp?.installCaCert?.data as { caId: string })?.caId || "";
// const [isStartDatePickerOpen, setIsStartDatePickerOpen] = useState(false);
const { data: cas } = useListWorkspaceCas({
projectSlug: currentWorkspace?.slug ?? "",
status: CaStatus.ACTIVE
});
const { data: ca } = useGetCaById(caId);
const { data: csr } = useGetCaCsr(caId);
const { mutateAsync: signIntermediate } = useSignIntermediate();
const { mutateAsync: importCaCertificate } = useImportCaCertificate();
const {
control,
handleSubmit,
reset,
formState: { isSubmitting },
setValue,
watch
} = useForm<FormData>({
resolver: zodResolver(schema),
defaultValues: {
maxPathLength: "0"
}
});
useEffect(() => {
if (cas?.length) {
setValue("parentCaId", cas[0].id);
}
}, [cas, setValue]);
const parentCaId = watch("parentCaId");
const { data: parentCa } = useGetCaById(parentCaId);
useEffect(() => {
if (parentCa?.maxPathLength) {
setValue(
"maxPathLength",
(parentCa.maxPathLength === -1 ? 3 : parentCa.maxPathLength - 1).toString()
);
}
if (parentCa?.notAfter) {
const parentCaNotAfter = new Date(parentCa.notAfter);
const middleDate = getMiddleDate(new Date(), parentCaNotAfter);
setValue("notAfter", format(middleDate, "yyyy-MM-dd"));
}
}, [parentCa]);
const onFormSubmit = async ({ notAfter, maxPathLength }: FormData) => {
try {
if (!csr || !caId || !currentWorkspace?.slug) return;
const { certificate, certificateChain } = await signIntermediate({
caId: parentCaId,
csr,
maxPathLength: Number(maxPathLength),
notAfter,
notBefore: new Date().toISOString()
});
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"
});
}
};
function generatePathLengthOpts(parentCaMaxPathLength: number): number[] {
if (parentCaMaxPathLength === -1) {
return [-1, 0, 1, 2, 3];
}
return Array.from({ length: parentCaMaxPathLength }, (_, index) => index);
}
const renderForm = (parentCaTypeInput: ParentCaType) => {
switch (parentCaTypeInput) {
case ParentCaType.Internal:
return (
<form onSubmit={handleSubmit(onFormSubmit)}>
<Controller
control={control}
name="parentCaId"
defaultValue=""
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Parent CA"
errorText={error?.message}
isError={Boolean(error)}
className="mt-4"
isRequired
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
>
{(cas || [])
.filter((c) => {
const isParentCaNotSelf = c.id !== ca?.id;
const isParentCaActive = c.status === CaStatus.ACTIVE;
const isParentCaAllowedChildrenCas =
c.maxPathLength && c.maxPathLength !== 0;
return (
isParentCaNotSelf && isParentCaActive && isParentCaAllowedChildrenCas
);
})
.map(({ id, type, dn }) => (
<SelectItem value={id} key={`parent-ca-${id}`}>
{`${caTypeToNameMap[type]}: ${dn}`}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
{/* <Controller
name="notAfter"
control={control}
defaultValue={getDefaultNotAfterDate()}
render={({ field: { onChange, ...field }, fieldState: { error } }) => {
return (
<FormControl
label="Validity"
errorText={error?.message}
isError={Boolean(error)}
className="mr-4"
>
<DatePicker
value={field.value || undefined}
onChange={(date) => {
onChange(date);
setIsStartDatePickerOpen(false);
}}
popUpProps={{
open: isStartDatePickerOpen,
onOpenChange: setIsStartDatePickerOpen
}}
popUpContentProps={{}}
/>
</FormControl>
);
}}
/> */}
<Controller
control={control}
name="notAfter"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Valid Until"
isError={Boolean(error)}
errorText={error?.message}
isRequired
>
<Input {...field} placeholder="YYYY-MM-DD" />
</FormControl>
)}
/>
<Controller
control={control}
name="maxPathLength"
// defaultValue="0"
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Path Length"
errorText={error?.message}
isError={Boolean(error)}
className="mt-4"
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
>
{generatePathLengthOpts(parentCa?.maxPathLength || 0).map((value) => (
<SelectItem value={String(value)} key={`ca-path-length-${value}`}>
{`${value}`}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
<div className="flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
Install
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpToggle("installCaCert", false)}
>
Cancel
</Button>
</div>
</form>
);
default:
return <div>External TODO</div>;
}
};
return (
<Modal
isOpen={popUp?.installCaCert?.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle("installCaCert", isOpen);
reset();
}}
>
<ModalContent title="Install Intermediate CA certificate">
{/* <FormControl label="Parent CA Type" className="mt-4">
<Select
defaultValue={ParentCaType.Internal}
value={parentCaType}
onValueChange={(e) => setParentCaType(e as ParentCaType)}
className="w-full"
>
<SelectItem
value={ParentCaType.Internal}
key={`parent-ca-type-${ParentCaType.Internal}`}
>
Infisical Private CA
</SelectItem>
<SelectItem
value={ParentCaType.External}
key={`parent-ca-type-${ParentCaType.External}`}
>
External Private CA
</SelectItem>
</Select>
</FormControl> */}
{renderForm(parentCaType)}
</ModalContent>
</Modal>
);
};
@@ -0,0 +1 @@
export { CaInstallCertModal } from "./CaInstallCertModal";
@@ -1,8 +1,7 @@
// import { useEffect } from "react";
import { useEffect } from "react";
import { Controller, useForm } from "react-hook-form";
// import { yupResolver } from "@hookform/resolvers/yup";
// import * as yup from "yup";
import { zodResolver } from "@hookform/resolvers/zod";
import { format } from "date-fns";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
@@ -14,21 +13,34 @@ import {
ModalContent,
Select,
SelectItem
// DatePicker
} from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { CertificateAuthorityType, useCreateCa } from "@app/hooks/api/ca";
// import { useCreateIdentity, useGetOrgRoles, useUpdateIdentity } from "@app/hooks/api";
import { CaType, useCreateCa, useGetCaById } from "@app/hooks/api/ca";
import { UsePopUpState } from "@app/hooks/usePopUp";
const isValidDate = (dateString: string) => {
const date = new Date(dateString);
return !Number.isNaN(date.getTime());
};
const getDateTenYearsFromToday = () => {
const date = new Date();
date.setFullYear(date.getFullYear() + 10);
return format(date, "yyyy-MM-dd");
};
const schema = z
.object({
type: z.enum(["root", "intermediate"]), // move to ref enum of hooks/api
type: z.enum([CaType.ROOT, CaType.INTERMEDIATE]),
organization: z.string(),
ou: z.string(),
country: z.string(),
province: z.string(),
locality: z.string(),
commonName: z.string()
commonName: z.string(),
notAfter: z.string().trim().refine(isValidDate, { message: "Invalid date format" }),
maxPathLength: z.string()
})
.required();
@@ -36,75 +48,71 @@ export type FormData = z.infer<typeof schema>;
type Props = {
popUp: UsePopUpState<["ca"]>;
// handlePopUpOpen: (
// popUpName: keyof UsePopUpState<["identityAuthMethod"]>,
// data: {
// identityId: string;
// name: string;
// authMethod?: IdentityAuthMethod;
// }
// ) => void;
handlePopUpToggle: (popUpName: keyof UsePopUpState<["ca"]>, state?: boolean) => void;
};
const caTypes = [
{ label: "Root", value: CertificateAuthorityType.ROOT },
{ label: "intermediate", value: CertificateAuthorityType.INTERMEDIATE }
{ label: "Root", value: CaType.ROOT },
{ label: "Intermediate", value: CaType.INTERMEDIATE }
];
export const CaModal = ({ popUp, handlePopUpToggle }: Props) => {
const { currentWorkspace } = useWorkspace();
console.log("CaModal currentWorkspace: ", currentWorkspace);
// const [isStartDatePickerOpen, setIsStartDatePickerOpen] = useState(false);
const { data: ca } = useGetCaById((popUp?.ca?.data as { caId: string })?.caId || "");
const { mutateAsync: createMutateAsync } = useCreateCa();
// const { data: roles } = useGetOrgRoles(orgId);
// const { mutateAsync: createMutateAsync } = useCreateIdentity();
// const { mutateAsync: updateMutateAsync } = useUpdateIdentity();
// const { mutateAsync: addMutateAsync } = useAddIdentityUniversalAuth();
const {
control,
handleSubmit,
reset,
formState: { isSubmitting }
formState: { isSubmitting },
watch
} = useForm<FormData>({
resolver: zodResolver(schema),
defaultValues: {
type: CertificateAuthorityType.ROOT,
type: CaType.ROOT,
organization: "",
ou: "",
country: "",
province: "",
locality: "",
commonName: ""
commonName: "",
notAfter: getDateTenYearsFromToday(),
maxPathLength: "-1"
}
});
// useEffect(() => {
// const identity = popUp?.identity?.data as {
// identityId: string;
// name: string;
// role: string;
// customRole: {
// name: string;
// slug: string;
// };
// };
const caType = watch("type");
// if (!roles?.length) return;
// if (identity) {
// reset({
// name: identity.name,
// role: identity?.customRole?.slug ?? identity.role
// });
// } else {
// reset({
// name: "",
// role: roles[0].slug
// });
// }
// }, [popUp?.identity?.data, roles]);
useEffect(() => {
if (ca) {
reset({
type: ca.type,
organization: ca.organization,
ou: ca.ou,
country: ca.country,
province: ca.province,
locality: ca.locality,
commonName: ca.commonName,
notAfter: ca.notAfter ? format(new Date(ca.notAfter), "yyyy-MM-dd") : "",
maxPathLength: ca.maxPathLength ? String(ca.maxPathLength) : ""
});
} else {
reset({
type: CaType.ROOT,
organization: "",
ou: "",
country: "",
province: "",
locality: "",
commonName: "",
notAfter: getDateTenYearsFromToday(),
maxPathLength: "-1"
});
}
}, [ca]);
const onFormSubmit = async ({
type,
@@ -113,18 +121,11 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => {
ou,
country,
locality,
province
province,
notAfter,
maxPathLength
}: FormData) => {
try {
console.log("onFormSubmit args: ", {
commonName,
organization,
ou,
country,
locality,
province
});
if (!currentWorkspace?.slug) return;
await createMutateAsync({
@@ -135,90 +136,47 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => {
ou,
country,
province,
locality
});
// const identity = popUp?.identity?.data as {
// identityId: string;
// name: string;
// role: string;
// };
// if (identity) {
// // update
// await updateMutateAsync({
// identityId: identity.identityId,
// name,
// role: role || undefined,
// organizationId: orgId
// });
// handlePopUpToggle("identity", false);
// } else {
// // create
// const {
// id: createdId,
// name: createdName,
// authMethod
// } = await createMutateAsync({
// name,
// role: role || undefined,
// organizationId: orgId
// });
// await addMutateAsync({
// organizationId: orgId,
// identityId: createdId,
// clientSecretTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }],
// accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }],
// accessTokenTTL: 2592000,
// accessTokenMaxTTL: 2592000,
// accessTokenNumUsesLimit: 0
// });
// handlePopUpToggle("identity", false);
// handlePopUpOpen("identityAuthMethod", {
// identityId: createdId,
// name: createdName,
// authMethod
// });
// }
createNotification({
text: `Successfully ${popUp?.ca?.data ? "updated" : "created"} CA`,
type: "success"
locality,
notAfter,
maxPathLength: Number(maxPathLength)
});
reset();
} catch (err) {
console.error(err);
const error = err as any;
const text =
error?.response?.data?.message ?? `Failed to ${popUp?.ca?.data ? "update" : "create"} CA`;
handlePopUpToggle("ca", false);
createNotification({
text,
text: "Successfully created CA",
type: "success"
});
} catch (err) {
console.error(err);
createNotification({
text: "Failed to create CA",
type: "error"
});
}
};
// const getDefaultNotAfterDate = () => {
// const date = new Date();
// date.setFullYear(date.getFullYear() + 10);
// return date;
// };
return (
<Modal
isOpen={popUp?.ca?.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle("ca", isOpen);
reset();
handlePopUpToggle("ca", isOpen);
}}
>
<ModalContent title={`${popUp?.ca?.data ? "Update" : "Create"} CA`}>
<ModalContent title={`${ca ? "View" : "Create"} Private CA`}>
<form onSubmit={handleSubmit(onFormSubmit)}>
<Controller
control={control}
name="type"
defaultValue={CertificateAuthorityType.ROOT}
defaultValue={CaType.ROOT}
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl label="CA Type" errorText={error?.message} isError={Boolean(error)}>
<Select
@@ -226,7 +184,7 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => {
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
// isDisabled={!!identityAuthMethodData?.authMethod}
isDisabled={Boolean(ca)}
>
{caTypes.map(({ label, value }) => (
<SelectItem value={String(value || "")} key={label}>
@@ -237,6 +195,80 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => {
</FormControl>
)}
/>
{caType === CaType.ROOT && (
<>
{/* <Controller
name="notAfter"
control={control}
defaultValue={getDefaultNotAfterDate()}
render={({ field: { onChange, ...field }, fieldState: { error } }) => {
return (
<FormControl
label="Validity"
errorText={error?.message}
isError={Boolean(error)}
className="mr-4"
>
<DatePicker
value={field.value || undefined}
onChange={(date) => {
onChange(date);
setIsStartDatePickerOpen(false);
}}
popUpProps={{
open: isStartDatePickerOpen,
onOpenChange: setIsStartDatePickerOpen
}}
popUpContentProps={{}}
/>
</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" isDisabled={Boolean(ca)} />
</FormControl>
)}
/>
<Controller
control={control}
name="maxPathLength"
defaultValue="-1"
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Path Length"
errorText={error?.message}
isError={Boolean(error)}
className="mt-4"
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
isDisabled={Boolean(ca)}
>
{[-1, 0, 1, 2, 3, 4].map((value) => (
<SelectItem value={String(value)} key={`ca-path-length-${value}`}>
{`${value}`}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
</>
)}
<Controller
control={control}
defaultValue=""
@@ -247,7 +279,7 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => {
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="Acme Corp" />
<Input {...field} placeholder="Acme Corp" isDisabled={Boolean(ca)} />
</FormControl>
)}
/>
@@ -261,7 +293,7 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => {
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="Engineering" />
<Input {...field} placeholder="Engineering" isDisabled={Boolean(ca)} />
</FormControl>
)}
/>
@@ -275,7 +307,7 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => {
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="United States (US)" />
<Input {...field} placeholder="United States (US)" isDisabled={Boolean(ca)} />
</FormControl>
)}
/>
@@ -289,7 +321,7 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => {
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="California" />
<Input {...field} placeholder="California" isDisabled={Boolean(ca)} />
</FormControl>
)}
/>
@@ -303,7 +335,7 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => {
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="San Francisco" />
<Input {...field} placeholder="San Francisco" isDisabled={Boolean(ca)} />
</FormControl>
)}
/>
@@ -317,28 +349,30 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => {
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="Example CA" />
<Input {...field} placeholder="Example CA" isDisabled={Boolean(ca)} />
</FormControl>
)}
/>
<div className="flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
{popUp?.ca?.data ? "Update" : "Create"}
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpToggle("ca", false)}
>
Cancel
</Button>
</div>
{!ca && (
<div className="flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
{popUp?.ca?.data ? "Update" : "Create"}
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpToggle("ca", false)}
>
Cancel
</Button>
</div>
)}
</form>
</ModalContent>
</Modal>
@@ -1,141 +1,126 @@
// import { useState } from "react";
import { faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
// import { createNotification } from "@app/components/notifications";
// import { OrgPermissionCan } from "@app/components/permissions";
import {
Button
// DeleteActionModal,
// EmailServiceSetupModal,
// UpgradePlanModal
} from "@app/components/v2";
// import {
// OrgPermissionActions,
// OrgPermissionSubjects,
// useOrganization,
// useSubscription
// } from "@app/context";
// import { useDeleteOrgMembership } from "@app/hooks/api";
import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions";
import { Button, DeleteActionModal } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { CaStatus,useDeleteCa, useUpdateCa } from "@app/hooks/api";
import { usePopUp } from "@app/hooks/usePopUp";
import { CaCertModal } from "./CaCertModal";
import { CaInstallCertModal } from "./CaInstallCertModal";
import { CaModal } from "./CaModal";
import { CaTable } from "./CaTable";
// import { AddOrgMemberModal } from "./AddOrgMemberModal";
// import { OrgMembersTable } from "./OrgMembersTable";
export const CaSection = () => {
// const { subscription } = useSubscription();
// const { currentOrg } = useOrganization();
// const orgId = currentOrg?.id ?? "";
const { currentWorkspace } = useWorkspace();
const { mutateAsync: deleteCa } = useDeleteCa();
const { mutateAsync: updateCa } = useUpdateCa();
// const [completeInviteLink, setCompleteInviteLink] = useState<string>("");
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"ca",
"caCert",
"installCaCert",
"deleteCa",
"caStatus" // enable / disable
] as const);
const {
popUp,
handlePopUpOpen,
// handlePopUpClose,
handlePopUpToggle
} = usePopUp(["ca"] as const);
const onRemoveCaSubmit = async (caId: string) => {
try {
if (!currentWorkspace?.slug) return;
// const { mutateAsync: deleteMutateAsync } = useDeleteOrgMembership();
await deleteCa({ caId, projectSlug: currentWorkspace.slug });
// const isMoreUsersNotAllowed = subscription?.memberLimit
// ? subscription.membersUsed >= subscription.memberLimit
// : false;
await createNotification({
text: "Successfully deleted CA",
type: "success"
});
// const handleAddMemberModal = () => {
// if (currentOrg?.authEnforced) {
// createNotification({
// text: "You cannot manage users from Infisical when org-level auth is enforced for your organization",
// type: "error"
// });
// return;
// }
handlePopUpClose("deleteCa");
} catch (err) {
console.error(err);
createNotification({
text: "Failed to delete CA",
type: "error"
});
}
};
// if (isMoreUsersNotAllowed) {
// handlePopUpOpen("upgradePlan", {
// description: "You can add more members if you upgrade your Infisical plan."
// });
// } else {
// handlePopUpOpen("addMember");
// }
// };
const onUpdateCaStatus = async ({ caId, status }: { caId: string; status: CaStatus }) => {
try {
if (!currentWorkspace?.slug) return;
// const onRemoveMemberSubmit = async (orgMembershipId: string) => {
// try {
// await deleteMutateAsync({
// orgId,
// membershipId: orgMembershipId
// });
await updateCa({ caId, projectSlug: currentWorkspace.slug, status });
// createNotification({
// text: "Successfully removed user from org",
// type: "success"
// });
// } catch (err) {
// console.error(err);
// createNotification({
// text: "Failed to remove user from the organization",
// type: "error"
// });
// }
await createNotification({
text: `Successfully ${status === CaStatus.ACTIVE ? "enabled" : "disabled"} CA`,
type: "success"
});
// handlePopUpClose("removeMember");
// };
handlePopUpClose("caStatus");
} catch (err) {
console.error(err);
createNotification({
text: `Failed to ${status === CaStatus.ACTIVE ? "enabled" : "disabled"} CA`,
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">Certificate Authorities</p>
{/* <OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Member}>
{(isAllowed) => ( */}
<Button
colorSchema="primary"
type="submit"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => handlePopUpOpen("ca")}
// isDisabled={!isAllowed}
<ProjectPermissionCan
I={ProjectPermissionActions.Create}
a={ProjectPermissionSub.CertificateAuthorities}
>
Create CA
</Button>
{/* )} */}
{/* </OrgPermissionCan> */}
{(isAllowed) => (
<Button
colorSchema="primary"
type="submit"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => handlePopUpOpen("ca")}
isDisabled={!isAllowed}
>
Create CA
</Button>
)}
</ProjectPermissionCan>
</div>
<CaModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<CaTable />
{/* <OrgMembersTable
handlePopUpOpen={handlePopUpOpen}
setCompleteInviteLink={setCompleteInviteLink}
/>
<AddOrgMemberModal
popUp={popUp}
handlePopUpToggle={handlePopUpToggle}
completeInviteLink={completeInviteLink}
setCompleteInviteLink={setCompleteInviteLink}
<CaInstallCertModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<CaCertModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<CaTable handlePopUpOpen={handlePopUpOpen} />
<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)}
/>
<DeleteActionModal
isOpen={popUp.removeMember.isOpen}
title={`Are you sure want to remove member with username ${
(popUp?.removeMember?.data as { username: string })?.username || ""
}?`}
onChange={(isOpen) => handlePopUpToggle("removeMember", isOpen)}
isOpen={popUp.caStatus.isOpen}
title={`Are you sure want to ${
(popUp?.caStatus?.data as { status: string })?.status === CaStatus.ACTIVE
? "enable"
: "disable"
} the CA ${(popUp?.caStatus?.data as { dn: string })?.dn || ""} from the project?`}
subTitle={
(popUp?.caStatus?.data as { status: string })?.status === CaStatus.ACTIVE
? "This action will allow the CA to start issuing certificates again."
: "This action will prevent the CA from issuing new certificates."
}
onChange={(isOpen) => handlePopUpToggle("caStatus", isOpen)}
deleteKey="confirm"
onDeleteApproved={() =>
onRemoveMemberSubmit(
(popUp?.removeMember?.data as { orgMembershipId: string })?.orgMembershipId
)
onUpdateCaStatus(popUp?.caStatus?.data as { caId: string; status: CaStatus })
}
/>
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text={(popUp.upgradePlan?.data as { description: string })?.description}
/>
<EmailServiceSetupModal
isOpen={popUp.setUpEmail?.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("setUpEmail", isOpen)}
/> */}
</div>
);
};
@@ -1,6 +1,20 @@
import { faCertificate } from "@fortawesome/free-solid-svg-icons";
import {
faBan,
faCertificate,
faEllipsis,
faEye,
faTrash
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { format } from "date-fns";
import { twMerge } from "tailwind-merge";
import { ProjectPermissionCan } from "@app/components/permissions";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
EmptyState,
Table,
TableContainer,
@@ -9,15 +23,29 @@ import {
Td,
Th,
THead,
Tr
} from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { useListWorkspaceCas } from "@app/hooks/api";
import { caTypeToNameMap } from "@app/hooks/api/ca/constants";
Tooltip,
Tr} from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { CaStatus, useListWorkspaceCas } from "@app/hooks/api";
import { caStatusToNameMap,caTypeToNameMap } from "@app/hooks/api/ca/constants";
import { UsePopUpState } from "@app/hooks/usePopUp";
export const CaTable = () => {
type Props = {
handlePopUpOpen: (
popUpName: keyof UsePopUpState<["installCaCert", "caCert", "ca", "deleteCa", "caStatus"]>,
data?: {
caId?: string;
dn?: string;
status?: CaStatus;
}
) => void;
};
export const CaTable = ({ handlePopUpOpen }: Props) => {
const { currentWorkspace } = useWorkspace();
const { data, isLoading } = useListWorkspaceCas(currentWorkspace?.slug ?? "");
const { data, isLoading } = useListWorkspaceCas({
projectSlug: currentWorkspace?.slug ?? ""
});
return (
<div>
<TableContainer>
@@ -27,6 +55,8 @@ export const CaTable = () => {
<Th>Subject</Th>
<Th>Status</Th>
<Th>Type</Th>
<Th>Valid Until</Th>
<Th />
</Tr>
</THead>
<TBody>
@@ -38,15 +68,148 @@ export const CaTable = () => {
return (
<Tr className="h-10" key={`ca-${ca.id}`}>
<Td>{ca.dn}</Td>
<Td>Pending</Td>
<Td>{caStatusToNameMap[ca.status]}</Td>
<Td>{caTypeToNameMap[ca.type]}</Td>
<Td>{ca.notAfter ? format(new Date(ca.notAfter), "yyyy-MM-dd") : "-"}</Td>
<Td className="flex justify-end">
<DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg">
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
<Tooltip content="More options">
<FontAwesomeIcon size="lg" icon={faEllipsis} />
</Tooltip>
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1">
{ca.status === CaStatus.PENDING_CERTIFICATE && (
<ProjectPermissionCan
I={ProjectPermissionActions.Create}
a={ProjectPermissionSub.CertificateAuthorities}
>
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
!isAllowed &&
"pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={async () => {
handlePopUpOpen("installCaCert", {
caId: ca.id
});
}}
disabled={!isAllowed}
icon={<FontAwesomeIcon icon={faCertificate} />}
>
Install Certificate
</DropdownMenuItem>
)}
</ProjectPermissionCan>
)}
{ca.status !== CaStatus.PENDING_CERTIFICATE && (
<ProjectPermissionCan
I={ProjectPermissionActions.Read}
a={ProjectPermissionSub.CertificateAuthorities}
>
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
!isAllowed &&
"pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={async () => {
handlePopUpOpen("caCert", {
caId: ca.id
});
}}
disabled={!isAllowed}
icon={<FontAwesomeIcon icon={faCertificate} />}
>
View Certificate
</DropdownMenuItem>
)}
</ProjectPermissionCan>
)}
<ProjectPermissionCan
I={ProjectPermissionActions.Read}
a={ProjectPermissionSub.CertificateAuthorities}
>
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={async () =>
handlePopUpOpen("ca", {
caId: ca.id
})
}
disabled={!isAllowed}
icon={<FontAwesomeIcon icon={faEye} />}
>
View CA
</DropdownMenuItem>
)}
</ProjectPermissionCan>
{(ca.status === CaStatus.ACTIVE || ca.status === CaStatus.DISABLED) && (
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={ProjectPermissionSub.CertificateAuthorities}
>
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
!isAllowed &&
"pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={async () =>
handlePopUpOpen("caStatus", {
caId: ca.id,
status:
ca.status === CaStatus.ACTIVE
? CaStatus.DISABLED
: CaStatus.ACTIVE
})
}
disabled={!isAllowed}
icon={<FontAwesomeIcon icon={faBan} />}
>
{`${ca.status === CaStatus.ACTIVE ? "Disable" : "Enable"} CA`}
</DropdownMenuItem>
)}
</ProjectPermissionCan>
)}
<ProjectPermissionCan
I={ProjectPermissionActions.Delete}
a={ProjectPermissionSub.CertificateAuthorities}
>
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={async () =>
handlePopUpOpen("deleteCa", {
caId: ca.id,
dn: ca.dn
})
}
disabled={!isAllowed}
icon={<FontAwesomeIcon icon={faTrash} />}
>
Delete CA
</DropdownMenuItem>
)}
</ProjectPermissionCan>
</DropdownMenuContent>
</DropdownMenu>
</Td>
</Tr>
);
})}
</TBody>
</Table>
{!isLoading && data?.length === 0 && (
<EmptyState title="No groups have been added to this project" icon={faCertificate} />
<EmptyState title="No certificate authorities have been created" icon={faCertificate} />
)}
</TableContainer>
</div>
@@ -0,0 +1,17 @@
import { motion } from "framer-motion";
import { CertificatesSection } from "./components";
export const CertificatesTab = () => {
return (
<motion.div
key="panel-certificates"
transition={{ duration: 0.15 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: 30 }}
>
<CertificatesSection />
</motion.div>
);
};
@@ -0,0 +1,135 @@
import { useEffect } from "react";
import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { IconButton,Modal, ModalContent } from "@app/components/v2";
import { useToggle } from "@app/hooks";
import { useGetCertCert } from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
type Props = {
popUp: UsePopUpState<["certificateCert"]>;
handlePopUpToggle: (popUpName: keyof UsePopUpState<["certificateCert"]>, state?: boolean) => void;
};
export const CertificateCertModal = ({ popUp, handlePopUpToggle }: Props) => {
const { data } = useGetCertCert(
(popUp?.certificateCert?.data as { certId: string })?.certId || ""
);
const [isSerialNumberCopied, setIsSerialNumberCopied] = useToggle(false);
const [isCertificateCopied, setIsCertificateCopied] = useToggle(false);
const [isCertificateChainCopied, setIsCertificateChainCopied] = useToggle(false);
const [isCertificateSkCopied, setIsCertificateSkCopied] = useToggle(false);
useEffect(() => {
let timer: NodeJS.Timeout;
if (isSerialNumberCopied) {
timer = setTimeout(() => setIsSerialNumberCopied.off(), 2000);
}
if (isCertificateCopied) {
timer = setTimeout(() => setIsCertificateCopied.off(), 2000);
}
if (isCertificateChainCopied) {
timer = setTimeout(() => setIsCertificateChainCopied.off(), 2000);
}
if (isCertificateSkCopied) {
timer = setTimeout(() => setIsCertificateSkCopied.off(), 2000);
}
return () => clearTimeout(timer);
}, [isSerialNumberCopied, isCertificateCopied, isCertificateChainCopied, isCertificateSkCopied]);
return (
<Modal
isOpen={popUp?.certificateCert?.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle("certificateCert", isOpen);
}}
>
<ModalContent title="Export Certificate">
{data ? (
<div>
<h2 className="mb-4">Serial Number</h2>
<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 break-all">{data.serialNumber}</p>
<IconButton
ariaLabel="copy icon"
colorSchema="secondary"
className="group relative"
onClick={() => {
navigator.clipboard.writeText(data.serialNumber);
setIsSerialNumberCopied.on();
}}
>
<FontAwesomeIcon icon={isSerialNumberCopied ? 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">
Click to copy
</span>
</IconButton>
</div>
<h2 className="mb-4">Certificate Body</h2>
<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">{data.certificate}</p>
<IconButton
ariaLabel="copy icon"
colorSchema="secondary"
className="group relative"
onClick={() => {
navigator.clipboard.writeText(data.certificate);
setIsCertificateCopied.on();
}}
>
<FontAwesomeIcon icon={isCertificateCopied ? 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">
Click to copy
</span>
</IconButton>
</div>
<h2 className="mb-4">Certificate Chain</h2>
<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">{data.certificateChain}</p>
<IconButton
ariaLabel="copy icon"
colorSchema="secondary"
className="group relative"
onClick={() => {
navigator.clipboard.writeText(data.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">
Click to copy
</span>
</IconButton>
</div>
<h2 className="mb-4">Certificate Private Key</h2>
<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">{data.privateKey}</p>
<IconButton
ariaLabel="copy icon"
colorSchema="secondary"
className="group relative"
onClick={() => {
navigator.clipboard.writeText(data.certificateChain);
setIsCertificateSkCopied.on();
}}
>
<FontAwesomeIcon icon={isCertificateSkCopied ? 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">
Click to copy
</span>
</IconButton>
</div>
</div>
) : (
<div />
)}
</ModalContent>
</Modal>
);
};
@@ -0,0 +1,223 @@
import { useEffect } from "react";
import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { format } from "date-fns";
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 {
CaStatus,
useCreateCertificate,
useGetCertById,
useListWorkspaceCas
} from "@app/hooks/api";
import { caTypeToNameMap } from "@app/hooks/api/ca/constants";
import { UsePopUpState } from "@app/hooks/usePopUp";
const isValidDate = (dateString: string) => {
if (dateString === "") return true;
const date = new Date(dateString);
return !Number.isNaN(date.getTime());
};
const schema = z.object({
caId: z.string(),
commonName: z.string().trim().min(1),
ttl: z.string().trim().optional(),
notAfter: z
.string()
.trim()
.refine(isValidDate, { message: "Invalid date format" })
.transform((val) => (val === "" ? undefined : val))
});
export type FormData = z.infer<typeof schema>;
type Props = {
popUp: UsePopUpState<["certificate"]>;
handlePopUpToggle: (popUpName: keyof UsePopUpState<["certificate"]>, state?: boolean) => void;
};
export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
const { currentWorkspace } = useWorkspace();
const { data: cert } = useGetCertById(
(popUp?.certificate?.data as { certId: string })?.certId || ""
);
const { data: cas } = useListWorkspaceCas({
projectSlug: currentWorkspace?.slug ?? "",
status: CaStatus.ACTIVE
});
const { mutateAsync: createCertificate } = useCreateCertificate();
const {
control,
handleSubmit,
reset,
formState: { isSubmitting },
setValue
} = useForm<FormData>({
resolver: zodResolver(schema)
});
useEffect(() => {
if (cert) {
reset({
caId: cert.caId,
commonName: cert.commonName,
ttl: "",
notAfter: format(new Date(cert.notAfter), "yyyy-MM-dd")
});
} else {
reset({
caId: "",
commonName: "",
ttl: "",
notAfter: ""
});
}
}, [cert]);
const onFormSubmit = async ({ caId, commonName, ttl, notAfter }: FormData) => {
try {
if (!currentWorkspace?.slug) return;
await createCertificate({
projectSlug: currentWorkspace.slug,
caId,
commonName,
ttl: ttl ? Number(ttl) : undefined,
notBefore: new Date().toISOString(),
notAfter
});
reset();
handlePopUpToggle("certificate", false);
createNotification({
text: "Successfully created certificate",
type: "success"
});
} catch (err) {
console.error(err);
createNotification({
text: "Failed to create certificate",
type: "error"
});
}
};
useEffect(() => {
if (cas?.length) {
setValue("caId", cas[0].id);
}
}, [cas, setValue]);
return (
<Modal
isOpen={popUp?.certificate?.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle("certificate", isOpen);
reset();
}}
>
<ModalContent title={`${cert ? "View" : "Issue"} Certificate`}>
<form onSubmit={handleSubmit(onFormSubmit)}>
<Controller
control={control}
name="caId"
defaultValue=""
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Issuing CA"
errorText={error?.message}
isError={Boolean(error)}
className="mt-4"
isRequired
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
isDisabled={Boolean(cert)}
>
{(cas || []).map(({ id, type, dn }) => (
<SelectItem value={id} key={`ca-${id}`}>
{`${caTypeToNameMap[type]}: ${dn}`}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
<Controller
control={control}
defaultValue=""
name="commonName"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Common Name (CN)"
isError={Boolean(error)}
errorText={error?.message}
isRequired
>
<Input {...field} placeholder="Acme Corp" isDisabled={Boolean(cert)} />
</FormControl>
)}
/>
<Controller
control={control}
name="ttl"
render={({ field, fieldState: { error } }) => (
<FormControl
label="TTL (seconds)"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="86400" isDisabled={Boolean(cert)} />
</FormControl>
)}
/>
<Controller
control={control}
defaultValue=""
name="notAfter"
render={({ field, fieldState: { error } }) => (
<FormControl label="Valid Until" isError={Boolean(error)} errorText={error?.message}>
<Input {...field} placeholder="YYYY-MM-DD" isDisabled={Boolean(cert)} />
</FormControl>
)}
/>
{!cert && (
<div className="flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
Create
</Button>
<Button colorSchema="secondary" variant="plain">
Cancel
</Button>
</div>
)}
</form>
</ModalContent>
</Modal>
);
};
@@ -0,0 +1,79 @@
import { faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createNotification } from "@app/components/notifications";
import { Button, DeleteActionModal } from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { useDeleteCert } from "@app/hooks/api";
import { usePopUp } from "@app/hooks/usePopUp";
import { CertificateCertModal } from "./CertificateCertModal";
import { CertificateModal } from "./CertificateModal";
import { CertificatesTable } from "./CertificatesTable";
export const CertificatesSection = () => {
const { currentWorkspace } = useWorkspace();
const { mutateAsync: deleteCert } = useDeleteCert();
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"certificate",
"certificateCert",
"deleteCertificate"
] as const);
const onRemoveCertificateSubmit = async (certId: string) => {
try {
if (!currentWorkspace?.slug) return;
await deleteCert({ certId, projectSlug: currentWorkspace.slug });
await createNotification({
text: "Successfully deleted certificate",
type: "success"
});
handlePopUpClose("deleteCertificate");
} catch (err) {
console.error(err);
createNotification({
text: "Failed to delete 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}
>
Issue Certificate
</Button>
{/* )} */}
{/* </OrgPermissionCan> */}
</div>
<CertificatesTable handlePopUpOpen={handlePopUpOpen} />
<CertificateModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<CertificateCertModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<DeleteActionModal
isOpen={popUp.deleteCertificate.isOpen}
title={`Are you sure want to remove the certificate ${
(popUp?.deleteCertificate?.data as { commonName: string })?.commonName || ""
} from the project?`}
onChange={(isOpen) => handlePopUpToggle("deleteCertificate", isOpen)}
deleteKey="confirm"
onDeleteApproved={() =>
onRemoveCertificateSubmit((popUp?.deleteCertificate?.data as { certId: string })?.certId)
}
/>
</div>
);
};
@@ -0,0 +1,160 @@
import {
faCertificate,
faEllipsis,
faEye,
faFileExport,
faTrash
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { format } from "date-fns";
import { twMerge } from "tailwind-merge";
import { ProjectPermissionCan } from "@app/components/permissions";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
EmptyState,
Table,
TableContainer,
TableSkeleton,
TBody,
Td,
Th,
THead,
Tooltip,
Tr} from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { useListWorkspaceCertificates } from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
type Props = {
handlePopUpOpen: (
popUpName: keyof UsePopUpState<["certificate", "deleteCertificate", "certificateCert"]>,
data?: {
certId?: string;
commonName?: string;
}
) => void;
};
export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
const { currentWorkspace } = useWorkspace();
const { data, isLoading } = useListWorkspaceCertificates(currentWorkspace?.slug ?? "");
return (
<div>
<TableContainer>
<Table>
<THead>
<Tr>
<Th>Certificate ID</Th>
<Th>Common Name</Th>
<Th>Valid Until</Th>
<Th />
</Tr>
</THead>
<TBody>
{isLoading && <TableSkeleton columns={3} innerKey="project-cas" />}
{!isLoading &&
data &&
data.length > 0 &&
data.map((certificate) => {
return (
<Tr className="h-10" key={`certificate-${certificate.id}`}>
<Td>{certificate.id}</Td>
<Td>{certificate.commonName}</Td>
<Td>
{certificate.notAfter
? format(new Date(certificate.notAfter), "yyyy-MM-dd")
: "-"}
</Td>
<Td className="flex justify-end">
<DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg">
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
<Tooltip content="More options">
<FontAwesomeIcon size="lg" icon={faEllipsis} />
</Tooltip>
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1">
<ProjectPermissionCan
I={ProjectPermissionActions.Read}
a={ProjectPermissionSub.Certificates}
>
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={async () =>
handlePopUpOpen("certificateCert", {
certId: certificate.id
})
}
disabled={!isAllowed}
icon={<FontAwesomeIcon icon={faFileExport} />}
>
Export Certificate
</DropdownMenuItem>
)}
</ProjectPermissionCan>
<ProjectPermissionCan
I={ProjectPermissionActions.Read}
a={ProjectPermissionSub.Certificates}
>
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={async () =>
handlePopUpOpen("certificate", {
certId: certificate.id
})
}
disabled={!isAllowed}
icon={<FontAwesomeIcon icon={faEye} />}
>
View Details
</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("deleteCertificate", {
certId: certificate.id,
commonName: certificate.commonName
})
}
disabled={!isAllowed}
icon={<FontAwesomeIcon icon={faTrash} />}
>
Delete Certificate
</DropdownMenuItem>
)}
</ProjectPermissionCan>
</DropdownMenuContent>
</DropdownMenu>
</Td>
</Tr>
);
})}
</TBody>
</Table>
{!isLoading && data?.length === 0 && (
<EmptyState title="No certificates have been created" icon={faCertificate} />
)}
</TableContainer>
</div>
);
};
@@ -0,0 +1 @@
export { CertificatesSection } from "./CertificatesSection";
@@ -0,0 +1 @@
export { CertificatesTab } from "./CertificatesTab";
@@ -1 +1,2 @@
export { CaTab } from "./CaTab";
export { CertificatesTab } from "./CertificatesTab";