mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
PKI UI improvements
This commit is contained in:
@@ -121,9 +121,7 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
|
||||
limit: z.coerce.number().min(1).max(100).default(20),
|
||||
search: z.string().optional(),
|
||||
enrollmentType: z.nativeEnum(EnrollmentType).optional(),
|
||||
caId: z.string().uuid().optional(),
|
||||
includeMetrics: z.coerce.boolean().optional().default(false),
|
||||
expiringDays: z.coerce.number().min(1).max(365).optional().default(7)
|
||||
caId: z.string().uuid().optional()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
@@ -195,10 +193,6 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
|
||||
params: z.object({
|
||||
id: z.string().uuid()
|
||||
}),
|
||||
querystring: z.object({
|
||||
includeMetrics: z.coerce.boolean().optional().default(false),
|
||||
expiringDays: z.coerce.number().min(1).max(365).optional().default(7)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
certificateProfile: PkiCertificateProfilesSchema.extend({
|
||||
@@ -232,16 +226,6 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
|
||||
autoRenew: z.boolean(),
|
||||
renewBeforeDays: z.number().optional()
|
||||
})
|
||||
.optional(),
|
||||
metrics: z
|
||||
.object({
|
||||
profileId: z.string(),
|
||||
totalCertificates: z.number(),
|
||||
activeCertificates: z.number(),
|
||||
expiredCertificates: z.number(),
|
||||
expiringCertificates: z.number(),
|
||||
revokedCertificates: z.number()
|
||||
})
|
||||
.optional()
|
||||
})
|
||||
})
|
||||
@@ -257,20 +241,6 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
|
||||
profileId: req.params.id
|
||||
});
|
||||
|
||||
let result = certificateProfile;
|
||||
|
||||
if (req.query.includeMetrics) {
|
||||
const metrics = await server.services.certificateProfile.getProfileMetrics({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
profileId: req.params.id,
|
||||
expiringDays: req.query.expiringDays
|
||||
});
|
||||
result = { ...certificateProfile, metrics };
|
||||
}
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: certificateProfile.projectId,
|
||||
@@ -283,7 +253,7 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
|
||||
}
|
||||
});
|
||||
|
||||
return { certificateProfile: result };
|
||||
return { certificateProfile };
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -89,6 +89,7 @@ const PkiSyncCertificateSchema = z.object({
|
||||
updatedAt: z.date(),
|
||||
certificateSerialNumber: z.string().optional(),
|
||||
certificateCommonName: z.string().optional(),
|
||||
certificateAltNames: z.string().optional(),
|
||||
certificateStatus: z.string().optional(),
|
||||
certificateNotBefore: z.date().optional(),
|
||||
certificateNotAfter: z.date().optional(),
|
||||
|
||||
@@ -10,10 +10,8 @@ import {
|
||||
TCertificateProfile,
|
||||
TCertificateProfileCertificate,
|
||||
TCertificateProfileInsert,
|
||||
TCertificateProfileMetrics,
|
||||
TCertificateProfileUpdate,
|
||||
TCertificateProfileWithConfigs,
|
||||
TCertificateProfileWithRawMetrics
|
||||
TCertificateProfileWithConfigs
|
||||
} from "./certificate-profile-types";
|
||||
|
||||
export type TCertificateProfileDALFactory = ReturnType<typeof certificateProfileDALFactory>;
|
||||
@@ -203,21 +201,11 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
|
||||
search?: string;
|
||||
enrollmentType?: EnrollmentType;
|
||||
caId?: string;
|
||||
includeMetrics?: boolean;
|
||||
expiringDays?: number;
|
||||
} = {},
|
||||
tx?: Knex
|
||||
): Promise<TCertificateProfile[] | TCertificateProfileWithRawMetrics[] | TCertificateProfileWithConfigs[]> => {
|
||||
): Promise<TCertificateProfile[] | TCertificateProfileWithConfigs[]> => {
|
||||
try {
|
||||
const {
|
||||
offset = 0,
|
||||
limit = 20,
|
||||
search,
|
||||
enrollmentType,
|
||||
caId,
|
||||
includeMetrics = false,
|
||||
expiringDays = 7
|
||||
} = options;
|
||||
const { offset = 0, limit = 20, search, enrollmentType, caId } = options;
|
||||
|
||||
let baseQuery = (tx || db)(TableName.PkiCertificateProfile).where(
|
||||
`${TableName.PkiCertificateProfile}.projectId`,
|
||||
@@ -242,7 +230,7 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
|
||||
baseQuery = baseQuery.where(`${TableName.PkiCertificateProfile}.caId`, caId);
|
||||
}
|
||||
|
||||
let query = baseQuery
|
||||
const query = baseQuery
|
||||
.leftJoin(
|
||||
TableName.PkiEstEnrollmentConfig,
|
||||
`${TableName.PkiCertificateProfile}.estConfigId`,
|
||||
@@ -267,52 +255,6 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
|
||||
db.ref("renewBeforeDays").withSchema(TableName.PkiApiEnrollmentConfig).as("apiRenewBeforeDays")
|
||||
);
|
||||
|
||||
if (includeMetrics) {
|
||||
query = query.leftJoin(
|
||||
TableName.Certificate,
|
||||
`${TableName.PkiCertificateProfile}.id`,
|
||||
`${TableName.Certificate}.profileId`
|
||||
);
|
||||
|
||||
const now = new Date();
|
||||
const expiringDate = new Date();
|
||||
expiringDate.setDate(now.getDate() + expiringDays);
|
||||
|
||||
query = query
|
||||
.select(
|
||||
selectAllTableCols(TableName.PkiCertificateProfile),
|
||||
db.ref("id").withSchema(TableName.PkiEstEnrollmentConfig).as("estId"),
|
||||
db
|
||||
.ref("disableBootstrapCaValidation")
|
||||
.withSchema(TableName.PkiEstEnrollmentConfig)
|
||||
.as("estDisableBootstrapCaValidation"),
|
||||
db.ref("hashedPassphrase").withSchema(TableName.PkiEstEnrollmentConfig).as("estHashedPassphrase"),
|
||||
db.ref("encryptedCaChain").withSchema(TableName.PkiEstEnrollmentConfig).as("estEncryptedCaChain"),
|
||||
db.ref("id").withSchema(TableName.PkiApiEnrollmentConfig).as("apiId"),
|
||||
db.ref("autoRenew").withSchema(TableName.PkiApiEnrollmentConfig).as("apiAutoRenew"),
|
||||
db.ref("renewBeforeDays").withSchema(TableName.PkiApiEnrollmentConfig).as("apiRenewBeforeDays"),
|
||||
db.raw("COUNT(certificates.id) as total_certificates"),
|
||||
db.raw(
|
||||
'COUNT(CASE WHEN certificates."revokedAt" IS NULL AND certificates."notAfter" > ? THEN 1 END) as active_certificates',
|
||||
[expiringDate]
|
||||
),
|
||||
db.raw(
|
||||
'COUNT(CASE WHEN certificates."revokedAt" IS NULL AND certificates."notAfter" <= ? THEN 1 END) as expired_certificates',
|
||||
[now]
|
||||
),
|
||||
db.raw(
|
||||
'COUNT(CASE WHEN certificates."revokedAt" IS NULL AND certificates."notAfter" > ? AND certificates."notAfter" <= ? THEN 1 END) as expiring_certificates',
|
||||
[now, expiringDate]
|
||||
),
|
||||
db.raw('COUNT(CASE WHEN certificates."revokedAt" IS NOT NULL THEN 1 END) as revoked_certificates')
|
||||
)
|
||||
.groupBy(
|
||||
`${TableName.PkiCertificateProfile}.id`,
|
||||
`${TableName.PkiEstEnrollmentConfig}.id`,
|
||||
`${TableName.PkiApiEnrollmentConfig}.id`
|
||||
);
|
||||
}
|
||||
|
||||
const results = (await query
|
||||
.orderBy(`${TableName.PkiCertificateProfile}.createdAt`, "desc")
|
||||
.offset(offset)
|
||||
@@ -353,17 +295,6 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
|
||||
apiConfig
|
||||
};
|
||||
|
||||
if (includeMetrics) {
|
||||
return {
|
||||
...baseProfile,
|
||||
total_certificates: result.total_certificates,
|
||||
active_certificates: result.active_certificates,
|
||||
expired_certificates: result.expired_certificates,
|
||||
expiring_certificates: result.expiring_certificates,
|
||||
revoked_certificates: result.revoked_certificates
|
||||
} as TCertificateProfileWithRawMetrics & TCertificateProfileWithConfigs;
|
||||
}
|
||||
|
||||
return baseProfile as TCertificateProfileWithConfigs;
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -485,45 +416,6 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
};
|
||||
|
||||
const getProfileMetrics = async (
|
||||
profileId: string,
|
||||
expiringDays: number = 7,
|
||||
tx?: Knex
|
||||
): Promise<TCertificateProfileMetrics> => {
|
||||
try {
|
||||
const now = new Date();
|
||||
const expiringDate = new Date();
|
||||
expiringDate.setDate(now.getDate() + expiringDays);
|
||||
|
||||
const metrics = await (tx || db)(TableName.Certificate)
|
||||
.where("profileId", profileId)
|
||||
.select(
|
||||
db.raw("COUNT(*) as total_certificates"),
|
||||
db.raw('COUNT(CASE WHEN "revokedAt" IS NULL AND "notAfter" > ? THEN 1 END) as active_certificates', [
|
||||
expiringDate
|
||||
]),
|
||||
db.raw('COUNT(CASE WHEN "revokedAt" IS NULL AND "notAfter" <= ? THEN 1 END) as expired_certificates', [now]),
|
||||
db.raw(
|
||||
'COUNT(CASE WHEN "revokedAt" IS NULL AND "notAfter" > ? AND "notAfter" <= ? THEN 1 END) as expiring_certificates',
|
||||
[now, expiringDate]
|
||||
),
|
||||
db.raw('COUNT(CASE WHEN "revokedAt" IS NOT NULL THEN 1 END) as revoked_certificates')
|
||||
)
|
||||
.first();
|
||||
|
||||
return {
|
||||
profileId,
|
||||
totalCertificates: parseInt(String((metrics as Record<string, unknown>)?.total_certificates || 0), 10),
|
||||
activeCertificates: parseInt(String((metrics as Record<string, unknown>)?.active_certificates || 0), 10),
|
||||
expiredCertificates: parseInt(String((metrics as Record<string, unknown>)?.expired_certificates || 0), 10),
|
||||
expiringCertificates: parseInt(String((metrics as Record<string, unknown>)?.expiring_certificates || 0), 10),
|
||||
revokedCertificates: parseInt(String((metrics as Record<string, unknown>)?.revoked_certificates || 0), 10)
|
||||
};
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Get certificate profile metrics" });
|
||||
}
|
||||
};
|
||||
|
||||
const isProfileInUse = async (profileId: string, tx?: Knex) => {
|
||||
try {
|
||||
const doc = await (tx || db)(TableName.Certificate).where("profileId", profileId).count("*").first();
|
||||
@@ -546,7 +438,6 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
|
||||
countByProjectId,
|
||||
findByNameAndProjectId,
|
||||
getCertificatesByProfile,
|
||||
getProfileMetrics,
|
||||
isProfileInUse
|
||||
};
|
||||
};
|
||||
|
||||
@@ -127,8 +127,3 @@ export const listCertificatesByProfileSchema = z.object({
|
||||
status: z.enum(["active", "expired", "revoked"]).optional(),
|
||||
search: z.string().optional()
|
||||
});
|
||||
|
||||
export const getCertificateProfileMetricsSchema = z.object({
|
||||
profileId: z.string().uuid(),
|
||||
expiringDays: z.coerce.number().min(1).max(365).default(30)
|
||||
});
|
||||
|
||||
@@ -47,7 +47,6 @@ describe("CertificateProfileService", () => {
|
||||
findByNameAndProjectId: vi.fn(),
|
||||
findByIdWithConfigs: vi.fn(),
|
||||
getCertificatesByProfile: vi.fn(),
|
||||
getProfileMetrics: vi.fn(),
|
||||
isProfileInUse: vi.fn(),
|
||||
transaction: vi.fn(),
|
||||
find: vi.fn(),
|
||||
@@ -493,9 +492,7 @@ describe("CertificateProfileService", () => {
|
||||
limit: 20,
|
||||
search: undefined,
|
||||
enrollmentType: undefined,
|
||||
caId: undefined,
|
||||
includeMetrics: false,
|
||||
expiringDays: 30
|
||||
caId: undefined
|
||||
});
|
||||
});
|
||||
|
||||
@@ -515,51 +512,7 @@ describe("CertificateProfileService", () => {
|
||||
limit: 5,
|
||||
search: "test",
|
||||
enrollmentType: EnrollmentType.API,
|
||||
caId: "ca-123",
|
||||
includeMetrics: false,
|
||||
expiringDays: 30
|
||||
});
|
||||
});
|
||||
|
||||
it("should list profiles with metrics when includeMetrics is true", async () => {
|
||||
const mockProfilesWithMetrics = [
|
||||
{
|
||||
...sampleProfile,
|
||||
total_certificates: 10,
|
||||
active_certificates: 8,
|
||||
expired_certificates: 1,
|
||||
expiring_certificates: 1,
|
||||
revoked_certificates: 0
|
||||
}
|
||||
];
|
||||
(mockCertificateProfileDAL.findByProjectId as any).mockResolvedValue(mockProfilesWithMetrics);
|
||||
|
||||
const result = await service.listProfiles({
|
||||
...mockActor,
|
||||
projectId: "project-123",
|
||||
includeMetrics: true,
|
||||
expiringDays: 15
|
||||
});
|
||||
|
||||
expect(result.profiles).toHaveLength(1);
|
||||
expect(result.profiles[0]).toHaveProperty("metrics");
|
||||
expect(result.profiles[0].metrics).toEqual({
|
||||
profileId: sampleProfile.id,
|
||||
totalCertificates: 10,
|
||||
activeCertificates: 8,
|
||||
expiredCertificates: 1,
|
||||
expiringCertificates: 1,
|
||||
revokedCertificates: 0
|
||||
});
|
||||
|
||||
expect(mockCertificateProfileDAL.findByProjectId).toHaveBeenCalledWith("project-123", {
|
||||
offset: 0,
|
||||
limit: 20,
|
||||
search: undefined,
|
||||
enrollmentType: undefined,
|
||||
caId: undefined,
|
||||
includeMetrics: true,
|
||||
expiringDays: 15
|
||||
caId: "ca-123"
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -659,54 +612,6 @@ describe("CertificateProfileService", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("getProfileMetrics", () => {
|
||||
const mockMetrics = {
|
||||
profileId: "profile-123",
|
||||
totalCertificates: 10,
|
||||
activeCertificates: 8,
|
||||
expiredCertificates: 1,
|
||||
expiringCertificates: 2,
|
||||
revokedCertificates: 1
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
(mockCertificateProfileDAL.findById as any).mockResolvedValue(sampleProfile);
|
||||
(mockCertificateProfileDAL.getProfileMetrics as any).mockResolvedValue(mockMetrics);
|
||||
});
|
||||
|
||||
it("should get profile metrics successfully", async () => {
|
||||
const result = await service.getProfileMetrics({
|
||||
...mockActor,
|
||||
profileId: "profile-123"
|
||||
});
|
||||
|
||||
expect(result).toEqual(mockMetrics);
|
||||
expect(mockCertificateProfileDAL.findById).toHaveBeenCalledWith("profile-123");
|
||||
expect(mockCertificateProfileDAL.getProfileMetrics).toHaveBeenCalledWith("profile-123", 30);
|
||||
});
|
||||
|
||||
it("should get profile metrics with custom expiring days", async () => {
|
||||
await service.getProfileMetrics({
|
||||
...mockActor,
|
||||
profileId: "profile-123",
|
||||
expiringDays: 60
|
||||
});
|
||||
|
||||
expect(mockCertificateProfileDAL.getProfileMetrics).toHaveBeenCalledWith("profile-123", 60);
|
||||
});
|
||||
|
||||
it("should throw NotFoundError when profile not found", async () => {
|
||||
(mockCertificateProfileDAL.findById as any).mockResolvedValue(null);
|
||||
|
||||
await expect(
|
||||
service.getProfileMetrics({
|
||||
...mockActor,
|
||||
profileId: "profile-123"
|
||||
})
|
||||
).rejects.toThrow(NotFoundError);
|
||||
});
|
||||
});
|
||||
|
||||
describe("comprehensive certificate profile scenarios", () => {
|
||||
describe("profile configuration validation", () => {
|
||||
it("should validate EST enrollment configuration", async () => {
|
||||
@@ -929,53 +834,6 @@ describe("CertificateProfileService", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("metrics and monitoring", () => {
|
||||
it("should calculate profile metrics correctly", async () => {
|
||||
const detailedMetrics = {
|
||||
profileId: "profile-123",
|
||||
totalCertificates: 50,
|
||||
activeCertificates: 40,
|
||||
expiredCertificates: 5,
|
||||
expiringCertificates: 3,
|
||||
revokedCertificates: 2
|
||||
};
|
||||
|
||||
(mockCertificateProfileDAL.findById as any).mockResolvedValue(sampleProfile);
|
||||
(mockCertificateProfileDAL.getProfileMetrics as any).mockResolvedValue(detailedMetrics);
|
||||
|
||||
const result = await service.getProfileMetrics({
|
||||
...mockActor,
|
||||
profileId: "profile-123",
|
||||
expiringDays: 14
|
||||
});
|
||||
|
||||
expect(result).toEqual(detailedMetrics);
|
||||
expect(mockCertificateProfileDAL.getProfileMetrics).toHaveBeenCalledWith("profile-123", 14);
|
||||
});
|
||||
|
||||
it("should handle zero certificate metrics", async () => {
|
||||
const emptyMetrics = {
|
||||
profileId: "profile-123",
|
||||
totalCertificates: 0,
|
||||
activeCertificates: 0,
|
||||
expiredCertificates: 0,
|
||||
expiringCertificates: 0,
|
||||
revokedCertificates: 0
|
||||
};
|
||||
|
||||
(mockCertificateProfileDAL.findById as any).mockResolvedValue(sampleProfile);
|
||||
(mockCertificateProfileDAL.getProfileMetrics as any).mockResolvedValue(emptyMetrics);
|
||||
|
||||
const result = await service.getProfileMetrics({
|
||||
...mockActor,
|
||||
profileId: "profile-123"
|
||||
});
|
||||
|
||||
expect(result.totalCertificates).toBe(0);
|
||||
expect(result.activeCertificates).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("error scenarios", () => {
|
||||
it("should handle database connection errors gracefully", async () => {
|
||||
(mockCertificateProfileDAL.findById as any).mockRejectedValue(new Error("Database connection failed"));
|
||||
|
||||
@@ -27,10 +27,8 @@ import {
|
||||
TCertificateProfile,
|
||||
TCertificateProfileCertificate,
|
||||
TCertificateProfileInsert,
|
||||
TCertificateProfileMetrics,
|
||||
TCertificateProfileUpdate,
|
||||
TCertificateProfileWithConfigs,
|
||||
TCertificateProfileWithRawMetrics
|
||||
TCertificateProfileWithConfigs
|
||||
} from "./certificate-profile-types";
|
||||
|
||||
const validateAndEncryptPemCaChain = async (
|
||||
@@ -361,18 +359,14 @@ export const certificateProfileServiceFactory = ({
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
profileId,
|
||||
includeMetrics = false,
|
||||
expiringDays = 30
|
||||
profileId
|
||||
}: {
|
||||
actor: ActorType;
|
||||
actorId: string;
|
||||
actorAuthMethod: ActorAuthMethod;
|
||||
actorOrgId: string;
|
||||
profileId: string;
|
||||
includeMetrics?: boolean;
|
||||
expiringDays?: number;
|
||||
}): Promise<TCertificateProfile & { metrics?: TCertificateProfileMetrics }> => {
|
||||
}): Promise<TCertificateProfile> => {
|
||||
const profile = await certificateProfileDAL.findById(profileId);
|
||||
if (!profile) {
|
||||
throw new NotFoundError({ message: "Certificate profile not found" });
|
||||
@@ -393,14 +387,6 @@ export const certificateProfileServiceFactory = ({
|
||||
|
||||
const converted = convertDalToService(profile);
|
||||
|
||||
if (includeMetrics) {
|
||||
const metrics = await certificateProfileDAL.getProfileMetrics(profileId, expiringDays);
|
||||
return {
|
||||
...converted,
|
||||
metrics
|
||||
};
|
||||
}
|
||||
|
||||
return converted;
|
||||
};
|
||||
|
||||
@@ -506,9 +492,7 @@ export const certificateProfileServiceFactory = ({
|
||||
limit = 20,
|
||||
search,
|
||||
enrollmentType,
|
||||
caId,
|
||||
includeMetrics = false,
|
||||
expiringDays = 30
|
||||
caId
|
||||
}: {
|
||||
actor: ActorType;
|
||||
actorId: string;
|
||||
@@ -520,10 +504,8 @@ export const certificateProfileServiceFactory = ({
|
||||
search?: string;
|
||||
enrollmentType?: EnrollmentType;
|
||||
caId?: string;
|
||||
includeMetrics?: boolean;
|
||||
expiringDays?: number;
|
||||
}): Promise<{
|
||||
profiles: (TCertificateProfileWithConfigs & { metrics?: TCertificateProfileMetrics })[];
|
||||
profiles: TCertificateProfileWithConfigs[];
|
||||
totalCount: number;
|
||||
}> => {
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
@@ -544,9 +526,7 @@ export const certificateProfileServiceFactory = ({
|
||||
limit,
|
||||
search,
|
||||
enrollmentType,
|
||||
caId,
|
||||
includeMetrics,
|
||||
expiringDays
|
||||
caId
|
||||
});
|
||||
|
||||
const totalCount = await certificateProfileDAL.countByProjectId(projectId, {
|
||||
@@ -591,27 +571,12 @@ export const certificateProfileServiceFactory = ({
|
||||
}
|
||||
|
||||
const converted = convertDalToService(profileWithConfigs);
|
||||
let result: TCertificateProfileWithConfigs & { metrics?: TCertificateProfileMetrics } = {
|
||||
const result: TCertificateProfileWithConfigs = {
|
||||
...converted,
|
||||
estConfig: decryptedEstConfig,
|
||||
apiConfig: profileWithConfigs.apiConfig
|
||||
};
|
||||
|
||||
if (includeMetrics) {
|
||||
const profileWithMetrics = profile as TCertificateProfileWithRawMetrics;
|
||||
result = {
|
||||
...result,
|
||||
metrics: {
|
||||
profileId: converted.id,
|
||||
totalCertificates: parseInt(String(profileWithMetrics.total_certificates || 0), 10),
|
||||
activeCertificates: parseInt(String(profileWithMetrics.active_certificates || 0), 10),
|
||||
expiredCertificates: parseInt(String(profileWithMetrics.expired_certificates || 0), 10),
|
||||
expiringCertificates: parseInt(String(profileWithMetrics.expiring_certificates || 0), 10),
|
||||
revokedCertificates: parseInt(String(profileWithMetrics.revoked_certificates || 0), 10)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return result;
|
||||
})
|
||||
);
|
||||
@@ -709,43 +674,6 @@ export const certificateProfileServiceFactory = ({
|
||||
return certificates;
|
||||
};
|
||||
|
||||
const getProfileMetrics = async ({
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
profileId,
|
||||
expiringDays = 30
|
||||
}: {
|
||||
actor: ActorType;
|
||||
actorId: string;
|
||||
actorAuthMethod: ActorAuthMethod;
|
||||
actorOrgId: string;
|
||||
profileId: string;
|
||||
expiringDays?: number;
|
||||
}): Promise<TCertificateProfileMetrics> => {
|
||||
const profile = await certificateProfileDAL.findById(profileId);
|
||||
if (!profile) {
|
||||
throw new NotFoundError({ message: "Certificate profile not found" });
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId: profile.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.CertificateManager
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionCertificateProfileActions.Read,
|
||||
ProjectPermissionSub.CertificateProfiles
|
||||
);
|
||||
|
||||
const metrics = await certificateProfileDAL.getProfileMetrics(profileId, expiringDays);
|
||||
return metrics;
|
||||
};
|
||||
|
||||
const getEstConfigurationByProfile = async (
|
||||
params:
|
||||
| {
|
||||
@@ -818,7 +746,6 @@ export const certificateProfileServiceFactory = ({
|
||||
listProfiles,
|
||||
deleteProfile,
|
||||
getProfileCertificates,
|
||||
getProfileMetrics,
|
||||
getEstConfigurationByProfile
|
||||
};
|
||||
};
|
||||
|
||||
@@ -54,18 +54,8 @@ export type TCertificateProfileWithConfigs = TCertificateProfile & {
|
||||
autoRenew: boolean;
|
||||
renewBeforeDays?: number;
|
||||
};
|
||||
metrics?: TCertificateProfileMetrics;
|
||||
};
|
||||
|
||||
export interface TCertificateProfileMetrics {
|
||||
profileId: string;
|
||||
totalCertificates: number;
|
||||
activeCertificates: number;
|
||||
expiredCertificates: number;
|
||||
expiringCertificates: number;
|
||||
revokedCertificates: number;
|
||||
}
|
||||
|
||||
export interface TCertificateProfileCertificate {
|
||||
id: string;
|
||||
serialNumber: string;
|
||||
@@ -76,11 +66,3 @@ export interface TCertificateProfileCertificate {
|
||||
revokedAt: Date | null;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
export type TCertificateProfileWithRawMetrics = TCertificateProfile & {
|
||||
total_certificates?: string;
|
||||
active_certificates?: string;
|
||||
expired_certificates?: string;
|
||||
expiring_certificates?: string;
|
||||
revoked_certificates?: string;
|
||||
};
|
||||
|
||||
@@ -180,6 +180,7 @@ export const certificateSyncDALFactory = (db: TDbClient) => {
|
||||
certificateDetails: (TCertificateSyncs & {
|
||||
certificateSerialNumber?: string;
|
||||
certificateCommonName?: string;
|
||||
certificateAltNames?: string;
|
||||
certificateStatus?: string;
|
||||
certificateNotBefore?: Date;
|
||||
certificateNotAfter?: Date;
|
||||
@@ -211,6 +212,7 @@ export const certificateSyncDALFactory = (db: TDbClient) => {
|
||||
.select(
|
||||
db.ref("serialNumber").withSchema(TableName.Certificate).as("certificateSerialNumber"),
|
||||
db.ref("commonName").withSchema(TableName.Certificate).as("certificateCommonName"),
|
||||
db.ref("altNames").withSchema(TableName.Certificate).as("certificateAltNames"),
|
||||
db.ref("status").withSchema(TableName.Certificate).as("certificateStatus"),
|
||||
db.ref("notBefore").withSchema(TableName.Certificate).as("certificateNotBefore"),
|
||||
db.ref("notAfter").withSchema(TableName.Certificate).as("certificateNotAfter"),
|
||||
@@ -229,6 +231,7 @@ export const certificateSyncDALFactory = (db: TDbClient) => {
|
||||
const certificateDetails = (await query) as (TCertificateSyncs & {
|
||||
certificateSerialNumber?: string;
|
||||
certificateCommonName?: string;
|
||||
certificateAltNames?: string;
|
||||
certificateStatus?: string;
|
||||
certificateNotBefore?: Date;
|
||||
certificateNotAfter?: Date;
|
||||
|
||||
@@ -520,7 +520,7 @@ export const awsCertificateManagerPkiSyncFactory = ({
|
||||
try {
|
||||
// Small delay to ensure AWS ACM has processed the certificate import
|
||||
await new Promise<void>((resolve) => {
|
||||
setTimeout(() => resolve(), 100);
|
||||
setTimeout(() => resolve(), 500);
|
||||
});
|
||||
|
||||
await withRateLimitRetry(
|
||||
|
||||
@@ -606,6 +606,7 @@ export const pkiSyncServiceFactory = ({
|
||||
updatedAt: detail.updatedAt,
|
||||
certificateSerialNumber: detail.certificateSerialNumber || undefined,
|
||||
certificateCommonName: detail.certificateCommonName || undefined,
|
||||
certificateAltNames: detail.certificateAltNames || undefined,
|
||||
certificateStatus: detail.certificateStatus || undefined,
|
||||
certificateNotBefore: detail.certificateNotBefore || undefined,
|
||||
certificateNotAfter: detail.certificateNotAfter || undefined,
|
||||
|
||||
@@ -170,13 +170,14 @@ export type TPkiSyncCertificate = {
|
||||
lastSyncedAt?: Date;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
certificate?: {
|
||||
serialNumber: string;
|
||||
commonName: string;
|
||||
status: string;
|
||||
notBefore: Date;
|
||||
notAfter: Date;
|
||||
};
|
||||
certificateSerialNumber?: string;
|
||||
certificateCommonName?: string;
|
||||
certificateAltNames?: string;
|
||||
certificateStatus?: string;
|
||||
certificateNotBefore?: Date;
|
||||
certificateNotAfter?: Date;
|
||||
pkiSyncName?: string;
|
||||
pkiSyncDestination?: string;
|
||||
};
|
||||
|
||||
export type TPkiSyncRaw = NonNullable<Awaited<ReturnType<TPkiSyncDALFactory["findById"]>>>;
|
||||
|
||||
@@ -17,9 +17,9 @@ import {
|
||||
Td,
|
||||
Th,
|
||||
THead,
|
||||
Tooltip,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { Badge } from "@app/components/v3";
|
||||
import { useProject } from "@app/context";
|
||||
import {
|
||||
CertStatus,
|
||||
@@ -299,119 +299,129 @@ export const CertificateManagementModal = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{allCertificates.length === 0 ? (
|
||||
<EmptyState title="No certificates found">
|
||||
{searchTerm
|
||||
? "No certificates match your search criteria."
|
||||
: "No certificates available for sync."}
|
||||
</EmptyState>
|
||||
) : (
|
||||
<>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th className="w-12">
|
||||
<Checkbox
|
||||
id="select-all-certificates"
|
||||
isChecked={
|
||||
allCertificates.length > 0 &&
|
||||
allCertificates.every((cert) => selectedIds.includes(cert.id))
|
||||
}
|
||||
onCheckedChange={handleSelectAll}
|
||||
/>
|
||||
</Th>
|
||||
<Th className="w-1/3">Common Name</Th>
|
||||
<Th className="w-1/3">Serial Number</Th>
|
||||
<Th className="w-1/6">Status</Th>
|
||||
<Th className="w-2/6">Expires</Th>
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{allCertificates.map((cert) => {
|
||||
const isExpired = new Date(cert.notAfter) < new Date();
|
||||
const isRevoked = cert.status === CertStatus.REVOKED;
|
||||
const cannotBeAdded = isExpired || isRevoked;
|
||||
const isAlreadySynced = syncedCertificateIds.includes(cert.id);
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th className="w-12">
|
||||
<Checkbox
|
||||
id="select-all-certificates"
|
||||
isChecked={
|
||||
allCertificates.length > 0 &&
|
||||
allCertificates.every((cert) => selectedIds.includes(cert.id))
|
||||
}
|
||||
onCheckedChange={handleSelectAll}
|
||||
/>
|
||||
</Th>
|
||||
<Th className="w-1/3">SAN / CN</Th>
|
||||
<Th className="w-1/4">Serial Number</Th>
|
||||
<Th className="w-1/6">Issued At</Th>
|
||||
<Th className="w-1/6">Expires At</Th>
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{allCertificates.map((cert) => {
|
||||
const isExpired = new Date(cert.notAfter) < new Date();
|
||||
const isRevoked = cert.status === CertStatus.REVOKED;
|
||||
const cannotBeAdded = isExpired || isRevoked;
|
||||
const isAlreadySynced = syncedCertificateIds.includes(cert.id);
|
||||
|
||||
return (
|
||||
<Tr
|
||||
key={cert.id}
|
||||
className={`cursor-pointer hover:bg-mineshaft-700 ${
|
||||
cannotBeAdded && !isAlreadySynced ? "opacity-50" : ""
|
||||
}`}
|
||||
onClick={() => {
|
||||
let originalDisplayName = "—";
|
||||
if (cert.altNames && cert.altNames.trim()) {
|
||||
originalDisplayName = cert.altNames.trim();
|
||||
} else if (cert.commonName && cert.commonName.trim()) {
|
||||
originalDisplayName = cert.commonName.trim();
|
||||
}
|
||||
|
||||
let displayName = originalDisplayName;
|
||||
let isTruncated = false;
|
||||
if (originalDisplayName.length > 34) {
|
||||
displayName = `${originalDisplayName.substring(0, 34)}...`;
|
||||
isTruncated = true;
|
||||
}
|
||||
|
||||
const truncatedSerial =
|
||||
cert.serialNumber.length > 8
|
||||
? `${cert.serialNumber.slice(0, 4)}...${cert.serialNumber.slice(-4)}`
|
||||
: cert.serialNumber;
|
||||
|
||||
return (
|
||||
<Tr
|
||||
key={cert.id}
|
||||
className={`cursor-pointer hover:bg-mineshaft-700 ${
|
||||
cannotBeAdded && !isAlreadySynced ? "opacity-50" : ""
|
||||
}`}
|
||||
onClick={() => {
|
||||
if (!cannotBeAdded || isAlreadySynced) {
|
||||
handleToggleSelection(cert.id);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Td className="max-w-0" onClick={(e) => e.stopPropagation()}>
|
||||
<Checkbox
|
||||
id={cert.id}
|
||||
isChecked={selectedIds.includes(cert.id)}
|
||||
onCheckedChange={() => {
|
||||
if (!cannotBeAdded || isAlreadySynced) {
|
||||
handleToggleSelection(cert.id);
|
||||
}
|
||||
}}
|
||||
isDisabled={cannotBeAdded && !isAlreadySynced}
|
||||
/>
|
||||
</Td>
|
||||
<Td className="max-w-0">
|
||||
{isTruncated ? (
|
||||
<Tooltip content={originalDisplayName} className="max-w-lg">
|
||||
<div className="truncate">{displayName}</div>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<div className="truncate">{displayName}</div>
|
||||
)}
|
||||
</Td>
|
||||
<Td className="max-w-0">
|
||||
<div
|
||||
className="font-mono text-xs text-bunker-300"
|
||||
title={cert.serialNumber}
|
||||
>
|
||||
<Td className="max-w-0">
|
||||
<Checkbox
|
||||
id={cert.id}
|
||||
isChecked={selectedIds.includes(cert.id)}
|
||||
onCheckedChange={() => {
|
||||
if (!cannotBeAdded || isAlreadySynced) {
|
||||
handleToggleSelection(cert.id);
|
||||
}
|
||||
}}
|
||||
isDisabled={cannotBeAdded && !isAlreadySynced}
|
||||
/>
|
||||
</Td>
|
||||
<Td className="max-w-0">
|
||||
<div className="truncate" title={cert.commonName}>
|
||||
{cert.commonName}
|
||||
</div>
|
||||
</Td>
|
||||
<Td className="max-w-0">
|
||||
<div
|
||||
className="truncate font-mono text-xs text-bunker-300"
|
||||
title={cert.serialNumber}
|
||||
>
|
||||
{cert.serialNumber}
|
||||
</div>
|
||||
</Td>
|
||||
<Td className="max-w-0">
|
||||
<Badge
|
||||
variant={
|
||||
cert.status === CertStatus.ACTIVE && !isExpired
|
||||
? "success"
|
||||
: "danger"
|
||||
}
|
||||
>
|
||||
{(() => {
|
||||
if (isRevoked) return "Revoked";
|
||||
if (isExpired) return "Expired";
|
||||
return cert.status === CertStatus.ACTIVE ? "Active" : cert.status;
|
||||
})()}
|
||||
</Badge>
|
||||
</Td>
|
||||
<Td className="max-w-0">
|
||||
<span
|
||||
className={`text-sm ${isExpired ? "text-red-400" : "text-bunker-300"}`}
|
||||
>
|
||||
{new Date(cert.notAfter).toLocaleDateString()}
|
||||
</span>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
</TBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
{truncatedSerial}
|
||||
</div>
|
||||
</Td>
|
||||
<Td className="max-w-0">
|
||||
<span className="text-sm text-bunker-300">
|
||||
{new Date(cert.notBefore).toLocaleDateString()}
|
||||
</span>
|
||||
</Td>
|
||||
<Td className="max-w-0">
|
||||
<span
|
||||
className={`text-sm ${isExpired ? "text-red-400" : "text-bunker-300"}`}
|
||||
>
|
||||
{new Date(cert.notAfter).toLocaleDateString()}
|
||||
</span>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
</TBody>
|
||||
</Table>
|
||||
{allCertificates.length === 0 && (
|
||||
<EmptyState title="No certificates found">
|
||||
{searchTerm
|
||||
? "No certificates match your search criteria."
|
||||
: "No certificates available for sync."}
|
||||
</EmptyState>
|
||||
)}
|
||||
</TableContainer>
|
||||
|
||||
{totalPages > 1 && (
|
||||
<div className="mt-4 flex justify-center">
|
||||
<Pagination
|
||||
count={totalCount}
|
||||
page={currentPage}
|
||||
perPage={pageSize}
|
||||
onChangePage={(page: number) => setCurrentPage(page)}
|
||||
onChangePerPage={() => {}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
{totalPages > 1 && (
|
||||
<div className="mt-4 flex justify-center">
|
||||
<Pagination
|
||||
count={totalCount}
|
||||
page={currentPage}
|
||||
perPage={pageSize}
|
||||
onChangePage={(page: number) => setCurrentPage(page)}
|
||||
onChangePerPage={() => {}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { Controller, useFormContext } from "react-hook-form";
|
||||
import { faEdit, faPlus, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faCertificate, faEdit, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import {
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
Td,
|
||||
Th,
|
||||
THead,
|
||||
Tooltip,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { useProject } from "@app/context";
|
||||
@@ -76,25 +77,71 @@ export const PkiSyncCertificatesFields = () => {
|
||||
>
|
||||
Add Certificates
|
||||
</Button>
|
||||
{selectedCertificates.length === 0 ? (
|
||||
<EmptyState title="No certificates selected" icon={faPlus} />
|
||||
) : (
|
||||
<div className="max-h-64 overflow-y-auto">
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th className="w-2/5">Common Name</Th>
|
||||
<Th className="w-2/5">Serial Number</Th>
|
||||
<Th className="w-1/5">Remove</Th>
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{selectedCertificates.map((cert) => (
|
||||
<div className="max-h-64 overflow-y-auto">
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th className="w-1/3">SAN / CN</Th>
|
||||
<Th className="w-1/4">Serial Number</Th>
|
||||
<Th className="w-1/6">Issued At</Th>
|
||||
<Th className="w-1/6">Expires At</Th>
|
||||
<Th className="w-12">Remove</Th>
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{selectedCertificates.map((cert) => {
|
||||
let originalDisplayName = "—";
|
||||
if (cert.altNames && cert.altNames.trim()) {
|
||||
originalDisplayName = cert.altNames.trim();
|
||||
} else if (cert.commonName && cert.commonName.trim()) {
|
||||
originalDisplayName = cert.commonName.trim();
|
||||
}
|
||||
|
||||
let displayName = originalDisplayName;
|
||||
let isTruncated = false;
|
||||
if (originalDisplayName.length > 34) {
|
||||
displayName = `${originalDisplayName.substring(0, 34)}...`;
|
||||
isTruncated = true;
|
||||
}
|
||||
|
||||
const truncatedSerial =
|
||||
cert.serialNumber.length > 8
|
||||
? `${cert.serialNumber.slice(0, 4)}...${cert.serialNumber.slice(-4)}`
|
||||
: cert.serialNumber;
|
||||
|
||||
const isExpired = new Date(cert.notAfter) < new Date();
|
||||
|
||||
return (
|
||||
<Tr key={cert.id}>
|
||||
<Td className="max-w-xs truncate">{cert.commonName}</Td>
|
||||
<Td className="font-mono text-xs text-bunker-300">
|
||||
{cert.serialNumber}
|
||||
<Td className="max-w-0">
|
||||
{isTruncated ? (
|
||||
<Tooltip content={originalDisplayName} className="max-w-lg">
|
||||
<div className="truncate">{displayName}</div>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<div className="truncate">{displayName}</div>
|
||||
)}
|
||||
</Td>
|
||||
<Td className="max-w-0">
|
||||
<div
|
||||
className="font-mono text-xs text-bunker-300"
|
||||
title={cert.serialNumber}
|
||||
>
|
||||
{truncatedSerial}
|
||||
</div>
|
||||
</Td>
|
||||
<Td className="max-w-0">
|
||||
<span className="text-sm text-bunker-300">
|
||||
{new Date(cert.notBefore).toLocaleDateString()}
|
||||
</span>
|
||||
</Td>
|
||||
<Td className="max-w-0">
|
||||
<span
|
||||
className={`text-sm ${isExpired ? "text-red-400" : "text-bunker-300"}`}
|
||||
>
|
||||
{new Date(cert.notAfter).toLocaleDateString()}
|
||||
</span>
|
||||
</Td>
|
||||
<Td>
|
||||
<Button
|
||||
@@ -112,12 +159,15 @@ export const PkiSyncCertificatesFields = () => {
|
||||
</Button>
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</TBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</div>
|
||||
)}
|
||||
);
|
||||
})}
|
||||
</TBody>
|
||||
</Table>
|
||||
{selectedCertificates.length === 0 && (
|
||||
<EmptyState title="No certificates selected" icon={faCertificate} />
|
||||
)}
|
||||
</TableContainer>
|
||||
</div>
|
||||
</div>
|
||||
</FormControl>
|
||||
)}
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
import { useFormContext } from "react-hook-form";
|
||||
|
||||
import { GenericFieldLabel } from "@app/components/v2";
|
||||
import {
|
||||
GenericFieldLabel,
|
||||
Table,
|
||||
TableContainer,
|
||||
TBody,
|
||||
Td,
|
||||
Th,
|
||||
THead,
|
||||
Tooltip,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { Badge } from "@app/components/v3";
|
||||
import { useProject } from "@app/context";
|
||||
import { PKI_SYNC_MAP } from "@app/helpers/pkiSyncs";
|
||||
@@ -45,20 +55,71 @@ export const PkiSyncReviewFields = () => {
|
||||
<div className="w-full border-b border-mineshaft-600">
|
||||
<span className="text-sm text-mineshaft-300">Certificates</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-x-8 gap-y-2">
|
||||
<div>
|
||||
{selectedCertificates.length === 0 ? (
|
||||
<span className="text-bunker-400">No certificates selected</span>
|
||||
) : (
|
||||
<div className="space-y-1">
|
||||
{selectedCertificates.map((cert) => (
|
||||
<div key={cert.id} className="text-sm">
|
||||
{cert.commonName}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="w-full">
|
||||
{selectedCertificates.length === 0 ? (
|
||||
<span className="text-bunker-400">No certificates selected</span>
|
||||
) : (
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th className="w-1/2">SAN / CN</Th>
|
||||
<Th className="w-1/4">Serial Number</Th>
|
||||
<Th className="w-1/4">Expires At</Th>
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{selectedCertificates.map((cert) => {
|
||||
let originalDisplayName = "—";
|
||||
if (cert.altNames && cert.altNames.trim()) {
|
||||
originalDisplayName = cert.altNames.trim();
|
||||
} else if (cert.commonName && cert.commonName.trim()) {
|
||||
originalDisplayName = cert.commonName.trim();
|
||||
}
|
||||
|
||||
let displayName = originalDisplayName;
|
||||
let isTruncated = false;
|
||||
if (originalDisplayName.length > 34) {
|
||||
displayName = `${originalDisplayName.substring(0, 34)}...`;
|
||||
isTruncated = true;
|
||||
}
|
||||
|
||||
const truncatedSerial =
|
||||
cert.serialNumber.length > 8
|
||||
? `${cert.serialNumber.slice(0, 4)}...${cert.serialNumber.slice(-4)}`
|
||||
: cert.serialNumber;
|
||||
|
||||
return (
|
||||
<Tr key={cert.id}>
|
||||
<Td className="max-w-0">
|
||||
{isTruncated ? (
|
||||
<Tooltip content={originalDisplayName} className="max-w-lg">
|
||||
<div className="truncate">{displayName}</div>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<div className="truncate">{displayName}</div>
|
||||
)}
|
||||
</Td>
|
||||
<Td className="max-w-0">
|
||||
<div
|
||||
className="font-mono text-xs text-bunker-300"
|
||||
title={cert.serialNumber}
|
||||
>
|
||||
{truncatedSerial}
|
||||
</div>
|
||||
</Td>
|
||||
<Td className="max-w-0">
|
||||
<span className="text-sm text-bunker-300">
|
||||
{new Date(cert.notAfter).toLocaleDateString()}
|
||||
</span>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
</TBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
@@ -79,11 +140,13 @@ export const PkiSyncReviewFields = () => {
|
||||
<div className="w-full border-b border-mineshaft-600">
|
||||
<span className="text-sm text-mineshaft-300">Sync Options</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-x-8 gap-y-2">
|
||||
<div className="flex flex-wrap gap-x-8 gap-y-3">
|
||||
<GenericFieldLabel label="Auto-Sync">
|
||||
<Badge variant={isAutoSyncEnabled ? "success" : "danger"}>
|
||||
{isAutoSyncEnabled ? "Enabled" : "Disabled"}
|
||||
</Badge>
|
||||
<div className="mt-1">
|
||||
<Badge variant={isAutoSyncEnabled ? "success" : "danger"}>
|
||||
{isAutoSyncEnabled ? "Enabled" : "Disabled"}
|
||||
</Badge>
|
||||
</div>
|
||||
</GenericFieldLabel>
|
||||
{/* Hidden for now - Import certificates functionality disabled
|
||||
{syncOptions?.canImportCertificates !== undefined && (
|
||||
@@ -96,9 +159,11 @@ export const PkiSyncReviewFields = () => {
|
||||
*/}
|
||||
{syncOptions?.canRemoveCertificates !== undefined && (
|
||||
<GenericFieldLabel label="Remove Certificates">
|
||||
<Badge variant={syncOptions.canRemoveCertificates ? "success" : "danger"}>
|
||||
{syncOptions.canRemoveCertificates ? "Enabled" : "Disabled"}
|
||||
</Badge>
|
||||
<div className="mt-1">
|
||||
<Badge variant={syncOptions.canRemoveCertificates ? "success" : "danger"}>
|
||||
{syncOptions.canRemoveCertificates ? "Enabled" : "Disabled"}
|
||||
</Badge>
|
||||
</div>
|
||||
</GenericFieldLabel>
|
||||
)}
|
||||
</div>
|
||||
|
||||
100
frontend/src/components/utilities/certificateDisplayUtils.tsx
Normal file
100
frontend/src/components/utilities/certificateDisplayUtils.tsx
Normal file
@@ -0,0 +1,100 @@
|
||||
import { ReactNode } from "react";
|
||||
|
||||
import { Tooltip } from "@app/components/v2";
|
||||
|
||||
interface CertificateNameData {
|
||||
altNames?: string | null;
|
||||
commonName?: string | null;
|
||||
certificateAltNames?: string | null;
|
||||
certificateCommonName?: string | null;
|
||||
}
|
||||
|
||||
interface DisplayNameResult {
|
||||
originalDisplayName: string;
|
||||
displayName: string;
|
||||
isTruncated: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts and formats the display name for a certificate from SAN/CN data
|
||||
* @param cert - Certificate object with potential altNames/commonName fields
|
||||
* @param maxLength - Maximum length before truncating (default: 64)
|
||||
* @param fallback - Fallback text when no name is found (default: "—")
|
||||
* @returns Object with original name, truncated name, and truncation flag
|
||||
*/
|
||||
export const getCertificateDisplayName = (
|
||||
cert: CertificateNameData,
|
||||
maxLength: number = 64,
|
||||
fallback: string = "—"
|
||||
): DisplayNameResult => {
|
||||
// Extract original display name - prioritize SAN over CN
|
||||
let originalDisplayName = fallback;
|
||||
|
||||
// Handle different property name variations
|
||||
const altNames = cert.altNames || cert.certificateAltNames;
|
||||
const commonName = cert.commonName || cert.certificateCommonName;
|
||||
|
||||
if (altNames && altNames.trim()) {
|
||||
originalDisplayName = altNames.trim();
|
||||
} else if (commonName && commonName.trim()) {
|
||||
originalDisplayName = commonName.trim();
|
||||
}
|
||||
|
||||
// Handle truncation
|
||||
let displayName = originalDisplayName;
|
||||
let isTruncated = false;
|
||||
|
||||
if (originalDisplayName.length > maxLength) {
|
||||
displayName = `${originalDisplayName.substring(0, maxLength)}...`;
|
||||
isTruncated = true;
|
||||
}
|
||||
|
||||
return {
|
||||
originalDisplayName,
|
||||
displayName,
|
||||
isTruncated
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders a certificate display name with optional tooltip for truncated names
|
||||
* @param cert - Certificate object with potential altNames/commonName fields
|
||||
* @param maxLength - Maximum length before truncating (default: 64)
|
||||
* @param fallback - Fallback text when no name is found (default: "—")
|
||||
* @param className - Optional CSS class for the display element
|
||||
* @param tooltipClassName - Optional CSS class for the tooltip (default: "max-w-lg")
|
||||
* @returns JSX element with certificate name and optional tooltip
|
||||
*/
|
||||
export const CertificateDisplayName = ({
|
||||
cert,
|
||||
maxLength = 64,
|
||||
fallback = "—",
|
||||
className = "truncate",
|
||||
tooltipClassName = "max-w-lg"
|
||||
}: {
|
||||
cert: CertificateNameData;
|
||||
maxLength?: number;
|
||||
fallback?: string;
|
||||
className?: string;
|
||||
tooltipClassName?: string;
|
||||
}): ReactNode => {
|
||||
const { originalDisplayName, displayName, isTruncated } = getCertificateDisplayName(
|
||||
cert,
|
||||
maxLength,
|
||||
fallback
|
||||
);
|
||||
|
||||
if (isTruncated) {
|
||||
return (
|
||||
<Tooltip content={originalDisplayName} className={tooltipClassName}>
|
||||
<div className={className}>{displayName}</div>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={className} title={originalDisplayName}>
|
||||
{displayName}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -152,7 +152,7 @@ export const useCreateCertificate = () => {
|
||||
});
|
||||
};
|
||||
|
||||
export const useCreateCertificateV3 = () => {
|
||||
export const useCreateCertificateV3 = (options?: { projectId?: string }) => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<TCreateCertificateV3Response, object, TCreateCertificateV3DTO>({
|
||||
mutationFn: async (body) => {
|
||||
@@ -167,6 +167,12 @@ export const useCreateCertificateV3 = () => {
|
||||
queryKey: projectKeys.forProjectCertificates(projectSlug)
|
||||
});
|
||||
|
||||
if (options?.projectId) {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: projectKeys.forProjectCertificates(options.projectId)
|
||||
});
|
||||
}
|
||||
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["certificate-profiles"]
|
||||
});
|
||||
|
||||
@@ -8,7 +8,6 @@ export {
|
||||
useGetCertificateProfileById,
|
||||
useGetCertificateProfileBySlug,
|
||||
useGetProfileCertificates,
|
||||
useGetProfileMetrics,
|
||||
useListCertificateProfiles
|
||||
} from "./queries";
|
||||
export type * from "./types";
|
||||
|
||||
@@ -4,7 +4,6 @@ import { apiRequest } from "@app/config/request";
|
||||
|
||||
import {
|
||||
TCertificateProfile,
|
||||
TCertificateProfileMetrics,
|
||||
TCertificateProfileWithDetails,
|
||||
TGetCertificateProfileByIdDTO,
|
||||
TGetCertificateProfileBySlugDTO,
|
||||
@@ -20,7 +19,6 @@ export const certificateProfileKeys = {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
search?: string;
|
||||
includeMetrics?: boolean;
|
||||
includeConfigs?: boolean;
|
||||
enrollmentType?: string;
|
||||
expiringDays?: number;
|
||||
@@ -51,10 +49,8 @@ export const useListCertificateProfiles = ({
|
||||
limit = 20,
|
||||
offset = 0,
|
||||
search,
|
||||
includeMetrics = false,
|
||||
includeConfigs = false,
|
||||
enrollmentType,
|
||||
expiringDays = 7
|
||||
enrollmentType
|
||||
}: TListCertificateProfilesDTO) => {
|
||||
return useQuery({
|
||||
queryKey: certificateProfileKeys.list({
|
||||
@@ -62,10 +58,8 @@ export const useListCertificateProfiles = ({
|
||||
limit,
|
||||
offset,
|
||||
search,
|
||||
includeMetrics,
|
||||
includeConfigs,
|
||||
enrollmentType,
|
||||
expiringDays
|
||||
enrollmentType
|
||||
}),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<{
|
||||
@@ -77,10 +71,8 @@ export const useListCertificateProfiles = ({
|
||||
limit,
|
||||
offset,
|
||||
search,
|
||||
includeMetrics,
|
||||
includeConfigs,
|
||||
enrollmentType,
|
||||
expiringDays
|
||||
enrollmentType
|
||||
}
|
||||
});
|
||||
return data;
|
||||
@@ -145,18 +137,3 @@ export const useGetProfileCertificates = ({
|
||||
enabled: Boolean(profileId)
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetProfileMetrics = ({ profileId, expiringDays = 7 }: TGetProfileMetricsDTO) => {
|
||||
return useQuery({
|
||||
queryKey: certificateProfileKeys.getMetrics(profileId, { expiringDays }),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<{
|
||||
metrics: TCertificateProfileMetrics;
|
||||
}>(`/api/v1/pki/certificate-profiles/${profileId}/metrics`, {
|
||||
params: { expiringDays }
|
||||
});
|
||||
return data.metrics;
|
||||
},
|
||||
enabled: Boolean(profileId)
|
||||
});
|
||||
};
|
||||
|
||||
@@ -10,7 +10,6 @@ export type TCertificateProfile = {
|
||||
apiConfigId?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
metrics?: TCertificateProfileMetrics;
|
||||
};
|
||||
|
||||
export type TCertificateProfileWithDetails = TCertificateProfile & {
|
||||
@@ -81,10 +80,8 @@ export type TListCertificateProfilesDTO = {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
search?: string;
|
||||
includeMetrics?: boolean;
|
||||
includeConfigs?: boolean;
|
||||
enrollmentType?: "api" | "est";
|
||||
expiringDays?: number;
|
||||
};
|
||||
|
||||
export type TGetCertificateProfileByIdDTO = {
|
||||
@@ -96,15 +93,6 @@ export type TGetCertificateProfileBySlugDTO = {
|
||||
slug: string;
|
||||
};
|
||||
|
||||
export type TCertificateProfileMetrics = {
|
||||
profileId: string;
|
||||
totalCertificates: number;
|
||||
activeCertificates: number;
|
||||
expiredCertificates: number;
|
||||
expiringCertificates: number;
|
||||
revokedCertificates: number;
|
||||
};
|
||||
|
||||
export type TProfileCertificate = {
|
||||
id: string;
|
||||
serialNumber: string;
|
||||
@@ -126,5 +114,4 @@ export type TGetProfileCertificatesDTO = {
|
||||
|
||||
export type TGetProfileMetricsDTO = {
|
||||
profileId: string;
|
||||
expiringDays?: number;
|
||||
};
|
||||
|
||||
@@ -9,6 +9,7 @@ export type TCertificate = {
|
||||
friendlyName: string;
|
||||
commonName: string;
|
||||
subjectAltNames: string;
|
||||
altNames?: string;
|
||||
serialNumber: string;
|
||||
notBefore: string;
|
||||
notAfter: string;
|
||||
|
||||
@@ -57,6 +57,7 @@ export type TPkiSyncCertificate = {
|
||||
updatedAt: string;
|
||||
certificateSerialNumber?: string;
|
||||
certificateCommonName?: string;
|
||||
certificateAltNames?: string;
|
||||
certificateStatus?: string;
|
||||
certificateNotBefore?: Date;
|
||||
certificateNotAfter?: Date;
|
||||
|
||||
@@ -52,9 +52,7 @@ export const PkiManagerLayout = () => {
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<Tab value={isActive ? "selected" : ""}>Certificate Management</Tab>
|
||||
)}
|
||||
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>Certificates</Tab>}
|
||||
</Link>
|
||||
<Link
|
||||
to="/projects/cert-management/$projectId/certificate-authorities"
|
||||
|
||||
@@ -122,11 +122,12 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
|
||||
|
||||
const { data: profilesData } = useListCertificateProfiles({
|
||||
projectId: currentProject?.id || "",
|
||||
includeMetrics: false,
|
||||
enrollmentType: "api"
|
||||
});
|
||||
|
||||
const { mutateAsync: createCertificate } = useCreateCertificateV3();
|
||||
const { mutateAsync: createCertificate } = useCreateCertificateV3({
|
||||
projectId: currentProject?.id
|
||||
});
|
||||
|
||||
const formResolver = useMemo(() => {
|
||||
return zodResolver(createSchema(shouldShowSubjectSection));
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useMemo, useState } from "react";
|
||||
import {
|
||||
faBan,
|
||||
faCertificate,
|
||||
faClockRotateLeft,
|
||||
faEllipsis,
|
||||
faEye,
|
||||
faFileExport,
|
||||
@@ -11,11 +12,14 @@ import {
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { format } from "date-fns";
|
||||
import { CircleQuestionMarkIcon } from "lucide-react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
CertificateDisplayName,
|
||||
getCertificateDisplayName
|
||||
} from "@app/components/utilities/certificateDisplayUtils";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -45,7 +49,6 @@ import { caSupportsCapability } from "@app/hooks/api/ca/constants";
|
||||
import { CaCapability, CaType } from "@app/hooks/api/ca/enums";
|
||||
import { useListCasByProjectId } from "@app/hooks/api/ca/queries";
|
||||
import { CertStatus } from "@app/hooks/api/certificates/enums";
|
||||
import { TCertificate } from "@app/hooks/api/certificates/types";
|
||||
import { useListWorkspaceCertificates } from "@app/hooks/api/projects";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
@@ -58,93 +61,6 @@ const isExpiringWithinOneDay = (notAfter: string): boolean => {
|
||||
return expiryDate <= oneDayFromNow;
|
||||
};
|
||||
|
||||
const getAutoRenewalInfo = (certificate: TCertificate) => {
|
||||
if (certificate.renewedByCertificateId) {
|
||||
return { text: "Renewed", variant: "neutral" as const };
|
||||
}
|
||||
|
||||
const isRevoked = certificate.status === CertStatus.REVOKED;
|
||||
const isExpired = new Date(certificate.notAfter) < new Date();
|
||||
const hasNoProfile = !certificate.profileId;
|
||||
const isExpiringWithinDay = isExpiringWithinOneDay(certificate.notAfter);
|
||||
|
||||
if (isRevoked) {
|
||||
return {
|
||||
text: "Not Available",
|
||||
variant: "neutral" as const,
|
||||
tooltip: "Renewal is not available for revoked certificates"
|
||||
};
|
||||
}
|
||||
|
||||
if (isExpired) {
|
||||
return {
|
||||
text: "Not Available",
|
||||
variant: "neutral" as const,
|
||||
tooltip: "Renewal is not available for expired certificates"
|
||||
};
|
||||
}
|
||||
|
||||
if (hasNoProfile) {
|
||||
return {
|
||||
text: "Not Available",
|
||||
variant: "neutral" as const,
|
||||
tooltip: "Renewal requires a certificate profile"
|
||||
};
|
||||
}
|
||||
|
||||
if (certificate.hasPrivateKey === false) {
|
||||
return {
|
||||
text: "Not Available",
|
||||
variant: "neutral" as const,
|
||||
tooltip: "Renewal is not available for certificates with externally generated private keys"
|
||||
};
|
||||
}
|
||||
|
||||
if (isExpiringWithinDay) {
|
||||
return {
|
||||
text: "Not Available",
|
||||
variant: "neutral" as const,
|
||||
tooltip: "Auto-renewal is not available for certificates expiring within 24 hours"
|
||||
};
|
||||
}
|
||||
|
||||
if (certificate.renewalError) {
|
||||
return {
|
||||
text: "Failed",
|
||||
variant: "danger" as const,
|
||||
tooltip: certificate.renewalError
|
||||
};
|
||||
}
|
||||
|
||||
if (!certificate.renewBeforeDays) {
|
||||
return { text: "Auto-Renewal Disabled", variant: "warning" as const };
|
||||
}
|
||||
|
||||
const notAfterDate = new Date(certificate.notAfter);
|
||||
const renewalDate = new Date(
|
||||
notAfterDate.getTime() - certificate.renewBeforeDays * 24 * 60 * 60 * 1000
|
||||
);
|
||||
const now = new Date();
|
||||
|
||||
if (renewalDate <= now) {
|
||||
return { text: "Due Now", variant: "danger" as const };
|
||||
}
|
||||
|
||||
const daysUntilRenewal = Math.floor(
|
||||
(renewalDate.getTime() - now.getTime()) / (24 * 60 * 60 * 1000)
|
||||
);
|
||||
|
||||
if (daysUntilRenewal === 0) {
|
||||
return { text: "Renews today", variant: "warning" as const };
|
||||
}
|
||||
|
||||
if (daysUntilRenewal <= 7) {
|
||||
return { text: `Renews in ${daysUntilRenewal}d`, variant: "warning" as const };
|
||||
}
|
||||
|
||||
return { text: `Renews in ${daysUntilRenewal}d`, variant: "success" as const };
|
||||
};
|
||||
|
||||
type Props = {
|
||||
handlePopUpOpen: (
|
||||
popUpName: keyof UsePopUpState<
|
||||
@@ -236,20 +152,18 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th>Common Name</Th>
|
||||
<Th>Status</Th>
|
||||
<Th>Not Before</Th>
|
||||
<Th>Not After</Th>
|
||||
<Th>Renewal Status</Th>
|
||||
<Th />
|
||||
<Th className="w-1/2">SAN / CN</Th>
|
||||
<Th className="w-1/6">Status</Th>
|
||||
<Th className="w-1/6">Not Before</Th>
|
||||
<Th className="w-1/6">Not After</Th>
|
||||
<Th className="w-12" />
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{isPending && <TableSkeleton columns={5} innerKey="project-cas" />}
|
||||
{isPending && <TableSkeleton columns={4} innerKey="project-cas" />}
|
||||
{!isPending &&
|
||||
data?.certificates.map((certificate) => {
|
||||
const { variant, label } = getCertValidUntilBadgeDetails(certificate.notAfter);
|
||||
const autoRenewalInfo = getAutoRenewalInfo(certificate);
|
||||
|
||||
const isRevoked = certificate.status === CertStatus.REVOKED;
|
||||
const isExpired = new Date(certificate.notAfter) < new Date();
|
||||
@@ -258,9 +172,24 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
|
||||
const isAutoRenewalEnabled = Boolean(
|
||||
certificate.renewBeforeDays && certificate.renewBeforeDays > 0
|
||||
);
|
||||
|
||||
const canShowAutoRenewalIcon = Boolean(
|
||||
certificate.profileId &&
|
||||
certificate.hasPrivateKey !== false &&
|
||||
!certificate.renewedByCertificateId &&
|
||||
!isRevoked &&
|
||||
!isExpired &&
|
||||
!isExpiringWithinDay
|
||||
);
|
||||
|
||||
// Still need originalDisplayName for other uses in the component
|
||||
const { originalDisplayName } = getCertificateDisplayName(certificate, 64, "—");
|
||||
|
||||
return (
|
||||
<Tr className="h-10" key={`certificate-${certificate.id}`}>
|
||||
<Td>{certificate.commonName}</Td>
|
||||
<Tr className="group h-10" key={`certificate-${certificate.id}`}>
|
||||
<Td className="max-w-0">
|
||||
<CertificateDisplayName cert={certificate} maxLength={64} fallback="—" />
|
||||
</Td>
|
||||
<Td>
|
||||
{certificate.status === CertStatus.REVOKED ? (
|
||||
<Badge variant="danger">Revoked</Badge>
|
||||
@@ -278,22 +207,64 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
|
||||
? format(new Date(certificate.notAfter), "yyyy-MM-dd")
|
||||
: "-"}
|
||||
</Td>
|
||||
<Td>
|
||||
{autoRenewalInfo &&
|
||||
(autoRenewalInfo.tooltip ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<Tooltip content={autoRenewalInfo.tooltip}>
|
||||
<Badge variant={autoRenewalInfo.variant}>
|
||||
{autoRenewalInfo.text}
|
||||
<CircleQuestionMarkIcon />
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
</div>
|
||||
) : (
|
||||
<Badge variant={autoRenewalInfo.variant}>{autoRenewalInfo.text}</Badge>
|
||||
))}
|
||||
</Td>
|
||||
<Td className="flex justify-end">
|
||||
<Td className="flex items-center justify-end gap-2">
|
||||
<div
|
||||
className={`transition-opacity ${(() => {
|
||||
if (!canShowAutoRenewalIcon) return "";
|
||||
if (isAutoRenewalEnabled) return "opacity-100";
|
||||
return "opacity-0 group-hover:opacity-100";
|
||||
})()}`}
|
||||
>
|
||||
{canShowAutoRenewalIcon && (
|
||||
<Tooltip
|
||||
content={(() => {
|
||||
if (hasFailed && certificate.renewalError) {
|
||||
return `Auto-renewal failed: ${certificate.renewalError}`;
|
||||
}
|
||||
if (isAutoRenewalEnabled) {
|
||||
const expiryDate = new Date(certificate.notAfter);
|
||||
const now = new Date();
|
||||
const daysUntilExpiry = Math.ceil(
|
||||
(expiryDate.getTime() - now.getTime()) / (24 * 60 * 60 * 1000)
|
||||
);
|
||||
const daysUntilRenewal = Math.max(
|
||||
0,
|
||||
daysUntilExpiry - (certificate.renewBeforeDays || 0)
|
||||
);
|
||||
return `Auto-renews in ${daysUntilRenewal}d`;
|
||||
}
|
||||
return "Set auto renewal";
|
||||
})()}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className={(() => {
|
||||
if (hasFailed) return "pr-1 text-red-500 hover:text-red-400";
|
||||
return "pr-1 text-primary-500 hover:text-primary-400";
|
||||
})()}
|
||||
aria-label="Certificate auto-renewal"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (hasFailed) return;
|
||||
|
||||
handlePopUpOpen("manageRenewal", {
|
||||
certificateId: certificate.id,
|
||||
commonName: originalDisplayName,
|
||||
profileId: certificate.profileId || "",
|
||||
renewBeforeDays: certificate.renewBeforeDays || 7,
|
||||
ttlDays: Math.ceil(
|
||||
(new Date(certificate.notAfter).getTime() -
|
||||
new Date(certificate.notBefore).getTime()) /
|
||||
(24 * 60 * 60 * 1000)
|
||||
)
|
||||
});
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faClockRotateLeft} />
|
||||
</button>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild className="rounded-lg">
|
||||
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
|
||||
@@ -370,20 +341,20 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
|
||||
<DropdownMenuItem
|
||||
className={twMerge(
|
||||
!isAllowed &&
|
||||
"pointer-events-none cursor-not-allowed opacity-50"
|
||||
"pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
onClick={async () => {
|
||||
const notAfterDate = new Date(certificate.notAfter);
|
||||
const notBeforeDate = certificate.notBefore
|
||||
? new Date(certificate.notBefore)
|
||||
: new Date(
|
||||
notAfterDate.getTime() - 365 * 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)
|
||||
(24 * 60 * 60 * 1000)
|
||||
)
|
||||
);
|
||||
handlePopUpOpen("manageRenewal", {
|
||||
@@ -433,7 +404,7 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
|
||||
<DropdownMenuItem
|
||||
className={twMerge(
|
||||
!isAllowed &&
|
||||
"pointer-events-none cursor-not-allowed opacity-50"
|
||||
"pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
onClick={async () => {
|
||||
await handleDisableAutoRenewal(
|
||||
@@ -470,7 +441,7 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
|
||||
<DropdownMenuItem
|
||||
className={twMerge(
|
||||
!isAllowed &&
|
||||
"pointer-events-none cursor-not-allowed opacity-50"
|
||||
"pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
onClick={async () => {
|
||||
handlePopUpOpen("renewCertificate", {
|
||||
@@ -532,7 +503,7 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
|
||||
<DropdownMenuItem
|
||||
className={twMerge(
|
||||
!isAllowed &&
|
||||
"pointer-events-none cursor-not-allowed opacity-50"
|
||||
"pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
onClick={async () =>
|
||||
handlePopUpOpen("revokeCertificate", {
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
faToggleOff,
|
||||
faToggleOn,
|
||||
faTrash,
|
||||
faTriangleExclamation,
|
||||
faXmark
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
@@ -47,7 +46,6 @@ import { useToggle } from "@app/hooks";
|
||||
import { PkiSyncStatus, TPkiSync, usePkiSyncOption } from "@app/hooks/api/pkiSyncs";
|
||||
|
||||
import { PkiSyncDestinationCol } from "./PkiSyncDestinationCol";
|
||||
import { PkiSyncTableCell } from "./PkiSyncTableCell";
|
||||
|
||||
type Props = {
|
||||
pkiSync: TPkiSync;
|
||||
@@ -163,23 +161,6 @@ export const PkiSyncRow = ({
|
||||
<p className="truncate text-xs leading-4 text-bunker-300">{destinationDetails.name}</p>
|
||||
</div>
|
||||
</Td>
|
||||
{subscriberId ? (
|
||||
<PkiSyncTableCell
|
||||
primaryText={pkiSync.subscriber?.name || subscriberId}
|
||||
secondaryText="PKI Subscriber"
|
||||
/>
|
||||
) : (
|
||||
<Td>
|
||||
<Tooltip content="The PKI subscriber for this sync has been deleted. Configure a new source or remove this sync.">
|
||||
<div className="w-min">
|
||||
<Badge variant="warning">
|
||||
<FontAwesomeIcon icon={faTriangleExclamation} />
|
||||
<span>Source Deleted</span>
|
||||
</Badge>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</Td>
|
||||
)}
|
||||
<PkiSyncDestinationCol pkiSync={pkiSync} />
|
||||
<Td>
|
||||
<div className="flex items-center gap-1">
|
||||
|
||||
@@ -57,7 +57,6 @@ import { PkiSyncRow } from "./PkiSyncRow";
|
||||
|
||||
enum PkiSyncsOrderBy {
|
||||
Destination = "destination",
|
||||
Source = "source",
|
||||
Name = "name",
|
||||
Status = "status"
|
||||
}
|
||||
@@ -160,14 +159,6 @@ export const PkiSyncsTable = ({ pkiSyncs }: Props) => {
|
||||
const [syncOne, syncTwo] = orderDirection === OrderByDirection.ASC ? [a, b] : [b, a];
|
||||
|
||||
switch (orderBy) {
|
||||
case PkiSyncsOrderBy.Source:
|
||||
return (syncOne.subscriber?.name ?? syncOne.subscriberId ?? "")
|
||||
.toLowerCase()
|
||||
.localeCompare(
|
||||
syncTwo.subscriber?.name?.toLowerCase() ??
|
||||
syncTwo.subscriberId?.toLowerCase() ??
|
||||
""
|
||||
);
|
||||
case PkiSyncsOrderBy.Destination:
|
||||
return getPkiSyncDestinationColValues(syncOne)
|
||||
.primaryText.toLowerCase()
|
||||
@@ -370,7 +361,7 @@ export const PkiSyncsTable = ({ pkiSyncs }: Props) => {
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th className="w-2" />
|
||||
<Th className="w-1/4">
|
||||
<Th className="w-1/2">
|
||||
<div className="flex items-center">
|
||||
Name
|
||||
<IconButton
|
||||
@@ -383,20 +374,7 @@ export const PkiSyncsTable = ({ pkiSyncs }: Props) => {
|
||||
</IconButton>
|
||||
</div>
|
||||
</Th>
|
||||
<Th className="w-1/3">
|
||||
<div className="flex items-center">
|
||||
Source
|
||||
<IconButton
|
||||
variant="plain"
|
||||
className={getClassName(PkiSyncsOrderBy.Source)}
|
||||
ariaLabel="sort"
|
||||
onClick={() => handleSort(PkiSyncsOrderBy.Source)}
|
||||
>
|
||||
<FontAwesomeIcon icon={getColSortIcon(PkiSyncsOrderBy.Source)} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</Th>
|
||||
<Th className="w-1/3">
|
||||
<Th className="w-1/4">
|
||||
<div className="flex items-center">
|
||||
Destination
|
||||
<IconButton
|
||||
@@ -409,7 +387,7 @@ export const PkiSyncsTable = ({ pkiSyncs }: Props) => {
|
||||
</IconButton>
|
||||
</div>
|
||||
</Th>
|
||||
<Th className="min-w-42">
|
||||
<Th className="w-1/4 min-w-42">
|
||||
<div className="flex items-center">
|
||||
Status
|
||||
<IconButton
|
||||
|
||||
@@ -26,7 +26,7 @@ export const PkiSyncAuditLogsSection = ({ pkiSync }: Props) => {
|
||||
return (
|
||||
<div className="flex max-h-full w-full flex-col gap-3 rounded-lg border border-mineshaft-600 bg-mineshaft-900 px-4 py-3">
|
||||
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-2">
|
||||
<h3 className="font-medium text-mineshaft-100">Sync Logs</h3>
|
||||
<h3 className="text-lg font-medium text-mineshaft-100">Sync Logs</h3>
|
||||
{subscription.auditLogs && (
|
||||
<p className="text-xs text-bunker-300">
|
||||
Displaying audit logs from the last {Math.min(auditLogsRetentionDays, 60)} days
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
import { useState } from "react";
|
||||
import { subject } from "@casl/ability";
|
||||
import { faEdit, faPlus, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faCertificate, faEdit, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { CertificateManagementModal } from "@app/components/pki-syncs/CertificateManagementModal";
|
||||
import {
|
||||
CertificateDisplayName,
|
||||
getCertificateDisplayName
|
||||
} from "@app/components/utilities/certificateDisplayUtils";
|
||||
import {
|
||||
DeleteActionModal,
|
||||
EmptyState,
|
||||
IconButton,
|
||||
Pagination,
|
||||
@@ -46,6 +51,11 @@ const getSyncStatusText = (status?: CertificateSyncStatus | null) => {
|
||||
|
||||
export const PkiSyncCertificatesSection = ({ pkiSync }: Props) => {
|
||||
const [isManageModalOpen, setIsManageModalOpen] = useState(false);
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
const [certificateToDelete, setCertificateToDelete] = useState<{
|
||||
id: string;
|
||||
displayName: string;
|
||||
} | null>(null);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const pageSize = 10;
|
||||
|
||||
@@ -74,6 +84,9 @@ export const PkiSyncCertificatesSection = ({ pkiSync }: Props) => {
|
||||
text: "Certificate removed from sync",
|
||||
type: "success"
|
||||
});
|
||||
|
||||
setIsDeleteModalOpen(false);
|
||||
setCertificateToDelete(null);
|
||||
} catch {
|
||||
createNotification({
|
||||
text: "Failed to remove certificate from sync",
|
||||
@@ -82,13 +95,18 @@ export const PkiSyncCertificatesSection = ({ pkiSync }: Props) => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteClick = (certificateId: string, displayName: string) => {
|
||||
setCertificateToDelete({ id: certificateId, displayName });
|
||||
setIsDeleteModalOpen(true);
|
||||
};
|
||||
|
||||
const totalPages = Math.ceil(totalCount / pageSize);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex w-full flex-col gap-3 rounded-lg border border-mineshaft-600 bg-mineshaft-900 px-4 py-3">
|
||||
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-2">
|
||||
<h3 className="font-medium text-mineshaft-100">Certificates ({totalCount})</h3>
|
||||
<h3 className="text-lg font-medium text-mineshaft-100">Certificates</h3>
|
||||
<ProjectPermissionCan I={ProjectPermissionPkiSyncActions.Edit} a={permissionSubject}>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
@@ -105,108 +123,120 @@ export const PkiSyncCertificatesSection = ({ pkiSync }: Props) => {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{syncCertificates.length === 0 ? (
|
||||
<EmptyState title="No certificates" icon={faPlus}>
|
||||
No certificates are currently synced with this PKI destination.
|
||||
</EmptyState>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th className="w-1/3">Common Name</Th>
|
||||
<Th className="w-1/3">Serial Number</Th>
|
||||
<Th className="w-1/9">Status</Th>
|
||||
<Th className="w-1/9">Expires</Th>
|
||||
<Th className="w-1/9">Actions</Th>
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{syncCertificates.map((syncCert) => {
|
||||
const isExpired = syncCert.certificateNotAfter
|
||||
? new Date(syncCert.certificateNotAfter) < new Date()
|
||||
: false;
|
||||
<div className="space-y-4">
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th className="w-1/3">SAN / CN</Th>
|
||||
<Th className="w-1/3">Serial Number</Th>
|
||||
<Th className="w-1/9">Status</Th>
|
||||
<Th className="w-1/9">Expires</Th>
|
||||
<Th className="w-1/9">Actions</Th>
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{syncCertificates.map((syncCert) => {
|
||||
const isExpired = syncCert.certificateNotAfter
|
||||
? new Date(syncCert.certificateNotAfter) < new Date()
|
||||
: false;
|
||||
|
||||
return (
|
||||
<Tr key={syncCert.id}>
|
||||
<Td className="max-w-0">
|
||||
<div
|
||||
className="truncate"
|
||||
title={syncCert.certificateCommonName || "Unknown"}
|
||||
>
|
||||
{syncCert.certificateCommonName || "Unknown"}
|
||||
</div>
|
||||
</Td>
|
||||
<Td className="max-w-0">
|
||||
<div
|
||||
className="truncate text-xs"
|
||||
title={syncCert.certificateSerialNumber || "Unknown"}
|
||||
>
|
||||
{syncCert.certificateSerialNumber || "Unknown"}
|
||||
</div>
|
||||
</Td>
|
||||
<Td>
|
||||
{syncCert.lastSyncMessage &&
|
||||
syncCert.syncStatus === CertificateSyncStatus.Failed ? (
|
||||
<Tooltip content={syncCert.lastSyncMessage}>
|
||||
<Badge variant="danger">Failed</Badge>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Badge variant={getSyncStatusVariant(syncCert.syncStatus)}>
|
||||
{getSyncStatusText(syncCert.syncStatus)}
|
||||
</Badge>
|
||||
const { originalDisplayName } = getCertificateDisplayName(
|
||||
{
|
||||
altNames: syncCert.certificateAltNames,
|
||||
commonName: syncCert.certificateCommonName
|
||||
},
|
||||
34,
|
||||
"Unknown"
|
||||
);
|
||||
|
||||
return (
|
||||
<Tr key={syncCert.id}>
|
||||
<Td className="max-w-0">
|
||||
<CertificateDisplayName
|
||||
cert={{
|
||||
altNames: syncCert.certificateAltNames,
|
||||
commonName: syncCert.certificateCommonName
|
||||
}}
|
||||
maxLength={34}
|
||||
fallback="Unknown"
|
||||
/>
|
||||
</Td>
|
||||
<Td className="max-w-0">
|
||||
<div
|
||||
className="truncate text-xs"
|
||||
title={syncCert.certificateSerialNumber || "Unknown"}
|
||||
>
|
||||
{syncCert.certificateSerialNumber || "Unknown"}
|
||||
</div>
|
||||
</Td>
|
||||
<Td>
|
||||
{syncCert.lastSyncMessage &&
|
||||
syncCert.syncStatus === CertificateSyncStatus.Failed ? (
|
||||
<Tooltip content={syncCert.lastSyncMessage}>
|
||||
<Badge variant="danger">Failed</Badge>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Badge variant={getSyncStatusVariant(syncCert.syncStatus)}>
|
||||
{getSyncStatusText(syncCert.syncStatus)}
|
||||
</Badge>
|
||||
)}
|
||||
</Td>
|
||||
<Td>
|
||||
<span
|
||||
className={`text-sm ${isExpired ? "text-red-400" : "text-bunker-300"}`}
|
||||
>
|
||||
{syncCert.certificateNotAfter
|
||||
? new Date(syncCert.certificateNotAfter).toLocaleDateString()
|
||||
: "Unknown"}
|
||||
</span>
|
||||
</Td>
|
||||
<Td className="flex items-center">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionPkiSyncActions.Edit}
|
||||
a={permissionSubject}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
size="xs"
|
||||
variant="plain"
|
||||
colorSchema="danger"
|
||||
ariaLabel="Remove certificate"
|
||||
isDisabled={!isAllowed}
|
||||
onClick={() =>
|
||||
handleDeleteClick(syncCert.certificateId, originalDisplayName)
|
||||
}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</IconButton>
|
||||
)}
|
||||
</Td>
|
||||
<Td>
|
||||
<span
|
||||
className={`text-sm ${isExpired ? "text-red-400" : "text-bunker-300"}`}
|
||||
>
|
||||
{syncCert.certificateNotAfter
|
||||
? new Date(syncCert.certificateNotAfter).toLocaleDateString()
|
||||
: "Unknown"}
|
||||
</span>
|
||||
</Td>
|
||||
<Td className="flex items-center">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionPkiSyncActions.Edit}
|
||||
a={permissionSubject}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
size="xs"
|
||||
variant="plain"
|
||||
colorSchema="danger"
|
||||
ariaLabel="Remove certificate"
|
||||
isDisabled={!isAllowed}
|
||||
onClick={() => handleRemoveCertificate(syncCert.certificateId)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</IconButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
</TBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
|
||||
{/* Pagination */}
|
||||
{totalPages > 1 && (
|
||||
<div className="flex justify-center">
|
||||
<Pagination
|
||||
count={totalCount}
|
||||
page={currentPage}
|
||||
perPage={pageSize}
|
||||
onChangePage={(page: number) => setCurrentPage(page)}
|
||||
onChangePerPage={() => {}}
|
||||
/>
|
||||
</div>
|
||||
</ProjectPermissionCan>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
</TBody>
|
||||
</Table>
|
||||
{syncCertificates.length === 0 && (
|
||||
<EmptyState
|
||||
title="No certificates are part of this certificate sync"
|
||||
icon={faCertificate}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</TableContainer>
|
||||
{/* Pagination */}
|
||||
{totalPages > 1 && (
|
||||
<div className="flex justify-center">
|
||||
<Pagination
|
||||
count={totalCount}
|
||||
page={currentPage}
|
||||
perPage={pageSize}
|
||||
onChangePage={(page: number) => setCurrentPage(page)}
|
||||
onChangePerPage={() => {}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -218,6 +248,23 @@ export const PkiSyncCertificatesSection = ({ pkiSync }: Props) => {
|
||||
refetchSyncCertificates();
|
||||
}}
|
||||
/>
|
||||
|
||||
<DeleteActionModal
|
||||
isOpen={isDeleteModalOpen}
|
||||
onClose={() => {
|
||||
setIsDeleteModalOpen(false);
|
||||
setCertificateToDelete(null);
|
||||
}}
|
||||
title="Remove Certificate from Sync"
|
||||
subTitle={`Are you sure you want to remove "${certificateToDelete?.displayName}" from this PKI sync?`}
|
||||
deleteKey="confirm"
|
||||
onDeleteApproved={async () => {
|
||||
if (certificateToDelete) {
|
||||
await handleRemoveCertificate(certificateToDelete.id);
|
||||
}
|
||||
}}
|
||||
buttonText="Remove Certificate"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -11,12 +11,15 @@ import { ProjectPermissionPkiSyncActions } from "@app/context/ProjectPermissionC
|
||||
import { PKI_SYNC_MAP } from "@app/helpers/pkiSyncs";
|
||||
import { PkiSync, TPkiSync } from "@app/hooks/api/pkiSyncs";
|
||||
|
||||
import { AzureKeyVaultPkiSyncDestinationSection } from "./PkiSyncDestinationSection/index";
|
||||
import {
|
||||
AwsCertificateManagerPkiSyncDestinationSection,
|
||||
AzureKeyVaultPkiSyncDestinationSection
|
||||
} from "./PkiSyncDestinationSection/index";
|
||||
|
||||
const GenericFieldLabel = ({ label, children }: { label: string; children: React.ReactNode }) => (
|
||||
<div>
|
||||
<label className="text-sm text-bunker-300">{label}</label>
|
||||
<div className="mt-1">{children}</div>
|
||||
<div className="mb-4">
|
||||
<p className="text-sm font-medium text-mineshaft-300">{label}</p>
|
||||
<div className="text-sm text-mineshaft-300">{children}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -32,6 +35,9 @@ export const PkiSyncDestinationSection = ({ pkiSync, onEditDestination }: Props)
|
||||
|
||||
let DestinationComponents: ReactNode;
|
||||
switch (destination) {
|
||||
case PkiSync.AwsCertificateManager:
|
||||
DestinationComponents = <AwsCertificateManagerPkiSyncDestinationSection pkiSync={pkiSync} />;
|
||||
break;
|
||||
case PkiSync.AzureKeyVault:
|
||||
DestinationComponents = <AzureKeyVaultPkiSyncDestinationSection pkiSync={pkiSync} />;
|
||||
break;
|
||||
@@ -47,7 +53,7 @@ export const PkiSyncDestinationSection = ({ pkiSync, onEditDestination }: Props)
|
||||
return (
|
||||
<div className="flex w-full flex-col gap-3 rounded-lg border border-mineshaft-600 bg-mineshaft-900 px-4 py-3">
|
||||
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-2">
|
||||
<h3 className="font-medium text-mineshaft-100">Destination Configuration</h3>
|
||||
<h3 className="text-lg font-medium text-mineshaft-100">Destination Configuration</h3>
|
||||
<ProjectPermissionCan I={ProjectPermissionPkiSyncActions.Edit} a={permissionSubject}>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
@@ -62,7 +68,7 @@ export const PkiSyncDestinationSection = ({ pkiSync, onEditDestination }: Props)
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
<div className="flex w-full flex-wrap gap-8">
|
||||
<div className="flex w-full flex-wrap gap-8 pt-2">
|
||||
<GenericFieldLabel label={`${destinationDetails.name} Connection`}>
|
||||
{pkiSync.appConnectionName || "Default Connection"}
|
||||
</GenericFieldLabel>
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { TPkiSync } from "@app/hooks/api/pkiSyncs";
|
||||
|
||||
const GenericFieldLabel = ({ label, children }: { label: string; children: React.ReactNode }) => (
|
||||
<div className="mb-4">
|
||||
<p className="text-sm font-medium text-mineshaft-300">{label}</p>
|
||||
<div className="text-sm text-mineshaft-300">{children}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
type Props = {
|
||||
pkiSync: TPkiSync;
|
||||
};
|
||||
|
||||
export const AwsCertificateManagerPkiSyncDestinationSection = ({ pkiSync }: Props) => {
|
||||
const region =
|
||||
pkiSync.destinationConfig && "region" in pkiSync.destinationConfig
|
||||
? pkiSync.destinationConfig.region
|
||||
: undefined;
|
||||
|
||||
return <GenericFieldLabel label="AWS Region">{region || "Not specified"}</GenericFieldLabel>;
|
||||
};
|
||||
@@ -2,9 +2,9 @@
|
||||
import { TAzureKeyVaultPkiSync } from "@app/hooks/api/pkiSyncs/types/azure-key-vault-sync";
|
||||
|
||||
const GenericFieldLabel = ({ label, children }: { label: string; children: React.ReactNode }) => (
|
||||
<div>
|
||||
<label className="text-sm text-bunker-300">{label}</label>
|
||||
<div className="mt-1">{children}</div>
|
||||
<div className="mb-4">
|
||||
<p className="text-sm font-medium text-mineshaft-300">{label}</p>
|
||||
<div className="text-sm text-mineshaft-300">{children}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export { AwsCertificateManagerPkiSyncDestinationSection } from "./AwsCertificateManagerPkiSyncDestinationSection";
|
||||
export { AzureKeyVaultPkiSyncDestinationSection } from "./AzureKeyVaultPkiSyncDestinationSection";
|
||||
|
||||
@@ -21,9 +21,9 @@ const GenericFieldLabel = ({
|
||||
children: React.ReactNode;
|
||||
labelClassName?: string;
|
||||
}) => (
|
||||
<div>
|
||||
<label className={`text-sm text-bunker-300 ${labelClassName || ""}`}>{label}</label>
|
||||
<div className="mt-1">{children}</div>
|
||||
<div className="mb-4">
|
||||
<p className={`text-sm font-medium text-mineshaft-300 ${labelClassName || ""}`}>{label}</p>
|
||||
<div className="text-sm text-mineshaft-300">{children}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -57,7 +57,7 @@ export const PkiSyncDetailsSection = ({ pkiSync, onEditDetails }: Props) => {
|
||||
return (
|
||||
<div className="flex w-full flex-col gap-3 rounded-lg border border-mineshaft-600 bg-mineshaft-900 px-4 py-3">
|
||||
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-2">
|
||||
<h3 className="font-medium text-mineshaft-100">Details</h3>
|
||||
<h3 className="text-lg font-medium text-mineshaft-100">Details</h3>
|
||||
<ProjectPermissionCan I={ProjectPermissionPkiSyncActions.Edit} a={permissionSubject}>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
@@ -72,31 +72,27 @@ export const PkiSyncDetailsSection = ({ pkiSync, onEditDetails }: Props) => {
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
<div>
|
||||
<div className="space-y-3">
|
||||
<GenericFieldLabel label="Name">{name}</GenericFieldLabel>
|
||||
<GenericFieldLabel label="Description">{description || "None"}</GenericFieldLabel>
|
||||
<GenericFieldLabel label="Source Subscriber">
|
||||
{subscriber ? subscriber.name : "Subscriber deleted"}
|
||||
<div className="pt-2">
|
||||
<GenericFieldLabel label="Name">{name}</GenericFieldLabel>
|
||||
<GenericFieldLabel label="Description">{description || "None"}</GenericFieldLabel>
|
||||
{subscriber && (
|
||||
<GenericFieldLabel label="Source Subscriber">{subscriber.name}</GenericFieldLabel>
|
||||
)}
|
||||
{syncStatus && (
|
||||
<GenericFieldLabel label="Status">
|
||||
<PkiSyncStatusBadge status={syncStatus} />
|
||||
</GenericFieldLabel>
|
||||
{syncStatus && (
|
||||
<GenericFieldLabel label="Status">
|
||||
<PkiSyncStatusBadge status={syncStatus} />
|
||||
</GenericFieldLabel>
|
||||
)}
|
||||
{lastSyncedAt && (
|
||||
<GenericFieldLabel label="Last Synced">
|
||||
{format(new Date(lastSyncedAt), "yyyy-MM-dd, h:mm aaa")}
|
||||
</GenericFieldLabel>
|
||||
)}
|
||||
{syncStatus === PkiSyncStatus.Failed && failureMessage && (
|
||||
<GenericFieldLabel labelClassName="text-red" label="Last Sync Error">
|
||||
<p className="rounded-sm bg-mineshaft-600 p-2 text-xs break-words">
|
||||
{failureMessage}
|
||||
</p>
|
||||
</GenericFieldLabel>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{lastSyncedAt && (
|
||||
<GenericFieldLabel label="Last Synced">
|
||||
{format(new Date(lastSyncedAt), "yyyy-MM-dd, h:mm aaa")}
|
||||
</GenericFieldLabel>
|
||||
)}
|
||||
{syncStatus === PkiSyncStatus.Failed && failureMessage && (
|
||||
<GenericFieldLabel labelClassName="text-red" label="Last Sync Error">
|
||||
<p className="rounded-sm bg-mineshaft-600 p-2 text-xs break-words">{failureMessage}</p>
|
||||
</GenericFieldLabel>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -3,13 +3,27 @@ import { faEdit } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { GenericFieldLabel } from "@app/components/secret-syncs";
|
||||
import { IconButton } from "@app/components/v2";
|
||||
import { Badge } from "@app/components/v3";
|
||||
import { ProjectPermissionSub } from "@app/context";
|
||||
import { ProjectPermissionPkiSyncActions } from "@app/context/ProjectPermissionContext/types";
|
||||
import { TPkiSync } from "@app/hooks/api/pkiSyncs";
|
||||
|
||||
const GenericFieldLabel = ({
|
||||
label,
|
||||
children,
|
||||
labelClassName
|
||||
}: {
|
||||
label: string;
|
||||
children: React.ReactNode;
|
||||
labelClassName?: string;
|
||||
}) => (
|
||||
<div className="mb-4">
|
||||
<p className={`text-sm font-medium text-mineshaft-300 ${labelClassName || ""}`}>{label}</p>
|
||||
<div className="text-sm text-mineshaft-300">{children}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
type Props = {
|
||||
pkiSync: TPkiSync;
|
||||
onEditOptions: VoidFunction;
|
||||
@@ -28,7 +42,7 @@ export const PkiSyncOptionsSection = ({ pkiSync, onEditOptions }: Props) => {
|
||||
<div>
|
||||
<div className="flex w-full flex-col gap-3 rounded-lg border border-mineshaft-600 bg-mineshaft-900 px-4 py-3">
|
||||
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-2">
|
||||
<h3 className="font-medium text-mineshaft-100">Sync Options</h3>
|
||||
<h3 className="text-lg font-medium text-mineshaft-100">Sync Options</h3>
|
||||
<ProjectPermissionCan I={ProjectPermissionPkiSyncActions.Edit} a={permissionSubject}>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
@@ -43,21 +57,19 @@ export const PkiSyncOptionsSection = ({ pkiSync, onEditOptions }: Props) => {
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
<div>
|
||||
<div className="space-y-3">
|
||||
{/* Hidden for now - Import certificates functionality disabled
|
||||
<div className="pt-1">
|
||||
{/* Hidden for now - Import certificates functionality disabled
|
||||
<GenericFieldLabel label="Certificate Import">
|
||||
<Badge variant={canImportCertificates ? "success" : "danger"}>
|
||||
{canImportCertificates ? "Enabled" : "Disabled"}
|
||||
</Badge>
|
||||
</GenericFieldLabel>
|
||||
*/}
|
||||
<GenericFieldLabel label="Certificate Removal">
|
||||
<Badge variant={canRemoveCertificates ? "success" : "danger"}>
|
||||
{canRemoveCertificates ? "Enabled" : "Disabled"}
|
||||
</Badge>
|
||||
</GenericFieldLabel>
|
||||
</div>
|
||||
<GenericFieldLabel label="Certificate Removal" labelClassName="mb-1">
|
||||
<Badge variant={canRemoveCertificates ? "success" : "danger"}>
|
||||
{canRemoveCertificates ? "Enabled" : "Disabled"}
|
||||
</Badge>
|
||||
</GenericFieldLabel>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -36,7 +36,7 @@ export const PoliciesPage = () => {
|
||||
<PageHeader
|
||||
scope={ProjectType.CertificateManager}
|
||||
title="Certificate Management"
|
||||
description="Manage certificate templates, profiles, certificates, and PKI collections for unified certificate issuance"
|
||||
description="Streamline certificate management by creating and maintaining templates, profiles, and certificates in one place"
|
||||
/>
|
||||
|
||||
<Tabs
|
||||
|
||||
@@ -29,8 +29,7 @@ export const ProfileList = ({ onEditProfile, onDeleteProfile }: Props) => {
|
||||
projectId: currentProject?.id || "",
|
||||
limit: 100,
|
||||
offset: 0,
|
||||
includeConfigs: true,
|
||||
includeMetrics: true
|
||||
includeConfigs: true
|
||||
});
|
||||
|
||||
const profiles = data?.certificateProfiles || [];
|
||||
@@ -42,10 +41,9 @@ export const ProfileList = ({ onEditProfile, onDeleteProfile }: Props) => {
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th>Name</Th>
|
||||
<Th>Enrollment Type</Th>
|
||||
<Th>Enrollment Method</Th>
|
||||
<Th>Issuing CA</Th>
|
||||
<Th>Certificate Template</Th>
|
||||
<Th>Certificates</Th>
|
||||
<Th className="w-5" />
|
||||
</Tr>
|
||||
</THead>
|
||||
@@ -67,10 +65,9 @@ export const ProfileList = ({ onEditProfile, onDeleteProfile }: Props) => {
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th>Name</Th>
|
||||
<Th>Enrollment Type</Th>
|
||||
<Th>Enrollment Method</Th>
|
||||
<Th>Issuing CA</Th>
|
||||
<Th>Certificate Template</Th>
|
||||
<Th>Certificates</Th>
|
||||
<Th className="w-5" />
|
||||
</Tr>
|
||||
</THead>
|
||||
|
||||
@@ -33,43 +33,6 @@ import { TCertificateProfile } from "@app/hooks/api/certificateProfiles";
|
||||
import { useGetCertificateTemplateV2ById } from "@app/hooks/api/certificateTemplates/queries";
|
||||
import { CertificateIssuanceModal } from "@app/pages/cert-manager/CertificatesPage/components/CertificateIssuanceModal";
|
||||
|
||||
const MetricsBadges = ({
|
||||
metrics
|
||||
}: {
|
||||
metrics?: {
|
||||
totalCertificates: number;
|
||||
activeCertificates: number;
|
||||
expiringCertificates: number;
|
||||
expiredCertificates: number;
|
||||
revokedCertificates: number;
|
||||
};
|
||||
}) => {
|
||||
if (!metrics) {
|
||||
return <Badge variant="warning">No metrics</Badge>;
|
||||
}
|
||||
|
||||
if (metrics.totalCertificates === 0) {
|
||||
return <Badge variant="warning">No certificates</Badge>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{metrics.activeCertificates > 0 && (
|
||||
<Badge variant="success">{metrics.activeCertificates} active</Badge>
|
||||
)}
|
||||
{metrics.expiringCertificates > 0 && (
|
||||
<Badge variant="warning">{metrics.expiringCertificates} expiring</Badge>
|
||||
)}
|
||||
{metrics.expiredCertificates > 0 && (
|
||||
<Badge variant="danger">{metrics.expiredCertificates} expired</Badge>
|
||||
)}
|
||||
{metrics.revokedCertificates > 0 && (
|
||||
<Badge variant="danger">{metrics.revokedCertificates} revoked</Badge>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
interface Props {
|
||||
profile: TCertificateProfile;
|
||||
onEditProfile: (profile: TCertificateProfile) => void;
|
||||
@@ -118,8 +81,8 @@ export const ProfileRow = ({ profile, onEditProfile, onDeleteProfile }: Props) =
|
||||
|
||||
const getEnrollmentTypeBadge = (enrollmentType: string) => {
|
||||
const config = {
|
||||
api: { variant: "success" as const, label: "API" },
|
||||
est: { variant: "warning" as const, label: "EST" }
|
||||
api: { variant: "ghost" as const, label: "API" },
|
||||
est: { variant: "ghost" as const, label: "EST" }
|
||||
} as const;
|
||||
|
||||
const configKey = Object.keys(config).includes(enrollmentType)
|
||||
@@ -153,11 +116,6 @@ export const ProfileRow = ({ profile, onEditProfile, onDeleteProfile }: Props) =
|
||||
{templateData?.name || profile.certificateTemplateId}
|
||||
</span>
|
||||
</Td>
|
||||
<Td>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
<MetricsBadges metrics={profile.metrics} />
|
||||
</div>
|
||||
</Td>
|
||||
<Td className="text-right">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild className="rounded-lg">
|
||||
|
||||
Reference in New Issue
Block a user