Address PR comments

This commit is contained in:
Carlos Monastyrski
2025-11-07 02:09:26 -03:00
parent bb6b1940d1
commit b37a9f4168
19 changed files with 327 additions and 264 deletions

View File

@@ -6,7 +6,7 @@ export async function up(knex: Knex): Promise<void> {
if (!(await knex.schema.hasTable(TableName.PkiAlertsV2))) {
await knex.schema.createTable(TableName.PkiAlertsV2, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.string("slug").notNullable();
t.string("name").notNullable();
t.text("description").nullable();
t.string("eventType").notNullable();
t.string("alertBefore").nullable();
@@ -17,9 +17,7 @@ export async function up(knex: Knex): Promise<void> {
t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
t.index("projectId");
t.index("eventType");
t.index("enabled");
t.unique(["slug", "projectId"]);
t.unique(["name", "projectId"]);
});
}
@@ -44,7 +42,7 @@ export async function up(knex: Knex): Promise<void> {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.uuid("alertId").notNullable();
t.timestamp("triggeredAt").defaultTo(knex.fn.now());
t.boolean("notificationSent").defaultTo(false);
t.boolean("hasNotificationSent").defaultTo(false);
t.text("notificationError").nullable();
t.timestamps(true, true, true);

View File

@@ -11,7 +11,7 @@ export const PkiAlertHistorySchema = z.object({
id: z.string().uuid(),
alertId: z.string().uuid(),
triggeredAt: z.date().nullable().optional(),
notificationSent: z.boolean().default(false).nullable().optional(),
hasNotificationSent: z.boolean().default(false).nullable().optional(),
notificationError: z.string().nullable().optional(),
createdAt: z.date(),
updatedAt: z.date()

View File

@@ -9,7 +9,7 @@ import { TImmutableDBKeys } from "./models";
export const PkiAlertsV2Schema = z.object({
id: z.string().uuid(),
slug: z.string(),
name: z.string(),
description: z.string().nullable().optional(),
eventType: z.string(),
alertBefore: z.string().nullable().optional(),

View File

@@ -1,12 +1,14 @@
import { z } from "zod";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { ApiDocsTags } from "@app/lib/api-docs";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import {
CreatePkiAlertV2Schema,
createSecureAlertBeforeValidator,
PkiAlertChannelType,
PkiAlertEventType,
PkiFilterRuleSchema,
UpdatePkiAlertV2Schema
@@ -22,32 +24,34 @@ export const registerPkiAlertRouter = async (server: FastifyZodProvider) => {
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Create a new PKI alert",
tags: ["PKI Alerts"],
body: z.object({
projectId: z.string().uuid().describe("Project ID"),
...CreatePkiAlertV2Schema.shape
tags: [ApiDocsTags.PkiAlerting],
body: CreatePkiAlertV2Schema.extend({
projectId: z.string().uuid().describe("Project ID")
}),
response: {
200: z.object({
id: z.string().uuid(),
slug: z.string(),
description: z.string().nullable(),
eventType: z.nativeEnum(PkiAlertEventType),
alertBefore: z.string(),
filters: z.array(z.any()),
enabled: z.boolean(),
channels: z.array(
z.object({
id: z.string().uuid(),
channelType: z.string(),
config: z.record(z.any()),
enabled: z.boolean(),
createdAt: z.date(),
updatedAt: z.date()
})
),
createdAt: z.date(),
updatedAt: z.date()
alert: z.object({
id: z.string().uuid(),
name: z.string(),
description: z.string().nullable(),
eventType: z.nativeEnum(PkiAlertEventType),
alertBefore: z.string(),
filters: z.array(PkiFilterRuleSchema),
enabled: z.boolean(),
projectId: z.string().uuid(),
channels: z.array(
z.object({
id: z.string().uuid(),
channelType: z.nativeEnum(PkiAlertChannelType),
config: z.record(z.any()),
enabled: z.boolean(),
createdAt: z.date(),
updatedAt: z.date()
})
),
createdAt: z.date(),
updatedAt: z.date()
})
})
}
},
@@ -67,14 +71,14 @@ export const registerPkiAlertRouter = async (server: FastifyZodProvider) => {
type: EventType.CREATE_PKI_ALERT,
metadata: {
pkiAlertId: alert.id,
name: alert.slug,
name: alert.name,
eventType: alert.eventType,
alertBefore: alert.alertBefore
}
}
});
return alert;
return { alert };
}
});
@@ -87,7 +91,7 @@ export const registerPkiAlertRouter = async (server: FastifyZodProvider) => {
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "List PKI alerts for a project",
tags: ["PKI Alerts"],
tags: [ApiDocsTags.PkiAlerting],
querystring: z.object({
projectId: z.string().uuid(),
search: z.string().optional(),
@@ -101,16 +105,16 @@ export const registerPkiAlertRouter = async (server: FastifyZodProvider) => {
alerts: z.array(
z.object({
id: z.string().uuid(),
slug: z.string(),
name: z.string(),
description: z.string().nullable(),
eventType: z.nativeEnum(PkiAlertEventType),
alertBefore: z.string(),
filters: z.array(z.any()),
filters: z.array(PkiFilterRuleSchema),
enabled: z.boolean(),
channels: z.array(
z.object({
id: z.string().uuid(),
channelType: z.string(),
channelType: z.nativeEnum(PkiAlertChannelType),
config: z.record(z.any()),
enabled: z.boolean(),
createdAt: z.date(),
@@ -147,31 +151,34 @@ export const registerPkiAlertRouter = async (server: FastifyZodProvider) => {
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Get a PKI alert by ID",
tags: ["PKI Alerts"],
tags: [ApiDocsTags.PkiAlerting],
params: z.object({
alertId: z.string().uuid().describe("Alert ID")
}),
response: {
200: z.object({
id: z.string().uuid(),
slug: z.string(),
description: z.string().nullable(),
eventType: z.nativeEnum(PkiAlertEventType),
alertBefore: z.string(),
filters: z.array(z.any()),
enabled: z.boolean(),
channels: z.array(
z.object({
id: z.string().uuid(),
channelType: z.string(),
config: z.record(z.any()),
enabled: z.boolean(),
createdAt: z.date(),
updatedAt: z.date()
})
),
createdAt: z.date(),
updatedAt: z.date()
alert: z.object({
id: z.string().uuid(),
name: z.string(),
description: z.string().nullable(),
eventType: z.nativeEnum(PkiAlertEventType),
alertBefore: z.string(),
filters: z.array(PkiFilterRuleSchema),
enabled: z.boolean(),
projectId: z.string().uuid(),
channels: z.array(
z.object({
id: z.string().uuid(),
channelType: z.nativeEnum(PkiAlertChannelType),
config: z.record(z.any()),
enabled: z.boolean(),
createdAt: z.date(),
updatedAt: z.date()
})
),
createdAt: z.date(),
updatedAt: z.date()
})
})
}
},
@@ -195,7 +202,7 @@ export const registerPkiAlertRouter = async (server: FastifyZodProvider) => {
}
});
return alert;
return { alert };
}
});
@@ -208,32 +215,35 @@ export const registerPkiAlertRouter = async (server: FastifyZodProvider) => {
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Update a PKI alert",
tags: ["PKI Alerts"],
tags: [ApiDocsTags.PkiAlerting],
params: z.object({
alertId: z.string().uuid().describe("Alert ID")
}),
body: UpdatePkiAlertV2Schema,
response: {
200: z.object({
id: z.string().uuid(),
slug: z.string(),
description: z.string().nullable(),
eventType: z.nativeEnum(PkiAlertEventType),
alertBefore: z.string(),
filters: z.array(z.any()),
enabled: z.boolean(),
channels: z.array(
z.object({
id: z.string().uuid(),
channelType: z.string(),
config: z.record(z.any()),
enabled: z.boolean(),
createdAt: z.date(),
updatedAt: z.date()
})
),
createdAt: z.date(),
updatedAt: z.date()
alert: z.object({
id: z.string().uuid(),
name: z.string(),
description: z.string().nullable(),
eventType: z.nativeEnum(PkiAlertEventType),
alertBefore: z.string(),
filters: z.array(PkiFilterRuleSchema),
enabled: z.boolean(),
projectId: z.string().uuid(),
channels: z.array(
z.object({
id: z.string().uuid(),
channelType: z.nativeEnum(PkiAlertChannelType),
config: z.record(z.any()),
enabled: z.boolean(),
createdAt: z.date(),
updatedAt: z.date()
})
),
createdAt: z.date(),
updatedAt: z.date()
})
})
}
},
@@ -254,14 +264,14 @@ export const registerPkiAlertRouter = async (server: FastifyZodProvider) => {
type: EventType.UPDATE_PKI_ALERT,
metadata: {
pkiAlertId: alert.id,
name: alert.slug,
name: alert.name,
eventType: alert.eventType,
alertBefore: alert.alertBefore
}
}
});
return alert;
return { alert };
}
});
@@ -274,31 +284,34 @@ export const registerPkiAlertRouter = async (server: FastifyZodProvider) => {
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Delete a PKI alert",
tags: ["PKI Alerts"],
tags: [ApiDocsTags.PkiAlerting],
params: z.object({
alertId: z.string().uuid().describe("Alert ID")
}),
response: {
200: z.object({
id: z.string().uuid(),
slug: z.string(),
description: z.string().nullable(),
eventType: z.nativeEnum(PkiAlertEventType),
alertBefore: z.string(),
filters: z.array(z.any()),
enabled: z.boolean(),
channels: z.array(
z.object({
id: z.string().uuid(),
channelType: z.string(),
config: z.record(z.any()),
enabled: z.boolean(),
createdAt: z.date(),
updatedAt: z.date()
})
),
createdAt: z.date(),
updatedAt: z.date()
alert: z.object({
id: z.string().uuid(),
name: z.string(),
description: z.string().nullable(),
eventType: z.nativeEnum(PkiAlertEventType),
alertBefore: z.string(),
filters: z.array(PkiFilterRuleSchema),
enabled: z.boolean(),
projectId: z.string().uuid(),
channels: z.array(
z.object({
id: z.string().uuid(),
channelType: z.nativeEnum(PkiAlertChannelType),
config: z.record(z.any()),
enabled: z.boolean(),
createdAt: z.date(),
updatedAt: z.date()
})
),
createdAt: z.date(),
updatedAt: z.date()
})
})
}
},
@@ -322,7 +335,7 @@ export const registerPkiAlertRouter = async (server: FastifyZodProvider) => {
}
});
return alert;
return { alert };
}
});
@@ -335,7 +348,7 @@ export const registerPkiAlertRouter = async (server: FastifyZodProvider) => {
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "List certificates that match an alert's filter rules",
tags: ["PKI Alerts"],
tags: [ApiDocsTags.PkiAlerting],
params: z.object({
alertId: z.string().uuid().describe("Alert ID")
}),
@@ -358,9 +371,7 @@ export const registerPkiAlertRouter = async (server: FastifyZodProvider) => {
status: z.string()
})
),
total: z.number(),
limit: z.number(),
offset: z.number()
total: z.number()
})
}
},
@@ -382,12 +393,12 @@ export const registerPkiAlertRouter = async (server: FastifyZodProvider) => {
method: "POST",
url: "/preview/certificates",
config: {
rateLimit: readLimit
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Preview certificates that would match the given filter rules",
tags: ["PKI Alerts"],
tags: [ApiDocsTags.PkiAlerting],
body: z.object({
projectId: z.string().uuid().describe("Project ID"),
filters: z.array(PkiFilterRuleSchema),
@@ -413,9 +424,7 @@ export const registerPkiAlertRouter = async (server: FastifyZodProvider) => {
status: z.string()
})
),
total: z.number(),
limit: z.number(),
offset: z.number()
total: z.number()
})
}
},

