mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-14 08:29:55 +00:00
Merge branch 'main' into feat/point-in-time-revamp
This commit is contained in:
@@ -296,8 +296,8 @@ export const ROUTE_PATHS = Object.freeze({
|
||||
},
|
||||
CertManager: {
|
||||
CertAuthDetailsByIDPage: setRoute(
|
||||
"/cert-manager/$projectId/ca/$caId",
|
||||
"/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/ca/$caId"
|
||||
"/cert-manager/$projectId/ca/$caName",
|
||||
"/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/ca/$caName"
|
||||
),
|
||||
SubscribersPage: setRoute(
|
||||
"/cert-manager/$projectId/subscribers",
|
||||
|
||||
@@ -68,6 +68,7 @@ export const eventToNameMap: { [K in EventType]: string } = {
|
||||
[EventType.IMPORT_CA_CERT]: "Import CA certificate",
|
||||
[EventType.GET_CA_CRL]: "Get CA CRL",
|
||||
[EventType.ISSUE_CERT]: "Issue certificate",
|
||||
[EventType.IMPORT_CERT]: "Import certificate",
|
||||
[EventType.GET_CERT]: "Get certificate",
|
||||
[EventType.DELETE_CERT]: "Delete certificate",
|
||||
[EventType.REVOKE_CERT]: "Revoke certificate",
|
||||
@@ -190,8 +191,16 @@ export const eventToNameMap: { [K in EventType]: string } = {
|
||||
[EventType.GET_IDENTITY_LDAP_AUTH]: "Retrieved LDAP Auth for identity",
|
||||
[EventType.REVOKE_IDENTITY_LDAP_AUTH]: "Revoked LDAP Auth for identity",
|
||||
|
||||
[EventType.UPDATE_ORG]: "Update Organization",
|
||||
[EventType.CREATE_PKI_SUBSCRIBER]: "Create PKI subscriber",
|
||||
[EventType.UPDATE_PKI_SUBSCRIBER]: "Update PKI subscriber",
|
||||
[EventType.DELETE_PKI_SUBSCRIBER]: "Delete PKI subscriber",
|
||||
[EventType.GET_PKI_SUBSCRIBER]: "Get PKI subscriber",
|
||||
[EventType.ISSUE_PKI_SUBSCRIBER_CERT]: "Issue PKI subscriber certificate",
|
||||
[EventType.SIGN_PKI_SUBSCRIBER_CERT]: "Sign PKI subscriber certificate",
|
||||
[EventType.AUTOMATED_RENEW_SUBSCRIBER_CERT]: "Automated renew PKI subscriber certificate",
|
||||
[EventType.LIST_PKI_SUBSCRIBER_CERTS]: "List PKI subscriber certificates",
|
||||
|
||||
[EventType.UPDATE_ORG]: "Update Organization",
|
||||
[EventType.CREATE_PROJECT]: "Create Project",
|
||||
[EventType.UPDATE_PROJECT]: "Update Project",
|
||||
[EventType.DELETE_PROJECT]: "Delete Project"
|
||||
|
||||
@@ -81,6 +81,7 @@ export enum EventType {
|
||||
IMPORT_CA_CERT = "import-certificate-authority-cert",
|
||||
GET_CA_CRL = "get-certificate-authority-crl",
|
||||
ISSUE_CERT = "issue-cert",
|
||||
IMPORT_CERT = "import-cert",
|
||||
GET_CERT = "get-cert",
|
||||
DELETE_CERT = "delete-cert",
|
||||
REVOKE_CERT = "revoke-cert",
|
||||
@@ -184,6 +185,14 @@ export enum EventType {
|
||||
MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_GET = "microsoft-teams-workflow-integration-get",
|
||||
MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_LIST = "microsoft-teams-workflow-integration-list",
|
||||
|
||||
CREATE_PKI_SUBSCRIBER = "create-pki-subscriber",
|
||||
UPDATE_PKI_SUBSCRIBER = "update-pki-subscriber",
|
||||
DELETE_PKI_SUBSCRIBER = "delete-pki-subscriber",
|
||||
GET_PKI_SUBSCRIBER = "get-pki-subscriber",
|
||||
ISSUE_PKI_SUBSCRIBER_CERT = "issue-pki-subscriber-cert",
|
||||
SIGN_PKI_SUBSCRIBER_CERT = "sign-pki-subscriber-cert",
|
||||
AUTOMATED_RENEW_SUBSCRIBER_CERT = "automated-renew-subscriber-cert",
|
||||
LIST_PKI_SUBSCRIBER_CERTS = "list-pki-subscriber-certs",
|
||||
UPDATE_ORG = "update-org",
|
||||
|
||||
CREATE_PROJECT = "create-project",
|
||||
|
||||
@@ -505,7 +505,8 @@ interface CreateCa {
|
||||
type: EventType.CREATE_CA;
|
||||
metadata: {
|
||||
caId: string;
|
||||
dn: string;
|
||||
name: string;
|
||||
dn?: string;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -514,12 +515,14 @@ interface GetCa {
|
||||
metadata: {
|
||||
caId: string;
|
||||
dn: string;
|
||||
name: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface UpdateCa {
|
||||
type: EventType.UPDATE_CA;
|
||||
metadata: {
|
||||
name: string;
|
||||
caId: string;
|
||||
dn: string;
|
||||
status: CaStatus;
|
||||
@@ -530,6 +533,7 @@ interface DeleteCa {
|
||||
type: EventType.DELETE_CA;
|
||||
metadata: {
|
||||
caId: string;
|
||||
name: string;
|
||||
dn: string;
|
||||
};
|
||||
}
|
||||
@@ -583,6 +587,14 @@ interface IssueCert {
|
||||
serialNumber: string;
|
||||
};
|
||||
}
|
||||
interface ImportCert {
|
||||
type: EventType.IMPORT_CERT;
|
||||
metadata: {
|
||||
certId: string;
|
||||
cn: string;
|
||||
serialNumber: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface GetCert {
|
||||
type: EventType.GET_CERT;
|
||||
@@ -895,6 +907,7 @@ export type Event =
|
||||
| ImportCaCert
|
||||
| GetCaCrl
|
||||
| IssueCert
|
||||
| ImportCert
|
||||
| GetCert
|
||||
| DeleteCert
|
||||
| RevokeCert
|
||||
|
||||
@@ -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,4 +1,4 @@
|
||||
export { CaRenewalType, CaStatus, CaType } from "./enums";
|
||||
export { AcmeDnsProvider, CaRenewalType, CaStatus, CaType, InternalCaType } from "./enums";
|
||||
export {
|
||||
useCreateCa,
|
||||
useCreateCertificate,
|
||||
@@ -9,10 +9,13 @@ export {
|
||||
useUpdateCa
|
||||
} from "./mutations";
|
||||
export {
|
||||
useGetCa,
|
||||
useGetCaById,
|
||||
useGetCaCert,
|
||||
useGetCaCerts,
|
||||
useGetCaCertTemplates,
|
||||
useGetCaCrls,
|
||||
useGetCaCsr
|
||||
useGetCaCsr,
|
||||
useListCasByProjectId,
|
||||
useListCasByTypeAndProjectId
|
||||
} from "./queries";
|
||||
|
||||
@@ -3,64 +3,78 @@ import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { workspaceKeys } from "../workspace";
|
||||
import { CaType } from "./enums";
|
||||
import { caKeys } from "./queries";
|
||||
import {
|
||||
TCertificateAuthority,
|
||||
TCreateCaDTO,
|
||||
TCreateCertificateAuthorityDTO,
|
||||
TCreateCertificateDTO,
|
||||
TCreateCertificateResponse,
|
||||
TDeleteCaDTO,
|
||||
TDeleteCertificateAuthorityDTO,
|
||||
TImportCaCertificateDTO,
|
||||
TImportCaCertificateResponse,
|
||||
TRenewCaDTO,
|
||||
TRenewCaResponse,
|
||||
TSignIntermediateDTO,
|
||||
TSignIntermediateResponse,
|
||||
TUpdateCaDTO
|
||||
TUnifiedCertificateAuthority,
|
||||
TUpdateCertificateAuthorityDTO
|
||||
} from "./types";
|
||||
|
||||
export const useCreateCa = () => {
|
||||
export const useUpdateCa = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<TCertificateAuthority, object, TCreateCaDTO>({
|
||||
mutationFn: async (body) => {
|
||||
const {
|
||||
data: { ca }
|
||||
} = await apiRequest.post<{ ca: TCertificateAuthority }>("/api/v1/pki/ca/", body);
|
||||
return ca;
|
||||
return useMutation<TUnifiedCertificateAuthority, object, TUpdateCertificateAuthorityDTO>({
|
||||
mutationFn: async ({ caName, ...body }) => {
|
||||
const { data } = await apiRequest.patch<TUnifiedCertificateAuthority>(
|
||||
`/api/v1/pki/ca/${body.type}/${caName}`,
|
||||
body
|
||||
);
|
||||
|
||||
return data;
|
||||
},
|
||||
onSuccess: (_, { projectSlug }) => {
|
||||
queryClient.invalidateQueries({ queryKey: workspaceKeys.getWorkspaceCas({ projectSlug }) });
|
||||
onSuccess: ({ projectId, type }) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: caKeys.listCasByTypeAndProjectId(type, projectId)
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateCa = () => {
|
||||
export const useCreateCa = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<TCertificateAuthority, object, TUpdateCaDTO>({
|
||||
mutationFn: async ({ caId, projectSlug, ...body }) => {
|
||||
const {
|
||||
data: { ca }
|
||||
} = await apiRequest.patch<{ ca: TCertificateAuthority }>(`/api/v1/pki/ca/${caId}`, body);
|
||||
return ca;
|
||||
return useMutation<TUnifiedCertificateAuthority, object, TCreateCertificateAuthorityDTO>({
|
||||
mutationFn: async (body) => {
|
||||
const { data } = await apiRequest.post<TUnifiedCertificateAuthority>(
|
||||
`/api/v1/pki/ca/${body.type}`,
|
||||
body
|
||||
);
|
||||
return data;
|
||||
},
|
||||
onSuccess: ({ id }, { projectSlug }) => {
|
||||
queryClient.invalidateQueries({ queryKey: workspaceKeys.getWorkspaceCas({ projectSlug }) });
|
||||
queryClient.invalidateQueries({ queryKey: caKeys.getCaById(id) });
|
||||
onSuccess: (_, { type, projectId }) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: caKeys.listCasByTypeAndProjectId(type, projectId)
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeleteCa = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<TCertificateAuthority, object, TDeleteCaDTO>({
|
||||
mutationFn: async ({ caId }) => {
|
||||
const {
|
||||
data: { ca }
|
||||
} = await apiRequest.delete<{ ca: TCertificateAuthority }>(`/api/v1/pki/ca/${caId}`);
|
||||
return ca;
|
||||
return useMutation<TUnifiedCertificateAuthority, object, TDeleteCertificateAuthorityDTO>({
|
||||
mutationFn: async ({ caName, type, projectId }) => {
|
||||
const { data } = await apiRequest.delete<TUnifiedCertificateAuthority>(
|
||||
`/api/v1/pki/ca/${type}/${caName}`,
|
||||
{
|
||||
data: {
|
||||
projectId
|
||||
}
|
||||
}
|
||||
);
|
||||
return data;
|
||||
},
|
||||
onSuccess: (_, { projectSlug }) => {
|
||||
queryClient.invalidateQueries({ queryKey: workspaceKeys.getWorkspaceCas({ projectSlug }) });
|
||||
onSuccess: (_, { type, projectId }) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: caKeys.listCasByTypeAndProjectId(type, projectId)
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -78,7 +92,7 @@ export const useSignIntermediate = () => {
|
||||
});
|
||||
};
|
||||
|
||||
export const useImportCaCertificate = () => {
|
||||
export const useImportCaCertificate = (projectId: string) => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<TImportCaCertificateResponse, object, TImportCaCertificateDTO>({
|
||||
mutationFn: async ({ caId, ...body }) => {
|
||||
@@ -92,6 +106,9 @@ export const useImportCaCertificate = () => {
|
||||
queryClient.invalidateQueries({ queryKey: workspaceKeys.getWorkspaceCas({ projectSlug }) });
|
||||
queryClient.invalidateQueries({ queryKey: caKeys.getCaCerts(caId) });
|
||||
queryClient.invalidateQueries({ queryKey: caKeys.getCaCert(caId) });
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: caKeys.listCasByTypeAndProjectId(CaType.INTERNAL, projectId)
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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"],
|
||||
getCaByNameAndProjectId: (caName: string, projectId: string) => [{ caName, projectId }, "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,53 @@ export const caKeys = {
|
||||
getCaEstConfig: (caId: string) => [{ caId }, "ca-est-config"]
|
||||
};
|
||||
|
||||
export const useGetCa = ({
|
||||
caName,
|
||||
projectId,
|
||||
type
|
||||
}: {
|
||||
caName: string;
|
||||
projectId: string;
|
||||
type: CaType;
|
||||
}) => {
|
||||
return useQuery({
|
||||
queryKey: caKeys.getCaByNameAndProjectId(caName, projectId),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<TUnifiedCertificateAuthority>(
|
||||
`/api/v1/pki/ca/${type}/${caName}?projectId=${projectId}`
|
||||
);
|
||||
return data;
|
||||
},
|
||||
enabled: Boolean(caName && projectId && type)
|
||||
});
|
||||
};
|
||||
|
||||
export const useListCasByTypeAndProjectId = (type: CaType, projectId: string) => {
|
||||
return useQuery({
|
||||
queryKey: caKeys.listCasByTypeAndProjectId(type, projectId),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<TUnifiedCertificateAuthority[]>(
|
||||
`/api/v1/pki/ca/${type}?projectId=${projectId}`
|
||||
);
|
||||
|
||||
return data;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
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,73 @@
|
||||
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;
|
||||
enableDirectIssuance: boolean;
|
||||
configuration: {
|
||||
dnsAppConnectionId: string;
|
||||
dnsProviderConfig: {
|
||||
provider: AcmeDnsProvider.ROUTE53;
|
||||
hostedZoneId: string;
|
||||
};
|
||||
directoryUrl: string;
|
||||
accountEmail: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type TInternalCertificateAuthority = {
|
||||
id: string;
|
||||
projectId: string;
|
||||
type: CaType.INTERNAL;
|
||||
status: CaStatus;
|
||||
name: string;
|
||||
enableDirectIssuance: 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 TCreateCertificateAuthorityDTO = Omit<TUnifiedCertificateAuthority, "id">;
|
||||
export type TUpdateCertificateAuthorityDTO = Partial<TUnifiedCertificateAuthority> & {
|
||||
caName: string;
|
||||
projectId: string;
|
||||
type: CaType;
|
||||
};
|
||||
|
||||
export type TDeleteCertificateAuthorityDTO = {
|
||||
caName: string;
|
||||
type: CaType;
|
||||
projectId: string;
|
||||
};
|
||||
|
||||
export type TCertificateAuthority = {
|
||||
id: string;
|
||||
parentCaId?: string;
|
||||
projectId: string;
|
||||
type: CaType;
|
||||
type: InternalCaType;
|
||||
status: CaStatus;
|
||||
friendlyName: string;
|
||||
organization: string;
|
||||
@@ -25,22 +87,6 @@ export type TCertificateAuthority = {
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
export type TCreateCaDTO = {
|
||||
projectSlug: string;
|
||||
type: string;
|
||||
friendlyName?: string;
|
||||
organization: string;
|
||||
ou: string;
|
||||
country: string;
|
||||
province: string;
|
||||
locality: string;
|
||||
commonName: string;
|
||||
notAfter?: string;
|
||||
maxPathLength: number;
|
||||
keyAlgorithm: CertKeyAlgorithm;
|
||||
requireTemplateForIssuance: boolean;
|
||||
};
|
||||
|
||||
export type TUpdateCaDTO = {
|
||||
projectSlug: string;
|
||||
caId: string;
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
export { useDeleteCert, useRevokeCert } from "./mutations";
|
||||
export { useDeleteCert, useImportCertificate, useRevokeCert } from "./mutations";
|
||||
export { useGetCert, useGetCertBody } from "./queries";
|
||||
|
||||
@@ -4,7 +4,13 @@ import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { pkiSubscriberKeys } from "../pkiSubscriber/queries";
|
||||
import { workspaceKeys } from "../workspace";
|
||||
import { TCertificate, TDeleteCertDTO, TRevokeCertDTO } from "./types";
|
||||
import {
|
||||
TCertificate,
|
||||
TDeleteCertDTO,
|
||||
TImportCertificateDTO,
|
||||
TImportCertificateResponse,
|
||||
TRevokeCertDTO
|
||||
} from "./types";
|
||||
|
||||
export const useDeleteCert = () => {
|
||||
const queryClient = useQueryClient();
|
||||
@@ -49,3 +55,21 @@ export const useRevokeCert = () => {
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useImportCertificate = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<TImportCertificateResponse, object, TImportCertificateDTO>({
|
||||
mutationFn: async (body) => {
|
||||
const { data } = await apiRequest.post<TImportCertificateResponse>(
|
||||
"/api/v1/pki/certificates/import-certificate",
|
||||
body
|
||||
);
|
||||
return data;
|
||||
},
|
||||
onSuccess: (_, { projectSlug }) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: workspaceKeys.forWorkspaceCertificates(projectSlug)
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -25,3 +25,21 @@ export type TRevokeCertDTO = {
|
||||
serialNumber: string;
|
||||
revocationReason: string;
|
||||
};
|
||||
|
||||
export type TImportCertificateDTO = {
|
||||
projectSlug: string;
|
||||
|
||||
certificatePem: string;
|
||||
privateKeyPem: string;
|
||||
chainPem: string;
|
||||
|
||||
pkiCollectionId?: string;
|
||||
friendlyName?: string;
|
||||
};
|
||||
|
||||
export type TImportCertificateResponse = {
|
||||
certificate: string;
|
||||
certificateChain: string;
|
||||
privateKey: string;
|
||||
serialNumber: string;
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@ export {
|
||||
useCreatePkiSubscriber,
|
||||
useDeletePkiSubscriber,
|
||||
useIssuePkiSubscriberCert,
|
||||
useOrderPkiSubscriberCert,
|
||||
useUpdatePkiSubscriber
|
||||
} from "./mutations";
|
||||
export { useGetPkiSubscriber, useGetPkiSubscriberCertificates } from "./queries";
|
||||
|
||||
@@ -108,3 +108,17 @@ export const useIssuePkiSubscriberCert = () => {
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useOrderPkiSubscriberCert = () => {
|
||||
return useMutation<{ message: string }, object, TIssuePkiSubscriberCertDTO>({
|
||||
mutationFn: async ({ subscriberName, projectId }) => {
|
||||
const { data } = await apiRequest.post(
|
||||
`/api/v1/pki/subscribers/${subscriberName}/order-certificate`,
|
||||
{
|
||||
projectId
|
||||
}
|
||||
);
|
||||
return data;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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";
|
||||
@@ -38,13 +39,16 @@ export const pkiSubscriberKeys = {
|
||||
] as const
|
||||
};
|
||||
|
||||
export const useGetPkiSubscriber = ({
|
||||
subscriberName,
|
||||
projectId
|
||||
}: {
|
||||
subscriberName: string;
|
||||
projectId: string;
|
||||
}) => {
|
||||
export const useGetPkiSubscriber = (
|
||||
{
|
||||
subscriberName,
|
||||
projectId
|
||||
}: {
|
||||
subscriberName: string;
|
||||
projectId: string;
|
||||
},
|
||||
options?: TReactQueryOptions["options"]
|
||||
) => {
|
||||
return useQuery({
|
||||
queryKey: pkiSubscriberKeys.getPkiSubscriber({ subscriberName, projectId }),
|
||||
queryFn: async () => {
|
||||
@@ -58,21 +62,25 @@ export const useGetPkiSubscriber = ({
|
||||
);
|
||||
return pkiSubscriber;
|
||||
},
|
||||
enabled: Boolean(subscriberName) && Boolean(projectId)
|
||||
enabled: Boolean(subscriberName) && Boolean(projectId),
|
||||
...options
|
||||
});
|
||||
};
|
||||
|
||||
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 +105,7 @@ export const useGetPkiSubscriberCertificates = ({
|
||||
);
|
||||
return { certificates, totalCount };
|
||||
},
|
||||
enabled: Boolean(subscriberName) && Boolean(projectId)
|
||||
enabled: Boolean(subscriberName) && Boolean(projectId),
|
||||
...options
|
||||
});
|
||||
};
|
||||
|
||||
@@ -5,6 +5,11 @@ export enum PkiSubscriberStatus {
|
||||
DISABLED = "disabled"
|
||||
}
|
||||
|
||||
export enum SubscriberOperationStatus {
|
||||
SUCCESS = "success",
|
||||
FAILED = "failed"
|
||||
}
|
||||
|
||||
export type TPkiSubscriber = {
|
||||
id: string;
|
||||
projectId: string;
|
||||
@@ -12,10 +17,16 @@ export type TPkiSubscriber = {
|
||||
name: string;
|
||||
commonName: string;
|
||||
status: PkiSubscriberStatus;
|
||||
ttl: string;
|
||||
ttl?: string;
|
||||
subjectAlternativeNames: string[];
|
||||
keyUsages: CertKeyUsage[];
|
||||
extendedKeyUsages: CertExtendedKeyUsage[];
|
||||
supportsImmediateCertIssuance?: boolean;
|
||||
enableAutoRenewal?: boolean;
|
||||
autoRenewalPeriodInDays?: number;
|
||||
lastOperationStatus?: SubscriberOperationStatus;
|
||||
lastOperationMessage?: string;
|
||||
lastOperationAt?: string;
|
||||
};
|
||||
|
||||
export type TCreatePkiSubscriberDTO = {
|
||||
@@ -23,10 +34,12 @@ export type TCreatePkiSubscriberDTO = {
|
||||
caId: string;
|
||||
name: string;
|
||||
commonName: string;
|
||||
ttl: string;
|
||||
ttl?: string;
|
||||
subjectAlternativeNames: string[];
|
||||
keyUsages: CertKeyUsage[];
|
||||
extendedKeyUsages: CertExtendedKeyUsage[];
|
||||
enableAutoRenewal?: boolean;
|
||||
autoRenewalPeriodInDays?: number;
|
||||
};
|
||||
|
||||
export type TUpdatePkiSubscriberDTO = {
|
||||
@@ -40,6 +53,8 @@ export type TUpdatePkiSubscriberDTO = {
|
||||
subjectAlternativeNames?: string[];
|
||||
keyUsages?: CertKeyUsage[];
|
||||
extendedKeyUsages?: CertExtendedKeyUsage[];
|
||||
enableAutoRenewal?: boolean;
|
||||
autoRenewalPeriodInDays?: number;
|
||||
};
|
||||
|
||||
export type TDeletePkiSubscriberDTO = {
|
||||
@@ -51,3 +66,8 @@ export type TIssuePkiSubscriberCertDTO = {
|
||||
subscriberName: string;
|
||||
projectId: string;
|
||||
};
|
||||
|
||||
export type TOrderPkiSubscriberCertDTO = {
|
||||
subscriberName: string;
|
||||
projectId: string;
|
||||
};
|
||||
|
||||
@@ -44,3 +44,25 @@ export const timeAgo = (inputDate: Date, currentDate: Date): string => {
|
||||
elapsedMilliseconds >= 0 ? "ago" : "from now"
|
||||
}`;
|
||||
};
|
||||
|
||||
export enum TimeUnit {
|
||||
DAY = "days",
|
||||
WEEK = "weeks",
|
||||
MONTH = "months",
|
||||
YEAR = "years"
|
||||
}
|
||||
|
||||
export const convertTimeUnitValueToDays = (unit: TimeUnit, value: number) => {
|
||||
switch (unit) {
|
||||
case TimeUnit.DAY:
|
||||
return value;
|
||||
case TimeUnit.WEEK:
|
||||
return value * 7;
|
||||
case TimeUnit.MONTH:
|
||||
return value * 30;
|
||||
case TimeUnit.YEAR:
|
||||
return value * 365;
|
||||
default:
|
||||
throw new Error(`Unknown time unit: ${unit}`);
|
||||
}
|
||||
};
|
||||
|
||||
+23
-19
@@ -16,7 +16,8 @@ import {
|
||||
} from "@app/components/v2";
|
||||
import { ROUTE_PATHS } from "@app/const/routes";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||
import { useDeleteCa, useGetCaById } from "@app/hooks/api";
|
||||
import { CaType, useDeleteCa, useGetCa } from "@app/hooks/api";
|
||||
import { TInternalCertificateAuthority } from "@app/hooks/api/ca/types";
|
||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||
import { usePopUp } from "@app/hooks/usePopUp";
|
||||
|
||||
@@ -31,14 +32,18 @@ import {
|
||||
} from "./components";
|
||||
|
||||
const Page = () => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const navigate = useNavigate();
|
||||
const params = useParams({
|
||||
from: ROUTE_PATHS.CertManager.CertAuthDetailsByIDPage.id
|
||||
});
|
||||
const caId = params.caId as string;
|
||||
const { data } = useGetCaById(caId);
|
||||
const { caName } = params as { caName: string };
|
||||
const { data } = useGetCa({
|
||||
caName,
|
||||
projectId: currentWorkspace?.id || "",
|
||||
type: CaType.INTERNAL
|
||||
}) as { data: TInternalCertificateAuthority };
|
||||
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const projectId = currentWorkspace?.id || "";
|
||||
|
||||
const { mutateAsync: deleteCa } = useDeleteCa();
|
||||
@@ -50,11 +55,15 @@ const Page = () => {
|
||||
"renewCa"
|
||||
] as const);
|
||||
|
||||
const onRemoveCaSubmit = async (caIdToDelete: string) => {
|
||||
const onRemoveCaSubmit = async () => {
|
||||
try {
|
||||
if (!currentWorkspace?.slug) return;
|
||||
|
||||
await deleteCa({ caId: caIdToDelete, projectSlug: currentWorkspace.slug });
|
||||
await deleteCa({
|
||||
caName,
|
||||
projectId: currentWorkspace.id,
|
||||
type: CaType.INTERNAL
|
||||
});
|
||||
|
||||
createNotification({
|
||||
text: "Successfully deleted CA",
|
||||
@@ -63,7 +72,7 @@ const Page = () => {
|
||||
|
||||
handlePopUpClose("deleteCa");
|
||||
navigate({
|
||||
to: `/${ProjectType.CertificateManager}/$projectId/certificates` as const,
|
||||
to: `/${ProjectType.CertificateManager}/$projectId/certificate-authorities` as const,
|
||||
params: {
|
||||
projectId
|
||||
}
|
||||
@@ -80,7 +89,7 @@ const Page = () => {
|
||||
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
|
||||
{data && (
|
||||
<div className="mx-auto mb-6 w-full max-w-7xl">
|
||||
<PageHeader title={data.friendlyName}>
|
||||
<PageHeader title={data.name}>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild className="rounded-lg">
|
||||
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
|
||||
@@ -101,12 +110,7 @@ const Page = () => {
|
||||
? "hover:!bg-red-500 hover:!text-white"
|
||||
: "pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
onClick={() =>
|
||||
handlePopUpOpen("deleteCa", {
|
||||
caId: data.id,
|
||||
dn: data.dn
|
||||
})
|
||||
}
|
||||
onClick={() => handlePopUpOpen("deleteCa")}
|
||||
disabled={!isAllowed}
|
||||
>
|
||||
Delete CA
|
||||
@@ -118,12 +122,12 @@ const Page = () => {
|
||||
</PageHeader>
|
||||
<div className="flex">
|
||||
<div className="mr-4 w-96">
|
||||
<CaDetailsSection caId={caId} handlePopUpOpen={handlePopUpOpen} />
|
||||
<CaDetailsSection caName={data.name} handlePopUpOpen={handlePopUpOpen} />
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<CaCertificatesSection caId={caId} />
|
||||
<CertificateTemplatesSection caId={caId} />
|
||||
<CaCrlsSection caId={caId} />
|
||||
<CaCertificatesSection caId={data.id} />
|
||||
<CertificateTemplatesSection caId={data.id} />
|
||||
<CaCrlsSection caId={data.id} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -139,7 +143,7 @@ const Page = () => {
|
||||
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}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
+65
-47
@@ -4,22 +4,24 @@ import { format } from "date-fns";
|
||||
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { Button, IconButton, Tooltip } from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||
import { useTimedReset } from "@app/hooks";
|
||||
import { CaStatus, CaType, useGetCaById } from "@app/hooks/api";
|
||||
import { CaStatus, CaType, InternalCaType, useGetCa } from "@app/hooks/api";
|
||||
import { caStatusToNameMap, caTypeToNameMap } from "@app/hooks/api/ca/constants";
|
||||
import { TInternalCertificateAuthority } from "@app/hooks/api/ca/types";
|
||||
import { certKeyAlgorithmToNameMap } from "@app/hooks/api/certificates/constants";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
type Props = {
|
||||
caId: string;
|
||||
caName: string;
|
||||
handlePopUpOpen: (
|
||||
popUpName: keyof UsePopUpState<["ca", "renewCa", "installCaCert"]>,
|
||||
data?: object
|
||||
) => void;
|
||||
};
|
||||
|
||||
export const CaDetailsSection = ({ caId, handlePopUpOpen }: Props) => {
|
||||
export const CaDetailsSection = ({ caName, handlePopUpOpen }: Props) => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const [copyTextId, isCopyingId, setCopyTextId] = useTimedReset<string>({
|
||||
initialState: "Copy ID to clipboard"
|
||||
});
|
||||
@@ -27,7 +29,13 @@ export const CaDetailsSection = ({ caId, handlePopUpOpen }: Props) => {
|
||||
initialState: "Copy ID to clipboard"
|
||||
});
|
||||
|
||||
const { data: ca } = useGetCaById(caId);
|
||||
const { data } = useGetCa({
|
||||
caName,
|
||||
projectId: currentWorkspace.id,
|
||||
type: CaType.INTERNAL
|
||||
});
|
||||
|
||||
const ca = data as TInternalCertificateAuthority;
|
||||
|
||||
return ca ? (
|
||||
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
@@ -45,7 +53,7 @@ export const CaDetailsSection = ({ caId, handlePopUpOpen }: Props) => {
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handlePopUpOpen("ca", {
|
||||
caId: ca.id
|
||||
name: ca.name
|
||||
});
|
||||
}}
|
||||
>
|
||||
@@ -59,7 +67,7 @@ export const CaDetailsSection = ({ caId, handlePopUpOpen }: Props) => {
|
||||
<div className="pt-4">
|
||||
<div className="mb-4">
|
||||
<p className="text-sm font-semibold text-mineshaft-300">CA Type</p>
|
||||
<p className="text-sm text-mineshaft-300">{caTypeToNameMap[ca.type]}</p>
|
||||
<p className="text-sm text-mineshaft-300">{caTypeToNameMap[ca.configuration.type]}</p>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<p className="text-sm font-semibold text-mineshaft-300">CA ID</p>
|
||||
@@ -82,36 +90,39 @@ export const CaDetailsSection = ({ caId, handlePopUpOpen }: Props) => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{ca.type === CaType.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">
|
||||
<p className="text-sm text-mineshaft-300">
|
||||
{ca.parentCaId ? ca.parentCaId : "N/A - External Parent CA"}
|
||||
</p>
|
||||
{ca.parentCaId && (
|
||||
<div className="opacity-0 transition-opacity duration-300 group-hover:opacity-100">
|
||||
<Tooltip content={copyTextParentId}>
|
||||
<IconButton
|
||||
ariaLabel="copy icon"
|
||||
variant="plain"
|
||||
className="group relative ml-2"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(ca.parentCaId as string);
|
||||
setCopyTextParentId("Copied");
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={isCopyingParentId ? faCheck : faCopy} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
{ca.configuration.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">
|
||||
<p className="text-sm text-mineshaft-300">
|
||||
{ca.configuration.parentCaId
|
||||
? ca.configuration.parentCaId
|
||||
: "N/A - External Parent CA"}
|
||||
</p>
|
||||
{ca.configuration.parentCaId && (
|
||||
<div className="opacity-0 transition-opacity duration-300 group-hover:opacity-100">
|
||||
<Tooltip content={copyTextParentId}>
|
||||
<IconButton
|
||||
ariaLabel="copy icon"
|
||||
variant="plain"
|
||||
className="group relative ml-2"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(ca.configuration.parentCaId as string);
|
||||
setCopyTextParentId("Copied");
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={isCopyingParentId ? faCheck : faCopy} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
<div className="mb-4">
|
||||
<p className="text-sm font-semibold text-mineshaft-300">Friendly Name</p>
|
||||
<p className="text-sm text-mineshaft-300">{ca.friendlyName}</p>
|
||||
<p className="text-sm font-semibold text-mineshaft-300">Name</p>
|
||||
<p className="text-sm text-mineshaft-300">{ca.name}</p>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<p className="text-sm font-semibold text-mineshaft-300">Status</p>
|
||||
@@ -119,29 +130,33 @@ export const CaDetailsSection = ({ caId, handlePopUpOpen }: Props) => {
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<p className="text-sm font-semibold text-mineshaft-300">Key Algorithm</p>
|
||||
<p className="text-sm text-mineshaft-300">{certKeyAlgorithmToNameMap[ca.keyAlgorithm]}</p>
|
||||
<p className="text-sm text-mineshaft-300">
|
||||
{certKeyAlgorithmToNameMap[ca.configuration.keyAlgorithm]}
|
||||
</p>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<p className="text-sm font-semibold text-mineshaft-300">Max Path Length</p>
|
||||
<p className="text-sm text-mineshaft-300">{ca.maxPathLength ?? "-"}</p>
|
||||
<p className="text-sm text-mineshaft-300">{ca.configuration.maxPathLength ?? "-"}</p>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<p className="text-sm font-semibold text-mineshaft-300">Not Before</p>
|
||||
<p className="text-sm text-mineshaft-300">
|
||||
{ca.notBefore ? format(new Date(ca.notBefore), "yyyy-MM-dd") : "-"}
|
||||
{ca.configuration.notBefore
|
||||
? format(new Date(ca.configuration.notBefore), "yyyy-MM-dd")
|
||||
: "-"}
|
||||
</p>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<p className="text-sm font-semibold text-mineshaft-300">Not After</p>
|
||||
<p className="text-sm text-mineshaft-300">
|
||||
{ca.notAfter ? format(new Date(ca.notAfter), "yyyy-MM-dd") : "-"}
|
||||
{ca.configuration.notAfter
|
||||
? format(new Date(ca.configuration.notAfter), "yyyy-MM-dd")
|
||||
: "-"}
|
||||
</p>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<p className="text-sm font-semibold text-mineshaft-300">Template Issuance Required</p>
|
||||
<p className="text-sm text-mineshaft-300">
|
||||
{ca.requireTemplateForIssuance ? "True" : "False"}
|
||||
</p>
|
||||
<p className="text-sm font-semibold text-mineshaft-300">Enable Direct Issuance</p>
|
||||
<p className="text-sm text-mineshaft-300">{ca.enableDirectIssuance ? "True" : "False"}</p>
|
||||
</div>
|
||||
{ca.status === CaStatus.ACTIVE && (
|
||||
<ProjectPermissionCan
|
||||
@@ -156,17 +171,20 @@ export const CaDetailsSection = ({ caId, handlePopUpOpen }: Props) => {
|
||||
colorSchema="primary"
|
||||
type="submit"
|
||||
onClick={() => {
|
||||
if (ca.type === CaType.INTERMEDIATE && !ca.parentCaId) {
|
||||
if (
|
||||
ca.configuration.type === InternalCaType.INTERMEDIATE &&
|
||||
!ca.configuration.parentCaId
|
||||
) {
|
||||
// intermediate CA with external parent CA
|
||||
handlePopUpOpen("installCaCert", {
|
||||
caId,
|
||||
caId: ca.id,
|
||||
isParentCaExternal: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("renewCa", {
|
||||
caId
|
||||
caId: ca.id
|
||||
});
|
||||
}}
|
||||
>
|
||||
@@ -190,7 +208,7 @@ export const CaDetailsSection = ({ caId, handlePopUpOpen }: Props) => {
|
||||
type="submit"
|
||||
onClick={() => {
|
||||
handlePopUpOpen("installCaCert", {
|
||||
caId
|
||||
caId: ca.id
|
||||
});
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { createFileRoute, linkOptions } from "@tanstack/react-router";
|
||||
import { CertAuthDetailsByIDPage } from "./CertAuthDetailsByIDPage";
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/ca/$caId"
|
||||
"/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/ca/$caName"
|
||||
)({
|
||||
component: CertAuthDetailsByIDPage,
|
||||
beforeLoad: ({ context, params }) => {
|
||||
|
||||
+3
-1
@@ -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 = () => {
|
||||
@@ -17,7 +18,7 @@ export const CertificateAuthoritiesPage = () => {
|
||||
<div className="mx-auto mb-6 w-full max-w-7xl">
|
||||
<PageHeader
|
||||
title="Certificate Authorities"
|
||||
description="Manage internal private certificate authorities for issuing and signing certificates, including root and intermediate CAs."
|
||||
description="Manage certificate authorities for issuing and signing certificates"
|
||||
/>
|
||||
<ProjectPermissionCan
|
||||
renderGuardBanner
|
||||
@@ -25,6 +26,7 @@ export const CertificateAuthoritiesPage = () => {
|
||||
a={ProjectPermissionSub.CertificateAuthorities}
|
||||
>
|
||||
<CaSection />
|
||||
<ExternalCaSection />
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+1
-1
@@ -41,7 +41,7 @@ export const ExternalCaInstallForm = ({ caId, handlePopUpToggle }: Props) => {
|
||||
});
|
||||
|
||||
const { data: csr } = useGetCaCsr(caId);
|
||||
const { mutateAsync: importCaCertificate } = useImportCaCertificate();
|
||||
const { mutateAsync: importCaCertificate } = useImportCaCertificate(currentWorkspace.id);
|
||||
|
||||
useEffect(() => {
|
||||
reset();
|
||||
|
||||
+1
-1
@@ -55,7 +55,7 @@ export const InternalCaInstallForm = ({ caId, handlePopUpToggle }: Props) => {
|
||||
const { data: csr } = useGetCaCsr(caId);
|
||||
|
||||
const { mutateAsync: signIntermediate } = useSignIntermediate();
|
||||
const { mutateAsync: importCaCertificate } = useImportCaCertificate();
|
||||
const { mutateAsync: importCaCertificate } = useImportCaCertificate(currentWorkspace.id);
|
||||
|
||||
const {
|
||||
control,
|
||||
|
||||
+137
-105
@@ -17,10 +17,18 @@ import {
|
||||
// DatePicker
|
||||
} from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { CaType, useCreateCa, useGetCaById, useUpdateCa } from "@app/hooks/api/ca";
|
||||
import {
|
||||
CaStatus,
|
||||
CaType,
|
||||
InternalCaType,
|
||||
useCreateCa,
|
||||
useGetCa,
|
||||
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";
|
||||
import { slugSchema } from "@app/lib/schemas";
|
||||
|
||||
const isValidDate = (dateString: string) => {
|
||||
const date = new Date(dateString);
|
||||
@@ -35,23 +43,31 @@ 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(),
|
||||
province: z.string(),
|
||||
locality: z.string(),
|
||||
commonName: z.string(),
|
||||
notAfter: z.string().trim().refine(isValidDate, { message: "Invalid date format" }),
|
||||
maxPathLength: z.string(),
|
||||
keyAlgorithm: z.enum([
|
||||
CertKeyAlgorithm.RSA_2048,
|
||||
CertKeyAlgorithm.RSA_4096,
|
||||
CertKeyAlgorithm.ECDSA_P256,
|
||||
CertKeyAlgorithm.ECDSA_P384
|
||||
]),
|
||||
requireTemplateForIssuance: z.boolean()
|
||||
type: z.nativeEnum(CaType),
|
||||
name: slugSchema({
|
||||
field: "Name"
|
||||
}),
|
||||
enableDirectIssuance: z.boolean(),
|
||||
status: z.nativeEnum(CaStatus),
|
||||
configuration: z
|
||||
.object({
|
||||
type: z.enum([InternalCaType.ROOT, InternalCaType.INTERMEDIATE]),
|
||||
organization: z.string(),
|
||||
ou: z.string(),
|
||||
country: z.string(),
|
||||
province: z.string(),
|
||||
locality: z.string(),
|
||||
commonName: z.string(),
|
||||
notAfter: z.string().trim().refine(isValidDate, { message: "Invalid date format" }),
|
||||
maxPathLength: z.string(),
|
||||
keyAlgorithm: z.enum([
|
||||
CertKeyAlgorithm.RSA_2048,
|
||||
CertKeyAlgorithm.RSA_4096,
|
||||
CertKeyAlgorithm.ECDSA_P256,
|
||||
CertKeyAlgorithm.ECDSA_P384
|
||||
])
|
||||
})
|
||||
.required()
|
||||
})
|
||||
.required();
|
||||
|
||||
@@ -63,15 +79,17 @@ 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) => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
// const [isStartDatePickerOpen, setIsStartDatePickerOpen] = useState(false);
|
||||
|
||||
const { data: ca } = useGetCaById((popUp?.ca?.data as { caId: string })?.caId || "");
|
||||
const { data: ca } = useGetCa({
|
||||
caName: (popUp?.ca?.data as { name: string })?.name || "",
|
||||
projectId: currentWorkspace?.id || "",
|
||||
type: CaType.INTERNAL
|
||||
});
|
||||
|
||||
const { mutateAsync: createMutateAsync } = useCreateCa();
|
||||
const { mutateAsync: updateMutateAsync } = useUpdateCa();
|
||||
@@ -85,42 +103,12 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
} = useForm<FormData>({
|
||||
resolver: zodResolver(schema),
|
||||
defaultValues: {
|
||||
type: CaType.ROOT,
|
||||
friendlyName: "",
|
||||
organization: "",
|
||||
ou: "",
|
||||
country: "",
|
||||
province: "",
|
||||
locality: "",
|
||||
commonName: "",
|
||||
notAfter: getDateTenYearsFromToday(),
|
||||
maxPathLength: "-1",
|
||||
keyAlgorithm: CertKeyAlgorithm.RSA_2048
|
||||
}
|
||||
});
|
||||
|
||||
const caType = watch("type");
|
||||
|
||||
useEffect(() => {
|
||||
if (ca) {
|
||||
reset({
|
||||
type: ca.type,
|
||||
friendlyName: ca.friendlyName,
|
||||
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) : "",
|
||||
keyAlgorithm: ca.keyAlgorithm,
|
||||
requireTemplateForIssuance: ca.requireTemplateForIssuance
|
||||
});
|
||||
} else {
|
||||
reset({
|
||||
type: CaType.ROOT,
|
||||
friendlyName: "",
|
||||
type: CaType.INTERNAL,
|
||||
name: "",
|
||||
status: CaStatus.ACTIVE,
|
||||
enableDirectIssuance: true,
|
||||
configuration: {
|
||||
type: InternalCaType.ROOT,
|
||||
organization: "",
|
||||
ou: "",
|
||||
country: "",
|
||||
@@ -129,25 +117,65 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
commonName: "",
|
||||
notAfter: getDateTenYearsFromToday(),
|
||||
maxPathLength: "-1",
|
||||
keyAlgorithm: CertKeyAlgorithm.RSA_2048,
|
||||
requireTemplateForIssuance: true
|
||||
keyAlgorithm: CertKeyAlgorithm.RSA_2048
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const caType = watch("configuration.type");
|
||||
|
||||
useEffect(() => {
|
||||
if (ca && ca.type === CaType.INTERNAL) {
|
||||
reset({
|
||||
type: ca.type,
|
||||
name: ca.name,
|
||||
status: ca.status,
|
||||
enableDirectIssuance: ca.enableDirectIssuance,
|
||||
configuration: {
|
||||
type: ca.configuration.type,
|
||||
organization: ca.configuration.organization,
|
||||
ou: ca.configuration.ou,
|
||||
country: ca.configuration.country,
|
||||
province: ca.configuration.province,
|
||||
locality: ca.configuration.locality,
|
||||
commonName: ca.configuration.commonName,
|
||||
notAfter: ca.configuration.notAfter
|
||||
? format(new Date(ca.configuration.notAfter), "yyyy-MM-dd")
|
||||
: "",
|
||||
maxPathLength: ca.configuration.maxPathLength
|
||||
? String(ca.configuration.maxPathLength)
|
||||
: "",
|
||||
keyAlgorithm: ca.configuration.keyAlgorithm
|
||||
}
|
||||
});
|
||||
} else {
|
||||
reset({
|
||||
type: CaType.INTERNAL,
|
||||
name: "",
|
||||
status: CaStatus.ACTIVE,
|
||||
enableDirectIssuance: true,
|
||||
configuration: {
|
||||
type: InternalCaType.ROOT,
|
||||
organization: "",
|
||||
ou: "",
|
||||
country: "",
|
||||
province: "",
|
||||
locality: "",
|
||||
commonName: "",
|
||||
notAfter: getDateTenYearsFromToday(),
|
||||
maxPathLength: "-1",
|
||||
keyAlgorithm: CertKeyAlgorithm.RSA_2048
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [ca]);
|
||||
|
||||
const onFormSubmit = async ({
|
||||
type,
|
||||
friendlyName,
|
||||
commonName,
|
||||
organization,
|
||||
ou,
|
||||
country,
|
||||
locality,
|
||||
province,
|
||||
notAfter,
|
||||
maxPathLength,
|
||||
keyAlgorithm,
|
||||
requireTemplateForIssuance
|
||||
name,
|
||||
enableDirectIssuance,
|
||||
status,
|
||||
configuration
|
||||
}: FormData) => {
|
||||
try {
|
||||
if (!currentWorkspace?.slug) return;
|
||||
@@ -155,26 +183,29 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
if (ca) {
|
||||
// update
|
||||
await updateMutateAsync({
|
||||
projectSlug: currentWorkspace.slug,
|
||||
caId: ca.id,
|
||||
requireTemplateForIssuance
|
||||
caName: ca.name,
|
||||
projectId: currentWorkspace.id,
|
||||
name,
|
||||
type: CaType.INTERNAL,
|
||||
status,
|
||||
enableDirectIssuance,
|
||||
configuration: {
|
||||
...configuration,
|
||||
maxPathLength: Number(configuration.maxPathLength)
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// create
|
||||
await createMutateAsync({
|
||||
projectSlug: currentWorkspace.slug,
|
||||
projectId: currentWorkspace.id,
|
||||
name,
|
||||
type,
|
||||
friendlyName,
|
||||
commonName,
|
||||
organization,
|
||||
ou,
|
||||
country,
|
||||
province,
|
||||
locality,
|
||||
notAfter,
|
||||
maxPathLength: Number(maxPathLength),
|
||||
keyAlgorithm,
|
||||
requireTemplateForIssuance
|
||||
status,
|
||||
enableDirectIssuance,
|
||||
configuration: {
|
||||
...configuration,
|
||||
maxPathLength: Number(configuration.maxPathLength)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -211,8 +242,8 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
)}
|
||||
<Controller
|
||||
control={control}
|
||||
name="type"
|
||||
defaultValue={CaType.ROOT}
|
||||
name="configuration.type"
|
||||
defaultValue={InternalCaType.ROOT}
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl label="CA Type" errorText={error?.message} isError={Boolean(error)}>
|
||||
<Select
|
||||
@@ -231,7 +262,7 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{caType === CaType.ROOT && (
|
||||
{caType === InternalCaType.ROOT && (
|
||||
<>
|
||||
{/* <Controller
|
||||
name="notAfter"
|
||||
@@ -264,7 +295,7 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name="notAfter"
|
||||
name="configuration.notAfter"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Valid Until"
|
||||
@@ -278,7 +309,7 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="maxPathLength"
|
||||
name="configuration.maxPathLength"
|
||||
defaultValue="-1"
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
@@ -307,7 +338,7 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
)}
|
||||
<Controller
|
||||
control={control}
|
||||
name="keyAlgorithm"
|
||||
name="configuration.keyAlgorithm"
|
||||
defaultValue={CertKeyAlgorithm.RSA_2048}
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
@@ -334,21 +365,22 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name="friendlyName"
|
||||
name="name"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Friendly Name"
|
||||
label="Name"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
isRequired
|
||||
>
|
||||
<Input {...field} placeholder="My CA" isDisabled={Boolean(ca)} />
|
||||
<Input {...field} placeholder="my-internal-ca" isDisabled={Boolean(ca)} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name="organization"
|
||||
name="configuration.organization"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Organization (O)"
|
||||
@@ -362,7 +394,7 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name="ou"
|
||||
name="configuration.ou"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Organization Unit (OU)"
|
||||
@@ -376,7 +408,7 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name="country"
|
||||
name="configuration.country"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Country Name (C)"
|
||||
@@ -390,7 +422,7 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name="province"
|
||||
name="configuration.province"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="State or Province Name"
|
||||
@@ -404,7 +436,7 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name="locality"
|
||||
name="configuration.locality"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Locality Name"
|
||||
@@ -418,7 +450,7 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name="commonName"
|
||||
name="configuration.commonName"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Common Name (CN)"
|
||||
@@ -431,16 +463,16 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="requireTemplateForIssuance"
|
||||
name="enableDirectIssuance"
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl isError={Boolean(error)} errorText={error?.message} className="my-8">
|
||||
<Switch
|
||||
id="is-active"
|
||||
id="enable-direct-issuance"
|
||||
onCheckedChange={(value) => field.onChange(value)}
|
||||
isChecked={field.value}
|
||||
>
|
||||
<p className="w-full">Require Template for Certificate Issuance</p>
|
||||
<p className="w-full">Enable Direct Issuance</p>
|
||||
</Switch>
|
||||
</FormControl>
|
||||
);
|
||||
|
||||
+10
-8
@@ -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, useDeleteCa, useUpdateCa } from "@app/hooks/api";
|
||||
import { usePopUp } from "@app/hooks/usePopUp";
|
||||
|
||||
import { CaCertModal } from "./CaCertModal";
|
||||
@@ -28,11 +28,11 @@ export const CaSection = () => {
|
||||
"upgradePlan"
|
||||
] as const);
|
||||
|
||||
const onRemoveCaSubmit = async (caId: string) => {
|
||||
const onRemoveCaSubmit = async (caName: string) => {
|
||||
try {
|
||||
if (!currentWorkspace?.slug) return;
|
||||
|
||||
await deleteCa({ caId, projectSlug: currentWorkspace.slug });
|
||||
await deleteCa({ caName, projectId: currentWorkspace.id, type: CaType.INTERNAL });
|
||||
|
||||
createNotification({
|
||||
text: "Successfully deleted CA",
|
||||
@@ -48,11 +48,11 @@ export const CaSection = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const onUpdateCaStatus = async ({ caId, status }: { caId: string; status: CaStatus }) => {
|
||||
const onUpdateCaStatus = async ({ caName, status }: { caName: string; status: CaStatus }) => {
|
||||
try {
|
||||
if (!currentWorkspace?.slug) return;
|
||||
|
||||
await updateCa({ caId, projectSlug: currentWorkspace.slug, status });
|
||||
await updateCa({ caName, projectId: currentWorkspace.id, type: CaType.INTERNAL, status });
|
||||
|
||||
createNotification({
|
||||
text: `Successfully ${status === CaStatus.ACTIVE ? "enabled" : "disabled"} CA`,
|
||||
@@ -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}
|
||||
@@ -102,7 +102,9 @@ export const CaSection = () => {
|
||||
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 { caName: string })?.caName)
|
||||
}
|
||||
/>
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.caStatus.isOpen}
|
||||
@@ -119,7 +121,7 @@ export const CaSection = () => {
|
||||
onChange={(isOpen) => handlePopUpToggle("caStatus", isOpen)}
|
||||
deleteKey="confirm"
|
||||
onDeleteApproved={() =>
|
||||
onUpdateCaStatus(popUp?.caStatus?.data as { caId: string; status: CaStatus })
|
||||
onUpdateCaStatus(popUp?.caStatus?.data as { caName: string; status: CaStatus })
|
||||
}
|
||||
/>
|
||||
<UpgradePlanModal
|
||||
|
||||
@@ -23,12 +23,13 @@ import {
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||
import { CaStatus, useListWorkspaceCas } from "@app/hooks/api";
|
||||
import { CaStatus, CaType, useListCasByTypeAndProjectId } from "@app/hooks/api";
|
||||
import {
|
||||
caStatusToNameMap,
|
||||
caTypeToNameMap,
|
||||
getCaStatusBadgeVariant
|
||||
} from "@app/hooks/api/ca/constants";
|
||||
import { TInternalCertificateAuthority } from "@app/hooks/api/ca/types";
|
||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
@@ -39,6 +40,7 @@ type Props = {
|
||||
>,
|
||||
data?: {
|
||||
caId?: string;
|
||||
caName?: string;
|
||||
dn?: string;
|
||||
status?: CaStatus;
|
||||
description?: string;
|
||||
@@ -49,9 +51,8 @@ type Props = {
|
||||
export const CaTable = ({ handlePopUpOpen }: Props) => {
|
||||
const navigate = useNavigate();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { data, isPending } = useListWorkspaceCas({
|
||||
projectSlug: currentWorkspace?.slug ?? ""
|
||||
});
|
||||
const { data, isPending } = useListCasByTypeAndProjectId(CaType.INTERNAL, currentWorkspace.id);
|
||||
const cas = data as TInternalCertificateAuthority[];
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -59,7 +60,7 @@ export const CaTable = ({ handlePopUpOpen }: Props) => {
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th>Friendly Name</Th>
|
||||
<Th>Name</Th>
|
||||
<Th>Status</Th>
|
||||
<Th>Type</Th>
|
||||
<Th>Valid Until</Th>
|
||||
@@ -69,33 +70,37 @@ export const CaTable = ({ handlePopUpOpen }: Props) => {
|
||||
<TBody>
|
||||
{isPending && <TableSkeleton columns={3} innerKey="project-cas" />}
|
||||
{!isPending &&
|
||||
data &&
|
||||
data.length > 0 &&
|
||||
data.map((ca) => {
|
||||
cas &&
|
||||
cas.length > 0 &&
|
||||
cas.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,
|
||||
to: `/${ProjectType.CertificateManager}/$projectId/ca/$caName` as const,
|
||||
params: {
|
||||
projectId: currentWorkspace.id,
|
||||
caId: ca.id
|
||||
caName: ca.name
|
||||
}
|
||||
})
|
||||
}
|
||||
>
|
||||
<Td>{ca.friendlyName}</Td>
|
||||
<Td>{ca.name}</Td>
|
||||
<Td>
|
||||
<Badge variant={getCaStatusBadgeVariant(ca.status)}>
|
||||
{caStatusToNameMap[ca.status]}
|
||||
</Badge>
|
||||
</Td>
|
||||
<Td>{caTypeToNameMap[ca.type]}</Td>
|
||||
<Td>{caTypeToNameMap[ca.configuration.type]}</Td>
|
||||
<Td>
|
||||
<div className="flex items-center">
|
||||
<p>{ca.notAfter ? format(new Date(ca.notAfter), "yyyy-MM-dd") : "-"}</p>
|
||||
<p>
|
||||
{ca.configuration.notAfter
|
||||
? format(new Date(ca.configuration.notAfter), "yyyy-MM-dd")
|
||||
: "-"}
|
||||
</p>
|
||||
</div>
|
||||
</Td>
|
||||
<Td className="flex justify-end">
|
||||
@@ -172,7 +177,7 @@ export const CaTable = ({ handlePopUpOpen }: Props) => {
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handlePopUpOpen("caStatus", {
|
||||
caId: ca.id,
|
||||
caName: ca.name,
|
||||
status:
|
||||
ca.status === CaStatus.ACTIVE
|
||||
? CaStatus.DISABLED
|
||||
@@ -199,8 +204,7 @@ export const CaTable = ({ handlePopUpOpen }: Props) => {
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handlePopUpOpen("deleteCa", {
|
||||
caId: ca.id,
|
||||
dn: ca.dn
|
||||
caName: ca.name
|
||||
});
|
||||
}}
|
||||
disabled={!isAllowed}
|
||||
|
||||
+410
@@ -0,0 +1,410 @@
|
||||
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,
|
||||
useCreateCa,
|
||||
useGetCa,
|
||||
useUpdateCa
|
||||
} from "@app/hooks/api/ca";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
import { slugSchema } from "@app/lib/schemas";
|
||||
|
||||
const schema = z
|
||||
.object({
|
||||
type: z.nativeEnum(CaType),
|
||||
name: slugSchema({
|
||||
field: "Name"
|
||||
}),
|
||||
enableDirectIssuance: z.boolean(),
|
||||
status: z.nativeEnum(CaStatus),
|
||||
configuration: z.object({
|
||||
dnsAppConnection: z.object({
|
||||
id: z.string(),
|
||||
name: z.string()
|
||||
}),
|
||||
// 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()
|
||||
})
|
||||
})
|
||||
.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 } = useGetCa({
|
||||
caName: (popUp?.ca?.data as { name: string })?.name || "",
|
||||
projectId: currentWorkspace?.id || "",
|
||||
type: (popUp?.ca?.data as { type: CaType })?.type || ""
|
||||
});
|
||||
|
||||
const { mutateAsync: createMutateAsync } = useCreateCa();
|
||||
const { mutateAsync: updateMutateAsync } = useUpdateCa();
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
reset,
|
||||
formState: { isSubmitting },
|
||||
watch
|
||||
} = useForm<FormData>({
|
||||
resolver: zodResolver(schema),
|
||||
defaultValues: {
|
||||
type: CaType.ACME,
|
||||
name: "",
|
||||
status: CaStatus.ACTIVE,
|
||||
enableDirectIssuance: true,
|
||||
configuration: {
|
||||
dnsAppConnection: {
|
||||
id: "",
|
||||
name: ""
|
||||
},
|
||||
dnsProviderConfig: {
|
||||
provider: AcmeDnsProvider.ROUTE53,
|
||||
hostedZoneId: ""
|
||||
},
|
||||
directoryUrl: "",
|
||||
accountEmail: ""
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const caType = watch("type");
|
||||
const dnsProvider = watch("configuration.dnsProviderConfig.provider");
|
||||
|
||||
const { data: availableConnections, isPending } = useListAvailableAppConnections(
|
||||
AppConnection.AWS,
|
||||
{
|
||||
enabled: dnsProvider === AcmeDnsProvider.ROUTE53
|
||||
}
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (ca) {
|
||||
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,
|
||||
enableDirectIssuance: ca.enableDirectIssuance,
|
||||
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,
|
||||
name: "",
|
||||
status: CaStatus.ACTIVE,
|
||||
enableDirectIssuance: true,
|
||||
configuration: {
|
||||
dnsAppConnection: {
|
||||
id: "",
|
||||
name: ""
|
||||
},
|
||||
dnsProviderConfig: {
|
||||
provider: AcmeDnsProvider.ROUTE53,
|
||||
hostedZoneId: ""
|
||||
},
|
||||
directoryUrl: "",
|
||||
accountEmail: ""
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [ca, availableConnections]);
|
||||
|
||||
const onFormSubmit = async ({
|
||||
type,
|
||||
name,
|
||||
enableDirectIssuance,
|
||||
status,
|
||||
configuration
|
||||
}: FormData) => {
|
||||
try {
|
||||
if (!currentWorkspace?.slug) return;
|
||||
|
||||
if (ca && type !== CaType.INTERNAL) {
|
||||
await updateMutateAsync({
|
||||
caName: ca.name,
|
||||
projectId: currentWorkspace.id,
|
||||
name,
|
||||
type,
|
||||
status,
|
||||
enableDirectIssuance,
|
||||
configuration: {
|
||||
...configuration,
|
||||
dnsAppConnectionId: configuration.dnsAppConnection.id
|
||||
}
|
||||
});
|
||||
} else {
|
||||
await createMutateAsync({
|
||||
projectId: currentWorkspace.id,
|
||||
name,
|
||||
type,
|
||||
status,
|
||||
enableDirectIssuance,
|
||||
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}
|
||||
isRequired
|
||||
>
|
||||
<Input {...field} placeholder="my-external-ca" isDisabled={Boolean(ca)} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{caType === CaType.ACME && (
|
||||
<>
|
||||
<Controller
|
||||
control={control}
|
||||
name="configuration.dnsProviderConfig.provider"
|
||||
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.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=""
|
||||
name="configuration.directoryUrl"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Directory URL"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
isRequired
|
||||
>
|
||||
<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}
|
||||
isRequired
|
||||
>
|
||||
<Input {...field} placeholder="[email protected]" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<Controller
|
||||
control={control}
|
||||
name="enableDirectIssuance"
|
||||
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">Enable 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>
|
||||
);
|
||||
};
|
||||
+141
@@ -0,0 +1,141 @@
|
||||
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, CaType, 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 (caName: string, type: CaType) => {
|
||||
try {
|
||||
if (!currentWorkspace?.id) return;
|
||||
|
||||
await deleteCa({ caName, type, projectId: currentWorkspace.id });
|
||||
|
||||
createNotification({
|
||||
text: "Successfully deleted CA",
|
||||
type: "success"
|
||||
});
|
||||
|
||||
handlePopUpClose("deleteCa");
|
||||
} catch {
|
||||
createNotification({
|
||||
text: "Failed to delete CA",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onUpdateCaStatus = async ({
|
||||
name,
|
||||
type,
|
||||
status
|
||||
}: {
|
||||
name: string;
|
||||
type: CaType;
|
||||
status: CaStatus;
|
||||
}) => {
|
||||
try {
|
||||
if (!currentWorkspace?.slug) return;
|
||||
|
||||
await updateCa({ caName: name, type, status, projectId: currentWorkspace.id });
|
||||
|
||||
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 ? "enable" : "disable"} 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"
|
||||
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 { name: string })?.name,
|
||||
(popUp?.deleteCa?.data as { type: CaType })?.type
|
||||
)
|
||||
}
|
||||
/>
|
||||
<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)}
|
||||
buttonText="Proceed"
|
||||
deleteKey="confirm"
|
||||
onDeleteApproved={() =>
|
||||
onUpdateCaStatus(
|
||||
popUp?.caStatus?.data as { name: string; type: CaType; status: CaStatus }
|
||||
)
|
||||
}
|
||||
/>
|
||||
<UpgradePlanModal
|
||||
isOpen={popUp.upgradePlan.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
||||
text={(popUp.upgradePlan?.data as { description: string })?.description}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+189
@@ -0,0 +1,189 @@
|
||||
import {
|
||||
faBan,
|
||||
faCertificate,
|
||||
faEllipsis,
|
||||
faPencil,
|
||||
faTrash
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
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 { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
type Props = {
|
||||
handlePopUpOpen: (
|
||||
popUpName: keyof UsePopUpState<["ca", "deleteCa", "caStatus", "upgradePlan"]>,
|
||||
data?: {
|
||||
name?: string;
|
||||
type?: CaType;
|
||||
status?: CaStatus;
|
||||
description?: string;
|
||||
}
|
||||
) => void;
|
||||
};
|
||||
|
||||
export const ExternalCaTable = ({ handlePopUpOpen }: Props) => {
|
||||
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={() => {
|
||||
handlePopUpOpen("ca", {
|
||||
name: ca.name,
|
||||
type: ca.type
|
||||
});
|
||||
}}
|
||||
>
|
||||
<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">
|
||||
<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", {
|
||||
name: ca.name,
|
||||
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}
|
||||
a={ProjectPermissionSub.CertificateAuthorities}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
className={twMerge(
|
||||
!isAllowed &&
|
||||
"pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handlePopUpOpen("caStatus", {
|
||||
name: ca.name,
|
||||
type: ca.type,
|
||||
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", {
|
||||
name: ca.name,
|
||||
type: ca.type
|
||||
});
|
||||
}}
|
||||
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>
|
||||
);
|
||||
};
|
||||
+244
@@ -0,0 +1,244 @@
|
||||
import { useState } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
Button,
|
||||
FormControl,
|
||||
Input,
|
||||
Modal,
|
||||
ModalContent,
|
||||
Select,
|
||||
SelectItem,
|
||||
TextArea
|
||||
} from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { useGetCert, useImportCertificate, useListWorkspacePkiCollections } from "@app/hooks/api";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
import { CertificateContent } from "./CertificateContent";
|
||||
|
||||
const schema = z.object({
|
||||
certificatePem: z.string().trim().min(1, "Certificate PEM is required"),
|
||||
privateKeyPem: z.string().trim().min(1, "Private Key PEM is required"),
|
||||
chainPem: z.string().trim().min(1, "Certificate Chain PEM is required"),
|
||||
|
||||
friendlyName: z.string(),
|
||||
collectionId: z.string().optional()
|
||||
});
|
||||
|
||||
export type FormData = z.infer<typeof schema>;
|
||||
|
||||
type Props = {
|
||||
popUp: UsePopUpState<["certificateImport"]>;
|
||||
handlePopUpToggle: (
|
||||
popUpName: keyof UsePopUpState<["certificateImport"]>,
|
||||
state?: boolean
|
||||
) => void;
|
||||
};
|
||||
|
||||
type TCertificateDetails = {
|
||||
serialNumber: string;
|
||||
certificate: string;
|
||||
certificateChain: string;
|
||||
privateKey: string;
|
||||
};
|
||||
|
||||
export const CertificateImportModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
const [certificateDetails, setCertificateDetails] = useState<TCertificateDetails | null>(null);
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { data: cert } = useGetCert(
|
||||
(popUp?.certificateImport?.data as { serialNumber: string })?.serialNumber || ""
|
||||
);
|
||||
|
||||
const { data } = useListWorkspacePkiCollections({
|
||||
workspaceId: currentWorkspace?.id || ""
|
||||
});
|
||||
|
||||
const { mutateAsync: importCertificate } = useImportCertificate();
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
reset,
|
||||
formState: { isSubmitting }
|
||||
} = useForm<FormData>({
|
||||
resolver: zodResolver(schema)
|
||||
});
|
||||
|
||||
const onFormSubmit = async ({
|
||||
certificatePem,
|
||||
privateKeyPem,
|
||||
chainPem,
|
||||
friendlyName,
|
||||
collectionId
|
||||
}: FormData) => {
|
||||
try {
|
||||
if (!currentWorkspace?.slug) return;
|
||||
|
||||
const { serialNumber, certificate, certificateChain, privateKey } = await importCertificate({
|
||||
projectSlug: currentWorkspace.slug,
|
||||
|
||||
certificatePem,
|
||||
privateKeyPem,
|
||||
chainPem,
|
||||
|
||||
friendlyName,
|
||||
pkiCollectionId: collectionId
|
||||
});
|
||||
|
||||
reset();
|
||||
|
||||
setCertificateDetails({
|
||||
serialNumber,
|
||||
certificate,
|
||||
certificateChain,
|
||||
privateKey
|
||||
});
|
||||
|
||||
createNotification({
|
||||
text: "Successfully imported certificate",
|
||||
type: "success"
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
text: "Failed to import certificate",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={popUp?.certificateImport?.isOpen}
|
||||
onOpenChange={(isOpen) => {
|
||||
handlePopUpToggle("certificateImport", isOpen);
|
||||
reset();
|
||||
setCertificateDetails(null);
|
||||
}}
|
||||
>
|
||||
<ModalContent title={`${cert ? "View" : "Import"} Certificate`}>
|
||||
{!certificateDetails ? (
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
<Controller
|
||||
control={control}
|
||||
name="collectionId"
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Certificate Collection"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
isOptional
|
||||
className="mt-4"
|
||||
>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className="w-full"
|
||||
isDisabled={Boolean(cert)}
|
||||
>
|
||||
{(data?.collections || []).map(({ id, name }) => (
|
||||
<SelectItem value={id} key={`pki-collection-${id}`}>
|
||||
{name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</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=""
|
||||
name="certificatePem"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Leaf Certificate PEM"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
isRequired
|
||||
>
|
||||
<TextArea {...field} isDisabled={Boolean(cert)} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name="privateKeyPem"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Private Key PEM"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
isRequired
|
||||
>
|
||||
<TextArea {...field} isDisabled={Boolean(cert)} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name="chainPem"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Certificate Chain PEM"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
isRequired
|
||||
>
|
||||
<TextArea {...field} isDisabled={Boolean(cert)} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{!cert && (
|
||||
<div className="mt-4 flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
>
|
||||
Create
|
||||
</Button>
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
variant="plain"
|
||||
onClick={() => handlePopUpToggle("certificateImport", false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
) : (
|
||||
<CertificateContent
|
||||
serialNumber={certificateDetails.serialNumber}
|
||||
certificate={certificateDetails.certificate}
|
||||
certificateChain={certificateDetails.certificateChain}
|
||||
privateKey={certificateDetails.privateKey}
|
||||
/>
|
||||
)}
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
+23
-10
@@ -1,4 +1,4 @@
|
||||
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faArrowRight, faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
@@ -13,6 +13,7 @@ import { useDeleteCert } from "@app/hooks/api";
|
||||
import { usePopUp } from "@app/hooks/usePopUp";
|
||||
|
||||
import { CertificateCertModal } from "./CertificateCertModal";
|
||||
import { CertificateImportModal } from "./CertificateImportModal";
|
||||
import { CertificateModal } from "./CertificateModal";
|
||||
import { CertificateRevocationModal } from "./CertificateRevocationModal";
|
||||
import { CertificatesTable } from "./CertificatesTable";
|
||||
@@ -23,6 +24,7 @@ export const CertificatesSection = () => {
|
||||
|
||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||
"certificate",
|
||||
"certificateImport",
|
||||
"certificateCert",
|
||||
"deleteCertificate",
|
||||
"revokeCertificate"
|
||||
@@ -58,20 +60,31 @@ export const CertificatesSection = () => {
|
||||
a={ProjectPermissionSub.Certificates}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
colorSchema="primary"
|
||||
type="submit"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => handlePopUpOpen("certificate")}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
Issue
|
||||
</Button>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
leftIcon={<FontAwesomeIcon icon={faArrowRight} />}
|
||||
onClick={() => handlePopUpOpen("certificateImport")}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
Import
|
||||
</Button>
|
||||
<Button
|
||||
colorSchema="primary"
|
||||
type="submit"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => handlePopUpOpen("certificate")}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
Issue
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
<CertificatesTable handlePopUpOpen={handlePopUpOpen} />
|
||||
<CertificateModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||
<CertificateImportModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||
<CertificateCertModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||
<CertificateRevocationModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||
<DeleteActionModal
|
||||
|
||||
+11
-6
@@ -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: 5000
|
||||
}
|
||||
);
|
||||
|
||||
const getCertStatusBadge = (status: string, notAfter: string) => {
|
||||
if (status === CertStatus.REVOKED) {
|
||||
|
||||
+69
-21
@@ -5,7 +5,14 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { Button, IconButton, Modal, ModalContent, Tooltip } from "@app/components/v2";
|
||||
import {
|
||||
Button,
|
||||
GenericFieldLabel,
|
||||
IconButton,
|
||||
Modal,
|
||||
ModalContent,
|
||||
Tooltip
|
||||
} from "@app/components/v2";
|
||||
import {
|
||||
ProjectPermissionPkiSubscriberActions,
|
||||
ProjectPermissionSub,
|
||||
@@ -13,8 +20,13 @@ import {
|
||||
useWorkspace
|
||||
} from "@app/context";
|
||||
import { useTimedReset } from "@app/hooks";
|
||||
import { useGetPkiSubscriber, useIssuePkiSubscriberCert } from "@app/hooks/api";
|
||||
import {
|
||||
useGetPkiSubscriber,
|
||||
useIssuePkiSubscriberCert,
|
||||
useOrderPkiSubscriberCert
|
||||
} from "@app/hooks/api";
|
||||
import { pkiSubscriberStatusToNameMap } from "@app/hooks/api/pkiSubscriber/constants";
|
||||
import { SubscriberOperationStatus } from "@app/hooks/api/pkiSubscriber/types";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
import { CertificateContent } from "../../CertificatesPage/components/CertificateContent";
|
||||
@@ -41,31 +53,48 @@ export const PkiSubscriberDetailsSection = ({ subscriberName, handlePopUpOpen }:
|
||||
initialState: "Copy ID to clipboard"
|
||||
});
|
||||
|
||||
const { data: pkiSubscriber } = useGetPkiSubscriber({
|
||||
subscriberName,
|
||||
projectId
|
||||
});
|
||||
const { data: pkiSubscriber } = useGetPkiSubscriber(
|
||||
{
|
||||
subscriberName,
|
||||
projectId
|
||||
},
|
||||
{
|
||||
refetchInterval: 5000
|
||||
}
|
||||
);
|
||||
|
||||
const { mutateAsync: issuePkiSubscriberCert, isPending: isIssuingCert } =
|
||||
useIssuePkiSubscriberCert();
|
||||
|
||||
const { mutateAsync: orderPkiSubscriberCert, isPending: isOrderingCert } =
|
||||
useOrderPkiSubscriberCert();
|
||||
|
||||
const onIssuePkiSubscriberCert = async () => {
|
||||
try {
|
||||
const response = await issuePkiSubscriberCert({ subscriberName, projectId });
|
||||
if (pkiSubscriber?.supportsImmediateCertIssuance) {
|
||||
const response = await issuePkiSubscriberCert({ subscriberName, projectId });
|
||||
|
||||
setCertificateDetails({
|
||||
serialNumber: response.serialNumber,
|
||||
certificate: response.certificate,
|
||||
certificateChain: response.certificateChain,
|
||||
privateKey: response.privateKey
|
||||
});
|
||||
setCertificateDetails({
|
||||
serialNumber: response.serialNumber,
|
||||
certificate: response.certificate,
|
||||
certificateChain: response.certificateChain,
|
||||
privateKey: response.privateKey
|
||||
});
|
||||
|
||||
setIsModalOpen(true);
|
||||
setIsModalOpen(true);
|
||||
|
||||
createNotification({
|
||||
text: "Successfully issued certificate",
|
||||
type: "success"
|
||||
});
|
||||
createNotification({
|
||||
text: "Successfully issued certificate",
|
||||
type: "success"
|
||||
});
|
||||
} else {
|
||||
await orderPkiSubscriberCert({ subscriberName, projectId });
|
||||
|
||||
createNotification({
|
||||
text: "Successfully ordered certificate. It will be issued after CA processing which could take a few minutes.",
|
||||
type: "info"
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
@@ -148,17 +177,36 @@ export const PkiSubscriberDetailsSection = ({ subscriberName, handlePopUpOpen }:
|
||||
<p className="text-sm font-semibold text-mineshaft-300">Common Name</p>
|
||||
<p className="text-sm text-mineshaft-300">{pkiSubscriber.commonName}</p>
|
||||
</div>
|
||||
{pkiSubscriber.lastOperationAt && (
|
||||
<div className="mb-4">
|
||||
<p className="text-sm font-semibold text-mineshaft-300">Last Operation (Local Time)</p>
|
||||
<p className="text-sm text-mineshaft-300">
|
||||
{new Date(pkiSubscriber.lastOperationAt).toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{pkiSubscriber.lastOperationStatus === SubscriberOperationStatus.FAILED && (
|
||||
<div className="mb-4">
|
||||
<GenericFieldLabel labelClassName="text-red" label="Last Operation Status">
|
||||
<p className="break-words rounded bg-mineshaft-600 p-2 text-xs">
|
||||
{pkiSubscriber.lastOperationMessage}
|
||||
</p>
|
||||
</GenericFieldLabel>
|
||||
</div>
|
||||
)}
|
||||
{canIssuePkiSubscriberCert && (
|
||||
<Button
|
||||
className="mt-4 w-full"
|
||||
className="mt-2 w-full"
|
||||
colorSchema="primary"
|
||||
type="button"
|
||||
isLoading={isIssuingCert}
|
||||
isLoading={isIssuingCert || isOrderingCert}
|
||||
onClick={() => {
|
||||
onIssuePkiSubscriberCert();
|
||||
}}
|
||||
>
|
||||
Issue Certificate
|
||||
{pkiSubscriber?.supportsImmediateCertIssuance
|
||||
? "Issue Certificate"
|
||||
: "Order Certificate"}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
+310
-172
@@ -1,4 +1,4 @@
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
@@ -16,14 +16,18 @@ import {
|
||||
Modal,
|
||||
ModalContent,
|
||||
Select,
|
||||
SelectItem
|
||||
SelectItem,
|
||||
Tab,
|
||||
TabList,
|
||||
TabPanel,
|
||||
Tabs
|
||||
} from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import {
|
||||
CaStatus,
|
||||
CaType,
|
||||
useCreatePkiSubscriber,
|
||||
useGetPkiSubscriber,
|
||||
useListWorkspaceCas,
|
||||
useListCasByProjectId,
|
||||
useListWorkspacePkiSubscribers,
|
||||
useUpdatePkiSubscriber
|
||||
} from "@app/hooks/api";
|
||||
@@ -33,19 +37,25 @@ import {
|
||||
} from "@app/hooks/api/certificates/constants";
|
||||
import { CertExtendedKeyUsage, CertKeyUsage } from "@app/hooks/api/certificates/enums";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
import { convertTimeUnitValueToDays, TimeUnit } from "@app/lib/fn/date";
|
||||
|
||||
type Props = {
|
||||
popUp: UsePopUpState<["pkiSubscriber"]>;
|
||||
handlePopUpToggle: (popUpName: keyof UsePopUpState<["pkiSubscriber"]>, state?: boolean) => void;
|
||||
};
|
||||
|
||||
enum FormTab {
|
||||
Configuration = "configuration",
|
||||
Advanced = "advanced"
|
||||
}
|
||||
|
||||
const schema = z
|
||||
.object({
|
||||
name: z.string().trim().min(1, "Name is required"),
|
||||
caId: z.string().min(1, "Issuing CA is required"),
|
||||
commonName: z.string().trim().min(1, "Common Name is required"),
|
||||
subjectAlternativeNames: z.string(),
|
||||
ttl: z.string().trim(),
|
||||
ttl: z.string().trim().optional(),
|
||||
keyUsages: z.object({
|
||||
[CertKeyUsage.DIGITAL_SIGNATURE]: z.boolean().optional(),
|
||||
[CertKeyUsage.KEY_ENCIPHERMENT]: z.boolean().optional(),
|
||||
@@ -64,7 +74,10 @@ const schema = z
|
||||
[CertExtendedKeyUsage.OCSP_SIGNING]: z.boolean().optional(),
|
||||
[CertExtendedKeyUsage.SERVER_AUTH]: z.boolean().optional(),
|
||||
[CertExtendedKeyUsage.TIMESTAMPING]: z.boolean().optional()
|
||||
})
|
||||
}),
|
||||
enableAutoRenewal: z.boolean().optional().default(false),
|
||||
renewalBefore: z.number().min(1).optional(),
|
||||
renewalUnit: z.nativeEnum(TimeUnit).optional()
|
||||
})
|
||||
.required();
|
||||
|
||||
@@ -74,10 +87,8 @@ 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 [tabValue, setTabValue] = useState<FormTab>(FormTab.Configuration);
|
||||
|
||||
const { data: pkiSubscriber } = useGetPkiSubscriber({
|
||||
subscriberName:
|
||||
@@ -93,6 +104,7 @@ export const PkiSubscriberModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
handleSubmit,
|
||||
reset,
|
||||
setValue,
|
||||
watch,
|
||||
formState: { isSubmitting }
|
||||
} = useForm<FormData>({
|
||||
resolver: zodResolver(schema),
|
||||
@@ -106,10 +118,17 @@ export const PkiSubscriberModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
[CertKeyUsage.DIGITAL_SIGNATURE]: true,
|
||||
[CertKeyUsage.KEY_ENCIPHERMENT]: true
|
||||
},
|
||||
extendedKeyUsages: {}
|
||||
extendedKeyUsages: {},
|
||||
enableAutoRenewal: false,
|
||||
renewalBefore: 7,
|
||||
renewalUnit: TimeUnit.DAY
|
||||
}
|
||||
});
|
||||
|
||||
const selectedCaId = watch("caId");
|
||||
const selectedCa = cas?.find((ca) => ca.id === selectedCaId);
|
||||
const selectedAutoRenewalState = watch("enableAutoRenewal");
|
||||
|
||||
useEffect(() => {
|
||||
if (pkiSubscriber) {
|
||||
reset({
|
||||
@@ -121,7 +140,10 @@ export const PkiSubscriberModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
keyUsages: Object.fromEntries((pkiSubscriber.keyUsages || []).map((name) => [name, true])),
|
||||
extendedKeyUsages: Object.fromEntries(
|
||||
(pkiSubscriber.extendedKeyUsages || []).map((name) => [name, true])
|
||||
)
|
||||
),
|
||||
enableAutoRenewal: pkiSubscriber.enableAutoRenewal || false,
|
||||
renewalBefore: pkiSubscriber.autoRenewalPeriodInDays || 7,
|
||||
renewalUnit: TimeUnit.DAY
|
||||
});
|
||||
} else {
|
||||
reset({
|
||||
@@ -134,7 +156,10 @@ export const PkiSubscriberModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
[CertKeyUsage.DIGITAL_SIGNATURE]: true,
|
||||
[CertKeyUsage.KEY_ENCIPHERMENT]: true
|
||||
},
|
||||
extendedKeyUsages: {}
|
||||
extendedKeyUsages: {},
|
||||
enableAutoRenewal: false,
|
||||
renewalBefore: 7,
|
||||
renewalUnit: TimeUnit.DAY
|
||||
});
|
||||
}
|
||||
}, [pkiSubscriber, reset]);
|
||||
@@ -152,7 +177,10 @@ export const PkiSubscriberModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
subjectAlternativeNames,
|
||||
ttl,
|
||||
keyUsages,
|
||||
extendedKeyUsages
|
||||
extendedKeyUsages,
|
||||
enableAutoRenewal,
|
||||
renewalBefore,
|
||||
renewalUnit
|
||||
}: FormData) => {
|
||||
try {
|
||||
if (!projectId) return;
|
||||
@@ -190,6 +218,10 @@ export const PkiSubscriberModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
.map((san) => san.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
const autoRenewalPeriodInDays = enableAutoRenewal
|
||||
? convertTimeUnitValueToDays(renewalUnit, renewalBefore)
|
||||
: undefined;
|
||||
|
||||
if (pkiSubscriber) {
|
||||
await updateMutateAsync({
|
||||
subscriberName: pkiSubscriber.name,
|
||||
@@ -200,7 +232,9 @@ export const PkiSubscriberModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
subjectAlternativeNames: subjectAlternativeNamesList,
|
||||
ttl,
|
||||
keyUsages: keyUsagesList,
|
||||
extendedKeyUsages: extendedKeyUsagesList
|
||||
extendedKeyUsages: extendedKeyUsagesList,
|
||||
enableAutoRenewal,
|
||||
autoRenewalPeriodInDays
|
||||
});
|
||||
} else {
|
||||
await createMutateAsync({
|
||||
@@ -211,7 +245,9 @@ export const PkiSubscriberModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
subjectAlternativeNames: subjectAlternativeNamesList,
|
||||
ttl,
|
||||
keyUsages: keyUsagesList,
|
||||
extendedKeyUsages: extendedKeyUsagesList
|
||||
extendedKeyUsages: extendedKeyUsagesList,
|
||||
enableAutoRenewal,
|
||||
autoRenewalPeriodInDays
|
||||
});
|
||||
}
|
||||
|
||||
@@ -240,169 +276,271 @@ export const PkiSubscriberModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
}}
|
||||
>
|
||||
<ModalContent title={`${pkiSubscriber ? "Update" : "Add"} PKI Subscriber`}>
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
{pkiSubscriber && (
|
||||
<FormControl label="Subscriber ID">
|
||||
<Input value={pkiSubscriber.id} isDisabled className="bg-white/[0.07]" />
|
||||
</FormControl>
|
||||
)}
|
||||
<Controller
|
||||
control={control}
|
||||
name="name"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Subscriber Name"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
isRequired
|
||||
>
|
||||
<Input {...field} placeholder="web-service" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="caId"
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Issuing CA"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
isRequired
|
||||
>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className="w-full"
|
||||
>
|
||||
{(cas || []).map(({ id, dn }) => (
|
||||
<SelectItem value={id} key={`ca-${id}`}>
|
||||
{dn}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="commonName"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Common Name"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
isRequired
|
||||
>
|
||||
<Input {...field} placeholder="web.example.com" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="subjectAlternativeNames"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Subject Alternative Names (SANs)"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="app1.example.com, app2.example.com, ..." />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="ttl"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="TTL"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
isRequired
|
||||
>
|
||||
<Input {...field} placeholder="2 days, 1d, 2h, 1y, ..." />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Accordion type="single" collapsible className="w-full">
|
||||
<AccordionItem value="key-usages" className="data-[state=open]:border-none">
|
||||
<AccordionTrigger className="h-fit flex-none pl-1 text-sm">
|
||||
<div className="order-1 ml-3">Key Usage</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<form
|
||||
onSubmit={handleSubmit(onFormSubmit, (fields) => {
|
||||
setTabValue(
|
||||
["name", "caId", "commonName", "subjectAlternativeNames", "ttl"].includes(
|
||||
Object.keys(fields)[0]
|
||||
)
|
||||
? FormTab.Configuration
|
||||
: FormTab.Advanced
|
||||
);
|
||||
})}
|
||||
>
|
||||
<Tabs value={tabValue} onValueChange={(value) => setTabValue(value as FormTab)}>
|
||||
<TabList>
|
||||
<Tab value={FormTab.Configuration}>Configuration</Tab>
|
||||
<Tab value={FormTab.Advanced}>Advanced</Tab>
|
||||
</TabList>
|
||||
<TabPanel value={FormTab.Configuration}>
|
||||
{pkiSubscriber && (
|
||||
<FormControl label="Subscriber ID">
|
||||
<Input value={pkiSubscriber.id} isDisabled className="bg-white/[0.07]" />
|
||||
</FormControl>
|
||||
)}
|
||||
<Controller
|
||||
control={control}
|
||||
name="name"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Subscriber Name"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
isRequired
|
||||
>
|
||||
<Input {...field} placeholder="web-service" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="caId"
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Issuing CA"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
isRequired
|
||||
>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className="w-full"
|
||||
>
|
||||
{(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>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="commonName"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Common Name"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
isRequired
|
||||
>
|
||||
<Input {...field} placeholder="web.example.com" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="subjectAlternativeNames"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Subject Alternative Names (SANs)"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="app1.example.com, app2.example.com, ..." />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{selectedCa?.type !== CaType.ACME && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="keyUsages"
|
||||
render={({ field: { onChange, value }, fieldState: { error } }) => {
|
||||
return (
|
||||
name="ttl"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="TTL"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
isRequired
|
||||
>
|
||||
<Input {...field} placeholder="2 days, 1d, 2h, 1y, ..." />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{selectedCa?.type !== CaType.ACME && (
|
||||
<Accordion type="single" collapsible className="w-full">
|
||||
<AccordionItem value="key-usages" className="data-[state=open]:border-none">
|
||||
<AccordionTrigger className="h-fit flex-none pl-1 text-sm">
|
||||
<div className="order-1 ml-3">Key Usage</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<Controller
|
||||
control={control}
|
||||
name="keyUsages"
|
||||
render={({ field: { onChange, value }, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl
|
||||
label="Key Usage"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
>
|
||||
<div className="mb-7 mt-2 grid grid-cols-2 gap-2">
|
||||
{KEY_USAGES_OPTIONS.map(({ label, value: optionValue }) => {
|
||||
return (
|
||||
<Checkbox
|
||||
id={optionValue}
|
||||
key={optionValue}
|
||||
className="data-[state=checked]:bg-primary"
|
||||
isChecked={value[optionValue]}
|
||||
onCheckedChange={(state) => {
|
||||
onChange({
|
||||
...value,
|
||||
[optionValue]: state
|
||||
});
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</Checkbox>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="extendedKeyUsages"
|
||||
render={({ field: { onChange, value }, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl
|
||||
label="Extended Key Usage"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
>
|
||||
<div className="mb-7 mt-2 grid grid-cols-2 gap-2">
|
||||
{EXTENDED_KEY_USAGES_OPTIONS.map(
|
||||
({ label, value: optionValue }) => {
|
||||
return (
|
||||
<Checkbox
|
||||
id={optionValue}
|
||||
key={optionValue}
|
||||
className="data-[state=checked]:bg-primary"
|
||||
isChecked={value[optionValue]}
|
||||
onCheckedChange={(state) => {
|
||||
onChange({
|
||||
...value,
|
||||
[optionValue]: state
|
||||
});
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</Checkbox>
|
||||
);
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
)}
|
||||
</TabPanel>
|
||||
<TabPanel value={FormTab.Advanced}>
|
||||
<Controller
|
||||
control={control}
|
||||
name="enableAutoRenewal"
|
||||
render={({ field: { onChange, value }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
tooltipText="If enabled, a new certificate will be issued automatically X days before the current certificate expires."
|
||||
>
|
||||
<Checkbox
|
||||
id="enableAutoRenewal"
|
||||
isChecked={value}
|
||||
onCheckedChange={onChange}
|
||||
className="data-[state=checked]:bg-primary"
|
||||
>
|
||||
Enable Certificate Auto Renewal
|
||||
</Checkbox>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{selectedAutoRenewalState && (
|
||||
<div className="flex items-center">
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue={7}
|
||||
name="renewalBefore"
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Key Usage"
|
||||
label="Renewal Before"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
className="w-full"
|
||||
isRequired
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
placeholder="5"
|
||||
type="number"
|
||||
min={1}
|
||||
onChange={(e) => onChange(Number(e.target.value))}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="renewalUnit"
|
||||
defaultValue={TimeUnit.DAY}
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
className="ml-4"
|
||||
label="Unit"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
>
|
||||
<div className="mb-7 mt-2 grid grid-cols-2 gap-2">
|
||||
{KEY_USAGES_OPTIONS.map(({ label, value: optionValue }) => {
|
||||
return (
|
||||
<Checkbox
|
||||
id={optionValue}
|
||||
key={optionValue}
|
||||
className="data-[state=checked]:bg-primary"
|
||||
isChecked={value[optionValue]}
|
||||
onCheckedChange={(state) => {
|
||||
onChange({
|
||||
...value,
|
||||
[optionValue]: state
|
||||
});
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</Checkbox>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className="w-48"
|
||||
>
|
||||
<SelectItem value={TimeUnit.DAY}>Days</SelectItem>
|
||||
<SelectItem value={TimeUnit.WEEK}>Weeks</SelectItem>
|
||||
<SelectItem value={TimeUnit.MONTH}>Months</SelectItem>
|
||||
<SelectItem value={TimeUnit.YEAR}>Years</SelectItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="extendedKeyUsages"
|
||||
render={({ field: { onChange, value }, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl
|
||||
label="Extended Key Usage"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
>
|
||||
<div className="mb-7 mt-2 grid grid-cols-2 gap-2">
|
||||
{EXTENDED_KEY_USAGES_OPTIONS.map(({ label, value: optionValue }) => {
|
||||
return (
|
||||
<Checkbox
|
||||
id={optionValue}
|
||||
key={optionValue}
|
||||
className="data-[state=checked]:bg-primary"
|
||||
isChecked={value[optionValue]}
|
||||
onCheckedChange={(state) => {
|
||||
onChange({
|
||||
...value,
|
||||
[optionValue]: state
|
||||
});
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</Checkbox>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
<div className="mt-4 flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
|
||||
@@ -1146,8 +1146,8 @@ const certManagerPkiSubscriberDetailsByIDPageRouteRoute =
|
||||
|
||||
const certManagerCertAuthDetailsByIDPageRouteRoute =
|
||||
certManagerCertAuthDetailsByIDPageRouteImport.update({
|
||||
id: '/ca/$caId',
|
||||
path: '/ca/$caId',
|
||||
id: '/ca/$caName',
|
||||
path: '/ca/$caName',
|
||||
getParentRoute: () => certManagerLayoutRoute,
|
||||
} as any)
|
||||
|
||||
@@ -2557,10 +2557,10 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof secretManagerIntegrationsListPageRouteImport
|
||||
parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdSecretManagerLayoutIntegrationsImport
|
||||
}
|
||||
'/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/ca/$caId': {
|
||||
id: '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/ca/$caId'
|
||||
path: '/ca/$caId'
|
||||
fullPath: '/cert-manager/$projectId/ca/$caId'
|
||||
'/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/ca/$caName': {
|
||||
id: '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/ca/$caName'
|
||||
path: '/ca/$caName'
|
||||
fullPath: '/cert-manager/$projectId/ca/$caName'
|
||||
preLoaderRoute: typeof certManagerCertAuthDetailsByIDPageRouteImport
|
||||
parentRoute: typeof certManagerLayoutImport
|
||||
}
|
||||
@@ -4232,7 +4232,7 @@ export interface FileRoutesByFullPath {
|
||||
'/ssh/$projectId/access-management': typeof projectAccessControlPageRouteSshRoute
|
||||
'/cert-manager/$projectId/subscribers/': typeof certManagerPkiSubscribersPageRouteRoute
|
||||
'/secret-manager/$projectId/integrations/': typeof secretManagerIntegrationsListPageRouteRoute
|
||||
'/cert-manager/$projectId/ca/$caId': typeof certManagerCertAuthDetailsByIDPageRouteRoute
|
||||
'/cert-manager/$projectId/ca/$caName': typeof certManagerCertAuthDetailsByIDPageRouteRoute
|
||||
'/cert-manager/$projectId/subscribers/$subscriberName': typeof certManagerPkiSubscriberDetailsByIDPageRouteRoute
|
||||
'/organization/app-connections/$appConnection/oauth/callback': typeof organizationAppConnectionsOauthCallbackPageRouteRoute
|
||||
'/secret-manager/$projectId/integrations/$integrationId': typeof secretManagerIntegrationsDetailsByIDPageRouteRoute
|
||||
@@ -4422,7 +4422,7 @@ export interface FileRoutesByTo {
|
||||
'/ssh/$projectId/access-management': typeof projectAccessControlPageRouteSshRoute
|
||||
'/cert-manager/$projectId/subscribers': typeof certManagerPkiSubscribersPageRouteRoute
|
||||
'/secret-manager/$projectId/integrations': typeof secretManagerIntegrationsListPageRouteRoute
|
||||
'/cert-manager/$projectId/ca/$caId': typeof certManagerCertAuthDetailsByIDPageRouteRoute
|
||||
'/cert-manager/$projectId/ca/$caName': typeof certManagerCertAuthDetailsByIDPageRouteRoute
|
||||
'/cert-manager/$projectId/subscribers/$subscriberName': typeof certManagerPkiSubscriberDetailsByIDPageRouteRoute
|
||||
'/organization/app-connections/$appConnection/oauth/callback': typeof organizationAppConnectionsOauthCallbackPageRouteRoute
|
||||
'/secret-manager/$projectId/integrations/$integrationId': typeof secretManagerIntegrationsDetailsByIDPageRouteRoute
|
||||
@@ -4630,7 +4630,7 @@ export interface FileRoutesById {
|
||||
'/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/access-management': typeof projectAccessControlPageRouteSshRoute
|
||||
'/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers/': typeof certManagerPkiSubscribersPageRouteRoute
|
||||
'/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/': typeof secretManagerIntegrationsListPageRouteRoute
|
||||
'/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/ca/$caId': typeof certManagerCertAuthDetailsByIDPageRouteRoute
|
||||
'/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/ca/$caName': typeof certManagerCertAuthDetailsByIDPageRouteRoute
|
||||
'/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers/$subscriberName': typeof certManagerPkiSubscriberDetailsByIDPageRouteRoute
|
||||
'/_authenticate/_inject-org-details/_org-layout/organization/app-connections/$appConnection/oauth/callback': typeof organizationAppConnectionsOauthCallbackPageRouteRoute
|
||||
'/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/$integrationId': typeof secretManagerIntegrationsDetailsByIDPageRouteRoute
|
||||
@@ -4832,7 +4832,7 @@ export interface FileRouteTypes {
|
||||
| '/ssh/$projectId/access-management'
|
||||
| '/cert-manager/$projectId/subscribers/'
|
||||
| '/secret-manager/$projectId/integrations/'
|
||||
| '/cert-manager/$projectId/ca/$caId'
|
||||
| '/cert-manager/$projectId/ca/$caName'
|
||||
| '/cert-manager/$projectId/subscribers/$subscriberName'
|
||||
| '/organization/app-connections/$appConnection/oauth/callback'
|
||||
| '/secret-manager/$projectId/integrations/$integrationId'
|
||||
@@ -5021,7 +5021,7 @@ export interface FileRouteTypes {
|
||||
| '/ssh/$projectId/access-management'
|
||||
| '/cert-manager/$projectId/subscribers'
|
||||
| '/secret-manager/$projectId/integrations'
|
||||
| '/cert-manager/$projectId/ca/$caId'
|
||||
| '/cert-manager/$projectId/ca/$caName'
|
||||
| '/cert-manager/$projectId/subscribers/$subscriberName'
|
||||
| '/organization/app-connections/$appConnection/oauth/callback'
|
||||
| '/secret-manager/$projectId/integrations/$integrationId'
|
||||
@@ -5227,7 +5227,7 @@ export interface FileRouteTypes {
|
||||
| '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/access-management'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers/'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/ca/$caId'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/ca/$caName'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers/$subscriberName'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/organization/app-connections/$appConnection/oauth/callback'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/$integrationId'
|
||||
@@ -5759,7 +5759,7 @@ export const routeTree = rootRoute
|
||||
"/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/settings",
|
||||
"/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/access-management",
|
||||
"/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers",
|
||||
"/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/ca/$caId",
|
||||
"/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/ca/$caName",
|
||||
"/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/identities/$identityId",
|
||||
"/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/members/$membershipId",
|
||||
"/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/pki-collections/$collectionId",
|
||||
@@ -6033,7 +6033,7 @@ export const routeTree = rootRoute
|
||||
"filePath": "secret-manager/IntegrationsListPage/route.tsx",
|
||||
"parent": "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations"
|
||||
},
|
||||
"/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/ca/$caId": {
|
||||
"/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/ca/$caName": {
|
||||
"filePath": "cert-manager/CertAuthDetailsByIDPage/route.tsx",
|
||||
"parent": "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout"
|
||||
},
|
||||
|
||||
@@ -306,7 +306,7 @@ const certManagerRoutes = route("/cert-manager/$projectId", [
|
||||
route("/certificates", "cert-manager/CertificatesPage/route.tsx"),
|
||||
route("/certificate-authorities", "cert-manager/CertificateAuthoritiesPage/route.tsx"),
|
||||
route("/alerting", "cert-manager/AlertingPage/route.tsx"),
|
||||
route("/ca/$caId", "cert-manager/CertAuthDetailsByIDPage/route.tsx"),
|
||||
route("/ca/$caName", "cert-manager/CertAuthDetailsByIDPage/route.tsx"),
|
||||
route("/pki-collections/$collectionId", "cert-manager/PkiCollectionDetailsByIDPage/routes.tsx"),
|
||||
route("/settings", "cert-manager/SettingsPage/route.tsx"),
|
||||
route("/access-management", "project/AccessControlPage/route-cert-manager.tsx"),
|
||||
|
||||
Reference in New Issue
Block a user