diff --git a/backend/src/db/migrations/20250512133213_add-external-ca-pki.ts b/backend/src/db/migrations/20250512133213_add-external-ca-pki.ts index fd1c42f84..907022035 100644 --- a/backend/src/db/migrations/20250512133213_add-external-ca-pki.ts +++ b/backend/src/db/migrations/20250512133213_add-external-ca-pki.ts @@ -88,6 +88,9 @@ export async function up(knex: Knex): Promise { if (await knex.schema.hasTable(TableName.PkiSubscriber)) { await knex.schema.alterTable(TableName.PkiSubscriber, (t) => { t.string("ttl").nullable().alter(); + t.string("lastOperationStatus"); + t.text("lastOperationMessage"); + t.string("lastOperationAt"); }); } } @@ -164,4 +167,12 @@ export async function down(knex: Knex): Promise { if (hasExternalCATable) { await knex.schema.dropTable(TableName.ExternalCertificateAuthority); } + + if (await knex.schema.hasTable(TableName.PkiSubscriber)) { + await knex.schema.alterTable(TableName.PkiSubscriber, (t) => { + t.dropColumn("lastOperationStatus"); + t.dropColumn("lastOperationMessage"); + t.dropColumn("lastOperationAt"); + }); + } } diff --git a/backend/src/db/schemas/pki-subscribers.ts b/backend/src/db/schemas/pki-subscribers.ts index 920338327..0f63d7fa8 100644 --- a/backend/src/db/schemas/pki-subscribers.ts +++ b/backend/src/db/schemas/pki-subscribers.ts @@ -19,7 +19,10 @@ export const PkiSubscribersSchema = z.object({ ttl: z.string().nullable().optional(), keyUsages: z.string().array(), extendedKeyUsages: z.string().array(), - status: z.string() + status: z.string(), + lastOperationStatus: z.string().nullable().optional(), + lastOperationMessage: z.string().nullable().optional(), + lastOperationAt: z.string().nullable().optional() }); export type TPkiSubscribers = z.infer; diff --git a/backend/src/server/routes/v1/pki-subscriber-router.ts b/backend/src/server/routes/v1/pki-subscriber-router.ts index 8de6b85e1..77fbc3a49 100644 --- a/backend/src/server/routes/v1/pki-subscriber-router.ts +++ b/backend/src/server/routes/v1/pki-subscriber-router.ts @@ -180,7 +180,7 @@ export const registerPkiSubscriberRouter = async (server: FastifyZodProvider) => ttl: z .string() .trim() - .refine((val) => ms(val) > 0, "TTL must be a positive number") + .refine((val) => !val || ms(val) > 0, "TTL must be a positive number") .optional() .describe(PKI_SUBSCRIBERS.UPDATE.ttl), keyUsages: z diff --git a/backend/src/services/certificate-authority/certificate-authority-dal.ts b/backend/src/services/certificate-authority/certificate-authority-dal.ts index 1e81e0a05..32f053117 100644 --- a/backend/src/services/certificate-authority/certificate-authority-dal.ts +++ b/backend/src/services/certificate-authority/certificate-authority-dal.ts @@ -86,8 +86,8 @@ export const certificateAuthorityDALFactory = (db: TDbClient) => { serialNumber: result.internalSerialNumber, maxPathLength: result.internalMaxPathLength, keyAlgorithm: result.internalKeyAlgorithm, - notBefore: result.internalNotBefore, - notAfter: result.internalNotAfter, + notBefore: result.internalNotBefore?.toISOString(), + notAfter: result.internalNotAfter?.toISOString(), activeCaCertId: result.internalActiveCaCertId, certificateAuthorityId: result.internalCertificateAuthorityId } @@ -232,8 +232,8 @@ export const certificateAuthorityDALFactory = (db: TDbClient) => { serialNumber: ca.internalSerialNumber, maxPathLength: ca.internalMaxPathLength, keyAlgorithm: ca.internalKeyAlgorithm, - notBefore: ca.internalNotBefore, - notAfter: ca.internalNotAfter, + notBefore: ca.internalNotBefore?.toISOString(), + notAfter: ca.internalNotAfter?.toISOString(), activeCaCertId: ca.internalActiveCaCertId, certificateAuthorityId: ca.internalCertificateAuthorityId } diff --git a/backend/src/services/certificate-authority/certificate-authority-queue.ts b/backend/src/services/certificate-authority/certificate-authority-queue.ts index 54b451888..192c1ae87 100644 --- a/backend/src/services/certificate-authority/certificate-authority-queue.ts +++ b/backend/src/services/certificate-authority/certificate-authority-queue.ts @@ -19,6 +19,7 @@ import { TAppConnectionServiceFactory } from "../app-connection/app-connection-s import { TCertificateBodyDALFactory } from "../certificate/certificate-body-dal"; import { TCertificateSecretDALFactory } from "../certificate/certificate-secret-dal"; import { TPkiSubscriberDALFactory } from "../pki-subscriber/pki-subscriber-dal"; +import { SubscriberOperationStatus } from "../pki-subscriber/pki-subscriber-types"; import { AcmeCertificateAuthorityFns } from "./acme/acme-certificate-authority-fns"; import { TCertificateAuthorityDALFactory } from "./certificate-authority-dal"; import { CaType } from "./certificate-authority-enums"; @@ -47,7 +48,7 @@ type TCertificateAuthorityQueueFactoryDep = { certificateBodyDAL: Pick; certificateSecretDAL: Pick; queueService: TQueueServiceFactory; - pkiSubscriberDAL: Pick; + pkiSubscriberDAL: Pick; }; export type TCertificateAuthorityQueueFactory = ReturnType; @@ -152,8 +153,20 @@ export const certificateAuthorityQueueFactory = ({ try { if (caType === CaType.ACME) { await acmeFns.orderSubscriberCertificate(subscriberId); + await pkiSubscriberDAL.updateById(subscriberId, { + lastOperationStatus: SubscriberOperationStatus.SUCCESS, + lastOperationMessage: "Certificate ordered successfully", + lastOperationAt: new Date().toISOString() + }); + } + } catch (e: unknown) { + if (e instanceof Error) { + await pkiSubscriberDAL.updateById(subscriberId, { + lastOperationStatus: SubscriberOperationStatus.FAILED, + lastOperationMessage: e.message, + lastOperationAt: new Date().toISOString() + }); } - } catch (e) { logger.error(e, `CaOrderCertificate Failed [subscriberId=${subscriberId}] [job=${job.name}]`); } finally { await lock.release(); diff --git a/backend/src/services/certificate-authority/internal/internal-certificate-authority-schemas.ts b/backend/src/services/certificate-authority/internal/internal-certificate-authority-schemas.ts index 5114acebe..73a6c0f0d 100644 --- a/backend/src/services/certificate-authority/internal/internal-certificate-authority-schemas.ts +++ b/backend/src/services/certificate-authority/internal/internal-certificate-authority-schemas.ts @@ -18,7 +18,7 @@ const InternalCertificateAuthorityConfigurationSchema = z organization: z.string().trim(), ou: z.string().trim(), dn: z.string().trim(), - parentCaId: z.string().uuid().optional(), + parentCaId: z.string().uuid().nullish(), serialNumber: z.string().trim().optional(), activeCaCertId: z.string().uuid().optional(), country: z.string().trim(), diff --git a/backend/src/services/pki-subscriber/pki-subscriber-schema.ts b/backend/src/services/pki-subscriber/pki-subscriber-schema.ts index c062b6406..5a0eefa57 100644 --- a/backend/src/services/pki-subscriber/pki-subscriber-schema.ts +++ b/backend/src/services/pki-subscriber/pki-subscriber-schema.ts @@ -12,7 +12,10 @@ export const sanitizedPkiSubscriber = PkiSubscribersSchema.pick({ subjectAlternativeNames: true, ttl: true, keyUsages: true, - extendedKeyUsages: true + extendedKeyUsages: true, + lastOperationStatus: true, + lastOperationMessage: true, + lastOperationAt: true }).extend({ supportsImmediateCertIssuance: z.boolean().optional() }); diff --git a/backend/src/services/pki-subscriber/pki-subscriber-types.ts b/backend/src/services/pki-subscriber/pki-subscriber-types.ts index a26a36a4c..d97fbe5f5 100644 --- a/backend/src/services/pki-subscriber/pki-subscriber-types.ts +++ b/backend/src/services/pki-subscriber/pki-subscriber-types.ts @@ -56,3 +56,8 @@ export type TListPkiSubscriberCertsDTO = { offset: number; limit: number; } & TProjectPermission; + +export enum SubscriberOperationStatus { + SUCCESS = "success", + FAILED = "failed" +} diff --git a/frontend/src/hooks/api/pkiSubscriber/queries.tsx b/frontend/src/hooks/api/pkiSubscriber/queries.tsx index 69e0e78d9..ee6791552 100644 --- a/frontend/src/hooks/api/pkiSubscriber/queries.tsx +++ b/frontend/src/hooks/api/pkiSubscriber/queries.tsx @@ -39,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 () => { @@ -59,7 +62,8 @@ export const useGetPkiSubscriber = ({ ); return pkiSubscriber; }, - enabled: Boolean(subscriberName) && Boolean(projectId) + enabled: Boolean(subscriberName) && Boolean(projectId), + ...options }); }; diff --git a/frontend/src/hooks/api/pkiSubscriber/types.ts b/frontend/src/hooks/api/pkiSubscriber/types.ts index 20f7bbcc0..4f6a7c5eb 100644 --- a/frontend/src/hooks/api/pkiSubscriber/types.ts +++ b/frontend/src/hooks/api/pkiSubscriber/types.ts @@ -5,6 +5,11 @@ export enum PkiSubscriberStatus { DISABLED = "disabled" } +export enum SubscriberOperationStatus { + SUCCESS = "success", + FAILED = "failed" +} + export type TPkiSubscriber = { id: string; projectId: string; @@ -17,6 +22,9 @@ export type TPkiSubscriber = { keyUsages: CertKeyUsage[]; extendedKeyUsages: CertExtendedKeyUsage[]; supportsImmediateCertIssuance?: boolean; + lastOperationStatus?: SubscriberOperationStatus; + lastOperationMessage?: string; + lastOperationAt?: string; }; export type TCreatePkiSubscriberDTO = { diff --git a/frontend/src/pages/cert-manager/PkiSubscriberDetailsByIDPage/components/PkiSubscriberCertificatesTable.tsx b/frontend/src/pages/cert-manager/PkiSubscriberDetailsByIDPage/components/PkiSubscriberCertificatesTable.tsx index e0ba984e6..6dbba3c19 100644 --- a/frontend/src/pages/cert-manager/PkiSubscriberDetailsByIDPage/components/PkiSubscriberCertificatesTable.tsx +++ b/frontend/src/pages/cert-manager/PkiSubscriberDetailsByIDPage/components/PkiSubscriberCertificatesTable.tsx @@ -56,7 +56,7 @@ export const PkiSubscriberCertificatesTable = ({ subscriberName, handlePopUpOpen limit: perPage }, { - refetchInterval: 10 * 1000 // 10 seconds + refetchInterval: 5000 } ); diff --git a/frontend/src/pages/cert-manager/PkiSubscriberDetailsByIDPage/components/PkiSubscriberDetailsSection.tsx b/frontend/src/pages/cert-manager/PkiSubscriberDetailsByIDPage/components/PkiSubscriberDetailsSection.tsx index cb403b12d..3f1826892 100644 --- a/frontend/src/pages/cert-manager/PkiSubscriberDetailsByIDPage/components/PkiSubscriberDetailsSection.tsx +++ b/frontend/src/pages/cert-manager/PkiSubscriberDetailsByIDPage/components/PkiSubscriberDetailsSection.tsx @@ -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, @@ -19,6 +26,7 @@ import { 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"; @@ -45,10 +53,15 @@ 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(); @@ -163,9 +176,26 @@ export const PkiSubscriberDetailsSection = ({ subscriberName, handlePopUpOpen }:

Common Name

{pkiSubscriber.commonName}

+ {pkiSubscriber.lastOperationAt && ( +
+

Last Operation (Local Time)

+

+ {new Date(pkiSubscriber.lastOperationAt).toLocaleString()} +

+
+ )} + {pkiSubscriber.lastOperationStatus === SubscriberOperationStatus.FAILED && ( +
+ +

+ {pkiSubscriber.lastOperationMessage} +

+
+
+ )} {canIssuePkiSubscriberCert && (