mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-06 13:27:22 +00:00
Address greptile comments
This commit is contained in:
@@ -5,8 +5,7 @@ import { TableName } from "../schemas";
|
|||||||
export async function up(knex: Knex): Promise<void> {
|
export async function up(knex: Knex): Promise<void> {
|
||||||
if (await knex.schema.hasColumn(TableName.PkiApiEnrollmentConfig, "autoRenewDays")) {
|
if (await knex.schema.hasColumn(TableName.PkiApiEnrollmentConfig, "autoRenewDays")) {
|
||||||
await knex.schema.alterTable(TableName.PkiApiEnrollmentConfig, (t) => {
|
await knex.schema.alterTable(TableName.PkiApiEnrollmentConfig, (t) => {
|
||||||
t.dropColumn("autoRenewDays");
|
t.renameColumn("autoRenewDays", "renewBeforeDays");
|
||||||
t.integer("renewBeforeDays");
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,8 +45,7 @@ export async function down(knex: Knex): Promise<void> {
|
|||||||
|
|
||||||
if (await knex.schema.hasColumn(TableName.PkiApiEnrollmentConfig, "renewBeforeDays")) {
|
if (await knex.schema.hasColumn(TableName.PkiApiEnrollmentConfig, "renewBeforeDays")) {
|
||||||
await knex.schema.alterTable(TableName.PkiApiEnrollmentConfig, (t) => {
|
await knex.schema.alterTable(TableName.PkiApiEnrollmentConfig, (t) => {
|
||||||
t.dropColumn("renewBeforeDays");
|
t.renameColumn("renewBeforeDays", "autoRenewDays");
|
||||||
t.integer("autoRenewDays");
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2147,9 +2147,6 @@ export const registerRoutes = async (
|
|||||||
const certificateV3Queue = certificateV3QueueServiceFactory({
|
const certificateV3Queue = certificateV3QueueServiceFactory({
|
||||||
queueService,
|
queueService,
|
||||||
certificateDAL,
|
certificateDAL,
|
||||||
certificateAuthorityDAL,
|
|
||||||
certificateProfileDAL,
|
|
||||||
projectDAL,
|
|
||||||
certificateV3Service,
|
certificateV3Service,
|
||||||
auditLogService
|
auditLogService
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -406,10 +406,14 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
|
|||||||
params: z.object({
|
params: z.object({
|
||||||
certificateId: z.string().uuid()
|
certificateId: z.string().uuid()
|
||||||
}),
|
}),
|
||||||
body: z.object({
|
body: z
|
||||||
renewBeforeDays: z.number().int().min(1).max(30).optional(),
|
.object({
|
||||||
disableAutoRenewal: z.boolean().optional()
|
renewBeforeDays: z.number().int().min(1).max(30).optional(),
|
||||||
}),
|
disableAutoRenewal: z.boolean().optional()
|
||||||
|
})
|
||||||
|
.refine((data) => !(data.renewBeforeDays !== undefined && data.disableAutoRenewal === true), {
|
||||||
|
message: "Cannot specify both renewBeforeDays and disableAutoRenewal"
|
||||||
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
message: z.string(),
|
message: z.string(),
|
||||||
|
|||||||
@@ -175,6 +175,36 @@ export enum CertSignatureAlgorithm {
|
|||||||
ECDSA_SHA512 = "ECDSA-SHA512"
|
ECDSA_SHA512 = "ECDSA-SHA512"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum CertificateRenewalErrorType {
|
||||||
|
TEMPLATE_VALIDATION_FAILED = "TEMPLATE_VALIDATION_FAILED",
|
||||||
|
CA_NOT_FOUND = "CA_NOT_FOUND",
|
||||||
|
CA_INACTIVE = "CA_INACTIVE",
|
||||||
|
CERTIFICATE_OUTLIVES_CA = "CERTIFICATE_OUTLIVES_CA",
|
||||||
|
TTL_TOO_SHORT = "TTL_TOO_SHORT",
|
||||||
|
NOT_ELIGIBLE = "NOT_ELIGIBLE",
|
||||||
|
VALIDITY_EXCEEDS_MAXIMUM = "VALIDITY_EXCEEDS_MAXIMUM",
|
||||||
|
NOT_ALLOWED_BY_TEMPLATE = "NOT_ALLOWED_BY_TEMPLATE",
|
||||||
|
UNKNOWN_ERROR = "UNKNOWN_ERROR"
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CERTIFICATE_RENEWAL_ERROR_MESSAGES = {
|
||||||
|
[CertificateRenewalErrorType.TEMPLATE_VALIDATION_FAILED]:
|
||||||
|
"Auto-renewal failed: certificate template policy has changed and this certificate no longer meets the requirements",
|
||||||
|
[CertificateRenewalErrorType.CA_NOT_FOUND]:
|
||||||
|
"Auto-renewal failed: Certificate Authority for this certificate is no longer available",
|
||||||
|
[CertificateRenewalErrorType.CA_INACTIVE]: "Auto-renewal failed: Certificate Authority is currently inactive",
|
||||||
|
[CertificateRenewalErrorType.CERTIFICATE_OUTLIVES_CA]:
|
||||||
|
"Auto-renewal failed: certificate would outlive the Certificate Authority",
|
||||||
|
[CertificateRenewalErrorType.TTL_TOO_SHORT]:
|
||||||
|
"Auto-renewal failed: certificate validity period is too short for the renewal threshold",
|
||||||
|
[CertificateRenewalErrorType.NOT_ELIGIBLE]: "Auto-renewal failed: certificate is not eligible for automatic renewal",
|
||||||
|
[CertificateRenewalErrorType.VALIDITY_EXCEEDS_MAXIMUM]:
|
||||||
|
"Auto-renewal failed: certificate validity period exceeds the maximum allowed by the profile template",
|
||||||
|
[CertificateRenewalErrorType.NOT_ALLOWED_BY_TEMPLATE]:
|
||||||
|
"Auto-renewal failed: certificate settings are no longer allowed by the profile template",
|
||||||
|
[CertificateRenewalErrorType.UNKNOWN_ERROR]: "Auto-renewal failed: an unexpected error occurred"
|
||||||
|
} as const;
|
||||||
|
|
||||||
export const CERTIFICATE_RENEWAL_CONFIG = {
|
export const CERTIFICATE_RENEWAL_CONFIG = {
|
||||||
MIN_RENEW_BEFORE_DAYS: 1,
|
MIN_RENEW_BEFORE_DAYS: 1,
|
||||||
MAX_RENEW_BEFORE_DAYS: 30,
|
MAX_RENEW_BEFORE_DAYS: 30,
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
import RE2 from "re2";
|
import RE2 from "re2";
|
||||||
|
|
||||||
|
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
|
|
||||||
import { CertExtendedKeyUsage, CertKeyUsage } from "../certificate/certificate-types";
|
import { CertExtendedKeyUsage, CertKeyUsage } from "../certificate/certificate-types";
|
||||||
import {
|
import {
|
||||||
CertExtendedKeyUsageType,
|
CertExtendedKeyUsageType,
|
||||||
|
CERTIFICATE_RENEWAL_ERROR_MESSAGES,
|
||||||
|
CertificateRenewalErrorType,
|
||||||
CertKeyUsageType,
|
CertKeyUsageType,
|
||||||
mapExtendedKeyUsageToLegacy,
|
mapExtendedKeyUsageToLegacy,
|
||||||
mapKeyUsageToLegacy,
|
mapKeyUsageToLegacy,
|
||||||
@@ -196,3 +200,74 @@ export const convertExtendedKeyUsageArrayToLegacy = (
|
|||||||
): CertExtendedKeyUsage[] | undefined => {
|
): CertExtendedKeyUsage[] | undefined => {
|
||||||
return usages?.map(convertToLegacyExtendedKeyUsage);
|
return usages?.map(convertToLegacyExtendedKeyUsage);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const categorizeCertificateRenewalError = (error: unknown): string => {
|
||||||
|
if (!error) {
|
||||||
|
return CERTIFICATE_RENEWAL_ERROR_MESSAGES[CertificateRenewalErrorType.UNKNOWN_ERROR];
|
||||||
|
}
|
||||||
|
|
||||||
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
|
|
||||||
|
if (error instanceof NotFoundError) {
|
||||||
|
if (errorMessage.includes("Certificate Authority")) {
|
||||||
|
return CERTIFICATE_RENEWAL_ERROR_MESSAGES[CertificateRenewalErrorType.CA_NOT_FOUND];
|
||||||
|
}
|
||||||
|
if (errorMessage.includes("Certificate template")) {
|
||||||
|
return CERTIFICATE_RENEWAL_ERROR_MESSAGES[CertificateRenewalErrorType.TEMPLATE_VALIDATION_FAILED];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error instanceof BadRequestError) {
|
||||||
|
if (errorMessage.includes("Certificate Authority is") && errorMessage.includes("must be ACTIVE")) {
|
||||||
|
return CERTIFICATE_RENEWAL_ERROR_MESSAGES[CertificateRenewalErrorType.CA_INACTIVE];
|
||||||
|
}
|
||||||
|
if (errorMessage.includes("would expire") && errorMessage.includes("after its issuing CA")) {
|
||||||
|
return CERTIFICATE_RENEWAL_ERROR_MESSAGES[CertificateRenewalErrorType.CERTIFICATE_OUTLIVES_CA];
|
||||||
|
}
|
||||||
|
if (errorMessage.includes("TTL") && errorMessage.includes("must be greater than renewal threshold")) {
|
||||||
|
return CERTIFICATE_RENEWAL_ERROR_MESSAGES[CertificateRenewalErrorType.TTL_TOO_SHORT];
|
||||||
|
}
|
||||||
|
if (errorMessage.includes("not eligible for renewal")) {
|
||||||
|
return CERTIFICATE_RENEWAL_ERROR_MESSAGES[CertificateRenewalErrorType.NOT_ELIGIBLE];
|
||||||
|
}
|
||||||
|
if (errorMessage.includes("Requested validity period exceeds maximum allowed duration")) {
|
||||||
|
return CERTIFICATE_RENEWAL_ERROR_MESSAGES[CertificateRenewalErrorType.VALIDITY_EXCEEDS_MAXIMUM];
|
||||||
|
}
|
||||||
|
if (errorMessage.includes("not allowed by template policy")) {
|
||||||
|
return CERTIFICATE_RENEWAL_ERROR_MESSAGES[CertificateRenewalErrorType.NOT_ALLOWED_BY_TEMPLATE];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error instanceof ForbiddenRequestError) {
|
||||||
|
if (errorMessage.includes("Template validation failed")) {
|
||||||
|
return CERTIFICATE_RENEWAL_ERROR_MESSAGES[CertificateRenewalErrorType.TEMPLATE_VALIDATION_FAILED];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errorMessage.includes("Template validation failed")) {
|
||||||
|
return CERTIFICATE_RENEWAL_ERROR_MESSAGES[CertificateRenewalErrorType.TEMPLATE_VALIDATION_FAILED];
|
||||||
|
}
|
||||||
|
if (errorMessage.includes("Certificate Authority") && errorMessage.includes("not found")) {
|
||||||
|
return CERTIFICATE_RENEWAL_ERROR_MESSAGES[CertificateRenewalErrorType.CA_NOT_FOUND];
|
||||||
|
}
|
||||||
|
if (errorMessage.includes("Certificate Authority is") && errorMessage.includes("must be ACTIVE")) {
|
||||||
|
return CERTIFICATE_RENEWAL_ERROR_MESSAGES[CertificateRenewalErrorType.CA_INACTIVE];
|
||||||
|
}
|
||||||
|
if (errorMessage.includes("would expire") && errorMessage.includes("after its issuing CA")) {
|
||||||
|
return CERTIFICATE_RENEWAL_ERROR_MESSAGES[CertificateRenewalErrorType.CERTIFICATE_OUTLIVES_CA];
|
||||||
|
}
|
||||||
|
if (errorMessage.includes("TTL") && errorMessage.includes("must be greater than renewal threshold")) {
|
||||||
|
return CERTIFICATE_RENEWAL_ERROR_MESSAGES[CertificateRenewalErrorType.TTL_TOO_SHORT];
|
||||||
|
}
|
||||||
|
if (errorMessage.includes("not eligible for renewal")) {
|
||||||
|
return CERTIFICATE_RENEWAL_ERROR_MESSAGES[CertificateRenewalErrorType.NOT_ELIGIBLE];
|
||||||
|
}
|
||||||
|
if (errorMessage.includes("Requested validity period exceeds maximum allowed duration")) {
|
||||||
|
return CERTIFICATE_RENEWAL_ERROR_MESSAGES[CertificateRenewalErrorType.VALIDITY_EXCEEDS_MAXIMUM];
|
||||||
|
}
|
||||||
|
if (errorMessage.includes("not allowed by template policy")) {
|
||||||
|
return CERTIFICATE_RENEWAL_ERROR_MESSAGES[CertificateRenewalErrorType.NOT_ALLOWED_BY_TEMPLATE];
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${CERTIFICATE_RENEWAL_ERROR_MESSAGES[CertificateRenewalErrorType.UNKNOWN_ERROR]}: ${errorMessage}`;
|
||||||
|
};
|
||||||
|
|||||||
@@ -5,19 +5,13 @@ import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
|
|||||||
|
|
||||||
import { ActorType } from "../auth/auth-type";
|
import { ActorType } from "../auth/auth-type";
|
||||||
import { TCertificateDALFactory } from "../certificate/certificate-dal";
|
import { TCertificateDALFactory } from "../certificate/certificate-dal";
|
||||||
import { CertStatus } from "../certificate/certificate-types";
|
|
||||||
import { TCertificateAuthorityDALFactory } from "../certificate-authority/certificate-authority-dal";
|
|
||||||
import { CERTIFICATE_RENEWAL_CONFIG } from "../certificate-common/certificate-constants";
|
import { CERTIFICATE_RENEWAL_CONFIG } from "../certificate-common/certificate-constants";
|
||||||
import { TCertificateProfileDALFactory } from "../certificate-profile/certificate-profile-dal";
|
import { categorizeCertificateRenewalError } from "../certificate-common/certificate-utils";
|
||||||
import { TProjectDALFactory } from "../project/project-dal";
|
|
||||||
import { TCertificateV3ServiceFactory } from "./certificate-v3-service";
|
import { TCertificateV3ServiceFactory } from "./certificate-v3-service";
|
||||||
|
|
||||||
type TCertificateV3QueueServiceFactoryDep = {
|
type TCertificateV3QueueServiceFactoryDep = {
|
||||||
queueService: TQueueServiceFactory;
|
queueService: TQueueServiceFactory;
|
||||||
certificateDAL: TCertificateDALFactory;
|
certificateDAL: Pick<TCertificateDALFactory, "findCertificatesEligibleForRenewal" | "updateById">;
|
||||||
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findByIdWithAssociatedCa">;
|
|
||||||
certificateProfileDAL: Pick<TCertificateProfileDALFactory, "findByIdWithConfigs">;
|
|
||||||
projectDAL: Pick<TProjectDALFactory, "findById">;
|
|
||||||
certificateV3Service: TCertificateV3ServiceFactory;
|
certificateV3Service: TCertificateV3ServiceFactory;
|
||||||
auditLogService: Pick<TAuditLogServiceFactory, "createAuditLog">;
|
auditLogService: Pick<TAuditLogServiceFactory, "createAuditLog">;
|
||||||
};
|
};
|
||||||
@@ -25,9 +19,6 @@ type TCertificateV3QueueServiceFactoryDep = {
|
|||||||
export const certificateV3QueueServiceFactory = ({
|
export const certificateV3QueueServiceFactory = ({
|
||||||
queueService,
|
queueService,
|
||||||
certificateDAL,
|
certificateDAL,
|
||||||
certificateAuthorityDAL,
|
|
||||||
certificateProfileDAL,
|
|
||||||
projectDAL,
|
|
||||||
certificateV3Service,
|
certificateV3Service,
|
||||||
auditLogService
|
auditLogService
|
||||||
}: TCertificateV3QueueServiceFactoryDep) => {
|
}: TCertificateV3QueueServiceFactoryDep) => {
|
||||||
@@ -38,190 +29,109 @@ export const certificateV3QueueServiceFactory = ({
|
|||||||
const { QUEUE_BATCH_SIZE } = CERTIFICATE_RENEWAL_CONFIG;
|
const { QUEUE_BATCH_SIZE } = CERTIFICATE_RENEWAL_CONFIG;
|
||||||
let offset = 0;
|
let offset = 0;
|
||||||
let hasMore = true;
|
let hasMore = true;
|
||||||
|
let totalCertificatesFound = 0;
|
||||||
|
let totalCertificatesRenewed = 0;
|
||||||
|
|
||||||
while (hasMore) {
|
while (hasMore) {
|
||||||
const certificates = await certificateDAL.find(
|
const certificates = await certificateDAL.findCertificatesEligibleForRenewal({
|
||||||
{
|
limit: QUEUE_BATCH_SIZE,
|
||||||
$notNull: ["profileId"],
|
offset
|
||||||
status: CertStatus.ACTIVE,
|
});
|
||||||
renewedById: null,
|
|
||||||
renewalError: null,
|
|
||||||
revokedAt: null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
limit: QUEUE_BATCH_SIZE,
|
|
||||||
offset
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (certificates.length === 0) {
|
if (certificates.length === 0) {
|
||||||
hasMore = false;
|
hasMore = false;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
await Promise.all(
|
totalCertificatesFound += certificates.length;
|
||||||
certificates.map(async (certificate) => {
|
logger.info(
|
||||||
try {
|
`${QueueJobs.CertificateV3DailyAutoRenewal}: found ${certificates.length} certificates eligible for renewal (batch ${Math.floor(offset / QUEUE_BATCH_SIZE) + 1}, total found so far: ${totalCertificatesFound})`
|
||||||
if (!certificate.profileId || !certificate.notAfter) {
|
);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const profile = await certificateProfileDAL.findByIdWithConfigs(certificate.profileId);
|
|
||||||
if (!profile) {
|
|
||||||
logger.warn(`Profile not found for certificate ${certificate.id}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(profile.caId);
|
|
||||||
if (!ca) {
|
|
||||||
logger.warn(`CA not found for certificate ${certificate.id}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const profileAutoRenewEnabled = profile.apiConfig?.autoRenew === true;
|
|
||||||
const certificateHasRenewalConfig =
|
|
||||||
certificate.renewBeforeDays != null && certificate.renewBeforeDays > 0;
|
|
||||||
|
|
||||||
if (!profileAutoRenewEnabled && !certificateHasRenewalConfig) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const now = new Date();
|
|
||||||
if (certificate.notAfter <= now) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const renewBeforeDays = certificate.renewBeforeDays || profile.apiConfig?.renewBeforeDays;
|
|
||||||
if (!renewBeforeDays) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
for (const certificate of certificates) {
|
||||||
|
try {
|
||||||
|
if (certificate.renewBeforeDays) {
|
||||||
const { MIN_RENEW_BEFORE_DAYS, MAX_RENEW_BEFORE_DAYS } = CERTIFICATE_RENEWAL_CONFIG;
|
const { MIN_RENEW_BEFORE_DAYS, MAX_RENEW_BEFORE_DAYS } = CERTIFICATE_RENEWAL_CONFIG;
|
||||||
if (renewBeforeDays < MIN_RENEW_BEFORE_DAYS || renewBeforeDays > MAX_RENEW_BEFORE_DAYS) {
|
if (
|
||||||
logger.warn(`Invalid renewal threshold ${renewBeforeDays} for certificate ${certificate.id}`);
|
certificate.renewBeforeDays < MIN_RENEW_BEFORE_DAYS ||
|
||||||
return;
|
certificate.renewBeforeDays > MAX_RENEW_BEFORE_DAYS
|
||||||
}
|
|
||||||
|
|
||||||
const expiryDate = new Date(certificate.notAfter);
|
|
||||||
const renewalDate = new Date(expiryDate.getTime() - renewBeforeDays * 24 * 60 * 60 * 1000);
|
|
||||||
|
|
||||||
const shouldRenew = renewalDate <= now;
|
|
||||||
|
|
||||||
if (shouldRenew) {
|
|
||||||
logger.info(`Auto-renewing certificate ${certificate.id} (common name: ${certificate.commonName})`);
|
|
||||||
|
|
||||||
const project = await projectDAL.findById(certificate.projectId);
|
|
||||||
if (!project) {
|
|
||||||
logger.error(`Project not found for certificate ${certificate.id}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await certificateV3Service.renewCertificate({
|
|
||||||
actor: ActorType.PLATFORM,
|
|
||||||
actorId: "",
|
|
||||||
actorAuthMethod: null,
|
|
||||||
actorOrgId: project.orgId,
|
|
||||||
certificateId: certificate.id,
|
|
||||||
internal: true
|
|
||||||
});
|
|
||||||
|
|
||||||
await certificateDAL.updateById(certificate.id, {
|
|
||||||
renewalError: null
|
|
||||||
});
|
|
||||||
|
|
||||||
await auditLogService.createAuditLog({
|
|
||||||
projectId: certificate.projectId,
|
|
||||||
actor: {
|
|
||||||
type: ActorType.PLATFORM,
|
|
||||||
metadata: {}
|
|
||||||
},
|
|
||||||
event: {
|
|
||||||
type: EventType.AUTOMATED_RENEW_CERTIFICATE,
|
|
||||||
metadata: {
|
|
||||||
certificateId: certificate.id,
|
|
||||||
commonName: certificate.commonName || "",
|
|
||||||
profileId: certificate.profileId,
|
|
||||||
renewBeforeDays: certificate.renewBeforeDays?.toString() || ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
logger.info(`Successfully auto-renewed certificate ${certificate.id}`);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(
|
|
||||||
error,
|
|
||||||
`Failed to auto-renew certificate ${certificate.id} (common name: ${certificate.commonName})`
|
|
||||||
);
|
|
||||||
|
|
||||||
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
||||||
let categorizedError = errorMessage;
|
|
||||||
|
|
||||||
if (errorMessage.includes("Template validation failed")) {
|
|
||||||
categorizedError =
|
|
||||||
"Auto-renewal failed: certificate template policy has changed and this certificate no longer meets the requirements";
|
|
||||||
} else if (errorMessage.includes("Certificate Authority") && errorMessage.includes("not found")) {
|
|
||||||
categorizedError =
|
|
||||||
"Auto-renewal failed: Certificate Authority for this certificate is no longer available";
|
|
||||||
} else if (errorMessage.includes("Certificate Authority is") && errorMessage.includes("must be ACTIVE")) {
|
|
||||||
categorizedError = "Auto-renewal failed: Certificate Authority is currently inactive";
|
|
||||||
} else if (errorMessage.includes("would expire") && errorMessage.includes("after its issuing CA")) {
|
|
||||||
categorizedError = "Auto-renewal failed: certificate would outlive the Certificate Authority";
|
|
||||||
} else if (
|
|
||||||
errorMessage.includes("TTL") &&
|
|
||||||
errorMessage.includes("must be greater than renewal threshold")
|
|
||||||
) {
|
) {
|
||||||
categorizedError =
|
// eslint-disable-next-line no-continue
|
||||||
"Auto-renewal failed: certificate validity period is too short for the renewal threshold";
|
continue;
|
||||||
} else if (errorMessage.includes("not eligible for renewal")) {
|
|
||||||
categorizedError = "Auto-renewal failed: certificate is not eligible for automatic renewal";
|
|
||||||
} else if (errorMessage.includes("Requested validity period exceeds maximum allowed duration")) {
|
|
||||||
categorizedError =
|
|
||||||
"Auto-renewal failed: certificate validity period exceeds the maximum allowed by the profile template";
|
|
||||||
} else if (errorMessage.includes("not allowed by template policy")) {
|
|
||||||
categorizedError =
|
|
||||||
"Auto-renewal failed: certificate settings are no longer allowed by the profile template";
|
|
||||||
} else {
|
|
||||||
categorizedError = `Auto-renewal failed: ${errorMessage}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await certificateDAL.updateById(certificate.id, {
|
|
||||||
renewalError: categorizedError
|
|
||||||
});
|
|
||||||
} catch (updateError) {
|
|
||||||
logger.error(updateError, `Failed to update renewal error for certificate ${certificate.id}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await auditLogService.createAuditLog({
|
|
||||||
projectId: certificate.projectId,
|
|
||||||
actor: {
|
|
||||||
type: ActorType.PLATFORM,
|
|
||||||
metadata: {}
|
|
||||||
},
|
|
||||||
event: {
|
|
||||||
type: EventType.AUTOMATED_RENEW_CERTIFICATE_FAILED,
|
|
||||||
metadata: {
|
|
||||||
certificateId: certificate.id,
|
|
||||||
commonName: certificate.commonName || "",
|
|
||||||
profileId: certificate.profileId || "",
|
|
||||||
renewBeforeDays: certificate.renewBeforeDays?.toString() || "",
|
|
||||||
error: categorizedError
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (auditError) {
|
|
||||||
logger.error(auditError, `Failed to create audit log for failed certificate renewal ${certificate.id}`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
|
||||||
);
|
await certificateV3Service.renewCertificate({
|
||||||
|
actor: ActorType.PLATFORM,
|
||||||
|
actorId: "",
|
||||||
|
actorAuthMethod: null,
|
||||||
|
actorOrgId: "",
|
||||||
|
certificateId: certificate.id,
|
||||||
|
internal: true
|
||||||
|
});
|
||||||
|
|
||||||
|
await certificateDAL.updateById(certificate.id, {
|
||||||
|
renewalError: null
|
||||||
|
});
|
||||||
|
totalCertificatesRenewed += 1;
|
||||||
|
|
||||||
|
await auditLogService.createAuditLog({
|
||||||
|
projectId: certificate.projectId,
|
||||||
|
actor: {
|
||||||
|
type: ActorType.PLATFORM,
|
||||||
|
metadata: {}
|
||||||
|
},
|
||||||
|
event: {
|
||||||
|
type: EventType.AUTOMATED_RENEW_CERTIFICATE,
|
||||||
|
metadata: {
|
||||||
|
certificateId: certificate.id,
|
||||||
|
commonName: certificate.commonName || "",
|
||||||
|
profileId: certificate.profileId!,
|
||||||
|
renewBeforeDays: certificate.renewBeforeDays?.toString() || ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
const categorizedError: string = categorizeCertificateRenewalError(error);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await certificateDAL.updateById(certificate.id, {
|
||||||
|
renewalError: categorizedError
|
||||||
|
});
|
||||||
|
} catch (updateError) {
|
||||||
|
logger.error(updateError, `Failed to update renewal error for certificate ${certificate.id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await auditLogService.createAuditLog({
|
||||||
|
projectId: certificate.projectId,
|
||||||
|
actor: {
|
||||||
|
type: ActorType.PLATFORM,
|
||||||
|
metadata: {}
|
||||||
|
},
|
||||||
|
event: {
|
||||||
|
type: EventType.AUTOMATED_RENEW_CERTIFICATE_FAILED,
|
||||||
|
metadata: {
|
||||||
|
certificateId: certificate.id,
|
||||||
|
commonName: certificate.commonName || "",
|
||||||
|
profileId: certificate.profileId || "",
|
||||||
|
renewBeforeDays: certificate.renewBeforeDays?.toString() || "",
|
||||||
|
error: categorizedError
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (auditError) {
|
||||||
|
logger.error(auditError, `Failed to create audit log for failed certificate renewal ${certificate.id}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
offset += QUEUE_BATCH_SIZE;
|
offset += QUEUE_BATCH_SIZE;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(`${QueueJobs.CertificateV3DailyAutoRenewal}: queue task completed`);
|
logger.info(
|
||||||
|
`${QueueJobs.CertificateV3DailyAutoRenewal}: queue task completed. Renewed ${totalCertificatesRenewed} certificates out of ${totalCertificatesFound}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1646,7 +1646,7 @@ describe("CertificateV3Service", () => {
|
|||||||
...mockActor
|
...mockActor
|
||||||
})
|
})
|
||||||
).rejects.toThrow(
|
).rejects.toThrow(
|
||||||
"Certificate renewal failed because requested validity period exceeds maximum allowed duration by the profile template"
|
"Certificate renewal failed because requested validity period exceeds maximum allowed duration by the profile template: Subject alternative name not allowed"
|
||||||
);
|
);
|
||||||
|
|
||||||
// Should store template validation error
|
// Should store template validation error
|
||||||
@@ -1788,7 +1788,7 @@ describe("CertificateV3Service", () => {
|
|||||||
certificateId: "cert-123",
|
certificateId: "cert-123",
|
||||||
...mockActor
|
...mockActor
|
||||||
})
|
})
|
||||||
).rejects.toThrow("New certificate would expire");
|
).rejects.toThrow(/New certificate would expire \(.+\) after its issuing CA \(.+\)/);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should allow manual renewal outside window (manual renewal always bypasses window)", async () => {
|
it("should allow manual renewal outside window (manual renewal always bypasses window)", async () => {
|
||||||
|
|||||||
@@ -733,8 +733,9 @@ export const certificateV3ServiceFactory = ({
|
|||||||
? originalCert.altNames.split(",").map((san) => {
|
? originalCert.altNames.split(",").map((san) => {
|
||||||
const trimmed = san.trim();
|
const trimmed = san.trim();
|
||||||
const isIp =
|
const isIp =
|
||||||
new RE2("^\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}$").test(trimmed) ||
|
trimmed.length <= 45 &&
|
||||||
new RE2("^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$").test(trimmed);
|
(new RE2("^\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}$").test(trimmed) ||
|
||||||
|
new RE2("^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$").test(trimmed));
|
||||||
return {
|
return {
|
||||||
type: isIp ? CertSubjectAlternativeNameType.IP_ADDRESS : CertSubjectAlternativeNameType.DNS_NAME,
|
type: isIp ? CertSubjectAlternativeNameType.IP_ADDRESS : CertSubjectAlternativeNameType.DNS_NAME,
|
||||||
value: trimmed
|
value: trimmed
|
||||||
|
|||||||
@@ -114,12 +114,55 @@ export const certificateDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const findCertificatesEligibleForRenewal = async ({
|
||||||
|
limit,
|
||||||
|
offset
|
||||||
|
}: {
|
||||||
|
limit: number;
|
||||||
|
offset: number;
|
||||||
|
}): Promise<TCertificates[]> => {
|
||||||
|
try {
|
||||||
|
const now = new Date();
|
||||||
|
const endOfDay = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59, 999);
|
||||||
|
|
||||||
|
const certs = (await db
|
||||||
|
.replicaNode()(TableName.Certificate)
|
||||||
|
.select(`${TableName.Certificate}.*`)
|
||||||
|
.where(`${TableName.Certificate}.status`, CertStatus.ACTIVE)
|
||||||
|
.whereNull(`${TableName.Certificate}.renewedById`)
|
||||||
|
.whereNull(`${TableName.Certificate}.renewalError`)
|
||||||
|
.whereNull(`${TableName.Certificate}.revokedAt`)
|
||||||
|
.whereNotNull(`${TableName.Certificate}.profileId`)
|
||||||
|
.whereNotNull(`${TableName.Certificate}.notAfter`)
|
||||||
|
.where(`${TableName.Certificate}.notAfter`, ">", now)
|
||||||
|
.where((queryBuilder) => {
|
||||||
|
void queryBuilder.where((subQuery) => {
|
||||||
|
void subQuery
|
||||||
|
.whereNotNull(`${TableName.Certificate}.renewBeforeDays`)
|
||||||
|
.where(`${TableName.Certificate}.renewBeforeDays`, ">", 0)
|
||||||
|
.whereRaw(
|
||||||
|
`"${TableName.Certificate}"."notAfter" - INTERVAL '1 day' * "${TableName.Certificate}"."renewBeforeDays" <= ?`,
|
||||||
|
[endOfDay]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.limit(limit)
|
||||||
|
.offset(offset)
|
||||||
|
.orderBy(`${TableName.Certificate}.notAfter`, "asc")) as TCertificates[];
|
||||||
|
|
||||||
|
return certs;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "Find certificates eligible for renewal" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...certificateOrm,
|
...certificateOrm,
|
||||||
countCertificatesInProject,
|
countCertificatesInProject,
|
||||||
countCertificatesForPkiSubscriber,
|
countCertificatesForPkiSubscriber,
|
||||||
findLatestActiveCertForSubscriber,
|
findLatestActiveCertForSubscriber,
|
||||||
findAllActiveCertsForSubscriber,
|
findAllActiveCertsForSubscriber,
|
||||||
findExpiredSyncedCertificates
|
findExpiredSyncedCertificates,
|
||||||
|
findCertificatesEligibleForRenewal
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ export const useUpdateRenewalConfig = () => {
|
|||||||
return useMutation<
|
return useMutation<
|
||||||
{ message: string; renewBeforeDays?: number },
|
{ message: string; renewBeforeDays?: number },
|
||||||
object,
|
object,
|
||||||
TUpdateRenewalConfigDTO & { disableAutoRenewal?: boolean }
|
TUpdateRenewalConfigDTO
|
||||||
>({
|
>({
|
||||||
mutationFn: async ({ certificateId, renewBeforeDays, disableAutoRenewal }) => {
|
mutationFn: async ({ certificateId, renewBeforeDays, disableAutoRenewal }) => {
|
||||||
const { data } = await apiRequest.patch<{ message: string; renewBeforeDays?: number }>(
|
const { data } = await apiRequest.patch<{ message: string; renewBeforeDays?: number }>(
|
||||||
|
|||||||
@@ -67,5 +67,6 @@ export type TRenewCertificateResponse = {
|
|||||||
export type TUpdateRenewalConfigDTO = {
|
export type TUpdateRenewalConfigDTO = {
|
||||||
certificateId: string;
|
certificateId: string;
|
||||||
renewBeforeDays?: number;
|
renewBeforeDays?: number;
|
||||||
|
disableAutoRenewal?: boolean;
|
||||||
projectSlug: string;
|
projectSlug: string;
|
||||||
};
|
};
|
||||||
|
|||||||
+100
-125
@@ -13,24 +13,91 @@ const DEFAULT_RENEWAL_BEFORE_DAYS = 20;
|
|||||||
const MIN_RENEWAL_BEFORE_DAYS = 1;
|
const MIN_RENEWAL_BEFORE_DAYS = 1;
|
||||||
const MAX_RENEWAL_BEFORE_DAYS = 30;
|
const MAX_RENEWAL_BEFORE_DAYS = 30;
|
||||||
|
|
||||||
const formSchema = z
|
const createFormSchema = (ttlDays: number, notAfter: string) =>
|
||||||
.object({
|
z.object({
|
||||||
renewBeforeDays: z
|
renewBeforeDays: z
|
||||||
.number()
|
.number()
|
||||||
.min(MIN_RENEWAL_BEFORE_DAYS, `Renewal days must be at least ${MIN_RENEWAL_BEFORE_DAYS}`)
|
.min(MIN_RENEWAL_BEFORE_DAYS, `Renewal days must be at least ${MIN_RENEWAL_BEFORE_DAYS}`)
|
||||||
.max(MAX_RENEWAL_BEFORE_DAYS, `Renewal days cannot exceed ${MAX_RENEWAL_BEFORE_DAYS}`)
|
.max(MAX_RENEWAL_BEFORE_DAYS, `Renewal days cannot exceed ${MAX_RENEWAL_BEFORE_DAYS}`)
|
||||||
})
|
.refine(
|
||||||
.refine(() => {
|
(value) => value < ttlDays,
|
||||||
return true;
|
(value) => ({
|
||||||
}, "Invalid renewal configuration");
|
message: `Renewal days (${value}) must be less than certificate TTL (${ttlDays} days)`
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.refine(
|
||||||
|
(value) => {
|
||||||
|
const expiryDate = new Date(notAfter);
|
||||||
|
const renewalDate = new Date(expiryDate.getTime() - value * 24 * 60 * 60 * 1000);
|
||||||
|
const tomorrow = new Date();
|
||||||
|
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||||
|
tomorrow.setHours(0, 0, 0, 0);
|
||||||
|
return renewalDate >= tomorrow;
|
||||||
|
},
|
||||||
|
() => ({
|
||||||
|
message: "Renewals can only be scheduled from tomorrow onwards."
|
||||||
|
})
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
type FormData = z.infer<typeof formSchema>;
|
type FormData = z.infer<ReturnType<typeof createFormSchema>>;
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
popUp: UsePopUpState<["manageRenewal"]>;
|
popUp: UsePopUpState<["manageRenewal"]>;
|
||||||
handlePopUpToggle: (popUpName: keyof UsePopUpState<["manageRenewal"]>, state?: boolean) => void;
|
handlePopUpToggle: (popUpName: keyof UsePopUpState<["manageRenewal"]>, state?: boolean) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const RenewalConfigForm = ({
|
||||||
|
control,
|
||||||
|
errors,
|
||||||
|
onSubmit,
|
||||||
|
isLoading,
|
||||||
|
buttonText,
|
||||||
|
onCancel
|
||||||
|
}: {
|
||||||
|
control: any;
|
||||||
|
errors: { renewBeforeDays?: { message?: string } };
|
||||||
|
onSubmit: (e?: React.BaseSyntheticEvent) => Promise<void>;
|
||||||
|
isLoading: boolean;
|
||||||
|
buttonText: string;
|
||||||
|
onCancel: () => void;
|
||||||
|
}) => (
|
||||||
|
<form onSubmit={onSubmit}>
|
||||||
|
<FormControl
|
||||||
|
label="Renewal Days Before Expiration"
|
||||||
|
errorText={errors.renewBeforeDays?.message}
|
||||||
|
className="mb-6"
|
||||||
|
>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="renewBeforeDays"
|
||||||
|
render={({ field }) => (
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
type="number"
|
||||||
|
min={MIN_RENEWAL_BEFORE_DAYS}
|
||||||
|
max={MAX_RENEWAL_BEFORE_DAYS}
|
||||||
|
onChange={(e) => {
|
||||||
|
const value = parseInt(e.target.value, 10);
|
||||||
|
field.onChange(value);
|
||||||
|
}}
|
||||||
|
placeholder="Enter days before expiration"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-3">
|
||||||
|
<Button type="button" colorSchema="secondary" variant="plain" onClick={onCancel}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" colorSchema="primary" isLoading={isLoading} isDisabled={isLoading}>
|
||||||
|
{buttonText}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
|
||||||
export const CertificateManageRenewalModal = ({ popUp, handlePopUpToggle }: Props) => {
|
export const CertificateManageRenewalModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||||
const { currentProject } = useProject();
|
const { currentProject } = useProject();
|
||||||
const { mutateAsync: updateRenewalConfig, isPending: isUpdatingConfig } =
|
const { mutateAsync: updateRenewalConfig, isPending: isUpdatingConfig } =
|
||||||
@@ -41,7 +108,7 @@ export const CertificateManageRenewalModal = ({ popUp, handlePopUpToggle }: Prop
|
|||||||
commonName: string;
|
commonName: string;
|
||||||
profileId: string;
|
profileId: string;
|
||||||
renewBeforeDays?: number;
|
renewBeforeDays?: number;
|
||||||
ttlDays: number;
|
ttlDays?: number;
|
||||||
notAfter: string;
|
notAfter: string;
|
||||||
renewalError?: string;
|
renewalError?: string;
|
||||||
renewedFromId?: string;
|
renewedFromId?: string;
|
||||||
@@ -54,6 +121,11 @@ export const CertificateManageRenewalModal = ({ popUp, handlePopUpToggle }: Prop
|
|||||||
|
|
||||||
const hasRenewalError = Boolean(certificateData?.renewalError);
|
const hasRenewalError = Boolean(certificateData?.renewalError);
|
||||||
|
|
||||||
|
const formSchema = createFormSchema(
|
||||||
|
certificateData?.ttlDays || 365,
|
||||||
|
certificateData?.notAfter || ""
|
||||||
|
);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
control,
|
control,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
@@ -84,30 +156,6 @@ export const CertificateManageRenewalModal = ({ popUp, handlePopUpToggle }: Prop
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.renewBeforeDays >= certificateData.ttlDays) {
|
|
||||||
createNotification({
|
|
||||||
text: `Renewal days (${data.renewBeforeDays}) must be less than certificate TTL (${certificateData.ttlDays} days)`,
|
|
||||||
type: "error"
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const expiryDate = new Date(certificateData.notAfter);
|
|
||||||
const renewalDate = new Date(
|
|
||||||
expiryDate.getTime() - data.renewBeforeDays * 24 * 60 * 60 * 1000
|
|
||||||
);
|
|
||||||
const tomorrow = new Date();
|
|
||||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
|
||||||
tomorrow.setHours(0, 0, 0, 0);
|
|
||||||
|
|
||||||
if (renewalDate < tomorrow) {
|
|
||||||
createNotification({
|
|
||||||
text: "The renewal date cannot be set to today or any past date. Renewals can only be scheduled from tomorrow onwards.",
|
|
||||||
type: "error"
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await updateRenewalConfig({
|
await updateRenewalConfig({
|
||||||
certificateId: certificateData.certificateId,
|
certificateId: certificateData.certificateId,
|
||||||
renewBeforeDays: data.renewBeforeDays,
|
renewBeforeDays: data.renewBeforeDays,
|
||||||
@@ -133,8 +181,6 @@ export const CertificateManageRenewalModal = ({ popUp, handlePopUpToggle }: Prop
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const isLoading = isUpdatingConfig;
|
|
||||||
|
|
||||||
const getModalTitle = () => {
|
const getModalTitle = () => {
|
||||||
if (hasRenewalError) {
|
if (hasRenewalError) {
|
||||||
return `Fix Auto-Renewal: ${certificateData?.commonName || ""}`;
|
return `Fix Auto-Renewal: ${certificateData?.commonName || ""}`;
|
||||||
@@ -145,6 +191,10 @@ export const CertificateManageRenewalModal = ({ popUp, handlePopUpToggle }: Prop
|
|||||||
return `Enable Auto-Renewal for ${certificateData?.commonName || ""}`;
|
return `Enable Auto-Renewal for ${certificateData?.commonName || ""}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (!certificateData) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={popUp?.manageRenewal?.isOpen}
|
isOpen={popUp?.manageRenewal?.isOpen}
|
||||||
@@ -153,7 +203,6 @@ export const CertificateManageRenewalModal = ({ popUp, handlePopUpToggle }: Prop
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ModalContent title={getModalTitle()}>
|
<ModalContent title={getModalTitle()}>
|
||||||
{/* Show renewal error if present */}
|
|
||||||
{hasRenewalError && (
|
{hasRenewalError && (
|
||||||
<div className="mb-6 rounded-md border border-red-600 bg-red-900/20 p-4">
|
<div className="mb-6 rounded-md border border-red-600 bg-red-900/20 p-4">
|
||||||
<div className="flex items-start gap-3">
|
<div className="flex items-start gap-3">
|
||||||
@@ -173,100 +222,26 @@ export const CertificateManageRenewalModal = ({ popUp, handlePopUpToggle }: Prop
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Configuration form - shown for all cases except when enabled and no error */}
|
|
||||||
{(!isAutoRenewalEnabled || hasRenewalError) && (
|
{(!isAutoRenewalEnabled || hasRenewalError) && (
|
||||||
<form onSubmit={handleSubmit(onUpdateRenewal)}>
|
<RenewalConfigForm
|
||||||
<FormControl
|
control={control}
|
||||||
label="Renewal Days Before Expiration"
|
errors={errors}
|
||||||
errorText={errors.renewBeforeDays?.message}
|
onSubmit={handleSubmit(onUpdateRenewal)}
|
||||||
className="mb-6"
|
isLoading={isUpdatingConfig}
|
||||||
>
|
buttonText={isAutoRenewalEnabled ? "Update Configuration" : "Enable Auto-Renewal"}
|
||||||
<Controller
|
onCancel={() => handlePopUpToggle("manageRenewal", false)}
|
||||||
control={control}
|
/>
|
||||||
name="renewBeforeDays"
|
|
||||||
render={({ field }) => (
|
|
||||||
<Input
|
|
||||||
{...field}
|
|
||||||
type="number"
|
|
||||||
min={MIN_RENEWAL_BEFORE_DAYS}
|
|
||||||
max={MAX_RENEWAL_BEFORE_DAYS}
|
|
||||||
onChange={(e) => {
|
|
||||||
const value = parseInt(e.target.value, 10);
|
|
||||||
field.onChange(value);
|
|
||||||
}}
|
|
||||||
placeholder="Enter days before expiration"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
|
|
||||||
<div className="flex justify-end gap-3">
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
colorSchema="secondary"
|
|
||||||
variant="plain"
|
|
||||||
onClick={() => handlePopUpToggle("manageRenewal", false)}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
colorSchema="primary"
|
|
||||||
isLoading={isUpdatingConfig}
|
|
||||||
isDisabled={isLoading}
|
|
||||||
>
|
|
||||||
{isAutoRenewalEnabled ? "Update Configuration" : "Enable Auto-Renewal"}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Show edit form for enabled auto-renewal without errors */}
|
|
||||||
{isAutoRenewalEnabled && !hasRenewalError && (
|
{isAutoRenewalEnabled && !hasRenewalError && (
|
||||||
<form onSubmit={handleSubmit(onUpdateRenewal)}>
|
<RenewalConfigForm
|
||||||
<FormControl
|
control={control}
|
||||||
label="Renewal Days Before Expiration"
|
errors={errors}
|
||||||
errorText={errors.renewBeforeDays?.message}
|
onSubmit={handleSubmit(onUpdateRenewal)}
|
||||||
className="mb-6"
|
isLoading={isUpdatingConfig}
|
||||||
>
|
buttonText="Update Configuration"
|
||||||
<Controller
|
onCancel={() => handlePopUpToggle("manageRenewal", false)}
|
||||||
control={control}
|
/>
|
||||||
name="renewBeforeDays"
|
|
||||||
render={({ field }) => (
|
|
||||||
<Input
|
|
||||||
{...field}
|
|
||||||
type="number"
|
|
||||||
min={MIN_RENEWAL_BEFORE_DAYS}
|
|
||||||
max={MAX_RENEWAL_BEFORE_DAYS}
|
|
||||||
onChange={(e) => {
|
|
||||||
const value = parseInt(e.target.value, 10);
|
|
||||||
field.onChange(value);
|
|
||||||
}}
|
|
||||||
placeholder="Enter days before expiration"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
|
|
||||||
<div className="flex justify-end gap-3">
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
colorSchema="secondary"
|
|
||||||
variant="plain"
|
|
||||||
onClick={() => handlePopUpToggle("manageRenewal", false)}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
colorSchema="primary"
|
|
||||||
isLoading={isUpdatingConfig}
|
|
||||||
isDisabled={isLoading}
|
|
||||||
>
|
|
||||||
Update Configuration
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
)}
|
)}
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
+18
-17
@@ -8,14 +8,21 @@ import { useProject } from "@app/context";
|
|||||||
import { useUpdateRenewalConfig } from "@app/hooks/api";
|
import { useUpdateRenewalConfig } from "@app/hooks/api";
|
||||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
const formSchema = z.object({
|
const createFormSchema = (ttlDays: number) =>
|
||||||
renewBeforeDays: z
|
z.object({
|
||||||
.number()
|
renewBeforeDays: z
|
||||||
.min(1, "Renewal days must be at least 1")
|
.number()
|
||||||
.max(365, "Renewal days cannot exceed 365")
|
.min(1, "Renewal days must be at least 1")
|
||||||
});
|
.max(365, "Renewal days cannot exceed 365")
|
||||||
|
.refine(
|
||||||
|
(value) => value < ttlDays,
|
||||||
|
(value) => ({
|
||||||
|
message: `Renewal days (${value}) must be less than certificate TTL (${ttlDays} days)`
|
||||||
|
})
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
type FormData = z.infer<typeof formSchema>;
|
type FormData = z.infer<ReturnType<typeof createFormSchema>>;
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
popUp: UsePopUpState<["configureRenewal"]>;
|
popUp: UsePopUpState<["configureRenewal"]>;
|
||||||
@@ -37,6 +44,8 @@ export const CertificateRenewalConfigModal = ({ popUp, handlePopUpToggle }: Prop
|
|||||||
ttlDays: number;
|
ttlDays: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const formSchema = createFormSchema(certificateData.ttlDays);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
control,
|
control,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
@@ -45,7 +54,7 @@ export const CertificateRenewalConfigModal = ({ popUp, handlePopUpToggle }: Prop
|
|||||||
} = useForm<FormData>({
|
} = useForm<FormData>({
|
||||||
resolver: zodResolver(formSchema),
|
resolver: zodResolver(formSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
renewBeforeDays: certificateData?.renewBeforeDays || 7
|
renewBeforeDays: certificateData?.renewBeforeDays || 1
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -53,14 +62,6 @@ export const CertificateRenewalConfigModal = ({ popUp, handlePopUpToggle }: Prop
|
|||||||
|
|
||||||
const onSubmit = async (data: FormData) => {
|
const onSubmit = async (data: FormData) => {
|
||||||
try {
|
try {
|
||||||
if (data.renewBeforeDays >= certificateData.ttlDays) {
|
|
||||||
createNotification({
|
|
||||||
text: `Renewal days (${data.renewBeforeDays}) must be less than certificate TTL (${certificateData.ttlDays} days)`,
|
|
||||||
type: "error"
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!currentProject?.slug) {
|
if (!currentProject?.slug) {
|
||||||
createNotification({
|
createNotification({
|
||||||
text: "Project not found",
|
text: "Project not found",
|
||||||
@@ -144,7 +145,7 @@ export const CertificateRenewalConfigModal = ({ popUp, handlePopUpToggle }: Prop
|
|||||||
{renewBeforeDays && certificateData?.ttlDays && (
|
{renewBeforeDays && certificateData?.ttlDays && (
|
||||||
<div className="mt-2 rounded bg-primary-900/20 p-2">
|
<div className="mt-2 rounded bg-primary-900/20 p-2">
|
||||||
<p className="text-sm text-primary-300">
|
<p className="text-sm text-primary-300">
|
||||||
{renewBeforeDays >= (certificateData.ttlDays || 0)
|
{renewBeforeDays >= certificateData.ttlDays
|
||||||
? "⚠️ Renewal days must be less than certificate TTL"
|
? "⚠️ Renewal days must be less than certificate TTL"
|
||||||
: `✓ Certificate will be renewed ${renewBeforeDays} days before expiration`}
|
: `✓ Certificate will be renewed ${renewBeforeDays} days before expiration`}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -91,10 +91,14 @@ const getAutoRenewalInfo = (certificate: TCertificate) => {
|
|||||||
return { text: "Due Now", variant: "danger" as const };
|
return { text: "Due Now", variant: "danger" as const };
|
||||||
}
|
}
|
||||||
|
|
||||||
const daysUntilRenewal = Math.ceil(
|
const daysUntilRenewal = Math.floor(
|
||||||
(renewalDate.getTime() - now.getTime()) / (24 * 60 * 60 * 1000)
|
(renewalDate.getTime() - now.getTime()) / (24 * 60 * 60 * 1000)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (daysUntilRenewal === 0) {
|
||||||
|
return { text: "Renews today", variant: "primary" as const };
|
||||||
|
}
|
||||||
|
|
||||||
if (daysUntilRenewal <= 7) {
|
if (daysUntilRenewal <= 7) {
|
||||||
return { text: `Renews in ${daysUntilRenewal}d`, variant: "primary" as const };
|
return { text: `Renews in ${daysUntilRenewal}d`, variant: "primary" as const };
|
||||||
}
|
}
|
||||||
@@ -204,6 +208,14 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
|
|||||||
data?.certificates.map((certificate) => {
|
data?.certificates.map((certificate) => {
|
||||||
const { variant, label } = getCertValidUntilBadgeDetails(certificate.notAfter);
|
const { variant, label } = getCertValidUntilBadgeDetails(certificate.notAfter);
|
||||||
const autoRenewalInfo = getAutoRenewalInfo(certificate);
|
const autoRenewalInfo = getAutoRenewalInfo(certificate);
|
||||||
|
|
||||||
|
const isRevoked = certificate.status === CertStatus.REVOKED;
|
||||||
|
const isExpired = new Date(certificate.notAfter) < new Date();
|
||||||
|
const isExpiringWithinDay = isExpiringWithinOneDay(certificate.notAfter);
|
||||||
|
const hasFailed = Boolean(certificate.renewalError);
|
||||||
|
const isAutoRenewalEnabled = Boolean(
|
||||||
|
certificate.renewBeforeDays && certificate.renewBeforeDays > 0
|
||||||
|
);
|
||||||
return (
|
return (
|
||||||
<Tr className="h-10" key={`certificate-${certificate.id}`}>
|
<Tr className="h-10" key={`certificate-${certificate.id}`}>
|
||||||
<Td>{certificate.commonName}</Td>
|
<Td>{certificate.commonName}</Td>
|
||||||
@@ -297,10 +309,6 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
|
|||||||
</ProjectPermissionCan>
|
</ProjectPermissionCan>
|
||||||
{/* Manage auto renewal option - not shown for failed renewals */}
|
{/* Manage auto renewal option - not shown for failed renewals */}
|
||||||
{(() => {
|
{(() => {
|
||||||
const isRevoked = certificate.status === CertStatus.REVOKED;
|
|
||||||
const isExpired = new Date(certificate.notAfter) < new Date();
|
|
||||||
const hasFailed = Boolean(certificate.renewalError);
|
|
||||||
const isExpiringWithinDay = isExpiringWithinOneDay(certificate.notAfter);
|
|
||||||
const canManageRenewal =
|
const canManageRenewal =
|
||||||
certificate.profileId &&
|
certificate.profileId &&
|
||||||
!certificate.renewedById &&
|
!certificate.renewedById &&
|
||||||
@@ -317,10 +325,6 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
|
|||||||
a={ProjectPermissionSub.Certificates}
|
a={ProjectPermissionSub.Certificates}
|
||||||
>
|
>
|
||||||
{(isAllowed) => {
|
{(isAllowed) => {
|
||||||
const isAutoRenewalEnabled = Boolean(
|
|
||||||
certificate.renewBeforeDays && certificate.renewBeforeDays > 0
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
@@ -329,10 +333,17 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
|
|||||||
)}
|
)}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
const notAfterDate = new Date(certificate.notAfter);
|
const notAfterDate = new Date(certificate.notAfter);
|
||||||
const notBeforeDate = new Date(certificate.notBefore);
|
const notBeforeDate = certificate.notBefore
|
||||||
const ttlDays = Math.ceil(
|
? new Date(certificate.notBefore)
|
||||||
(notAfterDate.getTime() - notBeforeDate.getTime()) /
|
: new Date(
|
||||||
(24 * 60 * 60 * 1000)
|
notAfterDate.getTime() - 365 * 24 * 60 * 60 * 1000
|
||||||
|
);
|
||||||
|
const ttlDays = Math.max(
|
||||||
|
1,
|
||||||
|
Math.ceil(
|
||||||
|
(notAfterDate.getTime() - notBeforeDate.getTime()) /
|
||||||
|
(24 * 60 * 60 * 1000)
|
||||||
|
)
|
||||||
);
|
);
|
||||||
handlePopUpOpen("manageRenewal", {
|
handlePopUpOpen("manageRenewal", {
|
||||||
certificateId: certificate.id,
|
certificateId: certificate.id,
|
||||||
@@ -360,12 +371,6 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
|
|||||||
})()}
|
})()}
|
||||||
{/* Disable auto renewal option - only shown when auto renewal is active */}
|
{/* Disable auto renewal option - only shown when auto renewal is active */}
|
||||||
{(() => {
|
{(() => {
|
||||||
const isRevoked = certificate.status === CertStatus.REVOKED;
|
|
||||||
const isExpired = new Date(certificate.notAfter) < new Date();
|
|
||||||
const isExpiringWithinDay = isExpiringWithinOneDay(certificate.notAfter);
|
|
||||||
const isAutoRenewalEnabled = Boolean(
|
|
||||||
certificate.renewBeforeDays && certificate.renewBeforeDays > 0
|
|
||||||
);
|
|
||||||
const canDisableRenewal =
|
const canDisableRenewal =
|
||||||
certificate.profileId &&
|
certificate.profileId &&
|
||||||
!certificate.renewedById &&
|
!certificate.renewedById &&
|
||||||
@@ -404,8 +409,6 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
|
|||||||
})()}
|
})()}
|
||||||
{/* Manual renewal action for profile-issued certificates that are not revoked/expired (including failed ones) */}
|
{/* Manual renewal action for profile-issued certificates that are not revoked/expired (including failed ones) */}
|
||||||
{(() => {
|
{(() => {
|
||||||
const isRevoked = certificate.status === CertStatus.REVOKED;
|
|
||||||
const isExpired = new Date(certificate.notAfter) < new Date();
|
|
||||||
const canRenew =
|
const canRenew =
|
||||||
certificate.profileId &&
|
certificate.profileId &&
|
||||||
!certificate.renewedById &&
|
!certificate.renewedById &&
|
||||||
|
|||||||
Reference in New Issue
Block a user