mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-14 15:40:47 +00:00
feat: POC for ACME done
This commit is contained in:
@@ -1,10 +1,10 @@
|
||||
import { SshCaStatus } from "../sshCa";
|
||||
import { SshCertTemplateStatus } from "../sshCertificateTemplates";
|
||||
import { CaStatus, CaType } from "./enums";
|
||||
import { CaStatus, InternalCaType } from "./enums";
|
||||
|
||||
export const caTypeToNameMap: { [K in CaType]: string } = {
|
||||
[CaType.ROOT]: "Root",
|
||||
[CaType.INTERMEDIATE]: "Intermediate"
|
||||
export const caTypeToNameMap: { [K in InternalCaType]: string } = {
|
||||
[InternalCaType.ROOT]: "Root",
|
||||
[InternalCaType.INTERMEDIATE]: "Intermediate"
|
||||
};
|
||||
|
||||
export const caStatusToNameMap: { [K in CaStatus]: string } = {
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
export enum CaType {
|
||||
INTERNAL = "internal",
|
||||
ACME = "acme"
|
||||
}
|
||||
|
||||
export enum InternalCaType {
|
||||
ROOT = "root",
|
||||
INTERMEDIATE = "intermediate"
|
||||
}
|
||||
@@ -12,3 +17,7 @@ export enum CaStatus {
|
||||
export enum CaRenewalType {
|
||||
EXISTING = "existing"
|
||||
}
|
||||
|
||||
export enum AcmeDnsProvider {
|
||||
ROUTE53 = "route53"
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
export { CaRenewalType, CaStatus, CaType } from "./enums";
|
||||
export { AcmeDnsProvider, CaRenewalType, CaStatus, CaType, InternalCaType } from "./enums";
|
||||
export {
|
||||
useCreateCa,
|
||||
useCreateCertificate,
|
||||
useCreateUnifiedCa,
|
||||
useDeleteCa,
|
||||
useImportCaCertificate,
|
||||
useRenewCa,
|
||||
@@ -10,9 +11,12 @@ export {
|
||||
} from "./mutations";
|
||||
export {
|
||||
useGetCaById,
|
||||
useGetCaByTypeAndId,
|
||||
useGetCaCert,
|
||||
useGetCaCerts,
|
||||
useGetCaCertTemplates,
|
||||
useGetCaCrls,
|
||||
useGetCaCsr
|
||||
useGetCaCsr,
|
||||
useListCasByProjectId,
|
||||
useListCasByTypeAndProjectId
|
||||
} from "./queries";
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
TCreateCaDTO,
|
||||
TCreateCertificateDTO,
|
||||
TCreateCertificateResponse,
|
||||
TCreateUnifiedCertificateAuthorityDTO,
|
||||
TDeleteCaDTO,
|
||||
TImportCaCertificateDTO,
|
||||
TImportCaCertificateResponse,
|
||||
@@ -16,9 +17,28 @@ import {
|
||||
TRenewCaResponse,
|
||||
TSignIntermediateDTO,
|
||||
TSignIntermediateResponse,
|
||||
TUnifiedCertificateAuthority,
|
||||
TUpdateCaDTO
|
||||
} from "./types";
|
||||
|
||||
export const useCreateUnifiedCa = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<TUnifiedCertificateAuthority, object, TCreateUnifiedCertificateAuthorityDTO>({
|
||||
mutationFn: async (body) => {
|
||||
const { data } = await apiRequest.post<TUnifiedCertificateAuthority>(
|
||||
`/api/v1/pki/ca/${body.type}`,
|
||||
body
|
||||
);
|
||||
return data;
|
||||
},
|
||||
onSuccess: (_, { type, projectId }) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: caKeys.listCasByTypeAndProjectId(type, projectId)
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useCreateCa = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<TCertificateAuthority, object, TCreateCaDTO>({
|
||||
|
||||
@@ -3,10 +3,14 @@ import { useQuery } from "@tanstack/react-query";
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { TCertificateTemplate } from "../certificateTemplates/types";
|
||||
import { TCertificateAuthority } from "./types";
|
||||
import { CaType } from "./enums";
|
||||
import { TCertificateAuthority, TUnifiedCertificateAuthority } from "./types";
|
||||
|
||||
export const caKeys = {
|
||||
getCaById: (caId: string) => [{ caId }, "ca"],
|
||||
getCaByTypeAndId: (type: CaType, caId: string) => [{ type, caId }, "ca"],
|
||||
listCasByTypeAndProjectId: (type: CaType, projectId: string) => [{ type, projectId }, "cas"],
|
||||
listCasByProjectId: (projectId: string) => [{ projectId }, "cas"],
|
||||
getCaCerts: (caId: string) => [{ caId }, "ca-cert"],
|
||||
getCaCrls: (caId: string) => [{ caId }, "ca-crls"],
|
||||
getCaCert: (caId: string) => [{ caId }, "ca-cert"],
|
||||
@@ -16,6 +20,47 @@ export const caKeys = {
|
||||
getCaEstConfig: (caId: string) => [{ caId }, "ca-est-config"]
|
||||
};
|
||||
|
||||
export const useGetCaByTypeAndId = (type: CaType, caId: string) => {
|
||||
return useQuery({
|
||||
queryKey: caKeys.getCaByTypeAndId(type, caId),
|
||||
queryFn: async () => {
|
||||
const {
|
||||
data: { certificateAuthority }
|
||||
} = await apiRequest.get<{ certificateAuthority: TUnifiedCertificateAuthority }>(
|
||||
`/api/v1/pki/ca/${type}/${caId}`
|
||||
);
|
||||
return certificateAuthority;
|
||||
},
|
||||
enabled: Boolean(caId)
|
||||
});
|
||||
};
|
||||
|
||||
export const useListCasByTypeAndProjectId = (type: CaType, projectId: string) => {
|
||||
return useQuery({
|
||||
queryKey: caKeys.listCasByTypeAndProjectId(type, projectId),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<{
|
||||
certificateAuthorities: TUnifiedCertificateAuthority[];
|
||||
}>(`/api/v1/pki/ca/${type}?projectId=${projectId}`);
|
||||
|
||||
return data.certificateAuthorities;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useListCasByProjectId = (projectId: string) => {
|
||||
return useQuery({
|
||||
queryKey: caKeys.listCasByProjectId(projectId),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<{
|
||||
certificateAuthorities: TUnifiedCertificateAuthority[];
|
||||
}>(`/api/v2/pki/ca?projectId=${projectId}`);
|
||||
|
||||
return data.certificateAuthorities;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetCaById = (caId: string) => {
|
||||
return useQuery({
|
||||
queryKey: caKeys.getCaById(caId),
|
||||
|
||||
@@ -1,11 +1,59 @@
|
||||
import { CertExtendedKeyUsage, CertKeyAlgorithm, CertKeyUsage } from "../certificates/enums";
|
||||
import { CaRenewalType, CaStatus, CaType } from "./enums";
|
||||
import { AcmeDnsProvider, CaRenewalType, CaStatus, CaType, InternalCaType } from "./enums";
|
||||
|
||||
export type TAcmeCertificateAuthority = {
|
||||
id: string;
|
||||
projectId: string;
|
||||
type: CaType.ACME;
|
||||
status: CaStatus;
|
||||
name: string;
|
||||
disableDirectIssuance: boolean;
|
||||
configuration: {
|
||||
dnsAppConnectionId: string;
|
||||
dnsProvider: AcmeDnsProvider;
|
||||
directoryUrl: string;
|
||||
accountEmail: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type TInternalCertificateAuthority = {
|
||||
id: string;
|
||||
projectId: string;
|
||||
type: CaType.INTERNAL;
|
||||
status: CaStatus;
|
||||
name: string;
|
||||
disableDirectIssuance: boolean;
|
||||
configuration: {
|
||||
type: InternalCaType;
|
||||
friendlyName?: string;
|
||||
commonName: string;
|
||||
organization: string;
|
||||
ou: string;
|
||||
country: string;
|
||||
province: string;
|
||||
locality: string;
|
||||
maxPathLength: number;
|
||||
keyAlgorithm: CertKeyAlgorithm;
|
||||
notAfter?: string;
|
||||
notBefore?: string;
|
||||
dn: string;
|
||||
parentCaId?: string;
|
||||
serialNumber: string;
|
||||
activeCaCertId: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type TUnifiedCertificateAuthority =
|
||||
| TAcmeCertificateAuthority
|
||||
| TInternalCertificateAuthority;
|
||||
|
||||
export type TCreateUnifiedCertificateAuthorityDTO = Omit<TUnifiedCertificateAuthority, "id">;
|
||||
|
||||
export type TCertificateAuthority = {
|
||||
id: string;
|
||||
parentCaId?: string;
|
||||
projectId: string;
|
||||
type: CaType;
|
||||
type: InternalCaType;
|
||||
status: CaStatus;
|
||||
friendlyName: string;
|
||||
organization: string;
|
||||
|
||||
+3
-3
@@ -6,7 +6,7 @@ import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { Button, IconButton, Tooltip } from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
||||
import { useTimedReset } from "@app/hooks";
|
||||
import { CaStatus, CaType, useGetCaById } from "@app/hooks/api";
|
||||
import { CaStatus, InternalCaType, useGetCaById } from "@app/hooks/api";
|
||||
import { caStatusToNameMap, caTypeToNameMap } from "@app/hooks/api/ca/constants";
|
||||
import { certKeyAlgorithmToNameMap } from "@app/hooks/api/certificates/constants";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
@@ -82,7 +82,7 @@ export const CaDetailsSection = ({ caId, handlePopUpOpen }: Props) => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{ca.type === CaType.INTERMEDIATE && ca.status !== CaStatus.PENDING_CERTIFICATE && (
|
||||
{ca.type === InternalCaType.INTERMEDIATE && ca.status !== CaStatus.PENDING_CERTIFICATE && (
|
||||
<div className="mb-4">
|
||||
<p className="text-sm font-semibold text-mineshaft-300">Parent CA ID</p>
|
||||
<div className="group flex align-top">
|
||||
@@ -156,7 +156,7 @@ export const CaDetailsSection = ({ caId, handlePopUpOpen }: Props) => {
|
||||
colorSchema="primary"
|
||||
type="submit"
|
||||
onClick={() => {
|
||||
if (ca.type === CaType.INTERMEDIATE && !ca.parentCaId) {
|
||||
if (ca.type === InternalCaType.INTERMEDIATE && !ca.parentCaId) {
|
||||
// intermediate CA with external parent CA
|
||||
handlePopUpOpen("installCaCert", {
|
||||
caId,
|
||||
|
||||
+2
@@ -5,6 +5,7 @@ import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { PageHeader } from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
||||
|
||||
import { ExternalCaSection } from "./components/ExternalCaSection";
|
||||
import { CaSection } from "./components";
|
||||
|
||||
export const CertificateAuthoritiesPage = () => {
|
||||
@@ -25,6 +26,7 @@ export const CertificateAuthoritiesPage = () => {
|
||||
a={ProjectPermissionSub.CertificateAuthorities}
|
||||
>
|
||||
<CaSection />
|
||||
<ExternalCaSection />
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
// DatePicker
|
||||
} from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { CaType, useCreateCa, useGetCaById, useUpdateCa } from "@app/hooks/api/ca";
|
||||
import { InternalCaType, useCreateCa, useGetCaById, useUpdateCa } from "@app/hooks/api/ca";
|
||||
import { certKeyAlgorithms } from "@app/hooks/api/certificates/constants";
|
||||
import { CertKeyAlgorithm } from "@app/hooks/api/certificates/enums";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
@@ -35,7 +35,7 @@ const getDateTenYearsFromToday = () => {
|
||||
|
||||
const schema = z
|
||||
.object({
|
||||
type: z.enum([CaType.ROOT, CaType.INTERMEDIATE]),
|
||||
type: z.enum([InternalCaType.ROOT, InternalCaType.INTERMEDIATE]),
|
||||
friendlyName: z.string(),
|
||||
organization: z.string(),
|
||||
ou: z.string(),
|
||||
@@ -63,8 +63,8 @@ type Props = {
|
||||
};
|
||||
|
||||
const caTypes = [
|
||||
{ label: "Root", value: CaType.ROOT },
|
||||
{ label: "Intermediate", value: CaType.INTERMEDIATE }
|
||||
{ label: "Root", value: InternalCaType.ROOT },
|
||||
{ label: "Intermediate", value: InternalCaType.INTERMEDIATE }
|
||||
];
|
||||
|
||||
export const CaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
@@ -85,7 +85,7 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
} = useForm<FormData>({
|
||||
resolver: zodResolver(schema),
|
||||
defaultValues: {
|
||||
type: CaType.ROOT,
|
||||
type: InternalCaType.ROOT,
|
||||
friendlyName: "",
|
||||
organization: "",
|
||||
ou: "",
|
||||
@@ -119,7 +119,7 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
});
|
||||
} else {
|
||||
reset({
|
||||
type: CaType.ROOT,
|
||||
type: InternalCaType.ROOT,
|
||||
friendlyName: "",
|
||||
organization: "",
|
||||
ou: "",
|
||||
@@ -212,7 +212,7 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
<Controller
|
||||
control={control}
|
||||
name="type"
|
||||
defaultValue={CaType.ROOT}
|
||||
defaultValue={InternalCaType.ROOT}
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl label="CA Type" errorText={error?.message} isError={Boolean(error)}>
|
||||
<Select
|
||||
@@ -231,7 +231,7 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{caType === CaType.ROOT && (
|
||||
{caType === InternalCaType.ROOT && (
|
||||
<>
|
||||
{/* <Controller
|
||||
name="notAfter"
|
||||
|
||||
@@ -72,7 +72,7 @@ export const CaSection = () => {
|
||||
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>
|
||||
<p className="text-xl font-semibold text-mineshaft-100">Internal Certificate Authorities</p>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Create}
|
||||
a={ProjectPermissionSub.CertificateAuthorities}
|
||||
|
||||
+350
@@ -0,0 +1,350 @@
|
||||
import { useEffect } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
Button,
|
||||
FilterableSelect,
|
||||
FormControl,
|
||||
Input,
|
||||
Modal,
|
||||
ModalContent,
|
||||
Select,
|
||||
SelectItem,
|
||||
Switch
|
||||
} from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { useListAvailableAppConnections } from "@app/hooks/api/appConnections";
|
||||
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||
import {
|
||||
AcmeDnsProvider,
|
||||
CaStatus,
|
||||
CaType,
|
||||
useCreateUnifiedCa,
|
||||
useGetCaById,
|
||||
useUpdateCa
|
||||
} from "@app/hooks/api/ca";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
const schema = z
|
||||
.object({
|
||||
type: z.enum([CaType.ACME]),
|
||||
name: z.string(),
|
||||
disableDirectIssuance: z.boolean(),
|
||||
status: z.enum([CaStatus.ACTIVE, CaStatus.DISABLED]),
|
||||
configuration: z.object({
|
||||
dnsAppConnection: z.object({
|
||||
id: z.string(),
|
||||
name: z.string()
|
||||
}),
|
||||
dnsProvider: z.nativeEnum(AcmeDnsProvider),
|
||||
directoryUrl: z.string(),
|
||||
accountEmail: z.string()
|
||||
})
|
||||
})
|
||||
.required();
|
||||
|
||||
export type FormData = z.infer<typeof schema>;
|
||||
|
||||
type Props = {
|
||||
popUp: UsePopUpState<["ca"]>;
|
||||
handlePopUpToggle: (popUpName: keyof UsePopUpState<["ca"]>, state?: boolean) => void;
|
||||
};
|
||||
|
||||
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 || "");
|
||||
|
||||
// SHEEN TODO: finish up CA management
|
||||
const { mutateAsync: createMutateAsync } = useCreateUnifiedCa();
|
||||
const { mutateAsync: updateMutateAsync } = useUpdateCa();
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
reset,
|
||||
formState: { isSubmitting },
|
||||
watch
|
||||
} = useForm<FormData>({
|
||||
resolver: zodResolver(schema),
|
||||
defaultValues: {
|
||||
type: CaType.ACME,
|
||||
name: "",
|
||||
status: CaStatus.ACTIVE,
|
||||
disableDirectIssuance: false,
|
||||
configuration: {
|
||||
dnsAppConnection: {
|
||||
id: "",
|
||||
name: ""
|
||||
},
|
||||
dnsProvider: AcmeDnsProvider.ROUTE53,
|
||||
directoryUrl: "",
|
||||
accountEmail: ""
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const caType = watch("type");
|
||||
const dnsProvider = watch("configuration.dnsProvider");
|
||||
|
||||
const { data: availableConnections, isPending } = useListAvailableAppConnections(
|
||||
AppConnection.AWS,
|
||||
{
|
||||
enabled: dnsProvider === AcmeDnsProvider.ROUTE53
|
||||
}
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (ca) {
|
||||
// reset({
|
||||
// type: ca.type,
|
||||
// name: ca.name,
|
||||
// disableDirectIssuance: ca.disableDirectIssuance
|
||||
// });
|
||||
} else {
|
||||
reset({
|
||||
type: CaType.ACME,
|
||||
name: "",
|
||||
status: CaStatus.ACTIVE,
|
||||
disableDirectIssuance: false,
|
||||
configuration: {
|
||||
dnsAppConnection: {
|
||||
id: "",
|
||||
name: ""
|
||||
},
|
||||
dnsProvider: AcmeDnsProvider.ROUTE53,
|
||||
directoryUrl: "",
|
||||
accountEmail: ""
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [ca]);
|
||||
|
||||
const onFormSubmit = async ({
|
||||
type,
|
||||
name,
|
||||
disableDirectIssuance,
|
||||
status,
|
||||
configuration
|
||||
}: FormData) => {
|
||||
try {
|
||||
if (!currentWorkspace?.slug) return;
|
||||
|
||||
if (ca) {
|
||||
// update
|
||||
// await updateMutateAsync({
|
||||
// projectSlug: currentWorkspace.slug,
|
||||
// caId: ca.id,
|
||||
// name,
|
||||
// disableDirectIssuance,
|
||||
// status
|
||||
// });
|
||||
} else {
|
||||
// create
|
||||
await createMutateAsync({
|
||||
projectId: currentWorkspace.id,
|
||||
name,
|
||||
type,
|
||||
status,
|
||||
disableDirectIssuance,
|
||||
configuration: {
|
||||
...configuration,
|
||||
dnsAppConnectionId: configuration.dnsAppConnection.id
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
reset();
|
||||
handlePopUpToggle("ca", false);
|
||||
|
||||
createNotification({
|
||||
text: `Successfully ${ca ? "updated" : "created"} CA`,
|
||||
type: "success"
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
text: "Failed to create CA",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={popUp?.ca?.isOpen}
|
||||
onOpenChange={(isOpen) => {
|
||||
reset();
|
||||
handlePopUpToggle("ca", isOpen);
|
||||
}}
|
||||
>
|
||||
<ModalContent title={`${ca ? "View" : "Create"} External CA`}>
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
{ca && (
|
||||
<FormControl label="CA ID">
|
||||
<Input value={ca.id} isDisabled className="bg-white/[0.07]" />
|
||||
</FormControl>
|
||||
)}
|
||||
<Controller
|
||||
control={control}
|
||||
name="type"
|
||||
defaultValue={CaType.ACME}
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl label="Type" errorText={error?.message} isError={Boolean(error)}>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className="w-full"
|
||||
isDisabled={Boolean(ca)}
|
||||
>
|
||||
{caTypes.map(({ label, value }) => (
|
||||
<SelectItem value={String(value || "")} key={label}>
|
||||
{label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name="name"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl label="Name" isError={Boolean(error)} errorText={error?.message}>
|
||||
<Input {...field} placeholder="my-external-ca" isDisabled={Boolean(ca)} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{caType === CaType.ACME && (
|
||||
<>
|
||||
<Controller
|
||||
control={control}
|
||||
name="configuration.dnsProvider"
|
||||
defaultValue={AcmeDnsProvider.ROUTE53}
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="DNS Provider"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className="w-full"
|
||||
isDisabled={Boolean(ca)}
|
||||
>
|
||||
<SelectItem
|
||||
value={String(AcmeDnsProvider.ROUTE53)}
|
||||
key={AcmeDnsProvider.ROUTE53}
|
||||
>
|
||||
Route53
|
||||
</SelectItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
tooltipText={`${dnsProvider === AcmeDnsProvider.ROUTE53 ? "Route53" : ""} requires an AWS App Connection. This can be created from the Organization Settings page.`}
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
label="DNS App Connection"
|
||||
>
|
||||
<FilterableSelect
|
||||
value={value}
|
||||
onChange={(newValue) => {
|
||||
onChange(newValue);
|
||||
}}
|
||||
isLoading={isPending}
|
||||
options={availableConnections}
|
||||
placeholder="Select connection..."
|
||||
getOptionLabel={(option) => option.name}
|
||||
getOptionValue={(option) => option.id}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
control={control}
|
||||
name="configuration.dnsAppConnection"
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name="configuration.directoryUrl"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Directory URL"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
placeholder="https://acme-v02.api.letsencrypt.org/directory"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name="configuration.accountEmail"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Account Email"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="[email protected]" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<Controller
|
||||
control={control}
|
||||
name="disableDirectIssuance"
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl isError={Boolean(error)} errorText={error?.message} className="my-8">
|
||||
<Switch
|
||||
id="is-active"
|
||||
onCheckedChange={(value) => field.onChange(value)}
|
||||
isChecked={field.value}
|
||||
>
|
||||
<p className="w-full">Disable Direct Issuance</p>
|
||||
</Switch>
|
||||
</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>
|
||||
</form>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
+126
@@ -0,0 +1,126 @@
|
||||
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
||||
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 { ExternalCaModal } from "./ExternalCaModal";
|
||||
import { ExternalCaTable } from "./ExternalCaTable";
|
||||
|
||||
export const ExternalCaSection = () => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { mutateAsync: deleteCa } = useDeleteCa();
|
||||
const { mutateAsync: updateCa } = useUpdateCa();
|
||||
|
||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||
"ca",
|
||||
"deleteCa",
|
||||
"caStatus", // enable / disable
|
||||
"upgradePlan"
|
||||
] as const);
|
||||
|
||||
const onRemoveCaSubmit = async (caId: string) => {
|
||||
try {
|
||||
if (!currentWorkspace?.slug) return;
|
||||
|
||||
await deleteCa({ caId, projectSlug: currentWorkspace.slug });
|
||||
|
||||
createNotification({
|
||||
text: "Successfully deleted CA",
|
||||
type: "success"
|
||||
});
|
||||
|
||||
handlePopUpClose("deleteCa");
|
||||
} catch {
|
||||
createNotification({
|
||||
text: "Failed to delete CA",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onUpdateCaStatus = async ({ caId, status }: { caId: string; status: CaStatus }) => {
|
||||
try {
|
||||
if (!currentWorkspace?.slug) return;
|
||||
|
||||
await updateCa({ caId, projectSlug: currentWorkspace.slug, status });
|
||||
|
||||
createNotification({
|
||||
text: `Successfully ${status === CaStatus.ACTIVE ? "enabled" : "disabled"} CA`,
|
||||
type: "success"
|
||||
});
|
||||
|
||||
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">External Certificate Authorities</p>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Create}
|
||||
a={ProjectPermissionSub.CertificateAuthorities}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
colorSchema="primary"
|
||||
type="submit"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => handlePopUpOpen("ca")}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
Create CA
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
<ExternalCaModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||
<ExternalCaTable 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.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={() =>
|
||||
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}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+165
@@ -0,0 +1,165 @@
|
||||
import { faBan, faCertificate, faEllipsis, 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";
|
||||
import {
|
||||
Badge,
|
||||
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 { 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 = {
|
||||
handlePopUpOpen: (
|
||||
popUpName: keyof UsePopUpState<["ca", "deleteCa", "caStatus", "upgradePlan"]>,
|
||||
data?: {
|
||||
caId?: string;
|
||||
dn?: string;
|
||||
status?: CaStatus;
|
||||
description?: string;
|
||||
}
|
||||
) => void;
|
||||
};
|
||||
|
||||
export const ExternalCaTable = ({ handlePopUpOpen }: Props) => {
|
||||
const navigate = useNavigate();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { data, isPending } = useListCasByTypeAndProjectId(CaType.ACME, currentWorkspace.id);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th>Name</Th>
|
||||
<Th>Type</Th>
|
||||
<Th>Status</Th>
|
||||
<Th />
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{isPending && <TableSkeleton columns={3} innerKey="project-cas" />}
|
||||
{!isPending &&
|
||||
data &&
|
||||
data.length > 0 &&
|
||||
data.map((ca) => {
|
||||
return (
|
||||
<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
|
||||
}
|
||||
})
|
||||
}
|
||||
>
|
||||
<Td>{ca.name}</Td>
|
||||
<Td>{ca.type}</Td>
|
||||
<Td>
|
||||
<Badge variant={getCaStatusBadgeVariant(ca.status)}>
|
||||
{caStatusToNameMap[ca.status]}
|
||||
</Badge>
|
||||
</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.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={(e) => {
|
||||
e.stopPropagation();
|
||||
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={(e) => {
|
||||
e.stopPropagation();
|
||||
// handlePopUpOpen("deleteCa", {
|
||||
// caId: ca.id,
|
||||
// dn: ca.dn
|
||||
// });
|
||||
}}
|
||||
disabled={!isAllowed}
|
||||
icon={<FontAwesomeIcon icon={faTrash} />}
|
||||
>
|
||||
Delete CA
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
</TBody>
|
||||
</Table>
|
||||
{!isPending && data?.length === 0 && (
|
||||
<EmptyState
|
||||
title="No external certificate authorities have been created"
|
||||
icon={faCertificate}
|
||||
/>
|
||||
)}
|
||||
</TableContainer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+13
-11
@@ -20,10 +20,10 @@ import {
|
||||
} from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import {
|
||||
CaStatus,
|
||||
CaType,
|
||||
useCreatePkiSubscriber,
|
||||
useGetPkiSubscriber,
|
||||
useListWorkspaceCas,
|
||||
useListCasByProjectId,
|
||||
useListWorkspacePkiSubscribers,
|
||||
useUpdatePkiSubscriber
|
||||
} from "@app/hooks/api";
|
||||
@@ -74,10 +74,7 @@ export const PkiSubscriberModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const projectId = currentWorkspace.id;
|
||||
const { data: subscribers } = useListWorkspacePkiSubscribers(projectId);
|
||||
const { data: cas } = useListWorkspaceCas({
|
||||
projectSlug: currentWorkspace?.slug ?? "",
|
||||
status: CaStatus.ACTIVE
|
||||
});
|
||||
const { data: cas } = useListCasByProjectId(projectId);
|
||||
|
||||
const { data: pkiSubscriber } = useGetPkiSubscriber({
|
||||
subscriberName:
|
||||
@@ -276,11 +273,16 @@ export const PkiSubscriberModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className="w-full"
|
||||
>
|
||||
{(cas || []).map(({ id, dn }) => (
|
||||
<SelectItem value={id} key={`ca-${id}`}>
|
||||
{dn}
|
||||
</SelectItem>
|
||||
))}
|
||||
{(cas || []).map(({ id, name, type, configuration }) => {
|
||||
const displayName =
|
||||
type === CaType.INTERNAL ? `${name} (${configuration.dn})` : name;
|
||||
|
||||
return (
|
||||
<SelectItem value={id} key={`ca-${id}`}>
|
||||
{displayName}
|
||||
</SelectItem>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user