View File

@@ -12,7 +12,7 @@ export const pkiAlertHistoryDALFactory = (db: TDbClient) => {
alertId: string,
certificateIds: string[],
options?: {
notificationSent?: boolean;
hasNotificationSent?: boolean;
notificationError?: string;
}
): Promise<TPkiAlertHistory> => {
@@ -21,7 +21,7 @@ export const pkiAlertHistoryDALFactory = (db: TDbClient) => {
const historyRecords = await tx(TableName.PkiAlertHistory)
.insert({
alertId,
notificationSent: options?.notificationSent || false,
hasNotificationSent: options?.hasNotificationSent || false,
notificationError: options?.notificationError
})
.returning("*");
@@ -91,7 +91,7 @@ export const pkiAlertHistoryDALFactory = (db: TDbClient) => {
.from(`${TableName.PkiAlertHistory} as hist`)
.join(`${TableName.PkiAlertHistoryCertificate} as cert`, "hist.id", "cert.alertHistoryId")
.where("hist.alertId", alertId)
.where("hist.notificationSent", true)
.where("hist.hasNotificationSent", true)
.where("hist.triggeredAt", ">=", cutoffDate)
.whereIn("cert.certificateId", certificateIds)) as Array<{ certificateId: string }>;

View File

@@ -102,7 +102,7 @@ export const pkiAlertV2DALFactory = (db: TDbClient) => {
}
};
const findByProjectId = async (
const findByProjectIdWithCount = async (
projectId: string,
filters?: {
search?: string;
@@ -112,15 +112,32 @@ export const pkiAlertV2DALFactory = (db: TDbClient) => {
offset?: number;
},
tx?: Knex
): Promise<TAlertWithChannels[]> => {
): Promise<{ alerts: TAlertWithChannels[]; total: number }> => {
try {
let countQuery = (tx || db.replicaNode())
.count("* as count")
.from(TableName.PkiAlertsV2)
.where(`${TableName.PkiAlertsV2}.projectId`, projectId);
if (filters?.search) {
countQuery = countQuery.whereILike(`${TableName.PkiAlertsV2}.name`, `%${sanitizeLikeInput(filters.search)}%`);
}
if (filters?.eventType) {
countQuery = countQuery.where(`${TableName.PkiAlertsV2}.eventType`, filters.eventType);
}
if (filters?.enabled !== undefined) {
countQuery = countQuery.where(`${TableName.PkiAlertsV2}.enabled`, filters.enabled);
}
let alertQuery = (tx || db.replicaNode())
.select(selectAllTableCols(TableName.PkiAlertsV2))
.from(TableName.PkiAlertsV2)
.where(`${TableName.PkiAlertsV2}.projectId`, projectId);
if (filters?.search) {
alertQuery = alertQuery.whereILike(`${TableName.PkiAlertsV2}.slug`, `%${sanitizeLikeInput(filters.search)}%`);
alertQuery = alertQuery.whereILike(`${TableName.PkiAlertsV2}.name`, `%${sanitizeLikeInput(filters.search)}%`);
}
if (filters?.eventType) {
@@ -141,13 +158,11 @@ export const pkiAlertV2DALFactory = (db: TDbClient) => {
alertQuery = alertQuery.offset(filters.offset);
}
const alerts = (await alertQuery) as TPkiAlertsV2[];
const [countResult, alerts] = await Promise.all([countQuery, alertQuery]);
if (alerts.length === 0) {
return [];
}
const total = parseInt((countResult[0] as { count: string }).count, 10);
const alertIds = alerts.map((alert) => alert.id);
const alertIds = (alerts as TPkiAlertsV2[]).map((alert) => alert.id);
const channels = (await (tx || db.replicaNode())
.select(selectAllTableCols(TableName.PkiAlertChannels))
.from(TableName.PkiAlertChannels)
@@ -164,17 +179,32 @@ export const pkiAlertV2DALFactory = (db: TDbClient) => {
{} as Record<string, TChannelResult[]>
);
const result: TAlertWithChannels[] = alerts.map((alert) => ({
const alertsWithChannels: TAlertWithChannels[] = (alerts as TPkiAlertsV2[]).map((alert) => ({
...alert,
channels: channelsByAlertId[alert.id] || []
}));
return result;
return { alerts: alertsWithChannels, total };
} catch (error) {
throw new DatabaseError({ error, name: "FindByProjectId" });
throw new DatabaseError({ error, name: "FindByProjectIdWithCount" });
}
};
const findByProjectId = async (
projectId: string,
filters?: {
search?: string;
eventType?: string;
enabled?: boolean;
limit?: number;
offset?: number;
},
tx?: Knex
): Promise<TAlertWithChannels[]> => {
const result = await findByProjectIdWithCount(projectId, filters, tx);
return result.alerts;
};
const countByProjectId = async (
projectId: string,
filters?: {
@@ -191,7 +221,7 @@ export const pkiAlertV2DALFactory = (db: TDbClient) => {
.where(`${TableName.PkiAlertsV2}.projectId`, projectId);
if (filters?.search) {
query = query.whereILike(`${TableName.PkiAlertsV2}.slug`, `%${sanitizeLikeInput(filters.search)}%`);
query = query.whereILike(`${TableName.PkiAlertsV2}.name`, `%${sanitizeLikeInput(filters.search)}%`);
}
if (filters?.eventType) {
@@ -257,7 +287,7 @@ export const pkiAlertV2DALFactory = (db: TDbClient) => {
let caCountQuery = (tx || db.replicaNode())
.count("* as count")
.from(TableName.CertificateAuthority)
.leftJoin(
.innerJoin(
`${TableName.InternalCertificateAuthority} as ica`,
`${TableName.CertificateAuthority}.id`,
`ica.caId`
@@ -268,16 +298,16 @@ export const pkiAlertV2DALFactory = (db: TDbClient) => {
if (options?.alertBefore) {
if (options.showFutureMatches) {
caCountQuery = caCountQuery
.whereRaw(`ica."notAfter" > NOW() + INTERVAL '${options.alertBefore}'`)
.whereRaw(`ica."notAfter" > NOW() + ?::interval`, [options.alertBefore])
.whereRaw(`ica."notAfter" > NOW()`);
} else if (options.showCurrentMatches) {
caCountQuery = caCountQuery
.whereRaw(`ica."notAfter" > NOW()`)
.whereRaw(`ica."notAfter" <= NOW() + INTERVAL '${options.alertBefore}'`);
.whereRaw(`ica."notAfter" <= NOW() + ?::interval`, [options.alertBefore]);
} else {
caCountQuery = caCountQuery
.whereRaw(`ica."notAfter" > NOW()`)
.whereRaw(`ica."notAfter" <= NOW() + INTERVAL '${options.alertBefore}'`);
.whereRaw(`ica."notAfter" <= NOW() + ?::interval`, [options.alertBefore]);
}
}
@@ -290,23 +320,23 @@ export const pkiAlertV2DALFactory = (db: TDbClient) => {
if (options?.showPreview) {
certCountQuery = certCountQuery
.whereRaw(`${TableName.Certificate}."notAfter" > NOW()`)
.whereRaw(`"${TableName.Certificate}"."notAfter" > NOW()`)
.whereNot(`${TableName.Certificate}.status`, "revoked");
} else if (options?.alertBefore) {
if (options.showFutureMatches) {
certCountQuery = certCountQuery
.whereRaw(`${TableName.Certificate}."notAfter" > NOW() + INTERVAL '${options.alertBefore}'`)
.whereRaw(`${TableName.Certificate}."notAfter" > NOW()`)
.whereRaw(`"${TableName.Certificate}"."notAfter" > NOW() + ?::interval`, [options.alertBefore])
.whereRaw(`"${TableName.Certificate}"."notAfter" > NOW()`)
.whereNot(`${TableName.Certificate}.status`, "revoked");
} else if (options.showCurrentMatches) {
certCountQuery = certCountQuery
.whereRaw(`${TableName.Certificate}."notAfter" > NOW()`)
.whereRaw(`${TableName.Certificate}."notAfter" <= NOW() + INTERVAL '${options.alertBefore}'`)
.whereRaw(`"${TableName.Certificate}"."notAfter" > NOW()`)
.whereRaw(`"${TableName.Certificate}"."notAfter" <= NOW() + ?::interval`, [options.alertBefore])
.whereNot(`${TableName.Certificate}.status`, "revoked");
} else {
certCountQuery = certCountQuery
.whereRaw(`${TableName.Certificate}."notAfter" > NOW()`)
.whereRaw(`${TableName.Certificate}."notAfter" <= NOW() + INTERVAL '${options.alertBefore}'`)
.whereRaw(`"${TableName.Certificate}"."notAfter" > NOW()`)
.whereRaw(`"${TableName.Certificate}"."notAfter" <= NOW() + ?::interval`, [options.alertBefore])
.whereNot(`${TableName.Certificate}.status`, "revoked");
}
}
@@ -322,7 +352,7 @@ export const pkiAlertV2DALFactory = (db: TDbClient) => {
`${TableName.PkiAlertHistoryCertificate}.alertHistoryId`
)
.where(`${TableName.PkiAlertHistory}.alertId`, options.alertId)
.whereRaw(`"${TableName.PkiAlertHistoryCertificate}"."certificateId" = "${TableName.Certificate}"."id"`)
.whereRaw(`"${TableName.PkiAlertHistoryCertificate}"."certificateId" = "${TableName.Certificate}".id`)
);
}
@@ -346,7 +376,7 @@ export const pkiAlertV2DALFactory = (db: TDbClient) => {
];
if (needsProfileJoin) {
selectColumns.push("profile.slug as profileName");
selectColumns.push("profile.name as profileName");
}
let certificateQuery = (tx || db.replicaNode()).select(selectColumns).from(TableName.Certificate);
@@ -355,23 +385,23 @@ export const pkiAlertV2DALFactory = (db: TDbClient) => {
if (options?.showPreview) {
certificateQuery = certificateQuery
.whereRaw(`${TableName.Certificate}."notAfter" > NOW()`)
.whereRaw(`"${TableName.Certificate}"."notAfter" > NOW()`)
.whereNot(`${TableName.Certificate}.status`, "revoked");
} else if (options?.alertBefore) {
if (options.showFutureMatches) {
certificateQuery = certificateQuery
.whereRaw(`${TableName.Certificate}."notAfter" > NOW() + INTERVAL '${options.alertBefore}'`)
.whereRaw(`${TableName.Certificate}."notAfter" > NOW()`)
.whereRaw(`"${TableName.Certificate}"."notAfter" > NOW() + ?::interval`, [options.alertBefore])
.whereRaw(`"${TableName.Certificate}"."notAfter" > NOW()`)
.whereNot(`${TableName.Certificate}.status`, "revoked");
} else if (options.showCurrentMatches) {
certificateQuery = certificateQuery
.whereRaw(`${TableName.Certificate}."notAfter" > NOW()`)
.whereRaw(`${TableName.Certificate}."notAfter" <= NOW() + INTERVAL '${options.alertBefore}'`)
.whereRaw(`"${TableName.Certificate}"."notAfter" > NOW()`)
.whereRaw(`"${TableName.Certificate}"."notAfter" <= NOW() + ?::interval`, [options.alertBefore])
.whereNot(`${TableName.Certificate}.status`, "revoked");
} else {
certificateQuery = certificateQuery
.whereRaw(`${TableName.Certificate}."notAfter" > NOW()`)
.whereRaw(`${TableName.Certificate}."notAfter" <= NOW() + INTERVAL '${options.alertBefore}'`)
.whereRaw(`"${TableName.Certificate}"."notAfter" > NOW()`)
.whereRaw(`"${TableName.Certificate}"."notAfter" <= NOW() + ?::interval`, [options.alertBefore])
.whereNot(`${TableName.Certificate}.status`, "revoked");
}
}
@@ -387,7 +417,7 @@ export const pkiAlertV2DALFactory = (db: TDbClient) => {
`${TableName.PkiAlertHistoryCertificate}.alertHistoryId`
)
.where(`${TableName.PkiAlertHistory}.alertId`, options.alertId)
.whereRaw(`"${TableName.PkiAlertHistoryCertificate}"."certificateId" = "${TableName.Certificate}"."id"`)
.whereRaw(`"${TableName.PkiAlertHistoryCertificate}"."certificateId" = "${TableName.Certificate}".id`)
);
}
@@ -447,7 +477,7 @@ export const pkiAlertV2DALFactory = (db: TDbClient) => {
`ica.notAfter`
)
.from(TableName.CertificateAuthority)
.leftJoin(
.innerJoin(
`${TableName.InternalCertificateAuthority} as ica`,
`${TableName.CertificateAuthority}.id`,
`ica.caId`
@@ -458,12 +488,12 @@ export const pkiAlertV2DALFactory = (db: TDbClient) => {
if (options?.alertBefore) {
if (options.showFutureMatches) {
caQuery = caQuery
.whereRaw(`ica."notAfter" > NOW() + INTERVAL '${options.alertBefore}'`)
.whereRaw(`ica."notAfter" > NOW() + ?::interval`, [options.alertBefore])
.whereRaw(`ica."notAfter" > NOW()`);
} else {
caQuery = caQuery
.whereRaw(`ica."notAfter" > NOW()`)
.whereRaw(`ica."notAfter" <= NOW() + INTERVAL '${options.alertBefore}'`);
.whereRaw(`ica."notAfter" <= NOW() + ?::interval`, [options.alertBefore]);
}
}
@@ -518,6 +548,7 @@ export const pkiAlertV2DALFactory = (db: TDbClient) => {
findById,
findByIdWithChannels,
findByProjectId,
findByProjectIdWithCount,
countByProjectId,
getDistinctProjectIds,
findMatchingCertificates

View File

@@ -7,6 +7,13 @@ import { logger } from "@app/lib/logger";
import { PkiFilterField, PkiFilterOperator, TPkiFilterRule } from "./pki-alert-v2-types";
export const sanitizeLikeInput = (input: string): string => {
const allowedCharsRegex = new RE2("^[a-zA-Z0-9\\s\\-_\\.@\\*]+$");
if (!allowedCharsRegex.test(input)) {
throw new Error(
"Invalid characters in input. Only alphanumeric characters, spaces, hyphens, underscores, dots, @ and * are allowed."
);
}
const backslashRegex = new RE2("\\\\", "g");
const percentRegex = new RE2("%", "g");
const underscoreRegex = new RE2("_", "g");
@@ -169,25 +176,32 @@ const applySanFilter = (query: Knex.QueryBuilder, filter: TPkiFilterRule): Knex.
if (Array.isArray(value)) {
return query.where((builder) => {
value.forEach((v, index) => {
const condition = `${columnName}::text ILIKE ?`;
const sanitizedValue = `%"${String(v)}"%`;
if (index === 0) {
void builder.whereRaw(condition, [`%"${String(v)}"%`]);
void builder.whereRaw(`??."altNames"::text ILIKE ?`, [TableName.Certificate, sanitizedValue]);
} else {
void builder.orWhereRaw(condition, [`%"${String(v)}"%`]);
void builder.orWhereRaw(`??."altNames"::text ILIKE ?`, [TableName.Certificate, sanitizedValue]);
}
});
});
}
return query.whereRaw(`${columnName}::text ILIKE ?`, [`%"${String(value)}"%`]);
{
const sanitizedValue = `%"${String(value)}"%`;
return query.whereRaw(`??."altNames"::text ILIKE ?`, [TableName.Certificate, sanitizedValue]);
}
case PkiFilterOperator.CONTAINS:
return applySanFilter(query, { ...filter, operator: PkiFilterOperator.MATCHES });
case PkiFilterOperator.STARTS_WITH:
return query.whereRaw(`${columnName}::text ILIKE ?`, [`%"${String(value)}%`]);
case PkiFilterOperator.STARTS_WITH: {
const startsWithValue = `%"${String(value)}%`;
return query.whereRaw(`??."altNames"::text ILIKE ?`, [TableName.Certificate, startsWithValue]);
}
case PkiFilterOperator.ENDS_WITH:
return query.whereRaw(`${columnName}::text ILIKE ?`, [`%${String(value)}"%`]);
case PkiFilterOperator.ENDS_WITH: {
const endsWithValue = `%${String(value)}"%`;
return query.whereRaw(`??."altNames"::text ILIKE ?`, [TableName.Certificate, endsWithValue]);
}
default:
logger.warn(`Unsupported operator for SAN: ${String(filter.operator)}`);
@@ -244,7 +258,7 @@ export const applyCaFilters = (
filters: TPkiFilterRule[],
projectId: string
): Knex.QueryBuilder => {
let filteredQuery = query.where(`${TableName.CertificateAuthority}.projectId`, projectId);
let filteredQuery = query.where(`${TableName.CertificateAuthority}.projectId`, projectId).whereNotNull("ica.caId"); // Only include CAs that have internal CA data
filters.forEach((filter) => {
switch (filter.field) {

View File

@@ -63,7 +63,7 @@ export const pkiAlertV2QueueServiceFactory = ({
const evaluateAlert = async (
alert: {
id: string;
slug: string;
name: string;
eventType: string;
alertBefore: string;
filters: TPkiFilterRule[];
@@ -135,7 +135,7 @@ export const pkiAlertV2QueueServiceFactory = ({
for (const alert of alerts) {
const typedAlert = alert as {
id: string;
slug: string;
name: string;
eventType: string;
alertBefore: string;
filters: TPkiFilterRule[];
@@ -147,13 +147,13 @@ export const pkiAlertV2QueueServiceFactory = ({
await pkiAlertV2Service.sendAlertNotifications(typedAlert.id, certificateIds);
notificationsSent += 1;
logger.info(
`Sent notification for alert ${typedAlert.id} (${typedAlert.slug}) with ${certificateIds.length} certificates`
`Sent notification for alert ${typedAlert.id} (${typedAlert.name}) with ${certificateIds.length} certificates`
);
}
alertsProcessed += 1;
} catch (error) {
logger.error(error, `Failed to process alert ${typedAlert.id} (${typedAlert.slug})`);
logger.error(error, `Failed to process alert ${typedAlert.id} (${typedAlert.name})`);
}
}

View File

@@ -39,8 +39,10 @@ type TPkiAlertV2ServiceFactoryDep = {
| "updateById"
| "deleteById"
| "findByProjectId"
| "findByProjectIdWithCount"
| "countByProjectId"
| "findMatchingCertificates"
| "transaction"
>;
pkiAlertChannelDAL: Pick<TPkiAlertChannelDALFactory, "create" | "findByAlertId" | "deleteByAlertId" | "insertMany">;
pkiAlertHistoryDAL: Pick<TPkiAlertHistoryDALFactory, "createWithCertificates" | "findByAlertId">;
@@ -59,7 +61,7 @@ export const pkiAlertV2ServiceFactory = ({
}: TPkiAlertV2ServiceFactoryDep) => {
type TAlertWithChannels = {
id: string;
slug: string;
name: string;
description: string;
eventType: string;
alertBefore: string;
@@ -81,7 +83,7 @@ export const pkiAlertV2ServiceFactory = ({
const formatAlertResponse = (alert: TAlertWithChannels): TAlertV2Response => {
return {
id: alert.id,
slug: alert.slug,
name: alert.name,
description: alert.description,
eventType: alert.eventType as PkiAlertEventType,
alertBefore: alert.alertBefore,
@@ -103,7 +105,7 @@ export const pkiAlertV2ServiceFactory = ({
const createAlert = async ({
projectId,
slug,
name,
description,
eventType,
alertBefore,
@@ -132,31 +134,36 @@ export const pkiAlertV2ServiceFactory = ({
throw new BadRequestError({ message: "Invalid alertBefore format. Use format like '30d', '1w', '3m', '1y'" });
}
const alert = await pkiAlertV2DAL.create({
projectId,
slug,
description,
eventType,
alertBefore,
filters,
enabled
return pkiAlertV2DAL.transaction(async (tx) => {
const alert = await pkiAlertV2DAL.create(
{
projectId,
name,
description,
eventType,
alertBefore,
filters,
enabled
},
tx
);
const channelInserts = channels.map((channel) => ({
alertId: alert.id,
channelType: channel.channelType,
config: channel.config,
enabled: channel.enabled
}));
await pkiAlertChannelDAL.insertMany(channelInserts, tx);
const completeAlert = await pkiAlertV2DAL.findByIdWithChannels(alert.id, tx);
if (!completeAlert) {
throw new NotFoundError({ message: "Failed to retrieve created alert" });
}
return formatAlertResponse(completeAlert as TAlertWithChannels);
});
const channelInserts = channels.map((channel) => ({
alertId: alert.id,
channelType: channel.channelType,
config: channel.config,
enabled: channel.enabled
}));
await pkiAlertChannelDAL.insertMany(channelInserts);
const completeAlert = await pkiAlertV2DAL.findByIdWithChannels(alert.id);
if (!completeAlert) {
throw new NotFoundError({ message: "Failed to retrieve created alert" });
}
return formatAlertResponse(completeAlert as TAlertWithChannels);
};
const getAlertById = async ({
@@ -208,10 +215,7 @@ export const pkiAlertV2ServiceFactory = ({
const filters = { search, eventType, enabled, limit, offset };
const [alerts, total] = await Promise.all([
pkiAlertV2DAL.findByProjectId(projectId, filters),
pkiAlertV2DAL.countByProjectId(projectId, { search, eventType, enabled })
]);
const { alerts, total } = await pkiAlertV2DAL.findByProjectIdWithCount(projectId, filters);
return {
alerts: alerts.map((alert) => formatAlertResponse(alert as TAlertWithChannels)),
@@ -221,7 +225,7 @@ export const pkiAlertV2ServiceFactory = ({
const updateAlert = async ({
alertId,
slug,
name,
description,
eventType,
alertBefore,
@@ -256,41 +260,43 @@ export const pkiAlertV2ServiceFactory = ({
}
const updateData: {
slug?: string;
name?: string;
description?: string;
eventType?: PkiAlertEventType;
alertBefore?: string;
filters?: TPkiFilterRule[];
enabled?: boolean;
} = {};
if (slug !== undefined) updateData.slug = slug;
if (name !== undefined) updateData.name = name;
if (description !== undefined) updateData.description = description;
if (eventType !== undefined) updateData.eventType = eventType;
if (alertBefore !== undefined) updateData.alertBefore = alertBefore;
if (filters !== undefined) updateData.filters = filters;
if (enabled !== undefined) updateData.enabled = enabled;
alert = await pkiAlertV2DAL.updateById(alertId, updateData);
return pkiAlertV2DAL.transaction(async (tx) => {
alert = await pkiAlertV2DAL.updateById(alertId, updateData, tx);
if (channels) {
await pkiAlertChannelDAL.deleteByAlertId(alertId);
if (channels) {
await pkiAlertChannelDAL.deleteByAlertId(alertId, tx);
const channelInserts = channels.map((channel) => ({
alertId,
channelType: channel.channelType,
config: channel.config,
enabled: channel.enabled
}));
const channelInserts = channels.map((channel) => ({
alertId,
channelType: channel.channelType,
config: channel.config,
enabled: channel.enabled
}));
await pkiAlertChannelDAL.insertMany(channelInserts);
}
await pkiAlertChannelDAL.insertMany(channelInserts, tx);
}
const completeAlert = await pkiAlertV2DAL.findByIdWithChannels(alertId);
if (!completeAlert) {
throw new NotFoundError({ message: "Failed to retrieve updated alert" });
}
const completeAlert = await pkiAlertV2DAL.findByIdWithChannels(alertId, tx);
if (!completeAlert) {
throw new NotFoundError({ message: "Failed to retrieve updated alert" });
}
return formatAlertResponse(completeAlert as TAlertWithChannels);
return formatAlertResponse(completeAlert as TAlertWithChannels);
});
};
const deleteAlert = async ({
@@ -365,9 +371,7 @@ export const pkiAlertV2ServiceFactory = ({
return {
certificates: result.certificates,
total: result.total,
limit,
offset
total: result.total
};
};
@@ -415,9 +419,7 @@ export const pkiAlertV2ServiceFactory = ({
return {
certificates: result.certificates,
total: result.total,
limit,
offset
total: result.total
};
};
@@ -445,7 +447,7 @@ export const pkiAlertV2ServiceFactory = ({
if (matchingCertificates.length === 0) return;
let notificationSent = false;
let hasNotificationSent = false;
let notificationError: string | undefined;
try {
@@ -455,7 +457,7 @@ export const pkiAlertV2ServiceFactory = ({
);
const alertBeforeDays = parseTimeToDays((alert as { alertBefore: string }).alertBefore);
const alertName = (alert as { slug: string }).slug;
const alertName = (alert as { name: string }).name;
const emailPromises = emailChannels.map((channel) => {
const config = channel.config as TEmailChannelConfig;
@@ -480,14 +482,14 @@ export const pkiAlertV2ServiceFactory = ({
await Promise.all(emailPromises);
notificationSent = true;
hasNotificationSent = true;
} catch (error) {
notificationError = error instanceof Error ? error.message : "Unknown error occurred";
logger.error(error, `Failed to send notifications for alert ${alertId}`);
}
await pkiAlertHistoryDAL.createWithCertificates(alertId, certificateIds, {
notificationSent,
hasNotificationSent,
notificationError
});
};

View File

@@ -3,12 +3,18 @@ import { z } from "zod";
import { TGenericPermission } from "@app/lib/types";
const createSecureSlugValidator = () => {
const slugRegex = new RE2("^[a-z0-9]+(?:-[a-z0-9]+)*$");
return (value: string) => slugRegex.test(value);
const createSecureNameValidator = () => {
// Validates name format: lowercase alphanumeric characters with optional hyphens
// Pattern: starts and ends with alphanumeric, allows hyphens between segments
// Examples: "my-alert", "alert1", "test-alert-2"
const nameRegex = new RE2("^[a-z0-9]+(?:-[a-z0-9]+)*$");
return (value: string) => nameRegex.test(value);
};
export const createSecureAlertBeforeValidator = () => {
// Validates alertBefore duration format: number followed by time unit
// Pattern: one or more digits followed by d(days), w(weeks), m(months), or y(years)
// Examples: "30d", "2w", "6m", "1y"
const alertBeforeRegex = new RE2("^\\d+[dwmy]$");
return (value: string) => {
if (value.length > 32) return false;
@@ -98,11 +104,11 @@ export const CreateChannelSchema = z.object({
export type TCreateChannel = z.infer<typeof CreateChannelSchema>;
export const CreatePkiAlertV2Schema = z.object({
slug: z
name: z
.string()
.min(1)
.max(255)
.refine(createSecureSlugValidator(), "Must be a valid slug (lowercase, numbers, hyphens only)"),
.refine(createSecureNameValidator(), "Must be a valid name (lowercase, numbers, hyphens only)"),
description: z.string().max(1000).optional(),
eventType: z.nativeEnum(PkiAlertEventType),
alertBefore: z.string().refine(createSecureAlertBeforeValidator(), "Must be in format like '30d', '1w', '3m', '1y'"),
@@ -169,7 +175,7 @@ export type TCertificatePreview = {
export type TAlertV2Response = {
id: string;
slug: string;
name: string;
description: string | null;
eventType: PkiAlertEventType;
alertBefore: string;
@@ -196,6 +202,4 @@ export type TListAlertsV2Response = {
export type TListMatchingCertificatesResponse = {
certificates: TCertificatePreview[];
total: number;
limit: number;
offset: number;
};

View File

@@ -10,8 +10,11 @@ export const useCreatePkiAlertV2 = () => {
return useMutation<TPkiAlertV2, unknown, TCreatePkiAlertV2>({
mutationFn: async (data) => {
const { data: response } = await apiRequest.post<TPkiAlertV2>("/api/v2/pki/alerts", data);
return response;
const { data: response } = await apiRequest.post<{ alert: TPkiAlertV2 }>(
"/api/v2/pki/alerts",
data
);
return response.alert;
},
onSuccess: (_, variables) => {
queryClient.invalidateQueries({
@@ -26,11 +29,11 @@ export const useUpdatePkiAlertV2 = () => {
return useMutation<TPkiAlertV2, unknown, TUpdatePkiAlertV2>({
mutationFn: async ({ alertId, ...data }) => {
const { data: response } = await apiRequest.patch<TPkiAlertV2>(
const { data: response } = await apiRequest.patch<{ alert: TPkiAlertV2 }>(
`/api/v2/pki/alerts/${alertId}`,
data
);
return response;
return response.alert;
},
onSuccess: (_, variables) => {
queryClient.invalidateQueries({
@@ -48,8 +51,10 @@ export const useDeletePkiAlertV2 = () => {
return useMutation<TPkiAlertV2, unknown, TDeletePkiAlertV2>({
mutationFn: async ({ alertId }) => {
const { data } = await apiRequest.delete<TPkiAlertV2>(`/api/v2/pki/alerts/${alertId}`);
return data;
const { data } = await apiRequest.delete<{ alert: TPkiAlertV2 }>(
`/api/v2/pki/alerts/${alertId}`
);
return data.alert;
},
onSuccess: (_, variables) => {
queryClient.invalidateQueries({

View File

@@ -31,8 +31,8 @@ const fetchPkiAlertsV2 = async (params: TGetPkiAlertsV2): Promise<TGetPkiAlertsV
};
const fetchPkiAlertV2ById = async ({ alertId }: TGetPkiAlertV2ById): Promise<TPkiAlertV2> => {
const { data } = await apiRequest.get<TPkiAlertV2>(`/api/v2/pki/alerts/${alertId}`);
return data;
const { data } = await apiRequest.get<{ alert: TPkiAlertV2 }>(`/api/v2/pki/alerts/${alertId}`);
return data.alert;
};
const fetchPkiAlertV2MatchingCertificates = async (

View File

@@ -51,7 +51,7 @@ export interface TPkiAlertChannelV2 {
export interface TPkiAlertV2 {
id: string;
projectId: string;
slug: string;
name: string;
description?: string;
eventType: PkiAlertEventTypeV2;
alertBefore?: string;
@@ -94,7 +94,7 @@ export interface TGetPkiAlertV2ById {
export interface TCreatePkiAlertV2 {
projectId: string;
slug: string;
name: string;
description?: string;
eventType: PkiAlertEventTypeV2;
alertBefore?: string;
@@ -105,7 +105,7 @@ export interface TCreatePkiAlertV2 {
export interface TUpdatePkiAlertV2 {
alertId: string;
slug?: string;
name?: string;
description?: string;
eventType?: PkiAlertEventTypeV2;
alertBefore?: string;
@@ -172,11 +172,11 @@ export const pkiAlertChannelV2Schema = z.object({
export const createPkiAlertV2Schema = z.object({
projectId: z.string().uuid(),
slug: z
name: z
.string()
.min(1)
.max(255)
.regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, "Must be a valid slug (lowercase, numbers, hyphens only)"),
.regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, "Must be a valid name (lowercase, numbers, hyphens only)"),
description: z.string().max(1000).optional(),
eventType: z.nativeEnum(PkiAlertEventTypeV2),
alertBefore: z

View File

@@ -45,7 +45,7 @@ export const PkiAlertsV2Page = ({ hideContainer = false }: Props) => {
const [deleteModal, setDeleteModal] = useState<{
isOpen: boolean;
alertId?: string;
slug?: string;
name?: string;
}>({
isOpen: false
});
@@ -113,7 +113,7 @@ export const PkiAlertsV2Page = ({ hideContainer = false }: Props) => {
setDeleteModal({
isOpen: true,
alertId: alert.id,
slug: alert.slug
name: alert.name
})
}
/>
@@ -199,8 +199,8 @@ export const PkiAlertsV2Page = ({ hideContainer = false }: Props) => {
<DeleteActionModal
isOpen={deleteModal.isOpen}
deleteKey="delete"
title={`Delete PKI Alert "${deleteModal.slug}"`}
onChange={(isOpen) => setDeleteModal({ isOpen, alertId: undefined, slug: undefined })}
title={`Delete PKI Alert "${deleteModal.name}"`}
onChange={(isOpen) => setDeleteModal({ isOpen, alertId: undefined, name: undefined })}
onDeleteApproved={handleDeleteAlert}
/>
</div>

View File

@@ -162,7 +162,7 @@ export const CreatePkiAlertV2FormSteps = () => {
render={({ field, fieldState: { error } }) => (
<FormControl label="Alert Type" isError={Boolean(error)} errorText={error?.message}>
<Select
defaultValue={field.value}
value={field.value}
onValueChange={(value) => field.onChange(value)}
className="w-full"
>
@@ -185,9 +185,9 @@ export const CreatePkiAlertV2FormSteps = () => {
<Controller
control={control}
name="slug"
name="name"
render={({ field, fieldState: { error } }) => (
<FormControl label="Alert Slug" isError={Boolean(error)} errorText={error?.message}>
<FormControl label="Alert Name" isError={Boolean(error)} errorText={error?.message}>
<Input {...field} placeholder="e.g., prod-cert-expiring-soon" />
</FormControl>
)}
@@ -490,12 +490,12 @@ export const CreatePkiAlertV2FormSteps = () => {
<div className="space-y-4">
<FormControl label="Email Recipients">
<TextArea
defaultValue={
value={
Array.isArray((watchedChannels?.[0]?.config as any)?.recipients)
? (watchedChannels[0].config as any).recipients.join(", ")
: ""
}
onBlur={(e) => {
onChange={(e) => {
const emailList = e.target.value
.split(",")
.map((email) => email.trim())
@@ -522,7 +522,7 @@ export const CreatePkiAlertV2FormSteps = () => {
<span className="text-sm text-mineshaft-300">Basic Information</span>
</div>
<div className="flex flex-wrap gap-x-8 gap-y-2">
<GenericFieldLabel label="Slug">{watch("slug") || "Not specified"}</GenericFieldLabel>
<GenericFieldLabel label="Name">{watch("name") || "Not specified"}</GenericFieldLabel>
<GenericFieldLabel label="Event Type">
{formatEventType(watchedEventType)}
</GenericFieldLabel>

View File

@@ -37,7 +37,7 @@ const FORM_TABS: { name: string; key: string; fields: (keyof TFormData)[] }[] =
{
name: "Details",
key: "basicInfo",
fields: ["slug", "description", "alertBefore"]
fields: ["name", "description", "alertBefore"]
},
{ name: "Filters", key: "filterRules", fields: ["filters"] },
{ name: "Preview", key: "preview", fields: [] },
@@ -61,7 +61,7 @@ export const CreatePkiAlertV2Modal = ({ isOpen, onOpenChange, alertToEdit, alert
resolver: zodResolver(isEditing ? updatePkiAlertV2Schema : createPkiAlertV2Schema),
defaultValues: {
projectId: currentProject?.id || "",
slug: "",
name: "",
description: "",
eventType: PkiAlertEventTypeV2.EXPIRATION,
alertBefore: "30d",
@@ -93,7 +93,7 @@ export const CreatePkiAlertV2Modal = ({ isOpen, onOpenChange, alertToEdit, alert
if (editingAlert && isEditing) {
reset({
projectId: currentProject?.id || "",
slug: editingAlert.slug,
name: editingAlert.name,
description: editingAlert.description || "",
eventType: editingAlert.eventType,
alertBefore: editingAlert.alertBefore || "30d",
@@ -121,7 +121,7 @@ export const CreatePkiAlertV2Modal = ({ isOpen, onOpenChange, alertToEdit, alert
} else if (!isEditing) {
reset({
projectId: currentProject?.id || "",
slug: "",
name: "",
description: "",
eventType: PkiAlertEventTypeV2.EXPIRATION,
alertBefore: "30d",

View File

@@ -87,7 +87,7 @@ export const PkiAlertV2Row = ({ alert, onView, onEdit, onDelete }: Props) => {
<Tr>
<Td>
<div className="flex items-center gap-2">
<div className="font-medium text-gray-200">{alert.slug}</div>
<div className="font-medium text-gray-200">{alert.name}</div>
{alert.description && (
<Tooltip content={alert.description}>
<FontAwesomeIcon icon={faCircleInfo} className="text-mineshaft-400" />

View File

@@ -60,7 +60,7 @@ export const UpdatePkiAlertV2Modal = ({ isOpen, onOpenChange, alertId }: Props)
useEffect(() => {
if (alert) {
reset({
slug: alert.slug,
name: alert.name,
description: alert.description || "",
eventType: alert.eventType,
alertBefore: alert.alertBefore || "",
@@ -190,7 +190,7 @@ export const UpdatePkiAlertV2Modal = ({ isOpen, onOpenChange, alertId }: Props)
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
<Controller
control={control}
name="slug"
name="name"
render={({ field, fieldState: { error } }) => (
<FormControl label="Alert Name" isError={Boolean(error)} errorText={error?.message}>
<Input {...field} placeholder="certificate-expiration-alert" />

View File

@@ -136,7 +136,7 @@ export const ViewPkiAlertV2Modal = ({ isOpen, onOpenChange, alertId }: Props) =>
<ModalContent title="Certificate Alert Details" className="max-w-6xl">
<div className="space-y-6">
<div className="flex items-center justify-between">
<h2 className="text-xl font-semibold text-gray-200">{alert.slug}</h2>
<h2 className="text-xl font-semibold text-gray-200">{alert.name}</h2>
<Badge variant={alert.enabled ? "success" : "neutral"}>
{alert.enabled ? "Enabled" : "Disabled"}
</Badge>
@@ -176,11 +176,11 @@ export const ViewPkiAlertV2Modal = ({ isOpen, onOpenChange, alertId }: Props) =>
<div className="space-y-3">
<h3 className="text-lg font-medium text-gray-200">Filter Rules</h3>
{alert.filters.length === 0 ? (
{(alert.filters || []).length === 0 ? (
<p className="text-gray-400">No filter rules configured</p>
) : (
<div className="space-y-2">
{alert.filters.map((filter) => {
{(alert.filters || []).map((filter) => {
const formatFilterText = () => {
const field = filter.field.replace(/_/g, " ");
const operator = filter.operator.replace(/_/g, " ");
@@ -207,14 +207,14 @@ export const ViewPkiAlertV2Modal = ({ isOpen, onOpenChange, alertId }: Props) =>
<div className="space-y-3">
<h3 className="text-lg font-medium text-gray-200">Notification Recipients</h3>
{alert.channels.some(
{(alert.channels || []).some(
(channel) =>
channel.channelType === "email" && (channel.config as any)?.recipients
) ? (
<div className="space-y-3">
<div>
<div className="flex flex-wrap gap-1">
{alert.channels
{(alert.channels || [])
.filter(
(channel) =>
channel.channelType === "email" &&