misc: addressed comments

This commit is contained in:
Sheen Capadngan
2025-05-17 03:42:49 +08:00
parent 60ea4bb579
commit edefa7698c
25 changed files with 744 additions and 303 deletions
+3 -1
View File
@@ -4,10 +4,12 @@ export {
useCreateCertificate,
useCreateUnifiedCa,
useDeleteCa,
useDeleteUnifiedCa,
useImportCaCertificate,
useRenewCa,
useSignIntermediate,
useUpdateCa
useUpdateCa,
useUpdateUnifiedCa
} from "./mutations";
export {
useGetCaById,
+43 -1
View File
@@ -11,6 +11,7 @@ import {
TCreateCertificateResponse,
TCreateUnifiedCertificateAuthorityDTO,
TDeleteCaDTO,
TDeleteUnifiedCertificateAuthorityDTO,
TImportCaCertificateDTO,
TImportCaCertificateResponse,
TRenewCaDTO,
@@ -18,9 +19,31 @@ import {
TSignIntermediateDTO,
TSignIntermediateResponse,
TUnifiedCertificateAuthority,
TUpdateCaDTO
TUpdateCaDTO,
TUpdateUnifiedCertificateAuthorityDTO
} from "./types";
export const useUpdateUnifiedCa = () => {
const queryClient = useQueryClient();
return useMutation<TUnifiedCertificateAuthority, object, TUpdateUnifiedCertificateAuthorityDTO>({
mutationFn: async ({ id, ...body }) => {
const {
data: { certificateAuthority }
} = await apiRequest.patch<{ certificateAuthority: TUnifiedCertificateAuthority }>(
`/api/v1/pki/ca/${body.type}/${id}`,
body
);
return certificateAuthority;
},
onSuccess: ({ projectId, type }) => {
queryClient.invalidateQueries({
queryKey: caKeys.listCasByTypeAndProjectId(type, projectId)
});
}
});
};
export const useCreateUnifiedCa = () => {
const queryClient = useQueryClient();
return useMutation<TUnifiedCertificateAuthority, object, TCreateUnifiedCertificateAuthorityDTO>({
@@ -39,6 +62,25 @@ export const useCreateUnifiedCa = () => {
});
};
export const useDeleteUnifiedCa = () => {
const queryClient = useQueryClient();
return useMutation<TUnifiedCertificateAuthority, object, TDeleteUnifiedCertificateAuthorityDTO>({
mutationFn: async ({ caId, type }) => {
const {
data: { certificateAuthority }
} = await apiRequest.delete<{ certificateAuthority: TUnifiedCertificateAuthority }>(
`/api/v1/pki/ca/${type}/${caId}`
);
return certificateAuthority;
},
onSuccess: (_, { type, projectId }) => {
queryClient.invalidateQueries({
queryKey: caKeys.listCasByTypeAndProjectId(type, projectId)
});
}
});
};
export const useCreateCa = () => {
const queryClient = useQueryClient();
return useMutation<TCertificateAuthority, object, TCreateCaDTO>({
+14 -1
View File
@@ -10,7 +10,10 @@ export type TAcmeCertificateAuthority = {
disableDirectIssuance: boolean;
configuration: {
dnsAppConnectionId: string;
dnsProvider: AcmeDnsProvider;
dnsProviderConfig: {
provider: AcmeDnsProvider.ROUTE53;
hostedZoneId: string;
};
directoryUrl: string;
accountEmail: string;
};
@@ -48,6 +51,16 @@ export type TUnifiedCertificateAuthority =
| TInternalCertificateAuthority;
export type TCreateUnifiedCertificateAuthorityDTO = Omit<TUnifiedCertificateAuthority, "id">;
export type TUpdateUnifiedCertificateAuthorityDTO = Partial<TUnifiedCertificateAuthority> & {
id: string;
type: CaType;
};
export type TDeleteUnifiedCertificateAuthorityDTO = {
caId: string;
type: CaType;
projectId: string;
};
export type TCertificateAuthority = {
id: string;
@@ -1,6 +1,7 @@
import { useQuery } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { TReactQueryOptions } from "@app/types/reactQuery";
import { TCertificate } from "../certificates/types";
import { TPkiSubscriber } from "./types";
@@ -62,17 +63,20 @@ export const useGetPkiSubscriber = ({
});
};
export const useGetPkiSubscriberCertificates = ({
subscriberName,
projectId,
offset,
limit
}: {
subscriberName: string;
projectId: string;
offset: number;
limit: number;
}) => {
export const useGetPkiSubscriberCertificates = (
{
subscriberName,
projectId,
offset,
limit
}: {
subscriberName: string;
projectId: string;
offset: number;
limit: number;
},
options?: TReactQueryOptions["options"]
) => {
return useQuery({
queryKey: pkiSubscriberKeys.specificPkiSubscriberCertificates({
subscriberName,
@@ -97,6 +101,7 @@ export const useGetPkiSubscriberCertificates = ({
);
return { certificates, totalCount };
},
enabled: Boolean(subscriberName) && Boolean(projectId)
enabled: Boolean(subscriberName) && Boolean(projectId),
...options
});
};
@@ -23,23 +23,27 @@ import {
CaStatus,
CaType,
useCreateUnifiedCa,
useGetCaById,
useUpdateCa
useGetCaByTypeAndId,
useUpdateUnifiedCa
} from "@app/hooks/api/ca";
import { UsePopUpState } from "@app/hooks/usePopUp";
const schema = z
.object({
type: z.enum([CaType.ACME]),
type: z.nativeEnum(CaType),
name: z.string(),
disableDirectIssuance: z.boolean(),
status: z.enum([CaStatus.ACTIVE, CaStatus.DISABLED]),
status: z.nativeEnum(CaStatus),
configuration: z.object({
dnsAppConnection: z.object({
id: z.string(),
name: z.string()
}),
dnsProvider: z.nativeEnum(AcmeDnsProvider),
// currently specific to Route53 but can be extended to others by differentiating via the provider property
dnsProviderConfig: z.object({
provider: z.nativeEnum(AcmeDnsProvider),
hostedZoneId: z.string()
}),
directoryUrl: z.string(),
accountEmail: z.string()
})
@@ -58,11 +62,13 @@ const caTypes = [{ label: "ACME", value: CaType.ACME }];
export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => {
const { currentWorkspace } = useWorkspace();
const { data: ca } = useGetCaById((popUp?.ca?.data as { caId: string })?.caId || "");
const { data: ca } = useGetCaByTypeAndId(
(popUp?.ca?.data as { type: CaType })?.type || "",
(popUp?.ca?.data as { caId: string })?.caId || ""
);
// SHEEN TODO: finish up CA management
const { mutateAsync: createMutateAsync } = useCreateUnifiedCa();
const { mutateAsync: updateMutateAsync } = useUpdateCa();
const { mutateAsync: updateMutateAsync } = useUpdateUnifiedCa();
const {
control,
@@ -82,7 +88,10 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => {
id: "",
name: ""
},
dnsProvider: AcmeDnsProvider.ROUTE53,
dnsProviderConfig: {
provider: AcmeDnsProvider.ROUTE53,
hostedZoneId: ""
},
directoryUrl: "",
accountEmail: ""
}
@@ -90,7 +99,7 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => {
});
const caType = watch("type");
const dnsProvider = watch("configuration.dnsProvider");
const dnsProvider = watch("configuration.dnsProviderConfig.provider");
const { data: availableConnections, isPending } = useListAvailableAppConnections(
AppConnection.AWS,
@@ -101,11 +110,30 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => {
useEffect(() => {
if (ca) {
// reset({
// type: ca.type,
// name: ca.name,
// disableDirectIssuance: ca.disableDirectIssuance
// });
if (ca.type !== CaType.INTERNAL && availableConnections?.length) {
const selectedConnection = availableConnections?.find(
(connection) => connection.id === ca?.configuration.dnsAppConnectionId
);
reset({
type: ca.type,
name: ca.name,
status: ca.status,
disableDirectIssuance: ca.disableDirectIssuance,
configuration: {
dnsAppConnection: {
id: ca.configuration.dnsAppConnectionId,
name: selectedConnection?.name || ""
},
dnsProviderConfig: {
provider: ca.configuration.dnsProviderConfig.provider,
hostedZoneId: ca.configuration.dnsProviderConfig.hostedZoneId
},
directoryUrl: ca.configuration.directoryUrl,
accountEmail: ca.configuration.accountEmail
}
});
}
} else {
reset({
type: CaType.ACME,
@@ -117,13 +145,16 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => {
id: "",
name: ""
},
dnsProvider: AcmeDnsProvider.ROUTE53,
dnsProviderConfig: {
provider: AcmeDnsProvider.ROUTE53,
hostedZoneId: ""
},
directoryUrl: "",
accountEmail: ""
}
});
}
}, [ca]);
}, [ca, availableConnections]);
const onFormSubmit = async ({
type,
@@ -135,17 +166,20 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => {
try {
if (!currentWorkspace?.slug) return;
if (ca) {
// update
// await updateMutateAsync({
// projectSlug: currentWorkspace.slug,
// caId: ca.id,
// name,
// disableDirectIssuance,
// status
// });
if (ca && type !== CaType.INTERNAL) {
await updateMutateAsync({
id: ca.id,
projectId: currentWorkspace.id,
name,
type,
status,
disableDirectIssuance,
configuration: {
...configuration,
dnsAppConnectionId: configuration.dnsAppConnection.id
}
});
} else {
// create
await createMutateAsync({
projectId: currentWorkspace.id,
name,
@@ -217,7 +251,12 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => {
defaultValue=""
name="name"
render={({ field, fieldState: { error } }) => (
<FormControl label="Name" isError={Boolean(error)} errorText={error?.message}>
<FormControl
label="Name"
isError={Boolean(error)}
errorText={error?.message}
isRequired
>
<Input {...field} placeholder="my-external-ca" isDisabled={Boolean(ca)} />
</FormControl>
)}
@@ -226,7 +265,7 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => {
<>
<Controller
control={control}
name="configuration.dnsProvider"
name="configuration.dnsProviderConfig.provider"
defaultValue={AcmeDnsProvider.ROUTE53}
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
@@ -275,6 +314,21 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => {
control={control}
name="configuration.dnsAppConnection"
/>
<Controller
control={control}
defaultValue=""
name="configuration.dnsProviderConfig.hostedZoneId"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Hosted Zone ID"
isError={Boolean(error)}
errorText={error?.message}
isRequired
>
<Input {...field} placeholder="Z040441124N1GOOMCQYX1" />
</FormControl>
)}
/>
<Controller
control={control}
defaultValue=""
@@ -284,6 +338,7 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => {
label="Directory URL"
isError={Boolean(error)}
errorText={error?.message}
isRequired
>
<Input
{...field}
@@ -301,6 +356,7 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => {
label="Account Email"
isError={Boolean(error)}
errorText={error?.message}
isRequired
>
<Input {...field} placeholder="[email protected]" />
</FormControl>
@@ -6,7 +6,7 @@ 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 { CaStatus, CaType, useDeleteUnifiedCa, useUpdateUnifiedCa } from "@app/hooks/api";
import { usePopUp } from "@app/hooks/usePopUp";
import { ExternalCaModal } from "./ExternalCaModal";
@@ -14,8 +14,8 @@ import { ExternalCaTable } from "./ExternalCaTable";
export const ExternalCaSection = () => {
const { currentWorkspace } = useWorkspace();
const { mutateAsync: deleteCa } = useDeleteCa();
const { mutateAsync: updateCa } = useUpdateCa();
const { mutateAsync: deleteCa } = useDeleteUnifiedCa();
const { mutateAsync: updateCa } = useUpdateUnifiedCa();
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"ca",
@@ -24,11 +24,11 @@ export const ExternalCaSection = () => {
"upgradePlan"
] as const);
const onRemoveCaSubmit = async (caId: string) => {
const onRemoveCaSubmit = async (caId: string, type: CaType) => {
try {
if (!currentWorkspace?.slug) return;
await deleteCa({ caId, projectSlug: currentWorkspace.slug });
await deleteCa({ caId, type, projectId: currentWorkspace.id });
createNotification({
text: "Successfully deleted CA",
@@ -44,11 +44,19 @@ export const ExternalCaSection = () => {
}
};
const onUpdateCaStatus = async ({ caId, status }: { caId: string; status: CaStatus }) => {
const onUpdateCaStatus = async ({
caId,
type,
status
}: {
caId: string;
type: CaType;
status: CaStatus;
}) => {
try {
if (!currentWorkspace?.slug) return;
await updateCa({ caId, projectSlug: currentWorkspace.slug, status });
await updateCa({ id: caId, type, status });
createNotification({
text: `Successfully ${status === CaStatus.ACTIVE ? "enabled" : "disabled"} CA`,
@@ -96,7 +104,12 @@ export const ExternalCaSection = () => {
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)}
onDeleteApproved={() =>
onRemoveCaSubmit(
(popUp?.deleteCa?.data as { caId: string })?.caId,
(popUp?.deleteCa?.data as { type: CaType })?.type
)
}
/>
<DeleteActionModal
isOpen={popUp.caStatus.isOpen}
@@ -111,9 +124,12 @@ export const ExternalCaSection = () => {
: "This action will prevent the CA from issuing new certificates."
}
onChange={(isOpen) => handlePopUpToggle("caStatus", isOpen)}
buttonText="Proceed"
deleteKey="confirm"
onDeleteApproved={() =>
onUpdateCaStatus(popUp?.caStatus?.data as { caId: string; status: CaStatus })
onUpdateCaStatus(
popUp?.caStatus?.data as { caId: string; type: CaType; status: CaStatus }
)
}
/>
<UpgradePlanModal
@@ -1,6 +1,11 @@
import { faBan, faCertificate, faEllipsis, faTrash } from "@fortawesome/free-solid-svg-icons";
import {
faBan,
faCertificate,
faEllipsis,
faPencil,
faTrash
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate } from "@tanstack/react-router";
import { twMerge } from "tailwind-merge";
import { ProjectPermissionCan } from "@app/components/permissions";
@@ -24,7 +29,6 @@ import {
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { CaStatus, CaType, useListCasByTypeAndProjectId } from "@app/hooks/api";
import { caStatusToNameMap, getCaStatusBadgeVariant } from "@app/hooks/api/ca/constants";
import { ProjectType } from "@app/hooks/api/workspace/types";
import { UsePopUpState } from "@app/hooks/usePopUp";
type Props = {
@@ -32,7 +36,7 @@ type Props = {
popUpName: keyof UsePopUpState<["ca", "deleteCa", "caStatus", "upgradePlan"]>,
data?: {
caId?: string;
dn?: string;
type?: CaType;
status?: CaStatus;
description?: string;
}
@@ -40,7 +44,6 @@ type Props = {
};
export const ExternalCaTable = ({ handlePopUpOpen }: Props) => {
const navigate = useNavigate();
const { currentWorkspace } = useWorkspace();
const { data, isPending } = useListCasByTypeAndProjectId(CaType.ACME, currentWorkspace.id);
@@ -66,15 +69,12 @@ export const ExternalCaTable = ({ handlePopUpOpen }: Props) => {
<Tr
className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
key={`ca-${ca.id}`}
onClick={() =>
navigate({
to: `/${ProjectType.CertificateManager}/$projectId/ca/$caId` as const,
params: {
projectId: currentWorkspace.id,
caId: ca.id
}
})
}
onClick={() => {
handlePopUpOpen("ca", {
caId: ca.id,
type: ca.type
});
}}
>
<Td>{ca.name}</Td>
<Td>{ca.type}</Td>
@@ -93,6 +93,29 @@ export const ExternalCaTable = ({ handlePopUpOpen }: Props) => {
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1">
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={ProjectPermissionSub.CertificateAuthorities}
>
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={(e) => {
e.stopPropagation();
handlePopUpOpen("ca", {
caId: ca.id,
type: ca.type
});
}}
disabled={!isAllowed}
icon={<FontAwesomeIcon icon={faPencil} />}
>
Edit CA
</DropdownMenuItem>
)}
</ProjectPermissionCan>
{(ca.status === CaStatus.ACTIVE || ca.status === CaStatus.DISABLED) && (
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
@@ -108,6 +131,7 @@ export const ExternalCaTable = ({ handlePopUpOpen }: Props) => {
e.stopPropagation();
handlePopUpOpen("caStatus", {
caId: ca.id,
type: ca.type,
status:
ca.status === CaStatus.ACTIVE
? CaStatus.DISABLED
@@ -133,10 +157,10 @@ export const ExternalCaTable = ({ handlePopUpOpen }: Props) => {
)}
onClick={(e) => {
e.stopPropagation();
// handlePopUpOpen("deleteCa", {
// caId: ca.id,
// dn: ca.dn
// });
handlePopUpOpen("deleteCa", {
caId: ca.id,
type: ca.type
});
}}
disabled={!isAllowed}
icon={<FontAwesomeIcon icon={faTrash} />}
@@ -48,12 +48,17 @@ export const PkiSubscriberCertificatesTable = ({ subscriberName, handlePopUpOpen
const [page, setPage] = useState(1);
const [perPage, setPerPage] = useState(PER_PAGE_INIT);
const { data, isPending } = useGetPkiSubscriberCertificates({
subscriberName,
projectId,
offset: (page - 1) * perPage,
limit: perPage
});
const { data, isPending } = useGetPkiSubscriberCertificates(
{
subscriberName,
projectId,
offset: (page - 1) * perPage,
limit: perPage
},
{
refetchInterval: 10 * 1000 // 10 seconds
}
);
const getCertStatusBadge = (status: string, notAfter: string) => {
if (status === CertStatus.REVOKED) {