Resolve PR issues

This commit is contained in:
Tuan Dang
2024-06-12 13:11:21 -07:00
parent 868d0345d6
commit f5322abe85
26 changed files with 244 additions and 243 deletions
+4 -2
View File
@@ -7,6 +7,7 @@ export type TCertificateAuthority = {
projectId: string;
type: CaType;
status: CaStatus;
friendlyName: string;
organization: string;
ou: string;
country: string;
@@ -25,6 +26,7 @@ export type TCertificateAuthority = {
export type TCreateCaDTO = {
projectSlug: string;
type: string;
friendlyName?: string;
organization: string;
ou: string;
country: string;
@@ -74,12 +76,12 @@ export type TImportCaCertificateResponse = {
caId: string;
};
// TODO: add TTL
export type TCreateCertificateDTO = {
projectSlug: string;
caId: string;
friendlyName?: string;
commonName: string;
ttl?: number;
ttl: string; // string compatible with ms
notBefore?: string;
notAfter?: string;
};
@@ -4,6 +4,7 @@ export type TCertificate = {
id: string;
caId: string;
status: CertStatus;
friendlyName: string;
commonName: string;
serialNumber: string;
notBefore: string;
@@ -14,7 +14,7 @@ export const CertificatesPage = withProjectPermission(
return (
<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>
<p className="mr-4 mb-4 text-3xl font-semibold text-white">Internal PKI</p>
<Tabs defaultValue={TabSections.Certificates}>
<TabList>
<Tab value={TabSections.Certificates}>Certificates</Tab>
@@ -35,6 +35,7 @@ const getDateTenYearsFromToday = () => {
const schema = z
.object({
type: z.enum([CaType.ROOT, CaType.INTERMEDIATE]),
friendlyName: z.string(),
organization: z.string(),
ou: z.string(),
country: z.string(),
@@ -81,6 +82,7 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => {
resolver: zodResolver(schema),
defaultValues: {
type: CaType.ROOT,
friendlyName: "",
organization: "",
ou: "",
country: "",
@@ -99,6 +101,7 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => {
if (ca) {
reset({
type: ca.type,
friendlyName: ca.friendlyName,
organization: ca.organization,
ou: ca.ou,
country: ca.country,
@@ -112,6 +115,7 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => {
} else {
reset({
type: CaType.ROOT,
friendlyName: "",
organization: "",
ou: "",
country: "",
@@ -127,6 +131,7 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => {
const onFormSubmit = async ({
type,
friendlyName,
commonName,
organization,
ou,
@@ -143,6 +148,7 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => {
await createMutateAsync({
projectSlug: currentWorkspace.slug,
type,
friendlyName,
commonName,
organization,
ou,
@@ -170,12 +176,6 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => {
}
};
// const getDefaultNotAfterDate = () => {
// const date = new Date();
// date.setFullYear(date.getFullYear() + 10);
// return date;
// };
return (
<Modal
isOpen={popUp?.ca?.isOpen}
@@ -308,6 +308,20 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => {
</FormControl>
)}
/>
<Controller
control={control}
defaultValue=""
name="friendlyName"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Friendly Name"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="My CA" isDisabled={Boolean(ca)} />
</FormControl>
)}
/>
<Controller
control={control}
defaultValue=""
@@ -56,7 +56,7 @@ export const CaTable = ({ handlePopUpOpen }: Props) => {
<Table>
<THead>
<Tr>
<Th>Subject</Th>
<Th>Friendly Name</Th>
<Th>Status</Th>
<Th>Type</Th>
<Th>Valid Until</Th>
@@ -71,7 +71,7 @@ export const CaTable = ({ handlePopUpOpen }: Props) => {
data.map((ca) => {
return (
<Tr className="h-10" key={`ca-${ca.id}`}>
<Td>{ca.dn}</Td>
<Td>{ca.friendlyName}</Td>
<Td>{caStatusToNameMap[ca.status]}</Td>
<Td>{caTypeToNameMap[ca.type]}</Td>
<Td>{ca.notAfter ? format(new Date(ca.notAfter), "yyyy-MM-dd") : "-"}</Td>
@@ -1,9 +1,9 @@
import { useEffect } from "react";
import { faCheck, faCopy, faDownload } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import FileSaver from "file-saver";
import { IconButton } from "@app/components/v2";
import { useToggle } from "@app/hooks";
import { IconButton, Tooltip } from "@app/components/v2";
import { useTimedReset } from "@app/hooks";
type Props = {
serialNumber: string;
@@ -18,42 +18,28 @@ export const CertificateContent = ({
certificateChain,
privateKey
}: Props) => {
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);
const [copyTextSerialNumber, isCopyingSerialNumber, setCopyTextSerialNumber] =
useTimedReset<string>({
initialState: "Copy to clipboard"
});
const [copyTextCertificate, isCopyingCertificate, setCopyTextCertificate] = useTimedReset<string>(
{
initialState: "Copy to clipboard"
}
);
const [copyTextCertificateChain, isCopyingCertificateChain, setCopyTextCertificateChain] =
useTimedReset<string>({
initialState: "Copy to clipboard"
});
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]);
const [copyTextCertificateSk, isCopyingCertificateSk, setCopyTextCertificateSk] =
useTimedReset<string>({
initialState: "Copy to clipboard"
});
const downloadTxtFile = (filename: string, content: string) => {
const blob = new Blob([content], { type: "text/plain" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
const blob = new Blob([content], { type: "text/plain;charset=utf-8" });
FileSaver.saveAs(blob, filename);
};
return (
@@ -61,51 +47,48 @@ export const CertificateContent = ({
<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">{serialNumber}</p>
<IconButton
ariaLabel="copy icon"
colorSchema="secondary"
className="group relative"
onClick={() => {
navigator.clipboard.writeText(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>
<div className="mb-4 flex items-center justify-between">
<h2>Certificate Body</h2>
<div className="flex">
<Tooltip content={copyTextSerialNumber}>
<IconButton
ariaLabel="copy icon"
colorSchema="secondary"
className="group relative"
onClick={() => {
navigator.clipboard.writeText(certificate);
setIsCertificateCopied.on();
navigator.clipboard.writeText(serialNumber);
setCopyTextSerialNumber("Copied");
}}
>
<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">
Copy
</span>
</IconButton>
<IconButton
ariaLabel="copy icon"
colorSchema="secondary"
className="group relative ml-2"
onClick={() => {
downloadTxtFile("cert.pem", certificate);
}}
>
<FontAwesomeIcon icon={faDownload} />
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
Download
</span>
<FontAwesomeIcon icon={isCopyingSerialNumber ? faCheck : faCopy} />
</IconButton>
</Tooltip>
</div>
<div className="mb-4 flex items-center justify-between">
<h2>Certificate Body</h2>
<div className="flex">
<Tooltip content={copyTextCertificate}>
<IconButton
ariaLabel="copy icon"
colorSchema="secondary"
className="group relative"
onClick={() => {
navigator.clipboard.writeText(certificate);
setCopyTextCertificate("Copied");
}}
>
<FontAwesomeIcon icon={isCopyingCertificate ? faCheck : faCopy} />
</IconButton>
</Tooltip>
<Tooltip content="Download">
<IconButton
ariaLabel="copy icon"
colorSchema="secondary"
className="group relative ml-2"
onClick={() => {
downloadTxtFile("cert.pem", certificate);
}}
>
<FontAwesomeIcon icon={faDownload} />
</IconButton>
</Tooltip>
</div>
</div>
<div className="mb-8 flex items-center justify-between rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
@@ -116,33 +99,31 @@ export const CertificateContent = ({
<div className="mb-4 flex items-center justify-between">
<h2>Certificate Chain</h2>
<div className="flex">
<IconButton
ariaLabel="copy icon"
colorSchema="secondary"
className="group relative"
onClick={() => {
navigator.clipboard.writeText(certificateChain);
setIsCertificateChainCopied.on();
}}
>
<FontAwesomeIcon icon={isCertificateChainCopied ? faCheck : faCopy} />
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
Copy
</span>
</IconButton>
<IconButton
ariaLabel="copy icon"
colorSchema="secondary"
className="group relative ml-2"
onClick={() => {
downloadTxtFile("chain.pem", certificateChain);
}}
>
<FontAwesomeIcon icon={faDownload} />
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
Download
</span>
</IconButton>
<Tooltip content={copyTextCertificateChain}>
<IconButton
ariaLabel="copy icon"
colorSchema="secondary"
className="group relative"
onClick={() => {
navigator.clipboard.writeText(certificateChain);
setCopyTextCertificateChain("Copied");
}}
>
<FontAwesomeIcon icon={isCopyingCertificateChain ? faCheck : faCopy} />
</IconButton>
</Tooltip>
<Tooltip content="Download">
<IconButton
ariaLabel="copy icon"
colorSchema="secondary"
className="group relative ml-2"
onClick={() => {
downloadTxtFile("chain.pem", certificateChain);
}}
>
<FontAwesomeIcon icon={faDownload} />
</IconButton>
</Tooltip>
</div>
</div>
<div className="mb-8 flex items-center justify-between rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
@@ -155,33 +136,31 @@ export const CertificateContent = ({
<div className="mb-4 flex items-center justify-between">
<h2>Certificate Private Key</h2>
<div className="flex">
<IconButton
ariaLabel="copy icon"
colorSchema="secondary"
className="group relative"
onClick={() => {
navigator.clipboard.writeText(privateKey);
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">
Copy
</span>
</IconButton>
<IconButton
ariaLabel="copy icon"
colorSchema="secondary"
className="group relative ml-2"
onClick={() => {
downloadTxtFile("private_key.txt", privateKey);
}}
>
<FontAwesomeIcon icon={faDownload} />
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
Download
</span>
</IconButton>
<Tooltip content={copyTextCertificateSk}>
<IconButton
ariaLabel="copy icon"
colorSchema="secondary"
className="group relative"
onClick={() => {
navigator.clipboard.writeText(privateKey);
setCopyTextCertificateSk("Copied");
}}
>
<FontAwesomeIcon icon={isCopyingCertificateSk ? faCheck : faCopy} />
</IconButton>
</Tooltip>
<Tooltip content={copyTextCertificateSk}>
<IconButton
ariaLabel="copy icon"
colorSchema="secondary"
className="group relative ml-2"
onClick={() => {
downloadTxtFile("private_key.txt", privateKey);
}}
>
<FontAwesomeIcon icon={faDownload} />
</IconButton>
</Tooltip>
</div>
</div>
<div className="mb-8 flex items-center justify-between rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
@@ -1,7 +1,6 @@
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";
@@ -21,21 +20,11 @@ import { UsePopUpState } from "@app/hooks/usePopUp";
import { CertificateContent } from "./CertificateContent";
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(),
friendlyName: 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))
ttl: z.string().trim()
});
export type FormData = z.infer<typeof schema>;
@@ -80,31 +69,30 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
if (cert) {
reset({
caId: cert.caId,
friendlyName: cert.friendlyName,
commonName: cert.commonName,
ttl: "",
notAfter: format(new Date(cert.notAfter), "yyyy-MM-dd")
ttl: ""
});
} else {
reset({
caId: "",
friendlyName: "",
commonName: "",
ttl: "",
notAfter: ""
ttl: ""
});
}
}, [cert]);
const onFormSubmit = async ({ caId, commonName, ttl, notAfter }: FormData) => {
const onFormSubmit = async ({ caId, friendlyName, commonName, ttl }: FormData) => {
try {
if (!currentWorkspace?.slug) return;
const { serialNumber, certificate, certificateChain, privateKey } = await createCertificate({
projectSlug: currentWorkspace.slug,
caId,
friendlyName,
commonName,
ttl: ttl ? Number(ttl) : undefined,
notBefore: new Date().toISOString(),
notAfter
ttl
});
reset();
@@ -175,6 +163,20 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
</FormControl>
)}
/>
<Controller
control={control}
defaultValue=""
name="friendlyName"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Friendly Name"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="My Certificate" isDisabled={Boolean(cert)} />
</FormControl>
)}
/>
<Controller
control={control}
defaultValue=""
@@ -195,25 +197,16 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
name="ttl"
render={({ field, fieldState: { error } }) => (
<FormControl
label="TTL (seconds)"
label="TTL"
isError={Boolean(error)}
errorText={error?.message}
isRequired
>
<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)} />
<Input
{...field}
placeholder="2 days, 1d, 2h, 1y, ..."
isDisabled={Boolean(cert)}
/>
</FormControl>
)}
/>
@@ -53,7 +53,7 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
<Table>
<THead>
<Tr>
<Th>Common Name</Th>
<Th>Friendly Name</Th>
<Th>Status</Th>
<Th>Valid Until</Th>
<Th />
@@ -67,7 +67,7 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
data.map((certificate) => {
return (
<Tr className="h-10" key={`certificate-${certificate.id}`}>
<Td>{certificate.commonName}</Td>
<Td>{certificate.friendlyName}</Td>
<Td>{certStatusToNameMap[certificate.status]}</Td>
<Td>
{certificate.notAfter
@@ -4,6 +4,7 @@ import {
faAnchorLock,
faArrowLeft,
faBook,
faCertificate,
faCog,
faKey,
faLock,
@@ -13,8 +14,7 @@ import {
faShield,
faTags,
faUser,
faUsers
} from "@fortawesome/free-solid-svg-icons";
faUsers} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
@@ -117,6 +117,18 @@ const SINGLE_PERMISSION_LIST = [
subtitle: "IP allowlist management control",
icon: faNetworkWired,
formName: "ip-allowlist"
},
{
title: "Certificate Authorities",
subtitle: "CA management control",
icon: faCertificate,
formName: "certificate-authorities"
},
{
title: "Certificates",
subtitle: "Certificate management control",
icon: faCertificate,
formName: "certificates"
}
] as const;
@@ -48,6 +48,8 @@ export const formSchema = z.object({
tags: generalPermissionSchema,
"audit-logs": generalPermissionSchema,
"ip-allowlist": generalPermissionSchema,
"certificate-authorities": generalPermissionSchema,
certificates: generalPermissionSchema,
// akhilmhdh: refactor all keys like below
[ProjectPermissionSub.SecretApproval]: generalPermissionSchema,
workspace: z
@@ -25,6 +25,8 @@ type Props = {
| "audit-logs"
| "ip-allowlist"
| "identity"
| "certificate-authorities"
| "certificates"
| ProjectPermissionSub.SecretApproval;
isNonEditable?: boolean;
setValue: UseFormSetValue<TFormSchema>;